feat(ci): M62 —— Linux runtime-smoke(xvfb 真渲染一帧)+ 像素非空白门禁

「编得过」不等于「跑得起来」:CI 里 Linux/macOS 此前只 build + test,从未创建过真实图形窗口。
本 commit 补第一份 Linux 真实图形环境运行证据,并把它做成不可伪造的门禁。

- ci.yml 新增 runtime-smoke(ubuntu-latest):装 xvfb + 软渲染/字体依赖,用 xvfb-run 起 GUI 进程,
  走既有 `--software-renderer --screenshot` 通道渲出一帧 PNG,再断言 ①退出码 0 ②PNG 宽高 > 0
  ③非空白(唯一色数 / 亮度σ / 强边缘占比三项过阈值),并把截图作为 artifact `runtime-smoke-screenshot` 上传。
  既有 fmt/clippy/test/build 四个 job 与触发器语义零改动(纯追加)。
- ci.yml 新增 runtime-smoke-macos 探针:只在 workflow_dispatch 触发且 continue-on-error。官方 macOS
  runner 处于 LaunchDaemon 上下文、无 WindowServer 会话,大概率建不了窗口;本机无 macOS 且 worker 禁
  push,这条路径尚无实测证据,故不拿它卡 PR 门禁(首跑结论决定升常驻或按 D26 归入 iced 重写)。
- packaging/check_screenshot.py:零第三方依赖的 PNG 解码(zlib + 反过滤 0~4)与三项指标判定,
  `--selftest` 打印阈值依据(人为纯色帧 1/0.0/0.0 必红,真实帧 342/19.45/0.00761 全过线)。
- packaging/linux-smoke-docker.sh:在 Docker 里复刻同一 job(TUNA apt/rustup 源),便于无 Linux 桌面的本地复跑。
- packaging/README.md §11:覆盖平台、阈值依据数值、本地复跑、已知跳过项、迁 iced 后需重新验证。

