这篇讲的是 PyStudio 运行时最容易被低估的一类问题:文件都在,命令也能看到,但脚本、解释器、动态库和 prefix 仍然可能指向错误位置。Termux 官方环境里能跑,不代表搬到 Android App 私有目录后还能跑。

我把它拆成几条线:shell 内建命令和外部 ELF 的差异、npm/pip 动态产物的 shebang 兼容、自家包和官方源包的边界,以及 proot 这种 syscall 敏感工具为什么必须单独验收。

只要 prefix 变了,脚本、wrapper、ELF loader 和动态库路径都可能跟着出问题。

为什么 pwd 和 cd 正常,ls 和 pkg 却卡住

最早的现象很迷惑:新 app 里运行 pwdcd 反应很快,但 lspkg 回车后没有反应。

后来回头看,这个现象其实很典型:

  • pwdcd 多数时候是 shell 内建命令。
  • ls 是外部 ELF。
  • pkg 是脚本和包管理逻辑。

内建命令能跑,只能证明 shell 基本活着。外部命令卡住,可能是:

  • ELF loader 路径不对。
  • 动态库查找路径不对。
  • shebang 指向不存在的解释器。
  • wrapper 脚本里的 PREFIX 写死。
  • app 私有目录权限或挂载行为和 Termux 不一致。

所以不能用 pwd 成功判断整个 runtime 成功。

npm 的典型坑:/usr/bin/env

一个实际错误是:

1
2
/data/data/com.vchangxiao.pystudio/files/usr/bin/nrm:
/usr/bin/env: bad interpreter: No such file or directory

原因是很多 npm 包安装命令行工具时,会生成这样的 shebang:

1
#!/usr/bin/env node

在标准 Linux 里 /usr/bin/env 存在。但 PyStudio 的运行时 prefix 在:

1
/data/data/com.vchangxiao.pystudio/files/usr

Android App 私有目录里不一定有 /usr/bin/env。于是 npm 安装出来的 CLI 文件看起来存在,执行时却找不到解释器。

最开始我们尝试过在 shell 启动时自动扫描并修复 shebang。这个方案能救一部分文件,但体验很差:用户输入命令后终端里可能出现一大串 Done 或被展开的 shell 逻辑,看起来像 app 出了怪问题。

更好的方向是分层处理。

我们自己的包和官方源包要区别对待

这里最重要的是把“我能控制的构建产物”和“用户动态装出来的产物”分开处理。
我们自己编译的包,可以在构建阶段就把路径修好:

  • prefix 使用 app 包名对应路径。
  • shebang 指向 $PREFIX/bin/env$PREFIX/bin/node
  • wrapper 脚本不写死 /data/data/com.termux/files/usr
  • ELF 和动态库路径按目标环境处理。

这类包安装后应该尽量不需要运行时修补。

官方源或 npm/pip 动态安装出来的包,就不能假设已经适配。它们需要兼容层:

1
2
3
4
5
6
7
8
9
我们构建的 .deb
构建期修补,安装后直接可用

官方源 / npm / pip 产物
安装后进入兼容处理
修 shebang
修 bin wrapper
修 npm global bin
修 PATH 和 npm prefix

这比“一股脑每次 shell 启动都扫描全目录”更稳。

npx 安装后找不到命令

另一个实际问题是:

1
2
3
4
5
$ npx reasonix code
Need to install the following packages:
reasonix@0.53.2
Ok to proceed? (y) y
/data/data/com.vchangxiao.pystudio/files/usr/bin/sh: 1: reasonix: not found

这类问题常见原因包括:

  • npm 临时安装目录没有加入 PATH。
  • npm 生成的 .bin wrapper shebang 不可用。
  • shell 执行时找不到刚安装的命令。
  • npm config 里存在旧环境残留。

解决方向不是简单地“多修一次 shebang”,而是要给 npm 建一个明确的兼容层:

  1. 固定 npm global prefix 到 $PREFIX 或 app 管理目录。
  2. 确保 $PREFIX/bin、npm global bin、项目 node_modules/.bin 都在 PATH。
  3. 安装完成后只扫描新增或变更文件,而不是每次全量扫描。
  4. /usr/bin/env/bin/sh/usr/bin/node 这类 shebang 做定向替换。
  5. 对 npx 临时目录做特殊处理。

运行时修补要安静,不能把内部 shell 片段刷到终端。

pip 不是只需要 python 和 pip

pip install numpypip install lxmlpip install cryptography 这类命令可能需要很多系统能力:

  • cmake
  • make
  • clang
  • pkg-config
  • openssl
  • libffi
  • libxml2
  • libxslt
  • zlib
  • lapack
  • openblas

所以 PyStudio 不能只提供 pythonpip。更合理的是提供可选扩展包组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python-runtime
python, pip, setuptools, wheel

python-build-base
clang, make, cmake, pkg-config

python-science
numpy, scipy, openblas, lapack

python-xml-html
lxml, libxml2, libxslt, html parser dependencies

python-crypto-network
openssl, libffi, rust/cargo if needed

这里要注意合规和维护成本。预编译包可以提高体验,但不能假装自己是 PyPI 镜像。更稳妥的定位是:提供 Android 上 pip 构建常见包所需的系统依赖,同时对少数高频包提供预构建扩展。

tkinter 缺失不是 Python 坏了

有些用户会发现:

1
import tkinter

失败。

这不一定说明 Python 包坏了。Termux/Android 环境里 GUI 栈和桌面 Linux 不一样,tkinter 依赖 Tcl/Tk 和图形显示能力,不是 Python core 的必然组成部分。类似模块应该在清单里明确标注:

  • 是否包含;
  • 依赖哪些系统包;
  • Android App 内是否有实际显示能力;
  • 是否建议作为可选 GUI 扩展。

不要把所有 Python 标准库相关体验都归咎于 python 一个包。

proot 的特殊性

proot 更敏感,因为它涉及 syscall、ptrace、clone/vfork、Android 高版本限制和社区补丁。

构建 proot 时要确认:

  • 使用较新的上游版本。
  • 应用 Android 高版本相关补丁。
  • 包含 proot 运行需要的依赖。
  • 不要混入不相关的 Python/pip,除非明确做成 proot+distro 组合包。
  • 用诊断脚本验证 forkclonevfork、基础命令运行。

我们后来还遇到过“proot 包为什么包含 Python/pip”的问题。这就是大包思路带来的副作用。拆包后,proot 应该是 proot,distro 是 distro,Python 是 Python。组合能力由 package-set 或 app 侧安装计划表达,而不是把所有东西塞进一个 tarball。

结论

Android App 私有目录里的 Termux 风格运行时,最容易出问题的不是“有没有文件”,而是路径假设。

几个经验:

  • shell 内建命令成功不代表外部命令成功。
  • npm/pip 动态安装出来的脚本要有兼容层。
  • 自己编译的包应该构建期修补路径。
  • 官方源包和自家包要分开处理。
  • Python 工具链要拆成运行时、构建依赖和扩展能力。
  • proot 要单独验证 Android 高版本兼容补丁。

最终目标不是把 Termux 原样搬进 app,而是建立一个明确知道自己 prefix、包名、路径和镜像来源的 PyStudio runtime。