NixOS module: infinite recursion when using map over config value in config block

I’m writing a custom NixOS module that defines a list of backup jobs via a submodule option. In the config block, I use map over the list from config to generate systemd units. This causes an infinite recursion error when I try to build.

Module definition

# modules/borg-backup.nix
{ config, lib, pkgs, ... }:

let
  moduleConfig = config.services.myBorgBackup;
in
{
  options.services.myBorgBackup = {
    backupJobs = lib.mkOption {
      type = lib.types.listOf (lib.types.submodule {
        options = {
          enable = lib.mkEnableOption "Borg backup job";

          repoPath = lib.mkOption {
            type = lib.types.str;
            example = "/backups/hostname";
            description = "Path to the borg repository.";
          };
        };
      });
      default = [ ];
      description = "List of borg backup jobs.";
    };
  };

  config =
    let
      mkJob = jobConfig: {
        systemd.sockets."borg-backup-socket" = {
          enable = true;
          description = "Socket for borg backup to ${jobConfig.repoPath}";
          # ...
        };
      };
    in
    lib.mkMerge (map mkJob moduleConfig.backupJobs);
}

Usage in a host configuration

# hosts/my-server.nix
{ config, ... }:

{
  services.myBorgBackup.backupJobs = [
    {
      enable = true;
      repoPath = "/backups/my-server";
    }
  ];
}

Error

nix build .#nixosConfigurations.my-server.config.system.build.topLevel returns

error:
       … while calling the 'seq' builtin
         at /nix/store/34r6krygvwvxll74244xxhbxq91f9gr9-source/lib/modules.nix:361:18:
          360|         options = checked options;
          361|         config = checked (removeAttrs config [ "_module" ]);
             |                  ^
          362|         _module = checked (config._module);

       … while evaluating a branch condition
         at /nix/store/34r6krygvwvxll74244xxhbxq91f9gr9-source/lib/modules.nix:297:9:
          296|       checkUnmatched =
          297|         if config._module.check && config._module.freeformType == null && merged.unmatchedDefns != [ ] then
             |         ^
          298|           let

       … while evaluating the option `_module.freeformType':

       … while evaluating the module argument `config' in "/nix/store/v3z1ng2j129anrhllqpd1dpzv8w0syhd-source/modules/mixins/borg-backup/test.nix":

       … if you get an infinite recursion here, you probably reference `config` in `imports`. If you are trying to achieve a conditional import behavior dependent on `config`, consider importing unconditionally, and using `mkEnableOption` and `mkIf` to control its effect.

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error: infinite recursion encountered

What I understand

The config block of my module calls map mkJob moduleConfig.backupJobs. This forces Nix to evaluate the length of the backupJobs list in order to produce the list of attribute sets for lib.mkMerge. But determining the value of backupJobs requires evaluating the merged config of all modules – including this module’s own config block. Right?

What is the idiomatic NixOS module pattern way here?

1 Like

I think something like systemd.sockets."borg-backup-socket" = lib.mkMerge (map mkJob moduleConfig.backupJobs) should work here. You cannot reference config while constructing the very root of your module’s config (like what your lib.mkMerge invocation is doing), because the module system needs that to get the structure of the config.

Thanks! That was it :innocent:

1 Like