How to split /etc/nixos/configuration.nix into separate modules?

6,132

Nix expressions

A Nix expression is like any programming language expression: anything that evaluates to a value or a function. A value in this case can also be a list or a set. As a Nix module (file with extension .nix) can contain any Nix expression, you would expect the NixOS configuration file (/etc/nixos/configuration.nix) to contain a single Nix expression as its file contents.

The NixOS configuration file contains a Nix expression of the form:

{config, pkgs, ...}: { /* various configuration options */ }

If you look closely, you can see it's a function, because functions follow the form pattern: form. You can also see it's a function that accepts a set and returns a set. E.g., if you have a function f = {x, y}: {a = x + y;}, then you could call it as f {x=1; y=2;} and get back a set {a=3;}.

So that means that when you call nixos-rebuild switch, something calls the function inside the NixOS configuration file with the set that must contain attributes config and pkgs.

imports

Following example of ./hardware-configuration.nix, the simple way to extract the list of packages into a separate module packages.nix is just to rip the environment.systemPackages option out and put ./packages.nix into imports option. Your /etc/nixos/configuration.nix would look like:

{ config, ... }:    
{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
      # Include the package list.
      ./packages.nix
    ];
  # SOME STUFF
  # SOME STUFF
}

Your /etc/nixos/packages.nix would look like:

{ pkgs, ... }:
{
  environment.systemPackages = with pkgs; [ emacs gitFull ];
}

How does that work? When you run nixos-rebuild switch, the process that evaluates Nix expressions and decides to install packages and so on calls configuration.nix with a set of attributes, some of which are config and pkgs.

It finds attribute imports inside the returned set, so it evaluates every Nix expression in the modules that imports contains with the same arguments (config, pkgs, etc).

You must have pkgs as an argument (or, technically speaking, an attribute of a set, which itself is an argument) of a function in packages.nix, because, from a Nix language perspective, the process might or might not call the function with the set that contains pkgs. If it doesn't, what attribute would you refer to, when running with pkgs?

You also must have ellipsis, because the function might be called with other attributes, not just pkgs.

Why isn't there pkgs in configuration.nix? You can have it, but if you don't refer to it anywhere in the file, you can safely omit it, as the ellipsis would include them anyway.

Updating an attribute by calling an external function

Another way is just to make a function that returns a set with some attribute, and the value of that attribute you would put inside environment.systemPackages. This is your configuration.nix:

{ config, pkgs, ... }:    
{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];
  # SOME STUFF
  environment.systemPackages = import ./packages.nix pkgs;
  # SOME STUFF
}

Your packages.nix:

pkgs: with pkgs; [ emacs gitFull ]

import ./packages.nix pkgs means: load and return the Nix expression in ./packages.nix and as it is a function, call it with an argument pkgs. with pkgs; [ emacs gitFull ] is a with-expression, it brings the scope of the expression before semicolon to the expression after semicolon. Without it, it would be [ pkgs.emacs pkgs.gitFull ].

Share:
6,132

Related videos on Youtube

Mirzhan Irkegulov
Author by

Mirzhan Irkegulov

I am a PhD student in University of Leicester, UK. My interests are functional programming, category theory, and game theory. All my contributions on the StackExchange network are placed under public domain to the extent possible by law. If you see my question or answer, and it's not good enough, for any reason, don't hesitate to leave a comment under it. I'll do my best to improve it. My goal is to leave answers that are very readable and require least background knowledge.

Updated on September 18, 2022

Comments

  • Mirzhan Irkegulov
    Mirzhan Irkegulov almost 2 years

    Suppose I have a very simple NixOS configuration file:

    { config, pkgs, ... }:    
    {
      imports =
        [ # Include the results of the hardware scan.
          ./hardware-configuration.nix
        ];
      # SOME STUFF
      environment.systemPackages = with pkgs; [ emacs gitFull ];
      # SOME STUFF
    }
    

    I know that NixOS implements a module system, and a module is a .nix file. Every .nix file should contain any valid Nix expression (e.g. a function or a set). This means that the NixOS configuration file /etc/nixos/configuration.nix is itself a module, containing a Nix expression.

    I also know that to make the Nix expression in another module visible to the module I'm working with, I can use a built-in import function.

    I want to split the declaration of system packages (the list containing emacs and gitFull) away into file packages.nix. How do I split the NixOS configuration file into separate modules?

  • BillehBawb
    BillehBawb almost 7 years
    How are imports merged? Do they use recursiveUpdate or something like it?
  • CMCDragonkai
    CMCDragonkai over 6 years
    Is there a way to do conditional imports?
  • Warbo
    Warbo almost 6 years
    @CMCDragonkai the value of imports is just a list, so you can append elements to that conditionally, e.g. imports = [ ./foo.nix ./bar.nix ] ++ (if baz then [ ./quux.nix ] else []);
  • Atemu
    Atemu over 2 years
    In practice, conditional imports often lead to infinite recursion errors. You should import the file unconditionally and guard its configuration behind the condition using mkIf: { lib, ... }: { config = lib.mkIf condition { environment.systemPackages = ...; } }