这篇讲下载入口怎么从“找个国内仓库放一下”变成一套更清楚的镜像架构。最早我想用 Gitee 做国内镜像,后来发现它适合轻量文本,不适合承载大量 release 附件和工具链包。

最后的分工更明确:GitHub Release 保存权威产物,ModelScope 承担大文件镜像,专用清单仓库保存 JSON,Cloudflare Pages 对外提供清单站。清单越轻,大文件镜像越独立,后面才容易按区域优化。

Gitee 的问题不是不能用,而是不适合在这套系统里承担大文件镜像角色。

Gitee 方案为什么失败

一开始选 Gitee 的理由很直接:大陆访问速度更好,轻量 JSON 清单应该很适合放上去。

实际踩坑后发现,Gitee release 附件有几个限制:

  • 单个附件大小有限制。
  • 仓库附件总容量有限制。
  • 大量 .deb 或工具链压缩包会很快逼近上限。
  • 上传失败后重试成本高。

比如 C/C++ 工具链压缩包很容易超过 100 MiB。即使某些项目等级能放到更大的单文件,也挡不住总容量限制。把构建产物全推到 Gitee,不是长期方案。

所以 Gitee 只适合“轻量索引”,不适合“包仓库镜像”。后来我们干脆舍弃 Gitee 同步,避免架构里多一个半可用的入口。

ModelScope 更适合放大文件

ModelScope 的定位更适合承载较大的数据集文件,因此它被用来做大文件镜像:

1
https://modelscope.cn/datasets/yourba/pystudio-termux-builds

常见路径形态类似:

1
2
3
repo/<owner>/<repo>/<release-tag>/<profile>/<arch>/<file>
bootstrap/<owner>/<repo>/<release-tag>/<profile>/<arch>/<file>
pool/<owner>/<repo>/<pool-release-tag>/<arch>/<file>

同步脚本使用的 token 应该保存在 CI secret 里,例如:

1
MODELSCOPE_TOKEN=<MODELSCOPE_TOKEN>

文章、日志和仓库里都不应该出现真实 token。

ModelScope 的角色很明确:

  • 存放 .deb
  • 存放 Packages.xz 镜像。
  • 存放 bootstrap 产物镜像。
  • 给国内下载提供备用路径。

它不负责构建,也不应该成为唯一权威源。

清单站应该只放轻量 JSON

大文件镜像解决后,还需要一个稳定入口告诉 app:

  • 当前有哪些 bootstrap。
  • 有哪些 package-set。
  • 各架构的包索引在哪里。
  • 哪些镜像优先。
  • ModelScope 是否同步过。
  • 调试索引在哪里。

这类文件都很小,适合静态站托管。

最后选了一个专用仓库:

1
https://github.com/vg188/pystudio-termux-manifests

这个仓库只放最终清单文件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
runtime-packages.json
origin/runtime-packages.json
mirror/runtime-packages.json
mirror-status.json
index.json
package-assets.json
package-indexes.json
package-index-cache.json
package-build-batches.json
debug/index.json
debug/package-assets.json
debug/package-indexes.json
debug/package-index-cache.json
debug/package-build-batches.json

它不放源码,不放构建脚本,不放 patch,不放 .deb

这样 Cloudflare Pages 可以直接绑定这个仓库,同步出来的站点就是纯清单站:

1
https://pystudio-termux-manifests.pages.dev/runtime-packages.json

为什么不直接用构建仓库的 GitHub Pages

构建仓库里有 workflow、脚本、patch、迁移清单、调试辅助文件。虽然 GitHub Pages 可以只部署一个生成目录,但从架构上看,它仍然把“构建系统”和“清单发布站”绑在同一个仓库。

专用清单仓库更干净:

  • Cloudflare Pages 可以直接绑定仓库。
  • 仓库内容就是线上内容。
  • app agent 检查清单时不会看到构建系统文件。
  • 后续切换 Pages、Workers、R2 或其他静态托管时更简单。

构建仓库只需要在 workflow 末尾把 dist/manifest-site 推到清单仓库。

GitHub Actions 跨仓库推送的坑

GITHUB_TOKEN 默认通常只适合操作当前仓库。构建仓库要推送到 vg188/pystudio-termux-manifests,需要一个额外 token。

做法是:

1
MANIFESTS_REPOSITORY_TOKEN=<GITHUB_PAT>

然后在 workflow 里使用:

1
remote_url="https://x-access-token:${MANIFESTS_TOKEN}@github.com/${MANIFEST_SITE_REPOSITORY}.git"

注意不要把 ${MANIFESTS_TOKEN} 打印到日志。GitHub 会帮忙遮蔽 secrets,但脚本里仍然应该避免 echo token。

清单里的镜像优先级

优先级不是写死在 App 里,而是跟随清单发布。下载失败时,App 才有可解释的回退路径。
现在默认清单入口是 Cloudflare Pages:

1
2
3
4
5
6
{
"id": "manifest-site",
"kind": "manifest",
"manifestUrl": "https://pystudio-termux-manifests.pages.dev/runtime-packages.json",
"priority": 1
}

ModelScope 作为 manifest mirror 和大文件 mirror 存在:

1
2
3
4
5
6
7
{
"id": "modelscope",
"kind": "manifest",
"manifestUrl": "https://modelscope.cn/datasets/yourba/pystudio-termux-builds/resolve/master/runtime-packages.json",
"priority": 30,
"region": "CN"
}

实际 app 侧推荐策略:

  1. 先拉 Cloudflare Pages 清单。
  2. 清单失败时回退 GitHub raw。
  3. 下载大文件时优先使用清单里 priority 更低的镜像。
  4. 国内场景可优先选择 region = "CN" 的 ModelScope。
  5. 校验 SHA256,失败就换镜像。

结论

这次镜像架构最后变成:

1
2
3
4
5
6
7
8
9
10
GitHub Actions
构建 .deb、bootstrap、Packages.xz
|
+-- GitHub Release 保存权威产物
|
+-- ModelScope 保存大文件镜像
|
+-- 专用清单仓库保存 JSON
|
+-- Cloudflare Pages 对外提供清单站

Gitee 的问题不是不能用,而是不适合承担这个系统里的大文件镜像角色。清单站越轻,越容易迁移;大文件镜像越独立,越容易按区域优化。