From ac0ef82504747d3d8213f828736adf63bb304bc4 Mon Sep 17 00:00:00 2001 From: cinereal Date: Sun, 15 Mar 2026 08:38:10 -0700 Subject: [PATCH 1/2] lib.modules: default to `emptyValue` Signed-off-by: cinereal --- lib/modules.nix | 2 ++ lib/tests/modules.sh | 11 +++++++++++ lib/tests/modules/defaults.nix | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 lib/tests/modules/defaults.nix diff --git a/lib/modules.nix b/lib/modules.nix index c67ea2247ca7..ff286ad7c4b2 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1246,6 +1246,8 @@ let allInvalid = filter (def: !type.check def.value) defsFinal; in throw "A definition for option `${showOption loc}' is not of type `${type.description}'. Definition values:${showDefs allInvalid}" + else if type.emptyValue ? value then + type.emptyValue.value else # (nixos-option detects this specific error message and gives it special # handling. If changed here, please change it there too.) diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index aa9d3443fb6a..bc89d4c20851 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -674,6 +674,17 @@ checkConfigOutput "{}" config.submodule.a ./emptyValues.nix checkConfigError 'The option .int.a. was accessed but has no value defined. Try setting the option.' config.int.a ./emptyValues.nix checkConfigError 'The option .nonEmptyList.a. was accessed but has no value defined. Try setting the option.' config.nonEmptyList.a ./emptyValues.nix +## defaults +checkConfigOutput "\[\]" config.list ./defaults.nix +checkConfigOutput "{}" config.attrs ./defaults.nix +checkConfigOutput "{}" config.attrsOf ./defaults.nix +checkConfigOutput "null" config.null ./defaults.nix +checkConfigOutput "{}" config.submodule ./defaults.nix +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 + # types.unique # requires a single definition checkConfigError 'The option .examples\.merged. is defined multiple times while it.s expected to be unique' config.examples.merged.a ./types-unique.nix diff --git a/lib/tests/modules/defaults.nix b/lib/tests/modules/defaults.nix new file mode 100644 index 000000000000..56eee2c9c286 --- /dev/null +++ b/lib/tests/modules/defaults.nix @@ -0,0 +1,33 @@ +{ lib, ... }: +let + inherit (lib) types; +in +{ + options = { + list = lib.mkOption { + type = types.listOf types.int; + }; + attrs = lib.mkOption { + type = types.attrs; + }; + attrsOf = lib.mkOption { + type = types.attrsOf types.int; + }; + null = lib.mkOption { + type = types.nullOr types.int; + }; + submodule = lib.mkOption { + type = types.submodule { }; + }; + unique = lib.mkOption { + type = types.unique { message = "hi"; } (types.listOf types.int); + }; + coercedTo = lib.mkOption { + type = types.coercedTo (types.attrsOf types.int) builtins.attrNames (types.listOf types.str); + }; + # no empty value + int = lib.mkOption { + type = types.int; + }; + }; +} From 4c7577dd9491626c418a167abab17f1735aa3345 Mon Sep 17 00:00:00 2001 From: cinereal Date: Sat, 21 Mar 2026 19:00:05 +0100 Subject: [PATCH 2/2] lib.options: reflect `emptyValue`-based option `default`s in documentation Signed-off-by: cinereal --- lib/options.nix | 15 ++++++++++----- lib/tests/misc.nix | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/options.nix b/lib/options.nix index c348491f0457..0cda30fc3544 100644 --- a/lib/options.nix +++ b/lib/options.nix @@ -589,11 +589,16 @@ rec { renderOptionValue opt.example ); } - // optionalAttrs (opt ? defaultText || opt ? default) { - default = builtins.addErrorContext "while evaluating the ${ - if opt ? defaultText then "defaultText" else "default value" - } of option `${name}`" (renderOptionValue (opt.defaultText or opt.default)); - } + // + optionalAttrs (opt ? defaultText || opt ? default || ((opt.type or { }).emptyValue or { }) ? value) + { + default = + builtins.addErrorContext + "while evaluating the ${ + if opt ? defaultText then "defaultText" else "default value" + } of option `${name}`" + (renderOptionValue (opt.defaultText or opt.default or opt.type.emptyValue.value)); + } // optionalAttrs (opt ? relatedPackages && opt.relatedPackages != null) { inherit (opt) relatedPackages; }; diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index d4bdc344deb9..f1a11cd7e6fb 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -3393,6 +3393,44 @@ runTests { ]; }; + testEmptyValueOption = { + expr = + let + module = + { lib, ... }: + { + options = { + "empty-value" = lib.mkOption { + type = lib.mkOptionType { + name = "propagate-empty-value-to-default"; + emptyValue.value = 2; + }; + }; + }; + }; + eval = evalModules { + modules = [ module ]; + }; + in + filter (o: o.name == "empty-value") (optionAttrSetToDocList eval.options); + expected = [ + { + declarations = [ ]; + default = { + _type = "literalExpression"; + text = "2"; + }; + description = null; + internal = false; + loc = [ "empty-value" ]; + name = "empty-value"; + readOnly = false; + type = "propagate-empty-value-to-default"; + visible = true; + } + ]; + }; + testDocOptionVisiblity = { expr = let