about summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/modules.nix172
-rwxr-xr-xlib/tests/modules.sh25
-rw-r--r--lib/tests/modules/custom-arg-define-enable.nix8
-rw-r--r--lib/tests/modules/define-if-loaOfSub-foo-enable.nix5
-rw-r--r--lib/tests/modules/define-loaOfSub-foo-enable-if.nix5
-rw-r--r--lib/tests/modules/define-loaOfSub-foo-if-enable.nix7
-rw-r--r--lib/tests/modules/define-loaOfSub-if-foo-enable.nix7
-rw-r--r--lib/tests/modules/define-module-check.nix3
-rw-r--r--lib/types.nix32
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.