Module system improvements for NixOS as a submodule (#75031)

Module system improvements for NixOS as a submodule
This commit is contained in:
Silvan Mosberger 2020-01-02 20:38:45 +01:00 committed by GitHub
commit cdf79db19d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 249 additions and 57 deletions

View file

@ -102,7 +102,7 @@ let
commitIdFromGitRepo cleanSourceWith pathHasContext commitIdFromGitRepo cleanSourceWith pathHasContext
canCleanSource; canCleanSource;
inherit (modules) evalModules closeModules unifyModuleSyntax inherit (modules) evalModules closeModules unifyModuleSyntax
applyIfFunction unpackSubmodule packSubmodule mergeModules applyIfFunction mergeModules
mergeModules' mergeOptionDecls evalOptionValue mergeDefinitions mergeModules' mergeOptionDecls evalOptionValue mergeDefinitions
pushDownProperties dischargeProperties filterOverrides pushDownProperties dischargeProperties filterOverrides
sortProperties fixupOptionType mkIf mkAssert mkMerge mkOverride sortProperties fixupOptionType mkIf mkAssert mkMerge mkOverride

View file

@ -103,42 +103,42 @@ rec {
toClosureList = file: parentKey: imap1 (n: x: toClosureList = file: parentKey: imap1 (n: x:
if isAttrs x || isFunction x then if isAttrs x || isFunction x then
let key = "${parentKey}:anon-${toString n}"; in let key = "${parentKey}:anon-${toString n}"; in
unifyModuleSyntax file key (unpackSubmodule (applyIfFunction key) x args) unifyModuleSyntax file key (applyIfFunction key x args)
else else
let file = toString x; key = toString x; in let file = toString x; key = toString x; in
unifyModuleSyntax file key (applyIfFunction key (import x) args)); unifyModuleSyntax file key (applyIfFunction key (import x) args));
in in
builtins.genericClosure { builtins.genericClosure {
startSet = toClosureList unknownModule "" modules; startSet = toClosureList unknownModule "" modules;
operator = m: toClosureList m.file m.key m.imports; operator = m: toClosureList m._file m.key m.imports;
}; };
/* Massage a module into canonical form, that is, a set consisting /* Massage a module into canonical form, that is, a set consisting
of options, config and imports attributes. */ of options, config and imports attributes. */
unifyModuleSyntax = file: key: m: unifyModuleSyntax = file: key: m:
let metaSet = if m ? meta let addMeta = config: if m ? meta
then { meta = m.meta; } then mkMerge [ config { meta = m.meta; } ]
else {}; else config;
in in
if m ? config || m ? options then if m ? config || m ? options then
let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta"]; in let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta"]; in
if badAttrs != {} then if badAttrs != {} then
throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by assignments to the top-level attributes `config' or `options'." throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by assignments to the top-level attributes `config' or `options'."
else else
{ file = m._file or file; { _file = m._file or file;
key = toString m.key or key; key = toString m.key or key;
disabledModules = m.disabledModules or []; disabledModules = m.disabledModules or [];
imports = m.imports or []; imports = m.imports or [];
options = m.options or {}; options = m.options or {};
config = mkMerge [ (m.config or {}) metaSet ]; config = addMeta (m.config or {});
} }
else else
{ file = m._file or file; { _file = m._file or file;
key = toString m.key or key; key = toString m.key or key;
disabledModules = m.disabledModules or []; disabledModules = m.disabledModules or [];
imports = m.require or [] ++ m.imports or []; imports = m.require or [] ++ m.imports or [];
options = {}; options = {};
config = mkMerge [ (removeAttrs m ["_file" "key" "disabledModules" "require" "imports"]) metaSet ]; config = addMeta (removeAttrs m ["_file" "key" "disabledModules" "require" "imports"]);
}; };
applyIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then applyIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then
@ -171,17 +171,6 @@ rec {
else else
f; f;
/* We have to pack and unpack submodules. We cannot wrap the expected
result of the function as we would no longer be able to list the arguments
of the submodule. (see applyIfFunction) */
unpackSubmodule = unpack: m: args:
if isType "submodule" m then
{ _file = m.file; } // (unpack m.submodule args)
else unpack m args;
packSubmodule = file: m:
{ _type = "submodule"; file = file; submodule = m; };
/* Merge a list of modules. This will recurse over the option /* Merge a list of modules. This will recurse over the option
declarations in all modules, combining them into a single set. declarations in all modules, combining them into a single set.
At the same time, for each option declaration, it will merge the At the same time, for each option declaration, it will merge the
@ -189,7 +178,7 @@ rec {
in the value attribute of each option. */ in the value attribute of each option. */
mergeModules = prefix: modules: mergeModules = prefix: modules:
mergeModules' prefix modules mergeModules' prefix modules
(concatMap (m: map (config: { inherit (m) file; inherit config; }) (pushDownProperties m.config)) modules); (concatMap (m: map (config: { file = m._file; inherit config; }) (pushDownProperties m.config)) modules);
mergeModules' = prefix: options: configs: mergeModules' = prefix: options: configs:
let let
@ -223,7 +212,7 @@ rec {
) {} modules; ) {} modules;
# an attrset 'name' => list of submodules that declare name. # an attrset 'name' => list of submodules that declare name.
declsByName = byName "options" (module: option: declsByName = byName "options" (module: option:
[{ inherit (module) file; options = option; }] [{ inherit (module) _file; options = option; }]
) options; ) options;
# an attrset 'name' => list of submodules that define name. # an attrset 'name' => list of submodules that define name.
defnsByName = byName "config" (module: value: defnsByName = byName "config" (module: value:
@ -250,7 +239,7 @@ rec {
firstOption = findFirst (m: isOption m.options) "" decls; firstOption = findFirst (m: isOption m.options) "" decls;
firstNonOption = findFirst (m: !isOption m.options) "" decls; firstNonOption = findFirst (m: !isOption m.options) "" decls;
in in
throw "The option `${showOption loc}' in `${firstOption.file}' is a prefix of options in `${firstNonOption.file}'." throw "The option `${showOption loc}' in `${firstOption._file}' is a prefix of options in `${firstNonOption._file}'."
else else
mergeModules' loc decls defns mergeModules' loc decls defns
)) ))
@ -267,7 +256,14 @@ rec {
'opts' is a list of modules. Each module has an options attribute which 'opts' is a list of modules. Each module has an options attribute which
correspond to the definition of 'loc' in 'opt.file'. */ correspond to the definition of 'loc' in 'opt.file'. */
mergeOptionDecls = loc: opts: mergeOptionDecls =
let
packSubmodule = file: m:
{ _file = file; imports = [ m ]; };
coerceOption = file: opt:
if isFunction opt then packSubmodule file opt
else packSubmodule file { options = opt; };
in loc: opts:
foldl' (res: opt: foldl' (res: opt:
let t = res.type; let t = res.type;
t' = opt.options.type; t' = opt.options.type;
@ -284,7 +280,7 @@ rec {
bothHave "apply" || bothHave "apply" ||
(bothHave "type" && (! typesMergeable)) (bothHave "type" && (! typesMergeable))
then then
throw "The option `${showOption loc}' in `${opt.file}' is already declared in ${showFiles res.declarations}." throw "The option `${showOption loc}' in `${opt._file}' is already declared in ${showFiles res.declarations}."
else else
let let
/* Add the modules of the current option to the list of modules /* Add the modules of the current option to the list of modules
@ -293,16 +289,14 @@ rec {
current option declaration as the file use for the submodule. If the current option declaration as the file use for the submodule. If the
submodule defines any filename, then we ignore the enclosing option file. */ submodule defines any filename, then we ignore the enclosing option file. */
options' = toList opt.options.options; options' = toList opt.options.options;
coerceOption = file: opt:
if isFunction opt then packSubmodule file opt
else packSubmodule file { options = opt; };
getSubModules = opt.options.type.getSubModules or null; getSubModules = opt.options.type.getSubModules or null;
submodules = submodules =
if getSubModules != null then map (packSubmodule opt.file) getSubModules ++ res.options if getSubModules != null then map (packSubmodule opt._file) getSubModules ++ res.options
else if opt.options ? options then map (coerceOption opt.file) options' ++ res.options else if opt.options ? options then map (coerceOption opt._file) options' ++ res.options
else res.options; else res.options;
in opt.options // res // in opt.options // res //
{ declarations = res.declarations ++ [opt.file]; { declarations = res.declarations ++ [opt._file];
options = submodules; options = submodules;
} // typeSet } // typeSet
) { inherit loc; declarations = []; options = []; } opts; ) { inherit loc; declarations = []; options = []; } opts;

