Repository: NixOS/nixpkgs
Pull request #395141: lib/modules: export `class` inside `specialArgs`
Author: login: Enzime
Labels: name: 10.rebuild-linux: 1-10
description: This PR causes between 1 and 10 packages to rebuild on Linux.
name: 10.rebuild-darwin: 1-10
description: This PR causes between 1 and 10 packages to rebuild on Darwin.
name: 6.topic: module system
description: About "NixOS" module system internals
name: 10.rebuild-darwin: 1
description: This PR causes 1 package to rebuild on Darwin.
name: 12.approvals: 1
description: This PR was reviewed and approved by one person.
name: 6.topic: lib
description: The Nixpkgs function library
Created: 2025-04-01T10:48:47Z | Merged: 2025-04-10T18:58:41Z

I think this would be useful for writing module system-aware/polymorphic modules

```
{ _class, lib, ... }: {
  config = lib.mkMerge [
    ({ environment.etc = ...; })
    (lib.mkIf (_class == "nixos") {
      virtualisation = {
        ...
    })
  };
}
```

Currently `type` is exported as `moduleType` so maybe this should be exported as `moduleClass` to reduce the chance of collision with any downstream users already adding an argument called `class`

## Things done

<!-- Please check what applies. Note that these are not hard requirements but merely serve as information for reviewers. -->

