diff --git a/lib/modules.nix b/lib/modules.nix index cc3148b0eea7..3330579952de 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,14 @@ 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.merge ? v2 then + # check and merge share the same closure + # .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 + checkedAndMerged.value + else if all (def: type.check def.value) defsFinal then type.merge loc defsFinal else let @@ -1177,6 +1185,43 @@ let throw "The option `${showOption loc}' was accessed but has no value defined. Try setting the option."; + 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 + let + r = type.merge.v2 { + inherit loc; + defs = defsFinal; + }; + in + r + // { + valueMeta = r.valueMeta // { + _internal = { + inherit type; + }; + }; + } + else + { + headError = null; + value = mergedValue; + valueMeta = { }; + } + ); + isDefined = defsFinal != [ ]; optionalValue = if isDefined then { value = mergedValue; } else { }; @@ -1586,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; 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/modules.sh b/lib/tests/modules.sh index 301808ae6651..fbbccd8172f6 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 @@ -761,6 +761,26 @@ 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.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 +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 <