Files
Unlucky-Launcher/crates/core/src/update/target.rs
T
AskaEth af82658d08 fix(core,app-iced): M308 Linux/macOS 的 .tar.gz 真的能自替换 + 重试按 retryable 分流 + 三处文档纠偏
M307 盘查抓到的一条真缺陷 + 两处交互/文档不一致,本条只修它们,不重做通道:

① Linux/macOS 自更新的真缺陷(选 A:真做)
   `job::self_replaceable` 只认裸 exe,而发布矩阵里那两平台的启动器是 `.tar.gz`(清单记作
   `portable-zip`)⇒ 查得到更新、按钮画得出来、点下去必失败。新增 `core::update::unpack`:
   **归档字节过完长度/摘要双检之后**,从归档里取出启动器本体(tar 头 mode 有可执行位 /
   `.exe` 名字的唯一普通文件),再交给原有的 `install_staged`(原子替换 + 回滚一字未改)。
   零新依赖(flate2/tar 早就是 core 的依赖,`core::game::java` 解 Temurin 用同一套)。
   三条具名错误:`LauncherMissing` / `LauncherNotExecutable`(魔数与 target 不符)/
   `LauncherSizeMismatch`(0 字节 / 超上限 / 与头声明不符);zip 容器**明确拒绝**(没有可靠的
   「哪个条目可执行」判据,不猜)。`target.rs` 的 `PackageFormat` 文档改成与判据逐字一致。

② 重试按钮按 `retryable` 分流
   验签类(`is_retryable == false`)不画「重试」,正文换成新 key `UpdateBannerFailedFinal`
   (「这次更新已被拒绝(重试没用):{reason}」)。文案抽到 `pages/update/failure.rs`
   (`pages/update.rs` 的红线),判据落在**真 Widget 树**上(探针 tag + 递归计数)。

③ 三处文档纠偏 + 同一条谎话的其它落点(都在 scope 内)
   `trust.rs`(正式表已有 `ull-release`,commit 68362e5)、`check_signing_ready.py` 自述、
   `target.rs`;顺带 `packaging/README.md`、`docs/{signing,signing-runbook}.md`、
   `check_test_key_off.py`;新文档 `docs/update-trust-anchor.md` 记「M254 审计的『正式表为空』
   已作废」。

判据:core 新增 `unpack_cases`(Linux tar.gz / macOS tar.gz 走完整条链 + 五种拒绝)、
`job_cases` 的形态表改成两种;app-iced 两条真 Widget 树用例(按钮在 / 不在)+ 一条取词入口用例
+ 一条源码守卫。i18n 加一条 key 并**重出金表**(`Key::ALL` 853 行,新行落在 index 834)。
2026-11-13 23:38:17 +08:00

