about summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/README.md73
-rw-r--r--lib/attrsets.nix36
-rw-r--r--lib/customisation.nix9
-rw-r--r--lib/generators.nix5
-rw-r--r--lib/licenses.nix7
-rw-r--r--lib/lists.nix70
-rw-r--r--lib/meta.nix11
-rw-r--r--lib/modules.nix36
-rw-r--r--lib/path/README.md21
-rw-r--r--lib/path/default.nix337
-rw-r--r--lib/path/tests/default.nix9
-rwxr-xr-xlib/path/tests/prop.sh7
-rw-r--r--lib/path/tests/unit.nix49
-rw-r--r--lib/systems/default.nix28
-rw-r--r--lib/systems/examples.nix4
-rw-r--r--lib/systems/platforms.nix7
-rw-r--r--lib/tests/misc.nix126
-rwxr-xr-xlib/tests/modules.sh38
-rw-r--r--lib/tests/modules/merge-typeless-option.nix25
-rw-r--r--lib/tests/modules/module-argument-default.nix9
-rw-r--r--lib/tests/modules/options-type-error-configuration.nix6
-rw-r--r--lib/tests/modules/options-type-error-typical-nested.nix5
-rw-r--r--lib/tests/modules/options-type-error-typical.nix5
-rwxr-xr-xlib/tests/sources.sh7
-rw-r--r--lib/trivial.nix4
-rw-r--r--lib/types.nix10
26 files changed, 786 insertions, 158 deletions
diff --git a/lib/README.md b/lib/README.md
new file mode 100644
index 0000000000000..ac7cbd4330add
--- /dev/null
+++ b/lib/README.md
@@ -0,0 +1,73 @@
+# Nixpkgs lib
+
+This directory contains the implementation, documentation and tests for the Nixpkgs `lib` library.
+
+## Overview
+
+The evaluation entry point for `lib` is [`default.nix`](default.nix).
+This file evaluates to an attribute set containing two separate kinds of attributes:
+- Sub-libraries:
+  Attribute sets grouping together similar functionality.
+  Each sub-library is defined in a separate file usually matching its attribute name.
+
+  Example: `lib.lists` is a sub-library containing list-related functionality such as `lib.lists.take` and `lib.lists.imap0`.
+  These are defined in the file [`lists.nix`](lists.nix).
+
+- Aliases:
+  Attributes that point to an attribute of the same name in some sub-library.
+
+  Example: `lib.take` is an alias for `lib.lists.take`.
+
+Most files in this directory are definitions of sub-libraries, but there are a few others:
+- [`minver.nix`](minver.nix): A string of the minimum version of Nix that is required to evaluate Nixpkgs.
+- [`tests`](tests): Tests, see [Running tests](#running-tests)
+  - [`release.nix`](tests/release.nix): A derivation aggregating all tests
+  - [`misc.nix`](tests/misc.nix): Evaluation unit tests for most sub-libraries
+  - `*.sh`: Bash scripts that run tests for specific sub-libraries
+  - All other files in this directory exist to support the tests
+- [`systems`](systems): The `lib.systems` sub-library, structured into a directory instead of a file due to its complexity
+- [`path`](path): The `lib.path` sub-library, which includes tests as well as a document describing the design goals of `lib.path`
+- All other files in this directory are sub-libraries
+
+### Module system
+
+The [module system](https://nixos.org/manual/nixpkgs/#module-system) spans multiple sub-libraries:
+- [`modules.nix`](modules.nix): `lib.modules` for the core functions and anything not relating to option definitions
+- [`options.nix`](options.nix): `lib.options` for anything relating to option definitions
+- [`types.nix`](types.nix): `lib.types` for module system types
+
+## Reference documentation
+
+Reference documentation for library functions is written above each function as a multi-line comment.
+These comments are processed using [nixdoc](https://github.com/nix-community/nixdoc) and [rendered in the Nixpkgs manual](https://nixos.org/manual/nixpkgs/stable/#chap-functions).
+The nixdoc README describes the [comment format](https://github.com/nix-community/nixdoc#comment-format).
+
+See the [chapter on contributing to the Nixpkgs manual](https://nixos.org/manual/nixpkgs/#chap-contributing) for how to build the manual.
+
+## Running tests
+
+All library tests can be run by building the derivation in [`tests/release.nix`](tests/release.nix):
+
+```bash
+nix-build tests/release.nix
+```
+
+Some commands for quicker iteration over parts of the test suite are also available:
+
+```bash
+# Run all evaluation unit tests in tests/misc.nix
+# if the resulting list is empty, all tests passed
+nix-instantiate --eval --strict tests/misc.nix
+
+# Run the module system tests
+tests/modules.sh
+
+# Run the lib.sources tests
+tests/sources.sh
+
+# Run the lib.filesystem tests
+tests/filesystem.sh
+
+# Run the lib.path property tests
+path/tests/prop.sh
+```
diff --git a/lib/attrsets.nix b/lib/attrsets.nix
index 0335146e2a6b4..77e36d3271f76 100644
--- a/lib/attrsets.nix
+++ b/lib/attrsets.nix
@@ -738,6 +738,42 @@ rec {
     sets:
     zipAttrsWith (name: values: values) sets;
 
+  /*
+    Merge a list of attribute sets together using the `//` operator.
+    In case of duplicate attributes, values from later list elements take precedence over earlier ones.
+    The result is the same as `foldl mergeAttrs { }`, but the performance is better for large inputs.
+    For n list elements, each with an attribute set containing m unique attributes, the complexity of this operation is O(nm log n).
+
+    Type:
+      mergeAttrsList :: [ Attrs ] -> Attrs
+
+    Example:
+      mergeAttrsList [ { a = 0; b = 1; } { c = 2; d = 3; } ]
+      => { a = 0; b = 1; c = 2; d = 3; }
+      mergeAttrsList [ { a = 0; } { a = 1; } ]
+      => { a = 1; }
+  */
+  mergeAttrsList = list:
+    let
+      # `binaryMerge start end` merges the elements at indices `index` of `list` such that `start <= index < end`
+      # Type: Int -> Int -> Attrs
+      binaryMerge = start: end:
+        # assert start < end; # Invariant
+        if end - start >= 2 then
+          # If there's at least 2 elements, split the range in two, recurse on each part and merge the result
+          # The invariant is satisfied because each half will have at least 1 element
+          binaryMerge start (start + (end - start) / 2)
+          // binaryMerge (start + (end - start) / 2) end
+        else
+          # Otherwise there will be exactly 1 element due to the invariant, in which case we just return it directly
+          elemAt list start;
+    in
+    if list == [ ] then
+      # Calling binaryMerge as below would not satisfy its invariant
+      { }
+    else
+      binaryMerge 0 (length list);
+
 
   /* Does the same as the update operator '//' except that attributes are
      merged until the given predicate is verified.  The predicate should
diff --git a/lib/customisation.nix b/lib/customisation.nix
index a9281b1ab698a..e13320076025c 100644
--- a/lib/customisation.nix
+++ b/lib/customisation.nix
@@ -269,10 +269,11 @@ rec {
     let self = f self // {
           newScope = scope: newScope (self // scope);
           callPackage = self.newScope {};
-          overrideScope = g: lib.warn
-            "`overrideScope` (from `lib.makeScope`) is deprecated. Do `overrideScope' (self: super: { … })` instead of `overrideScope (super: self: { … })`. All other overrides have the parameters in that order, including other definitions of `overrideScope`. This was the only definition violating the pattern."
-            (makeScope newScope (lib.fixedPoints.extends (lib.flip g) f));
-          overrideScope' = g: makeScope newScope (lib.fixedPoints.extends g f);
+          overrideScope = g: makeScope newScope (lib.fixedPoints.extends g f);
+          # Remove after 24.11 is released.
+          overrideScope' = g: lib.warnIf (lib.isInOldestRelease 2311)
+            "`overrideScope'` (from `lib.makeScope`) has been renamed to `overrideScope`."
+            (makeScope newScope (lib.fixedPoints.extends g f));
           packages = f;
         };
     in self;
diff --git a/lib/generators.nix b/lib/generators.nix
index a2dddedd2d3a7..c37be1942d82f 100644
--- a/lib/generators.nix
+++ b/lib/generators.nix
@@ -81,9 +81,10 @@ rec {
    */
   toKeyValue = {
     mkKeyValue ? mkKeyValueDefault {} "=",
-    listsAsDuplicateKeys ? false
+    listsAsDuplicateKeys ? false,
+    indent ? ""
   }:
-  let mkLine = k: v: mkKeyValue k v + "\n";
+  let mkLine = k: v: indent + mkKeyValue k v + "\n";
       mkLines = if listsAsDuplicateKeys
         then k: v: map (mkLine k) (if lib.isList v then v else [v])
         else k: v: [ (mkLine k v) ];
diff --git a/lib/licenses.nix b/lib/licenses.nix
index ee71488263a93..599e8ee53c93a 100644
--- a/lib/licenses.nix
+++ b/lib/licenses.nix
@@ -657,6 +657,13 @@ in mkLicense lset) ({
     redistributable = true;
   };
 
+  hl3 = {
+    fullName = "Hippocratic License v3.0";
+    url = "https://firstdonoharm.dev/version/3/0/core.txt";
+    free = false;
+    redistributable = true;
+  };
+
   issl = {
     fullName = "Intel Simplified Software License";
     url = "https://software.intel.com/en-us/license/intel-simplified-software-license";
diff --git a/lib/lists.nix b/lib/lists.nix
index c9821819821e7..0800aeb654516 100644
--- a/lib/lists.nix
+++ b/lib/lists.nix
@@ -3,7 +3,7 @@
 { lib }:
 let
   inherit (lib.strings) toInt;
-  inherit (lib.trivial) compare min;
+  inherit (lib.trivial) compare min id;
   inherit (lib.attrsets) mapAttrs;
 in
 rec {
@@ -180,18 +180,18 @@ rec {
       else if len != 1 then multiple
       else head found;
 
-  /* Find the first element in the list matching the specified
+  /* Find the first index in the list matching the specified
      predicate or return `default` if no such element exists.
 
-     Type: findFirst :: (a -> bool) -> a -> [a] -> a
+     Type: findFirstIndex :: (a -> Bool) -> b -> [a] -> (Int | b)
 
      Example:
-       findFirst (x: x > 3) 7 [ 1 6 4 ]
-       => 6
-       findFirst (x: x > 9) 7 [ 1 6 4 ]
-       => 7
+       findFirstIndex (x: x > 3) null [ 0 6 4 ]
+       => 1
+       findFirstIndex (x: x > 9) null [ 0 6 4 ]
+       => null
   */
-  findFirst =
+  findFirstIndex =
     # Predicate
     pred:
     # Default value to return
@@ -229,7 +229,33 @@ rec {
     if resultIndex < 0 then
       default
     else
-      elemAt list resultIndex;
+      resultIndex;
+
+  /* Find the first element in the list matching the specified
+     predicate or return `default` if no such element exists.
+
+     Type: findFirst :: (a -> bool) -> a -> [a] -> a
+
+     Example:
+       findFirst (x: x > 3) 7 [ 1 6 4 ]
+       => 6
+       findFirst (x: x > 9) 7 [ 1 6 4 ]
+       => 7
+  */
+  findFirst =
+    # Predicate
+    pred:
+    # Default value to return
+    default:
+    # Input list
+    list:
+    let
+      index = findFirstIndex pred null list;
+    in
+    if index == null then
+      default
+    else
+      elemAt list index;
 
   /* Return true if function `pred` returns true for at least one
      element of `list`.
@@ -671,6 +697,32 @@ rec {
        else if start + count > len then len - start
        else count);
 
+  /* The common prefix of two lists.
+
+  Type: commonPrefix :: [a] -> [a] -> [a]
+
+  Example:
+    commonPrefix [ 1 2 3 4 5 6 ] [ 1 2 4 8 ]
+    => [ 1 2 ]
+    commonPrefix [ 1 2 3 ] [ 1 2 3 4 5 ]
+    => [ 1 2 3 ]
+    commonPrefix [ 1 2 3 ] [ 4 5 6 ]
+    => [ ]
+  */
+  commonPrefix =
+    list1:
+    list2:
+    let
+      # Zip the lists together into a list of booleans whether each element matches
+      matchings = zipListsWith (fst: snd: fst != snd) list1 list2;
+      # Find the first index where the elements don't match,
+      # which will then also be the length of the common prefix.
+      # If all elements match, we fall back to the length of the zipped list,
+      # which is the same as the length of the smaller list.
+      commonPrefixLength = findFirstIndex id (length matchings) matchings;
+    in
+    take commonPrefixLength list1;
+
   /* Return the last element of a list.
 
      This function throws an error if the list is empty.
diff --git a/lib/meta.nix b/lib/meta.nix
index 5fd55c4e90d69..21404b3a2bfaa 100644
--- a/lib/meta.nix
+++ b/lib/meta.nix
@@ -132,10 +132,9 @@ rec {
         { shortName = licstr; }
       );
 
-  /* Get the path to the main program of a derivation with either
-     meta.mainProgram or pname or name
+  /* Get the path to the main program of a package based on meta.mainProgram
 
-     Type: getExe :: derivation -> string
+     Type: getExe :: package -> string
 
      Example:
        getExe pkgs.hello
@@ -144,5 +143,9 @@ rec {
        => "/nix/store/am9ml4f4ywvivxnkiaqwr0hyxka1xjsf-mustache-go-1.3.0/bin/mustache"
   */
   getExe = x:
-    "${lib.getBin x}/bin/${x.meta.mainProgram or (lib.getName x)}";
+    "${lib.getBin x}/bin/${x.meta.mainProgram or (
+      # This could be turned into an error when 23.05 is at end of life
+      lib.warn "getExe: Package ${lib.strings.escapeNixIdentifier x.meta.name or x.pname or x.name} does not have the meta.mainProgram attribute. We'll assume that the main program has the same name for now, but this behavior is deprecated, because it leads to surprising errors when the assumption does not hold. If the package has a main program, please set `meta.mainProgram` in its definition to make this warning go away. Otherwise, if the package does not have a main program, or if you don't control its definition, specify the full path to the program, such as \"\${lib.getBin foo}/bin/bar\"."
+      lib.getName x
+    )}";
 }
diff --git a/lib/modules.nix b/lib/modules.nix
index f16df20425ef3..9371ba4b27a4e 100644
--- a/lib/modules.nix
+++ b/lib/modules.nix
@@ -630,7 +630,13 @@ let
           loc = prefix ++ [name];
           defns = pushedDownDefinitionsByName.${name} or [];
           defns' = rawDefinitionsByName.${name} or [];
-          optionDecls = filter (m: isOption m.options) decls;
+          optionDecls = filter
+            (m: m.options?_type
+                && (m.options._type == "option"
+                    || throwDeclarationTypeError loc m.options._type
+                )
+            )
+            decls;
         in
           if length optionDecls == length decls then
             let opt = fixupOptionType loc (mergeOptionDecls loc decls);
@@ -639,7 +645,7 @@ let
               unmatchedDefns = [];
             }
           else if optionDecls != [] then
-              if all (x: x.options.type.name == "submodule") optionDecls
+              if all (x: x.options.type.name or null == "submodule") optionDecls
               # Raw options can only be merged into submodules. Merging into
               # attrsets might be nice, but ambiguous. Suppose we have
               # attrset as a `attrsOf submodule`. User declares option
@@ -692,6 +698,32 @@ let
           ) unmatchedDefnsByName);
     };
 
+  throwDeclarationTypeError = loc: actualTag:
+    let
+      name = lib.strings.escapeNixIdentifier (lib.lists.last loc);
+      path = showOption loc;
+      depth = length loc;
+
+      paragraphs = [
+        "Expected an option declaration at option path `${path}` but got an attribute set with type ${actualTag}"
+      ] ++ optional (actualTag == "option-type") ''
+          When declaring an option, you must wrap the type in a `mkOption` call. It should look somewhat like:
+              ${comment}
+              ${name} = lib.mkOption {
+                description = ...;
+                type = <the type you wrote for ${name}>;
+                ...
+              };
+        '';
+
+      # Ideally we'd know the exact syntax they used, but short of that,
+      # we can only reliably repeat the last. However, we repeat the
+      # full path in a non-misleading way here, in case they overlook
+      # the start of the message. Examples attract attention.
+      comment = optionalString (depth > 1) "\n    # ${showOption loc}";
+    in
+    throw (concatStringsSep "\n\n" paragraphs);
+
   /* Merge multiple option declarations into a single declaration.  In
      general, there should be only one declaration of each option.
      The exception is the ‘options’ attribute, which specifies
diff --git a/lib/path/README.md b/lib/path/README.md
index 87e552d120d77..89eec18b11300 100644
--- a/lib/path/README.md
+++ b/lib/path/README.md
@@ -187,6 +187,27 @@ Decision: All functions remove trailing slashes in their results.
 
 </details>
 
+### Prefer returning subpaths over components
+[subpath-preference]: #prefer-returning-subpaths-over-components
+
+Observing: Functions could return subpaths or lists of path component strings.
+
+Considering: Subpaths are used as inputs for some functions. Using them for outputs, too, makes the library more consistent and composable.
+
+Decision: Subpaths should be preferred over list of path component strings.
+
+<details>
+<summary>Arguments</summary>
+
+- (+) It is consistent with functions accepting subpaths, making the library more composable
+- (-) It is less efficient when the components are needed, because after creating the normalised subpath string, it will have to be parsed into components again
+  - (+) If necessary, we can still make it faster by adding builtins to Nix
+  - (+) Alternatively if necessary, versions of these functions that return components could later still be introduced.
+- (+) It makes the path library simpler because there's only two types (paths and subpaths). Only `lib.path.subpath.components` can be used to get a list of components.
+  And once we have a list of component strings, `lib.lists` and `lib.strings` can be used to operate on them.
+  For completeness, `lib.path.subpath.join` allows converting the list of components back to a subpath.
+</details>
+
 ## Other implementations and references
 
 - [Rust](https://doc.rust-lang.org/std/path/struct.Path.html)
diff --git a/lib/path/default.nix b/lib/path/default.nix
index 936e9b0302534..1a55a2a7be8dd 100644
--- a/lib/path/default.nix
+++ b/lib/path/default.nix
@@ -20,6 +20,7 @@ let
     concatMap
     foldl'
     take
+    drop
     ;
 
   inherit (lib.strings)
@@ -120,17 +121,18 @@ let
 
 in /* No rec! Add dependencies on this file at the top. */ {
 
-  /* Append a subpath string to a path.
+  /*
+    Append a subpath string to a path.
 
     Like `path + ("/" + string)` but safer, because it errors instead of returning potentially surprising results.
     More specifically, it checks that the first argument is a [path value type](https://nixos.org/manual/nix/stable/language/values.html#type-path"),
-    and that the second argument is a valid subpath string (see `lib.path.subpath.isValid`).
+    and that the second argument is a [valid subpath string](#function-library-lib.path.subpath.isValid).
 
     Laws:
 
-    - Not influenced by subpath normalisation
+    - Not influenced by subpath [normalisation](#function-library-lib.path.subpath.normalise):
 
-        append p s == append p (subpath.normalise s)
+          append p s == append p (subpath.normalise s)
 
     Type:
       append :: Path -> String -> Path
@@ -174,26 +176,26 @@ in /* No rec! Add dependencies on this file at the top. */ {
     path + ("/" + subpath);
 
   /*
-  Whether the first path is a component-wise prefix of the second path.
+    Whether the first path is a component-wise prefix of the second path.
 
-  Laws:
+    Laws:
 
-  - `hasPrefix p q` is only true if `q == append p s` for some subpath `s`.
+    - `hasPrefix p q` is only true if [`q == append p s`](#function-library-lib.path.append) for some [subpath](#function-library-lib.path.subpath.isValid) `s`.
 
-  - `hasPrefix` is a [non-strict partial order](https://en.wikipedia.org/wiki/Partially_ordered_set#Non-strict_partial_order) over the set of all path values
+    - `hasPrefix` is a [non-strict partial order](https://en.wikipedia.org/wiki/Partially_ordered_set#Non-strict_partial_order) over the set of all path values.
 
-  Type:
-    hasPrefix :: Path -> Path -> Bool
+    Type:
+      hasPrefix :: Path -> Path -> Bool
 
-  Example:
-    hasPrefix /foo /foo/bar
-    => true
-    hasPrefix /foo /foo
-    => true
-    hasPrefix /foo/bar /foo
-    => false
-    hasPrefix /. /foo
-    => true
+    Example:
+      hasPrefix /foo /foo/bar
+      => true
+      hasPrefix /foo /foo
+      => true
+      hasPrefix /foo/bar /foo
+      => false
+      hasPrefix /. /foo
+      => true
   */
   hasPrefix =
     path1:
@@ -217,44 +219,148 @@ in /* No rec! Add dependencies on this file at the top. */ {
               second argument: "${toString path2}" with root "${toString path2Deconstructed.root}"'';
         take (length path1Deconstructed.components) path2Deconstructed.components == path1Deconstructed.components;
 
+  /*
+    Remove the first path as a component-wise prefix from the second path.
+    The result is a [normalised subpath string](#function-library-lib.path.subpath.normalise).
 
-  /* Whether a value is a valid subpath string.
+    Laws:
 
-  - The value is a string
+    - Inverts [`append`](#function-library-lib.path.append) for [normalised subpath string](#function-library-lib.path.subpath.normalise):
 
-  - The string is not empty
+          removePrefix p (append p s) == subpath.normalise s
 
-  - The string doesn't start with a `/`
+    Type:
+      removePrefix :: Path -> Path -> String
 
-  - The string doesn't contain any `..` path components
+    Example:
+      removePrefix /foo /foo/bar/baz
+      => "./bar/baz"
+      removePrefix /foo /foo
+      => "./."
+      removePrefix /foo/bar /foo
+      => <error>
+      removePrefix /. /foo
+      => "./foo"
+  */
+  removePrefix =
+    path1:
+    assert assertMsg
+      (isPath path1)
+      "lib.path.removePrefix: First argument is of type ${typeOf path1}, but a path was expected.";
+    let
+      path1Deconstructed = deconstructPath path1;
+      path1Length = length path1Deconstructed.components;
+    in
+      path2:
+      assert assertMsg
+        (isPath path2)
+        "lib.path.removePrefix: Second argument is of type ${typeOf path2}, but a path was expected.";
+      let
+        path2Deconstructed = deconstructPath path2;
+        success = take path1Length path2Deconstructed.components == path1Deconstructed.components;
+        components =
+          if success then
+            drop path1Length path2Deconstructed.components
+          else
+            throw ''
+              lib.path.removePrefix: The first path argument "${toString path1}" is not a component-wise prefix of the second path argument "${toString path2}".'';
+      in
+        assert assertMsg
+        (path1Deconstructed.root == path2Deconstructed.root) ''
+          lib.path.removePrefix: Filesystem roots must be the same for both paths, but paths with different roots were given:
+              first argument: "${toString path1}" with root "${toString path1Deconstructed.root}"
+              second argument: "${toString path2}" with root "${toString path2Deconstructed.root}"'';
+        joinRelPath components;
+
+  /*
+    Split the filesystem root from a [path](https://nixos.org/manual/nix/stable/language/values.html#type-path).
+    The result is an attribute set with these attributes:
+    - `root`: The filesystem root of the path, meaning that this directory has no parent directory.
+    - `subpath`: The [normalised subpath string](#function-library-lib.path.subpath.normalise) that when [appended](#function-library-lib.path.append) to `root` returns the original path.
+
+    Laws:
+    - [Appending](#function-library-lib.path.append) the `root` and `subpath` gives the original path:
 
-  Type:
-    subpath.isValid :: String -> Bool
+          p ==
+            append
+              (splitRoot p).root
+              (splitRoot p).subpath
 
-  Example:
-    # Not a string
-    subpath.isValid null
-    => false
+    - Trying to get the parent directory of `root` using [`readDir`](https://nixos.org/manual/nix/stable/language/builtins.html#builtins-readDir) returns `root` itself:
 
-    # Empty string
-    subpath.isValid ""
-    => false
+          dirOf (splitRoot p).root == (splitRoot p).root
 
-    # Absolute path
-    subpath.isValid "/foo"
-    => false
+    Type:
+      splitRoot :: Path -> { root :: Path, subpath :: String }
+
+    Example:
+      splitRoot /foo/bar
+      => { root = /.; subpath = "./foo/bar"; }
 
-    # Contains a `..` path component
-    subpath.isValid "../foo"
-    => false
+      splitRoot /.
+      => { root = /.; subpath = "./."; }
 
-    # Valid subpath
-    subpath.isValid "foo/bar"
-    => true
+      # Nix neutralises `..` path components for all path values automatically
+      splitRoot /foo/../bar
+      => { root = /.; subpath = "./bar"; }
 
-    # Doesn't need to be normalised
-    subpath.isValid "./foo//bar/"
-    => true
+      splitRoot "/foo/bar"
+      => <error>
+  */
+  splitRoot =
+    # The path to split the root off of
+    path:
+    assert assertMsg
+      (isPath path)
+      "lib.path.splitRoot: Argument is of type ${typeOf path}, but a path was expected";
+    let
+      deconstructed = deconstructPath path;
+    in {
+      root = deconstructed.root;
+      subpath = joinRelPath deconstructed.components;
+    };
+
+  /*
+    Whether a value is a valid subpath string.
+
+    A subpath string points to a specific file or directory within an absolute base directory.
+    It is a stricter form of a relative path that excludes `..` components, since those could escape the base directory.
+
+    - The value is a string.
+
+    - The string is not empty.
+
+    - The string doesn't start with a `/`.
+
+    - The string doesn't contain any `..` path components.
+
+    Type:
+      subpath.isValid :: String -> Bool
+
+    Example:
+      # Not a string
+      subpath.isValid null
+      => false
+
+      # Empty string
+      subpath.isValid ""
+      => false
+
+      # Absolute path
+      subpath.isValid "/foo"
+      => false
+
+      # Contains a `..` path component
+      subpath.isValid "../foo"
+      => false
+
+      # Valid subpath
+      subpath.isValid "foo/bar"
+      => true
+
+      # Doesn't need to be normalised
+      subpath.isValid "./foo//bar/"
+      => true
   */
   subpath.isValid =
     # The value to check
@@ -262,15 +368,16 @@ in /* No rec! Add dependencies on this file at the top. */ {
     subpathInvalidReason value == null;
 
 
-  /* Join subpath strings together using `/`, returning a normalised subpath string.
+  /*
+    Join subpath strings together using `/`, returning a normalised subpath string.
 
     Like `concatStringsSep "/"` but safer, specifically:
 
-    - All elements must be valid subpath strings, see `lib.path.subpath.isValid`
+    - All elements must be [valid subpath strings](#function-library-lib.path.subpath.isValid).
 
-    - The result gets normalised, see `lib.path.subpath.normalise`
+    - The result gets [normalised](#function-library-lib.path.subpath.normalise).
 
-    - The edge case of an empty list gets properly handled by returning the neutral subpath `"./."`
+    - The edge case of an empty list gets properly handled by returning the neutral subpath `"./."`.
 
     Laws:
 
@@ -284,12 +391,12 @@ in /* No rec! Add dependencies on this file at the top. */ {
           subpath.join [ (subpath.normalise p) "./." ] == subpath.normalise p
           subpath.join [ "./." (subpath.normalise p) ] == subpath.normalise p
 
-    - Normalisation - the result is normalised according to `lib.path.subpath.normalise`:
+    - Normalisation - the result is [normalised](#function-library-lib.path.subpath.normalise):
 
           subpath.join ps == subpath.normalise (subpath.join ps)
 
-    - For non-empty lists, the implementation is equivalent to normalising the result of `concatStringsSep "/"`.
-      Note that the above laws can be derived from this one.
+    - For non-empty lists, the implementation is equivalent to [normalising](#function-library-lib.path.subpath.normalise) the result of `concatStringsSep "/"`.
+      Note that the above laws can be derived from this one:
 
           ps != [] -> subpath.join ps == subpath.normalise (concatStringsSep "/" ps)
 
@@ -336,78 +443,110 @@ in /* No rec! Add dependencies on this file at the top. */ {
               ${subpathInvalidReason path}''
       ) 0 subpaths;
 
-  /* Normalise a subpath. Throw an error if the subpath isn't valid, see
-  `lib.path.subpath.isValid`
+  /*
+    Split [a subpath](#function-library-lib.path.subpath.isValid) into its path component strings.
+    Throw an error if the subpath isn't valid.
+    Note that the returned path components are also [valid subpath strings](#function-library-lib.path.subpath.isValid), though they are intentionally not [normalised](#function-library-lib.path.subpath.normalise).
+
+    Laws:
+
+    - Splitting a subpath into components and [joining](#function-library-lib.path.subpath.join) the components gives the same subpath but [normalised](#function-library-lib.path.subpath.normalise):
+
+          subpath.join (subpath.components s) == subpath.normalise s
 
-  - Limit repeating `/` to a single one
+    Type:
+      subpath.components :: String -> [ String ]
+
+    Example:
+      subpath.components "."
+      => [ ]
+
+      subpath.components "./foo//bar/./baz/"
+      => [ "foo" "bar" "baz" ]
+
+      subpath.components "/foo"
+      => <error>
+  */
+  subpath.components =
+    # The subpath string to split into components
+    subpath:
+    assert assertMsg (isValid subpath) ''
+      lib.path.subpath.components: Argument is not a valid subpath string:
+          ${subpathInvalidReason subpath}'';
+    splitRelPath subpath;
 
-  - Remove redundant `.` components
+  /*
+    Normalise a subpath. Throw an error if the subpath isn't [valid](#function-library-lib.path.subpath.isValid).
+
+    - Limit repeating `/` to a single one.
 
-  - Remove trailing `/` and `/.`
+    - Remove redundant `.` components.
 
-  - Add leading `./`
+    - Remove trailing `/` and `/.`.
 
-  Laws:
+    - Add leading `./`.
 
-  - Idempotency - normalising multiple times gives the same result:
+    Laws:
 
-        subpath.normalise (subpath.normalise p) == subpath.normalise p
+    - Idempotency - normalising multiple times gives the same result:
 
-  - Uniqueness - there's only a single normalisation for the paths that lead to the same file system node:
+          subpath.normalise (subpath.normalise p) == subpath.normalise p
 
-        subpath.normalise p != subpath.normalise q -> $(realpath ${p}) != $(realpath ${q})
+    - Uniqueness - there's only a single normalisation for the paths that lead to the same file system node:
 
-  - Don't change the result when appended to a Nix path value:
+          subpath.normalise p != subpath.normalise q -> $(realpath ${p}) != $(realpath ${q})
 
-        base + ("/" + p) == base + ("/" + subpath.normalise p)
+    - Don't change the result when [appended](#function-library-lib.path.append) to a Nix path value:
 
-  - Don't change the path according to `realpath`:
+          append base p == append base (subpath.normalise p)
 
-        $(realpath ${p}) == $(realpath ${subpath.normalise p})
+    - Don't change the path according to `realpath`:
 
-  - Only error on invalid subpaths:
+          $(realpath ${p}) == $(realpath ${subpath.normalise p})
 
-        builtins.tryEval (subpath.normalise p)).success == subpath.isValid p
+    - Only error on [invalid subpaths](#function-library-lib.path.subpath.isValid):
 
-  Type:
-    subpath.normalise :: String -> String
+          builtins.tryEval (subpath.normalise p)).success == subpath.isValid p
+
+    Type:
+      subpath.normalise :: String -> String
 
-  Example:
-    # limit repeating `/` to a single one
-    subpath.normalise "foo//bar"
-    => "./foo/bar"
+    Example:
+      # limit repeating `/` to a single one
+      subpath.normalise "foo//bar"
+      => "./foo/bar"
 
-    # remove redundant `.` components
-    subpath.normalise "foo/./bar"
-    => "./foo/bar"
+      # remove redundant `.` components
+      subpath.normalise "foo/./bar"
+      => "./foo/bar"
 
-    # add leading `./`
-    subpath.normalise "foo/bar"
-    => "./foo/bar"
+      # add leading `./`
+      subpath.normalise "foo/bar"
+      => "./foo/bar"
 
-    # remove trailing `/`
-    subpath.normalise "foo/bar/"
-    => "./foo/bar"
+      # remove trailing `/`
+      subpath.normalise "foo/bar/"
+      => "./foo/bar"
 
-    # remove trailing `/.`
-    subpath.normalise "foo/bar/."
-    => "./foo/bar"
+      # remove trailing `/.`
+      subpath.normalise "foo/bar/."
+      => "./foo/bar"
 
-    # Return the current directory as `./.`
-    subpath.normalise "."
-    => "./."
+      # Return the current directory as `./.`
+      subpath.normalise "."
+      => "./."
 
-    # error on `..` path components
-    subpath.normalise "foo/../bar"
-    => <error>
+      # error on `..` path components
+      subpath.normalise "foo/../bar"
+      => <error>
 
-    # error on empty string
-    subpath.normalise ""
-    => <error>
+      # error on empty string
+      subpath.normalise ""
+      => <error>
 
-    # error on absolute path
-    subpath.normalise "/foo"
-    => <error>
+      # error on absolute path
+      subpath.normalise "/foo"
+      => <error>
   */
   subpath.normalise =
     # The subpath string to normalise
diff --git a/lib/path/tests/default.nix b/lib/path/tests/default.nix
index 6b8e515f43304..50d40cdfa476e 100644
--- a/lib/path/tests/default.nix
+++ b/lib/path/tests/default.nix
@@ -18,7 +18,14 @@ pkgs.runCommand "lib-path-tests" {
   ];
 } ''
   # Needed to make Nix evaluation work
-  export NIX_STATE_DIR=$(mktemp -d)
+  export TEST_ROOT=$(pwd)/test-tmp
+  export NIX_BUILD_HOOK=
+  export NIX_CONF_DIR=$TEST_ROOT/etc
+  export NIX_LOCALSTATE_DIR=$TEST_ROOT/var
+  export NIX_LOG_DIR=$TEST_ROOT/var/log/nix
+  export NIX_STATE_DIR=$TEST_ROOT/var/nix
+  export NIX_STORE_DIR=$TEST_ROOT/store
+  export PAGER=cat
 
   cp -r ${libpath} lib
   export TEST_LIB=$PWD/lib
diff --git a/lib/path/tests/prop.sh b/lib/path/tests/prop.sh
index 9fea521577836..f321fdf1cf450 100755
--- a/lib/path/tests/prop.sh
+++ b/lib/path/tests/prop.sh
@@ -1,9 +1,12 @@
 #!/usr/bin/env bash
 
-# Property tests for the `lib.path` library
-#
+# Property tests for lib/path/default.nix
 # It generates random path-like strings and runs the functions on
 # them, checking that the expected laws of the functions hold
+# Run:
+# [nixpkgs]$ lib/path/tests/prop.sh
+# or:
+# [nixpkgs]$ nix-build lib/tests/release.nix
 
 set -euo pipefail
 shopt -s inherit_errexit
diff --git a/lib/path/tests/unit.nix b/lib/path/tests/unit.nix
index 9c5b752cf64a3..bad6560f13a98 100644
--- a/lib/path/tests/unit.nix
+++ b/lib/path/tests/unit.nix
@@ -3,7 +3,7 @@
 { libpath }:
 let
   lib = import libpath;
-  inherit (lib.path) hasPrefix append subpath;
+  inherit (lib.path) hasPrefix removePrefix append splitRoot subpath;
 
   cases = lib.runTests {
     # Test examples from the lib.path.append documentation
@@ -57,6 +57,40 @@ let
       expected = true;
     };
 
+    testRemovePrefixExample1 = {
+      expr = removePrefix /foo /foo/bar/baz;
+      expected = "./bar/baz";
+    };
+    testRemovePrefixExample2 = {
+      expr = removePrefix /foo /foo;
+      expected = "./.";
+    };
+    testRemovePrefixExample3 = {
+      expr = (builtins.tryEval (removePrefix /foo/bar /foo)).success;
+      expected = false;
+    };
+    testRemovePrefixExample4 = {
+      expr = removePrefix /. /foo;
+      expected = "./foo";
+    };
+
+    testSplitRootExample1 = {
+      expr = splitRoot /foo/bar;
+      expected = { root = /.; subpath = "./foo/bar"; };
+    };
+    testSplitRootExample2 = {
+      expr = splitRoot /.;
+      expected = { root = /.; subpath = "./."; };
+    };
+    testSplitRootExample3 = {
+      expr = splitRoot /foo/../bar;
+      expected = { root = /.; subpath = "./bar"; };
+    };
+    testSplitRootExample4 = {
+      expr = (builtins.tryEval (splitRoot "/foo/bar")).success;
+      expected = false;
+    };
+
     # Test examples from the lib.path.subpath.isValid documentation
     testSubpathIsValidExample1 = {
       expr = subpath.isValid null;
@@ -204,6 +238,19 @@ let
       expr = (builtins.tryEval (subpath.normalise "..")).success;
       expected = false;
     };
+
+    testSubpathComponentsExample1 = {
+      expr = subpath.components ".";
+      expected = [ ];
+    };
+    testSubpathComponentsExample2 = {
+      expr = subpath.components "./foo//bar/./baz/";
+      expected = [ "foo" "bar" "baz" ];
+    };
+    testSubpathComponentsExample3 = {
+      expr = (builtins.tryEval (subpath.components "/foo")).success;
+      expected = false;
+    };
   };
 in
   if cases == [] then "Unit tests successful"
diff --git a/lib/systems/default.nix b/lib/systems/default.nix
index 78ccd50ba79a5..40a2c88f32b8f 100644
--- a/lib/systems/default.nix
+++ b/lib/systems/default.nix
@@ -85,17 +85,18 @@ rec {
         # is why we use the more obscure "bfd" and not "binutils" for this
         # choice.
         else                                     "bfd";
-      extensions = rec {
+      extensions = lib.optionalAttrs final.hasSharedLibraries {
         sharedLibrary =
-          /**/ if final.isDarwin  then ".dylib"
+          if      final.isDarwin  then ".dylib"
           else if final.isWindows then ".dll"
           else                         ".so";
+      } // {
         staticLibrary =
           /**/ if final.isWindows then ".lib"
           else                         ".a";
         library =
-          /**/ if final.isStatic then staticLibrary
-          else                        sharedLibrary;
+          /**/ if final.isStatic then final.extensions.staticLibrary
+          else                        final.extensions.sharedLibrary;
         executable =
           /**/ if final.isWindows then ".exe"
           else                         "";
@@ -132,6 +133,25 @@ rec {
          # uname -r
          release = null;
       };
+
+      # It is important that hasSharedLibraries==false when the platform has no
+      # dynamic library loader.  Various tools (including the gcc build system)
+      # have knowledge of which platforms are incapable of dynamic linking, and
+      # will still build on/for those platforms with --enable-shared, but simply
+      # omit any `.so` build products such as libgcc_s.so.  When that happens,
+      # it causes hard-to-troubleshoot build failures.
+      hasSharedLibraries = with final;
+        (isAndroid || isGnu || isMusl                                  # Linux (allows multiple libcs)
+         || isDarwin || isSunOS || isOpenBSD || isFreeBSD || isNetBSD  # BSDs
+         || isCygwin || isMinGW                                        # Windows
+        ) && !isStatic;
+
+      # The difference between `isStatic` and `hasSharedLibraries` is mainly the
+      # addition of the `staticMarker` (see make-derivation.nix).  Some
+      # platforms, like embedded machines without a libc (e.g. arm-none-eabi)
+      # don't support dynamic linking, but don't get the `staticMarker`.
+      # `pkgsStatic` sets `isStatic=true`, so `pkgsStatic.hostPlatform` always
+      # has the `staticMarker`.
       isStatic = final.isWasm || final.isRedox;
 
       # Just a guess, based on `system`
diff --git a/lib/systems/examples.nix b/lib/systems/examples.nix
index 9ad7c3e422d8e..8d9c09561ddb0 100644
--- a/lib/systems/examples.nix
+++ b/lib/systems/examples.nix
@@ -37,6 +37,10 @@ rec {
     config = "armv6l-unknown-linux-gnueabihf";
   } // platforms.raspberrypi;
 
+  bluefield2 = {
+    config = "aarch64-unknown-linux-gnu";
+  } // platforms.bluefield2;
+
   remarkable1 = {
     config = "armv7l-unknown-linux-gnueabihf";
   } // platforms.zero-gravitas;
diff --git a/lib/systems/platforms.nix b/lib/systems/platforms.nix
index d574943e47df3..d2e8f77bec03e 100644
--- a/lib/systems/platforms.nix
+++ b/lib/systems/platforms.nix
@@ -209,6 +209,13 @@ rec {
   # Legacy attribute, for compatibility with existing configs only.
   raspberrypi2 = armv7l-hf-multiplatform;
 
+  # Nvidia Bluefield 2 (w. crypto support)
+  bluefield2 = {
+    gcc = {
+      arch = "armv8-a+fp+simd+crc+crypto";
+    };
+  };
+
   zero-gravitas = {
     linux-kernel = {
       name = "zero-gravitas";
diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix
index 4f7035a53f5b4..dcfa4c540f0cb 100644
--- a/lib/tests/misc.nix
+++ b/lib/tests/misc.nix
@@ -1,6 +1,18 @@
-# to run these tests:
-# nix-instantiate --eval --strict nixpkgs/lib/tests/misc.nix
-# if the resulting list is empty, all tests passed
+/*
+Nix evaluation tests for various lib functions.
+
+Since these tests are implemented with Nix evaluation, error checking is limited to what `builtins.tryEval` can detect, which is `throw`'s and `abort`'s, without error messages.
+If you need to test error messages or more complex evaluations, see ./modules.sh, ./sources.sh or ./filesystem.sh as examples.
+
+To run these tests:
+
+  [nixpkgs]$ nix-instantiate --eval --strict lib/tests/misc.nix
+
+If the resulting list is empty, all tests passed.
+Alternatively, to run all `lib` tests:
+
+  [nixpkgs]$ nix-build lib/tests/release.nix
+*/
 with import ../default.nix;
 
 let
@@ -526,6 +538,39 @@ runTests {
     expected = { a = [ 2 3 ]; b = [7]; c = [8];};
   };
 
+  testListCommonPrefixExample1 = {
+    expr = lists.commonPrefix [ 1 2 3 4 5 6 ] [ 1 2 4 8 ];
+    expected = [ 1 2 ];
+  };
+  testListCommonPrefixExample2 = {
+    expr = lists.commonPrefix [ 1 2 3 ] [ 1 2 3 4 5 ];
+    expected = [ 1 2 3 ];
+  };
+  testListCommonPrefixExample3 = {
+    expr = lists.commonPrefix [ 1 2 3 ] [ 4 5 6 ];
+    expected = [ ];
+  };
+  testListCommonPrefixEmpty = {
+    expr = lists.commonPrefix [ ] [ 1 2 3 ];
+    expected = [ ];
+  };
+  testListCommonPrefixSame = {
+    expr = lists.commonPrefix [ 1 2 3 ] [ 1 2 3 ];
+    expected = [ 1 2 3 ];
+  };
+  testListCommonPrefixLazy = {
+    expr = lists.commonPrefix [ 1 ] [ 1 (abort "lib.lists.commonPrefix shouldn't evaluate this")];
+    expected = [ 1 ];
+  };
+  # This would stack overflow if `commonPrefix` were implemented using recursion
+  testListCommonPrefixLong =
+    let
+      longList = genList (n: n) 100000;
+    in {
+      expr = lists.commonPrefix longList longList;
+      expected = longList;
+    };
+
   testSort = {
     expr = sort builtins.lessThan [ 40 2 30 42 ];
     expected = [2 30 40 42];
@@ -556,45 +601,55 @@ runTests {
     expected = false;
   };
 
-  testFindFirstExample1 = {
-    expr = findFirst (x: x > 3) 7 [ 1 6 4 ];
-    expected = 6;
+  testFindFirstIndexExample1 = {
+    expr = lists.findFirstIndex (x: x > 3) (abort "index found, so a default must not be evaluated") [ 1 6 4 ];
+    expected = 1;
   };
 
-  testFindFirstExample2 = {
-    expr = findFirst (x: x > 9) 7 [ 1 6 4 ];
-    expected = 7;
+  testFindFirstIndexExample2 = {
+    expr = lists.findFirstIndex (x: x > 9) "a very specific default" [ 1 6 4 ];
+    expected = "a very specific default";
   };
 
-  testFindFirstEmpty = {
-    expr = findFirst (abort "when the list is empty, the predicate is not needed") null [];
+  testFindFirstIndexEmpty = {
+    expr = lists.findFirstIndex (abort "when the list is empty, the predicate is not needed") null [];
     expected = null;
   };
 
-  testFindFirstSingleMatch = {
-    expr = findFirst (x: x == 5) null [ 5 ];
-    expected = 5;
+  testFindFirstIndexSingleMatch = {
+    expr = lists.findFirstIndex (x: x == 5) null [ 5 ];
+    expected = 0;
   };
 
-  testFindFirstSingleDefault = {
-    expr = findFirst (x: false) null [ (abort "if the predicate doesn't access the value, it must not be evaluated") ];
+  testFindFirstIndexSingleDefault = {
+    expr = lists.findFirstIndex (x: false) null [ (abort "if the predicate doesn't access the value, it must not be evaluated") ];
     expected = null;
   };
 
-  testFindFirstNone = {
-    expr = builtins.tryEval (findFirst (x: x == 2) null [ 1 (throw "the last element must be evaluated when there's no match") ]);
+  testFindFirstIndexNone = {
+    expr = builtins.tryEval (lists.findFirstIndex (x: x == 2) null [ 1 (throw "the last element must be evaluated when there's no match") ]);
     expected = { success = false; value = false; };
   };
 
   # Makes sure that the implementation doesn't cause a stack overflow
-  testFindFirstBig = {
-    expr = findFirst (x: x == 1000000) null (range 0 1000000);
+  testFindFirstIndexBig = {
+    expr = lists.findFirstIndex (x: x == 1000000) null (range 0 1000000);
     expected = 1000000;
   };
 
-  testFindFirstLazy = {
-    expr = findFirst (x: x == 1) 7 [ 1 (abort "list elements after the match must not be evaluated") ];
-    expected = 1;
+  testFindFirstIndexLazy = {
+    expr = lists.findFirstIndex (x: x == 1) null [ 1 (abort "list elements after the match must not be evaluated") ];
+    expected = 0;
+  };
+
+  testFindFirstExample1 = {
+    expr = lists.findFirst (x: x > 3) 7 [ 1 6 4 ];
+    expected = 6;
+  };
+
+  testFindFirstExample2 = {
+    expr = lists.findFirst (x: x > 9) 7 [ 1 6 4 ];
+    expected = 7;
   };
 
 # ATTRSETS
@@ -647,6 +702,31 @@ runTests {
     };
   };
 
+
+  testMergeAttrsListExample1 = {
+    expr = attrsets.mergeAttrsList [ { a = 0; b = 1; } { c = 2; d = 3; } ];
+    expected = { a = 0; b = 1; c = 2; d = 3; };
+  };
+  testMergeAttrsListExample2 = {
+    expr = attrsets.mergeAttrsList [ { a = 0; } { a = 1; } ];
+    expected = { a = 1; };
+  };
+  testMergeAttrsListExampleMany =
+    let
+      list = genList (n:
+        listToAttrs (genList (m:
+          let
+            # Integer divide n by two to create duplicate attributes
+            str = "halfn${toString (n / 2)}m${toString m}";
+          in
+          nameValuePair str str
+        ) 100)
+      ) 100;
+    in {
+      expr = attrsets.mergeAttrsList list;
+      expected = foldl' mergeAttrs { } list;
+    };
+
   # code from the example
   testRecursiveUpdateUntil = {
     expr = recursiveUpdateUntil (path: l: r: path == ["foo"]) {
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index 50f24c09ca400..5f2e3f2a31144 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -1,7 +1,13 @@
 #!/usr/bin/env bash
-#
+
 # This script is used to test that the module system is working as expected.
+# Executing it runs tests for `lib.modules`, `lib.options` and `lib.types`.
 # By default it test the version of nixpkgs which is defined in the NIX_PATH.
+#
+# Run:
+# [nixpkgs]$ lib/tests/modules.sh
+# or:
+# [nixpkgs]$ nix-build lib/tests/release.nix
 
 set -o errexit -o noclobber -o nounset -o pipefail
 shopt -s failglob inherit_errexit
@@ -63,6 +69,28 @@ checkConfigOutput '^"one two"$' config.result ./shorthand-meta.nix
 
 checkConfigOutput '^true$' config.result ./test-mergeAttrDefinitionsWithPrio.nix
 
+# Check that a module argument is passed, also when a default is available
+# (but not needed)
+#
+# When the default is needed, we currently fail to do what the users expect, as
+# we pass our own argument anyway, even if it *turns out* not to exist.
+#
+# The reason for this is that we don't know at invocation time what is in the
+# _module.args option. That value is only available *after* all modules have been
+# invoked.
+#
+# Hypothetically, Nix could help support this by giving access to the default
+# values, through a new built-in function.
+# However the default values are allowed to depend on other arguments, so those
+# would have to be passed in somehow, making this not just a getter but
+# something more complicated.
+#
+# At that point we have to wonder whether the extra complexity is worth the cost.
+# Another - subjective - reason not to support it is that default values
+# contradict the notion that an option has a single value, where _module.args
+# is the option.
+checkConfigOutput '^true$' config.result ./module-argument-default.nix
+
 # types.pathInStore
 checkConfigOutput '".*/store/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv"' config.pathInStore.ok1 ./types.nix
 checkConfigOutput '".*/store/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15"' config.pathInStore.ok2 ./types.nix
@@ -365,6 +393,14 @@ checkConfigError \
   config.set \
   ./declare-set.nix ./declare-enable-nested.nix
 
+# Options: accidental use of an option-type instead of option (or other tagged type; unlikely)
+checkConfigError 'Expected an option declaration at option path .result. but got an attribute set with type option-type' config.result ./options-type-error-typical.nix
+checkConfigError 'Expected an option declaration at option path .result.here. but got an attribute set with type option-type' config.result.here ./options-type-error-typical-nested.nix
+checkConfigError 'Expected an option declaration at option path .result. but got an attribute set with type configuration' config.result ./options-type-error-configuration.nix
+
+# Check that that merging of option collisions doesn't depend on type being set
+checkConfigError 'The option .group..*would be a parent of the following options, but its type .<no description>. does not support nested options.\n\s*- option.s. with prefix .group.enable..*' config.group.enable ./merge-typeless-option.nix
+
 # Test that types.optionType merges types correctly
 checkConfigOutput '^10$' config.theOption.int ./optionTypeMerging.nix
 checkConfigOutput '^"hello"$' config.theOption.str ./optionTypeMerging.nix
diff --git a/lib/tests/modules/merge-typeless-option.nix b/lib/tests/modules/merge-typeless-option.nix
new file mode 100644
index 0000000000000..627d90b15db20
--- /dev/null
+++ b/lib/tests/modules/merge-typeless-option.nix
@@ -0,0 +1,25 @@
+{ lib, ... }:
+
+let
+  typeless =
+    { lib, ... }:
+
+    {
+      options.group = lib.mkOption { };
+    };
+  childOfTypeless =
+    { lib, ... }:
+
+    {
+      options.group.enable = lib.mkEnableOption "nothing";
+    };
+in
+
+{
+  imports = [
+    typeless
+    childOfTypeless
+  ];
+
+  config.group.enable = false;
+}
diff --git a/lib/tests/modules/module-argument-default.nix b/lib/tests/modules/module-argument-default.nix
new file mode 100644
index 0000000000000..8dbb783e2df1f
--- /dev/null
+++ b/lib/tests/modules/module-argument-default.nix
@@ -0,0 +1,9 @@
+{ a ? false, lib, ... }: {
+  options = {
+    result = lib.mkOption {};
+  };
+  config = {
+    _module.args.a = true;
+    result = a;
+  };
+}
diff --git a/lib/tests/modules/options-type-error-configuration.nix b/lib/tests/modules/options-type-error-configuration.nix
new file mode 100644
index 0000000000000..bcd6db89487a3
--- /dev/null
+++ b/lib/tests/modules/options-type-error-configuration.nix
@@ -0,0 +1,6 @@
+{ lib, ... }: {
+  options = {
+    # unlikely mistake, but we can catch any attrset with _type
+    result = lib.evalModules { modules = []; };
+  };
+}
diff --git a/lib/tests/modules/options-type-error-typical-nested.nix b/lib/tests/modules/options-type-error-typical-nested.nix
new file mode 100644
index 0000000000000..2c07e19fb8ae0
--- /dev/null
+++ b/lib/tests/modules/options-type-error-typical-nested.nix
@@ -0,0 +1,5 @@
+{ lib, ... }: {
+  options = {
+    result.here = lib.types.str;
+  };
+}
diff --git a/lib/tests/modules/options-type-error-typical.nix b/lib/tests/modules/options-type-error-typical.nix
new file mode 100644
index 0000000000000..416f436e0ad70
--- /dev/null
+++ b/lib/tests/modules/options-type-error-typical.nix
@@ -0,0 +1,5 @@
+{ lib, ... }: {
+  options = {
+    result = lib.types.str;
+  };
+}
diff --git a/lib/tests/sources.sh b/lib/tests/sources.sh
index cda77aa96b288..079c7eea56574 100755
--- a/lib/tests/sources.sh
+++ b/lib/tests/sources.sh
@@ -1,4 +1,11 @@
 #!/usr/bin/env bash
+
+# Tests lib/sources.nix
+# Run:
+# [nixpkgs]$ lib/tests/sources.sh
+# or:
+# [nixpkgs]$ nix-build lib/tests/release.nix
+
 set -euo pipefail
 shopt -s inherit_errexit
 
diff --git a/lib/trivial.nix b/lib/trivial.nix
index 34c100959e6ff..c23fc6070be46 100644
--- a/lib/trivial.nix
+++ b/lib/trivial.nix
@@ -307,14 +307,14 @@ rec {
 
   /* Reads a JSON file.
 
-     Type :: path -> any
+     Type: importJSON :: path -> any
   */
   importJSON = path:
     builtins.fromJSON (builtins.readFile path);
 
   /* Reads a TOML file.
 
-     Type :: path -> any
+     Type: importTOML :: path -> any
   */
   importTOML = path:
     builtins.fromTOML (builtins.readFile path);
diff --git a/lib/types.nix b/lib/types.nix
index ddd37f260c9a6..5ffbecda5db39 100644
--- a/lib/types.nix
+++ b/lib/types.nix
@@ -436,10 +436,12 @@ rec {
 
     # Deprecated; should not be used because it quietly concatenates
     # strings, which is usually not what you want.
-    string = separatedString "" // {
-      name = "string";
-      deprecationMessage = "See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types.";
-    };
+    # We use a lib.warn because `deprecationMessage` doesn't trigger in nested types such as `attrsOf string`
+    string = lib.warn
+      "The type `types.string` is deprecated. See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types."
+      (separatedString "" // {
+        name = "string";
+      });
 
     passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // {
       name = "passwdEntry ${entryType.name}";