根因(拿到的真实证据,不是猜的):v0.2.2(run 42322062879 / job 128248713926)与 v0.2.3 (run 42429017002 / job 128573547829)两份 check-run 注解逐字一致 —— 失败步骤是第 6 步 `Windows ARM64 工具链体检`,第 7 步 `cargo build` 状态 skipped。也就是说 v0.2.1~v0.2.3 五轮 全红都红在一个**编译前的启发式探针**上,真正的编译器/链接器一次都没说话。该探针的结论还与它 自己打印的快照矛盾:`SDK Lib 版本→um 架构: 10.0.22621.0→um:[arm,arm64,x64,x86,]` 明明在,却 同时报 `kernel32=缺失`(同一个 `find -path` 模式在本地合成目录树上命中)。runner 实况是 Windows 11 Arm64 + Visual Studio 2026 镜像,`VC.Tools.ARM64` 与 `Windows11SDK.22621/26100` 都装好了,M142 的 `setup.exe modify` 反倒返回非 0。 改: - release-build.yml:探针降级为「只取证」——vswhere 组件查询 + 各目录 `.lib` 计数 + arm64 `link.exe` 候选,一条 `::error::` 都不写、永不 exit 1;不再自动补装 VS。红绿只由 cargo/链接/静态 CRT 导入表门禁决定。cargo build 步给 arm64 显式备齐工具链: `rustup target add aarch64-pc-windows-msvc`、`vcvarsall.bat arm64` 取 `LIB`/`INCLUDE`、 vswhere 定位 arm64 `link.exe` 后导出 `CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER` (顺序不能反:只给链接器不给 LIB ⇒ LNK1181 kernel32.lib)。 - 新增 packaging/check_publish_slots.py(344 行,零依赖):名单 ⇄ 产物**两个方向**。 声明了缺 GUI/CLI ⇒ `::error::` + exit 1;有产物没声明 ⇒ `::warning::` 明说未发布(M142 之后 这个方向是静默的,裁剪步只 echo 一行 drop:)。另有源码级断言(名单每项必须存在且 mode=ci、 运行期名单必须等于文件里那行)与 8 条 `--self-test` 注入回归。 - release-publish.yml:裁剪前插一步跑那个守卫,原来内联的齐全性循环搬进脚本(一套判据一处实现); 重算 SHA256SUMS 的步骤只负责重算。 - release-guard.yml:PR 阶段跑守卫自测 + 源码级断言。 - 文档同步:release.yml / release-publish.yml / release-build.yml 头注释、packaging/README §12、 §1 矩阵、§3 构建与发布、§9 支持矩阵、§7.5 arm64 取证、release-notes.md、README/README.zh-CN 的产物矩阵与说明。口径统一为「arm64 构建取证中、未发布」,不写成已支持。 没做的(按 tower 裁决「方案乙」):`PUBLISH_SLOTS` 本轮**不加回** arm64。arm64 从未产出过 PE、 本机也证不了,写进名单就等于拿 x64 的发布去赌它(M142 移出名单正是为避免这个事故)。下一 tag 的 CI 真出包后翻一行即可,守卫与 SHA256SUMS 自动跟着扩。 验证:workflow YAML 全部可被 PyYAML 解析;所有 `run:` 块过 `bash -n`;探针步在本机(无 VS/SDK) 实跑 exit 0 并打出实况注解;守卫 `--self-test` 8 条全绿、正反两个方向都在合成 dist 上实测过; check_release_semantics.py(含 --self-mutation)/ release_marker / make_manifest 自检全绿; cargo metadata 正常。**arm64 产物从未被构建过、也从未在真机跑过**——本机 `rustc --target aarch64-pc-windows-msvc` 直接撞上没有 MSVC 链接器(git-bash 的 GNU `link` 被当成 link.exe), `cargo check --workspace --target aarch64-pc-windows-msvc` 也死在 aws-lc-sys 的 cc-rs 阶段 (GNU host 上没有该 target 的 C 编译器)。真实结论只能等下一 tag 的 CI。
8.9 KiB
打包与发布工程(packaging/)
单一真相源:单文件产物矩阵 + 发布契约。.github/workflows/release.yml 按本文实现,
packaging/make_artifacts.py 是本地与 CI 共用的打包脚本(同一份代码 → 同一套目录布局)。
本目录只放发布工程资产(脚本 / 说明 / 发布说明模板),不参与任何 crate 的编译。
索引:§1–§12 各在哪份文件(M145 拆分)
拆分原因:本文件与 .github/workflows/release.yml 都已是 400 行整,下一处改动必撞全仓行数红线(guards.yml 的
file-size,单文件 ≤400 行)。按主题分开后每份都留出余量,内容一字未改、只搬家;节号沿用原编号,
所以全仓那些「packaging/README.md §4 / §7.4 / §9」式引用不必改动 —— 按下表找承载文件即可。
| 节 | 主题 | 承载文件 |
|---|---|---|
| §1 | 产物矩阵 | packaging/docs/artifacts-and-version.md |
| §2 | 版本来源(唯一真相源) | packaging/docs/artifacts-and-version.md |
| §3 | 怎么构建(本地 / CI) | packaging/docs/build-and-ci.md |
| §4 | 每平台运行时依赖 | packaging/docs/runtime-deps.md |
| §5 | 软渲染兜底 | packaging/docs/runtime-deps.md |
| §6 | Cargo 构建档位 | packaging/docs/profiles-and-measurements.md |
| §7 | 实测(本机,不是估算) | packaging/docs/profiles-and-measurements.md |
| §8 | 发行合规 | packaging/docs/compliance-support-boundaries.md |
| §9 | 支持矩阵(对外口径) | packaging/docs/compliance-support-boundaries.md |
| §10 | 已知边界 / 后续 | packaging/docs/compliance-support-boundaries.md |
| §11 | CI 运行时冒烟(Linux 真渲染一帧) | 本文下方(细节在 packaging/ci-runtime-smoke.md) |
| §12 | 文件清单 | 本文下方 |
本文件仍是契约的唯一真相源入口:.github/workflows/release.yml(及拆出的 release-build.yml /
release-publish.yml)的注释、make_artifacts.py 的说明、根 README.md / README.zh-CN.md 与
packaging/release-notes.md 都按「packaging/README.md §N」引用。改契约时先改上表对应的承载文件,
再看一眼 §12 的行数预算 —— 任何一份超过 400 行都要继续按主题拆。
11. CI 运行时冒烟(Linux 真渲染一帧)
细节与依据在 packaging/ci-runtime-smoke.md(本节再增长也往那里写,两个文件都必须 ≤400 行)。
ci.yml 的 runtime-smoke job 在 ubuntu-latest 上补第一份真实图形环境证据:装 xvfb + 软渲染依赖 +
字体,用 xvfb-run 起 GUI 进程渲出一帧 PNG,再对像素本身断言(退出码 0 / 尺寸 / 非空白,artifact
runtime-smoke-screenshot)。
要点三条:
- 只看退出码不够:「合法 PNG + 退出码 0」不等于画面有内容;「完全没字体」与「非软渲染」都是硬失败(panic / 报错,不产生 PNG;review 实测)。
- 闸门下限 ≈「至少画出一条标题栏」(review 实测切片仍 pass):证明「场景被真实渲染过」,不证明界面元素齐全, 更不证明视觉正确——视觉靠人眼看 artifact。
- 只覆盖 Linux x64;macOS 是 dispatch-only 探针(无实测证据),Windows 已有本机人眼验收。
本地复跑:bash packaging/linux-smoke-docker.sh . D:/tmp/ull-smoke——脚本逐步复刻这个 job(同样的 apt 依赖、
--bin unlucky-launcher-iced、同样的 xvfb-run … --software-renderer --screenshot --data-dir);输出目录别写 /tmp,
Git Bash 下会静默落进 Docker VM(脚本内部用 cygpath -m 归一)。
M131 真跑过一次(本机 Docker / Debian 12 容器 / 2026-11-05):exit=0,截图 1180×760、[ok] 截图非空白。
12. 文件清单
| 文件 | 作用 | 行数上限 |
|---|---|---|
packaging/README.md |
本文:索引 + §11 / §12 / §13(契约入口;§1–§10 见 docs/) |
≤400 |
packaging/docs/artifacts-and-version.md |
§1 产物矩阵 + §2 版本来源 | ≤400 |
packaging/docs/build-and-ci.md |
§3 怎么构建(本地 / CI) | ≤400 |
packaging/docs/runtime-deps.md |
§4 每平台运行时依赖 + §5 软渲染兜底 | ≤400 |
packaging/docs/profiles-and-measurements.md |
§6 Cargo 构建档位 + §7 实测 | ≤400 |
packaging/docs/compliance-support-boundaries.md |
§8 发行合规 + §9 支持矩阵 + §10 已知边界 | ≤400 |
packaging/make_artifacts.py |
打包与校验(本地 + CI 共用) | ≤400 |
packaging/release-notes.md |
GitHub Release 说明模板({{VERSION}} / {{SUMS}} 占位) |
≤400 |
packaging/check_screenshot.py |
截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 |
packaging/check_publish_slots.py |
发布名单守卫(M164):名单 ⇄ 产物两个方向;--self-test 注入 8 条回归 |
≤400 |
packaging/ci-runtime-smoke.md |
§11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 |
packaging/linux-smoke-docker.sh |
本地复跑 runtime-smoke(Docker + xvfb) |
≤400 |
packaging/linux-check-docker.sh |
本地复跑 ci.yml 的两条 ubuntu 门禁(clippy / test,Docker) |
≤400 |
.github/workflows/release.yml |
tag 触发 → 选槽位(plan)→ 调下面两个可复用工作流 |
≤400 |
.github/workflows/release-build.yml |
workflow_call:单槽位构建(体检 / cargo / 打包 / 上传 artifact) |
≤400 |
packaging/make_manifest.py |
更新清单 manifest.json 的生成与自验(D50,见 §13) |
≤400 |
packaging/tests/manifest.golden.json |
清单 golden 样例:make_manifest.py --self-test 逐字节对拍 |
≤400 |
.github/workflows/release-publish.yml |
workflow_call:发布(PUBLISH_SLOTS 裁剪 / SHA256SUMS / 清单 / Release) |
≤400 |
13. 更新清单 manifest.json(D50)
清单作为固定文件名的 Release 资产挂着;客户端按 releases/latest/download/manifest.json 取(D50)。
字段形状的唯一权威是 crates/core/src/update/manifest.rs 的 ReleaseManifest(deny_unknown_fields +
逐字段校验:semver、size > 0、摘要长度/字符集、target/format 取值表)——生成器只负责把那套形状填对。
- 生成器:
packaging/make_manifest.py(零第三方依赖)。CI 在release-publish.yml里重算SHA256SUMS.txt之后调它,所以清单描述的是「真正要发的资产」,不是「构建出来的全部」。 - 同源,不双写:每个
sha256直接读SHA256SUMS.txt,size取自同一批文件的stat();产物名里的版本段 必须等于--version(tag 与Cargo.toml漂了就没有产物可对,当场失败)。--self-test用packaging/tests/manifest.golden.json逐字节对拍,并跑 14 条负例证明严格解析不是摆设。 - 只放启动器产物:
ull不进清单。DTO 的Artifact没有角色字段,Release::artifact_for()取「第一个 target 命中的产物」,把 CLI 并列写进去会让「该覆盖哪个文件」取决于数组顺序(ull照旧进SHA256SUMS.txt)。 url两种形态:默认裸文件名(相对源根,DirSource口径,本地/内网用);CI 传--asset-base-url拼绝对地址,且钉 tag 不钉 latest——清单里每条都指向与它自报版本相符的字节。min_supported/notes只是可填而不默认填:前者是「收回老版本支持」的发布策略(未拍板,写错会强制升级), 后者在 Release 正文里已有,两处各写一份迟早漂。- 本地复跑:
python3 packaging/make_artifacts.py --manifest→ 与 CI 同一段代码,产出dist/manifest.json。
D51 定案(2026-11-06,方案甲)+ M163 语义修正(2026-11-07):GitHub 的 releases/latest/download/*
只指向非 prerelease 的最新 Release。而自更新清单固定取 releases/latest/download/manifest.json,所以
「新版本必须被标成稳定版,latest 才会指过去」——M163 之前 push tag 默认打 --prerelease,v0.2.2 因此至今
releases/latest 404、更新通道取不到清单。现在的语义(唯一真相源 packaging/release_marker.py):
| 触发 | create(新建 Release) | edit(Release 已存在) |
|---|---|---|
push tag v*(含其 Re-run) |
稳定版(不带标记) | 不碰标记 |
workflow_dispatch + prerelease=true |
预发布 | 显式改成预发布 |
workflow_dispatch + prerelease=false(默认) |
稳定版 | 显式改成稳定版 |
v0.2.2 那个 Release 保留为历史预发布不动,稳定通道以 v0.2.3 为起点。发版 checklist 第六件 =
确认 prerelease 标记与通道一致(gh release view <tag> --json isPrerelease),守门是
packaging/check_release_semantics.py(CI:.github/workflows/release-guard.yml)。