nixpkgs-suyu/lib/options.nix

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

454 lines
18 KiB
Nix
Raw Normal View History

# Nixpkgs/NixOS option handling.
Convert libs to a fixed-point This does break the API of being able to import any lib file and get its libs, however I'm not sure people did this. I made this while exploring being able to swap out docFn with a stub in #2305, to avoid functor performance problems. I don't know if that is going to move forward (or if it is a problem or not,) but after doing all this work figured I'd put it up anyway :) Two notable advantages to this approach: 1. when a lib inherits another lib's functions, it doesn't automatically get put in to the scope of lib 2. when a lib implements a new obscure functions, it doesn't automatically get put in to the scope of lib Using the test script (later in this commit) I got the following diff on the API: + diff master fixed-lib 11764a11765,11766 > .types.defaultFunctor > .types.defaultTypeMerge 11774a11777,11778 > .types.isOptionType > .types.isType 11781a11786 > .types.mkOptionType 11788a11794 > .types.setType 11795a11802 > .types.types This means that this commit _adds_ to the API, however I can't find a way to fix these last remaining discrepancies. At least none are _removed_. Test script (run with nix-repl in the PATH): #!/bin/sh set -eux repl() { suff=${1:-} echo "(import ./lib)$suff" \ | nix-repl 2>&1 } attrs_to_check() { repl "${1:-}" \ | tr ';' $'\n' \ | grep "\.\.\." \ | cut -d' ' -f2 \ | sed -e "s/^/${1:-}./" \ | sort } summ() { repl "${1:-}" \ | tr ' ' $'\n' \ | sort \ | uniq } deep_summ() { suff="${1:-}" depth="${2:-4}" depth=$((depth - 1)) summ "$suff" for attr in $(attrs_to_check "$suff" | grep -v "types.types"); do if [ $depth -eq 0 ]; then summ "$attr" | sed -e "s/^/$attr./" else deep_summ "$attr" "$depth" | sed -e "s/^/$attr./" fi done } ( cd nixpkgs #git add . #git commit -m "Auto-commit, sorry" || true git checkout fixed-lib deep_summ > ../fixed-lib git checkout master deep_summ > ../master ) if diff master fixed-lib; then echo "SHALLOW MATCH!" fi ( cd nixpkgs git checkout fixed-lib repl .types )
2017-07-29 02:05:35 +02:00
{ lib }:
let
inherit (lib)
all
collect
concatLists
concatMap
2022-06-15 13:11:22 +02:00
concatMapStringsSep
filter
foldl'
head
tail
isAttrs
isBool
isDerivation
isFunction
isInt
isList
isString
length
mapAttrs
optional
optionals
take
;
inherit (lib.attrsets)
attrByPath
optionalAttrs
;
inherit (lib.strings)
concatMapStrings
concatStringsSep
;
inherit (lib.types)
mkOptionType
;
inherit (lib.lists)
last
;
prioritySuggestion = ''
Use `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions.
'';
in
rec {
/* Returns true when the given argument is an option
Type: isOption :: a -> bool
Example:
isOption 1 // => false
isOption (mkOption {}) // => true
*/
isOption = lib.isType "option";
/* Creates an Option attribute set. mkOption accepts an attribute set with the following keys:
All keys default to `null` when not given.
Example:
mkOption { } // => { _type = "option"; }
mkOption { default = "foo"; } // => { _type = "option"; default = "foo"; }
*/
mkOption =
{
# Default value used when no definition is given in the configuration.
default ? null,
# Textual representation of the default, for the manual.
defaultText ? null,
# Example value used in the manual.
example ? null,
# String describing the option.
description ? null,
2019-08-26 02:01:49 +02:00
# Related packages used in the manual (see `genRelatedPackages` in ../nixos/lib/make-options-doc/default.nix).
relatedPackages ? null,
# Option type, providing type-checking and value merging.
type ? null,
# Function that converts the option value to something else.
apply ? null,
# Whether the option is for NixOS developers only.
internal ? null,
# Whether the option shows up in the manual. Default: true. Use false to hide the option and any sub-options from submodules. Use "shallow" to hide only sub-options.
visible ? null,
# Whether the option can be set only once
readOnly ? null,
} @ attrs:
attrs // { _type = "option"; };
/* Creates an Option attribute set for a boolean value option i.e an
option to be toggled on or off:
Example:
mkEnableOption "foo"
=> { _type = "option"; default = false; description = "Whether to enable foo."; example = true; type = { ... }; }
*/
mkEnableOption =
# Name for the created option
name: mkOption {
default = false;
example = true;
description = "Whether to enable ${name}.";
type = lib.types.bool;
};
/* Creates an Option attribute set for an option that specifies the
package a module should use for some purpose.
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 (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
(using only the last element if the passed value is a list)
and serves as the fallback value for the `default` argument.
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).
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`.
2022-12-24 14:08:11 +01:00
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" { }
=> { ...; default = pkgs.hello; defaultText = literalExpression "pkgs.hello"; description = "The hello package to use."; type = package; }
Example:
mkPackageOption pkgs "GHC" {
default = [ "ghc" ];
2022-11-07 15:25:37 +01:00
example = "pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])";
}
=> { ...; 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 [ "python3Packages" "pytorch" ] {
extraDescription = "This is an example and doesn't actually do anything.";
}
=> { ...; 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 (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 (defaults to false)
nullable ? false,
# 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;
default' = if isList default then default else [ default ];
2023-10-06 12:51:23 +02:00
defaultText = concatStringsSep "." default';
defaultValue = attrByPath default'
2023-10-06 12:51:23 +02:00
(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 "${pkgsText}." + concatStringsSep "." example else example);
});
/* Alias of mkPackageOption. Previously used to create options with markdown
documentation, which is no longer required.
*/
mkPackageOptionMD = mkPackageOption;
/* This option accepts anything, but it does not produce any result.
This is useful for sharing a module across different module sets
without having to implement similar features as long as the
values of the options are not accessed. */
2014-12-21 14:50:46 +01:00
mkSinkUndeclaredOptions = attrs: mkOption ({
internal = true;
visible = false;
default = false;
description = "Sink for option definitions.";
type = mkOptionType {
name = "sink";
check = x: true;
merge = loc: defs: false;
};
apply = x: throw "Option value is not readable because the option is not declared.";
} // attrs);
mergeDefaultOption = loc: defs:
let list = getValues defs; in
2012-08-13 20:19:31 +02:00
if length list == 1 then head list
2013-11-12 13:48:19 +01:00
else if all isFunction list then x: mergeDefaultOption loc (map (f: f x) list)
else if all isList list then concatLists list
else if all isAttrs list then foldl' lib.mergeAttrs {} list
else if all isBool list then foldl' lib.or false list
2013-11-12 13:48:19 +01:00
else if all isString list then lib.concatStrings list
else if all isInt list && all (x: x == head list) list then head list
else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}";
mergeOneOption = mergeUniqueOption { message = ""; };
mergeUniqueOption = { message }: loc: defs:
if length defs == 1
then (head defs).value
else assert length defs > 1;
throw "The option `${showOption loc}' is defined multiple times while it's expected to be unique.\n${message}\nDefinition values:${showDefs defs}\n${prioritySuggestion}";
/* "Merge" option definitions by checking that they all have the same value. */
mergeEqualOption = loc: defs:
if defs == [] then abort "This case should never happen."
# Return early if we only have one element
# This also makes it work for functions, because the foldl' below would try
# to compare the first element with itself, which is false for functions
else if length defs == 1 then (head defs).value
else (foldl' (first: def:
if def.value != first.value then
throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}\n${prioritySuggestion}"
else
first) (head defs) (tail defs)).value;
/* Extracts values of all "value" keys of the given list.
Type: getValues :: [ { value :: a; } ] -> [a]
Example:
getValues [ { value = 1; } { value = 2; } ] // => [ 1 2 ]
getValues [ ] // => [ ]
*/
getValues = map (x: x.value);
/* Extracts values of all "file" keys of the given list
Type: getFiles :: [ { file :: a; } ] -> [a]
Example:
getFiles [ { file = "file1"; } { file = "file2"; } ] // => [ "file1" "file2" ]
getFiles [ ] // => [ ]
*/
getFiles = map (x: x.file);
# Generate documentation template from the list of option declaration like
# the set generated with filterOptionSets.
2013-10-28 14:25:58 +01:00
optionAttrSetToDocList = optionAttrSetToDocList' [];
optionAttrSetToDocList' = _: options:
concatMap (opt:
2013-10-28 14:25:58 +01:00
let
name = showOption opt.loc;
docOption = {
loc = opt.loc;
inherit name;
description = opt.description or null;
2013-10-28 14:25:58 +01:00
declarations = filter (x: x != unknownModule) opt.declarations;
internal = opt.internal or false;
visible =
if (opt?visible && opt.visible == "shallow")
then true
else opt.visible or true;
readOnly = opt.readOnly or false;
type = opt.type.description or "unspecified";
2013-10-28 14:25:58 +01:00
}
// optionalAttrs (opt ? example) {
example =
builtins.addErrorContext "while evaluating the example of option `${name}`" (
renderOptionValue opt.example
);
}
// optionalAttrs (opt ? defaultText || opt ? default) {
default =
builtins.addErrorContext "while evaluating the ${if opt?defaultText then "defaultText" else "default value"} of option `${name}`" (
renderOptionValue (opt.defaultText or opt.default)
);
}
// optionalAttrs (opt ? relatedPackages && opt.relatedPackages != null) { inherit (opt) relatedPackages; };
2013-10-28 14:25:58 +01:00
subOptions =
let ss = opt.type.getSubOptions opt.loc;
in if ss != {} then optionAttrSetToDocList' opt.loc ss else [];
subOptionsVisible = docOption.visible && opt.visible or null != "shallow";
2013-10-28 14:25:58 +01:00
in
# To find infinite recursion in NixOS option docs:
# builtins.trace opt.loc
[ docOption ] ++ optionals subOptionsVisible subOptions) (collect isOption options);
/* This function recursively removes all derivation attributes from
`x` except for the `name` attribute.
This is to make the generation of `options.xml` much more
efficient: the XML representation of derivations is very large
(on the order of megabytes) and is not actually used by the
manual generator.
This function was made obsolete by renderOptionValue and is kept for
compatibility with out-of-tree code.
*/
scrubOptionValue = x:
2013-10-30 16:19:07 +01:00
if isDerivation x then
{ type = "derivation"; drvPath = x.name; outPath = x.name; name = x.name; }
else if isList x then map scrubOptionValue x
else if isAttrs x then mapAttrs (n: v: scrubOptionValue v) (removeAttrs x ["_args"])
else x;
/* Ensures that the given option value (default or example) is a `_type`d string
by rendering Nix values to `literalExpression`s.
*/
renderOptionValue = v:
if v ? _type && v ? text then v
else literalExpression (lib.generators.toPretty {
multiline = true;
allowPrettyValues = true;
} v);
/* For use in the `defaultText` and `example` option attributes. Causes the
given string to be rendered verbatim in the documentation as Nix code. This
is necessary for complex values, e.g. functions, or values that depend on
other values or packages.
*/
literalExpression = text:
if ! isString text then throw "literalExpression expects a string."
else { _type = "literalExpression"; inherit text; };
literalExample = lib.warn "literalExample is deprecated, use literalExpression instead, or use literalMD for a non-Nix description." literalExpression;
/* Transition marker for documentation that's already migrated to markdown
syntax. This is a no-op and no longer needed.
*/
mdDoc = lib.id;
/* For use in the `defaultText` and `example` option attributes. Causes the
given MD text to be inserted verbatim in the documentation, for when
a `literalExpression` would be too hard to read.
*/
literalMD = text:
if ! isString text then throw "literalMD expects a string."
else { _type = "literalMD"; inherit text; };
# Helper functions.
2023-02-18 18:13:11 +01:00
/* Convert an option, described as a list of the option parts to a
human-readable version.
Example:
(showOption ["foo" "bar" "baz"]) == "foo.bar.baz"
2023-02-18 18:13:11 +01:00
(showOption ["foo" "bar.baz" "tux"]) == "foo.\"bar.baz\".tux"
(showOption ["windowManager" "2bwm" "enable"]) == "windowManager.\"2bwm\".enable"
Placeholders will not be quoted as they are not actual values:
(showOption ["foo" "*" "bar"]) == "foo.*.bar"
(showOption ["foo" "<name>" "bar"]) == "foo.<name>.bar"
*/
showOption = parts: let
escapeOptionPart = part:
let
lib/options/showOption: fix quoting of attr-names that are not identifiers Personally, I think that warnings such as warning: The option `services.redis.enable' defined in `/home/ma27/Projects/nixpkgs/test.nix@node-vm' has been renamed to `services.redis.servers..enable'. are fairly confusing because of the `..` and it's more correct to actually quote that. With this change the warning now looks like this: warning: The option `services.redis.enable' defined in `/home/ma27/Projects/nixpkgs/test.nix@node-vm' has been renamed to `services.redis.servers."".enable'. While implementing that I realized that you'd have a similar problem whenever you use attribute-names that aren't identifiers, e.g. services.nginx.virtualHosts."example.org".locations."/".invalid = 23; now results in the following error: error: The option `interactive.nodes.vm.services.nginx.virtualHosts."example.org".locations."/".invalid' does not exist. Definition values: - In `/home/ma27/Projects/nixpkgs/test.nix@node-vm': 23 Of course there are some corner-cases where this won't work: when generating the manual, you display submodules like this: services.nginx.virtualHosts.<name> Since `<name>` isn't a value, but an indicator for a submodule, it must not be quoted. This also applies to the following identifiers: * `*` for `listOf submodule` * `<function body>` for `functionTo` This might not be correct if you actually have a submodule with an attribute name called `<name>`, but I think it's an improvement over the current situation and for this you'd probably need to make even more complex changes to the module system.
2022-10-07 09:59:01 +02:00
# We assume that these are "special values" and not real configuration data.
# If it is real configuration data, it is rendered incorrectly.
specialIdentifiers = [
"<name>" # attrsOf (submodule {})
"*" # listOf (submodule {})
"<function body>" # functionTo
];
in if builtins.elem part specialIdentifiers
then part
lib/options/showOption: fix quoting of attr-names that are not identifiers Personally, I think that warnings such as warning: The option `services.redis.enable' defined in `/home/ma27/Projects/nixpkgs/test.nix@node-vm' has been renamed to `services.redis.servers..enable'. are fairly confusing because of the `..` and it's more correct to actually quote that. With this change the warning now looks like this: warning: The option `services.redis.enable' defined in `/home/ma27/Projects/nixpkgs/test.nix@node-vm' has been renamed to `services.redis.servers."".enable'. While implementing that I realized that you'd have a similar problem whenever you use attribute-names that aren't identifiers, e.g. services.nginx.virtualHosts."example.org".locations."/".invalid = 23; now results in the following error: error: The option `interactive.nodes.vm.services.nginx.virtualHosts."example.org".locations."/".invalid' does not exist. Definition values: - In `/home/ma27/Projects/nixpkgs/test.nix@node-vm': 23 Of course there are some corner-cases where this won't work: when generating the manual, you display submodules like this: services.nginx.virtualHosts.<name> Since `<name>` isn't a value, but an indicator for a submodule, it must not be quoted. This also applies to the following identifiers: * `*` for `listOf submodule` * `<function body>` for `functionTo` This might not be correct if you actually have a submodule with an attribute name called `<name>`, but I think it's an improvement over the current situation and for this you'd probably need to make even more complex changes to the module system.
2022-10-07 09:59:01 +02:00
else lib.strings.escapeNixIdentifier part;
in (concatStringsSep ".") (map escapeOptionPart parts);
showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files);
showDefs = defs: concatMapStrings (def:
let
# Pretty print the value for display, if successful
prettyEval = builtins.tryEval
(lib.generators.toPretty { }
(lib.generators.withRecursion { depthLimit = 10; throwOnDepthLimit = false; } def.value));
# Split it into its lines
lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value);
# Only display the first 5 lines, and indent them for better visibility
value = concatStringsSep "\n " (take 5 lines ++ optional (length lines > 5) "...");
result =
# Don't print any value if evaluating the value strictly fails
if ! prettyEval.success then ""
# Put it on a new line if it consists of multiple
else if length lines > 1 then ":\n " + value
else ": " + value;
in "\n- In `${def.file}'${result}"
) defs;
2022-06-15 13:11:22 +02:00
showOptionWithDefLocs = opt: ''
${showOption opt.loc}, with values defined in:
${concatMapStringsSep "\n" (defFile: " - ${defFile}") opt.files}
'';
2013-10-28 14:25:58 +01:00
unknownModule = "<unknown-file>";
}