about summary refs log tree commit diff
path: root/nixpkgs/lib
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2024-05-03 15:14:25 +0200
committerAlyssa Ross <hi@alyssa.is>2024-05-07 11:19:19 +0200
commitd92b2b6a1bbd322dd65a8b6f51019610d350046e (patch)
tree7f7c21927b9cc05676501f297c51eb76b49e326c /nixpkgs/lib
parent93c9e56b40530cc627d921cfc255c05b495d4017 (diff)
parent49050352f602fe87d16ff7b2b6a05b79eb20dc6f (diff)
downloadnixlib-d92b2b6a1bbd322dd65a8b6f51019610d350046e.tar
nixlib-d92b2b6a1bbd322dd65a8b6f51019610d350046e.tar.gz
nixlib-d92b2b6a1bbd322dd65a8b6f51019610d350046e.tar.bz2
nixlib-d92b2b6a1bbd322dd65a8b6f51019610d350046e.tar.lz
nixlib-d92b2b6a1bbd322dd65a8b6f51019610d350046e.tar.xz
nixlib-d92b2b6a1bbd322dd65a8b6f51019610d350046e.tar.zst
nixlib-d92b2b6a1bbd322dd65a8b6f51019610d350046e.zip
Merge remote-tracking branch 'nixpkgs/nixos-unstable-small'
Conflicts:
	nixpkgs/nixos/modules/services/mail/mailman.nix
	nixpkgs/nixos/modules/services/mail/public-inbox.nix
	nixpkgs/pkgs/build-support/go/module.nix
Diffstat (limited to 'nixpkgs/lib')
-rw-r--r--nixpkgs/lib/attrsets.nix52
-rw-r--r--nixpkgs/lib/default.nix15
-rw-r--r--nixpkgs/lib/deprecated/README.md11
-rw-r--r--nixpkgs/lib/deprecated/misc.nix (renamed from nixpkgs/lib/deprecated.nix)9
-rw-r--r--nixpkgs/lib/fileset/default.nix2
-rw-r--r--nixpkgs/lib/gvariant.nix61
-rw-r--r--nixpkgs/lib/kernel.nix5
-rw-r--r--nixpkgs/lib/licenses.nix26
-rw-r--r--nixpkgs/lib/lists.nix72
-rw-r--r--nixpkgs/lib/modules.nix10
-rw-r--r--nixpkgs/lib/options.nix6
-rw-r--r--nixpkgs/lib/strings.nix12
-rw-r--r--nixpkgs/lib/systems/default.nix11
-rw-r--r--nixpkgs/lib/systems/doubles.nix2
-rw-r--r--nixpkgs/lib/systems/examples.nix7
-rw-r--r--nixpkgs/lib/systems/parse.nix6
-rw-r--r--nixpkgs/lib/tests/misc.nix157
-rwxr-xr-xnixpkgs/lib/tests/modules.sh12
-rw-r--r--nixpkgs/lib/tests/modules/doRename-condition.nix4
-rw-r--r--nixpkgs/lib/tests/modules/docs.nix41
-rw-r--r--nixpkgs/lib/tests/modules/types-attrTag-wrong-decl.nix14
-rw-r--r--nixpkgs/lib/tests/modules/types-attrTag.nix135
-rw-r--r--nixpkgs/lib/tests/release.nix2
-rw-r--r--nixpkgs/lib/tests/systems.nix6
-rw-r--r--nixpkgs/lib/trivial.nix18
-rw-r--r--nixpkgs/lib/types.nix129
26 files changed, 732 insertions, 93 deletions
diff --git a/nixpkgs/lib/attrsets.nix b/nixpkgs/lib/attrsets.nix
index de5968b95348..83f8d0f34186 100644
--- a/nixpkgs/lib/attrsets.nix
+++ b/nixpkgs/lib/attrsets.nix
@@ -5,7 +5,7 @@
 
 let
   inherit (builtins) head length;
-  inherit (lib.trivial) mergeAttrs warn;
+  inherit (lib.trivial) isInOldestRelease mergeAttrs warn warnIf;
   inherit (lib.strings) concatStringsSep concatMapStringsSep escapeNixIdentifier sanitizeDerivationName;
   inherit (lib.lists) foldr foldl' concatMap elemAt all partition groupBy take foldl;
 in
