M118(D26):Slint 退役树 `crates/app` 不再进 workspace。目录与源码原样保留(不删一个文件), 只是配置上不再可编。 - 根 Cargo.toml:`[workspace] exclude = ["crates/app"]`。摘掉的是整棵 slint 1.18 + usvg 0.48 依赖树(本机单路冷编 ≈14 分钟;并行超配时表现为 rustc 退 1 且零诊断,见踩坑记录 p2 T7)。 Cargo.lock 由 cargo 自己重算:8639 → 6343 行(-2389/+93),`name = "slint"` 1→0、 `name = "i-slint` 11→0、`name = "slint-build"` 1→0。锁必须一起提交:旧锁配 `--locked` 会直接 `cannot update the lock file`(release.yml 与 ci.yml 的冒烟都带 `--locked`)。 - ci.yml:clippy/test/build 的 `--workspace` 自然不再覆盖 unlucky-app;runtime-smoke(Linux 与 macOS 探针)打的是 Slint 版 `--bin unlucky-launcher`,该 bin 已不存在 → 换成 iced 版 `unlucky-launcher-iced`(它的 cli.rs 照抄 Slint 版口径,`--data-dir` 仍被接受)。 - release.yml:**Slint 版停发**。旧写法 `--bin unlucky-launcher` 打的是 crates/app 的 bin, 也就是说流水线一直在发 Slint 版;改为 `--bin unlucky-launcher-iced --bin ull`,Windows 导入表自检的 EXE 指向构建产物 `unlucky-launcher-iced.exe`。对外产物文件名不变。 - make_artifacts.py:拆成 `LAUNCHER_BIN = "unlucky-launcher-iced"`(找产物)与 `LAUNCHER_ASSET = "unlucky-launcher"`(落盘名),tar 内文件名/权限随之按对外名走。 - guards.yml / README(中英)/ packaging/README.md:逐条写明排除后的覆盖口径,历史实现段落 全部保留并加限定语(不提供构建方式,也不暗示自救)。 门禁:fmt exit 0;clippy --workspace --all-targets -j 4 -- -D warnings exit 0(44.6s); check --workspace --all-targets exit 0(5m20s,slint 命中 0 行);cargo deny check exit 0。
370 lines
18 KiB
YAML
370 lines
18 KiB
YAML
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
|
||
|
||
# M118(D26):**Slint 版停发**。旧写法 `--bin unlucky-launcher` 打的是 `crates/app`
|
||
# (Slint 退役树)的 bin —— 也就是说这条流水线一直在发 Slint 版二进制,而 D26 早已把
|
||
# iced 定为产品 UI。现在 GUI 来源只有 `unlucky-launcher-iced`;`crates/app` 也已在根
|
||
# Cargo.toml 被 `exclude`,不参与任何 workspace 构建(它连 `--manifest-path` 都编不了)。
|
||
# 对外产物文件名**不变**(`unlucky-launcher-<ver>-<slot>.*`):改名发生在
|
||
# `packaging/make_artifacts.py` 内部(`LAUNCHER_BIN` 找 iced 产物、落盘用对外名),
|
||
# 下载链接 / SHA256SUMS / README 都不用跟着漂。
|
||
- 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-iced --bin ull
|
||
|
||
- name: Windows 单文件自检(导入表不得出现 VC++ 可再发行 DLL)
|
||
if: runner.os == 'Windows'
|
||
shell: bash
|
||
env:
|
||
# 跨槽位的关键点:产物在 target/<triple>/release 而不是 target/release。
|
||
# M118:校验的是**构建产物**(iced bin 的原始文件名)。对外名 `unlucky-launcher`
|
||
# 由 packaging/make_artifacts.py 在落盘时才换上,那一步之后才存在。
|
||
EXE: ${{ matrix.build_dir }}/unlucky-launcher-iced.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)。
|
||
# 每个槽位都要 GUI 与 CLI 两个资产——只按「槽位名出现过」判会被其中一个掩盖。
|
||
for slot in windows-x64 windows-arm64 linux-x64 linux-arm64 macos-arm64 macos-x64; do
|
||
for prefix in unlucky-launcher ull; do
|
||
if ! compgen -G "dist/$prefix-*-$slot*" >/dev/null; then
|
||
echo "::error::矩阵缺产物:$prefix / $slot"
|
||
exit 1
|
||
fi
|
||
done
|
||
done
|
||
# 各平台任务各写一份 SHA256SUMS.txt,合并后只有一份是真的 —— 在 Release 侧重算。
|
||
# GUI 与 CLI 都要进清单:漏了 ull 就是「发了但没有校验和」。
|
||
cd dist
|
||
rm -f SHA256SUMS.txt
|
||
sha256sum unlucky-launcher-* ull-* > 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 上,
|
||
# 保证每个下载平台的许可条款都随包分发。
|
||
# `ull` 是独立资产(不在 tar.gz 里):headless 用户下一个小文件直接跑,不用解档。
|
||
assets=(dist/unlucky-launcher-* dist/ull-* 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
|