View file

@ -164,6 +164,24 @@ checkConfigOutput "true" config.enableAlias ./alias-with-priority.nix
checkConfigOutput "false" config.enable ./alias-with-priority-can-override.nix checkConfigOutput "false" config.enable ./alias-with-priority-can-override.nix
checkConfigOutput "false" config.enableAlias ./alias-with-priority-can-override.nix checkConfigOutput "false" config.enableAlias ./alias-with-priority-can-override.nix
# submoduleWith
## specialArgs should work
checkConfigOutput "foo" config.submodule.foo ./declare-submoduleWith-special.nix
## shorthandOnlyDefines config behaves as expected
checkConfigOutput "true" config.submodule.config ./declare-submoduleWith-shorthand.nix ./define-submoduleWith-shorthand.nix
checkConfigError 'is not of type `boolean' config.submodule.config ./declare-submoduleWith-shorthand.nix ./define-submoduleWith-noshorthand.nix
checkConfigError 'value is a boolean while a set was expected' config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-shorthand.nix
checkConfigOutput "true" config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-noshorthand.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
## Paths should be allowed as values and work as expected
checkConfigOutput "true" config.submodule.enable ./declare-submoduleWith-path.nix
cat <<EOF cat <<EOF
====== module tests ====== ====== module tests ======
$pass Pass $pass Pass

