Skip to content

Conversation

lilyball
Copy link
Member

@lilyball lilyball commented Jul 4, 2025

This implements getSubOptions, getSubModules, and substSubModules for the either type. This allows us to generate documentation for a bunch of options that were getting missed before.

Because either is the key to making recursive types, I had to be careful about the implementation here to avoid breaking on a type like (pkgs.formats.json {}).type. To that end, this implementation only looks at child types if they're submodule or another either. This does mean types like either str (listOf submodule) will still be missed, but I'm not sure how else to handle this besides recursively searching all nested types to see if any are equal (==) to the current type, and I don't like that solution. I also considered simply stopping on attrsOf and listOf and allowing other children (this would allow e.g. either str (nullOr submodule)) but I thought that was sufficiently fragile. And I considered looking for any recursively-defined type in nixpkgs and overriding getSubOptions/getSubModules explicitly, but that would mean types defined outside of nixpkgs could still be broken by this.

For the case of either submodule submodule I decided that getSubOptions should merge all returned options, but getSubModules and substSubModules should ensure only one child actually has submodules. I don't expect this to matter because a type like that isn't really going to work, this logic makes more sense for something like either submodule (attrsOf submodule) except for the fact that we won't recurse into the attrsOf (to avoid problems on recursive types).

With this change, I found a number of options whose documentation were missing or broken. Fixes for each of those modules are included as separate commits. Most of them were pretty straightforward, but the tor module had a lot of generated options and it wasn't always clear what the best way to handle it was.

I tested this with nix-build nixos/release.nix -A manual.aarch64-linux, I'm not sure if there's any other documentation set that I should have tested too. I also didn't touch the lib tests, I'm not sure if this change should have an explicit test written for it, but I did make sure lib/tests/modules.sh passes. I'm also not sure if this change warrants a release note.

Things done

  • Built on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • For non-Linux: Is sandboxing enabled in nix.conf? (See Nix manual)
    • sandbox = relaxed
    • sandbox = true
  • Tested, as applicable:
  • Tested compilation of all packages that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD". Note: all changes have to be committed, also see nixpkgs-review usage
  • Tested basic functionality of all binary files (usually in ./result/bin/)
  • Nixpkgs 25.11 Release Notes (or backporting 25.05 Nixpkgs Release notes)
    • (Package updates) Added a release notes entry if the change is major or breaking
  • NixOS 25.11 Release Notes (or backporting 25.05 NixOS Release notes)
    • (Module updates) Added a release notes entry if the change is significant
    • (Module addition) Added a release notes entry if adding a new NixOS module
  • Fits CONTRIBUTING.md, pkgs/README.md, maintainers/README.md and other contributing documentation in corresponding paths.

Add a 👍 reaction to pull requests you find important.

@@ -205,6 +212,7 @@ let
optionFlags = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
internal = true;
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not entirely sure about this one, from my reading of the module it looks like flags isn't supposed to be set by users directly, it's calculated from the per-flag attributes.

@nix-owners nix-owners bot requested review from hsjobeki, peti, roberth and infinisil July 4, 2025 03:56
@nixpkgs-ci nixpkgs-ci bot added 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: module (update) This PR changes an existing module in `nixos/` 6.topic: module system About "NixOS" module system internals 6.topic: lib The Nixpkgs function library 10.rebuild-linux: 1-10 This PR causes between 1 and 10 packages to rebuild on Linux. 10.rebuild-darwin: 1-10 This PR causes between 1 and 10 packages to rebuild on Darwin. 10.rebuild-darwin: 1 This PR causes 1 package to rebuild on Darwin. labels Jul 4, 2025
@lilyball
Copy link
Member Author

lilyball commented Jul 7, 2025

A few thoughts on this PR:

  • I could split this up into several separate PRs, since the option documentation fixes don't depend on the either change (they just don't affect anything without it)
  • Because this change really only affects documentation (and nixos-option) maybe it is safe enough to go ahead and do the naive recursion and just fix nixpkgs's recursive types, since any third parties that define their own recursive types will only be affected if they also build documentation (though they would still hit the issue if they use nixos-option to inspect any option using their recursive type). I haven't tested to see if this would pick up any more options though, so maybe I should find that out.
  • home-manager and nix-darwin might want to test their own documentation against this PR to see if they have any options that need documentation fixes.

