BongoCat Windows Per-User 安装器设计:权限面、卸载语义与数据隔离边界

原创2026-09-30 21:18:59310 阅读
文章标签:桌面应用

BongoCat Windows Per-User 安装器设计:权限面、卸载语义与数据隔离边界

本文围绕仓库决策记录 ADR-0023 展开,解读 BongoCat(跨平台桌面宠物应用)在 Windows 首发时为何选择 per-user NSIS 安装器、如何界定安装器权限面、卸载语义与用户数据隔离红线,并结合当前 cargo-packager 打包实现 与后续 ADR 的演进,给出源码级佐证。读完你可以掌握一套"低权限、高隔离、职责单一"的 Windows 桌面应用安装方案的设计脉络,以及它在真实仓库中的落地方式与验证门禁。

一、这份 ADR 的状态与演进:哪些决策仍然有效

ADR-0023《Windows Per-User Installer》于 2026-09-05 被接受,是 BongoCat 关于 Windows 安装格式的第一份正式决策。它在 2026-09-14 被 ADR-0033《Build, Packaging and Release Toolchain》 部分取代,因此阅读时务必区分两层:

仍然有效的决策(本文的核心):

  • per-user 安装:安装器以当前用户身份安装,不请求管理员权限;
  • 权限面:不安装 service、driver 或机器级注册项,卸载元数据写在 HKCU;
  • 卸载语义:默认卸载只删除 product files,删除用户数据必须是独立且明确的用户操作;
  • 数据隔离:安装、升级、卸载都不得读取、导入、迁移或删除任何用户数据。

已被取代、不再据此实现的部分:

  • 自维护 NSIS 脚本、固定官方 nsis-3.11-setup.exe 的 MD5、通过 BONGOCAT_NSIS_SETUP_PATH / BONGOCAT_MAKENSIS_PATH 注入本机 NSIS 路径——安装器改由 cargo-packager 的 NSIS 模板与 install-mode 配置生成;
  • "x86 不构建或发布,ARM64 在 Cubism desktop Core 门禁通过前不生成可发布 artifact"——Windows 现在只有 x64(见 ADR-0010 与 ADR-0033);
  • "Windows packaging 仅接受已 Authenticode 签名的 payload"——新流水线在未配置签名身份时会生成未签名安装器并输出警告,签名验证降级为 release workflow 的显式门禁;
  • "未来 Rust update helper 只接收 ADR-0021 验证完成的本 target/arch artifact"——该句已随 ADR-0029 作废:不再有独立 Rust update helper,替换由 self_update 1.3.0 在进程内完成(其权限边界、原子性与失败恢复未经本项目威胁建模),当前更新信任模型又进一步由 ADR-0034 取代为 cargo-packager-updater + detached minisign 签名。

一句话概括:"per-user、低权限、数据不动、卸载只清产品文件"这一组语义自始至终成立,变的只是生成安装器的工具链。

二、背景:为什么 Windows 首发必须先钉死安装格式

ADR-0023 记录的背景非常简洁,但信息量很大。在写这份决策时,BongoCat 已经具备三块与安装格式强相关的基础设施:

  1. 当前用户 HKCU Run 启动项:应用通过注册表 HKCU Run 键实现开机自启,不触碰 HKLM 机器级启动项。对应实现见 crates/bongocat-platform/src/startup_item_native.rs,其中对 Windows 明确使用 WindowsEnableMode::CurrentUser。
  2. 环境隔离的数据根:Development 与 Production 两个构建环境各自使用独立数据目录(详见 ADR-0008)。
  3. 独立的签名更新 trust boundary:更新清单的验证体系由 ADR-0021 定义(该 ADR 后续被 ADR-0029、ADR-0034 取代)。

这三块基础设施决定了应用"安装后怎么运行、数据放哪里、更新怎么验证",但安装器本身的权限、卸载语义以及未来 update helper 的交接方式尚未固定。Windows 首发需要先确定安装格式,否则会出现:安装器提示提权(吓跑用户)、卸载时连带删掉用户数据(不可接受)、或者升级路径与既有数据布局冲突(无法修复)。

ADR-0023 正是为回答这三个问题而写的固定决策。

三、核心决策一:per-user NSIS 安装器与权限面

ADR-0023 的第一条决策是:

Windows 首发采用 per-user NSIS installer。每个受支持 target/arch 生成独立安装 artifact;当前只有 x86_64-pc-windows-msvc。

  • 每个 target/arch 独立安装包:安装、升级、卸载按 target 独立验证,互不干扰;
  • 只发布 x64:当时即只有 x86_64-pc-windows-msvc 一个 Windows target;ADR-0010 进一步明确 i686-pc-windows-msvc 与 aarch64-pc-windows-msvc 都不属于构建、CI、安装包、更新或测试目标,Windows ARM64 的原生构建因无法端到端验证被取消,ARM64 设备上的体验由 Windows 自身的 x64 仿真覆盖;
  • 以当前用户身份安装,不请求管理员权限:这是整个权限面的根基——没有 UAC 提权弹窗,安装即当前用户会话内完成;
  • 不安装 service、driver 或机器级注册项:安装器不做任何超出"用户级应用"的操作;
  • 卸载元数据写在 HKCU:卸载信息、注册项全部落在当前用户注册表 hive,卸载时不需要管理员权限即可移除自身。

源码落地:cargo-packager 的 NSISInstallerMode::CurrentUser

在 crates/bongocat-packaging/src/main.rs 中,Windows target 的 NSIS 配置明确写出:

if target == ReleaseTarget::WindowsX86_64 {
    let mut nsis = NsisConfig::new();
    // Per-user install: no administrator prompt, no machine-level registry keys.
    nsis.install_mode = NSISInstallerMode::CurrentUser;
    config.nsis = Some(nsis);
}

源码注释与 ADR 决策逐字呼应:"Per-user install: no administrator prompt, no machine-level registry keys"。NSISInstallerMode::CurrentUser 在 cargo-packager 中正是 per-user 安装模式,install-mode 配置由安装器模板消费。

同一文件中的 ReleaseTarget 枚举也印证了 target 收敛:

enum ReleaseTarget {
    MacosAarch64,
    MacosX86_64,
    WindowsX86_64,
}

Windows 侧只有一个 target,且 release_formats() 对 WindowsX86_64 只返回 PackageFormat::Nsis,即 Windows 的发布产物只有 NSIS 安装器一种。

安装目录的确定(ADR-0033 补充)

ADR-0033 在影响一节记录了 per-user 安装目录的最终形态:Windows 安装目录为 $LOCALAPPDATA\BongoCat(cargo-packager 的 currentUser 布局),注册表仍写在 HKCU。由于首个正式版本发布前不存在已安装实例,这一布局变化不需要任何迁移。对比早期自研脚本方案的 $LOCALAPPDATA\Programs\BongoCat,最终选择更简单、更贴近"当前用户私有安装"的语义。

四、核心决策二:升级语义——只在固定 product root 内替换

installer 升级只在固定 product root 内替换旧的 product files,避免遗留 binary。

这一条定义了升级的"可写范围":

  • 升级的写操作被严格限制在固定的 product root(即安装目录)内;
  • 只替换旧的 product files(可执行文件、资源等随应用发布的文件);
  • 目的:避免遗留 binary——旧版本的可执行文件不会被遗留在磁盘上,避免"新版已装、旧版文件仍在"导致的版本混乱或安全残留。

结合 per-user 语义,升级同样是当前用户身份完成,不需要提权。安装器在这条约束下是"自我替换型"工具:新安装包覆盖旧安装包,范围不出安装目录。

五、核心决策三:数据隔离红线——六类用户数据不可触碰

这是 ADR-0023 中语义最重的一条:

安装、升级和卸载不能读取、导入、迁移或删除 Development/Production 的 config、window state、models、backups、logs 或 updates 数据。

即安装器对以下六类数据一律不可读取、不可导入、不可迁移、不可删除:

数据类别 含义
config 应用配置(config.json)
window state 窗口状态(window-state.json)
models 用户模型
backups 备份
logs 日志
updates 更新相关数据

这一红线的存在是因为:BongoCat 的安装目录($LOCALAPPDATA\BongoCat)与数据目录(%APPDATA%\com.ayangweb.bongo-cat\...)在物理上是分离的——数据目录由应用在运行时维护,安装器无权也无需感知。安装、升级、卸载都只作用于产品文件,绝不允许安装器"顺手"清理或搬运任何用户数据。

数据根的来源:ADR-0008 的环境隔离

