Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce types.raw and types.unconditional #132448

Closed
wants to merge 6 commits into from
Closed
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
10 changes: 7 additions & 3 deletions lib/modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ rec {
# support for that, in turn it's lazy in its values. This means e.g.
# a `_module.args.pkgs = import (fetchTarball { ... }) {}` won't
# start a download when `pkgs` wasn't evaluated.
type = types.lazyAttrsOf types.unspecified;
type = types.lazyAttrsOf types.raw;
internal = true;
description = "Arguments passed to each module.";
};
Expand Down Expand Up @@ -642,16 +642,20 @@ rec {

# Type-check the remaining definitions, and merge them. Or throw if no definitions.
mergedValue =
if isDefined then
if defsFinal != [] then
if all (def: type.check def.value) defsFinal then type.merge loc defsFinal
else let allInvalid = filter (def: ! type.check def.value) defsFinal;
in throw "A definition for option `${showOption loc}' is not of type `${type.description}'. Definition values:${showDefs allInvalid}"
else if type.emptyValue ? value then
type.emptyValue.value
else
# (nixos-option detects this specific error message and gives it special
# handling. If changed here, please change it there too.)
throw "The option `${showOption loc}' is used but not defined.";

isDefined = defsFinal != [];
# Note: We use `or true` in case `lib.types` from an older nixpkgs version
# that doesn't set `conditional` is used. Avoids some trouble down the road
isDefined = type.conditional or true -> defsFinal != [];

optionalValue =
if isDefined then { value = mergedValue; }
Expand Down
25 changes: 25 additions & 0 deletions lib/tests/modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,31 @@ checkConfigOutput '^"a b"$' config.resultFoo ./declare-variants.nix ./define-var
checkConfigOutput '^"a y z"$' config.resultFooBar ./declare-variants.nix ./define-variant.nix
checkConfigOutput '^"a b c"$' config.resultFooFoo ./declare-variants.nix ./define-variant.nix

## emptyValue's
checkConfigOutput "[ ]" config.list ./emptyValues.nix
checkConfigOutput "{ }" config.attrs ./emptyValues.nix
checkConfigOutput "null" config.null ./emptyValues.nix
checkConfigOutput "{ }" config.submodule ./emptyValues.nix
# These types don't have empty values
checkConfigError 'The option .int. is used but not defined' config.int ./emptyValues.nix
checkConfigError 'The option .nonEmptyList. is used but not defined' config.nonEmptyList ./emptyValues.nix

## types.raw
checkConfigOutput "{ foo = <CODE>; }" config.unprocessedNesting ./raw.nix
checkConfigOutput "10" config.processedToplevel ./raw.nix
checkConfigError "The unique option .multiple. is defined multiple times" config.multiple ./raw.nix
checkConfigOutput "bar" config.priorities ./raw.nix

## types.unconditional
checkConfigOutput "10" config.ifTrue ./unconditional.nix
checkConfigError "The option .ifFalse. is used but not defined." config.ifFalse ./unconditional.nix
checkConfigOutput "10" config.attrsIfTrue.foo ./unconditional.nix
checkConfigError "The option .attrsIfFalse.foo. is used but not defined." config.attrsIfFalse.foo ./unconditional.nix
checkConfigOutput "[ \"foo\" ]" config.attrKeys ./unconditional.nix
checkConfigOutput "10" config.listIfTrue.0 ./unconditional.nix
checkConfigError "The option .listIfFalse.\[definition 1-entry 1\]. is used but not defined." config.listIfFalse.0 ./unconditional.nix
checkConfigOutput "1" config.listLength ./unconditional.nix

cat <<EOF
====== module tests ======
$pass Pass
Expand Down
24 changes: 24 additions & 0 deletions lib/tests/modules/emptyValues.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{ lib, ... }: {

options = {
int = lib.mkOption {
type = lib.types.int;
};
list = lib.mkOption {
type = lib.types.listOf lib.types.int;
};
nonEmptyList = lib.mkOption {
type = lib.types.nonEmptyListOf lib.types.int;
};
attrs = lib.mkOption {
type = lib.types.attrsOf lib.types.int;
};
null = lib.mkOption {
type = lib.types.nullOr lib.types.int;
};
submodule = lib.mkOption {
type = lib.types.submodule {};
};
};

}
30 changes: 30 additions & 0 deletions lib/tests/modules/raw.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{ lib, ... }: {

options = {
processedToplevel = lib.mkOption {
type = lib.types.raw;
};
unprocessedNesting = lib.mkOption {
type = lib.types.raw;
};
multiple = lib.mkOption {
type = lib.types.raw;
};
priorities = lib.mkOption {
type = lib.types.raw;
};
};

config = {
processedToplevel = lib.mkIf true 10;
unprocessedNesting.foo = throw "foo";
multiple = lib.mkMerge [
"foo"
"foo"
];
priorities = lib.mkMerge [
"foo"
(lib.mkForce "bar")
];
};
}
40 changes: 40 additions & 0 deletions lib/tests/modules/unconditional.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{ lib, config, ... }: {

options = {
ifTrue = lib.mkOption {
type = lib.types.unconditional lib.types.int;
};
ifFalse = lib.mkOption {
type = lib.types.unconditional lib.types.int;
};
attrsIfTrue = lib.mkOption {
type = lib.types.attrsOf (lib.types.unconditional lib.types.int);
};
attrsIfFalse = lib.mkOption {
type = lib.types.attrsOf (lib.types.unconditional lib.types.int);
};
attrKeys = lib.mkOption {
default = lib.attrNames config.attrsIfFalse;
};
listIfTrue = lib.mkOption {
type = lib.types.listOf (lib.types.unconditional lib.types.int);
};
listIfFalse = lib.mkOption {
type = lib.types.listOf (lib.types.unconditional lib.types.int);
};
listLength = lib.mkOption {
default = lib.length config.listIfFalse;
};
};

config = {
ifTrue = lib.mkIf true 10;
ifFalse = lib.mkIf false 10;
attrsIfTrue.foo = lib.mkIf true 10;
attrsIfFalse.foo = lib.mkIf false 10;
listIfTrue = [ (lib.mkIf true 10) ];
listIfFalse = [ (lib.mkIf false 10) ];
};


}
45 changes: 18 additions & 27 deletions lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,12 @@ rec {
# issue deprecation warnings recursively. Can also be used to reuse
# nested types
nestedTypes ? {}
# Whether `mkIf` assignments can cause the definiton to be optional in
# the parent option. If `processed` is false, this is implicitly false too
, conditional ? true
}:
{ _type = "option-type";
inherit name check merge emptyValue getSubOptions getSubModules substSubModules typeMerge functor deprecationMessage nestedTypes;
inherit name check merge emptyValue getSubOptions getSubModules substSubModules typeMerge functor deprecationMessage nestedTypes conditional;
description = if description == null then name else description;
};

Expand All @@ -162,6 +165,17 @@ rec {
# nixos/doc/manual/development/option-types.xml!
types = rec {

raw = mkOptionType rec {
name = "raw";
description = "raw value";
check = value: true;
merge = mergeOneOption;
};

unconditional = elemType: elemType // {
conditional = false;
};

anything = mkOptionType {
name = "anything";
description = "anything";
Expand Down Expand Up @@ -377,7 +391,7 @@ rec {
).optionalValue
) def.value
) defs)));
emptyValue = { value = {}; };
emptyValue = { value = []; };
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
getSubModules = elemType.getSubModules;
substSubModules = m: listOf (elemType.substSubModules m);
Expand All @@ -389,7 +403,7 @@ rec {
let list = addCheck (types.listOf elemType) (l: l != []);
in list // {
description = "non-empty " + list.description;
# Note: emptyValue is left as is, because another module may define an element.
emptyValue = { };
};