@lilyball
Copy link
Member Author

lilyball commented Jul 7, 2025

I tested out the "make either fully recursive" option and while it does add more options, they're entirely within services.tor, such as services.tor.settings.ControlPort.*.GroupWritable, and they're all ultimately of type oneOf [ ... (listOf (oneOf [ ... submodule ]))]. The downside is I had to update 12 recursive types in formats plus another 14 modules that declared their own custom recursive types. That's a lot more recursive types than I expected to find.

Alternatively this also suggests we could just update the 2 affected types in services.tor (by overriding getSubOptions/getSubModules/substSubModules to defer directly to the submodule) to get the same effect without making either fully recursive. There may be options in home-manager or nix-darwin that would also benefit from the fully-recursive either, but the fact that in all of nixpkgs it's just 2 of the types in services.tor that are affected suggests that a construct like either (listOf submodule) … (where the submodule isn't also at the top level of the either) is sufficiently rare that it may not show up anywhere else at all.

Edit: actually, the fully recursive change hurts some of the tor documentation because it modifies services.tor.settings.DNSPort sub-options so that e.g. services.tor.settings.DNSPort.IsolateClientAddr is now services.tor.settings.DNSPort.*.IsolateClientAddr, which happens because the either submodule (listOf submodule) now overrides the left's options with the right. So if we do want fully recursive we need to be smarter about merging options from both sides, though also this suggests that my getSubOptions impl might want to do t2.getSubOptions prefix // t1.getSubOptions prefix just to prioritize options from the left side.

Also we can't just trivially update those two types in services.tor as an alternative, because of how substSubModules works it's a lot more complicated to modify those types to defer to the submodule.

@lilyball
Copy link
Member Author

lilyball commented Jul 7, 2025

Another thing to consider here is either module (listOf module) such as services.tor.settings.TransPort, which really should generate option documentation for both the direct sub-options and the listOf sub-options, e.g. both services.tor.settings.TransPort.IsolateClientAddr and services.tor.settings.TransPort.*.IsolateClientAddr. But right now this can't happen because even though listOf adds "*" to the prefix, this isn't reflected in the returned attrset, so there's no way to merge the sub-options from the module and the listOf module, unless I want to change either to return something like { left = t1.getSubOptions prefix; right = t2.getSubOptions prefix; } and that has negative consequences for nixos-option.

With that context, we could consider changing listOf and attrsOf to put the placeholder into the returned attrset, e.g. having listOf return { "*" = elemType.getSubOptions (prefix ++ [ "*" ]); }. That way this can be merged with the non-list version of the same module. The downside is nixos-option would have to be updated to expect this.

@lilyball lilyball force-pushed the push-sxtxlvwouuno branch from 6fe4329 to 21241df Compare July 7, 2025 11:04
@lilyball
Copy link
Member Author

lilyball commented Jul 7, 2025

The changes to either I just pushed are:

  • getSubOptions now uses lib.recursiveUpdateUntil so either moduleA moduleB where the two submodules define distinct options nested within the same key will work, e.g. either (submodule [{options.foo.bar = mkOption { … }; }]) (submodule [{options.foo.qux = mkOption { … }; }])
  • getSubOptions now resolves conflicts in favor of the left type if both types define options
  • getSubModules now returns the left type's submodules if both types define submodules (this affects services.postgrey)
  • substSubModules similarly now substitutes the left type's submodules if both types define submodules

@nixpkgs-ci nixpkgs-ci bot added the 2.status: merge conflict This PR has merge conflicts with the target branch label Jul 26, 2025
Also implement getSubModules and substSubModules. Because either is key
to any recursively-defined type (such as `(pkgs.formats.json {}).type`),
we can't just blindly recurse into both sides or we'll hit infinite
recursion on a recursive type. To that end, this implementation only
recurses into a child type if that type is submodule or another either.
This means we'll generate documentation for `either str submodule` but
not for `either str (attrsOf submodule)`.

This change allows us to generate documentation for a bunch of options
that are currently being missed.
The `example` key of a couple of options didn't evaluate, and this went
unnoticed as this option wasn't part of generated documentation before.
Also fix usage of a deprecated function, as the documentation builder
complains about that.
@lilyball
Copy link
Member Author

