Merge pull request #156533 from hercules-ci/issue-146882-transparent-submodule-options

lib.modules: Let module declare options directly in bare submodule
This commit is contained in:
Silvan Mosberger 2022-03-16 21:44:35 +01:00 committed by GitHub
commit b97742c66c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 164 additions and 19 deletions

View File

@ -9,7 +9,7 @@ let
catAttrs catAttrs
concatLists concatLists
concatMap concatMap
count concatStringsSep
elem elem
filter filter
findFirst findFirst
@ -47,6 +47,20 @@ let
showOption showOption
unknownModule unknownModule
; ;
showDeclPrefix = loc: decl: prefix:
" - option(s) with prefix `${showOption (loc ++ [prefix])}' in module `${decl._file}'";
showRawDecls = loc: decls:
concatStringsSep "\n"
(sort (a: b: a < b)
(concatMap
(decl: map
(showDeclPrefix loc decl)
(attrNames decl.options)
)
decls
));
in in
rec { rec {
@ -474,26 +488,61 @@ rec {
[{ inherit (module) file; inherit value; }] [{ inherit (module) file; inherit value; }]
) configs; ) configs;
# Convert an option tree decl to a submodule option decl
optionTreeToOption = decl:
if isOption decl.options
then decl
else decl // {
options = mkOption {
type = types.submoduleWith {
modules = [ { options = decl.options; } ];
# `null` is not intended for use by modules. It is an internal
# value that means "whatever the user has declared elsewhere".
# This might become obsolete with https://github.com/NixOS/nixpkgs/issues/162398
shorthandOnlyDefinesConfig = null;
};
};
};
resultsByName = mapAttrs (name: decls: resultsByName = mapAttrs (name: decls:
# We're descending into attribute name. # We're descending into attribute name.
let let
loc = prefix ++ [name]; loc = prefix ++ [name];
defns = defnsByName.${name} or []; defns = defnsByName.${name} or [];
defns' = defnsByName'.${name} or []; defns' = defnsByName'.${name} or [];
nrOptions = count (m: isOption m.options) decls; optionDecls = filter (m: isOption m.options) decls;
in in
if nrOptions == length decls then if length optionDecls == length decls then
let opt = fixupOptionType loc (mergeOptionDecls loc decls); let opt = fixupOptionType loc (mergeOptionDecls loc decls);
in { in {
matchedOptions = evalOptionValue loc opt defns'; matchedOptions = evalOptionValue loc opt defns';
unmatchedDefns = []; unmatchedDefns = [];
} }
else if nrOptions != 0 then else if optionDecls != [] then
let if all (x: x.options.type.name == "submodule") optionDecls
firstOption = findFirst (m: isOption m.options) "" decls; # Raw options can only be merged into submodules. Merging into
firstNonOption = findFirst (m: !isOption m.options) "" decls; # attrsets might be nice, but ambiguous. Suppose we have
in # attrset as a `attrsOf submodule`. User declares option
throw "The option `${showOption loc}' in `${firstOption._file}' is a prefix of options in `${firstNonOption._file}'." # attrset.foo.bar, this could mean:
# a. option `bar` is only available in `attrset.foo`
# b. option `foo.bar` is available in all `attrset.*`
# c. reject and require "<name>" as a reminder that it behaves like (b).
# d. magically combine (a) and (c).
# All of the above are merely syntax sugar though.
then
let opt = fixupOptionType loc (mergeOptionDecls loc (map optionTreeToOption decls));
in {
matchedOptions = evalOptionValue loc opt defns';
unmatchedDefns = [];
}
else
let
firstNonOption = findFirst (m: !isOption m.options) "" decls;
nonOptions = filter (m: !isOption m.options) decls;
in
throw "The option `${showOption loc}' in module `${(lib.head optionDecls)._file}' would be a parent of the following options, but its type `${(lib.head optionDecls).options.type.description or "<no description>"}' does not support nested options.\n${
showRawDecls loc nonOptions
}"
else else
mergeModules' loc decls defns) declsByName; mergeModules' loc decls defns) declsByName;
@ -753,13 +802,14 @@ rec {
compare = a: b: (a.priority or 1000) < (b.priority or 1000); compare = a: b: (a.priority or 1000) < (b.priority or 1000);
in sort compare defs'; in sort compare defs';
/* Hack for backward compatibility: convert options of type
optionSet to options of type submodule. FIXME: remove
eventually. */
fixupOptionType = loc: opt: fixupOptionType = loc: opt:
let let
options = opt.options or options = opt.options or
(throw "Option `${showOption loc}' has type optionSet but has no option attribute, in ${showFiles opt.declarations}."); (throw "Option `${showOption loc}' has type optionSet but has no option attribute, in ${showFiles opt.declarations}.");
# Hack for backward compatibility: convert options of type
# optionSet to options of type submodule. FIXME: remove
# eventually.
f = tp: f = tp:
if tp.name == "option set" || tp.name == "submodule" then if tp.name == "option set" || tp.name == "submodule" then
throw "The option ${showOption loc} uses submodules without a wrapping type, in ${showFiles opt.declarations}." throw "The option ${showOption loc} uses submodules without a wrapping type, in ${showFiles opt.declarations}."

View File

@ -62,6 +62,13 @@ checkConfigError() {
checkConfigOutput '^false$' config.enable ./declare-enable.nix checkConfigOutput '^false$' config.enable ./declare-enable.nix
checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*: true' config.enable ./define-enable.nix checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*: true' config.enable ./define-enable.nix
checkConfigOutput '^1$' config.bare-submodule.nested ./declare-bare-submodule.nix ./declare-bare-submodule-nested-option.nix
checkConfigOutput '^2$' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-deep-option.nix
checkConfigOutput '^42$' config.bare-submodule.nested ./declare-bare-submodule.nix ./declare-bare-submodule-nested-option.nix ./declare-bare-submodule-deep-option.nix ./define-bare-submodule-values.nix
checkConfigOutput '^420$' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-nested-option.nix ./declare-bare-submodule-deep-option.nix ./define-bare-submodule-values.nix
checkConfigOutput '^2$' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-deep-option.nix ./define-shorthandOnlyDefinesConfig-true.nix
checkConfigError 'The option .bare-submodule.deep. in .*/declare-bare-submodule-deep-option.nix. is already declared in .*/declare-bare-submodule-deep-option-duplicate.nix' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-deep-option.nix ./declare-bare-submodule-deep-option-duplicate.nix
# Check integer types. # Check integer types.
# unsigned # unsigned
checkConfigOutput '^42$' config.value ./declare-int-unsigned-value.nix ./define-value-int-positive.nix checkConfigOutput '^42$' config.value ./declare-int-unsigned-value.nix ./define-value-int-positive.nix
@ -304,6 +311,12 @@ checkConfigOutput "10" config.processedToplevel ./raw.nix
checkConfigError "The option .multiple. is defined multiple times" config.multiple ./raw.nix checkConfigError "The option .multiple. is defined multiple times" config.multiple ./raw.nix
checkConfigOutput "bar" config.priorities ./raw.nix checkConfigOutput "bar" config.priorities ./raw.nix
## Option collision
checkConfigError \
'The option .set. in module .*/declare-set.nix. would be a parent of the following options, but its type .attribute set of signed integers. does not support nested options.\n\s*- option[(]s[)] with prefix .set.enable. in module .*/declare-enable-nested.nix.' \
config.set \
./declare-set.nix ./declare-enable-nested.nix
# Test that types.optionType merges types correctly # Test that types.optionType merges types correctly
checkConfigOutput '^10$' config.theOption.int ./optionTypeMerging.nix checkConfigOutput '^10$' config.theOption.int ./optionTypeMerging.nix
checkConfigOutput '^"hello"$' config.theOption.str ./optionTypeMerging.nix checkConfigOutput '^"hello"$' config.theOption.str ./optionTypeMerging.nix

View File

@ -0,0 +1,10 @@
{ lib, ... }:
let
inherit (lib) mkOption types;
in
{
options.bare-submodule.deep = mkOption {
type = types.int;
default = 2;
};
}

View File

@ -0,0 +1,10 @@
{ lib, ... }:
let
inherit (lib) mkOption types;
in
{
options.bare-submodule.deep = mkOption {
type = types.int;
default = 2;
};
}

View File

@ -0,0 +1,19 @@
{ config, lib, ... }:
let
inherit (lib) mkOption types;
in
{
options.bare-submodule = mkOption {
type = types.submoduleWith {
shorthandOnlyDefinesConfig = config.shorthandOnlyDefinesConfig;
modules = [
{
options.nested = mkOption {
type = types.int;
default = 1;
};
}
];
};
};
}

View File

@ -0,0 +1,18 @@
{ config, lib, ... }:
let
inherit (lib) mkOption types;
in
{
options.bare-submodule = mkOption {
type = types.submoduleWith {
modules = [ ];
shorthandOnlyDefinesConfig = config.shorthandOnlyDefinesConfig;
};
default = {};
};
# config-dependent options: won't recommend, but useful for making this test parameterized
options.shorthandOnlyDefinesConfig = mkOption {
default = false;
};
}

View File

@ -0,0 +1,12 @@
{ lib, ... }:
{
options.set = lib.mkOption {
default = { };
example = { a = 1; };
type = lib.types.attrsOf lib.types.int;
description = ''
Some descriptive text
'';
};
}

View File

@ -0,0 +1,4 @@
{
bare-submodule.nested = 42;
bare-submodule.deep = 420;
}

View File

@ -0,0 +1 @@
{ shorthandOnlyDefinesConfig = true; }

View File

@ -572,14 +572,18 @@ rec {
let let
inherit (lib.modules) evalModules; inherit (lib.modules) evalModules;
coerce = unify: value: if isFunction value shorthandToModule = if shorthandOnlyDefinesConfig == false
then setFunctionArgs (args: unify (value args)) (functionArgs value) then value: value
else unify (if shorthandOnlyDefinesConfig then { config = value; } else value); else value: { config = value; };
allModules = defs: imap1 (n: { value, file }: allModules = defs: imap1 (n: { value, file }:
if isAttrs value || isFunction value then if isFunction value
# Annotate the value with the location of its definition for better error messages then setFunctionArgs
coerce (lib.modules.unifyModuleSyntax file "${toString file}-${toString n}") value (args: lib.modules.unifyModuleSyntax file "${toString file}-${toString n}" (value args))
(functionArgs value)
else if isAttrs value
then
lib.modules.unifyModuleSyntax file "${toString file}-${toString n}" (shorthandToModule value)
else value else value
) defs; ) defs;
@ -647,7 +651,11 @@ rec {
then lhs.specialArgs // rhs.specialArgs then lhs.specialArgs // rhs.specialArgs
else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\""; else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\"";
shorthandOnlyDefinesConfig = shorthandOnlyDefinesConfig =
if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig if lhs.shorthandOnlyDefinesConfig == null
then rhs.shorthandOnlyDefinesConfig
else if rhs.shorthandOnlyDefinesConfig == null
then lhs.shorthandOnlyDefinesConfig
else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig
then lhs.shorthandOnlyDefinesConfig then lhs.shorthandOnlyDefinesConfig
else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values"; else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values";
}; };