Linux Dependency Management

2024-01-09
10分钟阅读时长

Import

多年以后,面对开源社区的欢呼,Eelco Dolstra 工程师将会回想起他撰写博士毕业论文的某个遥远的下午。[1]

Package Management

SAT

包管理器的依赖解决过程本质上是一个SAT(Boolean Satisfiability Problem) 求解器。

SAT 求解器是一种精确的求解方法,可以确定是否存在一组满足约束条件的变量赋值。

其目的是在满足

  1. 版本约束

    1. 用户要求的版本约束
    2. 依赖关系定义的版本约束
  2. 条件约束

    1. 编译选项约束
    2. 额外功能约束
    3. 文档约束
    4. 其他条件约束的约束

以上所有约束的情况下尽可能安装版本最高的软件。

SAT作为NPC问题,没有多项式时间复杂度的算法。

现代 SAT 求解器使用的主要技术 [2] 包括

  1. Davis-Putnam-Logemann-Loveland (DPLL) 完整的、基于回溯的搜索算法,思想和深度优先搜索相似
  2. 冲突驱动子句学习(conflict-driven clause learning CDCL) 在DPLL算法基础上发展的增强算法,通过学习新的子句来减少搜索空间,并使用冲突分析和回溯机制来指导搜索过程。区别在于CDCL 的回跳是非时间顺序的。
  3. 随机局部搜索算法(如 WalkSAT) 启发式算法通常用于解决大规模的SAT问题,尤其是当问题的解空间非常大时。

一般各个包管理器使用的是启发式算法。

用户在安装软件前等待了很长的时间,最终得到的也不一定是最优解,甚至可能得到的是无解。

Kiss

Arch能从一众发行版包中脱颖,是因为KISS原则。

KISS(Keep It Simple, Stupid)是一种设计原则,强调简单性和直接性。

在设计和开发过程中尽量保持简洁、直观和易于理解的方式来解决问题,避免复杂性和不必要的细节。

  1. 简洁而灵活的安装:用户可以根据自己的需求进行自定义安装
  2. 滚动更新:持续提供最新版本的软件包和系统更新
  3. 自由的软件选择:鼓励用户自由选择他们喜欢的软件包和工具,而不仅限于预设的选项
  4. 文档和社区支持:拥有详细而全面的文档,以及活跃的社区支持

其中,在包管理方面保持KISS原则的体现是:

  1. 分级管理
  2. 保证包的“相对最新性”
  3. 为每个软件仅提供“一个”相对最新的版本

上述方案避免了大量的依赖求解过程,显著减少求解空间

Arch将软件分为多个等级:

  1. core
  2. extra/multilib
  3. arch user repository

为什么是相对最新,因为绝对最新具有以下不可实现性:

  1. main分支并不保证所有功能都立即完全可用
  2. 较新tag的部分并不保证没有严重bug

以上所说的“一个”指代的是:

  1. 核心仓库仅提供“尽可能少”的相对最新版本,降低冲突可能性
  2. 用户有额外需要,可以从用户仓库获取其他各种版本

对于核心仓库,频繁维护;对于用户仓库,降低依赖冲突的检查力度,责任自负

Costs and Problems

Pacman

发行版的包管理方式都是有代价的,对于Pacman来说,具体有三个方面:

  1. 不是所有的人都遵循保持最新的理念,或者有意愿保持最新 当软件被陈旧的技术和依赖所裹挟的时候,它已经不再是一件精美的艺术品,而是一座难以言喻的山

    1. 典型技术:=gcc 4.8.5= 、=java8=
    2. 典型依赖:XX软件园、DLL下载专区

      我并不想批判追求技术和追求盈利的问题,因为

      1. 它们本就陌路 完全追求技术很难成功,而完全追求盈利却很可能成功 [3],于是产生了“软件灾难”
      2. 大部分人从利出发,对技术没有“足够”热爱 在这种背景下,如果某个程序的所有依赖(包括软件本身)都“碰巧”被其他人提供(而不是一种规范的提供方式),大部分人不会去思考有关依赖的深层次的问题

      因此,合理的原因造就了客观上的现实,不能不说是一种遗憾。

  2. 软件开发者的水平也是参差不齐,因为本可避免的问题而导致不能保持适配

    这里不得不说一句暴论,即绝大部分软件开发者(至少嵌入式软件开发者)的水平已经差到一种令人发指的程度——这很容易理解,因为大部分嵌入式工程师都是从硬件入行,他们也许并没有机会深刻学习和思考过软件的开发 [4]。

    无意批判已成定式的现实,但这种现象令人担忧:我们知道新人都很菜,也理解由新人成长为专家需要代价,但是当一群自以为是的“专家”因为害怕所谓的“新技术”会导致不可控因素而规劝新人不要尝试的时候,我很难抑制失望,进而厌恶“专家”本身。他们就像是行业蛀虫,“洋洋自得”地散发着肮脏的气息。

    1. 由于代码详细设计上能力的不足导致软件需要增加更多冗余的依赖关系
    2. 由于代码灵活性或可扩展性不足导致先期开发预设的立场后期不能改变
  3. 频繁的滚动更新对软件开发“不一定利”

    试想编译一个稍有规模的程序,这也许需要5-10分钟的时间。

    现在假设你的程序依赖的动态链接库是opencv 4.9.0-1,结果当你第二天顺手更新了系统的时候,它们变成opencv 4.9.0-2了,于是你的程序不能够再正常运行(因为动态链接库变化了);

    此时你正在开发,你尚不能知道是否有其他问题导致程序不能运行,但你已经对代码进行了修改,这导致你需要完全编译所有代码(而不是现行修改部分代码)来解决动态链接问题;

    同时,假设你希望回到上一次成功的生成来查看效果,也不现实,除非你通过版本管理工具回到上个版本重新编译源码,或者使用 patchelf 等工具;

    综合来说,你的调试不会特别顺利,这对开发是一种不小的打击。

