Nix函数的传参
这两天在探索 Nix 语言。我发现我还是很喜欢 NixOS 的理念的,NixOS 的决定性和可复现性几乎如其社区宣传的那般美好,然而 NixOS 的文档的确也如社区抱怨的那般糟糕。一体两面。
我在学习 Nix 的使用,我感觉现在没有一篇好的文章阐明 Nix 的传参究竟是怎样进行的。因此写就此文。
另一方面而言,本文是对《Flakes 的组合能力与 Nixpkgs Module 系统》一文的读后感,或者说是资料补足。
我是 Nix 的初学者。我的阐述可能带有口语化特征或者不严谨的地方。我已经让 LLM 结合 nixpkgs 仓库对本文进行 fact check。如果您仍然发现有不合理的地方,请联系我。
Nix 函数语法
以下介绍 Nix 语言所提供的函数语法。 Nix 函数的基本形式是:
pattern: body
一个简单例子是:{ x, y, z }: z + y + x
上述的函数只能使用一个恰好包含属性 x、y 和 z 的集合来调用。不允许有其他属性。如果您想允许额外的参数,可以使用省略号(...),...允许传入未显式列出的额外属性。一般都会带上...来使得函数具有更高的鲁棒性(允许函数接受field更丰富的参数)
...一般与@name语法一起用,name用于承载...所接收到的额外属性。
# 只有 @,没有 ...
args@{ a, b }: args.a + args.b
# 调用时 { a = 1; b = 2; } 没问题;{ a = 1; b = 2; c = 3; } 报错(c 未声明)
# 只有 ...,没有 @
{ a, b, ... }: a + b
# 可以接受额外属性 c,但无法引用 c
# 两者一起
{ a, b, ... }@args: a + b + args.c
# 即可接受额外属性,又能通过 args 引用它们
Nix import 语法
Nix 语言提供了一个 import 函数,它有一条特殊规则:
import的参数如果为文件夹路径,那么会返回该文件夹下的default.nix文件的执行结果
即,import的求值对象如果是一个.nix文件,那就对该.nix文件进行求值;如果是一个路径,那么对该路径下的default.nix文件进行求值
Nixpkgs 模块系统提供了一个与之类似的 imports 参数,它可以接受模块路径(.nix 文件或目录均可)、内联模块(attribute set 或函数)的列表,并将它们全部合并(Merge)到当前的属性集中。
nixosSystem 以及参数传递
简单提及一下 NixOS 的 nixpkgs.lib.nixosSystem。在以Flake进行管理的NixOS系统中,调用的起点为nixpkgs.lib.nixosSystem。
我们可以看到使用 Flake 特性的 NixOS 中,flake.nix 的 outputs 中几乎总是会包含 nixosSystem 构建起的几个元素。该 outputs 接收一些 modules,这些 modules 以类似于前述 import 语法的逻辑被引入。
更确切的说,该得自于 nixpkgs.lib 的函数接受了一个属性集,并以此进行NixOS系统的构建。nixosSystem函数的逻辑大概是:将所有的 module 合并并求值。
因为 import 加载一个返回函数的 Nix 文件时,得到的是函数值,模块系统后续会通过 evalModules 内部的 loadModule 调用 applyModuleArgsIfFunction 来自动调用该函数并注入参数。这方便我们将一段接收注入参数的函数逻辑塞入到整个大的配置合并流程里。因此 module 多以函数形式出现。
我们可能会import一个形同于此的module
{ config, lib, pkgs, ... }: # ← 这是一个函数
{
imports = [ ... ];
services.openssh.enable = true;
# ...
}
调用方的 import 得到了这个函数(函数语言中的函数本身是值,是一等公民)。这些模块会被 collectModules 递归遍历 imports 展开为扁平列表,然后由 nixosSystem 的 evalModules 统一调用每个模块函数并注入参数。即,执行的环节由一个 customized 的执行器去进行。nixpkgs 使得每个 module 收到的参数集为
{ config, lib, pkgs, options, modulesPath, ... }
evalModules会如一切广为流行的程序框架那样,向我们的代码里注入些东西。在此,它的注入颇为易于理解——它向我们所有的模块注入了以上五个参数。
非默认参数传递
NixOS 使用场景下的 Nix 语言的「参数传递」是一种颇具误导性的称呼。这种传递本质上是 nixosSystem 进行的参数注入,有类于在 Legacy 风格的 C 语言程序里调用那些全局变量,亦有类于 Spring 框架的依赖注入,而并非是逐级进行的函数传递。
以 specialArgs 或 _module.args 方式传递的参数,将被应用到同一求值域内的所有模块——即 evalModules 的求值范围,实际上包括了 nixosSystem 的 modules 引入的模块,以及这些模块中通过 imports 嵌套引入的模块。
此处说的「求值域内的所有模块」并非官方文档中的 「submodule」。后者特指 types.submodule/types.submoduleWith 创建的选项值——每个子模块对应一次独立的 evalModules 调用,不会自动继承父级的 _module.args。
非默认参数传递的细则参阅:《传递非默认参数到模块系统中》。