diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index f4e633d..d081786 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -1,9 +1,11 @@ name: Release / build(可复用) # 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.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` 里那一处。 # `env` 必须在被调方重复声明:**被调工作流不继承调用方的 workflow 级 `env`**(CARGO_TERM_COLOR / RUST_BACKTRACE @@ -67,64 +69,56 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - # (M141/M142)windows-arm64 跑在**原生** `windows-11-arm` 上,本步查这台机器上 ARM64 库齐不齐:`+crt-static` - # 要 MSVC 的 `lib/arm64/libcmt.lib`,链接要 SDK 的 `um/arm64/kernel32.lib`。诊断**只走注解**(job 日志下载要 - # token 我们没有,check-run 注解不用;每条一行,多行会被拼成一行截断,每步 error 注解上限 10 条)。 - # **M142 修本步的自毁 bug**:`shell: bash` 实为 `bash -eo pipefail`,脚本里的 `set -uo pipefail` **关不掉** -e—— - # M141 死在 `vc_lib=$(find 不存在的根 | head -n1)`(pipefail 把 find 的非零带进赋值、errexit 当场退出), - # **一条注解都没打**,上一轮只剩 `Process completed with exit code 1.`。四条规矩:① 显式 `set +e`;② 每处探测 - # 自带 `|| true`;③ 问题记进 ERRORS 而不当场退出;④ 快照先打完、再由显式 `exit` 定红绿。门禁不降级(缺了就 - # 就地补装再判),但 **arm64 红不挡发布**(PUBLISH_SLOTS 已无它),本槽位只取证。 - - name: Windows ARM64 工具链体检(红不挡发布,只取证) + # (M141/M142 → **M164 判定权移交**)windows-arm64 跑在**原生** `windows-11-arm` 上。本步**只取证,不判定**: + # M141/M142 的版本用 `find` 猜 SDK 的 `um/arm64/kernel32.lib` 在不在,猜缺就 `exit 1` —— 结果 v0.2.1~v0.2.3 + # 五轮全红在**这一步**、下面 `cargo build` 一次都没跑过(第 7 步永远 skipped),"arm64 修不好"始终是探针给的 + # 结论、不是编译器给的。而它跟自己打的快照自相矛盾:v0.2.3 的注解原文是 + # `[arm64sdk] SDK Lib 版本→um 架构: 10.0.22621.0→um:[arm,arm64,x64,x86,] 10.0.26100.0→um:[arm64,x64,x86,]` + # 同一份注解里却报 `kernel32=缺失`(同一个 `find` 模式在本地合成目录树上命中,故这条路本身不可靠)。 + # M164 把红绿交回编译器与链接器:本步只打印实况(vswhere 组件查询 / 目录里 .lib **计数** / arm64 link.exe + # 路径),一条 `::error::` 都不写、永不 `exit 1`;红不红由 `cargo build` 与静态 CRT 导入表门禁决定。 + # 也不再自动补装: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' shell: bash run: | - set +e; set -uo pipefail; ERRORS=() # -e 由 runner 命令行注入,只能显式关 - say() { printf '::%s::[arm64sdk] %s\n' "$1" "$2"; [ "$1" = error ] && ERRORS+=("$2"); return 0; } - vs=("C:/Program Files/Microsoft Visual Studio" "C:/Program Files (x86)/Microsoft Visual Studio") - # SDK 根要放宽:新版装 `(x86)`、也有装 `Program Files`;老安装器把 lib 留在 Microsoft SDKs; - # VS 自己还带一份「SDK in VS」(`/Windows Kits/10/Lib`),用 vswhere 定位后追加。 - sdk=("C:/Program Files (x86)/Windows Kits/10/Lib" "C:/Program Files/Windows Kits/10/Lib" - "C:/Program Files (x86)/Microsoft SDKs/Windows/v10.0A/Lib") - inst="C:/Program Files (x86)/Microsoft Visual Studio/Installer"; vspath="" - [ -x "$inst/vswhere.exe" ] && vspath=$("$inst/vswhere.exe" -latest -products '*' -property installationPath 2>/dev/null | head -n1 || true) - [ -n "$vspath" ] && sdk+=("${vspath//\\//}/Windows Kits/10/Lib") - # 下面两个探测正是 M141 断气处(find 起点不存在 → 非 0 → `| head` 的 pipefail 带进赋值 → errexit),故处处 - # `|| true`:探不到就是「没找到」,由结论下判断,不许由它决定进程生死。 - scan() { vc_lib=$(find "${vs[@]}" -maxdepth 11 -type f -path '*/VC/Tools/MSVC/*/lib/arm64/libcmt.lib' 2>/dev/null | head -n1 || true) - 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 + set +e; set -uo pipefail # -e 由 runner 命令行注入,只能显式关 + say() { printf '::%s::[arm64sdk] %s\n' "$1" "$2"; return 0; } + shopt -s nullglob # 计数走 glob 不走 find:find 的管道 + errexit 正是 M141 的死因 + vswhere="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" + [ -x "$vswhere" ] || say warning "找不到 vswhere($vswhere):下面会是 0 命中,那不代表工具链缺东西" + # ① 组件:用 vswhere 自己的 `-requires`(权威查询),不拿「某个文件名存在」当替身。 + for comp in Microsoft.VisualStudio.Component.VC.Tools.ARM64 \ + Microsoft.VisualStudio.Component.Windows11SDK.26100 \ + Microsoft.VisualStudio.Component.Windows11SDK.22621; do + hit="" + [ -x "$vswhere" ] && hit=$("$vswhere" -latest -products '*' -requires "$comp" -property installationPath 2>/dev/null | head -n1 || true) + if [ -n "$hit" ]; then say notice "组件 $comp: 已装($hit)" + else say warning "组件 $comp: 未装 / 这台 VS 不认这个 id(继续看下面的实际 .lib 计数)"; fi done - say "$sev" "SDK Lib 版本→um 架构: ${v:-<没有任何版本目录>}(SDK build=${build:-未探到})" - v="" - while IFS= read -r t; do v+="$(basename "$(dirname "$t")")→[$(ls "$t" 2>/dev/null | tr '\n' ',')] "; done \ - < <(find "${vs[@]}" -maxdepth 7 -type d -path '*/VC/Tools/MSVC/*/lib' 2>/dev/null | head -n4) - say "$sev" "MSVC lib/ 架构: ${v:-<无 MSVC lib 目录>}" - say "$sev" "关键库: libcmt=${vc_lib:-缺失} kernel32=${sdk_lib:-缺失}" - if [ "$sev" = notice ]; then echo "ARM64 工具链齐: vc=$vc_lib sdk=$sdk_lib"; exit 0; fi - # 补装:VS 安装器就地 modify(门禁不降级:先补再判)。组件 ID 随 VS 大版本不同;**优先补一个尚未装上的 - # SDK 版本**——同版本重装是 no-op,补不出 arm64 子目录。补装失败不致命,重探与结论给真相。 - id=Microsoft.VisualStudio.Component.Windows11SDK.$([ "$build" = "26100" ] && echo 22621 || echo 26100) - [ "$build" = "19041" ] && id=Microsoft.VisualStudio.Component.Windows10SDK.20348 - adds=""; [ -n "$vc_lib" ] || adds+="Microsoft.VisualStudio.Component.VC.Tools.ARM64 "; [ -n "$sdk_lib" ] || adds+="$id" - if [ -x "$inst/setup.exe" ] && [ -n "$vspath" ]; then - say error "补装: $vspath ← ${adds}(已装 SDK build=${build:-未装})" - timeout 1200 "$inst/setup.exe" modify --installPath "$vspath" --quiet --norestart --nocache --wait \ - --add ${adds} || say error "补装返回非 0:组件 ID 可能不被这台 VS 认(见下一条结论)" - scan - else say error "补装跳过: 无 vs_installer 或没定位到 VS 安装路径(vspath=${vspath:-<空>})"; fi - if [ -z "$vc_lib" ] || [ -z "$sdk_lib" ]; then - say error "结论: 仍缺(libcmt=${vc_lib:-缺失} kernel32=${sdk_lib:-缺失}),共记 ${#ERRORS[@]} 条探测问题;门禁红、不摘 arm64——本槽位不在 PUBLISH_SLOTS 里,x64 照发" - exit 1 - fi - echo "补装后齐全: vc=$vc_lib sdk=$sdk_lib" + # ② 实况计数:`+crt-static` 要 VC 的 `lib/arm64/libcmt.lib`,链接要 SDK 的 `um/arm64/*.lib`。 + for root in "C:/Program Files/Microsoft Visual Studio" "C:/Program Files (x86)/Microsoft Visual Studio"; do + hits=( "$root"/*/*/VC/Tools/MSVC/*/lib/arm64/libcmt.lib ) + say notice "VC $root → lib/arm64/libcmt.lib ${#hits[@]} 个${hits[0]:+(首个 ${hits[0]})}" + done + for root in "C:/Program Files (x86)/Windows Kits/10/Lib" "C:/Program Files/Windows Kits/10/Lib"; do + [ -d "$root" ] || { say notice "SDK $root:不存在"; continue; } + for ver in "$root"/*/; do + libs=( "$ver"um/arm64/*.lib ) + say notice "SDK $(basename "$ver") → um/arm64/*.lib ${#libs[@]} 个" + done + done + # ③ arm64 链接器候选:宿主 arm64 的优先(原生),x64 托管的次之。下面 cargo 步会显式导出它。 + for p in 'VC/Tools/MSVC/*/bin/Hostarm64/arm64/link.exe' 'VC/Tools/MSVC/*/bin/Hostx64/arm64/link.exe'; do + hit="" + [ -x "$vswhere" ] && hit=$("$vswhere" -latest -products '*' -find "$p" 2>/dev/null | head -n1 || true) + say notice "link.exe 候选 $p → ${hit:-未命中}" + done + echo "取证完毕:本步不判红;arm64 的结论只看下面的 cargo build / 链接 / 导入表门禁" # M118(D26):**Slint 版停发** —— GUI 只有 `unlucky-launcher-iced`(`crates/app` 已 exclude,不进 workspace)。 # 产物名由 make_artifacts.py 的 `asset_stem()` 生成 `<名字>_<版本号>_<平台>_<架构>`;不传 `--target`(M140 起全原生)。 @@ -132,14 +126,87 @@ jobs: # 官方产物自带微软登录所需的配置,装完即可登录;运行期同名环境变量仍优先于它,可覆盖。 # 值来自仓库 Secret(不进 git 文件);Secret 没配时这里是空串,client_id.rs 把空白当「没配」, # 于是非官方构建不烘 id。想自己烘一份就从源码构建:`ULL_MSA_CLIENT_ID= cargo build`。 + # (M164)arm64 槽位在编译前把工具链**显式**备齐,不再依赖「runner 反正自带」这种默契: + # ① `rustup target add aarch64-pc-windows-msvc`:宿主机本来就是 aarch64-pc-windows-msvc,这句在原生 + # runner 上是幂等 no-op;它把「缺 std」这类失败挡在编译前(缺 target 时 rustc 只会说找不到 crate std); + # ② MSVC 的 `LIB` / `INCLUDE` 用微软自己的 `vcvarsall.bat arm64` 取(**不手搓路径**:少一个目录就是 + # LNK1181),本步 `export` 一次、同时写进 `$GITHUB_ENV` 供下游 dumpbin / 打包步; + # ③ arm64 的 `link.exe` 用 vswhere 定位后显式导出 `CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER` + # (Hostarm64 原生优先、Hostx64 次之)。**②必须先于③**:只覆盖链接器而不给 LIB,rustc 会在 + # `link.exe` 里撞 `LNK1181: cannot open input file 'kernel32.lib'`——那正是我们要定位的错误,不是要制造的错误。 + # ①②③ 全是「定位不到就打印原因继续」,不用启发式决定红绿:真缺工具链时让链接器自己报出来。 - name: cargo build --release --locked shell: bash env: RUSTFLAGS: ${{ inputs.rustflags }} ULL_MSA_CLIENT_ID: ${{ secrets.ULL_MSA_CLIENT_ID }} + SLOT: ${{ inputs.slot }} + BUILD_DIR: ${{ inputs.build_dir }} run: | 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 + # ② MSVC 的 LIB/INCLUDE。**别把 vcvarsall 的路径直接塞给 `cmd //c "..."`**:MSYS 会把参数里的 + # `"` 改写成 `\"` 再交给 cmd.exe,而 cmd 的 /c 引号剥离接不住这一层,只会报 + # `'\"C:\...\vcvarsall.bat\"' 不是内部或外部命令` —— 结果 LIB/INCLUDE 全空、只剩 `set` 的空输出 + # (M164 复审实测)。故本步改为往 `$RUNNER_TEMP` 写一个 **CRLF** 的 wrapper .bat,引号留在 + # .bat 自己的 `call "..."` 里(由 cmd 解析,路径带空格也没事),`cmd //c` 只收**不含引号**的 + # 相对文件名(先 cd 进去),彻底绕开 MSYS 的引号改写。 + tmp=${RUNNER_TEMP:-${TEMP:-/tmp}} + mkdir -p "$tmp" + vcvars="" + # `-find` 是**单值**参数且官方文档给的是反斜杠形态:两种形态各试一次(与下面 dumpbin 步同规矩), + # 只赌一种就等于再写一个「静默退化成 warning」。 + for pat in 'VC\Auxiliary\Build\vcvarsall.bat' 'VC/Auxiliary/Build/vcvarsall.bat'; do + vcvars=$("$vswhere" -latest -products '*' -find "$pat" 2>/dev/null | head -n1 || true) + [ -n "$vcvars" ] && break + done + dump="" + if [ -n "$vcvars" ]; then + # CRLF 是必须的:cmd 解析只有 LF 的 .bat 会漏行/报错。 + { printf '@echo off\r\n' + printf 'call "%s" arm64 >nul 2>&1\r\n' "${vcvars//\//\\}" + printf 'set\r\n'; } > "$tmp/ull-msvc-arm64.bat" + dump=$(cd "$tmp" && cmd //c ull-msvc-arm64.bat 2>/dev/null | tr -d '\r') || dump="" + echo "wrapper: $tmp/ull-msvc-arm64.bat ← $vcvars(set 输出 ${#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/Hostarm64/arm64/link.exe' \ + 'VC\Tools\MSVC\*\bin\Hostx64\arm64\link.exe' 'VC/Tools/MSVC/*/bin/Hostx64/arm64/link.exe'; do + [ -n "$found" ] && break + hit=$("$vswhere" -latest -products '*' -find "$p" 2>/dev/null | head -n1 || true) + [ -n "$hit" ] && found=${hit//\\//} + 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 + # 产物清单只给 arm64 打:x64(当前唯一在发的槽位)这一步零变化,不额外给它加失败面。 + if [ "$SLOT" = "windows-arm64" ]; then + ls -l "$BUILD_DIR"/unlucky-launcher-iced* "$BUILD_DIR"/ull* || true + fi - name: Windows 单文件自检(导入表不得出现 VC++ 可再发行 DLL) if: runner.os == 'Windows' @@ -239,9 +306,11 @@ jobs: if-no-files-found: error retention-days: 14 - # (M142)arm64 若红在别的步骤(cargo / 打包 / 导入表门禁),也要有一条注解说清「红在哪」和「不挡发布」—— - # 只剩一句 `Process completed with exit code 1.` 的教训见上一条体检步骤。 + # (M142/M164)arm64 若红在别的步骤(cargo / 链接 / 打包 / 导入表门禁),也要有一条注解说清「红在哪」和 + # 「不挡发布」——只剩一句 `Process completed with exit code 1.` 的教训见上面那条体检步骤。 + # M164 起本槽位的红**只可能来自真实构建链路**(体检已降级为取证、永不 exit 1),所以这条注解同时也是 + # 「arm64 到底能不能编出来」的答案所在:红了就把上面的编译器/链接器原文当根因,别再回去改体检。 - name: arm64 失败收尾注解(红不挡发布) if: failure() && inputs.slot == 'windows-arm64' 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" diff --git a/.github/workflows/release-guard.yml b/.github/workflows/release-guard.yml index e62553d..dafade5 100644 --- a/.github/workflows/release-guard.yml +++ b/.github/workflows/release-guard.yml @@ -16,6 +16,12 @@ name: Release guard # 为什么不并进 guards.yml:那份文件已经 398 行(400 行红线),再挂一个 job 必越线。 # 改 `release.yml` 的 prerelease 输入 / 改那段 `gh release` 时,先跑 # `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: push: @@ -38,3 +44,7 @@ jobs: run: python3 packaging/check_release_semantics.py - name: 变异自测(守门抓不抓得住回归) 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 diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index ae77d73..fa23053 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -4,6 +4,8 @@ name: Release / publish(可复用) # job **逐字搬家**:裁剪白名单外的槽位产物、重算全量 `SHA256SUMS.txt`、渲染 `packaging/release-notes.md` 模板、 # `gh release create/edit` 的同 tag 幂等,全部照旧。留在调用方 `release.yml` 的只有三件事:`needs: build`、 # 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`)。 # 可复用工作流**拿不到**调用方未显式传入的 secret,写 `secrets.GITHUB_TOKEN` 会静默为空 → gh 401。 @@ -38,10 +40,17 @@ jobs: contents: write env: # ── 发布白名单:唯一决定「哪几个槽位进 Release」的开关(空格分隔,键同 matrix.slot)── - # **M142 起只有 `windows-x64`**:arm64 连试四轮未走通,而下面齐全性检查是「名单内少一档就 fail」——名字挂着 - # arm64 就等于 x64 编成功也发不出去。故把它移出名单:它照留在矩阵里跑取证(诊断注解是将来修它的唯一证据), - # **红了不挡发布**。**可逆**:arm64 修好后把键加回这一行即可(齐全性检查与 SHA256SUMS 自动跟着扩),再同步 - # README §1/§9 与 release-notes.md 两张表。四档 Linux/macOS 仍未验证,默认连构建都不跑(M139)。 + # **M142 起只有 `windows-x64`**:arm64 连试四轮未走通,而齐全性检查是「名单内少一档就 fail」——名字挂着 + # arm64 就等于 x64 编成功也发不出去。故把它移出名单:它照留在矩阵里跑取证,**红了不挡发布**。 + # (M164)**为什么现在还只有 x64**:arm64 五轮全红其实都红在一个误判的编译前体检步(见 release-build.yml + # 顶部那段),真正的 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" steps: - uses: actions/checkout@v4 @@ -51,6 +60,17 @@ jobs: path: dist 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: 裁掉白名单外的槽位产物(构建取证 ≠ 发布) shell: bash run: | @@ -67,30 +87,20 @@ jobs: echo "publish: $base" ;; *) - echo "drop: $base($platform_arch 不在发布白名单:四档未验证 / arm64 待修;仍留在 dist- artifact 里取证)" + echo "drop: $base($platform_arch 不在发布白名单:四档未验证 / arm64 待下一 tag 验证;仍留在 dist- artifact 里取证)" rm -f "$f" ;; esac done - - name: 发布白名单齐全性 + 重算全量 SHA256SUMS + - name: 重算全量 SHA256SUMS shell: bash run: | set -euo pipefail ls -l dist - # 白名单内槽位必须**全齐**才发版(宁可不出 Release,也不出少一档的 Release)。每个槽位都要 GUI 与 CLI 两个 - # 资产——只按「槽位名出现过」判会被其中一个掩盖;比较用下划线形态(资产名自 D49 起是 `..._windows_x64.exe`)。 - # M142 后白名单只剩 x64 一档:arm64 缺产物不再拦发布,但 x64 自己缺 GUI 或 CLI 照样在这里 fail。 - 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 侧按已发布的资产重算。 + # 齐全性(名单内每个槽位 GUI 与 CLI 都要在)**已由上上步的「发布名单守卫」断言**,且那一步看的是裁剪前的 + # 全量集 —— 同一套规则只留一份实现,免得两处判据漂开。这里只负责重算摘要:各槽位各写一份 SHA256SUMS.txt, + # 合并后只有一份是真的,故在 Release 侧按已发布的资产重算。 cd dist rm -f SHA256SUMS.txt sha256sum unlucky-launcher_* ull_* > SHA256SUMS.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 861f22c..d84f5a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,19 +3,25 @@ name: Release # 发布流水线:tag `v*` → **只构建两个 Windows 槽位**(各自原生 `windows-latest` / `windows-11-arm`)→ 单文件产物 + # SHA256SUMS → artifact + GitHub Release。**push tag 默认创建稳定版**(M163:不带 `--prerelease`,否则 # `releases/latest` 永远 404、自更新通道取不到清单);同 tag 重跑幂等(覆盖资产与说明、不 create、不碰标记)。 -# **构建 ≠ 发布**:只有 `PUBLISH_SLOTS` 里的槽位才进 Release 与 SHA256SUMS ——**M142 起只有 `windows-x64`**(arm64 连试 -# 四轮未成,仍留矩阵里取证、红了不挡 x64 发布;它修好后把键加回那一行即可)。四档 Linux/macOS 从未验证,默认连构建都 -# 不跑(M139:省配额、不留红叉);整表六槽位取证走 `gh workflow run release.yml -f all_slots=true`(槽位表在 `plan`)。 +# **构建 ≠ 发布**:只有 `PUBLISH_SLOTS` 里的槽位才进 Release 与 SHA256SUMS ——**M142 起只有 `windows-x64`**(arm64 仍留 +# 矩阵里取证、红了不挡 x64 发布)。**M164 的更正**:arm64 那五轮红其实全红在一个误判的编译前体检步(cargo/link 一次 +# 都没跑过,见 `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` # 只在 **tag ref** 上 dispatch 才可达(跑 tag 得 `gh workflow run release.yml --ref v0.2.1`);分支 ref 的 dispatch 与 # push 一样被 release job 的 `if: github.ref_type == 'tag'` 挡在发布之外。 # # M145 拆分(本文件与 packaging/README.md 都已顶到 400 行红线,下一处改动必越线):本文件只留**触发 + 槽位表 + 编排**, # 两个 job 的实体各搬一份可复用工作流(`workflow_call`),步骤逐字未改: -# - `.github/workflows/release-build.yml` = 原 `build` job 全流程(Linux 依赖 / arm64 工具链体检 / cargo build / -# 静态 CRT 导入表门禁 / 打包 / 归档自检 / 上传 artifact / arm64 收尾注解),矩阵键换成同名 `inputs.*`。 +# - `.github/workflows/release-build.yml` = 原 `build` job 全流程(Linux 依赖 / arm64 工具链取证(M164 起只取证)/ +# cargo build / 静态 CRT 导入表门禁 / 打包 / 归档自检 / 上传 artifact / arm64 收尾注解),矩阵键换成同名 `inputs.*`。 # - `.github/workflows/release-publish.yml` = 原 `release` job 全流程(`PUBLISH_SLOTS` 白名单 / 裁剪 / 重算 # SHA256SUMS / 渲染说明 / `gh release` 幂等),`secrets.GITHUB_TOKEN` 改为显式透传的 secret。 +# (M164 在裁剪前插了一步「发布名单守卫」:主线 7 步,本分支 8 步。) # `on` / `env` / `permissions` / `concurrency` 与 `plan` 的槽位表**一字未动**。 on: @@ -72,9 +78,11 @@ jobs: SLOTS = [ # 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"), - # Windows arm64:**原生** `windows-11-arm`(4C/16G,公开仓库免费;GitHub 文档的 Standard runners 也列 - # `windows-11-vs2026-arm`)。M140 前是在 windows-latest 上交叉,**CI 从未成功过一次**(v0.2.1 首跑红在 - # 工具链体检,M141/M142 在此修)。原生还让 `ull --version` 冒烟真跑,dumpbin 走 Hostarm64 分支。 + # Windows arm64:**原生** `windows-11-arm`(4C/16G,公开仓库免费)。该标签自 2026-09 起是 + # **Windows 11 Arm64 + Visual Studio 2026** 镜像(VS 在 `C:\Program Files\Microsoft Visual Studio\18`), + # 镜像清单里 `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", rustflags="-C target-feature=+crt-static", mode="ci"), # ↓ 四档**未验证,不纳入 CI**:从未真构建成功、也没在真机跑过,默认一个都不编;`all_slots=true` 时照编 diff --git a/README.md b/README.md index 0adf18f..99bc5fd 100644 --- a/README.md +++ b/README.md @@ -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 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 -asset. Of those rows `windows-arm64` is the one CI still builds — natively, on GitHub's -`windows-11-arm` runner, for evidence — but that slot's toolchain check has not gone green yet, so its -`.exe` stays out of the release. The row stays because the architecture is still supported; it rejoins -the download list once the build is green. +asset. A guard (`packaging/check_publish_slots.py`) watches both directions: a slot named in the list +whose artifacts are missing turns the release red, and artifacts that exist for an unlisted slot get an +explicit "not published" warning instead of being deleted quietly. Of those rows `windows-arm64` is the +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 directory. Windows ships a bare `.exe` (a single file cannot carry the texts), so those three diff --git a/README.zh-CN.md b/README.zh-CN.md index 29db965..34c11bb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -212,9 +212,12 @@ unlucky-launcher --version # CLI,版本 + 系统摘要 **本版只发布 `windows-x64`。** 发布 job 只留 `PUBLISH_SLOTS` 里点名的槽位 (`.github/workflows/release-publish.yml`),其余产物在写 `SHA256SUMS.txt` 之前就被删掉——所以表里标 -**暂未发布** 的行是命名规则,不是本版可下载的资产。这几行里 `windows-arm64` 是 CI 唯一真跑的那档:M140 起在 -GitHub 原生 `windows-11-arm` runner 上编来取证,但该槽位的工具链体检还没绿,所以它的 `.exe` 不进 Release; -行不删是因为架构仍要支持,绿了就把键加回 `PUBLISH_SLOTS`。 +**暂未发布** 的行是命名规则,不是本版可下载的资产。两个方向都有守卫(`packaging/check_publish_slots.py`): +名单里点名了却没产物 ⇒ Release 变红;产物在、名单没点名 ⇒ 显式打「未发布」警告,不再静默删掉。 +这几行里 `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` (一个文件装不下这三份文本),所以这三份作为并列资产挂在 Release 上——下 Windows 二进制不会把许可条款 diff --git a/packaging/README.md b/packaging/README.md index 2e05dd6..bf9de19 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -64,6 +64,7 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一) | `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 | diff --git a/packaging/check_publish_slots.py b/packaging/check_publish_slots.py new file mode 100644 index 0000000..827b4e3 --- /dev/null +++ b/packaging/check_publish_slots.py @@ -0,0 +1,345 @@ +#!/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[^"]+)"(?P.*?)\)', re.S) +MODE_RE = re.compile(r'mode="(?P[^"]+)"') +#: 发布白名单那一行(yaml 里是 `PUBLISH_SLOTS: "windows-x64"`)。 +PUBLISH_RE = re.compile(r'^\s*PUBLISH_SLOTS:\s*"(?P[^"]*)"', 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) + # 全部走 **stdout**:Actions 在 stdout 上同样识别 `::error::` / `::warning::`,而分两个流写会让 + # 「表格 → 注解」的顺序在 CI 日志里不可控(stderr 往往整段先刷出来,注解就跑到它解释的那张表前面)。 + for row in rows: + print(row) + for item in warnings: + print(f"::warning::{item}") + for item in errors: + print(f"::error::{item}") + if errors: + print(f"发布名单守卫:{len(errors)} 条不通过") + 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:])) diff --git a/packaging/docs/artifacts-and-version.md b/packaging/docs/artifacts-and-version.md index ef839b4..09db419 100644 --- a/packaging/docs/artifacts-and-version.md +++ b/packaging/docs/artifacts-and-version.md @@ -9,7 +9,7 @@ | 槽位 | 本版发布 | 产物文件名 | 形态 | 归档内容 | |------|---------|-----------|------|---------| | `windows-x64` | ✅ 发布 | `unlucky-launcher__windows_x64.exe` | **裸 exe**(双击即用) | exe 单文件;三份合规文本作为并列 Release 资产 | -| `windows-arm64` | ❌ 不发布(构建照跑取证) | `unlucky-launcher__windows_arm64.exe` | **裸 exe**(同 x64 策略) | 同上;CI 仍在**原生** `windows-11-arm` runner 上构建(M140 起不交叉),**红了不挡 x64 发布**(M142,见 §3) | +| `windows-arm64` | ❌ 不发布(构建取证中) | `unlucky-launcher__windows_arm64.exe` | **裸 exe**(同 x64 策略) | 同上;CI 在**原生** `windows-11-arm`(Windows 11 Arm64 + VS2026 镜像)上构建,**M164 起编译前体检只取证、不再判定红绿**(此前五轮全红其实全红在那个误判的体检步),**红了不挡 x64 发布**(M142,见 §3) | | `linux-x64` | ❌ 不构建(未验证) | `unlucky-launcher__linux_x64.tar.gz` | 单二进制归档 | `unlucky-launcher` + `LICENSE` + `NOTICE` + `README.md` | | `linux-arm64` | ❌ 不构建(未验证) | `unlucky-launcher__linux_arm64.tar.gz` | 单二进制归档(原生 ARM64 runner 构建) | 同上 | | `macos-arm64` | ❌ 不构建(未验证) | `unlucky-launcher__macos_arm64.tar.gz` | 单二进制归档 | 同上 | @@ -22,6 +22,8 @@ - **「不构建」= CI 默认根本不跑**(M139):那四档未验证,产物不给用户、构建配额也不花;要整表六槽位取证走 `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()`。 - **"单文件"指可执行文件本身零运行时依赖**(无 WebView、无 JRE、无 sidecar、无 DLL 旁挂);归档里的三份合规文本是许可义务、不是运行时依赖,Windows 因为"一个 exe 装不下文本", 把 `LICENSE` / `NOTICE` / `README.md` 作为 Release 的并列资产分发,条款同样随下载走。 diff --git a/packaging/docs/build-and-ci.md b/packaging/docs/build-and-ci.md index 81f76e3..dee484b 100644 --- a/packaging/docs/build-and-ci.md +++ b/packaging/docs/build-and-ci.md @@ -50,17 +50,33 @@ python packaging/make_artifacts.py --slot windows-arm64 \ `-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`**:每槽原生编译,交叉槽位与 `rustup - target add` 一并没了)。arm64 槽位多一步编译前体检:查 VC 的 `lib/arm64/libcmt.lib`(`+crt-static` - 靠它)与 SDK 的 `um/arm64/kernel32.lib`,缺了就 fail;**注解里固定打 4 行实况**(探测根 / SDK Lib 版本→um - 架构 / MSVC lib 架构 / 两个关键库)。M142 修的正是「诊断自己先死」:`set -uo pipefail` 关不掉 runner 注入的 - `-e`,`find` 扫不存在的根被 pipefail 判非零就当场退出 → 现在 `set +e` + 处处 `|| true`,清单先打完再定红绿。 +- 每个槽位 `cargo build --release --locked`(**不传 `--target`**:每槽原生编译,交叉槽位没了)。arm64 槽位在编译前 + **把工具链显式备齐**(M164):`rustup target add aarch64-pc-windows-msvc`、用微软自己的 `vcvarsall.bat arm64` 取 + `LIB`/`INCLUDE`(写进本步环境与 `$GITHUB_ENV`)、用 vswhere 定位 arm64 `link.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 "" 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`**:arm64 连试四轮未走通,而齐全性检查是「名单内少一档就 fail」,挂着它就等于 x64 编成功也发不出去; - 它仍留在矩阵里照跑取证、红了不挡发布。**可逆**:arm64 修好后把键加回 `release.yml` 那一行,再同步 §1/§9 与 - `release-notes.md`。四档未验证的(默认连构建都不跑)依旧不推给用户。 -- `release` job:下载全部 artifact → **按白名单裁剪** → 重算 `SHA256SUMS.txt` → 用 `packaging/release-notes.md` - 模板渲染支持矩阵 → 生成更新清单 `manifest.json` → `gh release create`(**push tag 默认建稳定版**:不带任何标记)。 + `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 条回归,本地一条命令可跑。 +- `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` + `gh release edit`,覆盖资产与说明而不是再 `create`(`create` 遇已存在会失败,把流水线最后一步染红)。 - **预发布标记的语义表(M163,唯一真相源 `packaging/release_marker.py`)**: @@ -87,8 +103,11 @@ python packaging/make_artifacts.py --slot windows-arm64 \ `.github/workflows/release-build.yml`(`workflow_call`,四个矩阵键换成同名 `inputs.*`),`release` job 的 6 步 逐字搬进 `.github/workflows/release-publish.yml`(同 `workflow_call`,`secrets.GITHUB_TOKEN` 改为调用方显式透传 的 secret)。触发(`on`)、`env`、顶层 `permissions`、`concurrency` 与 `plan` 的槽位表在 M145 搬家时**一字未动**; - 后来只有 M163 动过 `on` 里 `prerelease` 的默认值(`true` → `false`,见上面那张语义表)。改槽位仍只改 - `release.yml` 那一处(`PUBLISH_SLOTS` 白名单在 `release-publish.yml`,两个 arm64 修复时要改的仍是那两行)。 + 后来只有 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` 的 `build` job 按名显式透传(可复用工作流拿不到未显式传入的 diff --git a/packaging/docs/compliance-support-boundaries.md b/packaging/docs/compliance-support-boundaries.md index 4214261..a652eb6 100644 --- a/packaging/docs/compliance-support-boundaries.md +++ b/packaging/docs/compliance-support-boundaries.md @@ -21,7 +21,7 @@ | 平台 | 本版发布 | 级别 | 运行时要求 | |------|---------|------|-----------| | Windows 10/11 x64 | ✅ | 一等 | 无(UCRT 系统自带) | -| Windows 11 ARM64 | ❌ 不发布(构建取证中,待修) | 一等(启动器产物) | 无额外库;启动器原生运行,游戏本体走系统 x64 模拟层 + x64 JDK | +| Windows 11 ARM64 | ❌ 不发布(构建取证中) | 一等(启动器产物) | 无额外库;启动器原生运行,游戏本体走系统 x64 模拟层 + x64 JDK | | macOS 12+(arm64 / x64) | ❌ 不构建 | 一等(未真机验证) | 无第三方库;未签名需去 quarantine | | Linux x64 X11 | ❌ 不构建 | 一等(未真机验证) | glibc 2.35+、xkbcommon、fontconfig、libGL、xcb | | Linux x64 Wayland | ❌ 不构建 | 支持 | 上述 + wayland-client | @@ -34,7 +34,12 @@ published 一列,Termux 行标注 experimental)——改一边必须同步另一边,否则标题里的"一致"就是假话。 两个 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. 已知边界 / 后续 diff --git a/packaging/docs/profiles-and-measurements.md b/packaging/docs/profiles-and-measurements.md index efc24fc..ce4dc7b 100644 --- a/packaging/docs/profiles-and-measurements.md +++ b/packaging/docs/profiles-and-measurements.md @@ -120,5 +120,9 @@ libxcb 条目数 = 0,只把 fontconfig 那 7 个 `.so` 放进 `LD_LIBRARY_PATH **没在本地验证、只能由 CI 补的**:`windows-arm64` 在 `windows-11-arm` 上的真实链接与 ARM64 导入表、 `linux-arm64` 在原生 arm64 runner 上的真跑(本机既没有 ARM 机器,也连 `raw.githubusercontent.com` 都解析不了)。 -M141/M142 保留了编译前体检(查 VC 的 `lib/arm64/libcmt.lib` 与 SDK 的 `um/arm64/kernel32.lib`,无论齐不齐都先打 4 行 -实况注解)。闭环标准不是「两槽位都进 SHA256SUMS」(M142 起只有 x64 进),而是 arm64 注解转绿 + 把键加回 `PUBLISH_SLOTS`。 +**M164 的更正**:M141/M142 那个编译前体检不但没帮上忙,还是 arm64 五轮全红的唯一原因(`find` 猜 SDK 的 +`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**)。 diff --git a/packaging/release-notes.md b/packaging/release-notes.md index 4e74394..0e73fe3 100644 --- a/packaging/release-notes.md +++ b/packaging/release-notes.md @@ -33,11 +33,16 @@ Asset names are `___`, 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 ARM64 is not part of this release.** CI still builds it on a native `windows-11-arm` runner for -evidence, but that slot's toolchain check has not gone green yet, so +**Windows ARM64 is not part of this release.** CI still builds it on a native `windows-11-arm` runner (Windows 11 +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 -`SHA256SUMS.txt` instead of handing out a build nobody has verified. `windows-x64` is the only published -slot; ARM64 rejoins this table as soon as its build is green (one line in the workflow's `PUBLISH_SLOTS`). +`SHA256SUMS.txt` instead of handing out a build nobody has verified. Worth stating plainly: every ARM64 failure +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 (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 | | --- | --- | --- | --- | | 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+ 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` | @@ -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 | | 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 -(see Downloads above). Both arm64 rows are formal matrix slots, not the experimental Termux path, and -Linux arm64 is not built by default — someone has to run it on real hardware (via a manual all-slots run) -before it joins this list. The rows above and the matrix table in `packaging/README.md` §9 are the same -list — change one, change the other. +`windows-x64` is the only published slot this release; Windows ARM64 stays out until a CI run actually produces +its artifact (see Downloads above — so far every ARM64 failure was the pre-build probe, never the compiler). +Both arm64 rows are formal matrix slots, not the experimental Termux path, and Linux arm64 is not built by +default — someone has to run it on real hardware (via a manual all-slots run) before it joins this list. The +rows above and the matrix table in `packaging/docs/compliance-support-boundaries.md` §9 are the same list — +change one, change the other. ## Install