diff --git a/lib/default.nix b/lib/default.nix index 8af53152586..e31edeaaf9e 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -102,7 +102,7 @@ let commitIdFromGitRepo cleanSourceWith pathHasContext canCleanSource; inherit (modules) evalModules closeModules unifyModuleSyntax - applyIfFunction unpackSubmodule packSubmodule mergeModules + applyIfFunction mergeModules mergeModules' mergeOptionDecls evalOptionValue mergeDefinitions pushDownProperties dischargeProperties filterOverrides sortProperties fixupOptionType mkIf mkAssert mkMerge mkOverride diff --git a/lib/modules.nix b/lib/modules.nix index 44db77b5d1c..48788ae933d 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -103,42 +103,42 @@ rec { toClosureList = file: parentKey: imap1 (n: x: if isAttrs x || isFunction x then let key = "${parentKey}:anon-${toString n}"; in - unifyModuleSyntax file key (unpackSubmodule (applyIfFunction key) x args) + unifyModuleSyntax file key (applyIfFunction key x args) else let file = toString x; key = toString x; in unifyModuleSyntax file key (applyIfFunction key (import x) args)); in builtins.genericClosure { startSet = toClosureList unknownModule "" modules; - operator = m: toClosureList m.file m.key m.imports; + operator = m: toClosureList m._file m.key m.imports; }; /* Massage a module into canonical form, that is, a set consisting of ‘options’, ‘config’ and ‘imports’ attributes. */ unifyModuleSyntax = file: key: m: - let metaSet = if m ? meta - then { meta = m.meta; } - else {}; + let addMeta = config: if m ? meta + then mkMerge [ config { meta = m.meta; } ] + else config; in if m ? config || m ? options then let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta"]; in if badAttrs != {} then throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by assignments to the top-level attributes `config' or `options'." else - { file = m._file or file; + { _file = m._file or file; key = toString m.key or key; disabledModules = m.disabledModules or []; imports = m.imports or []; options = m.options or {}; - config = mkMerge [ (m.config or {}) metaSet ]; + config = addMeta (m.config or {}); } else - { file = m._file or file; + { _file = m._file or file; key = toString m.key or key; disabledModules = m.disabledModules or []; imports = m.require or [] ++ m.imports or []; options = {}; - config = mkMerge [ (removeAttrs m ["_file" "key" "disabledModules" "require" "imports"]) metaSet ]; + config = addMeta (removeAttrs m ["_file" "key" "disabledModules" "require" "imports"]); }; applyIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then @@ -171,17 +171,6 @@ rec { else f; - /* We have to pack and unpack submodules. We cannot wrap the expected - result of the function as we would no longer be able to list the arguments - of the submodule. (see applyIfFunction) */ - unpackSubmodule = unpack: m: args: - if isType "submodule" m then - { _file = m.file; } // (unpack m.submodule args) - else unpack m args; - - packSubmodule = file: m: - { _type = "submodule"; file = file; submodule = m; }; - /* Merge a list of modules. This will recurse over the option declarations in all modules, combining them into a single set. At the same time, for each option declaration, it will merge the @@ -189,7 +178,7 @@ rec { in the ‘value’ attribute of each option. */ mergeModules = prefix: modules: mergeModules' prefix modules - (concatMap (m: map (config: { inherit (m) file; inherit config; }) (pushDownProperties m.config)) modules); + (concatMap (m: map (config: { file = m._file; inherit config; }) (pushDownProperties m.config)) modules); mergeModules' = prefix: options: configs: let @@ -223,7 +212,7 @@ rec { ) {} modules; # an attrset 'name' => list of submodules that declare ‘name’. declsByName = byName "options" (module: option: - [{ inherit (module) file; options = option; }] + [{ inherit (module) _file; options = option; }] ) options; # an attrset 'name' => list of submodules that define ‘name’. defnsByName = byName "config" (module: value: @@ -250,7 +239,7 @@ rec { firstOption = findFirst (m: isOption m.options) "" decls; firstNonOption = findFirst (m: !isOption m.options) "" decls; in - throw "The option `${showOption loc}' in `${firstOption.file}' is a prefix of options in `${firstNonOption.file}'." + throw "The option `${showOption loc}' in `${firstOption._file}' is a prefix of options in `${firstNonOption._file}'." else mergeModules' loc decls defns )) @@ -267,7 +256,14 @@ rec { 'opts' is a list of modules. Each module has an options attribute which correspond to the definition of 'loc' in 'opt.file'. */ - mergeOptionDecls = loc: opts: + mergeOptionDecls = + let + packSubmodule = file: m: + { _file = file; imports = [ m ]; }; + coerceOption = file: opt: + if isFunction opt then packSubmodule file opt + else packSubmodule file { options = opt; }; + in loc: opts: foldl' (res: opt: let t = res.type; t' = opt.options.type; @@ -284,7 +280,7 @@ rec { bothHave "apply" || (bothHave "type" && (! typesMergeable)) then - throw "The option `${showOption loc}' in `${opt.file}' is already declared in ${showFiles res.declarations}." + throw "The option `${showOption loc}' in `${opt._file}' is already declared in ${showFiles res.declarations}." else let /* Add the modules of the current option to the list of modules @@ -293,16 +289,14 @@ rec { current option declaration as the file use for the submodule. If the submodule defines any filename, then we ignore the enclosing option file. */ options' = toList opt.options.options; - coerceOption = file: opt: - if isFunction opt then packSubmodule file opt - else packSubmodule file { options = opt; }; + getSubModules = opt.options.type.getSubModules or null; submodules = - if getSubModules != null then map (packSubmodule opt.file) getSubModules ++ res.options - else if opt.options ? options then map (coerceOption opt.file) options' ++ res.options + if getSubModules != null then map (packSubmodule opt._file) getSubModules ++ res.options + else if opt.options ? options then map (coerceOption opt._file) options' ++ res.options else res.options; in opt.options // res // - { declarations = res.declarations ++ [opt.file]; + { declarations = res.declarations ++ [opt._file]; options = submodules; } // typeSet ) { inherit loc; declarations = []; options = []; } opts; diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index cf344122cf4..4690e380ce3 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -164,6 +164,24 @@ checkConfigOutput "true" config.enableAlias ./alias-with-priority.nix checkConfigOutput "false" config.enable ./alias-with-priority-can-override.nix checkConfigOutput "false" config.enableAlias ./alias-with-priority-can-override.nix +# submoduleWith + +## specialArgs should work +checkConfigOutput "foo" config.submodule.foo ./declare-submoduleWith-special.nix + +## shorthandOnlyDefines config behaves as expected +checkConfigOutput "true" config.submodule.config ./declare-submoduleWith-shorthand.nix ./define-submoduleWith-shorthand.nix +checkConfigError 'is not of type `boolean' config.submodule.config ./declare-submoduleWith-shorthand.nix ./define-submoduleWith-noshorthand.nix +checkConfigError 'value is a boolean while a set was expected' config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-shorthand.nix +checkConfigOutput "true" config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-noshorthand.nix + +## submoduleWith should merge all modules in one swoop +checkConfigOutput "true" config.submodule.inner ./declare-submoduleWith-modules.nix +checkConfigOutput "true" config.submodule.outer ./declare-submoduleWith-modules.nix + +## Paths should be allowed as values and work as expected +checkConfigOutput "true" config.submodule.enable ./declare-submoduleWith-path.nix + cat < A set of sub options o. - o can be an attribute set or a function - returning an attribute set. Submodules are used in composed types to - create modular options. Submodule are detailed in + o can be an attribute set, a function + returning an attribute set, or a path to a file containing such a value. Submodules are used in + composed types to create modular options. This is equivalent to + types.submoduleWith { modules = toList o; shorthandOnlyDefinesConfig = true; }. + Submodules are detailed in . + + + types.submoduleWith { + modules, + specialArgs ? {}, + shorthandOnlyDefinesConfig ? false } + + + + Like types.submodule, but more flexible and with better defaults. + It has parameters + + + modules + A list of modules to use by default for this submodule type. This gets combined + with all option definitions to build the final list of modules that will be included. + + Only options defined with this argument are included in rendered documentation. + + + + specialArgs + An attribute set of extra arguments to be passed to the module functions. + The option _module.args should be used instead + for most arguments since it allows overriding. specialArgs should only be + used for arguments that can't go through the module fixed-point, because of + infinite recursion or other problems. An example is overriding the + lib argument, because lib itself is used + to define _module.args, which makes using + _module.args to define it impossible. + + + shorthandOnlyDefinesConfig + Whether definitions of this type should default to the config + section of a module (see ) if it is an attribute + set. Enabling this only has a benefit when the submodule defines an option named + config or options. In such a case it would + allow the option to be set with the-submodule.config = "value" + instead of requiring the-submodule.config.config = "value". + This is because only when modules don't set the + config or options keys, all keys are interpreted + as option definitions in the config section. Enabling this option + implicitly puts all attributes in the config section. + + + With this option enabled, defining a non-config section requires + using a function: the-submodule = { ... }: { options = { ... }; }. + + + + + diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix index b3f2af5b179..47b10e408c0 100644 --- a/nixos/modules/services/networking/syncthing.nix +++ b/nixos/modules/services/networking/syncthing.nix @@ -112,12 +112,12 @@ in { addresses = [ "tcp://192.168.0.10:51820" ]; }; }; - type = types.attrsOf (types.submodule ({ config, ... }: { + type = types.attrsOf (types.submodule ({ name, ... }: { options = { name = mkOption { type = types.str; - default = config._module.args.name; + default = name; description = '' Name of the device ''; @@ -175,7 +175,7 @@ in { devices = [ "bigbox" ]; }; }; - type = types.attrsOf (types.submodule ({ config, ... }: { + type = types.attrsOf (types.submodule ({ name, ... }: { options = { enable = mkOption { @@ -190,7 +190,7 @@ in { path = mkOption { type = types.str; - default = config._module.args.name; + default = name; description = '' The path to the folder which should be shared. ''; @@ -198,7 +198,7 @@ in { id = mkOption { type = types.str; - default = config._module.args.name; + default = name; description = '' The id of the folder. Must be the same on all devices. ''; @@ -206,7 +206,7 @@ in { label = mkOption { type = types.str; - default = config._module.args.name; + default = name; description = '' The label of the folder. '';