Files
Unlucky-Launcher/crates/app-iced/src/main.rs
T
AskaEth f986981c12 feat(acceptance): M98 —— 八类诊断日志 + 一次运行的全覆盖验收矩阵
验收要的是「一次运行、多角度覆盖、输出能直接判读」,而不是把朋友当测试机反复问 7 个问题。
这一轮把产品进程能**自己观测**的那部分日志补齐,并把人工问题压缩到 3 条。

新增 `renderer/acceptance/`(三块,全部 ≤400 行):

- `mod.rs`:`[backend]`(plan = 判据,final = **结果**:`result=started|failed` + 具名错误 +
  `via=respawn|refuse|none`)、`[perf]`(boot 三段实测 / session / scene 帧率只算首帧之后的间隔)、
  `[theme]`(来源 = 哪个参数决定,系统那条写 `pending` 等异步真值)、`[window]`(**请求值**,
  字段名自证 `requested` / `scale_requested`);开关 `ULL_ACCEPTANCE=1`、尺寸覆盖
  `ULL_WINDOW_SIZE=1520x900` 与它的解析/夹取(含 `size_source=` 四态)。
- `locale.rs`:`[env]`(真适配器枚举 + 硬件判定;显示器写 `needs-window` 不猜)、`[i18n]`
  (四级链每一级的原始值 + 结论; macOS 走 `defaults read -g AppleLanguages`)。
- `visual.rs`:`[font] chain`(令牌声明的回退链,与已有的实际落点 `[font] platform=` 互补)、
  `[caps]`(`bundle` / `menu_bar_name` 从可执行文件路径**读**出来;`controls=self-drawn` 由
  `decorations=false` 推出)。

接线:`app/boot.rs` 在正确的时刻把真值喂进去(探测产物 / 窗口设置 / 实测 `Instant`),
`pages/harness.rs` 同一套前缀(含 `[perf] scene`),`main.rs` 用法里写出两个开关。

反模式的防线(M97 那类「在需要结果的地方打印请求」):每一格都有能改坏的断言
(`acceptance/tests.rs` + `locale_tests.rs`,共 20 条新增),失败路径的 `error="…"` 会把
wgpu 自带的双引号换成单引号,保证单行可解析。

本机(Windows)实测:auto → `[backend] final=wgpu result=started` + `Selected: AdapterInfo`;
cpu → `tiny-skia`;`WGPU_BACKEND=metal` 负例 → exit 1 + 无图 + `via=refuse`;宽屏覆盖生效。
macOS 分支(Metal / AppleLanguages / 菜单栏名)需真机复核,期望字符串见 `renderer/mod.rs` 文档。

验收脚本不入库(用户要求):落在 gitignore 的 `DevDocs/ops/macos-acceptance.sh`,
由 tower 拷到分发目录。
2026-11-03 01:09:25 +08:00

