about summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
authorJörg Thalheim <Mic92@users.noreply.github.com>2024-03-06 20:26:06 +0100
committerGitHub <noreply@github.com>2024-03-06 20:26:06 +0100
commit39ac57b7c57412f660beb0df6765bd84b7b2bbb5 (patch)
tree2d24f5c79c31183c60395cf7c736d5a1ce03eeb9 /lib
parent88c08be88ffd6266451c3a547122aa55f31a2576 (diff)
parent557b9754bcad48f9365fef304abce7ecfae23442 (diff)
downloadnixlib-39ac57b7c57412f660beb0df6765bd84b7b2bbb5.tar
nixlib-39ac57b7c57412f660beb0df6765bd84b7b2bbb5.tar.gz
nixlib-39ac57b7c57412f660beb0df6765bd84b7b2bbb5.tar.bz2
nixlib-39ac57b7c57412f660beb0df6765bd84b7b2bbb5.tar.lz
nixlib-39ac57b7c57412f660beb0df6765bd84b7b2bbb5.tar.xz
nixlib-39ac57b7c57412f660beb0df6765bd84b7b2bbb5.tar.zst
nixlib-39ac57b7c57412f660beb0df6765bd84b7b2bbb5.zip
Merge branch 'master' into license-updates
Diffstat (limited to 'lib')
-rw-r--r--lib/.version1
-rw-r--r--lib/attrsets.nix125
-rw-r--r--lib/customisation.nix9
-rw-r--r--lib/default.nix6
-rw-r--r--lib/derivations.nix90
-rw-r--r--lib/fileset/default.nix37
-rw-r--r--lib/fileset/internal.nix42
-rwxr-xr-xlib/fileset/tests.sh137
-rw-r--r--lib/fixed-points.nix158
-rw-r--r--lib/licenses.nix19
-rw-r--r--lib/lists.nix37
-rw-r--r--lib/meta.nix8
-rw-r--r--lib/modules.nix81
-rw-r--r--lib/options.nix30
-rw-r--r--lib/strings.nix34
-rw-r--r--lib/systems/inspect.nix7
-rw-r--r--lib/tests/misc.nix68
-rwxr-xr-xlib/tests/modules.sh14
-rw-r--r--lib/tests/modules/doRename-condition-enable.nix10
-rw-r--r--lib/tests/modules/doRename-condition-migrated.nix10
-rw-r--r--lib/tests/modules/doRename-condition-no-enable.nix9
-rw-r--r--lib/tests/modules/doRename-condition.nix42
-rw-r--r--lib/tests/modules/error-nonEmptyListOf-submodule.nix7
-rw-r--r--lib/tests/modules/types-unique.nix27
-rw-r--r--lib/tests/packages-from-directory/c/not-a-namespace/not-a-package.nix1
-rw-r--r--lib/tests/packages-from-directory/c/support-definitions.nix1
-rw-r--r--lib/tests/release.nix55
-rw-r--r--lib/tests/test-with-nix.nix76
-rw-r--r--lib/trivial.nix25
-rw-r--r--lib/types.nix38
-rw-r--r--lib/versions.nix2
-rw-r--r--lib/zip-int-bits.nix39
32 files changed, 912 insertions, 333 deletions
diff --git a/lib/.version b/lib/.version
new file mode 100644
index 000000000000..420f61e8c7f6
--- /dev/null
+++ b/lib/.version
@@ -0,0 +1 @@
+24.05
\ No newline at end of file
diff --git a/lib/attrsets.nix b/lib/attrsets.nix
index 99b686918453..34054460ba76 100644
--- a/lib/attrsets.nix
+++ b/lib/attrsets.nix
@@ -2,10 +2,10 @@
 { lib }:
 
 let
-  inherit (builtins) head tail length;
-  inherit (lib.trivial) id mergeAttrs;
+  inherit (builtins) head length;
+  inherit (lib.trivial) mergeAttrs warn;
   inherit (lib.strings) concatStringsSep concatMapStringsSep escapeNixIdentifier sanitizeDerivationName;
-  inherit (lib.lists) foldr foldl' concatMap concatLists elemAt all partition groupBy take foldl;
+  inherit (lib.lists) foldr foldl' concatMap elemAt all partition groupBy take foldl;
 in
 
 rec {
@@ -216,8 +216,7 @@ rec {
     attrPath:
     # The nested attribute set to find the value in.
     set:
-    let errorMsg = "cannot find attribute `" + concatStringsSep "." attrPath + "'";
-    in attrByPath attrPath (abort errorMsg) set;
+    attrByPath attrPath (abort ("cannot find attribute `" + concatStringsSep "." attrPath + "'")) set;
 
   /* Map each attribute in the given set and merge them into a new attribute set.
 
@@ -369,7 +368,7 @@ rec {
      Type:
        attrValues :: AttrSet -> [Any]
   */
-  attrValues = builtins.attrValues or (attrs: attrVals (attrNames attrs) attrs);
+  attrValues = builtins.attrValues;
 
 
   /* Given a set of attribute names, return the set of the corresponding
@@ -398,8 +397,7 @@ rec {
      Type:
        catAttrs :: String -> [AttrSet] -> [Any]
   */
-  catAttrs = builtins.catAttrs or
-    (attr: l: concatLists (map (s: if s ? ${attr} then [s.${attr}] else []) l));
+  catAttrs = builtins.catAttrs;
 
 
   /* Filter an attribute set by removing all attributes for which the
@@ -608,9 +606,7 @@ rec {
      Type:
        mapAttrs :: (String -> Any -> Any) -> AttrSet -> AttrSet
   */
-  mapAttrs = builtins.mapAttrs or
-    (f: set:
-      listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)));
+  mapAttrs = builtins.mapAttrs;
 
 
   /* Like `mapAttrs`, but allows the name of each attribute to be
@@ -683,65 +679,79 @@ rec {
   attrsToList = mapAttrsToList nameValuePair;
 
 
-  /* Like `mapAttrs`, except that it recursively applies itself to
-     the *leaf* attributes of a potentially-nested attribute set:
-     the second argument of the function will never be an attrset.
-     Also, the first argument of the argument function is a *list*
-     of the attribute names that form the path to the leaf attribute.
+  /**
+    Like `mapAttrs`, except that it recursively applies itself to the *leaf* attributes of a potentially-nested attribute set:
+    the second argument of the function will never be an attrset.
+    Also, the first argument of the mapping function is a *list* of the attribute names that form the path to the leaf attribute.
 
-     For a function that gives you control over what counts as a leaf,
-     see `mapAttrsRecursiveCond`.
+    For a function that gives you control over what counts as a leaf, see `mapAttrsRecursiveCond`.
 
-     Example:
-       mapAttrsRecursive (path: value: concatStringsSep "-" (path ++ [value]))
-         { n = { a = "A"; m = { b = "B"; c = "C"; }; }; d = "D"; }
-       => { n = { a = "n-a-A"; m = { b = "n-m-b-B"; c = "n-m-c-C"; }; }; d = "d-D"; }
+    :::{#map-attrs-recursive-example .example}
+    # Map over leaf attributes
 
-     Type:
-       mapAttrsRecursive :: ([String] -> a -> b) -> AttrSet -> AttrSet
+    ```nix
+    mapAttrsRecursive (path: value: concatStringsSep "-" (path ++ [value]))
+      { n = { a = "A"; m = { b = "B"; c = "C"; }; }; d = "D"; }
+    ```
+    evaluates to
+    ```nix
+    { n = { a = "n-a-A"; m = { b = "n-m-b-B"; c = "n-m-c-C"; }; }; d = "d-D"; }
+    ```
+    :::
+
+    # Type
+    ```
+    mapAttrsRecursive :: ([String] -> a -> b) -> AttrSet -> AttrSet
+    ```
   */
   mapAttrsRecursive =
-    # A function, given a list of attribute names and a value, returns a new value.
+    # A function that, given an attribute path as a list of strings and the corresponding attribute value, returns a new value.
     f:
-    # Set to recursively map over.
+    # Attribute set to recursively map over.
     set:
     mapAttrsRecursiveCond (as: true) f set;
 
 
-  /* Like `mapAttrsRecursive`, but it takes an additional predicate
-     function that tells it whether to recurse into an attribute
-     set.  If it returns false, `mapAttrsRecursiveCond` does not
-     recurse, but does apply the map function.  If it returns true, it
-     does recurse, and does not apply the map function.
+  /**
+    Like `mapAttrsRecursive`, but it takes an additional predicate that tells it whether to recurse into an attribute set.
+    If the predicate returns false, `mapAttrsRecursiveCond` does not recurse, but instead applies the mapping function.
+    If the predicate returns true, it does recurse, and does not apply the mapping function.
 
-     Example:
-       # To prevent recursing into derivations (which are attribute
-       # sets with the attribute "type" equal to "derivation"):
-       mapAttrsRecursiveCond
-         (as: !(as ? "type" && as.type == "derivation"))
-         (x: ... do something ...)
-         attrs
+    :::{#map-attrs-recursive-cond-example .example}
+    # Map over an leaf attributes defined by a condition
 
-     Type:
-       mapAttrsRecursiveCond :: (AttrSet -> Bool) -> ([String] -> a -> b) -> AttrSet -> AttrSet
+    Map derivations to their `name` attribute.
+    Derivatons are identified as attribute sets that contain `{ type = "derivation"; }`.
+    ```nix
+    mapAttrsRecursiveCond
+      (as: !(as ? "type" && as.type == "derivation"))
+      (x: x.name)
+      attrs
+    ```
+    :::
+
+    # Type
+    ```
+    mapAttrsRecursiveCond :: (AttrSet -> Bool) -> ([String] -> a -> b) -> AttrSet -> AttrSet
+    ```
   */
   mapAttrsRecursiveCond =
-    # A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
+    # A function that, given the attribute set the recursion is currently at, determines if to recurse deeper into that attribute set.
     cond:
-    # A function, given a list of attribute names and a value, returns a new value.
+    # A function that, given an attribute path as a list of strings and the corresponding attribute value, returns a new value.
+    # The attribute value is either an attribute set for which `cond` returns false, or something other than an attribute set.
     f:
     # Attribute set to recursively map over.
     set:
     let
       recurse = path:
-        let
-          g =
-            name: value:
+        mapAttrs
+          (name: value:
             if isAttrs value && cond value
-              then recurse (path ++ [name]) value
-              else f (path ++ [name]) value;
-        in mapAttrs g;
-    in recurse [] set;
+            then recurse (path ++ [ name ]) value
+            else f (path ++ [ name ]) value);
+    in
+    recurse [ ] set;
 
 
   /* Generate an attribute set by mapping a function over a list of
@@ -873,10 +883,7 @@ rec {
      Type:
        zipAttrs :: [ AttrSet ] -> AttrSet
   */
-  zipAttrs =
-    # List of attribute sets to zip together.
-    sets:
-    zipAttrsWith (name: values: values) sets;
+  zipAttrs = zipAttrsWith (name: values: values);
 
   /*
     Merge a list of attribute sets together using the `//` operator.
@@ -1141,10 +1148,7 @@ rec {
    Type: chooseDevOutputs :: [Derivation] -> [String]
 
   */
