From 8bfabafe8a2f752715fdfa73fef0d11352a875c9 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 20:06:56 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(app-iced):=20M147=20=E4=BF=AE=E6=8E=89?= =?UTF-8?q?=20Linux=20=E4=BE=A7=20clippy=20=E7=9A=84=2014=20=E6=9D=A1?= =?UTF-8?q?=E9=94=99=E4=B8=8E=E5=86=99=E6=AD=BB=E5=B9=B3=E5=8F=B0=E7=9A=84?= =?UTF-8?q?=20=CE=B1=20=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两条红的成因都是「Windows-only 的东西没有平台门」:Linux + `--all-targets` + `-D warnings` 三件事凑齐时,Windows 专有的 DWM/穿透代码在 Linux 上变成未用导入 + 死代码。 clippy(14 条错,docker 内 exit 101 → 0): - dwm.rs `set_preference` 的 `#[cfg(not(windows))]` 分支以 `return` 结尾 ⇒ needless_return; 那个块本来就是该 cfg 下函数体的尾巴,去掉 `return`。 - dwm.rs 三常量 + 四读函数在非 Windows 上没有使用者(唯一调用方 `probe::run_one` 的 Windows 版被门着,`transparency.rs` 只 re-export 三项)⇒ dead_code。用**模块级** `#![cfg_attr(not(target_os = "windows"), allow(dead_code))]`: · 逐项 `cfg(target_os = "windows")` 会把它们从 Linux 构建里删掉,本文件那几条与平台 无关的单测(属性号照 Win32 头文件、无效句柄不许表现成成功)就连编译都过不了; · 逐项 `cfg_attr` 要多 7 行,本文件 394 行 → 会顶破 guards.yml 的 400 行红线(现 398 行)。 代价写明在文件头:非 Windows 上本模块的 dead_code 不再报。 - exstyle.rs `use super::raw_ex_style`:唯一使用者 `click_through` 自己按平台门着 ⇒ 补 cfg。 - probe.rs 的 `use super::dwm::{…}`:`CornerOutcome` 与平台无关(`ProbeRow` 字段 + 非 Windows 的 `empty_row`),其余九个符号只服务于 Windows 的 `run_one` ⇒ 拆成两条 use。 - window_alpha.rs `use …RawWindowHandle`:只有 `RawWindowHandle::Win32` 那条臂用它 ⇒ 补 cfg。 test(docker 内 622 passed / 1 failed → 623 passed / 0 failed): `opaque_or_refused_alpha_never_touches_the_window_style` 里「无效句柄 ⇒ Failed」是按 Windows 语义写死的;非 Windows 走 `transparency::set_layered` 的空实现,如实返回 `Unsupported`。 按平台拆成两条断言(Windows 断 `Failed`,其它断 `Unsupported`),两边都不许表现成 `Applied`/`Opaque`。顺带说明:`dispatch` 的 `layered=false` 那半条断言与平台无关,保留在 两条之外照跑。 验证(本机 Windows + Docker Debian 12,命令与 ci.yml 逐字一致): - docker `cargo clippy --workspace --all-targets -- -D warnings` → exit 0 - docker `cargo test -p unlucky-app-iced --lib` → 623 passed / 0 failed - Windows `cargo check -p unlucky-app-iced --all-targets` → 无 error/warning - Windows `cargo test -p unlucky-app-iced --lib` → 624 passed / 0 failed / 2 ignored --- crates/app-iced/src/app/window_alpha.rs | 13 +++++++++++++ .../app-iced/src/platform/win32/transparency/dwm.rs | 6 +++++- .../src/platform/win32/transparency/exstyle.rs | 3 +++ .../src/platform/win32/transparency/probe.rs | 7 ++++++- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/app-iced/src/app/window_alpha.rs b/crates/app-iced/src/app/window_alpha.rs index e3366d9..7ece895 100644 --- a/crates/app-iced/src/app/window_alpha.rs +++ b/crates/app-iced/src/app/window_alpha.rs @@ -52,6 +52,9 @@ impl Dispatch { /// 把 iced 的窗口句柄翻成 Win32 的 `HWND`(`isize`)。非 Windows 平台返回 `None`。 pub(super) fn probe_hwnd(handle: &dyn window::Window) -> Option { + // 只有 `RawWindowHandle::Win32` 那条臂用到它,其余平台只剩 `_ => None` + // ⇒ 无条件导入在非 Windows 上是 `unused_imports`(M147)。 + #[cfg(target_os = "windows")] use window::raw_window_handle::RawWindowHandle; let handle = handle.window_handle().ok()?; @@ -99,11 +102,21 @@ mod tests { // 无效句柄 + 合法 α:样式补不上(layered=false),FFI 也不会成功——两层都如实报告。 let invalid = dispatch(1, Some(0.5)); assert!(!invalid.layered, "无效句柄补不上 WS_EX_LAYERED"); + // 「做不到」的语义随平台不同(M147 实证:这条断言原先按 Windows 写死,Ubuntu 上 + // 622 passed / 1 failed):Windows 上 FFI 真被调用并失败 ⇒ `Failed`;非 Windows 走 + // `transparency.rs` 的空实现 ⇒ `Unsupported`。两边都不许表现成 `Applied` / `Opaque`。 + #[cfg(target_os = "windows")] assert!( matches!(invalid.alpha, AlphaOutcome::Failed { .. }), "{:?}", invalid.alpha ); + #[cfg(not(target_os = "windows"))] + assert_eq!( + invalid.alpha, + AlphaOutcome::Unsupported, + "非 Windows 的空实现必须如实说「这个平台不支持」,不许冒充成功" + ); } /// 本机真窗口的取证由 `App` 启动日志给(见 `app::appearance::log_appearance`), diff --git a/crates/app-iced/src/platform/win32/transparency/dwm.rs b/crates/app-iced/src/platform/win32/transparency/dwm.rs index 3990bae..a8707b2 100644 --- a/crates/app-iced/src/platform/win32/transparency/dwm.rs +++ b/crates/app-iced/src/platform/win32/transparency/dwm.rs @@ -18,6 +18,10 @@ //! 回读拿到的是 **DWM 侧的当前值**——它能抓住「设了 A 属性、读回还是旧值」这类事 //! (比如把 `DWMWA_WINDOW_CORNER_PREFERENCE` 写成别的号,回读会给出 `None` 或旧值)。 +// M147:本模块七项(三个属性号 + 四个读函数)只在 Windows 上有使用者——唯一调用方 `probe::run_one` +// 的 Windows 版被门着,Linux + `-D warnings` 下就是 dead_code 错。用模块级 `cfg_attr` 而不是 +// 逐项 `cfg`:`cfg` 会把这些项连同本文件那几条与平台无关的单测一起门掉(逐项 `cfg_attr` 多 7 行,顶破红线)。 +#![cfg_attr(not(target_os = "windows"), allow(dead_code))] /// `DWMWA_WINDOW_CORNER_PREFERENCE`:窗口圆角偏好的属性号(Win11 / Server 2025,build ≥ 22000)。 pub const DWMWA_WINDOW_CORNER_PREFERENCE: u32 = 34; /// `DWMWCP_DONOTROUND`:显式直角。 @@ -120,7 +124,7 @@ pub fn set_preference(hwnd: isize, preference: u32) -> CornerOutcome { #[cfg(not(target_os = "windows"))] { let _ = (hwnd, preference); - return CornerOutcome::UnsupportedPlatform; + CornerOutcome::UnsupportedPlatform } #[cfg(target_os = "windows")] { diff --git a/crates/app-iced/src/platform/win32/transparency/exstyle.rs b/crates/app-iced/src/platform/win32/transparency/exstyle.rs index 82c8ab4..ec1ce34 100644 --- a/crates/app-iced/src/platform/win32/transparency/exstyle.rs +++ b/crates/app-iced/src/platform/win32/transparency/exstyle.rs @@ -19,6 +19,9 @@ //! 「取证窗口真的把命中测试关掉了」的 OS 侧证据——比「出图 hash 不变」有判别力得多 //! (实测:同 argv 开/关穿透,hash 完全相同)。 +// 唯一使用者是下面的 `click_through`(它自己按平台门禁),非 Windows 上这个导入没人用 +// ⇒ `unused_imports` 在 `-D warnings` 下升成错(M147)。 +#[cfg(target_os = "windows")] use super::raw_ex_style; /// `WS_EX_TRANSPARENT`:**点击穿透**位(`0x0000_0020`)。 diff --git a/crates/app-iced/src/platform/win32/transparency/probe.rs b/crates/app-iced/src/platform/win32/transparency/probe.rs index 2a307ac..05ce55d 100644 --- a/crates/app-iced/src/platform/win32/transparency/probe.rs +++ b/crates/app-iced/src/platform/win32/transparency/probe.rs @@ -39,9 +39,14 @@ #[cfg(target_os = "windows")] use super::alpha::{apply_window_alpha, ensure_layered}; +// `CornerOutcome` 与平台无关(`ProbeRow` 的字段类型、非 Windows 的 `empty_row` 都用它), +// 其余九个符号只服务于 Windows 的 `run_one` —— 分开写,否则非 Windows 上整条导入无人使用 +// (`unused_imports`,M147)。 +use super::dwm::CornerOutcome; +#[cfg(target_os = "windows")] use super::dwm::{ corner_preference_hr, extended_frame_bounds, set_preference, visible_border_thickness, - window_rect, window_style, CornerOutcome, DWMWCP_DONOTROUND, DWMWCP_ROUND, + window_rect, window_style, DWMWCP_DONOTROUND, DWMWCP_ROUND, }; /// `WS_POPUP`(`0x8000_0000`):**带 owner 的弹出窗**才用它;产品主窗口没有 owner,不用。 From ae1410f34209a1fbadedbe4ffac02fa347348b22 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 20:07:00 +0800 Subject: [PATCH 2/4] =?UTF-8?q?ci:=20M147=20=E7=BB=99=E4=B8=A4=E4=B8=AA?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E7=9A=84=20test=20=E6=AD=A5=E5=8A=A0?= =?UTF-8?q?=E5=8F=96=E8=AF=81=E8=83=BD=E5=8A=9B=EF=BC=88--no-fail-fast=20+?= =?UTF-8?q?=20=E6=B3=A8=E8=A7=A3=20+=20=E9=80=80=E5=87=BA=E7=A0=81?= =?UTF-8?q?=E5=9B=9E=E6=98=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三条红里 `test (windows)` 至今只有一个「Process completed with exit code 1」——而 cargo 自己的失败码是 101,那个 1 的来源不明(pwsh 包装层洗码 / 进程被杀,两说都未排除)。job log 要登录才能读(匿名 403),但**注解匿名可读**(GET /check-runs//annotations → 200)。 所以把一条 `cargo test --workspace --all-targets` 拆成两个平台的取证版本,各做三件事: 1. `--no-fail-fast`:cargo 默认第一个失败的 target 就停(M144 实测 Linux 只跑了 7 个 test binary 就退),后面还有没有第二条失败完全看不到; 2. 失败时把 `error` / `failures:` / `test result: FAILED` / panicked 行升成 `::error::` 注解(`%` 转 `%25`,那是 workflow command 的保留字符)——拿不到 job log 时注解是匿名 侧唯一能取到的失败正文; 3. 显式 `echo "cargo exit="`,把「包装层把码洗成 1」「cargo 真失败(101)」「进程被杀 只给码、零诊断」三种情形分开。 两平台的码从哪来(不能混用): - Linux 走 bash(默认 shell 带 `-e` + `pipefail`):先 `set +e` 再自己取 `${PIPESTATUS[0]}` ——那是 cargo 的码,不是 `tee` 的码;不关 `-e` 的话管道一失败就在 tee 之后退出,后面三行 永远跑不到。 - Windows 走 pwsh:`PIPESTATUS` 是 bash 语法,改用 `$LASTEXITCODE`(`Tee-Object` 是 cmdlet, 不会改它)。注解的正则不锚 `error:` 那一支,因为 pwsh 把原生 stderr 折成 ErrorRecord 后 行首可能多一截前缀,锚死会漏行。 两条都以 `exit ` 结尾,退出码不再经过管道传递。 --- .github/workflows/ci.yml | 50 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e97b15..c04f1b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,7 +132,55 @@ jobs: # 同 clippy:`--workspace` 不含 `unlucky-app`(M118),Slint 退役树不再被 `--all-targets` 拉起。 # 也因此**不要**再写 `--exclude unlucky-app`:它已不是成员,那面旗子只会打一条 # `excluded package(s) … not found in workspace` 的警告然后空转(实测 exit 0,不报错)。 - - run: cargo test --workspace --all-targets + # + # ---- 取证(M147):下面是同一件事的两个平台版本,不是两条不同的测试 ---- + # M144 的三条红里两条已在 docker 里定位到行,第三条 `test (windows)` 在 run 里只留下 + # `Process completed with exit code 1`——而 cargo 自己的失败码是 **101**,所以那个 1 的 + # 来源不明(包装层洗码 / 进程被杀,两说都未排除)。job log 要登录才能读(匿名 403), + # 但**注解匿名可读**(`GET /check-runs//annotations` → 200,M144 实证)。所以这三件 + # 事各自对准一种「看不见」: + # 1) `--no-fail-fast`:cargo 默认在第一个失败的 target 就停(M144 实测 Linux 只跑了 + # 7 个 test binary 就退出),后面还有没有第二条失败完全看不到; + # 2) 把 `error` / `failures:` / panicked 行升成 `::error::` 注解:拿不到 job log 时, + # 注解是匿名侧唯一能取到的失败正文; + # 3) 显式打印 cargo 的退出码:把「包装层把码洗成 1」「cargo 真失败(101)」「进程被杀 + # 只给码、零诊断」三种情形分开——这正是 M144 没定的那一半。 + - name: cargo test --workspace --all-targets(ubuntu,失败升注解) + if: runner.os == 'Linux' + run: | + log="$RUNNER_TEMP/cargo-test.log" + # 默认 shell 带 `-e` + `pipefail`:管道里 cargo 一失败就会在 tee 之后立刻退出, + # 下面三行永远跑不到。所以这段先关 `-e`,自己取 `PIPESTATUS[0]`(cargo 的码, + # 不是 tee 的码)。 + set +e + cargo test --workspace --all-targets --no-fail-fast 2>&1 | tee "$log" + code=${PIPESTATUS[0]} + set -e + echo "cargo exit=$code" + if [ "$code" -ne 0 ]; then + # `%` 是 workflow command 的保留字符,要转成 `%25`,否则注解正文会被截断。 + grep -E '^(error(\[E[0-9]+\])?:|failures:|test result: FAILED|thread .* panicked at| *--> )' "$log" \ + | head -40 | sed 's/%/%25/g; s/^/::error::/' || true + fi + exit "$code" + - name: cargo test --workspace --all-targets(windows,失败升注解) + if: runner.os == 'Windows' + shell: pwsh + run: | + # pwsh 没有 PIPESTATUS(那是 bash 语法):拿 `$LASTEXITCODE` —— Tee-Object 是 cmdlet, + # 不会把它改掉,所以它是 cargo 自己的码。 + $log = Join-Path $env:RUNNER_TEMP 'cargo-test.log' + cargo test --workspace --all-targets --no-fail-fast 2>&1 | Tee-Object -FilePath $log + $code = $LASTEXITCODE + Write-Host "cargo exit=$code" + if ($code -ne 0) { + # 正则不锚 `error:` 那一支:pwsh 把原生 stderr 折成 ErrorRecord 后行首可能多一截前缀, + # 锚死会漏行;`%` 是 workflow command 的保留字符,转成 `%25`。 + Select-String -Path $log -Pattern '(error(\[E\d+\])?:|^failures:|test result: FAILED|thread .* panicked at|^\s+--> )' | + Select-Object -First 40 | + ForEach-Object { Write-Host "::error::$($_.Line.Replace('%', '%25'))" } + } + exit $code build: name: build release (${{ matrix.os }}) From fdb9ebb01405c578868c2f8b88e02bbb11dcffcf Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 20:07:04 +0800 Subject: [PATCH 3/4] =?UTF-8?q?build(packaging):=20M147=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20linux-check-docker.sh=EF=BC=8C=E6=9C=AC=E6=9C=BA?= =?UTF-8?q?=E5=A4=8D=E8=B7=91=20ci.yml=20=E7=9A=84=E4=B8=A4=E6=9D=A1=20ubu?= =?UTF-8?q?ntu=20=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `linux-smoke-docker.sh` 复刻的是 runtime-smoke(能不能真跑起来),本脚本复刻的是**编译期 门禁**(能不能编过 / 测过):`cargo clippy --workspace --all-targets -- -D warnings` 与 `cargo test --workspace --all-targets --no-fail-fast`,命令与 ci.yml 逐字一致。 为什么值得单独一条脚本:M144 的两条 ubuntu 红只在「Linux + --all-targets + -D warnings」 三件事凑齐时才炸,Windows 本机永远复现不了(那些 `#[cfg(target_os = "windows")]` 分支在 Windows 上都有使用者)。本脚本是「本机提前挡住它们」的唯一通道。 实现:在现成的 `ull-linux-smoke:bookworm` 上只加一层 `cmake` + `ninja-build`(`aws-lc-sys` 缺 cmake 编不过,M144 实测),缓存卷挂 `registry` 子目录(直接挂 /usr/local/cargo 会盖掉 镜像里的 cargo 本体,M62 踩过),Git Bash 路径用 `cygpath -m` 归一。 - `ULL_CHECK_STAGES`(默认 `clippy test`):两阶段各自跑完再汇总退出码,一处红不掩盖另一处; `ULL_CHECK_FAIL_FAST=1` 恢复首错即停。 - `ULL_CHECK_TEST_ARGS`(默认照抄 ci.yml):快查用 `-p unlucky-app-iced --lib`。 - 每阶段打印 `<命令> → exit=`,便于与 CI 注解里的码对照。 诚实边界写在脚本头:基础镜像 Debian 12 / runner Ubuntu 24.04,只对齐编译期判定,不覆盖 发行版差异面;也不跑 fmt / build / runtime-smoke。 M147 实测(本机 Docker / ull-linux-check:bookworm / 2026-11-06): - `ULL_CHECK_STAGES=clippy` → exit=0(1m27s,原 14 条错) - `ULL_CHECK_STAGES=test ULL_CHECK_TEST_ARGS='-p unlucky-app-iced --lib'` → 623 passed / exit=0 --- packaging/README.md | 1 + packaging/linux-check-docker.sh | 116 ++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 packaging/linux-check-docker.sh diff --git a/packaging/README.md b/packaging/README.md index 9f56fb5..a0766a9 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -66,6 +66,7 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一) | `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 | | `packaging/ci-runtime-smoke.md` | §11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 | | `packaging/linux-smoke-docker.sh` | 本地复跑 `runtime-smoke`(Docker + xvfb) | ≤400 | +| `packaging/linux-check-docker.sh` | 本地复跑 `ci.yml` 的两条 ubuntu 门禁(clippy / test,Docker) | ≤400 | | `.github/workflows/release.yml` | tag 触发 → 选槽位(`plan`)→ 调下面两个可复用工作流 | ≤400 | | `.github/workflows/release-build.yml` | `workflow_call`:单槽位构建(体检 / cargo / 打包 / 上传 artifact) | ≤400 | | `.github/workflows/release-publish.yml` | `workflow_call`:发布(`PUBLISH_SLOTS` 裁剪 / SHA256SUMS / Release) | ≤400 | diff --git a/packaging/linux-check-docker.sh b/packaging/linux-check-docker.sh new file mode 100644 index 0000000..8867c1b --- /dev/null +++ b/packaging/linux-check-docker.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# 本地复跑 `.github/workflows/ci.yml` 的两条 **ubuntu 门禁**:`clippy --workspace --all-targets +# -- -D warnings` 与 `test --workspace --all-targets`。 +# +# 为什么要有这条脚本(与 `linux-smoke-docker.sh` 同一动机,但受检对象不同): +# - smoke 脚本复刻的是 `runtime-smoke` job(能不能真跑起来);本脚本复刻的是**编译期门禁** +# (能不能编过 / 测过)。M144 的三条红里有两条(clippy ubuntu、test ubuntu)属于后者, +# 而它们只在「Linux + `--all-targets` + `-D warnings`」三件事凑齐时才炸—— +# 在 Windows 本机永远复现不了(Windows 上那些 `#[cfg(target_os = "windows")]` 分支都有使用者)。 +# 所以这条脚本是「本机提前挡住那两条红」的唯一通道。 +# +# 用法: +# bash packaging/linux-check-docker.sh [仓库根目录] +# ULL_CHECK_STAGES=clippy bash packaging/linux-check-docker.sh # 只跑 clippy +# ULL_CHECK_TEST_ARGS='-p unlucky-app-iced --lib' \ +# ULL_CHECK_STAGES=test bash packaging/linux-check-docker.sh # 只跑一个 crate 的单测 +# +# 前置:基础镜像 `ull-linux-smoke:bookworm`(由 `packaging/linux-smoke-docker.sh` 建)。 +# 本脚本在它之上只加一层 `cmake` + `ninja-build`:`aws-lc-sys`(rustls 的汇编后端) +# 缺 cmake 编不过,M144 实测过。 +# +# 诚实边界(别把它当 runner 的等价物): +# - 基础镜像是 **Debian 12(glibc 2.36)**,官方 runner 是 **Ubuntu 24.04(glibc 2.39)**; +# 两者 rustc 走同一份 `rust-toolchain.toml`(stable),所以**编译期 lint 与单测判定**可对齐, +# 但发行版差异面(系统库版本、字体)不受本脚本保护。 +# - 本脚本只跑 ci.yml 里那两条命令,不跑 fmt / build / runtime-smoke。 +# - 冷编很贵(全 workspace + `--all-targets`,M144 实测 15–25 分钟)。缓存卷 `ull-linux-check-target` +# 第二次起是热的;**别**把宿主机的 target 目录挂进来(路径与指纹都对不上,等于骗自己)。 + +set -euo pipefail + +repo_root=$(cd "${1:-.}" && pwd) +base_image=${ULL_SMOKE_IMAGE:-ull-linux-smoke:bookworm} +image=${ULL_CHECK_IMAGE:-ull-linux-check:bookworm} +cargo_volume=${ULL_CARGO_VOLUME:-ull-cargo-reg} +target_volume=${ULL_CHECK_TARGET_VOLUME:-ull-linux-check-target} +apt_mirror=${ULL_APT_MIRROR:-https://mirrors.tuna.tsinghua.edu.cn} +stages=${ULL_CHECK_STAGES:-clippy test} +test_args=${ULL_CHECK_TEST_ARGS:---workspace --all-targets --no-fail-fast} +fail_fast=${ULL_CHECK_FAIL_FAST:-0} + +# Git Bash 里路径是 MSYS 虚拟路径:直接喂给 `docker -v` 会被当成容器内路径(M62 踩过)。 +if command -v cygpath >/dev/null 2>&1; then + repo_root=$(cygpath -m "$repo_root") +fi +echo "== 仓库: $repo_root" +echo "== 阶段: $stages(test 参数: $test_args)" + +if ! docker image inspect "$base_image" >/dev/null 2>&1; then + echo "!! 缺基础镜像 $base_image —— 先跑一次:" >&2 + echo " bash packaging/linux-smoke-docker.sh . D:/tmp/ull-smoke" >&2 + echo " (它建的镜像里已经带了 ci.yml 那批图形栈编译依赖;本脚本只在其上加 cmake/ninja)" >&2 + exit 2 +fi + +# 一层派生镜像:cmake + ninja-build。只在缺镜像或显式 ULL_CHECK_REBUILD_IMAGE=1 时重建。 +if ! docker image inspect "$image" >/dev/null 2>&1 || [ "${ULL_CHECK_REBUILD_IMAGE:-0}" = "1" ]; then + context=$(mktemp -d) + trap 'rm -rf "$context"' EXIT + cat >"$context/Dockerfile" <&2; exit 2 ;; + esac + echo "== 开始: $label" + set +e + "$run" + code=$? + set -e + # 退出码单独成行:一眼能看出是「编译期 lint」还是「单测」,也能与 CI 注解里的码对照。 + echo "== 结束: $label → exit=$code" + if [ "$code" -ne 0 ]; then + overall=1 + if [ "$ULL_CHECK_FAIL_FAST" = "1" ]; then exit "$code"; fi + fi + done + echo "== 汇总: $ULL_CHECK_STAGES → exit=$overall" + exit "$overall" + ' + +echo "== 两条 ubuntu 门禁已跑完(上表 exit=0 才算绿)" From 166342b021a2b886a6a234d6b21064016bcf796e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 20:19:03 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(core,design):=20M147=20=E4=BF=AE?= =?UTF-8?q?=E6=8E=89=20test(ubuntu)=20=E5=89=A9=E4=B8=8B=E7=9A=84=E4=B8=A4?= =?UTF-8?q?=E6=9D=A1=E7=BA=A2=EF=BC=88M144=20=E8=A2=AB=20fail-fast=20?= =?UTF-8?q?=E6=8C=A1=E4=BD=8F=E6=B2=A1=E7=9C=8B=E8=A7=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo test --workspace --all-targets --no-fail-fast` 在 docker 里(Debian 12 / rustc 1.99.1) 第一次跑全:app-iced 那条修好后,还有两个 target 各有一条 **Linux 专有**的确定性失败—— M144 只看到第一条,因为 cargo 默认在第一个失败 target 就停。两条都是「夹具/断言按 Windows 语义写死」,不是产品逻辑错。 1. `unlucky-core`:`platform::wallpaper::windows::tests::current_theme_expands_local_app_data_env_var` (crates/core/src/platform/wallpaper/windows.rs:286,460 passed / 1 failed) 夹具用了**混合分隔符**:`LOCAL_APP_DATA` 写 `C:/Users/u/AppData/Local`,而 `CUSTOM_THEME` 写 `C:\Users\u\AppData\Local\…`。探针把 `%LOCALAPPDATA%` 展开后拼上 `\Microsoft\…`,得到 `C:/Users/u/AppData/Local\Microsoft\…`;`FakeIo` 却按 `CUSTOM_THEME` 建键。Windows 的 `Path` 把 `/` 与 `\` 都当分隔符 ⇒ 两个 PathBuf 相等;Linux 不 ⇒ 查不到文件 ⇒ 0 个候选。 修法:`LOCAL_APP_DATA` 改成反斜杠原样(`r"C:\Users\u\AppData\Local"`),展开结果与 `CUSTOM_THEME` 逐字节相同。测试仍能在 Linux 跑,不丢覆盖(不是加 cfg 门躲开)。 2. `unlucky-design`:`color::palettes::tests::accent_on_fixed_neutral_only_moves_the_neutral_hue` (crates/design/src/color/palettes.rs,162 passed / 1 failed) `assert_eq!(Palettes, Palettes)` 做 f64 位相等,而两侧算中性色相的路径不同:一侧取 `NEUTRAL_HUE_DEG` 字面量,一侧经 `atan2()` 现算——glibc 与 UCRT 差 1 ulp (239.51420577630955 vs …964)。这正是 palettes.rs:361-365 那条注释自己警告过的坑。 修法:把「固定中性色相」喂成**种子色相的实测值**(`per_seed.neutral.hue_deg()`),两侧同源 ⇒ 断言保持**逐位相等**(比放宽容差更紧),零额外行。`NEUTRAL_HUE_DEG` 与种子色相的绑定仍由 隔壁 `neutral_hue_is_the_default_seed_hue` 用 1e-9° 容差钉着。 ⚠️ 这里没有照 tower 建议的「1e-9° 容差逐项比」:那个写法在 rustfmt 下要多 9 行,而本文件正好 400 行、顶在 guards.yml 的红线上。改完 399 行(比改前还少 1 行)。 验证(命令与 ci.yml 逐字一致,容器 ull-linux-check:bookworm): - `cargo test --workspace --all-targets --no-fail-fast` → **exit 0**(原 exit 101,2 targets failed) - `cargo clippy --workspace --all-targets -- -D warnings` → exit 0 - Windows:`cargo test -p unlucky-core --lib` → 464 passed / 0 failed; `cargo test -p unlucky-design --lib` → 163 passed / 0 failed(不回归) --- crates/core/src/platform/wallpaper/windows.rs | 6 +++++- crates/design/src/color/palettes.rs | 11 +++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/core/src/platform/wallpaper/windows.rs b/crates/core/src/platform/wallpaper/windows.rs index f10ad58..041c01a 100644 --- a/crates/core/src/platform/wallpaper/windows.rs +++ b/crates/core/src/platform/wallpaper/windows.rs @@ -203,7 +203,11 @@ mod tests { use crate::platform::paths::{MapEnv, Platform}; const APP_DATA: &str = "C:/Users/u/AppData/Roaming"; - const LOCAL_APP_DATA: &str = "C:/Users/u/AppData/Local"; + // 反斜杠原样:本常量经 `%LOCALAPPDATA%` 展开后要与 `CUSTOM_THEME`(反斜杠原文)拼出**同一个 + // `PathBuf` 键**。写成 `C:/…` 会得到 `C:/Users/u/AppData/Local\Microsoft\…` 这种混合串—— + // Windows 的 `Path` 把 `/` 与 `\` 都当分隔符,Linux 只认 `/`,于是同一份夹具在 Linux 上永远 + // 查不到那个文件(M147 实测:CI 的 test(ubuntu) 就红在这条上)。 + const LOCAL_APP_DATA: &str = r"C:\Users\u\AppData\Local"; const DESKTOP_QUERY: &str = "reg.exe query HKCU\\Control Panel\\Desktop /v WallPaper"; const THEMES_QUERY: &str = "reg.exe query HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes /v CurrentTheme"; diff --git a/crates/design/src/color/palettes.rs b/crates/design/src/color/palettes.rs index e2a492e..e5b2d03 100644 --- a/crates/design/src/color/palettes.rs +++ b/crates/design/src/color/palettes.rs @@ -374,13 +374,12 @@ mod tests { #[test] fn accent_on_fixed_neutral_only_moves_the_neutral_hue() { let official = Palettes::official_baseline(); - // 中性色相 = 基线色相时,与官方的 from_seed 完全等价(不放宽算法)。 - let same = Palettes::accent_on_fixed_neutral(DEFAULT_SEED, NEUTRAL_HUE_DEG, 12.0); + // 中性色相取**种子色相的实测值**而不是 `NEUTRAL_HUE_DEG` 字面量:两者只差 `atan2()` 的末位 + // (见下一条测试),逐位比会在 glibc 与 UCRT 之间翻脸;取实测值两侧同源,断言仍是逐位相等。 let per_seed = Palettes::from_seed_with_neutral_chroma(DEFAULT_SEED, 12.0); - assert_eq!( - same, per_seed, - "中性色相取种子色相时必须与 TonalSpot 逐项相同" - ); + let seed_hue = per_seed.neutral.hue_deg(); + let same = Palettes::accent_on_fixed_neutral(DEFAULT_SEED, seed_hue, 12.0); + assert_eq!(same, per_seed, "中性色相取种子色相时须与 TonalSpot 相同"); // 换一个中性色相:只有中性两族的色相动,彩度与强调族一律不动。 let moved = Palettes::accent_on_fixed_neutral(DEFAULT_SEED, 120.0, 12.0);