From 2eda2ad48683fe6b3e6f11d77e9d16b917cf8dd4 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 18 Oct 2024 18:16:37 +0200 Subject: [PATCH] lib/modules: Declare, check, merge, expose and test opt.meta --- lib/modules.nix | 53 +++++++++++++++++++--- lib/tests/modules.sh | 4 ++ lib/tests/modules/option-meta-required.nix | 12 +++++ lib/tests/modules/option-meta.nix | 23 ++++++++++ lib/types.nix | 3 +- 5 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 lib/tests/modules/option-meta-required.nix create mode 100644 lib/tests/modules/option-meta.nix diff --git a/lib/modules.nix b/lib/modules.nix index 381480a1a0735..842645e3252d1 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -243,7 +243,7 @@ let (specialArgs.modulesPath or "") (regularModules ++ [ internalModule ]) ({ inherit lib options config specialArgs; } // specialArgs); - in mergeModules prefix (reverseList collected); + in mergeModules2 options._module prefix (reverseList collected); options = merged.matchedOptions; @@ -548,10 +548,16 @@ let } */ mergeModules = prefix: modules: - mergeModules' prefix modules + mergeModules2' {} prefix modules + (concatMap (m: map (config: { file = m._file; inherit config; }) (pushDownProperties m.config)) modules); + + mergeModules2 = _module: prefix: modules: + mergeModules2' _module prefix modules (concatMap (m: map (config: { file = m._file; inherit config; }) (pushDownProperties m.config)) modules); mergeModules' = prefix: modules: configs: + mergeModules2' { }; + mergeModules2' = _module: prefix: modules: configs: let # an attrset 'name' => list of submodules that declare ‘name’. declsByName = @@ -655,7 +661,7 @@ let if length optionDecls == length decls then let opt = fixupOptionType loc (mergeOptionDecls loc decls); in { - matchedOptions = evalOptionValue loc opt defns'; + matchedOptions = evalOptionValue' _module loc opt defns'; unmatchedDefns = []; } else if optionDecls != [] then @@ -672,7 +678,7 @@ let then let opt = fixupOptionType loc (mergeOptionDecls loc (map optionTreeToOption decls)); in { - matchedOptions = evalOptionValue loc opt defns'; + matchedOptions = evalOptionValue' _module loc opt defns'; unmatchedDefns = []; } else @@ -683,7 +689,7 @@ let showRawDecls loc nonOptions }" else - mergeModules' loc decls defns) declsByName; + mergeModules2' _module loc decls defns) declsByName; matchedOptions = mapAttrs (n: v: v.matchedOptions) resultsByName; @@ -789,7 +795,8 @@ let /* Merge all the definitions of an option to produce the final config value. */ - evalOptionValue = loc: opt: defs: + evalOptionValue = evalOptionValue' {}; + evalOptionValue' = _module: loc: opt: defs: let # Add in the default value for this option, if any. defs' = @@ -826,8 +833,39 @@ let inherit (res) isDefined; # This allows options to be correctly displayed using `${options.path.to.it}` __toString = _: showOption loc; + meta = checkOptionMeta _module opt; }; + checkOptionMeta = _module: opt: + let + metaConfiguration = + evalModules { + # The option path. Although the things we're checking happen to be options, + # that's not what we're checking against, and that's what the prefix is + # about. The checkable options are more like _file, and we'll make use of that. + prefix = [ "_modules" "optionMeta" ]; + modules = + [ + { options = _module.optionMeta or { }; } + ] + ++ lib.zipListsWith + (decl: pos: + lib.setDefaultModuleLocation + "option declaration at ${pos.file}:${toString pos.line}:${toString pos.column}" + { + config = + (lib.throwIf + (opt?meta._module) + "In option declarations, `meta._module` is not allowed, but option `${showOption opt.loc}` has it." + opt.meta or {}); + } + ) + opt.declarations + opt.declarationPositions; + }; + in + metaConfiguration.config; + # Merge definitions of a value of a given type. mergeDefinitions = loc: type: defs: rec { defsFinal' = @@ -1462,7 +1500,8 @@ private // defaultPriority doRename evalModules - evalOptionValue # for use by lib.types + evalOptionValue # for use by lib.types (?) + evalOptionValue' # for use by lib.types filterOverrides filterOverrides' fixMergeModules diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 2a48d88cbf732..ef3ec7cfffc6c 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -148,6 +148,10 @@ checkConfigError 'The option .sub.wrong2. does not exist. Definition values:' co checkConfigError '.*This can happen if you e.g. declared your options in .types.submodule.' config.sub ./error-mkOption-in-submodule-config.nix checkConfigError '.*A definition for option .bad. is not of type .non-empty .list of .submodule...\.' config.bad ./error-nonEmptyListOf-submodule.nix +checkConfigOutput '^true$' options.foo.meta.required ./option-meta.nix +checkConfigError '.*The option ._modules\.optionMeta\.reuired. does not exist.*' options.undeclared.meta.reuired ./option-meta.nix +checkConfigError '.*option-meta\.nix:[0-9]+:[0-9]+.: false' options.undeclared.meta.reuired ./option-meta.nix + # types.attrTag checkConfigOutput '^true$' config.okChecks ./types-attrTag.nix checkConfigError 'A definition for option .intStrings\.syntaxError. is not of type .attribute-tagged union' config.intStrings.syntaxError ./types-attrTag.nix diff --git a/lib/tests/modules/option-meta-required.nix b/lib/tests/modules/option-meta-required.nix new file mode 100644 index 0000000000000..2bc2f646f5e02 --- /dev/null +++ b/lib/tests/modules/option-meta-required.nix @@ -0,0 +1,12 @@ +{ lib, ... }: +let + inherit (lib) mkOption types; +in +{ + options._module.optionMeta = { + required = lib.mkOption { + type = types.bool; + description = "Whether an option is required or something; just an example meta attribute for testing."; + }; + }; +} diff --git a/lib/tests/modules/option-meta.nix b/lib/tests/modules/option-meta.nix new file mode 100644 index 0000000000000..1b5590c9255aa --- /dev/null +++ b/lib/tests/modules/option-meta.nix @@ -0,0 +1,23 @@ +{ lib, ... }: +let + inherit (lib) mkOption types; +in +{ + imports = [ + ./option-meta-required.nix + ]; + options = { + foo = lib.mkOption { + type = lib.types.str; + meta.required = true; + }; + undeclared = lib.mkOption { + type = lib.types.str; + # typo, no q + meta.reuired = false; + }; + }; + config = { + foo = "bar"; + }; +} diff --git a/lib/types.nix b/lib/types.nix index 86b717afd1227..e730ec44ad2a1 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -684,7 +684,8 @@ rec { if tags?${choice} then { ${choice} = - (lib.modules.evalOptionValue + (lib.modules.evalOptionValue' + { } # Can't have option meta attributes in attrTag for now; needs `merge` function interface upgrade (loc ++ [choice]) tags.${choice} checkedValueDefs