Skip to content
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
66 changes: 65 additions & 1 deletion lib/modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,35 @@ let
decls
));

# Private filtering function used by evalModules
# TODO: Consider moving something like this to `lib.attrsets`?
filterAttrsRecursiveWith =
{
continueRecursing ? _: true,
leafPredicate ? _: _: true,
branchPredicate ? _: _: true,
} @ args:
set:
lib.pipe set [
attrNames
(lib.concatMap (name:
let
value = set.${name};
filtered = filterAttrsRecursiveWith args value;
in
if isAttrs value && continueRecursing value then
lib.optional (branchPredicate name filtered) {
inherit name;
value = filtered;
}
else
lib.optional (leafPredicate name value) {
inherit name value;
}
))
lib.listToAttrs
];

/* See https://nixos.org/manual/nixpkgs/unstable/#module-system-lib-evalModules
or file://./../doc/module-system/module-system.chapter.md

Expand All @@ -90,6 +119,9 @@ let
# there's _module.args. If specialArgs.modulesPath is defined it will be
# used as the base path for disabledModules.
specialArgs ? {}
, # Whether to merge only defined options into the final config value.
# I.e. options that either have a default value or at least one definition.
definedOptionsOnly ? false
, # `class`:
# A nominal type for modules. When set and non-null, this adds a check to
# make sure that only compatible modules are imported.
Expand Down Expand Up @@ -202,6 +234,24 @@ let
description = "Whether to check whether all option definitions have matching declarations.";
};

_module.definedOptionsOnly = mkOption {
type = types.bool;
internal = true;
# This option is readOnly because we cannot use its value to determine how to evaluate the modules
readOnly = true;
default = definedOptionsOnly;
description = ''
Whether to merge only defined options into the final `config` value.
I.e. options that either have a **default** value or **at least one** definition.

By default, undefined options are merged in but will throw a _"used but not defined"_ error if
their value is evaluated.

This option does not affect merging of freeform definitions that is done when `freeformType` is
non-null.
'';
};

_module.freeformType = mkOption {
type = types.nullOr types.optionType;
internal = true;
Expand Down Expand Up @@ -249,9 +299,23 @@ let

config =
let
isNotOption = v: ! isOption v;

# If definedOptionsOnly is enabled, we first filter for defined options
options' =
# We'd like to use the _module.definedOptionsOnly option, but that's infinitely recursive.
# Therefore, we use a function arg passed to evalModules instead.
if definedOptionsOnly then
filterAttrsRecursiveWith {
continueRecursing = isNotOption;
leafPredicate = n: opt: opt.isDefined;
branchPredicate = n: set: set != { };
} options
else
options;

# For definitions that have an associated option
declaredConfig = mapAttrsRecursiveCond (v: ! isOption v) (_: v: v.value) options;
declaredConfig = mapAttrsRecursiveCond isNotOption (_: v: v.value) options';

# If freeformType is set, this is for definitions that don't have an associated option
freeformConfig =
Expand Down
5 changes: 5 additions & 0 deletions lib/tests/modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ checkConfigError 'is not of type `boolean' config.submodule.config ./declare-sub
checkConfigError "In module ..*define-submoduleWith-shorthand.nix., you're trying to define a value of type \`bool'\n\s*rather than an attribute set for the option" config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-shorthand.nix
checkConfigOutput '^true$' config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-noshorthand.nix

## definedOptionsOnly should behave as expected
checkConfigOutput '^false$' config.submodule.defined ./definedOptionsOnly-submodule.nix
checkConfigOutput '^false$' config.submodule.hasDefault ./definedOptionsOnly-submodule.nix
checkConfigError "^error: attribute 'undefined' in selection path 'config.submodule.undefined' not found" config.submodule.undefined ./definedOptionsOnly-submodule.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
Expand Down
20 changes: 20 additions & 0 deletions lib/tests/modules/definedOptionsOnly-submodule.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{ lib, ... }:
{
options.submodule = lib.mkOption {
type = lib.types.submoduleWith {
definedOptionsOnly = true;
modules = [
{
options.defined = lib.mkOption { type = lib.types.bool; };
options.hasDefault = lib.mkOption {
type = lib.types.bool;
default = false;
};
options.undefined = lib.mkOption { type = lib.types.bool; };
}
];
};
};

config.submodule.defined = false;
}
3 changes: 2 additions & 1 deletion lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ rec {
{ modules
, specialArgs ? {}
, shorthandOnlyDefinesConfig ? false
, definedOptionsOnly ? false
, description ? null
, class ? null
}@attrs:
Expand All @@ -852,7 +853,7 @@ rec {
) defs;

base = evalModules {
inherit class specialArgs;
inherit class definedOptionsOnly specialArgs;
modules = [{
# This is a work-around for the fact that some sub-modules,
# such as the one included in an attribute set, expects an "args"
Expand Down