Import list in `configuration.nix` vs `import` function

Hi! I am a new NixOS user, so please inform me if the following question is irrelevant, or has an easy answer.

I am trying to understand how the imports list works in the configuration.nix. I can’t really find how the imports list is used by NixOS. I expect it to work similar to the import function but that does not seem to be the case.

By default, the import function does not pass any variable/environmet to the evaluated expression. However, the NixOS configuration is readily available in the expressions part of the imports list.

So what I get is that NixOS is doing something like (I am using Haskell syntax, please excuse me)

map (\x -> import x currentSystemConfig) imports

and then merge the expression (with // or something similar). currentSystemConfig is the attribute set of the current configuration of NixOS containing the services.xxx attribute set, the programs.xxx attribute set and so on.

In any case, this is really confusing and I don’t find any documentation about the imports list. (There is plenty of information about the import function, which should probably be called eval, at least in my opinion).

Thanks!

7 Likes

As a follow up question:

What happens, if an expression in the imports list defines an imports list itself. Are the imports imported recursively?

configuration.nix file is a NixOS module. Module syntax is described in the NixOS manual.

When system is being built, transitive closure of imports of implicit modules from module-list.nix and the configuration module is loaded and system option is used.

I am not aware of any documentation but here are the main files implementing this:

  • Here, nixos-rebuild initiates building the system.
  • This is the <nixpkgs/nixos> file and its system attribute built by nixos-rebuild.
  • That file includes eval-config.nix, which is where the module system assembles all the modules and starts evaluating them.
  • Module system itself is defined is just a set of functions, mostly concerning how all the stuff declared and defined in modules gets merged together.
  • And here is the module that actually declares the main options for building the system. Most importantly, it defines the value for system.build.toplevel internal NixOS option, which contains the top-level derivation representing the system to be built.
3 Likes

Thanks for the references! The NixOS manual does not state how the imports list works (for example, what is the environment available in expressions elements of the imports list). I will have a look at your links!

1 Like

Yeah, that is mostly an implementation detail and I will send you to the module system source code if you want to learn more. I can only describe what it seems to do on the outside as I have not actually needed to dig any deeper than the stuff I linked.

The module system, roughly, collects all modules, starting with the module inside the <nixos-config> file (usually configuration.nix) and the default modules from module-list.nix, and then adding stuff listed in their imports, recursively. Then paths in imports are imported. Next, if a collected module is a function expression, it gets passed lib, config, options and pkgs as arguments (the latter through internal _module.args.pkgs option), plus whatever is passed to specialArgs or extraArgs (when calling eval-config manually, e.g. using lib.nixosSystem in a flake, but that is discouraged). Then all the resulting attribute sets (mainly containing options, config attributes) are merged resulting in a single big attribute set.

(By the steps described, I do not mean neatly delineated phases but rather climbing up the function call stack.)

7 Likes

There’s no difference between an imported NixOS module and configuration.nix in the sense that they are all just NixOS modules.

Thanks a lot!

I know that configuration.nix is not different from any other module. I was just confused by how different the imports list behaves compared to the import function. It seems the modules in the imports list are really imported and the definitions are merged, while the import function is more of an evaluation of the supplied expression with the given arguments.

The problem also partly stems from the fact that it is difficult to find documentation about the imports list. Why do I have to put my modules into this list? Is it just a shorthand for imports [a, b] = import a; import b;? But that doesn’t seem to be the case at all.

It will take a while until I read through your references. Thanks for your replies!

Those two ‘import’s are really just two different things with similar name.

Nix’s builtins.import really just loads and inserts a Nix expression from a different file in the place where it is called – you could just replace paste the file contents instead of the import call and it should work the same (modulo file path resolution).

Contrary to local nature of the Nix built-in, imports attribute of NixOS module makes the module system add modules to the list of modules to merge into the global namespace. It also supports wider range of arguments, not just paths, as described above.

But you could decide to sidestep the module system imports and use Nix primitives for loading a module. For example, instead of

{ pkgs, lib, config, options, ...}@args:
{
  imports  = [
    ./quxinator.nix
  ];
  config = {
    foo.bar = true;
  };
}

you could do something like

{ pkgs, lib, config, options, ...}@args:
lib.recursiveUpdate { # BEWARE!!!
  config = {
    foo.bar = true;
  };
}
(import ./quxinator.nix args)

The result might end up the same. Or it might fail horribly – especially if both modules declare overlapping options.

You might try to make merging the modules module-aware:

{ pkgs, lib, config, options, ...}@args:
lib.mkMerge [ # Probably safer but still BEWARE!!!
  {
    config = {
      foo.bar = true;
    };
  }
  (import ./quxinator.nix args)
]

but I give you no warranty and there is no reason to avoid using the module system’s imports feature.

3 Likes

I am not trying to avoid the imports list, but understand how it works. And your post above explains very well what is going on. It is actually more or less what I expected in the first post (the map with consecutive merges).

In particular, many examples on the NixOS manual have configurations like this one:

# configuration.nix
---
{ config, pkgs, ... }:

{
  imports =  [ ./module.nix ];
}

And then,

module.nix
---
{ config, ... }:

{   boot.loader.systemd-boot.enable = true; }

But then, the config parameter in the model expression is not needed. This was the source of my confusion. I thought it must be necessary, because it is always there, but it seems like it is not since the attribute set defined in module.nix is just merged into the one defined in configuration.nix.