Linux 并非生来就拥有完善的包管理生态。从 GNU/Linux 初具雏形到首款包管理器面世,中间存在着一段漫长的“手工分发”空窗期。在那个年代,软件的分发几乎完全依赖于源代码。
开发者通常将源码打包发布,用户下载后需在本地环境调用 gcc 和 make 等构建工具,手动执行经典的“源码三部曲”:./configure、make 以及 make install。这种做法属于典型的“黑盒安装”。

一份典型的源码结构,可以看到里面有 Makefile、configure 等文件

一段典型的 make install 脚本逻辑
这种分发方式存在相当多的痛点问题:
为了解决这些痛点问题,开发者们提出了“包”与“包管理器”的概念,开始尝试建立一套标准化的软件包管理和分发体系。
“包”就是包含元数据的软件,包括但不限于

一个典型的软件包结构——以 deb 格式为例
“包管理器”就是统一对“包”进行管理的工具,能够

包管理器是如何工作的
从第一款包管理器的出现到现在,Linux 的包管理器生态一直在不断演进。根据业界常用的分类方式,我将包管理器的类别分为 3 个大类:基础包管理器、高级包管理器、新一代包管理器。这 3 个大类也代表着 3 次设计理念的迭代。
我将最早期的包管理器称为基础包管理器。这类包管理器的出现主要是为了解决“有”和“无”的问题,无论做得好与不好,至少有个东西能够相对方便的管理软件包了。
典型的基础包管理器有 dpkg 和 rpm,它们是 Linux 上面出现的最早一批的包管理器,主要功能就是对软件包进行增删改查。

dpkg 的主要用法

rpm 的主要用法
随着时代的发展,人们发现包管理器仅有增删改查功能是不够的,于是这些开源社区在基础包管理器之上额外封装了高级包管理器。
相比于基础包管理器,高级包管理器额外提供了更多的功能

高级包管理器的典型功能——以 apt 为例
主流发行版所使用的高级管理器
另外需要提一下:基础包管理器的存在属于历史遗留问题,只有 Debian 系和 Red Hat 系才存在基础包管理器,后面出来的发行版(如 Arch、Gentoo 等)都是一个高级包管理器管理所有事情,不再单独提供基础包管理器。
传统包管理器(前面提到的那些)普遍存在一些缺陷
人们迫切希望能有一种新的包管理器,实现以下诉求
基于这些诉求,业界出现了 nix、snap、flatpak、appImage、玲珑......等新一代包管理器。

同一条赛道上的几个竞争对手
新一代包管理器的特点
按照这些包管理器出现的时间顺序,我整理了一个图,从图中我们可以直观感受它们的演进过程和先后关系。

包管理器演进过程
apt 的设计理念围绕着依赖管理、软件源管理、版本控制、用户友好性、可靠性和可扩展性展开。通过这些设计理念,apt 成为了一个强大且可靠的包管理系统,广泛应用于 Debian 及其衍生发行版中。
1. 依赖关系管理
核心目标:apt 的一个重要设计理念是自动处理软件包的依赖关系。它能够自动解析和安装软件包及其依赖项,确保系统中安装的软件包能够正常运行。
实现方式:apt 使用复杂的依赖解析算法,分析软件包的元数据(如 Depends、Pre-Depends、Recommends 等字段),并根据这些信息自动下载和安装所需的依赖项。
2. 软件源管理
核心目标:apt 支持从多个软件源(repository)安装软件包,这些源可以是本地文件系统、网络服务器或 CD-ROM 等。通过灵活的软件源管理,用户可以轻松获取最新的软件包。
实现方式:apt 使用 /etc/apt/sources.list 文件和 /etc/apt/sources.list.d/ 目录来管理软件源。用户可以添加或删除软件源,并通过 apt update 命令更新软件包索引。
3. 版本控制与升级
核心目标:apt 提供了强大的版本控制功能,能够自动处理软件包的升级和降级,确保系统的稳定性和一致性。
实现方式:
4. 可靠性和事务性
核心目标:apt 的设计强调操作的可靠性和事务性,确保软件包的安装和更新过程不会因意外中断而导致系统不稳定。
实现方式:
5. 可扩展性
核心目标:apt 的设计考虑了系统的可扩展性,支持多种插件和扩展机制,以满足不同用户的需求。
实现方式:
6. 社区和生态支持
核心目标:apt 的设计注重社区和生态系统的支持,通过开放的软件源和社区贡献,提供丰富的软件资源。
实现方式:
Arch Linux 能从一众发行版中脱颖而出,是因为 KISS 原则。KISS(Keep It Simple, Stupid)是一种设计原则,强调简单性和直接性。在设计和开发过程中尽量保持简洁、直观和易于理解的方式来解决问题,避免复杂性和不必要的细节。
Arch Linux 在包管理方面( pacman 包管理方案)保持 KISS 原则的体现主要有如下 3 点:
1. 分级管理
Arch Linux 将软件分为多个等级
2. 保证包的“相对最新性”
基于这一点,Arch Linux 做成了滚动发行版。
至于为什么是相对最新,因为客观条件限制无法做到绝对最新:
3. 为每个软件仅提供“一个”相对最新的版本
所说的“一个”指代的是:
Arch Linux 的这套设计理念有一定的优势,但也是有缺点的。首先,不是所有人都遵循保持最新的理念,或者有意愿保持最新;其次,不同的开源软件情况不一样。有些开源软件就需要依赖较老的软件,而 Arch Linux 里面始终提供最新的软件,这种情况下就很难将它引入 Arch Linux 中;最后,频繁的滚动更新对软件开发不一定有利,用户的系统和业务有可能会在滚动更新的过程中“滚挂了”。
这几点缺陷都是滚动发行版的缺陷,因此 Arch Linux 受众有限(不适用于企业和服务器),它的市场份额远小于 Debian、Red Hat 等主流发行版。
不同发行版的包组织方式、配置方式都完全不一样,不同发行版各自为战,无法大一统。同一个开源软件,需要有不同的人在不同的 Linux 软件源上面维护不同格式的软件包。

