2024-03-08 04:00:18 +01:00
# 模块系统与自定义 options {#module-system}
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
我们在前面的 NixOS 配置中通过设置各种 `options` 的值来配置 NixOS 或者 Home Manager, 这些
`options` 实际都在这两个位置定义:
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
- NixOS:
[nixpkgs/nixos/modules ](https://github.com/NixOS/nixpkgs/tree/23.11/nixos/modules ), 我们
在 < https: / / search . nixos . org / options > 中能看到的所有 NixOS options 都是在这里定义的。
- Home Manager:
[home-manager/modules ](https://github.com/nix-community/home-manager/blob/release-23.11/modules ):
可在 < https: / / nix-community . github . io / home-manager / options . xhtml > 中找到其所有的options.
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
> 如果你还使用 nix-darwin, 那么它的配置也是类似的, 其模块系统的实现位于
> [nix-darwin/modules](https://github.com/LnL7/nix-darwin/tree/master/modules)
2024-02-04 10:36:32 +01:00
2024-03-16 12:29:05 +01:00
而上述 NixOS Modules 跟 Home Manager Modules 的基础,是 Nixpkgs 中实现的一套通用模块系统
[lib/modules.nix][lib/modules.nix],这套模块系统的官方文档如下(即使是对熟练使用 NixOS 的
用户而言,要看懂这玩意儿也不是件容易的事...) :
2023-12-18 14:34:34 +01:00
2024-02-14 09:00:16 +01:00
- [Module System - Nixpkgs]
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
因为 Nixpkgs 的模块系统文档没人写,文档中直接建议读另一份专门针对 NixOS 模块系统的编写指
南,确实写得清晰一些,但也很难说它对新手有多友好:
2023-12-18 14:34:34 +01:00
2024-02-14 09:00:16 +01:00
- [Writing NixOS Modules - Nixpkgs]
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
总之,模块系统是由 Nixpkgs 实现的,并不是 Nix 包管理器的一部分,因此它的文档也不在 Nix 包
管理器的文档中。另外 NixOS 与 Home Manager 都是基于 Nixpkgs 的模块系统实现的。
2023-12-18 14:34:34 +01:00
2024-03-08 04:00:18 +01:00
## 模块系统有什么用? {#what-is-module-system}
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
我们作为一个普通用户,使用 NixOS 与 Home Manager 基于模块系统实现的各种 options 就已经能满
足我们大部分的需求了。那么深入学习模块系统对于我们来说,还有什么好处呢?
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
我们在前面介绍配置的模块化时,提到了核心点是将配置拆分为多个模块,再通过
`imports = [ ... ];` 来导入这些模块。这其实就是模块系统最基础的用法。但仅仅使用
`imports = [ ... ];` ,我们只能将模块中定义的配置原封不动地导入到当前模块中,无法对其做任何
定制,灵活性很差。在配置简单的情况下,这种方式已经足够了,但如果我们的配置比较复杂,那么这
种方式就显得力不从心了。
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
这里举个例子来说明其弊端,譬如说我通过一份配置管理了 A、B、C 跟 D 共 4 台 NixOS 主机,我希
望能在尽量减少配置重复的前提下实现如下功能:
2023-12-18 14:34:34 +01:00
- A、B、C 跟 D 都需要启用 docker 服务,设置开机自启
- A 需要将 docker 的存储驱动改为 `btrfs` ,其他不变
- B、C 是位于中国的服务器,需要在 docker 配置中设置国内镜像源
- C 是位于美国的服务器,无特殊要求
- D 是桌面主机,需要为 docker 设置 HTTP 代理加速下载
2024-03-16 12:29:05 +01:00
如果单纯使用 `imports` ,那么我们可能得将配置拆分成如下几个模块,然后在每台主机上导入不同的
模块:
2023-12-18 14:34:34 +01:00
```bash
› tree
.
├── docker-default.nix # 基础的 docker 配置,包含开机自启
├── docker-btrfs.nix # 导入了 docker-default.nix, 将存储驱动改为 btrfs
├── docker-china.nix # 导入了 docker-default.nix, 设置国内镜像源
└── docker-proxy.nix # 导入了 docker-default.nix, 设置 HTTP 代理
```
2024-03-16 12:29:05 +01:00
是否感觉到这样的配置很冗余?这还是一个简单的例子,如果我们的机器更多,不同机器的配置差异更
大,那么这种配置的冗余性就会更加明显。
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
显然,我们需要借助其他的手段来解决这个配置冗余的问题,自定义一些我们自己的 `options` 就是
一个很不错的选择。
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
在深入学习模块系统之前,我再强调一下,如下内容不是必须学习与使用的,有很多 NixOS 用户并未
自定义任何 `options` ,只是简单地使用 `imports` 就能满足他们的需求了。如果你是新手,可以考
虑在遇到类似上面这种,`imports` 解决不了的问题时再来学习这部分内容,这是完全 OK 的。
2023-12-18 14:34:34 +01:00
2024-03-08 04:00:18 +01:00
## 基本结构与用法 {#basic-structure-and-usage}
2023-12-18 14:34:34 +01:00
Nixpkgs 中定义的模块,其基本结构如下:
```nix
{ config, pkgs, ... }:
{
imports =
[ # import other modules here
];
options = {
# ...
};
config = {
# ...
};
}
```
2024-03-16 12:29:05 +01:00
其中的 `imports = [ ... ];` 我们已经很熟悉了,但另外两个部分,我们还没有接触过,这里简单介
绍下:
2023-12-18 14:34:34 +01:00
- `options = { ... };` : 它类似编程语言中的变量声明,用于声明一些可配置的选项。
- `config = { ... };` : 它类似编程语言中的变量赋值,用于为 `options` 中声明的选项赋值。
2024-03-16 12:29:05 +01:00
最典型的用法是:在同一 Nixpkgs 模块中,依据 `options = { ... };` 中声明的 `options` 当前的
值,在 `config = { .. };` 中为其他的 `options` 赋值,这样就实现了参数化配置的功能。
2023-12-18 14:34:34 +01:00
直接看个例子更容易理解:
```nix
2024-02-07 16:36:33 +01:00
# ./foo.nix
2023-12-18 14:34:34 +01:00
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.programs.foo;
in {
options.programs.foo = {
enable = mkEnableOption "the foo program";
package = mkOption {
type = types.package;
2024-03-02 15:18:14 +01:00
default = pkgs.hello;
defaultText = literalExpression "pkgs.hello";
2023-12-18 14:34:34 +01:00
description = "foo package to use.";
};
extraConfig = mkOption {
default = "";
example = ''
foo bar
'';
type = types.lines;
description = ''
Extra settings for foo.
'';
};
};
config = mkIf cfg.enable {
home.packages = [ cfg.package ];
xdg.configFile."foo/foorc" = mkIf (cfg.extraConfig != "") {
text = ''
# Generated by Home Manager.
${cfg.extraConfig}
'';
};
};
}
```
上面这个模块定义了三个 `options` :
- `programs.foo.enable` : 用于控制是否启用此模块
2024-03-16 12:29:05 +01:00
- `programs.foo.package` : 用于自定义 foo 这个包,比如说使用不同版本、设置不同编译参数等
等。
2023-12-18 14:34:34 +01:00
- `programs.foo.extraConfig` : 用于自定义 foo 的配置文件。
然后在 `config` 中,根据 `options` 中声明的这三个变量的值,做了不同的设置:
- 如果 `programs.foo.enable` 为 `false` 或者未定义,则不做任何设置。
- 这是借助 `lib.mkIf` 实现的。
- 否则
- 将 `programs.foo.package` 添加到 `home.packages` 中,以将其安装到用户环境中。
2024-02-07 16:34:51 +01:00
- 将 `programs.foo.extraConfig` 的值写入到 `~/.config/foo/foorc` 中。
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
这样,我们就可以在另一个 nix 文件中导入这个模块,并通过设置这里定义的 `options` 来实现对
foo 的自定义配置了,示例:
2023-12-18 14:34:34 +01:00
```nix
2024-02-07 16:36:33 +01:00
# ./bar.nix
2023-12-18 14:34:34 +01:00
{ config, lib, pkgs, ... }:
{
imports = [
./foo.nix
];
programs.foo ={
enable = true;
2024-03-02 15:18:14 +01:00
package = pkgs.hello;
2023-12-18 14:34:34 +01:00
extraConfig = ''
foo baz
'';
};
}
```
2024-03-16 12:29:05 +01:00
上面这个例子中我们为 `options` 赋值的方式实际上是一种**缩写**,当一个模块中只声明了
`options` ,而没有声明 `config` (以及其他模块系统的特殊参数)时,我们可以省略掉 `config`
前缀,直接使用 `options` 的名称进行赋值。
2023-12-18 14:34:34 +01:00
2024-03-08 04:00:18 +01:00
## 模块系统的赋值与延迟求值 {#module-system-assignment-and-lazy-evaluation}
2023-12-18 14:34:34 +01:00
模块系统充分利用了 Nix 的延迟求值特性,这也是它能实现参数化配置的关键。
先看个简单的例子:
```nix
2024-02-07 16:36:33 +01:00
# ./flake.nix
2023-12-18 14:34:34 +01:00
{
description = "NixOS Flake for Test";
2024-03-08 04:00:18 +01:00
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
2023-12-18 14:34:34 +01:00
outputs = {nixpkgs, ...}: {
nixosConfigurations = {
"test" = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
2024-03-08 04:00:18 +01:00
({config, lib, ...}: {
2023-12-18 14:34:34 +01:00
options = {
foo = lib.mkOption {
default = false;
type = lib.types.bool;
};
};
# 示例 1( 正常)
2024-03-02 15:54:27 +01:00
config.warnings = if config.foo then ["foo"] else [];
2023-12-18 14:34:34 +01:00
# 示例 2( 无限递归)
# error: infinite recursion encountered
# config = if config.foo then { warnings = ["foo"];} else {};
# 示例 3( 正常)
# config = lib.mkIf config.foo {warnings = ["foo"];};
})
];
};
};
};
}
```
2024-03-16 12:29:05 +01:00
上述配置中的示例 1、2、3 中,`config.warnings` 的值都依赖于 `config.foo` 的值,但它们的实
现方式却不同。将上述配置保存为 `flake.nix` ,然后使用命令
`nix eval .#nixosConfigurations.test.config.warnings` 分别测试示例 1、2、3, 可以发现示例
1、3 都能正常工作,而示例 2 则会报错 `error: infinite recursion encountered` 。
2023-12-18 14:34:34 +01:00
下面分别解释说明下:
1. 示例一计算流程:`config.warnings` => `config.foo` => `config`
2024-03-08 04:00:18 +01:00
1. 首先, Nix 尝试计算 `config.warnings` 的值,但发现它依赖于 `config.foo` .
2. 接着, Nix 尝试计算 `config.foo` 的值,它依赖于其外层的 `config` .
2024-03-16 12:29:05 +01:00
3. Nix 尝试计算 `config` 的值,`config` 中未被 `config.foo` 真正使用的内容都会被 Nix 延
迟求值,因此这里不会递归依赖 `config.warnings` 。
2024-03-08 04:00:18 +01:00
4. `config.foo` 求值结束,接着 `config.warnings` 被赋值,计算结束。
2023-12-18 14:34:34 +01:00
2. 示例二:`config` => `config.foo` => `config`
2024-03-08 04:00:18 +01:00
1. 首先, Nix 尝试计算 `config` 的值,但发现它依赖于 `config.foo` .
2. 接着, Nix 尝试计算 `config.foo` 的值,它依赖于其外层的 `config` .
3. Nix 尝试计算 `config` 的值,这又跳转到步骤 1, 于是进入无限递归, 最终报错。
2023-12-18 14:34:34 +01:00
3. 示例三:跟示例二唯一的区别是改用了 `lib.mkIf` 解决了无限递归问题。
2024-03-16 12:29:05 +01:00
其关键就在于 `lib.mkIf` 这个函数,使用它定义的 `config` 会被 Nix 延迟求值,也就是说会在
`config.foo` 求值结束后,才会真正计算 `config = lib.mkIf ...` 的值。
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
Nixpkgs 中的模块系统提供了一系列类似 `lib.mkIf` 的函数,用于实现参数化配置与智能的模块合
并:
2023-12-18 14:34:34 +01:00
1. `lib.mkIf` : 上面已经介绍过了。
2024-03-16 12:29:05 +01:00
1. `lib.mkOverride` / `lib.mkdDefault` / `lib.mkForce` : 在前面
[模块化 NixOS 配置 ](../nixos-with-flakes/modularize-the-configuration.md ) 中已经介绍过
了。
2023-12-18 14:34:34 +01:00
1. `lib.mkOrder` , `lib.mkBefore` 与 `lib.mkAfter` : 同上
2024-03-16 12:29:05 +01:00
1. 查看 [Option Definitions - NixOS][Option Definitions - NixOS] 了解更多与 options 赋值
( definition) 相关的函数。
2023-12-18 14:34:34 +01:00
2024-03-08 04:00:18 +01:00
## Options 声明与类型检查 {#option-declarations-and-type-checking}
2023-12-18 14:34:34 +01:00
2024-03-16 12:29:05 +01:00
模块系统的赋值是我们最常用的功能,而如果我们需要自定义一些 `options` ,还需要深入了解下
options 的声明与类型检查。
2023-12-18 14:34:34 +01:00
这个我觉得就还挺简单的,比赋值要简单挺多了,直接看官方文档就能懂个大概,这里就不再赘述了:
- [Option Declarations - NixOS][Option Declarations - NixOS]
- [Options Types - NixOS][Options Types - NixOS]
2024-03-08 04:00:18 +01:00
## 传递非默认参数到模块系统中 {#pass-non-default-parameters-to-the-module-system}
2024-02-14 09:00:16 +01:00
2024-03-16 12:29:05 +01:00
我们在
[使用 Flakes 来管理你的 NixOS ](../nixos-with-flakes/nixos-with-flakes-enabled.md#pass-non-default-parameters-to-submodules )
中已经介绍了如何使用 `specialArgs` 跟 `_module.args` 来传递额外的参数给其他 Modules 函数,
这里不再赘述。
2024-02-14 09:00:16 +01:00
2024-03-08 04:00:18 +01:00
## 如何选择性地导入模块 {#selectively-import-modules}
2024-03-16 12:29:05 +01:00
在上面的例子中,我们已经介绍了如何通过自定义的 options 来决定是否启用某个功能,但我们的代
码实现都是在同一个 nix 文件中的,那么如果我们的模块是分散在不同的文件中的,该如何实现呢?
2024-03-08 04:00:18 +01:00
我们先来看看一些常见的错误用法,然后再来介绍正确的使用方式。
### 错误用法一 - 在 `config = { ... };` 中使用 `imports` {#wrong-usage-1}
你最先想到的,大概是直接在 `config = { ... };` 中使用 `imports` ,类似这样:
```nix
# ./flake.nix
{
description = "NixOS Flake for Test";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
outputs = {nixpkgs, ...}: {
nixosConfigurations = {
"test" = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
({config, lib, ...}: {
options = {
foo = lib.mkOption {
default = false;
type = lib.types.bool;
};
};
config = lib.mkIf config.foo {
# 在 config 中使用 imports 会报错
imports = [
{warnings = ["foo"];}
# ...省略其他模块或文件路径
];
};
})
];
};
};
};
}
```
2024-03-16 12:29:05 +01:00
但这样是行不通的。你可以尝试使用上述 `flake.nix` 运行
`nix eval .#nixosConfigurations.test.config.warnings` ,会遇到报错
`error: The option 'imports' does not exist.`
2024-03-08 04:00:18 +01:00
2024-03-16 12:29:05 +01:00
这是因为 `config` 是一个普通的 attribute set, 而 `imports` 是模块系统的特殊参数。并不存在
`config.imports` 这样的 options 定义。
2024-03-08 04:00:18 +01:00
### 正确用法一 - 为所有需要条件导入的模块定义各自的 `options` {#correct-usage-1}
2024-03-16 12:29:05 +01:00
这是最推荐的方式。NixOS 系统中的模块都是这样实现的,在 < https: / / search . nixos . org / options >
中搜索 `enable` 能看到非常多的可通过 `enable` option 启用或关闭的系统模块。
2024-03-08 04:00:18 +01:00
2024-03-16 12:29:05 +01:00
具体的写法已经在前面的 [基本结构与用法 ](#basic-structure-and-usage ) 中介绍过了,这里不再赘
述。
2024-03-08 04:00:18 +01:00
2024-03-16 12:29:05 +01:00
它的缺点是,所有需要条件导入的 Nix 模块都要做改造,把其中的配置声明全部移到
`config = { ...};` 代码块中,代码复杂度会增加,同时也对新手不太友好。
2024-03-08 04:00:18 +01:00
### 正确用法二 - 在 `imports = [];` 中使用 `lib.optionals` {#correct-usage-2}
2024-03-16 12:29:05 +01:00
这种方式的主要好处是,它要比前面介绍的方法简单许多,不需要对模块内容做任何修改,只需要在
`imports` 中使用 `lib.optionals` 来决定是否导入某个模块即可。
2024-03-08 04:00:18 +01:00
2024-03-08 07:30:56 +01:00
> `lib.optionals` 函数的详细文档: <https://noogle.dev/f/lib/optionals>
2024-03-08 04:00:18 +01:00
直接看例子:
```nix
# ./flake.nix
{
description = "NixOS Flake for Test";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
outputs = {nixpkgs, ...}: {
nixosConfigurations = {
"test" = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { enableFoo = true; };
modules = [
2024-03-08 07:20:16 +01:00
({config, lib, enableFoo ? false, ...}: {
2024-03-08 04:00:18 +01:00
imports =
[
2024-03-08 07:28:05 +01:00
# 这里写其他模块
]
# 通过 lib.optionals 来决定是否foo.nix
++ (lib.optionals (enableFoo) [./foo.nix]);
2024-03-08 04:00:18 +01:00
})
];
};
};
};
}
```
```nix
# ./foo.nix
{ warnings = ["foo"];}
```
2024-03-16 12:29:05 +01:00
将上述两个 nix 文件保存到一个文件夹中,然后在文件夹中运行
`nix eval .#nixosConfigurations.test.config.warnings` ,运行正常:
2024-03-08 04:00:18 +01:00
```bash
› nix eval .#nixosConfigurations.test.config.warnings
[ "foo" ]
```
2024-03-16 12:29:05 +01:00
这里需要注意的一点是,**不能在 `imports =[ ... ];` 中使用由 `_module.args` 传递的参数**,
我们在前面
[传递非默认参数到模块系统中 ](../nixos-with-flakes/nixos-with-flakes-enabled#pass-non-default-parameters-to-submodules )
一章中已经做过详细说明。
2024-03-08 04:00:18 +01:00
2023-12-18 14:34:34 +01:00
## References
- [Best resources for learning about the NixOS module system? - Discourse ](https://discourse.nixos.org/t/best-resources-for-learning-about-the-nixos-module-system/1177/4 )
- [NixOS modules - NixOS Wiki ](https://nixos.wiki/wiki/NixOS_modules )
- [NixOS: config argument - NixOS Wiki ](https://nixos.wiki/wiki/NixOS:config_argument )
- [Module System - Nixpkgs][Module System - Nixpkgs]
- [Writing NixOS Modules - Nixpkgs][Writing NixOS Modules - Nixpkgs]
[lib/modules.nix]: https://github.com/NixOS/nixpkgs/blob/23.11/lib/modules.nix#L995
2024-03-16 12:29:05 +01:00
[Module System - Nixpkgs]:
https://github.com/NixOS/nixpkgs/blob/23.11/doc/module-system/module-system.chapter.md
[Writing NixOS Modules - Nixpkgs]:
https://github.com/NixOS/nixpkgs/blob/nixos-23.11/nixos/doc/manual/development/writing-modules.chapter.md
[Option Definitions - NixOS]:
https://github.com/NixOS/nixpkgs/blob/nixos-23.11/nixos/doc/manual/development/option-def.section.md
[Option Declarations - NixOS]:
https://github.com/NixOS/nixpkgs/blob/nixos-23.11/nixos/doc/manual/development/option-declarations.section.md
[Options Types - NixOS]:
https://github.com/NixOS/nixpkgs/blob/nixos-23.11/nixos/doc/manual/development/option-types.section.md