diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9555ff2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,200 @@ +name: Release + +# 发布流水线:tag `v*` → 三平台(四槽位)矩阵构建 → 单文件产物 + SHA256SUMS +# → 上传 artifact + 创建 GitHub Release(pre-release 默认开)。 +# +# 契约唯一真相源:packaging/README.md(命名/目录/运行时依赖)。 +# 打包本体在 packaging/make_artifacts.py(本地与 CI 同一份脚本,逐字节同布局)。 +# 手动触发(workflow_dispatch)只跑构建不建 Release —— 用来验证矩阵仍然编得过。 + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + prerelease: + description: "标记为 pre-release" + type: boolean + default: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: build (${{ matrix.slot }}) + strategy: + fail-fast: false + matrix: + include: + # Windows:MSVC + 静态 CRT(+crt-static)→ exe 不依赖 VC++ 可再发行包, + # 双击即用的单文件形态;UCRT 本身就是 Win10+ 的系统组件。 + - slot: windows-x64 + os: windows-latest + rustflags: -C target-feature=+crt-static + # Linux:固定 ubuntu-22.04(glibc 2.35 地板)而不是 latest, + # 否则产物的 glibc 底线会跟着 runner 漂移(24.04 = 2.39)。 + - slot: linux-x64 + os: ubuntu-22.04 + # macOS:latest = arm64;Intel x64 用 macos-15-intel(macos-13 已下线)。 + - slot: macos-arm64 + os: macos-latest + - slot: macos-x64 + os: macos-15-intel + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Linux 图形栈构建依赖(winit / femtovg 编译期需要) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libxkbcommon-dev libfontconfig1-dev libgl1-mesa-dev libegl1-mesa-dev \ + libwayland-dev libxcb1-dev + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: cargo build --release --locked + env: + RUSTFLAGS: ${{ matrix.rustflags }} + run: cargo build --release --locked --bin unlucky-launcher --bin ull + + - name: Windows 单文件自检(导入表不得出现 VC++ 可再发行 DLL) + if: runner.os == 'Windows' + shell: bash + run: | + set -euo pipefail + # +crt-static 是否真的生效,只看导入表:出现 VCRUNTIME/MSVCP/msvcr/ucrtbase 说明还得装 + # 可再发行包,那就不是"单文件"了,直接 fail。 + DUMPBIN=$(command -v dumpbin || true) + if [ -z "$DUMPBIN" ]; then + DUMPBIN=$(ls -d "C:/Program Files/Microsoft Visual Studio/"*/VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe 2>/dev/null | head -n1 || true) + fi + if [ -z "$DUMPBIN" ]; then + echo "::warning::找不到 dumpbin,跳过导入表自检(不阻塞发布)" + exit 0 + fi + "$DUMPBIN" -nologo -dependents target/release/unlucky-launcher.exe | tee dependents.txt + if grep -Eiq 'VCRUNTIME|MSVCP|MSVCR|ucrtbase\.dll' dependents.txt; then + echo "::error::静态 CRT 未生效:exe 仍动态依赖 VC++ 可再发行包" + exit 1 + fi + + - name: 打包(产物 + SHA256SUMS) + shell: bash + run: | + set -euo pipefail + # git-bash 与 Linux/macOS runner 的 python 入口名不一致,先探一遍。 + PY=$(command -v python3 || command -v python) + args=() + if [ "${{ github.ref_type }}" = "tag" ]; then + args+=(--version "${{ github.ref_name }}") # 与 Cargo.toml 版本交叉校验 + fi + "$PY" packaging/make_artifacts.py "${args[@]}" --out dist + ls -l dist + + - name: 归档内容自检(二进制定位 + 合规文本) + shell: bash + run: | + set -euo pipefail + for archive in dist/*.tar.gz; do + [ -e "$archive" ] || continue + listing=$(tar -tzf "$archive") + for required in unlucky-launcher LICENSE NOTICE README.md; do + if ! grep -q "/$required$" <<<"$listing"; then + echo "::error file=$archive::$archive 缺少 $required" + exit 1 + fi + done + echo "OK: $archive 含二进制与 LICENSE/NOTICE/README.md" + done + if ls dist/*.exe >/dev/null 2>&1; then + echo "Windows 单 exe 形态:LICENSE/NOTICE/README.md 作为 Release 资产并列分发" + fi + + - uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.slot }} + path: dist + if-no-files-found: error + retention-days: 14 + + release: + name: GitHub Release + needs: build + if: github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: 矩阵完整性 + 重算全量 SHA256SUMS + shell: bash + run: | + set -euo pipefail + ls -l dist + for slot in windows-x64 linux-x64 macos-arm64 macos-x64; do + if ! compgen -G "dist/*-$slot*" >/dev/null; then + echo "::error::矩阵缺产物:$slot" + exit 1 + fi + done + # 各平台任务各写一份 SHA256SUMS.txt,合并后只有一份是真的 —— 在 Release 侧重算。 + cd dist + rm -f SHA256SUMS.txt + sha256sum unlucky-launcher-* > SHA256SUMS.txt + sha256sum -c SHA256SUMS.txt + cat SHA256SUMS.txt + + - name: 生成 Release 说明(支持矩阵模板) + shell: bash + env: + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + python3 - <<'PY' + import os, pathlib + version = os.environ["TAG"].lstrip("v") + template = pathlib.Path("packaging/release-notes.md").read_text(encoding="utf-8") + sums = pathlib.Path("dist/SHA256SUMS.txt").read_text(encoding="utf-8").strip() + body = template.replace("{{VERSION}}", version).replace("{{SUMS}}", sums) + pathlib.Path("release-body.md").write_text(body, encoding="utf-8", newline="\n") + print(body[:400]) + PY + + - name: 创建 GitHub Release(pre-release 默认开) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + PRERELEASE: ${{ inputs.prerelease }} + shell: bash + run: | + set -euo pipefail + flags=(--title "Unlucky Launcher $TAG" --notes-file release-body.md) + if [ "${PRERELEASE:-true}" != "false" ]; then + flags+=(--prerelease) + fi + # Windows 单 exe 装不下 LICENSE/NOTICE/README.md,这三份与产物并列挂到 Release 上, + # 保证每个下载平台的许可条款都随包分发。 + gh release create "$TAG" \ + dist/unlucky-launcher-* \ + dist/SHA256SUMS.txt \ + LICENSE NOTICE README.md \ + "${flags[@]}" diff --git a/Cargo.toml b/Cargo.toml index 3b434e4..b6308f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ all = { level = "deny", priority = -1 } [profile.release] opt-level = "z" -lto = "thin" +lto = "fat" codegen-units = 1 panic = "abort" strip = "symbols" diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 0000000..6b18c70 --- /dev/null +++ b/packaging/README.md @@ -0,0 +1,237 @@ +# 打包与发布工程(packaging/) + +单一真相源:**单文件产物矩阵 + 发布契约**。`.github/workflows/release.yml` 按本文实现, +`packaging/make_artifacts.py` 是本地与 CI **共用**的打包脚本(同一份代码 → 同一套目录布局)。 +本目录只放发布工程资产(脚本 / 说明 / 发布说明模板),不参与任何 crate 的编译。 + +## 1. 产物矩阵 + +| 槽位 | 产物文件名 | 形态 | 归档内容 | +|------|-----------|------|---------| +| `windows-x64` | `unlucky-launcher--windows-x64.exe` | **裸 exe**(双击即用) | exe 单文件;三份合规文本作为并列 Release 资产 | +| `linux-x64` | `unlucky-launcher--linux-x64.tar.gz` | 单二进制归档 | `unlucky-launcher` + `LICENSE` + `NOTICE` + `README.md` | +| `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 不受打包时刻影响。 + +## 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 / Linux x64 / macOS arm64 / macOS x64), +其它组合直接报错退出——不在矩阵里的平台不出"凑合能跑"的产物。 + +### CI(三平台矩阵) + +`.github/workflows/release.yml`: + +- 触发:`push` tag `v*`;`workflow_dispatch` 只构建不发布(验证矩阵仍编得过)。 +- 矩阵:`windows-latest`(MSVC + `-C target-feature=+crt-static`)、`ubuntu-22.04`(glibc 2.35 地板)、 + `macos-latest`(arm64)、`macos-15-intel`(Intel x64;`macos-13` 已下线,`macos-26-intel` 为更新替位)。 +- 每个槽位 `cargo build --release --locked` → `make_artifacts.py` → 归档内容自检 → 上传 artifact。 +- `release` job:下载全部 artifact → 重算一份全量 `SHA256SUMS.txt` → 用 `packaging/release-notes.md` + 模板渲染支持矩阵 → `gh release create`(**pre-release 默认开**,`workflow_dispatch` 可关)。 + +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。 + +### 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 是启动游戏时才由启动器自己管理的东西)。 + +### 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),换来的是体积与启动时间;开发迭代走 `cargo build`(dev profile)完全不受影响。 +`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 链接更慢(`fat` 是全局单线程 LTO),只影响发布/验收构建,日常 `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)。 + +### 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` 把这条变成硬门禁, +一旦静态 CRT 没生效(出现 VCRUNTIME/MSVCP/ucrtbase)就直接 fail,不允许"悄悄要用户装可再发行包"。 +注意:本机只装了 GNU 工具链,MSVC 那个槽位的产物本身**没有在本机验证过**,由 CI 首跑验证。 + +## 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 系统自带) | +| macOS 12+(arm64 / x64) | 一等 | 无第三方库;未签名需去 quarantine | +| Linux x64 X11 | 一等 | glibc 2.35+、xkbcommon、fontconfig、libGL、xcb | +| Linux x64 Wayland | 支持 | 上述 + wayland-client | +| Windows 8.1/8/7 | 不支持 | 基线已上调(D21) | +| Android Termux + X11 | 试验级(不承诺) | 仅 CLI | + +## 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 槽位不会出现这条。 + +## 11. 文件清单 + +| 文件 | 作用 | 行数上限 | +|------|------|---------| +| `packaging/README.md` | 本文:矩阵/依赖/版本/合规契约 | ≤400 | +| `packaging/make_artifacts.py` | 打包与校验(本地 + CI 共用) | ≤400 | +| `packaging/release-notes.md` | GitHub Release 说明模板(`{{VERSION}}` / `{{SUMS}}` 占位) | ≤400 | +| `.github/workflows/release.yml` | tag 触发 → 矩阵构建 → Release | ≤400 | diff --git a/packaging/make_artifacts.py b/packaging/make_artifacts.py new file mode 100644 index 0000000..886c990 --- /dev/null +++ b/packaging/make_artifacts.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Build the single-file release artifacts for the current platform. + +Release matrix (see `packaging/README.md` for the full contract): + + Windows x64 unlucky-launcher--windows-x64.exe (bare exe, no zip) + Linux x64 unlucky-launcher--linux-x64.tar.gz + macOS arm64 unlucky-launcher--macos-arm64.tar.gz + macOS x64 unlucky-launcher--macos-x64.tar.gz + all SHA256SUMS.txt + +`--version` is taken from the cargo workspace so a tag can never drift away from +`Cargo.toml`; pass `--version` to cross-check a tag against it (mismatch is fatal). +The archives carry the launcher binary plus LICENSE / NOTICE / README.md, which is +what the license obligations ask for; Windows ships the bare `.exe` (the single-file +selling point) and the same three documents are attached to the Release next to it. + +Usage: + cargo build --release --locked + python3 packaging/make_artifacts.py [--version v0.1.0] [--out dist] [--no-smoke] +""" + +from __future__ import annotations + +import argparse +import gzip +import hashlib +import json +import os +import platform +import shutil +import subprocess +import sys +import tarfile +from pathlib import Path +from typing import NoReturn + +LAUNCHER_BIN = "unlucky-launcher" +CLI_BIN = "ull" +# 随包分发的合规文本(仓库根目录);英文主版 README 是对外分发口径。 +COMPLIANCE_DOCS = ("LICENSE", "NOTICE", "README.md") +SUMS_NAME = "SHA256SUMS.txt" + +REPO_ROOT = Path(__file__).resolve().parent.parent +TARGET_DIR = REPO_ROOT / "target" / "release" + + +def fail(message: str) -> NoReturn: + print(f"error: {message}", file=sys.stderr) + raise SystemExit(1) + + +def workspace_version() -> str: + """Read `[workspace.package] version` through cargo itself (single source of truth).""" + try: + raw = subprocess.run( + ["cargo", "metadata", "--no-deps", "--format-version", "1"], + cwd=REPO_ROOT, + check=True, + capture_output=True, + text=True, + # Windows 默认 GBK:cargo 的 package description 是中文,不显式指定会解码失败。 + encoding="utf-8", + errors="replace", + ).stdout + except (OSError, subprocess.CalledProcessError) as exc: + fail(f"cargo metadata failed: {exc}") + packages = json.loads(raw)["packages"] + versions = {pkg["version"] for pkg in packages} + if len(versions) != 1: + fail(f"workspace members disagree on version: {sorted(versions)}") + return versions.pop() + + +def platform_tag() -> str: + """Map the build host to the artifact matrix slot (hard-fail on unsupported ones).""" + os_name = sys.platform + machine = platform.machine().lower() + if os_name.startswith("win") and machine in ("amd64", "x86_64"): + return "windows-x64" + if os_name.startswith("linux") and machine in ("x86_64", "amd64"): + return "linux-x64" + if os_name == "darwin" and machine in ("arm64", "aarch64"): + return "macos-arm64" + if os_name == "darwin" and machine in ("x86_64", "amd64"): + return "macos-x64" + fail(f"unsupported build host {os_name}/{machine}; matrix is windows-x64 / linux-x64 / macos-arm64 / macos-x64") + + +def binary(name: str) -> Path: + path = TARGET_DIR / (f"{name}.exe" if sys.platform.startswith("win") else name) + if not path.is_file(): + fail(f"missing {path}; run `cargo build --release --locked` first") + return path + + +def smoke_version(cli: Path, version: str) -> None: + """`ull --version` must report the workspace version (guards tag/version drift).""" + proc = subprocess.run( + [str(cli), "--version"], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + out = (proc.stdout or "") + (proc.stderr or "") + if proc.returncode != 0: + fail(f"`{cli.name} --version` exited {proc.returncode}: {out.strip()}") + if version not in out: + fail(f"`{cli.name} --version` does not report {version}: {out.strip()}") + print(f"smoke: {cli.name} --version -> {version} (ok)") + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def write_sums(out_dir: Path, assets: list[Path]) -> Path: + sums = out_dir / SUMS_NAME + lines = [f"{sha256(path)} {path.name}" for path in sorted(assets)] + sums.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n") + return sums + + +def stage_archive(out_dir: Path, tag: str, version: str, launcher: Path) -> Path: + """tar.gz with one top-level dir holding the binary + compliance docs.""" + stem = f"unlucky-launcher-{version}-{tag}" + stage = out_dir / stem + if stage.exists(): + shutil.rmtree(stage) + stage.mkdir(parents=True) + shutil.copy2(launcher, stage / launcher.name) + for doc in COMPLIANCE_DOCS: + source = REPO_ROOT / doc + if not source.is_file(): + fail(f"compliance file {source} is missing") + shutil.copy2(source, stage / doc) + # tar 内的 mtime/uid/mode 与 gzip 头的 mtime 全部归零 → 同源构建的 sha256 与打包时刻无关。 + archive = out_dir / f"{stem}.tar.gz" + with archive.open("wb") as raw, gzip.GzipFile( + fileobj=raw, mode="wb", compresslevel=9, mtime=0 + ) as gz, tarfile.open(fileobj=gz, mode="w") as tar: + for entry in sorted(stage.iterdir()): + info = tar.gettarinfo(str(entry), arcname=f"{stem}/{entry.name}") + info.mtime = 0 + info.uid = info.gid = 0 + info.uname = info.gname = "" + info.mode = 0o755 if entry.name == launcher.name else 0o644 + with entry.open("rb") as handle: + tar.addfile(info, handle) + shutil.rmtree(stage) + return archive + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--version", help="expected version (a leading 'v' is stripped); cross-checks Cargo.toml") + parser.add_argument("--out", default="dist", help="output directory (default: dist/)") + parser.add_argument("--no-smoke", action="store_true", help="skip the `ull --version` smoke run") + args = parser.parse_args() + + version = workspace_version() + if args.version: + expected = args.version[1:] if args.version.startswith("v") else args.version + if expected != version: + fail(f"tag/version mismatch: requested {expected}, Cargo.toml says {version}") + tag = platform_tag() + launcher = binary(LAUNCHER_BIN) + cli = binary(CLI_BIN) + if not args.no_smoke: + smoke_version(cli, version) + + out_dir = Path(args.out) + if not out_dir.is_absolute(): + out_dir = REPO_ROOT / out_dir + out_dir.mkdir(parents=True, exist_ok=True) + + if tag == "windows-x64": + asset = out_dir / f"unlucky-launcher-{version}-{tag}.exe" + shutil.copy2(launcher, asset) + else: + asset = stage_archive(out_dir, tag, version, launcher) + + sums = write_sums(out_dir, [asset]) + size_mb = asset.stat().st_size / (1024 * 1024) + print(f"artifact: {asset.name} {size_mb:.2f} MiB sha256={sha256(asset)}") + print(f"sums: {sums.name}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packaging/release-notes.md b/packaging/release-notes.md new file mode 100644 index 0000000..abf2fbb --- /dev/null +++ b/packaging/release-notes.md @@ -0,0 +1,91 @@ +# Unlucky Launcher {{VERSION}} + +Rust-native **single-file** Minecraft launcher: Slint + Metro UI, built-in AI agent +(chat / config edits / crash diagnosis) and an MCP server other agent tools can attach to. +No WebView, no bundled runtime, no JRE download needed to start the launcher itself. + +## Downloads + +| Platform | Asset | Notes | +| --- | --- | --- | +| Windows 10/11 x64 | `unlucky-launcher-{{VERSION}}-windows-x64.exe` | single exe, double-click to run | +| Linux x64 (X11/Wayland) | `unlucky-launcher-{{VERSION}}-linux-x64.tar.gz` | single binary inside, no installer | +| macOS 12+ Apple Silicon | `unlucky-launcher-{{VERSION}}-macos-arm64.tar.gz` | single binary inside, no installer | +| macOS 12+ Intel | `unlucky-launcher-{{VERSION}}-macos-x64.tar.gz` | single binary inside, no installer | + +`LICENSE`, `NOTICE` and `README.md` ship in every archive; on Windows they are attached to +this release next to the `.exe` (a lone exe cannot carry them), so the license terms travel +with the download on every platform. + +## Support matrix + +| Platform | Level | Runtime requirements | +| --- | --- | --- | +| Windows 10/11 x64 | first class | nothing beyond the OS (UCRT is part of Win10+) | +| macOS 12+ (arm64 / x64) | first class | nothing; unsigned build, see Gatekeeper note | +| Linux x64, X11 | first class | `libxkbcommon`, `fontconfig`, `libGL` (or software renderer), `libxcb` | +| Linux x64, Wayland | supported | `wayland-client`, `libxkbcommon`, plus the X11 set for XWayland apps | +| Windows 8.1/8/7 | unsupported | baseline was raised to Win10 (decision D21) | + +## Install + +**Windows** — put the `.exe` anywhere and run it. Portable data lives next to the exe when a +`portable.marker` file is present, otherwise under `%APPDATA%`. + +**Linux** + +```bash +tar -xzf unlucky-launcher-{{VERSION}}-linux-x64.tar.gz +cd unlucky-launcher-{{VERSION}}-linux-x64 +chmod +x unlucky-launcher && ./unlucky-launcher +``` + +**macOS** + +```bash +tar -xzf unlucky-launcher-{{VERSION}}-macos-arm64.tar.gz +cd unlucky-launcher-{{VERSION}}-macos-arm64 +xattr -d com.apple.quarantine unlucky-launcher # unsigned build, Gatekeeper quarantine +chmod +x unlucky-launcher && ./unlucky-launcher +``` + +## No GPU? Software renderer + +The UI prefers OpenGL (femtovg) and falls back to Slint's CPU renderer when no usable GL +driver is present. Force it on any machine with: + +```bash +unlucky-launcher --software-renderer # or: SLINT_BACKEND=winit-software unlucky-launcher +``` + +## Verify the download + +```bash +# Linux / macOS +sha256sum -c SHA256SUMS.txt --ignore-missing +# Windows (PowerShell) +Get-FileHash .\unlucky-launcher-{{VERSION}}-windows-x64.exe -Algorithm SHA256 +``` + +## Checksums + +``` +{{SUMS}} +``` + +## Known limitations of this build + +- Microsoft (MSA) account login depends on a Mojang app-id whitelist that is still pending; + until it is granted, use offline accounts or authlib-injector (the 403 is reported with a + readable diagnostic instead of a silent failure). +- Binaries are not code-signed / notarized yet: Windows SmartScreen and macOS Gatekeeper warn + on first launch. Checksums above are the integrity reference for now. +- macOS ships the bare binary (single-file promise), not a `.app` bundle: no Dock icon or + Finder double-click — launch it from a terminal. + +## License + +Apache-2.0. Third-party notices (including Slint's Royalty-Free 2.0 track) are in `NOTICE`. +Unlucky Launcher is not affiliated with Mojang or Microsoft; it does not ship game files. + +Made with [Slint](https://slint.dev) — Slint Royalty-Free 2.0 track, unmodified.