From 2813e2984c07282405aac27896156479769f5b6f Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 8 Nov 2026 14:15:52 +0800 Subject: [PATCH] =?UTF-8?q?ci(m184):=20linux-x64=20=E8=BF=9B=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=90=8D=E5=8D=95=EF=BC=8C=E4=B8=A4=E6=A1=A3=20Linux?= =?UTF-8?q?=20=E8=BD=AC=20mode=3Dci=EF=BC=8Carm64=20=E5=8F=AA=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=B8=8D=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 发布流水线默认从 2 个槽位变 4 个:`linux-x64`(ubuntu-22.04)与 `linux-arm64` (ubuntu-22.04-arm)的 mode 由 probe 改 ci,tag 推送默认就编。 构建 ≠ 发布仍由 `PUBLISH_SLOTS` 决定,本轮只加 `linux-x64`: - `linux-x64` 有运行时证据 —— release 产物在 Linux 真机(Ubuntu 22.04 + WSLg 真窗口) 渲出一帧、[theme]/[backend] final 日志齐全、像素判定通过,真窗口 1180x760 从 Windows 侧截屏可见;`release-build.yml` 另加一条**同槽位**的 xvfb 运行时门禁(只在 `inputs.slot == 'linux-x64'` 上跑,不给没验过的 arm64 造红)。 - `linux-arm64` 只有构建证据、运行时零证据,产物照出 artifact 取证但不发布;解锁条件 (真机渲出一帧 + 主题日志)写进 release-notes 与 §9。 守卫跟着扩(`packaging/check_publish_slots.py`): - 自测从 8 条扩到 11 条,新增「声明的 linux-x64 构建红了 ⇒ 必须红(挡住发布)」、 「linux-arm64 有产物但没声明 ⇒ 只警告」、「提前把 linux-arm64 写进名单 ⇒ 缺产物必红」; - probe 档样例从 linux-x64 换成仍是 probe 的 macos-arm64(否则该用例会从「必须红」 变成「本来该绿」); - 新增「mode=ci 却本次没产物 ⇒ ::warning::」:红一档不能只表现为表里少一行。 文档矩阵同步(release-notes §Downloads/§Support matrix、compliance §9/§10、 artifacts §1、build-and-ci §3、README ×2),两份文档都把 Linux 写成 「已构建并发布(x64)/ 已构建未发布(arm64)」,并把新发现写实:Linux 上整窗透明 与圆角没有代码路径(hwnd=none / corner=policy=system),不是「未验」;Linux 上 `ull update apply` 会拒绝 portable-zip(清单里现在有 linux-x64 产物)。 证据账(含实测输出、CI 增量估算、未验清单):packaging/docs/linux-evidence.md。 --- .github/workflows/release-build.yml | 61 +++++- .github/workflows/release-guard.yml | 3 +- .github/workflows/release-publish.yml | 25 ++- .github/workflows/release.yml | 47 ++-- README.md | 78 ++++--- README.zh-CN.md | 61 ++++-- docs/images/linux-x64-wslg-window.png | Bin 0 -> 43084 bytes packaging/README.md | 7 +- packaging/check_publish_slots.py | 73 ++++++- packaging/docs/artifacts-and-version.md | 16 +- packaging/docs/build-and-ci.md | 47 ++-- .../docs/compliance-support-boundaries.md | 31 ++- packaging/docs/linux-evidence.md | 205 ++++++++++++++++++ packaging/release-notes.md | 105 ++++++--- 14 files changed, 608 insertions(+), 151 deletions(-) create mode 100644 docs/images/linux-x64-wslg-window.png create mode 100644 packaging/docs/linux-evidence.md diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index d081786..afbe7ae 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -6,6 +6,9 @@ name: Release / build(可复用) # `matrix.rustflags` → `inputs.rustflags`。 # **M164 改了两步**(只这两步):① `Windows ARM64 工具链体检` → `工具链取证`(不再 exit 1,判定权交回编译器; # 理由见该步上方的长注释);② `cargo build` 步给 arm64 槽位显式备齐 target / `LIB`+`INCLUDE` / arm64 `link.exe`。 +# **M184 加了一步**:`linux-x64` 的 xvfb 运行时冒烟(真渲一帧 + `[theme]` / `[backend] final` 日志断言 + 像素判定)—— +# 这一档进了 `PUBLISH_SLOTS`,「发出去的包真能跑」从此由 CI 每轮证明;判据只在 `linux-x64` 槽位上跑(arm64 没有 +# 对应的运行时证据,不给它加一条没验证过的门禁)。同一轮把 arm64 的失败收尾注解按槽位分支。 # 矩阵**仍由调用方** `release.yml` 的 `plan` job 生成并在外层展开(矩阵不能带 `if`,`strategy` 可读 `needs`), # 本文件只跑**单个槽位**,不持有槽位表 —— 改槽位只改 `release.yml` 里那一处。 # `env` 必须在被调方重复声明:**被调工作流不继承调用方的 workflow 级 `env`**(CARGO_TERM_COLOR / RUST_BACKTRACE @@ -259,6 +262,49 @@ jobs: echo "::error::静态 CRT 未生效:exe 仍动态依赖 VC++ 可再发行包"; exit 1 fi + # (M184)linux-x64 的**运行时门禁**:这一档已进 PUBLISH_SLOTS,所以「发出去的包真能开窗渲出一帧」得由 CI 每轮证明, + # 不能只靠一次性的本机验收(证据与实测输出见 `packaging/docs/linux-evidence.md`)。 + # **只在 linux-x64 槽位上跑**:判据是按 x64 软渲染实测标定的;`linux-arm64` 上这条链一次都没验过(没有 arm64 的 + # xvfb 证据),写成无条件的 `runner.os == 'Linux'` 就等于给一个没验证过的槽位加一条会红的判据——正是 M171 那个 + # 「豁免行要单独落」的同类事故。宁可这条只在有证据的槽位上真跑。 + - name: Linux x64 运行时冒烟(xvfb 真渲一帧 + 主题/后端日志断言) + if: inputs.slot == 'linux-x64' + shell: bash + env: + BUILD_DIR: ${{ inputs.build_dir }} + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends xvfb xauth \ + libxkbcommon-x11-0 libx11-6 libxcursor1 libxrandr2 libxi6 libfontconfig1 fonts-dejavu-core + out="$RUNNER_TEMP/linux-smoke" + mkdir -p "$out/data" + log="$out/launcher.log" + # 退出码单独取:`tee` 之后的 `$?` 是 tee 的,管道里 cargo/launcher 的失败会被吞成 0。 + set +e + xvfb-run -a --server-args="-screen 0 1280x1024x24" \ + "$BUILD_DIR/unlucky-launcher-iced" --software-renderer \ + --screenshot "$out/linux-x64-first-frame.png" --data-dir "$out/data" 2>&1 | tee "$log" + code=${PIPESTATUS[0]} + set -e + echo "launcher exit=$code" + # 两条日志断言(两行都是**恒开**的,不依赖 ULL_ACCEPTANCE): + # `[theme] source=…flavor=…follows_system=…` 证明主题是按预期来源定的(截图通道默认 Light), + # `[backend] final=… result=started` 证明渲染后端真的起来了(而不是起来前就崩了)。 + grep -q '^\[theme\] source=' "$log" || { echo "::error::缺少 [theme] 行(主题来源没打印)"; exit 1; } + grep -q '^\[backend\] final=.* result=started' "$log" || { echo "::error::缺少 [backend] final=… result=started(渲染后端没起来)"; exit 1; } + python3 packaging/check_screenshot.py "$out/linux-x64-first-frame.png" --json + ls -l "$out" + exit "$code" + + - name: 上传 Linux x64 首帧图(失败也传,人眼复核用) + if: always() && inputs.slot == 'linux-x64' + uses: actions/upload-artifact@v4 + with: + name: linux-x64-first-frame + path: ${{ runner.temp }}/linux-smoke/ + if-no-files-found: warn + - name: 打包(产物 + SHA256SUMS) shell: bash env: @@ -310,7 +356,18 @@ jobs: # 「不挡发布」——只剩一句 `Process completed with exit code 1.` 的教训见上面那条体检步骤。 # M164 起本槽位的红**只可能来自真实构建链路**(体检已降级为取证、永不 exit 1),所以这条注解同时也是 # 「arm64 到底能不能编出来」的答案所在:红了就把上面的编译器/链接器原文当根因,别再回去改体检。 + # M184:Linux arm64 进来(它现在是 `mode: ci`,tag 推送默认就编),措辞按槽位分支 —— 两个 arm64 都**不在**发布名单里, + # 但理由不同:windows-arm64 等「能不能出 PE」的答案,linux-arm64 等「真机跑不跑得起来」的答案。 - name: arm64 失败收尾注解(红不挡发布) - if: failure() && inputs.slot == 'windows-arm64' + if: failure() && (inputs.slot == 'windows-arm64' || inputs.slot == 'linux-arm64') shell: bash - run: echo "::error::[arm64] 本槽位已红,失败步骤见本 job 的步骤名(M164 起只会是 cargo build / 链接 / 导入表门禁 / 打包,体检步不再判红);PUBLISH_SLOTS 当前只有 windows-x64 ⇒ 本次 Release 仍照发 x64,arm64 产物不进 Release" + env: + SLOT: ${{ inputs.slot }} + run: | + set -euo pipefail + if [ "$SLOT" = "linux-arm64" ]; then + reason="本槽位只有构建证据、运行时完全未验(没有 ARM64 真机跑过一帧)" + else + reason="失败步骤见本 job 的步骤名(M164 起只会是 cargo build / 链接 / 导入表门禁 / 打包,体检步不再判红)" + fi + echo "::error::[$SLOT] 本槽位已红:$reason。PUBLISH_SLOTS 当前是 windows-x64 + linux-x64 ⇒ 本次 Release 仍照发那两档,$SLOT 的产物不进 Release" diff --git a/.github/workflows/release-guard.yml b/.github/workflows/release-guard.yml index c87a6cc..22785b4 100644 --- a/.github/workflows/release-guard.yml +++ b/.github/workflows/release-guard.yml @@ -20,7 +20,8 @@ name: Release guard # (M164)同一个 job 还挂**发布名单守卫** `packaging/check_publish_slots.py`:`PUBLISH_SLOTS` 与槽位表/产物 # 必须对得上。PR 阶段跑源码级断言(名单非空、每项都是 `release.yml` 槽位表里的 `mode: ci` 档)—— # 名单里写错一个字此前只会表现为「Release 侧一个也不匹配、齐全性检查红在别处」,根因要翻两层日志; -# `--self-test` 再注入 8 条回归(缺资产 / 拼错 / 声明了 probe 档 / 名单声明了没产物 / 有产物没声明)证明它会红。 +# `--self-test` 再注入 11 条回归(缺资产 / 拼错 / 声明了 probe 档 / 名单声明了没产物 / 有产物没声明 / +# `mode: ci` 却没产物 / 声明的 `linux-x64` 构建红了必须拦住发布 / 提前把 `linux-arm64` 写进名单必须红)证明它会红。 # 发布侧那道「产物 ⇄ 名单」的运行时断言在 `release-publish.yml`(同一份脚本,`--dist`)。 # # (M169)同一个 job 再挂**测试通道守门** `packaging/check_test_source_off.py`:core/cli/presenter/ diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index fa23053..f882add 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -40,18 +40,21 @@ jobs: contents: write env: # ── 发布白名单:唯一决定「哪几个槽位进 Release」的开关(空格分隔,键同 matrix.slot)── - # **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 两张表。 + # **M184 起是 `windows-x64` + `linux-x64`**:`linux-x64` 已真机验过 —— release 产物在 Linux 真机(WSLg 真窗口) + # 渲出一帧、主题/后端日志齐全,并有一条同槽位的 xvfb 运行时门禁常驻在 `release-build.yml`(证据与实测输出见 + # `packaging/docs/linux-evidence.md`)。进名单的代价是「每轮发版都得真出这个包」:齐全性检查是「名单内少一档就 fail」, + # 所以这条名单同时也是「linux-x64 构建红了就挡住整次发布」的闸 —— 与任务要求一致(声明为 ci 就得真能出包)。 + # **`linux-arm64` 不在名单里,且这一轮刻意不放**:它只有构建证据,运行时完全没有证据(没有 ARM64 真机跑过一帧, + # 也没有 arm64 的 xvfb 冒烟)。解锁条件 = arm64 真机(或原生 runner)上真跑出一帧 + 主题日志 → 再把键加进这一行。 + # 只加进矩阵不加进名单时,它的产物会被下面的裁剪步删掉,但守卫会先打 `::warning::` 明说「未发布、会被裁掉」, + # 不是静默丢弃(这条是任务里点名要验的反方向)。 + # `windows-arm64` 同理仍不在名单(M142/M164:它的五轮红全红在误判的体检步,判定权已交回编译器,等下一 tag 的 CI 证)。 + # **可逆,且只是一行**:真出包之后把键加进这一行(`"windows-x64 linux-x64 <新键>"`),齐全性检查与 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" + # macOS 两档仍未验证,默认连构建都不跑(M139)。 + PUBLISH_SLOTS: "windows-x64 linux-x64" steps: - uses: actions/checkout@v4 @@ -87,7 +90,7 @@ jobs: echo "publish: $base" ;; *) - echo "drop: $base($platform_arch 不在发布白名单:四档未验证 / arm64 待下一 tag 验证;仍留在 dist- artifact 里取证)" + echo "drop: $base($platform_arch 不在发布白名单:macOS 两档未构建验证 / 两个 arm64 槽位待真机跑一帧;仍留在 dist- artifact 里取证)" rm -f "$f" ;; esac diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d84f5a7..89d4d9b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,18 @@ 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 发布)。**M164 的更正**:arm64 那五轮红其实全红在一个误判的编译前体检步(cargo/link 一次 -# 都没跑过,见 `release-build.yml` 顶部),该门禁已拆、判定权交回编译器;但「arm64 能不能出 PE」只能由**下一 tag 的 -# CI** 证明,故名单仍只列 x64(tower 裁决:不拿 x64 的发布去赌一个没验证过的槽位)。真出包之后把键加回那一行即可。 -# 四档 Linux/macOS 从未验证,默认连构建都不跑(M139:省配额、不留红叉);整表六槽位取证走 +# 发布流水线:tag `v*` → **默认构建四个槽位**(原生 `windows-latest` / `windows-11-arm` / `ubuntu-22.04` / +# `ubuntu-22.04-arm`)→ 单文件产物 + SHA256SUMS → artifact + GitHub Release。**push tag 默认创建稳定版**(M163:不带 +# `--prerelease`,否则 `releases/latest` 永远 404、自更新通道取不到清单);同 tag 重跑幂等(覆盖资产与说明、不 create、 +# 不碰标记)。 +# **构建 ≠ 发布**:只有 `PUBLISH_SLOTS` 里的槽位才进 Release 与 SHA256SUMS ——**M184 起是 `windows-x64` + `linux-x64`**。 +# `linux-x64` 本轮翻牌的依据在 `packaging/docs/linux-evidence.md`:x86_64 槽位在 `ubuntu-22.04`(glibc 2.35,与本地 +# WSLg 验证同一地板)真编出 release 产物、真开窗渲出一帧,`release-build.yml` 里还有一条同槽位的 xvfb 运行时门禁 +# ——「声明为 ci 就必须真能跑出帧」,不是「矩阵里写了就算支持」。 +# **`linux-arm64` 本轮只构建、不发布**:它没有任何运行时证据(没有 ARM64 真机跑过一帧,也没有 arm64 的 xvfb 冒烟)。 +# 解锁条件写在 `packaging/release-notes.md` 与 `packaging/docs/compliance-support-boundaries.md` §9 ——真机跑一帧之后 +# 才把键加进 `PUBLISH_SLOTS`,不拿没跑过的包去发(M184 tower 裁决)。 +# `windows-arm64` 仍留矩阵里取证、红了不挡 x64 发布(M142/M164:它那五轮红全红在一个误判的编译前体检步,该门禁已拆, +# 判定权交回编译器)。`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::` 明说未发布。 @@ -31,7 +36,7 @@ on: workflow_dispatch: inputs: all_slots: - description: "构建全部六个槽位(含未验证的 Linux/macOS 四档);默认只跑两个 Windows" + description: "构建全部六个槽位(含未验证的 macOS 两档);默认只跑四个(Windows 两档 + Linux 两档)" type: boolean default: false prerelease: @@ -58,7 +63,7 @@ jobs: # M139:矩阵不写死在 `build` 里,改由本 job 按 `inputs.all_slots` 生成 —— 矩阵不能带 `if`,而 `jobs..strategy` # 可读 `needs`,故用 `fromJSON(needs.plan.outputs.matrix)`。**槽位表只在下面这一处**,改槽位只改这里。 plan: - name: 选槽位(默认两个 Windows) + name: 选槽位(默认四个:Windows 两档 + Linux 两档) runs-on: ubuntu-latest outputs: matrix: ${{ steps.pick.outputs.matrix }} @@ -85,11 +90,18 @@ jobs: # 原生还让 `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` 时照编 - # (将来验证 Linux/macOS 的唯一入口,别删)。真机验过后:mode 改 "ci" + 键加进 PUBLISH_SLOTS + - # 同步 README §1/§9 与 release-notes.md。Linux 固定 22.04 而非 latest:否则 glibc 地板跟着 runner 漂移。 - dict(slot="linux-x64", os="ubuntu-22.04", build_dir="target/release", rustflags="", mode="probe"), - dict(slot="linux-arm64", os="ubuntu-22.04-arm", build_dir="target/release", rustflags="", mode="probe"), + # ↓ Linux 两档(M184):**都改为 `mode: ci`** —— tag 推送默认就编,不再只在 `all_slots=true` 的取证跑里出现。 + # Linux 固定 22.04 而非 latest:否则 glibc 地板跟着 runner 漂移(22.04 = glibc 2.35,与本地 WSLg 验证同一地板)。 + # 两档的差别只在「有没有运行时证据」,而这件事由 `PUBLISH_SLOTS`(发布侧那一行)决定,不在这里: + # `linux-x64` 已真机验过(release 产物在 WSLg 真窗口渲出一帧 + 同槽位的 xvfb 门禁,证据见 + # `packaging/docs/linux-evidence.md`)⇒ 进名单;`linux-arm64` 只有构建、**运行时完全未验**(没有 arm64 真机 + # 跑过一帧)⇒ 不进名单,产物照出 artifact 供取证,靠 `check_publish_slots.py` 打 `::warning::` 明说未发布。 + # **别把 arm64 直接加进名单**:解锁条件是真机跑一帧,不是「构建绿了」。 + dict(slot="linux-x64", os="ubuntu-22.04", build_dir="target/release", rustflags="", mode="ci"), + dict(slot="linux-arm64", os="ubuntu-22.04-arm", build_dir="target/release", rustflags="", mode="ci"), + # ↓ 两档 macOS **未验证,不纳入 CI**:从未真构建成功、也没在真机跑过,默认一个都不编;`all_slots=true` 时照编 + # (将来验证 macOS 的唯一入口,别删)。真机验过后:mode 改 "ci" + 键加进 PUBLISH_SLOTS + + # 同步 README §1/§9 与 release-notes.md。 # macOS:latest = arm64;Intel x64 用 macos-15-intel(macos-13 已下线)。 dict(slot="macos-arm64", os="macos-latest", build_dir="target/release", rustflags="", mode="probe"), dict(slot="macos-x64", os="macos-15-intel", build_dir="target/release", rustflags="", mode="probe"), @@ -126,8 +138,9 @@ jobs: release: name: GitHub Release needs: build - # `always()` + `!cancelled()`:白名单外的槽位(现在的 `windows-arm64`,以及只在 `all_slots` 取证跑里存在的四档) - # 红了**不能连坐** x64 发布 —— 真正拦发布的是被调方「发布白名单齐全性」那一步:白名单里少任何一个才 fail。 + # `always()` + `!cancelled()`:白名单外的槽位(`windows-arm64`、`linux-arm64`,以及只在 `all_slots` 取证跑里 + # 存在的 macOS 两档)红了**不能连坐**那两档已发布的 —— 真正拦发布的是被调方「发布白名单齐全性」那一步: + # 白名单里少任何一个才 fail(M184 之后 `linux-x64` 也在白名单里,它红了就该拦)。 # tag 门禁留在这里(不在被调方):分支 ref 的 dispatch 连发布 job 都不该起。 if: always() && !cancelled() && github.ref_type == 'tag' # `contents: write` 必须由**调用方**授予:可复用工作流只能收窄权限、不能提权(交集语义)。 diff --git a/README.md b/README.md index 99bc5fd..1e630cf 100644 --- a/README.md +++ b/README.md @@ -220,17 +220,24 @@ This list is the reason to believe the rest. None of it is hidden behind marketi section*, *Add override*. Each writes `[page] 占位动作 …` to stderr and changes nothing — instance management from the GUI is the instance-wizard work still outstanding, and settings change as you edit them rather than on a Save. -- **Linux and macOS artifacts are unverified, and this release does not even build them.** The - reference machine is Windows; the `linux-x64`, `linux-arm64`, `macos-arm64` and `macos-x64` slots - sit in the CI matrix but the default release run skips them, and they have never been downloaded and - run end to end. `gh workflow run release.yml -f all_slots=true` builds them for evidence. Treat them - as untested output, not as "supported because the matrix lists them". -- **macOS is not finalised.** It has two rounds of real-device evidence (Apple Silicon), which is why - the build exists — but the menu-bar name, the placement of the three window buttons (macOS puts - them top-left; ours are drawn top-right), and 300s+ steady state still need a real machine. macOS - builds are unsigned, so the first launch needs the Gatekeeper quarantine attribute cleared - (`xattr -d com.apple.quarantine`). The macOS runtime smoke job is a manual `workflow_dispatch` - probe with `continue-on-error`, not a gate. +- **Linux x64 is published, and "published" means "it runs", not "it looks finished".** The release workflow + builds `linux-x64` on a native Ubuntu 22.04 runner by default and ships it, because the release artifact itself + was started under a real display server (WSLg), opened a window, rendered a non-blank frame and printed its + theme/backend lines — and that same check is now a per-slot CI gate on every release + (`packaging/docs/linux-evidence.md` has the raw output). Nobody has yet looked at it on a real desktop + distribution: dragging, resizing, font metrics and CJK shaping on a real desktop session are unverified. + Two things from the Windows list are simply not code paths on Linux, so there is nothing to verify: whole-window + transparency needs a native window handle the app does not have there (`hwnd=none`), and rounded corners are + delegated to the desktop compositor (`corner=policy=system`) instead of being drawn by the app. + `linux-arm64` is built (native `ubuntu-22.04-arm`) but **not** published — no ARM64 machine has ever run it, so + the artifact stays as build evidence only. +- **macOS is not built by the release workflow, and it is not finalised.** It has two rounds of real-device + evidence (Apple Silicon), which is why the build exists at all — but the menu-bar name, the placement of the + three window buttons (macOS puts them top-left; ours are drawn top-right), and 300s+ steady state still need a + real machine. macOS builds are unsigned, so the first launch needs the Gatekeeper quarantine attribute cleared + (`xattr -d com.apple.quarantine`). The macOS runtime smoke job is a manual `workflow_dispatch` probe with + `continue-on-error`, not a gate; `gh workflow run release.yml -f all_slots=true` builds the two slots for + evidence, but they are never published. - **The content library is cramped at low window heights**, and the window height is not wired into the page model yet — the page lays itself out for a nominal height rather than the one it gets. - **Launch-side failure text is not structured.** The tile takes a Chinese sentence straight from the @@ -253,25 +260,28 @@ Grab the file for your platform from | Slot | Asset | Shape | |------|-------|-------| | `windows-x64` | `unlucky-launcher__windows_x64.exe` | bare `.exe`, double-click to run | -| `windows-arm64` | `unlucky-launcher__windows_arm64.exe` | bare `.exe` — **not yet published**, see below | -| `linux-x64` | `unlucky-launcher__linux_x64.tar.gz` | one archive, one top-level directory — **not yet published** | -| `linux-arm64` | `unlucky-launcher__linux_arm64.tar.gz` | same — **not yet published** | -| `macos-arm64` | `unlucky-launcher__macos_arm64.tar.gz` | same — **not yet published** | -| `macos-x64` | `unlucky-launcher__macos_x64.tar.gz` | same — **not yet published** | +| `linux-x64` | `unlucky-launcher__linux_x64.tar.gz` | one archive, one top-level directory; glibc 2.35+ and the X11/Wayland + fontconfig stack | +| `windows-arm64` | `unlucky-launcher__windows_arm64.exe` | bare `.exe` — **built, not published**, see below | +| `linux-arm64` | `unlucky-launcher__linux_arm64.tar.gz` | same — **built, not published** (no runtime evidence yet) | +| `macos-arm64` | `unlucky-launcher__macos_arm64.tar.gz` | same — **not built** by the release workflow | +| `macos-x64` | `unlucky-launcher__macos_x64.tar.gz` | same — **not built** by the release workflow | | every published slot | `ull___[.exe]` | bare CLI binary, its own asset | | all | `SHA256SUMS.txt` | `sha256sum -c` eats it directly | -**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. 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. +**This release publishes `windows-x64` and `linux-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 published** is a naming rule, not a downloadable asset. A guard +(`packaging/check_publish_slots.py`) watches all three relations: a slot named in the list whose artifacts are +missing turns the release red (that is also what makes a red `linux-x64` build block the release), artifacts that +exist for an unlisted slot get an explicit "not published" warning instead of being deleted quietly, and a +`mode: ci` slot that produced nothing at all warns too — a red slot must never be visible only as a missing row. + +Of the unlisted rows, CI builds two. `windows-arm64` is built natively on GitHub's `windows-11-arm` runner +(Windows 11 Arm64 + Visual Studio 2026); every ARM64 failure so far came from a pre-build toolchain probe that +aborted before `cargo build` ran, that probe is now evidence-only, and the next CI run is the first real answer to +whether the slot can produce a binary. `linux-arm64` is built natively on `ubuntu-22.04-arm` (same glibc 2.35 floor +as x64) and is unpublished for a different reason: **nothing has ever run it on ARM64 hardware**. One frame plus the +theme/backend log lines on a real ARM64 machine is the unlock condition; until then the artifact is build evidence. 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 @@ -279,6 +289,18 @@ documents are attached to the release next to it — downloading the Windows bin licence. Download links never carry implementation detail in the name, and the binary inside an archive is always called `unlucky-launcher`. +On Linux x64 that is the whole install: + +```bash +tar -xzf unlucky-launcher__linux_x64.tar.gz +cd unlucky-launcher__linux_x64 +./unlucky-launcher # add --software-renderer if there is no usable GPU adapter +``` + +It asks for glibc 2.35+ (the slot is built on Ubuntu 22.04), `libxkbcommon`, `fontconfig` and `libGL`; on +Debian/Ubuntu `sudo apt-get install libxkbcommon0 libfontconfig1 libgl1 libx11-6 libxcursor1 libxrandr2 libxi6` +covers the load-time set, and the bare `ull__linux_x64` beside the archive needs no display at all. + The two shapes are not duplicates. The GUI binary needs the graphics stack when it opens a window (it `dlopen`s X11 / Wayland / GL and links `libfontconfig` at load); `ull` needs nothing beyond glibc, so it runs on a server or a CI container where no window can exist. On Linux that floor is glibc ≥ 2.35 @@ -345,7 +367,7 @@ merge. The macOS probe is not there yet — manual dispatch only, result recorde |----------|---------| | `.github/workflows/ci.yml` | fmt + clippy + test + release build (Windows / Linux matrix); Linux runtime smoke (Xvfb, one real frame, PNG asserted non-blank); macOS runtime probe (manual dispatch only) | | `.github/workflows/guards.yml` | zero-UI guards (`crates/core`, `crates/presenter`, `crates/design`), 400-lines-per-file guard, cargo-deny licence and source guard | -| `.github/workflows/release.yml` | tag `v*` → build both Windows slots (`windows-x64` + `windows-arm64`) on native runners → single-file artifacts + `SHA256SUMS.txt` → GitHub Release (publishes `windows-x64` only; `-f all_slots=true` builds all six slots for evidence) | +| `.github/workflows/release.yml` | tag `v*` → build the four default slots (`windows-x64` + `windows-arm64` + `linux-x64` + `linux-arm64`) on native runners → single-file artifacts + `SHA256SUMS.txt` → GitHub Release (publishes `windows-x64` and `linux-x64`; `linux-x64` also gets a per-slot Xvfb first-frame gate; `-f all_slots=true` builds all six slots for evidence) | | `.github/workflows/release-guard.yml` | release-marker semantics: push tag ⇒ a **stable** release, dispatch `prerelease=true` ⇒ pre-release; the table lives in `packaging/release_marker.py` and the guard mutation-tests itself | ## Architecture and module map diff --git a/README.zh-CN.md b/README.zh-CN.md index 34c11bb..da62a9b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -175,14 +175,18 @@ unlucky-launcher --version # CLI,版本 + 系统摘要 - **有一批按钮是占位。** 实例页:*新建实例*、*导入*、*复制*、*打开文件夹*、*删除*。日志页:*打开文件夹*。 设置页框架:*保存*、*重置本组*、*添加覆盖*。点下去只往 stderr 写一行 `[page] 占位动作 …`,什么都不改 ——图形界面里的实例管理就是还欠着的那块实例向导;设置项是边改边生效,而不是靠「保存」。 -- **Linux 与 macOS 产物未验证,而且本版连构建都不跑。** 参考机是 Windows;`linux-x64`、`linux-arm64`、 - `macos-arm64`、`macos-x64` 四档只躺在 CI 矩阵里,默认的发布流程会跳过它们,也还没被下载下来端到端跑过; - `gh workflow run release.yml -f all_slots=true` 才编它们来取证。把它们当**未经测试的产出**,而不是 - 「矩阵里有就算支持」。 -- **macOS 未定型。** 有两轮真机证据(Apple Silicon),构建因此存在——但菜单栏显示名、三个窗口按钮的位置 - (macOS 习惯在左上,我们自绘在右上)、300s+ 稳态仍需真机复核。macOS 产物未签名,首次启动要自己清掉 - Gatekeeper 隔离标记(`xattr -d com.apple.quarantine`)。macOS 运行期冒烟是手动 `workflow_dispatch` - 的探针、带 `continue-on-error`,不是门禁。 +- **Linux x64 已发布,但「已发布」= 真能跑,不等于观感已定型。** 发布流水线默认就在原生 Ubuntu 22.04 上编 + `linux-x64` 并发出去,依据是:release 产物本身在真显示服务(WSLg)下起过窗、渲出一帧非空白画面、主题与渲染 + 后端的日志行齐全(实测输出见 `packaging/docs/linux-evidence.md`),而且同一条判据已经常驻成发布流水线里 + **这个槽位**的门禁。还没人看过的:真实桌面发行版上的拖动/缩放,以及真桌面会话里的字体度量与中文整形。 + 「整窗透明度」与「圆角」这两条在 Linux 上**根本没有代码路径**(不是未验):前者要窗口句柄,Linux 上拿不到 + (`hwnd=none`);后者交给桌面合成器(`corner=policy=system`),我们不自己画。 + `linux-arm64` 只是**编出来了,没发布**——没有任何 ARM64 机器跑过它一帧,产物只作构建证据留着。 +- **macOS 不由发布流水线构建,也未定型。** 有两轮真机证据(Apple Silicon),构建因此存在——但菜单栏显示名、 + 三个窗口按钮的位置(macOS 习惯在左上,我们自绘在右上)、300s+ 稳态仍需真机复核。macOS 产物未签名, + 首次启动要自己清掉 Gatekeeper 隔离标记(`xattr -d com.apple.quarantine`)。macOS 运行期冒烟是手动 + `workflow_dispatch` 的探针、带 `continue-on-error`,不是门禁;`gh workflow run release.yml -f all_slots=true` + 能编这两档取证,但它们从不发布。 - **内容库页在低窗口高度下会挤**,而且窗口高度还没接进页面模型——这一页按一个名义高度排版,而不是按它 真正拿到的高度。 - **启动侧失败提示未结构化。** 磁贴直接采用启动层抛出的中文句子 @@ -202,22 +206,37 @@ unlucky-launcher --version # CLI,版本 + 系统摘要 | 槽位 | 产物 | 形态 | |------|------|------| | `windows-x64` | `unlucky-launcher_<版本>_windows_x64.exe` | 裸 `.exe`,双击即用 | -| `windows-arm64` | `unlucky-launcher_<版本>_windows_arm64.exe` | 裸 `.exe` —— **暂未发布**,见下 | -| `linux-x64` | `unlucky-launcher_<版本>_linux_x64.tar.gz` | 单个归档,解压出一个顶层目录 —— **暂未发布** | -| `linux-arm64` | `unlucky-launcher_<版本>_linux_arm64.tar.gz` | 同上 —— **暂未发布** | -| `macos-arm64` | `unlucky-launcher_<版本>_macos_arm64.tar.gz` | 同上 —— **暂未发布** | -| `macos-x64` | `unlucky-launcher_<版本>_macos_x64.tar.gz` | 同上 —— **暂未发布** | +| `linux-x64` | `unlucky-launcher_<版本>_linux_x64.tar.gz` | 单个归档,解压出一个顶层目录;要 glibc 2.35+ 与 X11/Wayland + fontconfig 那一套 | +| `windows-arm64` | `unlucky-launcher_<版本>_windows_arm64.exe` | 裸 `.exe` —— **编了、不发布**,见下 | +| `linux-arm64` | `unlucky-launcher_<版本>_linux_arm64.tar.gz` | 同上 —— **编了、不发布**(运行时零证据) | +| `macos-arm64` | `unlucky-launcher_<版本>_macos_arm64.tar.gz` | 同上 —— 发布流水线**不构建** | +| `macos-x64` | `unlucky-launcher_<版本>_macos_x64.tar.gz` | 同上 —— 发布流水线**不构建** | | 每个已发布槽位 | `ull_<版本>_<平台>_<架构>[.exe]` | 裸 CLI 文件,独立资产 | | 全平台 | `SHA256SUMS.txt` | `sha256sum -c` 可直吃 | -**本版只发布 `windows-x64`。** 发布 job 只留 `PUBLISH_SLOTS` 里点名的槽位 +**本版发布 `windows-x64` 与 `linux-x64`,就这两档。** 发布 job 只留 `PUBLISH_SLOTS` 里点名的槽位 (`.github/workflows/release-publish.yml`),其余产物在写 `SHA256SUMS.txt` 之前就被删掉——所以表里标 -**暂未发布** 的行是命名规则,不是本版可下载的资产。两个方向都有守卫(`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`(一行)。 +**不发布** 的行是命名规则,不是本版可下载的资产。三条关系都有守卫(`packaging/check_publish_slots.py`): +名单里点名了却没产物 ⇒ Release 变红(`linux-x64` 构建红了就挡住整次发布,靠的就是这条);产物在、名单没点名 +⇒ 显式打「未发布」警告,不再静默删掉;`mode: ci` 的槽位本次连产物都没有(那一档红了)⇒ 也打警告。 + +不发布的行里 CI 真编两档。`windows-arm64` 在 GitHub 原生 `windows-11-arm`(Windows 11 Arm64 + Visual Studio +2026)上编来取证——**诚实一句**:此前每一次 arm64 红都红在**编译前的工具链体检**,`cargo build` 一次都没跑过; +M164 把那个体检降级成只取证,所以**下一条 tag 的 CI 才是第一次真正问编译器**。`linux-arm64` 在原生 +`ubuntu-22.04-arm`(与 x64 同 glibc 地板)上编,不发布的理由完全不同:**没有任何 ARM64 机器跑过它**。 +解锁条件 = 真机渲出一帧 + 主题/后端日志行,之后才把键加进 `PUBLISH_SLOTS`。 + +Linux x64 的安装就是解开归档直接跑: + +```bash +tar -xzf unlucky-launcher_<版本>_linux_x64.tar.gz +cd unlucky-launcher_<版本>_linux_x64 +./unlucky-launcher # 没有可用 GPU 适配器就加 --software-renderer +``` + +载入期要 glibc 2.35+、`libxkbcommon`、`fontconfig`、`libGL`;Debian/Ubuntu 上 +`sudo apt-get install libxkbcommon0 libfontconfig1 libgl1 libx11-6 libxcursor1 libxrandr2 libxi6` 就够。 +与归档并列的裸 `ull_<版本>_linux_x64` 连显示都不需要。 UNIX 归档里是二进制加 `LICENSE`、`NOTICE`、`README.md`,都在一个顶层目录下。Windows 发的是裸 `.exe` (一个文件装不下这三份文本),所以这三份作为并列资产挂在 Release 上——下 Windows 二进制不会把许可条款 @@ -278,7 +297,7 @@ cargo deny check licenses bans sources # 依赖许可(CI 必 |----------|------| | `.github/workflows/ci.yml` | fmt + clippy + test + release build(Windows / Linux 矩阵);Linux runtime smoke(Xvfb 真渲染一帧、断言 PNG 非空白);macOS 运行期探针(仅手动 dispatch) | | `.github/workflows/guards.yml` | `crates/core` / `crates/presenter` / `crates/design` 零 UI 守门、单文件 ≤400 行守门、cargo-deny 许可与来源守门 | -| `.github/workflows/release.yml` | tag `v*` → 构建两个 Windows 槽位(`windows-x64` + `windows-arm64`,各自原生 runner)→ 单文件产物 + `SHA256SUMS.txt` → GitHub Release(只发布 `windows-x64`;`-f all_slots=true` 才整表六槽位取证) | +| `.github/workflows/release.yml` | tag `v*` → 构建默认四档(`windows-x64` + `windows-arm64` + `linux-x64` + `linux-arm64`,各自原生 runner)→ 单文件产物 + `SHA256SUMS.txt` → GitHub Release(发布 `windows-x64` 与 `linux-x64`;`linux-x64` 另有一条同槽位的 Xvfb 首帧门禁;`-f all_slots=true` 才整表六槽位取证) | | `.github/workflows/release-guard.yml` | 发布标记语义守门:push tag ⇒ **稳定版**、dispatch `prerelease=true` ⇒ 预发布;标记表在 `packaging/release_marker.py`,守门会对自己做变异自测 | ## 架构与模块地图 diff --git a/docs/images/linux-x64-wslg-window.png b/docs/images/linux-x64-wslg-window.png new file mode 100644 index 0000000000000000000000000000000000000000..bd972fab3d7349f63306c34ae8b0a471747f8c2e GIT binary patch literal 43084 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 literal 0 HcmV?d00001 diff --git a/packaging/README.md b/packaging/README.md index 3c3959e..0d9e29d 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -45,6 +45,10 @@ - 闸门下限 ≈「至少画出一条标题栏」(review 实测切片仍 pass):证明「场景被真实渲染过」,**不证明界面元素齐全, 更不证明视觉正确**——视觉靠人眼看 artifact。 - 只覆盖 Linux x64;macOS 是 dispatch-only 探针(无实测证据),Windows 已有本机人眼验收。 +- (M184)`linux-x64` 已进发布名单,所以这条**同类**判据也常驻在发布流水线里:`release-build.yml` 的 + `Linux x64 运行时冒烟` 步用同一个 `check_screenshot.py` 判 release 产物的首帧,外加 `[theme]` / + `[backend] final=… result=started` 两行日志断言。证据账(含真机 WSLg 实测输出)在 + [`packaging/docs/linux-evidence.md`](docs/linux-evidence.md)。 **本地复跑**:`bash packaging/linux-smoke-docker.sh . D:/tmp/ull-smoke`——脚本逐步复刻这个 job(同样的 apt 依赖、 `--bin unlucky-launcher-iced`、同样的 `xvfb-run … --software-renderer --screenshot --data-dir`);输出目录别写 `/tmp`, @@ -64,10 +68,11 @@ 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/check_publish_slots.py` | 发布名单守卫(M164/M184):名单 ⇄ 产物两个方向 + `mode: ci` 槽位无产物的警告;`--self-test` 注入 11 条回归 | ≤400 | | `packaging/check_test_source_off.py` | 测试通道守门(M169):workflow / 打包脚本 / `Cargo.toml default` 都不许打开 `update-test-source`;归属按 `jobs.*.steps[]` 列表项判、默认拒绝;`--self-test` 按 `tests/guard_vectors.json` 注入 | ≤400 | | `packaging/tests/guard_vectors.json` | 守门自测向量(基线 + 32 条:26 红 / 6 绿),数据与代码分开 | ≤400 | | `packaging/ci-runtime-smoke.md` | §11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 | +| `packaging/docs/linux-evidence.md` | Linux 槽位的证据账(M184):现有证据盘点、WSLg 真机实测输出、xvfb 复跑、CI 增量估算、未验清单 | ≤400 | | `packaging/linux-smoke-docker.sh` | 本地复跑 `runtime-smoke`(Docker + xvfb) | ≤400 | | `packaging/linux-check-docker.sh` | 本地复跑 `ci.yml` 的两条 ubuntu 门禁(clippy / test,Docker) | ≤400 | | `.github/workflows/release.yml` | tag 触发 → 选槽位(`plan`)→ 调下面两个可复用工作流 | ≤400 | diff --git a/packaging/check_publish_slots.py b/packaging/check_publish_slots.py index 827b4e3..62019bc 100644 --- a/packaging/check_publish_slots.py +++ b/packaging/check_publish_slots.py @@ -17,8 +17,12 @@ 3. **名单里每一项的 `mode` 都必须是 `ci`**:`mode: probe` 的槽位默认构建根本不跑,写进发布名单等于 每一轮都缺一档 —— 一个自己写死要红的组合。 4. **产物 ⇄ 名单两个方向**(`--dist`,只在发布侧有产物时跑): - - 名单声明了但没有 GUI 或 CLI 产物 ⇒ `::error::` + exit 1(少一档不发版,这条现在是脚本、本地可跑); - - 有产物但名单没声明 ⇒ `::warning::` 明说「未发布、会被裁掉」,**不许静默**; + - 名单声明了但没有 GUI 或 CLI 产物 ⇒ `::error::` + exit 1(少一档不发版)。声明为发布的多半就是 `mode: ci` + 的槽位,所以这条同时是「**声明为 ci 的槽位构建红了必须挡住发布**」的判据 —— 红在 build 矩阵里的那一档不会 + 让发布跳过(`release` job 是 `always()`),挡住它的就是这里; + - 有产物但名单没声明 ⇒ `::warning::` 明说「未发布、会被裁掉」,**不许静默**(M184 起 `linux-arm64` 正好是 + 这一档:它 `mode: ci`、每轮都编,但运行时未验所以不在名单,产物留着取证); + - `mode: ci` 却本次没有产物(且不在名单)⇒ 也打 `::warning::`:那一档的构建很可能红了,不能只表现为「表里少一行」; - 无论哪种,最后都打一张全槽位表:发布 / 未发布(无产物)/ 未发布(名单没声明)。 用法(零第三方依赖,本地与 CI 同一份代码): @@ -28,7 +32,8 @@ python3 packaging/check_publish_slots.py --self-test # 注入回归,断言每条都变红 `--self-test` 是这条守门的自测证据:把两个 workflow 复制进临时目录、造合成 dist,逐项注入真实回归 -(漏一个资产 / 名单拼错 / 名单写了 probe 档 / 名单声明了但产物缺 / 名单没声明却有产物),断言守门 +(漏一个资产 / 名单拼错 / 名单写了 probe 档 / 名单声明了但产物缺 / 名单没声明却有产物 / +`mode: ci` 却没产物 / 声明的 `linux-x64` 构建红了 / 提前把 `linux-arm64` 写进名单),断言守门 该红的红、该只警告的不红。守门抓不住回归时它自己先红。 """ @@ -150,7 +155,12 @@ def check_dist(dist: Path, declared: list[str], matrix: dict[str, str]) -> tuple 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)}*`)") + suffix = ( + "(它是 mode=ci 的槽位:构建红了就必须挡住本次发布,别把缺一档的 Release 发出去)" + if matrix.get(slot) == "ci" + else "" + ) + errors.append(f"发布白名单缺产物:{slot} 的 {role}(`{prefix_for(role)}_*_{stem(slot)}*`){suffix}") verdict = "发布" if not missing else "缺产物 ⇒ 不发版" wanted.discard(slot) elif present: @@ -160,6 +170,14 @@ def check_dist(dist: Path, declared: list[str], matrix: dict[str, str]) -> tuple "若这是修复后的首次成功构建,把槽位加进名单即可(齐全性检查与 SHA256SUMS 自动跟着扩)" ) verdict = "未发布(名单没声明)" + elif matrix.get(slot) == "ci": + # 声明为「默认在 CI 里构建」的槽位本次却没产物:那一档的构建红了(或矩阵被改窄了)。它不在名单里 + # ⇒ **不该**让发布变红(arm64 的策略就是「红了不挡发布」),但也不能静默:否则「又红了」只剩翻 job 列表。 + warnings.append( + f"{slot} 是 mode=ci 的槽位(tag 推送默认就编)却本次没有产物:那一档的构建红了或被裁了," + "本次未发布。它不在 PUBLISH_SLOTS 里,故不影响本次发布" + ) + verdict = "未发布(ci 槽位本次无产物)" else: verdict = "未发布(本次没有产物)" rows.append(f"{slot:<20} {'声明' if is_declared else '未声明':<8} {'、'.join(present) or '无':<15} {verdict}") @@ -269,11 +287,12 @@ def self_test() -> int: set_declared(typo, "windows-aarch64") cases.append(("名单拼错成 windows-aarch64", typo, fake_dist(Path(tmp) / "c4d", ("windows-aarch64",)), None, True)) - # 5. 名单写了 probe 档(默认不构建)⇒ 必须红。 + # 5. 名单写了 probe 档(默认不构建)⇒ 必须红。**要挑仍是 probe 的槽位**:`linux-x64` 已翻成 `mode: ci`, + # 再拿它当样例,这条用例就从「必须红」变成「本来该绿」——自测跟着矩阵走,否则它自己先变成假证据。 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)) + set_declared(probe, "windows-x64 macos-arm64") + cases.append(("名单写了 mode=probe 的槽位", probe, fake_dist(Path(tmp) / "c5d", ("windows-x64", "macos-arm64")), None, True)) # 6. 运行期名单与文件里那一行不一致 ⇒ 必须红(防步内覆盖)。 cases.append(("运行期名单与文件不一致", base, fake_dist(Path(tmp) / "c6", tuple(declared)), ["windows-arm64"], True)) @@ -284,6 +303,20 @@ def self_test() -> int: set_declared(flip, " ".join([*declared, "windows-arm64"])) cases.append(("名单加上 windows-arm64 且产物齐全(下一 tag 的翻牌预演)", flip, fake_dist(Path(tmp) / "c7d", (*declared, "windows-arm64")), None, False)) + # 8. (M184 核心)名单里的 `linux-x64` 构建红了(没有它的产物)⇒ **必须挡住发布**:`release` job 是 + # `always()`,构建矩阵里红一档不会把发布跳过;挡住它的只能是这条。 + dist = fake_dist(Path(tmp) / "c8", tuple(s for s in declared if s != "linux-x64") + ("linux-arm64",)) + cases.append(("名单声明的 linux-x64 构建红了(缺产物)", base, dist, None, True)) + + # 9. (M184 真实场景)`linux-arm64` 构建成功但不在名单 ⇒ 只警告、不挡发布(它是 `mode: ci` 的槽位,每轮都编)。 + cases.append(("linux-arm64 构建成功但不在名单(只警告)", base, fake_dist(Path(tmp) / "c9", (*declared, "linux-arm64")), None, False)) + + # 10. 反过来:有人把 `linux-arm64` 提前写进名单、而它没构建出来 ⇒ 必须红(解锁条件还没满足)。 + early = Path(tmp) / "c10" + snapshot(early) + set_declared(early, " ".join([*declared, "linux-arm64"])) + cases.append(("名单提前声明 linux-arm64 但缺产物", early, fake_dist(Path(tmp) / "c10d", tuple(declared)), None, True)) + for label, root, dist, expected, want_red in cases: errors, warnings, _rows = run_checks(root, dist, expected) got_red = bool(errors) @@ -295,13 +328,29 @@ def self_test() -> int: 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) + # 第 3 / 9 条还要断言**确实打了警告**:不静默才是这两条规则的重点。 + for ghost in ("windows-arm64", "linux-arm64"): + _e, warnings, _r = run_checks(base, fake_dist(Path(tmp) / f"c-warn-{ghost}", (*declared, ghost)), None) + if not any(ghost in w for w in warnings): + print(f"::error::自测失败:产物在但名单没声明时没有针对 {ghost} 的 ::warning::", file=sys.stderr) + failures += 1 + else: + print(f"OK: 产物在但名单没声明 → 有 {ghost} 的 ::warning::") + + # 第 9 条的另一半:`mode: ci` 的槽位**连产物都没有**(构建红了)时也要有注解,不能只在表里少一行。 + ci_slots = [slot for slot, mode in matrix_slots((base / MATRIX_YML).read_text(encoding="utf-8")).items() if mode == "ci"] + missing_ci = [slot for slot in ci_slots if slot not in declared] + if not missing_ci: + print("::error::自测失败:没有「mode=ci 但不在名单」的槽位,这条用例失去意义(矩阵是不是被改窄了?)", file=sys.stderr) failures += 1 else: - print("OK: 产物在但名单没声明 → 有 windows-arm64 的 ::warning::") + _e, warnings, _r = run_checks(base, fake_dist(Path(tmp) / "c-ci-red", tuple(declared)), None) + for slot in missing_ci: + if not any(slot in w and "ci" in w for w in warnings): + print(f"::error::自测失败:{slot} 是 mode=ci 却没产物时没有 ::warning::(红在哪都不知道)", file=sys.stderr) + failures += 1 + else: + print(f"OK: {slot} 是 mode=ci 却无产物 → 有 ::warning::") if failures: return 1 diff --git a/packaging/docs/artifacts-and-version.md b/packaging/docs/artifacts-and-version.md index 09db419..5ed1abb 100644 --- a/packaging/docs/artifacts-and-version.md +++ b/packaging/docs/artifacts-and-version.md @@ -9,9 +9,9 @@ | 槽位 | 本版发布 | 产物文件名 | 形态 | 归档内容 | |------|---------|-----------|------|---------| | `windows-x64` | ✅ 发布 | `unlucky-launcher__windows_x64.exe` | **裸 exe**(双击即用) | exe 单文件;三份合规文本作为并列 Release 资产 | -| `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 构建) | 同上 | +| `linux-x64` | ✅ 发布 | `unlucky-launcher__linux_x64.tar.gz` | 单二进制归档 | `unlucky-launcher` + `LICENSE` + `NOTICE` + `README.md`;CI 在原生 `ubuntu-22.04`(glibc 2.35 地板)上构建,**并有同槽位的 xvfb 运行时门禁**(真渲一帧 + `[theme]` / `[backend] final` 日志断言 + 像素判定);真机验收证据见 `docs/linux-evidence.md` | +| `windows-arm64` | ❌ 不发布(构建取证中) | `unlucky-launcher__windows_arm64.exe` | **裸 exe**(同 x64 策略) | 同上;CI 在**原生** `windows-11-arm`(Windows 11 Arm64 + VS2026 镜像)上构建,**M164 起编译前体检只取证、不再判定红绿**(此前五轮全红其实全红在那个误判的体检步),**红了不挡发布**(M142,见 §3) | +| `linux-arm64` | ❌ 已构建、不发布(运行时零证据) | `unlucky-launcher__linux_arm64.tar.gz` | 单二进制归档(原生 `ubuntu-22.04-arm` 构建,与 x64 同 glibc 地板) | 同上;M184 起**默认就编**(`mode: ci`),但没有任何 ARM64 真机跑过一帧 ⇒ 不进 `PUBLISH_SLOTS`。解锁条件 = 真机渲出一帧 + 主题日志 | | `macos-arm64` | ❌ 不构建(未验证) | `unlucky-launcher__macos_arm64.tar.gz` | 单二进制归档 | 同上 | | `macos-x64` | ❌ 不构建(未验证) | `unlucky-launcher__macos_x64.tar.gz` | 单二进制归档 | 同上 | | 每槽位 | 随所属槽位 | `ull__<平台>_<架构>[.exe]` | **裸 CLI 文件**(独立资产,不进 tar.gz) | 零 UI 依赖的 `ull` 本体,进同一份 `SHA256SUMS.txt` | @@ -20,10 +20,12 @@ 约定细节: -- **「不构建」= 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` 自动跟着扩。 +- **「不构建」= CI 默认根本不跑**(M139):macOS 那两档未验证,产物不给用户、构建配额也不花;要整表六槽位取证走 + `gh workflow run release.yml -f all_slots=true`(§3)。发布白名单**是 `windows-x64` + `linux-x64`**(M184)。 + 「已构建但不发布」是独立的一档(`linux-arm64`、`windows-arm64`):它们 `mode: ci`、每轮都编,产物留在 + `dist-` artifact 里取证,裁剪步会把它们剔出 Release。 + (M164/M184)名单 ⇄ 产物两个方向都有守卫 `packaging/check_publish_slots.py`:声明了缺产物 ⇒ 红;有产物但没声明 ⇒ + `::warning::` 明说「未发布、会被裁掉」;`mode: ci` 却本次没产物 ⇒ 也打 `::warning::`(那一档红了要看得见)。 - **产物命名**:`<名字>_<版本号>_<平台>_<架构>`,下划线分段、**不带实现细节字样**(不出现 `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 dee484b..b076911 100644 --- a/packaging/docs/build-and-ci.md +++ b/packaging/docs/build-and-ci.md @@ -32,15 +32,15 @@ python packaging/make_artifacts.py --slot windows-arm64 \ `--slot` 与宿主槽位不一致时,`ull --version` 冒烟会**自动跳过并打印原因**(arm64 产物在 x64 宿主机上根本执行不了;拿"跳过"当通过是假证据,所以跳过永远带原因)。**CI 不走交叉**(M140): -`windows-arm64` 在原生 `windows-11-arm` 上编,宿主=产物架构 → 冒烟**真跑**;`linux-arm64` 的同类 -冒烟只在 `all_slots=true` 的取证跑里出现(`ubuntu-22.04-arm` 原生 runner 上真跑 `ull --version`)。 +`windows-arm64` 在原生 `windows-11-arm` 上编,宿主=产物架构 → 冒烟**真跑**;`linux-arm64` 也是原生 +`ubuntu-22.04-arm` 构建 → 冒烟在 runner 上真跑(M184 起它默认就编,不再只在 `all_slots=true` 的取证跑里出现)。 -### CI(默认只跑两个 Windows 槽位;`all_slots=true` 才整表六槽位) +### CI(默认跑四个槽位:Windows 两档 + Linux 两档;`all_slots=true` 才整表六槽位) `.github/workflows/release.yml`: - 触发:`push` tag `v*`;`workflow_dispatch` 只构建不发布,且多一个 `all_slots` 开关(**默认 false = - 只跑两个 Windows 槽位**,true = 整表六槽位都编一遍取证)。`inputs.prerelease` **只在 tag ref 上 dispatch + 只跑四个槽位**,true = 整表六槽位都编一遍取证)。`inputs.prerelease` **只在 tag ref 上 dispatch 才可达**——Actions 页面的 "Run workflow" 只能选分支,要跑 tag 得 `gh workflow run release.yml --ref v0.2.3`。 分支 ref 的 dispatch 与 push 一样被 `release` job 的 `if: github.ref_type == 'tag'` 挡在发布之外,所以 「分支 ref 的 dispatch 只构建不发布」与「tag ref 上 dispatch 能选标记」是两条互不冲突的规则。 @@ -48,8 +48,15 @@ python packaging/make_artifacts.py --slot windows-arm64 \ 不能带 `if`,而 `strategy` 可读 `needs`),槽位表只在该 job 里有一份:`windows-latest` x64 与 **原生** `windows-11-arm` arm64(M140;后者是公开仓免费的标准 arm64 runner,4C/16G)——两者都是 MSVC + `-C target-feature=+crt-static`、产物都在 `target/release`,标 `mode: ci`(默认编); - `ubuntu-22.04`(glibc 2.35 地板)/ `ubuntu-22.04-arm`(原生 ARM64,同地板)/ `macos-latest` / - `macos-15-intel` 标 `mode: probe`——**未验证,默认一个都不编**,只有 `all_slots=true` 才进矩阵。 + (M184)`ubuntu-22.04`(glibc 2.35 地板)与 `ubuntu-22.04-arm`(原生 ARM64,同地板)**也标 `mode: ci`** —— + tag 推送默认就编这两档;只剩 `macos-latest` / `macos-15-intel` 是 `mode: probe`,**未验证、默认一个都不编**, + 只有 `all_slots=true` 才进矩阵。 +- **运行时证据决定发布,构建绿了不够**:`linux-x64` 进 `PUBLISH_SLOTS` 的依据是「release 产物真开窗渲出一帧」 + (真机 WSLg + 截图 + `[theme]` / `[backend] final` 日志,实测输出见 `packaging/docs/linux-evidence.md`), + 所以 `release-build.yml` 给**这个槽位**配了一条 xvfb 运行时冒烟步(`if: inputs.slot == 'linux-x64'`): + 装 xvfb → `xvfb-run … --software-renderer --screenshot` 起 **release** 产物 → 断言 `[theme] source=` 与 + `[backend] final=… result=started` 两行 → `check_screenshot.py` 判像素 → 上传首帧图。 + `linux-arm64` **刻意没有**这条判据:arm64 上这条路一次都没验过,给它加一条没验证过的门禁就是造红。 - 每个槽位 `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` 后显式导出 @@ -64,16 +71,25 @@ python packaging/make_artifacts.py --slot windows-arm64 \ 自相矛盾(`um/arm64` 明明在)。现在那一步只打印 vswhere 组件查询、各目录 `.lib` **计数**与 `link.exe` 候选, 永不 `exit 1`;**红绿只由编译器 / 链接器 / 静态 CRT 导入表门禁决定**。runner 实况:`windows-11-arm` 自 2026-09 是 Windows 11 Arm64 + **Visual Studio 2026** 镜像,`VC.Tools.ARM64` 与 `Windows11SDK.22621/26100` 都已装好。 -- **发布白名单(`PUBLISH_SLOTS`,唯一开关)**:只有白名单里的槽位进 Release 与 `SHA256SUMS.txt`。**M142 起只有 - `windows-x64`**:齐全性检查是「名单内少一档就 fail」,挂着没验证过的 arm64 就等于 x64 编成功也发不出去。 - (M164)arm64 那五轮红其实全红在误判的体检步(上一条),门禁拆掉后「arm64 能不能出 PE」只能由**下一 tag 的 CI** - 证明,所以名单这轮仍只列 x64 —— 不拿 x64 的发布去赌(tower M164 裁决)。**可逆且只是一行**:那次 CI 真出包后把 - 键加回 `release-publish.yml` 那一行,再同步 §1/§9 与 `release-notes.md`。四档未验证的(默认连构建都不跑)依旧不推给用户。 -- **名单 ⇄ 产物两个方向都有守卫(M164)**:`packaging/check_publish_slots.py`,同一份代码两处跑 —— PR 阶段 +- **发布白名单(`PUBLISH_SLOTS`,唯一开关)**:只有白名单里的槽位进 Release 与 `SHA256SUMS.txt`。**M184 起是 + `windows-x64 linux-x64`**:齐全性检查是「名单内少一档就 fail」,所以 `linux-x64` 进来之后它的构建红了就挡住整次 + 发布 —— 这正是「声明为 ci 的槽位失败必须阻断发布」那条要求,判据落在 `check_publish_slots.py` 的 `--dist` 模式里 + (`release` job 是 `always()`,构建矩阵里红一档不会让它跳过,挡住它的只能是那道守卫)。 + `linux-arm64` 本轮**只构建不发布**:它没有任何运行时证据(没有 ARM64 真机跑过一帧,也没有 arm64 的 xvfb 冒烟), + **解锁条件 = 真机上真跑出一帧 + 主题日志**,之后才把键加进 `release-publish.yml` 那一行,再同步 §1/§9 与 + `release-notes.md`。`windows-arm64` 同理仍未进名单(M164:它的五轮红全红在误判的体检步,等下一 tag 的 CI 证)。 + **可逆且只是一行**:翻牌只改 `PUBLISH_SLOTS` 那一行,齐全性与 SHA256SUMS 自动跟着扩。 +- **耗时(M184 估算,2 槽位 → 4 槽位)**:多出的两档是**并行**跑的(矩阵 `fail-fast: false`),所以墙钟增量 ≈ + 「`linux-x64` 那一档的时长(含新增 xvfb 冒烟步约 1~2 分钟)」与「`linux-arm64` 那一档的时长」取大者,而不是两者相加; + 真正增加的是**并发作业数与总机时**(约 +2× 单槽位机时,公开仓 arm64 Linux runner 不额外计费)。 + 发布侧只多两个资产的裁剪/摘要/上传(秒级)。细节与逐项拆解见 `packaging/docs/linux-evidence.md`。 +- **名单 ⇄ 产物两个方向都有守卫(M164,M184 扩)**:`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 条回归,本地一条命令可跑。 + `echo drop:`),一个真编出来的 arm64 会被就地删掉而没人知道。M184 补第三条:`mode: ci` 的槽位**本次连产物都没有** + (那一档构建红了)也打 `::warning::`,别只在表里少一行。`--self-test` 注入 11 条回归(含「声明的 `linux-x64` 构建红了 + ⇒ 必须红」「`linux-arm64` 有产物但没声明 ⇒ 只警告」「提前把 `linux-arm64` 写进名单 ⇒ 缺产物必红」),本地一条命令可跑。 - `release` job:下载全部 artifact → **发布名单守卫**(断言 + 未发布提示)→ 按白名单裁剪 → 重算 `SHA256SUMS.txt` → 用 `packaging/release-notes.md` 模板渲染支持矩阵 → 生成更新清单 `manifest.json` → `gh release create` (**push tag 默认建稳定版**:不带任何标记)。 @@ -106,8 +122,9 @@ python packaging/make_artifacts.py --slot windows-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`, - 翻牌时要改的仍是那两处)。 + 搬家时 6 步、主线 7 步)。M184 又在 `release-build.yml` 里给 `linux-x64` 插了「运行时冒烟 + 上传首帧图」两步 + (该文件 11 → 13 步,发布侧仍是 8 步)。改槽位仍只改 `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 a652eb6..cd611df 100644 --- a/packaging/docs/compliance-support-boundaries.md +++ b/packaging/docs/compliance-support-boundaries.md @@ -21,25 +21,32 @@ | 平台 | 本版发布 | 级别 | 运行时要求 | |------|---------|------|-----------| | Windows 10/11 x64 | ✅ | 一等 | 无(UCRT 系统自带) | +| Linux x64 X11 | ✅ | 一等 | glibc 2.35+(`ubuntu-22.04` 原生构建即地板)、xkbcommon、fontconfig、libGL(或软渲染)、xcb | +| Linux x64 Wayland | ✅(同一归档;**Wayland 路径未验**) | 支持 | 上述 + wayland-client(XWayland 应用仍需 X11 那套) | | Windows 11 ARM64 | ❌ 不发布(构建取证中) | 一等(启动器产物) | 无额外库;启动器原生运行,游戏本体走系统 x64 模拟层 + x64 JDK | +| Linux arm64 | ❌ 已构建、不发布(**运行时零证据**) | 一等(未真机验证) | glibc 2.35+(`ubuntu-22.04-arm` 原生构建,**与 x64 同地板**)、xkbcommon、fontconfig、libGL、xcb / wayland-client | | macOS 12+(arm64 / x64) | ❌ 不构建 | 一等(未真机验证) | 无第三方库;未签名需去 quarantine | -| Linux x64 X11 | ❌ 不构建 | 一等(未真机验证) | glibc 2.35+、xkbcommon、fontconfig、libGL、xcb | -| Linux x64 Wayland | ❌ 不构建 | 支持 | 上述 + wayland-client | -| Linux arm64 X11 | ❌ 不构建 | 一等(未真机验证) | glibc 2.35+(`ubuntu-22.04-arm` 原生构建,**与 x64 同地板**)、xkbcommon、fontconfig、libGL、xcb | -| Linux arm64 Wayland | ❌ 不构建 | 支持 | 上述 + wayland-client | | Windows 8.1/8/7 | — | 不支持 | 基线已上调(D21) | | Android Termux + X11 | — | 试验级(不承诺) | CLI 与 GUI 均需源码自建;GUI 已在单机真机验证两条路径各一次(CPU 软渲染 + 自编 Mesa/turnip GPU 加速,配方见 `packaging/docs/termux-glibc-build.md`、证据见 `README.termux.md`),不在本发布矩阵内 | `packaging/release-notes.md` 的 "Support matrix" 表与上表逐行对应(英文版同样带 published / not published 一列,Termux 行标注 experimental)——改一边必须同步另一边,否则标题里的"一致"就是假话。 -两个 arm64 槽位都是**正式矩阵槽位**(不是 Termux 那种源码自建旁路),但本版两个都**不发布**:`windows-arm64` 照构建取证 -(红了不挡 x64 发布),`linux-arm64` 连构建都不跑;发布开关是 `release-publish.yml` 的 `PUBLISH_SLOTS`(当前只有 `windows-x64`)。 +**M184 的翻牌(Linux x64 进发布名单,arm64 只构建)**:发布开关仍是 `release-publish.yml` 的 `PUBLISH_SLOTS`, +现在是 `windows-x64 linux-x64`;`release.yml` 槽位表里 `linux-x64` 与 `linux-arm64` 都是 `mode: ci`(tag 推送默认就编)。 +两档的唯一差别是**运行时证据**: -(M164)`windows-arm64` 为什么还没进名单:它 v0.2.1~v0.2.3 的红**全部**红在一个编译前体检步(`find` 猜 SDK 的 +- `linux-x64` 有:release 产物在 Linux 真机(Ubuntu 22.04 + WSLg 真窗口)渲出一帧、`[theme]` 与 + `[backend] final=… result=started` 两行齐全、像素判定通过;`release-build.yml` 里还有一条**同槽位**的 xvfb + 运行时门禁,每轮发布都重跑(证据与实测输出:`packaging/docs/linux-evidence.md`)。 +- `linux-arm64` 没有:没有任何 ARM64 机器跑过它一帧,也没有 arm64 的 xvfb 冒烟。**解锁条件 = 真机(或原生 + ARM64 runner)上真跑出一帧 + 主题日志**,之后才把键加进 `PUBLISH_SLOTS`。构建绿了不算解锁,别拿没跑过的包发人。 + +`windows-arm64` 为什么还没进名单(M164 遗留):它 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 真出包之后,翻一行名单即可。 +出 PE」只能由**下一 tag 的 CI** 证明。名单 ⇄ 产物的两个方向都有守卫(`packaging/check_publish_slots.py`): +声明了缺产物 ⇒ 红(这正是「声明为 ci 的槽位构建红了就挡住发布」那条闸),有产物没声明 ⇒ `::warning::` 明说未发布, +`mode: ci` 却本次没产物 ⇒ 也打 `::warning::`(红了要看得见)—— 所以那次 CI 真出包之后,翻一行名单即可。 ## 10. 已知边界 / 后续 @@ -57,3 +64,9 @@ published 一列,Termux 行标注 experimental)——改一边必须同步 `windows-arm64` / `linux-arm64` 的**真实产物只能由 CI 出**(前者靠 `dumpbin -dependents` 的静态 CRT 门禁 + 架构校验,后者在 `ubuntu-22.04-arm` 上真跑 `ull --version` 冒烟);本机能做且已做的是脚本层 取证(§7.5:宿主判定注入、可执行文件头识别、把真 exe 的 PE 机器码改成 ARM64 当 stub 走完全流程)。 +8. **Linux 上自更新只"能查不能装"**(M184 发布 Linux x64 之后才有意义):清单现在会带 `linux-x64` 的 + `portable-zip` 产物,而 `ull update apply` 只做便携单文件 `.exe` 的自替换 + (`crates/core/src/update/job.rs::ensure_self_replaceable`)——Linux 上它会带具名原因**拒绝** + ("发布里的 linux-x64 产物是 portable-zip 形态,不能原地替换启动器 … 压缩包请解压后再用"), + 不会半装坏。Linux 的更新方式仍是「下载新归档覆盖旧副本」。真要做 Linux 自更新(换目录里的文件再重启) + 是另一件事,未规划。 diff --git a/packaging/docs/linux-evidence.md b/packaging/docs/linux-evidence.md new file mode 100644 index 0000000..b61f7c9 --- /dev/null +++ b/packaging/docs/linux-evidence.md @@ -0,0 +1,205 @@ +# Linux 槽位的证据账(M184) + +本文是「Linux 到底验过什么、没验什么」的**唯一账本**。M184 把 `linux-x64` 写进了 `PUBLISH_SLOTS`(发布), +把 `linux-arm64` 留在「构建但不发布」,两者都不是凭感觉:下面按「已有哪些证据 → 本轮补的实测 → CI 增量 → +仍然没验的部分 → 解锁条件」逐条记账,任何一条被推翻都要回来改这里与 `compliance-support-boundaries.md` §9。 + +## 1. M184 之前的证据盘点 + +| 证据 | 位置 | 覆盖了什么 | 没覆盖什么 | +|------|------|-----------|-----------| +| xvfb 运行时冒烟(每次 PR/main) | `.github/workflows/ci.yml` 的 `runtime-smoke` | `ubuntu-latest`(x64)上装 xvfb + 软渲染依赖 + 字体,`xvfb-run … --software-renderer --screenshot` 真渲一帧,`packaging/check_screenshot.py` 对**像素本身**断言(唯一色数 / 亮度 σ / 强边缘占比),截图作为 artifact 留证 | 只有 x64;用的是 **debug** 档(`cargo build --bin unlucky-launcher-iced --locked`),不是发出去的那个 release 产物;不跑 GPU 通路;不验 Wayland;不校验 `[theme]` / `[backend] final` 日志行;没有 arm64 | +| 本地复跑脚本(开发机) | `packaging/linux-smoke-docker.sh` | 在 `rust:1-bookworm`(**Debian 12 / glibc 2.36**)里逐步复刻上面那个 job;M131 真跑过一次(2026-11-05):`exit=0`、截图 1180×760、`[ok] 截图非空白` | 基础镜像是 Debian 而不是 runner 的 Ubuntu;仍是 debug 档;同样没有 arm64 | +| 编译期门禁复跑 | `packaging/linux-check-docker.sh` | 在 Linux 上跑 `clippy --workspace --all-targets -- -D warnings` 与 `test --workspace --all-targets`(M144 那两条只能在 Linux 复现的红) | 与运行时无关,只证明「编得过、测得过」 | +| 阈值依据 | `packaging/ci-runtime-smoke.md` | 真实帧与纯色帧的实测数值(唯一色 342~2352、σ 19.45~27.46、强边缘 0.0076~0.0433)都在这里,阈值取在两者之间 | 标定样本来自 x64 软渲染 | +| `ull --version` 冒烟 | `packaging/make_artifacts.py::smoke_version` | 槽位与宿主一致时**真跑** CLI 并断言版本串(`linux-x64` 与 `linux-arm64` 在各自的 CI runner 上都满足这条,所以两个 arm64/x64 槽位都有一份 CLI 运行时证据) | 只覆盖 CLI(零图形栈),GUI 一帧都没有;`linux-arm64` 的 GUI 从未启动过 | + +**glibc 地板怎么定的**:槽位表把 Linux 两档钉在 `ubuntu-22.04` / `ubuntu-22.04-arm`,**不跟 `latest` 走** +(`release.yml` 的槽位表注释:跟 latest 走地板就会随镜像漂移)。Ubuntu 22.04 ⇒ glibc **2.35**,这就是 +`README` / §9 写的「glibc 2.35+」的来源,也是本轮真机验收为什么选 Ubuntu 22.04 的 WSL 发行版:**验证环境的地板 +与发布产物的地板一致**,在更新的发行版上验等于放宽了条件。 + +**一处诚实记录**:M184 的任务描述里提到「曾用过 WSLg 开真窗口跑首帧」。本仓库里查不到那次留下的任何文档或 +脚本痕迹(`grep -rn WSLg` 在本轮改动之前零命中),所以下面这一节是**重新做了一遍并留证**的,不是转述。 + +## 2. 本轮实测(WSLg 真机,linux-x64 release 产物) + +**环境**(2026-11-08,`wsl.exe -v` / 发行版内自报,逐字取自实测输出): + +``` +Linux 6.6.114.1-microsoft-standard-WSL2 x86_64 # WSL 2.7.3.0,WSLg 1.0.73 +PRETTY_NAME="Ubuntu 22.04.5 LTS" # == 发布槽位 ubuntu-22.04 的同一代 +ldd (Ubuntu GLIBC 2.35-0ubuntu3.15) 2.35 # == 文档写的 glibc 2.35 地板 +DISPLAY=:0 WAYLAND_DISPLAY=wayland-0 +rustc 1.99.1 / cargo 1.99.1(rust-toolchain.toml 的 stable) +``` + +**构建**(与 CI 同一条命令,12 核 WSL,crates 走 rsproxy 镜像;镜像只影响下载速度,不影响产物): + +``` +cargo build --release --locked --bin unlucky-launcher-iced --bin ull + Finished `release` profile [optimized] target(s) in 9m 38s # BUILD_WALL=579.19s +-rwxr-xr-x 19571328 unlucky-launcher-iced +-rwxr-xr-x 7534848 ull +``` + +**打包**(CI 的同一份脚本 `packaging/make_artifacts.py`,`--slot linux-x64`): + +``` +arch: unlucky-launcher-iced -> x64 (ok for linux-x64) +smoke: ull --version -> 0.2.3 (ok) # 真跑,不是跳过(宿主 == 产物) +arch: ull -> x64 (ok for linux-x64) +artifact: unlucky-launcher_0.2.3_linux_x64.tar.gz 8.05 MiB sha256=c78e9f6cbab44e0fa76912f5d3608b911d89e2de2adc473339341d8ac3b60c13 +artifact: ull_0.2.3_linux_x64 7.19 MiB sha256=42ff569152f1af89899b0925b4a7495bfcaeb66988889a0348722aa2d6ae2369 +sums: SHA256SUMS.txt +``` + +归档解开就是用户拿到的东西(`unlucky-launcher` 0755;`ELF 64-bit LSB pie executable, x86-64, stripped`): + +``` +-rw-r--r-- LICENSE 11345 / NOTICE 4721 / README.md 30542 +-rwxr-xr-x unlucky-launcher 19571328 +$ ./unlucky-launcher --version +unlucky-launcher 0.2.3(unlucky-cli) +os: Ubuntu 22.04.5 LTS · 6.6.114.1-microsoft-standard-WSL2(x86_64)· 满足最低要求 +``` + +**首帧(`--screenshot` 通道,真窗口在 WSLg 的真显示服务里开出来再渲)**,`exit=0`,关键日志行逐字: + +``` +[probe] adapters=0 hardware=false :: request=cpu -> ICED_BACKEND=tiny-skia +[font] platform=linux text="Noto Sans CJK SC" display="Noto Sans CJK SC" mono="DejaVu Sans Mono" | … source=env cjk=not-needed +[shot] 未指定主题,按 Light 出图(D30 可复现;CI 冒烟依赖这条默认值) +[theme] source=default-light flavor=light follows_system=false +[window] requested size=1180x760 min=880x560 resizable=true decorations=false transparent=false scale_requested=system size_source=default title="Unlucky Launcher" +[boot] first frame in 485 ms(run() 开始 → 首次 view,含后端探测与字体扫描) +[shot] 1180x760 scale=1 -> …/wslg-first-frame.png +[shot] screen=shell theme=light renderer=tiny-skia route=Detail("survival") +[backend] final=tiny-skia result=started elapsed=1333ms evidence=probe-single-item +``` + +像素判定(`packaging/check_screenshot.py --json`,图 132637 字节): + +``` +{"width": 1180, "height": 760, "pixels": 896800, "unique_colors": 1858, + "top_color": "#f6faff", "top_color_share": 0.5564, "luma_mean": 241.18, + "luma_stddev": 24.06, "edge_ratio": 0.0372, "problems": []} +[ok] 截图非空白 +``` + +**真的开了一个窗口**(交互模式,不加 `--screenshot`):进程 12 秒后仍存活,WSLg 侧出现 +`msrdc` 窗口 **"Unlucky Launcher (cyrene-wsl)"**,`GetWindowRect` 读回 **1180×760**(正是它请求的尺寸), +从 Windows 侧对这块矩形截屏后**人眼可见**:自绘标题栏三键 + 左侧导航轨(Instances / Versions / Logs / +Settings / Assistant)+ "No instance selected" 空态 + 左下 "Not signed in"。这张图已入库: +[`docs/images/linux-x64-wslg-window.png`](../../docs/images/linux-x64-wslg-window.png) +(1180×760,43084 字节,`check_screenshot.py`: 唯一色 871、σ 14.4、强边缘 0.0137 ⇒ 非空白 —— 比进程内那张低, +因为桌面截屏是**已经合成到 Windows 桌面**的结果,抗锯齿被放大过一次;两张都远在阈值之上)。 +交互模式的日志另有两行值得记: + +``` +[probe] adapters=1 [D3D12 (AMD Radeon(TM) Graphics) [Other]] hardware=false :: request=auto -> ICED_BACKEND=tiny-skia +[probe] 探测耗时 1527 ms(wgpu 实例 + 全后端适配器枚举) +[boot] first frame in 2273 ms(…) +[theme] source=system flavor=pending follows_system=true +[theme] 系统主题 = None +[appearance] hwnd=none(拿不到窗口句柄:整窗 α 无法下发) +[corner] boot corner=policy=system source=square os=unknown excluded_reason=unknown-os radius=system pref=none readback=n/a window=none frame=? layered=? +``` + +读法(别过度解读):① WSLg 能枚举到一个 D3D12 适配器但 `hardware=false`,所以 auto 也落到软渲染 +(`tiny-skia`)—— **GPU 通路在 Linux 上仍然没有证据**;② WSLg 没有"系统主题",所以 `follows_system=true` +而 `系统主题 = None`(真桌面上的系统主题跟随仍未验);③ **整窗透明与圆角在 Linux 上本来就是 no-op**:日志自己说 +`hwnd=none`、`corner=os=unknown`(用户令:这两件事交给桌面合成器,我们不自绘)—— 所以它们不是"未验",而是 +**在 Linux 上没有这条代码路径**。 + +**一处 WSLg 的坑**(写在这里免得下次重复踩):WSLg 的 X 侧只有 Weston 的代理窗口, +`import -window root` / `xwd -root` 一律 `X_GetImage … BadMatch`,**拿不到桌面级截屏**; +真窗口像素要么走进程内 `--screenshot`(CI 就是这么做的),要么从 Windows 侧对 `msrdc` 窗口矩形截屏(本节采用后者)。 + + +## 3. 本轮实测(Xvfb,同一个 release 产物) + +同一条命令在无头 X 里再跑一遍(Ubuntu 22.04 的 Xvfb,**release 产物**,判据与 CI 新增那步逐字一致): + +``` +xvfb-run -a --server-args="-screen 0 1280x1024x24" ./unlucky-launcher --software-renderer \ + --screenshot …/xvfb-first-frame.png --data-dir …/data-xvfb +launcher exit=0 +[theme] source=default-light flavor=light follows_system=false +[boot] first frame in 35 ms(run() 开始 → 首次 view,含后端探测与字体扫描) +[shot] 1180x760 scale=1 -> …/xvfb-first-frame.png +[backend] final=tiny-skia result=started elapsed=1147ms evidence=probe-single-item +{"width": 1180, "height": 760, "pixels": 896800, "unique_colors": 1858, "top_color": "#f6faff", + "top_color_share": 0.5564, "luma_mean": 241.18, "luma_stddev": 24.06, "edge_ratio": 0.0372, "problems": []} +[ok] 截图非空白 +``` + +软渲染是确定性的:WSLg 与 Xvfb 两张图的统计量**逐个相同**(1858 / 24.06 / 0.0372),也正是这一点让 +`check_screenshot.py` 的阈值(唯一色 ≥64、亮度 σ ≥3.0、强边缘 ≥0.001)有 20 倍以上余量 —— CI 里这条门禁不会因为 +"某次抗锯齿抖了"而假红。 + +本机 Docker 复跑(`packaging/linux-smoke-docker.sh`,Debian 12 容器 + **debug** 档,缓存卷是热的): + +``` +$ bash packaging/linux-smoke-docker.sh . C:/Temp/ull-m184/smoke-out +rustc 1.99.1 (58cf8f9c3 2026-10-13) / cargo 1.99.1 / 12.15 # 12.15 = Debian 版本号 + Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 26s +[font] platform=linux text="DejaVu Sans" display="DejaVu Sans" … source=default cjk=not-needed +[theme] source=default-light flavor=light follows_system=false +[boot] first frame in 69 ms(run() 开始 → 首次 view,含后端探测与字体扫描) +[backend] final=tiny-skia result=started elapsed=1736ms evidence=probe-single-item +exit=0 +/out/runtime-smoke.png: 1180x760 唯一色 1756 亮度σ 23.71 强边缘 0.03366 主色 #f6faff(55.7%) +[ok] 截图非空白 +``` + +两条差异值得写下来(说明"为什么容器绿了还不够"):① 容器里没有中文字体,字体解析落到 `DejaVu Sans`,而 WSL 里是 +`Noto Sans CJK SC` —— **字体选择随环境变**,观感类验收只能在真桌面上做;② 基础镜像是 Debian 12(glibc 2.36), +比发布地板 2.35 高一档,所以它能证明"这条运行时通路通",**不能**代替 `ubuntu-22.04` 上的证据(后者见 §2/§3 前面那段)。 + +单次启动的墙钟(`/usr/bin/time`,含 `xvfb-run` 起 X 的开销):**WALL=1.96s**,日志里 `launch elapsed=1188ms`。 + + +## 4. CI 增量:2 个槽位 → 4 个槽位 + +默认矩阵从 **2 档变 4 档**(`windows-x64` / `windows-arm64` / `linux-x64` / `linux-arm64`),多出来的两档是**并行**作业 +(`fail-fast: false`),所以: + +| 项 | 变化 | 依据 | +|----|------|------| +| 并发作业数 | 2 → 4 | `release.yml` 的 `plan` 输出(本地跑那段 python:默认 4 档、`all_slots=true` 6 档) | +| 每档新增步骤 | `linux-x64` 多 **2 步**(xvfb 运行时冒烟 + 上传首帧图);`linux-arm64` 多 **0 步** | `release-build.yml`:11 步 → 13 步(本地 YAML 解析计数) | +| `linux-x64` 那 2 步的耗时 | apt 装 xvfb ≈ 30~50 s,跑一帧 ≈ 2 s,日志/像素断言 ≈ 1 s,上传 ≈ 3 s ⇒ **≈ 1~2 分钟** | 本地 Xvfb 实测单次启动 WALL=1.96s(`/usr/bin/time`) | +| 单档 release 构建时长(**估算**) | 冷缓存 ≈ 20~30 min;`Swatinem/rust-cache` 命中后 ≈ 8~15 min | 本机 12 核实测 **9m38s**;官方 `ubuntu-22.04` runner 是 4 vCPU,编译墙钟按 2.5~3× 估,再乘缓存命中折扣 | +| 总机时(**估算**) | 每 tag **+约 40 分钟**机时(2 档 × ≈20 min) | 同上 | +| 墙钟(**估算**) | 增量 ≈ `max(0, linux 档时长 − 现有最慢档)`,量级 **0~10 分钟**;发布侧多两个资产,秒级 | 四档并行,墙钟由最慢那档决定 | +| 配额风险 | `ubuntu-22.04-arm` 的可用性**本机无法核实**(无 `gh` CLI、无 Actions API 凭据) | 见 §5 最后一条:它不在发布名单里,起不来也不会挡住那两档发布 | + +诚实边界:单档构建时长是**估算**(本机不是 runner,核数、缓存命中率、网络都不同)。下一次 tag 的真实数字应该回来 +把这张表换成实测——**别让估算一直冒充实测**。 + + +## 5. 仍然没验的部分(如实划出) + +- **真实桌面发行版的观感**:拖动 / 缩放 / 字体度量与中文整形,一个都没在真桌面(GNOME/KDE 的可交互窗口)上 + 人眼验收过。本轮做的是「起窗 + 渲出一帧 + 断言像素与日志」,它证明「能跑出画面」,不证明「观感正确」。 + (**整窗透明度与圆角不在这张清单里**:Linux 上它们没有代码路径 —— 见 §2 的三条读法,别把「不适用」写成「未验」。) +- **GPU 通路**:所有 Linux 证据都是软件渲染(`--software-renderer` / tiny-skia)。wgpu→Vulkan/GL 那条路在 Linux + 上一次都没跑过。 +- **Wayland 原生**:WSLg 同时提供 X11 与 Wayland,本轮只按 `DISPLAY` 走(见 §2 的实测环境),没单独验 + `WAYLAND_DISPLAY` 那条路。 +- **linux-arm64 运行时完全未验**:GUI 进程在 ARM64 上一次都没启动过;它只有「编得过 + `ull --version` 跑得过」。 +- **`ubuntu-22.04-arm` runner 的可用性未在本机核实**:本机没有 `gh` CLI,也没有到 Actions API 的可用凭据, + 所以「这档 runner 在这个仓库上真能起来」只能由下一次 CI 证明。风险是有界的:它**不在** `PUBLISH_SLOTS` 里, + 起不来/红了不会挡住 `windows-x64` + `linux-x64` 的发布(只会让那一条 job 红,外加一条 `::warning::`)。 + +## 6. `linux-arm64` 的解锁条件(写死在这里,别凭感觉放行) + +1. 在**真 ARM64 机器**(或原生 arm64 runner)上把 release 产物跑出一个非空白帧,`packaging/check_screenshot.py` + 通过; +2. 同一份日志里有 `[theme] source=…` 与 `[backend] final=… result=started` 两行(证明主题来源与渲染后端都按预期); +3. 把上面两条的原始输出(时间、命令、图尺寸、日志行)贴进本文 §2 的位置; +4. 然后把 `linux-arm64` 加进 `release-publish.yml` 的 `PUBLISH_SLOTS`(一行),并同步 §9 与 `release-notes.md`。 + +构建绿了**不算**解锁,`ull --version` 跑得过也**不算** —— 那两条只证明 CLI 与编译器,用户拿到的那个 `.tar.gz` 里 +装的是 GUI。 diff --git a/packaging/release-notes.md b/packaging/release-notes.md index 0e73fe3..47e7e1b 100644 --- a/packaging/release-notes.md +++ b/packaging/release-notes.md @@ -6,6 +6,11 @@ No WebView, no bundled runtime, no JRE download needed to start the launcher its ## What's new in {{VERSION}} +- **Linux x64 is a published platform.** The release workflow now builds `linux-x64` on a native `ubuntu-22.04` + runner by default (not only in manual all-slots runs) and publishes it: `unlucky-launcher_{{VERSION}}_linux_x64.tar.gz` + plus the bare `ull_{{VERSION}}_linux_x64`. The artifact itself was started on a Linux machine under a real display + server, rendered a frame and logged the expected theme/backend lines before this decision, and the same check is + now a per-slot CI gate. Linux ARM64 is built too, but stays unpublished until an ARM64 machine runs it. - **{{VERSION}} is the first release published as a stable release.** GitHub only points `releases/latest` at the newest **non-prerelease** release, so `0.2.2` — kept as a historical pre-release, untouched — never appeared there. A pushed tag now creates a stable release by default; @@ -32,6 +37,7 @@ Asset names are `___`, underscore separated — t | Platform | Asset | Notes | | --- | --- | --- | | 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) | +| Linux x64 | `unlucky-launcher_{{VERSION}}_linux_x64.tar.gz` | one archive, one top-level directory holding the binary plus `LICENSE` / `NOTICE` / `README.md`; needs glibc 2.35+ and the usual X11/Wayland + fontconfig stack | **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 @@ -40,21 +46,25 @@ Arm64 with Visual Studio 2026), but the build has not delivered an artifact yet, 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`). +one that can actually answer the question. 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 -on real machines yet, so nothing unverified is handed out and no build minutes are spent on them. They -stay out of the download list and out of `SHA256SUMS.txt` until that verification happens; a manual -workflow run can build all six slots for that purpose, but the four extra slots are never published. The -support matrix below marks those slots accordingly. +**Linux ARM64 is built but not published, and it has no runtime evidence yet.** `linux-arm64` joined the default +build matrix in this release (native `ubuntu-22.04-arm`, same glibc 2.35 floor as x64), so the artifact exists as +build evidence, but nothing has ever run it on ARM64 hardware — not one frame. It stays out of the download list +and out of `SHA256SUMS.txt` until an ARM64 machine renders a frame and the theme/backend log lines come out as +expected; that run is the single unlock condition, and copying a build nobody executed is exactly what this list +is meant to prevent. The publish job prunes that artifact with an explicit "not published" warning rather than +dropping it quietly. + +**macOS is not built by the release workflow at all.** The Apple Silicon and Intel slots never got past a manual +probe, so no build minutes are spent on them and nothing unverified is handed out. They stay out of the download +list and out of `SHA256SUMS.txt`; a manual workflow run can build all six slots for evidence, but the probe slots +are never published. The support matrix below marks those slots accordingly. The Windows asset is a bare `.exe`, so `LICENSE`, `NOTICE` and `README.md` are attached to this release -next to it: the license terms travel with every download. Slots that ship an archive (Linux / macOS, once -published) unpack to one top-level directory of the same name holding the binary plus those three -documents. +next to it: the license terms travel with every download. The Linux archive unpacks to one top-level directory +of the same name holding the binary plus those three documents. ## Command line @@ -65,6 +75,10 @@ Each published slot also ships the command line as its own small bare asset, .\ull_{{VERSION}}_windows_x64.exe instances list # servers and CI: no graphics stack needed ``` +```bash +./ull_{{VERSION}}_linux_x64 instances list # same binary shape on Linux, no display needed +``` + `ull` runs `instances`, `install`, `pack import`, `launch`, `accounts`, `sessions`, `logs`, `diag`, `config`, `java` and `agent`, and links no graphics library at all. The GUI binary accepts the same set before it ever opens a window, so a desktop machine does not need `ull`. @@ -84,22 +98,29 @@ 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+) | +| Linux x64, X11 | **published** | first class | glibc 2.35+ (`libxkbcommon`, `fontconfig`, `libGL` or the software renderer, `libxcb`) | +| Linux x64, Wayland | **published** (same archive, Wayland path unverified) | supported | the same archive; `wayland-client` + `libxkbcommon`, plus the X11 set for XWayland apps | | 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 | +| Linux arm64 | built, not published (no runtime evidence) | first class (unverified) | glibc 2.35+ (native `ubuntu-22.04-arm`, same floor as x64), `libxkbcommon`, `fontconfig`, `libGL`, `libxcb` / `wayland-client` | | 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` | -| Linux x64, Wayland | not built (unverified) | supported (unverified) | `wayland-client`, `libxkbcommon`, plus the X11 set for XWayland apps | -| Linux arm64, X11 | not built (unverified) | first class (unverified) | glibc 2.35+, `libxkbcommon`, `fontconfig`, `libGL`, `libxcb` | -| Linux arm64, Wayland | not built (unverified) | supported (unverified) | the above plus `wayland-client` | | 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 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. +Two slots are published this release: `windows-x64` and `linux-x64`. Linux x64 is not a claim taken on faith — the +release artifact itself was started on a Linux machine, opened a real window under a real display server, rendered a +frame (PNG asserted non-blank) and printed the expected `[theme]` and `[backend] final=… result=started` lines, and +the release build now carries the same check as a per-slot CI gate (`packaging/docs/linux-evidence.md` has the raw +output). What that does **not** cover: dragging/resizing and font metrics / CJK shaping on a real desktop session. +Whole-window transparency and rounded corners have no code path on Linux at all (transparency needs a native window +handle the app does not get there; corners are delegated to the desktop compositor), so they are not "unverified" — +they are not applicable. Nothing at all is known about Linux ARM64: no ARM64 machine has run the binary yet, which is +why that row stays unpublished. + +Both arm64 rows are formal matrix slots, not the experimental Termux path, and Linux arm64 is not published until +someone runs it on real hardware — a manual all-slots run builds it today, and one real frame on an ARM64 machine is +the unlock. 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 @@ -108,9 +129,26 @@ release, and it is also what you run on ARM64 Windows (the OS runs it through it The first run creates `.ULL/` and `.minecraft/` next to the exe (portable is the default data mode); the per-user `%APPDATA%\ULL` plus `%APPDATA%\.minecraft`, or a custom root, are selectable instead. -Linux / macOS are not part of this release; their asset names are already fixed by the same rule -(`unlucky-launcher_{{VERSION}}_linux_x64.tar.gz`, `_linux_arm64`, `_macos_arm64`, `_macos_x64`) so the -links will not change shape when those slots open. +**Linux x64** — unpack the archive and run the binary inside it; nothing is installed system-wide: + +```bash +tar -xzf unlucky-launcher_{{VERSION}}_linux_x64.tar.gz +cd unlucky-launcher_{{VERSION}}_linux_x64 +./unlucky-launcher # X11 or Wayland; --software-renderer forces the CPU rasteriser +``` + +It needs glibc 2.35+ (the slot is built on Ubuntu 22.04, so that is the floor), `libxkbcommon`, `fontconfig` +and `libGL` — or `--software-renderer` if there is no usable GPU adapter. On Debian/Ubuntu: + +```bash +sudo apt-get install libxkbcommon0 libfontconfig1 libgl1 libx11-6 libxcursor1 libxrandr2 libxi6 +``` + +The bare `ull_{{VERSION}}_linux_x64` next to the archive runs on a machine with no display at all. + +Linux arm64 and macOS are not part of this release; their asset names are already fixed by the same rule +(`unlucky-launcher_{{VERSION}}_linux_arm64.tar.gz`, `_macos_arm64`, `_macos_x64`) so the links will not change +shape when those slots open. ## No GPU? Software renderer @@ -119,8 +157,8 @@ adapter is present; `--software-renderer` (or its equivalent `--renderer=cpu`) f ## Verify the download -`SHA256SUMS.txt` lists exactly the assets attached to this release (Windows only), and `sha256sum -c` -checks the ones you actually downloaded: +`SHA256SUMS.txt` lists exactly the assets attached to this release (Windows x64 and Linux x64, GUI + `ull` each), and +`sha256sum -c` checks the ones you actually downloaded: ```bash sha256sum -c SHA256SUMS.txt --ignore-missing # Linux / WSL @@ -146,6 +184,19 @@ Get-FileHash .\unlucky-launcher_{{VERSION}}_windows_x64.exe -Algorithm SHA256 id. - Binaries are not code-signed / notarized yet: Windows SmartScreen and macOS Gatekeeper warn on first launch. The checksums above are the integrity reference for now. +- **Linux x64 is verified, not "finished".** The published archive was built on Ubuntu 22.04 and started under a + real display server (WSLg) where it opened a window, rendered a non-blank first frame and printed its theme and + backend lines; CI repeats that with Xvfb on every release. What nobody has looked at yet on a real desktop + distribution: dragging and resizing, font metrics and CJK shaping. Whole-window transparency and rounded corners + are not "unverified" on Linux — they are not code paths there: transparency needs a native window handle the app + does not get (`hwnd=none`), and corners are delegated to the desktop compositor (`corner=policy=system`). +- **On Linux the in-app update does not install itself.** The published Linux asset is a `.tar.gz`, and + `ull update apply` only self-replaces the portable single-file `.exe` form — on Linux it refuses with an explicit + message naming `portable-zip` instead of half-installing (see `crates/core/src/update/job.rs`). Updating on Linux + means downloading the new archive and unpacking it over the old copy; the update check itself (version + channel) + works normally. +- **Linux ARM64 has no runtime evidence at all.** It compiles, and that is the whole story; the artifact is not + attached to this release. - When the macOS slot ships it is the bare binary (single-file promise), not a `.app` bundle: no Dock icon or Finder double-click — launch it from a terminal.