↓ 跳过正文

NixOS(五):一次配置变更的生命周期——评估、构建、激活与验证

·3703 字·8 分钟
锦李本鲤
作者
锦李本鲤
光锥之内,皆为命运。
NixOS系列 - 这篇文章属于一个选集。
§ 5: 本文

缘起
#

前面几篇提到了nix flake check、nixos-rebuild build、test、switch等命令。它们看起来都和“更新系统”有关,但作用并不一样。有的只是检查配置,有的把系统构建出来,还有的会立即修改正在运行的系统。

如果每次都直接运行sudo nixos-rebuild switch,平时可能也能正常使用。不过一旦遇到构建报错、服务启动失败,或者更新后不知道要不要重启,就需要分清楚这些步骤了。

这里用一个很小的例子,把修改配置到实际生效的过程走一遍。之后让AI帮忙配置系统时,也比较容易看懂它到底做到哪一步。

准备一个简单的例子
#

假设我们已经有一个可以正常构建的NixOS flake,主机输出叫作laptop,对应的模块文件是hosts/laptop/configuration.nix。下面的路径和名字都是示例,使用时请换成自己的。

这次只做一件事:让NixOS创建一个/etc/nixos-lifecycle-demo文件,内容为version=1。假设这个文件原本不存在,我们就可以通过查看它,判断配置什么时候真正生效。

修改配置
#

先进入配置仓库,看一下有没有之前没提交的修改:

1
2
3
4
cd /path/to/nixos-config
git status --short
git diff
git diff --cached

然后在已有主机模块的配置属性集中,也就是配置的花括号内,加入下面一行:

1
environment.etc."nixos-lifecycle-demo".text = "version=1\n";

这里的environment.etc用来声明由NixOS管理的/etc文件,nixos-lifecycle-demo是文件名,text是文件内容,末尾的\n表示换行。

保存之后,/etc下面还不会出现这个文件。我们只是改好了配置,还没有应用它。

本例不需要更新flake.lock。添加一个设置时就把全部依赖一起更新,虽然也能做,但如果出了问题,就不容易知道是新设置还是依赖更新造成的。

如果是在已有文件里修改,不提交也能让本地Git flake读到改动。如果另外新建了模块,则要把它导入,并先用git add加入索引,否则Nix可能看不到这个新文件。详细说明可以参看flake文档。

检查配置
#

修改完后,先检查一下:

1
2
git diff --check
nix flake check --no-build --show-trace --no-update-lock-file

第一条检查diff中的一些格式问题。第二条会读取flake,组合各个模块和选项,检查配置能否通过评估(evaluation)。例如选项名写错了、两个地方的设置冲突了,可能就会在这里报错。

几个参数的意思是:

  • --no-build:不构建flake声明的checks;
  • --show-trace:出错时显示调用信息,方便查找错误来自哪里;
  • --no-update-lock-file:如果这次操作需要改变锁文件,就报错,而不是顺便更新它。

不加--no-build时,命令还会构建checks,具体见nix flake check说明。另外,评估时可能需要下载缺失的输入,所以不一定完全离线,也不一定很快。

这一步通过后,说明Nix能处理被检查的配置。编译是否成功、服务是否能用,还要往下看。

构建系统
#

接着构建laptop的系统:

1
nixos-rebuild build --flake .#laptop --no-update-lock-file

这里的.表示当前仓库,#laptop选择其中的主机输出。build只生成系统产物,不应用到正在运行的系统,因此不需要为了激活而使用sudo。

构建完成后,当前目录下会有一个result符号链接,可以查看它指向哪里:

1
readlink -f result

它会指向/nix/store中的一个系统目录。这个系统及其所有依赖合起来,就是前面说过的闭包(closure)。

“构建系统”听起来好像要把整个操作系统重新编译一遍,其实大部分时候没有那么夸张。已有的store路径可以复用,有缓存的产物可以下载,确实缺少的部分才需要构建。本例主要是生成一个配置文件,与更新内核的工作量很不一样。

也可以比较新产物和当前系统的软件包、闭包变化:

1
nix store diff-closures /run/current-system ./result

这个命令适合看看软件包版本和闭包大小的变化,不会逐行列出配置文件的差异。本例具体改了什么,还是看Git的diff最清楚。

至此,系统产物已经有了,但/etc/nixos-lifecycle-demo仍然不会出现,因为还没有激活。

构建很久怎么办
#

是否需要很久,和这次改了什么有关。迁移后的某次内核重建花了四个多小时,峰值磁盘占用约45GiB,其中一次尝试接近结束时因为空间不足失败。普通配置修改不需要按这个规模准备,但更新输入可能触发比预期更大的构建。

所以遇到内核或大量依赖更新时,最好先看看磁盘余量,安排一段不会被打断的时间。AI可以帮忙查日志和监控进度,但编译时间和磁盘空间还是省不掉的。

应用配置
#

先用test试一下
#

如果想先看看效果,可以使用test。在执行之前,先把当前系统路径记在变量里,后面恢复时会用到:

1
2
previous_system=$(readlink -f /run/current-system)
sudo nixos-rebuild test --flake .#laptop --no-update-lock-file

现在再读取文件:

