ci(cache): rust-cache 的 key 带上镜像维度,发布构建不再吃 24.04 编的 target/(M270)

M269 已端到端定性:`aws-lc-sys` 的 `bcm.o` 在 glibc≥2.38 的头下编出 `__isoc23_sscanf`,
被从 24.04 的缓存恢复到 22.04(glibc 2.35)去链接 ⇒ undefined。根因是 `Swatinem/rust-cache@v2`
的 key 只有 `-Linux-X64`:`ci.yml` 的 `build`(ubuntu-latest = 24.04)与 `release-build.yml`
的 `build`(ubuntu-22.04)同 job id、同 rustc、同一份 `Cargo.lock` ⇒ key 逐字节相同。

- 7 处 `Swatinem/rust-cache@v2` 全部加显式 `key:`(发布侧 `inputs.os-inputs.slot`,CI 侧矩阵
  维度或该 job 的字面量镜像标签)。`key` 落在 key 的**前缀段**,故主 key 与兜底 `restoreKey`
  两条恢复路径一起分开。
- 演算判据(按 config.ts 复刻拼接,12 站点 × 2 锁状态两两比对):改前主 key 相同 2 对、
  restoreKey 跨站点前缀命中 4 组;改后均为 0。
- 机制注释写进两个 workflow(`-C metadata` 不含 glibc ⇒ 恢复后 cargo 判 fresh 的因果链),
  完整推导、`cache-targets` 的取舍结论与「抓得住/抓不住」写进 packaging/docs/build-and-ci.md §3。
- 两个 workflow 仍 ≤400 行(397 / 399):加注释腾地方时按「判据一字不动、只并句」精简了
  M164/M171/M184/M147 几处既有注释。
This commit is contained in:
AskaEth committed 2026-11-12 10:18:06 +08:00
1 parent 14e8cc4ef2
commit 93dc27bed8
3 files changed
+148 -48

No files matched your search