@@ -87,9 +87,9 @@ rec {
     Nix has a [has attribute operator `?`](https://nixos.org/manual/nix/stable/language/operators#has-attribute), which is sufficient for such queries, as long as the number of attributes is static. For example:
 
     ```nix
-    (x?a.b) == hasAttryByPath ["a" "b"] x
+    (x?a.b) == hasAttrByPath ["a" "b"] x
     # and
-    (x?${f p}."example.com") == hasAttryByPath [ (f p) "example.com" ] x
+    (x?${f p}."example.com") == hasAttrByPath [ (f p) "example.com" ] x
     ```
 
     **Laws**:
@@ -885,15 +885,15 @@ rec {
     # Type
 
     ```
-    cartesianProductOfSets :: AttrSet -> [AttrSet]
+    cartesianProduct :: AttrSet -> [AttrSet]
     ```
 
     # Examples
     :::{.example}
-    ## `lib.attrsets.cartesianProductOfSets` usage example
+    ## `lib.attrsets.cartesianProduct` usage example
 
     ```nix
-    cartesianProductOfSets { a = [ 1 2 ]; b = [ 10 20 ]; }
+    cartesianProduct { a = [ 1 2 ]; b = [ 10 20 ]; }
     => [
          { a = 1; b = 10; }
          { a = 1; b = 20; }
@@ -904,7 +904,7 @@ rec {
 
     :::
   */
-  cartesianProductOfSets =
+  cartesianProduct =
     attrsOfLists:
     foldl' (listOfAttrs: attrName:
       concatMap (attrs:
@@ -914,6 +914,40 @@ rec {
 
 
   /**
+    Return the result of function f applied to the cartesian product of attribute set value combinations.
+    Equivalent to using cartesianProduct followed by map.
+
+    # Inputs
+
+    `f`
+
+    : A function, given an attribute set, it returns a new value.
+
+    `attrsOfLists`
+
+    : Attribute set with attributes that are lists of values
+
+    # Type
+
+    ```
+    mapCartesianProduct :: (AttrSet -> a) -> AttrSet -> [a]
+    ```
+
+    # Examples
+    :::{.example}
+    ## `lib.attrsets.mapCartesianProduct` usage example
+
+    ```nix
+    mapCartesianProduct ({a, b}: "${a}-${b}") { a = [ "1" "2" ]; b = [ "3" "4" ]; }
+    => [ "1-3" "1-4" "2-3" "2-4" ]
+    ```
+
+    :::
+
+  */
+  mapCartesianProduct = f: attrsOfLists: map f (cartesianProduct attrsOfLists);
+
+  /**
     Utility function that creates a `{name, value}` pair as expected by `builtins.listToAttrs`.
 
 
@@ -1999,4 +2033,8 @@ rec {
   # DEPRECATED
   zip = warn
     "lib.zip is a deprecated alias of lib.zipAttrsWith." zipAttrsWith;
+
+  # DEPRECATED
+  cartesianProductOfSets = warnIf (isInOldestRelease 2405)
+    "lib.cartesianProductOfSets is a deprecated alias of lib.cartesianProduct." cartesianProduct;
 }
diff --git a/nixpkgs/lib/default.nix b/nixpkgs/lib/default.nix
index 668c29640f9f..d5d47defb8e6 100644
--- a/nixpkgs/lib/default.nix
+++ b/nixpkgs/lib/default.nix
@@ -47,7 +47,7 @@ let
     # misc
     asserts = callLibs ./asserts.nix;
     debug = callLibs ./debug.nix;
-    misc = callLibs ./deprecated.nix;
+    misc = callLibs ./deprecated/misc.nix;
 
     # domain-specific
     fetchers = callLibs ./fetchers.nix;
@@ -69,7 +69,7 @@ let
       hasAttr head isAttrs isBool isInt isList isPath isString length
       lessThan listToAttrs pathExists readFile replaceStrings seq
       stringLength sub substring tail trace;
-    inherit (self.trivial) id const pipe concat or and bitAnd bitOr bitXor
+    inherit (self.trivial) id const pipe concat or and xor bitAnd bitOr bitXor
       bitNot boolToString mergeAttrs flip mapNullable inNixShell isFloat min max
       importJSON importTOML warn warnIf warnIfNot throwIf throwIfNot checkListOfEnum
       info showWarnings nixpkgsVersion version isInOldestRelease
@@ -86,10 +86,10 @@ let
       zipAttrsWithNames zipAttrsWith zipAttrs recursiveUpdateUntil
       recursiveUpdate matchAttrs mergeAttrsList overrideExisting showAttrPath getOutput
       getBin getLib getDev getMan chooseDevOutputs zipWithNames zip
-      recurseIntoAttrs dontRecurseIntoAttrs cartesianProductOfSets
-      updateManyAttrsByPath;
+      recurseIntoAttrs dontRecurseIntoAttrs cartesianProduct cartesianProductOfSets
+      mapCartesianProduct updateManyAttrsByPath;
     inherit (self.lists) singleton forEach foldr fold foldl foldl' imap0 imap1
-      concatMap flatten remove findSingle findFirst any all count
+      ifilter0 concatMap flatten remove findSingle findFirst any all count
       optional optionals toList range replicate partition zipListsWith zipLists
       reverseList listDfs toposort sort sortOn naturalSort compareLists take
       drop sublist last init crossLists unique allUnique intersectLists
@@ -97,7 +97,7 @@ let
     inherit (self.strings) concatStrings concatMapStrings concatImapStrings
       intersperse concatStringsSep concatMapStringsSep
       concatImapStringsSep concatLines makeSearchPath makeSearchPathOutput
-      makeLibraryPath makeBinPath optionalString
+      makeLibraryPath makeIncludePath makeBinPath optionalString
       hasInfix hasPrefix hasSuffix stringToCharacters stringAsChars escape
       escapeShellArg escapeShellArgs
       isStorePath isStringLike
@@ -128,7 +128,7 @@ let
       canCleanSource pathIsGitRepo;
     inherit (self.modules) evalModules setDefaultModuleLocation
       unifyModuleSyntax applyModuleArgsIfFunction mergeModules
-      mergeModules' mergeOptionDecls evalOptionValue mergeDefinitions
+      mergeModules' mergeOptionDecls mergeDefinitions
       pushDownProperties dischargeProperties filterOverrides
       sortProperties fixupOptionType mkIf mkAssert mkMerge mkOverride
       mkOptionDefault mkDefault mkImageMediaOverride mkForce mkVMOverride
@@ -138,6 +138,7 @@ let
       mkMergedOptionModule mkChangedOptionModule
       mkAliasOptionModule mkDerivedConfig doRename
       mkAliasOptionModuleMD;
+    evalOptionValue = lib.warn "External use of `lib.evalOptionValue` is deprecated. If your use case isn't covered by non-deprecated functions, we'd like to know more and perhaps support your use case well, instead of providing access to these low level functions. In this case please open an issue in https://github.com/nixos/nixpkgs/issues/." self.modules.evalOptionValue;
     inherit (self.options) isOption mkEnableOption mkSinkUndeclaredOptions
       mergeDefaultOption mergeOneOption mergeEqualOption mergeUniqueOption
       getValues getFiles
diff --git a/nixpkgs/lib/deprecated/README.md b/nixpkgs/lib/deprecated/README.md
new file mode 100644
index 000000000000..afeb34d449f5
--- /dev/null
+++ b/nixpkgs/lib/deprecated/README.md
@@ -0,0 +1,11 @@
+
+# lib/deprecated
+
+Do not add any new functions to this directory.
+
+This directory contains the `lib.misc` sublibrary, which - as a location - is deprecated.
+Furthermore, some of the functions inside are of *dubious* utility, and should perhaps be avoided,
+while some functions *may still be needed*.
+
+This directory does not play a role in the deprecation process for library functions.
+They should be deprecated in place, by putting a `lib.warn` or `lib.warnIf` call around the function.
diff --git a/nixpkgs/lib/deprecated.nix b/nixpkgs/lib/deprecated/misc.nix
index b76622b5d842..d556bccbec0b 100644
--- a/nixpkgs/lib/deprecated.nix
+++ b/nixpkgs/lib/deprecated/misc.nix
@@ -314,12 +314,13 @@ let
       else if isInt x then "int"
       else "string";
 
-  /* deprecated:
+  /**
+    # Deprecated
 
-     For historical reasons, imap has an index starting at 1.
+    For historical reasons, imap has an index starting at 1.
 
-     But for consistency with the rest of the library we want an index
-     starting at zero.
+    But for consistency with the rest of the library we want an index
+    starting at zero.
   */
   imap = imap1;
 
diff --git a/nixpkgs/lib/fileset/default.nix b/nixpkgs/lib/fileset/default.nix
index ce9afc796a3f..e29f30251c69 100644
--- a/nixpkgs/lib/fileset/default.nix
+++ b/nixpkgs/lib/fileset/default.nix
@@ -1,5 +1,5 @@
 /*
-  <!-- This anchor is here for backwards compatibity -->
+  <!-- This anchor is here for backwards compatibility -->
   []{#sec-fileset}
 
   The [`lib.fileset`](#sec-functions-library-fileset) library allows you to work with _file sets_.
diff --git a/nixpkgs/lib/gvariant.nix b/nixpkgs/lib/gvariant.nix
index 708213224d3e..54aa4ea80571 100644
--- a/nixpkgs/lib/gvariant.nix
+++ b/nixpkgs/lib/gvariant.nix
@@ -53,6 +53,53 @@ rec {
 
   inherit type isGVariant;
 
+  intConstructors = [
+    {
+      name = "mkInt32";
+      type = type.int32;
+      min = -2147483648;
+      max = 2147483647;
+    }
+    {
+      name = "mkUint32";
+      type = type.uint32;
+      min = 0;
+      max = 4294967295;
+    }
+    {
+      name = "mkInt64";
+      type = type.int64;
+      # Nix does not support such large numbers.
+      min = null;
+      max = null;
+    }
+    {
+      name = "mkUint64";
+      type = type.uint64;
+      min = 0;
+      # Nix does not support such large numbers.
+      max = null;
+    }
+    {
+      name = "mkInt16";
+      type = type.int16;
+      min = -32768;
+      max = 32767;
+    }
+    {
+      name = "mkUint16";
+      type = type.uint16;
+      min = 0;
+      max = 65535;
+    }
+    {
+      name = "mkUchar";
+      type = type.uchar;
+      min = 0;
+      max = 255;
+    }
+  ];
+
   /* Returns the GVariant value that most closely matches the given Nix value.
      If no GVariant value can be found unambiguously then error is thrown.
 
@@ -70,8 +117,20 @@ rec {
       mkArray v
     else if isGVariant v then
       v
+    else if builtins.isInt v then
+      let
+        validConstructors = builtins.filter ({ min, max, ... }: (min == null || min <= v) && (max == null || v <= max)) intConstructors;
+      in
+      throw ''
+        The GVariant type for number “${builtins.toString v}” is unclear.
+        Please wrap the value with one of the following, depending on the value type in GSettings schema:
+
+        ${lib.concatMapStringsSep "\n" ({ name, type, ...}: "- `lib.gvariant.${name}` for `${type}`") validConstructors}
+      ''
+    else if builtins.isAttrs v then
+      throw "Cannot construct GVariant value from an attribute set. If you want to construct a dictionary, you will need to create an array containing items constructed with `lib.gvariant.mkDictionaryEntry`."
     else
-      throw "The GVariant type of ${v} can't be inferred.";
+      throw "The GVariant type of “${builtins.typeOf v}” can't be inferred.";
 
   /* Returns the GVariant array from the given type of the elements and a Nix list.
 
diff --git a/nixpkgs/lib/kernel.nix b/nixpkgs/lib/kernel.nix
index 7391f9e5d079..d03d0103a678 100644
--- a/nixpkgs/lib/kernel.nix
+++ b/nixpkgs/lib/kernel.nix
@@ -16,9 +16,8 @@ in
   unset    = { tristate    = null; optional = false; };
   freeform = x: { freeform = x; optional = false; };
 
-  /*
-    Common patterns/legacy used in common-config/hardened/config.nix
-   */
+
+  #  Common patterns/legacy used in common-config/hardened/config.nix
   whenHelpers = version: {
     whenAtLeast = ver: mkIf (versionAtLeast version ver);
     whenOlder   = ver: mkIf (versionOlder version ver);
diff --git a/nixpkgs/lib/licenses.nix b/nixpkgs/lib/licenses.nix
index 358b77117a36..7d2a22bc25a4 100644
--- a/nixpkgs/lib/licenses.nix
+++ b/nixpkgs/lib/licenses.nix
@@ -93,12 +93,12 @@ in mkLicense lset) ({
     url = "https://aomedia.org/license/patent-license/";
   };
 
-  apsl10 = {
+  apple-psl10 = {
     spdxId = "APSL-1.0";
     fullName = "Apple Public Source License 1.0";
   };
 
-  apsl20 = {
+  apple-psl20 = {
     spdxId = "APSL-2.0";
     fullName = "Apple Public Source License 2.0";
   };
@@ -632,6 +632,11 @@ in mkLicense lset) ({
     url = "https://old.calculate-linux.org/packages/licenses/iASL";
   };
 
+  icu = {
+    spdxId = "ICU";
+    fullName = "ICU";
+  };
+
   ijg = {
     spdxId = "IJG";
     fullName = "Independent JPEG Group License";
@@ -1168,6 +1173,11 @@ in mkLicense lset) ({
     # channel and NixOS images.
   };
 
+  unicode-30 = {
+    spdxId = "Unicode-3.0";
+    fullName = "Unicode License v3";
+  };
+
   unicode-dfs-2015 = {
     spdxId = "Unicode-DFS-2015";
     fullName = "Unicode License Agreement - Data Files and Software (2015)";
@@ -1272,6 +1282,18 @@ in mkLicense lset) ({
   };
 } // {
   # TODO: remove legacy aliases
+  apsl10 = {
+    # deprecated for consistency with `apple-psl20`; use `apple-psl10`
+    spdxId = "APSL-1.0";
+    fullName = "Apple Public Source License 1.0";
+    deprecated = true;
+  };
+  apsl20 = {
+    # deprecated due to confusion with Apache-2.0; use `apple-psl20`
+    spdxId = "APSL-2.0";
+    fullName = "Apple Public Source License 2.0";
+    deprecated = true;
+  };
   gpl2 = {
     spdxId = "GPL-2.0";
     fullName = "GNU General Public License v2.0";
diff --git a/nixpkgs/lib/lists.nix b/nixpkgs/lib/lists.nix
index c162f921280d..ca436d7a9c94 100644
--- a/nixpkgs/lib/lists.nix
+++ b/nixpkgs/lib/lists.nix
@@ -4,7 +4,7 @@
 { lib }:
 let
   inherit (lib.strings) toInt;
-  inherit (lib.trivial) compare min id warn;
+  inherit (lib.trivial) compare min id warn pipe;
   inherit (lib.attrsets) mapAttrs;
 in
 rec {
@@ -334,6 +334,54 @@ rec {
   imap1 = f: list: genList (n: f (n + 1) (elemAt list n)) (length list);
 
   /**
+    Filter a list for elements that satisfy a predicate function.
+    The predicate function is called with both the index and value for each element.
+    It must return `true`/`false` to include/exclude a given element in the result.
+    This function is strict in the result of the predicate function for each element.
+    This function has O(n) complexity.
+
+    Also see [`builtins.filter`](https://nixos.org/manual/nix/stable/language/builtins.html#builtins-filter) (available as `lib.lists.filter`),
+    which can be used instead when the index isn't needed.
+
+    # Inputs
+
+    `ipred`
+
+    : The predicate function, it takes two arguments:
+      - 1. (int): the index of the element.
+      - 2. (a): the value of the element.
+
+      It must return `true`/`false` to include/exclude a given element from the result.
+
+    `list`
+
+    : The list to filter using the predicate.
+
+    # Type
+    ```
+    ifilter0 :: (int -> a -> bool) -> [a] -> [a]
+    ```
+
+    # Examples
+    :::{.example}
+    ## `lib.lists.ifilter0` usage example
+
+    ```nix
+    ifilter0 (i: v: i == 0 || v > 2) [ 1 2 3 ]
+    => [ 1 3 ]
+    ```
+    :::
+  */
+  ifilter0 =
+    ipred:
+    input:
+    map (idx: elemAt input idx) (
+      filter (idx: ipred idx (elemAt input idx)) (
+        genList (x: x) (length input)
+      )
+    );
+
+  /**
     Map and concatenate the result.
 
     # Type
@@ -1688,16 +1736,32 @@ rec {
     ## `lib.lists.crossLists` usage example
 
     ```nix
-    crossLists (x:y: "${toString x}${toString y}") [[1 2] [3 4]]
+    crossLists (x: y: "${toString x}${toString y}") [[1 2] [3 4]]
     => [ "13" "14" "23" "24" ]
     ```
 
+    The following function call is equivalent to the one deprecated above:
+
+    ```nix
+    mapCartesianProduct (x: "${toString x.a}${toString x.b}") { a = [1 2]; b = [3 4]; }
+    => [ "13" "14" "23" "24" ]
+    ```
     :::
   */
   crossLists = warn
-    "lib.crossLists is deprecated, use lib.cartesianProductOfSets instead."
-    (f: foldl (fs: args: concatMap (f: map f args) fs) [f]);
+    ''lib.crossLists is deprecated, use lib.mapCartesianProduct instead.
 
+    For example, the following function call:
+
+    nix-repl> lib.crossLists (x: y: x+y) [[1 2] [3 4]]
+    [ 4 5 5 6 ]
+
+    Can now be replaced by the following one:
+
+    nix-repl> lib.mapCartesianProduct ({x,y}: x+y) { x = [1 2]; y = [3 4]; }
+    [ 4 5 5 6 ]
+    ''
+    (f: foldl (fs: args: concatMap (f: map f args) fs) [f]);
 
   /**
     Remove duplicate elements from the `list`. O(n^2) complexity.
diff --git a/nixpkgs/lib/modules.nix b/nixpkgs/lib/modules.nix
index 61964d466781..79892f50c4fe 100644
--- a/nixpkgs/lib/modules.nix
+++ b/nixpkgs/lib/modules.nix
@@ -136,7 +136,7 @@ let
             # TODO: Change the type of this option to a submodule with a
             # freeformType, so that individual arguments can be documented
             # separately
-            description = lib.mdDoc ''
+            description = ''
               Additional arguments passed to each module in addition to ones
               like `lib`, `config`,
               and `pkgs`, `modulesPath`.
@@ -187,14 +187,14 @@ let
             type = types.bool;
             internal = true;
             default = true;
-            description = lib.mdDoc "Whether to check whether all option definitions have matching declarations.";
+            description = "Whether to check whether all option definitions have matching declarations.";
           };
 
           _module.freeformType = mkOption {
             type = types.nullOr types.optionType;
             internal = true;
             default = null;
-            description = lib.mdDoc ''
+            description = ''
               If set, merge all definitions that don't have an associated option
               together using this type. The result then gets combined with the
               values of all declared options to produce the final `
@@ -209,7 +209,7 @@ let
           _module.specialArgs = mkOption {
             readOnly = true;
             internal = true;
-            description = lib.mdDoc ''
+            description = ''
               Externally provided module arguments that can't be modified from
               within a configuration, but can be used in module imports.
             '';
@@ -1378,7 +1378,6 @@ let
       inherit
         applyModuleArgsIfFunction
         dischargeProperties
-        evalOptionValue
         mergeModules
         mergeModules'
         pushDownProperties
@@ -1399,6 +1398,7 @@ private //
     defaultPriority
     doRename
     evalModules
+    evalOptionValue  # for use by lib.types
     filterOverrides
     filterOverrides'
     fixMergeModules
diff --git a/nixpkgs/lib/options.nix b/nixpkgs/lib/options.nix
index 0d1d90efe217..7e64e6e510fb 100644
--- a/nixpkgs/lib/options.nix
+++ b/nixpkgs/lib/options.nix
@@ -400,9 +400,11 @@ rec {
   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.
+     syntax. Has been a no-op for some while and been removed from nixpkgs.
+     Kept here to alert downstream users who may not be aware of the migration's
+     completion that it should be removed from modules.
   */
-  mdDoc = lib.id;
+  mdDoc = lib.warn "lib.mdDoc will be removed from nixpkgs in 24.11. Option descriptions are now in Markdown by default; you can remove any remaining uses of lib.mdDoc.";
 
   /* For use in the `defaultText` and `example` option attributes. Causes the
      given MD text to be inserted verbatim in the documentation, for when
diff --git a/nixpkgs/lib/strings.nix b/nixpkgs/lib/strings.nix
index 32efc9bdb70e..67bb669d04e0 100644
--- a/nixpkgs/lib/strings.nix
+++ b/nixpkgs/lib/strings.nix
@@ -206,6 +206,18 @@ rec {
   */
   makeLibraryPath = makeSearchPathOutput "lib" "lib";
 
+  /* Construct an include search path (such as C_INCLUDE_PATH) containing the
+     header files for a set of packages or paths.
+
+     Example:
+       makeIncludePath [ "/usr" "/usr/local" ]
+       => "/usr/include:/usr/local/include"
+       pkgs = import <nixpkgs> { }
+       makeIncludePath [ pkgs.openssl pkgs.zlib ]
+       => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r-dev/include:/nix/store/wwh7mhwh269sfjkm6k5665b5kgp7jrk2-zlib-1.2.8-dev/include"
+  */
+  makeIncludePath = makeSearchPathOutput "dev" "include";
+
   /* Construct a binary search path (such as $PATH) containing the
      binaries for a set of packages.
 
diff --git a/nixpkgs/lib/systems/default.nix b/nixpkgs/lib/systems/default.nix
index 83ed32ed3275..7e9aadeef72e 100644
--- a/nixpkgs/lib/systems/default.nix
+++ b/nixpkgs/lib/systems/default.nix
@@ -27,7 +27,7 @@ let
   examples = import ./examples.nix { inherit lib; };
   architectures = import ./architectures.nix { inherit lib; };
 
-  /*
+  /**
     Elaborated systems contain functions, which means that they don't satisfy
     `==` for a lack of reflexivity.
 
@@ -45,10 +45,13 @@ let
     let removeFunctions = a: filterAttrs (_: v: !isFunction v) a;
     in a: b: removeFunctions a == removeFunctions b;
 
-  /* List of all Nix system doubles the nixpkgs flake will expose the package set
-     for. All systems listed here must be supported by nixpkgs as `localSystem`.
+  /**
+    List of all Nix system doubles the nixpkgs flake will expose the package set
+    for. All systems listed here must be supported by nixpkgs as `localSystem`.
 
-     **Warning**: This attribute is considered experimental and is subject to change.
+    :::{.warning}
+    This attribute is considered experimental and is subject to change.
+    :::
   */
   flakeExposed = import ./flake-systems.nix { };
 
diff --git a/nixpkgs/lib/systems/doubles.nix b/nixpkgs/lib/systems/doubles.nix
index 13f029ee1f40..b4cd5acfcc5b 100644
--- a/nixpkgs/lib/systems/doubles.nix
+++ b/nixpkgs/lib/systems/doubles.nix
@@ -13,7 +13,7 @@ let
     "x86_64-darwin" "i686-darwin" "aarch64-darwin" "armv7a-darwin"
 
     # FreeBSD
-    "i686-freebsd13" "x86_64-freebsd13"
+    "i686-freebsd" "x86_64-freebsd"
 
     # Genode
     "aarch64-genode" "i686-genode" "x86_64-genode"
diff --git a/nixpkgs/lib/systems/examples.nix b/nixpkgs/lib/systems/examples.nix
index 75578b974945..8a3726f36968 100644
--- a/nixpkgs/lib/systems/examples.nix
+++ b/nixpkgs/lib/systems/examples.nix
@@ -235,6 +235,11 @@ rec {
     libc = "newlib";
   };
 
+  microblaze-embedded = {
+    config = "microblazeel-none-elf";
+    libc = "newlib";
+  };
+
   #
   # Redox
   #
@@ -323,7 +328,7 @@ rec {
   # BSDs
 
   x86_64-freebsd = {
-    config = "x86_64-unknown-freebsd13";
+    config = "x86_64-unknown-freebsd";
     useLLVM = true;
   };
 
diff --git a/nixpkgs/lib/systems/parse.nix b/nixpkgs/lib/systems/parse.nix
index 191e9734b879..4890912d7fed 100644
--- a/nixpkgs/lib/systems/parse.nix
+++ b/nixpkgs/lib/systems/parse.nix
@@ -326,11 +326,7 @@ rec {
     # the normalized name for macOS.
     macos    = { execFormat = macho;   families = { inherit darwin; }; name = "darwin"; };
     ios      = { execFormat = macho;   families = { inherit darwin; }; };
-    # A tricky thing about FreeBSD is that there is no stable ABI across
-    # versions. That means that putting in the version as part of the
-    # config string is paramount.
-    freebsd12 = { execFormat = elf;     families = { inherit bsd; }; name = "freebsd"; version = 12; };
-    freebsd13 = { execFormat = elf;     families = { inherit bsd; }; name = "freebsd"; version = 13; };
+    freebsd  = { execFormat = elf;     families = { inherit bsd; }; name = "freebsd"; };
     linux    = { execFormat = elf;     families = { }; };
     netbsd   = { execFormat = elf;     families = { inherit bsd; }; };
     none     = { execFormat = unknown; families = { }; };
diff --git a/nixpkgs/lib/tests/misc.nix b/nixpkgs/lib/tests/misc.nix
index 6f1d9039db80..6774939023d2 100644
--- a/nixpkgs/lib/tests/misc.nix
+++ b/nixpkgs/lib/tests/misc.nix
@@ -1,17 +1,21 @@
-/*
-Nix evaluation tests for various lib functions.
+/**
+  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.
+  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.
 
-To run these tests:
+  If you need to test error messages or more complex evaluations, see
+  `lib/tests/modules.sh`, `lib/tests/sources.sh` or `lib/tests/filesystem.sh` as examples.
 
-  [nixpkgs]$ nix-instantiate --eval --strict lib/tests/misc.nix
+  To run these tests:
 
-If the resulting list is empty, all tests passed.
-Alternatively, to run all `lib` tests:
+    [nixpkgs]$ nix-instantiate --eval --strict lib/tests/misc.nix
 
-  [nixpkgs]$ nix-build lib/tests/release.nix
+  If the resulting list is empty, all tests passed.
+  Alternatively, to run all `lib` tests:
+
+    [nixpkgs]$ nix-build lib/tests/release.nix
 */
 
 let
@@ -29,7 +33,7 @@ let
     boolToString
     callPackagesWith
     callPackageWith
-    cartesianProductOfSets
+    cartesianProduct
     cli
     composeExtensions
     composeManyExtensions
@@ -59,17 +63,20 @@ let
     hasAttrByPath
     hasInfix
     id
+    ifilter0
     isStorePath
     lazyDerivation
+    length
     lists
     listToAttrs
     makeExtensible
+    makeIncludePath
     makeOverridable
     mapAttrs
+    mapCartesianProduct
     matchAttrs
     mergeAttrs
     meta
-    mkOption
     mod
     nameValuePair
     optionalDrvAttr
@@ -101,6 +108,7 @@ let
     types
     updateManyAttrsByPath
     versions
+    xor
     ;
 
   testingThrow = expr: {
@@ -111,7 +119,6 @@ let
     expr = (builtins.tryEval expr).success;
     expected = true;
   };
-  testingDeepThrow = expr: testingThrow (builtins.deepSeq expr expr);
 
   testSanitizeDerivationName = { name, expected }:
   let
@@ -198,10 +205,10 @@ runTests {
   };
 
   /*
-  testOr = {
-    expr = or true false;
-    expected = true;
-  };
+    testOr = {
+      expr = or true false;
+      expected = true;
+    };
   */
 
   testAnd = {
@@ -209,6 +216,21 @@ runTests {
     expected = false;
   };
 
+  testXor = {
+    expr = [
+      (xor true false)
+      (xor true true)
+      (xor false false)
+      (xor false true)
+    ];
+    expected = [
+      true
+      false
+      false
+      true
+    ];
+  };
+
   testFix = {
     expr = fix (x: {a = if x ? a then "a" else "b";});
     expected = {a = "a";};
@@ -296,6 +318,35 @@ runTests {
     expected = "a\nb\nc\n";
   };
 
+  testMakeIncludePathWithPkgs = {
+    expr = (makeIncludePath [
+      # makeIncludePath preferably selects the "dev" output
+      { dev.outPath = "/dev"; out.outPath = "/out"; outPath = "/default"; }
+      # "out" is used if "dev" is not found
+      { out.outPath = "/out"; outPath = "/default"; }
+      # And it returns the derivation directly if there's no "out" either
+      { outPath = "/default"; }
+      # Same if the output is specified explicitly, even if there's a "dev"
+      { dev.outPath = "/dev"; outPath = "/default"; outputSpecified = true; }
+    ]);
+    expected = "/dev/include:/out/include:/default/include:/default/include";
+  };
+
+  testMakeIncludePathWithEmptyList = {
+    expr = (makeIncludePath [ ]);
+    expected = "";
+  };
+
+  testMakeIncludePathWithOneString = {
+    expr = (makeIncludePath [ "/usr" ]);
+    expected = "/usr/include";
+  };
+
+  testMakeIncludePathWithManyString = {
+    expr = (makeIncludePath [ "/usr" "/usr/local" ]);
+    expected = "/usr/include:/usr/local/include";
+  };
+
   testReplicateString = {
     expr = strings.replicate 5 "hello";
     expected = "hellohellohellohellohello";
@@ -602,6 +653,31 @@ runTests {
     expected = ["b" "c"];
   };
 
+  testIfilter0Example = {
+    expr = ifilter0 (i: v: i == 0 || v > 2) [ 1 2 3 ];
+    expected = [ 1 3 ];
+  };
+  testIfilter0Empty = {
+    expr = ifilter0 (i: v: abort "shouldn't be evaluated!") [ ];
+    expected = [ ];
+  };
+  testIfilter0IndexOnly = {
+    expr = length (ifilter0 (i: v: mod i 2 == 0) [ (throw "0") (throw "1") (throw "2") (throw "3")]);
+    expected = 2;
+  };
+  testIfilter0All = {
+    expr = ifilter0 (i: v: true) [ 10 11 12 13 14 15 ];
+    expected = [ 10 11 12 13 14 15 ];
+  };
+  testIfilter0First = {
+    expr = ifilter0 (i: v: i == 0) [ 10 11 12 13 14 15 ];
+    expected = [ 10 ];
+  };
+  testIfilter0Last = {
+    expr = ifilter0 (i: v: i == 5) [ 10 11 12 13 14 15 ];
+    expected = [ 15 ];
+  };
+
   testFold =
     let
       f = op: fold: fold op 0 (range 0 100);
@@ -1267,7 +1343,7 @@ runTests {
     '';
   };
 
-  /* right now only invocation check */
+  # right now only invocation check
   testToJSONSimple =
     let val = {
       foobar = [ "baz" 1 2 3 ];
@@ -1278,7 +1354,7 @@ runTests {
       expected = builtins.toJSON val;
   };
 
-  /* right now only invocation check */
+  # right now only invocation check
   testToYAMLSimple =
     let val = {
       list = [ { one = 1; } { two = 2; } ];
@@ -1365,7 +1441,7 @@ runTests {
     };
 
   testToPrettyMultiline = {
-    expr = mapAttrs (const (generators.toPretty { })) rec {
+    expr = mapAttrs (const (generators.toPretty { })) {
       list = [ 3 4 [ false ] ];
       attrs = { foo = null; bar.foo = "baz"; };
       newlinestring = "\n";
@@ -1379,7 +1455,7 @@ runTests {
         there
         test'';
     };
-    expected = rec {
+    expected = {
       list = ''
         [
           3
@@ -1417,13 +1493,10 @@ runTests {
     expected  = "«foo»";
   };
 
-  testToPlist =
-    let
-      deriv = derivation { name = "test"; builder = "/bin/sh"; system = "aarch64-linux"; };
-    in {
+  testToPlist = {
     expr = mapAttrs (const (generators.toPlist { })) {
       value = {
-        nested.values = rec {
+        nested.values = {
           int = 42;
           float = 0.1337;
           bool = true;
@@ -1636,17 +1709,17 @@ runTests {
   };
 
   testCartesianProductOfEmptySet = {
-    expr = cartesianProductOfSets {};
+    expr = cartesianProduct {};
     expected = [ {} ];
   };
 
   testCartesianProductOfOneSet = {
-    expr = cartesianProductOfSets { a = [ 1 2 3 ]; };
+    expr = cartesianProduct { a = [ 1 2 3 ]; };
     expected = [ { a = 1; } { a = 2; } { a = 3; } ];
   };
 
   testCartesianProductOfTwoSets = {
-    expr = cartesianProductOfSets { a = [ 1 ]; b = [ 10 20 ]; };
+    expr = cartesianProduct { a = [ 1 ]; b = [ 10 20 ]; };
     expected = [
       { a = 1; b = 10; }
       { a = 1; b = 20; }
@@ -1654,12 +1727,12 @@ runTests {
   };
 
   testCartesianProductOfTwoSetsWithOneEmpty = {
-    expr = cartesianProductOfSets { a = [ ]; b = [ 10 20 ]; };
+    expr = cartesianProduct { a = [ ]; b = [ 10 20 ]; };
     expected = [ ];
   };
 
   testCartesianProductOfThreeSets = {
-    expr = cartesianProductOfSets {
+    expr = cartesianProduct {
       a = [   1   2   3 ];
       b = [  10  20  30 ];
       c = [ 100 200 300 ];
@@ -1703,6 +1776,30 @@ runTests {
     ];
   };
 
+  testMapCartesianProductOfOneSet = {
+    expr = mapCartesianProduct ({a}: a * 2) { a = [ 1 2 3 ]; };
+    expected = [ 2 4 6 ];
+  };
+
+  testMapCartesianProductOfTwoSets = {
+    expr = mapCartesianProduct ({a,b}: a + b) { a = [ 1 ]; b = [ 10 20 ]; };
+    expected = [ 11 21 ];
+  };
+
+  testMapCartesianProcutOfTwoSetsWithOneEmpty = {
+    expr = mapCartesianProduct (x: x.a + x.b) { a = [ ]; b = [ 10 20 ]; };
+    expected = [ ];
+  };
+
+  testMapCartesianProductOfThreeSets = {
+    expr = mapCartesianProduct ({a,b,c}: a + b + c) {
+      a = [ 1 2 3 ];
+      b = [ 10 20 30 ];
+      c = [ 100 200 300 ];
+    };
+    expected = [ 111 211 311 121 221 321 131 231 331 112 212 312 122 222 322 132 232 332 113 213 313 123 223 323 133 233 333 ];
+  };
+
   # The example from the showAttrPath documentation
   testShowAttrPathExample = {
     expr = showAttrPath [ "foo" "10" "bar" ];
diff --git a/nixpkgs/lib/tests/modules.sh b/nixpkgs/lib/tests/modules.sh
index b3bbdf9485ac..750b1d025e02 100755
--- a/nixpkgs/lib/tests/modules.sh
+++ b/nixpkgs/lib/tests/modules.sh
@@ -103,6 +103,18 @@ checkConfigError 'The option .sub.wrong2. does not exist. Definition values:' co
 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.attrTag
+checkConfigOutput '^true$' config.okChecks ./types-attrTag.nix
+checkConfigError 'A definition for option .intStrings\.syntaxError. is not of type .attribute-tagged union' config.intStrings.syntaxError ./types-attrTag.nix
+checkConfigError 'A definition for option .intStrings\.syntaxError2. is not of type .attribute-tagged union' config.intStrings.syntaxError2 ./types-attrTag.nix
+checkConfigError 'A definition for option .intStrings\.syntaxError3. is not of type .attribute-tagged union' config.intStrings.syntaxError3 ./types-attrTag.nix
+checkConfigError 'A definition for option .intStrings\.syntaxError4. is not of type .attribute-tagged union' config.intStrings.syntaxError4 ./types-attrTag.nix
+checkConfigError 'A definition for option .intStrings\.mergeError. is not of type .attribute-tagged union' config.intStrings.mergeError ./types-attrTag.nix
+checkConfigError 'A definition for option .intStrings\.badTagError. is not of type .attribute-tagged union' config.intStrings.badTagError ./types-attrTag.nix
+checkConfigError 'A definition for option .intStrings\.badTagTypeError\.left. is not of type .signed integer.' config.intStrings.badTagTypeError.left ./types-attrTag.nix
+checkConfigError 'A definition for option .nested\.right\.left. is not of type .signed integer.' config.nested.right.left ./types-attrTag.nix
+checkConfigError 'In attrTag, each tag value must be an option, but tag int was a bare type, not wrapped in mkOption.' config.opt.int ./types-attrTag-wrong-decl.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
diff --git a/nixpkgs/lib/tests/modules/doRename-condition.nix b/nixpkgs/lib/tests/modules/doRename-condition.nix
index c08b3035be6f..176c21a01a17 100644
--- a/nixpkgs/lib/tests/modules/doRename-condition.nix
+++ b/nixpkgs/lib/tests/modules/doRename-condition.nix
@@ -1,4 +1,4 @@
-/*
+/**
   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.
@@ -10,7 +10,7 @@
   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;
diff --git a/nixpkgs/lib/tests/modules/docs.nix b/nixpkgs/lib/tests/modules/docs.nix
new file mode 100644
index 000000000000..225aa7eac1de
--- /dev/null
+++ b/nixpkgs/lib/tests/modules/docs.nix
@@ -0,0 +1,41 @@
+/*
+  A basic documentation generating module.
+  Declares and defines a `docs` option, suitable for making assertions about
+  the extraction "phase" of documentation generation.
+ */
+{ lib, options, ... }:
+
+let
+  inherit (lib)
+    head
+    length
+    mkOption
+    types
+  ;
+
+  traceListSeq = l: v: lib.foldl' (a: b: lib.traceSeq b a) v l;
+
+in
+
+{
+  options.docs = mkOption {
+    type = types.lazyAttrsOf types.raw;
+    description = ''
+      All options to be rendered, without any visibility filtering applied.
+    '';
+  };
+  config.docs =
+    lib.zipAttrsWith
+      (name: values:
+        if length values > 1 then
+          traceListSeq values
+          abort "Multiple options with the same name: ${name}"
+        else
+          assert length values == 1;
+          head values
+      )
+      (map
+        (opt: { ${opt.name} = opt; })
+        (lib.optionAttrSetToDocList options)
+      );
+}
diff --git a/nixpkgs/lib/tests/modules/types-attrTag-wrong-decl.nix b/nixpkgs/lib/tests/modules/types-attrTag-wrong-decl.nix
new file mode 100644
index 000000000000..d03370bc10da
--- /dev/null
+++ b/nixpkgs/lib/tests/modules/types-attrTag-wrong-decl.nix
@@ -0,0 +1,14 @@
+{ lib, ... }:
+let
+  inherit (lib) types mkOption;
+in
+{
+  options = {
+    opt = mkOption {
+      type = types.attrTag {
+        int = types.int;
+      };
+      default = { int = 1; };
+    };
+  };
+}
diff --git a/nixpkgs/lib/tests/modules/types-attrTag.nix b/nixpkgs/lib/tests/modules/types-attrTag.nix
new file mode 100644
index 000000000000..b2e5158bb44b
--- /dev/null
+++ b/nixpkgs/lib/tests/modules/types-attrTag.nix
@@ -0,0 +1,135 @@
+{ lib, config, options, ... }:
+let
+  inherit (lib) mkOption types;
+  forceDeep = x: builtins.deepSeq x x;
+  mergedSubOption = (options.merged.type.getSubOptions options.merged.loc).extensible."merged.<name>";
+in
+{
+  options = {
+    intStrings = mkOption {
+      type = types.attrsOf
+        (types.attrTag {
+          left = mkOption {
+            type = types.int;
+          };
+          right = mkOption {
+            type = types.str;
+          };
+        });
+    };
+    nested = mkOption {
+      type = types.attrTag {
+        left = mkOption {
+          type = types.int;
+        };
+        right = mkOption {
+          type = types.attrTag {
+            left = mkOption {
+              type = types.int;
+            };
+            right = mkOption {
+              type = types.str;
+            };
+          };
+        };
+      };
+    };
+    merged = mkOption {
+      type = types.attrsOf (
+        types.attrTag {
+          yay = mkOption {
+            type = types.int;
+          };
+          extensible = mkOption {
+            type = types.enum [ "foo" ];
+          };
+        }
+      );
+    };
+    submodules = mkOption {
+      type = types.attrsOf (
+        types.attrTag {
+          foo = mkOption {
+            type = types.submodule {
+              options = {
+                bar = mkOption {
+                  type = types.int;
+                };
+              };
+            };
+          };
+          qux = mkOption {
+            type = types.str;
+            description = "A qux for when you don't want a foo";
+          };
+        }
+      );
+    };
+    okChecks = mkOption {};
+  };
+  imports = [
+    ./docs.nix
+    {
+      options.merged = mkOption {
+        type = types.attrsOf (
+          types.attrTag {
+            nay = mkOption {
+              type = types.bool;
+            };
+            extensible = mkOption {
+              type = types.enum [ "bar" ];
+            };
+          }
+        );
+      };
+    }
+  ];
+  config = {
+    intStrings.syntaxError = 1;
+    intStrings.syntaxError2 = {};
+    intStrings.syntaxError3 = { a = true; b = true; };
+    intStrings.syntaxError4 = lib.mkMerge [ { a = true; } { b = true; } ];
+    intStrings.mergeError = lib.mkMerge [ { int = throw "do not eval"; } { string = throw "do not eval"; } ];
+    intStrings.badTagError.rite = throw "do not eval";
+    intStrings.badTagTypeError.left = "bad";
+    intStrings.numberOne.left = 1;
+    intStrings.hello.right = "hello world";
+    nested.right.left = "not a number";
+    merged.negative.nay = false;
+    merged.positive.yay = 100;
+    merged.extensi-foo.extensible = "foo";
+    merged.extensi-bar.extensible = "bar";
+    okChecks = builtins.addErrorContext "while evaluating the assertions" (
+      assert config.intStrings.hello == { right = "hello world"; };
+      assert config.intStrings.numberOne == { left = 1; };
+      assert config.merged.negative == { nay = false; };
+      assert config.merged.positive == { yay = 100; };
+      assert config.merged.extensi-foo == { extensible = "foo"; };
+      assert config.merged.extensi-bar == { extensible = "bar"; };
+      assert config.docs."submodules.<name>.foo.bar".type == "signed integer";
+      assert config.docs."submodules.<name>.qux".type == "string";
+      assert config.docs."submodules.<name>.qux".declarations == [ __curPos.file ];
+      assert config.docs."submodules.<name>.qux".loc == [ "submodules" "<name>" "qux" ];
+      assert config.docs."submodules.<name>.qux".name == "submodules.<name>.qux";
+      assert config.docs."submodules.<name>.qux".description == "A qux for when you don't want a foo";
+      assert config.docs."submodules.<name>.qux".readOnly == false;
+      assert config.docs."submodules.<name>.qux".visible == true;
+      # Not available (yet?)
+      # assert config.docs."submodules.<name>.qux".declarationsWithPositions == [ ... ];
+      assert options.submodules.declarations == [ __curPos.file ];
+      assert lib.length options.submodules.declarationPositions == 1;
+      assert (lib.head options.submodules.declarationPositions).file == __curPos.file;
+      assert options.merged.declarations == [ __curPos.file __curPos.file ];
+      assert lib.length options.merged.declarationPositions == 2;
+      assert (lib.elemAt options.merged.declarationPositions 0).file == __curPos.file;
+      assert (lib.elemAt options.merged.declarationPositions 1).file == __curPos.file;
+      assert (lib.elemAt options.merged.declarationPositions 0).line != (lib.elemAt options.merged.declarationPositions 1).line;
+      assert mergedSubOption.declarations == [ __curPos.file __curPos.file ];
+      assert lib.length mergedSubOption.declarationPositions == 2;
+      assert (lib.elemAt mergedSubOption.declarationPositions 0).file == __curPos.file;
+      assert (lib.elemAt mergedSubOption.declarationPositions 1).file == __curPos.file;
+      assert (lib.elemAt mergedSubOption.declarationPositions 0).line != (lib.elemAt mergedSubOption.declarationPositions 1).line;
+      assert lib.length config.docs."merged.<name>.extensible".declarations == 2;
+      true);
+  };
+}
diff --git a/nixpkgs/lib/tests/release.nix b/nixpkgs/lib/tests/release.nix
index 5b2a9df1635c..1447e8817091 100644
--- a/nixpkgs/lib/tests/release.nix
+++ b/nixpkgs/lib/tests/release.nix
@@ -2,7 +2,7 @@
   # Don't test properties of pkgs.lib, but rather the lib in the parent directory
   pkgs ? import ../.. {} // { lib = throw "pkgs.lib accessed, but the lib tests should use nixpkgs' lib path directly!"; },
   nix ? pkgs-nixVersions.stable,
-  nixVersions ? [ pkgs-nixVersions.minimum nix pkgs-nixVersions.unstable ],
+  nixVersions ? [ pkgs-nixVersions.minimum nix pkgs-nixVersions.latest ],
   pkgs-nixVersions ? import ./nix-for-tests.nix { inherit pkgs; },
 }:
 
diff --git a/nixpkgs/lib/tests/systems.nix b/nixpkgs/lib/tests/systems.nix
index e142ff307fbd..03c5d6868962 100644
--- a/nixpkgs/lib/tests/systems.nix
+++ b/nixpkgs/lib/tests/systems.nix
@@ -40,7 +40,7 @@ lib.runTests (
 
   testarm = mseteq arm [ "armv5tel-linux" "armv6l-linux" "armv6l-netbsd" "armv6l-none" "armv7a-linux" "armv7a-netbsd" "armv7l-linux" "armv7l-netbsd" "arm-none" "armv7a-darwin" ];
   testarmv7 = mseteq armv7 [ "armv7a-darwin" "armv7a-linux" "armv7l-linux" "armv7a-netbsd" "armv7l-netbsd" ];
-  testi686 = mseteq i686 [ "i686-linux" "i686-freebsd13" "i686-genode" "i686-netbsd" "i686-openbsd" "i686-cygwin" "i686-windows" "i686-none" "i686-darwin" ];
+  testi686 = mseteq i686 [ "i686-linux" "i686-freebsd" "i686-genode" "i686-netbsd" "i686-openbsd" "i686-cygwin" "i686-windows" "i686-none" "i686-darwin" ];
   testmips = mseteq mips [ "mips-none" "mips64-none" "mips-linux" "mips64-linux" "mips64el-linux" "mipsel-linux" "mipsel-netbsd" ];
   testmmix = mseteq mmix [ "mmix-mmixware" ];
   testpower = mseteq power [ "powerpc-netbsd" "powerpc-none" "powerpc64-linux" "powerpc64le-linux" "powerpcle-none" ];
@@ -48,11 +48,11 @@ lib.runTests (
   testriscv32 = mseteq riscv32 [ "riscv32-linux" "riscv32-netbsd" "riscv32-none" ];
   testriscv64 = mseteq riscv64 [ "riscv64-linux" "riscv64-netbsd" "riscv64-none" ];
   tests390x = mseteq s390x [ "s390x-linux" "s390x-none" ];
-  testx86_64 = mseteq x86_64 [ "x86_64-linux" "x86_64-darwin" "x86_64-freebsd13" "x86_64-genode" "x86_64-redox" "x86_64-openbsd" "x86_64-netbsd" "x86_64-cygwin" "x86_64-solaris" "x86_64-windows" "x86_64-none" ];
+  testx86_64 = mseteq x86_64 [ "x86_64-linux" "x86_64-darwin" "x86_64-freebsd" "x86_64-genode" "x86_64-redox" "x86_64-openbsd" "x86_64-netbsd" "x86_64-cygwin" "x86_64-solaris" "x86_64-windows" "x86_64-none" ];
 
   testcygwin = mseteq cygwin [ "i686-cygwin" "x86_64-cygwin" ];
   testdarwin = mseteq darwin [ "x86_64-darwin" "i686-darwin" "aarch64-darwin" "armv7a-darwin" ];
-  testfreebsd = mseteq freebsd [ "i686-freebsd13" "x86_64-freebsd13" ];
+  testfreebsd = mseteq freebsd [ "i686-freebsd" "x86_64-freebsd" ];
   testgenode = mseteq genode [ "aarch64-genode" "i686-genode" "x86_64-genode" ];
   testredox = mseteq redox [ "x86_64-redox" ];
   testgnu = mseteq gnu (linux /* ++ kfreebsd ++ ... */);
diff --git a/nixpkgs/lib/trivial.nix b/nixpkgs/lib/trivial.nix
index 936ad207c03d..5b7a1ee30f7a 100644
--- a/nixpkgs/lib/trivial.nix
+++ b/nixpkgs/lib/trivial.nix
@@ -200,6 +200,24 @@ in {
   and = x: y: x && y;
 
   /**
+    boolean “exclusive or”
+
+
+    # Inputs
+
+    `x`
+
+    : 1\. Function argument
+
+    `y`
+
+    : 2\. Function argument
+  */
+  # We explicitly invert the arguments purely as a type assertion.
+  # This is invariant under XOR, so it does not affect the result.
+  xor = x: y: (!x) != (!y);
+
+  /**
     bitwise “not”
   */
   bitNot = builtins.sub (-1);
diff --git a/nixpkgs/lib/types.nix b/nixpkgs/lib/types.nix
index 12bf18633e3a..518b987dcd92 100644
--- a/nixpkgs/lib/types.nix
+++ b/nixpkgs/lib/types.nix
@@ -15,6 +15,7 @@ let
     isList
     isString
     isStorePath
+    throwIf
     toDerivation
     toList
     ;
@@ -65,6 +66,11 @@ let
     fixupOptionType
     mergeOptionDecls
     ;
+
+  inAttrPosSuffix = v: name:
+    let pos = builtins.unsafeGetAttrPos name v; in
+    if pos == null then "" else " at ${pos.file}:${toString pos.line}:${toString pos.column}";
+
   outer_types =
 rec {
   __attrsFailEvaluation = true;
@@ -152,7 +158,7 @@ rec {
       # If it doesn't, this should be {}
       # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`.
       emptyValue ? {}
-    , # Return a flat list of sub-options.  Used to generate
+    , # Return a flat attrset of sub-options.  Used to generate
       # documentation.
       getSubOptions ? prefix: {}
     , # List of modules if any, or null if none.
@@ -328,15 +334,24 @@ rec {
           "signedInt${toString bit}" "${toString bit} bit signed integer";
 
       in {
-        /* An int with a fixed range.
-        *
-        * Example:
-        *   (ints.between 0 100).check (-1)
-        *   => false
-        *   (ints.between 0 100).check (101)
-        *   => false
-        *   (ints.between 0 0).check 0
-        *   => true
+        # TODO: Deduplicate with docs in nixos/doc/manual/development/option-types.section.md
+        /**
+          An int with a fixed range.
+
+          # Example
+          :::{.example}
+          ## `lib.types.ints.between` usage example
+
+          ```nix
+          (ints.between 0 100).check (-1)
+          => false
+          (ints.between 0 100).check (101)
+          => false
+          (ints.between 0 0).check 0
+          => true
+          ```
+
+          :::
         */
         inherit between;
 
@@ -614,6 +629,100 @@ rec {
       nestedTypes.elemType = elemType;
     };
 
+    attrTag = tags:
+      let tags_ = tags; in
+      let
+        tags =
+          mapAttrs
+            (n: opt:
+              builtins.addErrorContext "while checking that attrTag tag ${lib.strings.escapeNixIdentifier n} is an option with a type${inAttrPosSuffix tags_ n}" (
+                throwIf (opt._type or null != "option")
+                  "In attrTag, each tag value must be an option, but tag ${lib.strings.escapeNixIdentifier n} ${
+                    if opt?_type then
+                      if opt._type == "option-type"
+                      then "was a bare type, not wrapped in mkOption."
+                      else "was of type ${lib.strings.escapeNixString opt._type}."
+                    else "was not."}"
+                opt // {
+                  declarations = opt.declarations or (
+                    let pos = builtins.unsafeGetAttrPos n tags_;
+                    in if pos == null then [] else [ pos.file ]
+                  );
+                  declarationPositions = opt.declarationPositions or (
+                    let pos = builtins.unsafeGetAttrPos n tags_;
+                    in if pos == null then [] else [ pos ]
+                  );
+                }
+              ))
+            tags_;
+        choicesStr = concatMapStringsSep ", " lib.strings.escapeNixIdentifier (attrNames tags);
+      in
+      mkOptionType {
+        name = "attrTag";
+        description = "attribute-tagged union";
+        descriptionClass = "noun";
+        getSubOptions = prefix:
+          mapAttrs
+            (tagName: tagOption: {
+              "${lib.showOption prefix}" =
+                tagOption // {
+                  loc = prefix ++ [ tagName ];
+                };
+            })
+            tags;
+        check = v: isAttrs v && length (attrNames v) == 1 && tags?${head (attrNames v)};
+        merge = loc: defs:
+          let
+            choice = head (attrNames (head defs).value);
+            checkedValueDefs = map
+              (def:
+                assert (length (attrNames def.value)) == 1;
+                if (head (attrNames def.value)) != choice
+                then throw "The option `${showOption loc}` is defined both as `${choice}` and `${head (attrNames def.value)}`, in ${showFiles (getFiles defs)}."
+                else { inherit (def) file; value = def.value.${choice}; })
+              defs;
+          in
+            if tags?${choice}
+            then
+              { ${choice} =
+                  (lib.modules.evalOptionValue
+                    (loc ++ [choice])
+                    tags.${choice}
+                    checkedValueDefs
+                  ).value;
+              }
+            else throw "The option `${showOption loc}` is defined as ${lib.strings.escapeNixIdentifier choice}, but ${lib.strings.escapeNixIdentifier choice} is not among the valid choices (${choicesStr}). Value ${choice} was defined in ${showFiles (getFiles defs)}.";
+        nestedTypes = tags;
+        functor = defaultFunctor "attrTag" // {
+          type = { tags, ... }: types.attrTag tags;
+          payload = { inherit tags; };
+          binOp =
+            let
+              # Add metadata in the format that submodules work with
+              wrapOptionDecl =
+                option: { options = option; _file = "<attrTag {...}>"; pos = null; };
+            in
+            a: b: {
+              tags = a.tags // b.tags //
+                mapAttrs
+                  (tagName: bOpt:
+                    lib.mergeOptionDecls
+                      # FIXME: loc is not accurate; should include prefix
+                      #        Fortunately, it's only used for error messages, where a "relative" location is kinda ok.
+                      #        It is also returned though, but use of the attribute seems rare?
+                      [tagName]
+                      [ (wrapOptionDecl a.tags.${tagName}) (wrapOptionDecl bOpt) ]
+                    // {
+                      # mergeOptionDecls is not idempotent in these attrs:
+                      declarations = a.tags.${tagName}.declarations ++ bOpt.declarations;
+                      declarationPositions = a.tags.${tagName}.declarationPositions ++ bOpt.declarationPositions;
+                    }
+                  )
+                  (builtins.intersectAttrs a.tags b.tags);
+          };
+        };
+      };
+
     uniq = unique { message = ""; };
 
     unique = { message }: type: mkOptionType rec {