Dependency

包管理器求解依赖有很多问题 [5]

  1. 依赖全面度 在FHS的模式下,维护者(甚至有时候是软件包开发者)无法精确地知道一个软件包究竟依赖哪些包,一旦 /usr/include/usr/lib 碰巧有程序所必须的内容,软件可能直接就成功运行了。但是这样的构建可能是不可复现的。

    就像一千个人眼中有一千个哈姆雷特一样。

  2. 依赖多样性 当存在同一软件的多个版本时,依赖路径问题难以解决。可能的解决办法是:

    1. 小版本软件迭代

      1. 子版本不同的软件的库文件放到不同的目录中
      2. 可执行文件或其他会冲突的文件使用实用工具进行软链接管理,如 eselect 、=archlinux-java-run= 等
    2. 大版本软件更新

      1. 可执行文件更名,如使用 python2python3
      2. 创建虚拟环境
  3. 多用户依赖冲突

    1. 当系统中存在多个具有管理员权限的用户时,每个用户可能都需要以系统管理员身份安装一些软件,这可能导致多用户依赖冲突。
    2. 产生依赖冲突问题时,在系统层面上可能无法做到在不破坏其他用户安装的依赖的情况下解决冲突
  4. 升级安全性 目前,一般的包管理工具均采用“同名替换”或“增量更新”策略,这会导致软件更新操作不是“直接可逆”的,在系统软件包的升级过程(对以前的Arch来说甚至是升级后)是危险的,需要配置快照等手段进行回滚。

Why Nix and why not

Nix is good

这就是我要说的重点。为什么人们应该需要Nix ?为什么自从遇见NixOS,我就一直在尝试接近它。

  1. NixOS是人类群星闪耀时的现代版本
  2. NixOS的推出是现代Linux的全面革新

曲一线说,五年高考三年模拟让每一位学生分享高品质教育;

我想说,NixOS会让每一位用户得到高品质使用体验。

Why Nix is good

如果说二十年前的Nix还是咿呀学语,天真烂漫,那么二十年后的Nix已经玲珑有致,狡黠率然。我热切的希望每个能认识到Nix优势的人们都能陷入和Nix沟通的美好意境中。

Nix包管理器解决了以上讨论的通用包管理器问题 [5]:

  1. 精确依赖 由于每一个软件的构建和运行都在完全隔绝的环境中,每一个通过Nix构建的软件它的依赖关系会完全展现在维护者面前;

    Nix不会从除了软件包所声明的依赖以外的其他任何地方寻找依赖,因此如果构建的软件依赖“不全”,软件将不会正确运行。这保证了所有成功构建的软件它们的依赖都是完全的。

  2. 多样性依赖共存 可以同时安装一个软件包的多个版本或变体。由于哈希方案,包的不同版本最终会出现在 Nix 存储中的不同路径中,因此它们不会相互干扰;
  3. 多用户支持 任何属于不同依赖关系的软件都会独立构建,任何被其他软件所依赖的软件互相隔离,因此多用户安装软件不会破坏已有的依赖关系;
  4. 原子升级和回滚 包管理操作不会覆盖已有的软件包,而是在不同路径中添加新版本,包升级不会干扰已有包的运行,而切换后自动替换到新包,因此不会产生问题;