deb 软件包格式

rpm 软件包格式
单用户依赖冲突
多用户依赖冲突

依赖冲突示意图

一个依赖冲突的例子
当存在同一软件的多个版本时,它们往往会互相覆盖。
目前常见的规避办法是:

apt 包管理方案对“python”这个软件的多版本共存处理方式:可执行文件更名
绝大部分开源软件,它们在 Makefile 里面所配置的安装路径都遵循 FHS(Filesystem Hierarchy Standard),因此传统的包管理器也同样遵循 FHS。
强依赖 FHS 会有什么后果?由于编译器默认会检索 /usr/lib 或 /usr/include 等全局路径,软件包往往会产生隐式依赖。如果你的系统目录刚好有一些库,你的软件包就能正常运行了,而你可能并未注意到你的软件包会依赖那些库。也就是说,这会使软件包作者对依赖识别不完整,最终无法达成“可重复构建”的目标。
这里引用一段来自 nix 社区的描述:
通常,当您为 RPM 等软件包管理系统创建软件包时,必须为每个软件包指定其依赖项,但无法保证此规范完整无误。如果您遗漏了某个依赖项,那么如果您的机器上安装了该依赖项,则软件包可以在您的机器上正确构建和运行,但如果最终用户的机器上没有安装该依赖项,则软件包将无法构建和运行。
hook 系统设计复杂,没有限制,可以通过 hook 系统做很多破坏系统的操作。deb、rpm 等软件包权限管控松散,可能允许恶意软件或未经授权的用户访问敏感数据,有较大的安全风险。

以 deb 软件包为例, 这些钩子脚本是没有任何限制的,可以编写任意命令
可靠性不足,没有冗余的恢复设计,缺乏校验机制。一旦包管理系统故障,系统基本没有修复的可能性。
目前,一般的包管理工具均采用“同名替换”或“增量更新”策略,这会导致软件更新操作不是“直接可逆”的,系统软件包的升级过程是危险的(尤其是像 Arch Linux 这种滚动发行版,很可能就“滚挂了”),需要配置快照等手段进行回滚。
nix 来自于学术界,源自 2006 年 Eelco Dolstra 的博士毕业论文《The Purely Functional Software Deployment Model》。论文中提出了一种名叫 nix 的软件部署系统 ,解决了很多现有部署系统的痛点,十几年过去了,当时强调的很多痛点仍然存在, nix 也逐渐发展出一套完善的生态系统和社区。
nix 的设计理念:
1. 精确依赖
由于每一个软件的构建和运行都在完全隔离的环境中(构建的时候是沙盒化隔离,运行的时候是目录隔离),每一个通过 nix 构建的软件,它的依赖关系会完全展现在维护者面前。
nix 不会从除了软件包所声明的依赖以外的其他任何地方寻找依赖,因此如果构建的软件依赖“不全”,软件将不会正确运行。这保证了所有成功构建的软件它们的依赖都是完全的。
2. 多版本共存
可以同时安装一个软件包的多个版本或变体。由于哈希方案,包的不同版本最终会出现在 nix 存储(/nix/store)中的不同路径中,因此它们不会相互干扰。

