The module system is dead, how to do input validation?

In certain parts of Nixpkgs, but also in some outside Nix projects, there occasionally is need to pass in some structured arguments and do some input validation on them. Examples that I’ve personally encountered:

  • Checking all attributes of a package’s meta
  • Checking the Nixpkgs configuration

In those situations, it is very tempting to simply re-use the NixOS options and module system for type checking, as has been attempted in these cases. However, this quickly leads to a lot of various problems. I won’t discuss them in detail here, but here are some related discussions:

At this point I am no longer convinced that improving the module system for non-NixOS use cases is the best way forward. Which yields the question, what to do instead? Create a new small type checker for that purpose (See also YANTS and types-simple)? Some JSON schema based validator? A new an improved option system?

2 Likes

As you may know, I do validation of the Bootspec partially in the module system and completely in Cuelang: https://github.com/NixOS/nixpkgs/blob/8df7a67abaf8aefc8a2839e0b48f92fdcf69a38b/nixos/modules/system/activation/bootspec.nix#L70-L72

Though the problem is that anything that uses this type of strategy is bound to require some extra build-time runtime which may not be desirable, e.g. Go.

1 Like

For crying out loud, it is not dead

It has two active maintainers, Infinisil and me.

What you’re implying is not true.
The non-NixOS use cases have same kind of laziness requirement that NixOS has.
Specifically, the module system checks what is used and does not check what isn’t used.

The problem with checking meta is that you’re not really using anything, but rather you want to check everything. You could pretend to use everything with deepSeq, or actually use it (e.g. write a function that presents all information in markdown) or you could indeed make additions to the module system types to make it do something more clever than deepSeq, in case deepSeq isn’t enough for your use case.
Have you tried running it with deepSeq?
ie something like

lib.deepSeq
  (evalModules {
    modules = [ ./package-meta-option.nix { config = meta; } ];
  }).config
  meta

If you’re not happy about the result, I’ve mentioned before the possibility of adding more methods to augment the functionality of the module system. I’d be happy to discuss such improvements to the type interface. (I think I’ve said this before, but not sure how explicitly)

9 Likes

I never stated the module system was unmaintained.

Also, even ignoring the admittedly more special case of checking meta, I still have a lot of problems with the module system even in top-level/config.nix.

  • Values are not properly type checked, even when accessed. I have code iterating on config.problems.matchers with some garbage input, it runs fine without complaining. Only a deepSeq actually triggers the error. (Edit: turns out the problem is laziness. Unless I read that specific attribute from that specific list element, even while iterating over most of it, it will just ignore any bad inputs.)
  • Lists of attribute sets are not a first class citizen, as they require using submodules which are rather clumsy.
    • Other complex things like sum types are even harder to properly implement
  • There is no way to add non-trivial checks to the configuration, and with good error messages. One can always addCheck to the type, but this would not allow for meaningful error messages. In NixOS, the best solution for this is to use assertions.
    • Assertions currently are not a module system feature but specific to NixOS.
    • Even with assertions working for config.nix, AFAIK they still wouldn’t work within submodules
    • Assertions have the downside of being decoupled from the actual options, which re-opens the basic questions about strictness. Evaluating them will cause all affected options to be forced regardless of whether they are actually used. Conversely, there is no way to evaluate assertions only when the affected options are used.
  • There is no concept of error handling other than throw. There is no way to evaluate a module system and get any error messages. It’s either throw and abort, or discard all help messages.
  • Things like mkRemovedOptionModule rely on laziness for working: instead of checking whether the option is actually set, it checks whether it is used somewhere instead. This means that as soon as mkRemovedOptionModule is involved anywhere, I have to tip-toe around it with seq for any other type checks.
    • mkRemovedOptionModule also relies on assertions for working. Which means that outside of the NixOS module system, there will actually be no error when defining a removed option.

I’m not sold on that one. I think that for many input validation purposes, there basically is only one module with a couple of options. There is no relevant merging going on and no cyclic references, so one can be a lot more strict here. At the very least one can be strict about the output of evalModules; checking all options eagerly should not be relevant to performance given how few options there are, and that most of the time most of them are used anyways.

The usage scenario is sufficiently different that I’d argue in favor of a dedicated solution, instead of trying to make the existing module system fit it.

1 Like

Not sure if N+1 is helpful, but I fumbled my way to a non-module variation on this theme for resholve’s Nix API:

While I do understand your point,

I think this is literally what Robert has been saying though… NixOS module system has explicitly decided to exploit laziness to its max. That’s why the deepSeq was recommended to you, I believe.

But isn’t this literally deepSeq \circ evalModules ?