A NixOS module that optionally includes other top-level modules

In my configuration, I am trying to build a “GUI enabled” module that will provide me with a single ‘enable’ switch to specify whether a machine has a GUI/Desktop or not. Then all my other modules can specify submodules to be included only if that single flag is enabled.

To be clear, the interface I’m trying to create is something like this:

# Somewhere in my top-level configuration file...
my.gui.enable = true;
# In another module managing command-line utilities...
environment.systemPackages = with pkgs; [ fish fzf htop tmux ];

# I want to add a terminal emulator, enable Flatpak, and set some nix options,
# but only if this is machine has a GUI
my.gui.modules = [{
  envrionment.systemPackages = with; pkgs [ alacritty ];
  services.flatpak.enable = true;
  nix.gc.automatic = true;
}];

To this very similar to how I use home-manager’s ‘sharedModules’ option, so I tried following their code to inspire my ‘my.gui’ module:

{ config, pkgs, lib, ... }:
with lib;
let
  cfg = config.my.gui;
in
{
  options.my.gui = {
    enable = mkEnableOption "GUI configurations";
    modules = mkOption {
      type = with type; listOf raw;
      default = [];
    };
    guiConfig = mkOption = {
      type = types.submoduleWith {
        modules = cfg.modules;
      };
      default = {};
    };
  };

  config = mkIf cfg.enable cfg.guiConfig;
}

This results in a recursion error, naturally. I’ve tried just about every permutation of mkIf and mkMerge that I could think of. I get that this is because one of the included modules could potentially set my.gui.enable = false;, which would cause a contradiction. Is there any work-around for this? For example, a way to enforce with types that the assembled guiConfig module can contain whatever attrs, but never a my.gui attribute?

I am currently using a setup where modules uses a mkIf and mkMerge like so:

config = mkMerge [
    {
      # Non-gui config...
    }
    ( mkIf config.my.gui.enable {
      # gui config...
    })
];

This works, but it looks messy, and for the sake of cleanliness I am trying to refactor my configuration so that no module depends on the configuration of other modules (that is, the module’s output configuration can be entirely determined based on the values of its options).

Is there any way I can achieve the UX I am hoping for with these modules?

This is indeed clumsy and has bugged me as well since the very beginning. In short: What you’re trying isn’t possible with the module system because of how module imports work.

It so happens that @infinisil and me recently discussed related ideas at the 23.11 ZHF hackathon.

Silvan, you’ll vividly remember how our brainstorming got me excited. Today I’m proud to show the one-weekend prototype as promised:

Thanks @roberth and @imincik for early feedback!

1 Like

super cool @fricklerhandwerk :+1: any plans to personally continue this? if so, where can i watch for updates?

:heart:

I can’t promise I’ll find time for doing more about it soon, but I guess “like and subscribe” on GitHub? Opening PRs will surely help me get annoyed enough about things being broken to fix them.

1 Like