release: v0.2.4 里程碑快照(孤儿提交,无父;树 = dev 68362e5)
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
This commit is contained in:
commit
a5d1dec495
1580 files changed
+320938
No files matched your search
@@ -0,0 +1,53 @@
|
||||
# 产物矩阵与版本来源(M145 拆自 `packaging/README.md`)
|
||||
|
||||
本文承载原 `packaging/README.md` 的 §1、§2,**节号沿用原编号**(全仓 `packaging/README.md §N` 这类引用照旧有效,
|
||||
对照表在 [`packaging/README.md`](../README.md) 的索引里)。内容一字未改、只搬家:本行以上是 M145 新增的导航说明,
|
||||
以下逐行取自原文件。
|
||||
|
||||
## 1. 产物矩阵
|
||||
|
||||
| 槽位 | 本版发布 | 产物文件名 | 形态 | 归档内容 |
|
||||
|------|---------|-----------|------|---------|
|
||||
| `windows-x64` | ✅ 发布 | `unlucky-launcher_<ver>_windows_x64.exe` | **裸 exe**(双击即用) | exe 单文件;三份合规文本作为并列 Release 资产 |
|
||||
| `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` |
|
||||
| 全平台 | 仅已发布资产 | `SHA256SUMS.txt` | 校验清单 | 每行 `<sha256>␠␠<文件名>`(`sha256sum -c` 可直吃) |
|
||||
| 全平台 | 仅已发布资产 | `manifest.json` | **更新清单**(D50,见 `README.md` §13) | 渠道 + 版本 + 各平台产物(`target`/`format`/`url`/`size`/`sha256`);生成器 `make_manifest.py` |
|
||||
|
||||
约定细节:
|
||||
|
||||
- **「不构建」= 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 的并列资产分发,条款同样随下载走。
|
||||
- 归档解压后是**一个顶层目录** `unlucky-launcher_<ver>_<平台>_<架构>/`,用户不会解压出一堆散文件。
|
||||
- **`ull` 是并列的裸资产,不塞进 tar.gz**:headless 用户要的就是「下一个小文件直接跑」;归档里放
|
||||
LICENSE/NOTICE/README.md 是许可义务,而这三份本来就作为并列 Release 资产分发,裸 `ull` 不缺条款。
|
||||
- `unlucky-launcher` 本体也认全部 CLI 子命令(M82),`ull` 不是第二个界面,而是**不链接图形栈**的同一套命令(差异见 §4 与 §7.4),两者不是重复品。
|
||||
M118:GUI 的**构建产物**名是 `unlucky-launcher-iced`(`crates/app` 已被 workspace `exclude`,不再产出 `unlucky-launcher`);对外文件名仍是 `unlucky-launcher`,改名在 `make_artifacts.py` 落盘时(`LAUNCHER_BIN` 找产物 / `LAUNCHER_ASSET` 定对外名),所以下载链接 / `SHA256SUMS` / 本文矩阵不变。
|
||||
- `<ver>` 不带前导 `v`(tag 是 `v0.2.2`,文件名是 `0.2.2`)——由脚本统一裁剪。
|
||||
- UNIX 归档内 `mtime`/`uid`/`gid` 归零、权限固定(二进制 0755、文本 0644),
|
||||
同源构建的 sha256 不受打包时刻影响。
|
||||
- **架构不靠猜**:脚本打包前回读二进制的可执行文件头(PE 机器码 / ELF `e_machine` / Mach-O
|
||||
`cputype`),与槽位声明的架构不符即失败——不会出现"名字是 arm64、内容还是 x64"的假产物。
|
||||
|
||||
## 2. 版本来源(唯一真相源)
|
||||
|
||||
版本号只有一个来源:**`Cargo.toml` 的 `[workspace.package] version`**。
|
||||
|
||||
- `<ver>` 由 `make_artifacts.py` 通过 `cargo metadata --no-deps` 读出(不是正则扒文件),
|
||||
任何一个 workspace member 版本不一致时直接失败。
|
||||
- 带 tag 构建时(CI)额外传 `--version "$TAG"`:脚本裁剪 `v` 后与 `Cargo.toml` 交叉校验,
|
||||
**不一致即失败**——避免出现 tag 说 0.2.2、产物里写 0.2.1 的发布事故。
|
||||
- `ull --version` 必报同一版本(脚本打包前跑一次冒烟 `ull --version` 断言版本串在其中),
|
||||
这是 `env!("CARGO_PKG_VERSION")` 的实际输出,不是估算。
|
||||
- 发布流程:先改 `Cargo.toml` 版本 + 提交 → `git tag v<ver>` → 推送 tag 触发流水线。
|
||||
@@ -0,0 +1,139 @@
|
||||
# 构建与 CI 流水线(M145 拆自 `packaging/README.md`)
|
||||
|
||||
本文承载原 `packaging/README.md` 的 §3,**节号沿用原编号**(全仓 `packaging/README.md §N` 这类引用照旧有效,
|
||||
对照表在 [`packaging/README.md`](../README.md) 的索引里)。内容一字未改、只搬家:本行以上是 M145 新增的导航说明,
|
||||
以下逐行取自原文件。
|
||||
|
||||
## 3. 怎么构建
|
||||
|
||||
### 本地(当前平台)
|
||||
|
||||
```bash
|
||||
cargo build --release --locked # 产出 target/release/{unlucky-launcher-iced,ull}(M118:Slint 版不再进 workspace)
|
||||
cargo build --profile fast --bins # 只是想快点看到画面/跑测试:快档(§6),产物在 target/fast/
|
||||
python packaging/make_artifacts.py --out dist # 产物落在 dist/,含 SHA256SUMS.txt
|
||||
python packaging/make_artifacts.py --version v0.2.2 # 顺带校验版本一致性
|
||||
python packaging/make_artifacts.py --no-smoke # 跳过 ull --version 冒烟
|
||||
```
|
||||
|
||||
脚本按宿主自动判定槽位(Windows x64 / Windows arm64 / Linux x64 / Linux arm64 / macOS arm64 /
|
||||
macOS x64),其它组合直接报错退出——不在矩阵里的平台不出"凑合能跑"的产物。
|
||||
|
||||
交叉构建(例如在 x64 宿主上产 Windows arm64 产物)靠两个参数把"产物根"和"槽位"显式喂进去,
|
||||
脚本不去猜宿主与产物是不是同一架构(**CI 不用这条路径**,见下):
|
||||
|
||||
```bash
|
||||
rustup target add aarch64-pc-windows-msvc
|
||||
cargo build --release --locked --target aarch64-pc-windows-msvc \
|
||||
--bin unlucky-launcher-iced --bin ull # 不用 --bins:矩阵只发 GUI + ull;GUI 的构建产物名是 iced 的 bin(M118)
|
||||
python packaging/make_artifacts.py --slot windows-arm64 \
|
||||
--build-dir target/aarch64-pc-windows-msvc/release --out dist
|
||||
```
|
||||
|
||||
`--slot` 与宿主槽位不一致时,`ull --version` 冒烟会**自动跳过并打印原因**(arm64 产物在 x64
|
||||
宿主机上根本执行不了;拿"跳过"当通过是假证据,所以跳过永远带原因)。**CI 不走交叉**(M140):
|
||||
`windows-arm64` 在原生 `windows-11-arm` 上编,宿主=产物架构 → 冒烟**真跑**;`linux-arm64` 也是原生
|
||||
`ubuntu-22.04-arm` 构建 → 冒烟在 runner 上真跑(M184 起它默认就编,不再只在 `all_slots=true` 的取证跑里出现)。
|
||||
|
||||
### CI(默认跑四个槽位:Windows 两档 + Linux 两档;`all_slots=true` 才整表六槽位)
|
||||
|
||||
`.github/workflows/release.yml`:
|
||||
|
||||
- 触发:`push` tag `v*`;`workflow_dispatch` 只构建不发布,且多一个 `all_slots` 开关(**默认 false =
|
||||
只跑四个槽位**,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 能选标记」是两条互不冲突的规则。
|
||||
- 矩阵**由 `plan` job 按 `all_slots` 生成**(`matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}`:矩阵本身
|
||||
不能带 `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`(默认编);
|
||||
(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` 后显式导出
|
||||
`CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER`(Hostarm64 原生优先、Hostx64 次之;`-find` 的斜杠/反斜杠两种形态都试)。
|
||||
顺序不能反:只覆盖链接器而不给 `LIB`,撞上的就是 `LNK1181: cannot open input file 'kernel32.lib'`。
|
||||
**`cmd //c` 不能直接收「带引号的 vcvarsall 路径」**:MSYS 会把 `"` 改写成 `\"`,cmd 的 `/c` 剥离规则接不住,
|
||||
结果 `LIB`/`INCLUDE` 全空(M164 复审实测;`cmd //c` 只调**不含引号**的路径是好的)。故本步往 `$RUNNER_TEMP`
|
||||
写一个 CRLF 的 wrapper `.bat`(`call "<vcvarsall>" arm64` + `set`),`cmd //c` 只收相对文件名——引号留在
|
||||
`.bat` 内部由 cmd 自己解析,路径带空格也不受影响。
|
||||
- **编译前体检已降级为「只取证」**(M164):M141/M142 的版本用 `find` 猜 `um/arm64/kernel32.lib` 在不在、猜缺就
|
||||
`exit 1` —— v0.2.1~v0.2.3 五轮全红在**那一步**,`cargo build` 一次都没跑过(永远 skipped),而它跟自己打的快照
|
||||
自相矛盾(`um/arm64` 明明在)。现在那一步只打印 vswhere 组件查询、各目录 `.lib` **计数**与 `link.exe` 候选,
|
||||
永不 `exit 1`;**红绿只由编译器 / 链接器 / 静态 CRT 导入表门禁决定**。runner 实况:`windows-11-arm` 自 2026-09
|
||||
是 Windows 11 Arm64 + **Visual Studio 2026** 镜像,`VC.Tools.ARM64` 与 `Windows11SDK.22621/26100` 都已装好。
|
||||
- **发布白名单(`PUBLISH_SLOTS`,唯一开关)**:只有白名单里的槽位进 Release 与 `SHA256SUMS.txt`。**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 会被就地删掉而没人知道。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 默认建稳定版**:不带任何标记)。
|
||||
- **同一 tag 重跑幂等**:`gh release view` 命中已存在的 Release 时改走 `gh release upload --clobber`
|
||||
+ `gh release edit`,覆盖资产与说明而不是再 `create`(`create` 遇已存在会失败,把流水线最后一步染红)。
|
||||
- **预发布标记的语义表(M163,唯一真相源 `packaging/release_marker.py`)**:
|
||||
|
||||
| 触发 | create(新建 Release) | edit(Release 已存在) |
|
||||
| --- | --- | --- |
|
||||
| `push` tag `v*`(含其 Re-run) | **稳定版**(不带标记) | 不碰标记 |
|
||||
| `workflow_dispatch` + `prerelease=true` | 预发布 | 显式改成预发布 |
|
||||
| `workflow_dispatch` + `prerelease=false`(默认) | 稳定版 | 显式改成稳定版 |
|
||||
|
||||
空串必须是稳定版:`releases/latest/download/*` 只指向**非 prerelease** 的最新 Release,而自更新清单固定取
|
||||
`releases/latest/download/manifest.json`(D50)——push tag 打成预发布,稳定通道就一直是 404(v0.2.2 至今
|
||||
如此,M163 修的就是这条;**v0.2.2 那个 Release 保留为历史预发布不动**,稳定通道以 v0.2.3 为起点)。
|
||||
edit 的空串是「不碰」而不是「改成稳定版」:同 tag 重跑**不会**把用户手工提成稳定版的 Release 翻回预发布。
|
||||
要改已发版本的标记仍是两条等价路:① `gh workflow run release.yml --ref <tag> -f prerelease=false`
|
||||
(落到 edit 分支显式断言)② 直接 `gh release edit <tag> --prerelease=false`(不经流水线,后续默认重跑不碰它)。
|
||||
- **这条语义有守门(M163)**:`packaging/check_release_semantics.py`(CI 在 `.github/workflows/release-guard.yml`)
|
||||
断言四件事:标记表逐条对(push tag 的 create 不含 `--prerelease`)、把 `release_marker.py --emit-bash` 的产物
|
||||
**真灌进 bash** 读回两个数组、`release-publish.yml` 的可执行内容里没有 `--prerelease` 字面量、dispatch 默认值
|
||||
`== false`(读 YAML 结构,不是 grep)。`--self-mutation` 再把「默认值改回 true」「塞回字面量」「表里 push 分支
|
||||
改成预发布」注入临时副本,断言守门**每一项都红**、基线绿——守门自己抓不住回归时它先红。
|
||||
- **M145 拆分(本节新增的唯一一段)**:上面所有口径都还成立,只是"在哪个文件里"变了 —— `release.yml` 只留
|
||||
触发、`concurrency`、槽位表(`plan`)与两个 `uses:` 编排;`build` job 的 11 步逐字搬进
|
||||
`.github/workflows/release-build.yml`(`workflow_call`,四个矩阵键换成同名 `inputs.*`),`release` job 的 6 步
|
||||
逐字搬进 `.github/workflows/release-publish.yml`(同 `workflow_call`,`secrets.GITHUB_TOKEN` 改为调用方显式透传
|
||||
的 secret)。触发(`on`)、`env`、顶层 `permissions`、`concurrency` 与 `plan` 的槽位表在 M145 搬家时**一字未动**;
|
||||
后来只有 M163 动过 `on` 里 `prerelease` 的默认值(`true` → `false`,见上面那张语义表),M164 改过
|
||||
`release-build.yml` 的 arm64 体检步与 `cargo build` 步、并在 `release-publish.yml` 裁剪前插了「发布名单守卫」
|
||||
(于是本分支 8 步:checkout / download / **守卫** / 裁剪 / 重算 SHA256SUMS / manifest / 说明 / `gh release`;
|
||||
搬家时 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 按名显式透传(可复用工作流拿不到未显式传入的
|
||||
secret;这里刻意不用 `secrets: inherit`——那会把仓库全部 secret 一起透进被调方)。Secret 未录入、或在整个
|
||||
Actions 之外求值时该表达式为空串,`client_id.rs` 把空白当「没配」⇒ 非官方构建只是不烘 id,不会拿空 id 去
|
||||
联网;本地自烘仍走 `ULL_MSA_CLIENT_ID=<id> cargo build`。**别把这个 env 导出到跑 `cargo test` 的 shell**:
|
||||
presenter 的 M132 入口锚会在「已烘焙 id」的构建里**故意红**(它要求一个两处都没有 id 的构建)。
|
||||
- 发布说明模板(`release-notes.md`)的 "Known limitations" 里,微软登录一条已改为「开箱即用、env 可覆盖」。
|
||||
|
||||
Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两者都属"标准工具链"(D21),
|
||||
但发布产物要**不依赖 VC++ 可再发行包**,`+crt-static` 把 CRT 静态链进 exe,Win10+ 的 UCRT 本身是
|
||||
系统组件,于是 exe 真正单文件可拷走。GNU 交叉产物在 CI 上还要拉 MinGW 链接器,收益为零。
|
||||
@@ -0,0 +1,72 @@
|
||||
# 发行合规、支持矩阵与已知边界(M145 拆自 `packaging/README.md`)
|
||||
|
||||
本文承载原 `packaging/README.md` 的 §8、§9、§10,**节号沿用原编号**(全仓 `packaging/README.md §N` 这类引用照旧有效,
|
||||
对照表在 [`packaging/README.md`](../README.md) 的索引里)。内容一字未改、只搬家:本行以上是 M145 新增的导航说明,
|
||||
以下逐行取自原文件。
|
||||
|
||||
## 8. 发行合规
|
||||
|
||||
- `LICENSE`(Apache-2.0 全文)与 `NOTICE`(含 Slint Royalty-Free 2.0 义务说明)随包分发,
|
||||
三份文本在 UNIX 归档内、在 Windows 侧作为并列 Release 资产。
|
||||
- Slint 的 RF 2.0 义务只跟 `v0.1.0` 那批资产:当时关于页有 `AboutSlint` 部件、选的是不触发 GPL 分支的 RF 分支
|
||||
(见 `deny.toml` 的许可证白名单注释)。现役 iced UI 里没有、也不需要任何 Slint 部件。
|
||||
- **M118 后这条义务只跟着已发布的 `v0.1.0` 资产走**:`crates/app` 已被 workspace `exclude`,发布矩阵里再没有链接 Slint 的二进制,新的 iced 版产物不链接 Slint;`deny.toml` 那条 RF-2.0 allow 因此暂时是死条目(不删,留给将来真有别的路径引入 Slint 时兜底)。
|
||||
- `README.md`(英文主版)随包分发;中文版 `README.zh-CN.md` 留在仓库不进包(避免包体噪音)。
|
||||
- 依赖许可门禁不变:`deny.toml` 白名单(Apache-2.0 / MIT / BSD / Zlib / BSL-1.0 / ISC / CC0 / Unlicense /
|
||||
Unicode-3.0 / CDLA-Permissive-2.0 / Slint-RF-2.0)。发布任务不新增任何依赖。
|
||||
- 发布产物不含游戏本体、不含 JRE、不含任何账号数据;`ull`/GUI 的数据目录在用户侧生成。
|
||||
|
||||
## 9. 支持矩阵(对外口径,与 release-notes 模板逐行一致)
|
||||
|
||||
| 平台 | 本版发布 | 级别 | 运行时要求 |
|
||||
|------|---------|------|-----------|
|
||||
| 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 |
|
||||
| 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)——改一边必须同步另一边,否则标题里的"一致"就是假话。
|
||||
|
||||
**M184 的翻牌(Linux x64 进发布名单,arm64 只构建)**:发布开关仍是 `release-publish.yml` 的 `PUBLISH_SLOTS`,
|
||||
现在是 `windows-x64 linux-x64`;`release.yml` 槽位表里 `linux-x64` 与 `linux-arm64` 都是 `mode: ci`(tag 推送默认就编)。
|
||||
两档的唯一差别是**运行时证据**:
|
||||
|
||||
- `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`):
|
||||
声明了缺产物 ⇒ 红(这正是「声明为 ci 的槽位构建红了就挡住发布」那条闸),有产物没声明 ⇒ `::warning::` 明说未发布,
|
||||
`mode: ci` 却本次没产物 ⇒ 也打 `::warning::`(红了要看得见)—— 所以那次 CI 真出包之后,翻一行名单即可。
|
||||
|
||||
## 10. 已知边界 / 后续
|
||||
|
||||
1. **GUI 的 `--version` 已解决**(M82):现在命中子命令分发,输出与 `ull --version` 同一条版本 + 系统摘要后退出,
|
||||
不再开窗;§7.2 里「无输出、8 秒后被 timeout 杀掉」那行是**改之前**的记录,留作对照。
|
||||
2. **签名/公证**:Windows 无 Authenticode、macOS 无 codesign/notarize(都要证书/开发者账号),
|
||||
当前以 SHA256SUMS + Release 说明作为完整性参考。
|
||||
3. **macOS `.app` bundle / `.dmg`**:与单文件承诺冲突,暂不做;若用户要 Dock 图标再单开 mission。
|
||||
4. **Linux musl 静态版**:可彻底摆脱 glibc 地板,但现役 iced 版(winit + wgpu)在 musl 上的可行性未验证,未纳入本矩阵。
|
||||
5. **`ull` 已随包**(M82 起):每个槽位与 GUI 并列发一份裸 `ull`,进同一份 `SHA256SUMS.txt`,矩阵
|
||||
完整性检查按「GUI 与 CLI 各自都要有」分开查(`release.yml`)。等 CI 首跑确认这条在真流水线上有效。
|
||||
6. **本机 GNU 链接器的已知无害告警**:`cargo build --release`(GNU 工具链)会打一行 `warning: linker
|
||||
stderr: corrupt .drectve at end of def file`(MinGW `ld` 的老式抱怨,不影响产物;CI 的 MSVC 槽位没有)。
|
||||
7. **两个 arm64 槽位本机跑不了**:本机是 x64 Windows,既无 ARM 机器也无 MSVC 的 ARM64 链接库组合,
|
||||
`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 自更新(换目录里的文件再重启)
|
||||
是另一件事,未规划。
|
||||
@@ -0,0 +1,235 @@
|
||||
# GPU 截图证据账(M249)
|
||||
|
||||
判据的**唯一实现**是 `packaging/check_gpu_shot.py`(零第三方依赖,19 条自测向量)。
|
||||
样本是 M246 在本机的实测产物 `DevDocs/artifacts/gpu-probe/`(`DevDocs/` 被 `.gitignore:6` 忽略,
|
||||
**图不进仓库**,所以这里只引它的读数,不引它的字节)。
|
||||
|
||||
一句话:这套判据把「这张图是 GPU 档出的」从**人读日志**变成**可复跑的退出码**,但它证明的是
|
||||
「像素由哪条光栅化路径产出」,**不是**「用户看到的窗口长这样」。第 4/5 节逐条划线。
|
||||
|
||||
**CI 接线(M253)在第 8 节**:`--self-test` 无条件跑,真实产物判据只在有可用产物时跑;
|
||||
**无 GPU 时显式跳过并写明原因,跳过与通过分开写**(跳过 ≠ 通过)。
|
||||
|
||||
## 1. 怎么跑
|
||||
|
||||
```text
|
||||
python packaging/check_gpu_shot.py <stderr 日志> <PNG> # 正例:六条判据,0/1 退出
|
||||
python packaging/check_gpu_shot.py <stderr 日志> --expect-refuse [PNG] # 负例:拒绝降级出图是否成立
|
||||
python packaging/check_gpu_shot.py --self-test # 19 条向量(数据 tests/gpu_shot_vectors.json,脚手架 tests/gpu_shot_selftest.py)
|
||||
```
|
||||
|
||||
退出码:`0` = 判据全过;`1` = 有判据没过(**逐条点名**,不只印 FAILED);`2` = 用法错(少参数 / 认不出的参数)。
|
||||
判据不看日志的**请求值**(`[probe] renderer = wgpu` 只是意图),只看**结果行**(`[shot] … renderer=`)。
|
||||
|
||||
**取日志必须带 `ULL_GPU_LOG=info`**:`Selected: AdapterInfo { … }` 那一行是 wgpu 的 info 日志,
|
||||
默认级别下根本不落盘(实测:a1b 那份 24 行 stderr 里 `^\[gpu\]` **0 条**,见 §5 第 2 条)。
|
||||
|
||||
## 2. 本机实跑退出码(M246 产物,逐条)
|
||||
|
||||
| 用例 | 命令(节选) | 退出码 | 判据怎么说话 |
|
||||
|------|-------------|--------|-------------|
|
||||
| A1 正例 GPU | `a1-gpu.stderr.log + a1-gpu-light.png` | **0** | 6/6 过:`renderer=wgpu`、`device_type=DiscreteGpu`(RX 6600 XT / Vulkan)、1180x760、`final=wgpu result=started`、唯一色 1856 / σ 33.21 / 强边缘 0.05778 |
|
||||
| A2 正例 GPU(preflight) | `a2-preflight.stderr.log + a2-preflight-gpu.png` | **0** | 6/6 过(与 A1 字节相同的图,另带 `[gpu-preflight] ok type=DiscreteGpu`) |
|
||||
| A4 正例 GPU(auto→shell) | `a4-auto.stderr.log + a4-auto-shell.png` | **0** | 6/6 过:`reason=auto-hardware-adapter` 也真的落到 wgpu |
|
||||
| A3 CPU 档 | `a3-cpu.stderr.log + a3-cpu-light.png` | **1** | 3 条红:`renderer=tiny-skia`、无 `Selected` 行、`final=tiny-skia`(PNG 本身没问题:唯一色 2519 / σ 35.43) |
|
||||
| A3b CPU 档重复 | `a3b-cpu-repeat.stderr.log + a3b-cpu-light.png` | **1** | 同上(重复性对照,结论一致) |
|
||||
| A5 CPU 档 shell | `a5-cpu-shell.stderr.log + a5-cpu-shell.png` | **1** | 同上 |
|
||||
| A6 负例,按正例口径判 | `a6-negative-refuse.stderr.log + a6-negative-refuse.png` | **1** | **6 条全红**(其中 3 条是「PNG 不存在」——负例本来就没图) |
|
||||
| A6 负例,`--expect-refuse`(传 PNG 路径) | `a6-negative-refuse.stderr.log --expect-refuse a6-negative-refuse.png` | **0** | **3/3** 过:`via=refuse`、无出图行、目标 PNG 不存在 |
|
||||
| A6 负例,`--expect-refuse`(不传 PNG) | `a6-negative-refuse.stderr.log --expect-refuse` | **0** | **2/2** 过(判据 9 不登记:`--expect-refuse` 的 PNG 参数是可选的) |
|
||||
| A1b 日志 + A1b 图 | `a1b-gpu-repeat.stderr.log + a1b-gpu-light.png` | **1** | 见 §5 第 2 条:这份日志没开 `ULL_GPU_LOG=info`,**没有** `Selected` 行 ⇒ 单独看它无法自证适配器 |
|
||||
| 串味:A1 日志 + A3 的 CPU 图 | `a1-gpu.stderr.log + a3-cpu-light.png` | **0** | **这是已知盲区**,见 §5 第 1 条(两张图同为 1180x760) |
|
||||
|
||||
A6 的负例形状(`WGPU_BACKEND=metal`):`Available adapters: []` → `wgpu 初始化失败(具名)` →
|
||||
`拒绝降级出图` → `[backend] final=none result=failed … via=refuse`,退出码 1,**没有 PNG**。
|
||||
|
||||
## 3. 六条判据:每条抓得住什么、抓不住什么
|
||||
|
||||
判据 1–3 来自 M246 的实测口径,4–6 是为「日志与图对不上号」补的钉子。每条都能单独判红、单独打印。
|
||||
|
||||
| # | 判据 | 抓得住 | 抓不住 |
|
||||
|---|------|--------|--------|
|
||||
| 1 | **全部** `[shot] … renderer=` 行都是 `wgpu`(`set(renderers) == {"wgpu"}`) | 「其实走了 CPU 档」——CPU 档在这行写 `renderer=tiny-skia`(M97 起这行打的是**实际**后端,不是请求值,见 `crates/app-iced/src/renderer/mod.rs:52`) | 只证明这些行被打印过;日志被手工拼装、或从别的进程拷来的行,它分不清(判据只读文本);「一次运行里混了两种 renderer」(`>>` 拼接、`via=respawn` 降级)**一律判红**,但**同种 renderer 的多次运行拼在一起**看不出来(§5 第 7 条) |
|
||||
| 2 | `Selected: AdapterInfo { device_type: X }` 且 `X ∉ {Cpu, Other}`;**取不到 `X` 一律判红** | 「wgpu 真建成了设备,而且选中的不是 WARP(`Cpu`) / Gl 兼容层(`Other`)」;与 `crates/app-iced/src/probe.rs:165-167` 的硬件判据同口径 | 选中的是**哪一块**卡不判(独显 vs 集显都算硬件);不看 `Available adapters` 那 12 条,只看 `Selected` 那条 —— 也就抓不住「枚举里有独显、实际选了集显」之外的更细的驱动行为;**多行 Debug / 字段缺失**时不知道真值是啥,只按「取不到即判红」处理(宁可假红:iced 换成 pretty-print Debug 时真 GPU 图会被判红,得回来改正则) |
|
||||
| 3 | PNG 能解码且像素数 > 0 | 0 字节 / 半截写入 / 根本不是 PNG(签名、IHDR、IDAT 逐层查) | 只管「能解出来」,不管内容对不对(那是判据 6 的事);不解调色板与隔行 PNG(沿用 `check_screenshot.py` 的同一条限制) |
|
||||
| 4 | PNG 宽高 = 日志 `[shot]` 声明的宽高 | 拿错文件的第一步:日志说 1180x760、图是别的尺寸 | **同尺寸互换抓不住**(见 §5 第 1 条);分辨率和内容是否同一帧也抓不住 |
|
||||
| 5 | `[backend] final=wgpu result=started` | 后端结论行与判据 1/2 自相矛盾(例如 `final=none … via=refuse`) | 需要 `ULL_ACCEPTANCE=1` 才有这行;只看这一行的话,A6 那种「一行都没出图」和「出了图但日志没写」分不清 |
|
||||
| 6 | PNG 过 `check_screenshot.py` 的非空白阈值(唯一色 ≥64 / 亮度σ ≥3.0 / 强边缘 ≥0.001) | 「进程起来了、PNG 合法、但一个像素都没画」的空帧 | 阈值很低(这是它的设计:跨主题/字体不误报),**不判视觉正确性**,也不判元素齐全 |
|
||||
|
||||
`--expect-refuse` 的三条(负例口径):`via=refuse` 存在、**没有**任何 `[shot]` 出图行、目标 PNG 不存在。
|
||||
它证明「该拒绝时真拒绝了」,**不证明**「GPU 能出图」——两者互为对照,缺一个都能蒙混过关。
|
||||
|
||||
### 3.1 判据对「行为被改坏」敏感(变异自证)
|
||||
|
||||
`--self-test` 的向量在 `packaging/tests/gpu_shot_vectors.json`,每条的 `expect_fail` 是**期望的失败判据
|
||||
清单,逐条对**。三个变异都能被抓住(本轮 r1 又补了判据 2 的「取不到即红」):
|
||||
|
||||
| 变异 | 抓它的向量 | 结果 |
|
||||
|------|-----------|------|
|
||||
| `DISALLOWED_DEVICE_TYPES` 里删掉 `Cpu`(= 允许 WARP 当 GPU) | 「renderer=wgpu 但 Selected 是 Cpu」 | 由红转绿 ⇒ 清单对不上 ⇒ `--self-test` 退出 1 |
|
||||
| 删掉判据 1(不再要求 `renderer=wgpu`) | 「删掉 [shot] renderer 行」、「日志里混入第二种 renderer」 | 由红转绿 ⇒ 清单对不上 ⇒ `--self-test` 退出 1 |
|
||||
| 判据 2 不再把「取不到 device_type」当失败(恢复旧写法) | 「Selected 换成多行 Debug」、「`Selected: AdapterInfo {` 空体」 | 由红转绿 ⇒ 清单对不上 ⇒ `--self-test` 退出 1 |
|
||||
|
||||
## 4. 这份证据**能**证明什么
|
||||
|
||||
逐条都能指到「哪个文件 + 哪条判据过了」:
|
||||
|
||||
1. **字形栅格与文字边缘的抗锯齿是 GPU 路径给的。** `a1-gpu-light.png` 的 1180×760 像素由 wgpu/Vulkan
|
||||
在 RX 6600 XT 上光栅化(判据 1+2+5 全过,`Selected` 是 `DiscreteGpu`),而同一屏的 CPU 档(`a3`)
|
||||
被同一套判据判红。⇒ 「12px 规格文本的覆盖率采样来自 MSAAx4 的那条路」这件事,是**图+日志**一起证的。
|
||||
2. **面板 α 与玻璃只有 GPU 档能出。** CPU 档在代码层就把它们强制掉了:`crates/app-iced/src/pages/
|
||||
appearance/alpha.rs` 的 `panel_is_forced_opaque(Backend::Cpu) == true` ⇒ `effective_panel_alpha`
|
||||
恒 1.0、`effective_glass` 恒 false。所以只要是**真的** GPU 档出的图,它拍到的面板 α/玻璃是产品
|
||||
在 GPU 档下的真实合成结果,而不是「CPU 兜底也能喂出来」的近似。
|
||||
3. **两个后端在同一屏上的一致性是可复核的。** `--diff` 的读数(M246):gallery `differing=51055/896800
|
||||
(5.69%) max_delta=200 best_shift=(0,0)`、shell `19088/896800 (2.13%) max_delta=75 best_shift=(0,0)`;
|
||||
差异 **99.3%** 落在亮度边缘上、`delta>32` 的 9301 像素连成 3002 块(最大 83 px);两个后端各自连跑
|
||||
两次 PNG **字节相同**(`52d4c1d6e464da16` ×2 / `10696a043bd9e7b9` ×2)。⇒ 「几何没有平移、差的是
|
||||
边缘覆盖率」这个结论成立,5.69% 不是抖动。
|
||||
4. **`--renderer=auto` 不是「悄悄走 CPU」。** A4 的 `reason=auto-hardware-adapter` 与 `Selected:
|
||||
DiscreteGpu` 同时成立,判据 0 退出。
|
||||
5. **断开的 RDP 会话也能出 GPU 图**(M246 的现场条件:`quser` 显示 `2 断开`)。⇒ 「断连 ⇒ GPU 截不了图」
|
||||
这个说法不成立;成立的是 M245 §4 的「断连打折首帧耗时与整窗 α,不打折像素内容」。
|
||||
6. **「该拒绝时真拒绝」有机器判据。** A6 的 `via=refuse` + 无 PNG + 退出码 1 由 `--expect-refuse` 判 0。
|
||||
|
||||
## 5. 这份证据**不能**证明什么(逐条,别把「没证」读成「没问题」)
|
||||
|
||||
1. **图与日志的绑定只到「尺寸」一级。** 同一尺寸的两张正经图互换,本脚本判绿(实测:A1 日志 + A3 的
|
||||
CPU 图 = 退出码 **0**)。要堵这个口得让产品把 PNG 的 sha256 打进日志(改 Rust,不在本 mission)。
|
||||
⇒ 现有用法是「一次运行、当场取日志与图」,**不是**「从一堆历史产物里随便挑两个配对」。
|
||||
2. **日志级别不够时判据 2 无从判定。** `Selected: AdapterInfo` 是 wgpu 的 info 级日志:a1b / a5 那两轮
|
||||
没带 `ULL_GPU_LOG=info`,两份 stderr 里 `^\[gpu\]` 各 **0 条**。a1b 的「合格」来自它与 a1 **字节相同**
|
||||
(`52d4c1d6e464da16`),**不是**来自它自己的日志 —— 所以本脚本对 a1b 判红是**如实**(这份日志单独
|
||||
确实证不了适配器),不是误报。
|
||||
3. **首帧 / 帧率不能证。** 全部 `[perf]` 都是墙钟,且冷热差距极大(A1 首次 `probe=1281ms`、`font=749ms`;
|
||||
后续进程 `584/587ms`、`55~57ms`)。A1 `3661ms` vs A3 `3146ms` **不能**读成「GPU 比 CPU 慢」。
|
||||
4. **整窗 α 与圆角观感不能证,任何截图都不行。** `window::screenshot` 读的是渲染目标,而整窗 α
|
||||
(`LWA_ALPHA` 字节)与圆角(`DWMWA_WINDOW_CORNER_PREFERENCE`)由 **DWM 在合成时**施加,不在渲染
|
||||
目标里 —— 同一份判据在 Windows 上拍到的是「DWM 之前」的像素。观感只能人眼看真显示器。
|
||||
5. **「窗口被 DWM 合成过」这件事没证。** 图是渲染目标,不是屏幕。DWM 是否真把这帧合成上屏、有没有掉帧、
|
||||
有没有被别的窗口遮挡,这张图一概不知道。
|
||||
6. **换机器不成立。** 全部证据来自一台机器(RX 6600 XT / Raphael iGPU、12 个 adapter、RDP 断开会话)。
|
||||
`VirtualGpu` 会不会被判成硬件只在**代码层**确认(`probe.rs:165-167` 会放行),**没在真 `VirtualGpu`
|
||||
适配器上验过**(本机 12 条里一条都没有)。
|
||||
7. **同种 renderer 的多次运行拼在一起,无法分辨图属于哪一次。** 判据 1 只挡住「混入**异种** renderer」
|
||||
(wgpu + tiny-skia 拼一起);两次都是 `renderer=wgpu` 的日志 `>>` 到同一个文件时所有行都自洽,
|
||||
判据 4 又只比尺寸 ⇒ 两张同为 1180×760 的 GPU 图互换仍然判绿。要堵它得把 PNG 的 sha256 / 运行 id
|
||||
打进日志(改 Rust)。⇒ 一次取证 = 一份干净的 stderr,别往同一个文件里续写。
|
||||
8. **交互启动那一支没验。** A6 验的是「取证启动(`--screenshot`)下拒绝降级出图」;交互启动的
|
||||
`via=respawn`(换进程降级)要求真窗口,本轮没开。
|
||||
9. **dark 主题、其它屏、`ULL_WINDOW_SIZE` 变尺寸、macOS / Linux** 全部没跑:只有 gallery + shell 的 light。
|
||||
|
||||
## 6. 历史 `--diff` 基线:**永久不可复核**
|
||||
|
||||
`DevDocs/00-债务台账-p1.md:29` 记着一条 `differing=8134/896800 (0.91%) max_delta=60 best_shift=(0,0)`,
|
||||
M246 本机实测是 **5.69% / 200**(gallery)与 **2.13% / 75**(shell)。两个数字**不可比**,原因是记录本身
|
||||
缺项:那条没有 commit、没有屏幕、对应图(`DevDocs/artifacts/m187/`)**已丢失**。
|
||||
|
||||
⇒ 这条基线在任何将来都**无法复核**:没有图就没有分母,没有 commit 就没有树状态,重跑也只会得到新读数。
|
||||
唯一还能比的是**几何判据**(`best_shift=(0,0)`),它一致。别把 0.91% 当「回归阈值」用;本账不承认它是基线。
|
||||
|
||||
## 7. 没验的部分(M249 自己)
|
||||
|
||||
- **M249 当时没接进 CI**:那一轮 `.github/workflows/**` 一行未动(接 CI 是策略决定:runner 上有真 GPU,
|
||||
还是接受「跑不起来就跳过」)。**M253 已接上**,见 §8;本节其余条目原样保留。
|
||||
- **没在任何非本机环境跑过**:Windows + 上述单卡 + RDP 断开会话之外,Linux/macOS/其它 GPU 全没跑。
|
||||
- **没改 Rust**:判据只读现有日志格式。若产品将来改了 `[shot]` / `Selected:` 的行格式,这里的正则
|
||||
会同时失效(`--self-test` 用的是**按现格式手拼**的合成日志,抓不到格式漂移——它守判据,不守格式)。
|
||||
- **没验 `--diff` 阈值化**:本文第 4 节引的是 M246 的读数,本 mission 没有重跑 `--diff`。
|
||||
|
||||
## 8. CI 接线(M253):无 GPU 时显式跳过,**跳过 ≠ 通过**
|
||||
|
||||
判据靠 **`.github/workflows/gpu-evidence.yml`** 跑(`push: main` / `pull_request` / `workflow_dispatch`,
|
||||
以及 `workflow_call`:日常线 `dev` 经 `dev-guards.yml` 复用同一份,见下)。
|
||||
**为什么新开一个文件而不是并进 `guards.yml`**:`guards.yml` 已是 399 行、红线 400(`file-size` 那道门
|
||||
就扫 `*.yml`),挂一个 job 必越线;`release-build.yml` 正好 400 行、动它得先拆。与 `release-guard.yml`
|
||||
当初单独成文的理由逐字相同。
|
||||
|
||||
**日常线 `dev` 也跑**(M253 r1 P2-1):`dev-guards.yml` 里多一个
|
||||
`gpu-evidence: uses: ./.github/workflows/gpu-evidence.yml`。`dev-guards.yml` 文件头自己写着
|
||||
「门禁存在但日常线上永不运行 = 空跑的门禁,长期等于没有」,而这条 workflow 原先只在 `main`/PR 触发,
|
||||
直推 `dev` 时**一次都不跑**。dev 上 `evidence` 照样显式跳过(无 GPU、无产物),所以 dev 拿到的是
|
||||
`selftest` 那一半:19 条向量 + 三个退出码 + 接线自检。**刻意不加**「只跑自测」的开关 —— 那会给这道
|
||||
门禁多开一个「可以合法关掉真判据」的旋钮。
|
||||
|
||||
三个 job,各管一件事:
|
||||
|
||||
| job | 什么时候跑 | 它的结论代表什么 |
|
||||
|-----|-----------|-----------------|
|
||||
| `selftest` | **无条件**(不需要 GPU、不需要真产物) | 「尺子合格」:19 条向量 + 三个退出码 0/1/2 用真 CLI 走一遍 |
|
||||
| `probe` | 无条件,只探测、不判定 | 工作区里有没有可判的产物;没有就把原因写进 job summary 并打 `::warning::` |
|
||||
| `evidence` | `if: needs.probe.outputs.available == 'true'` | 「这张图真是 GPU 档出的」:`check_gpu_shot.py` 的退出码(0 过 / 1 判据不过 / 2 用法错)即结论 |
|
||||
|
||||
**跳过与通过分开写**:没有可用产物时 `evidence` job 的 `if:` 不成立 ⇒ 它在 Actions 里是**灰色
|
||||
skipped**,不是一个绿色 ✓;同时 `probe` 打出(本地手模拟的原样输出,见 §8.1):
|
||||
|
||||
```text
|
||||
GPU 证据:无硬件适配器(/dev/dri 下没有 renderD*,也没有可用的 nvidia-smi),且工作区里没有可用的样例产物
|
||||
(约定路径要求 stderr 日志与同次运行的 PNG 同时存在;DevDocs/ 被 .gitignore 忽略,图不进仓库;event=push),
|
||||
跳过真实产物判据(判据实现自测已过:那证明「尺子合格」,不证明「这张图是 GPU 出的」)
|
||||
```
|
||||
|
||||
外加一条 `::warning::GPU 证据:跳过(…)—— 本次运行没有判过任何真实产物,不构成「GPU 证据已验证」`。
|
||||
`available` 同时作为 job output 交给 `evidence`,所以「跳过」与「通过」在数据流上也是两条路。
|
||||
|
||||
四条路径的口径:
|
||||
|
||||
| 情形 | 结果 | 为什么这么定 |
|
||||
|------|------|-------------|
|
||||
| 工作区里有 `DevDocs/artifacts/gpu-probe/gpu-shot.stderr.log` + `gpu-shot.png`(约定成对) | `available=true` ⇒ `evidence` 真判 | 一次取证 = 一份干净的 stderr + 同次运行的那张图(§5 第 1/7 条);**只给一半仍按跳过**(半套产物不算可用) |
|
||||
| `workflow_dispatch` 填了 `shot_log` / `shot_png` | 同上 | 显式点名 ⇒ 真判 |
|
||||
| 点了名但**文件不存在**(或正例没给 PNG) | **红**(`::error::` + exit 1) | 「明确说要判、东西不在」若能退化成跳过,就又是一次假绿 —— 有意为之的输入缺失是错误,不是 skip |
|
||||
| 托管 runner 常态(无 GPU + 无产物) | 跳过 + 原因 + 黄条 | 见上;托管 runner 本来就没有硬件适配器 |
|
||||
|
||||
**在 CI 里怎么让它真判**(r1 复审 P2-2:原先 §8 说的「把产物放到 `DevDocs/`」在 CI 里没人做得到 ——
|
||||
**checkout 只拿已跟踪文件**,而 `/DevDocs/` 在 `.gitignore:6` 里,也没有 `upload/download-artifact` 环节):
|
||||
|
||||
1. 把一次取证的那一对产物**提交进你打算 dispatch 的那个 ref**:`git add -f
|
||||
DevDocs/artifacts/gpu-probe/gpu-shot.stderr.log DevDocs/artifacts/gpu-probe/gpu-shot.png`(`-f` 是必需的,
|
||||
否则被 ignore);
|
||||
2. 对该 ref 跑 `workflow_dispatch`,或干脆让它走 `push` / PR —— `probe` 会认这对约定路径,`available=true`;
|
||||
3. 不想提交到 `DevDocs/` 的话,把两个文件放到任一**已跟踪**目录,再用 dispatch 的
|
||||
`shot_log` / `shot_png` 指过去(那两个输入只认「checkout 后确实存在的路径」)。
|
||||
|
||||
就地往 workspace 里拷文件(不提交、靠上一步的产物)**在这个设计里做不到**:没有 artifact 传递环节。
|
||||
|
||||
**接线自检的覆盖面**:`packaging/tests/gpu_shot_ci_selftest.py` 用 **10 条判据 + 33 条变异向量**
|
||||
(r1 复审表 20 条 + 1 条补充 + r2 复审 12 条)把上面这些口径钉住,逐条见
|
||||
`packaging/tests/gpu_shot_ci_vectors.json`:
|
||||
|
||||
1. 五步按 `id:` 必须在场且无条件(`vectors`/`exitcodes`/`wiring`/`detect`/`judge`),`exitcodes` 还得真断言 0/1/2;
|
||||
2. 自测步必须**指向本脚本且是单条命令**(`|| echo 先忽略` 无从下手);
|
||||
3. **解析 `if:` 表达式后求值**判「判定挂门」(`available` 取 `'true'`/`'false'`/`''` 必须真/假/假,认不出即红);
|
||||
4. 两种调用形态都在场,且产物来自 `needs.probe.outputs.*`;
|
||||
5. **判据步真跑两次**(正例必须 0、CPU 档必须非 0,`bash -e` 与裸 bash 两种模式都跑);
|
||||
6. 跳过路径**实跑 7 种输入**;
|
||||
7. `continue-on-error` 按 YAML 布尔语义判(`'true'` / `${{ true }}` 都算开启);
|
||||
8. 吞码标记表(`|| true` / `; true` / `|| exit 0` / 裸 `exit 0`,兜底用);
|
||||
9. `probe.outputs.*` 必须逐个绑到 `${{ steps.detect.outputs.* }}`;
|
||||
10. `on:` 必须同时有 `push`(含 `main`)/`pull_request`/`workflow_call`。
|
||||
|
||||
**它不覆盖**:连判据一起改(它不自守);`if:` 的 GitHub 语义(那是本脚本的小求值器模拟的);真 Actions 里的
|
||||
job skip / required check 行为;判据 4 的「合法调用替换」(后果是假红不是假绿);`exitcodes` 见证步自己的
|
||||
断言被削弱;判据 6 那 7 种输入之外的新分支。
|
||||
|
||||
**载体依赖**:整个 `gpu-evidence.yml` 被删掉/替换后,**没有任何东西会跑这个自检** —— 守门人住在被守对象
|
||||
自己的文件里(`guards.yml` 已经 399/400 行,塞不进去)。
|
||||
|
||||
本文件**不宣称**「不可能被改成总是绿」:r1 那一版这么写过,实测 19 条变异漏 11 条;r2 那一版
|
||||
(6 条判据)实测 40 条新变异漏 11 条。所以这里只写**能复跑的封闭清单**(10 类 / 33 条向量)。
|
||||
**这条门禁自 2026-11-11 起停止加固**:再往下是「守卫守守卫守守卫」,边际收益很低;要扩覆盖面必须由
|
||||
人类新开 mission。
|
||||
|
||||
### 8.1 这一节**没验**的部分
|
||||
|
||||
- **CI 上从没真跑过**:本 workflow 是本地写的,**一次 GitHub Actions 运行都没有**(未 push)。
|
||||
本地验的是两件事,且都**已固化成每次自检都重跑的功能断言**:① `detect` 的 `run` 原文跑 7 种输入
|
||||
(跳过 / 约定成对 / 约定半套 / dispatch 成对 / 点名不存在 / 正例缺 PNG / 负例 —— 判据 6);
|
||||
② `judge` 的 `run` 原文跑两次(正例必须 0、CPU 档必须非 0,`bash -e` 与裸 bash 各跑一遍 —— 判据 5)。
|
||||
**这不是 CI 实测**:`if:` 求值、job skip 在 UI 上的呈现、`needs` 的输出传递在真 Actions 里仍未跑过。
|
||||
- **没在真 GPU runner 上跑过**:`evidence` 的真判定只在本地用合成向量走过(0 与 1 都到达过),
|
||||
「真产物 + 真 GPU runner」这个组合没实测。
|
||||
- **没验「跳过是否满足 required status check」**:本 workflow 没挂成必需检查;GitHub 对 skipped job
|
||||
与 required check 的交互口径没实测,别把这一节读成「跳过也能当门过」。
|
||||
- **自检脚本自身的边界**在它自己的 docstring 里(不防「连判据一起改」;`!` 不在支持的表达式语法里,
|
||||
出现即红;只有 `probe` 那一步是功能断言,`vectors`/`exitcodes` 两步只做结构断言)。
|
||||
@@ -0,0 +1,208 @@
|
||||
# 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 并断言版本串(`slot != host_slot()` 才跳过)。`linux-x64` 侧本轮**真跑过**(见 §2 `smoke: ull --version -> 0.2.3 (ok)`) | **机制存在 ≠ 有过一次运行**:截至本分支,仓库里找不到任何一次 `linux-arm64` 成功出包的记录(`git grep -rn "all_slots" DevDocs/` 零命中),所以 **arm64 侧运行时证据为零** —— 连这条 CLI 冒烟都没在 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)有 **8~37 倍**余量
|
||||
(唯一色 1858/64≈29×、强边缘 0.0372/0.001≈37×、**亮度 σ 只有 24.06/3.0≈8×** —— 与 `check_screenshot.py` 自己
|
||||
写的「阈值取在实测下限的 1/5~1/8」一致,三者里最紧的是亮度 σ):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 上一次都没启动过。连 `make_artifacts.py` 那条 `ull --version`
|
||||
冒烟也**只是机制上会真跑**(`mode: ci` + 宿主==产物),**截至本分支没有一次 arm64 成功出包的记录**,所以 arm64
|
||||
侧一份运行时证据都没有 —— 别把「这条冒烟以后会跑」读成「它跑过了」。
|
||||
- **`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。(何况截至本分支 arm64 上连那条 CLI 冒烟都还没真跑过一次。)
|
||||
@@ -0,0 +1,128 @@
|
||||
# Cargo 构建档位与实测数据(M145 拆自 `packaging/README.md`)
|
||||
|
||||
本文承载原 `packaging/README.md` 的 §6、§7,**节号沿用原编号**(全仓 `packaging/README.md §N` 这类引用照旧有效,
|
||||
对照表在 [`packaging/README.md`](../README.md) 的索引里)。内容一字未改、只搬家:本行以上是 M145 新增的导航说明,
|
||||
以下逐行取自原文件。
|
||||
|
||||
## 6. Cargo 构建档位(release 体积档 / fast 速度档)
|
||||
|
||||
| 档位 | 用在哪 | opt-level | lto | codegen-units | panic |
|
||||
|------|--------|-----------|-----|---------------|-------|
|
||||
| `release` | 发布 / 验收产物 | `"z"`(workspace 自身 crate) | `fat` | 1 | `abort` |
|
||||
| `release.package."*"` | 同上,**只作用于依赖** | `"z"` | 不可覆盖 | 16 | 继承 |
|
||||
| `fast` | 日常迭代、本机跑测试 | 2 | `thin` | 16 | `unwind` |
|
||||
|
||||
- **`release` 是体积档**:`fat` + 单 CGU 换来实测 GUI −10.8% / CLI −18.7%(§7.1)。变慢的根源在 `codegen-units = 1`
|
||||
(单 CGU → LTO 只一个线程可跑;`fat` 只管"跨 crate 全量参与",不等于单线程),375 个 crate 排队链接就是十几分钟的量。
|
||||
- **`release.package."*"` 放宽的只有并行度,不是 opt-level**:实测把依赖 opt-level 抬到 3 反而**又慢 29.6%
|
||||
又大 19~24%**(§7.1),所以保持 `"z"`、只把 `codegen-units` 放到 16。package 覆盖**只允许调优化强度**:
|
||||
`panic` / `lto` / `rpath` 写进去 cargo 直接报错,`strip` 会改变整个二进制的运行时行为——都不许出现。
|
||||
- **`fast` 是速度档,不是发布物**:`thin` LTO + 16 CGU 可并行,`opt-level = 2` 介于 `z` 与 3 之间。
|
||||
`panic = "unwind"` 是给**编出来的二进制**留的(`release` 是 `abort`,`inherits` 会照抄);`cargo test`
|
||||
**不**依赖它——cargo 对 test 目标自己强制 unwind(实测 release/abort 档下 `cargo test --profile release`
|
||||
照跑、23 个测试全过,test 单元的 rustc 参数里没有 `-Cpanic=`)。要看这行是否生效,比 `cargo build -v`
|
||||
的 rustc 参数:release 有 `panic=abort`、fast 没有。它继承 `strip="symbols"`,所以能 unwind 但**没有符号
|
||||
回溯**——要栈回溯就给 fast 档加 `strip = false`,代价是体积再涨。
|
||||
- **`fast` 在机械盘机器上冷编译并无收益**(比 release 还慢、体积 2.3 倍,§7.1),价值在 SSD 机器与增量重编。
|
||||
- **`[profile.fast.package."*"]` 是承重墙**:`inherits` 会把 release 的 package 覆盖一起带过来(实测:删掉它,fast 档外部依赖静默退回 `"z"`),必须存在。
|
||||
- 本地开发迭代走 `cargo build`(dev profile)不受影响,但 **`ci.yml` 每次 push/PR 都跑
|
||||
`cargo build --workspace --release`**,会实打实吃这份 fat LTO 的链接时间——CI 慢的一部分是 release 档构建。
|
||||
|
||||
## 7. 实测(本机,不是估算)
|
||||
|
||||
构建环境:rustc 1.99.1 stable / `x86_64-pc-windows-gnu`(MinGW 链接器)。§7.2/§7.3 的早数字是另有 worker 并发做全量 debug 构建时测的(偏悲观);§7.1 的三行 M96 数据是空载冷编译。
|
||||
|
||||
**本节以下所有输出里的 `0.1.0` 都是当时那次运行的真实输出(workspace 版本那时是 `0.1.0`),属实测记录,不随后续版本号改动而回填**;当前版本见 `Cargo.toml` 的 `[workspace.package] version`。§2 的校验机制不依赖任何写死的数字,改版本号不会让它失效。产物名同理且更容易被误读成现状:§7.3 / §7.5 里的 `unlucky-launcher-0.1.0-windows-*.exe`(**连字符**)是 2026-11-01 那几次实测的原文,D49(2026-11-05)才改成下划线形态——同一槽位现在叫 `unlucky-launcher_0.2.3_windows_x64.exe`(规则见 §1);拿旧名去 Release 页或磁盘上找文件必然找不到,那不代表发布挂了。
|
||||
|
||||
### 7.1 体积与冷构建耗时(实测字节数,不是估算)
|
||||
|
||||
| 档位 | `unlucky-launcher.exe` | `unlucky-launcher-iced.exe` | `ull.exe` | 冷构建耗时 |
|
||||
|------|-----------------------|----------------------------|-----------|-----------|
|
||||
| `release`,`thin`(M46 前) | 16,366,080 B = 15.61 MiB | 未测(当时无 iced 版) | 7,572,992 B = 7.22 MiB | 11m52s |
|
||||
| `release`,`fat`,依赖全 `"z"`(M46,改前) | **14,595,584 B = 13.92 MiB** | 未测 | **6,157,824 B = 5.87 MiB** | 9m27s(增量) |
|
||||
| `release`,`fat` + 依赖 opt-3 / cu16(M96 试过的那版,**已否**) | 20,407,296 B = 19.46 MiB | 18,371,584 B = 17.52 MiB | 7,743,488 B = 7.38 MiB | 22m45s(冷) |
|
||||
| `release`,`fat` + 依赖 `"z"` / cu16(**M96 起,发布档**) | **16,479,232 B = 15.72 MiB** | **13,910,528 B = 13.27 MiB** | **6,256,128 B = 5.97 MiB** | **16m01s(冷)** |
|
||||
| `fast`,`thin` + cu16(M96 起,日常档) | 38,182,400 B = 36.41 MiB | 33,570,304 B = 32.02 MiB | 15,967,744 B = 15.23 MiB | 24m27s(冷) |
|
||||
|
||||
测量条件(M96 三行):12 核 / `x86_64-pc-windows-gnu` / `-j 4`,源码机械盘、`CARGO_TARGET_DIR` 在 SSD,均为**删空 target 后的
|
||||
冷编译**、同机同晚无并发负载;M46 两行是更早提交上的数字(依赖已热 / 另有并发),**跨行比耗时没意义**。
|
||||
① **依赖 opt-level 从 3 退回 `"z"` 又快 29.6% 又小 19~24%**(两臂同为 cu16,唯一自变量是 opt-level)——抬 opt-level 是拿编译时间和体积换运行期;
|
||||
`cu1 → cu16` 的收益本轮**没有同条件对照**,未归因。② `fast` **冷编译比 `release` 还慢**(24m27s vs 16m01s)、体积 2.3 倍,瓶颈在两个大 UI
|
||||
crate + 链接 + 机械盘 IO——定位是 **SSD 机器与增量重编**,不是冷编译加速。体积归因:GUI 相对旧基线 +12.9%(13.92 → 15.72 MiB)里
|
||||
「代码增长」与「依赖 cu16」**未分离**;`ull` 只 +1.7%,在 +1~3% 口径内。体积构成(**M118 前的 Slint 版,历史数**):Slint 运行时(双渲染器 + winit/accesskit/托盘)+ rustls/reqwest + rig/rmcp/axum 为主,`ull` 更小是 D18(CLI 零 UI 依赖)的结果。
|
||||
|
||||
### 7.2 冒烟(真跑,2026-10-31 夜间 + 11-01 早,本机 Win11 26100)
|
||||
|
||||
| 命令 | 实测结果 |
|
||||
|------|---------|
|
||||
| `ull.exe --version` | 输出 `ull 0.1.0(unlucky-cli)` + `os: Windows 11 · 10.0.26100(x86_64)· 满足最低要求`,exit 0 —— 版本与 workspace `0.1.0` 一致 |
|
||||
| `unlucky-launcher.exe --software-renderer --screenshot … --data-dir …`(Slint 版,历史实测) | exit 0,产出 1180×760 PNG(3,588,298 B);看图确认 Metro 实例页(侧栏/搜索/空态/AI 助手面板)正常渲染 → **软渲染路径真的能出图**,不依赖 GL |
|
||||
| `unlucky-launcher.exe --version` | **无输出**,8 秒后被 timeout 杀掉(exit 124)——GUI 当时不解析 `--version`,会直接开窗口。M82 后已修(走 CLI 分发,见 §10-1)|
|
||||
|
||||
### 7.3 打包验证(本机 Windows 槽位真跑;产物名是 D49 改名前的形态)
|
||||
|
||||
```
|
||||
smoke: ull.exe --version -> 0.1.0 (ok)
|
||||
artifact: unlucky-launcher-0.1.0-windows-x64.exe 13.92 MiB sha256=d4954c62…c3570
|
||||
sums: SHA256SUMS.txt
|
||||
$ sha256sum -c SHA256SUMS.txt
|
||||
unlucky-launcher-0.1.0-windows-x64.exe: OK
|
||||
```
|
||||
|
||||
版本守卫也实测过:`--version v9.9.9` 时脚本以 `tag/version mismatch: requested 9.9.9, Cargo.toml says 0.1.0` 失败退出(exit 1)。
|
||||
|
||||
UNIX 归档的可复现性单独取证(本机 Windows 槽位只走裸 exe 分支,tar 路径要另测):
|
||||
reviewer-packaging 在 M46 review 里连续调 `stage_archive()` 两次 → **两次 sha256 完全一致**
|
||||
`a99d6e9e727cd7e390434c566a15978451f340858fa89836e2f63790bbc392f8`;成员 `mtime=0`、
|
||||
`uid=gid=0`、`uname/gname` 为空、`mode` 0755(二进制)/ 0644(文本),gzip header 的 mtime 字段是
|
||||
`00000000`,归档内单顶层目录且含 `LICENSE`/`NOTICE`/`README.md`。
|
||||
即"同源构建 → 同 sha256"在 tar 路径上成立,不随打包时刻漂移(见 §1 的约定细节)。
|
||||
|
||||
### 7.4 运行时依赖证据(PE 导入表 / ELF `DT_NEEDED`)
|
||||
|
||||
`objdump -p target/release/unlucky-launcher.exe` 共 42 条 DLL 导入记录、33 个不同名字(大小写各记一次),全部是 Windows 自带:
|
||||
`KERNEL32/USER32/GDI32/ADVAPI32/SHELL32/SHLWAPI/ole32/oleaut32/combase/comctl32/dwmapi/dwrite/imm32/
|
||||
uxtheme/uiautomationcore/opengl32/ws2_32/bcrypt/bcryptprimitives/ntdll` + 一组 `api-ms-win-crt-*`(UCRT API set)。
|
||||
|
||||
**没有** `VCRUNTIME140.dll` / `MSVCP140.dll` 这类 MSVC 可再发行包依赖,也没有 `libwinpthread-1.dll` /
|
||||
`libgcc_s_*.dll` 之类 MinGW 运行时 —— 即"拷走一个 exe 就能跑"(Win10+ 自带 UCRT)。
|
||||
CI 的 MSVC `+crt-static` 产物同理(静态 CRT);`release.yml` 里用 `dumpbin -dependents` 把这条变成硬门禁,
|
||||
两侧都 fail、没有降级分支:**出现 VCRUNTIME/MSVCP/ucrtbase 就 fail**,**找不到 dumpbin 同样 fail**
|
||||
(`windows-latest` 与 `windows-11-arm` 两个镜像都自带 VS 工具链,基本找得到;跳过等于让门禁在最需要它时失效)。
|
||||
总之不允许"悄悄要用户装可再发行包"。
|
||||
注意:本机只装了 GNU 工具链,MSVC 那个槽位的产物本身**没有在本机验证过**,由 CI 首跑验证。
|
||||
|
||||
Linux 侧同一件事的对照(M82 实测:`readelf -d` + `ldd` + `nm -D`,容器内 Debian 12 glibc 2.36;被测的是 **Slint 版 GUI,历史实测**):Slint
|
||||
GUI 的 ELF `DT_NEEDED` 只有 `libfontconfig.so.1` / `libgcc_s.so.1` / `libm.so.6` / `libc.so.6` /
|
||||
`ld-linux-x86-64.so.2`;`libX11` / `libxcb` / `libxkbcommon` / `libwayland-client` / `libGL` / `libEGL`
|
||||
**都不在**导入表里,而是以字符串形式由 `dlopen` 在开窗那一刻加载(`nm -D` 可见 `U dlopen@GLIBC_2.34` /
|
||||
`dlsym` / `dlclose`)。所以 §4 那张「开窗要的库」不是启动前提,缺了也不影响进程起来跑 CLI;
|
||||
`ull` 的 `DT_NEEDED` 连 fontconfig 都没有,只剩 glibc 那一组。
|
||||
|
||||
**补一次真跑(M82 复审在 `debian:bookworm-slim` 上做的)**:镜像里 X11 / wayland / GL / EGL / xkbcommon /
|
||||
libxcb 条目数 = 0,只把 fontconfig 那 7 个 `.so` 放进 `LD_LIBRARY_PATH`,跑**改动后**的二进制 → `--version`
|
||||
与 `--help` exit 0、`instances list --json` 合法 JSON exit 0、`lst` exit 1 且带提示;`readelf -d` 复核仍是
|
||||
恰好 5 条 `DT_NEEDED`(无 `libstdc++`、无任何图形库),`ull` 4 条 —— 即「能跑完子命令」是实测,不是推论。
|
||||
|
||||
### 7.5 arm64 槽位的本机验证(脚本层取证,不假装跑得动 arm64 二进制)
|
||||
|
||||
本机没有 ARM 机器,也不具备 MSVC ARM64 链接库组合,所以只做**能真实证明的东西**(`python` 直接加载
|
||||
`packaging/make_artifacts.py` 调函数 + 走 CLI 全流程,证据见下):
|
||||
|
||||
| 取证项 | 结果 |
|
||||
|--------|------|
|
||||
| 宿主判定注入 | `win32/AMD64`→`windows-x64`、`win32/ARM64`→`windows-arm64`、`linux/x86_64`→`linux-x64`、`linux/aarch64`→`linux-arm64`、`linux/arm64`→`linux-arm64`、`darwin/arm64`→`macos-arm64`、`darwin/x86_64`→`macos-x64`、`freebsd/riscv64`→exit 1 |
|
||||
| 文件头架构识别 | 真 `unlucky-launcher.exe`→`x64`;合成 PE `0xAA64`→`arm64` / `0x8664`→`x64`;ELF `0xB7`→`arm64` / `0x3E`→`x64`;Mach-O `0x0100000C`→`arm64` / `0x01000007`→`x64`;垃圾字节→`None`(拒绝出包) |
|
||||
| `windows-arm64` stub 全流程 | 把真 exe 的 PE 机器码改成 ARM64 当 stub:`arch: … -> arm64 (ok for windows-arm64)`、`smoke: skipped (host is windows-x64, artifact is windows-arm64)`、产物 `unlucky-launcher-0.1.0-windows-arm64.exe`(当次实测的连字符名)14.01 MiB,`SHA256SUMS.txt` 行名一致 |
|
||||
| 架构守卫真拦 | x64 二进制冒充 `--slot windows-arm64` → exit 1:`is a x64 binary but slot windows-arm64 says arm64` |
|
||||
| `linux-arm64` stub 归档 | 成员 `unlucky-launcher-0.1.0-linux-arm64/{LICENSE,NOTICE,README.md,unlucky-launcher}`(旧连字符名,当次实测),权限 0644/0755、`uid=gid=0`、mtime 0、gzip 头 mtime 字段 `00 00 00 00`;连调两次 sha256 完全一致(`f949d0f5…`)→ 可复现规则与 x64 同源 |
|
||||
| `windows-x64` 原生回归 | `arch: … -> x64`、`smoke: ull.exe --version -> 0.1.0 (ok)`、产物 14.01 MiB、`sha256sum -c SHA256SUMS.txt` OK(这一次用的是当前工作树的二进制,与 §7.3 那次不是同一次构建,sha256 自然不同) |
|
||||
|
||||
**没在本地验证、只能由 CI 补的**:`windows-arm64` 在 `windows-11-arm` 上的真实链接与 ARM64 导入表、
|
||||
`linux-arm64` 在原生 arm64 runner 上的真跑(本机既没有 ARM 机器,也连 `raw.githubusercontent.com` 都解析不了)。
|
||||
**M164 的更正**:M141/M142 那个编译前体检不但没帮上忙,还是 arm64 五轮全红的唯一原因(`find` 猜 SDK 的
|
||||
`um/arm64/kernel32.lib`,猜缺就 `exit 1`,而 `cargo build` 永远 skipped);M164 把它降级为只取证(打印 vswhere 组件、
|
||||
目录 `.lib` 计数、`link.exe` 候选),红绿交回编译器。闭环标准不是「两槽位都进 SHA256SUMS」(M142 起只有 x64 进),
|
||||
而是 arm64 的 `cargo build` 真跑绿 + 把键加回 `PUBLISH_SLOTS`(守卫会自动跟着扩,并在产物有了却没声明时报 `::warning::`)。
|
||||
本机能补的只有脚本层证据:`python packaging/check_publish_slots.py --self-test`(11 条注入回归)与
|
||||
`cargo check --target aarch64-pc-windows-msvc`(依赖图对该 target 的类型检查,**不链接、不产出 PE**)。
|
||||
@@ -0,0 +1,86 @@
|
||||
# 每平台运行时依赖与软渲染兜底(M145 拆自 `packaging/README.md`)
|
||||
|
||||
本文承载原 `packaging/README.md` 的 §4、§5,**节号沿用原编号**(全仓 `packaging/README.md §N` 这类引用照旧有效,
|
||||
对照表在 [`packaging/README.md`](../README.md) 的索引里)。内容一字未改、只搬家:本行以上是 M145 新增的导航说明,
|
||||
以下逐行取自原文件。
|
||||
|
||||
## 4. 每平台运行时依赖
|
||||
|
||||
### Windows x64(Win10/11)
|
||||
|
||||
| 依赖 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| `KERNEL32` / `USER32` / `GDI32` / `ADVAPI32` / `SHELL32` / `ole32` / `WS2_32` … | 系统 | Win10+ 一律自带 |
|
||||
| `ucrtbase` / `api-ms-win-crt-*`(UCRT) | 系统 | Win10+ 内置,**不需要** VC++ 可再发行包 |
|
||||
| MSVC 运行时(`VCRUNTIME140` / `MSVCP140`) | **无** | `+crt-static` 静态链接,故不需要可再发行包 |
|
||||
| OpenGL(`opengl32`) | 系统 | 只在 GL 渲染路径用;缺失走软渲染 |
|
||||
|
||||
本机(GNU 工具链)实测导入表证据见 §7。
|
||||
|
||||
### Windows ARM64(Win11 on ARM)
|
||||
|
||||
**本版不发布**(CI 照构建取证,红了不挡 x64 发布;等它绿了把键加回 `PUBLISH_SLOTS` 即可)。产物形态与门禁跟 x64
|
||||
一样(裸 exe + 静态 CRT + dumpbin 导入表硬门禁),差别只在**怎么编**:**M140 起在原生 `windows-11-arm` 上编**
|
||||
(GitHub 文档 2026 的 Standard runners 表明列该标签:4C/16G、公开仓库免费),产物落在 `target/release`。原路线
|
||||
是在 `windows-latest` 上交叉到 `aarch64-pc-windows-msvc`,但**CI 上从未成功过一次**(v0.2.1 首次真跑即红在
|
||||
「ARM64 工具链体检」)。M142 已确认那几轮「只剩一行 exit code 1」是**诊断脚本自己先死**(`set -uo pipefail` 关不掉
|
||||
runner 注入的 `-e`,`find` 扫不存在的根被 pipefail 判非零就退出),已修;ARM64 的静态库在这台 runner 上到底缺不缺
|
||||
要看下一次运行的新注解(固定 4 行实况清单,见 §3)。原生的附赠好处:宿主=产物架构,`ull --version` 冒烟**真跑**。
|
||||
|
||||
运行时依赖与 x64 同表(UCRT 是 Win11 ARM 的系统组件,`+crt-static` 静态链 CRT,不需要可再发行包)。
|
||||
游戏侧如实说:**启动器原生运行**;Temurin 没有 windows-aarch64 产物,Microsoft OpenJDK 有该
|
||||
发行版(core 的 `microsoft` 源按 `<os>-<arch>` 拼名,命名已覆盖),但 Minecraft 本体没有
|
||||
Windows ARM64 原生库(Mojang/LWJGL 只发 x64/x86),实际游玩仍走系统 x64 模拟层 + x64 JDK。
|
||||
所以本槽位只承诺「启动器产物」支持,不替 Java/游戏管理做架构级承诺。
|
||||
|
||||
### Linux x64
|
||||
|
||||
| 依赖 | 说明 |
|
||||
|------|------|
|
||||
| glibc ≥ 2.35 | 由 `ubuntu-22.04` 构建决定(该槽位默认不编,`all_slots=true` 取证跑才有产物);更老的发行版请自行重建或等 musl 版 |
|
||||
| `libxkbcommon.so.0` | winit 键盘处理 |
|
||||
| `libfontconfig.so.1` + `libfreetype.so.6` | 文本渲染 |
|
||||
| `libGL.so.1` / `libEGL.so.1` | Slint 版:femtovg 的 GL 渲染;iced 版:wgpu 的 GL/EGL 后端(**推断,未实测**)。两者缺失都退到 CPU 软渲染(tiny-skia) |
|
||||
| `libxcb.so.1` / `libX11.so.6` | X11 后端(Wayland 会话下也需 XWayland 相关库) |
|
||||
| `libwayland-client.so.0` | Wayland 后端 |
|
||||
|
||||
Debian/Ubuntu 一行:`sudo apt install libxkbcommon0 fontconfig libgl1 libegl1 libxcb1 libx11-6 libwayland-client0`。
|
||||
运行期**不需要** `-dev` 包、不需要 Java(Java 是启动游戏时才由启动器自己管理的东西)。
|
||||
|
||||
**M118(D26)口径**:除上表里标注「推断,未实测」的那一行外,这张表与下面这条结论都是 **Slint 版 GUI** 的实测记录(M82:`readelf -d` + `ldd` + `nm -D`);iced 版(`crates/app-iced`)的 `DT_NEEDED` / `dlopen` 集合**尚未重测**,请按「同一族库、细节待验」读,别当成 iced 版的实测承诺。
|
||||
仍然成立的部分:上表是**开窗**要用的库,不是进程启动门槛——实测(§7.4,Slint 版)除 `libfontconfig.so.1` 外它们**都不在** ELF 的 `DT_NEEDED` 里,由 `dlopen` 在开窗那一刻才加载,所以没有 X11 / Wayland / GL 的机器上 `unlucky-launcher instances list` 照样跑得起来,`ull` 连 fontconfig 都不需要。
|
||||
arm64 侧同理(同一份依赖树;历史口径里的「winit + femtovg」中 femtovg 只属于 Slint 版)。
|
||||
|
||||
### Linux arm64(aarch64)
|
||||
|
||||
依赖与 x64 完全同一套(`libxkbcommon` / `fontconfig` / `libGL` / `libxcb` / `libwayland-client`),
|
||||
**glibc 地板也相同**:arm64 槽位跑在原生 `ubuntu-22.04-arm` runner 上(默认矩阵不编它,只有 `all_slots=true` 取证跑才出产物),地板 = **2.35**,与 x64
|
||||
用的 `ubuntu-22.04` 一致。选 22.04 而不是 24.04 就是为了这个地板——24.04 的 glibc 更高,会把
|
||||
Debian 12(2.36)、Raspberry Pi OS Bookworm(2.36)这些实际在跑的 ARM 发行版挡在门外。
|
||||
要抬地板(例如将来 ARM 生态整体上移)换 `release.yml` 里那个槽位的 `os` 即可,脚本与产物名都不用动;
|
||||
`ubuntu-24.04-arm` / `ubuntu-26.04-arm` 都是同一家族的标准公开仓库标签(4C/16GB、免费不限量)。
|
||||
|
||||
### macOS 12+
|
||||
|
||||
| 依赖 | 说明 |
|
||||
|------|------|
|
||||
| 系统框架(Cocoa / Metal / IOKit / Security …) | 由操作系统提供,无第三方动态库 |
|
||||
| `libSystem` | 系统 |
|
||||
|
||||
macOS 产物是**裸二进制**而非 `.app` bundle:符合单文件承诺,但没有 Dock 图标、Finder 双击不生效,
|
||||
从终端启动(README 里给了 `xattr -d com.apple.quarantine` 的 Gatekeeper 解法)。
|
||||
签名/公证(需 Apple Developer 账号)与 `.app` 打包是后续工作,不做假承诺。
|
||||
|
||||
## 5. 软渲染兜底(无 GPU / 老驱动 / 远程桌面)
|
||||
|
||||
**现役入口(iced 版)**:`unlucky-launcher-iced --software-renderer`(等价 `--renderer=cpu`,即 `ICED_BACKEND=tiny-skia`)
|
||||
走 tiny-skia 光栅化,这是现役构建唯一的软渲染入口——CI 的 runtime-smoke 与 `packaging/linux-smoke-docker.sh` 都这么跑(§11)。
|
||||
|
||||
**历史资产(`v0.1.0` 的 Slint 版)**:那批已发布二进制仍认 `unlucky-launcher --software-renderer`
|
||||
(解析在 `crates/app/src/main.rs` 的 `apply_renderer_flag()`,只认完全相等的那个串)。
|
||||
这条 Slint 路径(默认 `renderer-femtovg`/GL)已不在默认构建与发布矩阵内——`crates/app` 被 workspace `exclude`,
|
||||
源码仅作参考、不编译——所以它**对现役 `unlucky-launcher-iced` 无效**,别照抄到 iced 版上。
|
||||
|
||||
- 软渲染是 CPU 光栅化,无 GPU 也能出图;代价是动画与大窗口重绘更慢(Metro 纯色界面影响有限)。
|
||||
- 截图通道 `--screenshot <png>`:历史实现 `crates/app/src/screenshot.rs`,现役 `crates/app-iced/src/shot/`;
|
||||
软渲染下照样出图,是 CI 与远程机唯一可靠的"真出图"证据(发布冒烟见 §7,本机实跑 `--software-renderer --screenshot`)。
|
||||
@@ -0,0 +1,190 @@
|
||||
# 签名就绪度操作手册(M255)
|
||||
|
||||
本文件是**人类照做**的手册:从「信任表是空的」走到「发出去的 Release 用客户端判据能验过」。
|
||||
背景/威胁模型/轮换纪律在 [`signing.md`](signing.md);本文件只讲**步骤、命令、坑**。
|
||||
|
||||
> ⚠ 本文件**不含**任何真实私钥、种子或公钥。所有示例都是 `<尖括号占位符>`。
|
||||
> `--generate-key` 的输出是**敏感物**:只在你要贴进 Secret 的那一次运行里存在,别存文件、别贴 issue。
|
||||
|
||||
## 0. 现状读一遍(2026-11 的 M254 审计结论)
|
||||
|
||||
`crates/core/src/update/trust.rs` 的 `PRODUCTION` 表在 `PINNED-KEYS` 标记之间是**空的**。空表 =
|
||||
`KeyringEmpty` 一律拒装 ⇒ **今天任何正式二进制都验不过任何清单**(fail-closed 成立,但用户端表现为
|
||||
「更新永远失败」)。已存在的部分(都能用):
|
||||
|
||||
| 环节 | 在哪 | 今天的状态 |
|
||||
|---|---|---|
|
||||
| 生成钥 | `sign_manifest.py --generate-key` | 有;打印 Secret JSON + `PinnedKey` 那一行 |
|
||||
| 钥 ↔ 信任表对账 | `sign_manifest.py --check-pinned-keyring` | 有;**今天必然红**(表空 ⇒ 「信任表是空的」) |
|
||||
| 签名 | `sign_manifest.py --sign dist/manifest.json` → `.sig` | 有;**今天必然红**(`ULL_MANIFEST_SIGNING_KEY` 没有,前面先红) |
|
||||
| 回验 | `sign_manifest.py --verify-pinned dist/manifest.json` | 有;**今天必然红**(表空 ⇒ 「无从回验」) |
|
||||
| 发布接线 | `release-publish.yml` 的「签更新清单」步 | 有;上面四条任一非零 ⇒ 不出 Release(**这是对的**) |
|
||||
| 资产上传 | `release-publish.yml` 的 `assets=(… manifest.json manifest.json.sig …)` | 有 |
|
||||
| **操作步骤与可见性** | 本文件 + `packaging/check_signing_ready.py` + `signing-status.yml` | **本次补上的就是这两样** |
|
||||
|
||||
一句话:**工具链不缺,缺的是有人把钥钉进表里。** 那件事只有人类能做(塔不会自己生成生产私钥)。
|
||||
|
||||
## 1. 先看读数(不用碰任何钥)
|
||||
|
||||
```bash
|
||||
python3 packaging/check_signing_ready.py # 三行读数 + 退出码
|
||||
python3 packaging/check_signing_ready.py --dist dist # 顺手看一眼本地留档
|
||||
```
|
||||
|
||||
三行分别是 ① `PRODUCTION` 表空不空 ② 环境里有没有签名钥(并顺手对账「这把钥在不在表里」)
|
||||
③ 最近一次发布的 `.sig`/清单痕迹(取不到就写「未知」——本地没留档 ≠ 上次没签)。
|
||||
|
||||
退出码:`0` 就绪 / `1` 未就绪 / `2` 用法错(参数写错、`trust.rs` 读不到、标记丢了)。
|
||||
**未就绪时会打印一行修复命令**,直接照抄即可。CI 里同一条读数在 `Signing status(播报)` workflow 的
|
||||
job summary 里(见 §6 —— 它是播报,不是门禁)。
|
||||
|
||||
## 2. 人类要亲手做的 5 步(下单清单)
|
||||
|
||||
下面每一行都只有命令与占位符,**没有任何密钥值**。做完一步就把该步的验证跑一遍。
|
||||
|
||||
> 🔴 **顺序硬要求(M255 r1 P2-1):先合并「播报 workflow 的 ref 门」那笔改动,再录 Secret。**
|
||||
> 播报 workflow(`.github/workflows/signing-status.yml`)会把 `ULL_MANIFEST_SIGNING_KEY` 注入到
|
||||
> 就绪度那一步的环境里;那个注入**只在 `refs/heads/main` 上发生**,而且该 workflow **没有**
|
||||
> `pull_request` 触发。这两条是安全边界:仓库内分支的 PR 与任意分支的 `workflow_dispatch` 都拿得到
|
||||
> 仓库 Secret,而 CI 会在**那个 PR 自己的代码**上执行 checker —— 种子一旦进过那种运行,等于交给
|
||||
> 任何能推分支的人(这把种子能给**全量已安装客户端**签可信清单,即用户端 RCE)。
|
||||
> 所以:**ref 门那笔没合进去之前不要录 Secret**;录之前先确认线上版本里有
|
||||
> `github.ref == 'refs/heads/main'` 这个条件式(§6 也写了这条)。
|
||||
|
||||
```bash
|
||||
# ① 生成钥(⚠ 输出是敏感物;不要跑在 CI 里,不要重定向到仓库里的文件)
|
||||
python3 packaging/sign_manifest.py --generate-key --key-id <key-id,例 ull-release-2026a>
|
||||
|
||||
# ② 把打印出来的那一行 JSON({"key_id": …, "seed_hex": …})贴进仓库 Secret(⚠ 先读上面的顺序硬要求):
|
||||
# Settings → Secrets and variables → Actions → New repository secret
|
||||
# 名字必须**逐字**是:ULL_MANIFEST_SIGNING_KEY
|
||||
# (等价 CLI:gh secret set ULL_MANIFEST_SIGNING_KEY,然后按提示粘贴,别用 -b 把值留在 shell 历史里)
|
||||
|
||||
# ③ 把打印出来的那一行 PinnedKey { … } 贴进 trust.rs 的 PINNED-KEYS 标记之间(标记本身别动)
|
||||
grep -n 'PINNED-KEYS' crates/core/src/update/trust.rs
|
||||
# 贴完提交这次改动 —— 公钥进 git 是对的:它是信任锚,本来就该公开可审计
|
||||
|
||||
# ④ 对账(需要一个有钥的环境:本机就先 export,别写进任何文件)
|
||||
python3 packaging/sign_manifest.py --check-pinned-keyring # 期望:ok: 签名钥 <key-id> 已被钉住
|
||||
|
||||
# ⑤ 发一次带 .sig 的 Release,然后用**客户端的判据**回验(这一步不需要私钥)
|
||||
python3 packaging/sign_manifest.py --verify-pinned dist/manifest.json # 期望:ok: 用钉住的公钥能验过
|
||||
```
|
||||
|
||||
`--verify-pinned` 才是证据:`--sign` 说「签好了」不算 —— 客户端拿的是 `trust.rs` 里那把公钥。
|
||||
发布后建议再用**下载下来的资产**验一次(§5),因为「CI 上能验过」不等于「上传的那份就是被签的那份」。
|
||||
|
||||
### 每一步的判据(怎么知道这步真的成了)
|
||||
|
||||
| 步 | 成功长什么样 | 失败长什么样 |
|
||||
|---|---|---|
|
||||
| ① | 打印 ① 私钥 JSON、② 一行 `PinnedKey`、③ 公钥十六进制;脚本自带「自己签自己验」自检 | `error: 自己签自己验不过` ⇒ 不要用这把钥,环境有问题 |
|
||||
| ② | `gh secret list` 里能看到 `ULL_MANIFEST_SIGNING_KEY` | 列表里没有 / 名字拼错(大小写、下划线) |
|
||||
| ③ | `check_signing_ready.py` 的 ① 从「未就绪」翻成「就绪」 | ① 仍说 0 条 ⇒ 贴到了标记**外面**(读数里会点名「另有 N 条在标记之外(key_id:…)」) |
|
||||
| ④ | `ok: 签名钥 … 已被钉住` | `签名钥(…)不在信任表里` ⇒ ②③ 不是同一把钥 |
|
||||
| ⑤ | `ok: manifest.json.sig 用钉住的公钥 … 能验过(客户端会接受)` | `拿 trust.rs 里钉住的公钥回验不过 —— 这份 Release 的用户会全部拒装。别发版` |
|
||||
|
||||
## 3. 一次发布里签名是怎么被执行的(不用额外操作)
|
||||
|
||||
`release-publish.yml` 的「签更新清单」步按顺序跑四条,任一非零 ⇒ 本步红 ⇒ 不出 Release:
|
||||
|
||||
```bash
|
||||
sign_manifest.py --self-test # RFC 8032 向量:脚本自己没坏
|
||||
sign_manifest.py --check-pinned-keyring # Secret 里的钥必须已在 trust.rs 钉住
|
||||
sign_manifest.py --sign dist/manifest.json # 产出 dist/manifest.json.sig
|
||||
sign_manifest.py --verify-pinned dist/manifest.json # 用钉住的公钥回验产出
|
||||
```
|
||||
|
||||
这一步已经把「未配置 Secret」「钥没钉」「钥不匹配」都变成**发版前的红**,所以「发了但没签名」这条
|
||||
不会发生 —— 它只会在发布 job 上红,不会静默发出去。
|
||||
|
||||
## 4. 已知坑(按踩到的概率排序)
|
||||
|
||||
1. **Secret 名字拼错**(`ULL_MANIFEST_...` vs `ULL_MANIFEST_KEY`):可复用工作流只拿得到自己**声明过**的
|
||||
secret,名字不对就是空串 ⇒ 签名步红,而错误文案会反过来指控「Secret 没配」。`release-publish.yml`
|
||||
已经声明了这一个(`required: false`,缺省走 fail-closed),照抄名字即可。
|
||||
2. **只配 Secret 没钉表**:`--check-pinned-keyring` 报「信任表是空的(fail-closed)」,并**直接打印**
|
||||
该贴的那一行 `PinnedKey`(只含公钥,安全)。照贴即可。
|
||||
3. **只钉表没配 Secret**:`--check-pinned-keyring` 报「没有签名密钥」。这不影响已有用户(表非空 ⇒
|
||||
他们能验签),但发不了新版。
|
||||
4. **Secret 与 trust.rs 是两把不同的钥**:最常见于「换过钥但只更新了一边」。判据是**公钥字节**,
|
||||
`key_id` 只是标签;`--check-pinned-keyring` 会把两边的公钥都打出来对账。
|
||||
5. **把条目贴到标记外面**(`];` 之后、或贴着别的 `const`):等于没钉。`check_signing_ready.py` 会**点名**
|
||||
标记之外的 `key_id`;注意本仓 `TEST_ONLY` 那条**本来就在标记之外**(那是它的正确位置),所以读数是
|
||||
「另有 N 条在标记之外(key_id:…)」而不是「贴错了」—— 对着 `key_id` 自己认。
|
||||
6. **`set -x` / `echo $ULL_MANIFEST_SIGNING_KEY`**:一次就够把私钥泄进日志。工作流里那一步显式 `set +x`
|
||||
且只回显**公钥**;本机调试同理,别开 trace。
|
||||
7. **`--generate-key` 的输出落进文件/issue/聊天**:当成泄露处理,走 `signing.md` §5 轮换(旧钥签新钥)。
|
||||
8. **以为「非 main 分支的读数」就是仓库状态**:播报 workflow **只在 `refs/heads/main` 上注入** Secret
|
||||
(也没有 `pull_request` 触发 —— 见 §2 的顺序硬要求与 §6)。非 main ref 的运行里 ② 显示
|
||||
「**未判定** —— 非 main ref(按设计不注入 Secret)」,这是**策略**,不是「没配」。以 `main` 上的读数为准。
|
||||
9. **`.sig` 没上传到 Release**(手改过 `assets=(…)` 那一行):客户端取 `<产物根>/manifest.json.sig`
|
||||
会 404 ⇒ 所有人验签失败。上传后请在资产列表里**亲眼看到** `manifest.json` 与 `manifest.json.sig`
|
||||
两个名字(§5 的 `gh release view` 那条命令就是干这个的)。
|
||||
10. **删掉旧钥条目太早**:用户机器上装着旧二进制,它只认旧钥;删条目的那个版本必须**只**由新钥签,
|
||||
并且至少一个发布周期的双签名之后(`signing.md` §5)。
|
||||
11. **「昨天验过」不等于「今天验得过」**:`PRODUCTION` 是**编译期**常量,改 `trust.rs` 只影响**下一次构建**。
|
||||
已在用户盘上的旧二进制永远用旧表 —— 这是设计,不是 bug。
|
||||
12. **给播报 workflow 加回 `pull_request` 触发、或去掉注入行的 ref 门**:等于把生产签名种子交给任意推
|
||||
分支的人的代码(自更新链 = 用户端 RCE)。这两处各有接线用例钉着(去掉任一处都会红),
|
||||
别为了「PR 上也能看读数」去动它 —— 看读数请对 `main` 跑,或读 §1 在本地跑。
|
||||
|
||||
## 5. 发布后回验(发布侧,不需要私钥)
|
||||
|
||||
```bash
|
||||
# 资产里必须两个名字都在(漏一个等于全员验签失败)
|
||||
gh release view <tag> --json assets --jq '.assets[].name' | grep -E 'manifest\.json(\.sig)?$'
|
||||
|
||||
# 拿下载回来的真实资产复现客户端判据:manifest.json 与 .sig 必须成对、且是同一份字节
|
||||
mkdir -p /tmp/verify && gh release download <tag> --pattern 'manifest.json*' --dir /tmp/verify
|
||||
python3 packaging/sign_manifest.py --verify-pinned /tmp/verify/manifest.json
|
||||
```
|
||||
|
||||
退出码 0 = 「客户端会接受这份清单」。非零时**不要**手动 `edit` Release 绕过 —— 先看脚本 stderr
|
||||
(它会说清楚是表空、无 `.sig`、还是签名对不上)。
|
||||
|
||||
## 6. CI 里那条读数:`Signing status(播报)`
|
||||
|
||||
`.github/workflows/signing-status.yml` 在 `push: branches: [main]` 与**手动 dispatch** 上跑,把 §1 的三行
|
||||
搬进 job summary。**它不是门禁**:
|
||||
|
||||
* 它**不应被设为 required check**(分支保护里不要勾它),也不阻断任何其它 job;
|
||||
* 它就绪度那一步**恒 `exit 0`**:未就绪用 `::warning::` 播报,不用 error 级注解(那会让人以为构建挂了);
|
||||
* **生产签名种子只在 `refs/heads/main` 上注入**(`github.ref == 'refs/heads/main' && secrets.… || ''`),
|
||||
而且**没有 `pull_request` 触发**。理由是安全:仓库内分支的 PR / 任意分支的 dispatch 都拿得到仓库 Secret,
|
||||
而 CI 跑的是那个 ref 自己的代码 —— 种子进过那种运行就等于交给任何能推分支的人。
|
||||
非 main ref 的运行里 ② 显示「**未判定** —— 非 main ref(按设计不注入)」,**不写「没配」**;
|
||||
* 它**刻意不声明 `workflow_call:`**,所以别的 workflow 没法 `uses:` 它;接线自检还会扫全部 workflow,
|
||||
任何 `uses: …/signing-status.yml` 出现即红(防的是「顺手接进 dev 线把播报变成门禁」);
|
||||
* 它**绿**只说明脚本跑完了;结论只看 summary 里的三行字。
|
||||
* 同一 workflow 里另有两步跑自测(`packaging/tests/signing_ready_selftest.py` +
|
||||
`packaging/tests/signing_ready_wiring_selftest.py`):那两步**会红**,红 = **尺子坏了**(三行读数/退出码/
|
||||
变异重放/接线与 ref 门之一没过),与「签名未就绪」是两件不同的事,措辞在 summary 里分开写。
|
||||
|
||||
为什么不做成门禁:今天已经有四次「守卫假绿」的账(见 `packaging/README.md` §12 里那几条先例)。
|
||||
把一条**按定义今天必然失败**的检查做成 required,只会有两种结局 —— 要么所有人学会忽略红叉,
|
||||
要么有人把它改成永远绿。真正的红留在 `release-publish.yml` 的签名步:那里今天红是**对的**,
|
||||
因为签不出来的产物本来就不能发。
|
||||
|
||||
## 7. 本手册**没**覆盖 / 没验的部分(明账)
|
||||
|
||||
* **Authenticode / SmartScreen**:项目自有签名解决的是「这条自更新链是不是我们发的」,用户双击 exe 看到
|
||||
的「未知发布者」与此无关(`signing.md` §0)。
|
||||
* **托管签名**(如 SignPath):只是后续可替换路径,本手册与判据都不依赖它。
|
||||
* **本文档里的命令没有被真跑过一遍**:`--generate-key` 会产出生产私钥,塔不生成、也不跑它;
|
||||
②③ 两步要动 GitHub Secret 与 Rust 常量,同理。所以本手册是「按现有脚本的接口写的步骤」,
|
||||
首次执行时**以脚本的实际输出为准**,跑完请回来修正本文档。
|
||||
* **「用户机器上真的更新成功」**没有在本条里验:需要一次真 Release + 一台装着旧版本的机器。
|
||||
* **ref 门只在本地验证过**(抽 `run:` 块用 Git Bash 跑 + 接线用例的文本变异),没有在真 Actions 上看过
|
||||
`secrets` 的求值结果 —— 第一次真跑请确认非 main ref 的 ② 显示「未判定」。
|
||||
|
||||
## 8. 相关文件
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `packaging/check_signing_ready.py` | 三行就绪度 + `0/1/2` 退出码(只读、零第三方依赖) |
|
||||
| `packaging/tests/signing_ready_selftest.py` | 判据自测:三行/退出码/两向变异重放/`--key` 用法错/指针章节 |
|
||||
| `packaging/tests/signing_ready_wiring_selftest.py` | 接线自测:不是门禁 / 不被 `uses:` / **ref 门** / 文档路径存在性 + 三处变异 |
|
||||
| `.github/workflows/signing-status.yml` | 把三行搬进 job summary 的**播报** workflow(不是门禁;种子只在 main 注入) |
|
||||
| `packaging/docs/signing.md` | 为什么签名、威胁模型、轮换纪律、四条发布侧命令的红法对照表 |
|
||||
| `packaging/sign_manifest.py` | 生成钥 / 签名 / 对账 / 回验的唯一实现(`--self-test` 跑 RFC 8032 向量) |
|
||||
@@ -0,0 +1,160 @@
|
||||
# 产物签名与更新验签(M190)
|
||||
|
||||
本文件是**发布侧手册**:私钥从哪来、怎么用、怎么轮换、哪一步失败必须让发布变红。
|
||||
用户面的说明(README 里「为什么第一个包要手动下」那类话)不在本文件范围。
|
||||
|
||||
> **M255 补充:要照做的那几步在 [`signing-runbook.md`](signing-runbook.md)** —— 生成钥 → 私钥进 GitHub
|
||||
> Secret(名字逐字是 `ULL_MANIFEST_SIGNING_KEY`)→ `PinnedKey` 贴进 `trust.rs` 标记之间 → 发一次带 `.sig`
|
||||
> 的 Release → 用 `--verify-pinned` 回验 → 已知坑。本文件讲**为什么**,那份讲**怎么做**。
|
||||
> 现状读数用 `python3 packaging/check_signing_ready.py`(三行 + `0/1/2`;未就绪时会打印一行修复命令);
|
||||
> CI 里同一条读数挂在 `.github/workflows/signing-status.yml` 的 job summary 上 —— **那是播报,不是门禁**。
|
||||
|
||||
## 0. 先说清这件事**不**解决什么
|
||||
|
||||
| 有 / 没有 | 状态 |
|
||||
|---|---|
|
||||
| 更新器验「清单是不是我们发的」 | **有**(Ed25519 清单签名 + 二进制里钉住的公钥) |
|
||||
| Windows 上「未知发布者」提示消失 | **没有**。项目自有签名与 Authenticode 是两件事 |
|
||||
| SmartScreen 信誉 | **没有**。只有买了 OV/EV 证书并真签名才可能逐步积累 |
|
||||
|
||||
> 一句话:**项目自有签名保护的是自更新这条链,不是 Windows 的信任判断。** 用户双击 exe 时看到的
|
||||
> 「未知发布者」来自 SmartScreen,与本文件的方案无关;买证书是另一条线(见 M190 报告里的
|
||||
> 「后续要用户拍板的两件事」)。
|
||||
|
||||
## 1. 威胁模型(为什么要签名)
|
||||
|
||||
清单里的 `sha256` 只保证「包体与清单对得上」。**能改 Release 的人可以同时改包和摘要**,所以摘要
|
||||
本身不是信任根。签名的对象因此是**清单文件的原始字节**:改了任意一个字段(哪怕只改版本号里一个数字),
|
||||
签名立刻对不上。
|
||||
|
||||
```
|
||||
发布侧 用户机器
|
||||
------ --------
|
||||
manifest.json(字节)──签名──> manifest.json.sig
|
||||
│ │
|
||||
└── 挂到同一个 Release ─────────────┤
|
||||
├─ 取清单 + .sig
|
||||
├─ 用**编译进二进制**的公钥验签(trust.rs)
|
||||
├─ 过了 → 再解析、再比 sha256、再装
|
||||
└─ 不过 → 五类签名错误,**什么都不会被安装**
|
||||
```
|
||||
|
||||
## 2. 信任根在哪:`crates/core/src/update/trust.rs`
|
||||
|
||||
* 公钥是**编译期常量数组**(`PRODUCTION`),不是用户机器上的文件 —— 没有环境变量、没有设置键、
|
||||
没有「清单旁边的 keyring.json」这类运行期口子(`tests/signature_cases.rs::no_runtime_keyring_override`
|
||||
守着这条)。
|
||||
* 是**数组**而不是一把:换签名方时新旧钥可以并存(见 §5)。
|
||||
* `key_id` 只是查表标签,**判据永远是公钥字节**:把 `key_id` 伪造成钉住的那个也过不了
|
||||
(`key_id_spoofing_does_not_help`)。
|
||||
|
||||
### 空表 = fail-closed
|
||||
|
||||
正式表当前**是空的**:私钥只能来自仓库 Secret,任何代码仓库都不该自己生成并落盘私钥。
|
||||
空表的效果是「任何正式构建都拒装任何清单」,同时 `--check-pinned-keyring` 让发布流水线当场红。
|
||||
**这是有意的**:宁可不出 Release,也不出未签名的 Release。
|
||||
|
||||
## 3. 一次性录入(只做一次)
|
||||
|
||||
```bash
|
||||
# ① 生成:打印一段 Secret JSON + 一行可以直接贴进 trust.rs 的 PinnedKey
|
||||
python3 packaging/sign_manifest.py --generate-key --key-id ull-release-2026a
|
||||
```
|
||||
|
||||
* 打印出来的 **第一行 JSON**(含 `seed_hex`)= 私钥,**只**贴进仓库 Secret:
|
||||
Settings → Secrets and variables → Actions → New repository secret,名字
|
||||
**`ULL_MANIFEST_SIGNING_KEY`**。
|
||||
* 打印出来的 **`PinnedKey { … }` 那一行**贴进 `crates/core/src/update/trust.rs` 的
|
||||
`>>> PINNED-KEYS >>>` 与 `<<< PINNED-KEYS <<<` 标记之间(标记本身不要动,脚本按它解析)。
|
||||
* 提交这次改动(公钥进 git 是对的:它是信任锚,本来就该公开可审计)。
|
||||
|
||||
### 私钥纪律(违反任何一条都算事故)
|
||||
|
||||
1. **只**存在于仓库 Secret `ULL_MANIFEST_SIGNING_KEY`。本地需要签测试产物时,走 `--key <文件>`
|
||||
并把那个文件放在**仓库外**(gitignore 不保险,别放仓库里)。
|
||||
2. **不得**出现在日志、产物、提交、issue、聊天记录里。CI 里任何 `echo`/`cat`/`set -x` 都不许碰到它
|
||||
(工作流里的签名步骤显式 `set +x` 且只回显**公钥**)。
|
||||
3. 一旦怀疑泄露:立即按 §5 轮换,并在下一个 Release 的说明里点名旧 `key_id` 作废。
|
||||
4. 私钥文件/Secret 的备份不归仓库管;**丢了 = 旧钥作废**,用户机器上的旧二进制只能靠
|
||||
「先手动装一次新版本」恢复(`trust.rs` 里的旧条目那时才可以删)。
|
||||
|
||||
## 4. 发布时发生什么(`.github/workflows/release-publish.yml`)
|
||||
|
||||
在 `manifest.json` 生成之后、`gh release create/upload` 之前,多了一步:
|
||||
|
||||
```bash
|
||||
python3 packaging/sign_manifest.py --self-test # ① RFC 8032 向量自检
|
||||
python3 packaging/sign_manifest.py --check-pinned-keyring # ② 密钥 ↔ trust.rs 必须对得上
|
||||
python3 packaging/sign_manifest.py --sign dist/manifest.json # ③ 产出 dist/manifest.json.sig
|
||||
python3 packaging/sign_manifest.py --verify-pinned dist/manifest.json # ④ 用钉住的公钥回验产出
|
||||
```
|
||||
|
||||
第 ④ 步复现「客户端会怎么判」:它**不碰私钥**,只拿 `trust.rs` 里那把公钥去验刚落盘的 `.sig`。
|
||||
因此「签名脚本自己说签好了」不构成证据,证据是「按客户端的判据能验过」。
|
||||
|
||||
四条命令任何一条非零退出 ⇒ **这一步红 ⇒ 整个 Release job 红 ⇒ 本次不出 Release**。常见红法:
|
||||
|
||||
| 现象 | 原因 | 怎么补 |
|
||||
|---|---|---|
|
||||
| `没有签名密钥` | Secret `ULL_MANIFEST_SIGNING_KEY` 没配 / 名字拼错 | 按 §3 录入 |
|
||||
| `信任表是空的` | `trust.rs` 里还没钉任何公钥 | 把 §3 打印的 `PinnedKey` 那行贴进标记之间 |
|
||||
| `签名钥(…)不在信任表里` | Secret 与 `trust.rs` 是**两把不同的钥** | 用同一把钥重跑 §3,或回滚 `trust.rs` 的改动 |
|
||||
| `签出来的签名自己验不过` | 实现/环境异常 | 不要发布;把 `--self-test` 输出贴进 issue |
|
||||
|
||||
`dist/manifest.json.sig` 与 `dist/manifest.json` 一起作为 Release 资产上传(`assets=(...)` 那一行)。
|
||||
|
||||
## 5. 轮换(换钥 / 旧钥泄露 / 接托管签名)
|
||||
|
||||
**纪律:旧钥签新钥。** 不允许在没有旧钥签名的情况下替换信任表 —— 那等于让「能改仓库的人」变成
|
||||
「能改信任根的人」。
|
||||
|
||||
1. 生成新钥(§3),得到 `key_id = ull-release-2027a`。
|
||||
2. `trust.rs` 里**保留**旧条目、**追加**新条目(数组顺序无关;`key_id` 不能重名)。
|
||||
3. 在一次发布里,让清单**同时带两把钥的签名**(两条 `signatures`,客户端「任意一条通过即放行」):
|
||||
用旧钥签一次、新钥签一次,把两个信封的 `signatures` 数组拼起来放进 `manifest.json.sig`。
|
||||
这一步让「装着旧二进制的用户」和「装着新二进制的用户」都过得去。
|
||||
4. 之后至少**一个完整发布周期**保持双签名,再观察下载/更新失败率。
|
||||
5. 确认新钥已被广泛接受后,用新钥单独签发下一次清单,并从 `trust.rs` 删掉旧条目
|
||||
(删条目的那个版本必须**只**由新钥签)。
|
||||
6. changelog 里点名:新增/作废的 `key_id` 与公钥指纹。
|
||||
|
||||
托管签名(例如 SignPath Foundation)作为**后续可替换路径**:它的产物签名由 CI 侧调用其服务完成,
|
||||
`trust.rs` 的数组形状不用变(多一条 `PinnedKey` 即可);清单签名这一层保持本项目自有钥不动。
|
||||
**不允许**为了接托管签名把判据改成「只认证书」。
|
||||
|
||||
## 6. 三条守门(都能单独变红)
|
||||
|
||||
| 守门 | 脚本 / 用例 | 变红意味着 |
|
||||
|---|---|---|
|
||||
| 版本资源真的在产物里 | `packaging/check_pe_version.py <exe> --expect-version <版本>` | 读回产物字节找不到 `VS_VERSIONINFO` / 字段值不对(构建日志绿也没用) |
|
||||
| 篡改清单 ⇒ 验签失败且不安装 | `cargo test -p unlucky-core --lib update::tests::signature_cases` | 改字段/只改签名/漏签名/陌生钥 任一仍能装 |
|
||||
| 签名失败阻断发布 | `release-publish.yml` 的签名步 + `--check-pinned-keyring` | 没钥/钥没钉/钥不匹配时仍发出 Release |
|
||||
|
||||
另外 CI 的构建槽位会跑 `check_pe_version.py`(`release-build.yml`):Windows 槽位上直接读
|
||||
`target/release/unlucky-launcher-iced.exe` 与 `ull.exe` 的字节,断言 `CompanyName` 等字符串在。
|
||||
|
||||
## 7. 实现分工(M190 r1 复审后)
|
||||
|
||||
| 哪一半 | 在哪 | 用什么 |
|
||||
|---|---|---|
|
||||
| **验签**(进二进制) | `crates/core/src/update/ed25519.rs` | `ed25519-dalek::verify_strict`(在非 strict 的基础上额外拒**小阶公钥**与**小阶 `R`**;`S` 规范性是非 strict 的 `verify` 也查的) |
|
||||
| **签名**(只在发布侧) | `packaging/ed25519_ref.py` + `packaging/sign_manifest.py` | 纯 Python 参考实现,零第三方依赖(CI 不装 pip 包) |
|
||||
|
||||
两套实现互相独立,`--self-test` 用 RFC 8032 §7.1 的同一组向量把两边各自钉住。**第一版曾把验签写成
|
||||
仓内自写**(5x51 素域 + 扩展坐标);r1 复审裁定不安全——信任锚的微妙错误后果是「验签通过但本不该通过」,
|
||||
因此整段删除,改为官方实现。`crates/core/src/update/tests/ed25519_cases.rs` 现在钉的是
|
||||
**dalek 的 strict 语义**:谁把 `verify_strict` 换成 `verify`,`small_order_public_key_is_rejected` 与
|
||||
`small_order_r_is_rejected_only_by_strict` 会红(这是 r2 实测的结论——其余几条拒绝类用例在非 strict
|
||||
下仍绿,别把它们当 strict 的守卫)。
|
||||
|
||||
## 8. 本地自测(不需要私钥)
|
||||
|
||||
```bash
|
||||
python3 packaging/sign_manifest.py --self-test # RFC 8032 向量 + 标记解析
|
||||
python3 packaging/check_pe_version.py --self-test # 口径表与 pe_version.rs 对拍
|
||||
cargo test -p unlucky-core --lib update::tests::signature_cases
|
||||
cargo test -p unlucky-core --lib update::tests::ed25519_cases
|
||||
```
|
||||
|
||||
带 `update-test-source` 的验收构建里另有一把**公开的测试钥**(种子写在测试夹具里):它只用于
|
||||
「本地目录源 + 真二进制」的端到端验收,**不在**正式信任表里,签不出任何正式版本能装的清单。
|
||||
@@ -0,0 +1,279 @@
|
||||
# Termux + termux-x11 上的 glibc 构建(试验级)
|
||||
|
||||
本页记录**在 Termux 里构建 glibc 版 GUI,并在 termux-x11 里跑起来**的可复现步骤。
|
||||
2026-11-06 在一台 OnePlus 15(Android 17、Termux、KirinSU/root 环境)上实测通过:
|
||||
`--software-renderer --screenshot` 出 1180×760 PNG,中文正常,`msa client_id=configured source=build`。
|
||||
|
||||
**定位**:试验级、不承诺。发布矩阵里没有面向 Termux 的资产;本文只描述源码自建路径。
|
||||
GUI 只在上述设备 + CPU 渲染(tiny-skia)上验证过,GPU 路径见 §5(尚未在 glibc 侧验证)。
|
||||
|
||||
## 1. 为什么不能直接 `cargo build`
|
||||
|
||||
Termux 的 `rustc` host 是 `aarch64-linux-android`(`target_os = "android"`):
|
||||
|
||||
- `crates/app-iced/Cargo.toml` 的 `x11` / `wayland` feature 只挂在 `cfg(target_os = "linux")` 下;
|
||||
- iced 的窗口层 winit 0.30 在 android target 上走 Android(NativeActivity)后端,
|
||||
会拉 `android-activity` 并要求 `game-activity`/`native-activity` feature,普通进程编不过也跑不了。
|
||||
|
||||
所以要在 Termux 里产出一个 **`aarch64-unknown-linux-gnu`(glibc)** 二进制,靠 Termux 的
|
||||
[glibc 运行时](https://packages.termux.dev/apt/termux-glibc/) 原生执行。
|
||||
|
||||
## 2. 工具链(一次性)
|
||||
|
||||
以下路径用变量表示,实际可放在任意本地目录(示例放在仓库内的 `.gnu/`,已 gitignore):
|
||||
|
||||
```sh
|
||||
PREFIX="${PREFIX:-/data/data/com.termux/files/usr}"
|
||||
G="$PWD/.gnu"
|
||||
```
|
||||
|
||||
### 2.1 glibc 运行时
|
||||
|
||||
确认 `$PREFIX/glibc/lib/ld-linux-aarch64.so.1` 存在(`pkg install glibc-runner` 或装过 glibc 仓库
|
||||
的包即可)。**不需要 `gcc-glibc`**:链接用 Termux 自带的 clang + lld,glibc 的 CRT/头文件由
|
||||
`glibc` 包提供。
|
||||
|
||||
### 2.2 Rust gnu 工具链
|
||||
|
||||
下载 Rust 1.99.1(或与 `rust-version` 兼容的 stable)的 `rustc` / `rust-std` / `cargo`,
|
||||
以及跑门禁需要的 `clippy` / `rustfmt` 五个 `aarch64-unknown-linux-gnu` 组件。官方源在国内很慢,
|
||||
用 USTC 镜像:
|
||||
|
||||
```sh
|
||||
mkdir -p "$TMPDIR/ull-gnu" && cd "$TMPDIR/ull-gnu"
|
||||
for c in rustc rust-std cargo clippy rustfmt; do
|
||||
curl -fLO "https://mirrors.ustc.edu.cn/rust-static/dist/$c-1.99.1-aarch64-unknown-linux-gnu.tar.xz"
|
||||
done
|
||||
```
|
||||
|
||||
解包后按组件布局合并到 `$G/toolchain`(`rustc/bin`、`cargo/bin` → `toolchain/bin`,
|
||||
两者的 `lib/` → `toolchain/lib`,`rust-std-*/lib/rustlib/aarch64-unknown-linux-gnu` →
|
||||
`toolchain/lib/rustlib/`)。
|
||||
|
||||
### 2.3 改 interpreter / rpath
|
||||
|
||||
工具链里的 `rustc`、`cargo`、`rustdoc` 是 glibc ELF,默认 interpreter 是 `/lib/ld-linux-*.so.1`,
|
||||
在 Termux 上不存在:
|
||||
|
||||
```sh
|
||||
LD="$PREFIX/glibc/lib/ld-linux-aarch64.so.1"
|
||||
for f in rustc cargo rustdoc clippy-driver cargo-clippy rustfmt cargo-fmt; do
|
||||
patchelf --set-interpreter "$LD" --set-rpath "$PREFIX/glibc/lib:$G/toolchain/lib" \
|
||||
"$G/toolchain/bin/$f"
|
||||
done
|
||||
```
|
||||
|
||||
> **坑(实测)**:`cargo clippy` 会在 PATH 里找 `cargo-clippy`。如果只装了 Termux 的 Rust,
|
||||
> 找到的是 android 版 `cargo-clippy`,`aws-lc-sys` 等构建脚本会被当成
|
||||
> `--target=aarch64-linux-android` 编译并失败。把 gnu 工具链的 `bin` 放在 PATH 最前面即可;
|
||||
> `rustc`/`rustfmt` 同理。
|
||||
|
||||
### 2.4 gcc 运行时片段
|
||||
|
||||
`clang -fuse-ld=lld` 仍然需要 `crtbeginS.o` / `crtendS.o` / `libgcc.a`。从 Debian 的
|
||||
`libgcc-14-dev`(arm64)取即可(只解包,不安装):
|
||||
|
||||
```sh
|
||||
curl -fLO https://deb.debian.org/debian/pool/main/g/gcc-14/libgcc-14-dev_14.2.0-19_arm64.deb
|
||||
dpkg-deb -x libgcc-14-dev_14.2.0-19_arm64.deb "$G/gnu-support"
|
||||
# crt/libgcc 在 $G/gnu-support/usr/lib/gcc/aarch64-linux-gnu/14/
|
||||
```
|
||||
|
||||
### 2.5 linker 驱动
|
||||
|
||||
写一个 `$G/bin/cc-glibc`(`chmod +x`):
|
||||
|
||||
```sh
|
||||
#!/data/data/com.termux/files/usr/bin/sh
|
||||
unset LD_PRELOAD
|
||||
P=$PREFIX
|
||||
S="<仓库>/.gnu/gnu-support/usr/lib/gcc/aarch64-linux-gnu/14"
|
||||
C="clang --target=aarch64-unknown-linux-gnu --sysroot=/ -isystem $P/glibc/include \
|
||||
-B$P/glibc/lib -B$S -L$S -L$P/glibc/lib -fuse-ld=lld"
|
||||
for a in "$@"; do
|
||||
case "$a" in -c|-E|-S) exec $C "$@" ;; esac
|
||||
done
|
||||
exec $C -Wl,--dynamic-linker=$P/glibc/lib/ld-linux-aarch64.so.1 -Wl,-rpath,$P/glibc/lib "$@"
|
||||
```
|
||||
|
||||
要点:`--sysroot=/` 而不是 `--sysroot=$PREFIX/glibc`——glibc 的 `libc.so` 是含绝对路径的
|
||||
linker script,带 sysroot 会被二次拼接;`-B $P/glibc/lib` 让 clang 找到 `Scrt1.o`/`crti.o`/`crtn.o`。
|
||||
|
||||
## 3. 构建
|
||||
|
||||
```sh
|
||||
unset LD_PRELOAD # 必须:Termux 的 libtermux-exec 会被 glibc loader 当成坏 ELF
|
||||
G="$PWD/.gnu"
|
||||
export PATH="$G/toolchain/bin:$PATH"
|
||||
export CARGO_TARGET_DIR="$G/target"
|
||||
export RUSTFLAGS="-C linker=$G/bin/cc-glibc"
|
||||
export CC="$G/bin/cc-glibc" CXX="$G/bin/cc-glibc" AR="$PREFIX/glibc/bin/ar"
|
||||
export CARGO_HTTP_CAINFO="$PREFIX/etc/tls/cert.pem" # glibc 版 cargo 不认 Android 的 CA 路径
|
||||
export ULL_MSA_CLIENT_ID=<id> # 可选;烘微软登录 id,值不进 git
|
||||
|
||||
# crates 走 USTC 稀疏索引(官方源在部分网络下只有 ~20 KB/s)
|
||||
cargo --config 'source.crates-io.replace-with="ustc"' \
|
||||
--config 'source.ustc.registry="sparse+https://mirrors.ustc.edu.cn/crates.io-index/"' \
|
||||
build --release -p unlucky-app-iced --no-default-features
|
||||
```
|
||||
|
||||
`--no-default-features` 关掉 `gpu`,只编 tiny-skia(CPU)。产物:`.gnu/target/release/unlucky-launcher-iced`。
|
||||
|
||||
门禁(与 CI 同一组命令,环境同上):
|
||||
|
||||
```sh
|
||||
cargo fmt --all --check
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo test --workspace --all-targets --no-fail-fast
|
||||
```
|
||||
|
||||
Termux 上有三条**环境差异**失败(不是回归):`launch` 测试读
|
||||
`/proc/<pid>/task/<pid>/children`(Android 内核没有)、natives 事件的长路径被脱敏成
|
||||
`<opaque>`、`settings_bridge` 的 mtime 热重载偶发(flaky)。PC 的 Linux CI 不受影响。
|
||||
|
||||
## 4. 运行(termux-x11)
|
||||
|
||||
```sh
|
||||
# 一次性:glibc 版 X 运行库(cf 源慢时可把 sources.list 里的 packages-cf 换成 packages.termux.dev)
|
||||
pkg install libx11-glibc libxkbcommon-glibc fontconfig-glibc \
|
||||
libxcursor-glibc libxrandr-glibc libxi-glibc libxext-glibc \
|
||||
libxcb-glibc libwayland-glibc
|
||||
|
||||
termux-x11 :0 -ac & # 配合已安装的 termux-x11 App 使用
|
||||
DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced --software-renderer
|
||||
```
|
||||
|
||||
无头取证(CI 同款通道):
|
||||
|
||||
```sh
|
||||
DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \
|
||||
--software-renderer --screenshot /tmp/ull-termux.png --data-dir /tmp/ull-termux-data
|
||||
```
|
||||
|
||||
- 中文:GUI 在 Termux 上会把 `/system/fonts/NotoSansCJK-Regular.ttc` 注册进 cosmic-text
|
||||
(fontdb 不扫 Android 的字体目录),无需额外环境变量。
|
||||
- 启动日志里 `[auth] msa client_id=configured source=build` 表示构建期 id 已烘入。
|
||||
- **横屏优先**:Termux 上默认窗口尺寸会按当前 X 屏幕等比自适应(日志
|
||||
`[window] ... source=termux-screen`),横屏铺满、竖屏也不溢出;验收尺寸 1180×760 仍是
|
||||
默认基准,`ULL_WINDOW_SIZE=1520x900` 这类覆盖仍然优先。
|
||||
|
||||
### 4.1 Termux 上的 Java
|
||||
|
||||
`ull java install <major>` 在 Termux 上**默认先用系统包**(`openjdk-17/21/25`):
|
||||
|
||||
- 内置 SAU / TUNA / USTC / 官方四个 termux-main 镜像,按顺序尝试;
|
||||
- `apt-get` 的 lists/cache 隔离在 `$PREFIX/tmp/ull-java-termux/`,不改用户的 apt 配置;
|
||||
装完自动清理该目录(一次安装约 100 MB 临时占用);
|
||||
- 装完从 `$PREFIX/lib/jvm/java-<major>-openjdk` 发现,`ull java list` 直接可见;
|
||||
- `--vendor termux` 只走系统包;`temurin|oracle|microsoft|zulu` 仍走归档下载链。
|
||||
- 已实测:`ull java install 17` 从 SAU 镜像装完 `openjdk-17`(含 `openjdk-17-x`),
|
||||
约 20 秒;`ull java list` 随后列出 `[system]` 的 17。
|
||||
|
||||
### 4.2 跑 MC 需要的运行时(已手动验证)
|
||||
|
||||
bionic 的 Termux JVM 加载不了 MC 的 glibc natives,所以 MC 要用 **glibc JVM**:
|
||||
|
||||
```sh
|
||||
# Temurin 21 JRE(linux-aarch64),用 glibc loader 直接跑
|
||||
curl -L -o jre21.tar.gz \
|
||||
https://api.adoptium.net/v3/binary/latest/21/ga/linux/aarch64/jre/hotspot/normal/eclipse
|
||||
tar -xzf jre21.tar.gz -C "$G/jre"
|
||||
JRE="$G/jre/jdk-21.0.13+9-jre"
|
||||
$PREFIX/glibc/lib/ld-linux-aarch64.so.1 \
|
||||
--library-path "$JRE/lib:$JRE/lib/server:$PREFIX/glibc/lib" "$JRE/bin/java" -version
|
||||
```
|
||||
|
||||
- AWT/X11:`DISPLAY=:0` 下 Swing 窗口正常(`headless=false`、`swing-ok`)。
|
||||
- GL:需要 §5 的 Mesa 26 GL 构建(`-Dgallium-drivers=zink,softpipe -Dglx=dri`)。
|
||||
`zink` 目前卡在 `DRI3 not available`;**softpipe 可用**:
|
||||
`MESA_LOADER_DRIVER_OVERRIDE=swrast LIBGL_ALWAYS_SOFTWARE=1`。
|
||||
- 端到端探针:LWJGL 3.3.3 arm64 natives + GLFW + GLX → `GL_VERSION=3.3 (Compatibility
|
||||
Profile) Mesa 26.2.6`、`GL_RENDERER=softpipe`、`gl-ok`。
|
||||
|
||||
> **启动器已接线**(本分支):
|
||||
> - Linux/aarch64 上 LWJGL3 的 natives 自动改写成 `natives-linux-arm64`
|
||||
> (Mojang 的 `libraries.minecraft.net` 对 arm64 一律 404,下载兜底到阿里云 Maven 镜像);
|
||||
> - Termux glibc 构建启动 MC 时,子进程默认带 `MESA_LOADER_DRIVER_OVERRIDE=swrast`
|
||||
> 与 `LIBGL_ALWAYS_SOFTWARE=1`(用户自定义环境变量仍可覆盖)。
|
||||
>
|
||||
> **还差一步**:把实例的 Java 指到 glibc JRE。Temurin 解包后 `bin/java` 默认的
|
||||
> interpreter 在 Android 上不存在,先 patchelf 一次即可直接执行:
|
||||
>
|
||||
> ```sh
|
||||
> patchelf --set-interpreter "$PREFIX/glibc/lib/ld-linux-aarch64.so.1" \
|
||||
> --set-rpath "$PREFIX/glibc/lib:$JRE/lib:$JRE/lib/server" "$JRE/bin/java"
|
||||
> "$JRE/bin/java" -version
|
||||
> ```
|
||||
>
|
||||
> 之后在实例设置里把 Java 指向 `$JRE/bin/java`(或 `--java`)即可。
|
||||
>
|
||||
> **真机验证(1.20.4)**:`ull install 1.20.4 --name mc-test`(740 MB)成功后,
|
||||
> `ull launch mc-test --java "$JRE/bin/java" --account Tester`,
|
||||
> MC 主菜单在 termux-x11 中渲染(窗口 854×480;日志在实例目录
|
||||
> `versions/1.20.4/logs/latest.log`)。非致命项:narrator 的 flite 缺失、
|
||||
> LWJGL JNI 版本告警、oshi 的 cpuVendor NPE(都未阻断启动)。
|
||||
|
||||
## 5. GPU(已打通:自编 Mesa 26.2.6 + KGSL)
|
||||
|
||||
termux-x11 的 X 客户端可以走硬件;glibc 侧的问题在**驱动**,不在 loader:
|
||||
|
||||
1. `vulkan-icd-loader-glibc` + ICD JSON 本身正常。
|
||||
2. Termux 软件源的 glibc Mesa 24.2.6 不认识 Adreno 840
|
||||
(`chip_id = 44050A31 is unsupported`)。
|
||||
3. Arch 的 `vulkan-freedreno 26.2.6` 能加载,但它是 Linux/DRM 构建,缺 KGSL
|
||||
(`/dev/dri` 对 `untrusted_app` 被 SELinux 拒绝)。
|
||||
|
||||
**解法:给 glibc 自编一份带 KGSL 的 Mesa 26.2.6。** 本机实测通过:
|
||||
|
||||
```sh
|
||||
# 1) 源码 + 依赖
|
||||
# mesa-26.2.6.tar.xz(archive.mesa3d.org)
|
||||
# Debian libstdc++-14-dev 的头($G/gnu-support/usr/include/c++/14)
|
||||
# meson / ninja(pip 装 meson)
|
||||
# 2) 把源码里 *.py 的 shebang 从 #!/usr/bin/env python3 改成 Termux python3
|
||||
# (Android 没有 /usr/bin/env)
|
||||
# 3) meson setup(cc-glibc 包装器:-c 不带链接参数、C++ 加 -stdlib=libstdc++、
|
||||
# 共享库链接加 -lstdc++;详见 §3)
|
||||
meson setup build3 --native-file="$G/mesa-native.ini" --wrap-mode=nofallback \
|
||||
-Dbuildtype=release -Dstrip=true -Dplatforms=x11 \
|
||||
-Dvulkan-drivers=freedreno -Dfreedreno-kmds=kgsl \
|
||||
-Dgallium-drivers=zink,softpipe -Dglx=dri -Dopengl=true -Degl=enabled \
|
||||
-Dgles1=disabled -Dgles2=enabled -Dllvm=disabled -Dgbm=disabled \
|
||||
-Dvideo-codecs= -Dzstd=enabled -Dbuild-tests=false
|
||||
ninja -C build3
|
||||
```
|
||||
|
||||
`--wrap-mode=nofallback` 是为了跳过 libarchive 子项目(它只被 freedreno 的 decode 工具需要,
|
||||
且它自带的 `id_t` 兜底宏会和 glibc 头冲突)。
|
||||
|
||||
安装与启用:
|
||||
|
||||
```sh
|
||||
cp build3/src/freedreno/vulkan/libvulkan_freedreno.so \
|
||||
"$PREFIX/glibc/lib/libvulkan_freedreno_ull.so"
|
||||
cat > "$PREFIX/glibc/share/vulkan/icd.d/ull_freedreno.json" <<EOF
|
||||
{"ICD":{"api_version":"1.4.335","library_path":"$PREFIX/glibc/lib/libvulkan_freedreno_ull.so"},"file_format_version":"1.0.0"}
|
||||
EOF
|
||||
```
|
||||
|
||||
应用启动时(glibc 构建 + Termux + 该 JSON 存在)会自动把它设进
|
||||
`VK_ICD_FILENAMES` / `VK_DRIVER_FILES`(`app/boot/termux_gpu.rs`);用户显式设了这两个
|
||||
变量时不覆盖。实测日志:
|
||||
|
||||
```text
|
||||
[probe] adapters=1 [Adreno (TM) 840 [IntegratedGpu]] hardware=true :: request=auto -> ICED_BACKEND=wgpu
|
||||
[gpu] Selected: AdapterInfo { name: "Adreno (TM) 840", driver: "turnip Mesa driver",
|
||||
driver_info: "Mesa 26.2.6", backend: Vulkan }
|
||||
[shot] 1657x1067 renderer=wgpu,首帧 61 ms
|
||||
```
|
||||
|
||||
没装驱动/没有 GPU 时仍会打印具名错误并自动降级 tiny-skia(同一进程不能重建事件循环,
|
||||
降级走 respawn),所以 GPU 变体在纯 CPU 环境也能跑。
|
||||
|
||||
## 6. 已知边界
|
||||
|
||||
- 没有面向 Termux 的发布资产,也不在 CI 矩阵里;每次都要源码自建。
|
||||
- 只在 CPU 渲染 + 单台设备(Adreno 840 / Android 17)上验证过。
|
||||
- **装好 JDK ≠ 能跑 MC**:MC 的 LWJGL natives 是 glibc `.so`,bionic 的 Termux JVM 加载不了;
|
||||
这条与「启动器 GUI 能显示」是两件事,未在本文覆盖。
|
||||
- `ULL_MSA_CLIENT_ID` 属于仓库 Secret,构建时从环境/Secret 注入,**不写进任何 git 文件**。
|
||||
Reference in new issue
Block a user