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: Let module declare options directly in bare submodule #156533

Merged
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
74 changes: 62 additions & 12 deletions lib/modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ let
catAttrs
concatLists
concatMap
count
concatStringsSep
elem
filter
findFirst
Expand Down Expand Up @@ -47,6 +47,20 @@ let
showOption
unknownModule
;

showDeclPrefix = loc: decl: prefix:
" - option(s) with prefix `${showOption (loc ++ [prefix])}' in module `${decl._file}'";
showRawDecls = loc: decls:
concatStringsSep "\n"
(sort (a: b: a < b)
(concatMap
(decl: map
(showDeclPrefix loc decl)
(attrNames decl.options)
)
decls
));

in

rec {
Expand Down Expand Up @@ -474,26 +488,61 @@ rec {
[{ inherit (module) file; inherit value; }]
) configs;

# Convert an option tree decl to a submodule option decl
optionTreeToOption = decl:
if isOption decl.options
then decl
else decl // {
options = mkOption {
type = types.submoduleWith {
modules = [ { options = decl.options; } ];
# `null` is not intended for use by modules. It is an internal
# value that means "whatever the user has declared elsewhere".
# This might become obsolete with https://github.com/NixOS/nixpkgs/issues/162398
shorthandOnlyDefinesConfig = null;
Copy link
Member

Choose a reason for hiding this comment

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

This should have a comment explaining the reasoning behind this: How it's a bit hacky but needed as a default, that null is only an internally valid value, that it's only safe here because of the merging function patch and because it's always going to be merged with at least one other module.

In the future this might become obsolete with #162398

Copy link
Member Author

Choose a reason for hiding this comment

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

Added

                # `null` is not intended for use by modules. It is an internal
                # value that means "whatever the user has declared elsewhere".
                # This might become obsolete with https://github.com/NixOS/nixpkgs/issues/162398

};
};
};

resultsByName = mapAttrs (name: decls:
# We're descending into attribute ‘name’.
let
loc = prefix ++ [name];
defns = defnsByName.${name} or [];
defns' = defnsByName'.${name} or [];
nrOptions = count (m: isOption m.options) decls;
optionDecls = filter (m: isOption m.options) decls;
in
if nrOptions == length decls then
if length optionDecls == length decls then
let opt = fixupOptionType loc (mergeOptionDecls loc decls);
in {
matchedOptions = evalOptionValue loc opt defns';
unmatchedDefns = [];
}
else if nrOptions != 0 then
let
firstOption = findFirst (m: isOption m.options) "" decls;
firstNonOption = findFirst (m: !isOption m.options) "" decls;
in
throw "The option `${showOption loc}' in `${firstOption._file}' is a prefix of options in `${firstNonOption._file}'."
else if optionDecls != [] then
if all (x: x.options.type.name == "submodule") optionDecls
# Raw options can only be merged into submodules. Merging into
# attrsets might be nice, but ambiguous. Suppose we have
# attrset as a `attrsOf submodule`. User declares option
# attrset.foo.bar, this could mean:
# a. option `bar` is only available in `attrset.foo`
# b. option `foo.bar` is available in all `attrset.*`
# c. reject and require "<name>" as a reminder that it behaves like (b).
# d. magically combine (a) and (c).
# All of the above are merely syntax sugar though.
then
let opt = fixupOptionType loc (mergeOptionDecls loc (map optionTreeToOption decls));
in {
matchedOptions = evalOptionValue loc opt defns';
unmatchedDefns = [];
}
roberth marked this conversation as resolved.
Show resolved Hide resolved
else
let
firstNonOption = findFirst (m: !isOption m.options) "" decls;
nonOptions = filter (m: !isOption m.options) decls;
in
throw "The option `${showOption loc}' in module `${(lib.head optionDecls)._file}' would be a parent of the following options, but its type `${(lib.head optionDecls).options.type.description or "<no description>"}' does not support nested options.\n${
showRawDecls loc nonOptions
}"
else
mergeModules' loc decls defns) declsByName;

