Files
Unlucky-Launcher/.github/workflows/release.yml
T
AskaEth 4c1a6c54e5 fix(release): M51 r1 —— linux-arm64 改用 ubuntu-22.04-arm(与 x64 同 glibc 2.35 地板)
P2-1:24.04-arm 会给出 2.39 地板,把 Debian 12 / Raspberry Pi OS Bookworm
(2.36)挡在门外,且「arm64 只有 24.04 档」这句前提是错的(同段注释自己就承认
ubuntu-22.04-arm 有效)。改 os 为 ubuntu-22.04-arm,注释改成「与 x64 同地板
(2.35),覆盖 Debian 12 / Ubuntu 22.04 arm64 / RPi OS Bookworm」;
README §3/§4/§7.5/§9 与 release-notes 的 Downloads/Support matrix 全量订正
2.39 → 2.35+,删掉「arm64 地板高于 x64」的对比措辞。槽位名、产物名、六槽位
齐全性检查与打包脚本均未变。

P2-2:rustup target add 步骤改 env TARGET + `rustup target add "$TARGET"`,
并补 shell: bash(此前是全文件唯一没显式 shell 的 run,在 Windows 槽位会落进
pwsh);Linux 图形栈依赖那步也补了 shell: bash,现在所有 run 都显式 bash、
run 块内零 `${{ }}` 直插。

顺带 nit:host_slot() 报错改回原始 arch token(`riscv64` 不再显示成 None),
README §7.5 宣传的那个用例现在诊断可读。

验证:yaml 结构断言(六槽位 / linux-arm64=ubuntu-22.04-arm / 齐全性循环 6 /
run 块 ${{ }} 零泄漏 / run 步骤全带 shell)、全部 run 块 bash -n、grep 2.39
零残留、host_slot 7 组合注入 + freebsd/riscv64 报错含原名、image_arch 真 exe、
windows-arm64 stub 全流程、架构守卫拦截、linux-arm64 归档两次 sha256 一致
(f949d0f5… 与上一轮相同)、windows-x64 原生回归(冒烟 + sha256sum -c)。
2026-11-01 14:16:29 +08:00

