缘起#
上一篇介绍了我重新使用NixOS的缘起。使用NixOS时,经常会遇到Nix、nixpkgs、flake、derivation和generation等名称,它们分别涉及包管理、配置输入、构建和系统版本。
这里介绍几个常用概念,并结合配置和命令说明它们的用途,方便后面阅读和修改配置。不需要提前学完Nix语言,可以先从下面的例子开始。
声明式配置#
手动配置与声明式配置#
在Fedora上配置服务时,一般需要先安装软件包,再编辑配置文件,然后启用服务。如果要让其他电脑访问,还需要检查防火墙。
这种方式需要分别记录安装和配置步骤。几个月后在另一台电脑上重新配置时,如果没有完整记录,就容易遗漏某个文件或命令;软件版本变化也可能带来不同的默认设置。
NixOS采用的是声明式配置(declarative configuration):把希望系统有什么软件、启用什么服务、使用哪些设置,写在配置文件里。系统根据这些配置准备所需的软件和文件。
例如,想启用Tailscale,可以在NixOS模块中写:
| |
这里的意思是启用Tailscale服务。对应的NixOS模块会处理软件包和systemd服务的设置,我们不需要再手动安装包、创建服务文件。不过,首次使用时的账号认证等操作仍然需要完成。
再比如,允许TCP 22端口通过防火墙:
| |
NixOS会根据配置的防火墙后端生成规则。这里仅设置端口放行,SSH服务本身还要另外启用。
这样,一个系统的很多设置就可以集中保存在文件中,用Git记录修改。换电脑时,公共部分可以复用,硬件相关的部分再单独处理。当然,配置文件不会包含用户文档和数据库内容,这些数据仍然需要迁移或恢复。
Nix与/nix/store#
Nix是包管理工具,NixOS是使用Nix管理系统的Linux发行版,配置文件所使用的语言也叫Nix。阅读文档时,需要根据上下文区分包管理工具和配置语言。
在NixOS上,你会经常看到这样的路径:
| |
软件包通常保存在这里。前面是一段哈希,后面是名称。升级软件时,Nix会增加一个新的store路径,而不是直接把原来的包覆盖掉。这样,新旧版本可以同时保留,不同程序也可以使用各自需要的依赖版本。
如果所需的软件已经在store里,就可以直接复用;如果可信的二进制缓存中有对应产物,就可以下载,不必自己编译。
这里的哈希不能简单理解为最终文件内容的指纹。derivation描述构建配方和输入,普通derivation输出的路径通常根据这些信息计算,称为输入寻址。因此,路径相同不能单独证明两次构建的结果逐字节相同。Nix也支持内容寻址对象,详细区别可以参看Nix store路径规范。对于后面的回滚,先了解新旧软件包可以保存在不同路径下即可。
整个NixOS系统也有一个store路径。这个系统和它需要的所有依赖合起来,称为闭包(closure)。只要旧系统的这些路径还在,就有条件重新使用它。
nixpkgs#
nixpkgs是软件包定义和NixOS模块的集合。比如一个软件要从哪里下载源码、需要什么依赖、怎样构建,以及一个服务有哪些配置选项,都可以在这里找到对应定义。
配置文件中可用的软件和选项,也与nixpkgs版本有关。例如,按照教程配置后出现“选项不存在”的报错,除了检查拼写,还需要检查教程所用的nixpkgs版本是否与当前锁定的版本一致。
flake与flake.lock#
flake的输入与输出#
如果配置文件依赖nixpkgs、Home Manager和其他模块,就需要说明这些依赖从哪里来。flake把这部分称为输入(inputs),把可以得到的结果称为输出(outputs)。
对于系统配置,输出通常包括几台电脑各自的NixOS配置。例如,假设flake中有一个叫laptop的主机输出,可以用下面的命令应用它:
| |
/path/to/nixos-config是仓库目录,#后面的laptop是选择的输出名,不一定与网络主机名相同。这里都是示例名称,使用时要换成自己的值。
同一个仓库可以有多个输出,所以不需要为每台电脑单独复制一整份仓库。下一篇会介绍具体结构。
使用flake.lock锁定输入#
假设我们在flake里写了跟随nixpkgs的某个分支。这个分支会继续更新,今天和下个月拿到的版本可能不同。如果只记住分支名,就很难知道上一次构建到底用了什么。
flake.lock用于记录输入的具体版本和哈希。只要沿用这份锁文件,重建时就会继续使用锁定的输入,而不是自动跟随分支的最新内容。因此,flake.lock也应当提交到Git里。
想主动更新依赖时,可以运行:
| |
先看一下更新了哪些输入,再决定是否构建。锁文件方便我们重复使用相同输入,但不会替我们备份远程源码或应用数据。
构建、激活与回滚#
配置文件修改完,还需要让它生效。这个过程大致有几步:
- 评估:读取Nix文件,组合模块和选项,生成构建说明。语法错误、某些选项冲突等会在这里报错。
- 构建:准备系统需要的软件和文件,已有产物可以复用,也可以从缓存下载。
- 激活:把新配置应用到运行中的系统,包括修改托管文件、处理受影响的服务等。
- 重启后的检查:如果更新了内核或启动相关配置,还需要重启,确认新系统能够正常启动。
检查、构建和激活对应不同的操作,可以使用以下命令:
| |
不加--no-build时,nix flake check还会构建flake里的checks,具体可以参看命令说明。这些检查能发现很多问题,但服务到底能不能用,仍然要实际试一下。
NixOS的系统profile会保留一些代次(generation),可以查看:
| |
switch之后如果发现配置有问题,可以检查上一个代次是不是需要的版本,再回滚:
| |
如果新系统无法启动,也可以从启动菜单选择保留的旧代次。恢复旧内核则需要重新启动那个内核。
这里要注意,系统回滚不是整块磁盘的快照恢复。例如服务升级时已经修改了数据库,退回旧软件不一定能直接使用更新后的数据。另外,激活涉及多个操作,中途失败时也可能已经应用了一部分修改。
旧代次占用的store路径可以通过垃圾回收清理,但清理后就不能再依赖这些路径回滚了。关于临时测试怎么恢复、什么时候需要重启,以及如何清理旧系统,第五篇会结合一个小例子详细介绍。
有了这些概念,接下来就可以看一个flake如何组织多台电脑的配置了。
