From 4b2a2acf27290a88f22245707d275d5df441e283 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 20 Mar 2025 22:21:50 +0000 Subject: [PATCH 1/3] lib.modules: expose freeformConfig --- lib/modules.nix | 59 ++++++++++++------- ...adhoc-freeformType-survives-type-merge.nix | 6 +- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index d6061ec6d0394..42da991934be1 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -234,6 +234,27 @@ let within a configuration, but can be used in module imports. ''; }; + + _module.freeformConfig = mkOption { + # Do not show this in the documentation + internal = true; + visible = false; + readOnly = true; + # We need a valid type here, + # ideally we'd only define this whole option, if there is a 'freeformType', however that is not possible due to technical limitations currently + type = + if declaredConfig._module.freeformType != null then + declaredConfig._module.freeformType + else + types.raw; + description = '' + Read-only version of the freeform definitions. This includes all definitions that don't have matching options if the freeformType is set. + Otherwise this is an empty attribute set. + ''; + # It's currently(?) not possible to compute the option value normally, using the type. + # Instead, we set it directly here, without evaluating the merge result from the type. + apply = _value: freeformConfig; + }; }; config = { @@ -242,6 +263,8 @@ let moduleType = type; }; _module.specialArgs = specialArgs; + + _module.freeformConfig = lib.mkMerge (map mkDefinition freeformDefs); }; }; @@ -266,23 +289,19 @@ let options = merged.matchedOptions; - config = - let - - # For definitions that have an associated option - declaredConfig = mapAttrsRecursiveCond (v: !isOption v) (_: v: v.value) options; - - # 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; + freeformDefs = map (def: { + file = def.file; + value = setAttrByPath def.prefix def.value; + }) (merged.unmatchedDefns); + declaredConfig = mapAttrsRecursiveCond (v: !isOption v) (_: v: v.value) options; + freeformConfig = + let + defs = freeformDefs; in + if defs == [ ] then { } else declaredConfig._module.freeformType.merge prefix defs; + + config = if declaredConfig._module.freeformType == null then declaredConfig # Because all definitions that had an associated option ended in @@ -652,11 +671,11 @@ let name: _: addErrorContext (context name) (args.${name} or config._module.args.${name}) ) (functionArgs f); - # Note: we append in the opposite order such that we can add an error - # context on the explicit arguments of "args" too. This update - # operator is used to make the "args@{ ... }: with args.lib;" notation - # works. in + # Note: we append in the opposite order such that we can add an error + # context on the explicit arguments of "args" too. This update + # operator is used to make the "args@{ ... }: with args.lib;" notation + # works. f (args // extraArgs); /** @@ -1095,7 +1114,7 @@ let mergeDefinitions = loc: type: defs: rec { defsFinal' = let - # Process mkMerge and mkIf properties. + # Process properties defs' = concatMap ( m: map ( diff --git a/lib/tests/modules/adhoc-freeformType-survives-type-merge.nix b/lib/tests/modules/adhoc-freeformType-survives-type-merge.nix index 99fcd0e34bee1..93d601143ec7c 100644 --- a/lib/tests/modules/adhoc-freeformType-survives-type-merge.nix +++ b/lib/tests/modules/adhoc-freeformType-survives-type-merge.nix @@ -6,10 +6,10 @@ }; freeformType = let - a = lib.types.attrsOf (lib.types.submodule { options.bar = lib.mkOption { }; }); + a = lib.types.attrsOf lib.types.anything; in - # modifying types like this breaks type merging. - # This test makes sure that type merging is not performed when only a single declaration exists. + # Modifying types like this is unsafe. Type merging or using a submodule type will discard those modifications. + # This test makes sure that type.merge can return definitions that don't intersect with the original definitions (unmatchedDefns). # Don't modify types in practice! a // { From 08522184ca259a3783ce01d5375404c18263690b Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 30 Apr 2025 20:24:04 +0200 Subject: [PATCH 2/3] lib/modules: remove rec from internal module attribute --- lib/modules.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 42da991934be1..09e0b6873b989 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -129,10 +129,10 @@ let # When extended with extendModules or moduleType, a fresh instance of # this module is used, to avoid conflicts and allow chaining of # extendModules. - internalModule = rec { + internalModule = { _file = "lib/modules.nix"; - key = _file; + key = "lib/modules.nix"; options = { _module.args = mkOption { From 242dcb06668dcc53d38b72f9ec4995a2957446ee Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 7 May 2025 17:24:31 +0200 Subject: [PATCH 3/3] lib/modules: add tests for _module.freeformConfig --- lib/tests/modules.sh | 5 ++- lib/tests/modules/freeform-options.nix | 43 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 lib/tests/modules/freeform-options.nix diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 3b19c8c63f263..271a3542c3fdd 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -703,7 +703,10 @@ checkConfigOutput '"nixos"' config.sub.nixos.foo ./specialArgs-class.nix checkConfigOutput '"bar"' config.sub.conditionalImportAsNixos.foo ./specialArgs-class.nix 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 + +# Checks for _module.freeformConfig +checkConfigOutput 'true' config.result ./freeform-options.nix + cat <