这六类数据的目录形状由 ADR-0008《Application Identity and Isolated Storage Environments》 定义。BongoCat 的 Bundle ID 固定为 com.ayangweb.bongo-cat,数据根按构建环境隔离:

平台 Development Production
Windows %APPDATA%\com.ayangweb.bongo-cat\development\ %APPDATA%\com.ayangweb.bongo-cat\production\
macOS ~/Library/Application Support/com.ayangweb.bongo-cat/development/ ~/Library/Application Support/com.ayangweb.bongo-cat/production/

两个环境使用相同的目录结构与 schema:

<data-root>/
  config.json
  window-state.json
  models/
  backups/
  logs/
  updates/
  locks/

注意:locks(锁文件)也属于环境私有数据,锁、单实例命名、诊断和更新 channel 都必须携带环境身份,任何环境都不得探测、读取或回退到另一个环境的目录。这正是 ADR-0023"安装器不得触碰 Development/Production 数据"得以成立的下游支撑——数据目录的边界在应用层就已经被编译期环境强隔离,安装器只要"什么都不做"即可守住红线。

六、核心决策四:卸载语义——默认只删产品文件

默认卸载只删除 product files;删除用户数据必须是独立且明确的用户操作。

卸载语义与升级语义同构:

  • 默认卸载动作 = 删除 product files:卸载器移除安装目录中的产品文件、HKCU 中的卸载元数据与启动项,到此为止;
  • 用户数据不随卸载消失:config、window state、models、backups、logs、updates 全部保留在数据目录中;
  • "删除用户数据"是一个独立且明确的用户操作:如果用户确实想彻底清除(例如删除数据根目录),这必须由用户显式执行,而不是卸载流程的一部分。

这一语义避免了桌面应用最常见的卸载陷阱:"卸载 = 全部清空"。BongoCat 的定位是用户可能只是暂时卸载、日后重装,模型与配置属于用户资产,不应被安装器越权处理。同时也避免了安装器误删共享目录或环境交叉目录(Development/Production)的隐患。

七、核心决策五:installer 的职责边界——纯粹的 packaging tool

ADR-0023 对安装器职责给出了一条明确边界:

installer 不联网、不解析 update manifest,也不自行选择 artifact。NSIS 只作为安装 packaging tool,不承载 BongoCat 业务逻辑。

这一条是"职责单一原则"在安装层的体现:

  • 不联网:安装器在安装过程中不发起任何网络请求,不存在"安装器被劫持/中间人"的网络面;
  • 不解析 update manifest:版本判断、更新可用性判断全部由应用进程内的更新组件完成,安装器不参与;
  • 不自行选择 artifact:用户下载哪个安装包,安装器就装哪个,不做"根据系统情况挑选正确版本"的智能逻辑;
  • NSIS 只作为 packaging tool:安装器本质是"解包 + 落盘 + 注册卸载信息"的搬运工,任何 BongoCat 业务逻辑(输入、渲染、更新、诊断)都不允许进入安装器。

这条边界与更新体系的演变直接相关。ADR-0021 曾设计"未来 Rust update helper 只接收 ADR-0021 验证完成的本 target/arch artifact",即更新 helper 负责验证、安装器只负责执行。该句已随 ADR-0029 作废——不再有独立 update helper,更新改为进程内完成;ADR-0034 进一步将更新信任模型定为 detached minisign 签名。当前 crates/bongocat-update/src/runtime/mod.rs 中,Windows 更新以 install_mode: Some(WindowsUpdateInstallMode::Quiet) 调用安装器——静默模式正好利用了 ADR-0023 的 per-user 语义:因为安装不需要管理员权限,进程内更新才能无 UAC 干扰地静默运行安装器,完成自我替换。

八、备选方案分析:MSIX 与 WiX/MSI 为何被拒

ADR-0023 记录了被否决的两个主流 Windows 安装方案,理由与 per-user 决策互为镜像:

方案 拒绝理由
MSIX 会把启动、activation 与更新交由 package model 管理,无法作为当前 HKCU Run + 进程内 self_update 路径的无附加重构替代。即 MSIX 的包管理模型与 BongoCat 既有的"用户级启动项 + 应用自管更新"架构不兼容,强行采用需要重构启动与更新两条链路
WiX / MSI 主要服务机器级部署,通常引入管理员权限与企业安装策略;首发不需要该权限面。cargo-packager 的 wix format 因此不进入发布矩阵

