Generating similar NixOS modules dynamically

I’d like to split my configuration.nix into multiple fragments so I can mix and match them in configuration.nix.

This was the initial approach:

# ./configuration.nix
{ config, lib, pkgs, ... }:
{
	imports = [
		./fragments/category1/virtualbox.nix
		./fragments/category2/fonts.nix
	];
}
# ./fragments/category1/virtualbox.nix
{ config, lib, pkgs, ... }:
{
	virtualisation = {
		virtualbox.host.enable = true;
		virtualbox.host.enableExtensionPack = true;
	};
}
# ./fragments/category2/fonts.nix
{ config, lib, pkgs, ... }:
{
	fonts.fonts = with pkgs; [
		# ... some fonts ...
	];
}

However, this approach has several drawbacks because fragments aren’t actual modules:

  1. can’t use fragments in a custom NixOS module because they can’t be imported without modifying the system config
  2. ugly and noisy syntax

A better approach would be to wrap each fragment in an actual module, which will be disabled by default:

# ./configuration.nix
{ config, lib, pkgs, ... }:
{
	imports = [
		./fragments
	];

	fragments = {
		category1 = {
			virtualbox = true;
		};
		category2 = {
#			fonts = true;
		};
	};
}
# ./fragments/default.nix
{ config, lib, pkgs, ... }:
{
	imports = [
		./category1/virtualbox.nix
		./category2/fonts.nix
	];
}
# ./fragments/category1/virtualbox.nix
{ config, lib, pkgs, ... }: 
{
	options.fragments.category1.virtualbox = mkEnableOption "virtualbox";

	config = mkIf config.fragments.category1.virtualbox {
		virtualisation = {
			virtualbox.host.enable = true;
			virtualbox.host.enableExtensionPack = true;
		};
	};
}
# ./fragments/category2/fonts.nix
{ config, lib, pkgs, ... }: 
{
	options.fragments.category2.fonts = mkEnableOption "fonts";

	config = mkIf config.fragments.category2.fonts {
		fonts.fonts = with pkgs; [
			# ... some fonts ...
		];
	};
}

I wanted to simplify this since all modules have only one option (that’s the point). I figured the best thing to do would be to keep the fragments as in the initial approach and create all the modules in bulk (in ./fragments/default.nix).

I tried this first:

# ./configuration.nix
{ config, lib, pkgs, ... }:
{
	imports = [
		./fragments
	];

	fragments = {
		category1 = {
			virtualbox = true;
		};
		category2 = {
#			fonts = true;
		};
	};
}
# ./fragments/default.nix
{ config, lib, pkgs, ... }:

with lib;

let
	cfg = config.fragments;
in {
	options.fragments = {
		category1 = {
			virtualbox = mkEnableOption "virtualbox";
		};
		category2 = {
			fonts = mkEnableOption "fonts";
		};
	};

	config = mkIf cfg.category1.virtualbox (import ./category1/virtualbox.nix { inherit config lib pkgs; })
	      // mkIf cfg.category2.fonts (import ./category2/fonts.nix { inherit config lib pkgs; });
}
# ./fragments/category1/virtualbox.nix
{ config, lib, pkgs, ... }:
{
	virtualisation = {
		virtualbox.host.enable = true;
		virtualbox.host.enableExtensionPack = true;
	};
}
# ./fragments/category2/fonts.nix
{ config, lib, pkgs, ... }:
{
	fonts.fonts = with pkgs; [
		# ... some fonts ...
	];
}

That worked, so I replaced ./fragments/default.nix with this:

{ config, lib, pkgs, ... }:

with lib;

let
	paths = {
		category1 = {
			virtualbox = ./category1/virtualbox.nix;
		};
		category2 = {
			fonts = ./category2/fonts.nix;
		};
	};

	cfg = config.fragments;	
in {
	options.fragments = mapAttrsRecursive (path: value: mkEnableOption (concatStringsSep "." path)) paths;

	config = let
		getCfgOption = path: getAttrFromPath path cfg;
		importInherit = file: import file { inherit config lib pkgs; };

		paths' = mapAttrsRecursive (path: value: mkIf (getCfgOption path) (importInherit value)) paths;
		result = collect isOption paths';
	in mkMerge result;
}

That didn’t work due to infinite recursion, and I can’t figure out what’s causing it and how I could tackle this problem.

terminate called after throwing an instance of 'nix::EvalError'
  what():  infinite recursion encountered, at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/modules.nix:131:21

Anyone see anything obviously wrong with it? Is it a good approach at all?

Welcome to Discourse.

I highly recommend you to take a look at Module page at wiki, especially the “Structure” section. To summarize:

Module structure

Modules are files that contains either an attribute set (system configuration), or a function that takes an attribute set (module arguments) as a parameter and return an attribute set (system configuration).

Module arguments

If the module is in function form, the set that is the function parameter must contain at least the following keys, which are module argument:

  • config
  • options
  • pkgs
  • modulesPath
  • other parameters added with _module.args or specialArgs.

Module schema

The attribute set that produces the system configurations normally has three keys:

  • imports, A list of paths to other modules to import/use/require
  • options, A set of option declarations
  • config, A set of option definitions

But, if the module doesn’t declare any options, i.e., the options key is missing, you can unwrap the config set by moving its key-value pairs a level up to the module set.

With these in mind, let’s return to the premise of your question.

What you call fragments are actually also modules.

I am not sure what you mean by custom NixOS module but I assume you mean the ones that you create (as opposed to the ones provided with Nixpkgs).

The whole module system is there to let you modify the system config. Every option that you define (e.g. virtualisation.virtualbox.host.enable = true;) changes the system config.

As for importing, you are not really suppose to import the modules yourself like this

but rather specify the module paths you want to use inside the imports list and let the module system handle the actual importing.

Your later iterations are unarguably more complex and harder to understand. So I am not sure how you find your initial example uglier.

Are you maybe trying the keep the “configuration.nix” as small as possible or maybe limit the imports list to have a single element there?

I think you’re definitely overthinking this. There’s nothing wrong with the approach you’d settled on just before this paragraph:

In fact I think it’s actually preferable to have one module that lists the other modules, and self-contained modules that both declare their relevant option and define the config necessary to implement it. You only have to mess with one file to e.g. add a new option related to a particular part of your system.

But FWIW, you don’t merge modules with something like this:

You should use lib.mkMerge instead; the module system will handle that more appropriately.

Well, I wanted to save the hassle of maintaining the same boilerplate (the single enable-like option) in an entire class of modules that all have the same structure by creating some sort of module generator, but I guess it’s either impossible or infeasible.