+84
View File
@@ -137,3 +137,87 @@ python packaging/make_artifacts.py --slot windows-arm64 \
Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两者都属"标准工具链"(D21),
但发布产物要**不依赖 VC++ 可再发行包**,`+crt-static` 把 CRT 静态链进 exe,Win10+ 的 UCRT 本身是
系统组件,于是 exe 真正单文件可拷走。GNU 交叉产物在 CI 上还要拉 MinGW 链接器,收益为零。
### 构建缓存 key:必须带镜像维度(`Swatinem/rust-cache`,M270)
**事故形态(M269 端到端复现)**:`linux-x64` 发布构建红在链接期,`libaws_lc_sys-*.rlib` 里的 `bcm.o` 引用
`__isoc23_sscanf` / `__isoc23_strtol` —— 那是 glibc ≥ 2.38 的头把 `sscanf` / `strtol` 改写成 `__isoc23_*` 之后才有的符号,
而链接发生在 `ubuntu-22.04`(glibc 2.35)。干净 22.04 容器里同一条命令 **exit 0**,干净 24.04 也 exit 0 ⇒ 不是「镜像版本
差异」本身,是**缓存把 24.04 编出来的 `.o` 搬到了 22.04**。
**key 是怎么算出来的**(`rust-cache@v2` 的 `src/config.ts`;`add-job-id-key` 与 `add-rust-environment-hash-key` 两项默认都是 true):
```
key = prefix-key「v0-rust」
+ 「-<key 输入>」 ← 本仓 M270 才第一次用(下文「修法」)
+ 「-<GITHUB_JOB>」 ← 是 job **id**,不是 job 的 `name:`
+ 「-<os.type()>-<os.arch()>」 ← Linux-X64 / Windows_NT-X64 / Darwin-ARM64,**没有镜像标签**
+ 「-<env hash 前 8 位>」 ← rustc -vV(release + host + commit-hash)+ CARGO*/CC/CFLAGS/CXX/CMAKE/RUST* 的 sha1
restoreKey = 到这里为止的一整串 ← 兜底恢复 key(**不含**锁哈希)
key += 「-<manifest/lock hash 前 8 位>」 ← 各 Cargo.toml(版本号被抹平)+ Cargo.lock + .cargo/config.toml
cacheKey = key
```
两条恢复路径(`src/restore.ts` 就是一句 `restoreCache(paths, cacheKey, [restoreKey])`):
- ① 主 key **逐字节相同** → `full match = true`,**不清理** `target/`,直接吃别人的产物;
- ② 主 key 未命中、`restoreKey` **前缀匹配**到某条已有缓存 → `full match = false`,先 `cleanTargetDir`(只清 profile 层),
依赖的 `.o` / `.rlib` 留着算 fresh —— M269 那条「依赖 fresh + 旧系统链接」正是走这条。
**为什么 CI 与发布两侧会撞**:`ci.yml` 的 job id 是 `build`(跑 `ubuntu-latest` = 24.04),`release-build.yml` 的 job id
也是 `build`(跑 `ubuntu-22.04`);`rust-toolchain.toml` 是 `channel = "stable"`(同一天两边解析到同一版 rustc),
`Cargo.lock` 也是同一份 ⇒ env hash 与锁哈希全相同 ⇒ 两个 key **逐字节相同**。这串表达式里**没有任何一项**能反映出镜像。
**为什么 cargo 自己不发现**:freshness 判据是 fingerprint,记的是编译器版本、features、profile、依赖 metadata ——
而 `-C metadata` 里**没有 glibc 版本**。于是恢复来的 `.rlib` 被判 fresh、不重编,问题一路拖到最后的链接。
**修法**:两个文件里 7 处 `Swatinem/rust-cache@v2` 全部加显式 `key:` —— 发布侧 `${{ inputs.os }}-${{ inputs.slot }}`
(如 `ubuntu-22.04-linux-x64`),CI 侧用 `${{ matrix.os }}`(矩阵维度)或该 job 的字面量标签。`key` 输入落在**前缀段**
(`v0-rust-<key>-<job>-…`),所以主 key 与兜底 `restoreKey` 同时带上镜像维度,**两条恢复路径一起断**。
**演算(这次的核心判据)**:把上面那段拼接写成脚本,对 12 个站点 × 2 个 `Cargo.lock` 状态逐对做前缀判定,改前 / 改后:
| 对照 | 改前(7 处都没有 `key:`) | 改后 |
| --- | --- | --- |
| 路径①:主 key 逐字节相同 | **2 对**:`ci/build` == `rel/linux-x64` | 0 对 |
| 路径②:`restoreKey` 前缀命中**别的站点** | **4 组**(`v0-rust-build-Linux-X64-<env>` 命中 `…-<另一个锁哈希>`) | 0 组 |
拼出来长这样(env / 锁哈希用占位值,取不同的值不影响结论):
```
改前 ci/build restoreKey = v0-rust-build-Linux-X64-3f9a1c07
cacheKey = v0-rust-build-Linux-X64-3f9a1c07-b2d6e815
改前 rel/linux-x64 restoreKey = v0-rust-build-Linux-X64-3f9a1c07 ← 与上行完全相同 ⇒ 路径①当场命中
cacheKey = v0-rust-build-Linux-X64-3f9a1c07-b2d6e815
改后 ci/build restoreKey = v0-rust-ubuntu-latest-build-Linux-X64-3f9a1c07
cacheKey = v0-rust-ubuntu-latest-build-Linux-X64-3f9a1c07-b2d6e815
改后 rel/linux-x64 restoreKey = v0-rust-ubuntu-22.04-linux-x64-build-Linux-X64-3f9a1c07
cacheKey = v0-rust-ubuntu-22.04-linux-x64-build-Linux-X64-3f9a1c07-b2d6e815
```
`v0-rust-ubuntu-latest-…` 与 `v0-rust-ubuntu-22.04-…` 在第一个 `-` 之后就分叉,任何一方的 `restoreKey` 都不是另一方
`cacheKey` 的前缀(脚本对 12 个站点两两验证,主 key 相同 0 对、前缀命中 0 组)。前缀判定是**字面量**的 —— 只要
`key:` 里不出现 `-<jobid>-<os类型>-<arch>-<env哈希>` 这种形状的文本(runner 标签不会是),两条 key 就不可能重新互为
前缀;将来谁把自由文本塞进 `key:`,这一条要自己重新确认。
**`cache-targets` 要不要一起关掉:不关。** 关掉就只缓存 registry / git 源码,跨镜像确实彻底无风险,但每个 job 都要
**从头编译整棵依赖树**(M269 干净容器实测同一条命令 18m43s 量级),日更的 CI 矩阵和每次发版都得付这笔钱。加了镜像维度
之后,`target/` 的恢复永远来自**同一个镜像标签**下的缓存,M269 那条路径已经被切断;剩下被同一把 key 一起管住的还有
`cache-bin`(`~/.cargo/bin` 里的 rustup shim 也是宿主编译的二进制),它与 `target/` 同域,没有引入额外风险。
**抓得住什么 / 抓不住什么**:
- 抓得住**跨镜像复用**:不同 runner 标签之间(`ubuntu-22.04` ↔ `ubuntu-latest`)不再共享 `target/`,两条恢复路径都断,
判据就是上面那张演算表。
- **抓不住「同一个镜像标签、但底层镜像悄悄升级」**:`ubuntu-latest` 从 24.04 升到 26.04 时分母还是同一个标签,key 不变,
旧缓存照样能恢复;22.04 打一次 glibc 补丁同理。这一档**没有自动化机制**,靠手动 bump:改 `prefix-key`
(`v0-rust` → `v1-rust`)或改本处 `key:` 的字面量,一次性作废旧缓存。**别把这修法当成「以后不会再有这类问题」。**
- 顺带:`rust-toolchain.toml` 是 `stable`,rustc 一升版 env hash 就变 ⇒ **编译器升级自动冷启动**,这一档本来就不在风险里。
**代价与一次性影响**:缓存条数从「每 job 一条」变成「每 job × 每镜像一条」,GitHub 的 per-repo 上限是 10 GB(LRU 淘汰 +
7 天未访问过期),命中率靠 `restoreKey` 的锁哈希前缀兜着。**这次改动本身会让现有缓存一次性失效**(旧 key 再也匹配不上,
交给 LRU 自然淘汰),合入后第一轮是冷缓存,别把它当成回归。
**没验的部分**:真实 Actions 上的缓存命中 / 未命中本地验不了 —— 本机只能证明 key 字符串确实分开了(上面的演算)以及
workflow 仍被真 YAML 解析器接受(`python packaging/check_workflows_yaml.py`,M190 的门禁)。「命中后日志里不再出现
`__isoc23_*`」要等下一次 tag 的 `linux-x64` 构建日志说话。