-  chooseDevOutputs =
-    # List of packages to pick `dev` outputs from
-    drvs:
-    builtins.map getDev drvs;
+  chooseDevOutputs = builtins.map getDev;
 
   /* Make various Nix tools consider the contents of the resulting
      attribute set when looking for what to build, find, etc.
@@ -1197,9 +1201,10 @@ rec {
       (x // y) // mask;
 
   # DEPRECATED
-  zipWithNames = zipAttrsWithNames;
+  zipWithNames = warn
+    "lib.zipWithNames is a deprecated alias of lib.zipAttrsWithNames." zipAttrsWithNames;
 
   # DEPRECATED
-  zip = builtins.trace
-    "lib.zip is deprecated, use lib.zipAttrsWith instead" zipAttrsWith;
+  zip = warn
+    "lib.zip is a deprecated alias of lib.zipAttrsWith." zipAttrsWith;
 }
diff --git a/lib/customisation.nix b/lib/customisation.nix
index c233744e07ca..7be412bac353 100644
--- a/lib/customisation.nix
+++ b/lib/customisation.nix
@@ -203,7 +203,11 @@ rec {
 
     in if missingArgs == {}
        then makeOverridable f allArgs
-       else throw "lib.customisation.callPackageWith: ${error}";
+       # This needs to be an abort so it can't be caught with `builtins.tryEval`,
+       # which is used by nix-env and ofborg to filter out packages that don't evaluate.
+       # This way we're forced to fix such errors in Nixpkgs,
+       # which is especially relevant with allowAliases = false
+       else abort "lib.customisation.callPackageWith: ${error}";
 
 
   /* Like callPackage, but for a function that returns an attribute
@@ -217,9 +221,10 @@ rec {
     let
       f = if isFunction fn then fn else import fn;
       auto = intersectAttrs (functionArgs f) autoArgs;
+      mirrorArgs = mirrorFunctionArgs f;
       origArgs = auto // args;
       pkgs = f origArgs;
-      mkAttrOverridable = name: _: makeOverridable (newArgs: (f newArgs).${name}) origArgs;
+      mkAttrOverridable = name: _: makeOverridable (mirrorArgs (newArgs: (f newArgs).${name})) origArgs;
     in
       if isDerivation pkgs then throw
         ("function `callPackages` was called on a *single* derivation "
diff --git a/lib/default.nix b/lib/default.nix
index f6c94ae91634..668c29640f9f 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -84,8 +84,8 @@ let
       mapAttrs' mapAttrsToList attrsToList concatMapAttrs mapAttrsRecursive
       mapAttrsRecursiveCond genAttrs isDerivation toDerivation optionalAttrs
       zipAttrsWithNames zipAttrsWith zipAttrs recursiveUpdateUntil
-      recursiveUpdate matchAttrs overrideExisting showAttrPath getOutput getBin
-      getLib getDev getMan chooseDevOutputs zipWithNames zip
+      recursiveUpdate matchAttrs mergeAttrsList overrideExisting showAttrPath getOutput
+      getBin getLib getDev getMan chooseDevOutputs zipWithNames zip
       recurseIntoAttrs dontRecurseIntoAttrs cartesianProductOfSets
       updateManyAttrsByPath;
     inherit (self.lists) singleton forEach foldr fold foldl foldl' imap0 imap1
@@ -116,7 +116,7 @@ let
     inherit (self.customisation) overrideDerivation makeOverridable
       callPackageWith callPackagesWith extendDerivation hydraJob
       makeScope makeScopeWithSplicing makeScopeWithSplicing';
-    inherit (self.derivations) lazyDerivation;
+    inherit (self.derivations) lazyDerivation optionalDrvAttr;
     inherit (self.meta) addMetaAttrs dontDistribute setName updateName
       appendToName mapDerivationAttrset setPrio lowPrio lowPrioSet hiPrio
       hiPrioSet getLicenseFromSpdxId getExe getExe';
diff --git a/lib/derivations.nix b/lib/derivations.nix
index 5b7ed1868e86..6867458f9e87 100644
--- a/lib/derivations.nix
+++ b/lib/derivations.nix
@@ -1,7 +1,20 @@
 { lib }:
 
 let
-  inherit (lib) throwIfNot;
+  inherit (lib)
+    genAttrs
+    isString
+    throwIfNot
+    ;
+
+  showMaybeAttrPosPre = prefix: attrName: v:
+    let pos = builtins.unsafeGetAttrPos attrName v;
+    in if pos == null then "" else "${prefix}${pos.file}:${toString pos.line}:${toString pos.column}";
+
+  showMaybePackagePosPre = prefix: pkg:
+    if pkg?meta.position && isString pkg.meta.position
+    then "${prefix}${pkg.meta.position}"
+    else "";
 in
 {
   /*
@@ -64,6 +77,11 @@ in
       #
       # This can be used for adding package attributes, such as `tests`.
       passthru ? { }
+    , # Optional list of assumed outputs. Default: ["out"]
+      #
+      # This must match the set of outputs that the returned derivation has.
+      # You must use this when the derivation has multiple outputs.
+      outputs ? [ "out" ]
     }:
     let
       # These checks are strict in `drv` and some `drv` attributes, but the
@@ -71,11 +89,40 @@ in
       # Instead, the individual derivation attributes do depend on it.
       checked =
         throwIfNot (derivation.type or null == "derivation")
-          "lazySimpleDerivation: input must be a derivation."
+          "lazyDerivation: input must be a derivation."
           throwIfNot
-          (derivation.outputs == [ "out" ])
-          # Supporting multiple outputs should be a matter of inheriting more attrs.
-          "The derivation ${derivation.name or "<unknown>"} has multiple outputs. This is not supported by lazySimpleDerivation yet. Support could be added, and be useful as long as the set of outputs is known in advance, without evaluating the actual derivation."
+          # NOTE: Technically we could require our outputs to be a subset of the
+          # actual ones, or even leave them unchecked and fail on a lazy basis.
+          # However, consider the case where an output is added in the underlying
+          # derivation, such as dev. lazyDerivation would remove it and cause it
+          # to fail as a buildInputs item, without any indication as to what
+          # happened. Hence the more stringent condition. We could consider
+          # adding a flag to control this behavior if there's a valid case for it,
+          # but the documentation must have a note like this.
+          (derivation.outputs == outputs)
+          ''
+            lib.lazyDerivation: The derivation ${derivation.name or "<unknown>"} has outputs that don't match the assumed outputs.
+
+            Assumed outputs passed to lazyDerivation${showMaybeAttrPosPre ",\n    at " "outputs" args}:
+                ${lib.generators.toPretty { multiline = false; } outputs};
+
+            Actual outputs of the derivation${showMaybePackagePosPre ",\n    defined at " derivation}:
+                ${lib.generators.toPretty { multiline = false; } derivation.outputs}
+
+            If the outputs are known ahead of evaluating the derivation,
+            then update the lazyDerivation call to match the actual outputs, in the same order.
+            If lazyDerivation is passed a literal value, just change it to the actual outputs.
+            As a result it will work as before / as intended.
+
+            Otherwise, when the outputs are dynamic and can't be known ahead of time, it won't
+            be possible to add laziness, but lib.lazyDerivation may still be useful for trimming
+            the attributes.
+            If you want to keep trimming the attributes, make sure that the package is in a
+            variable (don't evaluate it twice!) and pass the variable and its outputs attribute
+            to lib.lazyDerivation. This largely defeats laziness, but keeps the trimming.
+            If none of the above works for you, replace the lib.lazyDerivation call by the
+            expression in the derivation argument.
+          ''
           derivation;
     in
     {
@@ -92,10 +139,39 @@ in
       # A fixed set of derivation values, so that `lazyDerivation` can return
       # its attrset before evaluating `derivation`.
       # This must only list attributes that are available on _all_ derivations.
-      inherit (checked) outputs out outPath outputName drvPath name system;
+      inherit (checked) outPath outputName drvPath name system;
+      inherit outputs;
 
       # The meta attribute can either be taken from the derivation, or if the
       # `lazyDerivation` caller knew a shortcut, be taken from there.
       meta = args.meta or checked.meta;
-    } // passthru;
+    }
+    // genAttrs outputs (outputName: checked.${outputName})
+    // passthru;
+
+  /* Conditionally set a derivation attribute.
+
+     Because `mkDerivation` sets `__ignoreNulls = true`, a derivation
+     attribute set to `null` will not impact the derivation output hash.
+     Thus, this function passes through its `value` argument if the `cond`
+     is `true`, but returns `null` if not.
+
+     Type: optionalDrvAttr :: Bool -> a -> a | Null
+
+     Example:
+       (stdenv.mkDerivation {
+         name = "foo";
+         x = optionalDrvAttr true 1;
+         y = optionalDrvAttr false 1;
+       }).drvPath == (stdenv.mkDerivation {
+         name = "foo";
+         x = 1;
+       }).drvPath
+       => true
+  */
+  optionalDrvAttr =
+    # Condition
+    cond:
+    # Attribute value
+    value: if cond then value else null;
 }
diff --git a/lib/fileset/default.nix b/lib/fileset/default.nix
index c007b60def0a..ce9afc796a3f 100644
--- a/lib/fileset/default.nix
+++ b/lib/fileset/default.nix
@@ -23,6 +23,10 @@
 
     Add files in file sets to the store to use as derivation sources.
 
