Trouble understanding why only one of two really similar lines produce an infinite recursion

tl;dr Config depend on itself. I understand why it could cause problem but from the following two lines, only one run into infinite recursion even tho the backups2 and backups3 variables are the same but one wraping the same value in an array itself:
backups_to_set = [config.nindouja.backups3];
VS
backups_to_set = config.nindouja.backups2

Basically, creating the array in my custom config breaks; but creating it where I need it does not. And I’ll need an array


All relevant files will be at the bottom as minified for posterity but also include link to the actual file on my GitLab instance.

Hello there, I’m trying to make my config really make it really easy to reuse it across different computers. In order to do that, I’ve defined a custom module under config; then each of my users will set attributes of this module based on their needs and then my whole config is generated using reference to my submodule of config.

Explaining that, it should be evident that some self recursion might occur. But it wasn’t that clear to me at first when I “designed” it that way, and also I’ve had some infinite recursion problems I’ve always managed to solve them. Well not this time !

I’ve simplified the file causing the recursion almost as much as possible. It’s below, and on my gitlab.
In the current state, it does build as i’m not referencing my actual config (cf comment ‘CASE 1’); if i replace that line with the one under ‘case 2’, it still build even tho i’m referencing my own config; but if i put the value from my config in an array in goes into an infinite recursion.

You’ll probably need access to my config module definition and usage. You find both minified version below with links to the actual.

Last note; I’ve tried striping backups2 and backups3 of their inner call to the config in the following snippet. But that changes nothing. remote = /run/media/nindouja/portable_bckp/Backups/${config.nindouja.user.username}__at__${config.nindouja.hostname}". As expected because its used in the version that does not cause infinite recursion anyway.

Thanks a lot for taking the time !


Minified version of the file causing infinite recursion

{
  lib,
  pkgs,
  config,
  ...
}: let
  # Common values
  username = config.nindouja.user.username;
  hostname = config.nindouja.hostname;

  # Secret key and resolved password file path
  backupRepoPasswordSopsKey = "users/${username}/backup_repo";
  backupPasswordFilePath = config.sops.secrets."${backupRepoPasswordSopsKey}".path;

  # Function to generate a full backup configuration from a backup submodule defined in ninja-config
  generic_backup = backup: {
    environment.etc."rclone-${backup.id}-backup.conf".text =
      ''
        [${backup.id}_backup]
      ''
      + backup.config;
  };

  # CASE 1 (OK): static data works great
  backups_to_set = [
    {
      id = "localtmp";
      config = ''
        type = alias
        remote = /run/media/${username}/portable_bckp/Backups/${username}__at__${hostname}
      '';
    }
  ];
  # CASE 2 (OK): getting one backup from personal config, and wraping it in a list
  #backups_to_set = [config.nindouja.backups3];
  # CASE 3 (KO): getting all backups from config
  #backups_to_set = config.nindouja.backups2;
in {
  config = lib.mkIf (lib.length backups_to_set > 0) (lib.mkMerge [
    # Individual backup configurations
    (lib.mkMerge (builtins.map generic_backup backups_to_set))

    # Base configuration
    {
      environment.systemPackages = with pkgs; [
        restic
        rclone
      ];

      sops.secrets."${backupRepoPasswordSopsKey}" = {
        owner = config.users.users.${username}.name;
        inherit (config.users.users.${username}) group;
      };
    }
  ]);
}

Relevant part of my config module definition

{
  lib,
  pkgs,
  config,
  ninja-base-config,
  ...
}: let
in {
  options.nindouja = {
    backups3 = lib.mkOption {
      type = submodule {
        options = {
          id = T_str;
          config = T_str; # REF: https://rclone.org/docs/
          includes = mkOption {
            type = listOf str;
            default = [];
          };

          excludes = mkOption {
            type = listOf str;
            default = [];
          };
          timer = mkOption {
            type = str;
            # REF: https://silentlad.com/systemd-timers-oncalendar-(cron)-format-explained
            default = "Monday *-*-* 20:00:00";
          };
        };
      };
    };

    backups2 = mkOption {
      type = listOf (submodule {
        options = {
          id = T_str;
          config = T_str; # REF: https://rclone.org/docs/
          includes = mkOption {
            type = listOf str;
            default = [];
          };
          excludes = mkOption {
            type = listOf str;
            default = [];
          };
          extraArgs = mkOption {
            type = listOf str;
            default = [];
          };
          timer = mkOption {
            type = str;
            # REF: https://silentlad.com/systemd-timers-oncalendar-(cron)-format-explained
            default = "Monday *-*-* 20:00:00";
          };
        };
      });
    };
}

Relevant part of my custom config module config usage

{
  lib,
  config,
  ...
}:  {
  config.nindouja = {
    backups3 = {
      id = "newlocal";
      config = ''
        type = alias
        remote = /run/media/nindouja/portable_bckp/Backups/${config.nindouja.user.username}__at__${config.nindouja.hostname}"
      '';
    };

    backups2 = [
      {
        id = "newlocal";
        config = ''
          type = alias
          remote = /run/media/nindouja/portable_bckp/Backups/${config.nindouja.user.username}__at__${config.nindouja.hostname}"
        '';
      }
    ];
  };
}

Ehhh… ignore what I wrote before.

I think the issue is the combination of a root-level mkIf with a mkMerge inside it, but I’m no longer confident I know the limits of what is and isn’t allowed here.

Haha, no worries, thanks for taking the time to look at it.

I really think it is the root of my problem indeed. But before declaring forfeit and having to rethink my whole config, I’d like to understand why only one of the two use cases causes an infinite recursion when they are as similar as possible in my opinion.
I’m probably wrong, and they are not as close as I think they are, but I need someone to guide me on the “what am I missing”

Can you get away with this?

config = lib.mkMerge [
    # Individual backup configurations
    (lib.mkMerge (builtins.map generic_backup backups_to_set))

    # Base configuration
    (lib.mkIf (lib.length backups_to_set > 0) {
      environment.systemPackages = with pkgs; [
        restic
        rclone
      ];

      sops.secrets."${backupRepoPasswordSopsKey}" = {
        owner = config.users.users.${username}.name;
        inherit (config.users.users.${username}) group;
      };
    })
  ];
1 Like

As for that, it’s probably due to the fact that Nix lists are lazy, so you can evaluate lib.length [ a ] to 1 without forcing an evaluation of a. But lib.length a does require evaluating a, of course.

1 Like

I’ve missed your original answer but thanks to the history i could read it.

Puting the mkmerge down was the solution ! I’ve achieve something similar in // of your second answer. But all the credits to you <3

config = lib.mkIf (lib.length backups_to_set > 0) (lib.mkMerge [
    # Individual backup configurations
    # (lib.mkMerge (builtins.map generic_backup backups_to_set))

    # Base configuration
    {
      environment = {
        etc = lib.mkMerge (builtins.map generic_backup_light backups_to_set);
        systemPackages = with pkgs; [
          restic
          rclone
        ];
      };

      sops.secrets."${backupRepoPasswordSopsKey}" = {
        owner = config.users.users.${username}.name;
        inherit (config.users.users.${username}) group;
      };
    }
  ]);

Obvisously I’ve adapted generic_backup_light to not rewrite environment.etc

I’ll try and port this to my not-minified version. I’ll have to duplicate the mkMerge as you suggested earlier. But that is actually the true solution, maybe you should edit back the 1st message and i accept it; or quote it in the current solution.

EDIT: first mkMerge is most certainly useless. But it works for sure

I new I should have asked help earlier; Nix has the best community <3