Adding custom options to nixpkgs modules with arbitrary attrSets

Hello everyone!

I am looking to modularize my nixOS configuration and for many of my modules, all I need to do is add options and config to existing nixpkgs modules.

An example of this is using restic. It works by creating an arbitrary attrSet of named backups, and creating systemd units for each one.

I am attempting to add the following option:

options.services.restic.backups = lib.mkOption {
  type = lib.types.attrsOf (
    lib.types.submodule (
      { name, ... }:
      {
        options = {
          dailySnapshotsToKeep = lib.mkOption {
            type = lib.types.int;
          };
        };
      }
    )
  );
};

This is a convenience option which I would like to use as a wrapper for restic’s pruneOpts option. Here is where I run into my issue; the following is my intuitive attempt to implement config based on this option:

config.services.restic.backups = lib.mapAttrs 
  (
    name: backup:
    backup // 
    { 
      pruneOpts = [ 
        "--keep-daily ${ lib.toString backup.dailySnapshotsToKeep }" 
      ]
    }
  ) 
  config.services.restic.backups

This implementation will not work due to infinite recursion, because config.services.restic.backups requires itself to be evaluated before itself.

After reading through the manual and watching this Nix Hour, it seems that the solution would be to push down the dependencies like so:

config.services.restic.backups.<backupName>.pruneOpts = [
  "--keep-daily ${ lib.toString config.services.restic.backups.<backupName>.dailySnapshotsToKeep }" 
];

This would prevent infinite recursion by using more specific paths. The issue in this case is that I can’t know what the attributes on backups are, so I can’t use these more specific paths.

I currently have two solutions for how to get around this infinite recursion, both of which are suboptimal in my opinion:

  1. Change the path to my option to something like options.myOptions.services.restic.backups

    This would allow me to loop over the attributes of my custom option, getting around the recursion, but would in turn require me to maintain the same set of backups attributes in two separate options.

  2. Disable the restic module from nixpkgs, paste its contents into my custom module

    Here I can implement all of the options and config in one place, but this would require me to maintain all the unrelated parts of the module, which is already being done by the module’s maintainers in nixpkgs

Are there any other options for making changes to arbitrarily defined attribute sets in this way, or have I run into a limitation of the nix module system?

Thanks for the help!

2 Likes

Here’s how to do it:

{ lib, ... }: {
  options.services.restic.backups = lib.mkOption {
    type = lib.types.attrsOf (
      lib.types.submodule (
        { name, config, ... }:
        {
          options = {
            dailySnapshotsToKeep = lib.mkOption {
              type = lib.types.int;
            };
          };

          config = {
            pruneOpts = [ 
              "--keep-daily ${ lib.toString config.dailySnapshotsToKeep }" 
            ];
          };
        }
      )
    );
  };
}

:slight_smile:

3 Likes