View file

@ -0,0 +1,30 @@
{ lib, ... }: {
options.submodule = lib.mkOption {
type = lib.types.submoduleWith {
modules = [
{
options.inner = lib.mkOption {
type = lib.types.bool;
default = false;
};
}
{
outer = true;
}
];
};
default = {};
};
config.submodule = lib.mkMerge [
({ lib, ... }: {
options.outer = lib.mkOption {
type = lib.types.bool;
default = false;
};
})
{
inner = true;
}
];
}

View file

@ -0,0 +1,13 @@
{ lib, ... }: let
sub.options.config = lib.mkOption {
type = lib.types.bool;
default = false;
};
in {
options.submodule = lib.mkOption {
type = lib.types.submoduleWith {
modules = [ sub ];
};
default = {};
};
}

View file

@ -0,0 +1,12 @@
{ lib, ... }: {
options.submodule = lib.mkOption {
type = lib.types.submoduleWith {
modules = [
./declare-enable.nix
];
};
default = {};
};
config.submodule = ./define-enable.nix;
}

View file

@ -0,0 +1,14 @@
{ lib, ... }: let
sub.options.config = lib.mkOption {
type = lib.types.bool;
default = false;
};
in {
options.submodule = lib.mkOption {
type = lib.types.submoduleWith {
modules = [ sub ];
shorthandOnlyDefinesConfig = true;
};
default = {};
};
}

View file

@ -0,0 +1,17 @@
{ lib, ... }: {
options.submodule = lib.mkOption {
type = lib.types.submoduleWith {
modules = [
({ lib, ... }: {
options.foo = lib.mkOption {
default = lib.foo;
};
})
];
specialArgs.lib = lib // {
foo = "foo";
};
};
default = {};
};
}

