缘起#
前面几篇提到了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。假设这个文件原本不存在,我们就可以通过查看它,判断配置什么时候真正生效。
修改配置#
先进入配置仓库,看一下有没有之前没提交的修改:
| |
然后在已有主机模块的配置属性集中,也就是配置的花括号内,加入下面一行:
| |
这里的environment.etc用来声明由NixOS管理的/etc文件,nixos-lifecycle-demo是文件名,text是文件内容,末尾的\n表示换行。
保存之后,/etc下面还不会出现这个文件。我们只是改好了配置,还没有应用它。
本例不需要更新flake.lock。添加一个设置时就把全部依赖一起更新,虽然也能做,但如果出了问题,就不容易知道是新设置还是依赖更新造成的。
如果是在已有文件里修改,不提交也能让本地Git flake读到改动。如果另外新建了模块,则要把它导入,并先用git add加入索引,否则Nix可能看不到这个新文件。详细说明可以参看flake文档。
检查配置#
修改完后,先检查一下:
| |
第一条检查diff中的一些格式问题。第二条会读取flake,组合各个模块和选项,检查配置能否通过评估(evaluation)。例如选项名写错了、两个地方的设置冲突了,可能就会在这里报错。
几个参数的意思是:
--no-build:不构建flake声明的checks;--show-trace:出错时显示调用信息,方便查找错误来自哪里;--no-update-lock-file:如果这次操作需要改变锁文件,就报错,而不是顺便更新它。
不加--no-build时,命令还会构建checks,具体见nix flake check说明。另外,评估时可能需要下载缺失的输入,所以不一定完全离线,也不一定很快。
这一步通过后,说明Nix能处理被检查的配置。编译是否成功、服务是否能用,还要往下看。
构建系统#
接着构建laptop的系统:
| |
这里的.表示当前仓库,#laptop选择其中的主机输出。build只生成系统产物,不应用到正在运行的系统,因此不需要为了激活而使用sudo。
构建完成后,当前目录下会有一个result符号链接,可以查看它指向哪里:
| |
它会指向/nix/store中的一个系统目录。这个系统及其所有依赖合起来,就是前面说过的闭包(closure)。
“构建系统”听起来好像要把整个操作系统重新编译一遍,其实大部分时候没有那么夸张。已有的store路径可以复用,有缓存的产物可以下载,确实缺少的部分才需要构建。本例主要是生成一个配置文件,与更新内核的工作量很不一样。
也可以比较新产物和当前系统的软件包、闭包变化:
| |
这个命令适合看看软件包版本和闭包大小的变化,不会逐行列出配置文件的差异。本例具体改了什么,还是看Git的diff最清楚。
至此,系统产物已经有了,但/etc/nixos-lifecycle-demo仍然不会出现,因为还没有激活。
构建很久怎么办#
是否需要很久,和这次改了什么有关。迁移后的某次内核重建花了四个多小时,峰值磁盘占用约45GiB,其中一次尝试接近结束时因为空间不足失败。普通配置修改不需要按这个规模准备,但更新输入可能触发比预期更大的构建。
所以遇到内核或大量依赖更新时,最好先看看磁盘余量,安排一段不会被打断的时间。AI可以帮忙查日志和监控进度,但编译时间和磁盘空间还是省不掉的。
应用配置#
先用test试一下#
如果想先看看效果,可以使用test。在执行之前,先把当前系统路径记在变量里,后面恢复时会用到:
| |
现在再读取文件:
| |
应该能看到:
| |
这时配置已经生效了。文件由NixOS管理,通常通过链接指向store里的内容。如果以后要修改它,应修改Nix配置,再重新应用。
test的特点是立即激活,但不改变默认启动配置。也就是说,如果只是试用了一个配置,重启后通常会回到原来的默认启动项。
不过,“试用”也会真的改动系统。服务可能被重启,网络配置可能断开连接,程序也可能写入数据。它不会自动在几分钟后撤销,所以远程改网络时,仍然需要考虑连接断开后怎么恢复。激活中途报错时,也可能已经应用了部分修改,应先看报错和实际状态。
确认后用switch#
文件内容正确之后,可以执行:
| |
switch会应用配置,并把它设为默认启动配置。这样以后重启,仍然会使用这份设置。本例只是增加文件,不需要为了让它生效而重启。
build、test和switch之间,最好保持配置与锁文件不变。后两条命令会再次评估和准备系统,并不是直接拿之前的result来执行。如果输入没有变,通常会复用已经生成的产物。
boot有什么用#
如果希望先把新配置准备好,等下次启动时再使用,可以运行:
| |
它会设置默认启动配置,但不立即激活。几个命令的区别可以放在一起看:
| 命令 | 准备系统产物 | 立即激活 | 设置默认启动配置 |
|---|---|---|---|
build | 是 | 否 | 否 |
test | 是 | 是 | 否 |
switch | 是 | 是 | 是 |
boot | 是 | 否 | 是 |
这里“准备产物”包括复用已有结果、下载缓存和构建,不一定都需要编译。nixos-rebuild文档也有这些命令的说明。
什么时候需要重启#
上面的文件在激活后就能使用。但如果更新的是内核或者早期启动配置,就需要重启后才能检查。
可以先看看这两个路径:
| |
current-system表示当前已激活的系统配置,booted-system表示本次启动使用的配置。比如本例执行过switch,但还没重启,两者就可能不同。这是正常的,并不意味着每次看到不同都需要重启。
如果想确认差异是否涉及内核,还可以看:
| |
不同的系统配置可以使用同一个内核;反过来,两个不同内核构建也可能显示相同版本号,所以只看uname -r还不够。
重启之后,可以再检查这些路径,同时看看有没有失败的服务:
| |
第二条应在对应用户的会话中执行。最后还要实际使用一下修改的功能:本例可以再次读文件,改了网络服务就试着连接,改了驱动就操作一下设备。没有failed服务不代表所有功能都检查过了。
配置改坏了怎么恢复#
只执行了test#
前面保存的previous_system仍然指向一个存在的旧系统时,可以在同一个Shell里执行:
| |
这样会重新激活之前保存的系统,并保持默认启动配置不变。使用前确认变量指向的是想恢复的版本,测试期间也不要把旧系统路径清理掉。
重启也是一种回到默认启动配置的方法,不过默认启动配置不一定与变量里保存的版本相同。
已经执行了switch#
先看看保留了哪些代次:
| |
如果上一个profile代次正是要恢复的版本,可以回滚:
| |
这里有个容易弄混的地方:test不会推进系统profile,所以switch --rollback不一定等于“撤销刚才的test”,它可能退到更早的代次。要按自己之前执行过的操作选择恢复方法。
新系统无法启动时,可以在启动菜单中选择保留的旧代次;需要恢复旧内核时,也要重新启动那个内核。
另外,回滚不会替我们修改Git里的源码。造成问题的配置还需要修复或撤销,否则下次重建又会应用回来。数据库、用户文件等也不会随系统回滚恢复,涉及这类数据时要使用相应备份。
清理旧代次#
更新次数多了,旧系统可能会占用不少空间。但代次数量不等于磁盘占用:多个代次会共享依赖,result等其他链接也可能保留store路径。
Nix通过垃圾回收root判断哪些路径还需要保留。旧代次是其中一种引用,删除代次之后,没有其他引用的路径才有机会被回收。因此,删除一个代次不一定马上腾出想象中那么多空间。具体可以参看垃圾回收文档。
建议先确认新系统能用,再决定哪些旧代次可以清理。Git里还有配置文件,不代表以后一定能迅速重建旧系统:可能需要重新下载,也可能要再次编译,甚至原来的源码已经无法获取。
平时让AI帮忙维护时,我会要求它说清楚执行了哪些步骤,尤其是有没有激活、有没有重启。这样看到“构建成功”,就知道还需要继续做什么,也方便下次接着处理。
下一篇会介绍NixOS模块、选项和覆盖规则,看看这些配置文件是怎样组合到一起的。
