diff --git a/lib/modules.nix b/lib/modules.nix index 3330579952dea..9236abbe87b8b 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1,3 +1,16 @@ +# This `let` must be top level +let + warn = builtins.warn or builtins.trace; + + # Only emitted once (per evaluator instance, per Nixpkgs source version) + freeformBadTypeContext = warn '' + freeformType does not pass type check: This module evaluation contains one or more instances of a problem with freeformType. If you are not the maintainer of any module, ignore these messages about freeformType. + The module system's `freeformType` is matched to a whole attribute set, but due to a bug, some types were *not* checked if they were assigned directly as the `freeformType`, notably types derived from `either`. + This problem can generally be fixed by wrapping it as it should: `freeformType = attrsOf ();`. + '' null; + +in + { lib }: let @@ -276,13 +289,24 @@ let # If freeformType is set, this is for definitions that don't have an associated option freeformConfig = - let - defs = map (def: { - file = def.file; - value = setAttrByPath def.prefix def.value; - }) merged.unmatchedDefns; - in - if defs == [ ] then { } else declaredConfig._module.freeformType.merge prefix defs; + if merged.unmatchedDefns == [ ] then + { } + else + let + defs = map (def: { + file = def.file; + value = setAttrByPath def.prefix def.value; + }) merged.unmatchedDefns; + mergedFreeform = mergeDefinitions prefix declaredConfig._module.freeformType defs; + in + # TODO (after 25.11): make this throw or just use mergedFreeform.checkedAndMerged.value, which throws + warnIf (mergedFreeform.checkedAndMerged.headError != null) + (builtins.seq freeformBadTypeContext "freeformType at `${showOption prefix}` does not pass type check; see preceding message.") + ( + # Use valueMeta to silence a warning from `either`, which is a more general but worse duplicate + mergedFreeform.checkedAndMerged.valueMeta._deprecatedFreeformTypeValueOverride + or mergedFreeform.checkedAndMerged.value + ); in if declaredConfig._module.freeformType == null then diff --git a/lib/types.nix b/lib/types.nix index 6b51f9254a00c..d455b9902fa71 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -1449,11 +1449,20 @@ let rec { valueMeta = { inherit headError; + # Allow freeformType to suppress the warning below, because it has a more specific solution. + _deprecatedFreeformTypeValueOverride = mergeOneOption loc defs; }; 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."; + value = + # TODO (after 25.11): Make this an error + lib.warn + "while accessing option ${showOption loc}: (t.merge.v2 defs).value must only be accessed when `.headError == null`. This is a bug in code that use the module system type interface incorrectly. This will be an error after Nixpkgs 25.11." + mergeOneOption + loc + defs; + # }; in checkedAndMerged;