nixpkgs-suyu/lib/options.nix

266 lines
9 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
elemAt
filter
foldl'
head
isAttrs
isBool
isDerivation
isFunction
isInt
isList
isString
length
mapAttrs
optional
optionals
take
;
inherit (lib.attrsets)
optionalAttrs
;
inherit (lib.strings)
concatMapStrings
concatStringsSep
;
inherit (lib.types)
mkOptionType
;
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 { defaultText = "foo"; } // => { _type = "option"; defaultText = "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.
visible ? null,
# Whether the option can be set only once
readOnly ? null,
# Deprecated, used by types.optionSet.
options ? 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;
};
/* 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 = loc: defs:
if defs == [] then abort "This case should never happen."
else if length defs != 1 then
throw "The unique option `${showOption loc}' is defined multiple times. Definition values:${showDefs defs}"
else (head defs).value;
/* "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 ]}"
else
first) (head defs) 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' = prefix: options:
concatMap (opt:
2013-10-28 14:25:58 +01:00
let
docOption = rec {
loc = opt.loc;
2013-10-28 14:25:58 +01:00
name = showOption opt.loc;
description = opt.description or (lib.warn "Option `${name}' has no description." "This option has no description.");
2013-10-28 14:25:58 +01:00
declarations = filter (x: x != unknownModule) opt.declarations;
internal = opt.internal or false;
visible = opt.visible or true;
readOnly = opt.readOnly or false;
2016-09-07 03:03:32 +02:00
type = opt.type.description or null;
2013-10-28 14:25:58 +01:00
}
// optionalAttrs (opt ? example) { example = scrubOptionValue opt.example; }
// optionalAttrs (opt ? default) { default = scrubOptionValue opt.default; }
// optionalAttrs (opt ? defaultText) { default = opt.defaultText; }
// 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 [];
in
[ docOption ] ++ optionals docOption.visible 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.
*/
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;
/* For use in the `example` option attribute. It causes the given
text to be included verbatim in documentation. This is necessary
for example values that are not simple values, e.g., functions.
*/
literalExample = text: { _type = "literalExample"; inherit text; };
# Helper functions.
/* Convert an option, described as a list of the option parts in to a
safe, human readable version.
Example:
(showOption ["foo" "bar" "baz"]) == "foo.bar.baz"
2020-04-14 20:34:47 +02:00
(showOption ["foo" "bar.baz" "tux"]) == "foo.bar.baz.tux"
Placeholders will not be quoted as they are not actual values:
(showOption ["foo" "*" "bar"]) == "foo.*.bar"
(showOption ["foo" "<name>" "bar"]) == "foo.<name>.bar"
Unlike attributes, options can also start with numbers:
(showOption ["windowManager" "2bwm" "enable"]) == "windowManager.2bwm.enable"
*/
showOption = parts: let
escapeOptionPart = part:
let
escaped = lib.strings.escapeNixString part;
in if escaped == "\"${part}\""
then part
else escaped;
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 {} 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;
2013-10-28 14:25:58 +01:00
unknownModule = "<unknown-file>";
}