attrsOf = elemType: mkOptionType rec {
Expand All @@ -410,30 +424,7 @@ rec {
nestedTypes.elemType = elemType;
};

# A version of attrsOf that's lazy in its values at the expense of
# conditional definitions not working properly. E.g. defining a value with
# `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with
# attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an
# error that it's not defined. Use only if conditional definitions don't make sense.
lazyAttrsOf = elemType: mkOptionType rec {
name = "lazyAttrsOf";
description = "lazy attribute set of ${elemType.description}s";
check = isAttrs;
merge = loc: defs:
zipAttrsWith (name: defs:
let merged = mergeDefinitions (loc ++ [name]) elemType defs;
# mergedValue will trigger an appropriate error when accessed
in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
)
# Push down position info.
(map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs);
emptyValue = { value = {}; };
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
getSubModules = elemType.getSubModules;
substSubModules = m: lazyAttrsOf (elemType.substSubModules m);
functor = (defaultFunctor name) // { wrapped = elemType; };
nestedTypes.elemType = elemType;
};
lazyAttrsOf = elemType: attrsOf (unconditional elemType);

# TODO: drop this in the future:
loaOf = elemType: types.attrsOf elemType // {
Expand Down
22 changes: 22 additions & 0 deletions nixos/doc/manual/development/option-types.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ merging is handled.
```
:::

`types.raw`

: A type that accepts exactly a single arbitrary value. This type is
recommended for options whose only value is defined by the same module
as the option, because its structure is known ahead-of time and doesn't
need to be typechecked internally. Especially useful for values where
type checking would be too expensive, or where type checking is handled
in another way.

`types.attrs`

: A free-form attribute set.
Expand Down Expand Up @@ -201,6 +210,19 @@ Value types are types that take a value parameter.
requires using a function:
`the-submodule = { ... }: { options = { ... }; }`.

`types.unconditional` *`t`*

: The same as *`t`*, but without full support for conditional definitions
using `lib.mkIf <cond>`. This has the advantage that when used as the
element type in `types.attrsOf` and `types.listOf`, the attribute keys and
the list length respectively can be known without evaluating each
individual attribute/list value, keeping these structures lazy.
For instance, this prevents a case of infinite recursion by allowing elements
in an attribute set to reference other elements of the same attribute set
via the `config` module argument.
If `lib.mkIf <cond>` is used on such a type, the value is assumed to exist,
but an error is thrown when evaluated and the condition is false.

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

Composed types are types that take a type as parameter. `listOf
Expand Down
41 changes: 41 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 @@ -92,6 +92,22 @@
</programlisting>
</listitem>
</varlistentry>
<varlistentry>
<term>
<literal>types.raw</literal>
</term>
<listitem>
<para>
A type that accepts exactly a single arbitrary value. This
type is recommended for options whose only value is defined
by the same module as the option, because its structure is
known ahead-of time and doesn’t need to be typechecked
internally. Especially useful for values where type checking
would be too expensive, or where type checking is handled in
another way.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>
<literal>types.attrs</literal>
Expand Down Expand Up @@ -392,6 +408,31 @@
</itemizedlist>
</listitem>
</varlistentry>
<varlistentry>
<term>
<literal>types.unconditional</literal>
<emphasis><literal>t</literal></emphasis>
</term>
<listitem>
<para>
The same as <emphasis><literal>t</literal></emphasis>, but
without full support for conditional definitions using
<literal>lib.mkIf &lt;cond&gt;</literal>. This has the
advantage that when used as the element type in
<literal>types.attrsOf</literal> and
<literal>types.listOf</literal>, the attribute keys and the
list length respectively can be known without evaluating
each individual attribute/list value, keeping these
structures lazy. For instance, this prevents a case of
infinite recursion by allowing elements in an attribute set
to reference other elements of the same attribute set via
the <literal>config</literal> module argument. If
<literal>lib.mkIf &lt;cond&gt;</literal> is used on such a
type, the value is assumed to exist, but an error is thrown
when evaluated and the condition is false.
</para>
</listitem>
</varlistentry>
</variablelist>
</section>
<section xml:id="sec-option-types-composed">
Expand Down