Skip to content
On this page

Modularize Your NixOS Configuration

At this point, the skeleton of the entire system is configured. The current configuration structure in /etc/nixos should be:

$ tree
.
├── flake.lock
├── flake.nix
├── home.nix
└── configuration.nix

The functions of these four files are:

  • flake.lock: An automatically generated version-lock file that records all input sources, hash values, and version numbers of the entire flake to ensure reproducibility.
  • flake.nix: The entry file that will be recognized and deployed when executing sudo nixos-rebuild switch.
  • configuration.nix: Imported as a Nix module in flake.nix, all system-level configuration is currently written here.
  • home.nix: Imported by Home-Manager as the configuration of the user ryan in flake.nix, containing all of ryan's configuration and managing ryan's home folder.

By modifying these files, you can declaratively change the system and home directory status.

As the configuration increases, it becomes difficult to maintain the configuration by relying solely on configuration.nix and home.nix. Therefore, a better solution is to use the Nix module system to split the configuration into multiple modules and write them in a classified manner.

The Nix module system provides a parameter, imports, which accepts a list of .nix files and merges all the configuration defined in these files into the current Nix module. Note that imports will not simply overwrite duplicate configuration, but handle it more reasonably. For example, if program.packages = [...] is defined in multiple modules, then imports will merge all program.packages defined in all Nix modules into one list. Attribute sets can also be merged correctly. The specific behavior can be explored by yourself.

I only found a description of imports in Nixpkgs-Unstable Official Manual - evalModules Parameters: A list of modules. These are merged together to form the final configuration., it's a bit ambiguous...

With the help of imports, we can split home.nix and configuration.nix into multiple Nix modules defined in different .nix files.

For example, ryan4yin/nix-config/v0.0.2 is the configuration of my previous NixOS system with i3 window manager. Its structure is:

shell
├── flake.lock
├── flake.nix
├── home
   ├── default.nix         # here we import all submodules by imports = [...]
   ├── fcitx5              # fcitx5 input method's configuration
      ├── default.nix
      └── rime-data-flypy
   ├── i3                  # i3 window manager's configuration
      ├── config
      ├── default.nix
      ├── i3blocks.conf
      ├── keybindings
      └── scripts
   ├── programs
      ├── browsers.nix
      ├── common.nix
      ├── default.nix   # here we import all modules in programs folder by imports = [...]
      ├── git.nix
      ├── media.nix
      ├── vscode.nix
      └── xdg.nix
   ├── rofi              #  rofi launcher's configuration
      ├── configs
         ├── arc_dark_colors.rasi
         ├── arc_dark_transparent_colors.rasi
         ├── power-profiles.rasi
         ├── powermenu.rasi
         ├── rofidmenu.rasi
         └── rofikeyhint.rasi
      └── default.nix
   └── shell             # shell/terminal related configuration
       ├── common.nix
       ├── default.nix
       ├── nushell
          ├── config.nu
          ├── default.nix
          └── env.nu
       ├── starship.nix
       └── terminals.nix
├── hosts
   ├── msi-rtx4090      # My main machine's configuration
      ├── default.nix  # This is the old configuration.nix, but most of the content has been split out to modules.
      └── hardware-configuration.nix  # hardware & disk related configuration, autogenerated by nixos
   └── nixos-test       # my test machine's configuration
       ├── default.nix
       └── hardware-configuration.nix
├── modules          # some common NixOS modules that can be reused
   ├── i3.nix
   └── system.nix
└── wallpaper.jpg    # wallpaper

For more details, see ryan4yin/nix-config/v0.0.2.

lib.mkOverride, lib.mkDefault, and lib.mkForce

Some people use lib.mkDefault and lib.mkForce to define values in Nix files. As their names suggest, lib.mkDefault and lib.mkForce are used to set default values or force values of options.

You can read the source code of lib.mkDefault and lib.mkForce by running nix repl -f '<nixpkgs>' and then entering :e lib.mkDefault. To learn the basic usage of nix repl, type :? to see the help information.

Here's the source code:

nix
# ......

  mkOverride = priority: content:
    { _type = "override";
      inherit priority content;
    };

  mkOptionDefault = mkOverride 1500; # priority of option defaults
  mkDefault = mkOverride 1000; # used in config sections of non-user modules to set a default
  mkImageMediaOverride = mkOverride 60; # image media profiles can be derived by inclusion into host config, hence needing to override host config, but do allow user to mkForce
  mkForce = mkOverride 50;
  mkVMOverride = mkOverride 10; # used by ‘nixos-rebuild build-vm’

  # ......