View file

@ -0,0 +1,3 @@
{
submodule.config.config = true;
}

View file

@ -0,0 +1,3 @@
{
submodule.config = true;
}

View file

@ -358,25 +358,43 @@ rec {
}; };
# A submodule (like typed attribute set). See NixOS manual. # A submodule (like typed attribute set). See NixOS manual.
submodule = opts: submodule = modules: submoduleWith {
shorthandOnlyDefinesConfig = true;
modules = toList modules;
};
submoduleWith =
{ modules
, specialArgs ? {}
, shorthandOnlyDefinesConfig ? false
}@attrs:
let let
opts' = toList opts;
inherit (lib.modules) evalModules; inherit (lib.modules) evalModules;
coerce = unify: value: if isFunction value
then setFunctionArgs (args: unify (value args)) (functionArgs value)
else unify (if shorthandOnlyDefinesConfig then { config = value; } else value);
allModules = defs: modules ++ imap1 (n: { value, file }:
if isAttrs value || isFunction value then
# Annotate the value with the location of its definition for better error messages
coerce (lib.modules.unifyModuleSyntax file "${toString file}-${toString n}") value
else value
) defs;
in in
mkOptionType rec { mkOptionType rec {
name = "submodule"; name = "submodule";
check = x: isAttrs x || isFunction x; check = x: isAttrs x || isFunction x || path.check x;
merge = loc: defs: merge = loc: defs:
let (evalModules {
coerce = def: if isFunction def then def else { config = def; }; modules = allModules defs;
modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs; inherit specialArgs;
in (evalModules {
inherit modules;
args.name = last loc; args.name = last loc;
prefix = loc; prefix = loc;
}).config; }).config;
getSubOptions = prefix: (evalModules getSubOptions = prefix: (evalModules
{ modules = opts'; inherit prefix; { inherit modules prefix specialArgs;
# This is a work-around due to the fact that some sub-modules, # This is a work-around due to the fact that some sub-modules,
# such as the one included in an attribute set, expects a "args" # such as the one included in an attribute set, expects a "args"
# attribute to be given to the sub-module. As the option # attribute to be given to the sub-module. As the option
@ -394,13 +412,29 @@ rec {
# It shouldn't cause an issue since this is cosmetic for the manual. # It shouldn't cause an issue since this is cosmetic for the manual.
args.name = "name"; args.name = "name";
}).options; }).options;
getSubModules = opts'; getSubModules = modules;
substSubModules = m: submodule m; substSubModules = m: submoduleWith (attrs // {
functor = (defaultFunctor name) // { modules = m;
# Merging of submodules is done as part of mergeOptionDecls, as we have to annotate });
# each submodule with its location. functor = defaultFunctor name // {
payload = []; type = types.submoduleWith;
binOp = lhs: rhs: []; payload = {
modules = modules;
specialArgs = specialArgs;
shorthandOnlyDefinesConfig = shorthandOnlyDefinesConfig;
};
binOp = lhs: rhs: {
modules = lhs.modules ++ rhs.modules;
specialArgs =
let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs;
in if intersecting == {}
then lhs.specialArgs // rhs.specialArgs
else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\"";
shorthandOnlyDefinesConfig =
if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig
then lhs.shorthandOnlyDefinesConfig
else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values";
};
}; };
}; };

View file

