`specialArgs` with `extendModules` only uses final extension

The specialArgs for extendModules only provides the final extension. For example, in the following case, config.test will be c no matter where it’s accessed:

a = nixpkgs.lib.nixosSystem {
  modules = [
    ({ test, ... }: { config.test = builtins.trace test test; })
  ];
  specialArgs.test = "a";
};
b = a.extendModules {
  modules = [
    ({ test, ... }: { config.test = builtins.trace test test; })
  ];
  specialArgs.test = "b";
};
c = b.extendModules {
  modules = [
    ({ test, ... }: { config.test = builtins.trace test test; })
  ];
  specialArgs.test = "c";
};

Is there any way for each module to get its defined value, such as the config.test for module a being a and the config.test for module b being b?

All modules in a module system invocation get the same arguments. That’s a basic principle of how it’s supposed to work. The “sequentiality” you’re looking for is something the module system is designed to avoid.

If you’d give more detail about why you want this, we might be able to suggest a better solution.

1 Like

Broadly, I’m trying to avoid some infinite recursion errors that occur when iterating over config.users.users and config.fileSystems. To that end I’ve split my config into a base layer, a default layer, a module layer, and an extension layer, though I realize now that only the last two would work as you described, since all the layers would get the module layer if it were passed into specialArgs.

Perhaps you can avoid iterating over them entirely. If you want to do a uniform process on every entry in an attrsOf submodule, you can do that be re-declaring the option and adding an additional module to the submodule type itself. Type merging will put it all together.

Hmm… Could you give me an example? In my case specifically, I have a loginUsers attribute that filters the normal users, so that I don’t have to constantly state the users necessary for home-manager, zpools, etc., as well as a filter for persisted filesystems that sets their neededForBoot attribute accordingly. Since fileSystems and users seems to depend on each other, this creates a cyclic dependency.

If you wish, I could give you the code for the loginUsers and fileSystems filter.

Code rather than descriptions would probably be helpful, yeah.

This seems much more as if you want loginUsers.<name> which holds various info you then use to apply stuff to your tools and home-manager.users.<name> and users.users.<name>

Originally, I was putting everything into loginUsers then merging it into users, but I needed the defaults and sometimes I wanted to change users as well, but I can’t change it and have loginUsers change as well.

loginUsers:

# TODO: This setup is causing recursion errors.
#       After I clean this up, I can remove all lib.syvl.users instances.
# TODO: Do the users really need the levels in their attribute sets?

{
  config,
  options,
  lib,
  levels,
  ...
}:
let
  rootConfig = config;
  cfg = config.users;
in
with lib;
{
  options = {
    loginUsers = mkOption {
      type = types.attrs;
      readOnly = true;
    };
    users.users = mkOption {
      type =
        with types;
        attrsOf (
          submodule (
            { config, ... }:
            {
              options = {
                level = mkOption {
                  type = nullOr (enum levels);
                  default = null;
                };
                domain = mkOption {
                  type = nonEmptyStr;
                  default = "syvl.org";
                };
                email = mkOption {
                  type = nonEmptyStr;
                  default = "${lib.syvl.mkUser.name config.description}@${rootConfig.networking.hostName}.${config.name}.${config.domain}";
                };
              };

              # Adapted From: https://discourse.nixos.org/t/how-to-change-the-default-nixos-option-in-a-list-of-attrsets-described-as/45561/9?u=syvlorg
              config.group = lib.syvl.mkUserDefault config.name;

            }
          )
        );
    };
  };
  config = {
    assertions = map (level: {
      assertion = (count (user: user.level == level) (attrValues config.loginUsers.users)) == 1;
      message = "there must be one and only one ${level} user";
    }) levels;
    users.groups = mapAttrs (user: info: { gid = info.uid; }) config.loginUsers.users;
    loginUsers =
      let
        users = (filterAttrs (n: getAttr "isNormalUser") cfg.users) // {
          inherit (cfg.users) root;
        };
        usernames = attrNames users;
      in
      mkMerge [
        (mapAttrs
          (
            n: v:
            if ((isAttrs v) && ((intersectLists usernames (attrNames v)) == usernames)) then
              (getAttrs usernames v)
            else
              v
          )
          (
            removeAttrs config.users (
              [ "users" ] ++ (attrNames (filterAttrs (n: v: (v._type or "") != "option") options.users))
            )
          )
        )
        (mapAttrs' (user: info: nameValuePair info.level info) (
          filterAttrs (user: info: info.level != null) users
        ))
        {
          inherit users;
        }
      ];
  };
}

fileSystems:

{
  config,
  super,
  lib,
  hostName,
  ...
}:
with lib;
let
  notNeededForBoot = attrNames (filterAttrs (n: v: !v.neededForBoot) super.config.fileSystems);
in
{
  fileSystems = mapAttrs' (
    n: v:
    let
      parents = filter (flip hasPrefix n) notNeededForBoot;
      p = if (parents == [ ]) then null else (syvl.last parents);
    in
    nameValuePair p {
      neededForBoot = true;
    }
  ) config.preservation.preserveAt;
}

For fileSystems, I’d like to be able to use users with disko, but since disko sets fileSystems, that’s not possible at the moment.