Options, optionals, mkif, mkmerge,.. Alternative way?

I get an error while building my config after a small change.

error: infinite recursion encountered

   at /nix/store/i0mxmspkbn8bzdz4g71gp441lrpv4r8r-nixpkgs-patched-d471a92/lib/modules.nix:182:21:

I want to install/build a new system without agenix using a hashed password. Afterwards I flip the ncfg.age option to true and rebuild the system. Now the system uses the agenix protected secrets

In my host/machine’s config I set the option ncfg.age = true;

I think it has something to do with the config.ncfg.age option.

What could I do to fix this?

Thank you

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

let
  cfg = config.ncfg;
in
{
  imports = [
    ./bluetooth
    ./network
    ./nix
    ./selinux
    ./shell
    ./smartd
    ./ssh
    ./sshd
    ./zfs
  ];

  options.ncfg = {
    age = lib.mkEnableOption "Enable age encrypted configurations";
    primaryUser.name = lib.mkOption {
      default = "myuser";
      example = "myuser";
    };
    # 'mkpasswd -m sha-512 | xclip'
    primaryUser.psswrd-age = lib.mkOption {
      default = "passwords/users/myuser";
    };
    rootUser.psswrd-age = lib.mkOption {
      default = "passwords/users/root";
    };
    personal = lib.mkEnableOption "Enable personal configurations";
    stateVersion = lib.mkOption {
      example = "21.11";
    };
  };

  config = {
    home-manager.useGlobalPkgs = true;

    system.stateVersion = cfg.stateVersion;
    home-manager.users = {
      "${cfg.primaryUser.name}" = { ... }: {
        home.stateVersion = cfg.stateVersion;
      };
      "root" = { ... }: {
        home.stateVersion = cfg.stateVersion;
      };
    };

    i18n = {
      defaultLocale = "en_IE.UTF-8";
      extraLocaleSettings = {
        LC_TIME = "en_GB.UTF-8";
      };
    };

    console = {
      # https://github.com/NixOS/nixpkgs/issues/114698
      earlySetup = true; # Sets the font size much earlier in the boot process
      colors = [
        "f8f8f8"
        "a60000"
        "005e00"
        "813e00"
        "0031a9"
        "721045"
        "00538b"
        "282828"
        "ffffff"
        "972500"
        "315b00"
        "70480f"
        "2544bb"
        "8f0075"
        "30517f"
        "000000"
      ];
      font = "Lat2-Terminus16";
      # font = "ter-v20n"
      # keyMap = "us";
      # keyMap = "colemak/colemak";
      useXkbConfig = true; # Use same config for linux console
    };

    services.xserver = {
      enable = false; # but still here so we can copy the XKB config to TTYs
      xkbVariant = "colemak";
      xkbOptions = "caps:super,compose:ralt,shift:both_capslock";
      autoRepeatDelay = 300;
      autoRepeatInterval = 35;
    };

    systemd.services.numLockOnTty = {
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        ExecStart = lib.mkForce (pkgs.writeShellScript "numLockOnTty" ''
          for tty in /dev/tty{1..6}; do
              /run/current-system/sw/bin/setleds -D +num < "$tty";
          done
        '');
      };
    };

    security = {
      # sudo.enable = false;
      sudo.enable = true; # TODO: Can we make the move to doas, should we?
      doas = {
        enable = true;
        extraRules = [
          {
            users = [ cfg.primaryUser.name ];
            noPass = true;
            cmd = "nix-collect-garbage";
            runAs = "root";
          }
        ];
      };
    };

    services.fwupd.enable = true; # firmware update

    users = {
      mutableUsers = false;
      users = {
        "${cfg.primaryUser.name}" = {
          isNormalUser = true;
          home = "/home/${cfg.primaryUser.name}";
          uid = 1000;
          description = "The primary user account";
          extraGroups = [ "systemd-journal" "wheel" ];
          passwordFile = config.age.secrets."${cfg.primaryUser.psswrd-age}".path;
        };
        root.passwordFile = config.age.secrets."${cfg.rootUser.psswrd-age}".path;
      };
    };
  ### true builds
  # } // lib.optionalAttrs (true) {
  ### config.ncfg.age doesn't build
  } // lib.optionalAttrs (config.ncfg.age) {
    age.secrets."passwords/users/${cfg.primaryUser.name}".file = ../../secrets/passwords/users/${cfg.primaryUser.name}.age;
    age.secrets."passwords/users/root".file = ../../secrets/passwords/users/root.age;
  };
}

this works.
Is there a more elegant solution?

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

let
  cfg = config.ncfg;
in
{
  options.ncfg = {
    age = lib.mkEnableOption "Enable age encrypted configurations";
    primaryUser.name = lib.mkOption {
      default = "someusername";
      example = "someusername";
    };
    primaryUser.psswrd-age = lib.mkOption {
      default = "passwords/users/someusername";
    };
    rootUser.psswrd-age = lib.mkOption {
      default = "passwords/users/root";
    };
  };

  config = lib.mkMerge [
    {
      users = {
        mutableUsers = false;
        users = lib.mkMerge [
          {
            "${cfg.primaryUser.name}" = {
              isNormalUser = true;
              home = "/home/${cfg.primaryUser.name}";
              uid = 1000;
              description = "The primary user account";
              extraGroups = [ "systemd-journal" "wheel" ];
            };
          }

          (lib.mkIf (! cfg.age) {
            ## mkpasswd -m sha-512
            "${cfg.primaryUser.name}".hashedPassword = "SomeHash";
            root.hashedPassword = "SomeHash";
          })

          (lib.mkIf cfg.age {
            "${cfg.primaryUser.name}".passwordFile = config.age.secrets."${cfg.primaryUser.psswrd-age}".path;
            root.passwordFile = config.age.secrets."${cfg.rootUser.psswrd-age}".path;
          })

        ];
      };
    }

    (lib.mkIf cfg.age {
      age.secrets."passwords/users/${cfg.primaryUser.name}".file = ../../secrets/passwords/users/${cfg.primaryUser.name}.age;
      age.secrets."passwords/users/root".file = ../../secrets/passwords/users/root.age;
    })
  ];
}

I’ve seen mkMerge and mkIf combos used a fair bit in nixpkgs and I think your solution is pretty elegant.

One alternative to mkMerge in this case could be something like this:

age.secrets = lib.mkIf cfg.age { ... };

And for the users section you could just push down the mkIf, so the mkMerge doesn’t have to be used.