feat(core): 新增 update 模块,做与更新源无关的自更新内核
更新源还没拍板,所以先把与源无关的那半边做掉:语义化版本比较、发布清单模型、 渠道准入、下载校验、原子替换 + 回滚。源抽象成 UpdateSource trait(清单 + 包体两个 方法),core 里没有任何 CDN 常量;内置的 DirSource 吃本地目录或 file://,既是测试 实现也是将来 HTTP 源的对照物。 取舍: - 版本比较严格按 semver 2.0 §11:数值比(0.9 < 0.10)、预发布 < 同号正式版、 预发布标识逐段比(alpha < beta < rc 是 ASCII 序的自然结果,不另立阶段权重表)、 构建元数据不参与排序与相等。 - 渠道准入两道判据:清单自报渠道的阶梯比较 + 版本号里的预发布阶段。清单可能写错, 版本号不会,所以正式版渠道即使拿到一份自称 stable 的 rc 也不会提示更新。 - 校验两道判据且顺序有意:先比长度(抓截断,不必读全文件)再比摘要(抓内容错), 长度要求**恰好**相等;失败即删暂存文件。StagedPackage 只能由 stage() 产出, 于是「未校验的包」在类型上就进不了安装路径。 - 替换跨平台统一成「旧文件改名挪开 → 新文件搬进来 → 失败把备份搬回去」:Windows 上 覆盖运行中的 exe 不存在,改名是唯一可用动作,而改名也可能被不共享删除的句柄拒绝, 所以失败必须能回滚。Unix 也照这个顺序跑,避免回滚路径只有 Windows 被真正跑过。 - core 零 UI 依赖不变:只有 serde/serde_json + 复用的 sha1/sha2,无新依赖, Cargo.toml 未改;不接 presenter、不接 UI。 单测 62 条(crates/core/src/update/tests/):版本边界、清单校验、渠道准入、 摘要不符、包体截断(短了/多了)、无权限写目标目录、Windows 占用文件、回滚与备份清理。
This commit is contained in:
1 parent
156abfe38b
commit
2e718af711
21 files changed
+3309
No files matched your search
@@ -0,0 +1,196 @@
|
||||
//! 产物目标平台与包格式:清单里 `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` 模块文档),
|
||||
/// 便携形态(portable-zip / appimage / portable-exe)才走自替换。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum PackageFormat {
|
||||
/// Windows 安装器。
|
||||
Msi,
|
||||
/// macOS 磁盘映像。
|
||||
Dmg,
|
||||
/// Linux AppImage。
|
||||
AppImage,
|
||||
/// Debian 包。
|
||||
Deb,
|
||||
/// RPM 包。
|
||||
Rpm,
|
||||
/// 便携 zip(解压即用)。
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user