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