NixOS Flake setup that inherits from a Flake (Nix syntax issue)

I have a generic NixOS setup as a flake in a public repository. I want to use that as a basis for other, more personalized NixOS setups, flakes in other repositories that inherit from the generic setup. I’m struggling a bit to get the last bits of the inheritance to work.

Generic Setup aka “parent”

The flake for the generic setup looks like this: (GitLab repo)

{
  description = "Multi-host operating system configuration with NixOS";

  inputs = {
    nixos-hardware.url = "github:NixOS/nixos-hardware?ref=master";
    nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
  };

  outputs = { nixpkgs, ... } @attrs:
    let
      commonModules = name: [
        ./hosts/${name}
        ./software
        ./system
        ./users
      ];

      mkSystem = name: cfg: nixpkgs.lib.nixosSystem {
        system = cfg.system or "x86_64-linux";
        modules = (commonModules name) ++ (cfg.modules or [ ]);
        specialArgs = attrs;
      };

      hostEntries = builtins.attrNames (builtins.readDir ./hosts);
      systems = builtins.listToAttrs (map (name: { inherit name; value = { }; }) hostEntries);
    in
    {
      nixosConfigurations = nixpkgs.lib.mapAttrs mkSystem systems;
    };
}

The idea is that you define hosts as directories in the hosts/ folder. A “generic” host definition serves for the “inheritance”, later.

Customized Setup aka “child”

The flake implementing the customized NixOS setup is almost identical, only that it has the generic setup, “painless”, as an input in addition. (Copier template)

{
  description = "Multi-host operating system configuration with NixOS";

  inputs = {
    nixos-hardware.url = "github:NixOS/nixos-hardware?ref=master";
    nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
    painless.url = "gitlab:painless-software/nixos-config?ref=main";
  };

  outputs = { nixpkgs, painless, ... } @attrs:
    let
      commonModules = name: [
        ./hosts/${name}
        ./software
        ./system
        ./users
      ];

      mkSystem = name: cfg: nixpkgs.lib.nixosSystem {
        system = cfg.system or "x86_64-linux";
        modules = (commonModules name) ++ (cfg.modules or [ ]) ++ painless.nixosConfigurations.generic.modules;
        specialArgs = attrs;
      };

      hostEntries = builtins.attrNames (builtins.readDir ./hosts);
      systems = builtins.listToAttrs (map (name: { inherit name; value = { }; }) hostEntries);
    in
    {
      nixosConfigurations = nixpkgs.lib.mapAttrs mkSystem systems;
    };
}

What I try to do is to concatenate the modules of the generic setup with the modules of the local setup. Unfortunately, this doesn’t work as expected. nix flake check errors out (complete log).

Error: attribute ‘modules’ missing

$ nix flake check
evaluating flake...
checking flake output 'nixosConfigurations'...
...
error:
       … while checking flake output 'nixosConfigurations'
         at /nix/store/vbp66ij6whnibdhldm5mpg5f3bvlykzd-source/flake.nix:29:7:
           28|     {
           29|       nixosConfigurations = nixpkgs.lib.mapAttrs mkSystem systems;
             |       ^
           30|     };
       … while checking the NixOS configuration 'nixosConfigurations.example'
       … while calling the 'seq' builtin
         at /nix/store/22r7q7s9552gn1vpjigkbhfgcvhsrz68-source/lib/modules.nix:334:18:
          333|         options = checked options;
          334|         config = checked (removeAttrs config [ "_module" ]);
             |                  ^
          335|         _module = checked (config._module);
       (stack trace truncated; use '--show-trace' to show the full, detailed trace)
       error: attribute 'modules' missing
       at /nix/store/vbp66ij6whnibdhldm5mpg5f3bvlykzd-source/flake.nix:21:67:
           20|         system = cfg.system or "x86_64-linux";
           21|         modules = (commonModules name) ++ (cfg.modules or [ ]) ++ painless.nixosConfigurations.generic.modules;
             |                                                                   ^
           22|         specialArgs = attrs;
       Did you mean _module?

How can I get that working? Or is the approach wrong?

I am using a similar setup (general dotfiles link, link to nixosSystem wrapper) to generate hosts. Works well enough.

In your case the error is specifically this:

attribute 'modules' missing
       at /nix/store/vbp66ij6whnibdhldm5mpg5f3bvlykzd-source/flake.nix:21:67:
           20|         system = cfg.system or "x86_64-linux";
           21|         modules = (commonModules name) ++ (cfg.modules or [ ]) ++ painless.nixosConfigurations.generic.modules;

nixosConfigurations is not an output that produces modules, it’s an output to produce the full NixOS configuration.

You could try referencing the individual modules by digging around painless.nixosConfigurations.generic._module.args.modules but you will probably have a better time if you have a separate nixosModules.generic output in painless flake that would be a collection of modules.

You can then reuse it in both flakes:

  • painless flake’s outputs can take self as an argument, so you can refer to flake’s own outputs in other outputs
  • “child” flake would refer to the modules as painless.nixosModules.<whateverYouDecideToCallTheModule>

Shameless plug with an example. The modules and nixosConfigurations are also explained on that page.

1 Like

That sounds like a very good idea, especially to make the setup more flexible. IIUC, the nixpkgs.lib.nixosSystem function in my setup generates a more rigid form of that under the hood instead. (Please note that I’m a Nix beginner, I might be mistaken.)

I see the value in this. My current approach is more static (in the sense that all composition of the setup happens in the file system) though, while you use importApply to include every module separately.

For the moment I’m happy to have all hosts that I set up inherit the same rigid configuration (i.e. “software”, “system”, “users”) except for what makes the hardware work (which is in “hosts”). Maybe I don’t need to refer to the modules of the parent repository one-by-one, or at all? What would be the simplest way to make the inheritance work?

while you use importApply to include every module separately.

That specific bit of code is not really relevant here. The flake-modules are a concept from flake.parts. Related to NixOS modules, but not exactly the same thing. flake.parts is a library that allows automating a lot of boilerplate that can be used cross flakes, but it is not necessary.

importApply is also a function from flake.parts.

Link in my previous discourse post also talks about flake.parts btw.

Maybe I don’t need to refer to the modules of the parent repository one-by-one, or at all? What would be the simplest way to make the inheritance work?

Depending on the purpose of child flake it might not be necessary at all. By sheer coincidence, there’s another post from today that talks about this.

You could also try using Haumea to auto-generate the imports.

Haumea looks like something I might go for in the long run. Nice!

For now, I was able to make my setup work. I’ve added nixosModules to the outputs of the parent flake: (Thank you!)

    {
      nixosConfigurations = nixpkgs.lib.mapAttrs mkSystem systems;
      nixosModules = builtins.listToAttrs (map (name: { inherit name; value = commonModules name; }) hostEntries);
    };

When I redefine values from the configuration I inherit from I need to now use lib.mkForce to override the value. Is that the way you’re meant to do it? Or is there a better way?

Check out the “Under the hood” section on modules wiki page.

Basically by using mkForce you’re telling the evaluator how exactly the values should be merged. Check this thread for more detail.