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