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 }}) 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,不用。 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); 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 才算绿)"