about summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/generators.nix26
-rw-r--r--lib/kernel.nix7
-rw-r--r--lib/modules.nix160
-rw-r--r--lib/options.nix6
-rw-r--r--lib/systems/default.nix2
-rw-r--r--lib/systems/doubles.nix3
-rw-r--r--lib/systems/examples.nix4
-rw-r--r--lib/systems/inspect.nix1
-rw-r--r--lib/systems/parse.nix2
-rw-r--r--lib/tests/misc.nix64
-rwxr-xr-xlib/tests/modules.sh21
-rw-r--r--lib/tests/modules/class-check.nix76
-rw-r--r--lib/tests/modules/define-enable-with-top-level-mkIf.nix5
-rw-r--r--lib/tests/modules/define-freeform-keywords-shorthand.nix15
-rw-r--r--lib/tests/modules/import-configuration.nix12
-rw-r--r--lib/tests/modules/module-class-is-darwin.nix4
-rw-r--r--lib/tests/modules/module-class-is-nixos.nix4
-rw-r--r--lib/tests/modules/module-imports-_type-check.nix3
-rw-r--r--lib/tests/systems.nix2
-rw-r--r--lib/tests/test-to-plist-expected.plist46
-rw-r--r--lib/types.nix10
21 files changed, 413 insertions, 60 deletions
diff --git a/lib/generators.nix b/lib/generators.nix
index 4ecbdac3c125..aace53e2f750 100644
--- a/lib/generators.nix
+++ b/lib/generators.nix
@@ -355,6 +355,7 @@ rec {
   # PLIST handling
   toPlist = {}: v: let
     isFloat = builtins.isFloat or (x: false);
+    isPath = x: builtins.typeOf x == "path";
     expr = ind: x:  with builtins;
       if x == null  then "" else
       if isBool x   then bool ind x else
@@ -362,6 +363,7 @@ rec {
       if isString x then str ind x else
       if isList x   then list ind x else
       if isAttrs x  then attrs ind x else
+      if isPath x   then str ind (toString x) else
       if isFloat x  then float ind x else
       abort "generators.toPlist: should never happen (v = ${v})";
 
@@ -434,6 +436,7 @@ ${expr "" v}
    Configuration:
      * multiline - by default is true which results in indented block-like view.
      * indent - initial indent.
+     * asBindings - by default generate single value, but with this use attrset to set global vars.
 
    Attention:
      Regardless of multiline parameter there is no trailing newline.
@@ -464,18 +467,35 @@ ${expr "" v}
     /* If this option is true, the output is indented with newlines for attribute sets and lists */
     multiline ? true,
     /* Initial indentation level */
-    indent ? ""
+    indent ? "",
+    /* Interpret as variable bindings */
+    asBindings ? false,
   }@args: v:
     with builtins;
     let
       innerIndent = "${indent}  ";
       introSpace = if multiline then "\n${innerIndent}" else " ";
       outroSpace = if multiline then "\n${indent}" else " ";
-      innerArgs = args // { indent = innerIndent; };
+      innerArgs = args // {
+        indent = if asBindings then indent else innerIndent;
+        asBindings = false;
+      };
       concatItems = concatStringsSep ",${introSpace}";
       isLuaInline = { _type ? null, ... }: _type == "lua-inline";
+
+      generatedBindings =
+          assert lib.assertMsg (badVarNames == []) "Bad Lua var names: ${toPretty {} badVarNames}";
+          libStr.concatStrings (
+            lib.attrsets.mapAttrsToList (key: value: "${indent}${key} = ${toLua innerArgs value}\n") v
+            );
+
+      # https://en.wikibooks.org/wiki/Lua_Programming/variable#Variable_names
+      matchVarName = match "[[:alpha:]_][[:alnum:]_]*(\\.[[:alpha:]_][[:alnum:]_]*)*";
+      badVarNames = filter (name: matchVarName name == null) (attrNames v);
     in
-    if v == null then
+    if asBindings then
+      generatedBindings
+    else if v == null then
       "nil"
     else if isInt v || isFloat v || isString v || isBool v then
       builtins.toJSON v
diff --git a/lib/kernel.nix b/lib/kernel.nix
index ffcbc268b76c..33da9663a8ed 100644
--- a/lib/kernel.nix
+++ b/lib/kernel.nix
@@ -8,9 +8,10 @@ with lib;
   option = x:
       x // { optional = true; };
 
