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 ==