从这两条拒绝理由可以反推出 ADR-0023 的价值取向:安装方案必须与既有架构(HKCU Run、环境隔离数据根、进程内更新)最小冲突。MSIX 与 WiX 的"能力更强"恰好是它们的负担——它们把启动、更新、部署策略收编进包模型,破坏 BongoCat 已经建立的低权限、自管更新链路。

ADR-0033 对工具链的替代也遵循同一逻辑:cargo-dist 被拒是因为其产物模型没有 .app/.dmg;直接用 cargo-packager CLI 被拒是因为版本无法随仓库固定、弱类型配置削弱编译期检查;保留自研脚本被拒正是因为"依赖个人机器状态"的 Windows NSIS 注入。最终选择以库形式集成 cargo-packager =0.11.8,打包配置是仓库内 Rust 源码,编译期检查,Cargo.lock 精确固定版本。

九、验证清单与发布门禁:尚未取得的证据

ADR-0023 在"验证"一节明确列出了尚未完成的验证,并强调"这些证据尚未取得,不能宣称 Windows package 可发布"。这是这份 ADR 最严谨的部分——决策已定,但发布门禁是开放的:

后续 installer job 必须在干净 Windows 10 1903+ 与 Windows 11 用户 profile 验证安装、升级、卸载、签名、环境数据保留、x64 artifact 选择和失败 rollback。

具体验证维度:

  1. 干净 Windows 10 1903+ 与 Windows 11 用户 profile:在无预置环境的全新用户 profile 上执行安装,排除"恰好能用"的环境依赖;
  2. 安装:per-user 安装、无提权、安装目录落位正确;
  3. 升级:固定 product root 内替换,无遗留 binary;
  4. 卸载:只删 product files,HKCU 卸载元数据清除,用户数据完整保留;
  5. 签名:安装包签名验证(在签名配置存在的前提下);
  6. 环境数据保留:安装/升级/卸载前后,Development/Production 的六类数据均不被触碰;
  7. x64 artifact 选择:每个 target/arch 独立 artifact 的对应关系正确;
  8. 失败 rollback:安装失败时的回滚行为符合预期。

ADR-0033 的后续项也复述了同类门禁:"原生 Intel 机器与干净 Windows 10 1903+ / Windows 11 profile 上的安装、升级、卸载与回滚验证"——说明这条验证清单在工具链切换后依然有效,只是尚未在真实发布流程中跑通。此外 ADR-0033 明确记录了接受的残余风险:cargo-packager 的 NSIS plugin 获取链(NSIS-ApplicationID.zip 只下载、不校验)替代了 ADR-0023 原先要求的"官方 acquisition artifact + MD5 + makensis /VERSION"边界,这是明确接受的降级,不能声称等价;Windows 端到端验证只能在 Windows runner 上进行,当时未在本机执行。

十、总结:一套可复用的 Windows 桌面应用安装边界

ADR-0023 的价值不在于"用 NSIS"这个工具选择,而在于它钉死了四组跨工具链不变的语义:

  1. 权限最小化:per-user、无管理员、无 service/driver/机器级注册项,卸载元数据在 HKCU;
  2. 升级自包含:只在固定 product root 内替换产品文件,不遗留 binary;
  3. 数据绝对隔离:config、window state、models、backups、logs、updates 六类数据在安装/升级/卸载全过程中不可读、不可导入、不可迁移、不可删除;默认卸载只删产品文件,删用户数据必须是独立明确的用户操作;
  4. 安装器职责单一:不联网、不解析 update manifest、不自选 artifact,纯粹是 packaging tool。

这些语义在源码中可逐条验证:NSISInstallerMode::CurrentUser(crates/bongocat-packaging/src/main.rs)、WindowsEnableMode::CurrentUser(crates/bongocat-platform/src/startup_item_native.rs)、静默安装的 WindowsUpdateInstallMode::Quiet(crates/bongocat-update/src/runtime/mod.rs),以及 ADR-0008 定义的环境隔离数据根。对于任何需要"低权限、高隔离、可静默更新"的 Windows 桌面应用,这组边界都是一份可以直接借鉴的安装设计蓝本——同时也提醒:决策文档必须与验证证据分离,方案已定不代表可发布,干净环境下的安装、升级、卸载与回滚证据一天未取得,发布门禁就一天不关闭。

登录后查看全文
BongoCat