Config Parts: modular nixos/hm configuration construction for flake-parts

Most nix flakes that I’ve read (especially “dendritic” ones) do two things when constructing nixos/hm modules:

  1. Import some host/user specific modules, often with some repetitive naming convention like self.nixosModules.myHost-shell
  2. Synchronize the flake output name with some configuration options like netwoking.hostName or home.username, usually via some mkSystem wrapper or (god forbid) specialArgs.

I wrote config-parts to move both of these concerns into the flake-parts module system. This lets you add modules to any nixos/hm configuration from anywhere in your flake. The Implementation is unopinionated and doesn’t affect the behavior of either lib.nixosSystem or lib.homeManagerConfiguration.

I would appreciate any feedback, especially on the option documentation as i had to infer some of the descriptions from usage in the upstream source code.

P.S I wanted to categorize this post as an announcement, but it’s marked as read-only. I assume thats due to my basic trust level. If a mod is able to re-categorize this i would appreciate it.

2 Likes

I have TL3 so I did it.

Oops, accidentally locked you out of replying (Meta/discourse: can't seem to reply to some announcements?), moved into Links which you should be able to reply to.

1 Like

How is this an advantage over flake.parts builtin modules flakeModule?

1 Like

my understanding of the builtin modules output is that is just a different way to expose modules, equivalent to the standard nixosModules and homeModules outputs. the nixosConfigurationArgs and homeConfigurationArgs outputs from this flake module represent the arguments to be passed to lib.nixosSystem or lib.homeManagerConfiguration.

so a configuration like

modules.nixos.myHost = { networking.hostName = "myHost"; };
nixosConfigurations.myHost = lib.nixosSystem {
  modules = [ self.modules.nixos.myHost ];
};

can be rewritten as

nixosConfigurationArgs.myHost.modules = [
  { networking.hostName = "myHost"; }
];

and the configuration will be implicitly created for you like this

nixosConfigurations.myHost =
  lib.nixosSystem self.nixosConfigurationArgs.myHost;

its also not just for modules

nixosConfigurationArgs.myHost = {
  system = "x86_64-linux";
  check = false;
};

the advantage being that you can write nixosConfigurationArgs.myHost = { ... } anywhere in your flake and it will be merged into the arguments for that config. meaning you dont have to manually export a module and import it into your config.

1 Like

Yes, I get that, but the way the flake-parts builtin works is like this:

example
# flake.nix
{
  [...]
  outputs = inputs@{flake-parts, ...}: flake-parts.lib.mkFlake { inherit inputs; } {
    imports = [
      flake-parts.flakeModules.modules
      ./a.nix
      ./b.nix
    ];
  };
}
# a.nix
{ self, inputs, ... }:
{
  flake.modules.nixos.myhost = {
    programs.bash.enable = true;
  };
  flake.nixosConfigurations.myhost = inputs.nixpkgs.lib.nixosSystem {
    modules = [
      self.modules.nixos.myhost
      # [ missing boilerplate like hardware-configuration.nix ]
    ];
  };
}
# b.nix
{
  flake.modules.nixos.myhost.hardware.bluetooth.enable = true;
}

And then the definitions from both a.nix and b.nix will be merged into a single modules.nixos.myhost output, and subsequently into the nixosConfigurations.myhost definition.

Which achieves the same goal but seems much simpler/more “flake-parts idiomatic” to me.

I agree that just using a module is simpler (i dont know about “much simpler” though), but that simplicity comes at the cost of repetition. Each new config you add will need it’s own lib.nixosSystem call site and module import. Thats not too bad in that simple example, but usually theres more boilerplate thats common between all configs (syncing networking.hostName with the output name, configuring nix/nixpkgs, etc.). config-parts makes it trivial to write that boilerplate once through the module system instead of some custom mkSystem function.

example of global config
{ lib, config, self, ... }: {
  flake = {
    nixosConfigurationArgs' = [{
      modules = [
        ({ outputName, ... }: {
          nixpkgs.config.allowUnfree = true; 
          networking.hostName = lib.mkDefault outputName;
        })
      ];
    }];

    homeConfigurationArgs' = [
      ({ outputUser, outputHost, ... }: {
        # Inherit the host configuration's `pkgs` by default.
        pkgs = lib.mkIf (self.nixosConfigurations ? ${outputHost})
          self.nixosConfigurations.${outputHost}.pkgs;

        modules = [{ home.username = lib.mkDefault outputUser; }];
      })
    ];
  };
}

I wont pretend this is anything more than improved ergonomics, so it really comes down to personal preference. For me this is the sweet spot; not a full framework like den, just enough to avoid manually calling the constructor for each new config.