包管理器
包管理器
前置文章:linux - 包管理与使用推荐
20240805 晚,辗转反侧,想到了点灵感,因此有了这篇文章。
Nix
一个月前我从 Archlinux 叛逃到了 NixOS,因为 Nix 宣传的特性确实很吸引我。然而现在我只觉得上了贼船,很多宣称的优势达不到我的心理预期。(这里先不谈 NixOS 的其他缺陷,单谈 Nix 包管理)
- Nix 的包多。
- 有点扯。虽然 repology 上 Nix 的数据很好看,但是
- Nix 有很多不同版本的相同软件是算不同包的;
- 编程语言(例如 haskell)的 dev package 占了很大一部分数量,而非可使用的二进制软件包。
- 有点扯。虽然 repology 上 Nix 的数据很好看,但是
- Nix 把所有包都放在一个 store 下,使用 hash 对包进行标识。
- 既然都上 hash 了,那我肯定期望我能安装任何包且不发生冲突,并且我更新的任何依赖都不应该 break 上层应用版本。然而:
- 安装同一个包的不同 feature 会冲突,例如 yt-dlp 和 yt-dlp-light。这倒能理解,毕竟系统也不知道要用哪个链到
/etc/profiles/per-user/<user>/bin/。 - 关于更新依赖 break 版本,我自己就亲身经历了一次:前段时间的 python 更新到 3.12 break 了一大堆包,例如我用的 activitywatch。原因大概是 nix 通过
flake.lock锁全局版本,而软件自己是不锁自身依赖版本的。pkgs.python(无版本后缀)是滚动更新的,它更新到 3.12,依赖它的包也会自动更新依赖。如果要指定旧版本应该需要依赖pkgs.python311,这也是 Nix 不同版本算不同包的一个原因吧。但这就让我很不爽,因为一个软件而锁死整个系统版本,要么就放弃这个软件,那跟一般的发行版不是差不多吗?你都上了 hash,膨胀了这么多路径,还这么捞? - 由上述,
pkgs.python311和pkgs.python313也是不能同时装的。。。因为这两个算是平等的两个包,而不是同一个包的新旧关系,系统不知道用哪个。。当然如果只是作为其他软件的依赖,是可以共存的。这算是一点微弱优势,但是挽回不了局面;并且很吃打包者水平(很多就直接依赖pkgs.python完事了,而不会去依赖pkgs.python311),否则此次 break 就不该发生。
- 安装同一个包的不同 feature 会冲突,例如 yt-dlp 和 yt-dlp-light。这倒能理解,毕竟系统也不知道要用哪个链到
- 既然都上 hash 了,那我肯定期望我能安装任何包且不发生冲突,并且我更新的任何依赖都不应该 break 上层应用版本。然而:
- Nix 不推荐打 bin 包,每个包都从源码编译(如果有的话),bin 则通过 cache server 和 mirror server 提供。
- 首先大家想必都喜欢用 bin 包吧,就连 Gentoo 也 goes Binary 了不是吗。
我超喜欢,甚至写了 bpm( - 第一次听到这个 cache 的思路觉得还挺清奇的,这样确实没有必要多打一个 bin。但是实际体验并不佳。
- 官方的 cachix 在国外,速度慢,没多少人用。
- 一般大家都是用各高校镜像站,然而 Nix 镜像不像其他滚动镜像只存一个最新版本,Nix 是需要存多个版本的。高校镜像的钱也不是吹来的,本来包就多,还有国内 PCDN 恶意刷流,在此之上还要保证足够服务质量算是比较困难了。于是镜像站同步速率放缓,也会导致 update 时有的软件包在 cache 里找不到需要自己编译,到头来不打 bin 包吃亏的还是自己。
- 有一些第三方提供自建 cache server 的服务,例如 garnix。但是人家是新兴商业公司,免费版限时,而且据说经常卡死,把时间配额耗完(后续应该改善了)。而且自建 cache server 只是换了个地方编译而已,本质还是编译,并没有为全球变暖做出什么改善。还有相当一部分人在白嫖 Github Action 做编译,每天 update flake,那不还是编译吗。
- 结果就是我也能看到有挺多人打包了 bin 上去,bin 和 unbin 混在一起,反而让人迷惑。
- 随着包越来越多,cache server 的编译负担会越来越重。服务器的性能不是无限增长的。别人已经编译过一次的 bin 包还需要 cache server 再次编译,感觉也不是很 bin。
- 自己改过的 drv 一定需要重新编译,即使只是改一个运行参数。
- 首先大家想必都喜欢用 bin 包吧,就连 Gentoo 也 goes Binary 了不是吗。
- store 下的包通过链接进系统。
- NixOS 管理整个系统,没什么好说。好处是可以玩一些 root on tmpfs 的骚操作,最大的问题是 NixOS 这样搞不支持 FHS 标准,装个啥软件,找个啥 lib,搞个啥编程开发,全部都要自己去写
shell.nix或flake.nix。像我这种愣头青,还不会 nix 语法就直接莽上 NixOS 的就很难受,干个啥都相当于打包(文档还烂)。- nix 语言的学习曲线也是公认的陡峭;而且难的不是语言,而是 builtin functions/variables,查也查不到。例如我想看看
wrapProgram的详细用法,结果到处搜不到文档,最后求助群友问哪里能看详细 manual,群友扔给我 Github 源码… 这种学习的 overheads 不可谓不高。
- nix 语言的学习曲线也是公认的陡峭;而且难的不是语言,而是 builtin functions/variables,查也查不到。例如我想看看
- 我感觉坏处是比好处大的。
- NixOS 管理整个系统,没什么好说。好处是可以玩一些 root on tmpfs 的骚操作,最大的问题是 NixOS 这样搞不支持 FHS 标准,装个啥软件,找个啥 lib,搞个啥编程开发,全部都要自己去写
- 版本回退时会清空应用数据,例如 microsoft edge,telegram。回退其实比想象中要频繁,例如一发 rebuild 在后期失败了,再次回到 old conf 时就算是回退。于是我莫名其妙丢失了许多应用数据与 cookies。
- Nix deriviation path 里的 hash 是 input hash 而不是 output hash,该 hash 是在构建之前就已经计算好的。这意味着:(1)包的构建很可能出现 input 不同而 output 相同的情况,例如只是添加注释。而用户关心的其实是终状态而不是初状态。如果使用 output hash,可以减少一部分不必要的构建。(2)不使用 output hash 就没法实现更高级的功能,例如 p2p 包网络。用户必须有能力进行防伪校验,而这是 input hash 做不到的。虽然 Nix 现在是有一些 output hash 方向的努力(Dynamic Derivation, Ca-derivations),但是还处于相当早期的阶段,而且不能复用 binary cache,那么谁会用这玩意呢。
除了这些特性,还有一些表现:
- Nix 每次更新下载全量 nixpkgs metadata,是一个经典的 40+M 的
.tar.gz,里面包的啥玩意没拆过不知道,但是这量还是有点大的。看看隔壁 Arch,人家 binary 数据库也就 10M 左右。(emmm,不过人家 core 包量级确实小,好像也没得说)
flake
flake 作为一个实验性功能,现在已经成了事实标准。这里随便聊聊 flake 吧。
我可以直接说,flake 就完全是另一套包管理方式。nixpkgs 是单一的版本,所有软件的版本都被锁在唯一 version;而 flake 的最大特征是可嵌套、可独立锁定版本,跟现代编程语言的包管理器比较像。
但是当前 flake 已经暴露出了许多痛点。每个 NixOS 用户的 flake 第一课就是 nixpkgs.follows = "nixpkgs";,如果没有这行,你的 input 就出现了多个 nixpkgs,而每个 nixpkgs 会占用 42MB 的空间(这还是在不计算从这个 pkgs 安装的包的前提下)。并且 nixpkgs 并不是 flake 的唯一 input,有的 flake 有几十个 input,那么你就要写几十次 follows。幸好有 nixpkgs 在自由度方面作出了一些妥协,换来了嵌套层数较少的、行为相对统一的 flake,否则 flake 的最终结果就是被 follows 淹没。
另外,由于 flake 是可变的,每次执行全量的 flake update 时,原有 flake 可能又引用了新的 flake,导致原来的 follows 失效。所以我每次更新系统都需要去找 flake.lock 里的所有 _2 _3 引用,花一堆时间把整颗 flake tree 都 follows 到一致的版本。
当然 flake 也解决了一些问题:
- 由于 flake 严格的 input/output 策略,你能自由 follows 所有子 flake、孙 flake... 的 input。这点是编程语言的包管理器所不具备的,它们只会警告你不要手动编辑 lock 文件。
打包
语言
Arch 用的 PKGBUILD 是简单粗暴 bash。虽然也可以引入个 python 依赖去写 py,但是总归是不方便的。
Nix 自身即高级函数式语言,但是前面提到学习曲线陡峭,加之 Nix 的报错非常模糊,因此我也不喜欢。
更新
Arch 软件包需要自己关注版本更新,并且手动 update version。虽然有例如 updpkgsums 等东西帮我把其他的活干完,但这也只能算是半自动。而且如果不开隔离环境,打出来的包可能在其他人系统上跑不了,因为我有的依赖别人不一定有。
Nix 好一点,Nix 打包默认隔离环境,也就是所有系统上的 build 表现应该是一样的。因此上 nixpkgs 提交软件包只要跑过了 CI 就没啥问题。但是 PR 也都要人来合,不像 Arch AUR 归自己,想 push 就 push。因此也只能算是半自动。
貌似也有一些 bot 提供了简单的全自动实现,例如 Arch 的 lilac,不过我没用过不清楚效果,nixpkgs 这里也不懂有没有 update bot(应该得有吧),反正有个 nixpkgs-merge-bot。
我的灵感
喷了一大通,总算来到了灵感环节。我夜里想到一个 universal package manager 的方案(名字被用了!)。可能有一些尚未考虑到的 cases,如果能帮我指出,我会非常感激。
前端
- 支持所有脚本语言编写构建脚本!与其做一个类似 nix 的万能但难学的语言,不如充分利用现有语言。提供内置的 python, lua, amber, nix, fish, zsh, nu, javascript... 语言解释器的下载。构建脚本继承 PKGBUILD 的简单粗暴,分成几个阶段(pre-, install, post-),每个阶段只需要用对应解释器运行对应代码即可。
- 不防恶意代码,但会在隔离环境构建(bwrap)。
- 所有的
-bin,-git和指定的 version 都算作同一个包,写在同一个 manifest 里面。安装时默认优先安装 bin 包,也可以指定行为。 - 可以用任意结构化格式储存 manifest!json, toml, yaml, ron, 甚至非结构化的 markdown!
- 打包脚本提供少量 builtin 变量,例如
#OUT#,#TMP#,#ARCH#等,执行时直接全局替换完事。也可以选择不使用,而直接在脚本中调库获取。这样可以最小化打包者的心智负担。 - 在打包脚本中写版本更新与测试逻辑!方便使用 CI 全自动更新版本。(经验来自 dependabot)
- 每个打包脚本内都有依赖锁(hash + version),保证依赖版本不变。更新单个包绝不会破坏其上层软件包。
后端
- 类似 Nix,使用 hash 前缀,将所有软件包放在一个地方统一管理!允许多版本共存,并将脚本中指定的内容遵循 FHS 标准链接到标准位置。
统一管理就不用像 pacman 那样存所有软件的安装文件位置。只需存一个 hash 即可。呃,还要存链接位置,差不多- 多版本名字冲突无法链接到同一位置?在链接后加上版本号解决;最高版本优先(无需添加版本号)。
- 显然一次链接几万个文件开销比较大,因此不能像 nix 那样玩 root on tmpfs。
- 链接的目的是什么?让依赖 FHS 的第三方软件/插件能够正常运行。但是通过包管理器本身安装的软件还是正常隔离环境运行的。
- 如果第三方软件不读符号链接?爬。
- 要么要用的人自己写 wrapper,从 store 链;要么就自己打包。
分发
- P2P torrent 网络分发,所有下载了包的人都自动加入上传。
- 为了防止恶意修改包后上传,每个包都会过两种不同的 hash 算法;hash 值是交给中心 server(github) 存储的,不会被篡改。
- (Optional) 只下载不上传?
学习 PT 站经验开个玩笑
基础设施
写一点 converter 把 AUR 和 Nix 那边的现有打包脚本偷来()
external
- 现代软件打包者的安全噩梦,但是我不太赞同