nix 包存储结构和依赖关系示意图
3. 多用户支持
任何属于不同依赖关系的软件都会独立构建,任何被其他软件所依赖的软件相互隔离,因此多用户安装软件不会破坏已有的依赖关系。
4. 原子升级和回滚
包管理操作不会覆盖已有的软件包,而是在不同路径中添加新版本,包升级不会干扰已有包的运行,而切换后自动替换到新包,因此不会产生问题。
另外,它的更新过程是事务性的更新,滚不挂,可以回滚。
5. 具备声明式特性与函数式特性
nix 的配置是声明式的,只需要关注最终的结果,无需关注过程。
每次运行 nix 命令时,都会根据配置文件生成一致的输出。这使得环境的构建具有高度的可重复性,非常适合在开发、测试和生产环境中保持一致性。
它将包视为纯函数式编程语言中的值——它们由没有副作用的函数构建,并且在构建后永远不会改变。这会导致一些不能 stateless 地用并且会全局修改的软件可能会不那么好使,虽有规避方案但并不能完美解决该问题。
flatpak 是一种用于打包、分发和运行应用程序的软件框架。它提供了一个沙箱环境,使得应用程序可以在各种 Linux 发行版上运行,而不受特定库或系统配置的限制。

flatpak 沙箱示意图
flatpak 的设计理念:
1. 通用性
flatpak 允许应用程序在几乎任何 Linux 发行版上安装和运行,包括非 GNU 发行版、无 systemd 发行版、不可变发行版和各种架构,而无需开发人员访问相关硬件。
2. 使开发者能够专注创新
flatpak 允许发行版维护者专注于创新他们的发行版,而不受打包问题的困扰。
3. 稳定性
flatpak 应用程序中的故障不会影响系统,因为它们在隔离环境中运行。(通过 mount namespace 与发行版的文件隔离)
4. 免 root 安装
flatpak 不需要提升权限来安装应用程序或运行时。
5. 沙盒应用程序
flatpak 的主要目标之一是提高桌面系统的安全性。这是通过将应用程序彼此隔离并限制其对主机环境的访问来实现的。
6. 去中心化分发
flatpak 提供去中心化托管和分发,允许开发人员或下游托管自己的应用程序和应用程序存储库。
7. 空间效率
flatpak 通过对多个应用程序使用的库和其他文件进行重复数据删除来节省存储空间。
虽然这几个新一代包管理器的设计理念都比较相似,但它们的具体的功能细节上还是会有一些差异。
玲珑包管理器官网里面的一个对比表格
这几个包管理器上手门槛都不低,需要理解各种概念才能良好地完成软件包的维护工作(由于那些新设计理念的存在,开发者需要掌握的内容要比传统包管理器多很多)。即使软件本身没有任何问题,手动打包一次也会消耗大量时间精力,学习成本高。
尤其是 nix,上手门槛最高。nix 破坏了 FHS(Filesystem Hierarchy Standard),而开源软件的开发者可能会做出各种符合 FHS 的假设,因此可能需要打补丁纠正,这是费时费力的。
nix 社区的官方文档里面是这么说的:
Nix 的主要用途之一是解决打包软件时遇到的常见困难,例如指定和获取依赖项。
从长远来看,Nix 可以极大地缓解此类问题。但首次使用 Nix 打包现有软件时,经常会遇到难以理解的错误。
由于做了环境隔离,如果要涉及跨包调用或者访问宿主机资源的场景,事情就会变得很麻烦。这就导致它们的适用范围是远远没法跟传统包管理方案相比的。
引用一段 flatpak 官方给出的结论:
总体而言,flatpak 最适合桌面应用程序。虽然命令行应用程序也可以使用,但 flatpak 在某些情况下可能不适合:
应用程序需要使用 su, sudo, pkexec 等来提升权限。flatpak无法在沙箱内运行 SUID 二进制文件。 应用程序需要访问主机上的 /proc 或未经过滤的进程访问权限。这是不允许的,因为 flatpak 拥有私有的 proc 文件系统。 该应用程序需要使用 flatpak 的 seccomp 过滤器列入黑名单的 syscall。例如,flatpak 不允许在沙盒中生成子命名空间。 内核模块和驱动程序,它们不是应用程序包,不能作为 flatpak 软件包正常工作。
一般来说,如果沙盒阻止了应用程序的核心功能,或让这些操作变得太不方便甚至反人类,此时 flatpak 可能不是最合适的打包选择。
一个软件相当于一个 docker 镜像,这相比于传统包管理方案更加耗费存储空间的,一个小软件的包都有几百 MB 大。

