diff --git a/lib/default.nix b/lib/default.nix index 0ff3a39807450..887cc514f3ad0 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -143,7 +143,6 @@ let mkMergedOptionModule mkChangedOptionModule mkAliasOptionModule mkDerivedConfig doRename mkAliasOptionModuleMD; - evalOptionValue = lib.warn "External use of `lib.evalOptionValue` 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/." self.modules.evalOptionValue; inherit (self.options) isOption mkEnableOption mkSinkUndeclaredOptions mergeDefaultOption mergeOneOption mergeEqualOption mergeUniqueOption getValues getFiles diff --git a/lib/modules.nix b/lib/modules.nix index 381480a1a0735..8e5efb2180fc2 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -226,6 +226,15 @@ let within a configuration, but can be used in module imports. ''; }; + _module.optionMeta = mkOption { + visible = false; + internal = true; + type = types.deferredModuleWith { + staticModules = [ + ./modules/default-meta-interface.nix + ]; + }; + }; }; config = { @@ -243,7 +252,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 +557,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 +670,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 +687,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 +698,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 +804,8 @@ let /* Merge all the definitions of an option to produce the final config value. */ - evalOptionValue = loc: opt: defs: + evalOptionValue = evalOptionValue' {}; + evalOptionValue' = _moduleOptions: loc: opt: defs: let # Add in the default value for this option, if any. defs' = @@ -826,8 +842,42 @@ let inherit (res) isDefined; # This allows options to be correctly displayed using `${options.path.to.it}` __toString = _: showOption loc; + meta = checkOptionMeta _moduleOptions opt; }; + checkOptionMeta = _moduleOptions: opt: + let + metaConfiguration = + evalModules { + specialArgs = { + optionDeclaration = opt; + }; + # 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 = [ "options" ] ++ opt.loc ++ [ "meta" ]; + modules = + [ + _moduleOptions.optionMeta.value + ] + ++ 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 +1512,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/modules/default-meta-interface.nix b/lib/modules/default-meta-interface.nix new file mode 100644 index 0000000000000..6c6196456841d --- /dev/null +++ b/lib/modules/default-meta-interface.nix @@ -0,0 +1,36 @@ +{ lib, optionDeclaration, ... }: +let + inherit (lib) types mkOption; +in +{ + options = { + defaultText = mkOption { + apply = + v: if v == null && !optionDeclaration ? default then null else lib.options.renderOptionValue v; + type = types.raw; + default = optionDeclaration.default or null; + }; + example = mkOption { + apply = + v: if v == null && !optionDeclaration ? default then null else lib.options.renderOptionValue v; + type = types.raw; + default = optionDeclaration.example or null; + }; + description = mkOption { + type = types.nullOr types.str; + default = optionDeclaration.description or null; + }; + internal = mkOption { + type = types.bool; + default = optionDeclaration.internal or false; + }; + visible = mkOption { + type = types.enum [ + true + false + "shallow" + ]; + default = optionDeclaration.visible or true; + }; + }; +} diff --git a/lib/options.nix b/lib/options.nix index f4d0d9d36cfc9..2a19d2b5463d8 100644 --- a/lib/options.nix +++ b/lib/options.nix @@ -67,6 +67,12 @@ rec { { # Default value used when no definition is given in the configuration. default ? null, + # Option type, providing type-checking and value merging. + type ? null, + # Whether the option can be set only once + readOnly ? null, + + # Textual representation of the default, for the manual. defaultText ? null, # Example value used in the manual. @@ -75,16 +81,13 @@ rec { description ? null, # Related packages used in the manual (see `genRelatedPackages` in ../nixos/lib/make-options-doc/default.nix). relatedPackages ? null, - # Option type, providing type-checking and value merging. - type ? null, # Function that converts the option value to something else. apply ? null, # Whether the option is for NixOS developers only. internal ? null, # Whether the option shows up in the manual. Default: true. Use false to hide the option and any sub-options from submodules. Use "shallow" to hide only sub-options. visible ? null, - # Whether the option can be set only once - readOnly ? null, + meta ? {}, } @ attrs: attrs // { _type = "option"; }; diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 2a48d88cbf732..d9d7afa0bf4a0 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -148,6 +148,12 @@ 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 +checkConfigOutput '^"ok"$' config.assertions ./option-meta.nix +checkConfigError '.*The option .options\.undeclared\.meta\.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 +checkConfigError '.*The option .options\.missingDef\.meta\.required. was accessed but has no value defined\. Try setting the option.*' options.missingDef.meta.required ./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..3114d1b89be37 --- /dev/null +++ b/lib/tests/modules/option-meta-required.nix @@ -0,0 +1,14 @@ +{ 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."; + # Most meta options should have a default, but for this test we don't define, + # as missingDef in ./option-meta.nix relies on this. + }; + }; +} diff --git a/lib/tests/modules/option-meta.nix b/lib/tests/modules/option-meta.nix new file mode 100644 index 0000000000000..d36825f2e99cf --- /dev/null +++ b/lib/tests/modules/option-meta.nix @@ -0,0 +1,45 @@ +{ + config, + lib, + options, + ... +}: +let + inherit (lib) isAttrs mkOption types; + nullAttrs = lib.mapAttrs (_: _: null); +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; + }; + missingDef = mkOption { + type = lib.types.str; + }; + brokenMeta = mkOption { + meta = abort "brokenMeta.meta is broken and must not be evaluated as a matter of laziness, performance, robustness."; + default = "ok"; + }; + + assertions = mkOption { }; + }; + config = { + foo = "bar"; + assertions = + assert options.foo.meta == { required = true; }; + # laziness + assert isAttrs options.missingDef.meta; + assert nullAttrs options.missingDef.meta == { required = null; }; + assert config.brokenMeta == "ok"; + "ok"; + }; +} diff --git a/lib/types.nix b/lib/types.nix index 86b717afd1227..8893f73e8cf95 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' + { } # Would need something like `attrTagWith { optionMeta = { }; }`, and a good way to retrieve the `meta`s (loc ++ [choice]) tags.${choice} checkedValueDefs diff --git a/nixos/modules/misc/optionMeta.nix b/nixos/modules/misc/optionMeta.nix new file mode 100644 index 0000000000000..000b850678ccf --- /dev/null +++ b/nixos/modules/misc/optionMeta.nix @@ -0,0 +1,13 @@ +{ lib, optionDeclaration, ... }: +let + inherit (lib) types mkOption; +in +{ + # Additional options available in mkOption { ..., meta = { ... } } + options = { + relatedPackages = mkOption { + type = types.raw; + default = optionDeclaration.relatedPackages or [ ]; + }; + }; +} diff --git a/nixos/modules/misc/register-optionMeta.nix b/nixos/modules/misc/register-optionMeta.nix new file mode 100644 index 0000000000000..bf0214a5d730a --- /dev/null +++ b/nixos/modules/misc/register-optionMeta.nix @@ -0,0 +1,3 @@ +{ + _module.optionMeta = ./optionMeta.nix; +} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index b06521dcb9b3a..ac05f8d3b7454 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -136,6 +136,7 @@ ./misc/man-db.nix ./misc/mandoc.nix ./misc/meta.nix + ./misc/register-optionMeta.nix ./misc/nixops-autoluks.nix ./misc/nixpkgs.nix ./misc/nixpkgs-flake.nix