NixOS modules

From NixOS Wiki
Revision as of 13:10, 6 November 2021 by imported>Milahu (clarify modulesPath)

Modules are files combined by NixOS to produce the full system configuration. A module contains a Nix expression. It declares options for other modules to define (give a value). It processes them and defines options declared in other modules.[1]

For example, /etc/nixos/configuration.nix is a module. Most other modules are in nixos/modules.

Structure

Modules have the following syntax:

{
  imports = [
    # paths to other modules
  ];

  options = {
    # option declarations
  };

  config = {
    # option definitions
  };
}

There is a shorthand for modules without any declarations:

{
  require = [
    # paths to other modules
   ./module.nix
   /path/to/absolute/module.nix
  ];

  # option definitions
}

Note: require is considered obsolete and there is no reason to use it anymore, however it may still linger in some legacy code. imports provides the same behavior.

Function

A module can be turned into a function accepting an attribute set.

{ config, pkgs, ... }:
{
  # ...
}

It may require the attribute set to contain:

config
The configuration of the entire system.
options
All option declarations refined with all definition and declaration references.
pkgs
The attribute set extracted from the Nix package collection and enhanced with the nixpkgs.config option.
modulesPath

The location of the module directory of NixOS.

Parsed from the first entry in NIX_PATH, for example:
NIX_PATH=/nix/var/nix/profiles/per-user/root/channels/nixos:nixpkgs=/etc/nixos/nixpkgs:nixos-config=/etc/nixos/configuration.nix
modulesPath = /nix/var/nix/profiles/per-user/root/channels/nixos/nixos/modules

note: NIX_PATH is empty when Using nix flakes with NixOS,
and nix will throw undefined variable 'modulesPath'

Imports

Imports are paths to other NixOS modules that should be included in the evaluation of the system configuration. A default set of modules is defined in  nixos/modules/module-list.nix. These don't need to be added in the import list.

Declarations

Declarations specify a module's external interfaces.

optionName = mkOption {
  # ...
}

They are created with mkOption, a function accepting a set with following attributes:[2][3]

type

The type of the option. It may be omitted, but that’s not advisable since it may lead to errors that are hard to diagnose.

default

The default value used if no value is defined by any module. A default is not required; but if a default is not given, then users of the module will have to define the value of the option, otherwise an error will be thrown.

example

An example value that will be shown in the NixOS manual.

description

A textual description of the option, in DocBook format, that will be included in the NixOS manual.

Rationale

Modules were introduced to allow extending NixOS without modifying its source code.[4] They also allow splitting up configuration.nix, making the system configuration easier to maintain and to reuse.

Example

put into hello.nix in the same folder as your configuration.nix.

{ lib, pkgs, config, ... }:
with lib;                      
let
  cfg = config.services.hello;
in {
  options.services.hello = {
    enable = mkEnableOption "hello service";
    greeter = mkOption {
      type = types.str;
      default = "world";
    };
  };

  config = mkIf cfg.enable {
    systemd.services.hello = {
      wantedBy = [ "multi-user.target" ];
      serviceConfig.ExecStart = "${pkgs.hello}/bin/hello -g'Hello, ${escapeShellArg cfg.greeter}!'";
    };
  };
}

Add the following to your configuration.nix

{
  imports = [ ./hello.nix ];
  ...
  services.hello = {
    enable = true;
    greeter = "Bob";
  };
}

Advanced Use Cases

Compatibility Issues with Different Nixpkgs Versions

Module options between Nixpkgs revisions can sometimes change in incompatible ways.

For example, the option services.nginx.virtualHosts.*.port in nixpkgs-17.03 was replaced by services.nginx.virtualHosts.*.listen in nixpkgs-17.09. If configuration.nix has to accommodate both variants, options can be inspected:

{ options, ... }: {
  services.nginx.virtualHosts.somehost = { /* common configuration */ }
    // (if builtins.hasAttr "port" (builtins.head options.services.nginx.virtualHosts.type.getSubModules).submodule.options
          then { port = 8000; }
          else { listen = [ { addr = "0.0.0.0"; port = 8000; } ]; });
}

Abstract imports

To import a module that's stored somewhere (but for which you have neither an absolute nor a relative path), you can use NIX_PATH elements or specialArgs from nixos/lib/eval-config.nix.

This is useful for e.g. pulling modules from a git repository without adding it as a channel, or if you just prefer using paths relative to a root you can change (as opposed to the current file, which could move in the future).

let
  inherit (import <nixpkgs> {}) writeShellScriptBin fetchgit;
  yourModules = fetchgit { ... };
in rec {
  nixos = import <nixpkgs/nixos/lib/eval-config.nix> {
    modules = [ ./configuration.nix ];
    specialArgs.mod = name: "${yourModules}/${name}";
  };

  /* use nixos here, e.g. for deployment or building an image */
}
{ config, lib, pkgs, mod, ... }: {
  imports = [
    (mod "foo.nix")
  ];

  ...
}

Under the hood

The following was taken from a comment by Infinisil on reddit [5].

A NixOS system is described by a single system derivation. nixos-rebuild builds this derivation with nix-build '<nixpkgs/nixos>' -A system and then switches to that system with result/bin/switch-to-configuration.

The entrypoint is the file at '<nixpkgs/nixos>' (./default.nix), which defines the system attribute to be the NixOS option config.system.build.toplevel. This 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 config.system.build.toplevel.

How do these options get evaluated though? That's what the NixOS module system does, which lives in the ./lib directory (in modules.nix, options.nix and 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 toplevel option you can evaluate with nix-instantiate --eval file.nix -A config.toplevel:

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 ];
}

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 imports statements
  • Collect all option declarations (with options) of all modules and merge them together if necessary
  • For each option, evaluate it by collecting all its definitions (with 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

The examples below contain:

  • a child `mkOption` inherits their default from a parent `mkOption`
  • reading default values from neighbouring `mkOption`(s) for conditional defaults
  • passing in the config, to read the hostName from a submodule (email system)
  • setting default values from attrset (email system)
  • generating documentation for custom modules (outside of nixpkgs). See here

Source:

(sorry, dont' have more time to make this into a nice little guide yet, but this links should be pretty good introductions into more advanced module system usages) qknight

Developing modules

To test your module out, you can run the following from a local checkout of nixpkgs with a copy of a configuration.nix:

nixos-rebuild build-vm --fast -I nixos-config=./configuration.nix -I nixpkgs=.

If you're developing on top of master, this will potentially cause the compilation of lots of packages, since changes on master might not cached on cache.nixos.org yet. To avoid that, you can develop your module on top of the nixos-unstable channel, tracked by the eponymous branch in https://github.com/NixOS/nixpkgs:

git checkout -b mymodule upstream/nixos-unstable

References

See also