diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bc6635..1c17122 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,10 @@ name: CI # 主干门禁:fmt / clippy / test / build 双平台矩阵。 # 架构守门(core 零 UI、文件 ≤400 行、依赖许可)见 guards.yml。 # 平台基线 Win10+(标准工具链产物),平台矩阵见 AGENTS.md §7。 +# (M270)每个 `Swatinem/rust-cache@v2` 都显式写 `key:` = **该 job 的 runner 镜像标签**(不是「更保险」):v2 的 key +# 只有 `os.type()-os.arch()`(`Linux-X64`),**没有镜像标签/glibc** ⇒ 本文件 `build`(ubuntu-latest = 24.04)与 +# `release-build.yml` 的 `build`(ubuntu-22.04 = glibc 2.35)共用同一份 `target/`。因果链见 `release-build.yml` +# 那处注释;`cache-targets` 取舍、剩余风险与 key 演算见 packaging/docs/build-and-ci.md §3「构建缓存」。 # # M118(D26):根 Cargo.toml 已 `exclude = ["crates/app"]`(Slint 退役树,源码原样保留)。 # 所以本文件里所有 `--workspace` 都**不再**覆盖 `unlucky-app`:CI 不再编 slint 1.18 + usvg 0.48 @@ -103,6 +107,8 @@ jobs: with: components: clippy - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.os }} # `--workspace` 现在是 8 个 crate,不含 `unlucky-app`(M118 的 workspace exclude), # 所以这条命令不会拉起 slint / usvg / i-slint-*。 - run: cargo clippy --workspace --all-targets -- -D warnings @@ -125,17 +131,17 @@ jobs: libwayland-dev libxcb1-dev - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.os }} # ---- 下载页选项区的四条像素守门(M127):先出图,再把路径喂给 `--lib` ---- - # 之前这四条靠开发者本机手动设 `ULL_DL_*_SHOT`(不设就 SKIP),而本文件里 grep 这些 - # 变量零命中 ⇒ **CI 里一次都没跑过**。空跑的守门长期等于没有,所以这两步是它们的第一处 - # 常驻调用点。(另有一道保险:变量缺失且 `CI` 已设置时,守门自己会失败而不是跳过。 - # 于是将来谁把下面出图那步重构掉,构建会红。) + # 之前这四条靠开发者本机手动设 `ULL_DL_*_SHOT`(不设就 SKIP),而本文件里 grep 这些变量零命中 ⇒ + # **CI 里一次都没跑过**。空跑的守门长期等于没有,所以这两步是它们的第一处常驻调用点。(另有一道保险: + # 变量缺失且 `CI` 已设置时,守门自己会失败而不是跳过 ⇒ 将来谁重构掉下面出图那步,构建会红。) # - # **为什么只在 Windows**:断言里有几条直接量墨迹(行高 44、版本号墨迹条数、图标药丸 - # 40×40),这些数字按本仓字体栈(Segoe UI / Consolas)标定;Linux/macOS runner 上 - # 这套字体不存在,回退字体的字高与行距一变就是假红。同一条限制也写在 - # `crates/app-iced/src/pages/download/tests/options_shot/feed.rs` 的模块文档里 - # (非 Windows 喂了图会当场失败),不靠「碰巧只配了一处」。 + # **为什么只在 Windows**:断言里有几条直接量墨迹(行高 44、版本号墨迹条数、图标药丸 40×40),这些数字 + # 按本仓字体栈(Segoe UI / Consolas)标定;Linux/macOS runner 上这套字体不存在,回退字体的字高与行距 + # 一变就是假红。同一条限制也写在 `crates/app-iced/src/pages/download/tests/options_shot/feed.rs` 的 + # 模块文档里(非 Windows 喂了图会当场失败),不靠「碰巧只配了一处」。 - name: 出四张像素守门用的图并写进 $GITHUB_ENV(仅 Windows) if: runner.os == 'Windows' shell: bash @@ -177,22 +183,20 @@ jobs: name: dl-shots path: ${{ runner.temp }}/dl-shots/ if-no-files-found: warn - # 同 clippy:`--workspace` 不含 `unlucky-app`(M118),Slint 退役树不再被 `--all-targets` 拉起。 - # 也因此**不要**再写 `--exclude unlucky-app`:它已不是成员,那面旗子只会打一条 - # `excluded package(s) … not found in workspace` 的警告然后空转(实测 exit 0,不报错)。 + # 同 clippy:`--workspace` 不含 `unlucky-app`(M118)。**别再写 `--exclude unlucky-app`**:它已不是成员, + # 只会打一条 `excluded package(s) … not found in workspace` 的警告然后空转(实测 exit 0,不报错)。 # # ---- 取证(M147):下面是同一件事的两个平台版本,不是两条不同的测试 ---- - # M144 的三条红里两条已在 docker 里定位到行,第三条 `test (windows)` 在 run 里只留下 - # `Process completed with exit code 1`——而 cargo 自己的失败码是 **101**,所以那个 1 的 - # 来源不明(包装层洗码 / 进程被杀,两说都未排除)。job log 要登录才能读(匿名 403), - # 但**注解匿名可读**(`GET /check-runs//annotations` → 200,M144 实证)。所以这三件 - # 事各自对准一种「看不见」: - # 1) `--no-fail-fast`:cargo 默认在第一个失败的 target 就停(M144 实测 Linux 只跑了 - # 7 个 test binary 就退出),后面还有没有第二条失败完全看不到; - # 2) 把 `error` / `failures:` / panicked 行升成 `::error::` 注解:拿不到 job log 时, - # 注解是匿名侧唯一能取到的失败正文; - # 3) 显式打印 cargo 的退出码:把「包装层把码洗成 1」「cargo 真失败(101)」「进程被杀 - # 只给码、零诊断」三种情形分开——这正是 M144 没定的那一半。 + # M144 的第三条红 `test (windows)` 只留下 `Process completed with exit code 1`,而 cargo 自己的失败码 + # 是 **101** ⇒ 那个 1 的来源不明(包装层洗码 / 进程被杀,两说都未排除);job log 要登录才能读 + # (匿名 403),但**注解匿名可读**(`GET /check-runs//annotations` → 200,M144 实证)。三件事 + # 各对准一种「看不见」: + # 1) `--no-fail-fast`:cargo 默认在第一个失败的 target 就停(M144 实测 Linux 只跑 7 个 test + # binary 就退出),后面还有没有第二条失败完全看不到; + # 2) `error` / `failures:` / panicked 行升成 `::error::` 注解:拿不到 job log 时,注解是匿名侧 + # 唯一能取到的失败正文; + # 3) 显式打印 cargo 的退出码:把「包装层把码洗成 1」「cargo 真失败(101)」「进程被杀只给码、零诊断」 + # 分开——这正是 M144 没定的那一半。 - name: cargo test --workspace --all-targets(ubuntu,失败升注解) if: runner.os == 'Linux' run: | @@ -215,8 +219,8 @@ jobs: if: runner.os == 'Windows' shell: pwsh env: - # M171 跨卷守卫:runner 到底有没有第二块可写卷没有核实手段,所以不赌 —— 显式声明豁免。 - # 豁免是惰性的:真探到第二块卷时它不参与判断(见 install_xdev_cases 的真值表用例)。 + # M171 跨卷守卫:runner 有没有第二块可写卷没核实手段,所以不赌 ⇒ 显式豁免(惰性:真探到就不参与 + # 判断,见 install_xdev_cases 的真值表用例)。 ULL_XDEV_TEST_WAIVE: "1" run: | # pwsh 没有 PIPESTATUS(那是 bash 语法):拿 `$LASTEXITCODE` —— Tee-Object 是 cmdlet, @@ -252,6 +256,10 @@ jobs: libwayland-dev libxcb1-dev - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + with: + # M270:本 job 就是与发布侧**同名同 key** 的那一个(都叫 build,镜像 24.04 vs 22.04)。 + # 镜像标签进 key 前缀之后,主 key 与兜底 restoreKey 两条恢复路径都不再跨镜像命中。 + key: ${{ matrix.os }} # 同上:`--workspace` 不含 `unlucky-app`(M118)。被摘掉的正是 release 档 # (lto=fat + codegen-units=1)最贵的那棵 slint + usvg 树。 - run: cargo build --workspace --release @@ -294,9 +302,10 @@ jobs: fonts-dejavu-core fonts-liberation2 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - # 用 debug 而不是 release:本 job 验的是「运行时渲染通路能通」,不是优化后二进制的行为; - # release 的 lto="fat" + codegen-units=1 会把这一步拖到十几分钟却不带来额外证据。 - # M118:bin 从 Slint 版 `unlucky-launcher` 换成 iced 版(见上方 job 注释)。 + with: + key: ubuntu-latest + # 用 debug 而不是 release:「运行时渲染通路能通」与优化无关,release 的 lto="fat" + codegen-units=1 + # 只会把这一步拖到十几分钟。M118:bin 从 Slint 版 `unlucky-launcher` 换成 iced 版(见上方 job 注释)。 - run: cargo build --bin unlucky-launcher-iced --locked - name: 在 Xvfb 里跑一帧并截图(退出码非 0 即失败) run: | @@ -335,6 +344,8 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + with: + key: macos-latest - run: cargo build --bin unlucky-launcher-iced --locked - name: 尝试截图(macOS 没有 X 服务器,直接跑;失败即为结论) run: | @@ -378,6 +389,8 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + with: + key: windows-latest - name: update-e2e run: > cargo test --release -p unlucky-cli --features update-test-source diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 20445bc..4b931ca 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -9,6 +9,8 @@ name: Release / build(可复用) # **M184 加了一步**:`linux-x64` 的 xvfb 运行时冒烟(真渲一帧 + `[theme]` / `[backend] final` 日志断言 + 像素判定)—— # 这一档进了 `PUBLISH_SLOTS`,「发出去的包真能跑」从此由 CI 每轮证明;判据只在 `linux-x64` 槽位上跑(arm64 没有 # 对应的运行时证据,不给它加一条没验证过的门禁)。同一轮把 arm64 的失败收尾注解按槽位分支。 +# **M270 给 `Swatinem/rust-cache` 加了显式 `key`(镜像 + 槽位)**:缓存会跨镜像串产物,机制、收益/剩余风险与 key 演算见 +# `packaging/docs/build-and-ci.md` §3「构建缓存」。 # 矩阵**仍由调用方** `release.yml` 的 `plan` job 生成并在外层展开(矩阵不能带 `if`,`strategy` 可读 `needs`), # 本文件只跑**单个槽位**,不持有槽位表 —— 改槽位只改 `release.yml` 里那一处。 # `env` 必须在被调方重复声明:**被调工作流不继承调用方的 workflow 级 `env`**(CARGO_TERM_COLOR / RUST_BACKTRACE @@ -70,7 +72,15 @@ jobs: libgl1-mesa-dev libegl1-mesa-dev libwayland-dev libxcb1-dev - uses: dtolnay/rust-toolchain@stable + # (M270)`key:` 必须带**镜像维度**,不是「加个 key 更保险」:v2 的 key = `v0-rust-` + 本行 key + job + + # `-Linux-X64`(os.type/os.arch)+ env 哈希 + 锁哈希 —— 里面**没有镜像标签/glibc**;本 job 与 `ci.yml` 的 + # `build` 同 job id、rustc 同版、同一份 `Cargo.lock` ⇒ 两边 key 逐字节相同,`ubuntu-latest`(24.04) 编的 + # `target/` 会被恢复到 `ubuntu-22.04`(glibc 2.35)。致命处:cargo 的 fingerprint(`.rlib` 的 `-C metadata`) + # **不含 glibc** ⇒ 恢复后判 fresh 不重编,24.04 编的 `aws-lc-sys` `bcm.o`(引 `__isoc23_sscanf`)在 2.35 上 + # 链接 ⇒ undefined。`key:` 落**前缀段**,主 key 与兜底 `restoreKey`(前缀去锁哈希)一起分开,演算见 docs §3。 - uses: Swatinem/rust-cache@v2 + with: + key: ${{ inputs.os }}-${{ inputs.slot }} # (M141/M142 → **M164 判定权移交**)windows-arm64 跑在**原生** `windows-11-arm` 上。本步**只取证,不判定**: # M141/M142 的版本用 `find` 猜 SDK 的 `um/arm64/kernel32.lib` 在不在,猜缺就 `exit 1` —— 结果 v0.2.1~v0.2.3 @@ -129,15 +139,12 @@ jobs: # 官方产物自带微软登录所需的配置,装完即可登录;运行期同名环境变量仍优先于它,可覆盖。 # 值来自仓库 Secret(不进 git 文件);Secret 没配时这里是空串,client_id.rs 把空白当「没配」, # 于是非官方构建不烘 id。想自己烘一份就从源码构建:`ULL_MSA_CLIENT_ID= cargo build`。 - # (M164)arm64 槽位在编译前把工具链**显式**备齐,不再依赖「runner 反正自带」这种默契: - # ① `rustup target add aarch64-pc-windows-msvc`:宿主机本来就是 aarch64-pc-windows-msvc,这句在原生 - # runner 上是幂等 no-op;它把「缺 std」这类失败挡在编译前(缺 target 时 rustc 只会说找不到 crate std); - # ② MSVC 的 `LIB` / `INCLUDE` 用微软自己的 `vcvarsall.bat arm64` 取(**不手搓路径**:少一个目录就是 - # LNK1181),本步 `export` 一次、同时写进 `$GITHUB_ENV` 供下游 dumpbin / 打包步; - # ③ arm64 的 `link.exe` 用 vswhere 定位后显式导出 `CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER` - # (Hostarm64 原生优先、Hostx64 次之)。**②必须先于③**:只覆盖链接器而不给 LIB,rustc 会在 - # `link.exe` 里撞 `LNK1181: cannot open input file 'kernel32.lib'`——那正是我们要定位的错误,不是要制造的错误。 - # ①②③ 全是「定位不到就打印原因继续」,不用启发式决定红绿:真缺工具链时让链接器自己报出来。 + # (M164)arm64 槽位在编译前把工具链**显式**备齐(不再靠「runner 反正自带」):① `rustup target add + # aarch64-pc-windows-msvc`(原生 runner 上幂等 no-op,把「缺 std」挡在编译前);② `LIB`/`INCLUDE` 用微软 + # 自己的 `vcvarsall.bat arm64` 取(**不手搓路径**,少一个目录就是 LNK1181)并写进 `$GITHUB_ENV`; + # ③ `link.exe` 由 vswhere 定位后导出 `CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER`(Hostarm64 优先)。 + # **②必须先于③**:只给链接器不给 LIB 撞的就是 `LNK1181: cannot open input file 'kernel32.lib'`。 + # 三道都是「定位不到就打印原因继续」——真缺工具链时让链接器自己报错。 - name: cargo build --release --locked shell: bash env: @@ -153,12 +160,10 @@ jobs: rustup target list --installed vswhere="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" if [ -x "$vswhere" ]; then - # ② MSVC 的 LIB/INCLUDE。**别把 vcvarsall 的路径直接塞给 `cmd //c "..."`**:MSYS 会把参数里的 - # `"` 改写成 `\"` 再交给 cmd.exe,而 cmd 的 /c 引号剥离接不住这一层,只会报 - # `'\"C:\...\vcvarsall.bat\"' 不是内部或外部命令` —— 结果 LIB/INCLUDE 全空、只剩 `set` 的空输出 - # (M164 复审实测)。故本步改为往 `$RUNNER_TEMP` 写一个 **CRLF** 的 wrapper .bat,引号留在 - # .bat 自己的 `call "..."` 里(由 cmd 解析,路径带空格也没事),`cmd //c` 只收**不含引号**的 - # 相对文件名(先 cd 进去),彻底绕开 MSYS 的引号改写。 + # ② MSVC 的 LIB/INCLUDE。**别把 vcvarsall 路径直接塞给 `cmd //c "..."`**:MSYS 会把 `"` 改写成 + # `\"`,cmd 的 /c 剥离接不住 ⇒ LIB/INCLUDE 全空、只剩 `set` 的空输出(M164 复审实测)。故往 + # `$RUNNER_TEMP` 写一个 **CRLF** 的 wrapper .bat(引号留在 .bat 的 `call "..."` 里),`cmd //c` + # 只收不含引号的文件名(先 cd 进去),彻底绕开 MSYS 的引号改写。 tmp=${RUNNER_TEMP:-${TEMP:-/tmp}} mkdir -p "$tmp" vcvars="" @@ -285,11 +290,9 @@ jobs: PY=$(command -v python3 || command -v python) "$PY" packaging/check_pe_subsystem.py --gui "${{ inputs.build_dir }}/unlucky-launcher-iced.exe" --cli "${{ inputs.build_dir }}/ull.exe" - # (M184)linux-x64 的**运行时门禁**:这一档已进 PUBLISH_SLOTS,所以「发出去的包真能开窗渲出一帧」得由 CI 每轮证明, - # 不能只靠一次性的本机验收(证据与实测输出见 `packaging/docs/linux-evidence.md`)。 - # **只在 linux-x64 槽位上跑**:判据是按 x64 软渲染实测标定的;`linux-arm64` 上这条链一次都没验过(没有 arm64 的 - # xvfb 证据),写成无条件的 `runner.os == 'Linux'` 就等于给一个没验证过的槽位加一条会红的判据——正是 M171 那个 - # 「豁免行要单独落」的同类事故。宁可这条只在有证据的槽位上真跑。 + # (M184)linux-x64 的**运行时门禁**:这一档已进 PUBLISH_SLOTS,「发出去的包真能开窗渲出一帧」得每轮由 CI 证明 + # (证据见 `packaging/docs/linux-evidence.md`)。**只在 linux-x64 跑**:判据按 x64 软渲染实测标定,`linux-arm64` + # 上一次都没验过,写成 `runner.os == 'Linux'` 就是给没验证过的槽位加会红的判据(M171 同类事故)。 - name: Linux x64 运行时冒烟(xvfb 真渲一帧 + 主题/后端日志断言) if: inputs.slot == 'linux-x64' shell: bash diff --git a/packaging/docs/build-and-ci.md b/packaging/docs/build-and-ci.md index b076911..12d8832 100644 --- a/packaging/docs/build-and-ci.md +++ b/packaging/docs/build-and-ci.md @@ -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」 + + 「-」 ← 本仓 M270 才第一次用(下文「修法」) + + 「-」 ← 是 job **id**,不是 job 的 `name:` + + 「--」 ← Linux-X64 / Windows_NT-X64 / Darwin-ARM64,**没有镜像标签** + + 「-」 ← rustc -vV(release + host + commit-hash)+ CARGO*/CC/CFLAGS/CXX/CMAKE/RUST* 的 sha1 +restoreKey = 到这里为止的一整串 ← 兜底恢复 key(**不含**锁哈希) +key += 「-」 ← 各 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 与兜底 `restoreKey` 同时带上镜像维度,**两条恢复路径一起断**。 + +**演算(这次的核心判据)**:把上面那段拼接写成脚本,对 12 个站点 × 2 个 `Cargo.lock` 状态逐对做前缀判定,改前 / 改后: + +| 对照 | 改前(7 处都没有 `key:`) | 改后 | +| --- | --- | --- | +| 路径①:主 key 逐字节相同 | **2 对**:`ci/build` == `rel/linux-x64` | 0 对 | +| 路径②:`restoreKey` 前缀命中**别的站点** | **4 组**(`v0-rust-build-Linux-X64-` 命中 `…-<另一个锁哈希>`) | 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:` 里不出现 `----` 这种形状的文本(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` 构建日志说话。