205 lines
6.9 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.
//! 产物目标平台与包格式:清单里 `target` / `format` 两个字段的取值。
//!
//! 系统家族直接复用 [`OsFamily`](`core::platform` 的口径),这里不另立一套
//! Windows/macOS/Linux 枚举——否则同一件事会有两个真相源。
//!
//! `target` 的清单写法是 `系统-架构`,例如 `win-x64` / `mac-arm64` / `linux-arm64`。
//! 解析容忍常见别名(`windows` / `macos` / `osx`、`x86_64` / `amd64`、`aarch64`),
//! 因为清单是人(或 CI 脚本)拼出来的,别让 `win-amd64` 这种写法静默变成「没有可用产物」。
use std::fmt;
use crate::platform::os_info::OsFamily;
/// 产物架构。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Arch {
/// x86_64 / amd64。
X64,
/// aarch64 / arm64。
Arm64,
/// i686 / x86 32 位。
X86,
/// 编译目标认不出的架构(对照组永远匹配不上,于是走「本平台没有产物」那条路)。
Unknown,
}
impl Arch {
/// 清单写法。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::X64 => "x64",
Self::Arm64 => "arm64",
Self::X86 => "x86",
Self::Unknown => "unknown",
}
}
/// 当前编译目标的架构。
#[must_use]
pub const fn current() -> Self {
if cfg!(target_arch = "x86_64") {
Self::X64
} else if cfg!(target_arch = "aarch64") {
Self::Arm64
} else if cfg!(target_arch = "x86") {
Self::X86
} else {
Self::Unknown
}
}
/// 解析清单写法(容忍 `x86_64` / `amd64` / `aarch64` / `i686` 等别名)。
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
match text.trim().to_ascii_lowercase().as_str() {
"x64" | "x86_64" | "x86-64" | "amd64" => Some(Self::X64),
"arm64" | "aarch64" | "arm64e" => Some(Self::Arm64),
"x86" | "i686" | "i386" | "x86_32" => Some(Self::X86),
_ => None,
}
}
}
/// 产物目标平台(系统 + 架构)。
///
/// 没派生 `Hash`/`Ord`:`OsFamily` 尚未派生这两个 trait(`core::platform` 不在本 mission
/// 的改动范围内),需要当 Map 键时再回头补,别在这里另造一套系统枚举绕过去。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Target {
/// 系统家族。
pub os: OsFamily,
/// 架构。
pub arch: Arch,
}
impl Target {
/// 构造。
#[must_use]
pub const fn new(os: OsFamily, arch: Arch) -> Self {
Self { os, arch }
}
/// 当前编译目标。
#[must_use]
pub fn current() -> Self {
Self::new(OsFamily::current(), Arch::current())
}
/// 解析清单写法(`系统-架构`)。
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
let (os, arch) = text.trim().split_once('-')?;
Some(Self::new(parse_os(os)?, Arch::parse(arch)?))
}
/// 清单写法。
#[must_use]
pub const fn as_str(self) -> &'static str {
match (self.os, self.arch) {
(OsFamily::Windows, Arch::X64) => "win-x64",
(OsFamily::Windows, Arch::Arm64) => "win-arm64",
(OsFamily::Windows, Arch::X86) => "win-x86",
(OsFamily::Windows, Arch::Unknown) => "win-unknown",
(OsFamily::MacOs, Arch::X64) => "mac-x64",
(OsFamily::MacOs, Arch::Arm64) => "mac-arm64",
(OsFamily::MacOs, Arch::X86) => "mac-x86",
(OsFamily::MacOs, Arch::Unknown) => "mac-unknown",
(OsFamily::Linux, Arch::X64) => "linux-x64",
(OsFamily::Linux, Arch::Arm64) => "linux-arm64",
(OsFamily::Linux, Arch::X86) => "linux-x86",
(OsFamily::Linux, Arch::Unknown) => "linux-unknown",
}
}
}
fn parse_os(text: &str) -> Option<OsFamily> {
match text.trim().to_ascii_lowercase().as_str() {
"win" | "windows" => Some(OsFamily::Windows),
"mac" | "macos" | "osx" | "darwin" => Some(OsFamily::MacOs),
"linux" => Some(OsFamily::Linux),
_ => None,
}
}
impl fmt::Display for Target {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// 安装包形态。
///
/// 只区分「安装器装出来的」与「解压即用的」两大类,具体怎么装由打包层决定:
/// 安装器形态(msi/dmg/deb/rpm)本模块**不主张**原地覆盖(见 `install` 模块文档);
/// 便携形态才走自替换,而**哪两种便携形态真的能变成启动器本体,判据只有一处**:
/// [`super::job::self_replaceable`](`portable-exe` 直接顶替、`portable-zip` 由
/// [`super::unpack`] 拆出本体)。改了那边的 `matches!` 就必须改这段文档,反之亦然——
/// M307 抓到的正是这两处各说一套(`portable-zip` 被文档列为可自替换、判据却只认裸 exe,
/// 于是 Linux/macOS 查得到更新、点下去必失败)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PackageFormat {
/// Windows 安装器。
Msi,
/// macOS 磁盘映像。
Dmg,
/// Linux AppImage。
AppImage,
/// Debian 包。
Deb,
/// RPM 包。
Rpm,
/// 便携归档(解压即用):清单里 Linux/macOS 的 `.tar.gz` 用的就是它。
///
/// 名字里的 `zip` 是历史包袱(DTO 早先只打算发 zip);`parse` 同时认 `zip` 与
/// `portable-zip`,而**字节的容器由魔数判**(tar.gz / tar / zip 三种都认,见
/// [`super::unpack`])——扩展名与清单词面都不能当判据。
PortableZip,
/// 便携单文件 exe。
PortableExe,
}
impl PackageFormat {
/// 清单写法。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Msi => "msi",
Self::Dmg => "dmg",
Self::AppImage => "appimage",
Self::Deb => "deb",
Self::Rpm => "rpm",
Self::PortableZip => "portable-zip",
Self::PortableExe => "portable-exe",
}
}
/// 解析清单写法。
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
match text.trim().to_ascii_lowercase().as_str() {
"msi" => Some(Self::Msi),
"dmg" => Some(Self::Dmg),
"appimage" => Some(Self::AppImage),
"deb" => Some(Self::Deb),
"rpm" => Some(Self::Rpm),
"portable-zip" | "zip" => Some(Self::PortableZip),
"portable-exe" | "exe" => Some(Self::PortableExe),
_ => None,
}
}
/// 是否需要安装器代劳(这些形态不该被本模块原地覆盖)。
#[must_use]
pub const fn needs_installer(self) -> bool {
matches!(self, Self::Msi | Self::Dmg | Self::Deb | Self::Rpm)
}
}
impl fmt::Display for PackageFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}