一篇搞懂所有安装包格式

概要:日常开发中安装软件,面对的不是一个「安装包」,而是十几种结构、权限模型、依赖模型、升级机制各不相同的分发格式。本文按平台逐一拆解各类安装包的二进制结构、依赖模型、权限要求、静默安装方式、升级与卸载机制、签名与校验链条,给出适用场景与已知陷阱,并以横向对比矩阵和选型决策树收尾。

内容范围: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 「安装」这个动作到底包含什么

把文件复制到磁盘只是安装的一小部分。一个完整的安装动作通常涉及以下步骤,而各类安装包的本质差异,就在于它们覆盖了其中哪几步、以及由谁来执行:

  1. 校验来源:签名 / 哈希 / 公证票据
  2. 解析依赖:需要哪些运行库、它们是否已存在
  3. 落盘:文件写到哪个前缀(prefix),是否可写用户目录即可
  4. 注册:注册表项、desktop 文件、服务单元、PATH、文件关联
  5. 执行脚本:建用户、建数据库、重建缓存、启动守护进程
  6. 记账:把「装了什么」写进某个数据库,供升级和卸载使用
  7. 升级路径:谁来发现新版本、如何替换、能否回滚
  8. 卸载:能否完全逆转第 3 到第 6 步

第 6 步「记账」是最容易被低估的一步。没有记账的安装方式(源码 make install、手动解压、拖拽复制)在卸载和升级时没有任何依据,这是后文反复出现的分界线。

1.2 三条决定性的分歧

所有格式的差异,基本可以归约到下面三条分歧上。确定了这三条,一个格式的其余特性大致就被决定了。

分歧选项 A选项 B被它决定的后果
依赖从哪来共享系统库自带全部依赖包体积、安全更新的扩散速度、跨发行版可移植性
装到哪个前缀系统目录(需提权)用户目录(免提权)是否需要管理员、能否多用户共享、受限环境能否使用
运行时是否隔离完全继承用户权限沙箱 + 显式授权卸载干净度、安全边界、与系统集成的难易

ℹ 第一条分歧有一个常被忽略的代价:自带依赖意味着安全更新责任转移给了应用作者。共享库模型下,OpenSSL 出一个漏洞,发行版更新一个包即修复全系统;自带依赖模型下,每个内嵌了该库的应用都必须各自重新打包发布,任何一个作者不跟进,漏洞就留在那台机器上。

二、Windows

2.1 格式总览

格式容器结构需管理员典型静默命令卸载记账位置
.msiOLE 结构化存储,内含关系型表通常需要msiexec /i x.msi /qnWindows Installer 数据库
.exe(安装器)自解压载荷 + 脚本引擎多数需要不统一,见 2.3注册表 Uninstall 项(如果作者写了)
.msix / .appxZIP + AppxManifest.xml不需要Add-AppxPackage x.msix包管理器(强制)
.msu / .cab系统更新载荷需要DISM /Online /Add-Package组件存储(WinSxS)
.nupkg(Chocolatey)NuGet ZIP + PowerShell 脚本需要choco install x -yChocolatey 自有目录 + 底层安装器
Scoop manifestJSON 描述 + 解压到用户目录不需要scoop install xScoop 自有目录
便携 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 经常被并列提及,但三者的工作原理完全不同,这直接决定了它们各自的能力边界。

wingetChocolateyScoop
包里装的是什么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 归档,内部结构:

成员作用
DistributionXML:安装界面、选项、JavaScript 形式的安装条件检查
PackageInfo标识符、版本、安装目标前缀
Payload实际文件,cpio 归档后 gzip 压缩
Scriptspreinstall / 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提示「已损坏,无法打开」
3Gatekeeper 校验:首次打开时结合隔离标记 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

formulacask
目标命令行工具与库图形界面应用
实际动作优先下载预编译二进制(bottle),否则本地编译下载官方 dmg/pkg,代为挂载与搬运
安装前缀/opt/homebrew(Apple Silicon)、/usr/local(Intel)/Applications 等标准位置
依赖解析有,Homebrew 自己的依赖图基本没有(由应用自带)
彻底清理brew uninstallbrew uninstall --zap(连带清理配置残留)

Homebrew 的价值在于把「下载 dmg、挂载、拖拽、卸载时手动清理」变成了有记账、可批量、可脚本化的操作,这是 macOS 上实现「环境即代码」的主要途径(配合 brew bundle dump 生成 Brewfile)。

3.7 Mac App Store

强制应用沙箱(entitlements 声明权限)、强制 receipt 校验、强制 Apple 审核。换来自动更新、家庭共享与统一退款渠道,代价是权限受限最严 —— 许多开发者工具因无法在沙箱内工作而不上架,或上架一个功能缩减版。

四、Linux:发行版原生包(共享库模型)

4.1 四种原生格式

格式容器结构低层工具高层工具依赖解析
.debar 归档:debian-binary + control.tar.* + data.tar.*dpkgapt低层不解,高层解
.rpm签名区 + 二进制头 + cpio 载荷(现代发行版用 zstd 压缩)rpmdnf / zypper低层不解,高层解
.pkg.tar.zstzstd 压缩 tar + .PKGINFOpacman -Upacman / AUR 助手解
.apk三段串接的 gzip 流(签名 / 控制 / 数据)apkapk解

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 四者总览

AppImageFlatpakSnapNix
本质ELF 运行时头 + SquashFS,单文件OSTree 对象库 + 分层 runtimeSquashFS,由 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 tarballtar.gz + package.jsonpostinstall 脚本可执行任意代码,是供应链攻击的集中入口。CI 中用 npm ci --ignore-scripts 可降低风险
.jar / .warZIP + META-INF/MANIFEST.MF依赖 JRE;fat jar(shaded jar)把依赖打进同一文件,是 Java 版的「自带依赖」
.vsixZIP + 清单VS Code 扩展,支持离线安装:code --install-extension x.vsix
.nupkgZIP + .nuspec.NET 包,与 Chocolatey 复用同一容器格式但用途不同
.cratetar.gzRust 源码包,总是本地编译,所以没有平台标签问题

八、横向对比矩阵

维度msiexemsixdmgpkgdeb/rpmAppImageFlatpakSnap容器
需管理员是多数是否否是是否否是是
自带依赖否部分是是是否是共享 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 安装前的审查清单

对来源不完全可信的安装包,以下操作都是只读的,成本很低:

格式审查命令
.msimsiexec /a app.msi /qb TARGETDIR=C:\extract(管理员安装,只解包)
setup.exe7z l setup.exe 看内部结构与制作工具
.pkgpkgutil --expand 后阅读 Scripts/ 下的内容(它们以 root 运行)
.debdpkg-deb -c app.deb 看文件清单,dpkg-deb -I 看控制信息与维护者脚本
.rpmrpm -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 卸载 MSImsiexec /x {GUID} /qn
静默装 Inno Setup 包setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
静默装 NSIS 包setup.exe /S /D=C:\Program Files\App(/D 放最后,不加引号)
装 MSIXAdd-AppxPackage app.msix
装 MSUDISM /Online /Add-Package /PackagePath:x.msu /NoRestart
命令行装 pkgsudo installer -pkg App.pkg -target /
挂载并复制 dmghdiutil 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
用户态装 Flatpakflatpak 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
posted @ 2026-10-14 14:06  起飞的乌龟  阅读(29)  评论(0)    收藏  举报