From eb0c65c8e73fa5849fda4ef6afcd8d46fe447a7d Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Fri, 9 Aug 2024 00:32:37 +0100 Subject: [PATCH] lib/modules: Add `definedOptionsOnly` to `evalModules` TODO: consider promoting custom filter function to `lib.attrsets`? --- lib/modules.nix | 66 ++++++++++++++++++- lib/tests/modules.sh | 5 ++ .../modules/definedOptionsOnly-submodule.nix | 20 ++++++ lib/types.nix | 3 +- 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 lib/tests/modules/definedOptionsOnly-submodule.nix diff --git a/lib/modules.nix b/lib/modules.nix index b9e9ca1e5d78a..68f5bf9806cc7 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -74,6 +74,35 @@ let decls )); + # Private filtering function used by evalModules + # TODO: Consider moving something like this to `lib.attrsets`? + filterAttrsRecursiveWith = + { + continueRecursing ? _: true, + leafPredicate ? _: _: true, + branchPredicate ? _: _: true, + } @ args: + set: + lib.pipe set [ + attrNames + (lib.concatMap (name: + let + value = set.${name}; + filtered = filterAttrsRecursiveWith args value; + in + if isAttrs value && continueRecursing value then + lib.optional (branchPredicate name filtered) { + inherit name; + value = filtered; + } + else + lib.optional (leafPredicate name value) { + inherit name value; + } + )) + lib.listToAttrs + ]; + /* See https://nixos.org/manual/nixpkgs/unstable/#module-system-lib-evalModules or file://./../doc/module-system/module-system.chapter.md @@ -90,6 +119,9 @@ let # there's _module.args. If specialArgs.modulesPath is defined it will be # used as the base path for disabledModules. specialArgs ? {} + , # Whether to merge only defined options into the final config value. + # I.e. options that either have a default value or at least one definition. + definedOptionsOnly ? false , # `class`: # A nominal type for modules. When set and non-null, this adds a check to # make sure that only compatible modules are imported. @@ -202,6 +234,24 @@ let description = "Whether to check whether all option definitions have matching declarations."; }; + _module.definedOptionsOnly = mkOption { + type = types.bool; + internal = true; + # This option is readOnly because we cannot use its value to determine how to evaluate the modules + readOnly = true; + default = definedOptionsOnly; + description = '' + Whether to merge only defined options into the final `config` value. + I.e. options that either have a **default** value or **at least one** definition. + + By default, undefined options are merged in but will throw a _"used but not defined"_ error if + their value is evaluated. + + This option does not affect merging of freeform definitions that is done when `freeformType` is + non-null. + ''; + }; + _module.freeformType = mkOption { type = types.nullOr types.optionType; internal = true; @@ -249,9 +299,23 @@ let config = let + isNotOption = v: ! isOption v; + + # If definedOptionsOnly is enabled, we first filter for defined options + options' = + # We'd like to use the _module.definedOptionsOnly option, but that's infinitely recursive. + # Therefore, we use a function arg passed to evalModules instead. + if definedOptionsOnly then + filterAttrsRecursiveWith { + continueRecursing = isNotOption; + leafPredicate = n: opt: opt.isDefined; + branchPredicate = n: set: set != { }; + } options + else + options; # For definitions that have an associated option - declaredConfig = mapAttrsRecursiveCond (v: ! isOption v) (_: v: v.value) options; + declaredConfig = mapAttrsRecursiveCond isNotOption (_: v: v.value) options'; # If freeformType is set, this is for definitions that don't have an associated option freeformConfig = diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 3a23766a17f53..c95627d90a8df 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -321,6 +321,11 @@ checkConfigError 'is not of type `boolean' config.submodule.config ./declare-sub checkConfigError "In module ..*define-submoduleWith-shorthand.nix., you're trying to define a value of type \`bool'\n\s*rather than an attribute set for the option" config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-shorthand.nix checkConfigOutput '^true$' config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-noshorthand.nix +## definedOptionsOnly should behave as expected +checkConfigOutput '^false$' config.submodule.defined ./definedOptionsOnly-submodule.nix +checkConfigOutput '^false$' config.submodule.hasDefault ./definedOptionsOnly-submodule.nix +checkConfigError "^error: attribute 'undefined' in selection path 'config.submodule.undefined' not found" config.submodule.undefined ./definedOptionsOnly-submodule.nix + ## submoduleWith should merge all modules in one swoop checkConfigOutput '^true$' config.submodule.inner ./declare-submoduleWith-modules.nix checkConfigOutput '^true$' config.submodule.outer ./declare-submoduleWith-modules.nix diff --git a/lib/tests/modules/definedOptionsOnly-submodule.nix b/lib/tests/modules/definedOptionsOnly-submodule.nix new file mode 100644 index 0000000000000..6c8e35a6f866a --- /dev/null +++ b/lib/tests/modules/definedOptionsOnly-submodule.nix @@ -0,0 +1,20 @@ +{ lib, ... }: +{ + options.submodule = lib.mkOption { + type = lib.types.submoduleWith { + definedOptionsOnly = true; + modules = [ + { + options.defined = lib.mkOption { type = lib.types.bool; }; + options.hasDefault = lib.mkOption { + type = lib.types.bool; + default = false; + }; + options.undefined = lib.mkOption { type = lib.types.bool; }; + } + ]; + }; + }; + + config.submodule.defined = false; +} diff --git a/lib/types.nix b/lib/types.nix index ae482eeef7514..879e74328d94e 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -839,6 +839,7 @@ rec { { modules , specialArgs ? {} , shorthandOnlyDefinesConfig ? false + , definedOptionsOnly ? false , description ? null , class ? null }@attrs: @@ -852,7 +853,7 @@ rec { ) defs; base = evalModules { - inherit class specialArgs; + inherit class definedOptionsOnly 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"