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

lib.modules: add mkForAllItems #314058

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ let
mkRenamedOptionModule mkRenamedOptionModuleWith
mkMergedOptionModule mkChangedOptionModule
mkAliasOptionModule mkDerivedConfig doRename
mkAliasOptionModuleMD;
mkAliasOptionModuleMD mkForAllItems;
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
Expand Down
12 changes: 12 additions & 0 deletions lib/modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,17 @@ let
defaultOrderPriority = 1000;
mkAfter = mkOrder 1500;

/*
For use with options of type `attrsOf *`, `lazyAttrsOf *`, or `listOf *`.
Accepts a value that is merged with every element defined elsewhere. May
also accept a function that takes the attribute name or index (1-based),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This thread preallocated for bikeshedding the choice of 1-based indexing.

I made an arbitrary choice here but AFAIK (not far) Nixpkgs is somewhat incoherent in which of these it uses by default. imap defaults to 1-indexed (but has explicit imap0 and imap1 options), elemAt defaults to 0-indexed (and has no elemAt1 alternative). Values displayed to users seem to be 1-indexed.

the result of which is used as above.
*/
mkForAllItems = content:
{ _type = "forAllItems";
content = if isFunction content then content else _: content;
};

# Convenient property used to transfer all definitions and their
# properties from one option to another. This property is useful for
# renaming options, and also for including properties from another module
Expand Down Expand Up @@ -1421,6 +1432,7 @@ private //
mkDefault
mkDerivedConfig
mkFixStrictness
mkForAllItems
mkForce
mkIf
mkImageMediaOverride
Expand Down
3 changes: 3 additions & 0 deletions lib/tests/modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,9 @@ checkConfigOutput '^34|23$' options.submoduleLine34.declarationPositions.1.line
# nested options work
checkConfigOutput '^30$' options.nested.nestedLine30.declarationPositions.0.line ./declaration-positions.nix

# mkForAllItems
checkConfigOutput '^true$' config.result ./for-all-items.nix