Expand Down Expand Up @@ -753,13 +802,14 @@ rec {
compare = a: b: (a.priority or 1000) < (b.priority or 1000);
in sort compare defs';

/* Hack for backward compatibility: convert options of type
optionSet to options of type submodule. FIXME: remove
eventually. */
fixupOptionType = loc: opt:
let
options = opt.options or
(throw "Option `${showOption loc}' has type optionSet but has no option attribute, in ${showFiles opt.declarations}.");

# Hack for backward compatibility: convert options of type
# optionSet to options of type submodule. FIXME: remove
# eventually.
f = tp:
let optionSetIn = type: (tp.name == type) && (tp.functor.wrapped.name == "optionSet");
in
Expand Down
13 changes: 13 additions & 0 deletions lib/tests/modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ checkConfigError() {
checkConfigOutput '^false$' config.enable ./declare-enable.nix
checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*: true' config.enable ./define-enable.nix

checkConfigOutput '^1$' config.bare-submodule.nested ./declare-bare-submodule.nix ./declare-bare-submodule-nested-option.nix
checkConfigOutput '^2$' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-deep-option.nix
checkConfigOutput '^42$' config.bare-submodule.nested ./declare-bare-submodule.nix ./declare-bare-submodule-nested-option.nix ./declare-bare-submodule-deep-option.nix ./define-bare-submodule-values.nix
checkConfigOutput '^420$' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-nested-option.nix ./declare-bare-submodule-deep-option.nix ./define-bare-submodule-values.nix
checkConfigOutput '^2$' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-deep-option.nix ./define-shorthandOnlyDefinesConfig-true.nix
checkConfigError 'The option .bare-submodule.deep. in .*/declare-bare-submodule-deep-option.nix. is already declared in .*/declare-bare-submodule-deep-option-duplicate.nix' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-deep-option.nix ./declare-bare-submodule-deep-option-duplicate.nix

# Check integer types.
# unsigned
checkConfigOutput '^42$' config.value ./declare-int-unsigned-value.nix ./define-value-int-positive.nix
Expand Down Expand Up @@ -304,6 +311,12 @@ checkConfigOutput "10" config.processedToplevel ./raw.nix
checkConfigError "The option .multiple. is defined multiple times" config.multiple ./raw.nix
checkConfigOutput "bar" config.priorities ./raw.nix

## Option collision
checkConfigError \
'The option .set. in module .*/declare-set.nix. would be a parent of the following options, but its type .attribute set of signed integers. does not support nested options.\n\s*- option[(]s[)] with prefix .set.enable. in module .*/declare-enable-nested.nix.' \
config.set \
./declare-set.nix ./declare-enable-nested.nix

# Test that types.optionType merges types correctly
checkConfigOutput '^10$' config.theOption.int ./optionTypeMerging.nix
checkConfigOutput '^"hello"$' config.theOption.str ./optionTypeMerging.nix
Expand Down
10 changes: 10 additions & 0 deletions lib/tests/modules/declare-bare-submodule-deep-option-duplicate.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{ lib, ... }:
let
inherit (lib) mkOption types;
in
{
options.bare-submodule.deep = mkOption {
type = types.int;
default = 2;
};
}
10 changes: 10 additions & 0 deletions lib/tests/modules/declare-bare-submodule-deep-option.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{ lib, ... }:
let
inherit (lib) mkOption types;
in
{
options.bare-submodule.deep = mkOption {
type = types.int;
default = 2;
};
}
19 changes: 19 additions & 0 deletions lib/tests/modules/declare-bare-submodule-nested-option.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{ config, lib, ... }:
let
inherit (lib) mkOption types;
in
{
options.bare-submodule = mkOption {
type = types.submoduleWith {
shorthandOnlyDefinesConfig = config.shorthandOnlyDefinesConfig;
modules = [
{
options.nested = mkOption {
type = types.int;
default = 1;
};
}
];
};
};
}
18 changes: 18 additions & 0 deletions lib/tests/modules/declare-bare-submodule.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{ config, lib, ... }:
let
inherit (lib) mkOption types;
in
{
options.bare-submodule = mkOption {
type = types.submoduleWith {
modules = [ ];
shorthandOnlyDefinesConfig = config.shorthandOnlyDefinesConfig;
};
default = {};
};

# config-dependent options: won't recommend, but useful for making this test parameterized
options.shorthandOnlyDefinesConfig = mkOption {
default = false;
};
}
12 changes: 12 additions & 0 deletions lib/tests/modules/declare-set.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{ lib, ... }:

{
options.set = lib.mkOption {
default = { };
example = { a = 1; };
type = lib.types.attrsOf lib.types.int;
description = ''
Some descriptive text
'';
};
}
4 changes: 4 additions & 0 deletions lib/tests/modules/define-bare-submodule-values.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
bare-submodule.nested = 42;
bare-submodule.deep = 420;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ shorthandOnlyDefinesConfig = true; }
22 changes: 15 additions & 7 deletions lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -562,14 +562,18 @@ rec {
let
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);
shorthandToModule = if shorthandOnlyDefinesConfig == false
then value: value
else value: { config = value; };

allModules = defs: 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
if isFunction value
then setFunctionArgs
(args: lib.modules.unifyModuleSyntax file "${toString file}-${toString n}" (value args))
(functionArgs value)
else if isAttrs value
then
lib.modules.unifyModuleSyntax file "${toString file}-${toString n}" (shorthandToModule value)
else value
) defs;

Expand Down Expand Up @@ -637,7 +641,11 @@ rec {
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
if lhs.shorthandOnlyDefinesConfig == null
then rhs.shorthandOnlyDefinesConfig
else if rhs.shorthandOnlyDefinesConfig == null
then lhs.shorthandOnlyDefinesConfig
else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig
Comment on lines +644 to +648
Copy link
Member

Choose a reason for hiding this comment

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

Since null is now an allowed value, we should also define its semantics and document it. The semantics should be that if this value is needed to make a decision and it's null, an error is thrown. This means that if you do any shorthand definitions, you should get an error, unless another option declaration specifies the value of shorthandOnlyDefinesConfig, e.g. whereas

let
  lib = import ./lib;
in lib.evalModules {
  modules = [
    {
      options.foo = lib.mkOption {
        type = lib.types.submoduleWith {
          modules = [];
          shorthandOnlyDefinesConfig = null;
        };
      };

      config.foo.bar = 10;
    }
  ];
}

with this PR currently throws

error: value is null while a Boolean was expected

       at /home/infinisil/src/nixpkgs/lib/types.nix:538:23:

          537|           then setFunctionArgs (args: unify (value args)) (functionArgs value)
          538|           else unify (if shorthandOnlyDefinesConfig then { config = value; } else value);
             |                       ^
          539|
(use '--show-trace' to show detailed location information)

it should instead be something like

error: shorthandOnlyDefinesConfig is null
(use '--show-trace' to show detailed location information)

Copy link
Member Author

Choose a reason for hiding this comment

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

Since null is now an allowed value

I disagree. You'd have to read the implementation to find that null is acceptable in some situations.

To keep things simple and, I've decided to default it to true. I don't see much point in documenting this though.

then lhs.shorthandOnlyDefinesConfig
else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values";
};
Expand Down