PyStudio Termux 构建踩坑记(二):像 Termux pkg 一样拆包,而不是发布工具链大包
这篇是 PyStudio Termux 构建系列的第二篇,主题是包管理模型。工具链大包一开始很诱人:用户要 Python 就下 python-toolchain,要 Node 就下 node-toolchain。但很快会遇到重复依赖、粗粒度更新、构建失败难定位和版本不可追踪。
最后更接近 Termux/TUR 的路线:一个包一个版本,四个架构对应四个 .deb,每个架构有自己的 Packages.xz。App 侧不必完整复刻 apt,但至少要有一个能解析依赖、跳过已安装包、校验下载结果的轻量 resolver。
文件数量变多不是坏事,失去索引才是坏事。
大包方案的坑
大包方案一开始很自然:
1 | python-toolchain-aarch64.tar.gz |
用户需要 Python,就下载 python-toolchain。需要 Node,就下载 node-toolchain。看起来简单,app 侧也容易做:下载、解压、刷新 PATH。
但后面会碰到几个现实问题。
依赖会重复。Python 包里可能有 openssl、libffi、xz、zlib;debug-tools 里为了 debugpy 又带一份 Python 相关依赖;cpp-toolchain 里可能也带 zlib、libxml2、ncurses。用户装得越多,重复越多。
更新也会变粗糙。如果 openssl 要更新,是否要重发所有工具链大包?如果只重发 Python,cpp-toolchain 里的 openssl 怎么办?如果 app 已经装了一个更新版本,下载另一个旧工具链会不会覆盖?
构建会被拖重。一个大包里只要一个包失败,整个工具链 job 就失败。调试时也很难判断到底是 clangd、bear、compiledb、python 还是依赖库出了问题。
还有生态割裂。Termux 的世界天然是 .deb、依赖字段和包索引。我们如果只发 tarball,就等于放弃了很多已有经验。
拆包后的基本模型
拆包后,每个构建产物更像这样:
1 | python_3.x.x-aarch64.deb |
每个架构有自己的 Packages.xz:
1 | dists/pystudio/main/binary-aarch64/Packages.xz |
app 侧不再下载“大工具链文件”,而是:
- 下载总清单
runtime-packages.json。 - 选中用户要安装的能力包,例如
python-runtime。 - 找到对应架构的
Packages.xz。 - 解析 Debian stanza。
- 递归解析依赖。
- 下载缺失
.deb。 - 安装并记录状态。
这个模型的关键不是“文件变多”,而是“每个文件都有清楚身份”。
总清单负责发现,不负责替代包管理
总清单的职责应该克制。它不是另一个 apt,也不应该手写每个包的完整依赖树。它主要负责告诉 app:
- 当前支持哪些架构。
- 有哪些 bootstrap。
- 有哪些 package-set。
- 每个 package-set 对应哪些仓库索引。
- 每个索引在哪里下载。
- GitHub Release 和 ModelScope 的镜像地址是什么。
- 调试清单在哪里。
真正的包级依赖关系,仍然以 Packages.xz 为准。
一个简化后的清单结构类似:
1 | { |
实际项目里的清单由 vg188/pystudio-termux-builds 生成,并同步到 vg188/pystudio-termux-manifests。
app 侧要做的不是 apt 全家桶
这套 resolver 的目标很克制:会读包索引、会算依赖、会校验文件,然后把安装过程交给终端。
app 侧不一定要完整复刻 apt。更现实的实现是一个“小型离线 apt resolver”:
- 能读取
Packages.xz。 - 能解析
Package、Version、Architecture、Filename、SHA256。 - 能解析
Depends和Pre-Depends。 - 能识别
Architecture: all。 - 能按已安装包版本跳过重复安装。
- 能对下载文件做 SHA256 校验。
- 能调用
dpkg或等价安装逻辑。
这已经足够覆盖 PyStudio 的运行时安装。
真正要小心的是状态管理。app 要记录:
1 | 包名 |
这样用户安装 debug-tools 时,如果 Python 已经存在,就不会再下载一份 Python。
调试清单的价值
构建侧还需要维护调试用清单,例如:
package-assets.jsonpackage-indexes.jsonpackage-index-cache.jsonpackage-build-batches.json
这些清单不是给普通用户看的,而是给构建维护者和 app agent 用的。
它们需要回答几个问题:
- 某个
.deb来自哪个 release tag? - 对应哪个源仓库和构建批次?
- 四个架构是否齐全?
- 哪些包是旧批次复用的?
- 哪些包需要因为上游补丁更新而重建?
如果没有这些信息,后期会很难判断“这个包为什么还没更新”。
文件数量多不是坏事,失去索引才是坏事
拆包后文件数量一定会增加。这个现象一开始会让人不安:是不是最后上传的产物会有几千上万个零碎文件?
答案是:包管理世界本来就是这样的,但必须有索引。
一个包一个版本四个架构四个文件,是清爽的。真正糟糕的是:
- 同一个包在多个地方重复上传。
- 文件名里塞太多调试信息。
- 没有总索引记录真实地址。
- 没有批次清单记录构建来源。
所以更好的做法是:文件名保持包管理语义,额外信息放到调试清单。
例如文件名保持:
1 | python_3.12.4-1_aarch64.deb |
而不是:
1 | python--source-primary--batch-r100--patched-android14--aarch64.deb |
后者看起来信息多,其实维护很痛。
结论
PyStudio 最后需要的是一个自己的轻量包管理层,而不是一堆越来越大的工具链压缩包。
拆包以后,用户只下载真正需要的包,已安装依赖可以复用,构建失败也更容易定位。上游补丁更新时,影响范围能落到具体包和具体批次,不必把整个工具链重新发一遍。
这条路前期会多写一些清单和 resolver,但长期看,比维护巨型 tarball 轻松得多,也更接近 Termux 本来就证明过的模型。