1# Nixpkgs/NixOS option handling.
2{ lib }:
3
4let
5 inherit (lib)
6 all
7 collect
8 concatLists
9 concatMap
10 concatMapStringsSep
11 filter
12 foldl'
13 head
14 tail
15 isAttrs
16 isBool
17 isDerivation
18 isFunction
19 isInt
20 isList
21 isString
22 length
23 mapAttrs
24 optional
25 optionals
26 take
27 ;
28 inherit (lib.attrsets)
29 attrByPath
30 optionalAttrs
31 ;
32 inherit (lib.strings)
33 concatMapStrings
34 concatStringsSep
35 ;
36 inherit (lib.types)
37 mkOptionType
38 ;
39 inherit (lib.lists)
40 last
41 ;
42 prioritySuggestion = ''
43 Use `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions.
44 '';
45in
46rec {
47
48 /* Returns true when the given argument is an option
49
50 Type: isOption :: a -> bool
51
52 Example:
53 isOption 1 // => false
54 isOption (mkOption {}) // => true
55 */
56 isOption = lib.isType "option";
57
58 /* Creates an Option attribute set. mkOption accepts an attribute set with the following keys:
59
60 All keys default to `null` when not given.
61
62 Example:
63 mkOption { } // => { _type = "option"; }
64 mkOption { default = "foo"; } // => { _type = "option"; default = "foo"; }
65 */
66 mkOption =
67 {
68 # Default value used when no definition is given in the configuration.
69 default ? null,
70 # Textual representation of the default, for the manual.
71 defaultText ? null,
72 # Example value used in the manual.
73 example ? null,
74 # String describing the option.
75 description ? null,
76 # Related packages used in the manual (see `genRelatedPackages` in ../nixos/lib/make-options-doc/default.nix).
77 relatedPackages ? null,
78 # Option type, providing type-checking and value merging.
79 type ? null,
80 # Function that converts the option value to something else.
81 apply ? null,
82 # Whether the option is for NixOS developers only.
83 internal ? null,
84 # 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.
85 visible ? null,
86 # Whether the option can be set only once
87 readOnly ? null,
88 } @ attrs:
89 attrs // { _type = "option"; };
90
91 /* Creates an Option attribute set for a boolean value option i.e an
92 option to be toggled on or off:
93
94 Example:
95 mkEnableOption "foo"
96 => { _type = "option"; default = false; description = "Whether to enable foo."; example = true; type = { ... }; }
97 */
98 mkEnableOption =
99 # Name for the created option
100 name: mkOption {
101 default = false;
102 example = true;
103 description =
104 if name ? _type && name._type == "mdDoc"
105 then lib.mdDoc "Whether to enable ${name.text}."
106 else "Whether to enable ${name}.";
107 type = lib.types.bool;
108 };
109
110 /* Creates an Option attribute set for an option that specifies the
111 package a module should use for some purpose.
112
113 The package is specified in the third argument under `default` as a list of strings
114 representing its attribute path in nixpkgs (or another package set).
115 Because of this, you need to pass nixpkgs itself (or a subset) as the first argument.
116
117 The second argument may be either a string or a list of strings.
118 It provides the display name of the package in the description of the generated option
119 (using only the last element if the passed value is a list)
120 and serves as the fallback value for the `default` argument.
121
122 To include extra information in the description, pass `extraDescription` to
123 append arbitrary text to the generated description.
124 You can also pass an `example` value, either a literal string or an attribute path.
125
126 The default argument can be omitted if the provided name is
127 an attribute of pkgs (if name is a string) or a
128 valid attribute path in pkgs (if name is a list).
129
130 If you wish to explicitly provide no default, pass `null` as `default`.
131
132 Type: mkPackageOption :: pkgs -> (string|[string]) -> { default? :: [string], example? :: null|string|[string], extraDescription? :: string } -> option
133
134 Example:
135 mkPackageOption pkgs "hello" { }
136 => { _type = "option"; default = «derivation /nix/store/3r2vg51hlxj3cx5vscp0vkv60bqxkaq0-hello-2.10.drv»; defaultText = { ... }; description = "The hello package to use."; type = { ... }; }
137
138 Example:
139 mkPackageOption pkgs "GHC" {
140 default = [ "ghc" ];
141 example = "pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])";
142 }
143 => { _type = "option"; default = «derivation /nix/store/jxx55cxsjrf8kyh3fp2ya17q99w7541r-ghc-8.10.7.drv»; defaultText = { ... }; description = "The GHC package to use."; example = { ... }; type = { ... }; }
144
145 Example:
146 mkPackageOption pkgs [ "python39Packages" "pytorch" ] {
147 extraDescription = "This is an example and doesn't actually do anything.";
148 }
149 => { _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 = { ... }; }
150
151 */
152 mkPackageOption =
153 # Package set (a specific version of nixpkgs or a subset)
154 pkgs:
155 # Name for the package, shown in option description
156 name:
157 {
158 # Whether the package can be null, for example to disable installing a package altogether.
159 nullable ? false,
160 # The attribute path where the default package is located (may be omitted)
161 default ? name,
162 # A string or an attribute path to use as an example (may be omitted)
163 example ? null,
164 # Additional text to include in the option description (may be omitted)
165 extraDescription ? "",
166 }:
167 let
168 name' = if isList name then last name else name;
169 in mkOption ({
170 type = with lib.types; (if nullable then nullOr else lib.id) package;
171 description = "The ${name'} package to use."
172 + (if extraDescription == "" then "" else " ") + extraDescription;
173 } // (if default != null then let
174 default' = if isList default then default else [ default ];
175 defaultPath = concatStringsSep "." default';
176 defaultValue = attrByPath default'
177 (throw "${defaultPath} cannot be found in pkgs") pkgs;
178 in {
179 default = defaultValue;
180 defaultText = literalExpression ("pkgs." + defaultPath);
181 } else if nullable then {
182 default = null;
183 } else { }) // lib.optionalAttrs (example != null) {
184 example = literalExpression
185 (if isList example then "pkgs." + concatStringsSep "." example else example);
186 });
187
188 /* Like mkPackageOption, but emit an mdDoc description instead of DocBook. */
189 mkPackageOptionMD = pkgs: name: extra:
190 let option = mkPackageOption pkgs name extra;
191 in option // { description = lib.mdDoc option.description; };
192
193 /* This option accepts anything, but it does not produce any result.
194
195 This is useful for sharing a module across different module sets
196 without having to implement similar features as long as the
197 values of the options are not accessed. */
198 mkSinkUndeclaredOptions = attrs: mkOption ({
199 internal = true;
200 visible = false;
201 default = false;
202 description = "Sink for option definitions.";
203 type = mkOptionType {
204 name = "sink";
205 check = x: true;
206 merge = loc: defs: false;
207 };
208 apply = x: throw "Option value is not readable because the option is not declared.";
209 } // attrs);
210
211 mergeDefaultOption = loc: defs:
212 let list = getValues defs; in
213 if length list == 1 then head list
214 else if all isFunction list then x: mergeDefaultOption loc (map (f: f x) list)
215 else if all isList list then concatLists list
216 else if all isAttrs list then foldl' lib.mergeAttrs {} list
217 else if all isBool list then foldl' lib.or false list
218 else if all isString list then lib.concatStrings list
219 else if all isInt list && all (x: x == head list) list then head list
220 else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}";
221
222 mergeOneOption = mergeUniqueOption { message = ""; };
223
224 mergeUniqueOption = { message }: loc: defs:
225 if length defs == 1
226 then (head defs).value
227 else assert length defs > 1;
228 throw "The option `${showOption loc}' is defined multiple times while it's expected to be unique.\n${message}\nDefinition values:${showDefs defs}\n${prioritySuggestion}";
229
230 /* "Merge" option definitions by checking that they all have the same value. */
231 mergeEqualOption = loc: defs:
232 if defs == [] then abort "This case should never happen."
233 # Return early if we only have one element
234 # This also makes it work for functions, because the foldl' below would try
235 # to compare the first element with itself, which is false for functions
236 else if length defs == 1 then (head defs).value
237 else (foldl' (first: def:
238 if def.value != first.value then
239 throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}\n${prioritySuggestion}"
240 else
241 first) (head defs) (tail defs)).value;
242
243 /* Extracts values of all "value" keys of the given list.
244
245 Type: getValues :: [ { value :: a; } ] -> [a]
246
247 Example:
248 getValues [ { value = 1; } { value = 2; } ] // => [ 1 2 ]
249 getValues [ ] // => [ ]
250 */
251 getValues = map (x: x.value);
252
253 /* Extracts values of all "file" keys of the given list
254
255 Type: getFiles :: [ { file :: a; } ] -> [a]
256
257 Example:
258 getFiles [ { file = "file1"; } { file = "file2"; } ] // => [ "file1" "file2" ]
259 getFiles [ ] // => [ ]
260 */
261 getFiles = map (x: x.file);
262
263 # Generate documentation template from the list of option declaration like
264 # the set generated with filterOptionSets.
265 optionAttrSetToDocList = optionAttrSetToDocList' [];
266
267 optionAttrSetToDocList' = _: options:
268 concatMap (opt:
269 let
270 name = showOption opt.loc;
271 docOption = {
272 loc = opt.loc;
273 inherit name;
274 description = opt.description or null;
275 declarations = filter (x: x != unknownModule) opt.declarations;
276 internal = opt.internal or false;
277 visible =
278 if (opt?visible && opt.visible == "shallow")
279 then true
280 else opt.visible or true;
281 readOnly = opt.readOnly or false;
282 type = opt.type.description or "unspecified";
283 }
284 // optionalAttrs (opt ? example) {
285 example =
286 builtins.addErrorContext "while evaluating the example of option `${name}`" (
287 renderOptionValue opt.example
288 );
289 }
290 // optionalAttrs (opt ? defaultText || opt ? default) {
291 default =
292 builtins.addErrorContext "while evaluating the ${if opt?defaultText then "defaultText" else "default value"} of option `${name}`" (
293 renderOptionValue (opt.defaultText or opt.default)
294 );
295 }
296 // optionalAttrs (opt ? relatedPackages && opt.relatedPackages != null) { inherit (opt) relatedPackages; };
297
298 subOptions =
299 let ss = opt.type.getSubOptions opt.loc;
300 in if ss != {} then optionAttrSetToDocList' opt.loc ss else [];
301 subOptionsVisible = docOption.visible && opt.visible or null != "shallow";
302 in
303 # To find infinite recursion in NixOS option docs:
304 # builtins.trace opt.loc
305 [ docOption ] ++ optionals subOptionsVisible subOptions) (collect isOption options);
306
307
308 /* This function recursively removes all derivation attributes from
309 `x` except for the `name` attribute.
310
311 This is to make the generation of `options.xml` much more
312 efficient: the XML representation of derivations is very large
313 (on the order of megabytes) and is not actually used by the
314 manual generator.
315
316 This function was made obsolete by renderOptionValue and is kept for
317 compatibility with out-of-tree code.
318 */
319 scrubOptionValue = x:
320 if isDerivation x then
321 { type = "derivation"; drvPath = x.name; outPath = x.name; name = x.name; }
322 else if isList x then map scrubOptionValue x
323 else if isAttrs x then mapAttrs (n: v: scrubOptionValue v) (removeAttrs x ["_args"])
324 else x;
325
326
327 /* Ensures that the given option value (default or example) is a `_type`d string
328 by rendering Nix values to `literalExpression`s.
329 */
330 renderOptionValue = v:
331 if v ? _type && v ? text then v
332 else literalExpression (lib.generators.toPretty {
333 multiline = true;
334 allowPrettyValues = true;
335 } v);
336
337
338 /* For use in the `defaultText` and `example` option attributes. Causes the
339 given string to be rendered verbatim in the documentation as Nix code. This
340 is necessary for complex values, e.g. functions, or values that depend on
341 other values or packages.
342 */
343 literalExpression = text:
344 if ! isString text then throw "literalExpression expects a string."
345 else { _type = "literalExpression"; inherit text; };
346
347 literalExample = lib.warn "literalExample is deprecated, use literalExpression instead, or use literalDocBook for a non-Nix description." literalExpression;
348
349
350 /* For use in the `defaultText` and `example` option attributes. Causes the
351 given DocBook text to be inserted verbatim in the documentation, for when
352 a `literalExpression` would be too hard to read.
353 */
354 literalDocBook = text:
355 if ! isString text then throw "literalDocBook expects a string."
356 else
357 lib.warnIf (lib.isInOldestRelease 2211)
358 "literalDocBook is deprecated, use literalMD instead"
359 { _type = "literalDocBook"; inherit text; };
360
361 /* Transition marker for documentation that's already migrated to markdown
362 syntax.
363 */
364 mdDoc = text:
365 if ! isString text then throw "mdDoc expects a string."
366 else { _type = "mdDoc"; inherit text; };
367
368 /* For use in the `defaultText` and `example` option attributes. Causes the
369 given MD text to be inserted verbatim in the documentation, for when
370 a `literalExpression` would be too hard to read.
371 */
372 literalMD = text:
373 if ! isString text then throw "literalMD expects a string."
374 else { _type = "literalMD"; inherit text; };
375
376 # Helper functions.
377
378 /* Convert an option, described as a list of the option parts to a
379 human-readable version.
380
381 Example:
382 (showOption ["foo" "bar" "baz"]) == "foo.bar.baz"
383 (showOption ["foo" "bar.baz" "tux"]) == "foo.\"bar.baz\".tux"
384 (showOption ["windowManager" "2bwm" "enable"]) == "windowManager.\"2bwm\".enable"
385
386 Placeholders will not be quoted as they are not actual values:
387 (showOption ["foo" "*" "bar"]) == "foo.*.bar"
388 (showOption ["foo" "<name>" "bar"]) == "foo.<name>.bar"
389 */
390 showOption = parts: let
391 escapeOptionPart = part:
392 let
393 # We assume that these are "special values" and not real configuration data.
394 # If it is real configuration data, it is rendered incorrectly.
395 specialIdentifiers = [
396 "<name>" # attrsOf (submodule {})
397 "*" # listOf (submodule {})
398 "<function body>" # functionTo
399 ];
400 in if builtins.elem part specialIdentifiers
401 then part
402 else lib.strings.escapeNixIdentifier part;
403 in (concatStringsSep ".") (map escapeOptionPart parts);
404 showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files);
405
406 showDefs = defs: concatMapStrings (def:
407 let
408 # Pretty print the value for display, if successful
409 prettyEval = builtins.tryEval
410 (lib.generators.toPretty { }
411 (lib.generators.withRecursion { depthLimit = 10; throwOnDepthLimit = false; } def.value));
412 # Split it into its lines
413 lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value);
414 # Only display the first 5 lines, and indent them for better visibility
415 value = concatStringsSep "\n " (take 5 lines ++ optional (length lines > 5) "...");
416 result =
417 # Don't print any value if evaluating the value strictly fails
418 if ! prettyEval.success then ""
419 # Put it on a new line if it consists of multiple
420 else if length lines > 1 then ":\n " + value
421 else ": " + value;
422 in "\n- In `${def.file}'${result}"
423 ) defs;
424
425 showOptionWithDefLocs = opt: ''
426 ${showOption opt.loc}, with values defined in:
427 ${concatMapStringsSep "\n" (defFile: " - ${defFile}") opt.files}
428 '';
429
430 unknownModule = "<unknown-file>";
431
432}