1
cat /etc/nixos-lifecycle-demo

应该能看到:

1
version=1

这时配置已经生效了。文件由NixOS管理,通常通过链接指向store里的内容。如果以后要修改它,应修改Nix配置,再重新应用。

test的特点是立即激活,但不改变默认启动配置。也就是说,如果只是试用了一个配置,重启后通常会回到原来的默认启动项。

不过,“试用”也会真的改动系统。服务可能被重启,网络配置可能断开连接,程序也可能写入数据。它不会自动在几分钟后撤销,所以远程改网络时,仍然需要考虑连接断开后怎么恢复。激活中途报错时,也可能已经应用了部分修改,应先看报错和实际状态。

确认后用switch
#

文件内容正确之后,可以执行:

1
2
sudo nixos-rebuild switch --flake .#laptop --no-update-lock-file
cat /etc/nixos-lifecycle-demo

switch会应用配置,并把它设为默认启动配置。这样以后重启,仍然会使用这份设置。本例只是增加文件,不需要为了让它生效而重启。

build、test和switch之间,最好保持配置与锁文件不变。后两条命令会再次评估和准备系统,并不是直接拿之前的result来执行。如果输入没有变,通常会复用已经生成的产物。

boot有什么用
#

如果希望先把新配置准备好,等下次启动时再使用,可以运行:

1
sudo nixos-rebuild boot --flake .#laptop --no-update-lock-file

它会设置默认启动配置,但不立即激活。几个命令的区别可以放在一起看:

命令准备系统产物立即激活设置默认启动配置
build是否否
test是是否
switch是是是
boot是否是

这里“准备产物”包括复用已有结果、下载缓存和构建,不一定都需要编译。nixos-rebuild文档也有这些命令的说明。

什么时候需要重启
#

上面的文件在激活后就能使用。但如果更新的是内核或者早期启动配置,就需要重启后才能检查。

可以先看看这两个路径:

1
2
readlink -f /run/current-system
readlink -f /run/booted-system

current-system表示当前已激活的系统配置,booted-system表示本次启动使用的配置。比如本例执行过switch,但还没重启,两者就可能不同。这是正常的,并不意味着每次看到不同都需要重启。

如果想确认差异是否涉及内核,还可以看:

1
2
3
readlink -f /run/current-system/kernel
readlink -f /run/booted-system/kernel
uname -r

不同的系统配置可以使用同一个内核;反过来,两个不同内核构建也可能显示相同版本号,所以只看uname -r还不够。

重启之后,可以再检查这些路径,同时看看有没有失败的服务:

1
2
systemctl --failed --no-pager
systemctl --user --failed --no-pager

第二条应在对应用户的会话中执行。最后还要实际使用一下修改的功能:本例可以再次读文件,改了网络服务就试着连接,改了驱动就操作一下设备。没有failed服务不代表所有功能都检查过了。

配置改坏了怎么恢复
#

只执行了test
#

前面保存的previous_system仍然指向一个存在的旧系统时,可以在同一个Shell里执行:

1
sudo "$previous_system/bin/switch-to-configuration" test

这样会重新激活之前保存的系统,并保持默认启动配置不变。使用前确认变量指向的是想恢复的版本,测试期间也不要把旧系统路径清理掉。

重启也是一种回到默认启动配置的方法,不过默认启动配置不一定与变量里保存的版本相同。

已经执行了switch
#

先看看保留了哪些代次:

1
sudo nixos-rebuild list-generations

如果上一个profile代次正是要恢复的版本,可以回滚:

1
sudo nixos-rebuild switch --rollback

这里有个容易弄混的地方:test不会推进系统profile,所以switch --rollback不一定等于“撤销刚才的test”,它可能退到更早的代次。要按自己之前执行过的操作选择恢复方法。

新系统无法启动时,可以在启动菜单中选择保留的旧代次;需要恢复旧内核时,也要重新启动那个内核。

另外,回滚不会替我们修改Git里的源码。造成问题的配置还需要修复或撤销,否则下次重建又会应用回来。数据库、用户文件等也不会随系统回滚恢复,涉及这类数据时要使用相应备份。

清理旧代次
#

更新次数多了,旧系统可能会占用不少空间。但代次数量不等于磁盘占用:多个代次会共享依赖,result等其他链接也可能保留store路径。

Nix通过垃圾回收root判断哪些路径还需要保留。旧代次是其中一种引用,删除代次之后,没有其他引用的路径才有机会被回收。因此,删除一个代次不一定马上腾出想象中那么多空间。具体可以参看垃圾回收文档。

建议先确认新系统能用,再决定哪些旧代次可以清理。Git里还有配置文件,不代表以后一定能迅速重建旧系统:可能需要重新下载,也可能要再次编译,甚至原来的源码已经无法获取。

平时让AI帮忙维护时,我会要求它说清楚执行了哪些步骤,尤其是有没有激活、有没有重启。这样看到“构建成功”,就知道还需要继续做什么,也方便下次接着处理。

下一篇会介绍NixOS模块、选项和覆盖规则,看看这些配置文件是怎样组合到一起的。

NixOS系列 - 这篇文章属于一个选集。
§ 5: 本文