about summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/customisation.nix88
-rw-r--r--lib/fileset/default.nix51
-rw-r--r--lib/fileset/internal.nix82
-rwxr-xr-xlib/fileset/tests.sh86
-rw-r--r--lib/options.nix90
-rw-r--r--lib/systems/parse.nix11
-rwxr-xr-xlib/tests/modules.sh8
-rw-r--r--lib/tests/modules/declare-mkPackageOption.nix34
8 files changed, 376 insertions, 74 deletions
diff --git a/lib/customisation.nix b/lib/customisation.nix
index 5ef4f29e6f6ac..61bb531d2f627 100644
--- a/lib/customisation.nix
+++ b/lib/customisation.nix
@@ -13,16 +13,7 @@ rec {
      scenarios (e.g. in ~/.config/nixpkgs/config.nix).  For instance,
      if you want to "patch" the derivation returned by a package
      function in Nixpkgs to build another version than what the
-     function itself provides, you can do something like this:
-
-       mySed = overrideDerivation pkgs.gnused (oldAttrs: {
-         name = "sed-4.2.2-pre";
-         src = fetchurl {
-           url = ftp://alpha.gnu.org/gnu/sed/sed-4.2.2-pre.tar.bz2;
-           hash = "sha256-MxBJRcM2rYzQYwJ5XKxhXTQByvSg5jZc5cSHEZoB2IY=";
-         };
-         patches = [];
-       });
+     function itself provides.
 
      For another application, see build-support/vm, where this
      function is used to build arbitrary derivations inside a QEMU
@@ -35,6 +26,19 @@ rec {
 
      You should in general prefer `drv.overrideAttrs` over this function;
      see the nixpkgs manual for more information on overriding.
+
+     Example:
+       mySed = overrideDerivation pkgs.gnused (oldAttrs: {
+         name = "sed-4.2.2-pre";
+         src = fetchurl {
+           url = ftp://alpha.gnu.org/gnu/sed/sed-4.2.2-pre.tar.bz2;
+           hash = "sha256-MxBJRcM2rYzQYwJ5XKxhXTQByvSg5jZc5cSHEZoB2IY=";
+         };
+         patches = [];
+       });
+
+     Type:
+       overrideDerivation :: Derivation -> ( Derivation -> AttrSet ) -> Derivation
   */
   overrideDerivation = drv: f:
     let
@@ -55,6 +59,10 @@ rec {
      injects `override` attribute which can be used to override arguments of
      the function.
 
+     Please refer to  documentation on [`<pkg>.overrideDerivation`](#sec-pkg-overrideDerivation) to learn about `overrideDerivation` and caveats
+     related to its use.
+
+     Example:
        nix-repl> x = {a, b}: { result = a + b; }
 
        nix-repl> y = lib.makeOverridable x { a = 1; b = 2; }
@@ -65,9 +73,8 @@ rec {
        nix-repl> y.override { a = 10; }
        { override = «lambda»; overrideDerivation = «lambda»; result = 12; }
 
-     Please refer to "Nixpkgs Contributors Guide" section
-     "<pkg>.overrideDerivation" to learn about `overrideDerivation` and caveats
-     related to its use.
+     Type:
+       makeOverridable :: (AttrSet -> a) -> AttrSet -> a
   */
   makeOverridable = f: lib.setFunctionArgs
     (origArgs: let
@@ -105,20 +112,29 @@ rec {
     `autoArgs`.  This function is intended to be partially
     parameterised, e.g.,
 
+      ```nix
       callPackage = callPackageWith pkgs;
       pkgs = {
         libfoo = callPackage ./foo.nix { };
         libbar = callPackage ./bar.nix { };
       };
+      ```
 
     If the `libbar` function expects an argument named `libfoo`, it is
     automatically passed as an argument.  Overrides or missing
     arguments can be supplied in `args`, e.g.
 
+      ```nix
       libbar = callPackage ./bar.nix {
         libfoo = null;
         enableX11 = true;
       };
+      ```
+
+    <!-- TODO: Apply "Example:" tag to the examples above -->
+
+    Type:
+      callPackageWith :: AttrSet -> ((AttrSet -> a) | Path) -> AttrSet -> a
   */
   callPackageWith = autoArgs: fn: args:
     let
@@ -129,7 +145,7 @@ rec {
       # This includes automatic ones and ones passed explicitly
       allArgs = builtins.intersectAttrs fargs autoArgs // args;
 
-      # A list of argument names that the function requires, but
+      # a list of argument names that the function requires, but
       # wouldn't be passed to it
       missingArgs = lib.attrNames
         # Filter out arguments that have a default value
@@ -176,7 +192,11 @@ rec {
 
   /* Like callPackage, but for a function that returns an attribute
      set of derivations. The override function is added to the
-     individual attributes. */
+     individual attributes.
+
+     Type:
+       callPackagesWith :: AttrSet -> ((AttrSet -> AttrSet) | Path) -> AttrSet -> AttrSet
+  */
   callPackagesWith = autoArgs: fn: args:
     let
       f = if lib.isFunction fn then fn else import fn;
@@ -193,7 +213,11 @@ rec {
 
 
   /* Add attributes to each output of a derivation without changing
-     the derivation itself and check a given condition when evaluating. */
+     the derivation itself and check a given condition when evaluating.
+
+     Type:
+       extendDerivation :: Bool -> Any -> Derivation -> Derivation
+  */
   extendDerivation = condition: passthru: drv:
     let
       outputs = drv.outputs or [ "out" ];
@@ -227,7 +251,11 @@ rec {
   /* Strip a derivation of all non-essential attributes, returning
      only those needed by hydra-eval-jobs. Also strictly evaluate the
      result to ensure that there are no thunks kept alive to prevent
-     garbage collection. */
+     garbage collection.
+
+     Type:
+       hydraJob :: (Derivation | Null) -> (Derivation | Null)
+  */
   hydraJob = drv:
     let
       outputs = drv.outputs or ["out"];
@@ -265,7 +293,11 @@ rec {
      called with the overridden packages. The package sets may be
      hierarchical: the packages in the set are called with the scope
      provided by `newScope` and the set provides a `newScope` attribute
-     which can form the parent scope for later package sets. */
+     which can form the parent scope for later package sets.
+
+     Type:
+       makeScope :: (AttrSet -> ((AttrSet -> a) | Path) -> AttrSet -> a) -> (AttrSet -> AttrSet) -> AttrSet
+  */
   makeScope = newScope: f:
     let self = f self // {
           newScope = scope: newScope (self // scope);
@@ -287,7 +319,25 @@ rec {
     { inherit otherSplices keep extra f; };
 
   /* Like makeScope, but aims to support cross compilation. It's still ugly, but
-     hopefully it helps a little bit. */
+     hopefully it helps a little bit.
+
+     Type:
+       makeScopeWithSplicing' ::
+         { splicePackages :: Splice -> AttrSet
+         , newScope :: AttrSet -> ((AttrSet -> a) | Path) -> AttrSet -> a
+         }
+         -> { otherSplices :: Splice, keep :: AttrSet -> AttrSet, extra :: AttrSet -> AttrSet }
+         -> AttrSet
+
+       Splice ::
+         { pkgsBuildBuild :: AttrSet
+         , pkgsBuildHost :: AttrSet
+         , pkgsBuildTarget :: AttrSet
+         , pkgsHostHost :: AttrSet
+         , pkgsHostTarget :: AttrSet
+         , pkgsTargetTarget :: AttrSet
+         }
+  */
   makeScopeWithSplicing' =
     { splicePackages
     , newScope
diff --git a/lib/fileset/default.nix b/lib/fileset/default.nix
index 7bd701670386c..0342be3e0371e 100644
--- a/lib/fileset/default.nix
+++ b/lib/fileset/default.nix
@@ -6,6 +6,7 @@ let
     _coerceMany
     _toSourceFilter
     _unionMany
+    _fileFilter
     _printFileset
     _intersection
     ;
@@ -41,6 +42,7 @@ let
     ;
 
   inherit (lib.trivial)
+    isFunction
     pipe
     ;
 
@@ -279,6 +281,55 @@ If a directory does not recursively contain any file, it is omitted from the sto
       ];
 
   /*
+    Filter a file set to only contain files matching some predicate.
+
+    Type:
+      fileFilter ::
+        ({
+          name :: String,
+          type :: String,
+          ...
+        } -> Bool)
+        -> FileSet
+        -> FileSet
+
+    Example:
+      # Include all regular `default.nix` files in the current directory
+      fileFilter (file: file.name == "default.nix") ./.
+
+      # Include all non-Nix files from the current directory
+      fileFilter (file: ! hasSuffix ".nix" file.name) ./.
+
+      # Include all files that start with a "." in the current directory
+      fileFilter (file: hasPrefix "." file.name) ./.
+
+      # Include all regular files (not symlinks or others) in the current directory
+      fileFilter (file: file.type == "regular")
+  */
+  fileFilter =
+    /*
+      The predicate function to call on all files contained in given file set.
+      A file is included in the resulting file set if this function returns true for it.
+
+      This function is called with an attribute set containing these attributes:
+
+      - `name` (String): The name of the file
+
+      - `type` (String, one of `"regular"`, `"symlink"` or `"unknown"`): The type of the file.
+        This matches result of calling [`builtins.readFileType`](https://nixos.org/manual/nix/stable/language/builtins.html#builtins-readFileType) on the file's path.
+
+      Other attributes may be added in the future.
+    */
+    predicate:
+    # The file set to filter based on the predicate function
+    fileset:
+    if ! isFunction predicate then
+      throw "lib.fileset.fileFilter: Expected the first argument to be a function, but it's a ${typeOf predicate} instead."
+    else
+      _fileFilter predicate
+        (_coerce "lib.fileset.fileFilter: second argument" fileset);
+
+  /*
     The file set containing all files that are in both of two given file sets.
     See also [Intersection (set theory)](https://en.wikipedia.org/wiki/Intersection_(set_theory)).
 
diff --git a/lib/fileset/internal.nix b/lib/fileset/internal.nix
index 546b93f158a14..76b95c6ae471b 100644
--- a/lib/fileset/internal.nix
+++ b/lib/fileset/internal.nix
@@ -172,11 +172,11 @@ rec {
     else if ! isPath value then
       if isStringLike value then
         throw ''
-          ${context} ("${toString value}") is a string-like value, but it should be a path instead.
+          ${context} ("${toString value}") is a string-like value, but it should be a file set or a path instead.
               Paths represented as strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.''
       else
         throw ''
-          ${context} is of type ${typeOf value}, but it should be a path instead.''
+          ${context} is of type ${typeOf value}, but it should be a file set or a path instead.''
     else if ! pathExists value then
       throw ''
         ${context} (${toString value}) does not exist.''
@@ -424,7 +424,7 @@ rec {
       # Filter suited when there's some files
       # This can't be used for when there's no files, because the base directory is always included
       nonEmpty =
-        path: _:
+        path: type:
         let
           # Add a slash to the path string, turning "/foo" to "/foo/",
           # making sure to not have any false prefix matches below.
@@ -433,25 +433,37 @@ rec {
           # meaning this function can never receive "/" as an argument
           pathSlash = path + "/";
         in
-        # Same as `hasPrefix pathSlash baseString`, but more efficient.
-        # With base /foo/bar we need to include /foo:
-        # hasPrefix "/foo/" "/foo/bar/"
-        if substring 0 (stringLength pathSlash) baseString == pathSlash then
-          true
-        # Same as `! hasPrefix baseString pathSlash`, but more efficient.
-        # With base /foo/bar we need to exclude /baz
-        # ! hasPrefix "/baz/" "/foo/bar/"
-        else if substring 0 baseLength pathSlash != baseString then
-          false
-        else
-          # Same as `removePrefix baseString path`, but more efficient.
-          # From the above code we know that hasPrefix baseString pathSlash holds, so this is safe.
-          # We don't use pathSlash here because we only needed the trailing slash for the prefix matching.
-          # With base /foo and path /foo/bar/baz this gives
-          # inTree (split "/" (removePrefix "/foo/" "/foo/bar/baz"))
-          # == inTree (split "/" "bar/baz")
-          # == inTree [ "bar" "baz" ]
-          inTree (split "/" (substring baseLength (-1) path));
+        (
+          # Same as `hasPrefix pathSlash baseString`, but more efficient.
+          # With base /foo/bar we need to include /foo:
+          # hasPrefix "/foo/" "/foo/bar/"
+          if substring 0 (stringLength pathSlash) baseString == pathSlash then
+            true
+          # Same as `! hasPrefix baseString pathSlash`, but more efficient.
+          # With base /foo/bar we need to exclude /baz
+          # ! hasPrefix "/baz/" "/foo/bar/"
+          else if substring 0 baseLength pathSlash != baseString then
+            false
+          else
+            # Same as `removePrefix baseString path`, but more efficient.
+            # From the above code we know that hasPrefix baseString pathSlash holds, so this is safe.
+            # We don't use pathSlash here because we only needed the trailing slash for the prefix matching.
+            # With base /foo and path /foo/bar/baz this gives
+            # inTree (split "/" (removePrefix "/foo/" "/foo/bar/baz"))
+            # == inTree (split "/" "bar/baz")
+            # == inTree [ "bar" "baz" ]
+            inTree (split "/" (substring baseLength (-1) path))
+        )
+        # This is a way have an additional check in case the above is true without any significant performance cost
+        && (
+          # This relies on the fact that Nix only distinguishes path types "directory", "regular", "symlink" and "unknown",
+          # so everything except "unknown" is allowed, seems reasonable to rely on that
+          type != "unknown"
+          || throw ''
+            lib.fileset.toSource: `fileset` contains a file that cannot be added to the store: ${path}
+                This file is neither a regular file nor a symlink, the only file types supported by the Nix store.
+                Therefore the file set cannot be added to the Nix store as is. Make sure to not include that file to avoid this error.''
+        );
     in
     # Special case because the code below assumes that the _internalBase is always included in the result
     # which shouldn't be done when we have no files at all in the base
@@ -638,4 +650,30 @@ rec {
     else
       # In all other cases it's the rhs
       rhs;
+
+  _fileFilter = predicate: fileset:
+    let
+      recurse = path: tree:
+        mapAttrs (name: subtree:
+          if isAttrs subtree || subtree == "directory" then
+            recurse (path + "/${name}") subtree
+          else if
+            predicate {
+              inherit name;
+              type = subtree;
+              # To ensure forwards compatibility with more arguments being added in the future,
+              # adding an attribute which can't be deconstructed :)
+              "lib.fileset.fileFilter: The predicate function passed as the first argument must be able to handle extra attributes for future compatibility. If you're using `{ name, file }:`, use `{ name, file, ... }:` instead." = null;
+            }
+          then
+            subtree
+          else
+            null
+        ) (_directoryEntries path tree);
+    in
+    if fileset._internalIsEmptyWithoutBase then
+      _emptyWithoutBase
+    else
+      _create fileset._internalBase
+        (recurse fileset._internalBase fileset._internalTree);
 }
diff --git a/lib/fileset/tests.sh b/lib/fileset/tests.sh
index 7a104654983f0..5b756b8fc5931 100755
--- a/lib/fileset/tests.sh
+++ b/lib/fileset/tests.sh
@@ -332,7 +332,7 @@ expectFailure 'with ((import <nixpkgs/lib>).extend (import <nixpkgs/lib/fileset/
 \s*`root`: root "'"$work"'/foo/mock-root"
 \s*`fileset`: root "'"$work"'/bar/mock-root"
 \s*Different roots are not supported.'
-rm -rf *
+rm -rf -- *
 
 # `root` needs to exist
 expectFailure 'toSource { root = ./a; fileset = ./.; }' 'lib.fileset.toSource: `root` \('"$work"'/a\) does not exist.'
@@ -342,7 +342,7 @@ touch a
 expectFailure 'toSource { root = ./a; fileset = ./a; }' 'lib.fileset.toSource: `root` \('"$work"'/a\) is a file, but it should be a directory instead. Potential solutions:
 \s*- If you want to import the file into the store _without_ a containing directory, use string interpolation or `builtins.path` instead of this function.
 \s*- If you want to import the file into the store _with_ a containing directory, set `root` to the containing directory, such as '"$work"', and set `fileset` to the file path.'
-rm -rf *
+rm -rf -- *
 
 # The fileset argument should be evaluated, even if the directory is empty
 expectFailure 'toSource { root = ./.; fileset = abort "This should be evaluated"; }' 'evaluation aborted with the following error message: '\''This should be evaluated'\'
@@ -352,11 +352,18 @@ mkdir a
 expectFailure 'toSource { root = ./a; fileset = ./.; }' 'lib.fileset.toSource: `fileset` could contain files in '"$work"', which is not under the `root` \('"$work"'/a\). Potential solutions:
 \s*- Set `root` to '"$work"' or any directory higher up. This changes the layout of the resulting store path.
 \s*- Set `fileset` to a file set that cannot contain files outside the `root` \('"$work"'/a\). This could change the files included in the result.'
-rm -rf *
+rm -rf -- *
+
+# non-regular and non-symlink files cannot be added to the Nix store
+mkfifo a
+expectFailure 'toSource { root = ./.; fileset = ./a; }' 'lib.fileset.toSource: `fileset` contains a file that cannot be added to the store: '"$work"'/a
+\s*This file is neither a regular file nor a symlink, the only file types supported by the Nix store.
+\s*Therefore the file set cannot be added to the Nix store as is. Make sure to not include that file to avoid this error.'
+rm -rf -- *
 
 # Path coercion only works for paths
-expectFailure 'toSource { root = ./.; fileset = 10; }' 'lib.fileset.toSource: `fileset` is of type int, but it should be a path instead.'
-expectFailure 'toSource { root = ./.; fileset = "/some/path"; }' 'lib.fileset.toSource: `fileset` \("/some/path"\) is a string-like value, but it should be a path instead.
+expectFailure 'toSource { root = ./.; fileset = 10; }' 'lib.fileset.toSource: `fileset` is of type int, but it should be a file set or a path instead.'
+expectFailure 'toSource { root = ./.; fileset = "/some/path"; }' 'lib.fileset.toSource: `fileset` \("/some/path"\) is a string-like value, but it should be a file set or a path instead.
 \s*Paths represented as strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.'
 
 # Path coercion errors for non-existent paths
@@ -493,7 +500,7 @@ expectFailure 'with ((import <nixpkgs/lib>).extend (import <nixpkgs/lib/fileset/
 \s*element 0: root "'"$work"'/foo/mock-root"
 \s*element 1: root "'"$work"'/bar/mock-root"
 \s*Different roots are not supported.'
-rm -rf *
+rm -rf -- *
 
 # Coercion errors show the correct context
 expectFailure 'toSource { root = ./.; fileset = union ./a ./.; }' 'lib.fileset.union: first argument \('"$work"'/a\) does not exist.'
@@ -678,6 +685,73 @@ tree=(
 checkFileset 'intersection (unions [ ./a/b ./c/d ./c/e ]) (unions [ ./a ./c/d/f ./c/e ])'
 
 
+## File filter
+
+# The predicate is not called when there's no files
+tree=()
+checkFileset 'fileFilter (file: abort "this is not needed") ./.'
+checkFileset 'fileFilter (file: abort "this is not needed") _emptyWithoutBase'
+
+# The predicate must be able to handle extra attributes
+touch a
+expectFailure 'toSource { root = ./.; fileset = fileFilter ({ name, type }: true) ./.; }' 'called with unexpected argument '\''"lib.fileset.fileFilter: The predicate function passed as the first argument must be able to handle extra attributes for future compatibility. If you'\''re using `\{ name, file \}:`, use `\{ name, file, ... \}:` instead."'\'
+rm -rf -- *
+
+# .name is the name, and it works correctly, even recursively
+tree=(
+    [a]=1
+    [b]=0
+    [c/a]=1
+    [c/b]=0
+    [d/c/a]=1
+    [d/c/b]=0
+)
+checkFileset 'fileFilter (file: file.name == "a") ./.'
+tree=(
+    [a]=0
+    [b]=1
+    [c/a]=0
+    [c/b]=1
+    [d/c/a]=0
+    [d/c/b]=1
+)
+checkFileset 'fileFilter (file: file.name != "a") ./.'
+
+# `.type` is the file type
+mkdir d
+touch d/a
+ln -s d/b d/b
+mkfifo d/c
+expectEqual \
+    'toSource { root = ./.; fileset = fileFilter (file: file.type == "regular") ./.; }' \
+    'toSource { root = ./.; fileset = ./d/a; }'
+expectEqual \
+    'toSource { root = ./.; fileset = fileFilter (file: file.type == "symlink") ./.; }' \
+    'toSource { root = ./.; fileset = ./d/b; }'
+expectEqual \
+    'toSource { root = ./.; fileset = fileFilter (file: file.type == "unknown") ./.; }' \
+    'toSource { root = ./.; fileset = ./d/c; }'
+expectEqual \
+    'toSource { root = ./.; fileset = fileFilter (file: file.type != "regular") ./.; }' \
+    'toSource { root = ./.; fileset = union ./d/b ./d/c; }'
+expectEqual \
+    'toSource { root = ./.; fileset = fileFilter (file: file.type != "symlink") ./.; }' \
+    'toSource { root = ./.; fileset = union ./d/a ./d/c; }'
+expectEqual \
+    'toSource { root = ./.; fileset = fileFilter (file: file.type != "unknown") ./.; }' \
+    'toSource { root = ./.; fileset = union ./d/a ./d/b; }'
+rm -rf -- *
+
+# It's lazy
+tree=(
+    [b]=1
+    [c/a]=1
+)
+# Note that union evaluates the first argument first if necessary, that's why we can use ./c/a here
+checkFileset 'union ./c/a (fileFilter (file: assert file.name != "a"; true) ./.)'
+# but here we need to use ./c
+checkFileset 'union (fileFilter (file: assert file.name != "a"; true) ./.) ./c'
+
 ## Tracing
 
 # The second trace argument is returned
diff --git a/lib/options.nix b/lib/options.nix
index c42bc1e6c67e3..7821924873dc6 100644
--- a/lib/options.nix
+++ b/lib/options.nix
@@ -109,7 +109,13 @@ rec {
 
      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 (usually `pkgs` in a module;
+     alternatively to nixpkgs itself, another package set) as the first argument.
+
+     If you pass another package set you should set the `pkgsText` option.
+     This option is used to display the expression for the package set. It is `"pkgs"` by default.
+     If your expression is complex you should parenthesize it, as the `pkgsText` argument
+     is usually immediately followed by an attribute lookup (`.`).
 
      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
@@ -118,68 +124,100 @@ rec {
 
      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.
 
-     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).
+     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 also set `default` to just a string in which case it is interpreted as an attribute name
+     (a singleton attribute path, if you will).
 
      If you wish to explicitly provide no default, pass `null` as `default`.
 
-     Type: mkPackageOption :: pkgs -> (string|[string]) -> { default? :: [string], example? :: null|string|[string], extraDescription? :: string } -> option
+     If you want users to be able to set no package, pass `nullable = true`.
+     In this mode a `default = null` will not be interpreted as no default and is interpreted literally.
+
+     Type: mkPackageOption :: pkgs -> (string|[string]) -> { nullable? :: bool, default? :: string|[string], example? :: null|string|[string], extraDescription? :: string, pkgsText? :: string } -> option
 
      Example:
        mkPackageOption pkgs "hello" { }
-       => { _type = "option"; default = «derivation /nix/store/3r2vg51hlxj3cx5vscp0vkv60bqxkaq0-hello-2.10.drv»; defaultText = { ... }; description = "The hello package to use."; type = { ... }; }
+       => { ...; default = pkgs.hello; defaultText = literalExpression "pkgs.hello"; description = "The hello package to use."; type = package; }
 
      Example:
        mkPackageOption pkgs "GHC" {
          default = [ "ghc" ];
          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 = { ... }; }
+       => { ...; default = pkgs.ghc; defaultText = literalExpression "pkgs.ghc"; description = "The GHC package to use."; example = literalExpression "pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])"; type = package; }
 
      Example:
-       mkPackageOption pkgs [ "python39Packages" "pytorch" ] {
+       mkPackageOption pkgs [ "python3Packages" "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 = { ... }; }
+       => { ...; default = pkgs.python3Packages.pytorch; defaultText = literalExpression "pkgs.python3Packages.pytorch"; description = "The pytorch package to use. This is an example and doesn't actually do anything."; type = package; }
+
+     Example:
+       mkPackageOption pkgs "nushell" {
+         nullable = true;
+       }
+       => { ...; default = pkgs.nushell; defaultText = literalExpression "pkgs.nushell"; description = "The nushell package to use."; type = nullOr package; }
+
+     Example:
+       mkPackageOption pkgs "coreutils" {
+         default = null;
+       }
+       => { ...; description = "The coreutils package to use."; type = package; }
+
+     Example:
+       mkPackageOption pkgs "dbus" {
+         nullable = true;
+         default = null;
+       }
+       => { ...; default = null; description = "The dbus package to use."; type = nullOr package; }
 
+     Example:
+       mkPackageOption pkgs.javaPackages "OpenJFX" {
+         default = "openjfx20";
+         pkgsText = "pkgs.javaPackages";
+       }
+       => { ...; default = pkgs.javaPackages.openjfx20; defaultText = literalExpression "pkgs.javaPackages.openjfx20"; description = "The OpenJFX package to use."; type = package; }
   */
   mkPackageOption =
-    # Package set (a specific version of nixpkgs or a subset)
+    # Package set (an instantiation of nixpkgs such as pkgs in modules or another package set)
     pkgs:
       # Name for the package, shown in option description
       name:
       {
-        # Whether the package can be null, for example to disable installing a package altogether.
+        # Whether the package can be null, for example to disable installing a package altogether (defaults to false)
         nullable ? false,
-        # The attribute path where the default package is located (may be omitted)
+        # The attribute path where the default package is located (may be omitted, in which case it is copied from `name`)
         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 ? "",
+        # Representation of the package set passed as pkgs (defaults to `"pkgs"`)
+        pkgsText ? "pkgs"
       }:
       let
         name' = if isList name then last name else name;
-      in mkOption ({
-        type = with lib.types; (if nullable then nullOr else lib.id) package;
-        description = "The ${name'} package to use."
-          + (if extraDescription == "" then "" else " ") + extraDescription;
-      } // (if default != null then let
         default' = if isList default then default else [ default ];
-        defaultPath = concatStringsSep "." default';
+        defaultText = concatStringsSep "." default';
         defaultValue = attrByPath default'
-          (throw "${defaultPath} cannot be found in pkgs") pkgs;
-      in {
-        default = defaultValue;
-        defaultText = literalExpression ("pkgs." + defaultPath);
-      } else if nullable then {
-        default = null;
-      } else { }) // lib.optionalAttrs (example != null) {
+          (throw "${defaultText} cannot be found in ${pkgsText}") pkgs;
+        defaults = if default != null then {
+          default = defaultValue;
+          defaultText = literalExpression ("${pkgsText}." + defaultText);
+        } else optionalAttrs nullable {
+          default = null;
+        };
+      in mkOption (defaults // {
+        description = "The ${name'} package to use."
+          + (if extraDescription == "" then "" else " ") + extraDescription;
+        type = with lib.types; (if nullable then nullOr else lib.id) package;
+      } // optionalAttrs (example != null) {
         example = literalExpression
-          (if isList example then "pkgs." + concatStringsSep "." example else example);
+          (if isList example then "${pkgsText}." + concatStringsSep "." example else example);
       });
 
   /* Alias of mkPackageOption. Previously used to create options with markdown
diff --git a/lib/systems/parse.nix b/lib/systems/parse.nix
index 34bfd94b3ce50..b69ad669e1874 100644
--- a/lib/systems/parse.nix
+++ b/lib/systems/parse.nix
@@ -29,6 +29,15 @@ let
       assert type.check value;
       setType type.name ({ inherit name; } // value));
 
+  # gnu-config will ignore the portion of a triple matching the
+  # regex `e?abi.*$` when determining the validity of a triple.  In
+  # other words, `i386-linuxabichickenlips` is a valid triple.
+  removeAbiSuffix = x:
+    let match = builtins.match "(.*)e?abi.*" x;
+    in if match==null
+       then x
+       else lib.elemAt match 0;
+
 in
 
 rec {
@@ -466,7 +475,7 @@ rec {
         else                     vendors.unknown;
       kernel = if hasPrefix "darwin" args.kernel      then getKernel "darwin"
                else if hasPrefix "netbsd" args.kernel then getKernel "netbsd"
-               else                                   getKernel args.kernel;
+               else                                   getKernel (removeAbiSuffix args.kernel);
       abi =
         /**/ if args ? abi       then getAbi args.abi
         else if isLinux parsed || isWindows parsed then
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index 05c99e6de83ce..21d4978a11609 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -227,8 +227,16 @@ checkConfigOutput '^false$' config.enableAlias ./alias-with-priority-can-overrid
 
 # Check mkPackageOption
 checkConfigOutput '^"hello"$' config.package.pname ./declare-mkPackageOption.nix
+checkConfigOutput '^"hello"$' config.namedPackage.pname ./declare-mkPackageOption.nix
+checkConfigOutput '^".*Hello.*"$' options.namedPackage.description ./declare-mkPackageOption.nix
+checkConfigOutput '^"hello"$' config.pathPackage.pname ./declare-mkPackageOption.nix
+checkConfigOutput '^"pkgs\.hello\.override \{ stdenv = pkgs\.clangStdenv; \}"$' options.packageWithExample.example.text ./declare-mkPackageOption.nix
+checkConfigOutput '^".*Example extra description\..*"$' options.packageWithExtraDescription.description ./declare-mkPackageOption.nix
 checkConfigError 'The option .undefinedPackage. is used but not defined' config.undefinedPackage ./declare-mkPackageOption.nix
 checkConfigOutput '^null$' config.nullablePackage ./declare-mkPackageOption.nix
+checkConfigOutput '^"null or package"$' options.nullablePackageWithDefault.type.description ./declare-mkPackageOption.nix
+checkConfigOutput '^"myPkgs\.hello"$' options.packageWithPkgsText.defaultText.text ./declare-mkPackageOption.nix
+checkConfigOutput '^"hello-other"$' options.packageFromOtherSet.default.pname ./declare-mkPackageOption.nix
 
 # submoduleWith
 
diff --git a/lib/tests/modules/declare-mkPackageOption.nix b/lib/tests/modules/declare-mkPackageOption.nix
index 640b19a7bf22f..e13e68447e096 100644
--- a/lib/tests/modules/declare-mkPackageOption.nix
+++ b/lib/tests/modules/declare-mkPackageOption.nix
@@ -7,6 +7,28 @@ in {
   options = {
     package = lib.mkPackageOption pkgs "hello" { };
 
+    namedPackage = lib.mkPackageOption pkgs "Hello" {
+      default = [ "hello" ];
+    };
+
+    namedPackageSingletonDefault = lib.mkPackageOption pkgs "Hello" {
+      default = "hello";
+    };
+
+    pathPackage = lib.mkPackageOption pkgs [ "hello" ] { };
+
+    packageWithExample = lib.mkPackageOption pkgs "hello" {
+      example = "pkgs.hello.override { stdenv = pkgs.clangStdenv; }";
+    };
+
+    packageWithPathExample = lib.mkPackageOption pkgs "hello" {
+      example = [ "hello" ];
+    };
+
+    packageWithExtraDescription = lib.mkPackageOption pkgs "hello" {
+      extraDescription = "Example extra description.";
+    };
+
     undefinedPackage = lib.mkPackageOption pkgs "hello" {
       default = null;
     };
@@ -15,5 +37,17 @@ in {
       nullable = true;
       default = null;
     };
+
+    nullablePackageWithDefault = lib.mkPackageOption pkgs "hello" {
+      nullable = true;
+    };
+
+    packageWithPkgsText = lib.mkPackageOption pkgs "hello" {
+      pkgsText = "myPkgs";
+    };
+
+    packageFromOtherSet = let myPkgs = {
+      hello = pkgs.hello // { pname = "hello-other"; };
+    }; in lib.mkPackageOption myPkgs "hello" { };
   };
 }