summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/attrsets.nix23
-rw-r--r--lib/customisation.nix8
-rw-r--r--lib/debug.nix22
-rw-r--r--lib/default.nix11
-rw-r--r--lib/flake.nix5
-rw-r--r--lib/generators.nix24
-rw-r--r--lib/licenses.nix40
-rw-r--r--lib/lists.nix6
-rw-r--r--lib/meta.nix12
-rw-r--r--lib/modules.nix37
-rw-r--r--lib/sources.nix130
-rw-r--r--lib/strings.nix14
-rw-r--r--lib/systems/default.nix33
-rw-r--r--lib/systems/doubles.nix62
-rw-r--r--lib/systems/examples.nix52
-rw-r--r--lib/systems/inspect.nix2
-rw-r--r--lib/systems/parse.nix43
-rw-r--r--lib/systems/platforms.nix24
-rw-r--r--lib/tests/misc.nix67
-rwxr-xr-xlib/tests/modules.sh12
-rw-r--r--lib/tests/modules/declare-submoduleWith-modules.nix4
-rw-r--r--lib/tests/modules/functionTo/list-order.nix25
-rw-r--r--lib/tests/modules/functionTo/merging-attrs.nix27
-rw-r--r--lib/tests/modules/functionTo/merging-list.nix24
-rw-r--r--lib/tests/modules/functionTo/trivial.nix17
-rw-r--r--lib/tests/modules/functionTo/wrong-type.nix18
-rw-r--r--lib/tests/release.nix4
-rwxr-xr-xlib/tests/sources.sh59
-rw-r--r--lib/tests/systems.nix8
-rw-r--r--lib/trivial.nix7
-rw-r--r--lib/types.nix59
31 files changed, 762 insertions, 117 deletions
diff --git a/lib/attrsets.nix b/lib/attrsets.nix
index d8bc73ca874..0b61819f6b4 100644
--- a/lib/attrsets.nix
+++ b/lib/attrsets.nix
@@ -183,6 +183,24 @@ rec {
     else
       [];
 
+  /* Return the cartesian product of attribute set value combinations.
+
+    Example:
+      cartesianProductOfSets { a = [ 1 2 ]; b = [ 10 20 ]; }
+      => [
+           { a = 1; b = 10; }
+           { a = 1; b = 20; }
+           { a = 2; b = 10; }
+           { a = 2; b = 20; }
+         ]
+  */
+  cartesianProductOfSets = attrsOfLists:
+    lib.foldl' (listOfAttrs: attrName:
+      concatMap (attrs:
+        map (listValue: attrs // { ${attrName} = listValue; }) attrsOfLists.${attrName}
+      ) listOfAttrs
+    ) [{}] (attrNames attrsOfLists);
+
 
   /* Utility function that creates a {name, value} pair as expected by
      builtins.listToAttrs.
@@ -225,6 +243,10 @@ rec {
   /* Call a function for each attribute in the given set and return
      the result in a list.
 
+     Type:
+       mapAttrsToList ::
+         (String -> a -> b) -> AttrSet -> [b]
+
      Example:
        mapAttrsToList (name: value: name + value)
           { x = "a"; y = "b"; }
@@ -493,5 +515,4 @@ rec {
   zipWithNames = zipAttrsWithNames;
   zip = builtins.trace
     "lib.zip is deprecated, use lib.zipAttrsWith instead" zipAttrsWith;
-
 }
diff --git a/lib/customisation.nix b/lib/customisation.nix
index 37a7951896b..c17cb0d0f8e 100644
--- a/lib/customisation.nix
+++ b/lib/customisation.nix
@@ -219,16 +219,17 @@ rec {
 
   /* Like the above, but aims to support cross compilation. It's still ugly, but
      hopefully it helps a little bit. */
-  makeScopeWithSplicing = splicePackages: newScope: otherSplices: keep: f:
+  makeScopeWithSplicing = splicePackages: newScope: otherSplices: keep: extra: f:
     let
-      spliced = splicePackages {
+      spliced0 = splicePackages {
         pkgsBuildBuild = otherSplices.selfBuildBuild;
         pkgsBuildHost = otherSplices.selfBuildHost;
         pkgsBuildTarget = otherSplices.selfBuildTarget;
         pkgsHostHost = otherSplices.selfHostHost;
         pkgsHostTarget = self; # Not `otherSplices.selfHostTarget`;
         pkgsTargetTarget = otherSplices.selfTargetTarget;
-      } // keep self;
+      };
+      spliced = extra spliced0 // spliced0 // keep self;
       self = f self // {
         newScope = scope: newScope (spliced // scope);
         callPackage = newScope spliced; # == self.newScope {};
@@ -239,6 +240,7 @@ rec {
           newScope
           otherSplices
           keep
+          extra
           (lib.fixedPoints.extends g f);
         packages = f;
       };
diff --git a/lib/debug.nix b/lib/debug.nix
index ea6aed60ab4..e3ca3352397 100644
--- a/lib/debug.nix
+++ b/lib/debug.nix
@@ -148,6 +148,28 @@ rec {
   /* A combination of `traceVal` and `traceSeqN`. */
   traceValSeqN = traceValSeqNFn id;
 
+  /* Trace the input and output of a function `f` named `name`,
+  both down to `depth`.
+
+  This is useful for adding around a function call,
+  to see the before/after of values as they are transformed.
+
+     Example:
+       traceFnSeqN 2 "id" (x: x) { a.b.c = 3; }
+       trace: { fn = "id"; from = { a.b = {…}; }; to = { a.b = {…}; }; }
+       => { a.b.c = 3; }
+  */
+  traceFnSeqN = depth: name: f: v:
+    let res = f v;
+    in lib.traceSeqN
+        (depth + 1)
+        {
+          fn = name;
+          from = v;
+          to = res;
+        }
+        res;
+
 
   # -- TESTING --
 
diff --git a/lib/default.nix b/lib/default.nix
index f985266ed93..8e29ef5c420 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -66,8 +66,9 @@ let
       stringLength sub substring tail trace;
     inherit (self.trivial) id const pipe concat or and bitAnd bitOr bitXor
       bitNot boolToString mergeAttrs flip mapNullable inNixShell isFloat min max
-      importJSON importTOML warn info showWarnings nixpkgsVersion version mod compare
-      splitByAndCompare functionArgs setFunctionArgs isFunction toHexString toBaseDigits;
+      importJSON importTOML warn warnIf info showWarnings nixpkgsVersion version
+      mod compare splitByAndCompare functionArgs setFunctionArgs isFunction
+      toHexString toBaseDigits;
     inherit (self.fixedPoints) fix fix' converge extends composeExtensions
       composeManyExtensions makeExtensible makeExtensibleWithCustomName;
     inherit (self.attrsets) attrByPath hasAttrByPath setAttrByPath
@@ -78,7 +79,7 @@ let
       zipAttrsWithNames zipAttrsWith zipAttrs recursiveUpdateUntil
       recursiveUpdate matchAttrs overrideExisting getOutput getBin
       getLib getDev getMan chooseDevOutputs zipWithNames zip
-      recurseIntoAttrs dontRecurseIntoAttrs;
+      recurseIntoAttrs dontRecurseIntoAttrs cartesianProductOfSets;
     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
@@ -114,7 +115,7 @@ let
       mergeModules' mergeOptionDecls evalOptionValue mergeDefinitions
       pushDownProperties dischargeProperties filterOverrides
       sortProperties fixupOptionType mkIf mkAssert mkMerge mkOverride
-      mkOptionDefault mkDefault mkForce mkVMOverride mkStrict
+      mkOptionDefault mkDefault mkForce mkVMOverride
       mkFixStrictness mkOrder mkBefore mkAfter mkAliasDefinitions
       mkAliasAndWrapDefinitions fixMergeModules mkRemovedOptionModule
       mkRenamedOptionModule mkMergedOptionModule mkChangedOptionModule
@@ -130,7 +131,7 @@ let
       assertMsg assertOneOf;
     inherit (self.debug) addErrorContextToAttrs traceIf traceVal traceValFn
       traceXMLVal traceXMLValMarked traceSeq traceSeqN traceValSeq
-      traceValSeqFn traceValSeqN traceValSeqNFn traceShowVal
+      traceValSeqFn traceValSeqN traceValSeqNFn traceFnSeqN traceShowVal
       traceShowValMarked showVal traceCall traceCall2 traceCall3
       traceValIfNot runTests testAllTrue traceCallXml attrNamesToStr;
     inherit (self.misc) maybeEnv defaultMergeArg defaultMerge foldArgs
diff --git a/lib/flake.nix b/lib/flake.nix
new file mode 100644
index 00000000000..f05bd40960a
--- /dev/null
+++ b/lib/flake.nix
@@ -0,0 +1,5 @@
+{
+  description = "Library of low-level helper functions for nix expressions.";
+
+  outputs = { self }: { lib = import ./lib; };
+}
diff --git a/lib/generators.nix b/lib/generators.nix
index 501a23599f4..c8144db50ac 100644
--- a/lib/generators.nix
+++ b/lib/generators.nix
@@ -307,4 +307,28 @@ rec {
 ${expr "" v}
 </plist>'';
 
+  /* Translate a simple Nix expression to Dhall notation.
+   * Note that integers are translated to Integer and never
+   * the Natural type.
+  */
+  toDhall = { }@args: v:
+    with builtins;
+    let concatItems = lib.strings.concatStringsSep ", ";
+    in if isAttrs v then
+      "{ ${
+        concatItems (lib.attrsets.mapAttrsToList
+          (key: value: "${key} = ${toDhall args value}") v)
+      } }"
+    else if isList v then
+      "[ ${concatItems (map (toDhall args) v)} ]"
+    else if isInt v then
+      "${if v < 0 then "" else "+"}${toString v}"
+    else if isBool v then
+      (if v then "True" else "False")
+    else if isFunction v then
+      abort "generators.toDhall: cannot convert a function to Dhall"
+    else if isNull v then
+      abort "generators.toDhall: cannot convert a null to Dhall"
+    else
+      builtins.toJSON v;
 }
diff --git a/lib/licenses.nix b/lib/licenses.nix
index 190eeefc1bf..4792f1cb592 100644
--- a/lib/licenses.nix
+++ b/lib/licenses.nix
@@ -7,7 +7,7 @@ let
 
 in
 
-lib.mapAttrs (n: v: v // { shortName = n; }) {
+lib.mapAttrs (n: v: v // { shortName = n; }) ({
   /* License identifiers from spdx.org where possible.
    * If you cannot find your license here, then look for a similar license or
    * add it to this list. The URL mentioned above is a good source for inspiration.
@@ -100,6 +100,11 @@ lib.mapAttrs (n: v: v // { shortName = n; }) {
     fullName = "BSD Zero Clause License";
   };
 
+  bsd1 = spdx {
+    spdxId = "BSD-1-Clause";
+    fullName = "BSD 1-Clause License";
+  };
+
   bsd2 = spdx {
     spdxId = "BSD-2-Clause";
     fullName = ''BSD 2-clause "Simplified" License'';
@@ -120,6 +125,11 @@ lib.mapAttrs (n: v: v // { shortName = n; }) {
     fullName = ''BSD 4-clause "Original" or "Old" License'';
   };
 
+  bsdOriginalUC = spdx {
+    spdxId = "BSD-4-Clause-UC";
+    fullName = "BSD 4-Clause University of California-Specific";
+  };
+
   bsdProtection = spdx {
     spdxId = "BSD-Protection";
     fullName = "BSD Protection License";
@@ -301,6 +311,11 @@ lib.mapAttrs (n: v: v // { shortName = n; }) {
     fullName = "GNU Free Documentation License v1.1 only";
   };
 
+  fdl11Plus = spdx {
+    spdxId = "GFDL-1.1-or-later";
+    fullName = "GNU Free Documentation License v1.1 or later";
+  };
+
   fdl12Only = spdx {
     spdxId = "GFDL-1.2-only";
     fullName = "GNU Free Documentation License v1.2 only";
@@ -331,6 +346,11 @@ lib.mapAttrs (n: v: v // { shortName = n; }) {
     fullName = "Unspecified free software license";
   };
 
+  ftl = spdx {
+    spdxId = "FTL";
+    fullName = "Freetype Project License";
+  };
+
   g4sl = {
     fullName = "Geant4 Software License";
     url = "https://geant4.web.cern.ch/geant4/license/LICENSE.html";
@@ -593,6 +613,11 @@ lib.mapAttrs (n: v: v // { shortName = n; }) {
     free = false;
   };
 
+  odbl = spdx {
+    spdxId = "ODbL-1.0";
+    fullName = "Open Data Commons Open Database License v1.0";
+  };
+
   ofl = spdx {
     spdxId = "OFL-1.1";
     fullName = "SIL Open Font License 1.1";
@@ -714,6 +739,12 @@ lib.mapAttrs (n: v: v // { shortName = n; }) {
     free = false;
   };
 
+  stk = {
+    shortName = "stk";
+    fullName = "Synthesis Tool Kit 4.3";
+    url = "https://github.com/thestk/stk/blob/master/LICENSE";
+  };
+
   tcltk = spdx {
     spdxId = "TCL";
     fullName = "TCL/TK License";
@@ -740,6 +771,11 @@ lib.mapAttrs (n: v: v // { shortName = n; }) {
     # channel and NixOS images.
   };
 
+  unicode-dfs-2015 = spdx {
+    spdxId = "Unicode-DFS-2015";
+    fullName = "Unicode License Agreement - Data Files and Software (2015)";
+  };
+
   unicode-dfs-2016 = spdx {
     spdxId = "Unicode-DFS-2016";
     fullName = "Unicode License Agreement - Data Files and Software (2016)";
@@ -867,4 +903,4 @@ lib.mapAttrs (n: v: v // { shortName = n; }) {
     fullName = "GNU Lesser General Public License v3.0";
     deprecated = true;
   };
-}
+})
diff --git a/lib/lists.nix b/lib/lists.nix
index 06cee2eb112..e469f3ef265 100644
--- a/lib/lists.nix
+++ b/lib/lists.nix
@@ -241,7 +241,7 @@ rec {
 
   /* Return a singleton list or an empty list, depending on a boolean
      value.  Useful when building lists with optional elements
-     (e.g. `++ optional (system == "i686-linux") flashplayer').
+     (e.g. `++ optional (system == "i686-linux") firefox').
 
      Type: optional :: bool -> a -> [a]
 
@@ -629,7 +629,9 @@ rec {
       crossLists (x:y: "${toString x}${toString y}") [[1 2] [3 4]]
       => [ "13" "14" "23" "24" ]
   */
-  crossLists = f: foldl (fs: args: concatMap (f: map f args) fs) [f];
+  crossLists = builtins.trace
+    "lib.crossLists is deprecated, use lib.cartesianProductOfSets instead"
+    (f: foldl (fs: args: concatMap (f: map f args) fs) [f]);
 
 
   /* Remove duplicate elements from the list. O(n^2) complexity.
diff --git a/lib/meta.nix b/lib/meta.nix
index 2e83c4247dd..bc04394dcf0 100644
--- a/lib/meta.nix
+++ b/lib/meta.nix
@@ -87,4 +87,16 @@ rec {
         then { system = elem; }
         else { parsed = elem; };
     in lib.matchAttrs pattern platform;
+
+  /* Check if a package is available on a given platform.
+
+     A package is available on a platform if both
+
+       1. One of `meta.platforms` pattern matches the given platform.
+
+       2. None of `meta.badPlatforms` pattern matches the given platform.
+  */
+  availableOn = platform: pkg:
+    lib.any (platformMatch platform) pkg.meta.platforms &&
+    lib.all (elem: !platformMatch platform elem) (pkg.meta.badPlatforms or []);
 }
diff --git a/lib/modules.nix b/lib/modules.nix
index 33a0d84a6d7..ab2bc4f7f8e 100644
--- a/lib/modules.nix
+++ b/lib/modules.nix
@@ -23,6 +23,7 @@ let
     isAttrs
     isBool
     isFunction
+    isList
     isString
     length
     mapAttrs
@@ -37,7 +38,7 @@ let
     setAttrByPath
     toList
     types
-    warn
+    warnIf
     ;
   inherit (lib.options)
     isOption
@@ -127,7 +128,7 @@ rec {
         let collected = collectModules
           (specialArgs.modulesPath or "")
           (modules ++ [ internalModule ])
-          ({ inherit lib options config; } // specialArgs);
+          ({ inherit lib options config specialArgs; } // specialArgs);
         in mergeModules prefix (reverseList collected);
 
       options = merged.matchedOptions;
@@ -188,6 +189,9 @@ rec {
       loadModule = args: fallbackFile: fallbackKey: m:
         if isFunction m || isAttrs m then
           unifyModuleSyntax fallbackFile fallbackKey (applyIfFunction fallbackKey m args)
+        else if isList m then
+          let defs = [{ file = fallbackFile; value = m; }]; in
+          throw "Module imports can't be nested lists. Perhaps you meant to remove one level of lists? Definitions: ${showDefs defs}"
         else unifyModuleSyntax (toString m) (toString m) (applyIfFunction (toString m) (import m) args);
 
       /*
@@ -295,13 +299,11 @@ rec {
       # a module will resolve strictly the attributes used as argument but
       # not their values.  The values are forwarding the result of the
       # evaluation of the option.
-      requiredArgs = builtins.attrNames (lib.functionArgs f);
       context = name: ''while evaluating the module argument `${name}' in "${key}":'';
-      extraArgs = builtins.listToAttrs (map (name: {
-        inherit name;
-        value = builtins.addErrorContext (context name)
-          (args.${name} or config._module.args.${name});
-      }) requiredArgs);
+      extraArgs = builtins.mapAttrs (name: _:
+        builtins.addErrorContext (context name)
+          (args.${name} or config._module.args.${name})
+      ) (lib.functionArgs f);
 
       # Note: we append in the opposite order such that we can add an error
       # context on the explicited arguments of "args" too. This update
@@ -361,6 +363,17 @@ rec {
       */
       byName = attr: f: modules:
         foldl' (acc: module:
+              if !(builtins.isAttrs module.${attr}) then
+                throw ''
+                  You're trying to declare a value of type `${builtins.typeOf module.${attr}}'
+                  rather than an attribute-set for the option
+                  `${builtins.concatStringsSep "." prefix}'!
+
+                  This usually happens if `${builtins.concatStringsSep "." prefix}' has option
+                  definitions inside that are not matched. Please check how to properly define
+                  this option by e.g. referring to `man 5 configuration.nix'!
+                ''
+              else
                 acc // (mapAttrs (n: v:
                                    (acc.${n} or []) ++ f module v
                                  ) module.${attr}
@@ -505,8 +518,8 @@ rec {
       value = if opt ? apply then opt.apply res.mergedValue else res.mergedValue;
 
       warnDeprecation =
-        if opt.type.deprecationMessage == null then id
-        else warn "The type `types.${opt.type.name}' of option `${showOption loc}' defined in ${showFiles opt.declarations} is deprecated. ${opt.type.deprecationMessage}";
+        warnIf (opt.type.deprecationMessage != null)
+          "The type `types.${opt.type.name}' of option `${showOption loc}' defined in ${showFiles opt.declarations} is deprecated. ${opt.type.deprecationMessage}";
 
     in warnDeprecation opt //
       { value = builtins.addErrorContext "while evaluating the option `${showOption loc}':" value;
@@ -700,9 +713,7 @@ rec {
   mkForce = mkOverride 50;
   mkVMOverride = mkOverride 10; # used by ‘nixos-rebuild build-vm’
 
-  mkStrict = builtins.trace "`mkStrict' is obsolete; use `mkOverride 0' instead." (mkOverride 0);
-
-  mkFixStrictness = id; # obsolete, no-op
+  mkFixStrictness = lib.warn "lib.mkFixStrictness has no effect and will be removed. It returns its argument unmodified, so you can just remove any calls." id;
 
   mkOrder = priority: content:
     { _type = "order";
diff --git a/lib/sources.nix b/lib/sources.nix
index 1a3afcae67d..407829b547b 100644
--- a/lib/sources.nix
+++ b/lib/sources.nix
@@ -1,6 +1,7 @@
 # Functions for copying sources to the Nix store.
 { lib }:
 
+# Tested in lib/tests/sources.sh
 let
   inherit (builtins)
     hasContext
@@ -11,14 +12,13 @@ let
     tryEval
     ;
   inherit (lib)
+    boolToString
     filter
     getAttr
     isString
     pathExists
     readFile
     ;
-in
-rec {
 
   # Returns the type of a path: regular (for file), symlink, or directory
   pathType = p: getAttr (baseNameOf p) (readDir (dirOf p));
@@ -84,18 +84,36 @@ rec {
   #
   cleanSourceWith = { filter ? _path: _type: true, src, name ? null }:
     let
-      isFiltered = src ? _isLibCleanSourceWith;
-      origSrc = if isFiltered then src.origSrc else src;
-      filter' = if isFiltered then name: type: filter name type && src.filter name type else filter;
-      name' = if name != null then name else if isFiltered then src.name else "source";
-    in {
-      inherit origSrc;
-      filter = filter';
-      outPath = builtins.path { filter = filter'; path = origSrc; name = name'; };
-      _isLibCleanSourceWith = true;
-      name = name';
+      orig = toSourceAttributes src;
+    in fromSourceAttributes {
+      inherit (orig) origSrc;
+      filter = path: type: filter path type && orig.filter path type;
+      name = if name != null then name else orig.name;
     };
 
+  /*
+    Add logging to a source, for troubleshooting the filtering behavior.
+    Type:
+      sources.trace :: sourceLike -> Source
+  */
+  trace =
+    # Source to debug. The returned source will behave like this source, but also log its filter invocations.
+    src:
+    let
+      attrs = toSourceAttributes src;
+    in
+      fromSourceAttributes (
+        attrs // {
+          filter = path: type:
+            let
+              r = attrs.filter path type;
+            in
+              builtins.trace "${attrs.name}.filter ${path} = ${boolToString r}" r;
+        }
+      ) // {
+        satisfiesSubpathInvariant = src ? satisfiesSubpathInvariant && src.satisfiesSubpathInvariant;
+      };
+
   # Filter sources by a list of regular expressions.
   #
   # E.g. `src = sourceByRegex ./my-subproject [".*\.py$" "^database.sql$"]`
@@ -110,14 +128,26 @@ rec {
       inherit src;
     };
 
-  # Get all files ending with the specified suffices from the given
-  # directory or its descendants.  E.g. `sourceFilesBySuffices ./dir
-  # [".xml" ".c"]'.
-  sourceFilesBySuffices = path: exts:
+  /*
+    Get all files ending with the specified suffices from the given
+    source directory or its descendants, omitting files that do not match
+    any suffix. The result of the example below will include files like
+    `./dir/module.c` and `./dir/subdir/doc.xml` if present.
+
+    Type: sourceLike -> [String] -> Source
+
+    Example:
+      sourceFilesBySuffices ./. [ ".xml" ".c" ]
+  */
+  sourceFilesBySuffices =
+    # Path or source containing the files to be returned
+    src:
+    # A list of file suffix strings
+    exts:
     let filter = name: type:
       let base = baseNameOf (toString name);
       in type == "directory" || lib.any (ext: lib.hasSuffix ext base) exts;
-    in cleanSourceWith { inherit filter; src = path; };
+    in cleanSourceWith { inherit filter src; };
 
   pathIsGitRepo = path: (tryEval (commitIdFromGitRepo path)).success;
 
@@ -138,12 +168,13 @@ rec {
              in if m == null
                 then throw ("File contains no gitdir reference: " + path)
                 else
-                  let gitDir     = absolutePath (dirOf path) (lib.head m);
-                      commonDir' = if pathIsRegularFile "${gitDir}/commondir"
-                                   then lib.fileContents "${gitDir}/commondir"
-                                   else gitDir;
-                      commonDir  = absolutePath gitDir commonDir';
-                      refFile    = lib.removePrefix "${commonDir}/" "${gitDir}/${file}";
+                  let gitDir      = absolutePath (dirOf path) (lib.head m);
+                      commonDir'' = if pathIsRegularFile "${gitDir}/commondir"
+                                    then lib.fileContents "${gitDir}/commondir"
+                                    else gitDir;
+                      commonDir'  = lib.removeSuffix "/" commonDir'';
+                      commonDir   = absolutePath gitDir commonDir';
+                      refFile     = lib.removePrefix "${commonDir}/" "${gitDir}/${file}";
                   in readCommitFromFile refFile commonDir
 
            else if pathIsRegularFile fileName
@@ -176,4 +207,57 @@ rec {
   pathHasContext = builtins.hasContext or (lib.hasPrefix storeDir);
 
   canCleanSource = src: src ? _isLibCleanSourceWith || !(pathHasContext (toString src));
+
+  # -------------------------------------------------------------------------- #
+  # Internal functions
+  #
+
+  # toSourceAttributes : sourceLike -> SourceAttrs
+  #
+  # Convert any source-like object into a simple, singular representation.
+  # We don't expose this representation in order to avoid having a fifth path-
+  # like class of objects in the wild.
+  # (Existing ones being: paths, strings, sources and x//{outPath})
+  # So instead of exposing internals, we build a library of combinator functions.
+  toSourceAttributes = src:
+    let
+      isFiltered = src ? _isLibCleanSourceWith;
+    in
+    {
+      # The original path
+      origSrc = if isFiltered then src.origSrc else src;
+      filter = if isFiltered then src.filter else _: _: true;
+      name = if isFiltered then src.name else "source";
+    };
+
+  # fromSourceAttributes : SourceAttrs -> Source
+  #
+  # Inverse of toSourceAttributes for Source objects.
+  fromSourceAttributes = { origSrc, filter, name }:
+    {
+      _isLibCleanSourceWith = true;
+      inherit origSrc filter name;
+      outPath = builtins.path { inherit filter name; path = origSrc; };
+    };
+
+in {
+  inherit
+    pathType
+    pathIsDirectory
+    pathIsRegularFile
+
+    pathIsGitRepo
+    commitIdFromGitRepo
+
+    cleanSource
+    cleanSourceWith
+    cleanSourceFilter
+    pathHasContext
+    canCleanSource
+
+    sourceByRegex
+    sourceFilesBySuffices
+
+    trace
+    ;
 }
diff --git a/lib/strings.nix b/lib/strings.nix
index 5010d9159cb..86c92bdaa15 100644
--- a/lib/strings.nix
+++ b/lib/strings.nix
@@ -95,7 +95,7 @@ rec {
      result with the specified separator interspersed between
      elements.
 
-     Type: concatMapStringsSep :: string -> (string -> string) -> [string] -> string
+     Type: concatMapStringsSep :: string -> (a -> string) -> [a] -> string
 
      Example:
         concatMapStringsSep "-" (x: toUpper x)  ["foo" "bar" "baz"]
@@ -112,7 +112,7 @@ rec {
   /* Same as `concatMapStringsSep`, but the mapping function
      additionally receives the position of its argument.
 
-     Type: concatIMapStringsSep :: string -> (int -> string -> string) -> [string] -> string
+     Type: concatIMapStringsSep :: string -> (int -> a -> string) -> [a] -> string
 
      Example:
        concatImapStringsSep "-" (pos: x: toString (x / pos)) [ 6 6 6 ]
@@ -606,7 +606,7 @@ rec {
      This function will fail if the input string is longer than the
      requested length.
 
-     Type: fixedWidthString :: int -> string -> string
+     Type: fixedWidthString :: int -> string -> string -> string
 
      Example:
        fixedWidthString 5 "0" (toString 15)
@@ -644,8 +644,8 @@ rec {
   floatToString = float: let
     result = toString float;
     precise = float == fromJSON result;
-  in if precise then result
-    else lib.warn "Imprecise conversion from float to string ${result}" result;
+  in lib.warnIf (!precise) "Imprecise conversion from float to string ${result}"
+    result;
 
   /* Check whether a value can be coerced to a string */
   isCoercibleToString = x:
@@ -659,7 +659,7 @@ rec {
      Example:
        isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11/bin/python"
        => false
-       isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11/"
+       isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11"
        => true
        isStorePath pkgs.python
        => true
@@ -667,7 +667,7 @@ rec {
        => false
   */
   isStorePath = x:
-    if isCoercibleToString x then
+    if !(isList x) && isCoercibleToString x then
       let str = toString x; in
       substring 0 1 str == "/"
       && dirOf str == storeDir
diff --git a/lib/systems/default.nix b/lib/systems/default.nix
index 1a89120e2bf..70ec98b03c1 100644
--- a/lib/systems/default.nix
+++ b/lib/systems/default.nix
@@ -41,6 +41,19 @@ rec {
         else if final.isNetBSD              then "nblibc"
         # TODO(@Ericson2314) think more about other operating systems
         else                                     "native/impure";
+      # Choose what linker we wish to use by default. Someday we might also
+      # choose the C compiler, runtime library, C++ standard library, etc. in
+      # this way, nice and orthogonally, and deprecate `useLLVM`. But due to
+      # the monolithic GCC build we cannot actually make those choices
+      # independently, so we are just doing `linker` and keeping `useLLVM` for
+      # now.
+      linker =
+        /**/ if final.useLLVM or false      then "lld"
+        else if final.isDarwin              then "cctools"
+        # "bfd" and "gold" both come from GNU binutils. The existance of Gold
+        # is why we use the more obscure "bfd" and not "binutils" for this
+        # choice.
+        else                                     "bfd";
       extensions = {
         sharedLibrary =
           /**/ if final.isDarwin  then ".dylib"
@@ -92,6 +105,8 @@ rec {
         else if final.isx86_32 then "i386"
         else if final.isx86_64 then "x86_64"
         else if final.isMips then "mips"
+        else if final.isPower then "powerpc"
+        else if final.isRiscV then "riscv"
         else final.parsed.cpu.name;
 
       qemuArch =
@@ -105,6 +120,24 @@ rec {
           powerpc64le = "ppc64le";
         }.${final.parsed.cpu.name} or final.parsed.cpu.name;
 
+      darwinArch = {
+        armv7a  = "armv7";
+        aarch64 = "arm64";
+      }.${final.parsed.cpu.name} or final.parsed.cpu.name;
+
+      darwinPlatform =
+        if final.isMacOS then "macos"
+        else if final.isiOS then "ios"
+        else null;
+      # The canonical name for this attribute is darwinSdkVersion, but some
+      # platforms define the old name "sdkVer".
+      darwinSdkVersion = final.sdkVer or (if final.isAarch64 then "11.0" else "10.12");
+      darwinMinVersion = final.darwinSdkVersion;
+      darwinMinVersionVariable =
+        if final.isMacOS then "MACOSX_DEPLOYMENT_TARGET"
+        else if final.isiOS then "IPHONEOS_DEPLOYMENT_TARGET"
+        else null;
+
       emulator = pkgs: let
         qemu-user = pkgs.qemu.override {
           smartcardSupport = false;
diff --git a/lib/systems/doubles.nix b/lib/systems/doubles.nix
index b0bc7dd1188..aa630b51ed8 100644
--- a/lib/systems/doubles.nix
+++ b/lib/systems/doubles.nix
@@ -6,42 +6,54 @@ let
   inherit (lib.attrsets) matchAttrs;
 
   all = [
-    "aarch64-linux"
-    "armv5tel-linux" "armv6l-linux" "armv7a-linux" "armv7l-linux"
+    # Cygwin
+    "i686-cygwin" "x86_64-cygwin"
 
-    "mipsel-linux"
+    # Darwin
+    "x86_64-darwin" "i686-darwin" "aarch64-darwin" "armv7a-darwin"
 
-    "i686-cygwin" "i686-freebsd" "i686-linux" "i686-netbsd" "i686-openbsd"
+    # FreeBSD
+    "i686-freebsd" "x86_64-freebsd"
 
-    "x86_64-cygwin" "x86_64-freebsd" "x86_64-linux"
-    "x86_64-netbsd" "x86_64-openbsd" "x86_64-solaris"
+    # Genode
+    "aarch64-genode" "i686-genode" "x86_64-genode"
 
-    "x86_64-darwin" "i686-darwin" "aarch64-darwin" "armv7a-darwin"
+    # illumos
+    "x86_64-solaris"
 
-    "x86_64-windows" "i686-windows"
+    # JS
+    "js-ghcjs"
 
-    "wasm64-wasi" "wasm32-wasi"
+    # Linux
+    "aarch64-linux" "armv5tel-linux" "armv6l-linux" "armv7a-linux"
+    "armv7l-linux" "i686-linux" "mipsel-linux" "powerpc64-linux"
+    "powerpc64le-linux" "riscv32-linux" "riscv64-linux" "x86_64-linux"
+    "m68k-linux" "s390-linux"
 
-    "x86_64-redox"
+    # MMIXware
+    "mmix-mmixware"
 
-    "powerpc64le-linux"
+    # NetBSD
+    "aarch64-netbsd" "armv6l-netbsd" "armv7a-netbsd" "armv7l-netbsd"
+    "i686-netbsd" "mipsel-netbsd" "powerpc-netbsd" "riscv32-netbsd"
+    "riscv64-netbsd" "x86_64-netbsd"
 
-    "riscv32-linux" "riscv64-linux"
+    # none
+    "aarch64-none" "arm-none" "armv6l-none" "avr-none" "i686-none" "msp430-none"
+    "or1k-none" "powerpc-none" "riscv32-none" "riscv64-none" "vc4-none" "m68k-none"
+    "s390-none" "x86_64-none"
 
-    "arm-none" "armv6l-none" "aarch64-none"
-    "avr-none"
-    "i686-none" "x86_64-none"
-    "powerpc-none"
-    "msp430-none"
-    "riscv64-none" "riscv32-none"
-    "vc4-none"
-    "or1k-none"
+    # OpenBSD
+    "i686-openbsd" "x86_64-openbsd"
 
-    "mmix-mmixware"
+    # Redox
+    "x86_64-redox"
 
-    "js-ghcjs"
+    # WASI
+    "wasm64-wasi" "wasm32-wasi"
 
-    "aarch64-genode" "i686-genode" "x86_64-genode"
+    # Windows
+    "x86_64-windows" "i686-windows"
   ];
 
   allParsed = map parse.mkSystemFromString all;
@@ -63,6 +75,8 @@ in {
   riscv         = filterDoubles predicates.isRiscV;
   vc4           = filterDoubles predicates.isVc4;
   or1k          = filterDoubles predicates.isOr1k;
+  m68k          = filterDoubles predicates.isM68k;
+  s390          = filterDoubles predicates.isS390;
   js            = filterDoubles predicates.isJavaScript;
 
   bigEndian     = filterDoubles predicates.isBigEndian;
@@ -85,5 +99,5 @@ in {
 
   embedded      = filterDoubles predicates.isNone;
 
-  mesaPlatforms = ["i686-linux" "x86_64-linux" "x86_64-darwin" "armv5tel-linux" "armv6l-linux" "armv7l-linux" "armv7a-linux" "aarch64-linux" "powerpc64le-linux"];
+  mesaPlatforms = ["i686-linux" "x86_64-linux" "x86_64-darwin" "armv5tel-linux" "armv6l-linux" "armv7l-linux" "armv7a-linux" "aarch64-linux" "powerpc64-linux" "powerpc64le-linux" "aarch64-darwin" "riscv64-linux"];
 }
diff --git a/lib/systems/examples.nix b/lib/systems/examples.nix
index e8cf15479c0..32b236d6960 100644
--- a/lib/systems/examples.nix
+++ b/lib/systems/examples.nix
@@ -21,6 +21,15 @@ rec {
     config = "powerpc64le-unknown-linux-musl";
   };
 
+  ppc64 = {
+    config = "powerpc64-unknown-linux-gnu";
+    gcc = { abi = "elfv2"; }; # for gcc configuration
+  };
+  ppc64-musl = {
+    config = "powerpc64-unknown-linux-musl";
+    gcc = { abi = "elfv2"; }; # for gcc configuration
+  };
+
   sheevaplug = {
     config = "armv5tel-unknown-linux-gnueabi";
   } // platforms.sheevaplug;
@@ -47,6 +56,7 @@ rec {
 
   armv7a-android-prebuilt = {
     config = "armv7a-unknown-linux-androideabi";
+    rustc.config = "armv7-linux-androideabi";
     sdkVer = "29";
     ndkVer = "21";
     useAndroidPrebuilt = true;
@@ -54,11 +64,21 @@ rec {
 
   aarch64-android-prebuilt = {
     config = "aarch64-unknown-linux-android";
+    rustc.config = "aarch64-linux-android";
     sdkVer = "29";
     ndkVer = "21";
     useAndroidPrebuilt = true;
   };
 
+  aarch64-android = {
+    config = "aarch64-unknown-linux-android";
+    sdkVer = "30";
+    ndkVer = "21";
+    libc = "bionic";
+    useAndroidPrebuilt = false;
+    useLLVM = true;
+  };
+
   scaleway-c1 = armv7l-hf-multiplatform // platforms.scaleway-c1;
 
   pogoplug4 = {
@@ -124,6 +144,14 @@ rec {
     libc = "newlib";
   };
 
+  m68k = {
+    config = "m68k-unknown-linux-gnu";
+  };
+
+  s390 = {
+    config = "s390-unknown-linux-gnu";
+  };
+
   arm-embedded = {
     config = "arm-none-eabi";
     libc = "newlib";
@@ -131,6 +159,12 @@ rec {
   armhf-embedded = {
     config = "arm-none-eabihf";
     libc = "newlib";
+    # GCC8+ does not build without this
+    # (https://www.mail-archive.com/gcc-bugs@gcc.gnu.org/msg552339.html):
+    gcc = {
+      arch = "armv5t";
+      fpu = "vfp";
+    };
   };
 
   aarch64-embedded = {
@@ -200,6 +234,7 @@ rec {
     sdkVer = "14.3";
     xcodeVer = "12.3";
     xcodePlatform = "iPhoneSimulator";
+    darwinPlatform = "ios-simulator";
     useiOSPrebuilt = true;
   };
 
@@ -209,9 +244,16 @@ rec {
     sdkVer = "14.3";
     xcodeVer = "12.3";
     xcodePlatform = "iPhoneSimulator";
+    darwinPlatform = "ios-simulator";
     useiOSPrebuilt = true;
   };
 
+  aarch64-darwin = {
+    config = "aarch64-apple-darwin";
+    xcodePlatform = "MacOSX";
+    platform = {};
+  };
+
   #
   # Windows
   #
@@ -231,11 +273,19 @@ rec {
 
   # BSDs
 
-  amd64-netbsd = {
+  amd64-netbsd = lib.warn "The amd64-netbsd system example is deprecated. Use x86_64-netbsd instead." x86_64-netbsd;
+
+  x86_64-netbsd = {
     config = "x86_64-unknown-netbsd";
     libc = "nblibc";
   };
 
+  x86_64-netbsd-llvm = {
+    config = "x86_64-unknown-netbsd";
+    libc = "nblibc";
+    useLLVM = true;
+  };
+
   #
   # WASM
   #
diff --git a/lib/systems/inspect.nix b/lib/systems/inspect.nix
index d2b7271210c..2fba95aa1a6 100644
--- a/lib/systems/inspect.nix
+++ b/lib/systems/inspect.nix
@@ -26,6 +26,8 @@ rec {
     isAvr          = { cpu = { family = "avr"; }; };
     isAlpha        = { cpu = { family = "alpha"; }; };
     isOr1k         = { cpu = { family = "or1k"; }; };
+    isM68k         = { cpu = { family = "m68k"; }; };
+    isS390         = { cpu = { family = "s390"; }; };
     isJavaScript   = { cpu = cpuTypes.js; };
 
     is32bit        = { cpu = { bits = 32; }; };
diff --git a/lib/systems/parse.nix b/lib/systems/parse.nix
index a06ac0d11f7..386f252f2ba 100644
--- a/lib/systems/parse.nix
+++ b/lib/systems/parse.nix
@@ -95,6 +95,8 @@ rec {
 
     mmix     = { bits = 64; significantByte = bigEndian;    family = "mmix"; };
 
+    m68k     = { bits = 32; significantByte = bigEndian; family = "m68k"; };
+
     powerpc  = { bits = 32; significantByte = bigEndian;    family = "power"; };
     powerpc64 = { bits = 64; significantByte = bigEndian; family = "power"; };
     powerpc64le = { bits = 64; significantByte = littleEndian; family = "power"; };
@@ -103,6 +105,8 @@ rec {
     riscv32  = { bits = 32; significantByte = littleEndian; family = "riscv"; };
     riscv64  = { bits = 64; significantByte = littleEndian; family = "riscv"; };
 
+    s390     = { bits = 32; significantByte = bigEndian; family = "s390"; };
+
     sparc    = { bits = 32; significantByte = bigEndian;    family = "sparc"; };
     sparc64  = { bits = 64; significantByte = bigEndian;    family = "sparc"; };
 
@@ -121,15 +125,28 @@ rec {
     js       = { bits = 32; significantByte = littleEndian; family = "js"; };
   };
 
-  # Determine where two CPUs are compatible with each other. That is,
-  # can we run code built for system b on system a? For that to
-  # happen, then the set of all possible possible programs that system
-  # b accepts must be a subset of the set of all programs that system
-  # a accepts. This compatibility relation forms a category where each
-  # CPU is an object and each arrow from a to b represents
-  # compatibility. CPUs with multiple modes of Endianness are
-  # isomorphic while all CPUs are endomorphic because any program
-  # built for a CPU can run on that CPU.
+  # GNU build systems assume that older NetBSD architectures are using a.out.
+  gnuNetBSDDefaultExecFormat = cpu:
+    if (cpu.family == "x86" && cpu.bits == 32) ||
+       (cpu.family == "arm" && cpu.bits == 32) ||
+       (cpu.family == "sparc" && cpu.bits == 32)
+    then execFormats.aout
+    else execFormats.elf;
+
+  # Determine when two CPUs are compatible with each other. That is,
+  # can code built for system B run on system A? For that to happen,
+  # the programs that system B accepts must be a subset of the
+  # programs that system A accepts.
+  #
+  # We have the following properties of the compatibility relation,
+  # which must be preserved when adding compatibility information for
+  # additional CPUs.
+  # - (reflexivity)
+  #   Every CPU is compatible with itself.
+  # - (transitivity)
+  #   If A is compatible with B and B is compatible with C then A is compatible with C.
+  # - (compatible under multiple endianness)
+  #   CPUs with multiple modes of endianness are pairwise compatible.
   isCompatible = a: b: with cpuTypes; lib.any lib.id [
     # x86
     (b == i386 && isCompatible a i486)
@@ -271,7 +288,7 @@ rec {
 
   kernels = with execFormats; with kernelFamilies; setTypes types.openKernel {
     # TODO(@Ericson2314): Don't want to mass-rebuild yet to keeping 'darwin' as
-    # the nnormalized name for macOS.
+    # the normalized name for macOS.
     macos    = { execFormat = macho;   families = { inherit darwin; }; name = "darwin"; };
     ios      = { execFormat = macho;   families = { inherit darwin; }; };
     freebsd  = { execFormat = elf;     families = { inherit bsd; }; };
@@ -458,8 +475,12 @@ rec {
     else "${cpu.name}-${kernel.name}";
 
   tripleFromSystem = { cpu, vendor, kernel, abi, ... } @ sys: assert isSystem sys; let
+    optExecFormat =
+      lib.optionalString (kernel.name == "netbsd" &&
+                          gnuNetBSDDefaultExecFormat cpu != kernel.execFormat)
+        kernel.execFormat.name;
     optAbi = lib.optionalString (abi != abis.unknown) "-${abi.name}";
-  in "${cpu.name}-${vendor.name}-${kernel.name}${optAbi}";
+  in "${cpu.name}-${vendor.name}-${kernel.name}${optExecFormat}${optAbi}";
 
   ################################################################################
 
diff --git a/lib/systems/platforms.nix b/lib/systems/platforms.nix
index f399c1873f5..92285346f75 100644
--- a/lib/systems/platforms.nix
+++ b/lib/systems/platforms.nix
@@ -300,17 +300,15 @@ rec {
       baseConfig = "multi_v7_defconfig";
       DTB = true;
       autoModules = true;
-      PreferBuiltin = true;
+      preferBuiltin = true;
       target = "zImage";
       extraConfig = ''
-        # Serial port for Raspberry Pi 3. Upstream forgot to add it to the ARMv7 defconfig.
+        # Serial port for Raspberry Pi 3. Wasn't included in ARMv7 defconfig
+        # until 4.17.
         SERIAL_8250_BCM2835AUX y
         SERIAL_8250_EXTENDED y
         SERIAL_8250_SHARE_IRQ y
 
-        # Fix broken sunxi-sid nvmem driver.
-        TI_CPTS y
-
         # Hangs ODROID-XU4
         ARM_BIG_LITTLE_CPUIDLE n
 
@@ -377,6 +375,13 @@ rec {
     };
   };
 
+  apple-m1 = {
+    gcc = {
+      arch = "armv8.3-a+crypto+sha2+aes+crc+fp16+lse+simd+ras+rdm+rcpc";
+      cpu = "apple-a13";
+    };
+  };
+
   ##
   ## MIPS
   ##
@@ -476,11 +481,11 @@ rec {
   riscv-multiplatform = {
     linux-kernel = {
       name = "riscv-multiplatform";
-      target = "vmlinux";
+      target = "Image";
       autoModules = true;
       baseConfig = "defconfig";
+      DTB = true;
       extraConfig = ''
-        FTRACE n
         SERIAL_OF_PLATFORM y
       '';
     };
@@ -497,7 +502,10 @@ rec {
         else if lib.versionOlder version "6" then sheevaplug
         else if lib.versionOlder version "7" then raspberrypi
         else armv7l-hf-multiplatform
-    else if platform.isAarch64 then aarch64-multiplatform
+
+    else if platform.isAarch64 then
+      if platform.isDarwin then apple-m1
+      else aarch64-multiplatform
 
     else if platform.isRiscV then riscv-multiplatform
 
diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix
index 35a5801c724..0d249968402 100644
--- a/lib/tests/misc.nix
+++ b/lib/tests/misc.nix
@@ -660,4 +660,71 @@ runTests {
     expected = [ [ "foo" ] [ "foo" "<name>" "bar" ] [ "foo" "bar" ] ];
   };
 
+  testCartesianProductOfEmptySet = {
+    expr = cartesianProductOfSets {};
+    expected = [ {} ];
+  };
+
+  testCartesianProductOfOneSet = {
+    expr = cartesianProductOfSets { a = [ 1 2 3 ]; };
+    expected = [ { a = 1; } { a = 2; } { a = 3; } ];
+  };
+
+  testCartesianProductOfTwoSets = {
+    expr = cartesianProductOfSets { a = [ 1 ]; b = [ 10 20 ]; };
+    expected = [
+      { a = 1; b = 10; }
+      { a = 1; b = 20; }
+    ];
+  };
+
+  testCartesianProductOfTwoSetsWithOneEmpty = {
+    expr = cartesianProductOfSets { a = [ ]; b = [ 10 20 ]; };
+    expected = [ ];
+  };
+
+  testCartesianProductOfThreeSets = {
+    expr = cartesianProductOfSets {
+      a = [   1   2   3 ];
+      b = [  10  20  30 ];
+      c = [ 100 200 300 ];
+    };
+    expected = [
+      { a = 1; b = 10; c = 100; }
+      { a = 1; b = 10; c = 200; }
+      { a = 1; b = 10; c = 300; }
+
+      { a = 1; b = 20; c = 100; }
+      { a = 1; b = 20; c = 200; }
+      { a = 1; b = 20; c = 300; }
+
+      { a = 1; b = 30; c = 100; }
+      { a = 1; b = 30; c = 200; }
+      { a = 1; b = 30; c = 300; }
+
+      { a = 2; b = 10; c = 100; }
+      { a = 2; b = 10; c = 200; }
+      { a = 2; b = 10; c = 300; }
+
+      { a = 2; b = 20; c = 100; }
+      { a = 2; b = 20; c = 200; }
+      { a = 2; b = 20; c = 300; }
+
+      { a = 2; b = 30; c = 100; }
+      { a = 2; b = 30; c = 200; }
+      { a = 2; b = 30; c = 300; }
+
+      { a = 3; b = 10; c = 100; }
+      { a = 3; b = 10; c = 200; }
+      { a = 3; b = 10; c = 300; }
+
+      { a = 3; b = 20; c = 100; }
+      { a = 3; b = 20; c = 200; }
+      { a = 3; b = 20; c = 300; }
+
+      { a = 3; b = 30; c = 100; }
+      { a = 3; b = 30; c = 200; }
+      { a = 3; b = 30; c = 300; }
+    ];
+  };
 }
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index 309c5311361..2e57c2f8e2a 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -169,12 +169,15 @@ checkConfigOutput "foo" config.submodule.foo ./declare-submoduleWith-special.nix
 ## shorthandOnlyDefines config behaves as expected
 checkConfigOutput "true" config.submodule.config ./declare-submoduleWith-shorthand.nix ./define-submoduleWith-shorthand.nix
 checkConfigError 'is not of type `boolean' config.submodule.config ./declare-submoduleWith-shorthand.nix ./define-submoduleWith-noshorthand.nix
-checkConfigError 'value is a boolean while a set was expected' config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-shorthand.nix
+checkConfigError "You're trying to declare a value of type \`bool'\nrather than an attribute-set for the option" config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-shorthand.nix
 checkConfigOutput "true" config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-noshorthand.nix
 
 ## submoduleWith should merge all modules in one swoop
 checkConfigOutput "true" config.submodule.inner ./declare-submoduleWith-modules.nix
 checkConfigOutput "true" config.submodule.outer ./declare-submoduleWith-modules.nix
+# Should also be able to evaluate the type name (which evaluates freeformType,
+# which evaluates all the modules defined by the type)
+checkConfigOutput "submodule" options.submodule.type.description ./declare-submoduleWith-modules.nix
 
 ## Paths should be allowed as values and work as expected
 checkConfigOutput "true" config.submodule.enable ./declare-submoduleWith-path.nix
@@ -262,6 +265,13 @@ checkConfigOutput true config.value.mkbefore ./types-anything/mk-mods.nix
 checkConfigOutput 1 config.value.nested.foo ./types-anything/mk-mods.nix
 checkConfigOutput baz config.value.nested.bar.baz ./types-anything/mk-mods.nix
 
+## types.functionTo
+checkConfigOutput "input is input" config.result ./functionTo/trivial.nix
+checkConfigOutput "a b" config.result ./functionTo/merging-list.nix
+checkConfigError 'A definition for option .fun.\[function body\]. is not of type .string.. Definition values:\n- In .*wrong-type.nix' config.result ./functionTo/wrong-type.nix
+checkConfigOutput "b a" config.result ./functionTo/list-order.nix
+checkConfigOutput "a c" config.result ./functionTo/merging-attrs.nix
+
 cat <<EOF
 ====== module tests ======
 $pass Pass
diff --git a/lib/tests/modules/declare-submoduleWith-modules.nix b/lib/tests/modules/declare-submoduleWith-modules.nix
index 4736ab41751..a8b82d17688 100644
--- a/lib/tests/modules/declare-submoduleWith-modules.nix
+++ b/lib/tests/modules/declare-submoduleWith-modules.nix
@@ -8,9 +8,6 @@
             default = false;
           };
         }
-        {
-          outer = true;
-        }
       ];
     };
     default = {};
@@ -25,6 +22,7 @@
     })
     {
       inner = true;
+      outer = true;
     }
   ];
 }
diff --git a/lib/tests/modules/functionTo/list-order.nix b/lib/tests/modules/functionTo/list-order.nix
new file mode 100644
index 00000000000..77a1a43a84f
--- /dev/null
+++ b/lib/tests/modules/functionTo/list-order.nix
@@ -0,0 +1,25 @@
+
+{ lib, config, ... }:
+let
+  inherit (lib) types;
+in {
+  options = {
+    fun = lib.mkOption {
+      type = types.functionTo (types.listOf types.str);
+    };
+
+    result = lib.mkOption {
+      type = types.str;
+      default = toString (config.fun {
+        a = "a";
+        b = "b";
+        c = "c";
+      });
+    };
+  };
+
+  config.fun = lib.mkMerge [
+    (input: lib.mkAfter [ input.a ])
+    (input: [ input.b ])
+  ];
+}
diff --git a/lib/tests/modules/functionTo/merging-attrs.nix b/lib/tests/modules/functionTo/merging-attrs.nix
new file mode 100644
index 00000000000..97c015f928a
--- /dev/null
+++ b/lib/tests/modules/functionTo/merging-attrs.nix
@@ -0,0 +1,27 @@
+{ lib, config, ... }:
+let
+  inherit (lib) types;
+in {
+  options = {
+    fun = lib.mkOption {
+      type = types.functionTo (types.attrsOf types.str);
+    };
+
+    result = lib.mkOption {
+      type = types.str;
+      default = toString (lib.attrValues (config.fun {
+        a = "a";
+        b = "b";
+        c = "c";
+      }));
+    };
+  };
+
+  config.fun = lib.mkMerge [
+    (input: { inherit (input) a; })
+    (input: { inherit (input) b; })
+    (input: {
+      b = lib.mkForce input.c;
+    })
+  ];
+}
diff --git a/lib/tests/modules/functionTo/merging-list.nix b/lib/tests/modules/functionTo/merging-list.nix
new file mode 100644
index 00000000000..15fcd2bdcc4
--- /dev/null
+++ b/lib/tests/modules/functionTo/merging-list.nix
@@ -0,0 +1,24 @@
+{ lib, config, ... }:
+let
+  inherit (lib) types;
+in {
+  options = {
+    fun = lib.mkOption {
+      type = types.functionTo (types.listOf types.str);
+    };
+
+    result = lib.mkOption {
+      type = types.str;
+      default = toString (config.fun {
+        a = "a";
+        b = "b";
+        c = "c";
+      });
+    };
+  };
+
+  config.fun = lib.mkMerge [
+    (input: [ input.a ])
+    (input: [ input.b ])
+  ];
+}
diff --git a/lib/tests/modules/functionTo/trivial.nix b/lib/tests/modules/functionTo/trivial.nix
new file mode 100644
index 00000000000..0962a0cf893
--- /dev/null
+++ b/lib/tests/modules/functionTo/trivial.nix
@@ -0,0 +1,17 @@
+{ lib, config, ... }:
+let
+  inherit (lib) types;
+in {
+  options = {
+    fun = lib.mkOption {
+      type = types.functionTo types.str;
+    };
+
+    result = lib.mkOption {
+      type = types.str;
+      default = config.fun "input";
+    };
+  };
+
+  config.fun = input: "input is ${input}";
+}
diff --git a/lib/tests/modules/functionTo/wrong-type.nix b/lib/tests/modules/functionTo/wrong-type.nix
new file mode 100644
index 00000000000..fd65b75088d
--- /dev/null
+++ b/lib/tests/modules/functionTo/wrong-type.nix
@@ -0,0 +1,18 @@
+
+{ lib, config, ... }:
+let
+  inherit (lib) types;
+in {
+  options = {
+    fun = lib.mkOption {
+      type = types.functionTo types.str;
+    };
+
+    result = lib.mkOption {
+      type = types.str;
+      default = config.fun 0;
+    };
+  };
+
+  config.fun = input: input + 1;
+}
diff --git a/lib/tests/release.nix b/lib/tests/release.nix
index 800d8a65c14..c3b05251f70 100644
--- a/lib/tests/release.nix
+++ b/lib/tests/release.nix
@@ -26,7 +26,11 @@ pkgs.runCommandNoCC "nixpkgs-lib-tests" {
     nix-store --init
 
     cp -r ${../.} lib
+    echo "Running lib/tests/modules.sh"
     bash lib/tests/modules.sh
 
+    echo "Running lib/tests/sources.sh"
+    TEST_LIB=$PWD/lib bash lib/tests/sources.sh
+
     touch $out
 ''
diff --git a/lib/tests/sources.sh b/lib/tests/sources.sh
new file mode 100755
index 00000000000..71fee719cb2
--- /dev/null
+++ b/lib/tests/sources.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Use
+#     || die
+die() {
+  echo >&2 "test case failed: " "$@"
+  exit 1
+}
+
+if test -n "${TEST_LIB:-}"; then
+  export NIX_PATH=nixpkgs="$(dirname "$TEST_LIB")"
+else
+  export NIX_PATH=nixpkgs="$(cd $(dirname ${BASH_SOURCE[0]})/../..; pwd)"
+fi
+
+work="$(mktemp -d)"
+clean_up() {
+  rm -rf "$work"
+}
+trap clean_up EXIT
+cd $work
+
+touch {README.md,module.o,foo.bar}
+
+# nix-instantiate doesn't write out the source, only computing the hash, so
+# this uses the experimental nix command instead.
+
+dir="$(nix eval --raw '(with import <nixpkgs/lib>; "${
+  cleanSource ./.
+}")')"
+(cd $dir; find) | sort -f | diff -U10 - <(cat <<EOF
+.
+./foo.bar
+./README.md
+EOF
+) || die "cleanSource 1"
+
+
+dir="$(nix eval --raw '(with import <nixpkgs/lib>; "${
+  cleanSourceWith { src = '"$work"'; filter = path: type: ! hasSuffix ".bar" path; }
+}")')"
+(cd $dir; find) | sort -f | diff -U10 - <(cat <<EOF
+.
+./module.o
+./README.md
+EOF
+) || die "cleanSourceWith 1"
+
+dir="$(nix eval --raw '(with import <nixpkgs/lib>; "${
+  cleanSourceWith { src = cleanSource '"$work"'; filter = path: type: ! hasSuffix ".bar" path; }
+}")')"
+(cd $dir; find) | sort -f | diff -U10 - <(cat <<EOF
+.
+./README.md
+EOF
+) || die "cleanSourceWith + cleanSource"
+
+echo >&2 tests ok
diff --git a/lib/tests/systems.nix b/lib/tests/systems.nix
index eed7ee725bc..6bd43f0d0d0 100644
--- a/lib/tests/systems.nix
+++ b/lib/tests/systems.nix
@@ -15,9 +15,9 @@ in
 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-none" "armv7a-linux" "armv7l-linux" "arm-none" "armv7a-darwin" ];
+  testarm = mseteq arm [ "armv5tel-linux" "armv6l-linux" "armv6l-netbsd" "armv6l-none" "armv7a-linux" "armv7a-netbsd" "armv7l-linux" "armv7l-netbsd" "arm-none" "armv7a-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 [ "mipsel-linux" ];
+  testmips = mseteq mips [ "mipsel-linux" "mipsel-netbsd" ];
   testmmix = mseteq mmix [ "mmix-mmixware" ];
   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" ];
 
@@ -28,8 +28,8 @@ with lib.systems.doubles; lib.runTests {
   testredox = mseteq redox [ "x86_64-redox" ];
   testgnu = mseteq gnu (linux /* ++ kfreebsd ++ ... */);
   testillumos = mseteq illumos [ "x86_64-solaris" ];
-  testlinux = mseteq linux [ "aarch64-linux" "armv5tel-linux" "armv6l-linux" "armv7a-linux" "armv7l-linux" "i686-linux" "mipsel-linux" "riscv32-linux" "riscv64-linux" "x86_64-linux" "powerpc64le-linux" ];
-  testnetbsd = mseteq netbsd [ "i686-netbsd" "x86_64-netbsd" ];
+  testlinux = mseteq linux [ "aarch64-linux" "armv5tel-linux" "armv6l-linux" "armv7a-linux" "armv7l-linux" "i686-linux" "mipsel-linux" "riscv32-linux" "riscv64-linux" "x86_64-linux" "powerpc64-linux" "powerpc64le-linux" "m68k-linux" "s390-linux" ];
+  testnetbsd = mseteq netbsd [ "aarch64-netbsd" "armv6l-netbsd" "armv7a-netbsd" "armv7l-netbsd" "i686-netbsd" "mipsel-netbsd" "powerpc-netbsd" "riscv32-netbsd" "riscv64-netbsd" "x86_64-netbsd" ];
   testopenbsd = mseteq openbsd [ "i686-openbsd" "x86_64-openbsd" ];
   testwindows = mseteq windows [ "i686-cygwin" "x86_64-cygwin" "i686-windows" "x86_64-windows" ];
   testunix = mseteq unix (linux ++ darwin ++ freebsd ++ openbsd ++ netbsd ++ illumos ++ cygwin ++ redox);
diff --git a/lib/trivial.nix b/lib/trivial.nix
index 5ee7fc99206..c8ef5599ccd 100644
--- a/lib/trivial.nix
+++ b/lib/trivial.nix
@@ -158,7 +158,7 @@ rec {
     seq deepSeq genericClosure;
 
 
-  ## nixpks version strings
+  ## nixpkgs version strings
 
   /* Returns the current full nixpkgs version number. */
   version = release + versionSuffix;
@@ -171,7 +171,7 @@ rec {
      On each release the first letter is bumped and a new animal is chosen
      starting with that new letter.
   */
-  codeName = "Okapi";
+  codeName = "Porcupine";
 
   /* Returns the current nixpkgs version suffix as string. */
   versionSuffix =
@@ -297,12 +297,15 @@ rec {
   # Usage:
   # {
   #   foo = lib.warn "foo is deprecated" oldFoo;
+  #   bar = lib.warnIf (bar == "") "Empty bar is deprecated" bar;
   # }
   #
   # TODO: figure out a clever way to integrate location information from
   # something like __unsafeGetAttrPos.
 
   warn = msg: builtins.trace "warning: ${msg}";
+  warnIf = cond: msg: if cond then warn msg else id;
+
   info = msg: builtins.trace "INFO: ${msg}";
 
   showWarnings = warnings: res: lib.foldr (w: x: warn w x) res warnings;
diff --git a/lib/types.nix b/lib/types.nix
index ee891f8231b..a0be2ff3a45 100644
--- a/lib/types.nix
+++ b/lib/types.nix
@@ -147,9 +147,13 @@ rec {
     , # The deprecation message to display when this type is used by an option
       # If null, the type isn't deprecated
       deprecationMessage ? null
+    , # The types that occur in the definition of this type. This is used to
+      # issue deprecation warnings recursively. Can also be used to reuse
+      # nested types
+      nestedTypes ? {}
     }:
     { _type = "option-type";
-      inherit name check merge emptyValue getSubOptions getSubModules substSubModules typeMerge functor deprecationMessage;
+      inherit name check merge emptyValue getSubOptions getSubModules substSubModules typeMerge functor deprecationMessage nestedTypes;
       description = if description == null then name else description;
     };
 
@@ -256,14 +260,14 @@ rec {
         };
         u8 = unsign 8 256;
         u16 = unsign 16 65536;
-        # the biggest int a 64-bit Nix accepts is 2^63 - 1 (9223372036854775808), for a 32-bit Nix it is 2^31 - 1 (2147483647)
-        # the smallest int a 64-bit Nix accepts is -2^63 (-9223372036854775807), for a 32-bit Nix it is -2^31 (-2147483648)
-        # u32 = unsign 32 4294967296;
+        # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808)
+        # the smallest int Nix accepts is -2^63 (-9223372036854775807)
+        u32 = unsign 32 4294967296;
         # u64 = unsign 64 18446744073709551616;
 
         s8 = sign 8 256;
         s16 = sign 16 65536;
-        # s32 = sign 32 4294967296;
+        s32 = sign 32 4294967296;
       };
 
     # Alias of u16 for a port number
@@ -283,6 +287,13 @@ rec {
       merge = mergeEqualOption;
     };
 
+    nonEmptyStr = mkOptionType {
+      name = "nonEmptyStr";
+      description = "non-empty string";
+      check = x: str.check x && builtins.match "[ \t\n]*" x == null;
+      inherit (str) merge;
+    };
+
     strMatching = pattern: mkOptionType {
       name = "strMatching ${escapeNixString pattern}";
       description = "string matching the pattern ${pattern}";
@@ -337,7 +348,7 @@ rec {
     };
 
     shellPackage = package // {
-      check = x: (package.check x) && (hasAttr "shellPath" x);
+      check = x: isDerivation x && hasAttr "shellPath" x;
     };
 
     path = mkOptionType {
@@ -365,6 +376,7 @@ rec {
       getSubModules = elemType.getSubModules;
       substSubModules = m: listOf (elemType.substSubModules m);
       functor = (defaultFunctor name) // { wrapped = elemType; };
+      nestedTypes.elemType = elemType;
     };
 
     nonEmptyListOf = elemType:
@@ -389,6 +401,7 @@ rec {
       getSubModules = elemType.getSubModules;
       substSubModules = m: attrsOf (elemType.substSubModules m);
       functor = (defaultFunctor name) // { wrapped = elemType; };
+      nestedTypes.elemType = elemType;
     };
 
     # A version of attrsOf that's lazy in its values at the expense of
@@ -413,6 +426,7 @@ rec {
       getSubModules = elemType.getSubModules;
       substSubModules = m: lazyAttrsOf (elemType.substSubModules m);
       functor = (defaultFunctor name) // { wrapped = elemType; };
+      nestedTypes.elemType = elemType;
     };
 
     # TODO: drop this in the future:
@@ -421,6 +435,7 @@ rec {
       deprecationMessage = "Mixing lists with attribute values is no longer"
         + " possible; please use `types.attrsOf` instead. See"
         + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation.";
+      nestedTypes.elemType = elemType;
     };
 
     # Value of given type but with no merging (i.e. `uniq list`s are not concatenated).
@@ -433,6 +448,7 @@ rec {
       getSubModules = elemType.getSubModules;
       substSubModules = m: uniq (elemType.substSubModules m);
       functor = (defaultFunctor name) // { wrapped = elemType; };
+      nestedTypes.elemType = elemType;
     };
 
     # Null or value of ...
@@ -451,6 +467,18 @@ rec {
       getSubModules = elemType.getSubModules;
       substSubModules = m: nullOr (elemType.substSubModules m);
       functor = (defaultFunctor name) // { wrapped = elemType; };
+      nestedTypes.elemType = elemType;
+    };
+
+    functionTo = elemType: mkOptionType {
+      name = "functionTo";
+      description = "function that evaluates to a(n) ${elemType.name}";
+      check = isFunction;
+      merge = loc: defs:
+        fnArgs: (mergeDefinitions (loc ++ [ "[function body]" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue;
+      getSubOptions = elemType.getSubOptions;
+      getSubModules = elemType.getSubModules;
+      substSubModules = m: functionTo (elemType.substSubModules m);
     };
 
     # A submodule (like typed attribute set). See NixOS manual.
@@ -524,6 +552,9 @@ rec {
         substSubModules = m: submoduleWith (attrs // {
           modules = m;
         });
+        nestedTypes = lib.optionalAttrs (freeformType != null) {
+          freeformType = freeformType;
+        };
         functor = defaultFunctor name // {
           type = types.submoduleWith;
           payload = {
@@ -557,7 +588,17 @@ rec {
       in
       mkOptionType rec {
         name = "enum";
-        description = "one of ${concatMapStringsSep ", " show values}";
+        description =
+          # Length 0 or 1 enums may occur in a design pattern with type merging
+          # where an "interface" module declares an empty enum and other modules
+          # provide implementations, each extending the enum with their own
+          # identifier.
+          if values == [] then
+            "impossible (empty enum)"
+          else if builtins.length values == 1 then
+            "value ${show (builtins.head values)} (singular enum)"
+          else
+            "one of ${concatMapStringsSep ", " show values}";
         check = flip elem values;
         merge = mergeEqualOption;
         functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); };
@@ -585,6 +626,8 @@ rec {
            then functor.type mt1 mt2
            else null;
       functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; };
+      nestedTypes.left = t1;
+      nestedTypes.right = t2;
     };
 
     # Any of the types in the given list
@@ -616,6 +659,8 @@ rec {
         substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
         typeMerge = t1: t2: null;
         functor = (defaultFunctor name) // { wrapped = finalType; };
+        nestedTypes.coercedType = coercedType;
+        nestedTypes.finalType = finalType;
       };
 
     # Obsolete alternative to configOf.  It takes its option