Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,23 @@ rec {
in mergedOption.type;
};

# A type that is one of several submodules, similiar to types.oneOf but is usable inside attrsOf or listOf
# submodules need an option with a type str which is used to find the corresponding type
taggedSubmodules =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
taggedSubmodules =
taggedSubmodule =

When used by itself, the option value is just a single submodule.
The naming convention describes the option value rather than the definition syntax (or whether multiple defs are ok).

Also apply to name below.

{ types
, specialArgs ? {}
}: mkOptionType rec {
name = "taggedSubmodules";
description = "one of ${concatStringsSep "," (attrNames types)}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description = "one of ${concatStringsSep "," (attrNames types)}";
description = "submodule with type tag";
descriptionClass = "composite";

The valid type values can be documented in a generated type option.

check = x: if x ? type then types.${x.type}.check x else throw "No type option set in:\n${lib.generators.toPretty {} x}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check should return a bool, as it's used by either to do rudimentary but lazy type checks.

Assuming we keep the syntax as { type = "gpt"; partitions = ...; }, this should do an evalModules to get type option value, with something like freeformType = lazyAttrsOf raw; to ignore the other options. Alternatively we could handle the module arguments in a custom manner if that leads to better error messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to use something like this in my own code. Could you offer more details? Where should evalModules be called? Shouldn't we pass it something like defs from merge? How do we do that in check wherex is only one of defs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm. I think I have figured out. evalModules should be called in merge. check should simply check if it's an attrset

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally you'd only call evalModules in one place to form the entrypoint of your configuration system. For example, in NixOS this would be nixosSystem (or a function that it calls), but often you'll be integrating your code into an existing configuration system like NixOS, nix-darwin, home-manager etc, which takes care of this for you. If that is the case, you may not even need evalModules functionality, or you would use types.submodule to do it for you and attach it to a new option in the existing configuration system. It is rare to have to deal with evalModules, and, I'd say, at least as rare to have to deal with merge and check directly, whose contracts follow specific rules that you may not expect, such as not checking the whole value, and doing more of the checking in the result of merge. (We should document them after our planned changes in #391544)
So do you really need to call these directly, or what is your use case?

Copy link
Contributor

@hgl hgl Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation. I need to define values that confirm to taggedSubmodule with type checking. I simply copied the implement to be able to use it right now and encountered this error mentioned by Ma27. I understand what evalModules does, just need to figure out how to solve it by sticking evalModules at the right place.

merge = loc: foldl'
(res: def: types.${def.value.type}.merge loc [
(lib.recursiveUpdate { value._module.args = specialArgs; } def)
])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes it impossible btw to use mkMerge if one of the items to be merged is missing a type. Minimal example in disko where I encountered this ~yesterday:

{ lib, ... }:

with lib;

let
  mkDataset = cfg: mkMerge [ cfg { type = "zfs_fs"; } ];
  mkLegacyMount = p: { mountpoint = p; options.mountpoint = "none"; };
in {
  disko.devices.zpool.tank.datasets = mapAttrs (const mkDataset) {
    "foo" = mkLegacyMount "/foo";
    "bar" =mkLegacyMount "/bar";
  };
}

This gives the following error:

       error: No type option set in:
       {
         mountpoint = "/bar";
         options = {
           mountpoint = "none";
         };
       }

AFAIU this happens because each declaration on its own is passed into the wrapped type, i.e. the stuff from mkLegacyMount is passed unmerged, i.e. without the type from mkDataset.

Anyways, having attr-sets inside the module system where the surroundings (i.e. mkIf/mkMerge/etc) "silently" stop working[1] isn't very nice and it would be cool if this could get fixed. Alternatively I'd also be fine with a better error message that catches this issue properly (it's already rather simple to break a module evaluation in a way that it's hard to figure out the culprit and I'd rather not have yet another case).

[1] well, it fails, but it doesn't tell you that it fails because of mkMerge.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could probably be solved by doing an evalModules just to figure out the type and then evaluate the submodule again. (See https://github.com/NixOS/nixpkgs/pull/254790/files#r1323465281)

{ };
Comment on lines +722 to +726
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same solution as in check should be used to get the type here.

nestedTypes = types;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO

  • return the options for documentation getSubOptions. Return an enum option for the type tag, or maybe it should be more recognizable.
  • type merging would be nice, but we can scope that out. If not, we should probably make it behave like enum, as in applying the union to the type tag, and merging modules for any overlapping definitions.


submoduleWith =
{ modules
, specialArgs ? {}
Expand Down
51 changes: 51 additions & 0 deletions nixos/doc/manual/development/option-types.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,57 @@ Submodules are detailed in [Submodule](#section-option-types-submodule).
options. This is equivalent to
`types.submoduleWith { modules = toList o; shorthandOnlyDefinesConfig = true; }`.

`types.taggedSubmodules` { *`types`*, *`specialArgs`* ? {} }

: Like `types.oneOf`, but takes an attrsSet of submodule in types. Those need to have a type option
which is used to find the correct submodule.

::: {#ex-tagged-submodules .example}
### Tagged submodules
```nix
let
submoduleA = submodule {
options = {
type = mkOption {
type = str;
};
foo = mkOption {
type = int;
};
};
};
submoduleB = submodule {
options = {
type = mkOption {
type = str;
};
bar = mkOption {
type = int;
};
};
};
in
options.mod = mkOption {
type = attrsOf (taggedSubmodule {
types = {
a = submoduleA;
b = submoduleB;
};
});
};
config.mod = {
someA = {
type = "a";
foo = 123;
};
someB = {
type = "b";
foo = 456;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foo = 456;
bar = 456;

};
};
```
:::

`types.submoduleWith` { *`modules`*, *`specialArgs`* ? {}, *`shorthandOnlyDefinesConfig`* ? false }

: Like `types.submodule`, but more flexible and with better defaults.
Expand Down