Rebased to fix merge conflict (conflict was due to the tor module being formatted)

@nixpkgs-ci nixpkgs-ci bot removed the 2.status: merge conflict This PR has merge conflicts with the target branch label Aug 11, 2025
Copy link
Contributor

@hsjobeki hsjobeki left a comment

Choose a reason for hiding this comment

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

Could you split the changes in types.nix and non-lib related things into a speerate PR? I.e. the fixes on some modules should be seperated. This is a common practise for us to avoid mixing lib changes with nixos fixes. I'll spend a bit more time these days looking at this. I fear that the changes in nixos might be due to a breaking change in the either type. Which me must preserve for downstream users. Moving the lib change out of this PR would make it more clear to us.

@roberth
Copy link
Member

roberth commented Aug 19, 2025

Mixing isn't so bad by itself.
The reason we usually ask this is that lib changes tend to require very thorough scrutiny, which shouldn't block other improvements even if they're somewhat related, which seems to be the case here.

@lilyball
Copy link
Member Author

I certainly can split this up, though the non-lib changes here are all fairly meaningless without the lib changes, as those documentations simply aren't rendered without the lib changes. So there's no problem with "blocking" those changes on the lib changes.

If I split this up, would it be better to do one PR for all the non-lib stuff, or a separate PR for each module (e.g. turning each commit into its own PR)?

@lilyball
Copy link
Member Author

lilyball commented Sep 6, 2025

I fear that the changes in nixos might be due to a breaking change in the either type.

To clarify on this point, the changes in nixos are to fix code that is currently broken but that brokenness wasn't noticed because the options aren't rendered into documentation until the lib change. Some of it is documentation where the nix code is simply wrong (e.g. the dconf module said example = literalExpression ''…'' without importing that, so the fix is to say example = lib.literalExpression ''…''), some of it fixes the documentation to conform to the restrictions in place when building the manual (pay-respects used a deprecated function and had a default key that referenced something from another module without using defaultText, rspamd also did the latter), and some was just options that were missing descriptions (movim and tor).

All of this stuff could be detected by using the repl to poke around at the module structure, but it went unnoticed because none of these options ended up in the documentation manual before. So technically this either change could be a breaking change just by exposing buggy code to documentation rendering, but getting those options in the rendered documentation is the whole point of this change.

Copy link
Contributor

@hsjobeki hsjobeki left a comment

Choose a reason for hiding this comment

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

After looking into this for a short bit i am not sure if implementing getSubOptions for either is the right approach to solve the documentation problem. getSubOptions is only meant to documentation to automatically traverse options and types via `optionAttrSetToDocList´ with either (and derived types such as oneOf) it is possible to create recursive types.

In case of those reursive types there is no formally correct way to stop / break on a certain point. Thats why we didn't implement getSubOptions historically i think.

Even before this PR either was already broken on recursive types - take this simple recursive type for example:

  simpleJson = types.oneOf [  # renders into a tree of either
    types.str
    (types.attrsOf simpleJson)
    (types.listOf simpleJson)
  ];

#... a module 
{
  options.foo = mkOption {
    type = simpleJson;
  };
}

This would fail to produce docs, because either has a recursive description, which would yield an infinitely long string in this case.

The actual JSON type solves this by overriding the description (this is also not ideal):

          valueType =
            nullOr (oneOf [
              #...
            ])
            // {
              description = "JSON value";
            };

We could maybe think about other solutions such as providing documentation hints, instead of trying to smartly recurse an infinite structure.

Some ideas how to solve this problem: Add something like a docsModel / docsType which requires to be finite and is provided through the interface while creating the type?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
6.topic: lib The Nixpkgs function library 6.topic: module system About "NixOS" module system internals 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: module (update) This PR changes an existing module in `nixos/` 10.rebuild-darwin: 1-10 This PR causes between 1 and 10 packages to rebuild on Darwin. 10.rebuild-darwin: 1 This PR causes 1 package to rebuild on Darwin. 10.rebuild-linux: 1-10 This PR causes between 1 and 10 packages to rebuild on Linux.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants