GitHub ↗

NixOS 学习笔记:从“装系统”到 OS as Code

NixOS 学习笔记:从“装系统”到 OS as Code

本文是对《OS as Code - 我的 NixOS 使用体会》的提炼、改写和实践化整理,不是原文的逐段转载。原文发表于 2024 年,文中关于社区规模、实验特性和版本的判断应视为当时的背景;这里保留的是更长期有效的思路,并补充本仓库的实际工作流。

先记住一句话

NixOS 的价值不只是“软件包多”或“可以回滚”,而是把系统环境尽可能写成一份可审查的程序:

配置源码 + 锁定的依赖
            │
            ▼
        Nix 求值 / 构建
            │
            ▼
    一个可激活的系统 generation

这让系统的安装、更新、迁移和回滚,从一串容易遗忘的手工操作,变成了可以阅读、比较、复现的工程流程。

1. 为什么会需要 NixOS

传统发行版的问题通常不是“今天不能用”,而是长期维护会形成一个没人完全说得清的状态:

  • 手动安装过什么、改过哪些配置,过一段时间后很难回忆;
  • 自动化脚本依赖当时的环境,换机器或升级后容易失效;
  • 快照能保存结果,却不一定保存“这个环境是如何构建出来的知识”;
  • 机器越多、定制越深、更新越频繁,重复劳动和排障成本越高。

Dockerfile、Ansible、虚拟机快照等工具分别解决一部分问题,但它们不一定适合完整桌面系统,也不天然保证所有输入和构建步骤都可复现。

NixOS 适合这样的场景:你愿意把系统当作代码维护,而且系统规模已经足以让“记住自己做过什么”变成负担。只有一台机器、几乎不定制、也不想学习 Nix 时,传统发行版可能更省心。

2. NixOS 的核心模型

声明式,而非命令式

命令式思路是:“执行这些命令,把系统改成目标状态。”

声明式思路是:“这是我希望系统具备的状态,请构建出它。”

例如,服务的重点不再是记住如何手动创建 unit、启用服务、安装依赖,而是把服务及其参数写进模块。NixOS 模块负责把这些声明转换成 systemd、文件、用户组、包和其他底层配置。

函数式与不可变存储

可以把 Nix 的基本直觉理解为:给定相同输入,得到相同输出。软件包和构建结果进入不可变的 /nix/store,不同版本可以并存,系统切换时只是让系统指向新的完整结果。

这不是“任何情况下都百分之百相同”:硬件、网络服务、密钥和用户运行时数据仍然是外部因素。但配置源码、依赖版本和构建过程越完整,系统就越容易复现。

generation、原子切换与回滚

一次成功的 nixos-rebuild switch 会产生一个新的系统 generation。失败的构建不会把系统切到半成品;已经激活的新 generation 出问题时,可以回到上一代,或在启动菜单中选择旧版本。

要注意:generation 主要回滚系统闭包,不会自动回滚浏览器资料、数据库、容器卷、keyring 或 Git 工作树。系统回滚和数据备份是两件事。

Flake 与 flake.lock

Flake 把输入、输出和常用检查组织在一起;flake.lock 则把输入锁定到精确版本。它们共同减少“同一份配置在不同时间得到不同系统”的不确定性。

在本仓库中,flake.nix 注册多个主机,flake.lock 锁定 nixpkgs、Nixarchy 等输入,公共系统配置放在 modules/,硬件和主机差异放在 hosts/。因此它已经是一个很具体的 OS as Code 示例。

3. 软件包:数量不是唯一问题

Nixpkgs 的包很多,而且包、模块和测试都在一个大型社区仓库中协作维护。实际痛点是:NixOS 不遵循传统 Linux 的 FHS 运行时布局,网上下载的预编译程序不一定能直接运行。

建议按这个顺序处理软件:

  1. 先在 Nixpkgs 搜索是否已有包;
  2. 再查 NUR 或社区方案;
  3. 最后自己写 derivation,必要时使用兼容层,但要明确它对可复现性的影响。

自己打包不是 NixOS 的失败状态,而是使用 NixOS 后迟早会掌握的基本能力。它把“某个程序能不能在这台机器上凑合跑”提升为“如何描述它的来源、依赖、构建和安装结果”。

4. NixOS 真正有吸引力的地方

这些能力互相配合,才形成 NixOS 的整体优势:

能力 实际收益
声明式配置 系统状态可以阅读、审查、diff 和分享
锁定输入 更新由你决定,而不是被环境变化推着走
原子构建/激活 构建失败通常不会留下半套新系统
generation 系统层面的快速回退和启动菜单选择
模块系统 用高层选项表达服务,必要时再深入 systemd 或底层实现
不可变 store 多版本依赖可共存,减少全局依赖互相覆盖
Flake 输出 同一套源码可以描述多台机器、开发环境和检查项

“可以复制别人的配置”并不等于盲目复制。硬件文件、磁盘 UUID、CPU 微码、密钥、用户名、显示器和桌面偏好都必须重新审查;可复制的是结构和思路。

