# 打包与发布工程(packaging/) 单一真相源:**单文件产物矩阵 + 发布契约**。`.github/workflows/release.yml` 按本文实现, `packaging/make_artifacts.py` 是本地与 CI **共用**的打包脚本(同一份代码 → 同一套目录布局)。 本目录只放发布工程资产(脚本 / 说明 / 发布说明模板),不参与任何 crate 的编译。 ## 1. 产物矩阵 | 槽位 | 产物文件名 | 形态 | 归档内容 | |------|-----------|------|---------| | `windows-x64` | `unlucky-launcher--windows-x64.exe` | **裸 exe**(双击即用) | exe 单文件;三份合规文本作为并列 Release 资产 | | `windows-arm64` | `unlucky-launcher--windows-arm64.exe` | **裸 exe**(同 x64 策略) | 同上;在 x64 宿主上交叉编译到 `aarch64-pc-windows-msvc` | | `linux-x64` | `unlucky-launcher--linux-x64.tar.gz` | 单二进制归档 | `unlucky-launcher` + `LICENSE` + `NOTICE` + `README.md` | | `linux-arm64` | `unlucky-launcher--linux-arm64.tar.gz` | 单二进制归档(原生 ARM64 runner 构建) | 同上 | | `macos-arm64` | `unlucky-launcher--macos-arm64.tar.gz` | 单二进制归档 | 同上 | | `macos-x64` | `unlucky-launcher--macos-x64.tar.gz` | 单二进制归档 | 同上 | | 全平台 | `SHA256SUMS.txt` | 校验清单 | 每行 `␠␠<文件名>`(`sha256sum -c` 可直吃) | 约定细节: - **"单文件"指可执行文件本身零运行时依赖**(无 WebView、无 JRE、无 sidecar 资源目录、无 DLL 旁挂)。 归档里额外放三份合规文本是许可义务要求,不是运行时依赖;Windows 因为"一个 exe 装不下文本", 把 `LICENSE` / `NOTICE` / `README.md` 作为 Release 的并列资产分发,条款同样随下载走。 - 归档解压后是**一个顶层目录** `unlucky-launcher--<槽位>/`,用户不会解压出一堆散文件。 - `` 不带前导 `v`(tag 是 `v0.1.0`,文件名是 `0.1.0`)——由脚本统一裁剪。 - UNIX 归档内 `mtime`/`uid`/`gid` 归零、权限固定(二进制 0755、文本 0644), 同源构建的 sha256 不受打包时刻影响。 - **架构不靠猜**:脚本打包前回读二进制的可执行文件头(PE 机器码 / ELF `e_machine` / Mach-O `cputype`),与槽位声明的架构不符即失败——交叉槽位(x64 宿主产 arm64 exe)不会 出现"名字是 arm64、内容还是 x64"的假产物。 ## 2. 版本来源(唯一真相源) 版本号只有一个来源:**`Cargo.toml` 的 `[workspace.package] version`**。 - `` 由 `make_artifacts.py` 通过 `cargo metadata --no-deps` 读出(不是正则扒文件), 四个 workspace member 版本不一致时直接失败。 - 带 tag 构建时(CI)额外传 `--version "$TAG"`:脚本裁剪 `v` 后与 `Cargo.toml` 交叉校验, **不一致即失败**——避免出现 tag 说 0.2.0、产物里写 0.1.0 的发布事故。 - `ull --version` 必报同一版本(脚本打包前跑一次冒烟 `ull --version` 断言版本串在其中), 这是 `env!("CARGO_PKG_VERSION")` 的实际输出,不是估算。 - 发布流程:先改 `Cargo.toml` 版本 + 提交 → `git tag v` → 推送 tag 触发流水线。 ## 3. 怎么构建 ### 本地(当前平台) ```bash cargo build --release --locked # 产出 target/release/{unlucky-launcher,ull} python packaging/make_artifacts.py --out dist # 产物落在 dist/,含 SHA256SUMS.txt python packaging/make_artifacts.py --version v0.1.0 # 顺带校验版本一致性 python packaging/make_artifacts.py --no-smoke # 跳过 ull --version 冒烟 ``` 脚本按宿主自动判定槽位(Windows x64 / Windows arm64 / Linux x64 / Linux arm64 / macOS arm64 / macOS x64),其它组合直接报错退出——不在矩阵里的平台不出"凑合能跑"的产物。 交叉构建(例如在 x64 宿主上产 Windows arm64 产物)靠两个参数把"产物根"和"槽位"显式喂进去, 脚本不去猜宿主与产物是不是同一架构: ```bash rustup target add aarch64-pc-windows-msvc cargo build --release --locked --target aarch64-pc-windows-msvc \ --bin unlucky-launcher --bin ull python packaging/make_artifacts.py --slot windows-arm64 \ --build-dir target/aarch64-pc-windows-msvc/release --out dist ``` `--slot` 与宿主槽位不一致时,`ull --version` 冒烟会**自动跳过并打印原因**(arm64 产物在 x64 宿主机上根本执行不了;拿"跳过"当通过是假证据,所以跳过永远带原因)——那条路径的验证只能落在 CI 真机上:`windows-arm64` 由 cross 构建产物 + M50 的 dumpbin 导入表门禁覆盖,`linux-arm64` 由 `ubuntu-22.04-arm` 原生 runner 上真跑冒烟覆盖。 ### CI(四平台六槽位矩阵) `.github/workflows/release.yml`: - 触发:`push` tag `v*`;`workflow_dispatch` 只构建不发布(验证矩阵仍编得过)。 `inputs.prerelease` **只在 tag ref 上 dispatch 才可达**——Actions 页面的 "Run workflow" 只能选分支, 要跑 tag 得 `gh workflow run release.yml --ref v0.1.0`。分支 ref 的 dispatch 与 push 一样被 `release` job 的 `if: github.ref_type == 'tag'` 挡在发布之外,所以「dispatch 只构建不发布」 与「dispatch 能关 pre-release」是两条互不冲突的规则。 - 矩阵(六槽位;`matrix.build_dir` 参数化产物根,脚本不再假设产物一定在 `target/release`): `windows-latest` x64(MSVC + `-C target-feature=+crt-static`)、`windows-latest` **交叉编译**到 `aarch64-pc-windows-msvc`(产物在 `target/aarch64-pc-windows-msvc/release`)、`ubuntu-22.04` (glibc 2.35 地板)、`ubuntu-22.04-arm`(原生 ARM64 runner,**与 x64 同 2.35 地板**)、`macos-latest`(arm64)、 `macos-15-intel`(Intel x64;`macos-13` 已下线,`macos-26-intel` 为更新替位)。 - 每个槽位 `cargo build --release --locked`(交叉槽位多两步:`rustup target add` + ARM64 库体检 ——查 VC 的 `lib/arm64/libcmt.lib`(`+crt-static` 靠它)与 SDK 的 `um/arm64/kernel32.lib`, 缺了直接给出"给 VS 加 `VC.Tools.ARM64` 或改用原生 `windows-11-arm`"的可执行提示)→ `make_artifacts.py --slot … --build-dir …` → 归档内容自检 → 上传 artifact。 - `release` job:下载全部 artifact → 重算一份全量 `SHA256SUMS.txt` → 用 `packaging/release-notes.md` 模板渲染支持矩阵 → `gh release create`(**pre-release 默认开**:首建时 `push` 触发按 pre-release,`workflow_dispatch` 可关)。 - **同一 tag 重跑幂等**:`gh release view` 命中已存在的 Release 时改走 `gh release upload --clobber` + `gh release edit`,覆盖资产与说明而不是再 `create`(`create` 遇已存在会失败,把流水线最后一步染红)。 - **pre-release 标记只按显式输入断言**:`gh release edit` 分支在默认重跑(`push` 触发的 Re-run, `inputs.prerelease` 为空)时**不传** `--prerelease`,所以重跑**不会**翻转标记;只有 tag ref 上的 `workflow_dispatch` 显式给了输入才断言——`prerelease=true` → `--prerelease`,`prerelease=false` → `--prerelease=false`。要改标记两条路等价且互不打架:① 走流水线 `gh workflow run release.yml --ref v0.1.0 -f prerelease=false`(会落到 edit 分支显式断言) ② 直接 `gh release edit --prerelease=false`(不经流水线;后续默认重跑不碰该标记, 不会被翻回去)。首建(`create`)仍保持「pre-release 默认开」的老契约,空输入按 `true` 处理。 Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两者都属"标准工具链"(D21), 但发布产物要**不依赖 VC++ 可再发行包**,`+crt-static` 把 CRT 静态链进 exe,Win10+ 的 UCRT 本身是 系统组件,于是 exe 真正单文件可拷走。GNU 交叉产物在 CI 上还要拉 MinGW 链接器,收益为零。 ## 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) 产物形态与门禁跟 x64 一模一样(裸 exe + 静态 CRT + dumpbin 导入表硬门禁),差别只在**怎么编**: CI 在 `windows-latest`(x64 宿主)上交叉编译到 `aarch64-pc-windows-msvc`,产物落在 `target/aarch64-pc-windows-msvc/release`(由 `matrix.build_dir` 注入,脚本不硬编码路径)。 GitHub 文档(2026)已把 `windows-11-arm` / `windows-11-vs2026-arm` 列为公开仓库**免费**的标准 arm64 runner,原生路线可行;本流水线仍选交叉编译,因为①双 Windows 槽位共用同一套已验证镜像与 工具链、②M50 的 dumpbin 定位链取的是 `Hostx64/x64/dumpbin.exe`(读 ARM64 PE 的导入表没问题), 原生 ARM 宿主上那条链得再补 `Hostarm64` 分支——不为一个槽位放宽刚立的门禁。 运行时依赖与 x64 同表(UCRT 是 Win11 ARM 的系统组件,`+crt-static` 静态链 CRT,不需要可再发行包)。 游戏侧如实说:**启动器原生运行**;Temurin 没有 windows-aarch64 产物,Microsoft OpenJDK 有该 发行版(core 的 `microsoft` 源按 `-` 拼名,命名已覆盖),但 Minecraft 本体没有 Windows ARM64 原生库(Mojang/LWJGL 只发 x64/x86),实际游玩仍走系统 x64 模拟层 + x64 JDK。 所以本槽位只承诺「启动器产物」支持,不替 Java/游戏管理做架构级承诺。 ### Linux x64 | 依赖 | 说明 | |------|------| | glibc ≥ 2.35 | 由 `ubuntu-22.04` 构建决定;更老的发行版请自行重建或等 musl 版 | | `libxkbcommon.so.0` | winit 键盘处理 | | `libfontconfig.so.1` + `libfreetype.so.6` | 文本渲染 | | `libGL.so.1` / `libEGL.so.1` | femtovg GL 渲染;缺失走软渲染 | | `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 是启动游戏时才由启动器自己管理的东西)。 ### Linux arm64(aarch64) 依赖与 x64 完全同一套(`libxkbcommon` / `fontconfig` / `libGL` / `libxcb` / `libwayland-client`), **glibc 地板也相同**:arm64 槽位跑在原生 `ubuntu-22.04-arm` runner 上,地板 = **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 / 老驱动 / 远程桌面) Slint 默认 `renderer-femtovg`(GL)。两条兜底路径,效果等价,任选其一: ```bash unlucky-launcher --software-renderer # 启动参数(本仓库解析后设 SLINT_BACKEND) SLINT_BACKEND=winit-software unlucky-launcher # 环境变量(Slint 原生开关) ``` - 参数解析在 `crates/app/src/main.rs` 的 `apply_renderer_flag()`,只认完全相等的 `--software-renderer`。 - 软渲染是 CPU 光栅化,无 GPU 也能出图;代价是动画与大窗口重绘更慢(Metro 纯色界面影响有限)。 - 验收/无交互桌面场景另有进程内截图通道:`--screenshot `(见 `crates/app/src/screenshot.rs`), 它同样能在软渲染下工作,是 CI 与远程机唯一可靠的"真出图"证据。 - 发布冒烟(本机实跑,见 §7)就是用 `--software-renderer --screenshot` 组合证明"无 GPU 也能起来"。 ## 6. Cargo 体积优化(release profile) `Cargo.toml`: ```toml [profile.release] opt-level = "z" # 体积优先(启动器不是算力密集;UI 走 Slint 渲染,热路径不在 Rust 侧) lto = "fat" # 全量 LTO:跨 crate 内联+死代码消除,比 thin 再小一截(实测见 §7) codegen-units = 1 # 单 CGU,配合 LTO 拿最大优化面 panic = "abort" # 去掉 unwind 表 strip = "symbols" # 剥符号表(GNU/MSVC 路径都生效) ``` 取舍:`lto = "fat"` + `codegen-units = 1` 让 release 链接明显变慢(本机全量 release 构建 10 分钟量级, 见 §7.1),换来的是体积与启动时间。变慢的根源在 `codegen-units = 1`:整个 crate 只切出一个 CGU, LTO 也就只有一个线程可跑(`fat` 本身只管"跨 crate 全量参与",不等于单线程)。 本地开发迭代走 `cargo build`(dev profile)不受影响,但 **`ci.yml` 每次 push/PR 都跑 `cargo build --workspace --release`**,会实打实吃这份 fat LTO 的链接时间——CI 慢的一部分是 release 档构建,不是测试本身。 `panic = "abort"` 意味着 panic 不带 backtrace 栈展开——UI 层已用 `Result` 承接错误, `unwrap` 只出现在测试里,这是可接受的取舍。 ## 7. 实测(本机,不是估算) 构建环境:rustc 1.99.1 stable(host `x86_64-pc-windows-gnu`,MinGW 链接器), `cargo build --release --locked --bin unlucky-launcher --bin ull`,12 核机器(同一时刻另有别的 worker 在做全量 debug 构建,属于偏悲观的真实负载)。产物:`target/release/unlucky-launcher.exe`(GUI,交付物) 与 `target/release/ull.exe`(CLI)。 ### 7.1 体积(实测字节数,不是估算) | release profile | `unlucky-launcher.exe` | `ull.exe` | 冷构建耗时(本机 12 核) | |-----------------|-----------------------|-----------|-----------| | `lto = "thin"`(改前) | 16,366,080 B = 15.61 MiB | 7,572,992 B = 7.22 MiB | 11m52s | | `lto = "fat"`(改后,本仓库采用) | **14,595,584 B = 13.92 MiB** | **6,157,824 B = 5.87 MiB** | 9m27s(增量,依赖已编好) | `fat` 相对 `thin` 的实测差:GUI **−1,770,496 B(−10.8%)**,CLI **−1,415,168 B(−18.7%)**。 一次全量 LTO 把跨 crate 的死代码与重复实例化一起消掉,收益远超常见印象里的"1~2%"; 代价是 release 链接更慢——慢在 `codegen-units = 1`(单 CGU → LTO 单线程),不是 `fat` 本身, 且只影响发布/验收构建,日常 `cargo build` 走 dev profile 不受影响。 体积构成(按依赖树判断):Slint 运行时(femtovg + software 双渲染器 + 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 …` | exit 0,产出 1180×760 PNG(3,588,298 B);看图确认 Metro 实例页(侧栏/搜索/空态/AI 助手面板)正常渲染 → **软渲染路径真的能出图**,不依赖 GL | | `unlucky-launcher.exe --version` | **无输出**,8 秒后被 timeout 杀掉(exit 124)——GUI 目前不解析 `--version`,会直接开窗口。已作为 finding 上报,见 §10-1 | ### 7.3 打包验证(本机 Windows 槽位真跑) ``` 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 导入表) `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` 自带 VS 工具链,基本找得到;"找不到就跳过"等于让门禁在最需要它的时候失效)。 总之不允许"悄悄要用户装可再发行包"。 注意:本机只装了 GNU 工具链,MSVC 那个槽位的产物本身**没有在本机验证过**,由 CI 首跑验证。 ### 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` 的交叉链接与真实 ARM64 导入表、`linux-arm64` 在 原生 arm64 runner 上的真跑。本机连 `raw.githubusercontent.com` 都解析不了,所以 runner 镜像里到底装没装 ARM64 的 MSVC 库**没法在这里查证**——`release.yml` 为此加了一道链接前的体检(查 `libcmt.lib` 与 `um/arm64/kernel32.lib`,缺了直接给"加 `VC.Tools.ARM64` 组件或改原生 runner"的提示),首跑即见分晓。 merge 后由 tower 重指 tag 重跑流水线,两槽位进 `SHA256SUMS.txt` 即算闭环。 ## 8. 发行合规 - `LICENSE`(Apache-2.0 全文)与 `NOTICE`(含 Slint Royalty-Free 2.0 义务说明)随包分发, 三份文本在 UNIX 归档内、在 Windows 侧作为并列 Release 资产。 - Slint 的 RF 2.0 义务:关于页保留 `AboutSlint` 部件(已在 UI 内),不得移除; 选 RF 分支即不触发 GPL 分支义务(见 `deny.toml` 的许可证白名单注释)。 - `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 系统自带) | | Windows 11 ARM64 | 一等(启动器产物) | 无额外库;启动器原生运行,游戏本体走系统 x64 模拟层 + x64 JDK | | macOS 12+(arm64 / x64) | 一等 | 无第三方库;未签名需去 quarantine | | Linux x64 X11 | 一等 | glibc 2.35+、xkbcommon、fontconfig、libGL、xcb | | Linux x64 Wayland | 支持 | 上述 + wayland-client | | 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 未验证,不在本发布矩阵内 | `packaging/release-notes.md` 的 "Support matrix" 表与上表逐行对应(英文版,Termux 行同样标注 experimental / not part of this release matrix)——改一边必须同步另一边,否则标题里的"一致"就是假话。 两个 arm64 槽位都是**正式发布槽位**(产物进 Release、进 `SHA256SUMS.txt`、release job 的六槽位齐全性 检查会拦下缺失),不要跟 Termux 那行混为一谈:Termux 是"源码自建的实验级旁路",Linux arm64 是 "原生 runner 编、原生跑"的一等产物。 ## 10. 已知边界 / 后续 1. **GUI 无 `--version`**:`unlucky-launcher.exe` 目前只解析 `--software-renderer` / `--screenshot*` / `--data-dir` / `--mock`,`--version` 会当成普通参数忽略并直接开窗口。版本自证目前靠: ①产物名里的 `` ②`ull --version`(同 workspace 版本)③tag 交叉校验。 加一个 `--version` 分支属于 `crates/app` 范围,已作为 finding 上报,不在本 mission 内改。 2. **签名/公证**:Windows 无 Authenticode、macOS 无 codesign/notarize(都要证书/开发者账号), 当前以 SHA256SUMS + Release 说明作为完整性参考。 3. **macOS `.app` bundle / `.dmg`**:与单文件承诺冲突,暂不做;若用户要 Dock 图标再单开 mission。 4. **Linux musl 静态版**:可彻底摆脱 glibc 地板,但 Slint 在 musl 上需额外验证,未纳入本矩阵。 5. **`ull` CLI 未随包**:本次矩阵只发 GUI 单文件(用户核心卖点是启动器本体);headless 用户可从 源码 `cargo build --release --bin ull`,或在后续把 `ull` 加进同一矩阵(脚本已按槽位参数化,改动很小)。 6. **本机 GNU 链接器的已知无害告警**:`cargo build --release`(GNU 工具链)会打一行 `warning: linker stderr: corrupt .drectve at end of def file`,来自 MinGW `ld` 对 `.drectve` 段的 老式抱怨,不影响产物(体积/导入表/冒烟均已实测正常);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):宿主判定注入(`win32/ARM64`、`linux/aarch64` 等全槽位)、 可执行文件头架构识别(真 x64 exe + 合成 PE/ELF/Mach-O 头)、以及把真 exe 的 PE 机器码改成 ARM64 当 stub 走完命名/校验/归档全流程 —— 不假装能在 x64 上执行 arm64 二进制。 ## 11. CI 运行时冒烟(Linux 真渲染一帧) **细节与依据在 [`packaging/ci-runtime-smoke.md`](ci-runtime-smoke.md)**(本节再增长也往那里写,两个文件都必须 ≤400 行)。 `ci.yml` 的 `runtime-smoke` job 在 `ubuntu-latest` 上补第一份**真实图形环境**运行证据:装 `xvfb` + 软渲染 依赖 + 字体,用 `xvfb-run` 起 GUI 进程,走既有进程内截图通道渲出一帧 PNG,再对**像素本身**断言 (退出码 0 / PNG 尺寸 / 非空白三项,artifact `runtime-smoke-screenshot`)。 要点三条: - 只看退出码不够:PNG 是进程自己编码写出的,「合法 PNG + 退出码 0」不等于画面有内容,像素断言是纵深防御; 而「完全没有字体」与「非软渲染」这两种失效都是**硬失败**(panic / 报错,不产生 PNG;review 实测),运行步就红。 - 闸门下限 ≈「至少画出一条标题栏」(review 实测切片仍 pass):证明「场景被真实渲染过」,**不证明界面元素齐全, 更不证明视觉正确**——视觉靠人眼看 artifact。 - 只覆盖 Linux x64;macOS 是 dispatch-only 探针(无实测证据),Windows 已有本机人眼验收。 **本地复跑**:`bash packaging/linux-smoke-docker.sh . D:/tmp/ull-smoke`(输出目录别写 `/tmp`,Git Bash 下会 静默落进 Docker VM;脚本内部用 `cygpath -m` 归一并回显真实路径)。 ## 12. 文件清单 | 文件 | 作用 | 行数上限 | |------|------|---------| | `packaging/README.md` | 本文:矩阵/依赖/版本/合规契约 | ≤400 | | `packaging/make_artifacts.py` | 打包与校验(本地 + CI 共用) | ≤400 | | `packaging/release-notes.md` | GitHub Release 说明模板(`{{VERSION}}` / `{{SUMS}}` 占位) | ≤400 | | `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 | | `packaging/ci-runtime-smoke.md` | §11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 | | `packaging/linux-smoke-docker.sh` | 本地复跑 `runtime-smoke`(Docker + xvfb) | ≤400 | | `.github/workflows/release.yml` | tag 触发 → 矩阵构建 → Release | ≤400 |