about summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/ascii-table.nix5
-rw-r--r--lib/attrsets.nix19
-rw-r--r--lib/customisation.nix12
-rw-r--r--lib/debug.nix66
-rw-r--r--lib/default.nix6
-rw-r--r--lib/fixed-points.nix4
-rw-r--r--lib/licenses.nix40
-rw-r--r--lib/lists.nix16
-rw-r--r--lib/meta.nix11
-rw-r--r--lib/options.nix82
-rw-r--r--lib/path/default.nix70
-rw-r--r--lib/path/tests/unit.nix40
-rw-r--r--lib/strings.nix32
-rw-r--r--lib/systems/architectures.nix19
-rw-r--r--lib/systems/doubles.nix3
-rw-r--r--lib/systems/inspect.nix23
-rw-r--r--lib/tests/maintainer-module.nix3
-rw-r--r--lib/tests/maintainers.nix9
-rw-r--r--lib/tests/misc.nix19
-rw-r--r--lib/tests/release.nix10
-rw-r--r--lib/tests/systems.nix3
21 files changed, 407 insertions, 85 deletions
diff --git a/lib/ascii-table.nix b/lib/ascii-table.nix
index c564e12bcc6ff..74989936ea402 100644
--- a/lib/ascii-table.nix
+++ b/lib/ascii-table.nix
@@ -1,4 +1,7 @@
-{ " "  = 32;
+{ "\t" =  9;
+  "\n" = 10;
+  "\r" = 13;
+  " "  = 32;
   "!"  = 33;
   "\"" = 34;
   "#"  = 35;
diff --git a/lib/attrsets.nix b/lib/attrsets.nix
index 1a7b90593b1d7..30952651adf40 100644
--- a/lib/attrsets.nix
+++ b/lib/attrsets.nix
@@ -168,7 +168,7 @@ rec {
        ] { a.b.c = 0; }
        => { a = { b = { d = 1; }; }; x = { y = "xy"; }; }
 
-    Type: updateManyAttrsByPath :: [{ path :: [String], update :: (Any -> Any) }] -> AttrSet -> AttrSet
+    Type: updateManyAttrsByPath :: [{ path :: [String]; update :: (Any -> Any); }] -> AttrSet -> AttrSet
   */
   updateManyAttrsByPath = let
     # When recursing into attributes, instead of updating the `path` of each
@@ -414,7 +414,7 @@ rec {
        => { name = "some"; value = 6; }
 
      Type:
-       nameValuePair :: String -> Any -> { name :: String, value :: Any }
+       nameValuePair :: String -> Any -> { name :: String; value :: Any; }
   */
   nameValuePair =
     # Attribute name
@@ -449,7 +449,7 @@ rec {
        => { foo_x = "bar-a"; foo_y = "bar-b"; }
 
      Type:
-       mapAttrs' :: (String -> Any -> { name = String; value = Any }) -> AttrSet -> AttrSet
+       mapAttrs' :: (String -> Any -> { name :: String; value :: Any; }) -> AttrSet -> AttrSet
   */
   mapAttrs' =
     # A function, given an attribute's name and value, returns a new `nameValuePair`.
@@ -480,8 +480,13 @@ rec {
 
 
   /* Like `mapAttrs`, except that it recursively applies itself to
-     attribute sets.  Also, the first argument of the argument
-     function is a *list* of the names of the containing attributes.
+     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.
+
+     For a function that gives you control over what counts as a leaf,
+     see `mapAttrsRecursiveCond`.
 
      Example:
        mapAttrsRecursive (path: value: concatStringsSep "-" (path ++ [value]))
@@ -644,7 +649,7 @@ rec {
 
      Example:
        zipAttrsWith (name: values: values) [{a = "x";} {a = "y"; b = "z";}]
-       => { a = ["x" "y"]; b = ["z"] }
+       => { a = ["x" "y"]; b = ["z"]; }
 
      Type:
        zipAttrsWith :: (String -> [ Any ] -> Any) -> [ AttrSet ] -> AttrSet
@@ -659,7 +664,7 @@ rec {
 
      Example:
        zipAttrs [{a = "x";} {a = "y"; b = "z";}]
-       => { a = ["x" "y"]; b = ["z"] }
+       => { a = ["x" "y"]; b = ["z"]; }
 
      Type:
        zipAttrs :: [ AttrSet ] -> AttrSet
diff --git a/lib/customisation.nix b/lib/customisation.nix
index 101c9e62b9e61..cb3a4b561151f 100644
--- a/lib/customisation.nix
+++ b/lib/customisation.nix
@@ -213,7 +213,14 @@ rec {
             outputSpecified = true;
             drvPath = assert condition; drv.${outputName}.drvPath;
             outPath = assert condition; drv.${outputName}.outPath;
-          };
+          } //
+            # TODO: give the derivation control over the outputs.
+            #       `overrideAttrs` may not be the only attribute that needs
+            #       updating when switching outputs.
+            lib.optionalAttrs (passthru?overrideAttrs) {
+              # TODO: also add overrideAttrs when overrideAttrs is not custom, e.g. when not splicing.
+              overrideAttrs = f: (passthru.overrideAttrs f).${outputName};
+            };
         };
 
       outputsList = map outputToAttrListElement outputs;
@@ -252,7 +259,8 @@ rec {
       outputsList = map makeOutput outputs;
 
       drv' = (lib.head outputsList).value;
-    in lib.deepSeq drv' drv';
+    in if drv == null then null else
+      lib.deepSeq drv' drv';
 
   /* Make a set of packages with a common scope. All packages called
      with the provided `callPackage` will be evaluated with the same
diff --git a/lib/debug.nix b/lib/debug.nix
index e3ca3352397ec..35ca4c7dfb202 100644
--- a/lib/debug.nix
+++ b/lib/debug.nix
@@ -109,6 +109,8 @@ rec {
        traceSeqN 2 { a.b.c = 3; } null
        trace: { a = { b = {…}; }; }
        => null
+
+     Type: traceSeqN :: Int -> a -> b -> b
    */
   traceSeqN = depth: x: y:
     let snip = v: if      isList  v then noQuotes "[…]" v
@@ -173,17 +175,63 @@ rec {
 
   # -- TESTING --
 
-  /* Evaluate a set of tests.  A test is an attribute set `{expr,
-     expected}`, denoting an expression and its expected result.  The
-     result is a list of failed tests, each represented as `{name,
-     expected, actual}`, denoting the attribute name of the failing
-     test and its expected and actual results.
+  /* Evaluates a set of tests.
 
-     Used for regression testing of the functions in lib; see
-     tests.nix for an example. Only tests having names starting with
-     "test" are run.
+     A test is an attribute set `{expr, expected}`,
+     denoting an expression and its expected result.
+
+     The result is a `list` of __failed tests__, each represented as
+     `{name, expected, result}`,
+
+     - expected
+       - What was passed as `expected`
+     - result
+       - The actual `result` of the test
 
-     Add attr { tests = ["testName"]; } to run these tests only.
+     Used for regression testing of the functions in lib; see
+     tests.nix for more examples.
+
+     Important: Only attributes that start with `test` are executed.
+
+     - If you want to run only a subset of the tests add the attribute `tests = ["testName"];`
+
+    Example:
+
+     runTests {
+       testAndOk = {
+         expr = lib.and true false;
+         expected = false;
+       };
+       testAndFail = {
+         expr = lib.and true false;
+         expected = true;
+       };
+     }
+     ->
+     [
+       {
+         name = "testAndFail";
+         expected = true;
+         result = false;
+       }
+     ]
+
+    Type:
+      runTests :: {
+        tests = [ String ];
+        ${testName} :: {
+          expr :: a;
+          expected :: a;
+        };
+      }
+      ->
+      [
+        {
+          name :: String;
+          expected :: a;
+          result :: a;
+        }
+      ]
   */
   runTests =
     # Tests to run
diff --git a/lib/default.nix b/lib/default.nix
index 8ce1de33f5dce..7948dbd5a1ef4 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -88,19 +88,19 @@ let
       updateManyAttrsByPath;
     inherit (self.lists) singleton forEach foldr fold foldl foldl' imap0 imap1
       concatMap flatten remove findSingle findFirst any all count
-      optional optionals toList range partition zipListsWith zipLists
+      optional optionals toList range replicate partition zipListsWith zipLists
       reverseList listDfs toposort sort naturalSort compareLists take
       drop sublist last init crossLists unique intersectLists
       subtractLists mutuallyExclusive groupBy groupBy';
     inherit (self.strings) concatStrings concatMapStrings concatImapStrings
       intersperse concatStringsSep concatMapStringsSep
-      concatImapStringsSep makeSearchPath makeSearchPathOutput
+      concatImapStringsSep concatLines makeSearchPath makeSearchPathOutput
       makeLibraryPath makeBinPath optionalString
       hasInfix hasPrefix hasSuffix stringToCharacters stringAsChars escape
       escapeShellArg escapeShellArgs
       isStorePath isStringLike
       isValidPosixName toShellVar toShellVars
-      escapeRegex escapeXML replaceChars lowerChars
+      escapeRegex escapeURL escapeXML replaceChars lowerChars
       upperChars toLower toUpper addContextFrom splitString
       removePrefix removeSuffix versionOlder versionAtLeast
       getName getVersion
diff --git a/lib/fixed-points.nix b/lib/fixed-points.nix
index bf1567a22a664..926428293c1c8 100644
--- a/lib/fixed-points.nix
+++ b/lib/fixed-points.nix
@@ -107,7 +107,7 @@ rec {
   # Same as `makeExtensible` but the name of the extending attribute is
   # customized.
   makeExtensibleWithCustomName = extenderName: rattrs:
-    fix' rattrs // {
+    fix' (self: (rattrs self) // {
       ${extenderName} = f: makeExtensibleWithCustomName extenderName (extends f rattrs);
-   };
+    });
 }
diff --git a/lib/licenses.nix b/lib/licenses.nix
index 6919859d6cb45..00f469b61a8e6 100644
--- a/lib/licenses.nix
+++ b/lib/licenses.nix
@@ -108,11 +108,26 @@ in mkLicense lset) ({
     fullName = "Apache License 2.0";
   };
 
+  asl20-llvm = {
+    spdxId = "Apache-2.0 WITH LLVM-exception";
+    fullName = "Apache License 2.0 with LLVM Exceptions";
+  };
+
   bitstreamVera = {
     spdxId = "Bitstream-Vera";
     fullName = "Bitstream Vera Font License";
   };
 
+  bitTorrent10 = {
+     spdxId = "BitTorrent-1.0";
+     fullName = " BitTorrent Open Source License v1.0";
+  };
+
+  bitTorrent11 = {
+    spdxId = "BitTorrent-1.1";
+    fullName = " BitTorrent Open Source License v1.1";
+  };
+
   bola11 = {
     url = "https://blitiri.com.ar/p/bola/";
     fullName = "Buena Onda License Agreement 1.1";
@@ -332,6 +347,13 @@ in mkLicense lset) ({
     free = false;
   };
 
+  ecl20 = {
+    fullName = "Educational Community License, Version 2.0";
+    url = "https://opensource.org/licenses/ECL-2.0";
+    shortName = "ECL 2.0";
+    spdxId = "ECL-2.0";
+  };
+
   efl10 = {
     spdxId = "EFL-1.0";
     fullName = "Eiffel Forum License v1.0";
@@ -557,6 +579,12 @@ in mkLicense lset) ({
     redistributable = false;
   };
 
+  fair = {
+    fullName = "Fair License";
+    spdxId = "Fair";
+    free = true;
+  };
+
   issl = {
     fullName = "Intel Simplified Software License";
     url = "https://software.intel.com/en-us/license/intel-simplified-software-license";
@@ -633,11 +661,6 @@ in mkLicense lset) ({
     url = "https://opensource.franz.com/preamble.html";
   };
 
-  llvm-exception = {
-    spdxId = "LLVM-exception";
-    fullName = "LLVM Exception"; # LLVM exceptions to the Apache 2.0 License
-  };
-
   lppl12 = {
     spdxId = "LPPL-1.2";
     fullName = "LaTeX Project Public License v1.2";
@@ -708,7 +731,12 @@ in mkLicense lset) ({
 
   ncsa = {
     spdxId = "NCSA";
-    fullName  = "University of Illinois/NCSA Open Source License";
+    fullName = "University of Illinois/NCSA Open Source License";
+  };
+
+  nlpl = {
+    spdxId = "NLPL";
+    fullName = "No Limit Public License";
   };
 
   nposl3 = {
diff --git a/lib/lists.nix b/lib/lists.nix
index 8b2c2d12801bb..2186cd4a79f60 100644
--- a/lib/lists.nix
+++ b/lib/lists.nix
@@ -303,10 +303,22 @@ rec {
     else
       genList (n: first + n) (last - first + 1);
 
+  /* Return a list with `n` copies of an element.
+
+    Type: replicate :: int -> a -> [a]
+
+    Example:
+      replicate 3 "a"
+      => [ "a" "a" "a" ]
+      replicate 2 true
+      => [ true true ]
+  */
+  replicate = n: elem: genList (_: elem) n;
+
   /* Splits the elements of a list in two lists, `right` and
      `wrong`, depending on the evaluation of a predicate.
 
-     Type: (a -> bool) -> [a] -> { right :: [a], wrong :: [a] }
+     Type: (a -> bool) -> [a] -> { right :: [a]; wrong :: [a]; }
 
      Example:
        partition (x: x > 2) [ 5 1 2 3 4 ]
@@ -374,7 +386,7 @@ rec {
   /* Merges two lists of the same size together. If the sizes aren't the same
      the merging stops at the shortest.
 
-     Type: zipLists :: [a] -> [b] -> [{ fst :: a, snd :: b}]
+     Type: zipLists :: [a] -> [b] -> [{ fst :: a; snd :: b; }]
 
      Example:
        zipLists [ 1 2 ] [ "a" "b" ]
diff --git a/lib/meta.nix b/lib/meta.nix
index 893c671b04fa8..5fd55c4e90d69 100644
--- a/lib/meta.nix
+++ b/lib/meta.nix
@@ -76,7 +76,9 @@ rec {
 
        1. (legacy) a system string.
 
-       2. (modern) a pattern for the platform `parsed` field.
+       2. (modern) a pattern for the entire platform structure (see `lib.systems.inspect.platformPatterns`).
+
+       3. (modern) a pattern for the platform `parsed` field (see `lib.systems.inspect.patterns`).
 
      We can inject these into a pattern for the whole of a structured platform,
      and then match that.
@@ -85,6 +87,8 @@ rec {
       pattern =
         if builtins.isString elem
         then { system = elem; }
+        else if elem?parsed
+        then elem
         else { parsed = elem; };
     in lib.matchAttrs pattern platform;
 
@@ -92,12 +96,13 @@ rec {
 
      A package is available on a platform if both
 
-       1. One of `meta.platforms` pattern matches the given platform.
+       1. One of `meta.platforms` pattern matches the given
+          platform, or `meta.platforms` is not present.
 
        2. None of `meta.badPlatforms` pattern matches the given platform.
   */
   availableOn = platform: pkg:
-    lib.any (platformMatch platform) pkg.meta.platforms &&
+    ((!pkg?meta.platforms) || lib.any (platformMatch platform) pkg.meta.platforms) &&
     lib.all (elem: !platformMatch platform elem) (pkg.meta.badPlatforms or []);
 
   /* Get the corresponding attribute in lib.licenses
diff --git a/lib/options.nix b/lib/options.nix
index ce66bfb9d5d9f..4780a56fc1c37 100644
--- a/lib/options.nix
+++ b/lib/options.nix
@@ -36,6 +36,12 @@ let
   inherit (lib.types)
     mkOptionType
     ;
+  inherit (lib.lists)
+    last
+    ;
+  prioritySuggestion = ''
+   Use `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions.
+  '';
 in
 rec {
 
@@ -104,17 +110,26 @@ rec {
   /* Creates an Option attribute set for an option that specifies the
      package a module should use for some purpose.
 
-     The package is specified as a list of strings representing its attribute path in nixpkgs.
+     The package is specified in the third argument under `default` as a list of strings
+     representing its attribute path in nixpkgs (or another package set).
+     Because of this, you need to pass nixpkgs itself (or a subset) as the first argument.
 
-     Because of this, you need to pass nixpkgs itself as the first argument.
+     The second argument may be either a string or a list of strings.
+     It provides the display name of the package in the description of the generated option
+     (using only the last element if the passed value is a list)
+     and serves as the fallback value for the `default` argument.
 
-     The second argument is the name of the option, used in the description "The <name> package to use.".
+     To include extra information in the description, pass `extraDescription` to
+     append arbitrary text to the generated description.
+     You can also pass an `example` value, either a literal string or an attribute path.
 
-     You can also pass an example value, either a literal string or a package's attribute path.
+     The default argument can be omitted if the provided name is
+     an attribute of pkgs (if name is a string) or a
+     valid attribute path in pkgs (if name is a list).
 
-     You can omit the default path if the name of the option is also attribute path in nixpkgs.
+     If you wish to explicitly provide no default, pass `null` as `default`.
 
-     Type: mkPackageOption :: pkgs -> string -> { default :: [string], example :: null | string | [string] } -> option
+     Type: mkPackageOption :: pkgs -> (string|[string]) -> { default? :: [string], example? :: null|string|[string], extraDescription? :: string } -> option
 
      Example:
        mkPackageOption pkgs "hello" { }
@@ -126,27 +141,46 @@ rec {
          example = "pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])";
        }
        => { _type = "option"; default = «derivation /nix/store/jxx55cxsjrf8kyh3fp2ya17q99w7541r-ghc-8.10.7.drv»; defaultText = { ... }; description = "The GHC package to use."; example = { ... }; type = { ... }; }
+
+     Example:
+       mkPackageOption pkgs [ "python39Packages" "pytorch" ] {
+         extraDescription = "This is an example and doesn't actually do anything.";
+       }
+       => { _type = "option"; default = «derivation /nix/store/gvqgsnc4fif9whvwd9ppa568yxbkmvk8-python3.9-pytorch-1.10.2.drv»; defaultText = { ... }; description = "The pytorch package to use. This is an example and doesn't actually do anything."; type = { ... }; }
+
   */
   mkPackageOption =
-    # Package set (a specific version of nixpkgs)
+    # Package set (a specific version of nixpkgs or a subset)
     pkgs:
       # Name for the package, shown in option description
       name:
-      { default ? [ name ], example ? null }:
-      let default' = if !isList default then [ default ] else default;
+      {
+        # The attribute path where the default package is located (may be omitted)
+        default ? name,
+        # A string or an attribute path to use as an example (may be omitted)
+        example ? null,
+        # Additional text to include in the option description (may be omitted)
+        extraDescription ? "",
+      }:
+      let
+        name' = if isList name then last name else name;
+        default' = if isList default then default else [ default ];
+        defaultPath = concatStringsSep "." default';
+        defaultValue = attrByPath default'
+          (throw "${defaultPath} cannot be found in pkgs") pkgs;
       in mkOption {
+        defaultText = literalExpression ("pkgs." + defaultPath);
         type = lib.types.package;
-        description = "The ${name} package to use.";
-        default = attrByPath default'
-          (throw "${concatStringsSep "." default'} cannot be found in pkgs") pkgs;
-        defaultText = literalExpression ("pkgs." + concatStringsSep "." default');
+        description = "The ${name'} package to use."
+          + (if extraDescription == "" then "" else " ") + extraDescription;
+        ${if default != null then "default" else null} = defaultValue;
         ${if example != null then "example" else null} = literalExpression
           (if isList example then "pkgs." + concatStringsSep "." example else example);
       };
 
   /* Like mkPackageOption, but emit an mdDoc description instead of DocBook. */
-  mkPackageOptionMD = args: name: extra:
-    let option = mkPackageOption args name extra;
+  mkPackageOptionMD = pkgs: name: extra:
+    let option = mkPackageOption pkgs name extra;
     in option // { description = lib.mdDoc option.description; };
 
   /* This option accepts anything, but it does not produce any result.
@@ -184,7 +218,7 @@ rec {
     if length defs == 1
     then (head defs).value
     else assert length defs > 1;
-      throw "The option `${showOption loc}' is defined multiple times.\n${message}\nDefinition values:${showDefs defs}";
+      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:
@@ -195,13 +229,13 @@ rec {
     else if length defs == 1 then (head defs).value
     else (foldl' (first: def:
       if def.value != first.value then
-        throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}"
+        throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}\n${prioritySuggestion}"
       else
         first) (head defs) (tail defs)).value;
 
   /* Extracts values of all "value" keys of the given list.
 
-     Type: getValues :: [ { value :: a } ] -> [a]
+     Type: getValues :: [ { value :: a; } ] -> [a]
 
      Example:
        getValues [ { value = 1; } { value = 2; } ] // => [ 1 2 ]
@@ -211,7 +245,7 @@ rec {
 
   /* Extracts values of all "file" keys of the given list
 
-     Type: getFiles :: [ { file :: a } ] -> [a]
+     Type: getFiles :: [ { file :: a; } ] -> [a]
 
      Example:
        getFiles [ { file = "file1"; } { file = "file2"; } ] // => [ "file1" "file2" ]
@@ -334,19 +368,17 @@ rec {
 
   # Helper functions.
 
-  /* Convert an option, described as a list of the option parts in to a
-     safe, human readable version.
+  /* Convert an option, described as a list of the option parts to a
+     human-readable version.
 
      Example:
        (showOption ["foo" "bar" "baz"]) == "foo.bar.baz"
-       (showOption ["foo" "bar.baz" "tux"]) == "foo.bar.baz.tux"
+       (showOption ["foo" "bar.baz" "tux"]) == "foo.\"bar.baz\".tux"
+       (showOption ["windowManager" "2bwm" "enable"]) == "windowManager.\"2bwm\".enable"
 
      Placeholders will not be quoted as they are not actual values:
        (showOption ["foo" "*" "bar"]) == "foo.*.bar"
        (showOption ["foo" "<name>" "bar"]) == "foo.<name>.bar"
-
-     Unlike attributes, options can also start with numbers:
-       (showOption ["windowManager" "2bwm" "enable"]) == "windowManager.2bwm.enable"
   */
   showOption = parts: let
     escapeOptionPart = part:
diff --git a/lib/path/default.nix b/lib/path/default.nix
index 96a9244407bf5..075e2fc0d1377 100644
--- a/lib/path/default.nix
+++ b/lib/path/default.nix
@@ -4,6 +4,7 @@ let
 
   inherit (builtins)
     isString
+    isPath
     split
     match
     ;
@@ -25,6 +26,10 @@ let
     assertMsg
     ;
 
+  inherit (lib.path.subpath)
+    isValid
+    ;
+
   # Return the reason why a subpath is invalid, or `null` if it's valid
   subpathInvalidReason = value:
     if ! isString value then
@@ -94,6 +99,52 @@ let
 
 in /* No rec! Add dependencies on this file at the top. */ {
 
+  /* Append a subpath string to a path.
+
+    Like `path + ("/" + string)` but safer, because it errors instead of returning potentially surprising results.
+    More specifically, it checks that the first argument is a [path value type](https://nixos.org/manual/nix/stable/language/values.html#type-path"),
+    and that the second argument is a valid subpath string (see `lib.path.subpath.isValid`).
+
+    Type:
+      append :: Path -> String -> Path
+
+    Example:
+      append /foo "bar/baz"
+      => /foo/bar/baz
+
+      # subpaths don't need to be normalised
+      append /foo "./bar//baz/./"
+      => /foo/bar/baz
+
+      # can append to root directory
+      append /. "foo/bar"
+      => /foo/bar
+
+      # first argument needs to be a path value type
+      append "/foo" "bar"
+      => <error>
+
+      # second argument needs to be a valid subpath string
+      append /foo /bar
+      => <error>
+      append /foo ""
+      => <error>
+      append /foo "/bar"
+      => <error>
+      append /foo "../bar"
+      => <error>
+  */
+  append =
+    # The absolute path to append to
+    path:
+    # The subpath string to append
+    subpath:
+    assert assertMsg (isPath path) ''
+      lib.path.append: The first argument is of type ${builtins.typeOf path}, but a path was expected'';
+    assert assertMsg (isValid subpath) ''
+      lib.path.append: Second argument is not a valid subpath string:
+          ${subpathInvalidReason subpath}'';
+    path + ("/" + subpath);
 
   /* Whether a value is a valid subpath string.
 
@@ -133,7 +184,9 @@ in /* No rec! Add dependencies on this file at the top. */ {
     subpath.isValid "./foo//bar/"
     => true
   */
-  subpath.isValid = value:
+  subpath.isValid =
+    # The value to check
+    value:
     subpathInvalidReason value == null;
 
 
@@ -150,11 +203,11 @@ in /* No rec! Add dependencies on this file at the top. */ {
 
   Laws:
 
-  - (Idempotency) Normalising multiple times gives the same result:
+  - Idempotency - normalising multiple times gives the same result:
 
         subpath.normalise (subpath.normalise p) == subpath.normalise p
 
-  - (Uniqueness) There's only a single normalisation for the paths that lead to the same file system node:
+  - Uniqueness - there's only a single normalisation for the paths that lead to the same file system node:
 
         subpath.normalise p != subpath.normalise q -> $(realpath ${p}) != $(realpath ${q})
 
@@ -210,9 +263,12 @@ in /* No rec! Add dependencies on this file at the top. */ {
     subpath.normalise "/foo"
     => <error>
   */
-  subpath.normalise = path:
-    assert assertMsg (subpathInvalidReason path == null)
-      "lib.path.subpath.normalise: Argument is not a valid subpath string: ${subpathInvalidReason path}";
-    joinRelPath (splitRelPath path);
+  subpath.normalise =
+    # The subpath string to normalise
+    subpath:
+    assert assertMsg (isValid subpath) ''
+      lib.path.subpath.normalise: Argument is not a valid subpath string:
+          ${subpathInvalidReason subpath}'';
+    joinRelPath (splitRelPath subpath);
 
 }
diff --git a/lib/path/tests/unit.nix b/lib/path/tests/unit.nix
index eccf3b7b1c33b..a1a45173a9098 100644
--- a/lib/path/tests/unit.nix
+++ b/lib/path/tests/unit.nix
@@ -3,9 +3,44 @@
 { libpath }:
 let
   lib = import libpath;
-  inherit (lib.path) subpath;
+  inherit (lib.path) append subpath;
 
   cases = lib.runTests {
+    # Test examples from the lib.path.append documentation
+    testAppendExample1 = {
+      expr = append /foo "bar/baz";
+      expected = /foo/bar/baz;
+    };
+    testAppendExample2 = {
+      expr = append /foo "./bar//baz/./";
+      expected = /foo/bar/baz;
+    };
+    testAppendExample3 = {
+      expr = append /. "foo/bar";
+      expected = /foo/bar;
+    };
+    testAppendExample4 = {
+      expr = (builtins.tryEval (append "/foo" "bar")).success;
+      expected = false;
+    };
+    testAppendExample5 = {
+      expr = (builtins.tryEval (append /foo /bar)).success;
+      expected = false;
+    };
+    testAppendExample6 = {
+      expr = (builtins.tryEval (append /foo "")).success;
+      expected = false;
+    };
+    testAppendExample7 = {
+      expr = (builtins.tryEval (append /foo "/bar")).success;
+      expected = false;
+    };
+    testAppendExample8 = {
+      expr = (builtins.tryEval (append /foo "../bar")).success;
+      expected = false;
+    };
+
+    # Test examples from the lib.path.subpath.isValid documentation
     testSubpathIsValidExample1 = {
       expr = subpath.isValid null;
       expected = false;
@@ -30,6 +65,7 @@ let
       expr = subpath.isValid "./foo//bar/";
       expected = true;
     };
+    # Some extra tests
     testSubpathIsValidTwoDotsEnd = {
       expr = subpath.isValid "foo/..";
       expected = false;
@@ -71,6 +107,7 @@ let
       expected = true;
     };
 
+    # Test examples from the lib.path.subpath.normalise documentation
     testSubpathNormaliseExample1 = {
       expr = subpath.normalise "foo//bar";
       expected = "./foo/bar";
@@ -107,6 +144,7 @@ let
       expr = (builtins.tryEval (subpath.normalise "/foo")).success;
       expected = false;
     };
+    # Some extra tests
     testSubpathNormaliseIsValidDots = {
       expr = subpath.normalise "./foo/.bar/.../baz...qux";
       expected = "./foo/.bar/.../baz...qux";
diff --git a/lib/strings.nix b/lib/strings.nix
index 2188fcb1dbfd1..3c3529c3285ee 100644
--- a/lib/strings.nix
+++ b/lib/strings.nix
@@ -4,6 +4,8 @@ let
 
 inherit (builtins) length;
 
+asciiTable = import ./ascii-table.nix;
+
 in
 
 rec {
@@ -128,6 +130,17 @@ rec {
     # List of input strings
     list: concatStringsSep sep (lib.imap1 f list);
 
+  /* Concatenate a list of strings, adding a newline at the end of each one.
+     Defined as `concatMapStrings (s: s + "\n")`.
+
+     Type: concatLines :: [string] -> string
+
+     Example:
+       concatLines [ "foo" "bar" ]
+       => "foo\nbar\n"
+  */
+  concatLines = concatMapStrings (s: s + "\n");
+
   /* Construct a Unix-style, colon-separated search path consisting of
      the given `subDir` appended to each of the given paths.
 
@@ -316,9 +329,7 @@ rec {
        => 40
 
   */
-  charToInt = let
-    table = import ./ascii-table.nix;
-  in c: builtins.getAttr c table;
+  charToInt = c: builtins.getAttr c asciiTable;
 
   /* Escape occurrence of the elements of `list` in `string` by
      prefixing it with a backslash.
@@ -344,6 +355,21 @@ rec {
   */
   escapeC = list: replaceStrings list (map (c: "\\x${ toLower (lib.toHexString (charToInt c))}") list);
 
+  /* Escape the string so it can be safely placed inside a URL
+     query.
+
+     Type: escapeURL :: string -> string
+
+     Example:
+       escapeURL "foo/bar baz"
+       => "foo%2Fbar%20baz"
+  */
+  escapeURL = let
+    unreserved = [ "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "-" "_" "." "~" ];
+    toEscape = builtins.removeAttrs asciiTable unreserved;
+  in
+    replaceStrings (builtins.attrNames toEscape) (lib.mapAttrsToList (_: c: "%${fixedWidthString 2 "0" (lib.toHexString c)}") toEscape);
+
   /* Quote string to be used safely within the Bourne shell.
 
      Type: escapeShellArg :: string -> string
diff --git a/lib/systems/architectures.nix b/lib/systems/architectures.nix
index 94127fa90b351..57b9184ca60cd 100644
--- a/lib/systems/architectures.nix
+++ b/lib/systems/architectures.nix
@@ -40,14 +40,21 @@ rec {
   # a superior CPU has all the features of an inferior and is able to build and test code for it
   inferiors = {
     # x86_64 Intel
+    # https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html
     default        = [ ];
     westmere       = [ ];
-    sandybridge    = [ "westmere"    ] ++ inferiors.westmere;
-    ivybridge      = [ "sandybridge" ] ++ inferiors.sandybridge;
-    haswell        = [ "ivybridge"   ] ++ inferiors.ivybridge;
-    broadwell      = [ "haswell"     ] ++ inferiors.haswell;
-    skylake        = [ "broadwell"   ] ++ inferiors.broadwell;
-    skylake-avx512 = [ "skylake"     ] ++ inferiors.skylake;
+    sandybridge    = [ "westmere"       ] ++ inferiors.westmere;
+    ivybridge      = [ "sandybridge"    ] ++ inferiors.sandybridge;
+    haswell        = [ "ivybridge"      ] ++ inferiors.ivybridge;
+    broadwell      = [ "haswell"        ] ++ inferiors.haswell;
+    skylake        = [ "broadwell"      ] ++ inferiors.broadwell;
+    skylake-avx512 = [ "skylake"        ] ++ inferiors.skylake;
+    cannonlake     = [ "skylake-avx512" ] ++ inferiors.skylake-avx512;
+    icelake-client = [ "cannonlake"     ] ++ inferiors.cannonlake;
+    icelake-server = [ "icelake-client" ] ++ inferiors.icelake-client;
+    cascadelake    = [ "skylake-avx512" ] ++ inferiors.cannonlake;
+    cooperlake     = [ "cascadelake"    ] ++ inferiors.cascadelake;
+    tigerlake      = [ "icelake-server" ] ++ inferiors.icelake-server;
 
     # x86_64 AMD
     # TODO: fill this (need testing)
diff --git a/lib/systems/doubles.nix b/lib/systems/doubles.nix
index 23a44d02e85e4..383dd30bfdb2f 100644
--- a/lib/systems/doubles.nix
+++ b/lib/systems/doubles.nix
@@ -68,6 +68,7 @@ in {
   none = [];
 
   arm           = filterDoubles predicates.isAarch32;
+  armv7         = filterDoubles predicates.isArmv7;
   aarch64       = filterDoubles predicates.isAarch64;
   x86           = filterDoubles predicates.isx86;
   i686          = filterDoubles predicates.isi686;
@@ -75,6 +76,7 @@ in {
   microblaze    = filterDoubles predicates.isMicroBlaze;
   mips          = filterDoubles predicates.isMips;
   mmix          = filterDoubles predicates.isMmix;
+  power         = filterDoubles predicates.isPower;
   riscv         = filterDoubles predicates.isRiscV;
   riscv32       = filterDoubles predicates.isRiscV32;
   riscv64       = filterDoubles predicates.isRiscV64;
@@ -83,6 +85,7 @@ in {
   or1k          = filterDoubles predicates.isOr1k;
   m68k          = filterDoubles predicates.isM68k;
   s390          = filterDoubles predicates.isS390;
+  s390x         = filterDoubles predicates.isS390x;
   js            = filterDoubles predicates.isJavaScript;
 
   bigEndian     = filterDoubles predicates.isBigEndian;
diff --git a/lib/systems/inspect.nix b/lib/systems/inspect.nix
index 53d84118bd30d..30615c9fde32c 100644
--- a/lib/systems/inspect.nix
+++ b/lib/systems/inspect.nix
@@ -7,6 +7,7 @@ let abis_ = abis; in
 let abis = lib.mapAttrs (_: abi: builtins.removeAttrs abi [ "assertions" ]) abis_; in
 
 rec {
+  # these patterns are to be matched against {host,build,target}Platform.parsed
   patterns = rec {
     isi686         = { cpu = cpuTypes.i686; };
     isx86_32       = { cpu = { family = "x86"; bits = 32; }; };
@@ -22,6 +23,9 @@ rec {
     ];
     isx86          = { cpu = { family = "x86"; }; };
     isAarch32      = { cpu = { family = "arm"; bits = 32; }; };
+    isArmv7        = map ({ arch, ... }: { cpu = { inherit arch; }; })
+                       (lib.filter (cpu: lib.hasPrefix "armv7" cpu.arch or "")
+                         (lib.attrValues cpuTypes));
     isAarch64      = { cpu = { family = "arm"; bits = 64; }; };
     isAarch        = { cpu = { family = "arm"; }; };
     isMicroBlaze   = { cpu = { family = "microblaze"; }; };
@@ -44,6 +48,7 @@ rec {
     isOr1k         = { cpu = { family = "or1k"; }; };
     isM68k         = { cpu = { family = "m68k"; }; };
     isS390         = { cpu = { family = "s390"; }; };
+    isS390x        = { cpu = { family = "s390"; bits = 64; }; };
     isJavaScript   = { cpu = cpuTypes.js; };
 
     is32bit        = { cpu = { bits = 32; }; };
@@ -77,8 +82,13 @@ rec {
     isMusl         = with abis; map (a: { abi = a; }) [ musl musleabi musleabihf muslabin32 muslabi64 ];
     isUClibc       = with abis; map (a: { abi = a; }) [ uclibc uclibceabi uclibceabihf ];
 
-    isEfi          = map (family: { cpu.family = family; })
-                       [ "x86" "arm" "aarch64" ];
+    isEfi = [
+      { cpu = { family = "arm"; version = "6"; }; }
+      { cpu = { family = "arm"; version = "7"; }; }
+      { cpu = { family = "arm"; version = "8"; }; }
+      { cpu = { family = "riscv"; }; }
+      { cpu = { family = "x86"; }; }
+    ];
   };
 
   matchAnyAttrs = patterns:
@@ -86,4 +96,13 @@ rec {
     else matchAttrs patterns;
 
   predicates = mapAttrs (_: matchAnyAttrs) patterns;
+
+  # these patterns are to be matched against the entire
+  # {host,build,target}Platform structure; they include a `parsed={}` marker so
+  # that `lib.meta.availableOn` can distinguish them from the patterns which
+  # apply only to the `parsed` field.
+
+  platformPatterns = mapAttrs (_: p: { parsed = {}; } // p) {
+    isStatic = { isStatic = true; };
+  };
 }
diff --git a/lib/tests/maintainer-module.nix b/lib/tests/maintainer-module.nix
index 8cf8411b476a4..afa12587a98d7 100644
--- a/lib/tests/maintainer-module.nix
+++ b/lib/tests/maintainer-module.nix
@@ -7,7 +7,8 @@ in {
       type = types.str;
     };
     email = lib.mkOption {
-      type = types.str;
+      type = types.nullOr types.str;
+      default = null;
     };
     matrix = lib.mkOption {
       type = types.nullOr types.str;
diff --git a/lib/tests/maintainers.nix b/lib/tests/maintainers.nix
index 8a9a2b26efaf7..be1c8aaa85c52 100644
--- a/lib/tests/maintainers.nix
+++ b/lib/tests/maintainers.nix
@@ -1,5 +1,6 @@
 # to run these tests (and the others)
 # nix-build nixpkgs/lib/tests/release.nix
+# These tests should stay in sync with the comment in maintainers/maintainers-list.nix
 { # The pkgs used for dependencies for the testing itself
   pkgs ? import ../.. {}
 , lib ? pkgs.lib
@@ -20,7 +21,7 @@ let
         ];
       }).config;
 
-      checkGithubId = lib.optional (checkedAttrs.github != null && checkedAttrs.githubId == null) ''
+      checks = lib.optional (checkedAttrs.github != null && checkedAttrs.githubId == null) ''
         echo ${lib.escapeShellArg (lib.showOption prefix)}': If `github` is specified, `githubId` must be too.'
         # Calling this too often would hit non-authenticated API limits, but this
         # shouldn't happen since such errors will get fixed rather quickly
@@ -28,8 +29,12 @@ let
         id=$(jq -r '.id' <<< "$info")
         echo "The GitHub ID for GitHub user ${checkedAttrs.github} is $id:"
         echo -e "    githubId = $id;\n"
+      '' ++ lib.optional (checkedAttrs.email == null && checkedAttrs.github == null && checkedAttrs.matrix == null) ''
+        echo ${lib.escapeShellArg (lib.showOption prefix)}': At least one of `email`, `github` or `matrix` must be specified, so that users know how to reach you.'
+      '' ++ lib.optional (checkedAttrs.email != null && lib.hasSuffix "noreply.github.com" checkedAttrs.email) ''
+        echo ${lib.escapeShellArg (lib.showOption prefix)}': If an email address is given, it should allow people to reach you. If you do not want that, you can just provide `github` or `matrix` instead.'
       '';
-    in lib.deepSeq checkedAttrs checkGithubId;
+    in lib.deepSeq checkedAttrs checks;
 
   missingGithubIds = lib.concatLists (lib.mapAttrsToList checkMaintainer lib.maintainers);
 
diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix
index faf2b96530c1c..07d04f5356c7b 100644
--- a/lib/tests/misc.nix
+++ b/lib/tests/misc.nix
@@ -153,6 +153,11 @@ runTests {
     expected = "a,b,c";
   };
 
+  testConcatLines = {
+    expr = concatLines ["a" "b" "c"];
+    expected = "a\nb\nc\n";
+  };
+
   testSplitStringsSimple = {
     expr = strings.splitString "." "a.b.c.d";
     expected = [ "a" "b" "c" "d" ];
@@ -342,6 +347,15 @@ runTests {
     expected = "Hello\\x20World";
   };
 
+  testEscapeURL = testAllTrue [
+    ("" == strings.escapeURL "")
+    ("Hello" == strings.escapeURL "Hello")
+    ("Hello%20World" == strings.escapeURL "Hello World")
+    ("Hello%2FWorld" == strings.escapeURL "Hello/World")
+    ("42%25" == strings.escapeURL "42%")
+    ("%20%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%09%3A%2F%40%24%27%28%29%2A%2C%3B" == strings.escapeURL " ?&=#+%!<>#\"{}|\\^[]`\t:/@$'()*,;")
+  ];
+
   testToInt = testAllTrue [
     # Naive
     (123 == toInt "123")
@@ -474,6 +488,11 @@ runTests {
     expected = [2 30 40 42];
   };
 
+  testReplicate = {
+    expr = replicate 3 "a";
+    expected = ["a" "a" "a"];
+  };
+
   testToIntShouldConvertStringToInt = {
     expr = toInt "27";
     expected = 27;
diff --git a/lib/tests/release.nix b/lib/tests/release.nix
index f67892ab962f2..dbf6683d49a85 100644
--- a/lib/tests/release.nix
+++ b/lib/tests/release.nix
@@ -1,11 +1,11 @@
 { # The pkgs used for dependencies for the testing itself
   # 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!"; }
+  pkgs ? import ../.. {} // { lib = throw "pkgs.lib accessed, but the lib tests should use nixpkgs' lib path directly!"; },
+  nix ? pkgs.nix,
 }:
 
 pkgs.runCommand "nixpkgs-lib-tests" {
   buildInputs = [
-    pkgs.nix
     (import ./check-eval.nix)
     (import ./maintainers.nix {
       inherit pkgs;
@@ -19,8 +19,12 @@ pkgs.runCommand "nixpkgs-lib-tests" {
       inherit pkgs;
     })
   ];
+  nativeBuildInputs = [
+    nix
+  ];
+  strictDeps = true;
 } ''
-    datadir="${pkgs.nix}/share"
+    datadir="${nix}/share"
     export TEST_ROOT=$(pwd)/test-tmp
     export NIX_BUILD_HOOK=
     export NIX_CONF_DIR=$TEST_ROOT/etc
diff --git a/lib/tests/systems.nix b/lib/tests/systems.nix
index 27c5ff565ca04..88e2e4206d56a 100644
--- a/lib/tests/systems.nix
+++ b/lib/tests/systems.nix
@@ -16,12 +16,15 @@ with lib.systems.doubles; lib.runTests {
   testall = mseteq all (linux ++ darwin ++ freebsd ++ openbsd ++ netbsd ++ illumos ++ wasi ++ windows ++ embedded ++ mmix ++ js ++ genode ++ redox);
 
   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" ];
   testmips = mseteq mips [ "mips64el-linux" "mipsel-linux" "mipsel-netbsd" ];
   testmmix = mseteq mmix [ "mmix-mmixware" ];
+  testpower = mseteq power [ "powerpc-netbsd" "powerpc-none" "powerpc64-linux" "powerpc64le-linux" "powerpcle-none" ];
   testriscv = mseteq riscv [ "riscv32-linux" "riscv64-linux" "riscv32-netbsd" "riscv64-netbsd" "riscv32-none" "riscv64-none" ];
   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" ];
 
   testcygwin = mseteq cygwin [ "i686-cygwin" "x86_64-cygwin" ];