一篇搞懂所有安装包格式
概要:日常开发中安装软件,面对的不是一个「安装包」,而是十几种结构、权限模型、依赖模型、升级机制各不相同的分发格式。本文按平台逐一拆解各类安装包的二进制结构、依赖模型、权限要求、静默安装方式、升级与卸载机制、签名与校验链条,给出适用场景与已知陷阱,并以横向对比矩阵和选型决策树收尾。
内容范围:Windows(MSI / EXE / MSIX / MSU / 包管理器 / 便携包)→ macOS(app / dmg / pkg / 签名公证 / Homebrew)→ Linux 发行版原生包(deb / rpm / pacman / apk)→ Linux 发行版无关包(AppImage / Flatpak / Snap / Nix)→ 源码与自解压 → 跨平台与语言级分发(容器 / wheel / npm / jar)→ 对比矩阵 → 选型建议 → 受限环境下的处置。
适用读者:需要为自己的软件选择分发格式的开发者,以及需要在受控或离线环境中部署软件的运维与工程人员。
一、判别框架:安装包之间的差异集中在哪里
1.1 「安装」这个动作到底包含什么
把文件复制到磁盘只是安装的一小部分。一个完整的安装动作通常涉及以下步骤,而各类安装包的本质差异,就在于它们覆盖了其中哪几步、以及由谁来执行:
- 校验来源:签名 / 哈希 / 公证票据
- 解析依赖:需要哪些运行库、它们是否已存在
- 落盘:文件写到哪个前缀(prefix),是否可写用户目录即可
- 注册:注册表项、desktop 文件、服务单元、PATH、文件关联
- 执行脚本:建用户、建数据库、重建缓存、启动守护进程
- 记账:把「装了什么」写进某个数据库,供升级和卸载使用
- 升级路径:谁来发现新版本、如何替换、能否回滚
- 卸载:能否完全逆转第 3 到第 6 步
第 6 步「记账」是最容易被低估的一步。没有记账的安装方式(源码 make install、手动解压、拖拽复制)在卸载和升级时没有任何依据,这是后文反复出现的分界线。
1.2 三条决定性的分歧
所有格式的差异,基本可以归约到下面三条分歧上。确定了这三条,一个格式的其余特性大致就被决定了。
| 分歧 | 选项 A | 选项 B | 被它决定的后果 |
|---|---|---|---|
| 依赖从哪来 | 共享系统库 | 自带全部依赖 | 包体积、安全更新的扩散速度、跨发行版可移植性 |
| 装到哪个前缀 | 系统目录(需提权) | 用户目录(免提权) | 是否需要管理员、能否多用户共享、受限环境能否使用 |
| 运行时是否隔离 | 完全继承用户权限 | 沙箱 + 显式授权 | 卸载干净度、安全边界、与系统集成的难易 |
ℹ 第一条分歧有一个常被忽略的代价:自带依赖意味着安全更新责任转移给了应用作者。共享库模型下,OpenSSL 出一个漏洞,发行版更新一个包即修复全系统;自带依赖模型下,每个内嵌了该库的应用都必须各自重新打包发布,任何一个作者不跟进,漏洞就留在那台机器上。
二、Windows
2.1 格式总览
| 格式 | 容器结构 | 需管理员 | 典型静默命令 | 卸载记账位置 |
|---|---|---|---|---|
.msi | OLE 结构化存储,内含关系型表 | 通常需要 | msiexec /i x.msi /qn | Windows Installer 数据库 |
.exe(安装器) | 自解压载荷 + 脚本引擎 | 多数需要 | 不统一,见 2.3 | 注册表 Uninstall 项(如果作者写了) |
.msix / .appx | ZIP + AppxManifest.xml | 不需要 | Add-AppxPackage x.msix | 包管理器(强制) |
.msu / .cab | 系统更新载荷 | 需要 | DISM /Online /Add-Package | 组件存储(WinSxS) |
.nupkg(Chocolatey) | NuGet ZIP + PowerShell 脚本 | 需要 | choco install x -y | Chocolatey 自有目录 + 底层安装器 |
| Scoop manifest | JSON 描述 + 解压到用户目录 | 不需要 | scoop install x | Scoop 自有目录 |
| 便携 ZIP | 纯压缩包 | 不需要 | Expand-Archive | 无 |
2.2 MSI:事务性安装的唯一选项
.msi 文件不是压缩包,而是一个 OLE 结构化存储(COM Compound File)容器,内部存放着上百张关系型表(File、Component、Registry、CustomAction、InstallExecuteSequence 等)。安装过程由系统组件 msiexec.exe 解释这些表来驱动,而不是由包自己执行代码。这一点带来了 MSI 与 .exe 安装器之间最本质的区别:
| 特性 | 说明 |
|---|---|
| 事务性与回滚 | 执行阶段写入回滚脚本,安装失败自动还原到安装前状态。这是 MSI 最核心的价值,.exe 安装器基本不具备 |
| 标准化的标识 | ProductCode(GUID)唯一标识一个产品,UpgradeCode 标识产品系列,卸载可用 msiexec /x {GUID} /qn,无需知道卸载程序在哪 |
| 可定制而不改包 | 通过转换文件 .mst 叠加定制(改默认路径、禁用某功能、预置许可证),原始包的签名保持有效 |
| 原生支持集中部署 | 组策略(GPO)软件分发只认 MSI;SCCM、Intune 对 MSI 的检测规则和返回码处理也最成熟 |
| 不解析依赖 | MSI 没有包管理器概念。前置条件只能写成 LaunchCondition,不满足就报错退出,不会自动去装 |
常用命令:
# 完全静默安装,带详细日志(排错时 /l*v 几乎是必须的)
msiexec /i app.msi /qn /norestart /l*v install.log
# 指定安装目录(属性名由包作者定义,INSTALLDIR / APPDIR / TARGETDIR 都见过)
msiexec /i app.msi /qn INSTALLDIR="D:\Apps\App"
# 叠加转换文件
msiexec /i app.msi TRANSFORMS=custom.mst /qn
# 按 ProductCode 卸载
msiexec /x {12345678-1234-1234-1234-123456789012} /qn
# 只解包不安装,用于审查内容
msiexec /a app.msi /qb TARGETDIR=C:\extract
⚠ MSI 不能嵌套安装。Windows Installer 用一个名为
_MSIExecute的全局互斥量保证同一时刻只有一个 MSI 处于执行阶段,并发时报错 1500(「另一个安装程序正在进行」)。这就是为什么「带运行库的安装包」必须做成一个.exe引导程序(bootstrapper,如 WiX Burn)去串行调用多个 MSI —— 而不是做成一个 MSI 内部调用另一个 MSI。
2.3 EXE 安装器:静默参数取决于制作工具
setup.exe 不是一种格式,而是「一个可执行文件,里面塞了载荷和一套安装逻辑」的统称。静默参数由制作工具决定,没有统一标准,这是实践中最常见的信息缺口。下表覆盖了绝大多数情况:
| 制作工具 | 识别特征 | 静默命令 |
|---|---|---|
| NSIS | 体积小,经典灰色向导界面 | /S /D=C:\path |
| Inno Setup | 向导带蓝色标题栏与图标 | /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /DIR="C:\path" |
| InstallShield(MSI 内核) | 步骤多,常有功能选择树 | /s /v"/qn" |
| InstallShield(旧 InstallScript) | 同上,无内嵌 MSI | 先 /r 录制应答文件,再 /s /f1"C:\setup.iss" |
| WiX Burn | 外层 exe,内含一个或多个 MSI | /quiet /install /norestart /log x.log |
| MSI 包装器 | 本质是自解压,内部就是 msi | /extract 或 /x 解包后直接用 msiexec |
| Squirrel(Electron 应用) | 安装到用户目录,无 UAC | --silent |
如何判断一个未知 setup.exe 属于哪种:用归档工具列出其内部结构,按特征文件判断。
7z l setup.exe
# 内部出现 .nsi 脚本或 $PLUGINSDIR -> NSIS
# 内部出现 {app}、script.bin、embedded -> Inno Setup
# 内部出现 setup.inx、data1.cab -> InstallShield
# 内部直接出现 .msi 文件 -> 包装器或 Burn,优先走 msiexec
# 内部出现 .nupkg、Update.exe -> Squirrel
◆ NSIS 的
/D=有两条硬性规则,踩错会静默装到默认目录:必须是最后一个参数,且路径不能加引号(含空格的路径也不加)。正确写法是setup.exe /S /D=C:\Program Files\App。
2.4 MSIX / APPX:容器化的现代格式
.msix 是一个 ZIP 容器,内含 AppxManifest.xml(声明身份、能力、入口点)、AppxBlockMap.xml(分块哈希,支撑差分更新与完整性校验)和 AppxSignature.p7x(签名)。它与前两种格式的根本差别是运行时虚拟化:
- 文件系统与注册表重定向:应用以为自己在写安装目录和
HKLM,实际写入的是属于该包的私有容器。因此卸载可以做到完全干净,不留注册表孤项和残留目录。 - 声明式能力:清单里声明需要哪些能力(网络、文件系统位置、设备),系统据此授予,而不是默认全开。
- 强制签名:未签名的包无法安装。企业自签名需要把签发证书导入「受信任的人」或「受信任的根证书颁发机构」存储。
- 衍生格式:
.msixbundle打包多架构(x86/x64/ARM64);.appinstaller是一个 XML 更新清单,让应用具备自动检查更新的能力。
适用场景:面向终端用户的现代桌面应用分发、Microsoft Store 上架、以及 Azure Virtual Desktop 的 MSIX App Attach(镜像按需挂载,不在镜像里烧进应用)。
⚠ MSIX 的虚拟化同时是它的限制:内核驱动、系统服务装不了;传统的全局 Shell 扩展必须改写成清单声明的包扩展,部分能力至今不支持。此外,老程序迁移到 MSIX 最常卡住的点是「往自己安装目录写配置文件」这个习惯 —— 在容器里它会被重定向,多用户或升级后行为与预期不同,应改为写入
LocalAppData。
2.5 MSU 与 CAB:系统更新载荷
.msu 是 Windows 更新的独立安装包,.cab 是其内部的组件载荷。它们由组件化服务栈(CBS)处理,写入组件存储 WinSxS,而非普通的程序安装路径。
# 在线安装到当前系统
DISM /Online /Add-Package /PackagePath:C:\update.msu /NoRestart
# PowerShell 等价写法
Add-WindowsPackage -Online -PackagePath C:\update.msu -NoRestart
# 注入到离线镜像(装机镜像集成补丁的标准做法)
DISM /Image:C:\mount /Add-Package /PackagePath:C:\update.msu
注意历史变化:早期用 wusa.exe /quiet 安装 .msu,但该用法自 Windows 10 起已被标记为弃用,官方建议改用 DISM 或 Add-WindowsPackage。
2.6 三个包管理器的本质区别
winget、Chocolatey、Scoop 经常被并列提及,但三者的工作原理完全不同,这直接决定了它们各自的能力边界。
| winget | Chocolatey | Scoop | |
|---|---|---|---|
| 包里装的是什么 | YAML 元数据:原厂安装包的 URL + SHA256 | .nupkg 内含 PowerShell 安装脚本 | JSON 清单:下载地址 + 解压与链接规则 |
| 实际安装动作 | 下载原厂包,按其静默参数调用 | 脚本化地「自动点下一步」 | 解压到用户目录,生成 shim |
| 安装位置 | 由原厂包决定 | C:\ProgramData\chocolatey\lib + 原厂包位置 | ~\scoop\apps\<名>\<版本> |
| 需要管理员 | 需要(因为底层仍是原厂安装器) | 需要 | 不需要(全局安装 -g 才需要) |
| 是否写注册表 | 是 | 是 | 否 |
| 多版本共存 | 否 | 有限 | 是,current 联接点切换 |
| 定位 | 系统自带,覆盖面广,官方源可信 | 企业场景成熟,含服务端软件 | 开发者 CLI 工具,受限环境友好 |
ℹ 常见误解:winget 不能消除 UAC 提示。它只是帮你找到并校验原厂安装包,再按该包的静默参数调用;原厂包要提权,winget 就会提权。真正能免管理员的是 Scoop 这种「解压到用户目录」的模型。
2.7 便携 ZIP:最被低估的格式
纯压缩包解压即用,不写注册表、不需提权、删除目录即卸载。代价是没有任何记账:不进开始菜单、不建文件关联、不自动更新,PATH 要自己配。
很多项目在发布页上同时提供 *-setup.exe 和 *-win-x64.zip,后者往往排在列表下方容易被忽略。在受限环境中,便携 ZIP 常常是唯一可行的选项(见第十节)。
三、macOS
3.1 四种实体的层级关系
macOS 这边最容易混淆的是「哪个才是安装包」。四者关系如下:
.dmg 运输容器(磁盘镜像) ──┐
.zip 运输容器(压缩包) ──┼──> 里面装着 .app,拖到 /Applications 即完成安装
│
.app 目录形式的程序包(bundle)─┘ 它本身就是安装单位,不需要安装器
.pkg 真正的安装器:有载荷、有脚本、有事务,由 installer(8) 执行
.mpkg 多个 .pkg 组成的分发包
3.2 .app bundle
.app 是一个带特定内部结构的目录,Finder 把它显示为单个文件:
App.app/
└── Contents/
├── Info.plist 标识、版本、入口、权限声明
├── MacOS/App 实际的可执行文件
├── Resources/ 图标、本地化、资源
├── Frameworks/ 自带的动态库(自带依赖模型)
└── _CodeSignature/ 签名摘要
由于依赖都放在 Frameworks/ 里自带,.app 天然是「自带依赖 + 免提权 + 删除即卸载」的组合 —— 这是 macOS 桌面应用分发体验较好的结构性原因。残留通常只有 ~/Library 下的配置与缓存。
3.3 .dmg
磁盘镜像,UDIF 格式。常见压缩变体:UDZO(zlib,兼容性最好)、ULFO(LZFSE,macOS 10.11+)、ULMO(LZMA,10.15+,压缩率最高)。
# 命令行挂载 / 复制 / 卸载(脚本化安装的标准三步)
hdiutil attach App.dmg -nobrowse -quiet
cp -R /Volumes/App/App.app /Applications/
hdiutil detach /Volumes/App -quiet
# 查看镜像信息
hdiutil imageinfo App.dmg
要点:.dmg 本身不执行任何安装逻辑,它只是运输容器。「安装」就是把里面的 .app 复制出来,不需要 root。
3.4 .pkg
扁平包(flat package)是一个 xar 归档,内部结构:
| 成员 | 作用 |
|---|---|
Distribution | XML:安装界面、选项、JavaScript 形式的安装条件检查 |
PackageInfo | 标识符、版本、安装目标前缀 |
Payload | 实际文件,cpio 归档后 gzip 压缩 |
Scripts | preinstall / postinstall 脚本 |
Bom | 物料清单,记录每个文件的路径与权限,是卸载和校验的依据 |
什么时候才该用 .pkg(而不是 dmg 拖拽):需要写入 /Library、安装 LaunchDaemon 或 LaunchAgent、安装系统扩展或驱动、安装命令行工具到 /usr/local/bin、或者必须执行安装脚本。
# 命令行静默安装
sudo installer -pkg App.pkg -target /
# 安装前审查:解包看脚本和载荷清单(强烈建议对来源不明的 pkg 执行)
pkgutil --expand App.pkg /tmp/App-expanded
pkgutil --payload-files App.pkg
# 查询已装包与其文件清单
pkgutil --pkgs
pkgutil --files com.vendor.app
# 注销记账(注意:这只删记录,不删文件)
sudo pkgutil --forget com.vendor.app
⚠
.pkg的preinstall/postinstall脚本以 root 身份执行任意代码。这是 macOS 上最值得警惕的分发形式 —— 一个来源不明的.pkg等同于交出 root。审查方式见上面的pkgutil --expand。
3.5 签名、公证与 Gatekeeper:三级链条
这是 macOS 分发中最常出问题的环节,原因是三个机制层层叠加,而失败时的报错具有误导性。
| 层级 | 机制 | 工具 | 缺失时的表现 |
|---|---|---|---|
| 1 | 代码签名:用 Developer ID 证书对 app 或 pkg 签名 | codesign / productsign | 提示「来自身份不明的开发者」 |
| 2 | 公证:上传给 Apple 做自动恶意代码扫描,换回票据并钉入包内 | notarytool + stapler | 提示「已损坏,无法打开」 |
| 3 | Gatekeeper 校验:首次打开时结合隔离标记 com.apple.quarantine 验证前两层 | 系统自动 | 直接拒绝启动 |
⚠ 「App 已损坏,无法打开,你应该将它移到废纸篓」这句报错几乎从不代表文件真的损坏。它在绝大多数情况下意味着「带着隔离标记、且公证校验未通过」。因为文案完全没提签名或公证,这个错误每年都在误导大量用户去重新下载。
# 诊断:这个 app 签名和公证状态如何
codesign -dv --verbose=4 /Applications/App.app
spctl -a -vvv /Applications/App.app
xattr -l /Applications/App.app # 看有没有 com.apple.quarantine
# 分发方:公证流程
xcrun notarytool submit App.dmg --apple-id ... --team-id ... --wait
xcrun stapler staple App.dmg
xcrun stapler validate App.dmg
# 使用方应急(自行承担风险,仅用于确认可信的自编译产物)
xattr -dr com.apple.quarantine /Applications/App.app
另需注意行为变化:较新的 macOS 版本(Sequoia 起)取消了「右键打开」绕过 Gatekeeper 的快捷方式,必须到「系统设置 → 隐私与安全性」中逐个放行。编写面向用户的安装文档时,旧的「右键打开」指引已不再适用。
3.6 Homebrew:formula 与 cask
| formula | cask | |
|---|---|---|
| 目标 | 命令行工具与库 | 图形界面应用 |
| 实际动作 | 优先下载预编译二进制(bottle),否则本地编译 | 下载官方 dmg/pkg,代为挂载与搬运 |
| 安装前缀 | /opt/homebrew(Apple Silicon)、/usr/local(Intel) | /Applications 等标准位置 |
| 依赖解析 | 有,Homebrew 自己的依赖图 | 基本没有(由应用自带) |
| 彻底清理 | brew uninstall | brew uninstall --zap(连带清理配置残留) |
Homebrew 的价值在于把「下载 dmg、挂载、拖拽、卸载时手动清理」变成了有记账、可批量、可脚本化的操作,这是 macOS 上实现「环境即代码」的主要途径(配合 brew bundle dump 生成 Brewfile)。
3.7 Mac App Store
强制应用沙箱(entitlements 声明权限)、强制 receipt 校验、强制 Apple 审核。换来自动更新、家庭共享与统一退款渠道,代价是权限受限最严 —— 许多开发者工具因无法在沙箱内工作而不上架,或上架一个功能缩减版。
四、Linux:发行版原生包(共享库模型)
4.1 四种原生格式
| 格式 | 容器结构 | 低层工具 | 高层工具 | 依赖解析 |
|---|---|---|---|---|
.deb | ar 归档:debian-binary + control.tar.* + data.tar.* | dpkg | apt | 低层不解,高层解 |
.rpm | 签名区 + 二进制头 + cpio 载荷(现代发行版用 zstd 压缩) | rpm | dnf / zypper | 低层不解,高层解 |
.pkg.tar.zst | zstd 压缩 tar + .PKGINFO | pacman -U | pacman / AUR 助手 | 解 |
.apk | 三段串接的 gzip 流(签名 / 控制 / 数据) | apk | apk | 解 |
4.2 共享库模型的收益与代价
| 说明 | |
|---|---|
| 收益:体积 | 只打包自己的文件,依赖动态链接系统库。同一个应用的 deb 常是其 AppImage 的十分之一甚至更小 |
| 收益:安全更新扩散快 | 一个库出漏洞,发行版更新该库的包,全系统一次性修复。这是原生包不可替代的优势 |
| 收益:系统集成完整 | 能注册 systemd 服务、建系统用户、装内核模块、改 PAM —— 自带依赖类格式基本都做不到 |
| 代价:与发行版版本绑定 | 为 Ubuntu 24.04 构建的 deb 在 20.04 上常因 glibc 或库版本不足而无法安装或无法启动。「Linux 版」必须细分到发行版和版本 |
| 代价:难以多版本共存 | 同一个包的两个版本通常无法同时安装(除了专门设计的 -compat 系列包) |
4.3 维护者脚本:能力与风险的来源
原生包能做系统集成,靠的是打包者编写的维护者脚本。它们以 root 执行,是「建用户、注册服务、重建缓存」的实现位置,同时也是「卸载不干净」和「升级炸掉配置」的主要来源。
Debian 系 RPM 系 执行时机
preinst %pre 解包前
postinst %post 解包后(注册服务、重建缓存通常在这里)
prerm %preun 删除前(停服务)
postrm %postun 删除后(清理残留)
-- %posttrans 整个事务结束后
相关的卸载语义差异值得记住:Debian 系的 apt remove 保留配置文件,要连配置一起删需用 apt purge。这个区别在「重装以恢复默认配置」的场景下经常让人困惑 —— 单纯 remove 再 install,旧配置还在。
4.4 单包安装 vs 配置仓库
这是实践中一个重要但常被跳过的区分:
| 手动装单个包 | 配置厂商仓库 | |
|---|---|---|
| 命令 | dpkg -i app.deb / rpm -i app.rpm | 加 /etc/apt/sources.list.d/ 或 /etc/yum.repos.d/ 条目 + 导入 GPG 公钥 |
| 依赖 | 不自动解析(dpkg -i 后常需 apt-get -f install 补救) | 自动解析 |
| 后续安全更新 | 不会自动升级 | 随 apt upgrade 一起升级 |
| 适用 | 一次性、离线、内部构建产物 | 长期使用的厂商软件 |
◆ 手动装单包时,优先用高层工具带路径的形式:
apt install ./app.deb或dnf install ./app.rpm。它们会解析依赖,避免dpkg -i留下半装状态。
4.5 Alpine 与 musl:容器场景的专有陷阱
Alpine 以体积小成为常见的容器基础镜像,但它用 musl libc 而非 glibc。这不是「小一点的 glibc」,而是不同实现,后果具体而频繁:
- Python 的
manylinux预编译 wheel 在 Alpine 上不可用,需要musllinux标签的 wheel(PEP 656),否则 pip 退回源码分发并现场编译,于是需要装编译工具链,镜像反而变大。 - Node.js 原生模块、Go 启用 CGO 的构建、各类预编译二进制,都可能需要针对 musl 重新编译。
- 一些 DNS 解析与本地化行为与 glibc 有差异,曾造成过难以定位的线上问题。
结论:Alpine 适合「纯静态二进制 + 极小镜像」的场景;若镜像里跑的是 Python、Node 这类依赖大量预编译二进制的运行时,debian-slim 往往总体更小更省事。
五、Linux:发行版无关包(自带依赖模型)
5.1 四者总览
| AppImage | Flatpak | Snap | Nix | |
|---|---|---|---|---|
| 本质 | ELF 运行时头 + SquashFS,单文件 | OSTree 对象库 + 分层 runtime | SquashFS,由 snapd 挂载 | /nix/store/<哈希>-名-版本 |
| 沙箱 | 无 | 有(bubblewrap + portals) | 有(AppArmor) | 无(但路径隔离) |
| 需 root | 不需要 | 不需要(--user) | 需要(装 snapd) | 不需要(store 已建好后) |
| 升级 | 外挂(zsync),实际多为手动 | flatpak update | 强制自动 | 原子切换 generation |
| 回滚 | 换回旧文件 | OSTree 历史 | snap revert(保留前一版本) | 瞬时,切回上一代 |
| 擅长 | 离线分发、免安装试用 | 桌面 GUI 应用 | 服务端守护进程 + CLI + GUI | 可重现的开发环境 |
5.2 AppImage
最接近 Windows「绿色版」的形态:一个文件,chmod +x 后直接运行,不写系统目录,删掉文件即卸载。文件结构是「一小段 ELF 运行时 + 一个 SquashFS 镜像」,运行时负责挂载镜像并执行里面的入口程序。
chmod +x App-x86_64.AppImage
./App-x86_64.AppImage
# 无 FUSE 环境(容器、精简系统)下的运行方式
./App-x86_64.AppImage --appimage-extract-and-run
# 解包审查内容
./App-x86_64.AppImage --appimage-extract
三个已知陷阱:
- FUSE 版本:较老的 AppImage 运行时依赖 libfuse2,而 Ubuntu 22.04 及更新版本默认只装 fuse3,于是直接运行会报错退不出来。解决办法是装
libfuse2,或用上面的--appimage-extract-and-run。 - 必须在足够老的系统上构建:glibc 是向后兼容的(新 glibc 能跑旧程序,反之不行),所以要让 AppImage 到处能跑,必须在较老的发行版上构建。这是分发方最常犯的错 —— 在最新系统上构建,结果只有最新系统能用。
- 没有沙箱:AppImage 以完整的用户权限运行,能读写你的全部家目录。它解决的是依赖可移植性,不是安全隔离。
此外它不自动集成桌面菜单和文件关联(需借助 AppImageLauncher 之类的工具),自动更新也要靠分发方额外接入 zsync。
5.3 Flatpak
设计上与 AppImage 相反:它不追求单文件,而是用分层 runtime + 内容去重来控制总体积,并把沙箱作为一等公民。
- runtime 分层:应用声明依赖某个 runtime(
org.freedesktop.Platform、org.gnome.Platform、org.kde.Platform),多个应用共享同一份。 - OSTree 存储:内容寻址 + 块级去重,更新走增量,并且天然带版本历史,可回滚。
- 沙箱与开孔:运行在 bubblewrap 命名空间里,访问文件、摄像头、打印机等需经 XDG Desktop Portal 由用户交互授权。权限可事后调整。
flatpak install --user flathub org.example.App
flatpak run org.example.App
# 查看与调整某应用的权限
flatpak info --show-permissions org.example.App
flatpak override --user --filesystem=~/projects org.example.App
# 清理不再被引用的 runtime
flatpak uninstall --unused
⚠
flatpak override --filesystem=host是网上最常见的「修复 Flatpak 应用看不到我的文件」的答案。它确实有效,但代价是把沙箱的主要价值直接放弃。需要访问项目目录时,应按目录精确开孔(如上例的--filesystem=~/projects),而不是整机放行。
主要代价:首次安装会连带拉取几百 MB 的 runtime(之后其他应用复用),以及对系统主题、字体、输入法的集成偶有不一致。
5.4 Snap
与 Flatpak 的核心差异不在打包技术,而在覆盖范围与治理模型:
| 优势 | 代价 |
|---|---|
| 覆盖面更广:除桌面应用外,还能分发服务端守护进程和 CLI 工具(Flatpak 基本只面向桌面 GUI) | 首次启动慢:SquashFS 需挂载解压,冷启动延迟明显 |
channel 机制完善:stable / candidate / beta / edge 四档风险等级 + 版本 track | /snap 下产生大量 loop 设备,df 和 mount 输出变得嘈杂 |
revision 回滚:保留前一版本,snap revert 一条命令退回 | 后端商店只有 Canonical 一家,不像 Flatpak 可自建远程仓库 —— 这是社区争议的主要来源 |
| 自动更新保证了安全补丁及时到位 | 自动更新难以彻底关闭:历史上只能按 snap 或全局延期(全局上限 90 天),较新的 snapd 才支持无限期 hold |
5.5 Nix:把安装变成纯函数
Nix 的做法与以上全部不同:每个包按构建输入的哈希寻址,安装到 /nix/store/<哈希>-名-版本,store 内容只读不可变。于是:
- 同一软件的多个版本天然共存,互不干扰,因为路径里带哈希。
- 安装与升级是原子的:切换 profile 符号链接即生效,中途断电不会留下半装状态。
- 回滚是瞬时的:切回上一个 generation,旧的 store 路径还在。
- 构建可重现:配合 flake 的 lock 文件,可以把整套工具链版本固定下来,别人拿到仓库能得到逐位一致的环境。
# 临时进入一个带指定工具的 shell,退出即消失,不污染系统
nix shell nixpkgs#jq nixpkgs#ripgrep
# 按项目目录声明的环境进入(flake.nix + flake.lock 锁定全部版本)
nix develop
# 回滚到上一代
nix profile rollback
代价:概念负担重(派生、固定输出、覆盖机制),错误信息晦涩,学习曲线陡峭;store 会持续增长,需定期 nix-collect-garbage。适合「开发环境即代码」和需要严格可重现构建的团队,不适合临时装一个软件。
六、源码与自解压
6.1 源码 tarball
./configure && make && sudo make install 是最后手段。关键问题是它不留记账:装了哪些文件没有记录,卸载只能靠 make uninstall(如果作者写了)或手工删除。两种补救方式:
# 方式一:checkinstall 把 make install 的结果打成真正的 deb/rpm,由包管理器记账
sudo checkinstall --pkgname=myapp --pkgversion=1.0 make install
# 方式二:装到独立前缀 + GNU Stow 用符号链接统一管理,卸载即 stow -D
./configure --prefix=/usr/local/stow/myapp-1.0
make && sudo make install
cd /usr/local/stow && sudo stow myapp-1.0
6.2 .run / .bin(makeself 自解压)
本质是「一段 shell 脚本 + 尾部附加的压缩载荷」,脚本读取自身尾部数据解压后执行安装逻辑。NVIDIA 驱动、各类旧商业软件常用此形式。
# 直接执行
chmod +x installer.run && ./installer.run
# 只解包不执行,用于审查里面到底要跑什么(强烈建议)
./installer.run --noexec --keep --target /tmp/inspect
6.3 静态单文件二进制:最省事的「安装包」
Go 和 Rust 的普及让这种形态成为 CLI 工具的事实标准:一个文件,复制到 PATH 里的任意目录即可用。没有依赖、没有 glibc 版本问题、交叉编译即完成多平台分发、卸载就是删文件。
ℹ 两点需要精确:Go 并非总是静态链接 —— 启用 CGO 时(某些
net、os/user场景)会动态链接 libc,需要CGO_ENABLED=0才保证全静态。Rust 默认目标也不是静态 ——x86_64-unknown-linux-gnu动态链接 glibc,要全静态需改用x86_64-unknown-linux-musl目标。
七、跨平台与语言级分发
7.1 容器镜像(OCI)
容器镜像把问题换了个层次:不再分发「软件」,而是分发整个用户态文件系统。镜像由多个内容寻址的层(tar 加压缩)加一份 manifest 与 config 组成,层可在镜像间复用。
依赖冲突由此彻底消失,代价是镜像体积、运行时开销,以及需要一套编排来管理生命周期。对服务端软件,它已基本取代了「发一个 deb/rpm」;对桌面和 CLI 工具则并不合适。
7.2 Python:wheel 与 sdist
.whl(wheel) | .tar.gz(sdist) | |
|---|---|---|
| 内容 | 预编译产物,ZIP 格式 | 源码 |
| 安装 | 解压即装,秒级 | 本机编译,需完整工具链 |
| 平台标签 | manylinux_2_17_x86_64 / musllinux_1_2_x86_64 / macosx_11_0_arm64 / win_amd64 / py3-none-any | 无 |
最常见的 pip 报错根因:没有匹配当前平台标签的 wheel,于是 pip 自动退回 sdist 并尝试现场编译,接着报出一堆「找不到头文件」「找不到编译器」。此时真正的问题不是缺某个头文件,而是平台标签不匹配。典型触发条件:Alpine 容器(需 musllinux 而非 manylinux)、刚发布的新 Python 版本(上游还没发对应 wheel)、以及 Windows 上缺少 MSVC 构建工具。
# 诊断:当前环境识别出的平台标签是什么
python -c "import sysconfig; print(sysconfig.get_platform())"
pip debug --verbose | head -40
# 强制只用 wheel,装不上就直接报错而不是转去编译(排错时非常有用)
pip install --only-binary :all: somepackage
7.3 其他语言生态
| 格式 | 结构 | 要点 |
|---|---|---|
| npm tarball | tar.gz + package.json | postinstall 脚本可执行任意代码,是供应链攻击的集中入口。CI 中用 npm ci --ignore-scripts 可降低风险 |
.jar / .war | ZIP + META-INF/MANIFEST.MF | 依赖 JRE;fat jar(shaded jar)把依赖打进同一文件,是 Java 版的「自带依赖」 |
.vsix | ZIP + 清单 | VS Code 扩展,支持离线安装:code --install-extension x.vsix |
.nupkg | ZIP + .nuspec | .NET 包,与 Chocolatey 复用同一容器格式但用途不同 |
.crate | tar.gz | Rust 源码包,总是本地编译,所以没有平台标签问题 |
八、横向对比矩阵
| 维度 | msi | exe | msix | dmg | pkg | deb/rpm | AppImage | Flatpak | Snap | 容器 |
|---|---|---|---|---|---|---|---|---|---|---|
| 需管理员 | 是 | 多数是 | 否 | 否 | 是 | 是 | 否 | 否 | 是 | 是 |
| 自带依赖 | 否 | 部分 | 是 | 是 | 是 | 否 | 是 | 共享 runtime | 是 | 是 |
| 沙箱隔离 | 无 | 无 | 强 | 无 | 无 | 无 | 无 | 强 | 强 | 强 |
| 卸载干净度 | 中 | 差 | 优 | 优 | 中 | 中 | 优 | 优 | 优 | 优 |
| 体积 | 小 | 小 | 中 | 中 | 中 | 最小 | 大 | 首次大,后续小 | 大 | 最大 |
| 自动更新 | 依赖外部 | 各异 | 内建 | 依赖外部 | 依赖外部 | 随仓库 | 弱 | 内建 | 强制 | 编排层负责 |
| 回滚能力 | 事务回滚 | 无 | 版本切换 | 无 | 无 | 降级安装 | 换文件 | OSTree 历史 | revision | 镜像 tag |
| 企业集中部署 | 最佳 | 差 | 好 | 中 | 好 | 好 | 中 | 中 | 中 | 好 |
| 离线分发 | 好 | 好 | 好 | 好 | 好 | 好 | 最佳 | 差 | 差 | 中 |
| 系统级集成 | 强 | 强 | 受限 | 弱 | 强 | 最强 | 弱 | 受限 | 受限 | 不适用 |
九、选型建议
9.1 作为分发方
| 目标场景 | 推荐 | 理由 |
|---|---|---|
| Windows 企业客户 | .msi(多组件时用 WiX Burn 包一层) | GPO/SCCM/Intune 只对 MSI 有完整支持,且有事务回滚 |
| Windows 消费级 | .msix,或 Inno Setup 生成的 .exe | 前者卸载干净、能自动更新;后者兼容性与定制自由度高 |
| Windows 开发者工具 | 额外提供便携 ZIP + winget/Scoop 清单 | 覆盖受限环境与自动化安装场景,成本极低 |
| macOS 图形应用 | .dmg + Developer ID 签名 + 公证 | 缺公证会得到「已损坏」的误导性报错,直接造成用户流失 |
| macOS 含服务或系统位置 | .pkg | 需要安装脚本和特权位置写入时的唯一选择 |
| Linux 桌面应用 | Flatpak(上 Flathub)+ AppImage 兜底 | Flatpak 负责日常与更新,AppImage 覆盖离线与非主流发行版 |
| Linux 服务端软件 | 官方 apt/yum 仓库,或容器镜像 | 仓库保证安全更新能到位,单个 deb/rpm 不会自动升级 |
| 跨平台 CLI 工具 | 静态单文件二进制 + Homebrew formula + Scoop manifest | 一份产物覆盖绝大多数需求,维护成本最低 |
9.2 作为使用方
有管理员权限?
├── 否 ──> 便携 ZIP / Scoop / AppImage / 静态二进制 / Flatpak --user
│
└── 是 ──> 需要多版本共存?
├── 是 ──> Nix、asdf/mise、容器
│
└── 否 ──> 只是临时试用一下?
├── 是 ──> AppImage、docker run --rm、nix shell
│
└── 否 ──> 该平台的包管理器
(winget/Scoop、brew、apt/dnf/pacman)
—— 因为它有记账,升级和卸载才有依据
一条贯穿性的原则:优先选有记账的安装方式。手动解压和源码编译在当下看起来最快,但半年后「这个东西当初装到哪了、还能不能升级、怎么卸干净」的成本会远超当初省下的时间。
9.3 安装前的审查清单
对来源不完全可信的安装包,以下操作都是只读的,成本很低:
| 格式 | 审查命令 |
|---|---|
.msi | msiexec /a app.msi /qb TARGETDIR=C:\extract(管理员安装,只解包) |
setup.exe | 7z l setup.exe 看内部结构与制作工具 |
.pkg | pkgutil --expand 后阅读 Scripts/ 下的内容(它们以 root 运行) |
.deb | dpkg-deb -c app.deb 看文件清单,dpkg-deb -I 看控制信息与维护者脚本 |
.rpm | rpm -qlp app.rpm 看文件,rpm -q --scripts -p app.rpm 看脚本 |
.run | ./installer.run --noexec --keep --target /tmp/x |
| AppImage | ./App.AppImage --appimage-extract |
| npm 包 | npm pack 后解压,重点看 package.json 的 scripts 段 |
十、受限环境下的处置
前面的选型建议假定环境是开放的。实际工作中常遇到两类约束,它们会直接改写选型顺序。
10.1 不允许执行下载来的可执行文件
这是本机当前的约束。后果是 Windows 侧绝大多数格式直接出局:
| 状态 | 格式 |
|---|---|
| 不可用 | .msi(需 msiexec 执行外来包)、NSIS/Inno/InstallShield 的 .exe、WiX Burn 引导程序、makeself .run、自解压安装包 |
| 可用 | 便携 ZIP(Expand-Archive 解压)、Scoop 式「解压到用户目录」的分发 |
实操建议:在项目发布页优先寻找 *-win-x64.zip 之类的便携包,而不是 *-setup.exe。很多项目两者都提供,只是便携包排在资产列表下方容易被漏掉。若只提供 setup.exe,先用 7z l 判断制作工具 —— 若内部是 MSI 包装器,有时可只解包取出文件树。
10.2 运行时不在 PATH 中(以 Python 为例)
本机未安装系统级 Python,官方的 python-3.x-amd64.exe 也因 10.1 的约束无法使用。此处恰好印证了第六节 6.3 的结论:静态单文件二进制形态的工具不受安装器限制。
uv 本身是一个静态二进制,且能自行下载并管理 CPython 运行时,因此在本机可用。本仓库统一采用这一方式:
# 本仓库约定:--no-project 必须加(本项目无 pyproject.toml)
uv run --no-project conf-kit\conf-wiki.py map-update
# 需要第三方库时,让 uv 临时取用,优先走 wheel 而非现场编译
uv run --no-project --with httpx script.py
与 7.2 呼应:本机没有 MSVC 构建工具,一旦 pip 退回 sdist 现场编译基本必然失败,因此需要第三方包时应确认存在 win_amd64 的 wheel,必要时用 --only-binary :all: 让问题早暴露、暴露得准确。
十一、命令速查
| 任务 | 命令 |
|---|---|
| 静默装 MSI 带日志 | msiexec /i app.msi /qn /norestart /l*v install.log |
| 按 GUID 卸载 MSI | msiexec /x {GUID} /qn |
| 静默装 Inno Setup 包 | setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART |
| 静默装 NSIS 包 | setup.exe /S /D=C:\Program Files\App(/D 放最后,不加引号) |
| 装 MSIX | Add-AppxPackage app.msix |
| 装 MSU | DISM /Online /Add-Package /PackagePath:x.msu /NoRestart |
| 命令行装 pkg | sudo installer -pkg App.pkg -target / |
| 挂载并复制 dmg | hdiutil attach App.dmg -nobrowse → cp -R → hdiutil detach |
| 查 app 签名状态 | codesign -dv --verbose=4 App.app;spctl -a -vvv App.app |
| 装单个 deb 并解析依赖 | sudo apt install ./app.deb |
| 装单个 rpm 并解析依赖 | sudo dnf install ./app.rpm |
| 连配置一起卸载(Debian) | sudo apt purge app(remove 会保留配置) |
| 无 FUSE 跑 AppImage | ./App.AppImage --appimage-extract-and-run |
| 用户态装 Flatpak | flatpak install --user flathub org.example.App |
| Snap 回滚 | sudo snap revert <snap名> |
| 临时试用工具(Nix) | nix shell nixpkgs#<包名> |
| 排查 pip 平台标签 | pip debug --verbose;pip install --only-binary :all: pkg |

浙公网安备 33010602011771号