Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ rec {
config = addFreeformType (addMeta (m.config or {}));
}
else
lib.throwIfNot (isAttrs m) "module ${file} (${key}) does not look like a module."
{ _file = toString m._file or file;
key = toString m.key or key;
disabledModules = m.disabledModules or [];
Expand Down
7 changes: 7 additions & 0 deletions lib/tests/modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ checkConfigOutput '^"submodule"$' options.submodule.type.description ./declare-s
## Paths should be allowed as values and work as expected
checkConfigOutput '^true$' config.submodule.enable ./declare-submoduleWith-path.nix

## deferredModule
# default module is merged into nodes.foo
checkConfigOutput '"beta"' config.nodes.foo.settingsDict.c ./deferred-module.nix
# errors from the default module are reported with accurate location
checkConfigError 'In `the-file-that-contains-the-bad-config.nix, via option default'\'': "bogus"' config.nodes.foo.bottom ./deferred-module.nix
checkConfigError '.*lib/tests/modules/deferred-module-error.nix, via option deferred [(]:anon-1:anon-1:anon-1[)] does not look like a module.' config.result ./deferred-module-error.nix

# Check the file location information is propagated into submodules
checkConfigOutput the-file.nix config.submodule.internalFiles.0 ./submoduleFiles.nix

Expand Down
20 changes: 20 additions & 0 deletions lib/tests/modules/deferred-module-error.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{ config, lib, ... }:
let
inherit (lib) types mkOption setDefaultModuleLocation evalModules;
inherit (types) deferredModule lazyAttrsOf submodule str raw enum;
in
{
options = {
deferred = mkOption {
type = deferredModule;
};
result = mkOption {
default = (evalModules { modules = [ config.deferred ]; }).config.result;
};
};
config = {
deferred = { ... }:
# this should be an attrset, so this fails
true;
};
}
58 changes: 58 additions & 0 deletions lib/tests/modules/deferred-module.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{ lib, ... }:
let
inherit (lib) types mkOption setDefaultModuleLocation;
inherit (types) deferredModule lazyAttrsOf submodule str raw enum;
in
{
imports = [
# generic module, declaring submodules:
# - nodes.<name>
# - default
# where all nodes include the default
({ config, ... }: {
_file = "generic.nix";
options.nodes = mkOption {
type = lazyAttrsOf (submodule { imports = [ config.default ]; });
default = {};
};
options.default = mkOption {
type = deferredModule;
default = { };
description = ''
Module that is included in all nodes.
'';
};
})

{
_file = "default-1.nix";
default = { config, ... }: {
options.settingsDict = lib.mkOption { type = lazyAttrsOf str; default = {}; };
options.bottom = lib.mkOption { type = enum []; };
};
}

{
_file = "default-a-is-b.nix";
default = ./define-settingsDict-a-is-b.nix;
}

{
_file = "nodes-foo.nix";
nodes.foo.settingsDict.b = "beta";
}

{
_file = "the-file-that-contains-the-bad-config.nix";
default.bottom = "bogus";
}

{
_file = "nodes-foo-c-is-a.nix";
nodes.foo = { config, ... }: {
settingsDict.c = config.settingsDict.a;
};
}

];
}
3 changes: 3 additions & 0 deletions lib/tests/modules/define-settingsDict-a-is-b.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{ config, ... }: {
settingsDict.a = config.settingsDict.b;
}
30 changes: 30 additions & 0 deletions lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,36 @@ rec {
modules = toList modules;
};

# A module to be imported in some other part of the configuration.
deferredModule = deferredModuleWith { };

# A module to be imported in some other part of the configuration.
# `staticModules`' options will be added to the documentation, unlike
# options declared via `config`.
deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType {
name = "deferredModule";
description = "module";
check = x: isAttrs x || isFunction x || path.check x;
merge = loc: defs: {
imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs;
};
inherit (submoduleWith { modules = staticModules; })
getSubOptions
getSubModules;
substSubModules = m: deferredModuleWith (attrs // {
staticModules = m;
});
functor = defaultFunctor "deferredModuleWith" // {
type = types.deferredModuleWith;
payload = {
inherit staticModules;
};
binOp = lhs: rhs: {
staticModules = lhs.staticModules ++ rhs.staticModules;
};
};
};

# The type of a type!
optionType = mkOptionType {
name = "optionType";
Expand Down
19 changes: 19 additions & 0 deletions nixos/doc/manual/development/option-types.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,25 @@ Value types are types that take a value parameter.
requires using a function:
`the-submodule = { ... }: { options = { ... }; }`.

`types.deferredModule`

: Whereas `submodule` represents an option tree, `deferredModule` represents
a module value, such as a module file or a configuration.

It can be set multiple times.

Module authors can use its value in `imports`, in `submoduleWith`'s `modules`
or in `evalModules`' `modules` parameter, among other places.

Note that `imports` must be evaluated before the module fixpoint. Because
of this, deferred modules can only be imported into "other" fixpoints, such
as submodules.

One use case for this type is the type of a "default" module that allow the
user to affect all submodules in an `attrsOf submodule` at once. This is
more convenient and discoverable than expecting the module user to
type-merge with the `attrsOf submodule` option.

## Composed Types {#sec-option-types-composed}

Composed types are types that take a type as parameter. `listOf
Expand Down
37 changes: 37 additions & 0 deletions nixos/doc/manual/from_md/development/option-types.section.xml
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,43 @@
</itemizedlist>
</listitem>
</varlistentry>
<varlistentry>
<term>
<literal>types.deferredModule</literal>
</term>
<listitem>
<para>
Whereas <literal>submodule</literal> represents an option
tree, <literal>deferredModule</literal> represents a module
value, such as a module file or a configuration.
</para>
<para>
It can be set multiple times.
</para>
<para>
Module authors can use its value in
<literal>imports</literal>, in
<literal>submoduleWith</literal><quote>s
<literal>modules</literal> or in
<literal>evalModules</literal></quote>
<literal>modules</literal> parameter, among other places.
</para>
<para>
Note that <literal>imports</literal> must be evaluated
before the module fixpoint. Because of this, deferred
modules can only be imported into <quote>other</quote>
fixpoints, such as submodules.
</para>
<para>
One use case for this type is the type of a
<quote>default</quote> module that allow the user to affect
all submodules in an <literal>attrsOf submodule</literal> at
once. This is more convenient and discoverable than
expecting the module user to type-merge with the
<literal>attrsOf submodule</literal> option.
</para>
</listitem>
</varlistentry>
</variablelist>
</section>
<section xml:id="sec-option-types-composed">
Expand Down