NixOS modules: Difference between revisions
imported>Workflow m Remove explicit lib call since lib is in scope |
imported>Fzakaria Added under the hood explanation by Infinisil |
||
Line 187: | Line 187: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
== Under the hood == | |||
The following was taken from a comment by Infinisil on reddit <ref>Infinisil, https://www.reddit.com/r/NixOS/comments/gdnzhy/question_how_nixos_options_works_underthehood/</ref>. | |||
A NixOS system is described by a single system derivation. {{ic|nixos-rebuild}} builds this derivation with {{ic|nix-build '<nixpkgs/nixos>' -A system}} and then switches to that system with {{ic|result/bin/switch-to-configuration}}. | |||
The entrypoint is the file at {{ic|'<nixpkgs/nixos>' (./default.nix)}}, which defines the {{ic|system}} attribute to be the NixOS option {{ic|config.system.build.toplevel}}. This {{ic|toplevel}} option is the topmost level of the NixOS evaluation and it's what almost all options eventually end up influencing through potentially a number of intermediate options. | |||
As an example: | |||
* The high-level option services.nginx.enable uses the lower-level option systemd.services.nginx | |||
* Which in turn uses the even-lower-level option systemd.units."nginx.service" | |||
* Which in turn uses environment.etc."systemd/system" | |||
* Which then ends up as result/etc/systemd/system/nginx.service in the toplevel derivation | |||
So high-level options use lower-level ones, eventually ending up at {{ic|config.system.build.toplevel}}. | |||
How do these options get evaluated though? That's what the NixOS module system does, which lives in the {{ic|./lib}} directory (in {{ic|modules.nix}}, {{ic|options.nix}} and {{ic|types.nix}}). The module system can even be used without NixOS, allowing you to use it for your own option sets. Here's a simple example of this, whose {{ic|toplevel}} option you can evaluate with {{ic|nix-instantiate --eval file.nix -A config.toplevel}}: | |||
<syntaxhighlight lang="nix"> | |||
let | |||
systemModule = { lib, config, ... }: { | |||
options.toplevel = lib.mkOption { | |||
type = lib.types.str; | |||
}; | |||
options.enableFoo = lib.mkOption { | |||
type = lib.types.bool; | |||
default = false; | |||
}; | |||
config.toplevel = '' | |||
Is foo enabled? ${lib.boolToString config.enableFoo} | |||
''; | |||
}; | |||
userModule = { | |||
enableFoo = true; | |||
}; | |||
in (import <nixpkgs/lib>).evalModules { | |||
modules = [ systemModule userModule ]; | |||
} | |||
</syntaxhighlight> | |||
The module system itself is rather complex, but here's a short overview. A module evaluation consists of a set of "modules", which can do three things: | |||
* Import other modules (through imports = [ ./other-module.nix ]) | |||
* Declare options (through options = { ... }) | |||
* Define option values (through |config = { ... }, or without the config key as a shorthand if you don't have imports or options) | |||
To do the actual evaluation, there's these rough steps: | |||
* Recursively collect all modules by looking at all {{ic|imports}} statements | |||
* Collect all option declarations (with {{ic|options}}) of all modules and merge them together if necessary | |||
* For each option, evaluate it by collecting all its definitions (with {{ic|config}}) from all modules and merging them together according to the options type. | |||
Note that the last step is lazy (only the options you need are evaluated) and depends on other options itself (all the ones that influence it) | |||
== More complex usages == | == More complex usages == |