summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
authorRobert Hensing <roberth@users.noreply.github.com>2020-08-15 12:13:58 +0200
committerGitHub <noreply@github.com>2020-08-15 12:13:58 +0200
commit6d0a85fe528e020eb7d8eb78d33ee87055c01c74 (patch)
treecd0e99185243f514ba6a4004d8697e019681db55 /lib
parent375611df00b01c668fd82fc0632cd8aec826b5ec (diff)
parent25d75155f39d293d16ecd6aab70024a8740e0baa (diff)
downloadnixpkgs-6d0a85fe528e020eb7d8eb78d33ee87055c01c74.tar
nixpkgs-6d0a85fe528e020eb7d8eb78d33ee87055c01c74.tar.gz
nixpkgs-6d0a85fe528e020eb7d8eb78d33ee87055c01c74.tar.bz2
nixpkgs-6d0a85fe528e020eb7d8eb78d33ee87055c01c74.tar.lz
nixpkgs-6d0a85fe528e020eb7d8eb78d33ee87055c01c74.tar.xz
nixpkgs-6d0a85fe528e020eb7d8eb78d33ee87055c01c74.tar.zst
nixpkgs-6d0a85fe528e020eb7d8eb78d33ee87055c01c74.zip
Merge pull request #82743 from Infinisil/partially-typed-v2
Freeform modules
Diffstat (limited to 'lib')
-rw-r--r--lib/modules.nix153
-rwxr-xr-xlib/tests/modules.sh23
-rw-r--r--lib/tests/modules/define-value-string-properties.nix12
-rw-r--r--lib/tests/modules/freeform-attrsOf.nix3
-rw-r--r--lib/tests/modules/freeform-lazyAttrsOf.nix3
-rw-r--r--lib/tests/modules/freeform-nested.nix7
-rw-r--r--lib/tests/modules/freeform-str-dep-unstr.nix8
-rw-r--r--lib/tests/modules/freeform-unstr-dep-str.nix8
-rw-r--r--lib/types.nix6
9 files changed, 180 insertions, 43 deletions
diff --git a/lib/modules.nix b/lib/modules.nix
index c18fec66c70..55a53b3909a 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,44 @@ 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 inherit (head merged.unmatchedDefns) file prefix;
+          in throw "The option `${showOption prefix}' defined in `${file}' does not exist."
+        else null;
+
+      result = builtins.seq checkUnmatched {
         inherit options;
         config = removeAttrs config [ "_module" ];
         inherit (config) _module;
@@ -174,12 +200,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 +218,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 +226,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 +263,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 +326,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 +337,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 +348,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.
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index 6258244457a..943deebe3c0 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -210,6 +210,29 @@ checkConfigOutput "empty" config.value.foo ./declare-lazyAttrsOf.nix ./attrsOf-c
 checkConfigError 'The option value .* in .* is not of type .*' \
   config.value ./declare-int-unsigned-value.nix ./define-value-list.nix ./define-value-int-positive.nix
 
+## Freeform modules
+# Assigning without a declared option should work
+checkConfigOutput 24 config.value ./freeform-attrsOf.nix ./define-value-string.nix
+# No freeform assigments shouldn't make it error
+checkConfigOutput '{ }' config ./freeform-attrsOf.nix
+# but only if the type matches
+checkConfigError 'The option value .* in .* is not of type .*' config.value ./freeform-attrsOf.nix ./define-value-list.nix
+# and properties should be applied
+checkConfigOutput yes config.value ./freeform-attrsOf.nix ./define-value-string-properties.nix
+# Options should still be declarable, and be able to have a type that doesn't match the freeform type
+checkConfigOutput false config.enable ./freeform-attrsOf.nix ./define-value-string.nix ./declare-enable.nix
+checkConfigOutput 24 config.value ./freeform-attrsOf.nix ./define-value-string.nix ./declare-enable.nix
+# and this should work too with nested values
+checkConfigOutput false config.nest.foo ./freeform-attrsOf.nix ./freeform-nested.nix
+checkConfigOutput bar config.nest.bar ./freeform-attrsOf.nix ./freeform-nested.nix
+# Check whether a declared option can depend on an freeform-typed one
+checkConfigOutput null config.foo ./freeform-attrsOf.nix ./freeform-str-dep-unstr.nix
+checkConfigOutput 24 config.foo ./freeform-attrsOf.nix ./freeform-str-dep-unstr.nix ./define-value-string.nix
+# Check whether an freeform-typed value can depend on a declared option, this can only work with lazyAttrsOf
+checkConfigError 'infinite recursion encountered' config.foo ./freeform-attrsOf.nix ./freeform-unstr-dep-str.nix
+checkConfigError 'The option .* is used but not defined' config.foo ./freeform-lazyAttrsOf.nix ./freeform-unstr-dep-str.nix
+checkConfigOutput 24 config.foo ./freeform-lazyAttrsOf.nix ./freeform-unstr-dep-str.nix ./define-value-string.nix
+
 cat <<EOF
 ====== module tests ======
 $pass Pass
diff --git a/lib/tests/modules/define-value-string-properties.nix b/lib/tests/modules/define-value-string-properties.nix
new file mode 100644
index 00000000000..972304c0112
--- /dev/null
+++ b/lib/tests/modules/define-value-string-properties.nix
@@ -0,0 +1,12 @@
+{ lib, ... }: {
+
+  imports = [{
+    value = lib.mkDefault "def";
+  }];
+
+  value = lib.mkMerge [
+    (lib.mkIf false "nope")
+    "yes"
+  ];
+
+}
diff --git a/lib/tests/modules/freeform-attrsOf.nix b/lib/tests/modules/freeform-attrsOf.nix
new file mode 100644
index 00000000000..8cc577f38a6
--- /dev/null
+++ b/lib/tests/modules/freeform-attrsOf.nix
@@ -0,0 +1,3 @@
+{ lib, ... }: {
+  freeformType = with lib.types; attrsOf (either str (attrsOf str));
+}
diff --git a/lib/tests/modules/freeform-lazyAttrsOf.nix b/lib/tests/modules/freeform-lazyAttrsOf.nix
new file mode 100644
index 00000000000..36d6c0b13fc
--- /dev/null
+++ b/lib/tests/modules/freeform-lazyAttrsOf.nix
@@ -0,0 +1,3 @@
+{ lib, ... }: {
+  freeformType = with lib.types; lazyAttrsOf (either str (lazyAttrsOf str));
+}
diff --git a/lib/tests/modules/freeform-nested.nix b/lib/tests/modules/freeform-nested.nix
new file mode 100644
index 00000000000..5da27f5a8b4
--- /dev/null
+++ b/lib/tests/modules/freeform-nested.nix
@@ -0,0 +1,7 @@
+{ lib, ... }: {
+  options.nest.foo = lib.mkOption {
+    type = lib.types.bool;
+    default = false;
+  };
+  config.nest.bar = "bar";
+}
diff --git a/lib/tests/modules/freeform-str-dep-unstr.nix b/lib/tests/modules/freeform-str-dep-unstr.nix
new file mode 100644
index 00000000000..a2dfbc80cfa
--- /dev/null
+++ b/lib/tests/modules/freeform-str-dep-unstr.nix
@@ -0,0 +1,8 @@
+{ lib, config, ... }: {
+  options.foo = lib.mkOption {
+    type = lib.types.nullOr lib.types.str;
+    default = null;
+  };
+
+  config.foo = lib.mkIf (config ? value) config.value;
+}
diff --git a/lib/tests/modules/freeform-unstr-dep-str.nix b/lib/tests/modules/freeform-unstr-dep-str.nix
new file mode 100644
index 00000000000..549d89afeca
--- /dev/null
+++ b/lib/tests/modules/freeform-unstr-dep-str.nix
@@ -0,0 +1,8 @@
+{ lib, config, ... }: {
+  options.value = lib.mkOption {
+    type = lib.types.nullOr lib.types.str;
+    default = null;
+  };
+
+  config.foo = lib.mkIf (config.value != null) config.value;
+}
diff --git a/lib/types.nix b/lib/types.nix
index 6fd6de7e1fd..1845b6ae339 100644
--- a/lib/types.nix
+++ b/lib/types.nix
@@ -486,9 +486,15 @@ rec {
           else value
         ) defs;
 
+        freeformType = (evalModules {
+          inherit modules specialArgs;
+          args.name = "‹name›";
+        })._module.freeformType;
+
       in
       mkOptionType rec {
         name = "submodule";
+        description = freeformType.description or name;
         check = x: isAttrs x || isFunction x || path.check x;
         merge = loc: defs:
           (evalModules {