自建发行版的折腾与反思

起因

当初创建自己的发行版,目的很简单——想完全掌控自己的电脑。

对于包管理工具而言,正常情况下更新都跟着包管理工具走。但在默认以二进制分发的发行版上,你无法掌握包的编译参数是否适合你的机器,也无法确认编译产物能否在你的硬件上正常运行。这也是滚动发行版可能存在的风险之一。

从 LFS 到自建包管理工具

很早之前就尝试过 LFS,前后编译过几次,但受限于当时的能力和生产力,多次卡在引导阶段,而自己并没有足够的认知来理解所处的困境。同时也逐渐意识到:LFS 没有包管理工具,终将是一艘驶向沉没的大船。

如今我几乎尝试过所有主流发行版,理解了不同发行版的特性。再加上 LLM 的能力加持,我觉得自己已经具备了创建一个发行版的条件——前提是拥有自己的包管理工具。

于是我借助 LLM 快速搭建了 MVP(Minimum Viable Product),功能很简单:内置类似 bubblewrap 的沙箱隔离机制,以及 SQLite 数据库。之所以选择内置这些,是因为我认识到通过文件系统的树状结构去检索文本,终将是低效且能力受限的。而关于「为什么要内置」,源于我最初的设计愿景——静态链接、无外部依赖。如果包管理工具本身不是自包含的,在 stage2 阶段(为目标机器构建临时工具链)会引入不必要的复杂度。同时由于是交叉编译,宿主机编译出的动态链接库和目标机器可能不一致。以下是我内置 glibc 的编译方式:

sh
RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu

最初我比较理想化,想用 musl + gcc 的方案。但最终意识到 gcc 默认搭档是 glibc,我尝试了多次编译 musl + gcc 的组合,为此消耗了大量时间和精力,结果却不尽人意。最终还是选择了 glibc + gcc。同时我也认识到,在某些决策上,经验比 LLM 的概率猜测更可靠——比如 init 进程的管理,我也选择了 systemd,不再理想化地去折腾 runit 之类的方案。

构建与引导

这次我把 LFS 手册和包管理工具的相关文档一起投喂给 LLM,基本上一路顺畅,构建完成后可以直接引导启动。尽管 systemd 引导时出现了不少错误,后续再通过 chroot 构建安装 firmware,也都逐一解决了。

再之后陆续解决了网卡驱动和代理的问题。这个过程令人烦躁——每次都要重启,如果有问题就得重启回宿主机,重新挂载,再进去测试。

图形环境与浏览器

接下来为目标机器构建 niri 及其所需依赖。图形相关的依赖让 LLM 参照 BLFS 手册来走,过程有些坎坷,但好在都解决了。

再往后,我准备在目标机上投入生产。发现 flatpak 的安装似乎存在问题——尽管我尽可能地把 dbus、portal 等依赖都处理了,还是无法正常使用。但我想尽快在目标机上进入生产状态,浏览器又是不可或缺的。flatpak 用不了,我就回到宿主机开始编译 GNOME Web(Epiphany)。

这个过程的依赖非常恼人,尤其是 WebKitGTK——经常把我的电脑内存撑爆导致卡死。后来改进了包管理工具做了资源限制,但整个编译过程依然又臭又长。

好不容易编译好了 Epiphany,启动后发现 niri 又崩了。这次我脱离手册,自行构建 Mesa 及其依赖。这时才真正意识到依赖关系的复杂程度:比如 SPIR-V 系列的依赖中,headers 和 tools 的版本要保持一致,spirv-llvm-translator 的大版本要和 LLVM 保持一致……维护这么多版本关系本身就是一种认知负担。最终构建出的 Mesa 也是经过多轮失败后才成功的。Mesa 本身并不小,因此不断失败重来的过程既无聊又痛苦。好在最终结果符合预期,niri 再次可用。

关于浏览器,我后来才意识到没必要自己编译——Firefox 提供了官方二进制包,前提只是需要编译好 GTK3。这个我之前已经做过了,但总觉得不够好,于是又尝试让 LLM 帮我重新构建。这个过程中我经历了地狱般的循环依赖——层层嵌套、反复失败。

反思

在不断失败和自我怀疑中,我逐渐意识到几件事:

现在很多发行版本质上只是建立在 stage3 之上的系统。而很多包管理工具并没有支持不同 stage 的概念,在处理包的时候也缺乏灵活性——这不是构建脚本灵活与否的问题,而是包管理工具本身灵活与否的问题。

由此我也构想了让自己的包管理工具支持多 phase 构建:让它遍历所有可能性,确保存在无循环依赖(无环)的构建路径——如果存在,那么这个构建就是可行的,包管理工具可以自行解析并构建依赖。设计虽好,但到这个时候我已经意识到了种种问题:

其一,包管理工具在后期会成为包袱。一旦更改配置方式,往往牵涉成百上千个配置文件。其二,自己维护这些繁琐的依赖关系代价极大,即便我后来设想了用 flatpak 配合系统基座的方案。其三,自行编译系统带来的时间和资源消耗也令人困扰——任何一个基础依赖的 ABI 变更,都会连锁导致大量上层包的重新编译。

我最开始的目的很纯粹也很开心:用一个包管理工具去跟踪文件,需要时可以干净地删除它们。但随着问题不断涌现,我逐渐发现了包管理工具定位的尴尬之处——任何一个发行版只需要自己的包管理工具加上打包文件,修改相关参数就可以自称一个新发行版。可意义是什么?如果没人用,就只是自己在维系这些复杂的依赖,自己打包、编译、测试,独自消耗心神罢了。

而且,包管理工具之于系统如此重要,却在表达系统的不同阶段和状态上如此无力。不同的包各自为政,它们在构建中并不互通有无,也不会通过包管理工具分析系统状态。我觉得包管理工具在跟踪系统状态这件事上,终究是相对脆弱的。

后续

感想就到这里。后续我会深入研究 Guix 和 NixOS——之前只是作为使用者浅尝辄止,现在准备从实现层面认真看看。

Comments (0)

No comments yet. Be the first to share your thoughts!