lib.mkDefault is used to set default values of options with a priority of 1000 internally, while lib.mkForce is used to force values of options with a priority of 50 internally. If you set a value of an option directly, it will be set with a default priority of 1000 (the same as lib.mkDefault).

The lower the priority value, the higher the actual priority. Therefore, lib.mkForce has a higher priority than lib.mkDefault. If you define multiple values with the same priority, Nix will throw an error.

These functions are useful for modularizing the configuration. You can set default values in a low-level module (base module) and force values in a high-level module.

As an example, in my configuration https://github.com/ryan4yin/nix-config/blob/main/modules/nixos/core-server.nix#L30, I define default values here:

nix
{ lib, pkgs, ... }:

{
  # ......

  nixpkgs.config.allowUnfree = lib.mkDefault false;

  # ......
}

And for my dekstop machine, I force the values to another value in https://github.com/ryan4yin/nix-config/blob/main/modules/nixos/core-desktop.nix#L15:

nix
{ lib, pkgs, ... }:

{
  # import the base module
  imports = [
    ./core-server.nix
  ];

  # override the default value defined in the base module
  nixpkgs.config.allowUnfree = lib.mkForce true;

  # ......
}

lib.mkOrder, lib.mkBefore, and lib.mkAfter

lib.mkBefore and lib.mkAfter are used to set the merge order of list-type options. Like lib.mkDefault and lib.mkForce, they're also useful for modularizing the configuration.

As I mentioned earlier, if you define multiple values with the same override priority, Nix will throw an error. However, with lib.mkOrder, lib.mkBefore, or lib.mkAfter, you can define multiple values with the same override priority, and they will be merged in the order you defined.

To take a look at the source code of lib.mkBefore, run nix repl -f '<nixpkgs>' and then enter :e lib.mkBefore. To learn the basic usage of nix repl, type :? to see the help information:

nix
# ......

  mkOrder = priority: content:
    { _type = "order";
      inherit priority content;
    };

  mkBefore = mkOrder 500;
  mkAfter = mkOrder 1500;

  # The default priority for things that don't have a priority specified.
  defaultPriority = 100;

  # ......

So lib.mkBefore is a shortcut for lib.mkOrder 500, and lib.mkAfter is a shortcut for lib.mkOrder 1500.

To test the usage of lib.mkBefore and lib.mkAfter, let's create a simple Flake project:

shell
# create flake.nix with the following content
 cat <<EOF | sudo tee flake.nix
{
  description = "Ryan's NixOS Flake";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
  };

  outputs = { self, nixpkgs, ... }@inputs: {
    nixosConfigurations = {
      "nixos-test" = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";

        modules = [
          # demo module 1, insert git at the head of list
          ({lib, pkgs, ...}: {
            environment.systemPackages = lib.mkBefore [pkgs.git];
          })

          # demo module 2, insert vim at the tail of list
          ({lib, pkgs, ...}: {
            environment.systemPackages = lib.mkAfter [pkgs.vim];
          })

          # demo module 3, just add curl to the list normally
          ({lib, pkgs, ...}: {
            environment.systemPackages = with pkgs; [curl];
          })
        ];
      };
    };
  };
}
EOF

# create flake.lock
 nix flake update

# enter nix repl environment
 nix repl
Welcome to Nix 2.13.3. Type :? for help.

# load the flake we just created
nix-repl> :lf .
Added 9 variables.

# check the order of systemPackages
nix-repl> outputs.nixosConfigurations.nixos-test.config.environment.systemPackages
[ «derivation /nix/store/0xvn7ssrwa0ax646gl4hwn8cpi05zl9j-git-2.40.1.drv»
  «derivation /nix/store/7x8qmbvfai68sf73zq9szs5q78mc0kny-curl-8.1.1.drv»
  «derivation /nix/store/bly81l03kh0dfly9ix2ysps6kyn1hrjl-nixos-container.drv»
  ......
  ......
  «derivation /nix/store/qpmpvq5azka70lvamsca4g4sf55j8994-vim-9.0.1441.drv» ]

As we can see, the order of systemPackages is git -> curl -> default packages -> vim, which is the same as the order we defined in flake.nix.

Though it's useless to adjust the order of systemPackages, it may be helpful at some other places...

References