Merge branch 'feat/cachedim-rust-cache-key-includes-image-d'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s

This commit is contained in:
AskaEth committed 2026-11-12 10:36:30 +08:00
commit 5a182e80de
3 files changed
+216 -64

No files matched your search

+41 -28
View File
@@ -3,6 +3,10 @@ name: CI
# 主干门禁:fmt / clippy / test / build 双平台矩阵。 # 主干门禁:fmt / clippy / test / build 双平台矩阵。
# 架构守门(core 零 UI、文件 ≤400 行、依赖许可)见 guards.yml。 # 架构守门(core 零 UI、文件 ≤400 行、依赖许可)见 guards.yml。
# 平台基线 Win10+(标准工具链产物),平台矩阵见 AGENTS.md §7。 # 平台基线 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 退役树,源码原样保留)。 # M118(D26):根 Cargo.toml 已 `exclude = ["crates/app"]`(Slint 退役树,源码原样保留)。
# 所以本文件里所有 `--workspace` 都**不再**覆盖 `unlucky-app`:CI 不再编 slint 1.18 + usvg 0.48 # 所以本文件里所有 `--workspace` 都**不再**覆盖 `unlucky-app`:CI 不再编 slint 1.18 + usvg 0.48
@@ -103,6 +107,8 @@ jobs:
with: with:
components: clippy components: clippy
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.os }}
# `--workspace` 现在是 8 个 crate,不含 `unlucky-app`(M118 的 workspace exclude), # `--workspace` 现在是 8 个 crate,不含 `unlucky-app`(M118 的 workspace exclude),
# 所以这条命令不会拉起 slint / usvg / i-slint-*。 # 所以这条命令不会拉起 slint / usvg / i-slint-*。
- run: cargo clippy --workspace --all-targets -- -D warnings - run: cargo clippy --workspace --all-targets -- -D warnings
@@ -125,17 +131,17 @@ jobs:
libwayland-dev libxcb1-dev libwayland-dev libxcb1-dev
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.os }}
# ---- 下载页选项区的四条像素守门(M127):先出图,再把路径喂给 `--lib` ---- # ---- 下载页选项区的四条像素守门(M127):先出图,再把路径喂给 `--lib` ----
# 之前这四条靠开发者本机手动设 `ULL_DL_*_SHOT`(不设就 SKIP),而本文件里 grep 这些 # 之前这四条靠开发者本机手动设 `ULL_DL_*_SHOT`(不设就 SKIP),而本文件里 grep 这些变量零命中 ⇒
# 变量零命中 ⇒ **CI 里一次都没跑过**。空跑的守门长期等于没有,所以这两步是它们的第一处 # **CI 里一次都没跑过**。空跑的守门长期等于没有,所以这两步是它们的第一处常驻调用点。(另有一道保险:
# 常驻调用点。(另有一道保险:变量缺失且 `CI` 已设置时,守门自己会失败而不是跳过。 # 变量缺失且 `CI` 已设置时,守门自己会失败而不是跳过 ⇒ 将来谁重构掉下面出图那步,构建会红。)
# 于是将来谁把下面出图那步重构掉,构建会红。)
# #
# **为什么只在 Windows**:断言里有几条直接量墨迹(行高 44、版本号墨迹条数、图标药丸 # **为什么只在 Windows**:断言里有几条直接量墨迹(行高 44、版本号墨迹条数、图标药丸 40×40),这些数字
# 40×40),这些数字按本仓字体栈(Segoe UI / Consolas)标定;Linux/macOS runner 上 # 按本仓字体栈(Segoe UI / Consolas)标定;Linux/macOS runner 上这套字体不存在,回退字体的字高与行距
# 这套字体不存在,回退字体的字高与行距一变就是假红。同一条限制也写在 # 一变就是假红。同一条限制也写在 `crates/app-iced/src/pages/download/tests/options_shot/feed.rs` 的
# `crates/app-iced/src/pages/download/tests/options_shot/feed.rs` 的模块文档里 # 模块文档里(非 Windows 喂了图会当场失败),不靠「碰巧只配了一处」。
# (非 Windows 喂了图会当场失败),不靠「碰巧只配了一处」。
- name: 出四张像素守门用的图并写进 $GITHUB_ENV(仅 Windows) - name: 出四张像素守门用的图并写进 $GITHUB_ENV(仅 Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: bash shell: bash
@@ -177,22 +183,20 @@ jobs:
name: dl-shots name: dl-shots
path: ${{ runner.temp }}/dl-shots/ path: ${{ runner.temp }}/dl-shots/
if-no-files-found: warn if-no-files-found: warn
# 同 clippy:`--workspace` 不含 `unlucky-app`(M118),Slint 退役树不再被 `--all-targets` 拉起。 # 同 clippy:`--workspace` 不含 `unlucky-app`(M118)。**别再写 `--exclude unlucky-app`**:它已不是成员,
# 也因此**不要**再写 `--exclude unlucky-app`:它已不是成员,那面旗子只会打一条 # 只会打一条 `excluded package(s) … not found in workspace` 的警告然后空转(实测 exit 0,不报错)。
# `excluded package(s) … not found in workspace` 的警告然后空转(实测 exit 0,不报错)。
# #
# ---- 取证(M147):下面是同一件事的两个平台版本,不是两条不同的测试 ---- # ---- 取证(M147):下面是同一件事的两个平台版本,不是两条不同的测试 ----
# M144 的三条红里两条已在 docker 里定位到行,第三条 `test (windows)` 在 run 里只留下 # M144 的第三条红 `test (windows)` 只留下 `Process completed with exit code 1`,而 cargo 自己的失败码
# `Process completed with exit code 1`——而 cargo 自己的失败码是 **101**,所以那个 1 的 # 是 **101** ⇒ 那个 1 的来源不明(包装层洗码 / 进程被杀,两说都未排除);job log 要登录才能读
# 来源不明(包装层洗码 / 进程被杀,两说都未排除)。job log 要登录才能读(匿名 403), # (匿名 403),但**注解匿名可读**(`GET /check-runs/<id>/annotations` → 200,M144 实证)。三件事
# 但**注解匿名可读**(`GET /check-runs/<id>/annotations` → 200,M144 实证)。所以这三件 # 各对准一种「看不见」:
# 事各自对准一种「看不见」: # 1) `--no-fail-fast`:cargo 默认在第一个失败的 target 就停(M144 实测 Linux 只跑 7 个 test
# 1) `--no-fail-fast`:cargo 默认在第一个失败的 target 就停(M144 实测 Linux 只跑了 # binary 就退出),后面还有没有第二条失败完全看不到;
# 7 个 test binary 就退出),后面还有没有第二条失败完全看不到; # 2) `error` / `failures:` / panicked 行升成 `::error::` 注解:拿不到 job log 时,注解是匿名侧
# 2) 把 `error` / `failures:` / panicked 行升成 `::error::` 注解:拿不到 job log 时, # 唯一能取到的失败正文;
# 注解是匿名侧唯一能取到的失败正文; # 3) 显式打印 cargo 的退出码:把「包装层把码洗成 1」「cargo 真失败(101)」「进程被杀只给码、零诊断」
# 3) 显式打印 cargo 的退出码:把「包装层把码洗成 1」「cargo 真失败(101)」「进程被杀 # 分开——这正是 M144 没定的那一半。
# 只给码、零诊断」三种情形分开——这正是 M144 没定的那一半。
- name: cargo test --workspace --all-targets(ubuntu,失败升注解) - name: cargo test --workspace --all-targets(ubuntu,失败升注解)
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: | run: |
@@ -215,8 +219,8 @@ jobs:
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: pwsh shell: pwsh
env: env:
# M171 跨卷守卫:runner 到底有没有第二块可写卷没有核实手段,所以不赌 —— 显式声明豁免。 # M171 跨卷守卫:runner 有没有第二块可写卷没核实手段,所以不赌 ⇒ 显式豁免(惰性:真探到就不参与
# 豁免是惰性的:真探到第二块卷时它不参与判断(见 install_xdev_cases 的真值表用例)。 # 判断,见 install_xdev_cases 的真值表用例)。
ULL_XDEV_TEST_WAIVE: "1" ULL_XDEV_TEST_WAIVE: "1"
run: | run: |
# pwsh 没有 PIPESTATUS(那是 bash 语法):拿 `$LASTEXITCODE` —— Tee-Object 是 cmdlet, # pwsh 没有 PIPESTATUS(那是 bash 语法):拿 `$LASTEXITCODE` —— Tee-Object 是 cmdlet,
@@ -252,6 +256,10 @@ jobs:
libwayland-dev libxcb1-dev libwayland-dev libxcb1-dev
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - 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 档 # 同上:`--workspace` 不含 `unlucky-app`(M118)。被摘掉的正是 release 档
# (lto=fat + codegen-units=1)最贵的那棵 slint + usvg 树。 # (lto=fat + codegen-units=1)最贵的那棵 slint + usvg 树。
- run: cargo build --workspace --release - run: cargo build --workspace --release
@@ -294,9 +302,10 @@ jobs:
fonts-dejavu-core fonts-liberation2 fonts-dejavu-core fonts-liberation2
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
# 用 debug 而不是 release:本 job 验的是「运行时渲染通路能通」,不是优化后二进制的行为; with:
# release 的 lto="fat" + codegen-units=1 会把这一步拖到十几分钟却不带来额外证据。 key: ubuntu-latest
# M118:bin 从 Slint 版 `unlucky-launcher` 换成 iced 版(见上方 job 注释)。 # 用 debug 而不是 release:「运行时渲染通路能通」与优化无关,release 的 lto="fat" + codegen-units=1
# 只会把这一步拖到十几分钟。M118:bin 从 Slint 版 `unlucky-launcher` 换成 iced 版(见上方 job 注释)。
- run: cargo build --bin unlucky-launcher-iced --locked - run: cargo build --bin unlucky-launcher-iced --locked
- name: 在 Xvfb 里跑一帧并截图(退出码非 0 即失败) - name: 在 Xvfb 里跑一帧并截图(退出码非 0 即失败)
run: | run: |
@@ -335,6 +344,8 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
key: macos-latest
- run: cargo build --bin unlucky-launcher-iced --locked - run: cargo build --bin unlucky-launcher-iced --locked
- name: 尝试截图(macOS 没有 X 服务器,直接跑;失败即为结论) - name: 尝试截图(macOS 没有 X 服务器,直接跑;失败即为结论)
run: | run: |
@@ -378,6 +389,8 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
key: windows-latest
- name: update-e2e - name: update-e2e
run: > run: >
cargo test --release -p unlucky-cli --features update-test-source cargo test --release -p unlucky-cli --features update-test-source
+38 -35
View File
@@ -1,14 +1,13 @@
name: Release / build(可复用) name: Release / build(可复用)
# Release 流水线的**构建侧**(M145 拆分:`release.yml` 与 `packaging/README.md` 都顶到 400 行红线,下一处改动必越线)。 # Release 流水线的**构建侧**(M145 拆分:`release.yml` 与 `packaging/README.md` 都顶到 400 行红线,下一处改动必越线)。
# M145 时本文件 = 拆分前 `.github/workflows/release.yml` 的 `build` job **逐字搬家**,改动只有四个矩阵键换成同名入参: # M145 时本文件 = 拆分前 `.github/workflows/release.yml` 的 `build` job **逐字搬家**,改动只有四个矩阵键换成同名入参
# `matrix.slot` → `inputs.slot`、`matrix.os` → `inputs.os`、`matrix.build_dir` → `inputs.build_dir`、 # (`matrix.{slot,os,build_dir,rustflags}` → `inputs.{slot,os,build_dir,rustflags}`)。
# `matrix.rustflags` → `inputs.rustflags`。
# **M164 改了两步**(只这两步):① `Windows ARM64 工具链体检` → `工具链取证`(不再 exit 1,判定权交回编译器; # **M164 改了两步**(只这两步):① `Windows ARM64 工具链体检` → `工具链取证`(不再 exit 1,判定权交回编译器;
# 理由见该步上方的长注释);② `cargo build` 步给 arm64 槽位显式备齐 target / `LIB`+`INCLUDE` / arm64 `link.exe`。 # 理由见该步上方的长注释);② `cargo build` 步给 arm64 槽位显式备齐 target / `LIB`+`INCLUDE` / arm64 `link.exe`。
# **M184 加了一步**:`linux-x64` 的 xvfb 运行时冒烟(真渲一帧 + `[theme]` / `[backend] final` 日志断言 + 像素判定)—— # **M184 加了一步**:`linux-x64` 的 xvfb 运行时冒烟(真渲一帧 + `[theme]` / `[backend] final` 日志断言 + 像素判定)——
# 这一档进了 `PUBLISH_SLOTS`,「发出去的包真能跑」从此由 CI 每轮证明;判据只在 `linux-x64` 槽位上跑(arm64 没有 # 进了 `PUBLISH_SLOTS` 就得由 CI 每轮证明「发出去的包真能跑」;只在 `linux-x64` 跑(arm64 没有运行时证据,不造没验证过的门禁)。
# 对应的运行时证据,不给它加一条没验证过的门禁)。同一轮把 arm64 的失败收尾注解按槽位分支。 # **M270 给 `Swatinem/rust-cache` 加了 `key`(= 镜像标签)与 `save-if`(tag run 不写)**:机制与取舍见 `packaging/docs/build-and-ci.md` §3「构建缓存」。
# 矩阵**仍由调用方** `release.yml` 的 `plan` job 生成并在外层展开(矩阵不能带 `if`,`strategy` 可读 `needs`), # 矩阵**仍由调用方** `release.yml` 的 `plan` job 生成并在外层展开(矩阵不能带 `if`,`strategy` 可读 `needs`),
# 本文件只跑**单个槽位**,不持有槽位表 —— 改槽位只改 `release.yml` 里那一处。 # 本文件只跑**单个槽位**,不持有槽位表 —— 改槽位只改 `release.yml` 里那一处。
# `env` 必须在被调方重复声明:**被调工作流不继承调用方的 workflow 级 `env`**(CARGO_TERM_COLOR / RUST_BACKTRACE # `env` 必须在被调方重复声明:**被调工作流不继承调用方的 workflow 级 `env`**(CARGO_TERM_COLOR / RUST_BACKTRACE
@@ -70,7 +69,20 @@ jobs:
libgl1-mesa-dev libegl1-mesa-dev libwayland-dev libxcb1-dev libgl1-mesa-dev libegl1-mesa-dev libwayland-dev libxcb1-dev
- uses: dtolnay/rust-toolchain@stable - 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` ⇒ 曾逐字节相同,`ubuntu-latest`(24.04) 编的 `target/`
# 被恢复到 `ubuntu-22.04`(2.35)。致命处:cargo 的 fingerprint(`.rlib` 的 `-C metadata`)**不含 glibc** ⇒
# 判 fresh 不重编,24.04 编的 `aws-lc-sys` `bcm.o`(引 `__isoc23_sscanf`)在 2.35 上链接 ⇒ undefined。
# 故 key 只取 `inputs.os`:linux-x64(22.04) 与 main 的 `ubuntu-latest` 从此不同 key(要断的就是这条),而
# windows-x64(windows-latest) 与 main 的 `ci/build` **故意同 key** —— 同镜像,借热缓存安全(M270 前 Windows
# 一直走这条且产物是绿的);slot 是冗余维度,每个槽位的 `inputs.os` 本来就互不相同。
# `save-if`:tag run 只能读 main / 同名 tag 的缓存 ⇒ 本侧写出的 target 缓存永远轮不到复用,只贡献冷编 +
# 死缓存 + 挤掉 main 的热缓存。故 tag 不写;留 `ref_type != 'tag'` 这个门是给 main 上 dispatch 预热用的。
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
key: ${{ inputs.os }}
save-if: ${{ github.ref_type != 'tag' }}
# (M141/M142 → **M164 判定权移交**)windows-arm64 跑在**原生** `windows-11-arm` 上。本步**只取证,不判定**: # (M141/M142 → **M164 判定权移交**)windows-arm64 跑在**原生** `windows-11-arm` 上。本步**只取证,不判定**:
# M141/M142 的版本用 `find` 猜 SDK 的 `um/arm64/kernel32.lib` 在不在,猜缺就 `exit 1` —— 结果 v0.2.1~v0.2.3 # M141/M142 的版本用 `find` 猜 SDK 的 `um/arm64/kernel32.lib` 在不在,猜缺就 `exit 1` —— 结果 v0.2.1~v0.2.3
@@ -129,15 +141,12 @@ jobs:
# 官方产物自带微软登录所需的配置,装完即可登录;运行期同名环境变量仍优先于它,可覆盖。 # 官方产物自带微软登录所需的配置,装完即可登录;运行期同名环境变量仍优先于它,可覆盖。
# 值来自仓库 Secret(不进 git 文件);Secret 没配时这里是空串,client_id.rs 把空白当「没配」, # 值来自仓库 Secret(不进 git 文件);Secret 没配时这里是空串,client_id.rs 把空白当「没配」,
# 于是非官方构建不烘 id。想自己烘一份就从源码构建:`ULL_MSA_CLIENT_ID=<id> cargo build`。 # 于是非官方构建不烘 id。想自己烘一份就从源码构建:`ULL_MSA_CLIENT_ID=<id> cargo build`。
# (M164)arm64 槽位在编译前把工具链**显式**备齐,不再依赖「runner 反正自带」这种默契: # (M164)arm64 槽位在编译前把工具链**显式**备齐(不再靠「runner 反正自带」):① `rustup target add
# ① `rustup target add aarch64-pc-windows-msvc`:宿主机本来就是 aarch64-pc-windows-msvc,这句在原生 # aarch64-pc-windows-msvc`(原生 runner 上幂等 no-op,把「缺 std」挡在编译前);② `LIB`/`INCLUDE` 用微软
# runner 上是幂等 no-op;它把「缺 std」这类失败挡在编译前(缺 target 时 rustc 只会说找不到 crate std); # 自己的 `vcvarsall.bat arm64` 取(**不手搓路径**,少一个目录就是 LNK1181)并写进 `$GITHUB_ENV`;
# ② MSVC 的 `LIB` / `INCLUDE` 用微软自己的 `vcvarsall.bat arm64` 取(**不手搓路径**:少一个目录就是 # ③ `link.exe` 由 vswhere 定位后导出 `CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER`(Hostarm64 优先)。
# LNK1181),本步 `export` 一次、同时写进 `$GITHUB_ENV` 供下游 dumpbin / 打包步; # **②必须先于③**:只给链接器不给 LIB 撞的就是 `LNK1181: cannot open input file 'kernel32.lib'`。
# ③ arm64 的 `link.exe` 用 vswhere 定位后显式导出 `CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER` # 三道都是「定位不到就打印原因继续」——真缺工具链时让链接器自己报错。
# (Hostarm64 原生优先、Hostx64 次之)。**②必须先于③**:只覆盖链接器而不给 LIB,rustc 会在
# `link.exe` 里撞 `LNK1181: cannot open input file 'kernel32.lib'`——那正是我们要定位的错误,不是要制造的错误。
# ①②③ 全是「定位不到就打印原因继续」,不用启发式决定红绿:真缺工具链时让链接器自己报出来。
- name: cargo build --release --locked - name: cargo build --release --locked
shell: bash shell: bash
env: env:
@@ -153,12 +162,10 @@ jobs:
rustup target list --installed rustup target list --installed
vswhere="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" vswhere="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
if [ -x "$vswhere" ]; then if [ -x "$vswhere" ]; then
# ② MSVC 的 LIB/INCLUDE。**别把 vcvarsall 的路径直接塞给 `cmd //c "..."`**:MSYS 会把参数里的 # ② MSVC 的 LIB/INCLUDE。**别把 vcvarsall 路径直接塞给 `cmd //c "..."`**:MSYS 会把 `"` 改写成
# `"` 改写成 `\"` 再交给 cmd.exe,而 cmd 的 /c 引号剥离接不住这一层,只会报 # `\"`,cmd 的 /c 剥离接不住 ⇒ LIB/INCLUDE 全空、只剩 `set` 的空输出(M164 复审实测)。故往
# `'\"C:\...\vcvarsall.bat\"' 不是内部或外部命令` —— 结果 LIB/INCLUDE 全空、只剩 `set` 的空输出 # `$RUNNER_TEMP` 写一个 **CRLF** 的 wrapper .bat(引号留在 .bat 的 `call "..."` 里),`cmd //c`
# (M164 复审实测)。故本步改为往 `$RUNNER_TEMP` 写一个 **CRLF** 的 wrapper .bat,引号留在 # 只收不含引号的文件名(先 cd 进去),彻底绕开 MSYS 的引号改写。
# .bat 自己的 `call "..."` 里(由 cmd 解析,路径带空格也没事),`cmd //c` 只收**不含引号**的
# 相对文件名(先 cd 进去),彻底绕开 MSYS 的引号改写。
tmp=${RUNNER_TEMP:-${TEMP:-/tmp}} tmp=${RUNNER_TEMP:-${TEMP:-/tmp}}
mkdir -p "$tmp" mkdir -p "$tmp"
vcvars="" vcvars=""
@@ -212,13 +219,12 @@ jobs:
ls -l "$BUILD_DIR"/unlucky-launcher-iced* "$BUILD_DIR"/ull* || true ls -l "$BUILD_DIR"/unlucky-launcher-iced* "$BUILD_DIR"/ull* || true
fi fi
# (M190)版本资源门禁:**读产物字节**,不信构建日志。`build.rs` 里写了 `CompanyName` 不等于 # (M190)版本资源门禁:**读产物字节**,不信构建日志。`build.rs` 里写了 `CompanyName` 不等于它进了 PE——
# 它进了 PE——资源在链接期被丢掉、或被后处理覆盖,都只有读回来才看得见。判据(字段值、四段式 # 资源在链接期被丢掉、或被后处理覆盖,都只有读回来才看得见。判据(字段值、四段式版本号、FIXEDFILEINFO
# 版本号、FIXEDFILEINFO 的 dwFileVersion)全在 `packaging/check_pe_version.py`,本机可跑。 # 的 dwFileVersion)全在 `packaging/check_pe_version.py`,本机可跑。只在 Windows 槽位跑:Linux/macOS 产物
# 只在 Windows 槽位跑:Linux/macOS 产物没有 PE 头,那边 `build.rs` 整段不执行(见 # 没有 PE 头,那边 `build.rs` 整段不执行(见 `packaging/pe_version.rs` 文件头)。**工具缺失也 fail**。
# `packaging/pe_version.rs` 文件头)。**工具缺失也 fail**:找不到 python 就等于门禁失效。 # 步骤体本身在 `packaging/ci-pe-version-gate.sh`:读回 + 同源取版本号 + 失败逐条转 `::error::` 注解
# 步骤体本身在 `packaging/ci-pe-version-gate.sh`:读回 + 同源取版本号 + 失败逐条转 `::error::` # (M264 从本文件拆出,给 400 行硬门禁腾地方;判据没动,仍由检查器 exit 1)。
# 注解(M264 从本文件拆出,给 400 行硬门禁腾地方;判据没动,仍由检查器 exit 1)。
- name: PE 版本资源读回(CompanyName / ProductName / 版权 / 版本号真的在产物里) - name: PE 版本资源读回(CompanyName / ProductName / 版权 / 版本号真的在产物里)
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: bash shell: bash
@@ -285,11 +291,9 @@ jobs:
PY=$(command -v python3 || command -v python) 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" "$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 每轮证明, # (M184)linux-x64 的**运行时门禁**:这一档已进 PUBLISH_SLOTS,「发出去的包真能开窗渲出一帧」得每轮由 CI 证明
# 不能只靠一次性的本机验收(证据与实测输出见 `packaging/docs/linux-evidence.md`)。 # (证据见 `packaging/docs/linux-evidence.md`)。**只在 linux-x64 跑**:判据按 x64 软渲染实测标定,`linux-arm64`
# **只在 linux-x64 槽位上跑**:判据是按 x64 软渲染实测标定的;`linux-arm64` 上这条链一次都没验过(没有 arm64 的 # 上一次都没验过,写成 `runner.os == 'Linux'` 就是给没验证过的槽位加会红的判据(M171 同类事故)。
# xvfb 证据),写成无条件的 `runner.os == 'Linux'` 就等于给一个没验证过的槽位加一条会红的判据——正是 M171 那个
# 「豁免行要单独落」的同类事故。宁可这条只在有证据的槽位上真跑。
- name: Linux x64 运行时冒烟(xvfb 真渲一帧 + 主题/后端日志断言) - name: Linux x64 运行时冒烟(xvfb 真渲一帧 + 主题/后端日志断言)
if: inputs.slot == 'linux-x64' if: inputs.slot == 'linux-x64'
shell: bash shell: bash
@@ -376,9 +380,8 @@ jobs:
retention-days: 14 retention-days: 14
# (M142/M164)arm64 若红在别的步骤(cargo / 链接 / 打包 / 导入表门禁),也要有一条注解说清「红在哪」和 # (M142/M164)arm64 若红在别的步骤(cargo / 链接 / 打包 / 导入表门禁),也要有一条注解说清「红在哪」和
# 「不挡发布」——只剩一句 `Process completed with exit code 1.` 的教训见上面那条体检步骤。 # 「不挡发布」——只剩一句 `Process completed with exit code 1.` 的教训见上面那条体检步骤。M164 起本槽位的红
# M164 起本槽位的红**只可能来自真实构建链路**(体检已降级为取证、永不 exit 1),所以这条注解同时也是 # **只可能来自真实构建链路**(体检永不 exit 1):红了就把上面的编译器/链接器原文当根因,别再回去改体检。
# 「arm64 到底能不能编出来」的答案所在:红了就把上面的编译器/链接器原文当根因,别再回去改体检。
# M184:Linux arm64 进来(它现在是 `mode: ci`,tag 推送默认就编),措辞按槽位分支 —— 两个 arm64 都**不在**发布名单里, # M184:Linux arm64 进来(它现在是 `mode: ci`,tag 推送默认就编),措辞按槽位分支 —— 两个 arm64 都**不在**发布名单里,
# 但理由不同:windows-arm64 等「能不能出 PE」的答案,linux-arm64 等「真机跑不跑得起来」的答案。 # 但理由不同:windows-arm64 等「能不能出 PE」的答案,linux-arm64 等「真机跑不跑得起来」的答案。
- name: arm64 失败收尾注解(红不挡发布) - name: arm64 失败收尾注解(红不挡发布)
+137 -1
View File
@@ -63,7 +63,8 @@ python packaging/make_artifacts.py --slot windows-arm64 \
`CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER`(Hostarm64 原生优先、Hostx64 次之;`-find` 的斜杠/反斜杠两种形态都试)。 `CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER`(Hostarm64 原生优先、Hostx64 次之;`-find` 的斜杠/反斜杠两种形态都试)。
顺序不能反:只覆盖链接器而不给 `LIB`,撞上的就是 `LNK1181: cannot open input file 'kernel32.lib'`。 顺序不能反:只覆盖链接器而不给 `LIB`,撞上的就是 `LNK1181: cannot open input file 'kernel32.lib'`。
**`cmd //c` 不能直接收「带引号的 vcvarsall 路径」**:MSYS 会把 `"` 改写成 `\"`,cmd 的 `/c` 剥离规则接不住, **`cmd //c` 不能直接收「带引号的 vcvarsall 路径」**:MSYS 会把 `"` 改写成 `\"`,cmd 的 `/c` 剥离规则接不住,
结果 `LIB`/`INCLUDE` 全空(M164 复审实测;`cmd //c` 只调**不含引号**的路径是好的)。故本步往 `$RUNNER_TEMP` 结果 `LIB`/`INCLUDE` 全空(M164 复审实测;`cmd //c` 只调**不含引号**的路径是好的)。当时的报错原文是
`'\"C:\...\vcvarsall.bat\"' 不是内部或外部命令`(将来搜到这句、想知道来龙去脉,就看这一段)。故本步往 `$RUNNER_TEMP`
写一个 CRLF 的 wrapper `.bat`(`call "<vcvarsall>" arm64` + `set`),`cmd //c` 只收相对文件名——引号留在 写一个 CRLF 的 wrapper `.bat`(`call "<vcvarsall>" arm64` + `set`),`cmd //c` 只收相对文件名——引号留在
`.bat` 内部由 cmd 自己解析,路径带空格也不受影响。 `.bat` 内部由 cmd 自己解析,路径带空格也不受影响。
- **编译前体检已降级为「只取证」**(M164):M141/M142 的版本用 `find` 猜 `um/arm64/kernel32.lib` 在不在、猜缺就 - **编译前体检已降级为「只取证」**(M164):M141/M142 的版本用 `find` 猜 `um/arm64/kernel32.lib` 在不在、猜缺就
@@ -137,3 +138,138 @@ python packaging/make_artifacts.py --slot windows-arm64 \
Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两者都属"标准工具链"(D21), Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两者都属"标准工具链"(D21),
但发布产物要**不依赖 VC++ 可再发行包**,`+crt-static` 把 CRT 静态链进 exe,Win10+ 的 UCRT 本身是 但发布产物要**不依赖 VC++ 可再发行包**,`+crt-static` 把 CRT 静态链进 exe,Win10+ 的 UCRT 本身是
系统组件,于是 exe 真正单文件可拷走。GNU 交叉产物在 CI 上还要拉 MinGW 链接器,收益为零。 系统组件,于是 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 **逐字节相同**。这串表达式里**没有任何一项**能反映出镜像。
`RUSTFLAGS` 也救不了:它只写在 `cargo build` 那一步的 `env:` 里,cache 步根本看不到它。
**为什么 cargo 自己不发现**:freshness 判据是 fingerprint,记的是编译器版本、features、profile、依赖 metadata ——
而 `-C metadata` 里**没有 glibc 版本**。于是恢复来的 `.rlib` 被判 fresh、不重编,问题一路拖到最后的链接。
**修法**(两个文件里 7 处 `Swatinem/rust-cache@v2`;全仓共 8 处,见本节末尾):
- **发布侧**:`key: ${{ inputs.os }}`(只用镜像标签,**不带 slot**)+ `save-if: ${{ github.ref_type != 'tag' }}`
(tag run 只读不写)。两条各有单独小节。
- **CI 侧**:三个矩阵 job 用 `key: ${{ matrix.os }}`(自动跟 `runs-on`);三个单 OS job 用它自己的镜像标签字面量。
后者的 `key:` 与 `runs-on:` 是**两份手写文本**:改 `runs-on` 时必须一起改 `key:`,否则镜像维度会静默失准(不报错,
只是继续跨镜像共用)。
- `key` 输入落在**前缀段**(`v0-rust-<key>-<job>-…`,在 `restoreKey` 定格之前),所以主 key 与兜底 `restoreKey`
同时带上镜像维度,**两条恢复路径一起分开**。
#### 发布侧 key 为什么只取 `inputs.os`(不带 slot)
两个方向同时成立,去掉 slot 才对:
- **必须断的**:`linux-x64` @ `ubuntu-22.04` vs `ci/build` @ `ubuntu-latest` ⇒ 不同 key。这就是 M269 那条路径。
- **故意保留的**:`windows-x64` @ `windows-latest` vs `ci/build` @ `windows-latest` ⇒ **同 key**。同镜像、同 rustc、
同一份锁 ⇒ 借 main 上的热缓存是安全的:M270 之前 Windows 走的正是这条路,而 `windows-x64` 一直在 `PUBLISH_SLOTS`
里、产物是绿的。slot 本来就是冗余维度(每个槽位的 `inputs.os` 互不相同)。
- 代价与取舍:加了 slot 的那一版会把上面这一对也分开,两者的实质差异只是 `RUSTFLAGS=-C target-feature=+crt-static`。
合并回同一把 key 后 cargo 按 fingerprint 区分(`RUSTFLAGS` 是否被计入本仓没有实测;但这是 M270 前线上一直成立的
状态),最坏情况是多编一遍,不会错编。
#### 发布侧为什么「只读不写」(tag run 的缓存作用域)
- 官方文档(`docs.github.com` → dependency-caching → "Restrictions for accessing a cache"):一次 run 只能恢复
**当前分支**或**默认分支(main)**创建的缓存;不同 tag 名之间互不可读(`release-a` 写的缓存,`release-b` 读不到)。
- 推论一:发布侧的 key 是 `ubuntu-22.04` / `ubuntu-22.04-arm` / `windows-11-arm` / `macos-*`,而 **main 上一个 job 都
不跑这些标签**(`ci.yml` 只有 `ubuntu-latest` / `windows-latest` / `macos-latest`)⇒ 这四个槽位**每次发布都是冷编**,
连 registry / git 源码也拿不到(rust-cache 是「一把 key 一个 entry」,所有路径捆在一起,没有分路径缓存)。
唯一还能借到热缓存的是 `windows-x64`(见上一小节)。
- 推论二:发布侧写出去的缓存只能被**同名 tag 的下一次 run** 读到,实际就是一次性死缓存;它还占 per-repo 的 10 GB
配额(超出按最后访问时间 LRU 淘汰、7 天未访问过期),会把 main 上真正在用的 CI 热缓存挤掉。**所以 tag run 不写。**
- 为什么不是写死 `save-if: false`:把「写」留给唯一有效的场景 —— **在 main 上预热**。`gh workflow run release.yml`
(在 main 的末梢上跑)那次 run 的缓存作用域是 **main**,之后从 main 末梢切出来的 tag run 才读得到它(锁哈希与
rustc 版本一致才命中)。真要彻底禁写就把这一行改成 `false`,那时预热也要临时改文件才做得到。
#### 演算(核心判据;r1 换了 key,r0 那张表作废)
把上面的拼接写成脚本,对 15 个站点(ci 9:clippy/test/build 各 2 个 OS + 3 个单 OS job;发布 6)× 2 个
`Cargo.lock` 状态两两判定:
| 关系 | 改前(7 处都没有 `key:`) | 改后(`key: inputs.os` + `save-if`) |
| --- | --- | --- |
| ① `rel/windows-x64` ↔ `ci/build`(**同镜像**) | 主 key 相同 | 主 key **仍相同**(故意保留 / 安全) |
| ① `rel/linux-x64` ↔ `ci/build`(**跨镜像**) | 主 key 相同 ⇒ **M269 的 bug** | **不相同**(必须断) |
| ② **跨镜像** `restoreKey` 前缀命中 | **2 组**(`ci/build@ubuntu-latest` ↔ `rel/linux-x64` 两个方向) | **0 组** |
拼出来(env / 锁哈希用占位值,取值不影响结论):
```
改后 ci/build@ubuntu-latest cacheKey = v0-rust-ubuntu-latest-build-Linux-x64-3f9a1c07-b2d6e815
改后 rel/linux-x64@22.04 cacheKey = v0-rust-ubuntu-22.04-build-Linux-x64-3f9a1c07-b2d6e815
↑ 前缀 v0-rust-ubuntu- 之后立刻分叉(`l` vs `2`);两边 restoreKey 都更长 ⇒ 前缀命中不可能
改后 ci/build@windows-latest cacheKey = v0-rust-windows-latest-build-Windows_NT-x64-3f9a1c07-b2d6e815
改后 rel/windows-x64 cacheKey = v0-rust-windows-latest-build-Windows_NT-x64-3f9a1c07-b2d6e815
↑ 逐字节相同(同镜像,这是要保留的复用);锁哈希不同时退化成路径②,仍是同镜像,安全
```
脚本按「**跨镜像**」这一条断言(15 个站点两两 + 两个锁状态):改后任意两个 **runner 标签不同**的站点之间,主 key
相同 0 对、`restoreKey` 前缀命中 0 组。仍存在的相同/前缀关系全部落在**同一个镜像标签内部** —— 也就是
`rel/windows-x64` ↔ `ci/build` 那一对(同 `windows-latest`,正是要保留的复用)以及同站点换锁哈希那种(本来就该
复用依赖)。判据的输出是一次性的脚本,跑完即删;上面这几行就是它的结论。
#### `cache-targets` 到底关不关:**不关**
- **CI 侧**:关掉就只缓存 registry / git 源码,每个 job 都要从头编整棵依赖树(M269 干净容器实测同一条命令 18m43s
量级),日更矩阵每次提交都付这笔钱,收益为 0 —— 加镜像维度之后 `target/` 的恢复永远来自**同一镜像标签**,
M269 那条路径已经断了。
- **发布侧**:关掉确实能连 target 缓存一起省,但它**把 Windows 借 main 热缓存这条路一起掐掉**(target 不进缓存就
没得借),而 `windows-x64` 是发布侧唯一还能借到的一档。发布侧的死缓存问题由 `save-if` 解决,比关 targets 精准:
**该写的一次都不落,该借的一次都不少。**
- 被同一把 key 一起管住的还有 `cache-bin`:`~/.cargo/bin` 里是 `dtolnay/rust-toolchain` 从 `static.rust-lang.org`
装下来的**官方预编译** rustup 代理(不是在这台 runner 上编的),与 `target/` 同域、同一个 key,不引入额外风险。
#### 抓得住什么 / 抓不住什么
- 抓得住**跨镜像复用**:不同 runner 标签之间(`ubuntu-22.04` ↔ `ubuntu-latest`)不再共享 `target/`,两条恢复路径
都断,判据就是上面那张表。
- **抓不住「同一个镜像标签、但底层镜像悄悄升级」**:`ubuntu-latest` 从 24.04 升到 26.04、或 22.04 打一次 glibc 补丁时,
标签不变 ⇒ key 不变 ⇒ 旧缓存照样能恢复。这一档**没有自动化机制**,靠手动 bump:改 `prefix-key`(`v0-rust` →
`v1-rust`)或改对应 `key:` 的字面量,一次性作废旧缓存。**别把这修法当成「以后不会再有这类问题」。**
- 顺带:`rust-toolchain.toml` 是 `stable`,rustc 一升版 env hash 就变 ⇒ **编译器升级自动冷启动**,这一档本来就不在
风险里。
#### 代价与一次性影响
- CI 侧:缓存条数从「每 job 一条」变成「每 job × 每镜像一条」;三个单 OS job 各多一次冷编。
- 发布侧:只有 `windows-x64` 还能借 main 的热缓存;`linux-x64` / `linux-arm64` / `windows-arm64` / `macos-*`
**每次发布都冷编** —— 这是断开 M269 那条路径的必然代价(22.04 在 main 侧没有任何产出者)。想省就按上面
「在 main 上预热」那条做。
- 合入后所有旧 key 的缓存一次性失配,靠 LRU 自然淘汰;第一次 CI run 是冷缓存,别把它当成回归。
- 全仓第 8 处 `rust-cache` 在 `.github/workflows/dev-guards.yml:128`(job id `clippy`,只跑 `ubuntu-latest`),
**本次刻意不动**:它唯一的 runner 就是 `ubuntu-latest`,job id 也不与发布侧 `build` 重名,今天不可能跨镜像相撞。
副作用是它的 key 与 `ci/clippy` 从此不再逐字节相同(改前是相同的)⇒ 合入后 dev-guards 那次 clippy 会冷编一次,
一次性、无害。
**没验的部分**:真实 Actions 上的缓存命中 / 未命中本地验不了 —— 本机只能证明 key 字符串(上面的演算)、YAML 仍被真
解析器接受(`python packaging/check_workflows_yaml.py`)、以及结构正确。「tag run 只能读 main / 同名 tag」这条来自
官方文档而不是观测;下一次 tag 正好顺手验三件事:`Cache restored from key:` 是否变成 `No cache found.`(linux-x64
应当是)、`Compiling aws-lc-sys` 是否重编、日志里还有没有 `__isoc23_*`。