- 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](https://nixos.org/manual/nix/stable/command-ref/conf-file.html))
  - [ ] `sandbox = relaxed`
  - [ ] `sandbox = true`
- [ ] Tested, as applicable:
  - [NixOS test(s)](https://nixos.org/manual/nixos/unstable/index.html#sec-nixos-tests) (look inside [nixos/tests](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests))
  - and/or [package tests](https://github.com/NixOS/nixpkgs/blob/master/pkgs/README.md#package-tests)
  - or, for functions and "core" functionality, tests in [lib/tests](https://github.com/NixOS/nixpkgs/blob/master/lib/tests) or [pkgs/test](https://github.com/NixOS/nixpkgs/blob/master/pkgs/test)
  - made sure NixOS tests are [linked](https://github.com/NixOS/nixpkgs/blob/master/pkgs/README.md#linking-nixos-module-tests-to-a-package) to the relevant packages
- [ ] 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](https://github.com/Mic92/nixpkgs-review#usage)
- [ ] Tested basic functionality of all binary files (usually in `./result/bin/`)
- [25.05 Release Notes](https://github.com/NixOS/nixpkgs/blob/master/nixos/doc/manual/release-notes/rl-2505.section.md) (or backporting [24.11](https://github.com/NixOS/nixpkgs/blob/master/nixos/doc/manual/release-notes/rl-2411.section.md) and [25.05](https://github.com/NixOS/nixpkgs/blob/master/nixos/doc/manual/release-notes/rl-2505.section.md) Release notes)
  - [ ] (Package updates) Added a release notes entry if the change is major or breaking
  - [ ] (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
- [x] Fits [CONTRIBUTING.md](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md).

---

Add a :+1: [reaction] to [pull requests you find important].

[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[pull requests you find important]: https://github.com/NixOS/nixpkgs/pulls?q=is%3Aopen+sort%3Areactions-%2B1-desc



Diff:
diff --git a/lib/modules.nix b/lib/modules.nix
index 7716e855ebb29..290b9ce0b6987 100644
--- a/lib/modules.nix
+++ b/lib/modules.nix
@@ -257,6 +257,7 @@ let
                     config
                     specialArgs
                     ;
+                  _class = class;
                 }
                 // specialArgs
               );
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index 9df6e61797b7f..d0f25c283dcbf 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -681,6 +681,18 @@ checkConfigOutput '^true$' config.viaConfig ./mkDefinition.nix
 checkConfigOutput '^true$' config.mkMerge ./mkDefinition.nix
 checkConfigOutput '^true$' config.mkForce ./mkDefinition.nix
 
+# specialArgs._class
+checkConfigOutput '"nixos"' config.nixos.config.foo ./specialArgs-class.nix
+checkConfigOutput '"bar"' config.conditionalImportAsNixos.config.foo ./specialArgs-class.nix
+checkConfigError 'attribute .*bar.* not found' config.conditionalImportAsNixos.config.bar ./specialArgs-class.nix
+checkConfigError 'attribute .*foo.* not found' config.conditionalImportAsDarwin.config.foo ./specialArgs-class.nix
+checkConfigOutput '"foo"' config.conditionalImportAsDarwin.config.bar ./specialArgs-class.nix
+checkConfigOutput '"nixos"' config.sub.nixos.foo ./specialArgs-class.nix
+checkConfigOutput '"bar"' config.sub.conditionalImportAsNixos.foo ./specialArgs-class.nix
+checkConfigError 'attribute .*bar.* not found' config.sub.conditionalImportAsNixos.bar ./specialArgs-class.nix
+checkConfigError 'attribute .*foo.* not found' config.sub.conditionalImportAsDarwin.foo ./specialArgs-class.nix
+checkConfigOutput '"foo"' config.sub.conditionalImportAsDarwin.bar ./specialArgs-class.nix
+
 cat <<EOF
 ====== module tests ======
 $pass Pass
diff --git a/lib/tests/modules/assert-module-class-is-nixos.nix b/lib/tests/modules/assert-module-class-is-nixos.nix
new file mode 100644
index 0000000000000..9efd296b41356
--- /dev/null
+++ b/lib/tests/modules/assert-module-class-is-nixos.nix
@@ -0,0 +1,3 @@
+{ _class, ... }:
+assert _class == "nixos";
+{ }
diff --git a/lib/tests/modules/class-check.nix b/lib/tests/modules/class-check.nix
index 207f363151948..14c45263c96cf 100644
--- a/lib/tests/modules/class-check.nix
+++ b/lib/tests/modules/class-check.nix
@@ -5,7 +5,9 @@
       nixosOk = lib.mkOption {
         type = lib.types.submoduleWith {
           class = "nixos";
-          modules = [ ];
+          modules = [
+            ./assert-module-class-is-nixos.nix
+          ];
         };
       };
       # Same but will have bad definition
@@ -45,6 +47,7 @@
       class = "nixos";
       modules = [
         ./module-class-is-nixos.nix
+        ./assert-module-class-is-nixos.nix
       ];
     };
 
diff --git a/lib/tests/modules/expose-module-class.nix b/lib/tests/modules/expose-module-class.nix
new file mode 100644
index 0000000000000..f3b5147464d8d
--- /dev/null
+++ b/lib/tests/modules/expose-module-class.nix
@@ -0,0 +1,8 @@
+{ _class, lib, ... }:
+{
+  options = {
+    foo = lib.mkOption {
+      default = _class;
+    };
+  };
+}
diff --git a/lib/tests/modules/polymorphic-module.nix b/lib/tests/modules/polymorphic-module.nix
new file mode 100644
index 0000000000000..ad11002035d0a
--- /dev/null
+++ b/lib/tests/modules/polymorphic-module.nix
@@ -0,0 +1,23 @@
+{ _class, lib, ... }:
+let
+  nixosModule =
+    { ... }:
+    {
+      options.foo = lib.mkOption {
+        default = "bar";
+      };
+    };
+  darwinModule =
+    { ... }:
+    {
+      options.bar = lib.mkOption {
+        default = "foo";
+      };
+    };
+in
+{
+  imports = [
+    (lib.optionalAttrs (_class == "nixos") nixosModule)
+    (lib.optionalAttrs (_class == "darwin") darwinModule)
+  ];
+}
diff --git a/lib/tests/modules/specialArgs-class.nix b/lib/tests/modules/specialArgs-class.nix
new file mode 100644
index 0000000000000..847496472eedb
--- /dev/null
+++ b/lib/tests/modules/specialArgs-class.nix
@@ -0,0 +1,54 @@
+{ lib, ... }:
+{
+  options = {
+    sub = {
+      nixos = lib.mkOption {
+        type = lib.types.submoduleWith {
+          class = "nixos";
+          modules = [
+            ./expose-module-class.nix
+          ];
+        };
+        default = { };
+      };
+
+      conditionalImportAsNixos = lib.mkOption {
+        type = lib.types.submoduleWith {
+          class = "nixos";
+          modules = [
+            ./polymorphic-module.nix
+          ];
+        };
+        default = { };
+      };
+
+      conditionalImportAsDarwin = lib.mkOption {
+        type = lib.types.submoduleWith {
+          class = "darwin";
+          modules = [
+            ./polymorphic-module.nix
+          ];
+        };
+        default = { };
+      };
+    };
+  };
+  config = {
+    _module.freeformType = lib.types.anything;
+
+    nixos = lib.evalModules {
+      class = "nixos";
+      modules = [ ./expose-module-class.nix ];
+    };
+
+    conditionalImportAsNixos = lib.evalModules {
+      class = "nixos";
+      modules = [ ./polymorphic-module.nix ];
+    };
+
+    conditionalImportAsDarwin = lib.evalModules {
+      class = "darwin";
+      modules = [ ./polymorphic-module.nix ];
+    };
+  };
+}


Reviews:
user: login: hsjobeki
body: Generally approving the idea. Would be more consistent, since modules also have access to `options`
Needs some tests.
Right now thinking at least the following would be nice:
- happy path, ensuring that your example works 
- collision with `class` in specialArgs (specialArgs should have higher precedence afaik)
- collision with setting `_module.class`. Should be an error now, no changes needed.
- conditionally import a submodule that defines options.

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T10:57:15Z
user: login: roberth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T15:33:26Z
user: login: Enzime
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T06:18:51Z
user: login: roberth
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T15:46:34Z

Review comments:
diff_hunk: @@ -232,19 +235,19 @@ let
 
         config = {
           _module.args = {
-            inherit extendModules class;
+            inherit extendModules;
             moduleType = type;
           };
-          _module.specialArgs = specialArgs;
+          _module.specialArgs = extendedSpecialArgs;
         };
       };
 
       merged =
         let collected = collectModules
           class
-          (specialArgs.modulesPath or "")
+          (extendedSpecialArgs.modulesPath or "")
           (regularModules ++ [ internalModule ])
-          ({ inherit lib options config specialArgs; } // specialArgs);
+          ({ inherit lib options config; specialArgs = extendedSpecialArgs; } // extendedSpecialArgs );
path: lib/modules.nix
user: login: roberth
body: This is a very literal interpretation of _add to specialArgs_. That's kinda ok, but not required.
What you can see (mostly) here is that _module arguments_ = _built-in arguments_ + _special args_ +  _\_module.args_.

So instead of making `_class` a special arg, we could make it a built-in arg, saving us the `extendedSpecialArgs` binding, a `//`, and most of all, it will be a bit simpler and IMO more consistent.
created_at: 2025-04-02T15:33:25Z
author_association: MEMBER
diff_hunk: @@ -232,19 +235,19 @@ let
 
         config = {
           _module.args = {
-            inherit extendModules class;
+            inherit extendModules;
             moduleType = type;
           };
-          _module.specialArgs = specialArgs;
+          _module.specialArgs = extendedSpecialArgs;
         };
       };
 
       merged =
         let collected = collectModules
           class
-          (specialArgs.modulesPath or "")
+          (extendedSpecialArgs.modulesPath or "")
           (regularModules ++ [ internalModule ])
-          ({ inherit lib options config specialArgs; } // specialArgs);
+          ({ inherit lib options config; specialArgs = extendedSpecialArgs; } // extendedSpecialArgs );
path: lib/modules.nix
user: login: Enzime
body: Is this change what you meant?
created_at: 2025-04-03T06:18:51Z
author_association: MEMBER

PR comments:
user: login: roberth
created_at: 2025-04-01T12:45:18Z
body: I'd prefer `specialArgs` in this case, because it allows use in `imports`, and would expect someone to want to write conditional imports based on this knowledge. I don't think it needs to be overridable.

> * collision with setting `_module.class`.

This won't be an error with a special arg. I don't know if users use that name, but maybe change to `specialArgs._class`?


> Needs some tests
> * conditionally import

A module like `{ _class, ... }: assert _class == "foo"; { }` could be used to check both the non-strictness of the module attrset in terms of `_class`, and assert the value in one go.

> Would be more consistent, since modules also have access to `options`

Like `configuration` objects:
```
nix-repl> (lib.evalModules { modules = []; class = "hi"; }).class
"hi"
```

In that regard `class` would be nicer than `_class`, but so far we've been consistently using `_` when we're sharing a namespace with user code, like in the `_class` module attribute. And not using it when we don't: the direct `evalModules` and `submoduleWith` interfaces.
author_association: MEMBER
user: login: hsjobeki
created_at: 2025-04-02T13:23:04Z
body: > I'd prefer `specialArgs` in this case, because it allows use in `imports`, and would expect someone to want to write conditional imports based on this knowledge. I don't think it needs to be overridable.

Agreed. I tried using specialArgs vs using _module.args. I think it was a bit redundant, but still worth to confirm.
_module.args.class leads to inifinite recursion, while specialArgs doesnt.

See below what i tried:
```nix
evalModules {
    class = "darwin";
    modules = [
      (
        { class, lib, ... }:
        {
          imports = lib.optionals (class == "nixos") [
            {
              options = {
                test = mkOption {
                  default = 1;
                };
              };
            }
          ];
        }
      )
    ];
  };
```

cc @Enzime Is this something you'd had in mind? Should i push my changes to use specialArgs to your PR? 
author_association: CONTRIBUTOR
user: login: Enzime
created_at: 2025-04-02T13:31:09Z
body: Yep, using `specialArgs` instead of `_module.args` sounds good to me, feel free to push :+1:
author_association: MEMBER
user: login: nixpkgs-ci[bot]
created_at: 2025-04-15T14:41:51Z
body: Backport failed for `release-24.11`, because it was unable to cherry-pick the commit(s).

Please cherry-pick the changes locally and resolve any conflicts.
```bash
git fetch origin release-24.11
git worktree add -d .worktree/backport-395141-to-release-24.11 origin/release-24.11
cd .worktree/backport-395141-to-release-24.11
git switch --create backport-395141-to-release-24.11
git cherry-pick -x 6de9039315c8798f9d56f0045cb454767775138b
```
author_association: CONTRIBUTOR