ci(release): M164 arm64 判定权交回编译器 + 发布名单守卫(不翻牌)
根因(拿到的真实证据,不是猜的):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。
This commit is contained in:
1 parent
15a19bb375
commit
e858664fca
13 files changed
+577
-120
No files matched your search
@@ -1,9 +1,11 @@
|
|||||||
name: Release / build(可复用)
|
name: Release / build(可复用)
|
||||||
|
|
||||||
# Release 流水线的**构建侧**(M145 拆分:`release.yml` 与 `packaging/README.md` 都顶到 400 行红线,下一处改动必越线)。
|
# Release 流水线的**构建侧**(M145 拆分:`release.yml` 与 `packaging/README.md` 都顶到 400 行红线,下一处改动必越线)。
|
||||||
# 本文件 = 拆分前 `.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` → `inputs.slot`、`matrix.os` → `inputs.os`、`matrix.build_dir` → `inputs.build_dir`、
|
||||||
# `matrix.rustflags` → `inputs.rustflags`。其余 `name` / `if` / `env` / `run` / `uses` 与拆分前逐行相同。
|
# `matrix.rustflags` → `inputs.rustflags`。
|
||||||
|
# **M164 改了两步**(只这两步):① `Windows ARM64 工具链体检` → `工具链取证`(不再 exit 1,判定权交回编译器;
|
||||||
|
# 理由见该步上方的长注释);② `cargo build` 步给 arm64 槽位显式备齐 target / `LIB`+`INCLUDE` / arm64 `link.exe`。
|
||||||
# 矩阵**仍由调用方** `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
|
||||||
@@ -67,64 +69,56 @@ jobs:
|
|||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
# (M141/M142)windows-arm64 跑在**原生** `windows-11-arm` 上,本步查这台机器上 ARM64 库齐不齐:`+crt-static`
|
# (M141/M142 → **M164 判定权移交**)windows-arm64 跑在**原生** `windows-11-arm` 上。本步**只取证,不判定**:
|
||||||
# 要 MSVC 的 `lib/arm64/libcmt.lib`,链接要 SDK 的 `um/arm64/kernel32.lib`。诊断**只走注解**(job 日志下载要
|
# M141/M142 的版本用 `find` 猜 SDK 的 `um/arm64/kernel32.lib` 在不在,猜缺就 `exit 1` —— 结果 v0.2.1~v0.2.3
|
||||||
# token 我们没有,check-run 注解不用;每条一行,多行会被拼成一行截断,每步 error 注解上限 10 条)。
|
# 五轮全红在**这一步**、下面 `cargo build` 一次都没跑过(第 7 步永远 skipped),"arm64 修不好"始终是探针给的
|
||||||
# **M142 修本步的自毁 bug**:`shell: bash` 实为 `bash -eo pipefail`,脚本里的 `set -uo pipefail` **关不掉** -e——
|
# 结论、不是编译器给的。而它跟自己打的快照自相矛盾:v0.2.3 的注解原文是
|
||||||
# M141 死在 `vc_lib=$(find 不存在的根 | head -n1)`(pipefail 把 find 的非零带进赋值、errexit 当场退出),
|
# `[arm64sdk] SDK Lib 版本→um 架构: 10.0.22621.0→um:[arm,arm64,x64,x86,] 10.0.26100.0→um:[arm64,x64,x86,]`
|
||||||
# **一条注解都没打**,上一轮只剩 `Process completed with exit code 1.`。四条规矩:① 显式 `set +e`;② 每处探测
|
# 同一份注解里却报 `kernel32=缺失`(同一个 `find` 模式在本地合成目录树上命中,故这条路本身不可靠)。
|
||||||
# 自带 `|| true`;③ 问题记进 ERRORS 而不当场退出;④ 快照先打完、再由显式 `exit` 定红绿。门禁不降级(缺了就
|
# M164 把红绿交回编译器与链接器:本步只打印实况(vswhere 组件查询 / 目录里 .lib **计数** / arm64 link.exe
|
||||||
# 就地补装再判),但 **arm64 红不挡发布**(PUBLISH_SLOTS 已无它),本槽位只取证。
|
# 路径),一条 `::error::` 都不写、永不 `exit 1`;红不红由 `cargo build` 与静态 CRT 导入表门禁决定。
|
||||||
- name: Windows ARM64 工具链体检(红不挡发布,只取证)
|
# 也不再自动补装:runner 镜像是 Windows 11 Arm64 + Visual Studio 2026(`windows-11-arm` 自 2026-09 切到 VS2026,
|
||||||
|
# VS 在 `C:\Program Files\Microsoft Visual Studio\18\Enterprise`),其镜像清单
|
||||||
|
# (actions/runner-images 的 `images/windows/Windows11-VS2026-Arm64-Readme.md`)里 `VC.Tools.ARM64`、
|
||||||
|
# `Windows11SDK.26100`、`Windows11SDK.22621` 三件都已装好;M142 那次 `setup.exe modify` 反倒返回非 0。
|
||||||
|
# 注解仍每条一行(job 日志下载要 token,check-run 注解不用;多行会被拼成一行截断)。
|
||||||
|
- name: Windows ARM64 工具链取证(只打印实况,红绿交给编译器)
|
||||||
if: inputs.slot == 'windows-arm64'
|
if: inputs.slot == 'windows-arm64'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set +e; set -uo pipefail; ERRORS=() # -e 由 runner 命令行注入,只能显式关
|
set +e; set -uo pipefail # -e 由 runner 命令行注入,只能显式关
|
||||||
say() { printf '::%s::[arm64sdk] %s\n' "$1" "$2"; [ "$1" = error ] && ERRORS+=("$2"); return 0; }
|
say() { printf '::%s::[arm64sdk] %s\n' "$1" "$2"; return 0; }
|
||||||
vs=("C:/Program Files/Microsoft Visual Studio" "C:/Program Files (x86)/Microsoft Visual Studio")
|
shopt -s nullglob # 计数走 glob 不走 find:find 的管道 + errexit 正是 M141 的死因
|
||||||
# SDK 根要放宽:新版装 `(x86)`、也有装 `Program Files`;老安装器把 lib 留在 Microsoft SDKs;
|
vswhere="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
|
||||||
# VS 自己还带一份「SDK in VS」(`<VS>/Windows Kits/10/Lib`),用 vswhere 定位后追加。
|
[ -x "$vswhere" ] || say warning "找不到 vswhere($vswhere):下面会是 0 命中,那不代表工具链缺东西"
|
||||||
sdk=("C:/Program Files (x86)/Windows Kits/10/Lib" "C:/Program Files/Windows Kits/10/Lib"
|
# ① 组件:用 vswhere 自己的 `-requires`(权威查询),不拿「某个文件名存在」当替身。
|
||||||
"C:/Program Files (x86)/Microsoft SDKs/Windows/v10.0A/Lib")
|
for comp in Microsoft.VisualStudio.Component.VC.Tools.ARM64 \
|
||||||
inst="C:/Program Files (x86)/Microsoft Visual Studio/Installer"; vspath=""
|
Microsoft.VisualStudio.Component.Windows11SDK.26100 \
|
||||||
[ -x "$inst/vswhere.exe" ] && vspath=$("$inst/vswhere.exe" -latest -products '*' -property installationPath 2>/dev/null | head -n1 || true)
|
Microsoft.VisualStudio.Component.Windows11SDK.22621; do
|
||||||
[ -n "$vspath" ] && sdk+=("${vspath//\\//}/Windows Kits/10/Lib")
|
hit=""
|
||||||
# 下面两个探测正是 M141 断气处(find 起点不存在 → 非 0 → `| head` 的 pipefail 带进赋值 → errexit),故处处
|
[ -x "$vswhere" ] && hit=$("$vswhere" -latest -products '*' -requires "$comp" -property installationPath 2>/dev/null | head -n1 || true)
|
||||||
# `|| true`:探不到就是「没找到」,由结论下判断,不许由它决定进程生死。
|
if [ -n "$hit" ]; then say notice "组件 $comp: 已装($hit)"
|
||||||
scan() { vc_lib=$(find "${vs[@]}" -maxdepth 11 -type f -path '*/VC/Tools/MSVC/*/lib/arm64/libcmt.lib' 2>/dev/null | head -n1 || true)
|
else say warning "组件 $comp: 未装 / 这台 VS 不认这个 id(继续看下面的实际 .lib 计数)"; fi
|
||||||
sdk_lib=$(find "${sdk[@]}" -maxdepth 6 -type f -path '*/um/arm64/kernel32.lib' 2>/dev/null | head -n1 || true); }
|
|
||||||
build=$(ls "${sdk[@]}" 2>/dev/null | grep -oE '^10\.0\.[0-9]+' | cut -d. -f3 | sort -u | tail -n1 || true)
|
|
||||||
scan
|
|
||||||
# 快照固定 4 行,**无论齐不齐都先打完**:齐了用 notice(绿跑也留证据 = 探测本身没坏),缺了用 error。
|
|
||||||
sev=error; [ -n "$vc_lib" ] && [ -n "$sdk_lib" ] && sev=notice
|
|
||||||
say "$sev" "探测根: $(for d in "${vs[@]}" "${sdk[@]}"; do [ -d "$d" ] && printf '%s=有 ' "$d" || printf '%s=无 ' "$d"; done)vswhere=$([ -x "$inst/vswhere.exe" ] && echo 有 || echo 无) vspath=${vspath:-<空>}"
|
|
||||||
v=""
|
|
||||||
for d in "${sdk[@]}"; do [ -d "$d" ] || continue
|
|
||||||
for t in "$d"/*/; do [ -d "$t" ] && v+="$(basename "$t")→um:[$(ls "$t/um" 2>/dev/null | tr '\n' ',')] "; done
|
|
||||||
done
|
done
|
||||||
say "$sev" "SDK Lib 版本→um 架构: ${v:-<没有任何版本目录>}(SDK build=${build:-未探到})"
|
# ② 实况计数:`+crt-static` 要 VC 的 `lib/arm64/libcmt.lib`,链接要 SDK 的 `um/arm64/*.lib`。
|
||||||
v=""
|
for root in "C:/Program Files/Microsoft Visual Studio" "C:/Program Files (x86)/Microsoft Visual Studio"; do
|
||||||
while IFS= read -r t; do v+="$(basename "$(dirname "$t")")→[$(ls "$t" 2>/dev/null | tr '\n' ',')] "; done \
|
hits=( "$root"/*/*/VC/Tools/MSVC/*/lib/arm64/libcmt.lib )
|
||||||
< <(find "${vs[@]}" -maxdepth 7 -type d -path '*/VC/Tools/MSVC/*/lib' 2>/dev/null | head -n4)
|
say notice "VC $root → lib/arm64/libcmt.lib ${#hits[@]} 个${hits[0]:+(首个 ${hits[0]})}"
|
||||||
say "$sev" "MSVC lib/ 架构: ${v:-<无 MSVC lib 目录>}"
|
done
|
||||||
say "$sev" "关键库: libcmt=${vc_lib:-缺失} kernel32=${sdk_lib:-缺失}"
|
for root in "C:/Program Files (x86)/Windows Kits/10/Lib" "C:/Program Files/Windows Kits/10/Lib"; do
|
||||||
if [ "$sev" = notice ]; then echo "ARM64 工具链齐: vc=$vc_lib sdk=$sdk_lib"; exit 0; fi
|
[ -d "$root" ] || { say notice "SDK $root:不存在"; continue; }
|
||||||
# 补装:VS 安装器就地 modify(门禁不降级:先补再判)。组件 ID 随 VS 大版本不同;**优先补一个尚未装上的
|
for ver in "$root"/*/; do
|
||||||
# SDK 版本**——同版本重装是 no-op,补不出 arm64 子目录。补装失败不致命,重探与结论给真相。
|
libs=( "$ver"um/arm64/*.lib )
|
||||||
id=Microsoft.VisualStudio.Component.Windows11SDK.$([ "$build" = "26100" ] && echo 22621 || echo 26100)
|
say notice "SDK $(basename "$ver") → um/arm64/*.lib ${#libs[@]} 个"
|
||||||
[ "$build" = "19041" ] && id=Microsoft.VisualStudio.Component.Windows10SDK.20348
|
done
|
||||||
adds=""; [ -n "$vc_lib" ] || adds+="Microsoft.VisualStudio.Component.VC.Tools.ARM64 "; [ -n "$sdk_lib" ] || adds+="$id"
|
done
|
||||||
if [ -x "$inst/setup.exe" ] && [ -n "$vspath" ]; then
|
# ③ arm64 链接器候选:宿主 arm64 的优先(原生),x64 托管的次之。下面 cargo 步会显式导出它。
|
||||||
say error "补装: $vspath ← ${adds}(已装 SDK build=${build:-未装})"
|
for p in 'VC/Tools/MSVC/*/bin/Hostarm64/arm64/link.exe' 'VC/Tools/MSVC/*/bin/Hostx64/arm64/link.exe'; do
|
||||||
timeout 1200 "$inst/setup.exe" modify --installPath "$vspath" --quiet --norestart --nocache --wait \
|
hit=""
|
||||||
--add ${adds} || say error "补装返回非 0:组件 ID 可能不被这台 VS 认(见下一条结论)"
|
[ -x "$vswhere" ] && hit=$("$vswhere" -latest -products '*' -find "$p" 2>/dev/null | head -n1 || true)
|
||||||
scan
|
say notice "link.exe 候选 $p → ${hit:-未命中}"
|
||||||
else say error "补装跳过: 无 vs_installer 或没定位到 VS 安装路径(vspath=${vspath:-<空>})"; fi
|
done
|
||||||
if [ -z "$vc_lib" ] || [ -z "$sdk_lib" ]; then
|
echo "取证完毕:本步不判红;arm64 的结论只看下面的 cargo build / 链接 / 导入表门禁"
|
||||||
say error "结论: 仍缺(libcmt=${vc_lib:-缺失} kernel32=${sdk_lib:-缺失}),共记 ${#ERRORS[@]} 条探测问题;门禁红、不摘 arm64——本槽位不在 PUBLISH_SLOTS 里,x64 照发"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "补装后齐全: vc=$vc_lib sdk=$sdk_lib"
|
|
||||||
|
|
||||||
# M118(D26):**Slint 版停发** —— GUI 只有 `unlucky-launcher-iced`(`crates/app` 已 exclude,不进 workspace)。
|
# M118(D26):**Slint 版停发** —— GUI 只有 `unlucky-launcher-iced`(`crates/app` 已 exclude,不进 workspace)。
|
||||||
# 产物名由 make_artifacts.py 的 `asset_stem()` 生成 `<名字>_<版本号>_<平台>_<架构>`;不传 `--target`(M140 起全原生)。
|
# 产物名由 make_artifacts.py 的 `asset_stem()` 生成 `<名字>_<版本号>_<平台>_<架构>`;不传 `--target`(M140 起全原生)。
|
||||||
@@ -132,14 +126,64 @@ 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 反正自带」这种默契:
|
||||||
|
# ① `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'`——那正是我们要定位的错误,不是要制造的错误。
|
||||||
|
# ①②③ 全是「定位不到就打印原因继续」,不用启发式决定红绿:真缺工具链时让链接器自己报出来。
|
||||||
- name: cargo build --release --locked
|
- name: cargo build --release --locked
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: ${{ inputs.rustflags }}
|
RUSTFLAGS: ${{ inputs.rustflags }}
|
||||||
ULL_MSA_CLIENT_ID: ${{ secrets.ULL_MSA_CLIENT_ID }}
|
ULL_MSA_CLIENT_ID: ${{ secrets.ULL_MSA_CLIENT_ID }}
|
||||||
|
SLOT: ${{ inputs.slot }}
|
||||||
|
BUILD_DIR: ${{ inputs.build_dir }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
echo "host=$(rustc -vV | sed -n 's/^host: //p') slot=$SLOT build_dir=$BUILD_DIR"
|
||||||
|
if [ "$SLOT" = "windows-arm64" ]; then
|
||||||
|
rustup target add aarch64-pc-windows-msvc
|
||||||
|
rustup target list --installed
|
||||||
|
vswhere="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
|
||||||
|
if [ -x "$vswhere" ]; then
|
||||||
|
vcvars=$("$vswhere" -latest -products '*' -find 'VC/Auxiliary/Build/vcvarsall.bat' 2>/dev/null | head -n1 || true)
|
||||||
|
dump=""
|
||||||
|
if [ -n "$vcvars" ]; then
|
||||||
|
# `cmd //c` 是 git-bash 调 cmd 的安全写法(`//c` 不会被当成路径);`set` 打出全量环境。
|
||||||
|
dump=$(cmd //c "\"${vcvars//\//\\}\" arm64 >nul 2>&1 && set" 2>/dev/null | tr -d '\r') || dump=""
|
||||||
|
fi
|
||||||
|
for key in LIB INCLUDE; do
|
||||||
|
value=$(printf '%s\n' "$dump" | sed -n "s/^${key}=//p" | head -n1)
|
||||||
|
if [ -n "$value" ]; then
|
||||||
|
export "$key=$value"
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$GITHUB_ENV"
|
||||||
|
echo "env $key=${value:0:90}…(${#value} 字符)"
|
||||||
|
else
|
||||||
|
echo "::warning::[arm64] 没拿到 $key(vcvarsall=${vcvars:-未定位}):留空交给 rustc 自己探测 VS"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
found=""
|
||||||
|
for p in 'VC/Tools/MSVC/*/bin/Hostarm64/arm64/link.exe' 'VC/Tools/MSVC/*/bin/Hostx64/arm64/link.exe'; do
|
||||||
|
hit=$("$vswhere" -latest -products '*' -find "$p" 2>/dev/null | head -n1 || true)
|
||||||
|
if [ -n "$hit" ]; then found=${hit//\\//}; break; fi
|
||||||
|
done
|
||||||
|
if [ -n "$found" ]; then
|
||||||
|
export CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER="$found"
|
||||||
|
printf 'CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER=%s\n' "$found" >> "$GITHUB_ENV"
|
||||||
|
echo "linker: $found"
|
||||||
|
else
|
||||||
|
echo "::warning::[arm64] 没定位到 arm64 link.exe:让 rustc 自己找(真的失败时错误里会带路径)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "::warning::[arm64] 没有 vswhere:target 已加,链接器交给 rustc 自己探测"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
cargo build --release --locked --bin unlucky-launcher-iced --bin ull
|
cargo build --release --locked --bin unlucky-launcher-iced --bin ull
|
||||||
|
ls -l "$BUILD_DIR"/unlucky-launcher-iced* "$BUILD_DIR"/ull*
|
||||||
|
|
||||||
- name: Windows 单文件自检(导入表不得出现 VC++ 可再发行 DLL)
|
- name: Windows 单文件自检(导入表不得出现 VC++ 可再发行 DLL)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
@@ -239,9 +283,11 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
# (M142)arm64 若红在别的步骤(cargo / 打包 / 导入表门禁),也要有一条注解说清「红在哪」和「不挡发布」——
|
# (M142/M164)arm64 若红在别的步骤(cargo / 链接 / 打包 / 导入表门禁),也要有一条注解说清「红在哪」和
|
||||||
# 只剩一句 `Process completed with exit code 1.` 的教训见上一条体检步骤。
|
# 「不挡发布」——只剩一句 `Process completed with exit code 1.` 的教训见上面那条体检步骤。
|
||||||
|
# M164 起本槽位的红**只可能来自真实构建链路**(体检已降级为取证、永不 exit 1),所以这条注解同时也是
|
||||||
|
# 「arm64 到底能不能编出来」的答案所在:红了就把上面的编译器/链接器原文当根因,别再回去改体检。
|
||||||
- name: arm64 失败收尾注解(红不挡发布)
|
- name: arm64 失败收尾注解(红不挡发布)
|
||||||
if: failure() && inputs.slot == 'windows-arm64'
|
if: failure() && inputs.slot == 'windows-arm64'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: echo "::error::[arm64] 本槽位已红(失败步骤见本 job 的步骤名与上面各条注解);PUBLISH_SLOTS 当前只有 windows-x64 ⇒ 本次 Release 仍照发 x64,arm64 产物不进 Release"
|
run: echo "::error::[arm64] 本槽位已红,失败步骤见本 job 的步骤名(M164 起只会是 cargo build / 链接 / 导入表门禁 / 打包,体检步不再判红);PUBLISH_SLOTS 当前只有 windows-x64 ⇒ 本次 Release 仍照发 x64,arm64 产物不进 Release"
|
||||||
@@ -16,6 +16,12 @@ name: Release guard
|
|||||||
# 为什么不并进 guards.yml:那份文件已经 398 行(400 行红线),再挂一个 job 必越线。
|
# 为什么不并进 guards.yml:那份文件已经 398 行(400 行红线),再挂一个 job 必越线。
|
||||||
# 改 `release.yml` 的 prerelease 输入 / 改那段 `gh release` 时,先跑
|
# 改 `release.yml` 的 prerelease 输入 / 改那段 `gh release` 时,先跑
|
||||||
# `python3 packaging/check_release_semantics.py`(本机可用 `python`,脚本会自己找 bash)。
|
# `python3 packaging/check_release_semantics.py`(本机可用 `python`,脚本会自己找 bash)。
|
||||||
|
#
|
||||||
|
# (M164)同一个 job 还挂**发布名单守卫** `packaging/check_publish_slots.py`:`PUBLISH_SLOTS` 与槽位表/产物
|
||||||
|
# 必须对得上。PR 阶段跑源码级断言(名单非空、每项都是 `release.yml` 槽位表里的 `mode: ci` 档)——
|
||||||
|
# 名单里写错一个字此前只会表现为「Release 侧一个也不匹配、齐全性检查红在别处」,根因要翻两层日志;
|
||||||
|
# `--self-test` 再注入 8 条回归(缺资产 / 拼错 / 声明了 probe 档 / 名单声明了没产物 / 有产物没声明)证明它会红。
|
||||||
|
# 发布侧那道「产物 ⇄ 名单」的运行时断言在 `release-publish.yml`(同一份脚本,`--dist`)。
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -38,3 +44,7 @@ jobs:
|
|||||||
run: python3 packaging/check_release_semantics.py
|
run: python3 packaging/check_release_semantics.py
|
||||||
- name: 变异自测(守门抓不抓得住回归)
|
- name: 变异自测(守门抓不抓得住回归)
|
||||||
run: python3 packaging/check_release_semantics.py --self-mutation
|
run: python3 packaging/check_release_semantics.py --self-mutation
|
||||||
|
- name: 发布名单守卫自测(声明⇒必须有产物 / 有产物没声明⇒显式警告)
|
||||||
|
run: python3 packaging/check_publish_slots.py --self-test
|
||||||
|
- name: 发布名单与矩阵槽位一致(拼错字 / 声明了 probe 档 当场红)
|
||||||
|
run: python3 packaging/check_publish_slots.py
|
||||||
@@ -4,6 +4,8 @@ name: Release / publish(可复用)
|
|||||||
# job **逐字搬家**:裁剪白名单外的槽位产物、重算全量 `SHA256SUMS.txt`、渲染 `packaging/release-notes.md` 模板、
|
# job **逐字搬家**:裁剪白名单外的槽位产物、重算全量 `SHA256SUMS.txt`、渲染 `packaging/release-notes.md` 模板、
|
||||||
# `gh release create/edit` 的同 tag 幂等,全部照旧。留在调用方 `release.yml` 的只有三件事:`needs: build`、
|
# `gh release create/edit` 的同 tag 幂等,全部照旧。留在调用方 `release.yml` 的只有三件事:`needs: build`、
|
||||||
# tag 门禁 `if: always() && !cancelled() && github.ref_type == 'tag'`、以及 `permissions: contents: write` 的授予。
|
# tag 门禁 `if: always() && !cancelled() && github.ref_type == 'tag'`、以及 `permissions: contents: write` 的授予。
|
||||||
|
# **M164 新增一步**(其余仍照旧):下载 artifact 之后、裁剪之前的「发布名单守卫」`packaging/check_publish_slots.py`
|
||||||
|
# —— 原来那份「名单内少一档就 fail」的内联循环搬进那个脚本,并补上反方向(有产物没声明 ⇒ `::warning::` 未发布)。
|
||||||
# 与拆分前的三处等价改写(都只涉及"从哪里读",不涉及行为):
|
# 与拆分前的三处等价改写(都只涉及"从哪里读",不涉及行为):
|
||||||
# ① `secrets.GITHUB_TOKEN` → 本文件声明的入参 secret `PUBLISH_TOKEN`(调用方透传 `secrets.GITHUB_TOKEN`)。
|
# ① `secrets.GITHUB_TOKEN` → 本文件声明的入参 secret `PUBLISH_TOKEN`(调用方透传 `secrets.GITHUB_TOKEN`)。
|
||||||
# 可复用工作流**拿不到**调用方未显式传入的 secret,写 `secrets.GITHUB_TOKEN` 会静默为空 → gh 401。
|
# 可复用工作流**拿不到**调用方未显式传入的 secret,写 `secrets.GITHUB_TOKEN` 会静默为空 → gh 401。
|
||||||
@@ -38,10 +40,17 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
env:
|
env:
|
||||||
# ── 发布白名单:唯一决定「哪几个槽位进 Release」的开关(空格分隔,键同 matrix.slot)──
|
# ── 发布白名单:唯一决定「哪几个槽位进 Release」的开关(空格分隔,键同 matrix.slot)──
|
||||||
# **M142 起只有 `windows-x64`**:arm64 连试四轮未走通,而下面齐全性检查是「名单内少一档就 fail」——名字挂着
|
# **M142 起只有 `windows-x64`**:arm64 连试四轮未走通,而齐全性检查是「名单内少一档就 fail」——名字挂着
|
||||||
# arm64 就等于 x64 编成功也发不出去。故把它移出名单:它照留在矩阵里跑取证(诊断注解是将来修它的唯一证据),
|
# arm64 就等于 x64 编成功也发不出去。故把它移出名单:它照留在矩阵里跑取证,**红了不挡发布**。
|
||||||
# **红了不挡发布**。**可逆**:arm64 修好后把键加回这一行即可(齐全性检查与 SHA256SUMS 自动跟着扩),再同步
|
# (M164)**为什么现在还只有 x64**:arm64 五轮全红其实都红在一个误判的编译前体检步(见 release-build.yml
|
||||||
# README §1/§9 与 release-notes.md 两张表。四档 Linux/macOS 仍未验证,默认连构建都不跑(M139)。
|
# 顶部那段),真正的 cargo/link 一次都没跑过;M164 把那个门禁拆了、判定权交回编译器,但「arm64 能不能编出
|
||||||
|
# PE」只能由**下一 tag 的 CI** 证明,本机没有 ARM64 真机/工具链证不了。在拿到那次绿之前把 arm64 写进来,
|
||||||
|
# 等于拿 x64 的发布去赌它 —— 同一个事故不演第二遍(tower M164 裁决)。
|
||||||
|
# **可逆,且只是一行**:那句 CI 真出包之后把键加回这一行(`"windows-x64 windows-arm64"`),齐全性检查与
|
||||||
|
# SHA256SUMS 自动跟着扩,再同步 README §1/§9 与 release-notes.md 两张表。
|
||||||
|
# 判据不再靠注释保证:`packaging/check_publish_slots.py`(下面第二个 step 跑它;PR 阶段由 release-guard.yml
|
||||||
|
# 跑同一份脚本)两个方向都查——声明了缺产物 ⇒ `::error::` + exit 1;有产物但没声明 ⇒ `::warning::` 明说未发布。
|
||||||
|
# 四档 Linux/macOS 仍未验证,默认连构建都不跑(M139)。
|
||||||
PUBLISH_SLOTS: "windows-x64"
|
PUBLISH_SLOTS: "windows-x64"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -51,6 +60,17 @@ jobs:
|
|||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
|
# (M164)发布名单守卫:**声明了就必须有产物**(名单内某个槽位缺 GUI 或 CLI 就红,少一档不发版),
|
||||||
|
# 反方向的「产物在、名单没声明」则打 `::warning::` 明说未发布 —— M142 之后那个方向是**静默**的(裁剪步只
|
||||||
|
# echo 一行 `drop:`,没有注解、也没有非零退出),一个真编出来的 arm64 会被就地 `rm` 掉而没人知道。
|
||||||
|
# 判据全在 `packaging/check_publish_slots.py`:同一份代码本地可跑,`--self-test` 注入 8 条回归断言每条都红。
|
||||||
|
# 它跑在**裁剪前**的全量集上(裁剪只删名单外的文件),所以是原来「裁剪后再查」那次检查的严格超集。
|
||||||
|
- name: 发布名单守卫(声明 ⇒ 必须有产物;有产物没声明 ⇒ 显式未发布)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 packaging/check_publish_slots.py --dist dist --slots "$PUBLISH_SLOTS"
|
||||||
|
|
||||||
- name: 裁掉白名单外的槽位产物(构建取证 ≠ 发布)
|
- name: 裁掉白名单外的槽位产物(构建取证 ≠ 发布)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -67,30 +87,20 @@ jobs:
|
|||||||
echo "publish: $base"
|
echo "publish: $base"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "drop: $base($platform_arch 不在发布白名单:四档未验证 / arm64 待修;仍留在 dist-<slot> artifact 里取证)"
|
echo "drop: $base($platform_arch 不在发布白名单:四档未验证 / arm64 待下一 tag 验证;仍留在 dist-<slot> artifact 里取证)"
|
||||||
rm -f "$f"
|
rm -f "$f"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: 发布白名单齐全性 + 重算全量 SHA256SUMS
|
- name: 重算全量 SHA256SUMS
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ls -l dist
|
ls -l dist
|
||||||
# 白名单内槽位必须**全齐**才发版(宁可不出 Release,也不出少一档的 Release)。每个槽位都要 GUI 与 CLI 两个
|
# 齐全性(名单内每个槽位 GUI 与 CLI 都要在)**已由上上步的「发布名单守卫」断言**,且那一步看的是裁剪前的
|
||||||
# 资产——只按「槽位名出现过」判会被其中一个掩盖;比较用下划线形态(资产名自 D49 起是 `..._windows_x64.exe`)。
|
# 全量集 —— 同一套规则只留一份实现,免得两处判据漂开。这里只负责重算摘要:各槽位各写一份 SHA256SUMS.txt,
|
||||||
# M142 后白名单只剩 x64 一档:arm64 缺产物不再拦发布,但 x64 自己缺 GUI 或 CLI 照样在这里 fail。
|
# 合并后只有一份是真的,故在 Release 侧按已发布的资产重算。
|
||||||
for slot in $PUBLISH_SLOTS; do
|
|
||||||
stem=${slot//-/_}
|
|
||||||
for prefix in unlucky-launcher ull; do
|
|
||||||
if ! compgen -G "dist/${prefix}_*_${stem}*" >/dev/null; then
|
|
||||||
echo "::error::发布白名单缺产物:$prefix / $slot"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
# 各槽位各写一份 SHA256SUMS.txt,合并后只有一份是真的 —— 在 Release 侧按已发布的资产重算。
|
|
||||||
cd dist
|
cd dist
|
||||||
rm -f SHA256SUMS.txt
|
rm -f SHA256SUMS.txt
|
||||||
sha256sum unlucky-launcher_* ull_* > SHA256SUMS.txt
|
sha256sum unlucky-launcher_* ull_* > SHA256SUMS.txt
|
||||||
|
|||||||
@@ -3,19 +3,25 @@ name: Release
|
|||||||
# 发布流水线:tag `v*` → **只构建两个 Windows 槽位**(各自原生 `windows-latest` / `windows-11-arm`)→ 单文件产物 +
|
# 发布流水线:tag `v*` → **只构建两个 Windows 槽位**(各自原生 `windows-latest` / `windows-11-arm`)→ 单文件产物 +
|
||||||
# SHA256SUMS → artifact + GitHub Release。**push tag 默认创建稳定版**(M163:不带 `--prerelease`,否则
|
# SHA256SUMS → artifact + GitHub Release。**push tag 默认创建稳定版**(M163:不带 `--prerelease`,否则
|
||||||
# `releases/latest` 永远 404、自更新通道取不到清单);同 tag 重跑幂等(覆盖资产与说明、不 create、不碰标记)。
|
# `releases/latest` 永远 404、自更新通道取不到清单);同 tag 重跑幂等(覆盖资产与说明、不 create、不碰标记)。
|
||||||
# **构建 ≠ 发布**:只有 `PUBLISH_SLOTS` 里的槽位才进 Release 与 SHA256SUMS ——**M142 起只有 `windows-x64`**(arm64 连试
|
# **构建 ≠ 发布**:只有 `PUBLISH_SLOTS` 里的槽位才进 Release 与 SHA256SUMS ——**M142 起只有 `windows-x64`**(arm64 仍留
|
||||||
# 四轮未成,仍留矩阵里取证、红了不挡 x64 发布;它修好后把键加回那一行即可)。四档 Linux/macOS 从未验证,默认连构建都
|
# 矩阵里取证、红了不挡 x64 发布)。**M164 的更正**:arm64 那五轮红其实全红在一个误判的编译前体检步(cargo/link 一次
|
||||||
# 不跑(M139:省配额、不留红叉);整表六槽位取证走 `gh workflow run release.yml -f all_slots=true`(槽位表在 `plan`)。
|
# 都没跑过,见 `release-build.yml` 顶部),该门禁已拆、判定权交回编译器;但「arm64 能不能出 PE」只能由**下一 tag 的
|
||||||
|
# CI** 证明,故名单仍只列 x64(tower 裁决:不拿 x64 的发布去赌一个没验证过的槽位)。真出包之后把键加回那一行即可。
|
||||||
|
# 四档 Linux/macOS 从未验证,默认连构建都不跑(M139:省配额、不留红叉);整表六槽位取证走
|
||||||
|
# `gh workflow run release.yml -f all_slots=true`(槽位表在 `plan`)。
|
||||||
|
# 名单 ⇄ 产物两个方向都有守卫(M164):`packaging/check_publish_slots.py`(PR 阶段在 release-guard.yml,发布侧在
|
||||||
|
# release-publish.yml 的 `--dist` 模式)——声明了缺产物 ⇒ 红;有产物没声明 ⇒ `::warning::` 明说未发布。
|
||||||
# 契约唯一真相源 packaging/README.md,打包本体 packaging/make_artifacts.py(本地与 CI 同一份脚本)。`inputs.prerelease`
|
# 契约唯一真相源 packaging/README.md,打包本体 packaging/make_artifacts.py(本地与 CI 同一份脚本)。`inputs.prerelease`
|
||||||
# 只在 **tag ref** 上 dispatch 才可达(跑 tag 得 `gh workflow run release.yml --ref v0.2.1`);分支 ref 的 dispatch 与
|
# 只在 **tag ref** 上 dispatch 才可达(跑 tag 得 `gh workflow run release.yml --ref v0.2.1`);分支 ref 的 dispatch 与
|
||||||
# push 一样被 release job 的 `if: github.ref_type == 'tag'` 挡在发布之外。
|
# push 一样被 release job 的 `if: github.ref_type == 'tag'` 挡在发布之外。
|
||||||
#
|
#
|
||||||
# M145 拆分(本文件与 packaging/README.md 都已顶到 400 行红线,下一处改动必越线):本文件只留**触发 + 槽位表 + 编排**,
|
# M145 拆分(本文件与 packaging/README.md 都已顶到 400 行红线,下一处改动必越线):本文件只留**触发 + 槽位表 + 编排**,
|
||||||
# 两个 job 的实体各搬一份可复用工作流(`workflow_call`),步骤逐字未改:
|
# 两个 job 的实体各搬一份可复用工作流(`workflow_call`),步骤逐字未改:
|
||||||
# - `.github/workflows/release-build.yml` = 原 `build` job 全流程(Linux 依赖 / arm64 工具链体检 / cargo build /
|
# - `.github/workflows/release-build.yml` = 原 `build` job 全流程(Linux 依赖 / arm64 工具链取证(M164 起只取证)/
|
||||||
# 静态 CRT 导入表门禁 / 打包 / 归档自检 / 上传 artifact / arm64 收尾注解),矩阵键换成同名 `inputs.*`。
|
# cargo build / 静态 CRT 导入表门禁 / 打包 / 归档自检 / 上传 artifact / arm64 收尾注解),矩阵键换成同名 `inputs.*`。
|
||||||
# - `.github/workflows/release-publish.yml` = 原 `release` job 全流程(`PUBLISH_SLOTS` 白名单 / 裁剪 / 重算
|
# - `.github/workflows/release-publish.yml` = 原 `release` job 全流程(`PUBLISH_SLOTS` 白名单 / 裁剪 / 重算
|
||||||
# SHA256SUMS / 渲染说明 / `gh release` 幂等),`secrets.GITHUB_TOKEN` 改为显式透传的 secret。
|
# SHA256SUMS / 渲染说明 / `gh release` 幂等),`secrets.GITHUB_TOKEN` 改为显式透传的 secret。
|
||||||
|
# (M164 在裁剪前插了一步「发布名单守卫」,故它是 7 步而不是搬家时的 6 步。)
|
||||||
# `on` / `env` / `permissions` / `concurrency` 与 `plan` 的槽位表**一字未动**。
|
# `on` / `env` / `permissions` / `concurrency` 与 `plan` 的槽位表**一字未动**。
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -72,9 +78,11 @@ jobs:
|
|||||||
SLOTS = [
|
SLOTS = [
|
||||||
# Windows x64:MSVC + 静态 CRT(+crt-static)→ exe 不依赖 VC++ 可再发行包,双击即用的单文件。
|
# Windows x64:MSVC + 静态 CRT(+crt-static)→ exe 不依赖 VC++ 可再发行包,双击即用的单文件。
|
||||||
dict(slot="windows-x64", os="windows-latest", build_dir="target/release", rustflags="-C target-feature=+crt-static", mode="ci"),
|
dict(slot="windows-x64", os="windows-latest", build_dir="target/release", rustflags="-C target-feature=+crt-static", mode="ci"),
|
||||||
# Windows arm64:**原生** `windows-11-arm`(4C/16G,公开仓库免费;GitHub 文档的 Standard runners 也列
|
# Windows arm64:**原生** `windows-11-arm`(4C/16G,公开仓库免费)。该标签自 2026-09 起是
|
||||||
# `windows-11-vs2026-arm`)。M140 前是在 windows-latest 上交叉,**CI 从未成功过一次**(v0.2.1 首跑红在
|
# **Windows 11 Arm64 + Visual Studio 2026** 镜像(VS 在 `C:\Program Files\Microsoft Visual Studio\18`),
|
||||||
# 工具链体检,M141/M142 在此修)。原生还让 `ull --version` 冒烟真跑,dumpbin 走 Hostarm64 分支。
|
# 镜像清单里 `VC.Tools.ARM64` 与 `Windows11SDK.22621/26100` 都装好了。M140 前是在 windows-latest 上交叉;
|
||||||
|
# v0.2.1~v0.2.3 的红全红在 M141/M142 那个**编译前体检**步(cargo 永远 skipped),M164 已把它降级为只取证。
|
||||||
|
# 原生还让 `ull --version` 冒烟真跑,dumpbin 走 Hostarm64 分支。
|
||||||
dict(slot="windows-arm64", os="windows-11-arm", build_dir="target/release",
|
dict(slot="windows-arm64", os="windows-11-arm", build_dir="target/release",
|
||||||
rustflags="-C target-feature=+crt-static", mode="ci"),
|
rustflags="-C target-feature=+crt-static", mode="ci"),
|
||||||
# ↓ 四档**未验证,不纳入 CI**:从未真构建成功、也没在真机跑过,默认一个都不编;`all_slots=true` 时照编
|
# ↓ 四档**未验证,不纳入 CI**:从未真构建成功、也没在真机跑过,默认一个都不编;`all_slots=true` 时照编
|
||||||
|
|||||||
@@ -264,10 +264,14 @@ Grab the file for your platform from
|
|||||||
**This release publishes `windows-x64` and nothing else.** The publish job keeps only the slots named
|
**This release publishes `windows-x64` and nothing else.** The publish job keeps only the slots named
|
||||||
in `PUBLISH_SLOTS` (`.github/workflows/release-publish.yml`) and deletes every other artifact before
|
in `PUBLISH_SLOTS` (`.github/workflows/release-publish.yml`) and deletes every other artifact before
|
||||||
it writes `SHA256SUMS.txt`, so a row marked **not yet published** is a naming rule, not a downloadable
|
it writes `SHA256SUMS.txt`, so a row marked **not yet published** is a naming rule, not a downloadable
|
||||||
asset. Of those rows `windows-arm64` is the one CI still builds — natively, on GitHub's
|
asset. A guard (`packaging/check_publish_slots.py`) watches both directions: a slot named in the list
|
||||||
`windows-11-arm` runner, for evidence — but that slot's toolchain check has not gone green yet, so its
|
whose artifacts are missing turns the release red, and artifacts that exist for an unlisted slot get an
|
||||||
`.exe` stays out of the release. The row stays because the architecture is still supported; it rejoins
|
explicit "not published" warning instead of being deleted quietly. Of those rows `windows-arm64` is the
|
||||||
the download list once the build is green.
|
one CI still builds — natively, on GitHub's `windows-11-arm` runner (Windows 11 Arm64 + Visual Studio
|
||||||
|
2026), for evidence. Every ARM64 failure so far came from a pre-build toolchain probe that aborted before
|
||||||
|
`cargo build` ran; that probe is now evidence-only, so an upcoming CI run is the first real answer to
|
||||||
|
whether the slot can produce a binary. Its `.exe` stays out of the release until then; the row stays
|
||||||
|
because the architecture is still supported.
|
||||||
|
|
||||||
Each UNIX archive contains the executable plus `LICENSE`, `NOTICE` and `README.md`, in one top-level
|
Each UNIX archive contains the executable plus `LICENSE`, `NOTICE` and `README.md`, in one top-level
|
||||||
directory. Windows ships a bare `.exe` (a single file cannot carry the texts), so those three
|
directory. Windows ships a bare `.exe` (a single file cannot carry the texts), so those three
|
||||||
|
|||||||
+6
-3
@@ -212,9 +212,12 @@ unlucky-launcher --version # CLI,版本 + 系统摘要
|
|||||||
|
|
||||||
**本版只发布 `windows-x64`。** 发布 job 只留 `PUBLISH_SLOTS` 里点名的槽位
|
**本版只发布 `windows-x64`。** 发布 job 只留 `PUBLISH_SLOTS` 里点名的槽位
|
||||||
(`.github/workflows/release-publish.yml`),其余产物在写 `SHA256SUMS.txt` 之前就被删掉——所以表里标
|
(`.github/workflows/release-publish.yml`),其余产物在写 `SHA256SUMS.txt` 之前就被删掉——所以表里标
|
||||||
**暂未发布** 的行是命名规则,不是本版可下载的资产。这几行里 `windows-arm64` 是 CI 唯一真跑的那档:M140 起在
|
**暂未发布** 的行是命名规则,不是本版可下载的资产。两个方向都有守卫(`packaging/check_publish_slots.py`):
|
||||||
GitHub 原生 `windows-11-arm` runner 上编来取证,但该槽位的工具链体检还没绿,所以它的 `.exe` 不进 Release;
|
名单里点名了却没产物 ⇒ Release 变红;产物在、名单没点名 ⇒ 显式打「未发布」警告,不再静默删掉。
|
||||||
行不删是因为架构仍要支持,绿了就把键加回 `PUBLISH_SLOTS`。
|
这几行里 `windows-arm64` 是 CI 唯一真跑的那档:在 GitHub 原生 `windows-11-arm`(Windows 11 Arm64 +
|
||||||
|
Visual Studio 2026)runner 上编来取证。**诚实一句**:此前每一次 arm64 红都红在**编译前的工具链体检**,
|
||||||
|
`cargo build` 一次都没跑过;M164 把那个体检降级成只取证,所以**下一条 tag 的 CI 才是第一次真正问编译器**。
|
||||||
|
在那之前它的 `.exe` 不进 Release;行不删是因为架构仍要支持,真出包了就把键加回 `PUBLISH_SLOTS`(一行)。
|
||||||
|
|
||||||
UNIX 归档里是二进制加 `LICENSE`、`NOTICE`、`README.md`,都在一个顶层目录下。Windows 发的是裸 `.exe`
|
UNIX 归档里是二进制加 `LICENSE`、`NOTICE`、`README.md`,都在一个顶层目录下。Windows 发的是裸 `.exe`
|
||||||
(一个文件装不下这三份文本),所以这三份作为并列资产挂在 Release 上——下 Windows 二进制不会把许可条款
|
(一个文件装不下这三份文本),所以这三份作为并列资产挂在 Release 上——下 Windows 二进制不会把许可条款
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)
|
|||||||
| `packaging/make_artifacts.py` | 打包与校验(本地 + CI 共用) | ≤400 |
|
| `packaging/make_artifacts.py` | 打包与校验(本地 + CI 共用) | ≤400 |
|
||||||
| `packaging/release-notes.md` | GitHub Release 说明模板(`{{VERSION}}` / `{{SUMS}}` 占位) | ≤400 |
|
| `packaging/release-notes.md` | GitHub Release 说明模板(`{{VERSION}}` / `{{SUMS}}` 占位) | ≤400 |
|
||||||
| `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤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/ci-runtime-smoke.md` | §11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 |
|
||||||
| `packaging/linux-smoke-docker.sh` | 本地复跑 `runtime-smoke`(Docker + xvfb) | ≤400 |
|
| `packaging/linux-smoke-docker.sh` | 本地复跑 `runtime-smoke`(Docker + xvfb) | ≤400 |
|
||||||
| `packaging/linux-check-docker.sh` | 本地复跑 `ci.yml` 的两条 ubuntu 门禁(clippy / test,Docker) | ≤400 |
|
| `packaging/linux-check-docker.sh` | 本地复跑 `ci.yml` 的两条 ubuntu 门禁(clippy / test,Docker) | ≤400 |
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""发布名单守卫(M164):**名单声明与实际产物必须对得上**,两个方向都管。
|
||||||
|
|
||||||
|
起因:`PUBLISH_SLOTS` 是唯一决定「哪几个槽位进 Release」的开关,而它此前只有一半是守住的 ——
|
||||||
|
「名单里有、产物没有」会被 `release-publish.yml` 的齐全性检查拦下,但反方向**静默**:某个槽位(例如
|
||||||
|
`windows-arm64`)真编出了产物、名单却还没声明它时,那些文件会在裁剪步被 `rm` 掉,日志里只有一行
|
||||||
|
`drop:`,没有任何注解、也没有任何非零退出码。修复的那个 tag 里「arm64 其实已经能出包了」这件事就
|
||||||
|
这么被吞掉,没人会注意到(M164 的任务清单点名要这条)。
|
||||||
|
|
||||||
|
四条断言:
|
||||||
|
|
||||||
|
1. **名单非空且可解析**:`release-publish.yml` 里必须有 `PUBLISH_SLOTS: "..."`。开关消失时不要静默
|
||||||
|
变成「一个都不发」。
|
||||||
|
2. **名单里每一项都是矩阵槽位**:`release.yml` 的槽位表(`dict(slot=... )`)才是槽位的唯一定义。
|
||||||
|
名单里写错一个字(`windows-aarch64`)此前只会表现为「Release 侧什么也不匹配、齐全性检查红在别处」,
|
||||||
|
根因要翻两层日志;这条在 PR 阶段就把它点出来。
|
||||||
|
3. **名单里每一项的 `mode` 都必须是 `ci`**:`mode: probe` 的槽位默认构建根本不跑,写进发布名单等于
|
||||||
|
每一轮都缺一档 —— 一个自己写死要红的组合。
|
||||||
|
4. **产物 ⇄ 名单两个方向**(`--dist`,只在发布侧有产物时跑):
|
||||||
|
- 名单声明了但没有 GUI 或 CLI 产物 ⇒ `::error::` + exit 1(少一档不发版,这条现在是脚本、本地可跑);
|
||||||
|
- 有产物但名单没声明 ⇒ `::warning::` 明说「未发布、会被裁掉」,**不许静默**;
|
||||||
|
- 无论哪种,最后都打一张全槽位表:发布 / 未发布(无产物)/ 未发布(名单没声明)。
|
||||||
|
|
||||||
|
用法(零第三方依赖,本地与 CI 同一份代码):
|
||||||
|
|
||||||
|
python3 packaging/check_publish_slots.py # 源码级断言(PR CI)
|
||||||
|
python3 packaging/check_publish_slots.py --dist dist --slots "$PUBLISH_SLOTS"
|
||||||
|
python3 packaging/check_publish_slots.py --self-test # 注入回归,断言每条都变红
|
||||||
|
|
||||||
|
`--self-test` 是这条守门的自测证据:把两个 workflow 复制进临时目录、造合成 dist,逐项注入真实回归
|
||||||
|
(漏一个资产 / 名单拼错 / 名单写了 probe 档 / 名单声明了但产物缺 / 名单没声明却有产物),断言守门
|
||||||
|
该红的红、该只警告的不红。守门抓不住回归时它自己先红。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PACKAGING = Path(__file__).resolve().parent
|
||||||
|
ROOT = PACKAGING.parent
|
||||||
|
|
||||||
|
MATRIX_YML = Path(".github/workflows/release.yml")
|
||||||
|
PUBLISH_YML = Path(".github/workflows/release-publish.yml")
|
||||||
|
|
||||||
|
#: 槽位表条目(`plan` job 里那份):`dict(slot="windows-x64", ..., mode="ci")`,一条可能跨两行。
|
||||||
|
SLOT_RE = re.compile(r'dict\(\s*slot="(?P<slot>[^"]+)"(?P<rest>.*?)\)', re.S)
|
||||||
|
MODE_RE = re.compile(r'mode="(?P<mode>[^"]+)"')
|
||||||
|
#: 发布白名单那一行(yaml 里是 `PUBLISH_SLOTS: "windows-x64"`)。
|
||||||
|
PUBLISH_RE = re.compile(r'^\s*PUBLISH_SLOTS:\s*"(?P<value>[^"]*)"', re.M)
|
||||||
|
|
||||||
|
#: 每个槽位必须**两样都有**:GUI 与 CLI 各自一个资产(只按「槽位名出现过」判会被其中一个掩盖)。
|
||||||
|
ASSET_PREFIXES = (("unlucky-launcher", "GUI"), ("ull", "CLI"))
|
||||||
|
|
||||||
|
|
||||||
|
def stem(slot: str) -> str:
|
||||||
|
"""槽位键落到文件名里是下划线形态(`windows-arm64` → `windows_arm64`)。"""
|
||||||
|
return slot.replace("-", "_")
|
||||||
|
|
||||||
|
|
||||||
|
# ────────────────────────── 读两个 workflow ──────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def matrix_slots(text: str) -> dict[str, str]:
|
||||||
|
"""`release.yml` 的槽位表 → {槽位: mode}。表在 `plan` job 里,是槽位的唯一定义。"""
|
||||||
|
slots: dict[str, str] = {}
|
||||||
|
for match in SLOT_RE.finditer(text):
|
||||||
|
mode = MODE_RE.search(match.group("rest"))
|
||||||
|
slots[match.group("slot")] = mode.group("mode") if mode else ""
|
||||||
|
return slots
|
||||||
|
|
||||||
|
|
||||||
|
def declared_slots(text: str) -> list[str] | None:
|
||||||
|
"""`release-publish.yml` 的 `PUBLISH_SLOTS` → 槽位列表;整行不见了返回 `None`(区别于「空名单」)。"""
|
||||||
|
match = PUBLISH_RE.search(text)
|
||||||
|
return None if match is None else match.group("value").split()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_slots_arg(raw: str) -> list[str]:
|
||||||
|
"""`--slots "windows-x64 windows-arm64"` 的取值解析(CI 直接透传 `$PUBLISH_SLOTS`)。"""
|
||||||
|
return raw.replace(",", " ").split()
|
||||||
|
|
||||||
|
|
||||||
|
# ────────────────────────── 1~3. 源码级断言 ──────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def check_source(root: Path, expected: list[str] | None = None) -> list[str]:
|
||||||
|
problems: list[str] = []
|
||||||
|
matrix_path = root / MATRIX_YML
|
||||||
|
publish_path = root / PUBLISH_YML
|
||||||
|
for path in (matrix_path, publish_path):
|
||||||
|
if not path.is_file():
|
||||||
|
return [f"找不到 {path.relative_to(root)}(流水线搬家了?这条守门要跟着搬)"]
|
||||||
|
|
||||||
|
matrix = matrix_slots(matrix_path.read_text(encoding="utf-8"))
|
||||||
|
if not matrix:
|
||||||
|
return [f"{MATRIX_YML} 里读不到任何槽位(`dict(slot=...)` 的写法变了?槽位表是槽位的唯一定义)"]
|
||||||
|
|
||||||
|
declared = declared_slots(publish_path.read_text(encoding="utf-8"))
|
||||||
|
if declared is None:
|
||||||
|
return [f"{PUBLISH_YML} 里没有 `PUBLISH_SLOTS:` 这一行:发布白名单消失了(别让它静默变成「一个都不发」)"]
|
||||||
|
if not declared:
|
||||||
|
problems.append("`PUBLISH_SLOTS` 是空名单:任何一个槽位都不会进 Release 与 SHA256SUMS")
|
||||||
|
if expected is not None and expected != declared:
|
||||||
|
# 运行期实际用的名单必须就是文件里那一行(有人在步内覆盖 PUBLISH_SLOTS 时就对不上)。
|
||||||
|
problems.append(f"运行期名单 {expected} 与 {PUBLISH_YML} 里声明的 {declared} 不一致")
|
||||||
|
|
||||||
|
for slot in declared:
|
||||||
|
if slot not in matrix:
|
||||||
|
problems.append(
|
||||||
|
f"名单里的 `{slot}` 不是矩阵槽位(矩阵只有 {' / '.join(matrix)}):"
|
||||||
|
"写错一个字,裁剪步就一个也匹配不到,而齐全性检查会红在别处、根因埋两层"
|
||||||
|
)
|
||||||
|
elif matrix[slot] != "ci":
|
||||||
|
problems.append(
|
||||||
|
f"名单里的 `{slot}` 在矩阵里是 mode={matrix[slot]!r}(默认不构建):"
|
||||||
|
"声明为发布却默认不编,等于每一轮都缺一档"
|
||||||
|
)
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
# ────────────────────────── 4. 产物 ⇄ 名单(发布侧) ──────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def assets_for(dist: Path, slot: str) -> dict[str, list[str]]:
|
||||||
|
"""{资产前缀: 命中的文件名}——按 `make_artifacts.py::asset_stem()` 的命名规则找。"""
|
||||||
|
out: dict[str, list[str]] = {}
|
||||||
|
for prefix, _role in ASSET_PREFIXES:
|
||||||
|
out[prefix] = sorted(p.name for p in dist.glob(f"{prefix}_*_{stem(slot)}*") if p.is_file())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def check_dist(dist: Path, declared: list[str], matrix: dict[str, str]) -> tuple[list[str], list[str], list[str]]:
|
||||||
|
"""返回 (errors, warnings, 表格行)。errors 非空 ⇒ 调用方 exit 1。"""
|
||||||
|
errors: list[str] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
rows: list[str] = ["槽位 名单 本次产物 结论"]
|
||||||
|
rows.append("-" * 78)
|
||||||
|
wanted = set(declared)
|
||||||
|
|
||||||
|
# 顺序:先按矩阵顺序(对外口径稳定),再有名单里多出来的(check_source 已负责报它是错字)。
|
||||||
|
order = list(matrix) + [s for s in declared if s not in matrix]
|
||||||
|
for slot in order:
|
||||||
|
found = assets_for(dist, slot)
|
||||||
|
present = [f"{prefix}({len(found[prefix])})" for prefix, _ in ASSET_PREFIXES if found[prefix]]
|
||||||
|
is_declared = slot in wanted
|
||||||
|
if is_declared:
|
||||||
|
missing = [role for prefix, role in ASSET_PREFIXES if not found[prefix]]
|
||||||
|
for role in missing:
|
||||||
|
errors.append(f"发布白名单缺产物:{slot} 的 {role}(`{prefix_for(role)}_*_{stem(slot)}*`)")
|
||||||
|
verdict = "发布" if not missing else "缺产物 ⇒ 不发版"
|
||||||
|
wanted.discard(slot)
|
||||||
|
elif present:
|
||||||
|
# **反方向**:真编出来了却没声明。裁剪步会把它 rm 掉,此前这里一句注解都没有。
|
||||||
|
warnings.append(
|
||||||
|
f"{slot} 有产物({'、'.join(present)})但不在 PUBLISH_SLOTS 里:本次不发布,产物会被裁掉。"
|
||||||
|
"若这是修复后的首次成功构建,把槽位加进名单即可(齐全性检查与 SHA256SUMS 自动跟着扩)"
|
||||||
|
)
|
||||||
|
verdict = "未发布(名单没声明)"
|
||||||
|
else:
|
||||||
|
verdict = "未发布(本次没有产物)"
|
||||||
|
rows.append(f"{slot:<20} {'声明' if is_declared else '未声明':<8} {'、'.join(present) or '无':<15} {verdict}")
|
||||||
|
|
||||||
|
for slot in sorted(wanted):
|
||||||
|
errors.append(f"发布白名单里的 `{slot}` 既不是矩阵槽位、也没有产物(详见上面的源码级断言)")
|
||||||
|
return errors, warnings, rows
|
||||||
|
|
||||||
|
|
||||||
|
def prefix_for(role: str) -> str:
|
||||||
|
return next(prefix for prefix, name in ASSET_PREFIXES if name == role)
|
||||||
|
|
||||||
|
|
||||||
|
# ────────────────────────── 汇总 ──────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def run_checks(root: Path, dist: Path | None = None, expected: list[str] | None = None) -> tuple[list[str], list[str], list[str]]:
|
||||||
|
problems = check_source(root, expected)
|
||||||
|
if dist is None or problems:
|
||||||
|
return problems, [], []
|
||||||
|
matrix = matrix_slots((root / MATRIX_YML).read_text(encoding="utf-8"))
|
||||||
|
declared = declared_slots((root / PUBLISH_YML).read_text(encoding="utf-8")) or []
|
||||||
|
errors, warnings, rows = check_dist(dist, declared, matrix)
|
||||||
|
return problems + errors, warnings, rows
|
||||||
|
|
||||||
|
|
||||||
|
def report(root: Path, dist: Path | None, expected: list[str] | None = None) -> int:
|
||||||
|
errors, warnings, rows = run_checks(root, dist, expected)
|
||||||
|
for row in rows:
|
||||||
|
print(row)
|
||||||
|
for item in warnings:
|
||||||
|
print(f"::warning::{item}")
|
||||||
|
# 表格之后再看结论:注解必须排在它解释的那张表后面,人读日志的顺序才对得上。
|
||||||
|
for item in errors:
|
||||||
|
print(f"::error::{item}", file=sys.stderr)
|
||||||
|
if errors:
|
||||||
|
print(f"发布名单守卫:{len(errors)} 条不通过", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"OK: 发布名单守卫全绿({len(warnings)} 条「未发布」提示)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# ────────────────────────── 自测:注入回归,断言每条都红 ──────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot(dst: Path) -> None:
|
||||||
|
for rel in (MATRIX_YML, PUBLISH_YML):
|
||||||
|
target = dst / rel
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(ROOT / rel, target)
|
||||||
|
|
||||||
|
|
||||||
|
def fake_dist(parent: Path, slots: tuple[str, ...]) -> Path:
|
||||||
|
"""合成 dist:每槽位一个 GUI + 一个 CLI 资产(名字形状与 `make_artifacts.py` 一致)。"""
|
||||||
|
dist = parent / "dist"
|
||||||
|
dist.mkdir(parents=True, exist_ok=True)
|
||||||
|
version = "0.0.0"
|
||||||
|
for slot in slots:
|
||||||
|
platform, arch = slot.split("-", 1)
|
||||||
|
suffix = ".exe" if platform == "windows" else ".tar.gz"
|
||||||
|
(dist / f"unlucky-launcher_{version}_{platform}_{arch}{suffix}").write_bytes(b"gui")
|
||||||
|
(dist / f"ull_{version}_{platform}_{arch}{suffix}").write_bytes(b"cli")
|
||||||
|
return dist
|
||||||
|
|
||||||
|
|
||||||
|
def set_declared(root: Path, value: str) -> None:
|
||||||
|
path = root / PUBLISH_YML
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
replaced, count = PUBLISH_RE.subn(f'PUBLISH_SLOTS: "{value}"', text)
|
||||||
|
if count != 1:
|
||||||
|
raise SystemExit(f"::error::自测自身失效:{PUBLISH_YML} 里找到 {count} 处 PUBLISH_SLOTS")
|
||||||
|
path.write_text(replaced, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def self_test() -> int:
|
||||||
|
failures = 0
|
||||||
|
with tempfile.TemporaryDirectory(prefix="ull-publish-slots-") as tmp:
|
||||||
|
base = Path(tmp) / "base"
|
||||||
|
snapshot(base)
|
||||||
|
declared = declared_slots((base / PUBLISH_YML).read_text(encoding="utf-8")) or []
|
||||||
|
if not declared:
|
||||||
|
print("::error::自测的基线读不出 PUBLISH_SLOTS", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
cases: list[tuple[str, Path, Path | None, list[str] | None, bool]] = []
|
||||||
|
|
||||||
|
# 0. 基线:名单齐全、产物齐全 ⇒ 绿。
|
||||||
|
cases.append(("基线(名单与产物都齐)", base, fake_dist(Path(tmp) / "c0", tuple(declared)), None, False))
|
||||||
|
|
||||||
|
# 1. 名单声明了,但缺 CLI 资产 ⇒ 必须红(这条就是「少一档不发版」)。
|
||||||
|
dist = fake_dist(Path(tmp) / "c1", tuple(declared))
|
||||||
|
for stale in dist.glob("ull_*"):
|
||||||
|
stale.unlink()
|
||||||
|
cases.append(("名单声明了但缺 CLI 产物", base, dist, None, True))
|
||||||
|
|
||||||
|
# 2. 名单声明了,但整个槽位没有产物 ⇒ 必须红。
|
||||||
|
cases.append(("名单声明了但整个槽位没产物", base, fake_dist(Path(tmp) / "c2", ()), None, True))
|
||||||
|
|
||||||
|
# 3. 有产物、名单没声明 ⇒ 只警告不红(反方向的静默必须被打破,但发布本身没错)。
|
||||||
|
# 名单内的槽位照旧齐全,只是多出一档没人声明的产物。
|
||||||
|
cases.append(("产物在但名单没声明(arm64 修复后的典型场景)", base, fake_dist(Path(tmp) / "c3", (*declared, "windows-arm64")), None, False))
|
||||||
|
|
||||||
|
# 4. 名单拼错一个字 ⇒ 必须红(此前要翻两层日志才能看出根因)。
|
||||||
|
typo = Path(tmp) / "c4"
|
||||||
|
snapshot(typo)
|
||||||
|
set_declared(typo, "windows-aarch64")
|
||||||
|
cases.append(("名单拼错成 windows-aarch64", typo, fake_dist(Path(tmp) / "c4d", ("windows-aarch64",)), None, True))
|
||||||
|
|
||||||
|
# 5. 名单写了 probe 档(默认不构建)⇒ 必须红。
|
||||||
|
probe = Path(tmp) / "c5"
|
||||||
|
snapshot(probe)
|
||||||
|
set_declared(probe, "windows-x64 linux-x64")
|
||||||
|
cases.append(("名单写了 mode=probe 的槽位", probe, fake_dist(Path(tmp) / "c5d", ("windows-x64", "linux-x64")), None, True))
|
||||||
|
|
||||||
|
# 6. 运行期名单与文件里那一行不一致 ⇒ 必须红(防步内覆盖)。
|
||||||
|
cases.append(("运行期名单与文件不一致", base, fake_dist(Path(tmp) / "c6", tuple(declared)), ["windows-arm64"], True))
|
||||||
|
|
||||||
|
# 7. 「把 arm64 加回名单」这一行改动本身是干净的:名单+产物都对得上 ⇒ 绿。
|
||||||
|
flip = Path(tmp) / "c7"
|
||||||
|
snapshot(flip)
|
||||||
|
set_declared(flip, " ".join([*declared, "windows-arm64"]))
|
||||||
|
cases.append(("名单加上 windows-arm64 且产物齐全(下一 tag 的翻牌预演)", flip, fake_dist(Path(tmp) / "c7d", (*declared, "windows-arm64")), None, False))
|
||||||
|
|
||||||
|
for label, root, dist, expected, want_red in cases:
|
||||||
|
errors, warnings, _rows = run_checks(root, dist, expected)
|
||||||
|
got_red = bool(errors)
|
||||||
|
if got_red != want_red:
|
||||||
|
want = "红" if want_red else "绿"
|
||||||
|
detail = ";".join(errors) or "(无 error)"
|
||||||
|
print(f"::error::自测失败:{label} 应该{want},实际 {'红' if got_red else '绿'} —— {detail}", file=sys.stderr)
|
||||||
|
failures += 1
|
||||||
|
else:
|
||||||
|
print(f"OK: {label} → {'红' if got_red else '绿'}({len(warnings)} 条未发布提示)")
|
||||||
|
|
||||||
|
# 第 3 条还要断言**确实打了警告**:不静默才是这条规则的重点。
|
||||||
|
_e, warnings, _r = run_checks(base, fake_dist(Path(tmp) / "c8", (*declared, "windows-arm64")), None)
|
||||||
|
if not any("windows-arm64" in w for w in warnings):
|
||||||
|
print("::error::自测失败:产物在但名单没声明时没有针对 windows-arm64 的 ::warning::", file=sys.stderr)
|
||||||
|
failures += 1
|
||||||
|
else:
|
||||||
|
print("OK: 产物在但名单没声明 → 有 windows-arm64 的 ::warning::")
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
return 1
|
||||||
|
print("OK: 发布名单守卫自测全部通过(该红的红、该绿的不红)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
for stream in (sys.stdout, sys.stderr):
|
||||||
|
if hasattr(stream, "reconfigure"):
|
||||||
|
stream.reconfigure(errors="backslashreplace")
|
||||||
|
|
||||||
|
root = ROOT
|
||||||
|
dist: Path | None = None
|
||||||
|
expected: list[str] | None = None
|
||||||
|
args = list(argv)
|
||||||
|
while args:
|
||||||
|
arg = args.pop(0)
|
||||||
|
if arg == "--root" and args:
|
||||||
|
root = Path(args.pop(0)).resolve()
|
||||||
|
elif arg == "--dist" and args:
|
||||||
|
dist = Path(args.pop(0)).resolve()
|
||||||
|
elif arg == "--slots" and args:
|
||||||
|
expected = parse_slots_arg(args.pop(0))
|
||||||
|
elif arg == "--self-test":
|
||||||
|
return self_test()
|
||||||
|
elif arg in ("-h", "--help"):
|
||||||
|
print(__doc__)
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print(f"::error::不认识的参数:{arg}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if dist is not None and not dist.is_dir():
|
||||||
|
print(f"::error::--dist {dist} 不是目录(下载 artifact 那一步没跑成?)", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return report(root, dist, expected)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
| 槽位 | 本版发布 | 产物文件名 | 形态 | 归档内容 |
|
| 槽位 | 本版发布 | 产物文件名 | 形态 | 归档内容 |
|
||||||
|------|---------|-----------|------|---------|
|
|------|---------|-----------|------|---------|
|
||||||
| `windows-x64` | ✅ 发布 | `unlucky-launcher_<ver>_windows_x64.exe` | **裸 exe**(双击即用) | exe 单文件;三份合规文本作为并列 Release 资产 |
|
| `windows-x64` | ✅ 发布 | `unlucky-launcher_<ver>_windows_x64.exe` | **裸 exe**(双击即用) | exe 单文件;三份合规文本作为并列 Release 资产 |
|
||||||
| `windows-arm64` | ❌ 不发布(构建照跑取证) | `unlucky-launcher_<ver>_windows_arm64.exe` | **裸 exe**(同 x64 策略) | 同上;CI 仍在**原生** `windows-11-arm` runner 上构建(M140 起不交叉),**红了不挡 x64 发布**(M142,见 §3) |
|
| `windows-arm64` | ❌ 不发布(构建取证中) | `unlucky-launcher_<ver>_windows_arm64.exe` | **裸 exe**(同 x64 策略) | 同上;CI 在**原生** `windows-11-arm`(Windows 11 Arm64 + VS2026 镜像)上构建,**M164 起编译前体检只取证、不再判定红绿**(此前五轮全红其实全红在那个误判的体检步),**红了不挡 x64 发布**(M142,见 §3) |
|
||||||
| `linux-x64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_linux_x64.tar.gz` | 单二进制归档 | `unlucky-launcher` + `LICENSE` + `NOTICE` + `README.md` |
|
| `linux-x64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_linux_x64.tar.gz` | 单二进制归档 | `unlucky-launcher` + `LICENSE` + `NOTICE` + `README.md` |
|
||||||
| `linux-arm64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_linux_arm64.tar.gz` | 单二进制归档(原生 ARM64 runner 构建) | 同上 |
|
| `linux-arm64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_linux_arm64.tar.gz` | 单二进制归档(原生 ARM64 runner 构建) | 同上 |
|
||||||
| `macos-arm64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_macos_arm64.tar.gz` | 单二进制归档 | 同上 |
|
| `macos-arm64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_macos_arm64.tar.gz` | 单二进制归档 | 同上 |
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
- **「不构建」= CI 默认根本不跑**(M139):那四档未验证,产物不给用户、构建配额也不花;要整表六槽位取证走
|
- **「不构建」= CI 默认根本不跑**(M139):那四档未验证,产物不给用户、构建配额也不花;要整表六槽位取证走
|
||||||
`gh workflow run release.yml -f all_slots=true`(§3)。发布白名单**只有 `windows-x64`**(M142:arm64 照构建、不发布)。
|
`gh workflow run release.yml -f all_slots=true`(§3)。发布白名单**只有 `windows-x64`**(M142:arm64 照构建、不发布)。
|
||||||
|
(M164)名单 ⇄ 产物两个方向都有守卫 `packaging/check_publish_slots.py`:声明了缺产物 ⇒ 红;有产物但没声明 ⇒
|
||||||
|
`::warning::` 明说「未发布、会被裁掉」——arm64 真出包之后把键加回名单是一行改动,守卫与 `SHA256SUMS.txt` 自动跟着扩。
|
||||||
- **产物命名**:`<名字>_<版本号>_<平台>_<架构>`,下划线分段、**不带实现细节字样**(不出现 `iced`);真相源是 `make_artifacts.py::asset_stem()`。
|
- **产物命名**:`<名字>_<版本号>_<平台>_<架构>`,下划线分段、**不带实现细节字样**(不出现 `iced`);真相源是 `make_artifacts.py::asset_stem()`。
|
||||||
- **"单文件"指可执行文件本身零运行时依赖**(无 WebView、无 JRE、无 sidecar、无 DLL 旁挂);归档里的三份合规文本是许可义务、不是运行时依赖,Windows 因为"一个 exe 装不下文本",
|
- **"单文件"指可执行文件本身零运行时依赖**(无 WebView、无 JRE、无 sidecar、无 DLL 旁挂);归档里的三份合规文本是许可义务、不是运行时依赖,Windows 因为"一个 exe 装不下文本",
|
||||||
把 `LICENSE` / `NOTICE` / `README.md` 作为 Release 的并列资产分发,条款同样随下载走。
|
把 `LICENSE` / `NOTICE` / `README.md` 作为 Release 的并列资产分发,条款同样随下载走。
|
||||||
|
|||||||
@@ -50,17 +50,29 @@ python packaging/make_artifacts.py --slot windows-arm64 \
|
|||||||
`-C target-feature=+crt-static`、产物都在 `target/release`,标 `mode: ci`(默认编);
|
`-C target-feature=+crt-static`、产物都在 `target/release`,标 `mode: ci`(默认编);
|
||||||
`ubuntu-22.04`(glibc 2.35 地板)/ `ubuntu-22.04-arm`(原生 ARM64,同地板)/ `macos-latest` /
|
`ubuntu-22.04`(glibc 2.35 地板)/ `ubuntu-22.04-arm`(原生 ARM64,同地板)/ `macos-latest` /
|
||||||
`macos-15-intel` 标 `mode: probe`——**未验证,默认一个都不编**,只有 `all_slots=true` 才进矩阵。
|
`macos-15-intel` 标 `mode: probe`——**未验证,默认一个都不编**,只有 `all_slots=true` 才进矩阵。
|
||||||
- 每个槽位 `cargo build --release --locked`(**不传 `--target`**:每槽原生编译,交叉槽位与 `rustup
|
- 每个槽位 `cargo build --release --locked`(**不传 `--target`**:每槽原生编译,交叉槽位没了)。arm64 槽位在编译前
|
||||||
target add` 一并没了)。arm64 槽位多一步编译前体检:查 VC 的 `lib/arm64/libcmt.lib`(`+crt-static`
|
**把工具链显式备齐**(M164):`rustup target add aarch64-pc-windows-msvc`、用微软自己的 `vcvarsall.bat arm64` 取
|
||||||
靠它)与 SDK 的 `um/arm64/kernel32.lib`,缺了就 fail;**注解里固定打 4 行实况**(探测根 / SDK Lib 版本→um
|
`LIB`/`INCLUDE`(写进本步环境与 `$GITHUB_ENV`)、用 vswhere 定位 arm64 `link.exe` 后显式导出
|
||||||
架构 / MSVC lib 架构 / 两个关键库)。M142 修的正是「诊断自己先死」:`set -uo pipefail` 关不掉 runner 注入的
|
`CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER`(Hostarm64 原生优先)。顺序不能反:只覆盖链接器而不给 `LIB`,
|
||||||
`-e`,`find` 扫不存在的根被 pipefail 判非零就当场退出 → 现在 `set +e` + 处处 `|| true`,清单先打完再定红绿。
|
撞上的就是 `LNK1181: cannot open input file 'kernel32.lib'`。
|
||||||
|
- **编译前体检已降级为「只取证」**(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 起只有
|
- **发布白名单(`PUBLISH_SLOTS`,唯一开关)**:只有白名单里的槽位进 Release 与 `SHA256SUMS.txt`。**M142 起只有
|
||||||
`windows-x64`**:arm64 连试四轮未走通,而齐全性检查是「名单内少一档就 fail」,挂着它就等于 x64 编成功也发不出去;
|
`windows-x64`**:齐全性检查是「名单内少一档就 fail」,挂着没验证过的 arm64 就等于 x64 编成功也发不出去。
|
||||||
它仍留在矩阵里照跑取证、红了不挡发布。**可逆**:arm64 修好后把键加回 `release.yml` 那一行,再同步 §1/§9 与
|
(M164)arm64 那五轮红其实全红在误判的体检步(上一条),门禁拆掉后「arm64 能不能出 PE」只能由**下一 tag 的 CI**
|
||||||
`release-notes.md`。四档未验证的(默认连构建都不跑)依旧不推给用户。
|
证明,所以名单这轮仍只列 x64 —— 不拿 x64 的发布去赌(tower M164 裁决)。**可逆且只是一行**:那次 CI 真出包后把
|
||||||
- `release` job:下载全部 artifact → **按白名单裁剪** → 重算 `SHA256SUMS.txt` → 用 `packaging/release-notes.md`
|
键加回 `release-publish.yml` 那一行,再同步 §1/§9 与 `release-notes.md`。四档未验证的(默认连构建都不跑)依旧不推给用户。
|
||||||
模板渲染支持矩阵 → 生成更新清单 `manifest.json` → `gh release create`(**push tag 默认建稳定版**:不带任何标记)。
|
- **名单 ⇄ 产物两个方向都有守卫(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 条回归,本地一条命令可跑。
|
||||||
|
- `release` job:下载全部 artifact → **发布名单守卫**(断言 + 未发布提示)→ 按白名单裁剪 → 重算 `SHA256SUMS.txt` →
|
||||||
|
用 `packaging/release-notes.md` 模板渲染支持矩阵 → 生成更新清单 `manifest.json` → `gh release create`
|
||||||
|
(**push tag 默认建稳定版**:不带任何标记)。
|
||||||
- **同一 tag 重跑幂等**:`gh release view` 命中已存在的 Release 时改走 `gh release upload --clobber`
|
- **同一 tag 重跑幂等**:`gh release view` 命中已存在的 Release 时改走 `gh release upload --clobber`
|
||||||
+ `gh release edit`,覆盖资产与说明而不是再 `create`(`create` 遇已存在会失败,把流水线最后一步染红)。
|
+ `gh release edit`,覆盖资产与说明而不是再 `create`(`create` 遇已存在会失败,把流水线最后一步染红)。
|
||||||
- **预发布标记的语义表(M163,唯一真相源 `packaging/release_marker.py`)**:
|
- **预发布标记的语义表(M163,唯一真相源 `packaging/release_marker.py`)**:
|
||||||
@@ -87,8 +99,10 @@ python packaging/make_artifacts.py --slot windows-arm64 \
|
|||||||
`.github/workflows/release-build.yml`(`workflow_call`,四个矩阵键换成同名 `inputs.*`),`release` job 的 6 步
|
`.github/workflows/release-build.yml`(`workflow_call`,四个矩阵键换成同名 `inputs.*`),`release` job 的 6 步
|
||||||
逐字搬进 `.github/workflows/release-publish.yml`(同 `workflow_call`,`secrets.GITHUB_TOKEN` 改为调用方显式透传
|
逐字搬进 `.github/workflows/release-publish.yml`(同 `workflow_call`,`secrets.GITHUB_TOKEN` 改为调用方显式透传
|
||||||
的 secret)。触发(`on`)、`env`、顶层 `permissions`、`concurrency` 与 `plan` 的槽位表在 M145 搬家时**一字未动**;
|
的 secret)。触发(`on`)、`env`、顶层 `permissions`、`concurrency` 与 `plan` 的槽位表在 M145 搬家时**一字未动**;
|
||||||
后来只有 M163 动过 `on` 里 `prerelease` 的默认值(`true` → `false`,见上面那张语义表)。改槽位仍只改
|
后来只有 M163 动过 `on` 里 `prerelease` 的默认值(`true` → `false`,见上面那张语义表),M164 改过
|
||||||
`release.yml` 那一处(`PUBLISH_SLOTS` 白名单在 `release-publish.yml`,两个 arm64 修复时要改的仍是那两行)。
|
`release-build.yml` 的 arm64 体检步与 `cargo build` 步、并在 `release-publish.yml` 裁剪前插了「发布名单守卫」
|
||||||
|
(于是它现在是 7 步)。改槽位仍只改 `release.yml` 那一处(`PUBLISH_SLOTS` 白名单在 `release-publish.yml`,
|
||||||
|
翻牌时要改的仍是那两处)。
|
||||||
- **M149 微软登录 id 的烘焙**:`release-build.yml` 的 `cargo build` 步带 `env.ULL_MSA_CLIENT_ID`(值
|
- **M149 微软登录 id 的烘焙**:`release-build.yml` 的 `cargo build` 步带 `env.ULL_MSA_CLIENT_ID`(值
|
||||||
`${{ secrets.ULL_MSA_CLIENT_ID }}`),`option_env!` 把它烘进二进制,于是发布产物装完即可登录。id **不进
|
`${{ secrets.ULL_MSA_CLIENT_ID }}`),`option_env!` 把它烘进二进制,于是发布产物装完即可登录。id **不进
|
||||||
git 文件**:真值放仓库 Secret,由 `release.yml` 的 `build` job 按名显式透传(可复用工作流拿不到未显式传入的
|
git 文件**:真值放仓库 Secret,由 `release.yml` 的 `build` job 按名显式透传(可复用工作流拿不到未显式传入的
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
| 平台 | 本版发布 | 级别 | 运行时要求 |
|
| 平台 | 本版发布 | 级别 | 运行时要求 |
|
||||||
|------|---------|------|-----------|
|
|------|---------|------|-----------|
|
||||||
| Windows 10/11 x64 | ✅ | 一等 | 无(UCRT 系统自带) |
|
| Windows 10/11 x64 | ✅ | 一等 | 无(UCRT 系统自带) |
|
||||||
| Windows 11 ARM64 | ❌ 不发布(构建取证中,待修) | 一等(启动器产物) | 无额外库;启动器原生运行,游戏本体走系统 x64 模拟层 + x64 JDK |
|
| Windows 11 ARM64 | ❌ 不发布(构建取证中) | 一等(启动器产物) | 无额外库;启动器原生运行,游戏本体走系统 x64 模拟层 + x64 JDK |
|
||||||
| macOS 12+(arm64 / x64) | ❌ 不构建 | 一等(未真机验证) | 无第三方库;未签名需去 quarantine |
|
| macOS 12+(arm64 / x64) | ❌ 不构建 | 一等(未真机验证) | 无第三方库;未签名需去 quarantine |
|
||||||
| Linux x64 X11 | ❌ 不构建 | 一等(未真机验证) | glibc 2.35+、xkbcommon、fontconfig、libGL、xcb |
|
| Linux x64 X11 | ❌ 不构建 | 一等(未真机验证) | glibc 2.35+、xkbcommon、fontconfig、libGL、xcb |
|
||||||
| Linux x64 Wayland | ❌ 不构建 | 支持 | 上述 + wayland-client |
|
| Linux x64 Wayland | ❌ 不构建 | 支持 | 上述 + wayland-client |
|
||||||
@@ -34,7 +34,12 @@
|
|||||||
published 一列,Termux 行标注 experimental)——改一边必须同步另一边,否则标题里的"一致"就是假话。
|
published 一列,Termux 行标注 experimental)——改一边必须同步另一边,否则标题里的"一致"就是假话。
|
||||||
|
|
||||||
两个 arm64 槽位都是**正式矩阵槽位**(不是 Termux 那种源码自建旁路),但本版两个都**不发布**:`windows-arm64` 照构建取证
|
两个 arm64 槽位都是**正式矩阵槽位**(不是 Termux 那种源码自建旁路),但本版两个都**不发布**:`windows-arm64` 照构建取证
|
||||||
(红了不挡 x64 发布),`linux-arm64` 连构建都不跑;发布开关是 `release.yml` 的 `PUBLISH_SLOTS`(当前只有 `windows-x64`)。
|
(红了不挡 x64 发布),`linux-arm64` 连构建都不跑;发布开关是 `release-publish.yml` 的 `PUBLISH_SLOTS`(当前只有 `windows-x64`)。
|
||||||
|
|
||||||
|
(M164)`windows-arm64` 为什么还没进名单:它 v0.2.1~v0.2.3 的红**全部**红在一个编译前体检步(`find` 猜 SDK 的
|
||||||
|
`um/arm64/kernel32.lib`),`cargo build` 一次都没跑过;M164 把那个门禁降级为只取证、判定权交回编译器,但「能不能
|
||||||
|
出 PE」只能由**下一 tag 的 CI** 证明。名单 ⇄ 产物的两个方向现在都有守卫(`packaging/check_publish_slots.py`):
|
||||||
|
声明了缺产物 ⇒ 红,有产物没声明 ⇒ `::warning::` 明说未发布 —— 所以那次 CI 真出包之后,翻一行名单即可。
|
||||||
|
|
||||||
## 10. 已知边界 / 后续
|
## 10. 已知边界 / 后续
|
||||||
|
|
||||||
|
|||||||
@@ -120,5 +120,9 @@ libxcb 条目数 = 0,只把 fontconfig 那 7 个 `.so` 放进 `LD_LIBRARY_PATH
|
|||||||
|
|
||||||
**没在本地验证、只能由 CI 补的**:`windows-arm64` 在 `windows-11-arm` 上的真实链接与 ARM64 导入表、
|
**没在本地验证、只能由 CI 补的**:`windows-arm64` 在 `windows-11-arm` 上的真实链接与 ARM64 导入表、
|
||||||
`linux-arm64` 在原生 arm64 runner 上的真跑(本机既没有 ARM 机器,也连 `raw.githubusercontent.com` 都解析不了)。
|
`linux-arm64` 在原生 arm64 runner 上的真跑(本机既没有 ARM 机器,也连 `raw.githubusercontent.com` 都解析不了)。
|
||||||
M141/M142 保留了编译前体检(查 VC 的 `lib/arm64/libcmt.lib` 与 SDK 的 `um/arm64/kernel32.lib`,无论齐不齐都先打 4 行
|
**M164 的更正**:M141/M142 那个编译前体检不但没帮上忙,还是 arm64 五轮全红的唯一原因(`find` 猜 SDK 的
|
||||||
实况注解)。闭环标准不是「两槽位都进 SHA256SUMS」(M142 起只有 x64 进),而是 arm64 注解转绿 + 把键加回 `PUBLISH_SLOTS`。
|
`um/arm64/kernel32.lib`,猜缺就 `exit 1`,而 `cargo build` 永远 skipped);M164 把它降级为只取证(打印 vswhere 组件、
|
||||||
|
目录 `.lib` 计数、`link.exe` 候选),红绿交回编译器。闭环标准不是「两槽位都进 SHA256SUMS」(M142 起只有 x64 进),
|
||||||
|
而是 arm64 的 `cargo build` 真跑绿 + 把键加回 `PUBLISH_SLOTS`(守卫会自动跟着扩,并在产物有了却没声明时报 `::warning::`)。
|
||||||
|
本机能补的只有脚本层证据:`python packaging/check_publish_slots.py --self-test`(8 条注入回归)与
|
||||||
|
`cargo check --target aarch64-pc-windows-msvc`(依赖图对该 target 的类型检查,**不链接、不产出 PE**)。
|
||||||
+16
-10
@@ -33,11 +33,16 @@ Asset names are `<name>_<version>_<platform>_<arch>`, underscore separated — t
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Windows 10/11 x64 | `unlucky-launcher_{{VERSION}}_windows_x64.exe` | single exe, double-click to run; this is also what you run on ARM64 Windows (x64 emulation) |
|
| Windows 10/11 x64 | `unlucky-launcher_{{VERSION}}_windows_x64.exe` | single exe, double-click to run; this is also what you run on ARM64 Windows (x64 emulation) |
|
||||||
|
|
||||||
**Windows ARM64 is not part of this release.** CI still builds it on a native `windows-11-arm` runner for
|
**Windows ARM64 is not part of this release.** CI still builds it on a native `windows-11-arm` runner (Windows 11
|
||||||
evidence, but that slot's toolchain check has not gone green yet, so
|
Arm64 with Visual Studio 2026), but the build has not delivered an artifact yet, so
|
||||||
`unlucky-launcher_{{VERSION}}_windows_arm64.exe` is deliberately kept out of the download list and out of
|
`unlucky-launcher_{{VERSION}}_windows_arm64.exe` is deliberately kept out of the download list and out of
|
||||||
`SHA256SUMS.txt` instead of handing out a build nobody has verified. `windows-x64` is the only published
|
`SHA256SUMS.txt` instead of handing out a build nobody has verified. Worth stating plainly: every ARM64 failure
|
||||||
slot; ARM64 rejoins this table as soon as its build is green (one line in the workflow's `PUBLISH_SLOTS`).
|
so far happened in a **pre-build toolchain probe** that guessed whether the SDK's ARM64 import libraries were
|
||||||
|
present and aborted before `cargo build` ever ran — the compiler and linker have not been heard from yet. That
|
||||||
|
probe is now evidence-only (it prints what it sees and never decides the job), so the next CI run is the first
|
||||||
|
one that can actually answer the question. `windows-x64` is the only published slot; ARM64 rejoins this table as
|
||||||
|
soon as a run produces its artifact (one line in the workflow's `PUBLISH_SLOTS`, guarded on both sides by
|
||||||
|
`packaging/check_publish_slots.py`).
|
||||||
|
|
||||||
**This release ships Windows only, and CI builds Windows only.** The Linux (x64 / arm64) and macOS
|
**This release ships Windows only, and CI builds Windows only.** The Linux (x64 / arm64) and macOS
|
||||||
(Apple Silicon / Intel) slots are not built by the release workflow at all — they have not been verified
|
(Apple Silicon / Intel) slots are not built by the release workflow at all — they have not been verified
|
||||||
@@ -79,7 +84,7 @@ Published = shipped as a download of **this** release, listed in `SHA256SUMS.txt
|
|||||||
| Platform | Status in this release | Level | Runtime requirements |
|
| Platform | Status in this release | Level | Runtime requirements |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Windows 10/11 x64 | **published** | first class | nothing beyond the OS (UCRT is part of Win10+) |
|
| Windows 10/11 x64 | **published** | first class | nothing beyond the OS (UCRT is part of Win10+) |
|
||||||
| Windows 11 ARM64 | not published (build chasing, unverified) | first class (launcher artifact) | nothing extra; the launcher runs natively, the game itself runs through the OS x64 emulation layer with an x64 JDK |
|
| Windows 11 ARM64 | not published (build evidence only) | first class (launcher artifact) | nothing extra; the launcher runs natively, the game itself runs through the OS x64 emulation layer with an x64 JDK |
|
||||||
| macOS 12+ Apple Silicon | not built (unverified) | first class (unverified) | nothing; unsigned build, needs the Gatekeeper workaround |
|
| macOS 12+ Apple Silicon | not built (unverified) | first class (unverified) | nothing; unsigned build, needs the Gatekeeper workaround |
|
||||||
| macOS 12+ Intel | not built (unverified) | first class (unverified) | same as above |
|
| macOS 12+ Intel | not built (unverified) | first class (unverified) | same as above |
|
||||||
| Linux x64, X11 | not built (unverified) | first class (unverified) | `libxkbcommon`, `fontconfig`, `libGL` (or software renderer), `libxcb` |
|
| Linux x64, X11 | not built (unverified) | first class (unverified) | `libxkbcommon`, `fontconfig`, `libGL` (or software renderer), `libxcb` |
|
||||||
@@ -89,11 +94,12 @@ Published = shipped as a download of **this** release, listed in `SHA256SUMS.txt
|
|||||||
| Windows 8.1/8/7 | not built | unsupported | the baseline is Windows 10 |
|
| Windows 8.1/8/7 | not built | unsupported | the baseline is Windows 10 |
|
||||||
| Android — Termux + X11 | not built (no CI slot) | experimental, not promised | CLI and GUI from source only; GUI verified on-device twice: CPU software rendering and self-built Mesa/turnip GPU acceleration (see `packaging/docs/termux-glibc-build.md` and `README.termux.md`) |
|
| Android — Termux + X11 | not built (no CI slot) | experimental, not promised | CLI and GUI from source only; GUI verified on-device twice: CPU software rendering and self-built Mesa/turnip GPU acceleration (see `packaging/docs/termux-glibc-build.md` and `README.termux.md`) |
|
||||||
|
|
||||||
`windows-x64` is the only published slot this release; Windows ARM64 is kept out until its build is green
|
`windows-x64` is the only published slot this release; Windows ARM64 stays out until a CI run actually produces
|
||||||
(see Downloads above). Both arm64 rows are formal matrix slots, not the experimental Termux path, and
|
its artifact (see Downloads above — so far every ARM64 failure was the pre-build probe, never the compiler).
|
||||||
Linux arm64 is not built by default — someone has to run it on real hardware (via a manual all-slots run)
|
Both arm64 rows are formal matrix slots, not the experimental Termux path, and Linux arm64 is not built by
|
||||||
before it joins this list. The rows above and the matrix table in `packaging/README.md` §9 are the same
|
default — someone has to run it on real hardware (via a manual all-slots run) before it joins this list. The
|
||||||
list — change one, change the other.
|
rows above and the matrix table in `packaging/docs/compliance-support-boundaries.md` §9 are the same list —
|
||||||
|
change one, change the other.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user