P2-1(核心:上一轮的 ② 在 git-bash 下是死代码):`cmd //c "\"${vcvars//\//\}\" arm64 …"`
不行——MSYS 把参数里的 `"` 改写成 `\"` 交给 cmd.exe,cmd 的 `/c` 引号剥离接不住,LIB/INCLUDE
全空,只剩 `set` 的空输出;于是 ③「显式 link.exe」在**没有 LIB** 的状态下生效,正是本步注释里
写的那种会造出 `LNK1181: cannot open input file 'kernel32.lib'` 的组合。改法(复审验过的路线):
往 `$RUNNER_TEMP` 写一个 **CRLF** 的 wrapper `.bat`(`call "<vcvarsall>" arm64 >/dev/null 2>&1` + `set`),
`cmd //c` 只收**不含引号**的相对文件名(先 cd 进去)——引号留在 .bat 内部由 cmd 解析,路径带空格
也不受影响。同源的 ③ 也补齐:`vswhere -find` 的 `vcvarsall.bat` 与两个 `link.exe` 候选都改成
「反斜杠形态 + 正斜杠形态各试一次」(和同文件 dumpbin 步同规矩),不再赌一种形态。
P2-2:cargo build 步末尾那条 `ls -l` 移到 `if [ "$SLOT" = "windows-arm64" ]` 门内(并加 `|| true`):
x64 是当前唯一在发的槽位,不给它加一个失败面。
P2-3:① `release-publish.yml` 步数改对——主线 7 步、本分支 8 步(`release.yml` 与
`packaging/docs/build-and-ci.md` 两处都改,并列出 8 步是哪几步);② `check_publish_slots.py`
的 report() 全部改走 **stdout**:注解(`::error::`/`::warning::`)在 stdout 上同样被 Actions 识别,
而分两个流写会让「表格 → 注解」的顺序不可控(原注释承诺的顺序当时并不成立)。顺带在
`build-and-ci.md` 的 arm64 工具链段补一句「`cmd //c` 别收带引号的路径、要 wrapper .bat」,
免得下一个 agent 再把 ② 写回死代码。
本地证据(同一 worktree 复跑):
- P2-1 新证据:把本步 `run:` 块原样抽出,用 `bash --noprofile --norc -eo pipefail`(与 runner 同
语义)、假 vswhere + **路径带空格**的假 `vcvarsall.bat`(真 cmd)跑:新写法 LIB/INCLUDE/linker
三行全部写进 `$GITHUB_ENV`、rc=0;同一套假件跑 e858664 的老写法 ⇒ LIB/INCLUDE 全空、只有 linker
(与复审结果一致)。降级路径也复测:vswhere 不存在 ⇒ 只 1 条 warning、rc=0;vswhere 不给
vcvarsall ⇒ ② warning、③ 仍拿到 Hostx64 兜底、rc=0。
- 体检步 5 场景(无 VS / vswhere 全成功 / 报错退出 / 垃圾+非零 / 路径不存在)全部 rc=0、0 条
`::error::`。
- `check_publish_slots.py --self-test` 8 条 + 警告断言全绿;`--dist` 六场景复测(x64-only 绿 /
多一份 arm64 绿+warning / 缺 CLI rc=1 / 空 dist rc=1 / `--slots` 漂移 rc=1 / `--dist` 非目录
rc=1),并且缺产物时输出顺序是「表格 → warning → error → 汇总」,stderr 为空。
- 7 个 workflow `yaml.safe_load` OK;全部 `run:` 块 `bash -n` OK;全仓 400 行扫描 0 个超限
(release-build.yml 316、check_publish_slots.py 345)。
- `check_release_semantics.py`(+`--self-mutation`)、`release_marker.py --self-test`、
`make_manifest.py --self-test`、`cargo metadata --no-deps` 全绿。
边界不变:arm64 产物仍**从未被构建过、也从未在真机跑过**;`PUBLISH_SLOTS` 仍是 `"windows-x64"`,
发布语义与 x64 静态 CRT 门禁未动。下一次 tag 的 arm64 仍可能红,红点大概率换到 C 依赖编译
(aws-lc-sys 的 CMake + aarch64 cl.exe)或链接器(LNK1181)——**不是「arm64 修好了」**。
12 KiB
构建与 CI 流水线(M145 拆自 packaging/README.md)
本文承载原 packaging/README.md 的 §3,节号沿用原编号(全仓 packaging/README.md §N 这类引用照旧有效,
对照表在 packaging/README.md 的索引里)。内容一字未改、只搬家:本行以上是 M145 新增的导航说明,
以下逐行取自原文件。
3. 怎么构建
本地(当前平台)
cargo build --release --locked # 产出 target/release/{unlucky-launcher-iced,ull}(M118:Slint 版不再进 workspace)
cargo build --profile fast --bins # 只是想快点看到画面/跑测试:快档(§6),产物在 target/fast/
python packaging/make_artifacts.py --out dist # 产物落在 dist/,含 SHA256SUMS.txt
python packaging/make_artifacts.py --version v0.2.2 # 顺带校验版本一致性
python packaging/make_artifacts.py --no-smoke # 跳过 ull --version 冒烟
脚本按宿主自动判定槽位(Windows x64 / Windows arm64 / Linux x64 / Linux arm64 / macOS arm64 / macOS x64),其它组合直接报错退出——不在矩阵里的平台不出"凑合能跑"的产物。
交叉构建(例如在 x64 宿主上产 Windows arm64 产物)靠两个参数把"产物根"和"槽位"显式喂进去, 脚本不去猜宿主与产物是不是同一架构(CI 不用这条路径,见下):
rustup target add aarch64-pc-windows-msvc
cargo build --release --locked --target aarch64-pc-windows-msvc \
--bin unlucky-launcher-iced --bin ull # 不用 --bins:矩阵只发 GUI + ull;GUI 的构建产物名是 iced 的 bin(M118)
python packaging/make_artifacts.py --slot windows-arm64 \
--build-dir target/aarch64-pc-windows-msvc/release --out dist
--slot 与宿主槽位不一致时,ull --version 冒烟会自动跳过并打印原因(arm64 产物在 x64
宿主机上根本执行不了;拿"跳过"当通过是假证据,所以跳过永远带原因)。CI 不走交叉(M140):
windows-arm64 在原生 windows-11-arm 上编,宿主=产物架构 → 冒烟真跑;linux-arm64 的同类
冒烟只在 all_slots=true 的取证跑里出现(ubuntu-22.04-arm 原生 runner 上真跑 ull --version)。
CI(默认只跑两个 Windows 槽位;all_slots=true 才整表六槽位)
.github/workflows/release.yml:
-
触发:
pushtagv*;workflow_dispatch只构建不发布,且多一个all_slots开关(默认 false = 只跑两个 Windows 槽位,true = 整表六槽位都编一遍取证)。inputs.prerelease只在 tag ref 上 dispatch 才可达——Actions 页面的 "Run workflow" 只能选分支,要跑 tag 得gh workflow run release.yml --ref v0.2.3。 分支 ref 的 dispatch 与 push 一样被releasejob 的if: github.ref_type == 'tag'挡在发布之外,所以 「分支 ref 的 dispatch 只构建不发布」与「tag ref 上 dispatch 能选标记」是两条互不冲突的规则。 -
矩阵由
planjob 按all_slots生成(matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}:矩阵本身 不能带if,而strategy可读needs),槽位表只在该 job 里有一份:windows-latestx64 与 原生windows-11-armarm64(M140;后者是公开仓免费的标准 arm64 runner,4C/16G)——两者都是 MSVC +-C target-feature=+crt-static、产物都在target/release,标mode: ci(默认编);ubuntu-22.04(glibc 2.35 地板)/ubuntu-22.04-arm(原生 ARM64,同地板)/macos-latest/macos-15-intel标mode: probe——未验证,默认一个都不编,只有all_slots=true才进矩阵。 -
每个槽位
cargo build --release --locked(不传--target:每槽原生编译,交叉槽位没了)。arm64 槽位在编译前 把工具链显式备齐(M164):rustup target add aarch64-pc-windows-msvc、用微软自己的vcvarsall.bat arm64取LIB/INCLUDE(写进本步环境与$GITHUB_ENV)、用 vswhere 定位 arm64link.exe后显式导出CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER(Hostarm64 原生优先、Hostx64 次之;-find的斜杠/反斜杠两种形态都试)。 顺序不能反:只覆盖链接器而不给LIB,撞上的就是LNK1181: cannot open input file 'kernel32.lib'。cmd //c不能直接收「带引号的 vcvarsall 路径」:MSYS 会把"改写成\",cmd 的/c剥离规则接不住, 结果LIB/INCLUDE全空(M164 复审实测;cmd //c只调不含引号的路径是好的)。故本步往$RUNNER_TEMP写一个 CRLF 的 wrapper.bat(call "<vcvarsall>" arm64+set),cmd //c只收相对文件名——引号留在.bat内部由 cmd 自己解析,路径带空格也不受影响。 -
编译前体检已降级为「只取证」(M164):M141/M142 的版本用
find猜um/arm64/kernel32.lib在不在、猜缺就exit 1—— v0.2.1~v0.2.3 五轮全红在那一步,cargo build一次都没跑过(永远 skipped),而它跟自己打的快照 自相矛盾(um/arm64明明在)。现在那一步只打印 vswhere 组件查询、各目录.lib计数与link.exe候选, 永不exit 1;红绿只由编译器 / 链接器 / 静态 CRT 导入表门禁决定。runner 实况:windows-11-arm自 2026-09 是 Windows 11 Arm64 + Visual Studio 2026 镜像,VC.Tools.ARM64与Windows11SDK.22621/26100都已装好。 -
发布白名单(
PUBLISH_SLOTS,唯一开关):只有白名单里的槽位进 Release 与SHA256SUMS.txt。M142 起只有windows-x64:齐全性检查是「名单内少一档就 fail」,挂着没验证过的 arm64 就等于 x64 编成功也发不出去。 (M164)arm64 那五轮红其实全红在误判的体检步(上一条),门禁拆掉后「arm64 能不能出 PE」只能由下一 tag 的 CI 证明,所以名单这轮仍只列 x64 —— 不拿 x64 的发布去赌(tower M164 裁决)。可逆且只是一行:那次 CI 真出包后把 键加回release-publish.yml那一行,再同步 §1/§9 与release-notes.md。四档未验证的(默认连构建都不跑)依旧不推给用户。 -
名单 ⇄ 产物两个方向都有守卫(M164):
packaging/check_publish_slots.py,同一份代码两处跑 —— PR 阶段 (.github/workflows/release-guard.yml)断言「名单非空 / 每项都是槽位表里mode: ci的档」;发布侧 (release-publish.yml的--dist模式,跑在裁剪前的全量集上)断言「声明了就得有 GUI 与 CLI 两个产物, 缺一个就红」,并把「有产物但名单没声明」打成::warning::明说未发布 —— M142 之后那个方向是静的(裁剪步只echo drop:),一个真编出来的 arm64 会被就地删掉而没人知道。--self-test注入 8 条回归,本地一条命令可跑。 -
releasejob:下载全部 artifact → 发布名单守卫(断言 + 未发布提示)→ 按白名单裁剪 → 重算SHA256SUMS.txt→ 用packaging/release-notes.md模板渲染支持矩阵 → 生成更新清单manifest.json→gh release create(push tag 默认建稳定版:不带任何标记)。 -
同一 tag 重跑幂等:
gh release view命中已存在的 Release 时改走gh release upload --clobbergh release edit,覆盖资产与说明而不是再create(create遇已存在会失败,把流水线最后一步染红)。
-
预发布标记的语义表(M163,唯一真相源
packaging/release_marker.py):触发 create(新建 Release) edit(Release 已存在) pushtagv*(含其 Re-run)稳定版(不带标记) 不碰标记 workflow_dispatch+prerelease=true预发布 显式改成预发布 workflow_dispatch+prerelease=false(默认)稳定版 显式改成稳定版 空串必须是稳定版:
releases/latest/download/*只指向非 prerelease 的最新 Release,而自更新清单固定取releases/latest/download/manifest.json(D50)——push tag 打成预发布,稳定通道就一直是 404(v0.2.2 至今 如此,M163 修的就是这条;v0.2.2 那个 Release 保留为历史预发布不动,稳定通道以 v0.2.3 为起点)。 edit 的空串是「不碰」而不是「改成稳定版」:同 tag 重跑不会把用户手工提成稳定版的 Release 翻回预发布。 要改已发版本的标记仍是两条等价路:①gh workflow run release.yml --ref <tag> -f prerelease=false(落到 edit 分支显式断言)② 直接gh release edit <tag> --prerelease=false(不经流水线,后续默认重跑不碰它)。 -
这条语义有守门(M163):
packaging/check_release_semantics.py(CI 在.github/workflows/release-guard.yml) 断言四件事:标记表逐条对(push tag 的 create 不含--prerelease)、把release_marker.py --emit-bash的产物 真灌进 bash 读回两个数组、release-publish.yml的可执行内容里没有--prerelease字面量、dispatch 默认值== false(读 YAML 结构,不是 grep)。--self-mutation再把「默认值改回 true」「塞回字面量」「表里 push 分支 改成预发布」注入临时副本,断言守门每一项都红、基线绿——守门自己抓不住回归时它先红。 -
M145 拆分(本节新增的唯一一段):上面所有口径都还成立,只是"在哪个文件里"变了 ——
release.yml只留 触发、concurrency、槽位表(plan)与两个uses:编排;buildjob 的 11 步逐字搬进.github/workflows/release-build.yml(workflow_call,四个矩阵键换成同名inputs.*),releasejob 的 6 步 逐字搬进.github/workflows/release-publish.yml(同workflow_call,secrets.GITHUB_TOKEN改为调用方显式透传 的 secret)。触发(on)、env、顶层permissions、concurrency与plan的槽位表在 M145 搬家时一字未动; 后来只有 M163 动过on里prerelease的默认值(true→false,见上面那张语义表),M164 改过release-build.yml的 arm64 体检步与cargo build步、并在release-publish.yml裁剪前插了「发布名单守卫」 (于是本分支 8 步:checkout / download / 守卫 / 裁剪 / 重算 SHA256SUMS / manifest / 说明 /gh release; 搬家时 6 步、主线 7 步)。改槽位仍只改release.yml那一处(PUBLISH_SLOTS白名单在release-publish.yml, 翻牌时要改的仍是那两处)。 -
M149 微软登录 id 的烘焙:
release-build.yml的cargo build步带env.ULL_MSA_CLIENT_ID(值${{ secrets.ULL_MSA_CLIENT_ID }}),option_env!把它烘进二进制,于是发布产物装完即可登录。id 不进 git 文件:真值放仓库 Secret,由release.yml的buildjob 按名显式透传(可复用工作流拿不到未显式传入的 secret;这里刻意不用secrets: inherit——那会把仓库全部 secret 一起透进被调方)。Secret 未录入、或在整个 Actions 之外求值时该表达式为空串,client_id.rs把空白当「没配」⇒ 非官方构建只是不烘 id,不会拿空 id 去 联网;本地自烘仍走ULL_MSA_CLIENT_ID=<id> cargo build。别把这个 env 导出到跑cargo test的 shell: presenter 的 M132 入口锚会在「已烘焙 id」的构建里故意红(它要求一个两处都没有 id 的构建)。 -
发布说明模板(
release-notes.md)的 "Known limitations" 里,微软登录一条已改为「开箱即用、env 可覆盖」。
Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两者都属"标准工具链"(D21),
但发布产物要不依赖 VC++ 可再发行包,+crt-static 把 CRT 静态链进 exe,Win10+ 的 UCRT 本身是
系统组件,于是 exe 真正单文件可拷走。GNU 交叉产物在 CI 上还要拉 MinGW 链接器,收益为零。