diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b12c566..d2870b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Release -# 发布流水线:tag `v*` → 三平台(四槽位)矩阵构建 → 单文件产物 + SHA256SUMS +# 发布流水线:tag `v*` → 四平台(六槽位)矩阵构建 → 单文件产物 + SHA256SUMS # → 上传 artifact + 创建 GitHub Release(pre-release 默认开)。同一 tag 重跑幂等: # Release 已存在时覆盖资产与说明,不再 `gh release create`(见 release job 末步); # 重跑**不碰** pre-release 标记,该标记只在 `inputs.prerelease` 显式给出时才被断言。 @@ -42,26 +42,56 @@ jobs: fail-fast: false matrix: include: - # Windows:MSVC + 静态 CRT(+crt-static)→ exe 不依赖 VC++ 可再发行包, + # Windows x64:MSVC + 静态 CRT(+crt-static)→ exe 不依赖 VC++ 可再发行包, # 双击即用的单文件形态;UCRT 本身就是 Win10+ 的系统组件。 + # 产物根 target/release;自检与打包都从 matrix.build_dir 取路径(不硬编码)。 - slot: windows-x64 os: windows-latest + build_dir: target/release + rustflags: -C target-feature=+crt-static + # Windows arm64:在 windows-latest(x64 宿主)上**交叉编译**到 aarch64-pc-windows-msvc。 + # 为什么不用 windows-11-arm 原生 runner(GitHub 文档 2026 已把 `windows-11-arm` 与 + # `windows-11-vs2026-arm` 列为公开仓库**免费**的标准 arm64 runner,原生路线可行): + # ① 双 Windows 槽位共用同一套已验证镜像/工具链,tag 重跑行为与既有槽位一致; + # ② M50 的 dumpbin 定位链取的是 `Hostx64/x64/dumpbin.exe`,dumpbin 读 ARM64 PE 的 + # 导入表没问题,而原生 ARM 宿主上那条链得再长一条 `Hostarm64/arm64` 分支——为一个 + # 槽位放宽刚立的门禁不值得; + # ③ 交叉编译产物路径变成 target//release,已用 matrix.build_dir 参数化。 + # 若将来改成原生:只需把 os 换成 windows-11-arm、删掉 target/rustflags 依赖并给 dumpbin + # 定位链补 Hostarm64 分支(届时再评估,不在这里预先放宽)。 + - slot: windows-arm64 + os: windows-latest + target: aarch64-pc-windows-msvc + build_dir: target/aarch64-pc-windows-msvc/release rustflags: -C target-feature=+crt-static # Linux:固定 ubuntu-22.04(glibc 2.35 地板)而不是 latest, - # 否则产物的 glibc 底线会跟着 runner 漂移(24.04 = 2.39)。 + # 否则产物的 glibc 底线会跟着 runner 漂移(24.04 的 glibc 明显更高)。 - slot: linux-x64 os: ubuntu-22.04 + build_dir: target/release + # Linux arm64:公开仓库免费的原生 ARM64 runner(原生构建,不做交叉)。 + # 用 `ubuntu-22.04-arm` 而**不是** 24.04:与 x64 同 glibc 地板(2.35), + # 覆盖 Debian 12 / Ubuntu 22.04 arm64 / Raspberry Pi OS Bookworm(2.36)这类实际在跑的 + # ARM 发行版;换 24.04 会把地板抬高、把上面前两个挡在门外。该家族标签有 + # `ubuntu-22.04-arm` / `ubuntu-24.04-arm` / `ubuntu-26.04-arm`,均 4C/16GB、 + # 公开仓库免费不限量(GitHub 文档 Standard runners for public repositories)。 + - slot: linux-arm64 + os: ubuntu-22.04-arm + build_dir: target/release # macOS:latest = arm64;Intel x64 用 macos-15-intel(macos-13 已下线)。 - slot: macos-arm64 os: macos-latest + build_dir: target/release - slot: macos-x64 os: macos-15-intel + build_dir: target/release runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Linux 图形栈构建依赖(winit / femtovg 编译期需要) if: runner.os == 'Linux' + shell: bash run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ @@ -71,14 +101,56 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + # 只有交叉编译槽位需要额外 target(原生槽位留空 → 本步整体跳过)。 + # 不把 `${{ }}` 直接拼进 bash:与后面的打包步骤同一纪律,一律经 env 传入再展开。 + - name: 追加交叉编译目标 ${{ matrix.target }} + if: matrix.target != '' + shell: bash + env: + TARGET: ${{ matrix.target }} + run: rustup target add "$TARGET" + + # 交叉链接的前提是镜像里真有 ARM64 的 MSVC 库(VC `/lib/arm64/libcmt.lib`,+crt-static + # 就靠它)与 Windows SDK 的 ARM64 导入库(`um/arm64/kernel32.lib`)。缺了照样是失败, + # 但先给一句能照做的话,而不是让链接器吐一屏乱码才发现方向:门禁不降级,只把失败说清楚。 + - name: Windows ARM64 交叉工具链体检 + if: matrix.target == 'aarch64-pc-windows-msvc' + shell: bash + run: | + set -euo pipefail + roots=("C:/Program Files/Microsoft Visual Studio" "C:/Program Files (x86)/Microsoft Visual Studio") + vc_lib=$(find "${roots[@]}" -maxdepth 11 -type f -path '*/VC/Tools/MSVC/*/lib/arm64/libcmt.lib' 2>/dev/null | head -n1 || true) + sdk_version=$(ls -d "C:/Program Files (x86)/Windows Kits/10/Lib"/* 2>/dev/null | tail -n1 || true) + sdk_lib="" + if [ -n "$sdk_version" ]; then + sdk_lib=$(find "$sdk_version/um/arm64" -maxdepth 1 -name 'kernel32.lib' 2>/dev/null | head -n1 || true) + fi + echo "MSVC ARM64 静态 CRT: ${vc_lib:-<缺失>}" + echo "SDK ARM64 导入库: ${sdk_lib:-<缺失>}" + if [ -z "$vc_lib" ] || [ -z "$sdk_lib" ]; then + echo "::error::交叉链接缺 ARM64 库:给 VS 加上 Microsoft.VisualStudio.Component.VC.Tools.ARM64(含 ARM64 Windows SDK),或把该槽位改用原生 windows-11-arm runner" + exit 1 + fi + - name: cargo build --release --locked + shell: bash env: RUSTFLAGS: ${{ matrix.rustflags }} - run: cargo build --release --locked --bin unlucky-launcher --bin ull + TARGET: ${{ matrix.target }} + run: | + set -euo pipefail + target_args=() + if [ -n "$TARGET" ]; then + target_args+=(--target "$TARGET") + fi + cargo build --release --locked "${target_args[@]}" --bin unlucky-launcher --bin ull - name: Windows 单文件自检(导入表不得出现 VC++ 可再发行 DLL) if: runner.os == 'Windows' shell: bash + env: + # 跨槽位的关键点:产物在 target//release 而不是 target/release。 + EXE: ${{ matrix.build_dir }}/unlucky-launcher.exe run: | set -euo pipefail # +crt-static 是否真的生效,只看导入表:出现 VCRUNTIME/MSVCP/msvcr/ucrtbase 说明还得装 @@ -89,6 +161,7 @@ jobs: # ② find 兜底:Program Files 与 Program Files (x86) 两个 VS 根都扫(Build Tools 落在 x86 根); # ③ PATH:镜像若已把工具链预置进 PATH 则直接可用。 # PATH 排最后是刻意的:PATH 最不可控,官方定位器优先。 + # Hostx64 的 dumpbin 读 ARM64 PE 的导入表没问题,所以 arm64 槽位共用同一条链。 VSWHERE="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" DUMPBIN="" VIA="" @@ -133,7 +206,8 @@ jobs: exit 1 fi echo "dumpbin: $DUMPBIN(定位自 $VIA)" - "$DUMPBIN" -nologo -dependents target/release/unlucky-launcher.exe | tee dependents.txt + ls -l "$EXE" + "$DUMPBIN" -nologo -dependents "$EXE" | tee dependents.txt if grep -Eiq 'VCRUNTIME|MSVCP|MSVCR|ucrtbase\.dll' dependents.txt; then echo "::error::静态 CRT 未生效:exe 仍动态依赖 VC++ 可再发行包" exit 1 @@ -145,11 +219,15 @@ jobs: # 不把 `${{ }}` 直接拼进 bash 脚本:git ref 名允许 `$`/反引号/`"`,一律经 env 传入再展开。 REF_TYPE: ${{ github.ref_type }} TAG: ${{ github.ref_name }} + SLOT: ${{ matrix.slot }} + BUILD_DIR: ${{ matrix.build_dir }} run: | set -euo pipefail # git-bash 与 Linux/macOS runner 的 python 入口名不一致,先探一遍。 PY=$(command -v python3 || command -v python) - args=() + # 槽位与产物根都从矩阵来:交叉槽位的二进制不在 target/release,脚本据此定位 + # 并回读可执行文件头校验架构(宿主 x64 ≠ 产物 arm64 时冒烟自动跳过,只等 CI 真机)。 + args=(--slot "$SLOT" --build-dir "$BUILD_DIR") if [ "$REF_TYPE" = "tag" ]; then args+=(--version "$TAG") # 与 Cargo.toml 版本交叉校验 fi @@ -202,7 +280,8 @@ jobs: run: | set -euo pipefail ls -l dist - for slot in windows-x64 linux-x64 macos-arm64 macos-x64; do + # 六槽位全齐才发版:缺任意一个就 fail(宁可不出 Release,也不出少一档的 Release)。 + for slot in windows-x64 windows-arm64 linux-x64 linux-arm64 macos-arm64 macos-x64; do if ! compgen -G "dist/*-$slot*" >/dev/null; then echo "::error::矩阵缺产物:$slot" exit 1 diff --git a/packaging/README.md b/packaging/README.md index 909280d..a53d8c3 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -9,7 +9,9 @@ | 槽位 | 产物文件名 | 形态 | 归档内容 | |------|-----------|------|---------| | `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` 可直吃) | @@ -23,6 +25,9 @@ - `` 不带前导 `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. 版本来源(唯一真相源) @@ -47,10 +52,26 @@ 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), -其它组合直接报错退出——不在矩阵里的平台不出"凑合能跑"的产物。 +脚本按宿主自动判定槽位(Windows x64 / Windows arm64 / Linux x64 / Linux arm64 / macOS arm64 / +macOS x64),其它组合直接报错退出——不在矩阵里的平台不出"凑合能跑"的产物。 -### CI(三平台矩阵) +交叉构建(例如在 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`: @@ -59,9 +80,15 @@ python packaging/make_artifacts.py --no-smoke # 跳过 ull --version 冒 要跑 tag 得 `gh workflow run release.yml --ref v0.1.0`。分支 ref 的 dispatch 与 push 一样被 `release` job 的 `if: github.ref_type == 'tag'` 挡在发布之外,所以「dispatch 只构建不发布」 与「dispatch 能关 pre-release」是两条互不冲突的规则。 -- 矩阵:`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。 +- 矩阵(六槽位;`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` @@ -91,6 +118,22 @@ Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两 本机(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 | 依赖 | 说明 | @@ -105,6 +148,15 @@ Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两 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+ | 依赖 | 说明 | @@ -216,6 +268,26 @@ CI 的 MSVC `+crt-static` 产物同理(静态 CRT);`release.yml` 里用 `d 总之不允许"悄悄要用户装可再发行包"。 注意:本机只装了 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 义务说明)随包分发, @@ -232,15 +304,22 @@ CI 的 MSVC `+crt-static` 产物同理(静态 CRT);`release.yml` 里用 `d | 平台 | 级别 | 运行时要求 | |------|------|-----------| | 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*` / @@ -256,6 +335,12 @@ experimental / not part of this release matrix)——改一边必须同步另 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. 文件清单 diff --git a/packaging/make_artifacts.py b/packaging/make_artifacts.py index 886c990..b71f69b 100644 --- a/packaging/make_artifacts.py +++ b/packaging/make_artifacts.py @@ -3,11 +3,13 @@ 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 + Windows x64 unlucky-launcher--windows-x64.exe (bare exe, no zip) + Windows arm64 unlucky-launcher--windows-arm64.exe (bare exe, cross-built) + Linux x64 unlucky-launcher--linux-x64.tar.gz + Linux arm64 unlucky-launcher--linux-arm64.tar.gz + macOS arm64 unlucky-launcher--macos-arm64.tar.gz + macOS x64 unlucky-launcher--macos-x64.tar.gz + 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). @@ -15,9 +17,17 @@ The archives carry the launcher binary plus LICENSE / NOTICE / README.md, which 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. +Slot and build directory are overridable because a cross build's binaries live in +`target//release` and the build host (x64) is not the artifact (arm64): CI +passes both from its matrix, and the executable header is read back so a binary can +never be shipped under a slot name it was not built for. + Usage: cargo build --release --locked python3 packaging/make_artifacts.py [--version v0.1.0] [--out dist] [--no-smoke] + # cross build (arm64 exe from an x64 host): + python3 packaging/make_artifacts.py --slot windows-arm64 \\ + --build-dir target/aarch64-pc-windows-msvc/release """ from __future__ import annotations @@ -26,7 +36,6 @@ import argparse import gzip import hashlib import json -import os import platform import shutil import subprocess @@ -40,9 +49,32 @@ CLI_BIN = "ull" # 随包分发的合规文本(仓库根目录);英文主版 README 是对外分发口径。 COMPLIANCE_DOCS = ("LICENSE", "NOTICE", "README.md") SUMS_NAME = "SHA256SUMS.txt" +DEFAULT_BUILD_DIR = "target/release" + +# 发布矩阵:槽位 -> (OS 家族, 架构, 产物形态)。键名即产物文件名里的槽位段, +# 单一真相源是 packaging/README.md §1;未知槽位一律拒绝,不出"凑合能跑"的产物。 +SLOTS = { + "windows-x64": ("windows", "x64", "exe"), + "windows-arm64": ("windows", "arm64", "exe"), + "linux-x64": ("linux", "x64", "tar"), + "linux-arm64": ("linux", "arm64", "tar"), + "macos-arm64": ("macos", "arm64", "tar"), + "macos-x64": ("macos", "x64", "tar"), +} +# `sys.platform` / `platform.machine()` 的别名归一(aarch64 与 arm64 是同一架构的两种叫法)。 +OS_FAMILIES = (("win", "windows"), ("linux", "linux"), ("darwin", "macos")) +MACHINE_ALIASES = {"amd64": "x64", "x86_64": "x64", "aarch64": "arm64", "arm64": "arm64"} +# 可执行文件头里的机器码 -> 架构(PE / ELF / Mach-O)。 +IMAGE_ARCH = { + ("pe", 0x8664): "x64", + ("pe", 0xAA64): "arm64", + ("elf", 0x3E): "x64", + ("elf", 0xB7): "arm64", + ("macho", 0x01000007): "x64", + ("macho", 0x0100000C): "arm64", +} REPO_ROOT = Path(__file__).resolve().parent.parent -TARGET_DIR = REPO_ROOT / "target" / "release" def fail(message: str) -> NoReturn: @@ -72,28 +104,59 @@ def workspace_version() -> str: 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 host_slot(os_name: str | None = None, machine: str | None = None) -> str: + """Map the build host to a matrix slot. + + Both arguments are injectable so the mapping can be exercised for a host we do not + own (no ARM machine here); they default to the real ones. + """ + os_name = (sys.platform if os_name is None else os_name).lower() + raw_machine = platform.machine() if machine is None else machine + arch = MACHINE_ALIASES.get(raw_machine.lower()) + family = next((fam for prefix, fam in OS_FAMILIES if os_name.startswith(prefix)), None) + for slot, (slot_os, slot_arch, _) in SLOTS.items(): + if (slot_os, slot_arch) == (family, arch): + return slot + # 报错带**原始**取值而不是归一后的 None:`riscv64` 这类不认识的架构得在诊断里看得见。 + fail(f"unsupported build host {os_name}/{raw_machine}; matrix is {' / '.join(SLOTS)}") -def binary(name: str) -> Path: - path = TARGET_DIR / (f"{name}.exe" if sys.platform.startswith("win") else name) +def binary(name: str, build_dir: Path, family: str) -> Path: + path = build_dir / (f"{name}.exe" if family == "windows" else name) if not path.is_file(): fail(f"missing {path}; run `cargo build --release --locked` first") return path +def image_arch(path: Path) -> str | None: + """Architecture stamped into the executable header, or `None` if unreadable.""" + with path.open("rb") as handle: + head = handle.read(0x40) + if head[:4] == b"\x7fELF": + order = "little" if head[5] == 1 else "big" + return IMAGE_ARCH.get(("elf", int.from_bytes(head[18:20], order))) + if head[:2] == b"MZ": + handle.seek(int.from_bytes(head[0x3C:0x40], "little")) + pe = handle.read(6) + if pe[:4] == b"PE\0\0": + return IMAGE_ARCH.get(("pe", int.from_bytes(pe[4:6], "little"))) + return None + if head[:4] == b"\xcf\xfa\xed\xfe": # thin 64-bit little-endian Mach-O + return IMAGE_ARCH.get(("macho", int.from_bytes(head[4:8], "little"))) + return None + + +def verify_arch(path: Path, slot: str) -> None: + """Prove the binary really is built for the slot it is about to be named after.""" + want = SLOTS[slot][1] + got = image_arch(path) + if got is None: + fail(f"cannot read the machine type of {path}; refusing to ship an unverified binary") + if got != want: + fail(f"{path} is a {got} binary but slot {slot} says {want}") + print(f"arch: {path.name} -> {got} (ok for {slot})") + + def smoke_version(cli: Path, version: str) -> None: """`ull --version` must report the workspace version (guards tag/version drift).""" proc = subprocess.run( @@ -108,7 +171,7 @@ def smoke_version(cli: Path, version: str) -> None: 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)") + print(f"smoke: {cli.name} --version -> {version} (ok)") def sha256(path: Path) -> str: @@ -160,6 +223,8 @@ 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("--slot", choices=list(SLOTS), help="matrix slot; defaults to the one derived from the host") + parser.add_argument("--build-dir", default=DEFAULT_BUILD_DIR, help=f"directory holding the built binaries (default: {DEFAULT_BUILD_DIR})") parser.add_argument("--no-smoke", action="store_true", help="skip the `ull --version` smoke run") args = parser.parse_args() @@ -168,22 +233,32 @@ def main() -> int: 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) + native = host_slot() + slot = args.slot or native + family, _, kind = SLOTS[slot] + build_dir = Path(args.build_dir) + if not build_dir.is_absolute(): + build_dir = REPO_ROOT / build_dir + launcher = binary(LAUNCHER_BIN, build_dir, family) + verify_arch(launcher, slot) + # 跨槽位产物(宿主 x64 / 目标 arm64)在本机根本执行不了,冒烟只能等 CI 真机; + # 拿"跳过了"当通过是假证据,所以把跳过原因原样打出来。 + if args.no_smoke or slot != native: + reason = "--no-smoke" if args.no_smoke else f"host is {native}, artifact is {slot}" + print(f"smoke: skipped ({reason})") + else: + smoke_version(binary(CLI_BIN, build_dir, family), 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" + if kind == "exe": + asset = out_dir / f"unlucky-launcher-{version}-{slot}.exe" shutil.copy2(launcher, asset) else: - asset = stage_archive(out_dir, tag, version, launcher) + asset = stage_archive(out_dir, slot, version, launcher) sums = write_sums(out_dir, [asset]) size_mb = asset.stat().st_size / (1024 * 1024) diff --git a/packaging/release-notes.md b/packaging/release-notes.md index ff06805..97558ae 100644 --- a/packaging/release-notes.md +++ b/packaging/release-notes.md @@ -9,7 +9,9 @@ No WebView, no bundled runtime, no JRE download needed to start the launcher its | Platform | Asset | Notes | | --- | --- | --- | | Windows 10/11 x64 | `unlucky-launcher-{{VERSION}}-windows-x64.exe` | single exe, double-click to run | +| Windows 11 ARM64 | `unlucky-launcher-{{VERSION}}-windows-arm64.exe` | single exe, runs natively on ARM64 Windows | | Linux x64 (X11/Wayland) | `unlucky-launcher-{{VERSION}}-linux-x64.tar.gz` | single binary inside, no installer | +| Linux arm64 (X11/Wayland) | `unlucky-launcher-{{VERSION}}-linux-arm64.tar.gz` | single binary inside; needs glibc 2.35+ | | 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 | @@ -22,22 +24,30 @@ with the download on every platform. | Platform | Level | Runtime requirements | | --- | --- | --- | | Windows 10/11 x64 | first class | nothing beyond the OS (UCRT is part of Win10+) | +| Windows 11 ARM64 | first class (launcher artifact) | nothing extra; launcher runs natively, the game itself runs through the OS x64 emulation layer with an x64 JDK | | 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 | +| Linux arm64, X11 | first class | glibc 2.35+ (built on `ubuntu-22.04-arm`, same floor as x64), `libxkbcommon`, `fontconfig`, `libGL`, `libxcb` | +| Linux arm64, Wayland | supported | the above plus `wayland-client` | | Windows 8.1/8/7 | unsupported | baseline was raised to Win10 (decision D21) | | Android — Termux + X11 | experimental, not promised | CLI only (build from source), GUI unverified; not part of this release matrix | +Both arm64 rows are **formally released slots** (the assets ship in this release and in +`SHA256SUMS.txt`), not the experimental Termux path: Linux arm64 is built and smoke-tested on a +native ARM64 runner, Windows ARM64 is cross-built with the same static-CRT gate as x64. + ## Install -**Windows** — put the `.exe` anywhere and run it. Portable data lives next to the exe when a +**Windows** — put the `.exe` anywhere and run it (pick `-windows-arm64.exe` on ARM64 Windows, +`-windows-x64.exe` everywhere else). 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 +tar -xzf unlucky-launcher-{{VERSION}}-linux-x64.tar.gz # use linux-arm64 on aarch64 +cd unlucky-launcher-{{VERSION}}-linux-x64 # ditto chmod +x unlucky-launcher && ./unlucky-launcher ```