104 lines
4.6 KiB
Rust
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.
//! 入口。所有实际逻辑在 lib(`unlucky_app_iced`)里:
//!
//! 一个二进制两种用法(M82),分发在进 UI 之前完成:
//!
//! - 被子命令命中(`instances` / `install` / `launch` / `accounts` / `sessions` / `logs` /
//! `diag` / `config` / `java` / `agent`,或 `--help` / `--version`)→ 直接转给
//! `unlucky_cli::run`,**不初始化任何 UI**(不 probe 后端、不扫字体、不碰图形库)。
//! 子命令表不在这里抄一份:唯一真相源是 `crates/cli` 的解析器
//! (`unlucky_cli::cli::looks_like_cli`,本文件只调用)。
//! - 其余参数(`--software-renderer` / `--renderer` / `--theme` / `--screenshot*` /
//! `--diff` …):开窗跑骨架,`--screenshot` 出图后退出;`--diff <a> <b>` 只读两张 PNG
//! 做逐像素对比(D30 的 GPU/CPU 几何一致性检查),不开窗。
//!
//! 零 UI 依赖的 `ull` 仍然保留(D18):它是唯一连 fontconfig 都不需要的产物,
//! 给服务器 / CI / Termux 这类没有图形库的机器用。
//!
//! 退出码:`0` 成功 / `1` 截图有差异(diff 模式)或 CLI 用户错误 / `2` 参数或运行错误。
use std::process::ExitCode;
use unlucky_app_iced::shot;
use unlucky_app_iced::Options;
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
if let Some(code) = run_cli_if_requested(&args) {
return code;
}
// M97:把 iced_wgpu / wgpu 的 `log` 输出接到 stderr。`Selected: AdapterInfo{…}` 那行
// 是「GPU 真生效」的实证——没有它,日志只能证明我们**请求过** GPU。
// CLI 子命令在上面就返回了;`--diff` 虽然也装,但它不碰 wgpu,不会多出任何一行。
unlucky_app_iced::renderer::log_bridge::install();
match run_gui() {
Ok(code) => code,
Err(message) => {
eprintln!("[cli] {message}");
eprintln!(
"用法:unlucky-launcher-iced [--renderer=auto|gpu|cpu] [--software-renderer] \
[--theme=light|dark] [--screenshot <png>] [--screenshot-theme=light|dark] \
[--scale-factor <f32>] [--diff <a.png> <b.png>]"
);
// M98:验收开关写在用法里,不然「日志怎么变详细了」只能靠翻文档。
eprintln!(
"验收诊断:ULL_ACCEPTANCE=1([env]/[backend]/[window]/[font]/[theme]/[perf]/[i18n]/[caps])\
,ULL_WINDOW_SIZE=1520x900 覆盖窗口尺寸"
);
ExitCode::from(2)
}
}
}
/// 子命令命中 CLI 就跑 CLI 并以它的退出码收尾;返回 `None` 表示「这不是 CLI 调用,去开窗」。
///
/// 这条路径上**不构造窗口、不探后端、不扫字体、不依赖任何图形库**(这些库缺失时同一条命令照样
/// 跑完,实测见 packaging/README.md §7.4)。`unlucky-launcher-iced launch …` 的耗时因此与首帧无关——
/// 这句是结构性判断,**没有重量**(原因见 §10-8)。
///
/// 「这条命令行算不算 CLI 调用」的判据不在这里重复:`unlucky_cli::cli::looks_like_cli` 是唯一
/// 一处(M82 复审去重),两个 GUI 二进制都只调它,改语义不会漂。
fn run_cli_if_requested(args: &[String]) -> Option<ExitCode> {
if !unlucky_cli::cli::looks_like_cli(args) {
return None;
}
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
let code = unlucky_cli::run(args, &mut stdout, &mut stderr);
let _ = std::io::Write::flush(&mut stdout);
let _ = std::io::Write::flush(&mut stderr);
Some(ExitCode::from(u8::try_from(code).unwrap_or(2)))
}
/// 开窗路径:解析 GUI 参数 → (`--diff` 走比对)→ 起 UI。
fn run_gui() -> Result<ExitCode, String> {
let options = Options::from_env()?;
if let Some((left, right)) = options.diff.clone() {
let scan = i32::try_from(options.diff_scan).unwrap_or(i32::MAX);
return match shot::diff_files_with_scan(&left, &right, scan) {
Ok(diff) => {
eprintln!("{}", diff.describe());
if diff.is_identical() {
Ok(ExitCode::SUCCESS)
} else {
Ok(ExitCode::from(1))
}
}
Err(message) => {
eprintln!("[diff] {message}");
Ok(ExitCode::from(2))
}
};
}
match unlucky_app_iced::run(options) {
Ok(()) => Ok(ExitCode::SUCCESS),
Err(error) => {
eprintln!("[run] {error}");
Ok(ExitCode::FAILURE)
}
}
}