AnduinOS

给大伙简单demo一下anduinos

AnduinOS 的官网在 https://www.anduinos.com 。它在 2024 年 9 月 1 日发布了第一个正式版。现在它的完成度已经非常高,可以用于日常工作和学习。而且界面非常让 Windows 用户感到熟悉。它的安装过程也非常简单,只需要下载一个 iso 文件,然后用制作一个启动盘,插入电脑,重启,就能安装了。

为什么有这个项目:最开始源于 Windows,就是给电脑写一键初始化脚本。后来开始用Linux。后来想把初始化好的配置分享,就开始分享脚本。突然有一天,听了好友的意见,装了 Arch 和 nixos,觉得安装系统和出发行版本质上不是一件事嘛,那我完全可以自己出个发行版玩玩。

我做这件事不是因为它容易,而是我以为它容易。我最开始问 codgician 出发行版难不难。他说每个 nixos,gentoo用户,都相当于在用自己编译的 Linux。既然很多人都能做到,你也做到就不难。

我信了他的邪。我又问他出发行版最累的是什么?他说做一个好用的安装程序和分区工具呗。能引导用户把系统装上不就完了。是啊,我回想了一下我安装Arch的经历,无非就是自己手工分好区,然后把接下来要跑的命令都做好一键自动化命令,不就是能用了吗?处理的一些细节无非就是:

磁盘管理,配置日期时间,本地化,机器名,网络栈,DNS,grub引导,root密码,默认用户,包管理,证书和签名,硬件的微码,display server,desktop env,window manager,acpi 事件,休眠,听歌,防火墙,键盘鼠标数位板,指纹麦克摄像头,字体终端打印机,等等亿点点工作罢了。

抱着这样的想法,我打算三天把这个项目搞定。实际上这是我最大的错误。我花费了整整一个月,160小时时间,你看相当于平均每天要在这个项目上花费6小时的时间。所以基本上我没有一天在凌晨3点前睡觉。其实主要时间都是浪费在了测试上。我为了知道我一个变更靠谱不靠谱,我不但要改代码,还要等一次完整的编译,得到 ISO,再创建出虚拟机,跑完整个安装程序,然后才能看到运行结果。基本上测一次就是10分钟。

我真的和不少人仔细探讨过这个问题,就是出一个Linux发行版究竟多难。大家的答案基本上都是:如果不打算自己维护软件源的话,很简单。三五天。

简单是因为:Linux已经太成熟了。把内核搞过来,弄个包管理,装个sh就有命令行了。装个gdm,装个gnome,装个mutter,图形界面已经来了。装几个应用已经能用了。手搓操作系统,一天不是梦。

难是因为:你要想保证你这个操作系统可以安全稳定快速的持续运行下去,你得一直有稳定的版本、持续的安全更新,这背后就需要你充分理解用户可能在这个操作系统上干出来多少事情、安装多少软件、这些软件都有哪些分支、都是如何管理质量的、怎么从中避免breaking change的同时、如何修复它们的vulnerbility ……这就不是十几个人一两个下午搞得定的了。

这里我可以简单科普一下,就是Linux的发行版大体有两种,一种是像Arch这种滚动更新的,每个包随时可以更新,更新混合着新功能和修正。当然可能也会有breaking change。这种系统往往能享受到非常新的功能和内容。还有一种就是像Ubuntu这种固定发布周期的,每隔几年出一个版本,将主要依赖的软件版本锁死,更新基本上都是安全更新和错误修正的。

我还是希望我的操作系统是一个门槛比较低,比较稳定,用的包的来源比较流行,不常有breaking change,甚至可以用于工业场景的。所以我就选择了蹭Ubuntu的基础设施。

我最开始的尝试非常简单,就是下载Ubuntu server,找到里面 squashfs 的文件,把它mount了,稍微改改装几个软件,再压回去,完事儿了压成一个ISO,应该三天搞定不难吧?

实践证明这么干是完全可行的。Ubuntu server我只需要装个gnome-shell它就原地有了桌面环境了。拿出去也是绝对能用的。