@ -257,14 +257,68 @@
<listitem> <listitem>
<para> <para>
A set of sub options <replaceable>o</replaceable>. A set of sub options <replaceable>o</replaceable>.
<replaceable>o</replaceable> can be an attribute set or a function <replaceable>o</replaceable> can be an attribute set, a function
returning an attribute set. Submodules are used in composed types to returning an attribute set, or a path to a file containing such a value. Submodules are used in
create modular options. Submodule are detailed in composed types to create modular options. This is equivalent to
<literal>types.submoduleWith { modules = toList o; shorthandOnlyDefinesConfig = true; }</literal>.
Submodules are detailed in
<xref <xref
linkend='section-option-types-submodule' />. linkend='section-option-types-submodule' />.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term>
<varname>types.submoduleWith</varname> {
<replaceable>modules</replaceable>,
<replaceable>specialArgs</replaceable> ? {},
<replaceable>shorthandOnlyDefinesConfig</replaceable> ? false }
</term>
<listitem>
<para>
Like <varname>types.submodule</varname>, but more flexible and with better defaults.
It has parameters
<itemizedlist>
<listitem><para>
<replaceable>modules</replaceable>
A list of modules to use by default for this submodule type. This gets combined
with all option definitions to build the final list of modules that will be included.
<note><para>
Only options defined with this argument are included in rendered documentation.
</para></note>
</para></listitem>
<listitem><para>
<replaceable>specialArgs</replaceable>
An attribute set of extra arguments to be passed to the module functions.
The option <literal>_module.args</literal> should be used instead
for most arguments since it allows overriding. <replaceable>specialArgs</replaceable> should only be
used for arguments that can&apos;t go through the module fixed-point, because of
infinite recursion or other problems. An example is overriding the
<varname>lib</varname> argument, because <varname>lib</varname> itself is used
to define <literal>_module.args</literal>, which makes using
<literal>_module.args</literal> to define it impossible.
</para></listitem>
<listitem><para>
<replaceable>shorthandOnlyDefinesConfig</replaceable>
Whether definitions of this type should default to the <literal>config</literal>
section of a module (see <xref linkend='ex-module-syntax'/>) if it is an attribute
set. Enabling this only has a benefit when the submodule defines an option named
<literal>config</literal> or <literal>options</literal>. In such a case it would
allow the option to be set with <literal>the-submodule.config = "value"</literal>
instead of requiring <literal>the-submodule.config.config = "value"</literal>.
This is because only when modules <emphasis>don&apos;t</emphasis> set the
<literal>config</literal> or <literal>options</literal> keys, all keys are interpreted
as option definitions in the <literal>config</literal> section. Enabling this option
implicitly puts all attributes in the <literal>config</literal> section.
</para>
<para>
With this option enabled, defining a non-<literal>config</literal> section requires
using a function: <literal>the-submodule = { ... }: { options = { ... }; }</literal>.
</para></listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
</variablelist> </variablelist>
</section> </section>

View file

@ -112,12 +112,12 @@ in {
addresses = [ "tcp://192.168.0.10:51820" ]; addresses = [ "tcp://192.168.0.10:51820" ];
}; };
}; };
type = types.attrsOf (types.submodule ({ config, ... }: { type = types.attrsOf (types.submodule ({ name, ... }: {
options = { options = {
name = mkOption { name = mkOption {
type = types.str; type = types.str;
default = config._module.args.name; default = name;
description = '' description = ''
Name of the device Name of the device
''; '';
@ -175,7 +175,7 @@ in {
devices = [ "bigbox" ]; devices = [ "bigbox" ];
}; };
}; };
type = types.attrsOf (types.submodule ({ config, ... }: { type = types.attrsOf (types.submodule ({ name, ... }: {
options = { options = {
enable = mkOption { enable = mkOption {
@ -190,7 +190,7 @@ in {
path = mkOption { path = mkOption {
type = types.str; type = types.str;
default = config._module.args.name; default = name;
description = '' description = ''
The path to the folder which should be shared. The path to the folder which should be shared.
''; '';
@ -198,7 +198,7 @@ in {
id = mkOption { id = mkOption {
type = types.str; type = types.str;
default = config._module.args.name; default = name;
description = '' description = ''
The id of the folder. Must be the same on all devices. The id of the folder. Must be the same on all devices.
''; '';
@ -206,7 +206,7 @@ in {
label = mkOption { label = mkOption {
type = types.str; type = types.str;
default = config._module.args.name; default = name;
description = '' description = ''
The label of the folder. The label of the folder.
''; '';