Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 41 additions & 22 deletions lib/modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = {
Expand All @@ -242,6 +263,8 @@ let
moduleType = type;
};
_module.specialArgs = specialArgs;

_module.freeformConfig = lib.mkMerge (map mkDefinition freeformDefs);
};
};

Expand All @@ -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
Expand Down Expand Up @@ -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);

/**
Expand Down Expand Up @@ -1095,7 +1114,7 @@ let
mergeDefinitions = loc: type: defs: rec {
defsFinal' =
let
# Process mkMerge and mkIf properties.
# Process properties
defs' = concatMap (
m:
map (
Expand Down
5 changes: 4 additions & 1 deletion lib/tests/modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
====== module tests ======
Expand Down
6 changes: 3 additions & 3 deletions lib/tests/modules/adhoc-freeformType-survives-type-merge.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
// {
Expand Down
43 changes: 43 additions & 0 deletions lib/tests/modules/freeform-options.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
lib,
config,
options,
...
}:
{
freeformType = with lib.types; attrsOf int;

# Regular options
options.bar = lib.mkOption {
default = 1;
};

# This should be in _module.freeformConfig
# With 'definitions' and 'type' in the corresponding options._module.freeformConfig
config.foo = 42;

imports = [
{
foo = 42;
}
{
bar = 1;
}
];

options.result = lib.mkOption {
default =
assert config._module.freeformConfig == { foo = 42; };
# option has the merged value
assert options._module.freeformConfig.value == { foo = 42; };
# option contains freeform definitions
assert
options._module.freeformConfig.definitions == [
{ foo = 42; }
{ foo = 42; }
];
# The type should be inherited from freeformType
assert options._module.freeformConfig.type.nestedTypes.elemType.name == "int";
true;
};
}