但是我发现这样干有非常多的问题:首先Ubuntu已经很大了,它的安装过程也不是简单的像Windows一样解压,而是现场开一个基于 cdrom 的apt服务器,现场格式化好盘,拷进去apt,然后现场chroot进去,apt install软件。就像Arch的安装程序一样。只是软件源是离线的。

我如果想基于Ubuntu server魔改,我还得自己把软件塞进它的光盘apt服务器里,再魔改它的安装器。我觉得这不像是推出一个Linux发行版应该的样子。我还是觉得更应该from scratch一些。这样完全可以控制整个过程,也不怕被 Ubuntu 卡脖子了。

于是,我想出来了一个非常奇怪的思路。codgician 发给了我很多 Debian 的工具,指出基于 Debian 制作自己的 Linux 发行版是非常成熟,而且有套路的。文档也都写得很好。我完全可以直接用 Debian 的工具。而 Ubuntu 的软件又都比较流行和稳定。可以结合一下。

于是最终 AnduinOS 的定位就变得很有趣了。它确实是一个基于 Debian 的 Linux 发行版,但是我们想想看,不同 Linux 发行版都有什么区别呢?无非就是预装的软件不一样,默认包管理可能不一样,软件源,目录结构可能不一样。如果我开局就魔改 Debian 把包的来源换成Ubuntu的,这样我的发行版就能安装Ubuntu的软件。这个系统确实是 Debian 的派生,但是蹭了 Ubuntu 的软件源。我自己真正需要做的工作就很少了。

不过,这时我的系统究竟基于谁这个问题,一般的,考虑到它的使用体验,软件生态,内核版本都非常接近 Ubuntu,我也会自己找 Ubuntu 的社区去 Debug 我自己系统的问题。所以我也不纠结这究竟是基于 Debian 还是基于 Ubuntu 的了。这个问题本来也是模糊的。甚至很多配置文件我还会故意让它假装自己是个 Ubuntu,来保证许多面向 Ubuntu 的软件在上面可以不变更就能继续使用。

有了上面的基本的原则,我就可以开始构建我的系统了。

我们都知道,一个 Linux 要想启动,实际上是非常容易的。我们只需要一个 initrd,也就是 initramfs ,一个 vmlinuz,也就是内核,就已经是一个操作系统了。所以,我 from scratch 去 build AnduinOS 的思路非常简单:像 Ubuntu,Arch 那样,写一个安装器,安装器里启动一个 live CD,引导用户分好磁盘,弄出来 boot 分区和根分区,然后 chroot 进去,安装好内核 vmlinuz,安装好 initramfs,安装好 grub,安装好用户空间,安装好软件包,安装好配置文件,安装好用户,安装好桌面环境,就已经能用了。

这里使用了一个非常重要的 Linux 概念: chroot 。 chroot 是一个 Linux 命令,它可以改变当前进程的根目录。这样我们就可以在一个目录里面,看到这个目录是整个文件系统的根目录。这样我们就可以在这个目录里面安装软件,配置系统。

但是,我是个懒汉。我又是个 Windows 老用户。我觉得这个过程太复杂了。我不想写一个安装器。你看人家 Windows 的安装方法就非常先进对吧,整个系统都在 install.wim 里,直接把它解压到硬盘上就可以用了。我就想,我能不能也这样做呢?当然我发现这么干是非常容易而且很合理的。我的安装程序本质上就是个解压器,帮用户解压好我已经预配置好的系统就行了。

这里可以看到现在 anduinos 的构建过程:非常偷懒:它的构建方法就是把我已经配好的,测过的,能用的系统,直接根分区压缩成一个 squashfs,用户需要安装的时候把它解压就能用了。这样我的安装程序可以省去本地启动apt服务器这种繁琐的工作。开发和测试都好做多了。

当然它如今就是 from scratch 来构建的。我不会真的手工搓出来上面提到的所谓“能用的系统”。我会先新建一个文件夹,将最重要的工具,例如apt,sh等拷贝进去,建好bin,usr,var等目录,把dev,proc,sys等目录直接mount我主机的,这时已经可以chroot进去了。chroot进去以后,我就可以用apt安装软件,配置系统,安装内核,安装grub,安装用户,安装桌面环境,安装软件包,安装配置文件,安装用户,安装桌面环境,就能用了。最后我退出 chroot,再把这个文件夹压缩成一个 squashfs,这就是我要的系统。

