-
-
Notifications
You must be signed in to change notification settings - Fork 14.4k
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
lib.modules: Let module declare options directly in bare submodule #156533
Conversation
dd5a249
to
8040564
Compare
3a25e26
to
5a617d0
Compare
lib/modules.nix
Outdated
type = types.submoduleWith { | ||
modules = [ { options = decl.options; } ]; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This leads to an error when a users option is declared with just types.submodule
:
let
lib = import ./lib;
in lib.evalModules {
modules = [
{
_file = "a.nix";
options.foo = lib.mkOption {
type = lib.types.submodule {};
default = {};
};
}
{
_file = "b.nix";
options.foo.bar = lib.mkOption {
type = lib.types.str;
};
}
];
}
gives
error: A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values
(use '--show-trace' to show detailed location information)
Not sure if this can be fixed nicely or if it even makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
submodule
is the legacy one, but yeah, not great. Seems like "don't care" needs to be representable and pick true
as the default. false
was probably a bad idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added null
support, but only when merging.
I wonder if it would be feasible for option trees (like |
It occurred to me too, but it wasn't clear to me whether it will be effective. |
a855279
to
d77b16d
Compare
if lhs.shorthandOnlyDefinesConfig == null | ||
then rhs.shorthandOnlyDefinesConfig | ||
else if rhs.shorthandOnlyDefinesConfig == null | ||
then lhs.shorthandOnlyDefinesConfig | ||
else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
... where a bare submodule is an option that has a type like `submoduleWith x`, as opposed to `attrsOf (submoduleWith x)`. This makes migration unnecessary when introducing a freeform type in an existing option tree. Closes NixOS#146882
This scans the options with fewer function calls, improving performance. It also removes a let Env from the happy flow of the new logic.
This should save about four calls per module.
1d0be4d
to
2050669
Compare
lib/modules.nix
Outdated
# d. magically combine (a) and (c). | ||
# All of the above are merely syntax sugar though. | ||
then | ||
let opt = mergeOptionDecls loc (map optionTreeToOption decls); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a lot of trial and error, trying to prove why fixupOptionType
should be used here or not, I figured it out: It's needed for the sake of file locations in error messages. E.g. with
let lib = import ./lib;
in lib.evalModules {
modules = [
{
_file = "a.nix";
options.foo = lib.mkOption {
type = lib.types.submodule {
options.x = lib.mkOption {
type = lib.types.int;
};
};
default = {};
};
}
{
_file = "b.nix";
options.foo.x = lib.mkOption {
type = lib.types.str;
};
}
];
}
it currently produces this error:
$ nix-instantiate --eval -A config.foo.x
error: The option `foo.x' in `<unknown-file>' is already declared in `<unknown-file>'.
(use '--show-trace' to show detailed location information)
whereas with a fixupOptionType
here, it's
error: The option `foo.x' in `a.nix' is already declared in `b.nix'.
(use '--show-trace' to show detailed location information)
would be good to have a test case for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
options = mkOption { | ||
type = types.submoduleWith { | ||
modules = [ { options = decl.options; } ]; | ||
shorthandOnlyDefinesConfig = null; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
…tion" This reverts commit 6b077c4. Thanks Infinisil for discovering this problem: > After a lot of trial and error, trying to prove why fixupOptionType should > be used here or not, I figured it out: It's needed for the sake of file > locations in error messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good!
Could you explain the example for systemd a bit? Would this allow something like:
(That would be amazing) |
@bobvanderlinden Yes, while this was already valid, it can now be mixed with "top-level" options like
This was not allowed to coexist with your example. |
Today pennae said on IRC:
and it struck me that such a refactoring probably increases strictness by merging attrsets too soon. I don't think this insight affects this PR itself, because while it makes the introduction of submodules in existing option trees more feasible, it is not this change but those introductions that cause the increased strictness. Perhaps a better solution would be to treat bare submodules as option trees with a prefix. This eliminates the extra strictness caused by bare submodule introductions. |
tl;dr we can improve laziness by translating bare submodules to option trees rather than the other way around, or we can implement a special "import at prefix" that does only what we need. Submodules are more than just option trees though, so a "prefixing" operation on modules seems like it'd need quite a lot of code. |
This pull request has been mentioned on NixOS Discourse. There might be relevant details there: https://discourse.nixos.org/t/genodepkgs-extending-nixpkgs-nixos-to-genode/8779/7 |
This PR reinterprets parts of the option tree based on information of explicitly declared options. I call it static merging for two reasons:
Motivating example: hercules-ci/flake-parts#142 |
I wonder if we could allow for merging an option with its default values, no matter what the type is. |
Motivation for this change
Let module declare options directly in bare submodule, where a bare submodule is an option that has a type like
submoduleWith x
, as opposed toattrsOf (submoduleWith x)
.This makes migration unnecessary when introducing a freeform type in an existing option tree and it removes some unwieldy syntax. Refs https://github.com/NixOS/nixpkgs/pull/156503/files#r790768951
Mainly, it allows the introduction of a freeformType at any place in an existing options tree without breaking compatibility with existing option declarations inside the new freeformType.
Another use case is for modules that should be imported in multiple locations in the options tree, such as
systemd
andsystemd.user
.Closes #146882
Things done
sandbox = true
set innix.conf
? (See Nix manual)nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD"
. Note: all changes have to be committed, also see nixpkgs-review usage./result/bin/
)nixos/doc/manual/md-to-db.sh
to update generated release notes