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)。
205 lines
6.9 KiB
Rust
205 lines
6.9 KiB
Rust
//! 产物目标平台与包格式:清单里 `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())
|
||
}
|
||
}
|