我们为了演示一下 AnduinOS 的源码是如何工作的,我可以在这里真的 build 一次。这没有问题。

但是上面的过程听上去优点很多,但其实坑是非常多的。安装过程 = 解压文件,这个不假。但是我还是需要一个安装器,安装器需要引导用户配置语言、时区、密码、分区这些东西。为了让用户插好光盘可以启动安装器,我仍然需要额外构建一个 Live CD。

但是我仔细想想这件事我就觉得很奇怪:我为了构造一个 Linux 发行版,我还得先构建一个 Linux 发行版。这不成了自我指涉了吗?我们都知道在 Windows 里解决这个问题是非常优雅的:它有两个文件 boot.wim 和 install.wim ,install.wim 是真正需要解压的系统,boot.wim 是一个小系统,只有一个安装器。这样用户插好光盘,启动安装器,安装器解压 install.wim 到硬盘上,重启,就能用了。

我试过使用 Windows 的这个思路,但是很快我发现我的安装器和主系统的 squashfs 是非常类似的。如果我也像 Windows 一样塞进去两个巨大无比的 squashfs,我的 iso 文件就变得巨大无比了。所以这里我只能继续 hack:我仍然只用同一个 squashfs,但是启动的时候加不同的参数,来区分安装器和主系统。

这样仍然没有违背 AnduinOS 容易测试的优势,仍然是只有一个文件,只需要不同的参数,就能控制它启动以后是否是安装器。当然,这个思路听着优势很多,但是也需要继续 Hack 。

例如我显然需要在 squashfs 里集成安装器,而安装好的系统肯定不需要安装器。我只能去魔改安装器本身,让它在完成解压了以后,再去 chroot 到部署好的系统里,除了要创建用户,修改语言,修改时区,修改 apt mirror 之外,我还得把安装器自己给删掉。这样用户重启以后就不会再看到安装器了。

实际的工作过程比我描述的要复杂痛苦的多。比如 ISO 光盘本身启动也需要 vmlinuz 和 initrd,它们又都需要对 initrd 进行大量的自定义才能正确理解我上面传入的参数,执行不同的脚本,还得自己卸载掉自己。这部分代码使用了大量的 shell 脚本,我已经感受到它非常难以理解了。

但是幸运且有趣的就是,最终 build 出来的 iso 很 work,就三个大文件:vmlinuz,initrd,squashfs,就能启动,就能安装,就能用。Codgician 安慰我说,没事儿,自己做系统是这样的,全是 Hack。NixOS 里的 Hack 比你这个多多了。所以最终来看,我这个非常 Hack 的解压缩式安装,还是非常成功而且有效的。

折腾安装器的过程确实是让我在自己搓发行版的过程里学到东西最多的过程。我完全理解了 Linux 整个设计哲学里贯穿始终的自我指涉问题和它的解决方法。比如 Linux 的文件系统本身也是文件,而要想读取这些文件又需要一个文件系统。再例如我也接触了很多根分区加密,毕竟解密的程序本身也需要保护自己。

这些过程让我理解了 Linux 的启动过程为什么要设计成两个阶段,需要一次 chroot,这样可以在第一个阶段先加载一些重要的设备驱动,解锁磁盘的根分区,然后再进入第二个阶段,加载所有的驱动,启动所有的服务。这样可以在折腾很多例如 live环境,根分区加密,根分区换文件系统,根分区 raid0 等等问题的时候非常容易。同样,也让我制作这种压缩式安装和安装器主系统共享一个根分区变得可能。

基于上面的知识,我已经可以非常熟练的从什么都没有的状态原地搓出来一个能用,能安装,能搓出来 iso 安装盘的 Linux 发行版了。这个过程现在也都是完全自动化的了。我为了方便以后大家想出自己的发行版,也做好了模组化。很容易的允许用户自定义软件包,来构建一个发行版了。这些过程也都花了很多晚上重构了好多版本的模组功能。

