lib.modules: submodule emptyValue must evaluate sub-option defaults

`emptyValue` for `types.submodule` was `{ value = { }; }`, i.e. none
of the type-declared options or their defaults.

Previously submodules without defs would simply fail for lack of a default,
but since PR #500104 repurposed `emptyValue` as the fallback when no
definitions exist, submodule options without an explicit `default = { }`
(new additions) silently lost their sub-option defaults
(e.g. `requiredFeatures.devnet` in the nixos-test-driver, #511413).

Main change: `emptyValue`: `{ }` -> `{ value = base.config; }`

Additionally, skip `emptyValue`-based default rendering in docs for
types with submodules, because their sub-options are already documented
individually, and forcing evaluation here can break on modules with
invalid or incomplete definitions.
This commit is contained in:
Robert Hensing 2026-04-19 20:28:17 +01:00
commit 77b5864637
4 changed files with 37 additions and 3 deletions

View file

@ -590,7 +590,16 @@ rec {
);
}
//
optionalAttrs (opt ? defaultText || opt ? default || ((opt.type or { }).emptyValue or { }) ? value)
optionalAttrs
(
opt ? defaultText
|| opt ? default
# Render emptyValue-based defaults, but only for types without
# submodules (e.g. types.submodule). Submodules may evaluate to
# error without user defs, and their sub-options are documented
# individually, so best to skip those here.
|| ((opt.type or { }).emptyValue or { }) ? value && (opt.type or { }).getSubModules or null == null
)
{
default =
builtins.addErrorContext

View file

@ -686,6 +686,8 @@ checkConfigOutput "\[\]" config.unique ./defaults.nix
checkConfigOutput "\[\]" config.coercedTo ./defaults.nix
# These types don't have empty values
checkConfigError 'The option .int. was accessed but has no value defined. Try setting the option.' config.int ./defaults.nix
## submodule emptyValue must evaluate sub-option defaults
checkConfigOutput "ok" config.result ./defaults.nix
# types.unique
# requires a single definition

View file

@ -1,4 +1,4 @@
{ lib, ... }:
{ lib, config, ... }:
let
inherit (lib) types;
in
@ -19,6 +19,18 @@ in
submodule = lib.mkOption {
type = types.submodule { };
};
submoduleWithDefaults = lib.mkOption {
type = types.submodule {
options.enabled = lib.mkOption {
type = types.bool;
default = true;
};
options.count = lib.mkOption {
type = types.int;
default = 13;
};
};
};
unique = lib.mkOption {
type = types.unique { message = "hi"; } (types.listOf types.int);
};
@ -29,5 +41,16 @@ in
int = lib.mkOption {
type = types.int;
};
result = lib.mkOption {
type = types.str;
default =
assert
config.submoduleWithDefaults == {
enabled = true;
count = 13;
};
"ok";
};
};
}

View file

@ -1320,7 +1320,7 @@ rec {
};
};
emptyValue = {
value = { };
value = base.config;
};
getSubOptions =
prefix: