From e7e64233c9078f03de13a6d8750c1daaa673a0c2 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 7 Feb 2023 20:39:56 +0100 Subject: [PATCH 01/23] lib/tests/modules.sh: Unload implicit modules I had some trouble understanding this. Let's try to keep new tests a bit more stateless and explicit. --- lib/tests/modules.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index c1cf0a94a1b3..2af58ff5db9f 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -166,6 +166,7 @@ checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*' checkConfigOutput '^true$' "$@" ./define-module-check.nix # Check coerced value. +set -- checkConfigOutput '^"42"$' config.value ./declare-coerced-value.nix checkConfigOutput '^"24"$' config.value ./declare-coerced-value.nix ./define-value-string.nix checkConfigError 'A definition for option .* is not.*string or signed integer convertible to it.*. Definition values:\n\s*- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix From fe1527939038ac964df2028e6b90c35121db9955 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 13 Feb 2023 10:51:50 +0100 Subject: [PATCH 02/23] lib/modules.nix: Use explicit exports --- lib/modules.nix | 56 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 9c3e2085e378..de03b50e2698 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -63,10 +63,6 @@ let decls )); -in - -rec { - /* Evaluate a set of modules. The result is a set with the attributes: @@ -1218,4 +1214,56 @@ rec { _file = file; config = lib.importTOML file; }; + +in +{ + inherit + applyModuleArgsIfFunction + collectModules + defaultOrderPriority + defaultOverridePriority + defaultPriority + dischargeProperties + doRename + evalModules + evalOptionValue + filterOverrides + filterOverrides' + fixMergeModules + fixupOptionType + importJSON + importTOML + mergeDefinitions + mergeModules + mergeModules' + mergeOptionDecls + mkAfter + mkAliasAndWrapDefinitions + mkAliasAndWrapDefsWithPriority + mkAliasDefinitions + mkAliasIfDef + mkAliasOptionModule + mkAliasOptionModuleMD + mkAssert + mkBefore + mkChangedOptionModule + mkDefault + mkDerivedConfig + mkFixStrictness + mkForce + mkIf + mkImageMediaOverride + mkMerge + mkMergedOptionModule + mkOptionDefault + mkOrder + mkOverride + mkRemovedOptionModule + mkRenamedOptionModule + mkRenamedOptionModuleWith + mkVMOverride + pushDownProperties + setDefaultModuleLocation + sortProperties + unifyModuleSyntax; } From 3633bf98be70af326056ebc87a9adedd0e8a24c7 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 13 Feb 2023 11:09:35 +0100 Subject: [PATCH 03/23] lib/modules.nix: Make some functions private The supposedly public nature of these functions has been holding back module system maintenance, while usages of these functions are expected to be rare. If used anywhere, presumably they're emulating module system behavior because some use case isn't supported properly. We should try to support such a use case directly, if it even exists. --- lib/modules.nix | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index de03b50e2698..0a842aa0063e 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1215,28 +1215,41 @@ let config = lib.importTOML file; }; + private = lib.mapAttrs + (k: lib.warn "External use of `lib.modules.${k}` is deprecated. If your use case isn't covered by non-deprecated functions, we'd like to know more and perhaps support your use case well, instead of providing access to these low level functions. In this case please open an issue in https://github.com/nixos/nixpkgs/issues/.") + { + inherit + applyModuleArgsIfFunction + collectModules + dischargeProperties + evalOptionValue + mergeModules + mergeModules' + pushDownProperties + unifyModuleSyntax + ; + }; + in +private // { + # NOTE: not all of these functions are necessarily public interfaces; some + # are just needed by types.nix, but are not meant to be consumed + # externally. inherit - applyModuleArgsIfFunction - collectModules defaultOrderPriority defaultOverridePriority defaultPriority - dischargeProperties doRename evalModules - evalOptionValue filterOverrides filterOverrides' fixMergeModules - fixupOptionType + fixupOptionType # should be private? importJSON importTOML mergeDefinitions - mergeModules - mergeModules' - mergeOptionDecls + mergeOptionDecls # should be private? mkAfter mkAliasAndWrapDefinitions mkAliasAndWrapDefsWithPriority @@ -1262,8 +1275,6 @@ in mkRenamedOptionModule mkRenamedOptionModuleWith mkVMOverride - pushDownProperties setDefaultModuleLocation - sortProperties - unifyModuleSyntax; + sortProperties; } From b8ff2807a29861236a7ac3ed01c4565ba725e1b1 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 24 Oct 2022 14:30:19 +0200 Subject: [PATCH 04/23] lib/modules: Add class concept to check imports This improves the error message when an incompatible module is imported. --- lib/modules.nix | 26 ++++++++++++--- lib/tests/modules.sh | 5 +++ lib/tests/modules/class-check.nix | 34 ++++++++++++++++++++ lib/tests/modules/module-class-is-darwin.nix | 4 +++ lib/tests/modules/module-class-is-nixos.nix | 4 +++ 5 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 lib/tests/modules/class-check.nix create mode 100644 lib/tests/modules/module-class-is-darwin.nix create mode 100644 lib/tests/modules/module-class-is-nixos.nix diff --git a/lib/modules.nix b/lib/modules.nix index 0a842aa0063e..1e8f085e6f4f 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -105,6 +105,10 @@ let # when resolving module structure (like in imports). For everything else, # there's _module.args. If specialArgs.modulesPath is defined it will be # used as the base path for disabledModules. + # + # `specialArgs.class`: + # A nominal type for modules. When set and non-null, this adds a check to + # make sure that only compatible modules are imported. specialArgs ? {} , # This would be remove in the future, Prefer _module.args option instead. args ? {} @@ -256,6 +260,7 @@ let merged = let collected = collectModules + (specialArgs.class or null) (specialArgs.modulesPath or "") (regularModules ++ [ internalModule ]) ({ inherit lib options config specialArgs; } // specialArgs); @@ -349,11 +354,11 @@ let }; in result; - # collectModules :: (modulesPath: String) -> (modules: [ Module ]) -> (args: Attrs) -> [ Module ] + # collectModules :: (class: String) -> (modulesPath: String) -> (modules: [ Module ]) -> (args: Attrs) -> [ Module ] # # Collects all modules recursively through `import` statements, filtering out # all modules in disabledModules. - collectModules = let + collectModules = class: let # Like unifyModuleSyntax, but also imports paths and calls functions if necessary loadModule = args: fallbackFile: fallbackKey: m: @@ -364,6 +369,17 @@ let throw "Module imports can't be nested lists. Perhaps you meant to remove one level of lists? Definitions: ${showDefs defs}" else unifyModuleSyntax (toString m) (toString m) (applyModuleArgsIfFunction (toString m) (import m) args); + checkModule = + if class != null + then + m: + if m.class != null -> m.class == class + then m + else + throw "The module ${m._file or m.key} was imported into ${class} instead of ${m.class}." + else + m: m; + /* Collects all modules recursively into the form @@ -397,7 +413,7 @@ let }; in parentFile: parentKey: initialModules: args: collectResults (imap1 (n: x: let - module = loadModule args parentFile "${parentKey}:anon-${toString n}" x; + module = checkModule (loadModule args parentFile "${parentKey}:anon-${toString n}" x); collectedImports = collectStructuredModules module._file module.key module.imports args; in { key = module.key; @@ -461,7 +477,7 @@ let else config; in if m ? config || m ? options then - let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta" "freeformType"]; in + let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta" "freeformType" "class"]; 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 @@ -471,6 +487,7 @@ let imports = m.imports or []; options = m.options or {}; config = addFreeformType (addMeta (m.config or {})); + class = m.class or null; } else # shorthand syntax @@ -481,6 +498,7 @@ let imports = m.require or [] ++ m.imports or []; options = {}; config = addFreeformType (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"]); + class = m.class or null; }; applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 2af58ff5db9f..073dc6054860 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -360,6 +360,11 @@ checkConfigOutput 'ok' config.freeformItems.foo.bar ./adhoc-freeformType-survive # because of an `extendModules` bug, issue 168767. checkConfigOutput '^1$' config.sub.specialisation.value ./extendModules-168767-imports.nix +# Class checks +checkConfigOutput '^{ }$' config.ok.config ./class-check.nix +checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.fail.config ./class-check.nix +checkConfigError 'The module foo.nix#darwinModules.default was imported into nixos instead of darwin.' config.fail-anon.config ./class-check.nix + # doRename works when `warnings` does not exist. checkConfigOutput '^1234$' config.c.d.e ./doRename-basic.nix # doRename adds a warning. diff --git a/lib/tests/modules/class-check.nix b/lib/tests/modules/class-check.nix new file mode 100644 index 000000000000..6e02f8c30920 --- /dev/null +++ b/lib/tests/modules/class-check.nix @@ -0,0 +1,34 @@ +{ lib, ... }: { + config = { + _module.freeformType = lib.types.anything; + ok = + lib.evalModules { + specialArgs.class = "nixos"; + modules = [ + ./module-class-is-nixos.nix + ]; + }; + + fail = + lib.evalModules { + specialArgs.class = "nixos"; + modules = [ + ./module-class-is-nixos.nix + ./module-class-is-darwin.nix + ]; + }; + + fail-anon = + lib.evalModules { + specialArgs.class = "nixos"; + modules = [ + ./module-class-is-nixos.nix + { _file = "foo.nix#darwinModules.default"; + class = "darwin"; + imports = []; + } + ]; + }; + + }; +} diff --git a/lib/tests/modules/module-class-is-darwin.nix b/lib/tests/modules/module-class-is-darwin.nix new file mode 100644 index 000000000000..d8b60203f707 --- /dev/null +++ b/lib/tests/modules/module-class-is-darwin.nix @@ -0,0 +1,4 @@ +{ + class = "darwin"; + config = {}; +} diff --git a/lib/tests/modules/module-class-is-nixos.nix b/lib/tests/modules/module-class-is-nixos.nix new file mode 100644 index 000000000000..04b6e860e890 --- /dev/null +++ b/lib/tests/modules/module-class-is-nixos.nix @@ -0,0 +1,4 @@ +{ + class = "nixos"; + config = {}; +} From 58f385f68005a6fed7b526ee2c19fef11d87038c Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 7 Feb 2023 20:41:29 +0100 Subject: [PATCH 05/23] lib/modules: Check against importing things with a _type --- lib/modules.nix | 9 ++++++++- lib/tests/modules.sh | 4 ++++ lib/tests/modules/define-enable-with-top-level-mkIf.nix | 5 +++++ lib/tests/modules/module-imports-_type-check.nix | 3 +++ 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 lib/tests/modules/define-enable-with-top-level-mkIf.nix create mode 100644 lib/tests/modules/module-imports-_type-check.nix diff --git a/lib/modules.nix b/lib/modules.nix index 1e8f085e6f4f..8233b4b9e84e 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -362,8 +362,15 @@ let # Like unifyModuleSyntax, but also imports paths and calls functions if necessary loadModule = args: fallbackFile: fallbackKey: m: - if isFunction m || isAttrs m then + if isFunction m then unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgsIfFunction fallbackKey m args) + else if isAttrs m then + if m._type or "module" == "module" then + unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgsIfFunction fallbackKey m args) + else if m._type == "if" || m._type == "override" then + loadModule args fallbackFile fallbackKey { config = m; } + else + throw "Could not load a value as a module, because it is of type ${lib.strings.escapeNixString m._type}${lib.optionalString (fallbackFile != null) ", in file ${toString fallbackFile}."}" else if isList m then let defs = [{ file = fallbackFile; value = m; }]; in throw "Module imports can't be nested lists. Perhaps you meant to remove one level of lists? Definitions: ${showDefs defs}" diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 073dc6054860..4da0cb38f685 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -365,6 +365,10 @@ checkConfigOutput '^{ }$' config.ok.config ./class-check.nix checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.fail.config ./class-check.nix checkConfigError 'The module foo.nix#darwinModules.default was imported into nixos instead of darwin.' config.fail-anon.config ./class-check.nix +# _type check +checkConfigError 'Could not load a value as a module, because it is of type "flake", in file .*/module-imports-_type-check.nix' config.ok.config ./module-imports-_type-check.nix +checkConfigOutput '^true$' "$@" config.enable ./declare-enable.nix ./define-enable-with-top-level-mkIf.nix + # doRename works when `warnings` does not exist. checkConfigOutput '^1234$' config.c.d.e ./doRename-basic.nix # doRename adds a warning. diff --git a/lib/tests/modules/define-enable-with-top-level-mkIf.nix b/lib/tests/modules/define-enable-with-top-level-mkIf.nix new file mode 100644 index 000000000000..4909c16d82b4 --- /dev/null +++ b/lib/tests/modules/define-enable-with-top-level-mkIf.nix @@ -0,0 +1,5 @@ +{ lib, ... }: +# I think this might occur more realistically in a submodule +{ + imports = [ (lib.mkIf true { enable = true; }) ]; +} diff --git a/lib/tests/modules/module-imports-_type-check.nix b/lib/tests/modules/module-imports-_type-check.nix new file mode 100644 index 000000000000..1e29c469daa5 --- /dev/null +++ b/lib/tests/modules/module-imports-_type-check.nix @@ -0,0 +1,3 @@ +{ + imports = [ { _type = "flake"; } ]; +} From 2e689d58cbbe6b7047bb132dc79097016e606dd0 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 7 Feb 2023 20:51:17 +0100 Subject: [PATCH 06/23] lib/modules: Improve error when a configuration is imported This is appears to be a fairly common mistake for beginners who want to build larger things from the system configurations, such as NixOps networks, etc. Further explanation seems appropriate. --- lib/modules.nix | 1 + lib/tests/modules.sh | 1 + lib/tests/modules/import-configuration.nix | 12 ++++++++++++ 3 files changed, 14 insertions(+) create mode 100644 lib/tests/modules/import-configuration.nix diff --git a/lib/modules.nix b/lib/modules.nix index 8233b4b9e84e..3bcd8d280b74 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -347,6 +347,7 @@ let }; result = withWarnings { + _type = "configuration"; options = checked options; config = checked (removeAttrs config [ "_module" ]); _module = checked (config._module); diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 4da0cb38f685..d12e503c4b1d 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -368,6 +368,7 @@ checkConfigError 'The module foo.nix#darwinModules.default was imported into nix # _type check checkConfigError 'Could not load a value as a module, because it is of type "flake", in file .*/module-imports-_type-check.nix' config.ok.config ./module-imports-_type-check.nix checkConfigOutput '^true$' "$@" config.enable ./declare-enable.nix ./define-enable-with-top-level-mkIf.nix +checkConfigError 'Could not load a value as a module, because it is of type "configuration", in file .*/import-configuration.nix' config ./import-configuration.nix # doRename works when `warnings` does not exist. checkConfigOutput '^1234$' config.c.d.e ./doRename-basic.nix diff --git a/lib/tests/modules/import-configuration.nix b/lib/tests/modules/import-configuration.nix new file mode 100644 index 000000000000..a2a32bbee4ca --- /dev/null +++ b/lib/tests/modules/import-configuration.nix @@ -0,0 +1,12 @@ +{ lib, ... }: +let + myconf = lib.evalModules { modules = [ { } ]; }; +in +{ + imports = [ + # We can't do this. A configuration is not equal to its set of a modules. + # Equating those would lead to a mess, as specialArgs, anonymous modules + # that can't be deduplicated, and possibly more come into play. + myconf + ]; +} From 9714487f743d0ac9aec50a5513cb89a97932d4a6 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 7 Feb 2023 21:13:06 +0100 Subject: [PATCH 07/23] lib/modules: Explain that a configuration can't be loaded as a module --- lib/modules.nix | 7 ++++++- lib/tests/modules.sh | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 3bcd8d280b74..cb8f5cd198e6 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -371,7 +371,12 @@ let else if m._type == "if" || m._type == "override" then loadModule args fallbackFile fallbackKey { config = m; } else - throw "Could not load a value as a module, because it is of type ${lib.strings.escapeNixString m._type}${lib.optionalString (fallbackFile != null) ", in file ${toString fallbackFile}."}" + throw ( + "Could not load a value as a module, because it is of type ${lib.strings.escapeNixString m._type}" + + lib.optionalString (fallbackFile != unknownModule) ", in file ${toString fallbackFile}." + + lib.optionalString (m._type == "configuration") " If you do intend to import this configuration, please only import the modules that make up the configuration. You may have to create a `let` binding, file or attribute to give yourself access to the relevant modules.\nWhile loading a configuration into the module system is a very sensible idea, it can not be done cleanly in practice." + # Extended explanation: That's because a finalized configuration is more than just a set of modules. For instance, it has its own `specialArgs` that, by the nature of `specialArgs` can't be loaded through `imports` or the the `modules` argument. So instead, we have to ask you to extract the relevant modules and use those instead. This way, we keep the module system comparatively simple, and hopefully avoid a bad surprise down the line. + ) else if isList m then let defs = [{ file = fallbackFile; value = m; }]; in throw "Module imports can't be nested lists. Perhaps you meant to remove one level of lists? Definitions: ${showDefs defs}" diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index d12e503c4b1d..c2a8e566cb8c 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -368,7 +368,7 @@ checkConfigError 'The module foo.nix#darwinModules.default was imported into nix # _type check checkConfigError 'Could not load a value as a module, because it is of type "flake", in file .*/module-imports-_type-check.nix' config.ok.config ./module-imports-_type-check.nix checkConfigOutput '^true$' "$@" config.enable ./declare-enable.nix ./define-enable-with-top-level-mkIf.nix -checkConfigError 'Could not load a value as a module, because it is of type "configuration", in file .*/import-configuration.nix' config ./import-configuration.nix +checkConfigError 'Could not load a value as a module, because it is of type "configuration", in file .*/import-configuration.nix.*please only import the modules that make up the configuration.*' config ./import-configuration.nix # doRename works when `warnings` does not exist. checkConfigOutput '^1234$' config.c.d.e ./doRename-basic.nix From 439f6790bd6e133c125935a8a2007227f671a360 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 13 Feb 2023 11:12:46 +0100 Subject: [PATCH 08/23] lib/modules.nix: Restore old collectModules interface --- lib/modules.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules.nix b/lib/modules.nix index cb8f5cd198e6..44155913e564 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1251,7 +1251,6 @@ let { inherit applyModuleArgsIfFunction - collectModules dischargeProperties evalOptionValue mergeModules @@ -1259,6 +1258,7 @@ let pushDownProperties unifyModuleSyntax ; + collectModules = collectModules null; }; in From 06ca78663c912eb62075ad4aea1f24c7f35cb0c3 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 31 Mar 2023 11:18:43 +0200 Subject: [PATCH 09/23] lib/modules.nix: Refactor: evaluate applyModuleArgsIfFunction in attrs case --- lib/modules.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules.nix b/lib/modules.nix index 44155913e564..3b64afddaf5c 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -367,7 +367,7 @@ let unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgsIfFunction fallbackKey m args) else if isAttrs m then if m._type or "module" == "module" then - unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgsIfFunction fallbackKey m args) + unifyModuleSyntax fallbackFile fallbackKey m else if m._type == "if" || m._type == "override" then loadModule args fallbackFile fallbackKey { config = m; } else From 1f4a58ef038184eaf4757e96ec1f09b08a01c8ab Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 31 Mar 2023 11:25:44 +0200 Subject: [PATCH 10/23] lib/modules.nix: Refactor: extract applyModuleArgs --- lib/modules.nix | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 3b64afddaf5c..9377c2e1e9dc 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -364,7 +364,7 @@ let # Like unifyModuleSyntax, but also imports paths and calls functions if necessary loadModule = args: fallbackFile: fallbackKey: m: if isFunction m then - unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgsIfFunction fallbackKey m args) + unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgs fallbackKey m args) else if isAttrs m then if m._type or "module" == "module" then unifyModuleSyntax fallbackFile fallbackKey m @@ -514,7 +514,10 @@ let class = m.class or null; }; - applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then + applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: + if isFunction f then applyModuleArgs key f args else f; + + applyModuleArgs = key: f: args@{ config, options, lib, ... }: let # Module arguments are resolved in a strict manner when attribute set # deconstruction is used. As the arguments are now defined with the @@ -538,9 +541,7 @@ let # context on the explicit arguments of "args" too. This update # operator is used to make the "args@{ ... }: with args.lib;" notation # works. - in f (args // extraArgs) - else - f; + in f (args // extraArgs); /* Merge a list of modules. This will recurse over the option declarations in all modules, combining them into a single set. From 84b1b017026bb1d0a37a8d3ef553f073225b4e8d Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 10 Apr 2023 17:26:25 +0200 Subject: [PATCH 11/23] lib/modules: Only interpret class declaration in non-shorthand mode This is to avoid stealing keys from submodules. `class` might be common enough that reinterpreting existing `class` attributes in configurations as a declaration leads to fairly widespread problems. --- lib/modules.nix | 2 +- lib/tests/modules.sh | 2 ++ lib/tests/modules/class-check.nix | 1 + .../define-freeform-keywords-shorthand.nix | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 lib/tests/modules/define-freeform-keywords-shorthand.nix diff --git a/lib/modules.nix b/lib/modules.nix index 9377c2e1e9dc..4533252c60e2 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -511,7 +511,7 @@ let imports = m.require or [] ++ m.imports or []; options = {}; config = addFreeformType (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"]); - class = m.class or null; + class = null; }; applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index c2a8e566cb8c..116a0778aebc 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -255,6 +255,8 @@ checkConfigError 'A definition for option .* is not of type .*' \ ## Freeform modules # Assigning without a declared option should work checkConfigOutput '^"24"$' config.value ./freeform-attrsOf.nix ./define-value-string.nix +# Shorthand modules interpret `meta` and `class` as config items +checkConfigOutput '^true$' options._module.args.value.result ./freeform-attrsOf.nix ./define-freeform-keywords-shorthand.nix # No freeform assignments shouldn't make it error checkConfigOutput '^{ }$' config ./freeform-attrsOf.nix # but only if the type matches diff --git a/lib/tests/modules/class-check.nix b/lib/tests/modules/class-check.nix index 6e02f8c30920..f492c844abfb 100644 --- a/lib/tests/modules/class-check.nix +++ b/lib/tests/modules/class-check.nix @@ -25,6 +25,7 @@ ./module-class-is-nixos.nix { _file = "foo.nix#darwinModules.default"; class = "darwin"; + config = {}; imports = []; } ]; diff --git a/lib/tests/modules/define-freeform-keywords-shorthand.nix b/lib/tests/modules/define-freeform-keywords-shorthand.nix new file mode 100644 index 000000000000..8de1ec6a7475 --- /dev/null +++ b/lib/tests/modules/define-freeform-keywords-shorthand.nix @@ -0,0 +1,15 @@ +{ config, ... }: { + class = { "just" = "data"; }; + a = "one"; + b = "two"; + meta = "meta"; + + _module.args.result = + let r = builtins.removeAttrs config [ "_module" ]; + in builtins.trace (builtins.deepSeq r r) (r == { + a = "one"; + b = "two"; + class = { "just" = "data"; }; + meta = "meta"; + }); +} From 79703eef083d70046873dbb86f08e2ff08f58197 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 10 Apr 2023 17:55:34 +0200 Subject: [PATCH 12/23] nixos,nixpkgs: Add module classes This allows modules that declare their class to be checked. While that's not most user modules, frameworks can take advantage of this by setting declaring the module class for their users. That way, the mistake of importing a module into the wrong hierarchy can be reported more clearly in some cases. --- doc/doc-support/default.nix | 5 ++++- nixos/lib/eval-cacheable-options.nix | 1 + nixos/lib/eval-config-minimal.nix | 4 +++- nixos/lib/testing/default.nix | 5 ++++- nixos/modules/misc/documentation.nix | 1 + pkgs/top-level/default.nix | 1 + 6 files changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/doc-support/default.nix b/doc/doc-support/default.nix index bea3e12a70b3..67195a4a58b0 100644 --- a/doc/doc-support/default.nix +++ b/doc/doc-support/default.nix @@ -45,7 +45,10 @@ let # NB: This file describes the Nixpkgs manual, which happens to use module # docs infra originally developed for NixOS. optionsDoc = pkgs.nixosOptionsDoc { - inherit (pkgs.lib.evalModules { modules = [ ../../pkgs/top-level/config.nix ]; }) options; + inherit (pkgs.lib.evalModules { + modules = [ ../../pkgs/top-level/config.nix ]; + specialArgs.class = "nixpkgsConfig"; + }) options; documentType = "none"; transformOptions = opt: opt // { diff --git a/nixos/lib/eval-cacheable-options.nix b/nixos/lib/eval-cacheable-options.nix index c3ba2ce66375..d26967ebe09b 100644 --- a/nixos/lib/eval-cacheable-options.nix +++ b/nixos/lib/eval-cacheable-options.nix @@ -33,6 +33,7 @@ let ]; specialArgs = { inherit config pkgs utils; + class = "nixos"; }; }; docs = import "${nixosPath}/doc/manual" { diff --git a/nixos/lib/eval-config-minimal.nix b/nixos/lib/eval-config-minimal.nix index d45b9ffd4261..7e28f4305127 100644 --- a/nixos/lib/eval-config-minimal.nix +++ b/nixos/lib/eval-config-minimal.nix @@ -40,7 +40,9 @@ let inherit prefix modules; specialArgs = { modulesPath = builtins.toString ../modules; - } // specialArgs; + } // specialArgs // { + class = "nixos"; + }; }; in diff --git a/nixos/lib/testing/default.nix b/nixos/lib/testing/default.nix index 9d4f9dbc43d7..1bd6278ce684 100644 --- a/nixos/lib/testing/default.nix +++ b/nixos/lib/testing/default.nix @@ -1,7 +1,10 @@ { lib }: let - evalTest = module: lib.evalModules { modules = testModules ++ [ module ]; }; + evalTest = module: lib.evalModules { + modules = testModules ++ [ module ]; + specialArgs.class = "nixosTest"; + }; runTest = module: (evalTest ({ config, ... }: { imports = [ module ]; result = config.test; })).config.result; testModules = [ diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix index e0c6af4abe10..1821dd866cb1 100644 --- a/nixos/modules/misc/documentation.nix +++ b/nixos/modules/misc/documentation.nix @@ -39,6 +39,7 @@ let _module.check = false; } ] ++ docModules.eager; specialArgs = specialArgs // { + class = "nixos"; pkgs = scrubDerivations "pkgs" pkgs; # allow access to arbitrary options for eager modules, eg for getting # option types from lazy modules diff --git a/pkgs/top-level/default.nix b/pkgs/top-level/default.nix index b32aabc3a1fd..f54ce84aedad 100644 --- a/pkgs/top-level/default.nix +++ b/pkgs/top-level/default.nix @@ -82,6 +82,7 @@ in let config = config1; }) ]; + specialArgs.class = "nixpkgsConfig"; }; # take all the rest as-is From ee1e14be0c8789745986698950e32f5946a7d272 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 10 Apr 2023 18:45:16 +0200 Subject: [PATCH 13/23] doc: Add Module System chapter start --- doc/manual.xml | 4 ++ doc/module-system/module-system.chapter.md | 69 ++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 doc/module-system/module-system.chapter.md diff --git a/doc/manual.xml b/doc/manual.xml index 8aca36017684..de3d40f553c0 100644 --- a/doc/manual.xml +++ b/doc/manual.xml @@ -12,7 +12,11 @@ + + + Nixpkgs <code>lib</code> + Standard environment diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md new file mode 100644 index 000000000000..34214a85148a --- /dev/null +++ b/doc/module-system/module-system.chapter.md @@ -0,0 +1,69 @@ +# Module System {#module-system} + +## Introduction {#module-system-introduction} + +The module system is a language for handling configuration, implemented as a Nix library. + +Compared to plain Nix, it adds documentation, type checking and composition or extensibility. + +NOTE: This chapter is new and not complete yet. For a gentle introduction to the module system, in the context of NixOS, see [Writing NixOS Modules](https://nixos.org/manual/nixos/unstable/index.html#sec-writing-modules) in the NixOS manual. + +## `lib.evalModules` {#module-system-lib-evalModules} + +Evaluate a set of modules. The result is a set with the attributes: + +### Parameters {#module-system-lib-evalModules-parameters} + +#### `modules` {#module-system-lib-evalModules-param-modules} + +A list of modules. These are merged together using various methods to form the final configuration. + +#### `specialArgs` {#module-system-lib-evalModules-param-specialArgs} + +An attribute set of module arguments that can be used in `imports`. + +This is in contrast to `config._module.args`, which is only available within the module fixpoint, which does not exist before all imports are resolved. + +#### `specialArgs.class` {#module-system-lib-evalModules-param-specialArgs-class} + +If the `class` attribute is set in `specialArgs`, the module system will rejected modules with a different `class`. + +This improves the error message that users will encounter when they import an incompatible module that was designed for a different class of configurations. + +The `class` value should be in camelcase, and, if applicable, it should match the prefix of the attributes used in (experimental) flakes. Some examples are: + + - `nixos`: NixOS modules + - `nixosTest`: modules that constitute a [NixOS VM test](https://nixos.org/manual/nixos/stable/index.html#sec-nixos-tests) + +#### `prefix` {#module-system-lib-evalModules-param-prefix} + +A list of strings representing the location at or below which all options are evaluated. This is used by `types.submodule` to improve error reporting and find the implicit `name` module argument. + +### Return value {#module-system-lib-evalModules-return-value} + +#### `options` {#module-system-lib-evalModules-return-value-options} + +The nested set of all option declarations. + +#### `config` {#module-system-lib-evalModules-return-value-config} + +The nested set of all option values. + +#### `type` {#module-system-lib-evalModules-return-value-type} + +A module system type representing the module set as a submodule, to be extended by configuration from the containing module set. + +This is also available as the module argument `moduleType`. + +#### `extendModules` {#module-system-lib-evalModules-return-value-extendModules} + +A function similar to `evalModules` but building on top of the module set. Its arguments, `modules` and `specialArgs` are added to the existing values. + +Using `extendModules` a few times has no performance impact as long as you only reference the final `options` and `config`. +If you do reference multiple `config` (or `options`) from before and after `extendModules`, performance is the same as with multiple `evalModules` invocations, because the new modules' ability to override existing configuration fundamentally requires a new fixpoint to be constructed. + +This is also available as a module argument. + +#### `_module` {#module-system-lib-evalModules-return-value-_module} + +A portion of the configuration tree which is elided from `config`. It contains some values that are mostly internal to the module system implementation. From 03a465f0489ce35c86606dd5ef7dffa877c91dde Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 6 May 2023 18:32:12 +0200 Subject: [PATCH 14/23] fixup! doc: Add Module System chapter start --- doc/module-system/module-system.chapter.md | 46 ++++++++++++++-------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md index 34214a85148a..2f19096577fd 100644 --- a/doc/module-system/module-system.chapter.md +++ b/doc/module-system/module-system.chapter.md @@ -6,34 +6,39 @@ The module system is a language for handling configuration, implemented as a Nix Compared to plain Nix, it adds documentation, type checking and composition or extensibility. -NOTE: This chapter is new and not complete yet. For a gentle introduction to the module system, in the context of NixOS, see [Writing NixOS Modules](https://nixos.org/manual/nixos/unstable/index.html#sec-writing-modules) in the NixOS manual. +::: {.note} +This chapter is new and not complete yet. For a gentle introduction to the module system, in the context of NixOS, see [Writing NixOS Modules](https://nixos.org/manual/nixos/unstable/index.html#sec-writing-modules) in the NixOS manual. +::: + ## `lib.evalModules` {#module-system-lib-evalModules} -Evaluate a set of modules. The result is a set with the attributes: +Evaluate a set of modules. This function is typically only used once per application (e.g. once in NixOS, once in Home Manager, ...). ### Parameters {#module-system-lib-evalModules-parameters} #### `modules` {#module-system-lib-evalModules-param-modules} -A list of modules. These are merged together using various methods to form the final configuration. +A list of modules. These are merged together to form the final configuration. + #### `specialArgs` {#module-system-lib-evalModules-param-specialArgs} An attribute set of module arguments that can be used in `imports`. -This is in contrast to `config._module.args`, which is only available within the module fixpoint, which does not exist before all imports are resolved. +This is in contrast to `config._module.args`, which is only available after all `imports` have been resolved. #### `specialArgs.class` {#module-system-lib-evalModules-param-specialArgs-class} -If the `class` attribute is set in `specialArgs`, the module system will rejected modules with a different `class`. +If the `class` attribute is set in `specialArgs`, the module system will reject modules with a different `class`. -This improves the error message that users will encounter when they import an incompatible module that was designed for a different class of configurations. +The `class` value should be in lower [camel case](https://en.wikipedia.org/wiki/Camel_case). -The `class` value should be in camelcase, and, if applicable, it should match the prefix of the attributes used in (experimental) flakes. Some examples are: +If applicable, the `class` should match the "prefix" of the attributes used in (experimental) [flakes](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html#description). Some examples are: - - `nixos`: NixOS modules + - `nixos` as in `flake.nixosModules` - `nixosTest`: modules that constitute a [NixOS VM test](https://nixos.org/manual/nixos/stable/index.html#sec-nixos-tests) + #### `prefix` {#module-system-lib-evalModules-param-prefix} @@ -41,29 +46,38 @@ A list of strings representing the location at or below which all options are ev ### Return value {#module-system-lib-evalModules-return-value} +The result is an attribute set with the following attributes: + #### `options` {#module-system-lib-evalModules-return-value-options} -The nested set of all option declarations. +The nested attribute set of all option declarations. #### `config` {#module-system-lib-evalModules-return-value-config} -The nested set of all option values. +The nested attribute set of all option values. #### `type` {#module-system-lib-evalModules-return-value-type} -A module system type representing the module set as a submodule, to be extended by configuration from the containing module set. +A module system type. This type is an instance of `types.submoduleWith` containing the current [`modules`](#module-system-lib-evalModules-param-modules). -This is also available as the module argument `moduleType`. +The option definitions that are typed with this type will extend the current set of modules, like [`extendModules`](#module-system-lib-evalModules-return-value-extendModules). + +However, the value returned from the type is just the [`config`](#module-system-lib-evalModules-return-value-config), like any submodule. + +This type is also available to the [`modules`](#module-system-lib-evalModules-param-modules) as the module argument `moduleType`. #### `extendModules` {#module-system-lib-evalModules-return-value-extendModules} -A function similar to `evalModules` but building on top of the module set. Its arguments, `modules` and `specialArgs` are added to the existing values. +A function similar to `evalModules` but building on top of the already passed [`modules`](#module-system-lib-evalModules-param-modules). Its arguments, `modules` and `specialArgs` are added to the existing values. -Using `extendModules` a few times has no performance impact as long as you only reference the final `options` and `config`. -If you do reference multiple `config` (or `options`) from before and after `extendModules`, performance is the same as with multiple `evalModules` invocations, because the new modules' ability to override existing configuration fundamentally requires a new fixpoint to be constructed. +The real work of module evaluation happens while computing the values in `config` and `options`, so multiple invocations of `extendModules` have a particularly small cost, as long as only the final `config` and `options` are evaluated. -This is also available as a module argument. +If you do reference multiple `config` (or `options`) from before and after `extendModules`, evaluation performance is the same as with multiple `evalModules` invocations, because the new modules' ability to override existing configuration fundamentally requires constructing a new `config` and `options` fixpoint. + +This functionality is also available to modules as the `extendModules` module argument. #### `_module` {#module-system-lib-evalModules-return-value-_module} A portion of the configuration tree which is elided from `config`. It contains some values that are mostly internal to the module system implementation. + + From 73f584c3cc20015bfd1c4c72ebc6240897c10e48 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 16 Apr 2023 19:03:05 +0200 Subject: [PATCH 15/23] lib/modules.nix: Deduplicate documentation `file://./..` looks redundant, but makes the url clickable in vscode. --- lib/modules.nix | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 4533252c60e2..e83d5b6d1cae 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -63,35 +63,8 @@ let decls )); - /* - Evaluate a set of modules. The result is a set with the attributes: - - ‘options’: The nested set of all option declarations, - - ‘config’: The nested set of all option values. - - ‘type’: A module system type representing the module set as a submodule, - to be extended by configuration from the containing module set. - - This is also available as the module argument ‘moduleType’. - - ‘extendModules’: A function similar to ‘evalModules’ but building on top - of the module set. Its arguments, ‘modules’ and ‘specialArgs’ are - added to the existing values. - - Using ‘extendModules’ a few times has no performance impact as long - as you only reference the final ‘options’ and ‘config’. - If you do reference multiple ‘config’ (or ‘options’) from before and - after ‘extendModules’, performance is the same as with multiple - ‘evalModules’ invocations, because the new modules' ability to - override existing configuration fundamentally requires a new - fixpoint to be constructed. - - This is also available as a module argument. - - ‘_module’: A portion of the configuration tree which is elided from - ‘config’. It contains some values that are mostly internal to the - module system implementation. + /* See https://nixos.org/manual/nixpkgs/unstable/#module-system-lib-evalModules + or file://./../doc/module-system/module-system.chapter.md !!! Please think twice before adding to this argument list! The more that is specified here instead of in the modules themselves the harder From 5fac39307dc75586d93e69920c4b837e277f0c85 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 16 Apr 2023 19:25:09 +0200 Subject: [PATCH 16/23] module-system.chapter.md: Add mental model to `type` and `extendModules` --- doc/module-system/module-system.chapter.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md index 2f19096577fd..2f9716c89dbc 100644 --- a/doc/module-system/module-system.chapter.md +++ b/doc/module-system/module-system.chapter.md @@ -64,12 +64,17 @@ The option definitions that are typed with this type will extend the current set However, the value returned from the type is just the [`config`](#module-system-lib-evalModules-return-value-config), like any submodule. +If you're familiar with prototype inheritance, you can think of this `evalModules` invocation as the prototype, and usages of this type as the instances. + This type is also available to the [`modules`](#module-system-lib-evalModules-param-modules) as the module argument `moduleType`. + #### `extendModules` {#module-system-lib-evalModules-return-value-extendModules} A function similar to `evalModules` but building on top of the already passed [`modules`](#module-system-lib-evalModules-param-modules). Its arguments, `modules` and `specialArgs` are added to the existing values. +If you're familiar with prototype inheritance, you can think of the current, actual `evalModules` invocation as the prototype, and the return value of `extendModules` as the instance. + The real work of module evaluation happens while computing the values in `config` and `options`, so multiple invocations of `extendModules` have a particularly small cost, as long as only the final `config` and `options` are evaluated. If you do reference multiple `config` (or `options`) from before and after `extendModules`, evaluation performance is the same as with multiple `evalModules` invocations, because the new modules' ability to override existing configuration fundamentally requires constructing a new `config` and `options` fixpoint. From 8f02e95aff2b9cb94470ec379e2a3f55858cb03d Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 16 Apr 2023 19:44:18 +0200 Subject: [PATCH 17/23] module-system.chapter.md: Elaborate on extendModules performance --- doc/module-system/module-system.chapter.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md index 2f9716c89dbc..9a24ab70c6c1 100644 --- a/doc/module-system/module-system.chapter.md +++ b/doc/module-system/module-system.chapter.md @@ -75,11 +75,20 @@ A function similar to `evalModules` but building on top of the already passed [` If you're familiar with prototype inheritance, you can think of the current, actual `evalModules` invocation as the prototype, and the return value of `extendModules` as the instance. +This functionality is also available to modules as the `extendModules` module argument. + +::: {.note} + +**Evaluation Performance** + +`extendModules` returns a configuration that shares very little with the original `evalModules` invocation, because the module arguments may be different. + +So if you have a configuration that has been (or will be) largely evaluated, almost none of the computation is shared with the configuration returned by `extendModules`. + The real work of module evaluation happens while computing the values in `config` and `options`, so multiple invocations of `extendModules` have a particularly small cost, as long as only the final `config` and `options` are evaluated. If you do reference multiple `config` (or `options`) from before and after `extendModules`, evaluation performance is the same as with multiple `evalModules` invocations, because the new modules' ability to override existing configuration fundamentally requires constructing a new `config` and `options` fixpoint. - -This functionality is also available to modules as the `extendModules` module argument. +::: #### `_module` {#module-system-lib-evalModules-return-value-_module} From 8054785157119ea12e526481924d6676427904bb Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 17 Apr 2023 19:48:53 +0200 Subject: [PATCH 18/23] lib/modules: Move class out of specialArgs --- doc/doc-support/default.nix | 2 +- doc/module-system/module-system.chapter.md | 6 ++--- lib/modules.nix | 26 ++++++++++++++++------ lib/tests/modules/class-check.nix | 6 ++--- lib/types.nix | 10 +++++++-- nixos/lib/eval-config-minimal.nix | 5 ++--- nixos/lib/testing/default.nix | 2 +- nixos/modules/misc/documentation.nix | 2 +- pkgs/top-level/default.nix | 2 +- 9 files changed, 39 insertions(+), 22 deletions(-) diff --git a/doc/doc-support/default.nix b/doc/doc-support/default.nix index 67195a4a58b0..cfa7cbdc8283 100644 --- a/doc/doc-support/default.nix +++ b/doc/doc-support/default.nix @@ -47,7 +47,7 @@ let optionsDoc = pkgs.nixosOptionsDoc { inherit (pkgs.lib.evalModules { modules = [ ../../pkgs/top-level/config.nix ]; - specialArgs.class = "nixpkgsConfig"; + class = "nixpkgsConfig"; }) options; documentType = "none"; transformOptions = opt: diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md index 9a24ab70c6c1..51e600d75640 100644 --- a/doc/module-system/module-system.chapter.md +++ b/doc/module-system/module-system.chapter.md @@ -28,11 +28,11 @@ An attribute set of module arguments that can be used in `imports`. This is in contrast to `config._module.args`, which is only available after all `imports` have been resolved. -#### `specialArgs.class` {#module-system-lib-evalModules-param-specialArgs-class} +#### `class` {#module-system-lib-evalModules-param-class} -If the `class` attribute is set in `specialArgs`, the module system will reject modules with a different `class`. +If the `class` attribute is set and non-`null`, the module system will reject `imports` with a different `class`. -The `class` value should be in lower [camel case](https://en.wikipedia.org/wiki/Camel_case). +The `class` value should be a string in lower [camel case](https://en.wikipedia.org/wiki/Camel_case). If applicable, the `class` should match the "prefix" of the attributes used in (experimental) [flakes](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html#description). Some examples are: diff --git a/lib/modules.nix b/lib/modules.nix index e83d5b6d1cae..3550ebddaeac 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -78,13 +78,13 @@ let # when resolving module structure (like in imports). For everything else, # there's _module.args. If specialArgs.modulesPath is defined it will be # used as the base path for disabledModules. - # - # `specialArgs.class`: + specialArgs ? {} + , # `class`: # A nominal type for modules. When set and non-null, this adds a check to # make sure that only compatible modules are imported. - specialArgs ? {} - , # This would be remove in the future, Prefer _module.args option instead. - args ? {} + # This would be remove in the future, Prefer _module.args option instead. + class ? null + , args ? {} , # This would be remove in the future, Prefer _module.check option instead. check ? true }: @@ -220,6 +220,16 @@ let within a configuration, but can be used in module imports. ''; }; + + _module.class = mkOption { + readOnly = true; + internal = true; + description = lib.mdDoc '' + If the `class` attribute is set and non-`null`, the module system will reject `imports` with a different `class`. + + This option contains the expected `class` attribute of the current module evaluation. + ''; + }; }; config = { @@ -227,13 +237,14 @@ let inherit extendModules; moduleType = type; }; + _module.class = class; _module.specialArgs = specialArgs; }; }; merged = let collected = collectModules - (specialArgs.class or null) + class (specialArgs.modulesPath or "") (regularModules ++ [ internalModule ]) ({ inherit lib options config specialArgs; } // specialArgs); @@ -310,13 +321,14 @@ let prefix ? [], }: evalModules (evalModulesArgs // { + inherit class; modules = regularModules ++ modules; specialArgs = evalModulesArgs.specialArgs or {} // specialArgs; prefix = extendArgs.prefix or evalModulesArgs.prefix or []; }); type = lib.types.submoduleWith { - inherit modules specialArgs; + inherit modules specialArgs class; }; result = withWarnings { diff --git a/lib/tests/modules/class-check.nix b/lib/tests/modules/class-check.nix index f492c844abfb..02d1431cc88b 100644 --- a/lib/tests/modules/class-check.nix +++ b/lib/tests/modules/class-check.nix @@ -3,7 +3,7 @@ _module.freeformType = lib.types.anything; ok = lib.evalModules { - specialArgs.class = "nixos"; + class = "nixos"; modules = [ ./module-class-is-nixos.nix ]; @@ -11,7 +11,7 @@ fail = lib.evalModules { - specialArgs.class = "nixos"; + class = "nixos"; modules = [ ./module-class-is-nixos.nix ./module-class-is-darwin.nix @@ -20,7 +20,7 @@ fail-anon = lib.evalModules { - specialArgs.class = "nixos"; + class = "nixos"; modules = [ ./module-class-is-nixos.nix { _file = "foo.nix#darwinModules.default"; diff --git a/lib/types.nix b/lib/types.nix index 666e6502d161..e0da18a2febb 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -696,6 +696,7 @@ rec { , specialArgs ? {} , shorthandOnlyDefinesConfig ? false , description ? null + , class ? null }@attrs: let inherit (lib.modules) evalModules; @@ -707,7 +708,7 @@ rec { ) defs; base = evalModules { - inherit specialArgs; + inherit class specialArgs; modules = [{ # This is a work-around for the fact that some sub-modules, # such as the one included in an attribute set, expects an "args" @@ -762,9 +763,14 @@ rec { functor = defaultFunctor name // { type = types.submoduleWith; payload = { - inherit modules specialArgs shorthandOnlyDefinesConfig description; + inherit modules class specialArgs shorthandOnlyDefinesConfig description; }; binOp = lhs: rhs: { + class = + if lhs.class == null then rhs.class + else if rhs.class == null then lhs.class + else if lhs.class == rhs.class then lhs.class + else throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\"."; modules = lhs.modules ++ rhs.modules; specialArgs = let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs; diff --git a/nixos/lib/eval-config-minimal.nix b/nixos/lib/eval-config-minimal.nix index 7e28f4305127..036389121973 100644 --- a/nixos/lib/eval-config-minimal.nix +++ b/nixos/lib/eval-config-minimal.nix @@ -38,11 +38,10 @@ let # is experimental. lib.evalModules { inherit prefix modules; + class = "nixos"; specialArgs = { modulesPath = builtins.toString ../modules; - } // specialArgs // { - class = "nixos"; - }; + } // specialArgs; }; in diff --git a/nixos/lib/testing/default.nix b/nixos/lib/testing/default.nix index 1bd6278ce684..a89f734b1e64 100644 --- a/nixos/lib/testing/default.nix +++ b/nixos/lib/testing/default.nix @@ -3,7 +3,7 @@ let evalTest = module: lib.evalModules { modules = testModules ++ [ module ]; - specialArgs.class = "nixosTest"; + class = "nixosTest"; }; runTest = module: (evalTest ({ config, ... }: { imports = [ module ]; result = config.test; })).config.result; diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix index 1821dd866cb1..31486a2216ad 100644 --- a/nixos/modules/misc/documentation.nix +++ b/nixos/modules/misc/documentation.nix @@ -38,8 +38,8 @@ let modules = [ { _module.check = false; } ] ++ docModules.eager; + class = "nixos"; specialArgs = specialArgs // { - class = "nixos"; pkgs = scrubDerivations "pkgs" pkgs; # allow access to arbitrary options for eager modules, eg for getting # option types from lazy modules diff --git a/pkgs/top-level/default.nix b/pkgs/top-level/default.nix index f54ce84aedad..ba00e78ce2e6 100644 --- a/pkgs/top-level/default.nix +++ b/pkgs/top-level/default.nix @@ -82,7 +82,7 @@ in let config = config1; }) ]; - specialArgs.class = "nixpkgsConfig"; + class = "nixpkgsConfig"; }; # take all the rest as-is From 7459c024950282da952d43762ad93ff30995cc6a Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 17 Apr 2023 20:00:07 +0200 Subject: [PATCH 19/23] lib/tests/modules.sh: Add submodule + class tests --- lib/tests/modules.sh | 9 ++++++- lib/tests/modules/class-check.nix | 41 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 116a0778aebc..8f4553464ad0 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -362,11 +362,18 @@ checkConfigOutput 'ok' config.freeformItems.foo.bar ./adhoc-freeformType-survive # because of an `extendModules` bug, issue 168767. checkConfigOutput '^1$' config.sub.specialisation.value ./extendModules-168767-imports.nix -# Class checks +# Class checks, evalModules checkConfigOutput '^{ }$' config.ok.config ./class-check.nix checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.fail.config ./class-check.nix checkConfigError 'The module foo.nix#darwinModules.default was imported into nixos instead of darwin.' config.fail-anon.config ./class-check.nix +# Class checks, submoduleWith +checkConfigOutput '^{ }$' config.sub.nixosOk ./class-check.nix +checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.sub.nixosFail.config ./class-check.nix + +# submoduleWith type merge with different class +checkConfigError 'error: A submoduleWith option is declared multiple times with conflicting class values "darwin" and "nixos".' config.sub.mergeFail.config ./class-check.nix + # _type check checkConfigError 'Could not load a value as a module, because it is of type "flake", in file .*/module-imports-_type-check.nix' config.ok.config ./module-imports-_type-check.nix checkConfigOutput '^true$' "$@" config.enable ./declare-enable.nix ./define-enable-with-top-level-mkIf.nix diff --git a/lib/tests/modules/class-check.nix b/lib/tests/modules/class-check.nix index 02d1431cc88b..7874d0e28ec7 100644 --- a/lib/tests/modules/class-check.nix +++ b/lib/tests/modules/class-check.nix @@ -1,4 +1,43 @@ { lib, ... }: { + options = { + sub = { + nixosOk = lib.mkOption { + type = lib.types.submoduleWith { + class = "nixos"; + modules = [ ]; + }; + }; + # Same but will have bad definition + nixosFail = lib.mkOption { + type = lib.types.submoduleWith { + class = "nixos"; + modules = [ ]; + }; + }; + + mergeFail = lib.mkOption { + type = lib.types.submoduleWith { + class = "nixos"; + modules = [ ]; + }; + default = { }; + }; + }; + }; + imports = [ + { + options = { + sub = { + mergeFail = lib.mkOption { + type = lib.types.submoduleWith { + class = "darwin"; + modules = [ ]; + }; + }; + }; + }; + } + ]; config = { _module.freeformType = lib.types.anything; ok = @@ -31,5 +70,7 @@ ]; }; + sub.nixosOk = { config = {}; class = "nixos"; }; + sub.nixosFail = { imports = [ ./module-class-is-darwin.nix ]; }; }; } From fd88c79418bbb26758a89d111f136abdc8825acd Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 17 Apr 2023 20:14:07 +0200 Subject: [PATCH 20/23] lib.modules: Change class declaration in module to _class --- doc/module-system/module-system.chapter.md | 2 +- lib/modules.nix | 12 ++++++------ lib/tests/modules/class-check.nix | 4 ++-- lib/tests/modules/module-class-is-darwin.nix | 2 +- lib/tests/modules/module-class-is-nixos.nix | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md index 51e600d75640..714d8f67cfe2 100644 --- a/doc/module-system/module-system.chapter.md +++ b/doc/module-system/module-system.chapter.md @@ -30,7 +30,7 @@ This is in contrast to `config._module.args`, which is only available after all #### `class` {#module-system-lib-evalModules-param-class} -If the `class` attribute is set and non-`null`, the module system will reject `imports` with a different `class`. +If the `class` attribute is set and non-`null`, the module system will reject `imports` with a different `_class` declaration. The `class` value should be a string in lower [camel case](https://en.wikipedia.org/wiki/Camel_case). diff --git a/lib/modules.nix b/lib/modules.nix index 3550ebddaeac..c6d178e7ff9a 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -371,10 +371,10 @@ let if class != null then m: - if m.class != null -> m.class == class + if m._class != null -> m._class == class then m else - throw "The module ${m._file or m.key} was imported into ${class} instead of ${m.class}." + throw "The module ${m._file or m.key} was imported into ${class} instead of ${m._class}." else m: m; @@ -475,28 +475,28 @@ let else config; in if m ? config || m ? options then - let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta" "freeformType" "class"]; in + let badAttrs = removeAttrs m ["_class" "_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 { _file = toString m._file or file; + _class = m._class or null; key = toString m.key or key; disabledModules = m.disabledModules or []; imports = m.imports or []; options = m.options or {}; config = addFreeformType (addMeta (m.config or {})); - class = m.class or null; } else # shorthand syntax lib.throwIfNot (isAttrs m) "module ${file} (${key}) does not look like a module." { _file = toString m._file or file; + _class = m._class or null; key = toString m.key or key; disabledModules = m.disabledModules or []; imports = m.require or [] ++ m.imports or []; options = {}; - config = addFreeformType (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"]); - class = null; + config = addFreeformType (removeAttrs m ["_class" "_file" "key" "disabledModules" "require" "imports" "freeformType"]); }; applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: diff --git a/lib/tests/modules/class-check.nix b/lib/tests/modules/class-check.nix index 7874d0e28ec7..293fd4abd628 100644 --- a/lib/tests/modules/class-check.nix +++ b/lib/tests/modules/class-check.nix @@ -63,14 +63,14 @@ modules = [ ./module-class-is-nixos.nix { _file = "foo.nix#darwinModules.default"; - class = "darwin"; + _class = "darwin"; config = {}; imports = []; } ]; }; - sub.nixosOk = { config = {}; class = "nixos"; }; + sub.nixosOk = { _class = "nixos"; }; sub.nixosFail = { imports = [ ./module-class-is-darwin.nix ]; }; }; } diff --git a/lib/tests/modules/module-class-is-darwin.nix b/lib/tests/modules/module-class-is-darwin.nix index d8b60203f707..bacf45626d71 100644 --- a/lib/tests/modules/module-class-is-darwin.nix +++ b/lib/tests/modules/module-class-is-darwin.nix @@ -1,4 +1,4 @@ { - class = "darwin"; + _class = "darwin"; config = {}; } diff --git a/lib/tests/modules/module-class-is-nixos.nix b/lib/tests/modules/module-class-is-nixos.nix index 04b6e860e890..6d62feedae48 100644 --- a/lib/tests/modules/module-class-is-nixos.nix +++ b/lib/tests/modules/module-class-is-nixos.nix @@ -1,4 +1,4 @@ { - class = "nixos"; + _class = "nixos"; config = {}; } From 4c7aa7d831aa691a7331967a4a839b1088a48493 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 17 Apr 2023 21:52:04 +0200 Subject: [PATCH 21/23] doc/module-system: `_module` is not internal --- doc/module-system/module-system.chapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md index 714d8f67cfe2..9df7a1672a63 100644 --- a/doc/module-system/module-system.chapter.md +++ b/doc/module-system/module-system.chapter.md @@ -92,6 +92,6 @@ If you do reference multiple `config` (or `options`) from before and after `exte #### `_module` {#module-system-lib-evalModules-return-value-_module} -A portion of the configuration tree which is elided from `config`. It contains some values that are mostly internal to the module system implementation. +A portion of the configuration tree which is elided from `config`. From 89491bef8dc72496812d94c32b5fdd962cbbe1f6 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 27 Apr 2023 19:50:20 +0200 Subject: [PATCH 22/23] lib.modules: in evalModules return move _module.class -> configurationClass --- doc/module-system/module-system.chapter.md | 8 ++++++++ lib/modules.nix | 12 +----------- lib/tests/modules.sh | 1 + 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md index 9df7a1672a63..b82a546e4aa6 100644 --- a/doc/module-system/module-system.chapter.md +++ b/doc/module-system/module-system.chapter.md @@ -95,3 +95,11 @@ If you do reference multiple `config` (or `options`) from before and after `exte A portion of the configuration tree which is elided from `config`. + +#### `_type` {#module-system-lib-evalModules-return-value-_type} + +A nominal type marker, always `"configuration"`. + +#### `configurationClass` {#module-system-lib-evalModules-return-value-_configurationClass} + +Equal to the [`class` parameter](#module-system-lib-evalModules-param-class). diff --git a/lib/modules.nix b/lib/modules.nix index c6d178e7ff9a..5e7f5d7a86d4 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -220,16 +220,6 @@ let within a configuration, but can be used in module imports. ''; }; - - _module.class = mkOption { - readOnly = true; - internal = true; - description = lib.mdDoc '' - If the `class` attribute is set and non-`null`, the module system will reject `imports` with a different `class`. - - This option contains the expected `class` attribute of the current module evaluation. - ''; - }; }; config = { @@ -237,7 +227,6 @@ let inherit extendModules; moduleType = type; }; - _module.class = class; _module.specialArgs = specialArgs; }; }; @@ -337,6 +326,7 @@ let config = checked (removeAttrs config [ "_module" ]); _module = checked (config._module); inherit extendModules type; + configurationClass = class; }; in result; diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 8f4553464ad0..0aac5b946a3b 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -364,6 +364,7 @@ checkConfigOutput '^1$' config.sub.specialisation.value ./extendModules-168767-i # Class checks, evalModules checkConfigOutput '^{ }$' config.ok.config ./class-check.nix +checkConfigOutput '"nixos"' config.ok.configurationClass ./class-check.nix checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.fail.config ./class-check.nix checkConfigError 'The module foo.nix#darwinModules.default was imported into nixos instead of darwin.' config.fail-anon.config ./class-check.nix From eab660d91e5d7a2796bc6e4b94b5939ed593db29 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 27 Apr 2023 20:03:44 +0200 Subject: [PATCH 23/23] lib.modules: configurationClass -> class This simplifies the documentation. `configuration` is implied by `_type`. --- doc/module-system/module-system.chapter.md | 4 ++-- lib/modules.nix | 2 +- lib/tests/modules.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md index b82a546e4aa6..927f66073748 100644 --- a/doc/module-system/module-system.chapter.md +++ b/doc/module-system/module-system.chapter.md @@ -100,6 +100,6 @@ A portion of the configuration tree which is elided from `config`. A nominal type marker, always `"configuration"`. -#### `configurationClass` {#module-system-lib-evalModules-return-value-_configurationClass} +#### `class` {#module-system-lib-evalModules-return-value-_configurationClass} -Equal to the [`class` parameter](#module-system-lib-evalModules-param-class). +The [`class` argument](#module-system-lib-evalModules-param-class). diff --git a/lib/modules.nix b/lib/modules.nix index 5e7f5d7a86d4..4dc8c663b2fe 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -326,7 +326,7 @@ let config = checked (removeAttrs config [ "_module" ]); _module = checked (config._module); inherit extendModules type; - configurationClass = class; + class = class; }; in result; diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 0aac5b946a3b..45c247cbbea6 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -364,7 +364,7 @@ checkConfigOutput '^1$' config.sub.specialisation.value ./extendModules-168767-i # Class checks, evalModules checkConfigOutput '^{ }$' config.ok.config ./class-check.nix -checkConfigOutput '"nixos"' config.ok.configurationClass ./class-check.nix +checkConfigOutput '"nixos"' config.ok.class ./class-check.nix checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.fail.config ./class-check.nix checkConfigError 'The module foo.nix#darwinModules.default was imported into nixos instead of darwin.' config.fail-anon.config ./class-check.nix