摘抄自 https://ludocode.com/blog/flatpak-is-not-the-future
snap、flatpak、appImage、玲珑,这几款包管理方案,它们的软件包是运行在沙盒化的环境中,这会引入性能开支。虽然它们一直在不断的试图优化自己的运行性能,但它的打包模式几乎决定了它们的上限,无法与 deb/rpm 这样的原生软件包相比。

网络上别人做的一个关于启动速度的比较
除了 nix 和玲珑以外,其他几款新一代包管理器(snap、flatpak、appImage)的设计初衷主要是为了给桌面应用程序使用的。例如“火狐浏览器” 这样的软件。
虽然它们也支持打包纯命令行程序,但这不是它们最适用的场景。从现在的数据来看,它们的软件源里面并没有太多命令行程序,大多是以桌面应用程序为主。
简单来说,这几款更接近于 macOS 上面的那套桌面应用程序的管理方式,而不是对标 macOS 上面的 homebrew。

Snap Store 里面基本上都是桌面应用程序
这些下一代包管理器都主张自己是“应用程序分发的未来”。虽然都自称自己是未来,但这些新的方案争议其实也很大,也有相当多的用户给予差评。
客观来说,它们只能算是新的尝试,并不能作为传统包管理器的完美上位替代。它们只是走了新的路线,而这新的路线是有缺陷的。

也有相当一部分开发者对此给予差评
包管理方案是一个整体的解决方案,除了包管理器这个软件本身以外,软件源(软件仓库)也是包管理方案的一部分。软件包不会凭空出现,如果只有包管理器而没有软件源,包管理器将无法在线安装软件包。
以下是一些常见发行版及其官方软件源:
软件源不一定能混用
不同的 Linux 发行版,哪怕使用相同的包管理器,其软件源通常也不能混用。这取决于发行版的维护策略和底层库的兼容性。
以 Debian 系为例,Debian 的子子孙孙非常多,大家都用 apt 作为包管理器,但它们的软件源是不能混用的。因为不同的衍生版本,系统里面的情况并不完全相同,比如它们的 libc 版本可能就不一样。如果软件源混用,下载下来的软件有可能是不能正常使用的。
第三方与社区软件源
各大发行版都会维护自己的官方软件源,但它们普遍也给了一个渠道让非官方的用户托管自己的软件,这样可以减少官方团队的维护负担。
例如 Arch 就提供了 AUR 源,Ubuntu 就提供了 PPA 源。这些源的准入门槛比官方源要宽松很多,因此软件包数量规模特别大,但质量和兼容性没有保障。
软件源镜像站
官方软件源服务器通常部署在海外(如欧洲或北美),由于跨国网络带宽限制,直接从官方源下载软件的速度往往较慢。
为了解决这个问题,全球各地的大学、科研机构和企业会建立镜像站。镜像站会通过定期同步,完整地复制官方源的所有内容。本地的用户通过镜像站来获取资源,可以得到最佳的网络体验。
国内常见镜像站:
软件包的发布模式也是包管理解决方案的一部分。我们常见的发布模式就是这两种模式(以及它们的混合体)。
滚动发布(Rolling Release)
周期发布(Fixed Release, Point Release)
发布模式并不是由包管理器决定的,而是由包管理器和软件源共同决定的。比如,Debian 系都用 apt 作为包管理器,但 Debian 系里面既存在固定发行版(比如 Ubuntu),也存在滚动发行版(比如 Purism PureOS)。
不可变发行版是一种特殊的 Linux 发行版,其核心特点是根文件系统默认为只读状态,不允许用户或程序随意修改。
这种设计使得操作系统的核心部分在正常使用过程中保持不变,从而提高了系统的安全性、稳定性和可靠性。
传统上,不可变发行版主要用于服务器(如云原生基础设施)、嵌入式设备或测试环境,以确保环境的一致性。随着技术的成熟,它开始应用于日常桌面用户,解决系统因配置冲突或误操作导致的系统损坏问题。
为什么要刻意提到这个知识呢?这是因为,系统是否能达成“不可变” 的目标,跟包管理方案的支持也是有很大关系的。我前面提到的那些“新一代包管理器”,就被广泛应用于不可变发行版中。
以下是一些常见的不可变发行版:
在上述发行版中,Vanilla OS 和 blendOS 展现了独特的设计逻辑:它们不局限于单一的发行版生态,而是通过容器化技术实现了对多种包管理器的原生支持。
Vanilla OS
Vanilla OS 通过核心组件 vso 管理应用环境,它作为宿主系统与容器化子系统之间的协调者,实现了跨发行版软件包的无感集成。
主要特点:
BlendOS
blendOS 则定位为一个“融合”发行版,其核心工具为 blend。
主要特点: