From 4746f6d03e4f8dc6e7399f45aaba0ca3aac32761 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 10 Mar 2022 22:45:41 +0100 Subject: [PATCH 1/8] lib.types: Add deferredModule --- lib/tests/modules.sh | 3 ++ lib/tests/modules/deferred-module.nix | 54 +++++++++++++++++++ lib/types.nix | 8 +++ .../development/option-types.section.md | 19 +++++++ .../development/option-types.section.xml | 34 ++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 lib/tests/modules/deferred-module.nix diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 36af32ca89da..9b1348f58c9c 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -194,6 +194,9 @@ checkConfigOutput '^"submodule"$' options.submodule.type.description ./declare-s ## Paths should be allowed as values and work as expected checkConfigOutput '^true$' config.submodule.enable ./declare-submoduleWith-path.nix +## Deferred module +checkConfigOutput '"beta"' config.nodes.foo.settingsDict.c ./deferred-module.nix + # Check the file location information is propagated into submodules checkConfigOutput the-file.nix config.submodule.internalFiles.0 ./submoduleFiles.nix diff --git a/lib/tests/modules/deferred-module.nix b/lib/tests/modules/deferred-module.nix new file mode 100644 index 000000000000..faf459a991fa --- /dev/null +++ b/lib/tests/modules/deferred-module.nix @@ -0,0 +1,54 @@ +{ lib, ... }: +let + inherit (lib) types mkOption setDefaultModuleLocation; + inherit (types) deferredModule lazyAttrsOf submodule str raw; +in +{ + imports = [ + # generic module, declaring submodules: + # - nodes. + # - default + # where all nodes include the default + ({ config, ... }: { + _file = "generic.nix"; + options.nodes = mkOption { + type = lazyAttrsOf (submodule { imports = config.default; }); + default = {}; + }; + options.default = mkOption { + type = deferredModule; + default = { }; + description = '' + Module that is included in all nodes. + ''; + }; + }) + + { + _file = "default-1.nix"; + default = { config, ... }: { + options.settingsDict = lib.mkOption { type = lazyAttrsOf str; default = {}; }; + }; + } + + { + _file = "default-a-is-b.nix"; + default = { config, ... }: { + settingsDict.a = config.settingsDict.b; + }; + } + + { + _file = "nodes-foo.nix"; + nodes.foo.settingsDict.b = "beta"; + } + + { + _file = "nodes-foo-c-is-a.nix"; + nodes.foo = { config, ... }: { + settingsDict.c = config.settingsDict.a; + }; + } + + ]; +} diff --git a/lib/types.nix b/lib/types.nix index caaa6dccc6d4..22a329264457 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -539,6 +539,14 @@ rec { modules = toList modules; }; + # A module to be imported in some other part of the configuration. + deferredModule = mkOptionType { + name = "deferredModule"; + description = "module"; + check = t: isAttrs t || isFunction t; + merge = loc: defs: map (def: lib.setDefaultModuleLocation "${showOption loc} from ${def.file}" def.value) defs; + }; + # The type of a type! optionType = mkOptionType { name = "optionType"; diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index d32d4fc50ad7..0241aae1dc89 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -220,6 +220,25 @@ Value types are types that take a value parameter. requires using a function: `the-submodule = { ... }: { options = { ... }; }`. +`types.deferredModule` + +: Whereas `submodule` represents an option tree, `deferredModule` represents + a module value, such as a module file or a configuration. + + It can be set multiple times. + + Module authors can use its value, which is always a list of module values, + in `imports` or in `submoduleWith`'s `modules` parameter. + Note that `imports` must be evaluated before the module fixpoint. Because + of this, deferred modules can only be imported into "other" fixpoints, such + as submodules. + + One use case for this type is the type of a "default" module that allow the + user to affect all submodules in an `attrsOf submodule` at once. This is + more convenient and discoverable than expecting the module user to + type-merge with the `attrsOf submodule` option. NixOps uses this type in + `network.defaults`. + ## Composed Types {#sec-option-types-composed} Composed types are types that take a type as parameter. `listOf diff --git a/nixos/doc/manual/from_md/development/option-types.section.xml b/nixos/doc/manual/from_md/development/option-types.section.xml index c67e183581c2..820646be671f 100644 --- a/nixos/doc/manual/from_md/development/option-types.section.xml +++ b/nixos/doc/manual/from_md/development/option-types.section.xml @@ -427,6 +427,40 @@ + + + types.deferredModule + + + + Whereas submodule represents an option + tree, deferredModule represents a module + value, such as a module file or a configuration. + + + It can be set multiple times. + + + Module authors can use its value, which is always a list of + module values, in imports or in + submoduleWith’s + modules parameter. Note that + imports must be evaluated before the + module fixpoint. Because of this, deferred modules can only + be imported into other fixpoints, such as + submodules. + + + One use case for this type is the type of a + default module that allow the user to affect + all submodules in an attrsOf submodule at + once. This is more convenient and discoverable than + expecting the module user to type-merge with the + attrsOf submodule option. NixOps uses + this type in network.defaults. + + +
From 38b7709a6f02ea33fe67220b7ff14fb21ab08e14 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 18 May 2022 18:42:18 +0200 Subject: [PATCH 2/8] lib/test/modules.sh: Test deferredModule error location file --- lib/tests/modules.sh | 5 ++++- lib/tests/modules/deferred-module.nix | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 9b1348f58c9c..155d7e5fa3d2 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -194,8 +194,11 @@ checkConfigOutput '^"submodule"$' options.submodule.type.description ./declare-s ## Paths should be allowed as values and work as expected checkConfigOutput '^true$' config.submodule.enable ./declare-submoduleWith-path.nix -## Deferred module +## deferredModule +# default module is merged into nodes.foo checkConfigOutput '"beta"' config.nodes.foo.settingsDict.c ./deferred-module.nix +# errors from the default module are reported with accurate location +checkConfigError 'In `default from the-file-that-contains-the-bad-config.nix'\'': "bogus"' config.nodes.foo.bottom ./deferred-module.nix # Check the file location information is propagated into submodules checkConfigOutput the-file.nix config.submodule.internalFiles.0 ./submoduleFiles.nix diff --git a/lib/tests/modules/deferred-module.nix b/lib/tests/modules/deferred-module.nix index faf459a991fa..dc8072d4e6c3 100644 --- a/lib/tests/modules/deferred-module.nix +++ b/lib/tests/modules/deferred-module.nix @@ -1,7 +1,7 @@ { lib, ... }: let inherit (lib) types mkOption setDefaultModuleLocation; - inherit (types) deferredModule lazyAttrsOf submodule str raw; + inherit (types) deferredModule lazyAttrsOf submodule str raw enum; in { imports = [ @@ -28,6 +28,7 @@ in _file = "default-1.nix"; default = { config, ... }: { options.settingsDict = lib.mkOption { type = lazyAttrsOf str; default = {}; }; + options.bottom = lib.mkOption { type = enum []; }; }; } @@ -43,6 +44,11 @@ in nodes.foo.settingsDict.b = "beta"; } + { + _file = "the-file-that-contains-the-bad-config.nix"; + default.bottom = "bogus"; + } + { _file = "nodes-foo-c-is-a.nix"; nodes.foo = { config, ... }: { From a2c29561e722e9546c61323a91f9faffcc3bf268 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 18 May 2022 18:46:35 +0200 Subject: [PATCH 3/8] lib.types.deferredModule: Improve reported location --- lib/tests/modules.sh | 2 +- lib/types.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 155d7e5fa3d2..29f6272ea50a 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -198,7 +198,7 @@ checkConfigOutput '^true$' config.submodule.enable ./declare-submoduleWith-path. # default module is merged into nodes.foo checkConfigOutput '"beta"' config.nodes.foo.settingsDict.c ./deferred-module.nix # errors from the default module are reported with accurate location -checkConfigError 'In `default from the-file-that-contains-the-bad-config.nix'\'': "bogus"' config.nodes.foo.bottom ./deferred-module.nix +checkConfigError 'In `the-file-that-contains-the-bad-config.nix, via option default'\'': "bogus"' config.nodes.foo.bottom ./deferred-module.nix # Check the file location information is propagated into submodules checkConfigOutput the-file.nix config.submodule.internalFiles.0 ./submoduleFiles.nix diff --git a/lib/types.nix b/lib/types.nix index 22a329264457..83882179d06d 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -544,7 +544,7 @@ rec { name = "deferredModule"; description = "module"; check = t: isAttrs t || isFunction t; - merge = loc: defs: map (def: lib.setDefaultModuleLocation "${showOption loc} from ${def.file}" def.value) defs; + merge = loc: defs: map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; }; # The type of a type! From 781c2e0789f7f6b75454a8e986c675ad36e6ee36 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 May 2022 16:31:47 +0200 Subject: [PATCH 4/8] lib.types.deferredModule: Allow path-typed module references --- lib/tests/modules/deferred-module.nix | 4 +--- lib/tests/modules/define-settingsDict-a-is-b.nix | 3 +++ lib/types.nix | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 lib/tests/modules/define-settingsDict-a-is-b.nix diff --git a/lib/tests/modules/deferred-module.nix b/lib/tests/modules/deferred-module.nix index dc8072d4e6c3..e963c5a845c1 100644 --- a/lib/tests/modules/deferred-module.nix +++ b/lib/tests/modules/deferred-module.nix @@ -34,9 +34,7 @@ in { _file = "default-a-is-b.nix"; - default = { config, ... }: { - settingsDict.a = config.settingsDict.b; - }; + default = ./define-settingsDict-a-is-b.nix; } { diff --git a/lib/tests/modules/define-settingsDict-a-is-b.nix b/lib/tests/modules/define-settingsDict-a-is-b.nix new file mode 100644 index 000000000000..42363f45f78d --- /dev/null +++ b/lib/tests/modules/define-settingsDict-a-is-b.nix @@ -0,0 +1,3 @@ +{ config, ... }: { + settingsDict.a = config.settingsDict.b; +} diff --git a/lib/types.nix b/lib/types.nix index 83882179d06d..f5d13ea10d28 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -543,7 +543,7 @@ rec { deferredModule = mkOptionType { name = "deferredModule"; description = "module"; - check = t: isAttrs t || isFunction t; + check = x: isAttrs x || isFunction x || path.check x; merge = loc: defs: map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; }; From 19a069ab8b17834da249b5ecb507c2eabe76a3e3 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 25 May 2022 17:45:28 +0200 Subject: [PATCH 5/8] lib.types: Add deferredModuleWith --- lib/modules.nix | 4 +++- lib/types.nix | 24 ++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index d8ae497fb2d8..1e8ba3471dd0 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -467,7 +467,9 @@ rec { disabledModules = m.disabledModules or []; imports = m.require or [] ++ m.imports or []; options = {}; - config = addFreeformType (addMeta (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"])); + config = + lib.throwIfNot (isAttrs m) "module ${file} (${key}) does not look like a module." + addFreeformType (addMeta (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"])); }; applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then diff --git a/lib/types.nix b/lib/types.nix index f5d13ea10d28..68dfa5843dee 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -540,11 +540,31 @@ rec { }; # A module to be imported in some other part of the configuration. - deferredModule = mkOptionType { + deferredModule = deferredModuleWith { }; + + # A module to be imported in some other part of the configuration. + # `staticModules`' options will be added to the documentation, unlike + # options declared via `config`. + deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType { name = "deferredModule"; description = "module"; check = x: isAttrs x || isFunction x || path.check x; - merge = loc: defs: map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; + merge = loc: defs: staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; + inherit (submoduleWith { modules = staticModules; }) + getSubOptions + getSubModules; + substSubModules = m: deferredModuleWith (attrs // { + staticModules = m; + }); + functor = defaultFunctor "deferredModuleWith" // { + type = types.deferredModuleWith; + payload = { + inherit staticModules; + }; + binOp = lhs: rhs: { + staticModules = lhs.staticModules ++ rhs.staticModules; + }; + }; }; # The type of a type! From dfd98a5da26c341cf3b7e6fe7e2dbbaeb0af519c Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 14 Jun 2022 02:12:43 +0200 Subject: [PATCH 6/8] lib.deferredModule: Make it properly singular --- lib/tests/modules/deferred-module.nix | 2 +- lib/types.nix | 4 +++- .../development/option-types.section.md | 5 +++-- .../development/option-types.section.xml | 20 +++++++++++-------- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/tests/modules/deferred-module.nix b/lib/tests/modules/deferred-module.nix index e963c5a845c1..d03c60b029bf 100644 --- a/lib/tests/modules/deferred-module.nix +++ b/lib/tests/modules/deferred-module.nix @@ -12,7 +12,7 @@ in ({ config, ... }: { _file = "generic.nix"; options.nodes = mkOption { - type = lazyAttrsOf (submodule { imports = config.default; }); + type = lazyAttrsOf (submodule { imports = [ config.default ]; }); default = {}; }; options.default = mkOption { diff --git a/lib/types.nix b/lib/types.nix index 68dfa5843dee..354714b28733 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -549,7 +549,9 @@ rec { name = "deferredModule"; description = "module"; check = x: isAttrs x || isFunction x || path.check x; - merge = loc: defs: staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; + merge = loc: defs: { + imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; + }; inherit (submoduleWith { modules = staticModules; }) getSubOptions getSubModules; diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index 0241aae1dc89..e4e6cfec957f 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -227,8 +227,9 @@ Value types are types that take a value parameter. It can be set multiple times. - Module authors can use its value, which is always a list of module values, - in `imports` or in `submoduleWith`'s `modules` parameter. + Module authors can use its value in `imports`, in `submoduleWith`'s `modules` + or in `evalModules`' `modules` parameter, among other places. + Note that `imports` must be evaluated before the module fixpoint. Because of this, deferred modules can only be imported into "other" fixpoints, such as submodules. diff --git a/nixos/doc/manual/from_md/development/option-types.section.xml b/nixos/doc/manual/from_md/development/option-types.section.xml index 820646be671f..e3d161e4d351 100644 --- a/nixos/doc/manual/from_md/development/option-types.section.xml +++ b/nixos/doc/manual/from_md/development/option-types.section.xml @@ -441,14 +441,18 @@ It can be set multiple times. - Module authors can use its value, which is always a list of - module values, in imports or in - submoduleWith’s - modules parameter. Note that - imports must be evaluated before the - module fixpoint. Because of this, deferred modules can only - be imported into other fixpoints, such as - submodules. + Module authors can use its value in + imports, in + submoduleWiths + modules or in + evalModules + modules parameter, among other places. + + + Note that imports must be evaluated + before the module fixpoint. Because of this, deferred + modules can only be imported into other + fixpoints, such as submodules. One use case for this type is the type of a From 3c4a49f506575306f8b355e86d2b19cf02c84688 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 14 Jun 2022 23:12:46 +0200 Subject: [PATCH 7/8] lib/modules: Throw earlier when module function does not return attrs `m` must always be an attrset at this point. It is basically always evaluated. This will make it throw when any of the attrs is accessed, rather than just `config`. We assume that this will improve the error message in more scenarios. --- lib/modules.nix | 5 ++--- lib/tests/modules.sh | 1 + lib/tests/modules/deferred-module-error.nix | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 lib/tests/modules/deferred-module-error.nix diff --git a/lib/modules.nix b/lib/modules.nix index 1e8ba3471dd0..1e48f5440798 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -462,14 +462,13 @@ rec { config = addFreeformType (addMeta (m.config or {})); } else + lib.throwIfNot (isAttrs m) "module ${file} (${key}) does not look like a module." { _file = toString m._file or file; key = toString m.key or key; disabledModules = m.disabledModules or []; imports = m.require or [] ++ m.imports or []; options = {}; - config = - lib.throwIfNot (isAttrs m) "module ${file} (${key}) does not look like a module." - addFreeformType (addMeta (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"])); + config = addFreeformType (addMeta (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"])); }; applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 29f6272ea50a..c92cc62023b5 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -199,6 +199,7 @@ checkConfigOutput '^true$' config.submodule.enable ./declare-submoduleWith-path. checkConfigOutput '"beta"' config.nodes.foo.settingsDict.c ./deferred-module.nix # errors from the default module are reported with accurate location checkConfigError 'In `the-file-that-contains-the-bad-config.nix, via option default'\'': "bogus"' config.nodes.foo.bottom ./deferred-module.nix +checkConfigError '.*lib/tests/modules/deferred-module-error.nix, via option deferred [(]:anon-1:anon-1:anon-1[)] does not look like a module.' config.result ./deferred-module-error.nix # Check the file location information is propagated into submodules checkConfigOutput the-file.nix config.submodule.internalFiles.0 ./submoduleFiles.nix diff --git a/lib/tests/modules/deferred-module-error.nix b/lib/tests/modules/deferred-module-error.nix new file mode 100644 index 000000000000..d48ae092e8fe --- /dev/null +++ b/lib/tests/modules/deferred-module-error.nix @@ -0,0 +1,20 @@ +{ config, lib, ... }: +let + inherit (lib) types mkOption setDefaultModuleLocation evalModules; + inherit (types) deferredModule lazyAttrsOf submodule str raw enum; +in +{ + options = { + deferred = mkOption { + type = deferredModule; + }; + result = mkOption { + default = (evalModules { modules = [ config.deferred ]; }).config.result; + }; + }; + config = { + deferred = { ... }: + # this should be an attrset, so this fails + true; + }; +} From d9dccae07c638adb2d1eed6a722e01e9063bda97 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 14 Jun 2022 23:15:51 +0200 Subject: [PATCH 8/8] nixos/doc: Hold off on NixOps 2 info until released --- nixos/doc/manual/development/option-types.section.md | 3 +-- nixos/doc/manual/from_md/development/option-types.section.xml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index e4e6cfec957f..9b35e6630144 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -237,8 +237,7 @@ Value types are types that take a value parameter. One use case for this type is the type of a "default" module that allow the user to affect all submodules in an `attrsOf submodule` at once. This is more convenient and discoverable than expecting the module user to - type-merge with the `attrsOf submodule` option. NixOps uses this type in - `network.defaults`. + type-merge with the `attrsOf submodule` option. ## Composed Types {#sec-option-types-composed} diff --git a/nixos/doc/manual/from_md/development/option-types.section.xml b/nixos/doc/manual/from_md/development/option-types.section.xml index e3d161e4d351..929d5302ed41 100644 --- a/nixos/doc/manual/from_md/development/option-types.section.xml +++ b/nixos/doc/manual/from_md/development/option-types.section.xml @@ -460,8 +460,7 @@ all submodules in an attrsOf submodule at once. This is more convenient and discoverable than expecting the module user to type-merge with the - attrsOf submodule option. NixOps uses - this type in network.defaults. + attrsOf submodule option.