-
-
Notifications
You must be signed in to change notification settings - Fork 13.9k
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
nixos: use structured attrs for assertions
/warnings
#342372
base: master
Are you sure you want to change the base?
Conversation
# This might be replaced when tagged submodules or similar are available, | ||
# see https://github.com/NixOS/nixpkgs/pull/254790 | ||
checkedAssertionItemType = let | ||
check = x: x ? assertion && x ? message; | ||
in lib.types.addCheck assertionItemType check; |
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.
There is a limitation here, where the submodule won't be accepted unless it is in the form of an attrset. Function submodules and submodules by path will not be accepted by the check, and ignored. Not sure if there is much to do about it? I don't think anyone will write either of the following anytime soon:
{
assertions.a.b.c = { ... }: {
assertion = true;
message = "....";
};
assertions.x.y.z = ./myAssertion.nix;
}
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.
You don't need an extra config
fixpoint, or options
introspection, or anything like that, so a record would be a better type constructor for this, instead of submodule
. It does the thing you need, and basically no redundant or confusing features.
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.
I'm currently using both config
and options
for the lazyness toggle in a submodule fixpoint though. I'll have a look at it, but I'm not sure how well it would work with the current solution.
cab0fe9
to
b01fa5a
Compare
Can't review right now, but I already have some (incomplete) thoughts:
|
ofborg seems to fail due to an assertion message that is now being evaluated eagerly. It is building
# nixos/modules/misc/nixpkgs.nix:354
let
nixosExpectedSystem =
if config.nixpkgs.crossSystem != null
then config.nixpkgs.crossSystem.system or (lib.systems.parse.doubleFromSystem (lib.systems.parse.mkSystemFromString config.nixpkgs.crossSystem.config))
else config.nixpkgs.localSystem.system or (lib.systems.parse.doubleFromSystem (lib.systems.parse.mkSystemFromString config.nixpkgs.localSystem.config));
nixosOption =
if config.nixpkgs.crossSystem != null
then "nixpkgs.crossSystem"
else "nixpkgs.localSystem";
pkgsSystem = finalPkgs.stdenv.targetPlatform.system;
in {
assertion = constructedByMe -> !hasPlatform -> nixosExpectedSystem == pkgsSystem;
message = "The NixOS nixpkgs.pkgs option was set to a Nixpkgs invocation that compiles to target system ${pkgsSystem} but NixOS was configured for system ${nixosExpectedSystem} via NixOS option ${nixosOption}. The NixOS system settings must match the Nixpkgs target system.";
} Eventually leading to triggering the # nixos/modules/misc/nixpkgs.nix:283
system = lib.mkOption {
type = lib.types.str;
example = "i686-linux";
default =
if opt.hostPlatform.isDefined
then
throw ''
Neither ${opt.system} nor any other option in nixpkgs.* is meant
to be read by modules and configurations.
Use pkgs.stdenv.hostPlatform instead.
''
else
throw ''
Neither ${opt.hostPlatform} nor the legacy option ${opt.system} has been set.
You can set ${opt.hostPlatform} in hardware-configuration.nix by re-running
a recent version of nixos-generate-config.
The option ${opt.system} is still fully supported for NixOS 22.05 interoperability,
but will be deprecated in the future, so we recommend to set ${opt.hostPlatform}.
'';
defaultText = lib.literalMD ''
Traditionally `builtins.currentSystem`, but unset when invoking NixOS through `lib.nixosSystem`.
'';
...
}; Sorry to ping you about this @roberth, but I could really use some help to decide how to go about this. I currently see 3 possible solutions:
I've verified that the command (and then probably also ofborg) evaluates completely if I remove |
# This might be replaced when tagged submodules or similar are available, | ||
# see https://github.com/NixOS/nixpkgs/pull/254790 | ||
checkedAssertionItemType = let | ||
check = x: x ? assertion && x ? message; |
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.
Maybe removing check
is sufficient to make it lazy again?
The unfortunate thing about strictness is that it is not lazy, unless you're checking something that was going to be evaluated anyway, such as most check that are done in the merge function (though for lists or attrsets, merge
can still evaluate too much if not careful).
I see that you're trying to do a good job with thorough checking, which as a fan of good DX and static checking, I can very much relate to. However, the module system is very different from most other languages or configuration systems, by virtue of its fairly unique architecture, doing runtime checks in a lazy language. This has profound implications for the implementation trade-offs. The module system is very sensitive when it comes to the specifics of its behavior, especially regarding strictness (laziness). It is very important for it to be as lazy as possible, so that it avoids running into errors that shouldn't be triggered, or infinite recursions. We also need to care for users who write their own modules with
As said, we must not regress in laziness, so this should be solved in the module system; not the code that uses the module system.
Lazy and unchecked are fast. The module system will check the message because it's a submodule anyway; just later than you envisaged. This will make it run a bit faster, and a bit more reliably.
That module already needs to balance complicated compatibility and error reporting at the cost of basically no simplicity and very little room to spare in terms of making literally anything more strict. I would be happy to review or discuss changes that improve the end user experience of that module, which I believe is still possible, but I feel like this may be the wrong reason. I hope my earlier suggestion helps to make it work again. |
Okay, so I've had another go at it. Turns out that the message was evaluated in 3 places:
This was expected, mitigated by adding the All legacy options are
This is the most problematic part. We can't hash the message without evaling the message. Currently, I've switched to using the I am also not sure how much I should strive to make this common with
The |
8a86a2b
to
6fc2a83
Compare
warnings.networking.bonds.deprecatedOptions = lib.mapAttrs (bondName: bond: | ||
lib.mergeAttrsList (map (optName: { | ||
${optName} = { | ||
condition = (cfg.bonds.${bondName}.${optName} or null) != 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.
condition = (cfg.bonds.${bondName}.${optName} or null) != null; | |
condition = (cfg.bond.${optName} or null) != 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.
Actually after 7 years it's probably time to remove these legacy options and not waste any more time on them.
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.
Are you suggesting I remove the change, or do I just delete the deprecation warnings altogether?
Description of changes
This PR reworks the
assertions
module to take a freeform tree-like structure forassertions
andwarnings
. This has one major advantage over the existing list structure, which is the ability for a downstream user to forcefully disable an assertion or warning they believe to be erroneous without needing to resort to any tricky hacks. It might also open up for ideas where we could add more metadata or properties to the assertions/warnings, like different types of warnings or whether the message should be lazily evaluated. It will also let you extract the tree structure of non-firing warnings/assertions for whatever kinds of research purposes you might need it for. I don't suspect that an entire manual appendix of warnings/assertions is particularly useful, but maybe the data is useful for some?Example
Here is an example of the old and new way to write assertions and warnings:
If the user believes either of these to be an error in nixpkgs, they should of course make an issue first, but in the meanwhile they can do this:
Backwards compatibility
The module takes any existing assertions/warnings that are declared as lists, and coerces them to the new format under
{assertions,warnings}.legacy.<sha256-sum of message string>
. While this makes them a bit harder to disable, it ensures a smooth transition. Whileassertions
andwarnings
are marked withinternal = true
, I believe a lot of people are using them for out-of-tree modules. I have explicitly opted out of adding a note to the release docs, but I have changed the nixos manual.Other things worth mentioning, and maybe discussing
This PR depends on lib: add
lib.attrsets.collect'
#342330I've modified some of the core modules and library functions to use the new scheme, in addition to a few modules with a large set of assertions/warnings as a proof of concept. Most assertions have just been placed in the tree, but some needed slight modifications. Some assertions and warnings that earlier were aggregated have been split up to be separate assertions.
There is obviously no established scheme for the naming or namespacing of assertions and warnings. I've tried naming them roughly to reflect what they are warning about and what they are trying to assert, but I'm very open for changes and/or standardisation of some kind of scheme.
Should we consider making these options non-internal, considering widespread use outside nixpkgs?
I'm not sure about the evaluation speed impact of all the coercion stuff I've added. It doesn't seem much slower on my PC, but I haven't made any measurements.
Due to typechecking of the assertion/warning submodule, the message strings will now become non-lazy. In one way, that's a good thing because we get to typecheck the messages. On another hand, there are some message strings that depend on their boolean error condition to make any sense (e.g if the assertion asserts that a setting should not be set, the message might refer to the content of that setting without any regard for what happens if the setting is not there). The warningThis is not the case anymore. Lazyness is on by default for legacy assertions, and opt-out for new ones.mkIf
s handle this properly, but the assertions might not. I'm not sure whether we should try to hunt these down, or make typechecking opt-in, or how to go about this? Building all the nixos tests is a good start I suppose.There might be a case where identical assertions that were earlier part of the assertion list can have differing boolean values while still getting the same hash. I believe this could trigger an eval error where the same option is set to differing values? I'm not sure if we should handle those cases as they come and move towards the new format, or try to handle it with something likeThis is not relevant as long aslib.types.boolByOr
assertions
is usinglib.imap0
for type coercion.Testing
I've only done basic testing, opening a nix repl and evaluated attributes as follows:
Things done
nix.conf
? (See Nix manual)sandbox = relaxed
sandbox = true
nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD"
. Note: all changes have to be committed, also see nixpkgs-review usage./result/bin/
)Add a 👍 reaction to pull requests you find important.