How to change the default NixOS option in a list of attrsets described as "*"

I want to have a module that looks like this:

{ config, lib, ... }:
{
  swapDevices.*.options = lib.mkDefault [ "defaults" "nofail" "discard=pages" ];
}

This doesn’t work because using * to mean, “all elements of the list,” isn’t a recognized syntax.

Is there an easy way to do this? I can think of two ideas, both of which seem pretty terrible.

  1. Specify my swap device(s) before this module gets imported, and change it to use lib.lists.forEach to modify each device.
  2. Patch <nixpkgs>/nixos/modules/config/swap.nix, where it hard-codes the option’s default value.
  1. define a template attrset and then merge that into each of your others
let template = { options = [ … ]}; in {
  swapDevices = {
    foo = template // { … };
    bar = template // { … };
  };
};

Multiple option/type declarations are mergeable. So something along the lines of

{ lib, ... }:
{
  options.swapDevices = lib.mkOption {
    type = types.listOf (types.submodule {
      config = {
        options = [ "default" ... ];
      };
    });
  };
}

(untested though)

I want to specify my device(s) in separate modules from this one, so I don’t think that’ll work.

If that strategy works, then I think the correct code would be this:

# swap-defaults.nix
{ config, lib, ... }:
{
  options.swapDevices = lib.mkOption {
    type = lib.types.listOf (lib.types.submodule (
      { config, options, ... }: {
        options.options = lib.mkOption {
          default = [ "defaults" "nofail" "discard=pages" ];
        };
      }
    ));
  };
}

This by itself passes nix flake check, but when I try to add another module that actually adds a swap device…

swapDevices = [ { device = "/dev/whatever"; } ];

I get this error:

The option `swapDevices."[definition 1-entry 1]".options' in `/nix/store/...-source/nixos/modules/config/swap.nix' is already declared in `/nix/store/.../swap-defaults.nix'.

Honestly, I think I’m going to patch nixpkgs after all, unless/until someone else jumps in and gives a better answer. :face_exhaling:

I just bodged together a stupid little proof of concept in Nixpkgs that would solve this problem with a new function lib.mkForAllAttrs, like so:

{ config, lib, ... }:
{
  swapDevices = lib.mkForAllAttrs (name: {
    options = lib.mkDefault [ "defaults" "nofail" "discard=pages" ];
  });
}

Should I try to clean it up and submit a PR? I think it’d be generally useful, and I also don’t know of a better way to do this.

3 Likes

Sure, that’d be awesome!

I’d also like to set random-encryption-related swap options in this way, each in a different module, so hopefully it won’t be too hard to make sure that your PR also works for options that are declared in nested submodules like that.

How does this work without running into infrec?

This should work:

{ lib, ... }:
{
  options.swapDevices = lib.mkOption {
    type = lib.types.listOf (lib.types.submodule {
        config.options = lib.mkDefault [ "defaults" "nofail" "discard=pages" ];
    });
  };
}

@rhendric Interesting, I don’t think that should work without running into infinite recursion, but I’d be happy to be proven wrong!

3 Likes

It’s a patch to the merge logic of the attrsOf (and in a real version probably lazyAttrsOf as well) type. Currently that merge function collects all definitions, zips over their attributes, and recursively merges the list of values at each attribute. In my patch, the merge function collects all non-mkForAllAttrs definitions, zips over their attributes, appends the values at each attribute with the results of applying any mkForAllAttrs functions to that attribute name, and recursively merges that combined list.

If this is redundant with the option merging approach it may not be worth it to pursue, but it’s a little easier to use perhaps?

1 Like

PR here: lib.modules: add mkForAllValues by rhendric · Pull Request #314058 · NixOS/nixpkgs · GitHub

2 Likes

This should work

It does! And for options that are declared in more-deeply-nested submodules, just use dot notation to navigate all the way down.

{ lib, ... }:
{
  options.swapDevices = lib.mkOption {
    type = lib.types.listOf (lib.types.submodule {
      config.randomEncryption.enable = lib.mkDefault true;
    });
  };
}

My PR got through a light round of feedback, but seems likely to languish indefinitely if there isn’t more interest in it. I’m not going to push on it because this isn’t a feature I need. If you’d want to use it, show your support.

1 Like

I get around this issue by defining a new option and constructing the final configuration from that. E.g.

{ config, lib, ... }:
{
  options.custom.swapDevices = lib.mkOption {
    type = lib.types.listOf lib.types.attrs; # Probably want to use a more complete type...
  };

  config.swapDevices = map (attrs: attrs // {
    options = (attrs.options or []) ++ [ "defaults" "nofail" "discard=pages" ];
  }) config.custom.swapDevices;
}

This doesn’t work, though, when you do not control every module that adds entries to swapDevices (and not to custom.swapDevices). In that case the PR by @rhendric could solve it.