PyStudio Termux 构建踩坑记(四):Android 私有目录里的路径、npm、pip 和 proot
这篇讲的是 PyStudio 运行时最容易被低估的一类问题:文件都在,命令也能看到,但脚本、解释器、动态库和 prefix 仍然可能指向错误位置。Termux 官方环境里能跑,不代表搬到 Android App 私有目录后还能跑。
我把它拆成几条线:shell 内建命令和外部 ELF 的差异、npm/pip 动态产物的 shebang 兼容、自家包和官方源包的边界,以及 proot 这种 syscall 敏感工具为什么必须单独验收。
只要 prefix 变了,脚本、wrapper、ELF loader 和动态库路径都可能跟着出问题。
为什么 pwd 和 cd 正常,ls 和 pkg 却卡住
最早的现象很迷惑:新 app 里运行 pwd、cd 反应很快,但 ls、pkg 回车后没有反应。
后来回头看,这个现象其实很典型:
pwd、cd多数时候是 shell 内建命令。ls是外部 ELF。pkg是脚本和包管理逻辑。
内建命令能跑,只能证明 shell 基本活着。外部命令卡住,可能是:
- ELF loader 路径不对。
- 动态库查找路径不对。
- shebang 指向不存在的解释器。
- wrapper 脚本里的 PREFIX 写死。
- app 私有目录权限或挂载行为和 Termux 不一致。
所以不能用 pwd 成功判断整个 runtime 成功。
npm 的典型坑:/usr/bin/env
一个实际错误是:
1 | /data/data/com.vchangxiao.pystudio/files/usr/bin/nrm: |
原因是很多 npm 包安装命令行工具时,会生成这样的 shebang:
1 |
在标准 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 | 我们构建的 .deb |
这比“一股脑每次 shell 启动都扫描全目录”更稳。
npx 安装后找不到命令
另一个实际问题是:
1 | $ npx reasonix code |
这类问题常见原因包括:
- npm 临时安装目录没有加入 PATH。
- npm 生成的
.binwrapper shebang 不可用。 - shell 执行时找不到刚安装的命令。
- npm config 里存在旧环境残留。
解决方向不是简单地“多修一次 shebang”,而是要给 npm 建一个明确的兼容层:
- 固定 npm global prefix 到
$PREFIX或 app 管理目录。 - 确保
$PREFIX/bin、npm global bin、项目node_modules/.bin都在 PATH。 - 安装完成后只扫描新增或变更文件,而不是每次全量扫描。
- 对
/usr/bin/env、/bin/sh、/usr/bin/node这类 shebang 做定向替换。 - 对 npx 临时目录做特殊处理。
运行时修补要安静,不能把内部 shell 片段刷到终端。
pip 不是只需要 python 和 pip
pip install numpy、pip install lxml、pip install cryptography 这类命令可能需要很多系统能力:
cmakemakeclangpkg-configopenssllibffilibxml2libxsltzliblapackopenblas
所以 PyStudio 不能只提供 python 和 pip。更合理的是提供可选扩展包组:
1 | python-runtime |
这里要注意合规和维护成本。预编译包可以提高体验,但不能假装自己是 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 组合包。
- 用诊断脚本验证
fork、clone、vfork、基础命令运行。
我们后来还遇到过“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。