这篇是 PyStudio Termux 构建系列的第一篇,也是一开始方向转弯最大的一篇。最初我们想直接打一个“什么都有”的 bootstrap,后来发现能编译一次不等于能长期维护。真正的问题不是 CI 多跑几次,而是 bootstrap 承担了太多职责。

后来路线变成:bootstrap 只做基础环境,工具链拆成可选包,包索引负责依赖,清单站负责发现,大文件镜像负责下载。这条线一旦拆清楚,后面 Python LSP、C++、debug、proot 才有地方放。

这张图是这个系列的底图:bootstrap 越小,后续包管理和镜像策略越有空间。

最早的问题:能编译不等于能长期维护

最开始的目标很朴素:给 PyStudio 打出一个能在 Android App 私有目录里运行的 Termux 风格环境。能运行 pwdcdlspkgpythonpipnode,再逐步加上 C/C++、LSP、debugger、proot、distro 这些能力。

早期方案有几个典型坑:

  1. bootstrap 成功解压,不代表命令都能跑。
  2. pwdcd 这类 shell 内建命令快,不代表 lspkg 这些 ELF 或脚本没有路径问题。
  3. 把 Python、pip、node、clangd、lldb、proot 全塞进一个大包,下载和更新都会变重。
  4. GitHub Actions 能编译成功一次,不代表下一次上游变动后还能成功。
  5. Android 私有 app 路径不是 /data/data/com.termux/files/usr,很多上游包默认路径会失效。

这类问题不是靠多跑几次 CI 能解决的,根子在架构。

termux-generator 给出的启发

msmt2018/termux-generator 的价值在于,它证明了一件事:bootstrap 可以先变成一个小而清楚的产物,只负责把基础终端环境搭起来。

这比“直接把所有功能烤进 bootstrap”更好,因为:

  • bootstrap 构建失败时,问题范围小。
  • 工具链可以按需安装,不必每个用户都下载一大坨。
  • Python、Node、C/C++、debug、proot 可以分组构建和分组发布。
  • app 侧可以做自己的包管理,已经安装的包可以复用。

于是构建体系开始从“大包思路”转向“Termux pkg 思路”。

真正需要拆开的几层

构建侧的职责不是只产出一个压缩包,而是把包、索引、镜像和最终清单一起交付。
后来稳定下来的分层大致是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bootstrap
只负责基础 rootfs、shell、核心命令和 pkg 入口

component packages
类似 Termux/TUR 的 .deb 拆包产物

package index
Packages.xz、package-assets.json、package-indexes.json

runtime manifest
app 用来发现 bootstrap、包源、镜像和调试清单

mirror site
只放轻量 JSON 清单,不放大文件

large file mirror
ModelScope 或 GitHub Release 承担 .deb/bootstrap 下载

这样做以后,构建和安装的边界都清楚很多。

构建侧负责:

  • 找源仓库。
  • 应用 PyStudio 适配补丁。
  • 按架构构建 .deb
  • 生成 Packages.xz 和清单。
  • 把产物发布到 GitHub Release。
  • 可选同步大文件到 ModelScope。
  • 把最终 JSON 清单推到专用清单仓库。

app 侧负责:

  • 拉取 runtime-packages.json
  • 根据设备 ABI 选择包源。
  • 解析 Packages.xz
  • 递归解析 Depends / Pre-Depends
  • 下载缺失 .deb
  • 校验 SHA256。
  • 安装并记录已安装包版本。

这就接近 Termux 官方 pkg 的思路,而不是“下载一个巨型工具链压缩包”。

主仓库和薄仓库的关系

仓库架构也踩过坑。最开始容易把每个工具链都做成一个“完整仓库”,里面复制大量公共构建脚本和源仓库逻辑。这样短期方便,长期会变成维护灾难。

后来更合理的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pystudio-termux-builds
主构建仓库
保存公共 workflow、清单生成、ModelScope 同步、包索引生成

pystudio-termux-source-*
源仓库 fork 或补丁适配层
跟踪 termux、termux-packages2、TUR 等上游

toolchain 子项目
只保留差异化构建入口或参数
不复制整套源仓库

pystudio-termux-manifests
专用清单仓库
只保存最终 JSON
给 Cloudflare Pages 直接同步

这个结构的好处是:大部分重复工作在主仓库完成,工具链只是构建目标不同。将来再接入新的源仓库,也不需要为每个工具链复制一套。

GitHub Actions 里的几个坑

GitHub Actions 很适合做这种构建,但要注意几个细节。

构建矩阵要按风险分组。稳定包可以一起跑,新包、大包、容易失败的包单独跑。比如 git 这种已经成功过的包,就不要和 python-viz、cpp-lsp、debug-tools 这类复杂组合绑在一个 job 里,否则排查成本很高。

构建复用必须看版本。复用旧包不是简单地“文件存在就跳过”。上游补丁、PyStudio 适配补丁、构建脚本版本、Android 兼容补丁都可能变化。调试清单里要记录批次、包名、版本、release tag、源 commit 或补丁信息,否则后面很难判断一个包是不是“旧但还能用”。

发布和清单更新要集中处理。GitHub API 有速率限制,不要为了每个文件单独查一次是否存在。更好的办法是先拉一次 release asset 列表,建立本地索引,再查漏补缺。清单更新也尽量集中提交,减少请求次数。

跨仓库同步需要专门 token。GITHUB_TOKEN 通常只对当前仓库有权限。构建仓库要推送到专用清单仓库时,需要一个有目标仓库写权限的 <GITHUB_PAT>,在 Actions secrets 里保存为类似 <MANIFESTS_REPOSITORY_TOKEN> 的名字。

结论

这次最大的经验是:bootstrap 不应该承载全部野心。

对于 PyStudio 这类 App 内置运行时,合理结构应该是:

  • bootstrap 小而稳定;
  • 工具链拆成可选包;
  • 包源像 Termux 一样用 .debPackages.xz 管;
  • 清单站只放轻量 JSON;
  • 大文件交给 GitHub Release 和 ModelScope;
  • app 侧做自己的解析、依赖复用和安装状态管理。

这样后续要加 Python LSP、C++ LSP、debug、proot、distro、treesitter,都不会逼着用户重复下载同一份 Python 或 openssl,也不会让每次构建都从头烤一遍完整世界。