nixos: use structured attrs for assertions/warnings#342372
nixos: use structured attrs for assertions/warnings#342372h7x4 wants to merge 9 commits intoNixOS:masterfrom
assertions/warnings#342372Conversation
nixos/modules/misc/assertions.nix
Outdated
There was a problem hiding this comment.
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.
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.
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 |
nixos/modules/misc/assertions.nix
Outdated
| # 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.
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
There was a problem hiding this comment.
| condition = (cfg.bonds.${bondName}.${optName} or null) != null; | |
| condition = (cfg.bond.${optName} or null) != null; |
There was a problem hiding this comment.
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.
Are you suggesting I remove the change, or do I just delete the deprecation warnings altogether?
6fc2a83 to
82ba7f6
Compare
82ba7f6 to
4b1d5ff
Compare
4b1d5ff to
2ffa44a
Compare
2ffa44a to
c9013d2
Compare
1be09b4 to
ba90ce5
Compare
ba90ce5 to
46a1259
Compare
46a1259 to
757f726
Compare
Description of changes
This PR reworks the
assertionsmodule to take a freeform tree-like structure forassertionsandwarnings. 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. Whileassertionsandwarningsare 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.mkIfs 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.boolByOrassertionsis usinglib.imap0for 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 = relaxedsandbox = truenix-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.