356 lines
17 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Release
# 发布流水线:tag `v*` → 四平台(六槽位)矩阵构建 → 单文件产物 + SHA256SUMS
# → 上传 artifact + 创建 GitHub Release(pre-release 默认开)。同一 tag 重跑幂等:
# Release 已存在时覆盖资产与说明,不再 `gh release create`(见 release job 末步);
# 重跑**不碰** pre-release 标记,该标记只在 `inputs.prerelease` 显式给出时才被断言。
#
# 契约唯一真相源:packaging/README.md(命名/目录/运行时依赖)。
# 打包本体在 packaging/make_artifacts.py(本地与 CI 同一份脚本,逐字节同布局)。
# 手动触发(workflow_dispatch)默认只跑构建不建 Release —— 用来验证矩阵仍然编得过。
# 注意 `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」并不矛盾。
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
prerelease:
description: "标记为 pre-release(仅在 tag ref 上 dispatch 时才生效)"
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 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/<triple>/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 的 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 \
libxkbcommon-dev libfontconfig1-dev libgl1-mesa-dev libegl1-mesa-dev \
libwayland-dev libxcb1-dev
- 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 }}
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/<triple>/release 而不是 target/release。
EXE: ${{ matrix.build_dir }}/unlucky-launcher.exe
run: |
set -euo pipefail
# +crt-static 是否真的生效,只看导入表:出现 VCRUNTIME/MSVCP/msvcr/ucrtbase 说明还得装
# 可再发行包,那就不是"单文件"了,直接 fail。
#
# dumpbin 定位链(三级,全不命中才 fail —— 门禁不降级,只是把"猜路径"换成官方定位):
# ① vswhere:VS 官方定位器,安装器自身路径固定,与 VS 版本/版次/安装布局无关;
# ② 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=""
if [ -x "$VSWHERE" ]; then
# -find 的模式优先用反斜杠(vswhere 官方文档即此形式),再试正斜杠以规避分隔符差异。
for pattern in 'VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe' \
'VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe'; do
hit=$("$VSWHERE" -latest -products '*' -find "$pattern" 2>/dev/null | head -n1 || true)
if [ -n "$hit" ]; then
DUMPBIN=${hit//\\//}
VIA="vswhere"
break
fi
done
fi
if [ -z "$DUMPBIN" ]; then
# 镜像里的 VS 根形如 <年>/<版次>(2022/Enterprise、2022/BuildTools……),
# 固定层数的 glob 会在 <年>/<版次> 这两级踩空,所以按路径模式扫。
for root in "C:/Program Files/Microsoft Visual Studio" \
"C:/Program Files (x86)/Microsoft Visual Studio"; do
[ -d "$root" ] || continue
hit=$(find "$root" -maxdepth 11 -type f \
-path '*/VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe' 2>/dev/null | head -n1 || true)
if [ -n "$hit" ]; then
DUMPBIN=$hit
VIA="find: $root"
break
fi
done
fi
if [ -z "$DUMPBIN" ]; then
hit=$(command -v dumpbin || true)
if [ -n "$hit" ]; then
DUMPBIN=$hit
VIA="PATH"
fi
fi
# 工具缺失同样 fail:windows-latest 自带 VS 工具链,基本找得到;
# "找不到就跳过"等于让这道门禁在最需要它的时候静默失效。
if [ -z "$DUMPBIN" ]; then
echo "::error::找不到 dumpbin,无法校验导入表(静态 CRT 门禁不降级)"
exit 1
fi
echo "dumpbin: $DUMPBIN(定位自 $VIA)"
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
fi
- name: 打包(产物 + SHA256SUMS)
shell: bash
env:
# 不把 `${{ }}` 直接拼进 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)
# 槽位与产物根都从矩阵来:交叉槽位的二进制不在 target/release,脚本据此定位
# 并回读可执行文件头校验架构(宿主 x64 ≠ 产物 arm64 时冒烟自动跳过,只等 CI 真机)。
args=(--slot "$SLOT" --build-dir "$BUILD_DIR")
if [ "$REF_TYPE" = "tag" ]; then
args+=(--version "$TAG") # 与 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
# 六槽位全齐才发版:缺任意一个就 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
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(同 tag 重跑幂等)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
PRERELEASE: ${{ inputs.prerelease }}
shell: bash
run: |
set -euo pipefail
base_flags=(--title "Unlucky Launcher $TAG" --notes-file release-body.md)
# pre-release 标记只在**显式要求**时才断言:
# PRERELEASE=true / false —— 来自 tag ref 上的 workflow_dispatch 输入;
# PRERELEASE 空 —— push 触发的运行或它的 Re-run,此时**不碰**该标记,
# 否则会把用户手动提升为 stable 的 Release 静默翻回 pre-release。
# 首次创建(create)仍保持「pre-release 默认开」的老契约:空值按 true 处理。
create_mark=()
edit_mark=()
case "${PRERELEASE:-}" in
true)
create_mark=(--prerelease)
edit_mark=(--prerelease)
;;
false)
edit_mark=(--prerelease=false)
;;
"")
create_mark=(--prerelease)
;;
*)
echo "::error::非预期的 inputs.prerelease 取值:$PRERELEASE"
exit 1
;;
esac
# Windows 单 exe 装不下 LICENSE/NOTICE/README.md,这三份与产物并列挂到 Release 上,
# 保证每个下载平台的许可条款都随包分发。
assets=(dist/unlucky-launcher-* dist/SHA256SUMS.txt LICENSE NOTICE README.md)
if gh release view "$TAG" >/dev/null 2>&1; then
# 同 tag 重跑(补丁 / 重编):Release 已存在 → 覆盖资产 + 刷新说明,
# 不再 `create`(那会因已存在而失败,把整条流水线在最后一步染红)。
gh release upload "$TAG" "${assets[@]}" --clobber
gh release edit "$TAG" "${base_flags[@]}" "${edit_mark[@]}"
else
gh release create "$TAG" "${assets[@]}" "${base_flags[@]}" "${create_mark[@]}"
fi