还有一些其他优点,如:

  1. 记录式、声明式的构建
  2. Nix具有高度可移植性

这是Nix的完美之处,卓越之处,精彩之处,绝妙之处,非凡之处。每一次和nix的交流都是有效的;构建的每一个结果都是精确且可复现的;换言之,蓝图所描述的每一句都会被Nix用于构建安全、稳定的“未来”。

人最害怕什么?“不确定性”。人生就像囚徒,困在某个维度的茧房之中动弹受限。随着时间推移,囚牢中的人越来越少,同时不可言喻的东西也就越来越多。在不确定性扩大的过程中,解脱不是理想的结果,但解脱客观上已经渐渐成为了一种理想。

Why Nix is bad

这一节其实想探讨的不是Nix的问题,而是“目前”Nix所“面临”的问题 [6] 。

  1. 软件发行与维护问题

    1. 开发者不一定比发行版维护者更懂Linux

      国内很多网页默认Linux端就是安卓,因此你能在Linux上正常访问网页已经很不容易了。

      开发者可能会作出各种符合 FHS 标准的假设,因此可能需要打补丁纠正。

      1. 软件开发者不一定开放源代码
      2. 开发者不使用标准的编译方式,或者使用了非正常的目录结构
      3. 开发者不声明完全所需的依赖,或运行时额外需要其他依赖,导致 autopatchelf 不能达到预期效果
      4. 程序主动探测运行环境或对其本身的修改 (SaaS类型软件)
      5. 手动打包需要掌握的前置知识过多

        即使没有上述问题,手动完成一次打包也可能需要耗费大量的时间和精力,学习门槛过高。

    2. 软件间通信问题

      1. 通常可以用Xdg Open来解决软件“唤起”的问题
      2. 如果已经为某个包创建了FHS虚拟环境,当它唤起其他非FHS环境的包的时候存在问题
      3. 使用动态链接库魔改浏览器内核的方式唤起页面内浏览器,基本不可行
  2. 使用理念问题 使用理念问题可以归为“狂热Nix信徒”,“自由Nix门徒”两派。

    1. 狂热Nix信徒希望 all in nix way

      1. VSCode 所有插件均转为Nix表达式
      2. Python所有功能包均转为Nix表达式

        尽管现在稍新一些的发行版都已禁用Python全局安装功能包,但是它们支持创建类似conda的虚拟环境,而这对于NixOS来说似乎是不可接受的,因为有观点认为Nix已经是最好的conda了。

      3. Rust/Nodejs所有依赖库均转为Nix表达式
      4. 家目录下所有配置文件均转为Nix表达式
    2. 自由Nix门徒则希望 all in soft way

      1. VSCode 安装FHS版本,想装哪个插件就装哪个
      2. Python/Rust/Nodejs等等的问题留给它们自己的包管理器解决
      3. 家目录只管理重要的配置
  3. 存储空间占用问题 Nix目前对依赖的复用介于“完全复用”和“完全不复用”之间,完全安装一个软件相较于其他发行版可能更加耗费存储空间

Why Nix Combined with Arch is bad

既然完全使用Nix会面临以上问题,于是很自然地想到一个可能性,平常用Arch,编程开发用Nix环境。

  1. 很难在非NixOS环境上安装Nix软件包

    1. 一般视频都用的是 nix runnix-shell -p 这种临时性选项
    2. nix profile installnix-env -iA 这种方式没有 flake.nix 的优点
    3. 目前我能想到的方案是单独安装 home manager
  2. 使用Nix软件不方便访问到 /nix 目录以外的依赖,而使用Arch的工具则很难查找 /nix 内的依赖

    1. 使用 Nix 软件时, gccg++ 自动默认用 nix 提供的
    2. cmake 在 Nix下安装时,见 001-search-path.diff
    3. 图形化程序不能正常工作 如 pangolin 由于依赖 xorg.libX11 , 在 /nix 内的 xorg.libX11 不能正确显示图形,如果改用 Arch安装的 pangolin ,则需要增加 cmake 的搜索路径
  3. 环境变量问题

    1. 需要安装 direnvnix-direnv
    2. 更改 $HOME/.config/direnv/direnvrc 中的内容

Summary

没有绝对完美的包管理系统,Nix还有很长的路要走。

有人说当一个人开始回忆起过去的时候,他就老了。Nix无疑是成功的,但是我的探索Nix的应用之旅却显得有些失败。

我不知道Eelco Dolstra 工程师回忆起过去是什么样的,我回忆起4个月前刚接触NixOS的时候,脑海里想到的一句话是“花未凋,月未缺,人就在天涯,一切都很好。”