cat <<EOF
====== module tests ======
$pass Pass
Expand Down
74 changes: 74 additions & 0 deletions lib/tests/modules/for-all-items.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{ config, lib, ... }:
let
inherit (lib.types) attrsOf bool either int lazyAttrsOf listOf str submodule;
testSubmodule = submodule {
options = {
x = lib.mkOption { type = listOf int; };
y = lib.mkOption { type = int; };
z = lib.mkOption { type = either int str; };
};
};
testAttrDefs = lib.mkMerge [
{
a.x = lib.mkBefore [ 0 ];
b.y = lib.mkForce 3;
b.z = "4";
}
(lib.modules.mkForAllItems {
x = [ 1 ];
y = 2;
})
(lib.modules.mkForAllItems (name: {
z = lib.mkDefault name;
}))
];
testListDefs = lib.mkMerge [
[
{
x = lib.mkBefore [ 0 ];
}
{
y = lib.mkForce 3;
z = "4";
}
]
(lib.modules.mkForAllItems {
x = [ 1 ];
y = 2;
})
(lib.modules.mkForAllItems (name: {
z = lib.mkDefault name;
}))
];
expected = {
a = {
x = [ 0 1 ];
y = 2;
z = "a";
};
b = {
x = [ 1 ];
y = 3;
z = "4";
};
};
in
{
options = {
testAttrs = lib.mkOption { type = attrsOf testSubmodule; };
testLazyAttrs = lib.mkOption { type = lazyAttrsOf testSubmodule; };
testList = lib.mkOption { type = listOf testSubmodule; };
result = lib.mkOption { type = bool; };
};

config = {
testAttrs = testAttrDefs;
testLazyAttrs = testAttrDefs;
testList = testListDefs;

result =
config.testAttrs == expected &&
config.testLazyAttrs == expected &&
config.testList == [ (expected.a // { z = 1; }) expected.b ];
};
}
33 changes: 26 additions & 7 deletions lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ let
imap1
last
length
partition
tail
;
inherit (lib.attrsets)
Expand Down Expand Up @@ -548,17 +549,23 @@ rec {
name = "listOf";
description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
descriptionClass = "composite";
check = isList;
check = x: isList x || x._type or "" == "forAllItems";
merge = loc: defs:
let
# partitionedDefs.right: all forAllItems definitions
# partitionedDefs.wrong: all regular definitions
partitionedDefs = partition (d: d.value._type or "" == "forAllItems") defs;
extraDefs = idx: map (def: { inherit (def) file; value = def.value.content idx; }) partitionedDefs.right;
in
map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def:
imap1 (m: def':
(mergeDefinitions
(loc ++ ["[definition ${toString n}-entry ${toString m}]"])
elemType
[{ inherit (def) file; value = def'; }]
([{ inherit (def) file; value = def'; }] ++ extraDefs m)
).optionalValue
) def.value
) defs)));
) partitionedDefs.wrong)));
emptyValue = { value = []; };
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
getSubModules = elemType.getSubModules;
Expand All @@ -581,11 +588,17 @@ rec {
descriptionClass = "composite";
check = isAttrs;
merge = loc: defs:
let
# partitionedDefs.right: all forAllItems definitions
# partitionedDefs.wrong: all regular definitions
partitionedDefs = partition (d: d.value._type or "" == "forAllItems") defs;
extraDefs = name: map (def: { inherit (def) file; value = def.value.content name; }) partitionedDefs.right;
in
mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
(mergeDefinitions (loc ++ [name]) elemType defs).optionalValue
(mergeDefinitions (loc ++ [name]) elemType (defs ++ extraDefs name)).optionalValue
)
# Push down position info.
(map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs)));
(map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) partitionedDefs.wrong)));
emptyValue = { value = {}; };
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
getSubModules = elemType.getSubModules;
Expand All @@ -605,13 +618,19 @@ rec {
descriptionClass = "composite";
check = isAttrs;
merge = loc: defs:
let
# partitionedDefs.right: all forAllItems definitions
# partitionedDefs.wrong: all regular definitions
partitionedDefs = partition (d: d.value._type or "" == "forAllItems") defs;
extraDefs = name: map (def: { inherit (def) file; value = def.value.content name; }) partitionedDefs.right;
in
zipAttrsWith (name: defs:
let merged = mergeDefinitions (loc ++ [name]) elemType defs;
let merged = mergeDefinitions (loc ++ [name]) elemType (defs ++ extraDefs name);
# 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);
(map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) partitionedDefs.wrong);
emptyValue = { value = {}; };
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
getSubModules = elemType.getSubModules;
Expand Down
35 changes: 35 additions & 0 deletions nixos/doc/manual/development/option-def.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,38 @@ they were declared in separate modules. This can be done using
];
}
```

## Defining Over All Elements/Attributes {#sec-option-definitions-for-all-items}

For options that take lists or attrsets, it may be useful to define
defaults or overrides on all of the elements or attributes of that option
independently of the population of the list or set. For example, one
module can define a list of `swapDevices`, while another module can
define that for every element of the list of `swapDevices`,
`swapDevices.*.discardPolicy` should have a particular value. This can be
done using `mkForAllItems`:

```nix
{
swapDevices = mkForAllItems {
discardPolicy = mkDefault "once";
};
}
```

The argument to `mkForAllItems` will be merged with every element of the
list, or every attribute of the attrset. As usual, use `mkDefault`,
`mkForce`, `mkBefore`, `mkAfter`, etc. to control how multiple
definitions of an option are combined.

`mkForAllItems` can also accept a function which will receive the element
index (starting at 1) or attribute name for each element or attribute on
which it is used. The result of this function is used as described above. For example:

```nix
{
services.wordpress.sites = mkForAllItems (name: {
uploadsDir = mkDefault "/mnt/wordpress-data/${name}/uploads";
});
}
```
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2411.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

- New library function `lib.mkForAllItems` allows for defining values uniformly on lists and attrsets; see [documentation](#sec-option-definitions-for-all-items).

- To facilitate dependency injection, the `imgui` package now builds a static archive using vcpkg' CMake rules.
The derivation now installs "impl" headers selectively instead of by a wildcard.
Use `imgui.src` if you just want to access the unpacked sources.