这些模组其实我们真的仔细看看它们在做什么,无非就是在打 patch 。patch 主要面向的就是 Gnome,我们对 Gnome 打了大量的 patch。AnduinOS 的界面逻辑我们今天来看是非常接近 Windows 11 的。这都是通过大量的 Gnome Patch 实现的。

说来也有趣,这个桌面体验最开始还是 Codgician 在 KDE 上花了几分钟搓出来,向我炫耀说:看,KDE 只需要拖拖鼠标就能搞出来一个 Windows 11 的界面。我当时就觉得这个界面非常好看,但是我自己更喜欢 Gnome,我就把它在 Gnome 上也搞出来了。想来这都是快一年前的事情了。

实际上,我自己的感受也正是这样:Windows 的 UI 元素逻辑是非常合理的。它没有顶部的 top panel,这使得我可以盲点屏幕的右上角来关闭应用。同样,我也需要一个始终展示的任务栏,让我能够随时知道当前专注的应用。如果任务栏放左侧,又没有一个舒服的地方展示通知、音量、网络等。尤其是放在左侧以后屏幕就不再左右对称。最终我只能把任务栏放在底下。

这些都是 Windows 的理念,我认为这些理念都是非常合理的。它设计非常合理,专注的应用正好在屏幕中央下方,上方的控制按钮完全可以盲点,不常用的按钮,例如修改音量,亮度,通知,网络,主题等都可以一次鼠标就能呼出。这种设计也都延续到了 AnduinOS 上。

另外大家可能都知道,AnduinOS 是有一个应用商店的。但是这个应用商店做得非常凑合。就是我收录了一大堆软件的安装方法,做了个网页,就算是应用商店了。这里受到过很多批评:毕竟我们没有人是专门为了一个操作系统而去用一个操作系统的。我们从来都是为了运行这个操作系统里的应用程序。因此有一个非常好用的应用商店非常重要。

很多人劝我说,既然你把安装方法都收录好了,为什么不干脆做个安装CLI,比如 APKG,以后安装 VSCode,直接跑 sudo apkg install vsocde 就行了。其实我想过这件事,如果我这么搞,相当于搞了一个winget类似的东西。

winget 这个包管理我说老实话觉得它做得并不好。它并没有自己分发,而是完全基于软件本身的供应商来分发的,所以它就是一个安装索引器。但是一个索引器它又做得太弱了。它连依赖管理做的都很差。如果我模仿 winget,我一定会对这个东西做得也非常不满意。

比如用户可能自己通过解压安装了appimage,flatpak,snap的软件,我的 apkg 可能检测不到。或者用 apkg 安装的软件,再用 apt 卸载了,我的 apkg 又检测不到等等。我还需要向用户解释为什么 gnome-chess 这个软件竟然在 AnduinOS 有两种安装方法,还各有利弊。这样产生的太多奇奇怪怪的问题,我觉得我还是很难解决的好。

就像我想,真正解决的好,还是需要用真正的包管理。包管理又面临两个方向:Monolithic 还是基于文件的管理。如果我选择了左边 Monolithc,我写的肯定不如人家 snap,flatpak,docker 写得好。而如果我选择了全局文件的包管理,我又肯定不如人家 apt,dnf, pacman写得好。两条路无论我怎么走都是重复造轮子,还做得不好。这件事听着就很尴尬。

我在思考这一点的时候,也明白了一件事,就是为什么现在几乎所有 Linux 发行版都开始推行 Monolithc 的包管理,比如 Ubuntu 在推 Snap,Fedora Workstation 在推 Flatpak。这是因为这种包管理的优势非常明显:它可以在用户安装了各种千奇百怪的软件了以后不会弄乱整个系统。而系统里一个包的升降级也不会影响到软件本身的行为。虽然我知道这种包管理的劣势也很明显:它会占用很多磁盘空间,会有很多重复的库。但是它确实能够非常完美的解决好对用户安装的应用的可靠性和管理的问题。

所以到了最后,我还是决定暂时不管理用户安装的软件。换句话说,AnduinOS 的用户想用什么包管理,无论是 Snap,Flatpak,AppImage,还是 Apt,Dnf,甚至 Nix,我觉得都先交给用户自己选择。我只是提供一个软件的安装方法的文档索引,让用户自己去选择。

...

(secure boot)