From 644527dd57127fd46c40de1fc90d096a6db962b4 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 18 Mar 2025 17:11:57 +0000 Subject: [PATCH 01/14] lib.modules: init types checkAndMerge to allow adding 'valueMeta' attributes This allows individual types to add attributes that would be discarded during normal evaluation. Some examples: types.submodule performs a submodule evluation which yields an 'evalModules' result. It returns '.config' but makes the original result accessible via 'valueMeta' allowing introspection of '.options' and all other kinds of module evaluation results types.attrsOf returns an attribute set of the nestedType. It makes each valueMeta available under the corresponding attribute name. --- lib/modules.nix | 14 +++++- lib/types.nix | 129 +++++++++++++++++++++++++++++++----------------- 2 files changed, 98 insertions(+), 45 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index cc3148b0eea7..8394cd0b77a8 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1121,6 +1121,7 @@ let files = map (def: def.file) res.defsFinal; definitionsWithLocations = res.defsFinal; inherit (res) isDefined; + inherit (res.checkedAndMerged) valueMeta; # This allows options to be correctly displayed using `${options.path.to.it}` __toString = _: showOption loc; }; @@ -1164,7 +1165,9 @@ let # Type-check the remaining definitions, and merge them. Or throw if no definitions. mergedValue = if isDefined then - if all (def: type.check def.value) defsFinal then + if type.checkAndMerge or null != null then + checkedAndMerged.value + else if all (def: type.check def.value) defsFinal then type.merge loc defsFinal else let @@ -1177,6 +1180,15 @@ let throw "The option `${showOption loc}' was accessed but has no value defined. Try setting the option."; + checkedAndMerged = + if type.checkAndMerge or null != null then + type.checkAndMerge loc defsFinal + else + { + value = mergedValue; + valueMeta = { }; + }; + isDefined = defsFinal != [ ]; optionalValue = if isDefined then { value = mergedValue; } else { }; diff --git a/lib/types.nix b/lib/types.nix index dcb536c4723b..9ef1712ef1d1 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -48,6 +48,7 @@ let mergeOneOption mergeUniqueOption showFiles + showDefs showOption ; inherit (lib.strings) @@ -204,6 +205,10 @@ let # definition values and locations (e.g. [ { file = "/foo.nix"; # value = 1; } { file = "/bar.nix"; value = 2 } ]). merge ? mergeDefaultOption, + # + # This field does not have a default implementation, so that users' changes + # to `check` and `merge` are propagated. + checkAndMerge ? null, # Whether this type has a value representing nothingness. If it does, # this should be a value of the form { value = ; } # If it doesn't, this should be {} @@ -252,6 +257,7 @@ let deprecationMessage nestedTypes descriptionClass + checkAndMerge ; functor = if functor ? wrappedDeprecationMessage then @@ -705,10 +711,11 @@ let }"; descriptionClass = "composite"; check = isList; - merge = + merge = loc: defs: (checkAndMerge loc defs).value; + checkAndMerge = loc: defs: - map (x: x.value) ( - filter (x: x ? value) ( + let + evals = filter (x: x.optionalValue ? value) ( concatLists ( imap1 ( n: def: @@ -719,12 +726,16 @@ let inherit (def) file; value = def'; } - ]).optionalValue + ]) ) def.value ) defs ) - ) - ); + ); + in + { + value = map (x: x.optionalValue.value or x.mergedValue) evals; + valueMeta.list = map (v: v.checkedAndMerged.valueMeta) evals; + }; emptyValue = { value = [ ]; }; @@ -740,14 +751,16 @@ let nonEmptyListOf = elemType: let - list = addCheck (types.listOf elemType) (l: l != [ ]); + list = types.listOf elemType; in - list - // { - description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}"; - emptyValue = { }; # no .value attr, meaning unset - substSubModules = m: nonEmptyListOf (elemType.substSubModules m); - }; + addCheck ( + list + // { + description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}"; + emptyValue = { }; # no .value attr, meaning unset + substSubModules = m: nonEmptyListOf (elemType.substSubModules m); + } + ) (l: l != [ ]); attrsOf = elemType: attrsWith { inherit elemType; }; @@ -801,42 +814,38 @@ let lazy ? false, placeholder ? "name", }: - mkOptionType { + mkOptionType rec { name = if lazy then "lazyAttrsOf" else "attrsOf"; description = (if lazy then "lazy attribute set" else "attribute set") + " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; descriptionClass = "composite"; check = isAttrs; - merge = - if lazy then - ( - # Lazy merge Function - loc: defs: - zipAttrsWith - ( - name: defs: - let - merged = mergeDefinitions (loc ++ [ name ]) elemType defs; - # mergedValue will trigger an appropriate error when accessed - in - merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue - ) - # Push down position info. - (pushPositions defs) - ) - else - ( - # Non-lazy merge Function - loc: defs: - mapAttrs (n: v: v.value) ( - filterAttrs (n: v: v ? value) ( - zipAttrsWith (name: defs: (mergeDefinitions (loc ++ [ name ]) elemType (defs)).optionalValue) - # Push down position info. - (pushPositions defs) - ) - ) - ); + merge = loc: defs: (checkAndMerge loc defs).value; + checkAndMerge = + loc: defs: + let + evals = + if lazy then + zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs) + else + # Filtering makes the merge function more strict + # Meaning it is less lazy + filterAttrs (n: v: v.optionalValue ? value) ( + zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs) + ); + in + { + value = mapAttrs ( + n: v: + if lazy then + v.optionalValue.value or elemType.emptyValue.value or v.mergedValue + else + v.optionalValue.value + ) evals; + valueMeta.attrs = mapAttrs (n: v: v.checkedAndMerged.valueMeta) evals; + }; + emptyValue = { value = { }; }; @@ -1236,6 +1245,18 @@ let modules = [ { _module.args.name = last loc; } ] ++ allModules defs; prefix = loc; }).config; + checkAndMerge = + loc: defs: + let + configuration = base.extendModules { + modules = [ { _module.args.name = last loc; } ] ++ allModules defs; + prefix = loc; + }; + in + { + value = configuration.config; + valueMeta = configuration; + }; emptyValue = { value = { }; }; @@ -1451,6 +1472,7 @@ let nestedTypes.coercedType = coercedType; nestedTypes.finalType = finalType; }; + /** Augment the given type with an additional type check function. @@ -1459,7 +1481,26 @@ let Fixing is not trivial, we appreciate any help! ::: */ - addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; + addCheck = + elemType: check: + elemType + // { + check = x: elemType.check x && check x; + } + // (lib.optionalAttrs (elemType.checkAndMerge != null) { + checkAndMerge = + loc: defs: + let + v = (elemType.checkAndMerge loc defs); + in + if all (def: elemType.check def.value && check def.value) defs then + v + else + let + allInvalid = filter (def: !elemType.check def.value || !check def.value) defs; + in + throw "A definition for option `${showOption loc}' is not of type `${elemType.description}'. Definition values:${showDefs allInvalid}"; + }); }; From 8fa33000a388cc63e1d66cae5da47937fe05886f Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 20 Mar 2025 16:20:03 +0000 Subject: [PATCH 02/14] lib.modules: add tests for option valueMeta --- lib/tests/modules.sh | 13 ++++ .../modules/composed-types-valueMeta.nix | 75 +++++++++++++++++++ lib/tests/modules/types-valueMeta.nix | 60 +++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 lib/tests/modules/composed-types-valueMeta.nix create mode 100644 lib/tests/modules/types-valueMeta.nix diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 301808ae6651..9daf0cd8453d 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -761,6 +761,19 @@ checkConfigOutput '"bar"' config.sub.conditionalImportAsNixos.foo ./specialArgs- checkConfigError 'attribute .*bar.* not found' config.sub.conditionalImportAsNixos.bar ./specialArgs-class.nix checkConfigError 'attribute .*foo.* not found' config.sub.conditionalImportAsDarwin.foo ./specialArgs-class.nix checkConfigOutput '"foo"' config.sub.conditionalImportAsDarwin.bar ./specialArgs-class.nix +# Check that some types expose the 'valueMeta' +checkConfigOutput '\{\}' options.str.valueMeta ./types-valueMeta.nix +checkConfigOutput '["foo", "bar"]' config.attrsOfResult ./types-valueMeta.nix +checkConfigOutput '2' config.listOfResult ./types-valueMeta.nix + +# Check that composed types expose the 'valueMeta' +# attrsOf submodule (also on merged options,types) +checkConfigOutput '42' options.attrsOfModule.valueMeta.attrs.foo.options.bar.value ./composed-types-valueMeta.nix +checkConfigOutput '42' options.mergedAttrsOfModule.valueMeta.attrs.foo.options.bar.value ./composed-types-valueMeta.nix + +# listOf submodule (also on merged options,types) +checkConfigOutput '42' config.listResult ./composed-types-valueMeta.nix +checkConfigOutput '42' config.mergedListResult ./composed-types-valueMeta.nix cat < Date: Fri, 30 May 2025 17:24:18 +0200 Subject: [PATCH 03/14] lib/types: add 'checkDefsForError' utility for checking defs with a given check --- lib/types.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/types.nix b/lib/types.nix index 9ef1712ef1d1..88c5effd07f1 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -100,6 +100,13 @@ let }is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead. '' payload.elemType; + checkDefsForError = + check: loc: defs: + let + invalidDefs = filter (def: !check def.value) defs; + in + if invalidDefs != [ ] then "Definition values: ${showDefs invalidDefs}" else null; + outer_types = rec { isType = type: x: (x._type or "") == type; From 70ab11c2f2ed1c8375da40d891f428139146a05d Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 30 May 2025 17:25:51 +0200 Subject: [PATCH 04/14] lib/modules: add new merge.v2 for 'types.{either,coercedTo}' --- lib/modules.nix | 38 +++++-- lib/types.nix | 270 +++++++++++++++++++++++++++++------------------- 2 files changed, 193 insertions(+), 115 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 8394cd0b77a8..8b9a925d718a 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1165,8 +1165,13 @@ let # Type-check the remaining definitions, and merge them. Or throw if no definitions. mergedValue = if isDefined then - if type.checkAndMerge or null != null then - checkedAndMerged.value + if type.merge ? v2 then + # check and merge share the same closure + # .headError is either non-present null or an error describing string + if checkedAndMerged.headError or null != null then + throw "A definition for option `${showOption loc}' is not of type `${type.description}'. TypeError: ${checkedAndMerged.headError}" + else + checkedAndMerged.value else if all (def: type.check def.value) defsFinal then type.merge loc defsFinal else @@ -1180,14 +1185,29 @@ let throw "The option `${showOption loc}' was accessed but has no value defined. Try setting the option."; - checkedAndMerged = - if type.checkAndMerge or null != null then - type.checkAndMerge loc defsFinal + ensureMergedValueEnvelope = + v: + if attrNames v == attrNames defaultCheckedAndMerged then + v else - { - value = mergedValue; - valueMeta = { }; - }; + throw "Invalid 'type.merge.v2' of type: '${type.name}' must return exactly the following attributes: ${builtins.toJSON (attrNames defaultCheckedAndMerged)} but got ${builtins.toJSON (attrNames v)}"; + + defaultCheckedAndMerged = { + headError = null; + value = mergedValue; + valueMeta = { }; + }; + + checkedAndMerged = ensureMergedValueEnvelope ( + if type.merge ? v2 then + type.merge.v2 { + inherit loc; + defs = defsFinal; + } + else + defaultCheckedAndMerged + + ); isDefined = defsFinal != [ ]; diff --git a/lib/types.nix b/lib/types.nix index 88c5effd07f1..b550125a4f28 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -212,10 +212,6 @@ let # definition values and locations (e.g. [ { file = "/foo.nix"; # value = 1; } { file = "/bar.nix"; value = 2 } ]). merge ? mergeDefaultOption, - # - # This field does not have a default implementation, so that users' changes - # to `check` and `merge` are propagated. - checkAndMerge ? null, # Whether this type has a value representing nothingness. If it does, # this should be a value of the form { value = ; } # If it doesn't, this should be {} @@ -264,7 +260,6 @@ let deprecationMessage nestedTypes descriptionClass - checkAndMerge ; functor = if functor ? wrappedDeprecationMessage then @@ -718,31 +713,36 @@ let }"; descriptionClass = "composite"; check = isList; - merge = loc: defs: (checkAndMerge loc defs).value; - checkAndMerge = - loc: defs: - let - evals = filter (x: x.optionalValue ? value) ( - concatLists ( - imap1 ( - n: def: + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + evals = filter (x: x.optionalValue ? value) ( + concatLists ( imap1 ( - m: def': - (mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [ - { - inherit (def) file; - value = def'; - } - ]) - ) def.value - ) defs - ) - ); - in - { - value = map (x: x.optionalValue.value or x.mergedValue) evals; - valueMeta.list = map (v: v.checkedAndMerged.valueMeta) evals; - }; + n: def: + imap1 ( + m: def': + (mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [ + { + inherit (def) file; + value = def'; + } + ]) + ) def.value + ) defs + ) + ); + in + { + headError = checkDefsForError check loc defs; + value = map (x: x.optionalValue.value or x.mergedValue) evals; + valueMeta.list = map (v: v.checkedAndMerged.valueMeta) evals; + }; + }; emptyValue = { value = [ ]; }; @@ -828,30 +828,35 @@ let + " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; descriptionClass = "composite"; check = isAttrs; - merge = loc: defs: (checkAndMerge loc defs).value; - checkAndMerge = - loc: defs: - let - evals = - if lazy then - zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs) - else - # Filtering makes the merge function more strict - # Meaning it is less lazy - filterAttrs (n: v: v.optionalValue ? value) ( + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + evals = + if lazy then zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs) - ); - in - { - value = mapAttrs ( - n: v: - if lazy then - v.optionalValue.value or elemType.emptyValue.value or v.mergedValue - else - v.optionalValue.value - ) evals; - valueMeta.attrs = mapAttrs (n: v: v.checkedAndMerged.valueMeta) evals; - }; + else + # Filtering makes the merge function more strict + # Meaning it is less lazy + filterAttrs (n: v: v.optionalValue ? value) ( + zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs) + ); + in + { + headError = checkDefsForError check loc defs; + value = mapAttrs ( + n: v: + if lazy then + v.optionalValue.value or elemType.emptyValue.value or v.mergedValue + else + v.optionalValue.value + ) evals; + valueMeta.attrs = mapAttrs (n: v: v.checkedAndMerged.valueMeta) evals; + }; + }; emptyValue = { value = { }; @@ -1234,6 +1239,7 @@ let name = "submodule"; + check = x: isAttrs x || isFunction x || path.check x; in mkOptionType { inherit name; @@ -1245,25 +1251,25 @@ let docsEval = base.extendModules { modules = [ noCheckForDocsModule ]; }; in docsEval._module.freeformType.description or name; - check = x: isAttrs x || isFunction x || path.check x; - merge = - loc: defs: - (base.extendModules { - modules = [ { _module.args.name = last loc; } ] ++ allModules defs; - prefix = loc; - }).config; - checkAndMerge = - loc: defs: - let - configuration = base.extendModules { - modules = [ { _module.args.name = last loc; } ] ++ allModules defs; - prefix = loc; + inherit check; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + configuration = base.extendModules { + modules = [ { _module.args.name = last loc; } ] ++ allModules defs; + prefix = loc; + }; + in + { + headError = checkDefsForError check loc defs; + value = configuration.config; + valueMeta = configuration; }; - in - { - value = configuration.config; - valueMeta = configuration; - }; + }; emptyValue = { value = { }; }; @@ -1411,17 +1417,48 @@ let }"; descriptionClass = "conjunction"; check = x: t1.check x || t2.check x; - merge = - loc: defs: - let - defList = map (d: d.value) defs; - in - if all (x: t1.check x) defList then - t1.merge loc defs - else if all (x: t2.check x) defList then - t2.merge loc defs - else - mergeOneOption loc defs; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + t1CheckedAndMerged = + if t1.merge ? v2 then + t1.merge.v2 { inherit loc defs; } + else + { + value = t1.merge loc defs; + headError = checkDefsForError t1.check loc defs; + valueMeta = { }; + }; + t2CheckedAndMerged = + if t2.merge ? v2 then + t2.merge.v2 { inherit loc defs; } + else + { + value = t2.merge loc defs; + headError = checkDefsForError t2.check loc defs; + valueMeta = { }; + }; + + checkedAndMerged = + if t1CheckedAndMerged.headError == null then + t1CheckedAndMerged + else if t2CheckedAndMerged.headError == null then + t2CheckedAndMerged + else + rec { + valueMeta = { + inherit headError; + }; + headError = "The option `${showOption loc}` is neither a value of type `${t1.description}` nor `${t2.description}`, Definition values: ${showDefs defs}"; + value = null; + }; + in + checkedAndMerged; + }; typeMerge = f': let @@ -1462,12 +1499,43 @@ let optionDescriptionPhrase (class: class == "noun") coercedType } convertible to it"; check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x; - merge = - loc: defs: - let - coerceVal = val: if coercedType.check val then coerceFunc val else val; - in - finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); + merge = { + __funtor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + isMergeV2 = coercedType.merge ? v2; + coerceDef = + def: + let + merged = coercedType.merge.v2 { + inherit loc; + defs = [ def ]; + }; + in + if isMergeV2 then + if merged.headError == null then coerceFunc def.value else def.value + else if coercedType.check def.value then + coerceFunc def.value + else + def.value; + + finalDefs = (map (def: def // { value = coerceDef def; }) defs); + in + if finalType.merge ? v2 then + finalType.merge.v2 { + inherit loc; + defs = finalDefs; + } + else + { + value = finalType.merge loc finalDefs; + valueMeta = { }; + headError = checkDefsForError check loc finalDefs; + }; + }; emptyValue = finalType.emptyValue; getSubOptions = finalType.getSubOptions; getSubModules = finalType.getSubModules; @@ -1490,25 +1558,15 @@ let */ addCheck = elemType: check: - elemType - // { - check = x: elemType.check x && check x; - } - // (lib.optionalAttrs (elemType.checkAndMerge != null) { - checkAndMerge = - loc: defs: - let - v = (elemType.checkAndMerge loc defs); - in - if all (def: elemType.check def.value && check def.value) defs then - v - else - let - allInvalid = filter (def: !elemType.check def.value || !check def.value) defs; - in - throw "A definition for option `${showOption loc}' is not of type `${elemType.description}'. Definition values:${showDefs allInvalid}"; - }); - + let + final = elemType // { + check = x: elemType.check x && check x; + # addCheck discards the merge.v2 function + # + merge = if elemType.merge ? v2 then elemType.merge.__functor { inherit final; } else elemType.merge; + }; + in + final; }; /** From 9f787b30e520d9fd823c2eec8547acd87af974c1 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 30 May 2025 17:36:20 +0200 Subject: [PATCH 05/14] lib/modules: fix test by matching error message more generically --- lib/tests/modules.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 9daf0cd8453d..2934064d2665 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -345,14 +345,14 @@ checkConfigOutput '^true$' "$@" ./define-module-check.nix set -- checkConfigOutput '^"42"$' config.value ./declare-coerced-value.nix checkConfigOutput '^"24"$' config.value ./declare-coerced-value.nix ./define-value-string.nix -checkConfigError 'A definition for option .* is not.*string or signed integer convertible to it.*. Definition values:\n\s*- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix +checkConfigError 'A definition for option .*. is not of type .*.\n\s*- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix # Check coerced option merging. checkConfigError 'The option .value. in .*/declare-coerced-value.nix. is already declared in .*/declare-coerced-value-no-default.nix.' config.value ./declare-coerced-value.nix ./declare-coerced-value-no-default.nix # Check coerced value with unsound coercion checkConfigOutput '^12$' config.value ./declare-coerced-value-unsound.nix -checkConfigError 'A definition for option .* is not of type .*. Definition values:\n\s*- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix +checkConfigError 'A definition for option .* is not of type .*.\n\s*- In .*: 1000' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix checkConfigError 'toInt: Could not convert .* to int' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix # Check `graph` attribute From 5d72133a228d173e3048e8efa2bb6578c2cb2bd3 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 30 May 2025 22:04:52 +0200 Subject: [PATCH 06/14] lib/addCheck: add support for new merge --- lib/types.nix | 49 +++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/lib/types.nix b/lib/types.nix index b550125a4f28..a6b3e6fb83fb 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -20,7 +20,6 @@ let toList ; inherit (lib.lists) - all concatLists count elemAt @@ -758,16 +757,14 @@ let nonEmptyListOf = elemType: let - list = types.listOf elemType; + list = addCheck (types.listOf elemType) (l: l != [ ]); in - addCheck ( - list - // { - description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}"; - emptyValue = { }; # no .value attr, meaning unset - substSubModules = m: nonEmptyListOf (elemType.substSubModules m); - } - ) (l: l != [ ]); + list + // { + description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}"; + emptyValue = { }; # no .value attr, meaning unset + substSubModules = m: nonEmptyListOf (elemType.substSubModules m); + }; attrsOf = elemType: attrsWith { inherit elemType; }; @@ -1500,7 +1497,7 @@ let } convertible to it"; check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x; merge = { - __funtor = + __functor = self: loc: defs: (self.v2 { inherit loc defs; }).value; v2 = @@ -1558,15 +1555,31 @@ let */ addCheck = elemType: check: - let - final = elemType // { + if elemType.merge ? v2 then + elemType + // { + check = x: elemType.check x && check x; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + orig = elemType.merge.v2 { inherit loc defs; }; + headError' = if orig.headError != null then orig.headError else checkDefsForError check loc defs; + in + orig + // { + headError = headError'; + }; + }; + } + else + elemType + // { check = x: elemType.check x && check x; - # addCheck discards the merge.v2 function - # - merge = if elemType.merge ? v2 then elemType.merge.__functor { inherit final; } else elemType.merge; }; - in - final; }; /** From ebafc3eb747aa13967aac8b35590c3ebb7d889a7 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 6 Jun 2025 17:07:17 +0200 Subject: [PATCH 07/14] lib/modules: optimize performance by inlining bindings --- lib/modules.nix | 49 ++++++++++++++++++++++++++----------------------- lib/types.nix | 40 ++++++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 8b9a925d718a..d7125e60b8e5 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1185,29 +1185,32 @@ let throw "The option `${showOption loc}' was accessed but has no value defined. Try setting the option."; - ensureMergedValueEnvelope = - v: - if attrNames v == attrNames defaultCheckedAndMerged then - v - else - throw "Invalid 'type.merge.v2' of type: '${type.name}' must return exactly the following attributes: ${builtins.toJSON (attrNames defaultCheckedAndMerged)} but got ${builtins.toJSON (attrNames v)}"; - - defaultCheckedAndMerged = { - headError = null; - value = mergedValue; - valueMeta = { }; - }; - - checkedAndMerged = ensureMergedValueEnvelope ( - if type.merge ? v2 then - type.merge.v2 { - inherit loc; - defs = defsFinal; - } - else - defaultCheckedAndMerged - - ); + checkedAndMerged = + ( + # This function (which is immediately applied) checks that type.merge + # returns the proper attrset. + # Once use of the merge.v2 feature has propagated, consider removing this + # for an estimated one thousandth performance improvement (NixOS by nr.thunks). + { + headError, + value, + valueMeta, + }@args: + args + ) + ( + if type.merge ? v2 then + type.merge.v2 { + inherit loc; + defs = defsFinal; + } + else + { + headError = null; + value = mergedValue; + valueMeta = { }; + } + ); isDefined = defsFinal != [ ]; diff --git a/lib/types.nix b/lib/types.nix index a6b3e6fb83fb..40d0eb6c49f8 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -1503,23 +1503,27 @@ let v2 = { loc, defs }: let - isMergeV2 = coercedType.merge ? v2; - coerceDef = - def: - let - merged = coercedType.merge.v2 { - inherit loc; - defs = [ def ]; - }; - in - if isMergeV2 then - if merged.headError == null then coerceFunc def.value else def.value - else if coercedType.check def.value then - coerceFunc def.value - else - def.value; - - finalDefs = (map (def: def // { value = coerceDef def; }) defs); + finalDefs = ( + map ( + def: + def + // { + value = + let + merged = coercedType.merge.v2 { + inherit loc; + defs = [ def ]; + }; + in + if coercedType.merge ? v2 then + if merged.headError == null then coerceFunc def.value else def.value + else if coercedType.check def.value then + coerceFunc def.value + else + def.value; + } + ) defs + ); in if finalType.merge ? v2 then finalType.merge.v2 { @@ -1530,7 +1534,7 @@ let { value = finalType.merge loc finalDefs; valueMeta = { }; - headError = checkDefsForError check loc finalDefs; + headError = checkDefsForError check loc defs; }; }; emptyValue = finalType.emptyValue; From 17653700514991920ed518813ec9331dd4df54c1 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 6 Jun 2025 17:08:04 +0200 Subject: [PATCH 08/14] lib/modules: test revert unentional regression in check --- lib/tests/modules.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 2934064d2665..f48f04ae985a 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -352,7 +352,7 @@ checkConfigError 'The option .value. in .*/declare-coerced-value.nix. is already # Check coerced value with unsound coercion checkConfigOutput '^12$' config.value ./declare-coerced-value-unsound.nix -checkConfigError 'A definition for option .* is not of type .*.\n\s*- In .*: 1000' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix +checkConfigError 'A definition for option .* is not of type .*.\n\s*- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix checkConfigError 'toInt: Could not convert .* to int' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix # Check `graph` attribute From 50bef19448743f2bd8c304c355e99d86470d97a2 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 6 Jun 2025 17:44:53 +0200 Subject: [PATCH 09/14] lib/modules: add nested 'headError.message' This should make headError extensible other information needs to be passed This seems to improve performance slightly --- lib/modules.nix | 2 +- lib/types.nix | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index d7125e60b8e5..bb16acac7e24 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1169,7 +1169,7 @@ let # check and merge share the same closure # .headError is either non-present null or an error describing string if checkedAndMerged.headError or null != null then - throw "A definition for option `${showOption loc}' is not of type `${type.description}'. TypeError: ${checkedAndMerged.headError}" + throw "A definition for option `${showOption loc}' is not of type `${type.description}'. TypeError: ${checkedAndMerged.headError.message}" else checkedAndMerged.value else if all (def: type.check def.value) defsFinal then diff --git a/lib/types.nix b/lib/types.nix index 40d0eb6c49f8..6b8386089eac 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -104,7 +104,7 @@ let let invalidDefs = filter (def: !check def.value) defs; in - if invalidDefs != [ ] then "Definition values: ${showDefs invalidDefs}" else null; + if invalidDefs != [ ] then { message = "Definition values: ${showDefs invalidDefs}"; } else null; outer_types = rec { isType = type: x: (x._type or "") == type; @@ -1450,8 +1450,10 @@ let valueMeta = { inherit headError; }; - headError = "The option `${showOption loc}` is neither a value of type `${t1.description}` nor `${t2.description}`, Definition values: ${showDefs defs}"; - value = null; + headError = { + message = "The option `${showOption loc}` is neither a value of type `${t1.description}` nor `${t2.description}`, Definition values: ${showDefs defs}"; + }; + value = abort "(t.merge.v2 defs).value must only be accessed when `.headError == null`. This is a bug in code that consumes a module system type."; }; in checkedAndMerged; From cd2e5bd46c016666f8cd3c00f95d692479c3027d Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Jun 2025 08:30:58 +0200 Subject: [PATCH 10/14] types/merge: move 'configuration' of submodules into nested attribute set --- lib/tests/modules.sh | 4 ++-- lib/tests/modules/composed-types-valueMeta.nix | 4 ++-- lib/types.nix | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index f48f04ae985a..bbc4ee9bdfe6 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -768,8 +768,8 @@ checkConfigOutput '2' config.listOfResult ./types-valueMeta.nix # Check that composed types expose the 'valueMeta' # attrsOf submodule (also on merged options,types) -checkConfigOutput '42' options.attrsOfModule.valueMeta.attrs.foo.options.bar.value ./composed-types-valueMeta.nix -checkConfigOutput '42' options.mergedAttrsOfModule.valueMeta.attrs.foo.options.bar.value ./composed-types-valueMeta.nix +checkConfigOutput '42' options.attrsOfModule.valueMeta.attrs.foo.configuration.options.bar.value ./composed-types-valueMeta.nix +checkConfigOutput '42' options.mergedAttrsOfModule.valueMeta.attrs.foo.configuration.options.bar.value ./composed-types-valueMeta.nix # listOf submodule (also on merged options,types) checkConfigOutput '42' config.listResult ./composed-types-valueMeta.nix diff --git a/lib/tests/modules/composed-types-valueMeta.nix b/lib/tests/modules/composed-types-valueMeta.nix index b95a0c8c6112..734917f8b67d 100644 --- a/lib/tests/modules/composed-types-valueMeta.nix +++ b/lib/tests/modules/composed-types-valueMeta.nix @@ -64,10 +64,10 @@ in ]; # Result options to expose the list module to bash as plain attribute path options.listResult = mkOption { - default = (builtins.head options.listOfModule.valueMeta.list).options.bar.value; + default = (builtins.head options.listOfModule.valueMeta.list).configuration.options.bar.value; }; options.mergedListResult = mkOption { - default = (builtins.head options.mergedListOfModule.valueMeta.list).options.bar.value; + default = (builtins.head options.mergedListOfModule.valueMeta.list).configuration.options.bar.value; }; } ) diff --git a/lib/types.nix b/lib/types.nix index 6b8386089eac..6b51f9254a00 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -1264,7 +1264,7 @@ let { headError = checkDefsForError check loc defs; value = configuration.config; - valueMeta = configuration; + valueMeta = { inherit configuration; }; }; }; emptyValue = { From 45ed757e104a361ba2f8034181f78b1249b4faf1 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Jun 2025 09:01:15 +0200 Subject: [PATCH 11/14] types/addCheck: add tests for merge v1 and v2 --- lib/tests/modules.sh | 7 +++++++ lib/tests/modules/add-check.nix | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 lib/tests/modules/add-check.nix diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index bbc4ee9bdfe6..fbbccd8172f6 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -775,6 +775,13 @@ checkConfigOutput '42' options.mergedAttrsOfModule.valueMeta.attrs.foo.configura checkConfigOutput '42' config.listResult ./composed-types-valueMeta.nix checkConfigOutput '42' config.mergedListResult ./composed-types-valueMeta.nix +# Add check +checkConfigOutput '^0$' config.v1CheckedPass ./add-check.nix +checkConfigError 'A definition for option .* is not of type .signed integer.*' config.v1CheckedFail ./add-check.nix +checkConfigOutput '^true$' config.v2checkedPass ./add-check.nix +checkConfigError 'A definition for option .* is not of type .attribute set of signed integer.*' config.v2checkedFail ./add-check.nix + + cat < Date: Fri, 15 Aug 2025 15:13:07 +0200 Subject: [PATCH 12/14] lib/tests: introduce lib cross version checks Needed to ensure backwards stability of types.merge.v2 added in #391544 --- lib/tests/checkAndMergeCompat.nix | 379 ++++++++++++++++++++++++++++++ lib/tests/nix-unit.nix | 25 ++ lib/tests/release.nix | 3 + 3 files changed, 407 insertions(+) create mode 100644 lib/tests/checkAndMergeCompat.nix create mode 100644 lib/tests/nix-unit.nix diff --git a/lib/tests/checkAndMergeCompat.nix b/lib/tests/checkAndMergeCompat.nix new file mode 100644 index 000000000000..dc6f26fbf917 --- /dev/null +++ b/lib/tests/checkAndMergeCompat.nix @@ -0,0 +1,379 @@ +{ + pkgs ? import ../.. { }, + currLibPath ? ../., + prevLibPath ? "${ + pkgs.fetchFromGitHub { + owner = "nixos"; + repo = "nixpkgs"; + # Parent commit of [#391544](https://github.com/NixOS/nixpkgs/pull/391544) + # Which was before the type.merge.v2 introduction + rev = "bcf94dd3f07189b7475d823c8d67d08b58289905"; + hash = "sha256-MuMiIY3MX5pFSOCvutmmRhV6RD0R3CG0Hmazkg8cMFI="; + } + }/lib", +}: +let + lib = import currLibPath; + + lib_with_merge_v2 = lib; + lib_with_merge_v1 = import prevLibPath; + + getMatrix = + { + getType ? null, + # If getType is set this is only used as test prefix + # And the type from getType is used + outerTypeName, + innerTypeName, + value, + testAttrs, + }: + let + evalModules.call_v1 = lib_with_merge_v1.evalModules; + evalModules.call_v2 = lib_with_merge_v2.evalModules; + outerTypes.outer_v1 = lib_with_merge_v1.types; + outerTypes.outer_v2 = lib_with_merge_v2.types; + innerTypes.inner_v1 = lib_with_merge_v1.types; + innerTypes.inner_v2 = lib_with_merge_v2.types; + in + lib.mapAttrs ( + _: evalModules: + lib.mapAttrs ( + _: outerTypes: + lib.mapAttrs (_: innerTypes: { + "test_${outerTypeName}_${innerTypeName}" = testAttrs // { + expr = + (evalModules { + modules = [ + (m: { + options.foo = m.lib.mkOption { + type = + if getType != null then + getType outerTypes innerTypes + else + outerTypes.${outerTypeName} innerTypes.${innerTypeName}; + default = value; + }; + }) + ]; + }).config.foo; + }; + }) innerTypes + ) outerTypes + ) evalModules; +in +{ + # AttrsOf string + attrsOf_str_ok = getMatrix { + outerTypeName = "attrsOf"; + innerTypeName = "str"; + value = { + bar = "test"; + }; + testAttrs = { + expected = { + bar = "test"; + }; + }; + }; + attrsOf_str_err_inner = getMatrix { + outerTypeName = "attrsOf"; + innerTypeName = "str"; + value = { + bar = 1; # not a string + }; + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = "A definition for option `foo.bar' is not of type `string'.*"; + }; + }; + }; + attrsOf_str_err_outer = getMatrix { + outerTypeName = "attrsOf"; + innerTypeName = "str"; + value = [ "foo" ]; # not an attrset + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = "A definition for option `foo' is not of type `attribute set of string'.*"; + }; + }; + }; + + # listOf string + listOf_str_ok = getMatrix { + outerTypeName = "listOf"; + innerTypeName = "str"; + value = [ + "foo" + "bar" + ]; + testAttrs = { + expected = [ + "foo" + "bar" + ]; + }; + }; + listOf_str_err_inner = getMatrix { + outerTypeName = "listOf"; + innerTypeName = "str"; + value = [ + "foo" + 1 + ]; # not a string + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = ''A definition for option `foo."\[definition 1-entry 2\]"' is not of type `string'.''; + }; + }; + }; + listOf_str_err_outer = getMatrix { + outerTypeName = "listOf"; + innerTypeName = "str"; + value = { + foo = 42; + }; # not a list + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = "A definition for option `foo' is not of type `list of string'.*"; + }; + }; + }; + + attrsOf_submodule_ok = getMatrix { + getType = + a: b: + a.attrsOf ( + b.submodule (m: { + options.nested = m.lib.mkOption { + type = m.lib.types.str; + }; + }) + ); + outerTypeName = "attrsOf"; + innerTypeName = "submodule"; + value = { + foo = { + nested = "test1"; + }; + bar = { + nested = "test2"; + }; + }; + testAttrs = { + expected = { + foo = { + nested = "test1"; + }; + bar = { + nested = "test2"; + }; + }; + }; + }; + attrsOf_submodule_err_inner = getMatrix { + outerTypeName = "attrsOf"; + innerTypeName = "submodule"; + getType = + a: b: + a.attrsOf ( + b.submodule (m: { + options.nested = m.lib.mkOption { + type = m.lib.types.str; + }; + }) + ); + value = { + foo = [ 1 ]; # not a submodule + bar = { + nested = "test2"; + }; + }; + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = "A definition for option `foo.foo' is not of type `submodule'.*"; + }; + }; + }; + attrsOf_submodule_err_outer = getMatrix { + outerTypeName = "attrsOf"; + innerTypeName = "submodule"; + getType = + a: b: + a.attrsOf ( + b.submodule (m: { + options.nested = m.lib.mkOption { + type = m.lib.types.str; + }; + }) + ); + value = [ 123 ]; # not an attrsOf + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = ''A definition for option `foo' is not of type `attribute set of \(submodule\).*''; + }; + }; + }; + + # either + either_str_attrsOf_ok = getMatrix { + outerTypeName = "either"; + innerTypeName = "str_or_attrsOf_str"; + + getType = a: b: a.either b.str (b.attrsOf a.str); + value = "string value"; + testAttrs = { + expected = "string value"; + }; + }; + either_str_attrsOf_err_1 = getMatrix { + outerTypeName = "either"; + innerTypeName = "str_or_attrsOf_str"; + + getType = a: b: a.either b.str (b.attrsOf a.str); + value = 1; + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = "A definition for option `foo' is not of type `string or attribute set of string'.*"; + }; + }; + }; + either_str_attrsOf_err_2 = getMatrix { + outerTypeName = "either"; + innerTypeName = "str_or_attrsOf_str"; + + getType = a: b: a.either b.str (b.attrsOf a.str); + value = { + bar = 1; # not a string + }; + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = "A definition for option `foo.bar' is not of type `string'.*"; + }; + }; + }; + + # Coereced to + coerce_attrsOf_str_to_listOf_str_run = getMatrix { + outerTypeName = "coercedTo"; + innerTypeName = "attrsOf_str->listOf_str"; + getType = a: b: a.coercedTo (b.attrsOf b.str) builtins.attrValues (b.listOf b.str); + value = { + bar = "test1"; # coerced to listOf string + foo = "test2"; # coerced to listOf string + }; + testAttrs = { + expected = [ + "test1" + "test2" + ]; + }; + }; + coerce_attrsOf_str_to_listOf_str_final = getMatrix { + outerTypeName = "coercedTo"; + innerTypeName = "attrsOf_str->listOf_str"; + getType = a: b: a.coercedTo (b.attrsOf b.str) (abort "This shouldnt run") (b.listOf b.str); + value = [ + "test1" + "test2" + ]; # already a listOf string + testAttrs = { + expected = [ + "test1" + "test2" + ]; # Order should be kept + }; + }; + coerce_attrsOf_str_to_listOf_err_coercer_input = getMatrix { + outerTypeName = "coercedTo"; + innerTypeName = "attrsOf_str->listOf_str"; + getType = a: b: a.coercedTo (b.attrsOf b.str) builtins.attrValues (b.listOf b.str); + value = [ + { } + { } + ]; # not coercible to listOf string, with the given coercer + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = ''A definition for option `foo."\[definition 1-entry 1\]"' is not of type `string'.*''; + }; + }; + }; + coerce_attrsOf_str_to_listOf_err_coercer_ouput = getMatrix { + outerTypeName = "coercedTo"; + innerTypeName = "attrsOf_str->listOf_str"; + getType = a: b: a.coercedTo (b.attrsOf b.str) builtins.attrValues (b.listOf b.str); + value = { + foo = { + bar = 1; + }; # coercer produces wrong type -> [ { bar = 1; } ] + }; + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = ''A definition for option `foo."\[definition 1-entry 1\]"' is not of type `string'.*''; + }; + }; + }; + coerce_str_to_int_coercer_ouput = getMatrix { + outerTypeName = "coercedTo"; + innerTypeName = "int->str"; + getType = a: b: a.coercedTo b.int builtins.toString a.str; + value = [ ]; + testAttrs = { + expectedError = { + type = "ThrownError"; + msg = ''A definition for option `foo' is not of type `string or signed integer convertible to it.*''; + }; + }; + }; + + # Submodule + submodule_with_ok = getMatrix { + outerTypeName = "submoduleWith"; + innerTypeName = "mixed_types"; + getType = + a: b: + a.submodule (m: { + options.attrs = m.lib.mkOption { + type = b.attrsOf b.str; + }; + options.list = m.lib.mkOption { + type = b.listOf b.str; + }; + options.either = m.lib.mkOption { + type = b.either a.str a.int; + }; + }); + value = { + attrs = { + foo = "bar"; + }; + list = [ + "foo" + "bar" + ]; + either = 123; # int + }; + testAttrs = { + expected = { + attrs = { + foo = "bar"; + }; + list = [ + "foo" + "bar" + ]; + either = 123; + }; + }; + }; +} diff --git a/lib/tests/nix-unit.nix b/lib/tests/nix-unit.nix new file mode 100644 index 000000000000..ff62aff4d96b --- /dev/null +++ b/lib/tests/nix-unit.nix @@ -0,0 +1,25 @@ +{ + pkgs ? import ../.. { }, +}: +let + prevNixpkgs = pkgs.fetchFromGitHub { + owner = "nixos"; + repo = "nixpkgs"; + # Parent commit of [#391544](https://github.com/NixOS/nixpkgs/pull/391544) + # Which was before the type.merge.v2 introduction + rev = "bcf94dd3f07189b7475d823c8d67d08b58289905"; + hash = "sha256-MuMiIY3MX5pFSOCvutmmRhV6RD0R3CG0Hmazkg8cMFI="; + }; +in +(pkgs.runCommand "lib-cross-eval-merge-v2" + { + nativeBuildInputs = [ pkgs.nix-unit ]; + } + '' + export HOME=$TMPDIR + nix-unit --eval-store "$HOME" ${./checkAndMergeCompat.nix} \ + --arg currLibPath "${../.}" \ + --arg prevLibPath "${prevNixpkgs}/lib" + mkdir $out + '' +) diff --git a/lib/tests/release.nix b/lib/tests/release.nix index 3eb62912ffc4..5a515a55dcdc 100644 --- a/lib/tests/release.nix +++ b/lib/tests/release.nix @@ -29,6 +29,9 @@ in pkgsBB.symlinkJoin { name = "nixpkgs-lib-tests"; paths = map testWithNix nixVersions ++ [ + (import ./nix-unit.nix { + inherit pkgs; + }) (import ./maintainers.nix { inherit pkgs; lib = import ../.; From 4f802d935ca444a51ae83d45baa42c2a27a9c683 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 18 Aug 2025 08:01:24 +0200 Subject: [PATCH 13/14] lib/modules: fix typo --- lib/modules.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules.nix b/lib/modules.nix index bb16acac7e24..13844feb303f 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1167,7 +1167,7 @@ let if isDefined then if type.merge ? v2 then # check and merge share the same closure - # .headError is either non-present null or an error describing string + # .headError is either not-present, null, or a string describing the error if checkedAndMerged.headError or null != null then throw "A definition for option `${showOption loc}' is not of type `${type.description}'. TypeError: ${checkedAndMerged.headError.message}" else From bb0bd3d41349fd1ecabba07ed94741dcb81bf062 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 20 Aug 2025 21:55:19 +0200 Subject: [PATCH 14/14] lib/modules: add _internal to valueMeta of checkedAndMerged --- lib/modules.nix | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 13844feb303f..3330579952de 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1200,9 +1200,19 @@ let ) ( if type.merge ? v2 then - type.merge.v2 { - inherit loc; - defs = defsFinal; + let + r = type.merge.v2 { + inherit loc; + defs = defsFinal; + }; + in + r + // { + valueMeta = r.valueMeta // { + _internal = { + inherit type; + }; + }; } else { @@ -1621,13 +1631,11 @@ let New option path as list of strings. */ to, - /** Release number of the first release that contains the rename, ignoring backports. Set it to the upcoming release, matching the nixpkgs/.version file. */ sinceRelease, - }: doRename { inherit from to;