ci(m184): linux-x64 进发布名单,两档 Linux 转 mode=ci,arm64 只构建不发布

发布流水线默认从 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。
This commit is contained in:
AskaEth committed 2026-11-08 21:35:26 +08:00
1 parent dc3fa2be87
commit 2813e2984c
14 files changed
+608 -151

No files matched your search

+59 -2
View File
@@ -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"
+2 -1
View File
@@ -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/
+14 -11
View File
@@ -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-<slot> artifact 里取证)"
echo "drop: $base($platform_arch 不在发布白名单:macOS 两档未构建验证 / 两个 arm64 槽位待真机跑一帧;仍留在 dist-<slot> artifact 里取证)"
rm -f "$f"
;;
esac
+30 -17
View File
@@ -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.<id>.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` 必须由**调用方**授予:可复用工作流只能收窄权限、不能提权(交集语义)。
+50 -28
View File
@@ -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_<version>_windows_x64.exe` | bare `.exe`, double-click to run |
| `windows-arm64` | `unlucky-launcher_<version>_windows_arm64.exe` | bare `.exe` — **not yet published**, see below |
| `linux-x64` | `unlucky-launcher_<version>_linux_x64.tar.gz` | one archive, one top-level directory — **not yet published** |
| `linux-arm64` | `unlucky-launcher_<version>_linux_arm64.tar.gz` | same — **not yet published** |
| `macos-arm64` | `unlucky-launcher_<version>_macos_arm64.tar.gz` | same — **not yet published** |
| `macos-x64` | `unlucky-launcher_<version>_macos_x64.tar.gz` | same — **not yet published** |
| `linux-x64` | `unlucky-launcher_<version>_linux_x64.tar.gz` | one archive, one top-level directory; glibc 2.35+ and the X11/Wayland + fontconfig stack |
| `windows-arm64` | `unlucky-launcher_<version>_windows_arm64.exe` | bare `.exe` — **built, not published**, see below |
| `linux-arm64` | `unlucky-launcher_<version>_linux_arm64.tar.gz` | same — **built, not published** (no runtime evidence yet) |
| `macos-arm64` | `unlucky-launcher_<version>_macos_arm64.tar.gz` | same — **not built** by the release workflow |
| `macos-x64` | `unlucky-launcher_<version>_macos_x64.tar.gz` | same — **not built** by the release workflow |
| every published slot | `ull_<version>_<platform>_<arch>[.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_<version>_linux_x64.tar.gz
cd unlucky-launcher_<version>_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_<version>_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
+40 -21
View File
@@ -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`,守门会对自己做变异自测 |
## 架构与模块地图
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+6 -1
View File
@@ -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 |
+61 -12
View File
@@ -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
+9 -7
View File
@@ -9,9 +9,9 @@
| 槽位 | 本版发布 | 产物文件名 | 形态 | 归档内容 |
|------|---------|-----------|------|---------|
| `windows-x64` | ✅ 发布 | `unlucky-launcher_<ver>_windows_x64.exe` | **裸 exe**(双击即用) | exe 单文件;三份合规文本作为并列 Release 资产 |
| `windows-arm64` | ❌ 不发布(构建取证中) | `unlucky-launcher_<ver>_windows_arm64.exe` | **裸 exe**(同 x64 策略) | 同上;CI 在**原生** `windows-11-arm`(Windows 11 Arm64 + VS2026 镜像)上构建,**M164 起编译前体检只取证、不再判定红绿**(此前五轮全红其实全红在那个误判的体检步),**红了不挡 x64 发布**(M142,见 §3) |
| `linux-x64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_linux_x64.tar.gz` | 单二进制归档 | `unlucky-launcher` + `LICENSE` + `NOTICE` + `README.md` |
| `linux-arm64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_linux_arm64.tar.gz` | 单二进制归档(原生 ARM64 runner 构建) | 同上 |
| `linux-x64` | ✅ 发布 | `unlucky-launcher_<ver>_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_<ver>_windows_arm64.exe` | **裸 exe**(同 x64 策略) | 同上;CI 在**原生** `windows-11-arm`(Windows 11 Arm64 + VS2026 镜像)上构建,**M164 起编译前体检只取证、不再判定红绿**(此前五轮全红其实全红在那个误判的体检步),**红了不挡发布**(M142,见 §3) |
| `linux-arm64` | ❌ 已构建、不发布(运行时零证据) | `unlucky-launcher_<ver>_linux_arm64.tar.gz` | 单二进制归档(原生 `ubuntu-22.04-arm` 构建,与 x64 同 glibc 地板) | 同上;M184 起**默认就编**(`mode: ci`),但没有任何 ARM64 真机跑过一帧 ⇒ 不进 `PUBLISH_SLOTS`。解锁条件 = 真机渲出一帧 + 主题日志 |
| `macos-arm64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_macos_arm64.tar.gz` | 单二进制归档 | 同上 |
| `macos-x64` | ❌ 不构建(未验证) | `unlucky-launcher_<ver>_macos_x64.tar.gz` | 单二进制归档 | 同上 |
| 每槽位 | 随所属槽位 | `ull_<ver>_<平台>_<架构>[.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-<slot>` 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 的并列资产分发,条款同样随下载走。
+32 -15
View File
@@ -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 按名显式透传(可复用工作流拿不到未显式传入的
@@ -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 自更新(换目录里的文件再重启)
是另一件事,未规划。
+205
View File
@@ -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。
+78 -27
View File
@@ -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 `<name>_<version>_<platform>_<arch>`, 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.