本机实测(Debian 12 容器 + xvfb + 软渲染):退出码 0,PNG 1180x760,唯一色 342 / σ 19.45 / 强边缘 0.00761。
CI(Ubuntu 24.04 runner)首跑仍未验证——本 job 证明「能出图」,不证明视觉正确。
This commit is contained in:
AskaEth committed 2026-11-01 16:15:47 +08:00
1 parent a681ccb526
commit ea5f66cf92
4 files changed
+531 -1

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# 本地复跑 `.github/workflows/ci.yml` 的 `runtime-smoke` job:Linux + Xvfb + 软渲染截图。
#
# 为什么要有这条脚本:CI 的 runtime-smoke 在 GitHub 的 ubuntu-latest 上跑,
# 但「改动这个 job 的人」需要在没有 Linux 桌面的开发机上先自证一遍——否则改的就是没人验过的 YAML。
# 本脚本在 Docker 里复刻 job 的步骤(同样的 apt 依赖、同样的构建、同样的 xvfb-run 命令),
# 并把截图与判定结果留在宿主目录,供人眼与 `check_screenshot.py` 复核。
#
# 用法:
# bash packaging/linux-smoke-docker.sh [仓库根目录] [输出目录]
# ULL_SMOKE_SKIP_BUILD=1 bash packaging/linux-smoke-docker.sh # 复用缓存的二进制
#
# 说明(诚实边界):
# - 基础镜像 `rust:1-bookworm` 是 **Debian 12(glibc 2.36)**,官方 runner 是 **Ubuntu 24.04(glibc 2.39)**。
# 两者图形栈(X11/xvfb/Mesa 软渲染)来源不同,因此本脚本证明的是「Linux 上这条通道能真跑出图」,
# 不能代替官方 runner 的首跑验证。
# - 国内网络慢时可换 TUNA 镜像:脚本默认就用 TUNA 的 Debian 源(`ULL_APT_MIRROR` 可覆盖)。
set -euo pipefail
repo_root=$(cd "${1:-.}" && pwd)
out_dir=${2:-/tmp/ull-smoke}
image=${ULL_SMOKE_IMAGE:-ull-linux-smoke:bookworm}
cargo_volume=${ULL_CARGO_VOLUME:-ull-cargo-reg}
target_volume=${ULL_CARGO_TARGET_VOLUME:-ull-linux-target}
apt_mirror=${ULL_APT_MIRROR:-https://mirrors.tuna.tsinghua.edu.cn}
rustup_mirror=${ULL_RUSTUP_MIRROR:-https://mirrors.tuna.tsinghua.edu.cn/rustup}
mkdir -p "$out_dir"
out_dir=$(cd "$out_dir" && pwd)
echo "== 仓库: $repo_root"
echo "== 输出: $out_dir"
# 1) 构建镜像:rust 工具链 + xvfb + 图形栈编译依赖 + 字体(软渲染器要从系统字体库取字形)
if ! docker image inspect "$image" >/dev/null 2>&1 || [ "${ULL_SMOKE_REBUILD_IMAGE:-0}" = "1" ]; then
context=$(mktemp -d)
trap 'rm -rf "$context"' EXIT
cat >"$context/Dockerfile" <<EOF
FROM rust:1-bookworm
# 工具链组件(rustfmt/clippy)在 rust-toolchain.toml 里是必备项,rustup 会去补齐;
# 默认 dist 服务器在国内会挂成「rustc 0.6% CPU 睡 12 分钟」的假死(M62 实测),所以走 TUNA。
ENV RUSTUP_DIST_SERVER=$rustup_mirror \\
RUSTUP_UPDATE_ROOT=$rustup_mirror/rustup
RUN sed -i 's|http://deb.debian.org|$apt_mirror|g' /etc/apt/sources.list.d/debian.sources \\
&& apt-get update \\
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \\
xvfb xauth python3 \\
libxkbcommon-dev libxkbcommon-x11-0 libfontconfig1-dev \\
libgl1-mesa-dev libegl1-mesa-dev libwayland-dev libxcb1-dev \\
libx11-6 libxcursor1 libxrandr2 libxi6 \\
fonts-dejavu-core fonts-liberation2 \\
&& rm -rf /var/lib/apt/lists/*
RUN rustup component add rustfmt clippy && rustup toolchain list
WORKDIR /src
EOF
docker build -t "$image" "$context"
fi
# 2) 跑容器:构建 → xvfb 截图 → 像素判定
# 注意缓存卷要挂到 `registry` 子目录:直接挂 /usr/local/cargo 会把镜像里的 `bin/`(cargo 本体)盖掉,
# 变成 `cargo: command not found`(M62 实测踩到)。
MSYS_NO_PATHCONV=1 docker run --rm \
-v "$repo_root:/src" \
-v "$cargo_volume:/usr/local/cargo/registry" \
-v "$target_volume:/target" \
-v "$out_dir:/out" \
-w /src \
-e CARGO_TARGET_DIR=/target \
-e ULL_SMOKE_SKIP_BUILD="${ULL_SMOKE_SKIP_BUILD:-0}" \
-e CARGO_TERM_COLOR=never \
"$image" bash -euo pipefail -c '
rustc --version && cargo --version && cat /etc/debian_version
if [ "$ULL_SMOKE_SKIP_BUILD" != "1" ]; then
# 共享热缓存卷可能喂陈旧产物(踩坑记录 N7):先清掉受检 crate 再编
cargo clean -p unlucky-app || true
cargo build --bin unlucky-launcher --locked -j 6
fi
mkdir -p /out/data
set +e
xvfb-run -a --server-args="-screen 0 1280x1024x24" \
/target/debug/unlucky-launcher --software-renderer \
--screenshot /out/runtime-smoke.png --data-dir /out/data
code=$?
set -e
echo "exit=$code"
python3 packaging/check_screenshot.py /out/runtime-smoke.png
exit $code
'
echo "== 截图与数据目录已留在 $out_dir"