+  - [`lib.fileset.toList`](#function-library-lib.fileset.toList):
+
+    The list of files contained in a file set.
+
   Combinators:
   - [`lib.fileset.union`](#function-library-lib.fileset.union)/[`lib.fileset.unions`](#function-library-lib.fileset.unions):
 
@@ -102,6 +106,7 @@ let
     _coerceMany
     _toSourceFilter
     _fromSourceFilter
+    _toList
     _unionMany
     _fileFilter
     _printFileset
@@ -412,6 +417,38 @@ in {
         filter = sourceFilter;
       };
 
+
+  /*
+    The list of file paths contained in the given file set.
+
+    :::{.note}
+    This function is strict in the entire file set.
+    This is in contrast with combinators [`lib.fileset.union`](#function-library-lib.fileset.union),
+    [`lib.fileset.intersection`](#function-library-lib.fileset.intersection) and [`lib.fileset.difference`](#function-library-lib.fileset.difference).
+
+    Thus it is recommended to call `toList` on file sets created using the combinators,
+    instead of doing list processing on the result of `toList`.
+    :::
+
+    The resulting list of files can be turned back into a file set using [`lib.fileset.unions`](#function-library-lib.fileset.unions).
+
+    Type:
+      toList :: FileSet -> [ Path ]
+
+    Example:
+      toList ./.
+      [ ./README.md ./Makefile ./src/main.c ./src/main.h ]
+
+      toList (difference ./. ./src)
+      [ ./README.md ./Makefile ]
+  */
+  toList =
+    # The file set whose file paths to return.
+    # This argument can also be a path,
+    # which gets [implicitly coerced to a file set](#sec-fileset-path-coercion).
+    fileset:
+    _toList (_coerce "lib.fileset.toList: Argument" fileset);
+
   /*
     The file set containing all files that are in either of two given file sets.
     This is the same as [`unions`](#function-library-lib.fileset.unions),
diff --git a/lib/fileset/internal.nix b/lib/fileset/internal.nix
index 4059d2e24426..0d97ef174568 100644
--- a/lib/fileset/internal.nix
+++ b/lib/fileset/internal.nix
@@ -5,6 +5,7 @@ let
     isAttrs
     isPath
     isString
+    nixVersion
     pathExists
     readDir
     split
@@ -17,6 +18,8 @@ let
     attrNames
     attrValues
     mapAttrs
+    mapAttrsToList
+    optionalAttrs
     zipAttrsWith
     ;
 
@@ -27,6 +30,7 @@ let
   inherit (lib.lists)
     all
     commonPrefix
+    concatLists
     elemAt
     filter
     findFirst
@@ -56,6 +60,7 @@ let
     substring
     stringLength
     hasSuffix
+    versionAtLeast
     ;
 
   inherit (lib.trivial)
@@ -536,6 +541,27 @@ rec {
           ${baseNameOf root} = rootPathType;
         };
 
+  # Turns a file set into the list of file paths it includes.
+  # Type: fileset -> [ Path ]
+  _toList = fileset:
+    let
+      recurse = path: tree:
+        if isAttrs tree then
+          concatLists (mapAttrsToList (name: value:
+            recurse (path + "/${name}") value
+          ) tree)
+        else if tree == "directory" then
+          recurse path (readDir path)
+        else if tree == null then
+          [ ]
+        else
+          [ path ];
+    in
+    if fileset._internalIsEmptyWithoutBase then
+      [ ]
+    else
+      recurse fileset._internalBase fileset._internalTree;
+
   # Transforms the filesetTree of a file set to a shorter base path, e.g.
   # _shortenTreeBase [ "foo" ] (_create /foo/bar null)
   # => { bar = null; }
@@ -840,6 +866,10 @@ rec {
   # https://github.com/NixOS/nix/commit/55cefd41d63368d4286568e2956afd535cb44018
   _fetchGitSubmodulesMinver = "2.4";
 
+  # Support for `builtins.fetchGit` with `shallow = true` was introduced in 2.4
+  # https://github.com/NixOS/nix/commit/d1165d8791f559352ff6aa7348e1293b2873db1c
+  _fetchGitShallowMinver = "2.4";
+
   # Mirrors the contents of a Nix store path relative to a local path as a file set.
   # Some notes:
   # - The store path is read at evaluation time.
@@ -894,7 +924,17 @@ rec {
           # However a simpler alternative still would be [a builtins.gitLsFiles](https://github.com/NixOS/nix/issues/2944).
           fetchResult = fetchGit ({
             url = path;
-          } // extraFetchGitAttrs);
+          }
+          # In older Nix versions, repositories were always assumed to be deep clones, which made `fetchGit` fail for shallow clones
+          # For newer versions this was fixed, but the `shallow` flag is required.
+          # The only behavioral difference is that for shallow clones, `fetchGit` doesn't return a `revCount`,
+          # which we don't need here, so it's fine to always pass it.
+
+          # Unfortunately this means older Nix versions get a poor error message for shallow repositories, and there's no good way to improve that.
+          # Checking for `.git/shallow` doesn't seem worth it, especially since that's more of an implementation detail,
+          # and would also require more code to handle worktrees where `.git` is a file.
+          // optionalAttrs (versionAtLeast nixVersion _fetchGitShallowMinver) { shallow = true; }
+          // extraFetchGitAttrs);
         in
         # We can identify local working directories by checking for .git,
         # see https://git-scm.com/docs/gitrepository-layout#_description.
diff --git a/lib/fileset/tests.sh b/lib/fileset/tests.sh
index e809aef6935a..405fa04d8e06 100755
--- a/lib/fileset/tests.sh
+++ b/lib/fileset/tests.sh
@@ -275,7 +275,6 @@ createTree() {
 # )
 # checkFileset './a' # Pass the fileset as the argument
 checkFileset() {
-    # New subshell so that we can have a separate trap handler, see `trap` below
     local fileset=$1
 
     # Create the tree
@@ -283,16 +282,20 @@ checkFileset() {
 
     # Process the tree into separate arrays for included paths, excluded paths and excluded files.
     local -a included=()
+    local -a includedFiles=()
     local -a excluded=()
     local -a excludedFiles=()
     for p in "${!tree[@]}"; do
         case "${tree[$p]}" in
             1)
                 included+=("$p")
+                # If keys end with a `/` we treat them as directories, otherwise files
+                if [[ ! "$p" =~ /$ ]]; then
+                    includedFiles+=("$p")
+                fi
                 ;;
             0)
                 excluded+=("$p")
-                # If keys end with a `/` we treat them as directories, otherwise files
                 if [[ ! "$p" =~ /$ ]]; then
                     excludedFiles+=("$p")
                 fi
@@ -302,6 +305,10 @@ checkFileset() {
         esac
     done
 
+    # Test that lib.fileset.toList contains exactly the included files.
+    # The /#/./ part prefixes each element with `./`
+    expectEqual "toList ($fileset)" "sort lessThan [ ${includedFiles[*]/#/./} ]"
+
     expression="toSource { root = ./.; fileset = $fileset; }"
 
     # We don't have lambda's in bash unfortunately,
@@ -338,13 +345,17 @@ checkFileset() {
 
 #### Error messages #####
 
+# We're using [[:blank:]] here instead of \s, because only the former is POSIX
+# (see https://pubs.opengroup.org/onlinepubs/007908799/xbd/re.html#tag_007_003_005).
+# And indeed, Darwin's bash only supports the former
+
 # Absolute paths in strings cannot be passed as `root`
 expectFailure 'toSource { root = "/nix/store/foobar"; fileset = ./.; }' 'lib.fileset.toSource: `root` \(/nix/store/foobar\) is a string-like value, but it should be a path instead.
-\s*Paths in strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.'
+[[:blank:]]*Paths in strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.'
 
 expectFailure 'toSource { root = cleanSourceWith { src = ./.; }; fileset = ./.; }' 'lib.fileset.toSource: `root` is a `lib.sources`-based value, but it should be a path instead.
-\s*To use a `lib.sources`-based value, convert it to a file set using `lib.fileset.fromSource` and pass it as `fileset`.
-\s*Note that this only works for sources created from paths.'
+[[:blank:]]*To use a `lib.sources`-based value, convert it to a file set using `lib.fileset.fromSource` and pass it as `fileset`.
+[[:blank:]]*Note that this only works for sources created from paths.'
 
 # Only paths are accepted as `root`
 expectFailure 'toSource { root = 10; fileset = ./.; }' 'lib.fileset.toSource: `root` is of type int, but it should be a path instead.'
@@ -354,9 +365,9 @@ mkdir -p {foo,bar}/mock-root
 expectFailure 'with ((import <nixpkgs/lib>).extend (import <nixpkgs/lib/fileset/mock-splitRoot.nix>)).fileset;
   toSource { root = ./foo/mock-root; fileset = ./bar/mock-root; }
 ' 'lib.fileset.toSource: Filesystem roots are not the same for `fileset` and `root` \('"$work"'/foo/mock-root\):
-\s*`root`: Filesystem root is "'"$work"'/foo/mock-root"
-\s*`fileset`: Filesystem root is "'"$work"'/bar/mock-root"
-\s*Different filesystem roots are not supported.'
+[[:blank:]]*`root`: Filesystem root is "'"$work"'/foo/mock-root"
+[[:blank:]]*`fileset`: Filesystem root is "'"$work"'/bar/mock-root"
+[[:blank:]]*Different filesystem roots are not supported.'
 rm -rf -- *
 
 # `root` needs to exist
@@ -365,8 +376,8 @@ expectFailure 'toSource { root = ./a; fileset = ./.; }' 'lib.fileset.toSource: `
 # `root` needs to be a file
 touch a
 expectFailure 'toSource { root = ./a; fileset = ./a; }' 'lib.fileset.toSource: `root` \('"$work"'/a\) is a file, but it should be a directory instead. Potential solutions:
-\s*- If you want to import the file into the store _without_ a containing directory, use string interpolation or `builtins.path` instead of this function.
-\s*- If you want to import the file into the store _with_ a containing directory, set `root` to the containing directory, such as '"$work"', and set `fileset` to the file path.'
+[[:blank:]]*- If you want to import the file into the store _without_ a containing directory, use string interpolation or `builtins.path` instead of this function.
+[[:blank:]]*- If you want to import the file into the store _with_ a containing directory, set `root` to the containing directory, such as '"$work"', and set `fileset` to the file path.'
 rm -rf -- *
 
 # The fileset argument should be evaluated, even if the directory is empty
@@ -375,36 +386,36 @@ expectFailure 'toSource { root = ./.; fileset = abort "This should be evaluated"
 # Only paths under `root` should be able to influence the result
 mkdir a
 expectFailure 'toSource { root = ./a; fileset = ./.; }' 'lib.fileset.toSource: `fileset` could contain files in '"$work"', which is not under the `root` \('"$work"'/a\). Potential solutions:
-\s*- Set `root` to '"$work"' or any directory higher up. This changes the layout of the resulting store path.
-\s*- Set `fileset` to a file set that cannot contain files outside the `root` \('"$work"'/a\). This could change the files included in the result.'
+[[:blank:]]*- Set `root` to '"$work"' or any directory higher up. This changes the layout of the resulting store path.
+[[:blank:]]*- Set `fileset` to a file set that cannot contain files outside the `root` \('"$work"'/a\). This could change the files included in the result.'
 rm -rf -- *
 
 # non-regular and non-symlink files cannot be added to the Nix store
 mkfifo a
 expectFailure 'toSource { root = ./.; fileset = ./a; }' 'lib.fileset.toSource: `fileset` contains a file that cannot be added to the store: '"$work"'/a
-\s*This file is neither a regular file nor a symlink, the only file types supported by the Nix store.
-\s*Therefore the file set cannot be added to the Nix store as is. Make sure to not include that file to avoid this error.'
+[[:blank:]]*This file is neither a regular file nor a symlink, the only file types supported by the Nix store.
+[[:blank:]]*Therefore the file set cannot be added to the Nix store as is. Make sure to not include that file to avoid this error.'
 rm -rf -- *
 
 # Path coercion only works for paths
 expectFailure 'toSource { root = ./.; fileset = 10; }' 'lib.fileset.toSource: `fileset` is of type int, but it should be a file set or a path instead.'
 expectFailure 'toSource { root = ./.; fileset = "/some/path"; }' 'lib.fileset.toSource: `fileset` \("/some/path"\) is a string-like value, but it should be a file set or a path instead.
-\s*Paths represented as strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.'
+[[:blank:]]*Paths represented as strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.'
 expectFailure 'toSource { root = ./.; fileset = cleanSourceWith { src = ./.; }; }' 'lib.fileset.toSource: `fileset` is a `lib.sources`-based value, but it should be a file set or a path instead.
-\s*To convert a `lib.sources`-based value to a file set you can use `lib.fileset.fromSource`.
-\s*Note that this only works for sources created from paths.'
+[[:blank:]]*To convert a `lib.sources`-based value to a file set you can use `lib.fileset.fromSource`.
+[[:blank:]]*Note that this only works for sources created from paths.'
 
 # Path coercion errors for non-existent paths
 expectFailure 'toSource { root = ./.; fileset = ./a; }' 'lib.fileset.toSource: `fileset` \('"$work"'/a\) is a path that does not exist.
-\s*To create a file set from a path that may not exist, use `lib.fileset.maybeMissing`.'
+[[:blank:]]*To create a file set from a path that may not exist, use `lib.fileset.maybeMissing`.'
 
 # File sets cannot be evaluated directly
 expectFailure 'union ./. ./.' 'lib.fileset: Directly evaluating a file set is not supported.
-\s*To turn it into a usable source, use `lib.fileset.toSource`.
-\s*To pretty-print the contents, use `lib.fileset.trace` or `lib.fileset.traceVal`.'
+[[:blank:]]*To turn it into a usable source, use `lib.fileset.toSource`.
+[[:blank:]]*To pretty-print the contents, use `lib.fileset.trace` or `lib.fileset.traceVal`.'
 expectFailure '_emptyWithoutBase' 'lib.fileset: Directly evaluating a file set is not supported.
-\s*To turn it into a usable source, use `lib.fileset.toSource`.
-\s*To pretty-print the contents, use `lib.fileset.trace` or `lib.fileset.traceVal`.'
+[[:blank:]]*To turn it into a usable source, use `lib.fileset.toSource`.
+[[:blank:]]*To pretty-print the contents, use `lib.fileset.trace` or `lib.fileset.traceVal`.'
 
 # Past versions of the internal representation are supported
 expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 0; _internalBase = ./.; }' \
@@ -416,9 +427,9 @@ expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 2;
 
 # Future versions of the internal representation are unsupported
 expectFailure '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 4; }' '<tests>: value is a file set created from a future version of the file set library with a different internal representation:
-\s*- Internal version of the file set: 4
-\s*- Internal version of the library: 3
-\s*Make sure to update your Nixpkgs to have a newer version of `lib.fileset`.'
+[[:blank:]]*- Internal version of the file set: 4
+[[:blank:]]*- Internal version of the library: 3
+[[:blank:]]*Make sure to update your Nixpkgs to have a newer version of `lib.fileset`.'
 
 # _create followed by _coerce should give the inputs back without any validation
 expectEqual '{
@@ -511,6 +522,19 @@ expectEqual '_toSourceFilter (_create /. { foo = "regular"; }) "/foo" ""' 'true'
 expectEqual '_toSourceFilter (_create /. { foo = null; }) "/foo" ""' 'false'
 
 
+## lib.fileset.toList
+# This function is mainly tested in checkFileset
+
+# The error context for an invalid argument must be correct
+expectFailure 'toList null' 'lib.fileset.toList: Argument is of type null, but it should be a file set or a path instead.'
+
+# Works for the empty fileset
+expectEqual 'toList _emptyWithoutBase' '[ ]'
+
+# Works on empty paths
+expectEqual 'toList ./.' '[ ]'
+
+
 ## lib.fileset.union, lib.fileset.unions
 
 
@@ -519,16 +543,16 @@ mkdir -p {foo,bar}/mock-root
 expectFailure 'with ((import <nixpkgs/lib>).extend (import <nixpkgs/lib/fileset/mock-splitRoot.nix>)).fileset;
   toSource { root = ./.; fileset = union ./foo/mock-root ./bar/mock-root; }
 ' 'lib.fileset.union: Filesystem roots are not the same:
-\s*First argument: Filesystem root is "'"$work"'/foo/mock-root"
-\s*Second argument: Filesystem root is "'"$work"'/bar/mock-root"
-\s*Different filesystem roots are not supported.'
+[[:blank:]]*First argument: Filesystem root is "'"$work"'/foo/mock-root"
+[[:blank:]]*Second argument: Filesystem root is "'"$work"'/bar/mock-root"
+[[:blank:]]*Different filesystem roots are not supported.'
 
 expectFailure 'with ((import <nixpkgs/lib>).extend (import <nixpkgs/lib/fileset/mock-splitRoot.nix>)).fileset;
   toSource { root = ./.; fileset = unions [ ./foo/mock-root ./bar/mock-root ]; }
 ' 'lib.fileset.unions: Filesystem roots are not the same:
-\s*Element 0: Filesystem root is "'"$work"'/foo/mock-root"
-\s*Element 1: Filesystem root is "'"$work"'/bar/mock-root"
-\s*Different filesystem roots are not supported.'
+[[:blank:]]*Element 0: Filesystem root is "'"$work"'/foo/mock-root"
+[[:blank:]]*Element 1: Filesystem root is "'"$work"'/bar/mock-root"
+[[:blank:]]*Different filesystem roots are not supported.'
 rm -rf -- *
 
 # Coercion errors show the correct context
@@ -632,9 +656,9 @@ mkdir -p {foo,bar}/mock-root
 expectFailure 'with ((import <nixpkgs/lib>).extend (import <nixpkgs/lib/fileset/mock-splitRoot.nix>)).fileset;
   toSource { root = ./.; fileset = intersection ./foo/mock-root ./bar/mock-root; }
 ' 'lib.fileset.intersection: Filesystem roots are not the same:
-\s*First argument: Filesystem root is "'"$work"'/foo/mock-root"
-\s*Second argument: Filesystem root is "'"$work"'/bar/mock-root"
-\s*Different filesystem roots are not supported.'
+[[:blank:]]*First argument: Filesystem root is "'"$work"'/foo/mock-root"
+[[:blank:]]*Second argument: Filesystem root is "'"$work"'/bar/mock-root"
+[[:blank:]]*Different filesystem roots are not supported.'
 rm -rf -- *
 
 # Coercion errors show the correct context
@@ -741,8 +765,8 @@ rm -rf -- *
 # Also not the other way around
 mkdir a
 expectFailure 'toSource { root = ./a; fileset = difference ./. ./a; }' 'lib.fileset.toSource: `fileset` could contain files in '"$work"', which is not under the `root` \('"$work"'/a\). Potential solutions:
-\s*- Set `root` to '"$work"' or any directory higher up. This changes the layout of the resulting store path.
-\s*- Set `fileset` to a file set that cannot contain files outside the `root` \('"$work"'/a\). This could change the files included in the result.'
+[[:blank:]]*- Set `root` to '"$work"' or any directory higher up. This changes the layout of the resulting store path.
+[[:blank:]]*- Set `fileset` to a file set that cannot contain files outside the `root` \('"$work"'/a\). This could change the files included in the result.'
 rm -rf -- *
 
 # Difference actually works
@@ -819,7 +843,7 @@ expectFailure 'fileFilter null (abort "this is not needed")' 'lib.fileset.fileFi
 
 # The second argument needs to be an existing path
 expectFailure 'fileFilter (file: abort "this is not needed") _emptyWithoutBase' 'lib.fileset.fileFilter: Second argument is a file set, but it should be a path instead.
-\s*If you need to filter files in a file set, use `intersection fileset \(fileFilter pred \./\.\)` instead.'
+[[:blank:]]*If you need to filter files in a file set, use `intersection fileset \(fileFilter pred \./\.\)` instead.'
 expectFailure 'fileFilter (file: abort "this is not needed") null' 'lib.fileset.fileFilter: Second argument is of type null, but it should be a path instead.'
 expectFailure 'fileFilter (file: abort "this is not needed") ./a' 'lib.fileset.fileFilter: Second argument \('"$work"'/a\) is a path that does not exist.'
 
@@ -1083,7 +1107,7 @@ rm -rf -- *
 
 # String-like values are not supported
 expectFailure 'fromSource (lib.cleanSource "")' 'lib.fileset.fromSource: The source origin of the argument is a string-like value \(""\), but it should be a path instead.
-\s*Sources created from paths in strings cannot be turned into file sets, use `lib.sources` or derivations instead.'
+[[:blank:]]*Sources created from paths in strings cannot be turned into file sets, use `lib.sources` or derivations instead.'
 
 # Wrong type
 expectFailure 'fromSource null' 'lib.fileset.fromSource: The source origin of the argument is of type null, but it should be a path instead.'
@@ -1400,10 +1424,10 @@ expectEqual '(import '"$storePath"' { fs = lib.fileset; }).outPath' \""$storePat
 
 ## But it fails if the path is imported with a fetcher that doesn't remove .git (like just using "${./.}")
 expectFailure 'import "${./.}" { fs = lib.fileset; }' 'lib.fileset.gitTracked: The argument \(.*\) is a store path within a working tree of a Git repository.
-\s*This indicates that a source directory was imported into the store using a method such as `import "\$\{./.\}"` or `path:.`.
-\s*This function currently does not support such a use case, since it currently relies on `builtins.fetchGit`.
-\s*You could make this work by using a fetcher such as `fetchGit` instead of copying the whole repository.
-\s*If you can'\''t avoid copying the repo to the store, see https://github.com/NixOS/nix/issues/9292.'
+[[:blank:]]*This indicates that a source directory was imported into the store using a method such as `import "\$\{./.\}"` or `path:.`.
+[[:blank:]]*This function currently does not support such a use case, since it currently relies on `builtins.fetchGit`.
+[[:blank:]]*You could make this work by using a fetcher such as `fetchGit` instead of copying the whole repository.
+[[:blank:]]*If you can'\''t avoid copying the repo to the store, see https://github.com/NixOS/nix/issues/9292.'
 
 ## Even with submodules
 if [[ -n "$fetchGitSupportsSubmodules" ]]; then
@@ -1427,18 +1451,31 @@ if [[ -n "$fetchGitSupportsSubmodules" ]]; then
 
     ## But it fails if the path is imported with a fetcher that doesn't remove .git (like just using "${./.}")
     expectFailure 'import "${./.}" { fs = lib.fileset; }' 'lib.fileset.gitTrackedWith: The second argument \(.*\) is a store path within a working tree of a Git repository.
-    \s*This indicates that a source directory was imported into the store using a method such as `import "\$\{./.\}"` or `path:.`.
-    \s*This function currently does not support such a use case, since it currently relies on `builtins.fetchGit`.
-    \s*You could make this work by using a fetcher such as `fetchGit` instead of copying the whole repository.
-    \s*If you can'\''t avoid copying the repo to the store, see https://github.com/NixOS/nix/issues/9292.'
+    [[:blank:]]*This indicates that a source directory was imported into the store using a method such as `import "\$\{./.\}"` or `path:.`.
+    [[:blank:]]*This function currently does not support such a use case, since it currently relies on `builtins.fetchGit`.
+    [[:blank:]]*You could make this work by using a fetcher such as `fetchGit` instead of copying the whole repository.
+    [[:blank:]]*If you can'\''t avoid copying the repo to the store, see https://github.com/NixOS/nix/issues/9292.'
     expectFailure 'import "${./.}/sub" { fs = lib.fileset; }' 'lib.fileset.gitTracked: The argument \(.*/sub\) is a store path within a working tree of a Git repository.
-    \s*This indicates that a source directory was imported into the store using a method such as `import "\$\{./.\}"` or `path:.`.
-    \s*This function currently does not support such a use case, since it currently relies on `builtins.fetchGit`.
-    \s*You could make this work by using a fetcher such as `fetchGit` instead of copying the whole repository.
-    \s*If you can'\''t avoid copying the repo to the store, see https://github.com/NixOS/nix/issues/9292.'
+    [[:blank:]]*This indicates that a source directory was imported into the store using a method such as `import "\$\{./.\}"` or `path:.`.
+    [[:blank:]]*This function currently does not support such a use case, since it currently relies on `builtins.fetchGit`.
+    [[:blank:]]*You could make this work by using a fetcher such as `fetchGit` instead of copying the whole repository.
+    [[:blank:]]*If you can'\''t avoid copying the repo to the store, see https://github.com/NixOS/nix/issues/9292.'
 fi
 rm -rf -- *
 
+# shallow = true is not supported on all Nix versions
+# and older versions don't support shallow clones at all
+if [[ "$(nix-instantiate --eval --expr "$prefixExpression (versionAtLeast builtins.nixVersion _fetchGitShallowMinver)")" == true ]]; then
+    createGitRepo full
+    # Extra commit such that there's a commit that won't be in the shallow clone
+    git -C full commit --allow-empty -q -m extra
+    git clone -q --depth 1 "file://${PWD}/full" shallow
+    cd shallow
+    checkGitTracked
+    cd ..
+    rm -rf -- *
+fi
+
 # Go through all stages of Git files
 # See https://www.git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository
 
diff --git a/lib/fixed-points.nix b/lib/fixed-points.nix
index 3b5fdc9e8ea1..3bd18fdd2a5a 100644
--- a/lib/fixed-points.nix
+++ b/lib/fixed-points.nix
@@ -103,42 +103,154 @@ rec {
       else converge f x';
 
   /*
-    Modify the contents of an explicitly recursive attribute set in a way that
-    honors `self`-references. This is accomplished with a function
+    Extend a function using an overlay.
+
+    Overlays allow modifying and extending fixed-point functions, specifically ones returning attribute sets.
+    A fixed-point function is a function which is intended to be evaluated by passing the result of itself as the argument.
+    This is possible due to Nix's lazy evaluation.
+
+
+    A fixed-point function returning an attribute set has the form
+
+    ```nix
+    final: { # attributes }
+    ```
+
+    where `final` refers to the lazily evaluated attribute set returned by the fixed-point function.
+
+    An overlay to such a fixed-point function has the form
 
     ```nix
-    g = self: super: { foo = super.foo + " + "; }
+    final: prev: { # attributes }
+    ```
+
+    where `prev` refers to the result of the original function to `final`, and `final` is the result of the composition of the overlay and the original function.
+
+    Applying an overlay is done with `extends`:
+
+    ```nix
+    let
+      f = final: { # attributes };
+      overlay = final: prev: { # attributes };
+    in extends overlay f;
+    ```
+
+    To get the value of `final`, use `lib.fix`:
+
+    ```nix
+    let
+      f = final: { # attributes };
+      overlay = final: prev: { # attributes };
+      g = extends overlay f;
+    in fix g
+    ```
+
+    :::{.note}
+    The argument to the given fixed-point function after applying an overlay will *not* refer to its own return value, but rather to the value after evaluating the overlay function.
+
+    The given fixed-point function is called with a separate argument than if it was evaluated with `lib.fix`.
+    :::
+
+    :::{.example}
+
+    # Extend a fixed-point function with an overlay
+
+    Define a fixed-point function `f` that expects its own output as the argument `final`:
+
+    ```nix-repl
+    f = final: {
+      # Constant value a
+      a = 1;
+
+      # b depends on the final value of a, available as final.a
+      b = final.a + 2;
+    }
+    ```
+
+    Evaluate this using [`lib.fix`](#function-library-lib.fixedPoints.fix) to get the final result:
+
+    ```nix-repl
+    fix f
+    => { a = 1; b = 3; }
     ```
 
-    that has access to the unmodified input (`super`) as well as the final
-    non-recursive representation of the attribute set (`self`). `extends`
-    differs from the native `//` operator insofar as that it's applied *before*
-    references to `self` are resolved:
+    An overlay represents a modification or extension of such a fixed-point function.
+    Here's an example of an overlay:
+
+    ```nix-repl
+    overlay = final: prev: {
+      # Modify the previous value of a, available as prev.a
+      a = prev.a + 10;
 
+      # Extend the attribute set with c, letting it depend on the final values of a and b
+      c = final.a + final.b;
+    }
     ```
-    nix-repl> fix (extends g f)
-    { bar = "bar"; foo = "foo + "; foobar = "foo + bar"; }
+
+    Use `extends overlay f` to apply the overlay to the fixed-point function `f`.
+    This produces a new fixed-point function `g` with the combined behavior of `f` and `overlay`:
+
+    ```nix-repl
+    g = extends overlay f
     ```
 
-    The name of the function is inspired by object-oriented inheritance, i.e.
-    think of it as an infix operator `g extends f` that mimics the syntax from
-    Java. It may seem counter-intuitive to have the "base class" as the second
-    argument, but it's nice this way if several uses of `extends` are cascaded.
+    The result is a function, so we can't print it directly, but it's the same as:
 
-    To get a better understanding how `extends` turns a function with a fix
-    point (the package set we start with) into a new function with a different fix
-    point (the desired packages set) lets just see, how `extends g f`
-    unfolds with `g` and `f` defined above:
+    ```nix-repl
+    g' = final: {
+      # The constant from f, but changed with the overlay
+      a = 1 + 10;
 
+      # Unchanged from f
+      b = final.a + 2;
+
+      # Extended in the overlay
+      c = final.a + final.b;
+    }
     ```
-    extends g f = self: let super = f self; in super // g self super;
-                = self: let super = { foo = "foo"; bar = "bar"; foobar = self.foo + self.bar; }; in super // g self super
-                = self: { foo = "foo"; bar = "bar"; foobar = self.foo + self.bar; } // g self { foo = "foo"; bar = "bar"; foobar = self.foo + self.bar; }
-                = self: { foo = "foo"; bar = "bar"; foobar = self.foo + self.bar; } // { foo = "foo" + " + "; }
-                = self: { foo = "foo + "; bar = "bar"; foobar = self.foo + self.bar; }
+
+    Evaluate this using [`lib.fix`](#function-library-lib.fixedPoints.fix) again to get the final result:
+
+    ```nix-repl
+    fix g
+    => { a = 11; b = 13; c = 24; }
     ```
+    :::
+
+    Type:
+      extends :: (Attrs -> Attrs -> Attrs) # The overlay to apply to the fixed-point function
+              -> (Attrs -> Attrs) # A fixed-point function
+              -> (Attrs -> Attrs) # The resulting fixed-point function
+
+    Example:
+      f = final: { a = 1; b = final.a + 2; }
+
+      fix f
+      => { a = 1; b = 3; }
+
+      fix (extends (final: prev: { a = prev.a + 10; }) f)
+      => { a = 11; b = 13; }
+
+      fix (extends (final: prev: { b = final.a + 5; }) f)
+      => { a = 1; b = 6; }
+
+      fix (extends (final: prev: { c = final.a + final.b; }) f)
+      => { a = 1; b = 3; c = 4; }
   */
-  extends = f: rattrs: self: let super = rattrs self; in super // f self super;
+  extends =
+    # The overlay to apply to the fixed-point function
+    overlay:
+    # The fixed-point function
+    f:
+    # Wrap with parenthesis to prevent nixdoc from rendering the `final` argument in the documentation
+    # The result should be thought of as a function, the argument of that function is not an argument to `extends` itself
+    (
+      final:
+      let
+        prev = f final;
+      in
+      prev // overlay final prev
+    );
 
   /*
     Compose two extending functions of the type expected by 'extends'
diff --git a/lib/licenses.nix b/lib/licenses.nix
index 0dfd8ef5e69c..30ca31ff71f2 100644
--- a/lib/licenses.nix
+++ b/lib/licenses.nix
@@ -104,6 +104,7 @@ in mkLicense lset) ({
   };
 
   arphicpl = {
+    spdxId = "Arphic-1999";
     fullName = "Arphic Public License";
     url = "https://www.freedesktop.org/wiki/Arphic_Public_License/";
   };
@@ -236,6 +237,7 @@ in mkLicense lset) ({
   };
 
   cal10 = {
+    spdxId = "CAL-1.0";
     fullName = "Cryptographic Autonomy License version 1.0 (CAL-1.0)";
     url = "https://opensource.org/licenses/CAL-1.0";
   };
@@ -335,6 +337,11 @@ in mkLicense lset) ({
     fullName = "Creative Commons Attribution 1.0";
   };
 
+  cc-by-20 = {
+    spdxId = "CC-BY-2.0";
+    fullName = "Creative Commons Attribution 2.0";
+  };
+
   cc-by-30 = {
     spdxId = "CC-BY-3.0";
     fullName = "Creative Commons Attribution 3.0";
@@ -434,6 +441,7 @@ in mkLicense lset) ({
   };
 
   elastic20 = {
+    spdxId = "Elastic-2.0";
     fullName = "Elastic License 2.0";
     url = "https://github.com/elastic/elasticsearch/blob/main/licenses/ELASTIC-LICENSE-2.0.txt";
     free = false;
@@ -603,6 +611,7 @@ in mkLicense lset) ({
 
   # Intel's license, seems free
   iasl = {
+    spdxId = "Intel-ACPI";
     fullName = "iASL";
     url = "https://old.calculate-linux.org/packages/licenses/iASL";
   };
@@ -614,7 +623,7 @@ in mkLicense lset) ({
 
   imagemagick = {
     fullName = "ImageMagick License";
-    spdxId = "imagemagick";
+    spdxId = "ImageMagick";
   };
 
   imlib2 = {
@@ -808,6 +817,7 @@ in mkLicense lset) ({
   };
 
   miros = {
+    spdxId = "MirOS";
     fullName = "MirOS License";
     url = "https://opensource.org/licenses/MirOS";
   };
@@ -849,6 +859,11 @@ in mkLicense lset) ({
     fullName = "Mozilla Public License 2.0";
   };
 
+  mplus = {
+    spdxId = "mplus";
+    fullName = "M+ Font License";
+  };
+
   mspl = {
     spdxId = "MS-PL";
     fullName = "Microsoft Public License";
@@ -1148,6 +1163,7 @@ in mkLicense lset) ({
   };
 
   upl = {
+    spdxId = "UPL-1.0";
     fullName = "Universal Permissive License";
     url = "https://oss.oracle.com/licenses/upl/";
   };
@@ -1204,6 +1220,7 @@ in mkLicense lset) ({
   };
 
   xfig = {
+    spdxId = "Xfig";
     fullName = "xfig";
     url = "https://mcj.sourceforge.net/authors.html#xfig";
   };
diff --git a/lib/lists.nix b/lib/lists.nix
index 9397acf148fc..05216c1a66eb 100644
--- a/lib/lists.nix
+++ b/lib/lists.nix
@@ -2,9 +2,8 @@
 { lib }:
 let
   inherit (lib.strings) toInt;
-  inherit (lib.trivial) compare min id;
+  inherit (lib.trivial) compare min id warn;
   inherit (lib.attrsets) mapAttrs;
-  inherit (lib.lists) sort;
 in
 rec {
 
@@ -172,7 +171,7 @@ rec {
        concatMap (x: [x] ++ ["z"]) ["a" "b"]
        => [ "a" "z" "b" "z" ]
   */
-  concatMap = builtins.concatMap or (f: list: concatLists (map f list));
+  concatMap = builtins.concatMap;
 
   /* Flatten the argument into a single list; that is, nested lists are
      spliced into the top-level lists.
@@ -316,7 +315,7 @@ rec {
        any isString [ 1 { } ]
        => false
   */
-  any = builtins.any or (pred: foldr (x: y: if pred x then true else y) false);
+  any = builtins.any;
 
   /* Return true if function `pred` returns true for all elements of
      `list`.
@@ -329,7 +328,7 @@ rec {
        all (x: x < 3) [ 1 2 3 ]
        => false
   */
-  all = builtins.all or (pred: foldr (x: y: if pred x then y else false) true);
+  all = builtins.all;
 
   /* Count how many elements of `list` match the supplied predicate
      function.
@@ -428,12 +427,7 @@ rec {
        partition (x: x > 2) [ 5 1 2 3 4 ]
        => { right = [ 5 3 4 ]; wrong = [ 1 2 ]; }
   */
-  partition = builtins.partition or (pred:
-    foldr (h: t:
-      if pred h
-      then { right = [h] ++ t.right; wrong = t.wrong; }
-      else { right = t.right; wrong = [h] ++ t.wrong; }
-    ) { right = []; wrong = []; });
+  partition = builtins.partition;
 
   /* Splits the elements of a list into many lists, using the return value of a predicate.
      Predicate should return a string which becomes keys of attrset `groupBy` returns.
@@ -602,22 +596,7 @@ rec {
      Type:
        sort :: (a -> a -> Bool) -> [a] -> [a]
   */
-  sort = builtins.sort or (
-    strictLess: list:
-    let
-      len = length list;
-      first = head list;
-      pivot' = n: acc@{ left, right }: let el = elemAt list n; next = pivot' (n + 1); in
-        if n == len
-          then acc
-        else if strictLess first el
-          then next { inherit left; right = [ el ] ++ right; }
-        else
-          next { left = [ el ] ++ left; inherit right; };
-      pivot = pivot' 1 { left = []; right = []; };
-    in
-      if len < 2 then list
-      else (sort strictLess pivot.left) ++  [ first ] ++  (sort strictLess pivot.right));
+  sort = builtins.sort;
 
   /*
     Sort a list based on the default comparison of a derived property `b`.
@@ -848,8 +827,8 @@ rec {
       crossLists (x:y: "${toString x}${toString y}") [[1 2] [3 4]]
       => [ "13" "14" "23" "24" ]
   */
-  crossLists = builtins.trace
-    "lib.crossLists is deprecated, use lib.cartesianProductOfSets instead"
+  crossLists = warn
+    "lib.crossLists is deprecated, use lib.cartesianProductOfSets instead."
     (f: foldl (fs: args: concatMap (f: map f args) fs) [f]);
 
 
diff --git a/lib/meta.nix b/lib/meta.nix
index 5d5f71d6c3cb..675e1912d4be 100644
--- a/lib/meta.nix
+++ b/lib/meta.nix
@@ -87,6 +87,10 @@ rec {
 
      We can inject these into a pattern for the whole of a structured platform,
      and then match that.
+
+     Example:
+      lib.meta.platformMatch { system = "aarch64-darwin"; } "aarch64-darwin"
+      => true
   */
   platformMatch = platform: elem: (
     # Check with simple string comparison if elem was a string.
@@ -112,6 +116,10 @@ rec {
           platform, or `meta.platforms` is not present.
 
        2. None of `meta.badPlatforms` pattern matches the given platform.
+
+     Example:
+       lib.meta.availableOn { system = "aarch64-darwin"; } pkg.zsh
+       => true
   */
   availableOn = platform: pkg:
     ((!pkg?meta.platforms) || any (platformMatch platform) pkg.meta.platforms) &&
diff --git a/lib/modules.nix b/lib/modules.nix
index 64939a1eae81..61964d466781 100644
--- a/lib/modules.nix
+++ b/lib/modules.nix
@@ -81,9 +81,9 @@ let
                 , # `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.args option instead.
+                  args ? {}
                 , # This would be remove in the future, Prefer _module.check option instead.
                   check ? true
                 }:
@@ -1256,7 +1256,78 @@ let
       (opt.highestPrio or defaultOverridePriority)
       (f opt.value);
 
-  doRename = { from, to, visible, warn, use, withPriority ? true }:
+  /*
+    Return a module that help declares an option that has been renamed.
+    When a value is defined for the old option, it is forwarded to the `to` option.
+   */
+  doRename = {
+    # List of strings representing the attribute path of the old option.
+    from,
+    # List of strings representing the attribute path of the new option.
+    to,
+    # Boolean, whether the old option is to be included in documentation.
+    visible,
+    # Whether to warn when a value is defined for the old option.
+    # NOTE: This requires the NixOS assertions module to be imported, so
+    #        - this generally does not work in submodules
+    #        - this may or may not work outside NixOS
+    warn,
+    # A function that is applied to the option value, to form the value
+    # of the old `from` option.
+    #
+    # For example, the identity function can be passed, to return the option value unchanged.
+    # ```nix
+    # use = x: x;
+    # ```
+    #
+    # To add a warning, you can pass the partially applied `warn` function.
+    # ```nix
+    # use = lib.warn "Obsolete option `${opt.old}' is used. Use `${opt.to}' instead.";
+    # ```
+    use,
+    # Legacy option, enabled by default: whether to preserve the priority of definitions in `old`.
+    withPriority ? true,
+    # A boolean that defines the `mkIf` condition for `to`.
+    # If the condition evaluates to `true`, and the `to` path points into an
+    # `attrsOf (submodule ...)`, then `doRename` would cause an empty module to
+    # be created, even if the `from` option is undefined.
+    # By setting this to an expression that may return `false`, you can inhibit
+    # this undesired behavior.
+    #
+    # Example:
+    #
+    # ```nix
+    # { config, lib, ... }:
+    # let
+    #   inherit (lib) mkOption mkEnableOption types doRename;
+    # in
+    # {
+    #   options = {
+    #
+    #     # Old service
+    #     services.foo.enable = mkEnableOption "foo";
+    #
+    #     # New multi-instance service
+    #     services.foos = mkOption {
+    #       type = types.attrsOf (types.submodule …);
+    #     };
+    #   };
+    #   imports = [
+    #     (doRename {
+    #       from = [ "services" "foo" "bar" ];
+    #       to = [ "services" "foos" "" "bar" ];
+    #       visible = true;
+    #       warn = false;
+    #       use = x: x;
+    #       withPriority = true;
+    #       # Only define services.foos."" if needed. (It's not just about `bar`)
+    #       condition = config.services.foo.enable;
+    #     })
+    #   ];
+    # }
+    # ```
+    condition ? true
+  }:
     { config, options, ... }:
     let
       fromOpt = getAttrFromPath from options;
@@ -1272,7 +1343,7 @@ let
       } // optionalAttrs (toType != null) {
         type = toType;
       });
-      config = mkMerge [
+      config = mkIf condition (mkMerge [
         (optionalAttrs (options ? warnings) {
           warnings = optional (warn && fromOpt.isDefined)
             "The option `${showOption from}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption to}'.";
@@ -1280,7 +1351,7 @@ let
         (if withPriority
           then mkAliasAndWrapDefsWithPriority (setAttrByPath to) fromOpt
           else mkAliasAndWrapDefinitions (setAttrByPath to) fromOpt)
-      ];
+      ]);
     };
 
   /* Use this function to import a JSON file as NixOS configuration.
diff --git a/lib/options.nix b/lib/options.nix
index 9c10dfc8b36a..0d1d90efe217 100644
--- a/lib/options.nix
+++ b/lib/options.nix
@@ -254,13 +254,31 @@ rec {
     else if all isInt list && all (x: x == head list) list then head list
     else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}";
 
+  /*
+    Require a single definition.
+
+    WARNING: Does not perform nested checks, as this does not run the merge function!
+    */
   mergeOneOption = mergeUniqueOption { message = ""; };
 
-  mergeUniqueOption = { message }: loc: defs:
-    if length defs == 1
-    then (head defs).value
-    else assert length defs > 1;
-      throw "The option `${showOption loc}' is defined multiple times while it's expected to be unique.\n${message}\nDefinition values:${showDefs defs}\n${prioritySuggestion}";
+  /*
+    Require a single definition.
+
+    NOTE: When the type is not checked completely by check, pass a merge function for further checking (of sub-attributes, etc).
+   */
+  mergeUniqueOption = args@{
+      message,
+      # WARNING: the default merge function assumes that the definition is a valid (option) value. You MUST pass a merge function if the return value needs to be
+      #   - type checked beyond what .check does (which should be very litte; only on the value head; not attribute values, etc)
+      #   - if you want attribute values to be checked, or list items
+      #   - if you want coercedTo-like behavior to work
+      merge ? loc: defs: (head defs).value }:
+    loc: defs:
+      if length defs == 1
+      then merge loc defs
+      else
+        assert length defs > 1;
+        throw "The option `${showOption loc}' is defined multiple times while it's expected to be unique.\n${message}\nDefinition values:${showDefs defs}\n${prioritySuggestion}";
 
   /* "Merge" option definitions by checking that they all have the same value. */
   mergeEqualOption = loc: defs:
@@ -379,7 +397,7 @@ rec {
     if ! isString text then throw "literalExpression expects a string."
     else { _type = "literalExpression"; inherit text; };
 
-  literalExample = lib.warn "literalExample is deprecated, use literalExpression instead, or use literalMD for a non-Nix description." literalExpression;
+  literalExample = lib.warn "lib.literalExample is deprecated, use lib.literalExpression instead, or use lib.literalMD for a non-Nix description." literalExpression;
 
   /* Transition marker for documentation that's already migrated to markdown
      syntax. This is a no-op and no longer needed.
diff --git a/lib/strings.nix b/lib/strings.nix
index 49654d8abaa7..32efc9bdb70e 100644
--- a/lib/strings.nix
+++ b/lib/strings.nix
@@ -95,8 +95,7 @@ rec {
         concatStringsSep "/" ["usr" "local" "bin"]
         => "usr/local/bin"
   */
-  concatStringsSep = builtins.concatStringsSep or (separator: list:
-    lib.foldl' (x: y: x + y) "" (intersperse separator list));
+  concatStringsSep = builtins.concatStringsSep;
 
   /* Maps a function over a list of strings and then concatenates the
      result with the specified separator interspersed between
@@ -561,7 +560,7 @@ rec {
     ["&quot;" "&apos;" "&lt;" "&gt;" "&amp;"];
 
   # warning added 12-12-2022
-  replaceChars = lib.warn "replaceChars is a deprecated alias of replaceStrings, replace usages of it with replaceStrings." builtins.replaceStrings;
+  replaceChars = lib.warn "lib.replaceChars is a deprecated alias of lib.replaceStrings." builtins.replaceStrings;
 
   # Case conversion utilities.
   lowerChars = stringToCharacters "abcdefghijklmnopqrstuvwxyz";
@@ -1039,30 +1038,32 @@ rec {
        toInt "3.14"
        => error: floating point JSON numbers are not supported
   */
-  toInt = str:
+  toInt =
+    let
+      matchStripInput = match "[[:space:]]*(-?[[:digit:]]+)[[:space:]]*";
+      matchLeadingZero = match "0[[:digit:]]+";
+    in
+    str:
     let
       # RegEx: Match any leading whitespace, possibly a '-', one or more digits,
       # and finally match any trailing whitespace.
-      strippedInput = match "[[:space:]]*(-?[[:digit:]]+)[[:space:]]*" str;
+      strippedInput = matchStripInput str;
 
       # RegEx: Match a leading '0' then one or more digits.
-      isLeadingZero = match "0[[:digit:]]+" (head strippedInput) == [];
+      isLeadingZero = matchLeadingZero (head strippedInput) == [];
 
       # Attempt to parse input
       parsedInput = fromJSON (head strippedInput);
 
       generalError = "toInt: Could not convert ${escapeNixString str} to int.";
 
-      octalAmbigError = "toInt: Ambiguity in interpretation of ${escapeNixString str}"
-      + " between octal and zero padded integer.";
-
     in
       # Error on presence of non digit characters.
       if strippedInput == null
       then throw generalError
       # Error on presence of leading zero/octal ambiguity.
       else if isLeadingZero
-      then throw octalAmbigError
+      then throw "toInt: Ambiguity in interpretation of ${escapeNixString str} between octal and zero padded integer."
       # Error if parse function fails.
       else if !isInt parsedInput
       then throw generalError
@@ -1090,15 +1091,20 @@ rec {
        toIntBase10 "3.14"
        => error: floating point JSON numbers are not supported
   */
-  toIntBase10 = str:
+  toIntBase10 =
+    let
+      matchStripInput = match "[[:space:]]*0*(-?[[:digit:]]+)[[:space:]]*";
+      matchZero = match "0+";
+    in
+    str:
     let
       # RegEx: Match any leading whitespace, then match any zero padding,
       # capture possibly a '-' followed by one or more digits,
       # and finally match any trailing whitespace.
-      strippedInput = match "[[:space:]]*0*(-?[[:digit:]]+)[[:space:]]*" str;
+      strippedInput = matchStripInput str;
 
       # RegEx: Match at least one '0'.
-      isZero = match "0+" (head strippedInput) == [];
+      isZero = matchZero (head strippedInput) == [];
 
       # Attempt to parse input
       parsedInput = fromJSON (head strippedInput);
@@ -1133,7 +1139,7 @@ rec {
             "/prefix/nix-profiles-library-paths.patch"
             "/prefix/compose-search-path.patch" ]
   */
-  readPathsFromFile = lib.warn "lib.readPathsFromFile is deprecated, use a list instead"
+  readPathsFromFile = lib.warn "lib.readPathsFromFile is deprecated, use a list instead."
     (rootPath: file:
       let
         lines = lib.splitString "\n" (readFile file);
diff --git a/lib/systems/inspect.nix b/lib/systems/inspect.nix
index 073df78797c7..c6a33781ae28 100644
--- a/lib/systems/inspect.nix
+++ b/lib/systems/inspect.nix
@@ -48,6 +48,7 @@ rec {
     isRiscV64      = { cpu = { family = "riscv"; bits = 64; }; };
     isRx           = { cpu = { family = "rx"; }; };
     isSparc        = { cpu = { family = "sparc"; }; };
+    isSparc64      = { cpu = { family = "sparc"; bits = 64; }; };
     isWasm         = { cpu = { family = "wasm"; }; };
     isMsp430       = { cpu = { family = "msp430"; }; };
     isVc4          = { cpu = { family = "vc4"; }; };
@@ -62,7 +63,8 @@ rec {
 
     is32bit        = { cpu = { bits = 32; }; };
     is64bit        = { cpu = { bits = 64; }; };
-    isILP32        = map (a: { abi = { abi = a; }; }) [ "n32" "ilp32" "x32" ];
+    isILP32        = [ { cpu = { family = "wasm"; bits = 32; }; } ] ++
+                     map (a: { abi = { abi = a; }; }) [ "n32" "ilp32" "x32" ];
     isBigEndian    = { cpu = { significantByte = significantBytes.bigEndian; }; };
     isLittleEndian = { cpu = { significantByte = significantBytes.littleEndian; }; };
 
@@ -98,6 +100,9 @@ rec {
       { cpu = { family = "riscv"; }; }
       { cpu = { family = "x86"; }; }
     ];
+
+    isElf          = { kernel.execFormat = execFormats.elf; };
+    isMacho        = { kernel.execFormat = execFormats.macho; };
   };
 
   # given two patterns, return a pattern which is their logical AND.
diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix
index cf7fa9f2e284..98d1f4e71c48 100644
--- a/lib/tests/misc.nix
+++ b/lib/tests/misc.nix
@@ -55,6 +55,24 @@ runTests {
     expected = { a = false; b = false; c = true; };
   };
 
+  testCallPackageWithOverridePreservesArguments =
+    let
+      f = { a ? 0, b }: {};
+      f' = callPackageWith { a = 1; b = 2; } f {};
+    in {
+      expr = functionArgs f'.override;
+      expected = functionArgs f;
+    };
+
+  testCallPackagesWithOverridePreservesArguments =
+    let
+      f = { a ? 0, b }: { nested = {}; };
+      f' = callPackagesWith { a = 1; b = 2; } f {};
+    in {
+      expr = functionArgs f'.nested.override;
+      expected = functionArgs f;
+    };
+
 # TRIVIAL
 
   testId = {
@@ -1902,7 +1920,7 @@ runTests {
     expected = true;
   };
 
-  # lazyDerivation
+  # DERIVATIONS
 
   testLazyDerivationIsLazyInDerivationForAttrNames = {
     expr = attrNames (lazyDerivation {
@@ -1955,10 +1973,58 @@ runTests {
     expected = derivation;
   };
 
+  testOptionalDrvAttr = let
+    mkDerivation = args: derivation (args // {
+      builder = "builder";
+      system = "system";
+      __ignoreNulls = true;
+    });
+  in {
+    expr = (mkDerivation {
+      name = "foo";
+      x = optionalDrvAttr true 1;
+      y = optionalDrvAttr false 1;
+    }).drvPath;
+    expected = (mkDerivation {
+      name = "foo";
+      x = 1;
+    }).drvPath;
+  };
+
+  testLazyDerivationMultiOutputReturnsDerivationAttrs = let
+    derivation = {
+      type = "derivation";
+      outputs = ["out" "dev"];
+      dev = "test dev";
+      out = "test out";
+      outPath = "test outPath";
+      outputName = "out";
+      drvPath = "test drvPath";
+      name = "test name";
+      system = "test system";
+      meta.position = "/hi:23";
+    };
+  in {
+    expr = lazyDerivation { inherit derivation; outputs = ["out" "dev"]; passthru.meta.position = "/hi:23"; };
+    expected = derivation;
+  };
+
   testTypeDescriptionInt = {
     expr = (with types; int).description;
     expected = "signed integer";
   };
+  testTypeDescriptionIntsPositive = {
+    expr = (with types; ints.positive).description;
+    expected = "positive integer, meaning >0";
+  };
+  testTypeDescriptionIntsPositiveOrEnumAuto = {
+    expr = (with types; either ints.positive (enum ["auto"])).description;
+    expected = ''positive integer, meaning >0, or value "auto" (singular enum)'';
+  };
+  testTypeDescriptionListOfPositive = {
+    expr = (with types; listOf ints.positive).description;
+    expected = "list of (positive integer, meaning >0)";
+  };
   testTypeDescriptionListOfInt = {
     expr = (with types; listOf int).description;
     expected = "list of signed integer";
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index a90ff4ad9a2f..b3bbdf9485ac 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -101,6 +101,7 @@ checkConfigError 'It seems as if you.re trying to declare an option by placing i
 checkConfigError 'It seems as if you.re trying to declare an option by placing it into .config. rather than .options.' config.nest.wrong2 ./error-mkOption-in-config.nix
 checkConfigError 'The option .sub.wrong2. does not exist. Definition values:' config.sub ./error-mkOption-in-submodule-config.nix
 checkConfigError '.*This can happen if you e.g. declared your options in .types.submodule.' config.sub ./error-mkOption-in-submodule-config.nix
+checkConfigError '.*A definition for option .bad. is not of type .non-empty .list of .submodule...\.' config.bad ./error-nonEmptyListOf-submodule.nix
 
 # types.pathInStore
 checkConfigOutput '".*/store/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv"' config.pathInStore.ok1 ./types.nix
@@ -406,6 +407,16 @@ checkConfigOutput "{}" config.submodule.a ./emptyValues.nix
 checkConfigError 'The option .int.a. is used but not defined' config.int.a ./emptyValues.nix
 checkConfigError 'The option .nonEmptyList.a. is used but not defined' config.nonEmptyList.a ./emptyValues.nix
 
+# types.unique
+#   requires a single definition
+checkConfigError 'The option .examples\.merged. is defined multiple times while it.s expected to be unique' config.examples.merged.a ./types-unique.nix
+#   user message is printed
+checkConfigError 'We require a single definition, because seeing the whole value at once helps us maintain critical invariants of our system.' config.examples.merged.a ./types-unique.nix
+#   let the inner merge function check the values (on demand)
+checkConfigError 'A definition for option .examples\.badLazyType\.a. is not of type .string.' config.examples.badLazyType.a ./types-unique.nix
+#   overriding still works (unlike option uniqueness)
+checkConfigOutput '^"bee"$' config.examples.override.b ./types-unique.nix
+
 ## types.raw
 checkConfigOutput '^true$' config.unprocessedNestingEvaluates.success ./raw.nix
 checkConfigOutput "10" config.processedToplevel ./raw.nix
@@ -464,6 +475,9 @@ checkConfigOutput '^1234$' config.c.d.e ./doRename-basic.nix
 checkConfigOutput '^"The option `a\.b. defined in `.*/doRename-warnings\.nix. has been renamed to `c\.d\.e.\."$' \
   config.result \
   ./doRename-warnings.nix
+checkConfigOutput "^true$" config.result ./doRename-condition.nix ./doRename-condition-enable.nix
+checkConfigOutput "^true$" config.result ./doRename-condition.nix ./doRename-condition-no-enable.nix
+checkConfigOutput "^true$" config.result ./doRename-condition.nix ./doRename-condition-migrated.nix
 
 # Anonymous modules get deduplicated by key
 checkConfigOutput '^"pear"$' config.once.raw ./merge-module-with-key.nix
diff --git a/lib/tests/modules/doRename-condition-enable.nix b/lib/tests/modules/doRename-condition-enable.nix
new file mode 100644
index 000000000000..e6eabfa6f89a
--- /dev/null
+++ b/lib/tests/modules/doRename-condition-enable.nix
@@ -0,0 +1,10 @@
+{ config, lib, ... }:
+{
+  config = {
+    services.foo.enable = true;
+    services.foo.bar = "baz";
+    result =
+      assert config.services.foos == { "" = { bar = "baz"; }; };
+      true;
+  };
+}
diff --git a/lib/tests/modules/doRename-condition-migrated.nix b/lib/tests/modules/doRename-condition-migrated.nix
new file mode 100644
index 000000000000..8d21610e8ec6
--- /dev/null
+++ b/lib/tests/modules/doRename-condition-migrated.nix
@@ -0,0 +1,10 @@
+{ config, lib, ... }:
+{
+  config = {
+    services.foos."".bar = "baz";
+    result =
+      assert config.services.foos == { "" = { bar = "baz"; }; };
+      assert config.services.foo.bar == "baz";
+      true;
+  };
+}
diff --git a/lib/tests/modules/doRename-condition-no-enable.nix b/lib/tests/modules/doRename-condition-no-enable.nix
new file mode 100644
index 000000000000..66ec004d3147
--- /dev/null
+++ b/lib/tests/modules/doRename-condition-no-enable.nix
@@ -0,0 +1,9 @@
+{ config, lib, options, ... }:
+{
+  config = {
+    result =
+      assert config.services.foos == { };
+      assert ! options.services.foo.bar.isDefined;
+      true;
+  };
+}
diff --git a/lib/tests/modules/doRename-condition.nix b/lib/tests/modules/doRename-condition.nix
new file mode 100644
index 000000000000..c08b3035be6f
--- /dev/null
+++ b/lib/tests/modules/doRename-condition.nix
@@ -0,0 +1,42 @@
+/*
+  Simulate a migration from a single-instance `services.foo` to a multi instance
+  `services.foos.<name>` module, where `name = ""` serves as the legacy /
+  compatibility instance.
+
+  - No instances must exist, unless one is defined in the multi-instance module,
+  or if the legacy enable option is set to true.
+  - The legacy instance options must be renamed to the new instance, if it exists.
+
+  The relevant scenarios are tested in separate files:
+  - ./doRename-condition-enable.nix
+  - ./doRename-condition-no-enable.nix
+ */
+{ config, lib, ... }:
+let
+  inherit (lib) mkOption mkEnableOption types doRename;
+in
+{
+  options = {
+    services.foo.enable = mkEnableOption "foo";
+    services.foos = mkOption {
+      type = types.attrsOf (types.submodule {
+        options = {
+          bar = mkOption { type = types.str; };
+        };
+      });
+      default = { };
+    };
+    result = mkOption {};
+  };
+  imports = [
+    (doRename {
+      from = [ "services" "foo" "bar" ];
+      to = [ "services" "foos" "" "bar" ];
+      visible = true;
+      warn = false;
+      use = x: x;
+      withPriority = true;
+      condition = config.services.foo.enable;
+    })
+  ];
+}
diff --git a/lib/tests/modules/error-nonEmptyListOf-submodule.nix b/lib/tests/modules/error-nonEmptyListOf-submodule.nix
new file mode 100644
index 000000000000..1189e8891560
--- /dev/null
+++ b/lib/tests/modules/error-nonEmptyListOf-submodule.nix
@@ -0,0 +1,7 @@
+{ lib, ... }:
+{
+  options.bad = lib.mkOption {
+    type = lib.types.nonEmptyListOf (lib.types.submodule { });
+    default = [ ];
+  };
+}
diff --git a/lib/tests/modules/types-unique.nix b/lib/tests/modules/types-unique.nix
new file mode 100644
index 000000000000..115be0126975
--- /dev/null
+++ b/lib/tests/modules/types-unique.nix
@@ -0,0 +1,27 @@
+{ lib, ... }:
+let
+  inherit (lib) mkOption types;
+in
+{
+  options.examples = mkOption {
+    type = types.lazyAttrsOf
+      (types.unique
+        { message = "We require a single definition, because seeing the whole value at once helps us maintain critical invariants of our system."; }
+        (types.attrsOf types.str));
+  };
+  imports = [
+    { examples.merged = { b = "bee"; }; }
+    { examples.override = lib.mkForce { b = "bee"; }; }
+  ];
+  config.examples = {
+    merged = {
+      a = "aye";
+    };
+    override = {
+      a = "aye";
+    };
+    badLazyType = {
+      a = true;
+    };
+  };
+}
diff --git a/lib/tests/packages-from-directory/c/not-a-namespace/not-a-package.nix b/lib/tests/packages-from-directory/c/not-a-namespace/not-a-package.nix
index e69de29bb2d1..ffcd4415b08f 100644
--- a/lib/tests/packages-from-directory/c/not-a-namespace/not-a-package.nix
+++ b/lib/tests/packages-from-directory/c/not-a-namespace/not-a-package.nix
@@ -0,0 +1 @@
+{ }
diff --git a/lib/tests/packages-from-directory/c/support-definitions.nix b/lib/tests/packages-from-directory/c/support-definitions.nix
index e69de29bb2d1..ffcd4415b08f 100644
--- a/lib/tests/packages-from-directory/c/support-definitions.nix
+++ b/lib/tests/packages-from-directory/c/support-definitions.nix
@@ -0,0 +1 @@
+{ }
diff --git a/lib/tests/release.nix b/lib/tests/release.nix
index 96d34be8c2d3..5b2a9df1635c 100644
--- a/lib/tests/release.nix
+++ b/lib/tests/release.nix
@@ -9,60 +9,7 @@
 let
   lib = import ../.;
   testWithNix = nix:
-    pkgs.runCommand "nixpkgs-lib-tests-nix-${nix.version}" {
-      buildInputs = [
-        (import ./check-eval.nix)
-        (import ./maintainers.nix {
-          inherit pkgs;
-          lib = import ../.;
-        })
-        (import ./teams.nix {
-          inherit pkgs;
-          lib = import ../.;
-        })
-        (import ../path/tests {
-          inherit pkgs;
-        })
-      ];
-      nativeBuildInputs = [
-        nix
-        pkgs.gitMinimal
-      ] ++ lib.optional pkgs.stdenv.isLinux pkgs.inotify-tools;
-      strictDeps = true;
-    } ''
-      datadir="${nix}/share"
-      export TEST_ROOT=$(pwd)/test-tmp
-      export HOME=$(mktemp -d)
-      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
-      cacheDir=$TEST_ROOT/binary-cache
-
-      nix-store --init
-
-      cp -r ${../.} lib
-      echo "Running lib/tests/modules.sh"
-      bash lib/tests/modules.sh
-
-      echo "Running lib/tests/filesystem.sh"
-      TEST_LIB=$PWD/lib bash lib/tests/filesystem.sh
-
-      echo "Running lib/tests/sources.sh"
-      TEST_LIB=$PWD/lib bash lib/tests/sources.sh
-
-      echo "Running lib/fileset/tests.sh"
-      TEST_LIB=$PWD/lib bash lib/fileset/tests.sh
-
-      echo "Running lib/tests/systems.nix"
-      [[ $(nix-instantiate --eval --strict lib/tests/systems.nix | tee /dev/stderr) == '[ ]' ]];
-
-      mkdir $out
-      echo success > $out/${nix.version}
-    '';
+    import ./test-with-nix.nix { inherit lib nix pkgs; };
 
 in
   pkgs.symlinkJoin {
diff --git a/lib/tests/test-with-nix.nix b/lib/tests/test-with-nix.nix
new file mode 100644
index 000000000000..9d66b91cab42
--- /dev/null
+++ b/lib/tests/test-with-nix.nix
@@ -0,0 +1,76 @@
+/**
+ * Instantiate the library tests for a given Nix version.
+ *
+ * IMPORTANT:
+ * This is used by the github.com/NixOS/nix CI.
+ *
+ * Try not to change the interface of this file, or if you need to, ping the
+ * Nix maintainers for help. Thank you!
+ */
+{
+  pkgs,
+  lib,
+  # Only ever use this nix; see comment at top
+  nix,
+}:
+
+pkgs.runCommand "nixpkgs-lib-tests-nix-${nix.version}" {
+  buildInputs = [
+    (import ./check-eval.nix)
+    (import ./maintainers.nix {
+      inherit pkgs;
+      lib = import ../.;
+    })
+    (import ./teams.nix {
+      inherit pkgs;
+      lib = import ../.;
+    })
+    (import ../path/tests {
+      inherit pkgs;
+    })
+  ];
+  nativeBuildInputs = [
+    nix
+    pkgs.gitMinimal
+  ] ++ lib.optional pkgs.stdenv.isLinux pkgs.inotify-tools;
+  strictDeps = true;
+} ''
+  datadir="${nix}/share"
+  export TEST_ROOT=$(pwd)/test-tmp
+  export HOME=$(mktemp -d)
+  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
+  cacheDir=$TEST_ROOT/binary-cache
+
+  nix-store --init
+
+  cp -r ${../.} lib
+  echo "Running lib/tests/modules.sh"
+  bash lib/tests/modules.sh
+
+  echo "Checking lib.version"
+  nix-instantiate lib -A version --eval || {
+    echo "lib.version does not evaluate when lib is isolated from the rest of the nixpkgs tree"
+    exit 1
+  }
+
+  echo "Running lib/tests/filesystem.sh"
+  TEST_LIB=$PWD/lib bash lib/tests/filesystem.sh
+
+  echo "Running lib/tests/sources.sh"
+  TEST_LIB=$PWD/lib bash lib/tests/sources.sh
+
+  echo "Running lib/fileset/tests.sh"
+  TEST_LIB=$PWD/lib bash lib/fileset/tests.sh
+
+  echo "Running lib/tests/systems.nix"
+  [[ $(nix-instantiate --eval --strict lib/tests/systems.nix | tee /dev/stderr) == '[ ]' ]];
+
+  mkdir $out
+  echo success > $out/${nix.version}
+''
diff --git a/lib/trivial.nix b/lib/trivial.nix
index b2796096e8bc..c197822a4f8e 100644
--- a/lib/trivial.nix
+++ b/lib/trivial.nix
@@ -95,21 +95,6 @@ in {
   /* boolean “and” */
   and = x: y: x && y;
 
-  /* bitwise “and” */
-  bitAnd = builtins.bitAnd
-    or (import ./zip-int-bits.nix
-        (a: b: if a==1 && b==1 then 1 else 0));
-
-  /* bitwise “or” */
-  bitOr = builtins.bitOr
-    or (import ./zip-int-bits.nix
-        (a: b: if a==1 || b==1 then 1 else 0));
-
-  /* bitwise “xor” */
-  bitXor = builtins.bitXor
-    or (import ./zip-int-bits.nix
-        (a: b: if a!=b then 1 else 0));
-
   /* bitwise “not” */
   bitNot = builtins.sub (-1);
 
@@ -165,8 +150,8 @@ in {
   inherit (builtins)
     pathExists readFile isBool
     isInt isFloat add sub lessThan
-    seq deepSeq genericClosure;
-
+    seq deepSeq genericClosure
+    bitAnd bitOr bitXor;
 
   ## nixpkgs version strings
 
@@ -174,7 +159,7 @@ in {
   version = release + versionSuffix;
 
   /* Returns the current nixpkgs release number as string. */
-  release = lib.strings.fileContents ../.version;
+  release = lib.strings.fileContents ./.version;
 
   /* The latest release that is supported, at the time of release branch-off,
      if applicable.
@@ -189,7 +174,7 @@ in {
      they take effect as soon as the oldest release reaches end of life. */
   oldestSupportedRelease =
     # Update on master only. Do not backport.
-    2305;
+    2311;
 
   /* Whether a feature is supported in all supported releases (at the time of
      release branch-off, if applicable). See `oldestSupportedRelease`. */
@@ -230,7 +215,7 @@ in {
        else if lib.pathExists revisionFile then lib.fileContents revisionFile
        else default;
 
-  nixpkgsVersion = builtins.trace "`lib.nixpkgsVersion` is deprecated, use `lib.version` instead!" version;
+  nixpkgsVersion = warn "lib.nixpkgsVersion is a deprecated alias of lib.version." version;
 
   /* Determine whether the function is being called from inside a Nix
      shell.
diff --git a/lib/types.nix b/lib/types.nix
index 4378568c141f..12bf18633e3a 100644
--- a/lib/types.nix
+++ b/lib/types.nix
@@ -113,9 +113,14 @@ rec {
     , # Description of the type, defined recursively by embedding the wrapped type if any.
       description ? null
       # A hint for whether or not this description needs parentheses. Possible values:
-      #  - "noun": a simple noun phrase such as "positive integer"
-      #  - "conjunction": a phrase with a potentially ambiguous "or" connective.
+      #  - "noun": a noun phrase
+      #    Example description: "positive integer",
+      #  - "conjunction": a phrase with a potentially ambiguous "or" connective
+      #    Example description: "int or string"
       #  - "composite": a phrase with an "of" connective
+      #    Example description: "list of string"
+      #  - "nonRestrictiveClause": a noun followed by a comma and a clause
+      #    Example description: "positive integer, meaning >0"
       # See the `optionDescriptionPhrase` function.
     , descriptionClass ? null
     , # DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING!
@@ -338,10 +343,12 @@ rec {
         unsigned = addCheck types.int (x: x >= 0) // {
           name = "unsignedInt";
           description = "unsigned integer, meaning >=0";
+          descriptionClass = "nonRestrictiveClause";
         };
         positive = addCheck types.int (x: x > 0) // {
           name = "positiveInt";
           description = "positive integer, meaning >0";
+          descriptionClass = "nonRestrictiveClause";
         };
         u8 = unsign 8 256;
         u16 = unsign 16 65536;
@@ -383,10 +390,12 @@ rec {
       nonnegative = addCheck number (x: x >= 0) // {
         name = "numberNonnegative";
         description = "nonnegative integer or floating point number, meaning >=0";
+        descriptionClass = "nonRestrictiveClause";
       };
       positive = addCheck number (x: x > 0) // {
         name = "numberPositive";
         description = "positive integer or floating point number, meaning >0";
+        descriptionClass = "nonRestrictiveClause";
       };
     };
 
@@ -463,6 +472,7 @@ rec {
     passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // {
       name = "passwdEntry ${entryType.name}";
       description = "${optionDescriptionPhrase (class: class == "noun") entryType}, not containing newlines or colons";
+      descriptionClass = "nonRestrictiveClause";
     };
 
     attrs = mkOptionType {
@@ -547,6 +557,7 @@ rec {
       in list // {
         description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
         emptyValue = { }; # no .value attr, meaning unset
+        substSubModules = m: nonEmptyListOf (elemType.substSubModules m);
       };
 
     attrsOf = elemType: mkOptionType rec {
@@ -603,23 +614,12 @@ rec {
       nestedTypes.elemType = elemType;
     };
 
-    # Value of given type but with no merging (i.e. `uniq list`s are not concatenated).
-    uniq = elemType: mkOptionType rec {
-      name = "uniq";
-      inherit (elemType) description descriptionClass check;
-      merge = mergeOneOption;
-      emptyValue = elemType.emptyValue;
-      getSubOptions = elemType.getSubOptions;
-      getSubModules = elemType.getSubModules;
-      substSubModules = m: uniq (elemType.substSubModules m);
-      functor = (defaultFunctor name) // { wrapped = elemType; };
-      nestedTypes.elemType = elemType;
-    };
+    uniq = unique { message = ""; };
 
     unique = { message }: type: mkOptionType rec {
       name = "unique";
       inherit (type) description descriptionClass check;
-      merge = mergeUniqueOption { inherit message; };
+      merge = mergeUniqueOption { inherit message; inherit (type) merge; };
       emptyValue = type.emptyValue;
       getSubOptions = type.getSubOptions;
       getSubModules = type.getSubModules;
@@ -870,7 +870,13 @@ rec {
     # Either value of type `t1` or `t2`.
     either = t1: t2: mkOptionType rec {
       name = "either";
-      description = "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite") t2}";
+      description =
+        if t1.descriptionClass or null == "nonRestrictiveClause"
+        then
+          # Plain, but add comma
+          "${t1.description}, or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t2}"
+        else
+          "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite") t2}";
       descriptionClass = "conjunction";
       check = x: t1.check x || t2.check x;
       merge = loc: defs:
diff --git a/lib/versions.nix b/lib/versions.nix
index 986e7e5f9b37..720d19e8ca29 100644
--- a/lib/versions.nix
+++ b/lib/versions.nix
@@ -9,7 +9,7 @@ rec {
        splitVersion "1.2.3"
        => ["1" "2" "3"]
   */
-  splitVersion = builtins.splitVersion or (lib.splitString ".");
+  splitVersion = builtins.splitVersion;
 
   /* Get the major version string from a string.
 
diff --git a/lib/zip-int-bits.nix b/lib/zip-int-bits.nix
deleted file mode 100644
index 53efd2bb0a04..000000000000
--- a/lib/zip-int-bits.nix
+++ /dev/null
@@ -1,39 +0,0 @@
-/* Helper function to implement a fallback for the bit operators
-   `bitAnd`, `bitOr` and `bitXor` on older nix version.
-   See ./trivial.nix
-*/
-f: x: y:
-  let
-    # (intToBits 6) -> [ 0 1 1 ]
-    intToBits = x:
-      if x == 0 || x == -1 then
-        []
-      else
-        let
-          headbit  = if (x / 2) * 2 != x then 1 else 0;          # x & 1
-          tailbits = if x < 0 then ((x + 1) / 2) - 1 else x / 2; # x >> 1
-        in
-          [headbit] ++ (intToBits tailbits);
-
-    # (bitsToInt [ 0 1 1 ] 0) -> 6
-    # (bitsToInt [ 0 1 0 ] 1) -> -6
-    bitsToInt = l: signum:
-      if l == [] then
-        (if signum == 0 then 0 else -1)
-      else
-        (builtins.head l) + (2 * (bitsToInt (builtins.tail l) signum));
-
-    xsignum = if x < 0 then 1 else 0;
-    ysignum = if y < 0 then 1 else 0;
-    zipListsWith' = fst: snd:
-      if fst==[] && snd==[] then
-        []
-      else if fst==[] then
-        [(f xsignum             (builtins.head snd))] ++ (zipListsWith' []                  (builtins.tail snd))
-      else if snd==[] then
-        [(f (builtins.head fst) ysignum            )] ++ (zipListsWith' (builtins.tail fst) []                 )
-      else
-        [(f (builtins.head fst) (builtins.head snd))] ++ (zipListsWith' (builtins.tail fst) (builtins.tail snd));
-  in
-    assert (builtins.isInt x) && (builtins.isInt y);
-    bitsToInt (zipListsWith' (intToBits x) (intToBits y)) (f xsignum ysignum)