1# Nixpkgs/NixOS option handling.
2{ lib }:
3
4let
5 inherit (lib)
6 all
7 collect
8 concatLists
9 concatMap
10 elemAt
11 filter
12 foldl'
13 head
14 isAttrs
15 isBool
16 isDerivation
17 isFunction
18 isInt
19 isList
20 isString
21 length
22 mapAttrs
23 optional
24 optionals
25 take
26 ;
27 inherit (lib.attrsets)
28 optionalAttrs
29 ;
30 inherit (lib.strings)
31 concatMapStrings
32 concatStringsSep
33 ;
34 inherit (lib.types)
35 mkOptionType
36 ;
37in
38rec {
39
40 /* Returns true when the given argument is an option
41
42 Type: isOption :: a -> bool
43
44 Example:
45 isOption 1 // => false
46 isOption (mkOption {}) // => true
47 */
48 isOption = lib.isType "option";
49
50 /* Creates an Option attribute set. mkOption accepts an attribute set with the following keys:
51
52 All keys default to `null` when not given.
53
54 Example:
55 mkOption { } // => { _type = "option"; }
56 mkOption { defaultText = "foo"; } // => { _type = "option"; defaultText = "foo"; }
57 */
58 mkOption =
59 {
60 # Default value used when no definition is given in the configuration.
61 default ? null,
62 # Textual representation of the default, for the manual.
63 defaultText ? null,
64 # Example value used in the manual.
65 example ? null,
66 # String describing the option.
67 description ? null,
68 # Related packages used in the manual (see `genRelatedPackages` in ../nixos/lib/make-options-doc/default.nix).
69 relatedPackages ? null,
70 # Option type, providing type-checking and value merging.
71 type ? null,
72 # Function that converts the option value to something else.
73 apply ? null,
74 # Whether the option is for NixOS developers only.
75 internal ? null,
76 # Whether the option shows up in the manual.
77 visible ? null,
78 # Whether the option can be set only once
79 readOnly ? null,
80 # Deprecated, used by types.optionSet.
81 options ? 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 = "Whether to enable ${name}.";
98 type = lib.types.bool;
99 };
100
101 /* This option accepts anything, but it does not produce any result.
102
103 This is useful for sharing a module across different module sets
104 without having to implement similar features as long as the
105 values of the options are not accessed. */
106 mkSinkUndeclaredOptions = attrs: mkOption ({
107 internal = true;
108 visible = false;
109 default = false;
110 description = "Sink for option definitions.";
111 type = mkOptionType {
112 name = "sink";
113 check = x: true;
114 merge = loc: defs: false;
115 };
116 apply = x: throw "Option value is not readable because the option is not declared.";
117 } // attrs);
118
119 mergeDefaultOption = loc: defs:
120 let list = getValues defs; in
121 if length list == 1 then head list
122 else if all isFunction list then x: mergeDefaultOption loc (map (f: f x) list)
123 else if all isList list then concatLists list
124 else if all isAttrs list then foldl' lib.mergeAttrs {} list
125 else if all isBool list then foldl' lib.or false list
126 else if all isString list then lib.concatStrings list
127 else if all isInt list && all (x: x == head list) list then head list
128 else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}";
129
130 mergeOneOption = loc: defs:
131 if defs == [] then abort "This case should never happen."
132 else if length defs != 1 then
133 throw "The unique option `${showOption loc}' is defined multiple times. Definition values:${showDefs defs}"
134 else (head defs).value;
135
136 /* "Merge" option definitions by checking that they all have the same value. */
137 mergeEqualOption = loc: defs:
138 if defs == [] then abort "This case should never happen."
139 # Return early if we only have one element
140 # This also makes it work for functions, because the foldl' below would try
141 # to compare the first element with itself, which is false for functions
142 else if length defs == 1 then (head defs).value
143 else (foldl' (first: def:
144 if def.value != first.value then
145 throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}"
146 else
147 first) (head defs) defs).value;
148
149 /* Extracts values of all "value" keys of the given list.
150
151 Type: getValues :: [ { value :: a } ] -> [a]
152
153 Example:
154 getValues [ { value = 1; } { value = 2; } ] // => [ 1 2 ]
155 getValues [ ] // => [ ]
156 */
157 getValues = map (x: x.value);
158
159 /* Extracts values of all "file" keys of the given list
160
161 Type: getFiles :: [ { file :: a } ] -> [a]
162
163 Example:
164 getFiles [ { file = "file1"; } { file = "file2"; } ] // => [ "file1" "file2" ]
165 getFiles [ ] // => [ ]
166 */
167 getFiles = map (x: x.file);
168
169 # Generate documentation template from the list of option declaration like
170 # the set generated with filterOptionSets.
171 optionAttrSetToDocList = optionAttrSetToDocList' [];
172
173 optionAttrSetToDocList' = prefix: options:
174 concatMap (opt:
175 let
176 docOption = rec {
177 loc = opt.loc;
178 name = showOption opt.loc;
179 description = opt.description or (lib.warn "Option `${name}' has no description." "This option has no description.");
180 declarations = filter (x: x != unknownModule) opt.declarations;
181 internal = opt.internal or false;
182 visible = opt.visible or true;
183 readOnly = opt.readOnly or false;
184 type = opt.type.description or null;
185 }
186 // optionalAttrs (opt ? example) { example = scrubOptionValue opt.example; }
187 // optionalAttrs (opt ? default) { default = scrubOptionValue opt.default; }
188 // optionalAttrs (opt ? defaultText) { default = opt.defaultText; }
189 // optionalAttrs (opt ? relatedPackages && opt.relatedPackages != null) { inherit (opt) relatedPackages; };
190
191 subOptions =
192 let ss = opt.type.getSubOptions opt.loc;
193 in if ss != {} then optionAttrSetToDocList' opt.loc ss else [];
194 in
195 [ docOption ] ++ optionals docOption.visible subOptions) (collect isOption options);
196
197
198 /* This function recursively removes all derivation attributes from
199 `x` except for the `name` attribute.
200
201 This is to make the generation of `options.xml` much more
202 efficient: the XML representation of derivations is very large
203 (on the order of megabytes) and is not actually used by the
204 manual generator.
205 */
206 scrubOptionValue = x:
207 if isDerivation x then
208 { type = "derivation"; drvPath = x.name; outPath = x.name; name = x.name; }
209 else if isList x then map scrubOptionValue x
210 else if isAttrs x then mapAttrs (n: v: scrubOptionValue v) (removeAttrs x ["_args"])
211 else x;
212
213
214 /* For use in the `example` option attribute. It causes the given
215 text to be included verbatim in documentation. This is necessary
216 for example values that are not simple values, e.g., functions.
217 */
218 literalExample = text: { _type = "literalExample"; inherit text; };
219
220 # Helper functions.
221
222 /* Convert an option, described as a list of the option parts in to a
223 safe, human readable version.
224
225 Example:
226 (showOption ["foo" "bar" "baz"]) == "foo.bar.baz"
227 (showOption ["foo" "bar.baz" "tux"]) == "foo.bar.baz.tux"
228
229 Placeholders will not be quoted as they are not actual values:
230 (showOption ["foo" "*" "bar"]) == "foo.*.bar"
231 (showOption ["foo" "<name>" "bar"]) == "foo.<name>.bar"
232
233 Unlike attributes, options can also start with numbers:
234 (showOption ["windowManager" "2bwm" "enable"]) == "windowManager.2bwm.enable"
235 */
236 showOption = parts: let
237 escapeOptionPart = part:
238 let
239 escaped = lib.strings.escapeNixString part;
240 in if escaped == "\"${part}\""
241 then part
242 else escaped;
243 in (concatStringsSep ".") (map escapeOptionPart parts);
244 showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files);
245
246 showDefs = defs: concatMapStrings (def:
247 let
248 # Pretty print the value for display, if successful
249 prettyEval = builtins.tryEval (lib.generators.toPretty {} def.value);
250 # Split it into its lines
251 lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value);
252 # Only display the first 5 lines, and indent them for better visibility
253 value = concatStringsSep "\n " (take 5 lines ++ optional (length lines > 5) "...");
254 result =
255 # Don't print any value if evaluating the value strictly fails
256 if ! prettyEval.success then ""
257 # Put it on a new line if it consists of multiple
258 else if length lines > 1 then ":\n " + value
259 else ": " + value;
260 in "\n- In `${def.file}'${result}"
261 ) defs;
262
263 unknownModule = "<unknown-file>";
264
265}