diff options
Diffstat (limited to 'lib/modules.nix')
-rw-r--r-- | lib/modules.nix | 165 |
1 files changed, 121 insertions, 44 deletions
diff --git a/lib/modules.nix b/lib/modules.nix index c18fec66c705..decb96ffe111 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -58,6 +58,23 @@ rec { default = check; description = "Whether to check whether all option definitions have matching declarations."; }; + + _module.freeformType = mkOption { + # Disallow merging for now, but could be implemented nicely with a `types.optionType` + type = types.nullOr (types.uniq types.attrs); + internal = true; + default = null; + description = '' + If set, merge all definitions that don't have an associated option + together using this type. The result then gets combined with the + values of all declared options to produce the final <literal> + config</literal> value. + + If this is <literal>null</literal>, definitions without an option + will throw an error unless <option>_module.check</option> is + turned off. + ''; + }; }; config = { @@ -65,35 +82,55 @@ rec { }; }; - collected = collectModules - (specialArgs.modulesPath or "") - (modules ++ [ internalModule ]) - ({ inherit config options lib; } // specialArgs); - - options = mergeModules prefix (reverseList collected); - - # Traverse options and extract the option values into the final - # config set. At the same time, check whether all option - # definitions have matching declarations. - # !!! _module.check's value can't depend on any other config values - # without an infinite recursion. One way around this is to make the - # 'config' passed around to the modules be unconditionally unchecked, - # and only do the check in 'result'. - config = yieldConfig prefix options; - yieldConfig = prefix: set: - let res = removeAttrs (mapAttrs (n: v: - if isOption v then v.value - else yieldConfig (prefix ++ [n]) v) set) ["_definedNames"]; - in - if options._module.check.value && set ? _definedNames then - foldl' (res: m: - foldl' (res: name: - if set ? ${name} then res else throw "The option `${showOption (prefix ++ [name])}' defined in `${m.file}' does not exist.") - res m.names) - res set._definedNames - else - res; - result = { + merged = + let collected = collectModules + (specialArgs.modulesPath or "") + (modules ++ [ internalModule ]) + ({ inherit lib options config; } // specialArgs); + in mergeModules prefix (reverseList collected); + + 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; + + in if declaredConfig._module.freeformType == null then declaredConfig + # Because all definitions that had an associated option ended in + # declaredConfig, freeformConfig can only contain the non-option + # paths, meaning recursiveUpdate will never override any value + else recursiveUpdate freeformConfig declaredConfig; + + checkUnmatched = + if config._module.check && config._module.freeformType == null && merged.unmatchedDefns != [] then + let + firstDef = head merged.unmatchedDefns; + baseMsg = "The option `${showOption (prefix ++ firstDef.prefix)}' defined in `${firstDef.file}' does not exist."; + in + if attrNames options == [ "_module" ] + then throw '' + ${baseMsg} + + However there are no options defined in `${showOption prefix}'. Are you sure you've + declared your options properly? This can happen if you e.g. declared your options in `types.submodule' + under `config' rather than `options'. + '' + else throw baseMsg + else null; + + result = builtins.seq checkUnmatched { inherit options; config = removeAttrs config [ "_module" ]; inherit (config) _module; @@ -174,12 +211,16 @@ rec { /* Massage a module into canonical form, that is, a set consisting of ‘options’, ‘config’ and ‘imports’ attributes. */ unifyModuleSyntax = file: key: m: - let addMeta = config: if m ? meta - then mkMerge [ config { meta = m.meta; } ] - else config; + let + addMeta = config: if m ? meta + then mkMerge [ config { meta = m.meta; } ] + else config; + addFreeformType = config: if m ? freeformType + then mkMerge [ config { _module.freeformType = m.freeformType; } ] + else config; in if m ? config || m ? options then - let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta"]; in + let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta" "freeformType"]; in if badAttrs != {} then throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by introducing a top-level `config' or `options' attribute. Add configuration attributes immediately on the top level instead, or move all of them (namely: ${toString (attrNames badAttrs)}) into the explicit `config' attribute." else @@ -188,7 +229,7 @@ rec { disabledModules = m.disabledModules or []; imports = m.imports or []; options = m.options or {}; - config = addMeta (m.config or {}); + config = addFreeformType (addMeta (m.config or {})); } else { _file = m._file or file; @@ -196,7 +237,7 @@ rec { disabledModules = m.disabledModules or []; imports = m.require or [] ++ m.imports or []; options = {}; - config = addMeta (removeAttrs m ["_file" "key" "disabledModules" "require" "imports"]); + config = addFreeformType (addMeta (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"])); }; applyIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then @@ -233,7 +274,23 @@ rec { declarations in all modules, combining them into a single set. At the same time, for each option declaration, it will merge the corresponding option definitions in all machines, returning them - in the ‘value’ attribute of each option. */ + in the ‘value’ attribute of each option. + + This returns a set like + { + # A recursive set of options along with their final values + matchedOptions = { + foo = { _type = "option"; value = "option value of foo"; ... }; + bar.baz = { _type = "option"; value = "option value of bar.baz"; ... }; + ... + }; + # A list of definitions that weren't matched by any option + unmatchedDefns = [ + { file = "file.nix"; prefix = [ "qux" ]; value = "qux"; } + ... + ]; + } + */ mergeModules = prefix: modules: mergeModules' prefix modules (concatMap (m: map (config: { file = m._file; inherit config; }) (pushDownProperties m.config)) modules); @@ -280,9 +337,9 @@ rec { defnsByName' = byName "config" (module: value: [{ inherit (module) file; inherit value; }] ) configs; - in - (flip mapAttrs declsByName (name: decls: - # We're descending into attribute ‘name’. + + resultsByName = flip mapAttrs declsByName (name: decls: + # We're descending into attribute ‘name’. let loc = prefix ++ [name]; defns = defnsByName.${name} or []; @@ -291,7 +348,10 @@ rec { in if nrOptions == length decls then let opt = fixupOptionType loc (mergeOptionDecls loc decls); - in evalOptionValue loc opt defns' + in { + matchedOptions = evalOptionValue loc opt defns'; + unmatchedDefns = []; + } else if nrOptions != 0 then let firstOption = findFirst (m: isOption m.options) "" decls; @@ -299,9 +359,27 @@ rec { in throw "The option `${showOption loc}' in `${firstOption._file}' is a prefix of options in `${firstNonOption._file}'." else - mergeModules' loc decls defns - )) - // { _definedNames = map (m: { inherit (m) file; names = attrNames m.config; }) configs; }; + mergeModules' loc decls defns); + + matchedOptions = mapAttrs (n: v: v.matchedOptions) resultsByName; + + # an attrset 'name' => list of unmatched definitions for 'name' + unmatchedDefnsByName = + # Propagate all unmatched definitions from nested option sets + mapAttrs (n: v: v.unmatchedDefns) resultsByName + # Plus the definitions for the current prefix that don't have a matching option + // removeAttrs defnsByName' (attrNames matchedOptions); + in { + inherit matchedOptions; + + # Transforms unmatchedDefnsByName into a list of definitions + unmatchedDefns = concatLists (mapAttrsToList (name: defs: + map (def: def // { + # Set this so we know when the definition first left unmatched territory + prefix = [name] ++ (def.prefix or []); + }) defs + ) unmatchedDefnsByName); + }; /* Merge multiple option declarations into a single declaration. In general, there should be only one declaration of each option. @@ -535,7 +613,6 @@ rec { if tp.name == "option set" || tp.name == "submodule" then throw "The option ${showOption loc} uses submodules without a wrapping type, in ${showFiles opt.declarations}." else if optionSetIn "attrsOf" then types.attrsOf (types.submodule options) - else if optionSetIn "loaOf" then types.loaOf (types.submodule options) else if optionSetIn "listOf" then types.listOf (types.submodule options) else if optionSetIn "nullOr" then types.nullOr (types.submodule options) else tp; |