diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/modules.nix | 172 | ||||
-rwxr-xr-x | lib/tests/modules.sh | 25 | ||||
-rw-r--r-- | lib/tests/modules/custom-arg-define-enable.nix | 8 | ||||
-rw-r--r-- | lib/tests/modules/define-if-loaOfSub-foo-enable.nix | 5 | ||||
-rw-r--r-- | lib/tests/modules/define-loaOfSub-foo-enable-if.nix | 5 | ||||
-rw-r--r-- | lib/tests/modules/define-loaOfSub-foo-if-enable.nix | 7 | ||||
-rw-r--r-- | lib/tests/modules/define-loaOfSub-if-foo-enable.nix | 7 | ||||
-rw-r--r-- | lib/tests/modules/define-module-check.nix | 3 | ||||
-rw-r--r-- | lib/types.nix | 32 |
9 files changed, 212 insertions, 52 deletions
diff --git a/lib/modules.nix b/lib/modules.nix index d0b8f90e5ce67..dcede0c46c63f 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -9,25 +9,69 @@ rec { /* Evaluate a set of modules. The result is a set of two attributes: ‘options’: the nested set of all option declarations, - and ‘config’: the nested set of all option values. */ - evalModules = { modules, prefix ? [], args ? {}, check ? true }: + and ‘config’: the nested set of all option values. + !!! Please think twice before adding to this argument list! The more + that is specified here instead of in the modules themselves the harder + it is to transparently move a set of modules to be a submodule of another + config (as the proper arguments need to be replicated at each call to + evalModules) and the less declarative the module set is. */ + evalModules = { modules + , prefix ? [] + , # This would be remove in the future, Prefer _module.args option instead. + args ? {} + , # This would be remove in the future, Prefer _module.check option instead. + check ? true + }: let - args' = args // { lib = import ./.; } // result; - closed = closeModules modules args'; + # This internal module declare internal options under the `_module' + # attribute. These options are fragile, as they are used by the + # module system to change the interpretation of modules. + internalModule = rec { + _file = ./modules.nix; + + key = _file; + + options = { + _module.args = mkOption { + type = types.attrsOf types.unspecified; + internal = true; + description = "Arguments passed to each module."; + }; + + _module.check = mkOption { + type = types.uniq types.bool; + internal = true; + default = check; + description = "Whether to check whether all option definitions have matching declarations."; + }; + }; + + config = { + _module.args = args; + }; + }; + + closed = closeModules (modules ++ [ internalModule ]) { inherit config options; lib = import ./.; }; + # Note: the list of modules is reversed to maintain backward # compatibility with the old module system. Not sure if this is # the most sensible policy. options = mergeModules prefix (reverseList closed); + # 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 check && set ? _definedNames then + if options._module.check.value && set ? _definedNames then fold (m: res: fold (name: res: if set ? ${name} then res else throw "The option `${showOption (prefix ++ [name])}' defined in `${m.file}' does not exist.") @@ -43,7 +87,7 @@ rec { let toClosureList = file: parentKey: imap (n: x: if isAttrs x || isFunction x then - unifyModuleSyntax file "${parentKey}:anon-${toString n}" (applyIfFunction x args) + unifyModuleSyntax file "${parentKey}:anon-${toString n}" (unpackSubmodule applyIfFunction x args) else unifyModuleSyntax (toString x) (toString x) (applyIfFunction (import x) args)); in @@ -74,7 +118,39 @@ rec { config = removeAttrs m ["key" "_file" "require" "imports"]; }; - applyIfFunction = f: arg: if isFunction f then f arg else f; + applyIfFunction = f: arg@{ config, options, lib }: if isFunction f then + let + # Module arguments are resolved in a strict manner when attribute set + # deconstruction is used. As the arguments are now defined with the + # config._module.args option, the strictness used on the attribute + # set argument would cause an infinite loop, if the result of the + # option is given as argument. + # + # To work-around the strictness issue on the deconstruction of the + # attributes set argument, we create a new attribute set which is + # constructed to satisfy the expected set of attributes. Thus calling + # a module will resolve strictly the attributes used as argument but + # not their values. The values are forwarding the result of the + # evaluation of the option. + requiredArgs = builtins.attrNames (builtins.functionArgs f); + extraArgs = builtins.listToAttrs (map (name: { + inherit name; + value = config._module.args.${name}; + }) requiredArgs); + in f (extraArgs // arg) + 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. @@ -106,12 +182,9 @@ rec { else [] ) configs); nrOptions = count (m: isOption m.options) decls; - # Process mkMerge and mkIf properties. - defns' = concatMap (m: - if m.config ? ${name} - then map (m': { inherit (m) file; value = m'; }) (dischargeProperties m.config.${name}) - else [] - ) configs; + # Extract the definitions for this loc + defns' = map (m: { inherit (m) file; value = m.config.${name}; }) + (filter (m: m.config ? ${name}) configs); in if nrOptions == length decls then let opt = fixupOptionType loc (mergeOptionDecls loc decls); @@ -156,15 +229,12 @@ 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; - addModuleFile = m: - if isFunction m then args: { _file = opt.file; } // (m args) - else { _file = opt.file; } // m; coerceOption = file: opt: - if isFunction opt then args: { _file = file; } // (opt args) - else { _file = file; options = 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 addModuleFile getSubModules ++ 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 // @@ -177,27 +247,17 @@ rec { config value. */ evalOptionValue = loc: opt: defs: let - # Process mkOverride properties, adding in the default - # value specified in the option declaration (if any). - defsFinal' = filterOverrides - ((if opt ? default then [{ file = head opt.declarations; value = mkOptionDefault opt.default; }] else []) ++ defs); - # Sort mkOrder properties. - defsFinal = - # Avoid sorting if we don't have to. - if any (def: def.value._type or "" == "order") defsFinal' - then sortProperties defsFinal' - else defsFinal'; + # Add in the default value for this option, if any. + defs' = (optional (opt ? default) + { file = head opt.declarations; value = mkOptionDefault opt.default; }) ++ defs; + + # Handle properties, check types, and merge everything together + inherit (mergeDefinitions loc opt.type defs') isDefined defsFinal mergedValue; files = map (def: def.file) defsFinal; - # Type-check the remaining definitions, and merge them if - # possible. merged = - if defsFinal == [] then - throw "The option `${showOption loc}' is used but not defined." - else - fold (def: res: - if opt.type.check def.value then res - else throw "The option value `${showOption loc}' in `${def.file}' is not a ${opt.type.name}.") - (opt.type.merge loc defsFinal) defsFinal; + if isDefined then mergedValue + else throw "The option `${showOption loc}' is used but not defined."; + # Finally, apply the ‘apply’ function to the merged # value. This allows options to yield a value computed # from the definitions. @@ -205,10 +265,42 @@ rec { in opt // { value = addErrorContext "while evaluating the option `${showOption loc}':" value; definitions = map (def: def.value) defsFinal; - isDefined = defsFinal != []; - inherit files; + inherit isDefined files; }; + # Merge definitions of a value of a given type + mergeDefinitions = loc: type: defs: rec { + defsFinal = + let + # Process mkMerge and mkIf properties + processIfAndMerge = defs: concatMap (m: + map (value: { inherit (m) file; inherit value; }) (dischargeProperties m.value) + ) defs; + + # Process mkOverride properties + processOverride = defs: filterOverrides defs; + + # Sort mkOrder properties + processOrder = defs: + # Avoid sorting if we don't have to. + if any (def: def.value._type or "" == "order") defs + then sortProperties defs + else defs; + in + processOrder (processOverride (processIfAndMerge defs)); + + # Type-check the remaining definitions, and merge them + mergedValue = fold (def: res: + if type.check def.value then res + else throw "The option value `${showOption loc}' in `${def.file}' is not a ${type.name}.") + (type.merge loc defsFinal) defsFinal; + + isDefined = defsFinal != []; + optionalValue = + if isDefined then { value = mergedValue; } + else {}; + }; + /* Given a config set, expand mkMerge properties, and push down the other properties into the children. The result is a list of config sets that do not have properties at top-level. For diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 58231a3563691..66c6f560fbe80 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -57,13 +57,17 @@ checkConfigError() { fi } +# Check boolean option. checkConfigOutput "false" config.enable ./declare-enable.nix checkConfigError 'The option .* defined in .* does not exist.' config.enable ./define-enable.nix + +# Check mkForce without submodules. set -- config.enable ./declare-enable.nix ./define-enable.nix checkConfigOutput "true" "$@" checkConfigOutput "false" "$@" ./define-force-enable.nix checkConfigOutput "false" "$@" ./define-enable-force.nix +# Check mkForce with option and submodules. checkConfigError 'attribute .*foo.* .* not found' config.loaOfSub.foo.enable ./declare-loaOfSub-any-enable.nix checkConfigOutput 'false' config.loaOfSub.foo.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo.nix set -- config.loaOfSub.foo.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo-enable.nix @@ -73,6 +77,7 @@ checkConfigOutput 'false' "$@" ./define-loaOfSub-force-foo-enable.nix checkConfigOutput 'false' "$@" ./define-loaOfSub-foo-force-enable.nix checkConfigOutput 'false' "$@" ./define-loaOfSub-foo-enable-force.nix +# Check overriding effect of mkForce on submodule definitions. checkConfigError 'attribute .*bar.* .* not found' config.loaOfSub.bar.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo.nix checkConfigOutput 'false' config.loaOfSub.bar.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo.nix ./define-loaOfSub-bar.nix set -- config.loaOfSub.bar.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo.nix ./define-loaOfSub-bar-enable.nix @@ -82,6 +87,26 @@ checkConfigError 'attribute .*bar.* .* not found' "$@" ./define-loaOfSub-force-f checkConfigOutput 'true' "$@" ./define-loaOfSub-foo-force-enable.nix checkConfigOutput 'true' "$@" ./define-loaOfSub-foo-enable-force.nix +# Check mkIf with submodules. +checkConfigError 'attribute .*foo.* .* not found' config.loaOfSub.foo.enable ./declare-enable.nix ./declare-loaOfSub-any-enable.nix +set -- config.loaOfSub.foo.enable ./declare-enable.nix ./declare-loaOfSub-any-enable.nix +checkConfigError 'attribute .*foo.* .* not found' "$@" ./define-if-loaOfSub-foo-enable.nix +checkConfigError 'attribute .*foo.* .* not found' "$@" ./define-loaOfSub-if-foo-enable.nix +checkConfigError 'attribute .*foo.* .* not found' "$@" ./define-loaOfSub-foo-if-enable.nix +checkConfigOutput 'false' "$@" ./define-loaOfSub-foo-enable-if.nix +checkConfigOutput 'true' "$@" ./define-enable.nix ./define-if-loaOfSub-foo-enable.nix +checkConfigOutput 'true' "$@" ./define-enable.nix ./define-loaOfSub-if-foo-enable.nix +checkConfigOutput 'true' "$@" ./define-enable.nix ./define-loaOfSub-foo-if-enable.nix +checkConfigOutput 'true' "$@" ./define-enable.nix ./define-loaOfSub-foo-enable-if.nix + +# Check _module.args. +checkConfigOutput "true" config.enable ./declare-enable.nix ./custom-arg-define-enable.nix + +# Check _module.check. +set -- config.enable ./declare-enable.nix ./define-enable.nix ./define-loaOfSub-foo.nix +checkConfigError 'The option .* defined in .* does not exist.' "$@" +checkConfigOutput "true" "$@" ./define-module-check.nix + cat <<EOF ====== module tests ====== $pass Pass diff --git a/lib/tests/modules/custom-arg-define-enable.nix b/lib/tests/modules/custom-arg-define-enable.nix new file mode 100644 index 0000000000000..f04d30dd9b9fc --- /dev/null +++ b/lib/tests/modules/custom-arg-define-enable.nix @@ -0,0 +1,8 @@ +{ lib, custom, ... }: + +{ + config = { + _module.args.custom = true; + enable = custom; + }; +} diff --git a/lib/tests/modules/define-if-loaOfSub-foo-enable.nix b/lib/tests/modules/define-if-loaOfSub-foo-enable.nix new file mode 100644 index 0000000000000..4288d74dec003 --- /dev/null +++ b/lib/tests/modules/define-if-loaOfSub-foo-enable.nix @@ -0,0 +1,5 @@ +{ config, lib, ... }: + +lib.mkIf config.enable { + loaOfSub.foo.enable = true; +} diff --git a/lib/tests/modules/define-loaOfSub-foo-enable-if.nix b/lib/tests/modules/define-loaOfSub-foo-enable-if.nix new file mode 100644 index 0000000000000..44b2c96cd021e --- /dev/null +++ b/lib/tests/modules/define-loaOfSub-foo-enable-if.nix @@ -0,0 +1,5 @@ +{ config, lib, ... }: + +{ + loaOfSub.foo.enable = lib.mkIf config.enable true; +} diff --git a/lib/tests/modules/define-loaOfSub-foo-if-enable.nix b/lib/tests/modules/define-loaOfSub-foo-if-enable.nix new file mode 100644 index 0000000000000..236b2840ee51b --- /dev/null +++ b/lib/tests/modules/define-loaOfSub-foo-if-enable.nix @@ -0,0 +1,7 @@ +{ config, lib, ... }: + +{ + loaOfSub.foo = lib.mkIf config.enable { + enable = true; + }; +} diff --git a/lib/tests/modules/define-loaOfSub-if-foo-enable.nix b/lib/tests/modules/define-loaOfSub-if-foo-enable.nix new file mode 100644 index 0000000000000..bd2d068d31a26 --- /dev/null +++ b/lib/tests/modules/define-loaOfSub-if-foo-enable.nix @@ -0,0 +1,7 @@ +{ config, lib, ... }: + +{ + loaOfSub = lib.mkIf config.enable { + foo.enable = true; + }; +} diff --git a/lib/tests/modules/define-module-check.nix b/lib/tests/modules/define-module-check.nix new file mode 100644 index 0000000000000..5a0707c975fa6 --- /dev/null +++ b/lib/tests/modules/define-module-check.nix @@ -0,0 +1,3 @@ +{ + _module.check = false; +} diff --git a/lib/types.nix b/lib/types.nix index 1e7abf36535fb..f22c766163458 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -6,6 +6,7 @@ with import ./attrsets.nix; with import ./options.nix; with import ./trivial.nix; with import ./strings.nix; +with {inherit (import ./modules.nix) mergeDefinitions; }; rec { @@ -109,11 +110,15 @@ rec { listOf = elemType: mkOptionType { name = "list of ${elemType.name}s"; - check = value: isList value && all elemType.check value; + check = isList; merge = loc: defs: - concatLists (imap (n: def: imap (m: def': - elemType.merge (loc ++ ["[${toString n}-${toString m}]"]) - [{ inherit (def) file; value = def'; }]) def.value) defs); + map (x: x.value) (filter (x: x ? value) (concatLists (imap (n: def: imap (m: def': + (mergeDefinitions + (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) + elemType + [{ inherit (def) file; value = def'; }] + ).optionalValue + ) def.value) defs))); getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); getSubModules = elemType.getSubModules; substSubModules = m: listOf (elemType.substSubModules m); @@ -121,12 +126,14 @@ rec { attrsOf = elemType: mkOptionType { name = "attribute set of ${elemType.name}s"; - check = x: isAttrs x && all elemType.check (attrValues x); + check = isAttrs; merge = loc: defs: - zipAttrsWith (name: elemType.merge (loc ++ [name])) + mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: + (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue + ) # Push down position info. (map (def: listToAttrs (mapAttrsToList (n: def': - { name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs); + { name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs))); getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); getSubModules = elemType.getSubModules; substSubModules = m: attrsOf (elemType.substSubModules m); @@ -150,10 +157,7 @@ rec { attrOnly = attrsOf elemType; in mkOptionType { name = "list or attribute set of ${elemType.name}s"; - check = x: - if isList x then listOnly.check x - else if isAttrs x then attrOnly.check x - else false; + check = x: isList x || isAttrs x; merge = loc: defs: attrOnly.merge loc (imap convertIfList defs); getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); getSubModules = elemType.getSubModules; @@ -194,7 +198,11 @@ rec { let coerce = def: if isFunction def then def else { config = def; }; modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs; - in (evalModules { inherit modules; args.name = last loc; prefix = loc; }).config; + in (evalModules { + inherit modules; + args.name = last loc; + prefix = loc; + }).config; getSubOptions = prefix: (evalModules { modules = opts'; inherit prefix; # FIXME: hack to get shit to evaluate. |