5. 必须接受的成本

NixOS 不是免费午餐,学习成本主要来自几个方向:

  • Nix 语言、惰性求值、函数、attribute set 和模块合并规则;
  • 软件包与 NixOS 模块的源码组织方式;
  • 报错有时离真正的问题很远,需要学会缩小求值和构建范围;
  • 许多“看起来只是装个软件”的事情,都要考虑来源、依赖和声明位置;
  • 官方文档、社区文章和不同版本之间可能存在落差。

较好的学习方式不是一开始就理解整个 Nixpkgs,而是逐层下钻:

使用模块选项
  → 阅读模块定义
    → 理解 systemd / package / derivation
      → 自定义模块或打包软件

这也是 NixOS 的一个优点:初学时可以停留在高层接口,遇到需求再深入一层,不必第一天就掌握所有实现细节。

6. 在本仓库里的对应关系

本仓库可以作为学习材料,按下面的地图阅读:

学习概念 本仓库中的位置
Flake 入口与主机输出 flake.nix
锁定依赖 flake.lock
公共系统模块 modules/core.nix、modules/keyd.nix、modules/desktop.nix、modules/zsh.nix
主机差异与硬件 hosts/
日常 build / switch / rollback README.md
更新范围与双层回滚 docs/nixarchy/update-and-rollback.md
NixOS 与 Nixarchy 的安装接线 docs/nixarchy/setup.md

这里还有一个很重要的边界:需要 NixOS 求值、构建或写入 /etc 的内容属于本仓库;用户家目录里的私人偏好属于 ~/chezmoi,可以公开复用的通用配置属于 ~/dotfiles。把边界分清,本身就是可维护性的一部分。

7. 推荐的学习顺序

第一阶段:建立 Nix 直觉

先掌握:表达式、函数、attribute set、列表、let ... in、with、条件表达式、导入文件,以及 nix repl 的基本使用。

目标不是写复杂程序,而是能读懂常见配置:知道一个值从哪里传入、一个函数返回什么、一个模块选项为何会影响多个系统文件。

第二阶段:读懂 Flake

围绕本仓库回答这些问题:

  1. inputs 如何锁定外部依赖?
  2. outputs 如何产生 nixosConfigurations.hx90?
  3. 主机配置如何 import 共享模块?
  4. 修改 modules/core.nix 后,哪些主机会受到影响?

第三阶段:做小改动并验证

每次只做一个可解释的小改动,例如新增一个系统包、打开一个服务或调整一个模块选项:

cd ~/nixos-config
git diff
nix flake check --no-build
nixos-rebuild build --flake .#hx90

build 只构建、不激活。确认输出合理并实际检查系统后,再执行:

sudo nixos-rebuild switch --flake .#hx90

第四阶段:学习排错和回滚

先看求值/构建错误,再判断是配置错误、依赖版本变化、硬件差异还是运行时数据问题。系统刚切换后若出现问题:

sudo nixos-rebuild switch --rollback

如果无法进入桌面,则从 systemd-boot 选择上一代。若问题来自已经提交的锁文件更新,还要通过 Git 恢复或 git revert 锁文件,再重新构建;不要把“系统回滚”误认为“源码和数据也回滚”。

第五阶段:模块化自己的配置

当 configuration.nix 变长时,按职责拆分模块,而不是继续堆叠命令。一个好模块应当:

  • 有清晰的职责;
  • 尽量通过选项表达意图;
  • 把机器专属内容留在 host;
  • 注释说明“为什么这样配”,而不只是重复“配了什么”;
  • 能被 nix flake check 和实际 build 验证。

8. 一套适合本仓库的安全工作流

查看 git 状态
    ↓
修改源文件(不改 /etc/nixos、不改 /nix/store)
    ↓
nix flake check --no-build
    ↓
nixos-rebuild build --flake .#主机名
    ↓
人工检查 diff 与构建结果
    ↓
sudo nixos-rebuild switch --flake .#主机名
    ↓
验证桌面、网络、音频、输入法和关键服务
    ↓
提交可解释的 Git 变更

更新外部输入时,先看 flake.lock 的 diff,再 build,最后 switch。输入更新可能让很多软件重新构建,即使锁文件只改了几行;这属于依赖图的正常结果。

9. 最后形成自己的判断

学习 NixOS 不应只背“它能回滚、能复现”。更有用的问题是:

  • 我的系统是否已经复杂到值得声明式管理?
  • 哪些状态已经写进代码,哪些仍然依赖手工记忆?
  • 换一台硬件不同的机器,我能否解释哪些文件需要重做?
  • 一次升级失败,我是否知道如何回滚系统、锁文件和数据?
  • 我是在复用模块提供的抽象,还是在不必要地重造脚本?

如果这些问题逐渐有了清晰答案,NixOS 就不再只是“一个配置很怪的发行版”,而会变成一套管理复杂 Linux 环境的方法。

参考