-  yes      = { tristate    = "y"; optional = false; };
-  no       = { tristate    = "n"; optional = false; };
-  module   = { tristate    = "m"; optional = false; };
+  yes      = { tristate    = "y";  optional = false; };
+  no       = { tristate    = "n";  optional = false; };
+  module   = { tristate    = "m";  optional = false; };
+  unset    = { tristate    = null; optional = false; };
   freeform = x: { freeform = x; optional = false; };
 
   /*
diff --git a/lib/modules.nix b/lib/modules.nix
index 9c3e2085e378..4dc8c663b2fe 100644
--- a/lib/modules.nix
+++ b/lib/modules.nix
@@ -63,39 +63,8 @@ let
           decls
       ));
 
-in
-
-rec {
-
-  /*
-    Evaluate a set of modules.  The result is a set with the attributes:
-
-      ‘options’: The nested set of all option declarations,
-
-      ‘config’: The nested set of all option values.
-
-      ‘type’: A module system type representing the module set as a submodule,
-            to be extended by configuration from the containing module set.
-
-            This is also available as the module argument ‘moduleType’.
-
-      ‘extendModules’: A function similar to ‘evalModules’ but building on top
-            of the module set. Its arguments, ‘modules’ and ‘specialArgs’ are
-            added to the existing values.
-
-            Using ‘extendModules’ a few times has no performance impact as long
-            as you only reference the final ‘options’ and ‘config’.
-            If you do reference multiple ‘config’ (or ‘options’) from before and
-            after ‘extendModules’, performance is the same as with multiple
-            ‘evalModules’ invocations, because the new modules' ability to
-            override existing configuration fundamentally requires a new
-            fixpoint to be constructed.
-
-            This is also available as a module argument.
-
-      ‘_module’: A portion of the configuration tree which is elided from
-            ‘config’. It contains some values that are mostly internal to the
-            module system implementation.
+  /* See https://nixos.org/manual/nixpkgs/unstable/#module-system-lib-evalModules
+     or file://./../doc/module-system/module-system.chapter.md
 
      !!! Please think twice before adding to this argument list! The more
      that is specified here instead of in the modules themselves the harder
@@ -110,8 +79,12 @@ rec {
                   # there's _module.args. If specialArgs.modulesPath is defined it will be
                   # used as the base path for disabledModules.
                   specialArgs ? {}
-                , # This would be remove in the future, Prefer _module.args option instead.
-                  args ? {}
+                , # `class`:
+                  # A nominal type for modules. When set and non-null, this adds a check to
+                  # make sure that only compatible modules are imported.
+                  # This would be remove in the future, Prefer _module.args option instead.
+                  class ? null
+                , args ? {}
                 , # This would be remove in the future, Prefer _module.check option instead.
                   check ? true
                 }:
@@ -260,6 +233,7 @@ rec {
 
       merged =
         let collected = collectModules
+          class
           (specialArgs.modulesPath or "")
           (regularModules ++ [ internalModule ])
           ({ inherit lib options config specialArgs; } // specialArgs);
@@ -336,38 +310,64 @@ rec {
         prefix ? [],
         }:
           evalModules (evalModulesArgs // {
+            inherit class;
             modules = regularModules ++ modules;
             specialArgs = evalModulesArgs.specialArgs or {} // specialArgs;
             prefix = extendArgs.prefix or evalModulesArgs.prefix or [];
           });
 
       type = lib.types.submoduleWith {
-        inherit modules specialArgs;
+        inherit modules specialArgs class;
       };
 
       result = withWarnings {
+        _type = "configuration";
         options = checked options;
         config = checked (removeAttrs config [ "_module" ]);
         _module = checked (config._module);
         inherit extendModules type;
+        class = class;
       };
     in result;
 
-  # collectModules :: (modulesPath: String) -> (modules: [ Module ]) -> (args: Attrs) -> [ Module ]
+  # collectModules :: (class: String) -> (modulesPath: String) -> (modules: [ Module ]) -> (args: Attrs) -> [ Module ]
   #
   # Collects all modules recursively through `import` statements, filtering out
   # all modules in disabledModules.
-  collectModules = let
+  collectModules = class: let
 
       # Like unifyModuleSyntax, but also imports paths and calls functions if necessary
       loadModule = args: fallbackFile: fallbackKey: m:
-        if isFunction m || isAttrs m then
-          unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgsIfFunction fallbackKey m args)
+        if isFunction m then
+          unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgs fallbackKey m args)
+        else if isAttrs m then
+          if m._type or "module" == "module" then
+            unifyModuleSyntax fallbackFile fallbackKey m
+          else if m._type == "if" || m._type == "override" then
+            loadModule args fallbackFile fallbackKey { config = m; }
+          else
+            throw (
+              "Could not load a value as a module, because it is of type ${lib.strings.escapeNixString m._type}"
+              + lib.optionalString (fallbackFile != unknownModule) ", in file ${toString fallbackFile}."
+              + lib.optionalString (m._type == "configuration") " If you do intend to import this configuration, please only import the modules that make up the configuration. You may have to create a `let` binding, file or attribute to give yourself access to the relevant modules.\nWhile loading a configuration into the module system is a very sensible idea, it can not be done cleanly in practice."
+               # Extended explanation: That's because a finalized configuration is more than just a set of modules. For instance, it has its own `specialArgs` that, by the nature of `specialArgs` can't be loaded through `imports` or the the `modules` argument. So instead, we have to ask you to extract the relevant modules and use those instead. This way, we keep the module system comparatively simple, and hopefully avoid a bad surprise down the line.
+            )
         else if isList m then
           let defs = [{ file = fallbackFile; value = m; }]; in
           throw "Module imports can't be nested lists. Perhaps you meant to remove one level of lists? Definitions: ${showDefs defs}"
         else unifyModuleSyntax (toString m) (toString m) (applyModuleArgsIfFunction (toString m) (import m) args);
 
+      checkModule =
+        if class != null
+        then
+          m:
+            if m._class != null -> m._class == class
+            then m
+            else
+              throw "The module ${m._file or m.key} was imported into ${class} instead of ${m._class}."
+        else
+          m: m;
+
       /*
       Collects all modules recursively into the form
 
@@ -401,7 +401,7 @@ rec {
           };
         in parentFile: parentKey: initialModules: args: collectResults (imap1 (n: x:
           let
-            module = loadModule args parentFile "${parentKey}:anon-${toString n}" x;
+            module = checkModule (loadModule args parentFile "${parentKey}:anon-${toString n}" x);
             collectedImports = collectStructuredModules module._file module.key module.imports args;
           in {
             key = module.key;
@@ -465,11 +465,12 @@ rec {
         else config;
     in
     if m ? config || m ? options then
-      let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta" "freeformType"]; in
+      let badAttrs = removeAttrs m ["_class" "_file" "key" "disabledModules" "imports" "options" "config" "meta" "freeformType"]; in
       if badAttrs != {} then
         throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by introducing a top-level `config' or `options' attribute. Add configuration attributes immediately on the top level instead, or move all of them (namely: ${toString (attrNames badAttrs)}) into the explicit `config' attribute."
       else
         { _file = toString m._file or file;
+          _class = m._class or null;
           key = toString m.key or key;
           disabledModules = m.disabledModules or [];
           imports = m.imports or [];
@@ -480,14 +481,18 @@ rec {
       # shorthand syntax
       lib.throwIfNot (isAttrs m) "module ${file} (${key}) does not look like a module."
       { _file = toString m._file or file;
+        _class = m._class or null;
         key = toString m.key or key;
         disabledModules = m.disabledModules or [];
         imports = m.require or [] ++ m.imports or [];
         options = {};
-        config = addFreeformType (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"]);
+        config = addFreeformType (removeAttrs m ["_class" "_file" "key" "disabledModules" "require" "imports" "freeformType"]);
       };
 
-  applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then
+  applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }:
+    if isFunction f then applyModuleArgs key f args else f;
+
+  applyModuleArgs = key: f: args@{ config, options, lib, ... }:
     let
       # Module arguments are resolved in a strict manner when attribute set
       # deconstruction is used.  As the arguments are now defined with the
@@ -511,9 +516,7 @@ rec {
       # context on the explicit arguments of "args" too. This update
       # operator is used to make the "args@{ ... }: with args.lib;" notation
       # works.
-    in f (args // extraArgs)
-  else
-    f;
+    in f (args // extraArgs);
 
   /* Merge a list of modules.  This will recurse over the option
      declarations in all modules, combining them into a single set.
@@ -1218,4 +1221,67 @@ rec {
     _file = file;
     config = lib.importTOML file;
   };
+
+  private = lib.mapAttrs
+    (k: lib.warn "External use of `lib.modules.${k}` is deprecated. If your use case isn't covered by non-deprecated functions, we'd like to know more and perhaps support your use case well, instead of providing access to these low level functions. In this case please open an issue in https://github.com/nixos/nixpkgs/issues/.")
+    {
+      inherit
+        applyModuleArgsIfFunction
+        dischargeProperties
+        evalOptionValue
+        mergeModules
+        mergeModules'
+        pushDownProperties
+        unifyModuleSyntax
+        ;
+      collectModules = collectModules null;
+    };
+
+in
+private //
+{
+  # NOTE: not all of these functions are necessarily public interfaces; some
+  #       are just needed by types.nix, but are not meant to be consumed
+  #       externally.
+  inherit
+    defaultOrderPriority
+    defaultOverridePriority
+    defaultPriority
+    doRename
+    evalModules
+    filterOverrides
+    filterOverrides'
+    fixMergeModules
+    fixupOptionType  # should be private?
+    importJSON
+    importTOML
+    mergeDefinitions
+    mergeOptionDecls  # should be private?
+    mkAfter
+    mkAliasAndWrapDefinitions
+    mkAliasAndWrapDefsWithPriority
+    mkAliasDefinitions
+    mkAliasIfDef
+    mkAliasOptionModule
+    mkAliasOptionModuleMD
+    mkAssert
+    mkBefore
+    mkChangedOptionModule
+    mkDefault
+    mkDerivedConfig
+    mkFixStrictness
+    mkForce
+    mkIf
+    mkImageMediaOverride
+    mkMerge
+    mkMergedOptionModule
+    mkOptionDefault
+    mkOrder
+    mkOverride
+    mkRemovedOptionModule
+    mkRenamedOptionModule
+    mkRenamedOptionModuleWith
+    mkVMOverride
+    setDefaultModuleLocation
+    sortProperties;
 }
diff --git a/lib/options.nix b/lib/options.nix
index 4780a56fc1c3..d71d9421b7b1 100644
--- a/lib/options.nix
+++ b/lib/options.nix
@@ -261,7 +261,7 @@ rec {
     concatMap (opt:
       let
         name = showOption opt.loc;
-        docOption = rec {
+        docOption = {
           loc = opt.loc;
           inherit name;
           description = opt.description or null;
@@ -280,9 +280,9 @@ rec {
               renderOptionValue opt.example
             );
         }
-        // optionalAttrs (opt ? default) {
+        // optionalAttrs (opt ? defaultText || opt ? default) {
           default =
-            builtins.addErrorContext "while evaluating the default value of option `${name}`" (
+            builtins.addErrorContext "while evaluating the ${if opt?defaultText then "defaultText" else "default value"} of option `${name}`" (
               renderOptionValue (opt.defaultText or opt.default)
             );
         }
diff --git a/lib/systems/default.nix b/lib/systems/default.nix
index e58fb5ed1e66..0494d365d6ba 100644
--- a/lib/systems/default.nix
+++ b/lib/systems/default.nix
@@ -50,6 +50,7 @@ rec {
         else if final.isFreeBSD             then "fblibc"
         else if final.isNetBSD              then "nblibc"
         else if final.isAvr                 then "avrlibc"
+        else if final.isGhcjs               then null
         else if final.isNone                then "newlib"
         # TODO(@Ericson2314) think more about other operating systems
         else                                     "native/impure";
@@ -136,6 +137,7 @@ rec {
         else if final.isPower then "powerpc"
         else if final.isRiscV then "riscv"
         else if final.isS390 then "s390"
+        else if final.isLoongArch64 then "loongarch"
         else final.parsed.cpu.name;
 
       qemuArch =
diff --git a/lib/systems/doubles.nix b/lib/systems/doubles.nix
index 6b19309d11ff..6d2f015674e0 100644
--- a/lib/systems/doubles.nix
+++ b/lib/systems/doubles.nix
@@ -26,7 +26,7 @@ let
 
     # Linux
     "aarch64-linux" "armv5tel-linux" "armv6l-linux" "armv7a-linux"
-    "armv7l-linux" "i686-linux" "m68k-linux" "microblaze-linux"
+    "armv7l-linux" "i686-linux" "loongarch64-linux" "m68k-linux" "microblaze-linux"
     "microblazeel-linux" "mipsel-linux" "mips64el-linux" "powerpc64-linux"
     "powerpc64le-linux" "riscv32-linux" "riscv64-linux" "s390-linux"
     "s390x-linux" "x86_64-linux"
@@ -86,6 +86,7 @@ in {
   m68k          = filterDoubles predicates.isM68k;
   s390          = filterDoubles predicates.isS390;
   s390x         = filterDoubles predicates.isS390x;
+  loongarch64   = filterDoubles predicates.isLoongArch64;
   js            = filterDoubles predicates.isJavaScript;
 
   bigEndian     = filterDoubles predicates.isBigEndian;
diff --git a/lib/systems/examples.nix b/lib/systems/examples.nix
index 9ea2e3b56e92..4edbf4df4b61 100644
--- a/lib/systems/examples.nix
+++ b/lib/systems/examples.nix
@@ -135,6 +135,10 @@ rec {
     libc = "newlib";
   };
 
+  loongarch64-linux = {
+    config = "loongarch64-unknown-linux-gnu";
+  };
+
   mmix = {
     config = "mmix-unknown-mmixware";
     libc = "newlib";
diff --git a/lib/systems/inspect.nix b/lib/systems/inspect.nix
index 1700049ca4bf..89e9f4231d97 100644
--- a/lib/systems/inspect.nix
+++ b/lib/systems/inspect.nix
@@ -57,6 +57,7 @@ rec {
     isM68k         = { cpu = { family = "m68k"; }; };
     isS390         = { cpu = { family = "s390"; }; };
     isS390x        = { cpu = { family = "s390"; bits = 64; }; };
+    isLoongArch64  = { cpu = { family = "loongarch"; bits = 64; }; };
     isJavaScript   = { cpu = cpuTypes.javascript; };
 
     is32bit        = { cpu = { bits = 32; }; };
diff --git a/lib/systems/parse.nix b/lib/systems/parse.nix
index bd3366e140bf..ea8e1ff8fcf0 100644
--- a/lib/systems/parse.nix
+++ b/lib/systems/parse.nix
@@ -131,6 +131,8 @@ rec {
 
     or1k     = { bits = 32; significantByte = bigEndian; family = "or1k"; };
 
+    loongarch64 = { bits = 64; significantByte = littleEndian; family = "loongarch"; };
+
     javascript = { bits = 32; significantByte = littleEndian; family = "javascript"; };
   };
 
diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix
index 49336b8b9630..231f19c513eb 100644
--- a/lib/tests/misc.nix
+++ b/lib/tests/misc.nix
@@ -4,6 +4,11 @@
 with import ../default.nix;
 
 let
+  testingThrow = expr: {
+    expr = (builtins.tryEval (builtins.seq expr "didn't throw"));
+    expected = { success = false; value = false; };
+  };
+  testingDeepThrow = expr: testingThrow (builtins.deepSeq expr expr);
 
   testSanitizeDerivationName = { name, expected }:
   let
@@ -914,6 +919,30 @@ runTests {
     expected  = "«foo»";
   };
 
+  testToPlist =
+    let
+      deriv = derivation { name = "test"; builder = "/bin/sh"; system = "aarch64-linux"; };
+    in {
+    expr = mapAttrs (const (generators.toPlist { })) {
+      value = {
+        nested.values = rec {
+          int = 42;
+          float = 0.1337;
+          bool = true;
+          emptystring = "";
+          string = "fn\${o}\"r\\d";
+          newlinestring = "\n";
+          path = /. + "/foo";
+          null_ = null;
+          list = [ 3 4 "test" ];
+          emptylist = [];
+          attrs = { foo = null; "foo b/ar" = "baz"; };
+          emptyattrs = {};
+        };
+      };
+    };
+    expected = { value = builtins.readFile ./test-to-plist-expected.plist; };
+  };
 
   testToLuaEmptyAttrSet = {
     expr = generators.toLua {} {};
@@ -962,6 +991,41 @@ runTests {
     expected = ''{ 41, 43 }'';
   };
 
+  testToLuaEmptyBindings = {
+    expr = generators.toLua { asBindings = true; } {};
+    expected = "";
+  };
+
+  testToLuaBindings = {
+    expr = generators.toLua { asBindings = true; } { x1 = 41; _y = { a = 43; }; };
+    expected = ''
+      _y = {
+        ["a"] = 43
+      }
+      x1 = 41
+    '';
+  };
+
+  testToLuaPartialTableBindings = {
+    expr = generators.toLua { asBindings = true; } { "x.y" = 42; };
+    expected = ''
+      x.y = 42
+    '';
+  };
+
+  testToLuaIndentedBindings = {
+    expr = generators.toLua { asBindings = true; indent = "  "; } { x = { y = 42; }; };
+    expected = "  x = {\n    [\"y\"] = 42\n  }\n";
+  };
+
+  testToLuaBindingsWithSpace = testingThrow (
+    generators.toLua { asBindings = true; } { "with space" = 42; }
+  );
+
+  testToLuaBindingsWithLeadingDigit = testingThrow (
+    generators.toLua { asBindings = true; } { "11eleven" = 42; }
+  );
+
   testToLuaBasicExample = {
     expr = generators.toLua {} {
       cmd = [ "typescript-language-server" "--stdio" ];
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index c1cf0a94a1b3..45c247cbbea6 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -166,6 +166,7 @@ checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*'
 checkConfigOutput '^true$' "$@" ./define-module-check.nix
 
 # Check coerced value.
+set --
 checkConfigOutput '^"42"$' config.value ./declare-coerced-value.nix
 checkConfigOutput '^"24"$' config.value ./declare-coerced-value.nix ./define-value-string.nix
 checkConfigError 'A definition for option .* is not.*string or signed integer convertible to it.*. Definition values:\n\s*- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix
@@ -254,6 +255,8 @@ checkConfigError 'A definition for option .* is not of type .*' \
 ## Freeform modules
 # Assigning without a declared option should work
 checkConfigOutput '^"24"$' config.value ./freeform-attrsOf.nix ./define-value-string.nix
+# Shorthand modules interpret `meta` and `class` as config items
+checkConfigOutput '^true$' options._module.args.value.result ./freeform-attrsOf.nix ./define-freeform-keywords-shorthand.nix
 # No freeform assignments shouldn't make it error
 checkConfigOutput '^{ }$' config ./freeform-attrsOf.nix
 # but only if the type matches
@@ -359,6 +362,24 @@ checkConfigOutput 'ok' config.freeformItems.foo.bar ./adhoc-freeformType-survive
 # because of an `extendModules` bug, issue 168767.
 checkConfigOutput '^1$' config.sub.specialisation.value ./extendModules-168767-imports.nix
 
+# Class checks, evalModules
+checkConfigOutput '^{ }$' config.ok.config ./class-check.nix
+checkConfigOutput '"nixos"' config.ok.class ./class-check.nix
+checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.fail.config ./class-check.nix
+checkConfigError 'The module foo.nix#darwinModules.default was imported into nixos instead of darwin.' config.fail-anon.config ./class-check.nix
+
+# Class checks, submoduleWith
+checkConfigOutput '^{ }$' config.sub.nixosOk ./class-check.nix
+checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.sub.nixosFail.config ./class-check.nix
+
+# submoduleWith type merge with different class
+checkConfigError 'error: A submoduleWith option is declared multiple times with conflicting class values "darwin" and "nixos".' config.sub.mergeFail.config ./class-check.nix
+
+# _type check
+checkConfigError 'Could not load a value as a module, because it is of type "flake", in file .*/module-imports-_type-check.nix' config.ok.config ./module-imports-_type-check.nix
+checkConfigOutput '^true$' "$@" config.enable ./declare-enable.nix ./define-enable-with-top-level-mkIf.nix
+checkConfigError 'Could not load a value as a module, because it is of type "configuration", in file .*/import-configuration.nix.*please only import the modules that make up the configuration.*' config ./import-configuration.nix
+
 # doRename works when `warnings` does not exist.
 checkConfigOutput '^1234$' config.c.d.e ./doRename-basic.nix
 # doRename adds a warning.
