Skip to content

Commit

Permalink
Merge pull request #103 from ryan4yin/conditional-imports
Browse files Browse the repository at this point in the history
feat: conditional imports
  • Loading branch information
ryan4yin committed Mar 8, 2024
2 parents be0143d + 945e0bb commit 11bec60
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 51 deletions.
134 changes: 113 additions & 21 deletions docs/other-usage-of-flakes/module-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,25 +170,14 @@ Let's start with a simple example:
# ./flake.nix
{
description = "NixOS Flake for Test";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
home-manager = {
url = "github:nix-community/home-manager/release-23.11";
inputs.nixpkgs.follows = "nixpkgs";
};
};
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
outputs = {nixpkgs, ...}: {
nixosConfigurations = {
"test" = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
({
config,
lib,
...
}: {
({config, lib, ...}: {
options = {
foo = lib.mkOption {
default = false;
Expand Down Expand Up @@ -217,15 +206,17 @@ In the examples 1, 2, and 3 of the above configuration, the value of `config.war
Let's explain each case:

1. Example 1 evaluation flow: `config.warnings` => `config.foo` => `config`
1. First, Nix attempts to compute the value of `config.warnings` but finds that it depends on `config.foo`.
2. Next, Nix tries to compute the value of `config.foo`, which depends on its outer `config`.
3. Nix attempts to compute the value of `config`, and since the contents not genuinely used by `config.foo` are lazily evaluated by Nix, there is no recursive dependency on `config.warnings` at this point.
4. The evaluation of `config.foo` is completed, followed by the assignment of `config.warnings`, and the computation ends.

1. First, Nix attempts to compute the value of `config.warnings` but finds that it depends on `config.foo`.
2. Next, Nix tries to compute the value of `config.foo`, which depends on its outer `config`.
3. Nix attempts to compute the value of `config`, and since the contents not genuinely used by `config.foo` are lazily evaluated by Nix, there is no recursive dependency on `config.warnings` at this point.
4. The evaluation of `config.foo` is completed, followed by the assignment of `config.warnings`, and the computation ends.

2. Example 2: `config` => `config.foo` => `config`
1. Initially, Nix tries to compute the value of `config` but finds that it depends on `config.foo`.
2. Next, Nix attempts to compute the value of `config.foo`, which depends on its outer `config`.
3. Nix tries to compute the value of `config`, and this loops back to step 1, leading to an infinite recursion and eventually an error.

1. Initially, Nix tries to compute the value of `config` but finds that it depends on `config.foo`.
2. Next, Nix attempts to compute the value of `config.foo`, which depends on its outer `config`.
3. Nix tries to compute the value of `config`, and this loops back to step 1, leading to an infinite recursion and eventually an error.

3. Example 3: The only difference from example 2 is the use of `lib.mkIf` to address the infinite recursion issue.

Expand All @@ -249,6 +240,108 @@ While assignment is the most commonly used feature of the module system, if you

We have already introduced how to use `specialArgs` and `_module.args` to pass additional parameters to other Modules functions in [Managing Your NixOS with Flakes](../nixos-with-flakes/nixos-with-flakes-enabled.md#pass-non-default-parameters-to-submodules). No further elaboration is needed here.

## How to Selectively Import Modules {#selectively-import-modules}

In the examples above, we have introduced how to enable or disable certain features through custom options. However, our code implementations are all within the same Nix file. If our modules are scattered across different files, how can we achieve selective import?

Let's first look at some common incorrect usage patterns, and then introduce the correct way to do it.

### Incorrect Usage #1 - Using `imports` in `config = { ... };` {#wrong-usage-1}

The first thought might be to directly use `imports` in `config = { ... };`, like this:

```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 {
# Using imports in config will cause an error
imports = [
{warnings = ["foo"];}
# ...omit other module or file paths
];
};
})
];
};
};
};
}
```

But this won't work.
You can try save the above `flake.nix` in a new directory, and then run `nix eval .#nixosConfigurations.test.config.warnings` in it, some error like `error: The option 'imports' does not exist.` will be encountered.

This is because `config` is a regular attribute set, while `imports` is a special parameter of the module system. There is no such definition as `config.imports`.

### Correct Usage #1 - Define Individual `options` for All Modules That Require Conditional Import {#correct-usage-1}

This is the most recommended method. Modules in NixOS systems are implemented in this way, and searching for `enable` in <https://search.nixos.org/options> will show a large number of system modules that can be enabled or disabled through the `enable` option.

The specific writing method has been introduced in the previous [Basic Structure and Usage](#basic-structure-and-usage) section and will not be repeated here.

The disadvantage of this method is that all Nix modules that require conditional import need to be modified, moving all configuration declarations in the module to the `config = { ... };` code block, increasing code complexity and being less friendly to beginners.

### Correct Usage #2 - Use `lib.optionals` in `imports = [];` {#correct-usage-2}

The main advantage of this method is that it is much simpler than the methods previously introduced, requiring no modification to the module content, just using `lib.optionals` in `imports` to decide whether to import a module or not.

Let's look at an example directly:

```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 = [
({config, lib, enableSteam ? false, ...}: {
imports =
[
# Use lib.optionals to decide whether to import foo.nix
(lib.optionals (enableFoo) ./foo.nix)
];
})
];
};
};
};
}
```

```nix
# ./foo.nix
{ warnings = ["foo"];}
```

Save the two Nix files above in a folder, and then run `nix eval .#nixosConfigurations.test.config.warnings` in the folder, and the operation is normal:

```bash
› nix eval .#nixosConfigurations.test.config.warnings
[ "foo" ]
```

One thing to note here is that **you cannot use parameters passed by `_module.args` in `imports =[ ... ];`**. We have already provided a detailed explanation in the previous section [Passing Non-default Parameters to Submodules](../nixos-with-flakes/nixos-with-flakes-enabled#pass-non-default-parameters-to-submodules).

## 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)
Expand All @@ -257,7 +350,6 @@ We have already introduced how to use `specialArgs` and `_module.args` to pass a
- [Module System - Nixpkgs]
- [Writing NixOS Modules - Nixpkgs]


[lib/modules.nix]: https://github.com/NixOS/nixpkgs/blob/23.11/lib/modules.nix#L995
[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
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/nixos-with-flakes/nixos-with-flakes-enabled.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ Nixpkgs 的模块系统提供了两种方式来传递非默认参数:
这两者的区别在于:

1. 在任何 Module 中都能使用 `_module.args` 这个 option,通过它互相传递参数,这要比只能在 `nixpkgs.lib.nixosSystem` 函数中使用的 `specialArgs` 更灵活。
1. `_module.args` 是在 Module 中声明使用的,因此必须在所有 Modules 都已经被求值后,才能使用它。这导致如果你在 `imports = [ ... ];` 中使用 `_module.args` 传递的参数,会报错 `infinite recursion`,这种场景下你必须改用 `specialArgs` 才行。
1. `_module.args` 是在 Module 中声明使用的,因此必须在所有 Modules 都已经被求值后,才能使用它。这导致**如果你在 `imports = [ ... ];` 中使用 `_module.args` 传递的参数,会报错 `infinite recursion`,这种场景下你必须改用 `specialArgs` 才行**

NixOS 社区比较推荐优先使用 `_module.args` 这个 options,仅在无法使用 `_module.args` 时才改用 `specialArgs`

Expand Down
Loading

0 comments on commit 11bec60

Please sign in to comment.