这篇是 PyStudio Termux 构建系列的第二篇,主题是包管理模型。工具链大包一开始很诱人:用户要 Python 就下 python-toolchain,要 Node 就下 node-toolchain。但很快会遇到重复依赖、粗粒度更新、构建失败难定位和版本不可追踪。

最后更接近 Termux/TUR 的路线:一个包一个版本,四个架构对应四个 .deb,每个架构有自己的 Packages.xz。App 侧不必完整复刻 apt,但至少要有一个能解析依赖、跳过已安装包、校验下载结果的轻量 resolver。

文件数量变多不是坏事,失去索引才是坏事。

大包方案的坑

大包方案一开始很自然:

1
2
3
4
python-toolchain-aarch64.tar.gz
node-toolchain-aarch64.tar.gz
cpp-toolchain-aarch64.tar.gz
debug-tools-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
2
3
4
5
6
7
8
python_3.x.x-aarch64.deb
python_3.x.x-arm.deb
python_3.x.x-i686.deb
python_3.x.x-x86_64.deb

openssl_3.x.x-aarch64.deb
xz_5.x.x-aarch64.deb
libffi_3.x.x-aarch64.deb

每个架构有自己的 Packages.xz

1
2
3
4
dists/pystudio/main/binary-aarch64/Packages.xz
dists/pystudio/main/binary-arm/Packages.xz
dists/pystudio/main/binary-i686/Packages.xz
dists/pystudio/main/binary-x86_64/Packages.xz

app 侧不再下载“大工具链文件”,而是:

  1. 下载总清单 runtime-packages.json
  2. 选中用户要安装的能力包,例如 python-runtime
  3. 找到对应架构的 Packages.xz
  4. 解析 Debian stanza。
  5. 递归解析依赖。
  6. 下载缺失 .deb
  7. 安装并记录状态。

这个模型的关键不是“文件变多”,而是“每个文件都有清楚身份”。

总清单负责发现,不负责替代包管理

总清单的职责应该克制。它不是另一个 apt,也不应该手写每个包的完整依赖树。它主要负责告诉 app:

  • 当前支持哪些架构。
  • 有哪些 bootstrap。
  • 有哪些 package-set。
  • 每个 package-set 对应哪些仓库索引。
  • 每个索引在哪里下载。
  • GitHub Release 和 ModelScope 的镜像地址是什么。
  • 调试清单在哪里。

真正的包级依赖关系,仍然以 Packages.xz 为准。

一个简化后的清单结构类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"schemaVersion": 5,
"architectures": ["aarch64", "arm", "i686", "x86_64"],
"manifestMirrors": [
{
"id": "manifest-site",
"manifestUrl": "https://pystudio-termux-manifests.pages.dev/runtime-packages.json",
"priority": 1
}
],
"repositories": {
"repo:python:aarch64": {
"architecture": "aarch64",
"index": {
"downloadUrl": "https://example.invalid/Packages.xz",
"sha256": "<SHA256>"
}
}
},
"entries": [
{
"id": "python-runtime",
"kind": "package-set",
"packages": ["python", "python-pip"]
}
]
}

实际项目里的清单由 vg188/pystudio-termux-builds 生成,并同步到 vg188/pystudio-termux-manifests

app 侧要做的不是 apt 全家桶

这套 resolver 的目标很克制:会读包索引、会算依赖、会校验文件,然后把安装过程交给终端。
app 侧不一定要完整复刻 apt。更现实的实现是一个“小型离线 apt resolver”:

  • 能读取 Packages.xz
  • 能解析 PackageVersionArchitectureFilenameSHA256
  • 能解析 DependsPre-Depends
  • 能识别 Architecture: all
  • 能按已安装包版本跳过重复安装。
  • 能对下载文件做 SHA256 校验。
  • 能调用 dpkg 或等价安装逻辑。

这已经足够覆盖 PyStudio 的运行时安装。

真正要小心的是状态管理。app 要记录:

1
2
3
4
5
6
7
8
包名
版本
架构
安装时间
来源仓库
SHA256
是否为用户显式安装
是否为依赖安装

这样用户安装 debug-tools 时,如果 Python 已经存在,就不会再下载一份 Python。

调试清单的价值

构建侧还需要维护调试用清单,例如:

  • package-assets.json
  • package-indexes.json
  • package-index-cache.json
  • package-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 本来就证明过的模型。