diff --git a/lib/tests/modules/class-check.nix b/lib/tests/modules/class-check.nix
new file mode 100644
index 000000000000..293fd4abd628
--- /dev/null
+++ b/lib/tests/modules/class-check.nix
@@ -0,0 +1,76 @@
+{ lib, ... }: {
+  options = {
+    sub = {
+      nixosOk = lib.mkOption {
+        type = lib.types.submoduleWith {
+          class = "nixos";
+          modules = [ ];
+        };
+      };
+      # Same but will have bad definition
+      nixosFail = lib.mkOption {
+        type = lib.types.submoduleWith {
+          class = "nixos";
+          modules = [ ];
+        };
+      };
+
+      mergeFail = lib.mkOption {
+        type = lib.types.submoduleWith {
+          class = "nixos";
+          modules = [ ];
+        };
+        default = { };
+      };
+    };
+  };
+  imports = [
+    {
+      options = {
+        sub = {
+          mergeFail = lib.mkOption {
+            type = lib.types.submoduleWith {
+              class = "darwin";
+              modules = [ ];
+            };
+          };
+        };
+      };
+    }
+  ];
+  config = {
+    _module.freeformType = lib.types.anything;
+    ok =
+      lib.evalModules {
+        class = "nixos";
+        modules = [
+          ./module-class-is-nixos.nix
+        ];
+      };
+
+    fail =
+      lib.evalModules {
+        class = "nixos";
+        modules = [
+          ./module-class-is-nixos.nix
+          ./module-class-is-darwin.nix
+        ];
+      };
+
+    fail-anon =
+      lib.evalModules {
+        class = "nixos";
+        modules = [
+          ./module-class-is-nixos.nix
+          { _file = "foo.nix#darwinModules.default";
+            _class = "darwin";
+            config = {};
+            imports = [];
+          }
+        ];
+      };
+
+    sub.nixosOk = { _class = "nixos"; };
+    sub.nixosFail = { imports = [ ./module-class-is-darwin.nix ]; };
+  };
+}
diff --git a/lib/tests/modules/define-enable-with-top-level-mkIf.nix b/lib/tests/modules/define-enable-with-top-level-mkIf.nix
new file mode 100644
index 000000000000..4909c16d82b4
--- /dev/null
+++ b/lib/tests/modules/define-enable-with-top-level-mkIf.nix
@@ -0,0 +1,5 @@
+{ lib, ... }:
+# I think this might occur more realistically in a submodule
+{
+  imports = [ (lib.mkIf true { enable = true; }) ];
+}
diff --git a/lib/tests/modules/define-freeform-keywords-shorthand.nix b/lib/tests/modules/define-freeform-keywords-shorthand.nix
new file mode 100644
index 000000000000..8de1ec6a7475
--- /dev/null
+++ b/lib/tests/modules/define-freeform-keywords-shorthand.nix
@@ -0,0 +1,15 @@
+{ config, ... }: {
+  class = { "just" = "data"; };
+  a = "one";
+  b = "two";
+  meta = "meta";
+
+  _module.args.result =
+    let r = builtins.removeAttrs config [ "_module" ];
+    in builtins.trace (builtins.deepSeq r r) (r == {
+      a = "one";
+      b = "two";
+      class = { "just" = "data"; };
+      meta = "meta";
+    });
+}
diff --git a/lib/tests/modules/import-configuration.nix b/lib/tests/modules/import-configuration.nix
new file mode 100644
index 000000000000..a2a32bbee4ca
--- /dev/null
+++ b/lib/tests/modules/import-configuration.nix
@@ -0,0 +1,12 @@
+{ lib, ... }:
+let
+  myconf = lib.evalModules { modules = [ { } ]; };
+in
+{
+  imports = [
+    # We can't do this. A configuration is not equal to its set of a modules.
+    # Equating those would lead to a mess, as specialArgs, anonymous modules
+    # that can't be deduplicated, and possibly more come into play.
+    myconf
+  ];
+}
diff --git a/lib/tests/modules/module-class-is-darwin.nix b/lib/tests/modules/module-class-is-darwin.nix
new file mode 100644
index 000000000000..bacf45626d71
--- /dev/null
+++ b/lib/tests/modules/module-class-is-darwin.nix
@@ -0,0 +1,4 @@
+{
+  _class = "darwin";
+  config = {};
+}
diff --git a/lib/tests/modules/module-class-is-nixos.nix b/lib/tests/modules/module-class-is-nixos.nix
new file mode 100644
index 000000000000..6d62feedae48
--- /dev/null
+++ b/lib/tests/modules/module-class-is-nixos.nix
@@ -0,0 +1,4 @@
+{
+  _class = "nixos";
+  config = {};
+}
diff --git a/lib/tests/modules/module-imports-_type-check.nix b/lib/tests/modules/module-imports-_type-check.nix
new file mode 100644
index 000000000000..1e29c469daa5
--- /dev/null
+++ b/lib/tests/modules/module-imports-_type-check.nix
@@ -0,0 +1,3 @@
+{
+  imports = [ { _type = "flake"; } ];
+}
diff --git a/lib/tests/systems.nix b/lib/tests/systems.nix
index 88e2e4206d56..792aa94f3356 100644
--- a/lib/tests/systems.nix
+++ b/lib/tests/systems.nix
@@ -34,7 +34,7 @@ with lib.systems.doubles; lib.runTests {
   testredox = mseteq redox [ "x86_64-redox" ];
   testgnu = mseteq gnu (linux /* ++ kfreebsd ++ ... */);
   testillumos = mseteq illumos [ "x86_64-solaris" ];
-  testlinux = mseteq linux [ "aarch64-linux" "armv5tel-linux" "armv6l-linux" "armv7a-linux" "armv7l-linux" "i686-linux" "mips64el-linux" "mipsel-linux" "riscv32-linux" "riscv64-linux" "x86_64-linux" "powerpc64-linux" "powerpc64le-linux" "m68k-linux" "s390-linux" "s390x-linux" "microblaze-linux" "microblazeel-linux" ];
+  testlinux = mseteq linux [ "aarch64-linux" "armv5tel-linux" "armv6l-linux" "armv7a-linux" "armv7l-linux" "i686-linux" "mips64el-linux" "mipsel-linux" "riscv32-linux" "riscv64-linux" "x86_64-linux" "powerpc64-linux" "powerpc64le-linux" "m68k-linux" "s390-linux" "s390x-linux" "microblaze-linux" "microblazeel-linux" "loongarch64-linux" ];
   testnetbsd = mseteq netbsd [ "aarch64-netbsd" "armv6l-netbsd" "armv7a-netbsd" "armv7l-netbsd" "i686-netbsd" "m68k-netbsd" "mipsel-netbsd" "powerpc-netbsd" "riscv32-netbsd" "riscv64-netbsd" "x86_64-netbsd" ];
   testopenbsd = mseteq openbsd [ "i686-openbsd" "x86_64-openbsd" ];
   testwindows = mseteq windows [ "i686-cygwin" "x86_64-cygwin" "i686-windows" "x86_64-windows" ];
diff --git a/lib/tests/test-to-plist-expected.plist b/lib/tests/test-to-plist-expected.plist
new file mode 100644
index 000000000000..df0528a60767
--- /dev/null
+++ b/lib/tests/test-to-plist-expected.plist
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>nested</key>
+	<dict>
+		<key>values</key>
+		<dict>
+			<key>attrs</key>
+			<dict>
+				<key>foo b/ar</key>
+				<string>baz</string>
+			</dict>
+			<key>bool</key>
+			<true/>
+			<key>emptyattrs</key>
+			<dict>
+
+			</dict>
+			<key>emptylist</key>
+			<array>
+
+			</array>
+			<key>emptystring</key>
+			<string></string>
+			<key>float</key>
+			<real>0.133700</real>
+			<key>int</key>
+			<integer>42</integer>
+			<key>list</key>
+			<array>
+				<integer>3</integer>
+				<integer>4</integer>
+				<string>test</string>
+			</array>
+			<key>newlinestring</key>
+			<string>
+</string>
+			<key>path</key>
+			<string>/foo</string>
+			<key>string</key>
+			<string>fn${o}"r\d</string>
+		</dict>
+	</dict>
+</dict>
+</plist>
\ No newline at end of file
diff --git a/lib/types.nix b/lib/types.nix
index 666e6502d161..e0da18a2febb 100644
--- a/lib/types.nix
+++ b/lib/types.nix
@@ -696,6 +696,7 @@ rec {
       , specialArgs ? {}
       , shorthandOnlyDefinesConfig ? false
       , description ? null
+      , class ? null
       }@attrs:
       let
         inherit (lib.modules) evalModules;
@@ -707,7 +708,7 @@ rec {
         ) defs;
 
         base = evalModules {
-          inherit specialArgs;
+          inherit class specialArgs;
           modules = [{
             # This is a work-around for the fact that some sub-modules,
             # such as the one included in an attribute set, expects an "args"
@@ -762,9 +763,14 @@ rec {
         functor = defaultFunctor name // {
           type = types.submoduleWith;
           payload = {
-            inherit modules specialArgs shorthandOnlyDefinesConfig description;
+            inherit modules class specialArgs shorthandOnlyDefinesConfig description;
           };
           binOp = lhs: rhs: {
+            class =
+              if lhs.class == null then rhs.class
+              else if rhs.class == null then lhs.class
+              else if lhs.class == rhs.class then lhs.class
+              else throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\".";
             modules = lhs.modules ++ rhs.modules;
             specialArgs =
               let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs;