1# Definitions related to run-time type checking. Used in particular
2# to type-check NixOS configurations.
3{ lib }:
4
5let
6 inherit (lib)
7 elem
8 flip
9 functionArgs
10 isAttrs
11 isBool
12 isDerivation
13 isFloat
14 isFunction
15 isInt
16 isList
17 isString
18 isStorePath
19 setFunctionArgs
20 toDerivation
21 toList
22 ;
23 inherit (lib.lists)
24 all
25 concatLists
26 count
27 elemAt
28 filter
29 foldl'
30 head
31 imap1
32 last
33 length
34 tail
35 unique
36 ;
37 inherit (lib.attrsets)
38 attrNames
39 filterAttrs
40 hasAttr
41 mapAttrs
42 optionalAttrs
43 zipAttrsWith
44 ;
45 inherit (lib.options)
46 getFiles
47 getValues
48 mergeDefaultOption
49 mergeEqualOption
50 mergeOneOption
51 showFiles
52 showOption
53 ;
54 inherit (lib.strings)
55 concatMapStringsSep
56 concatStringsSep
57 escapeNixString
58 isCoercibleToString
59 ;
60 inherit (lib.trivial)
61 boolToString
62 ;
63
64 inherit (lib.modules) mergeDefinitions;
65 outer_types =
66rec {
67 isType = type: x: (x._type or "") == type;
68
69 setType = typeName: value: value // {
70 _type = typeName;
71 };
72
73
74 # Default type merging function
75 # takes two type functors and return the merged type
76 defaultTypeMerge = f: f':
77 let wrapped = f.wrapped.typeMerge f'.wrapped.functor;
78 payload = f.binOp f.payload f'.payload;
79 in
80 # cannot merge different types
81 if f.name != f'.name
82 then null
83 # simple types
84 else if (f.wrapped == null && f'.wrapped == null)
85 && (f.payload == null && f'.payload == null)
86 then f.type
87 # composed types
88 else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null)
89 then f.type wrapped
90 # value types
91 else if (f.payload != null && f'.payload != null) && (payload != null)
92 then f.type payload
93 else null;
94
95 # Default type functor
96 defaultFunctor = name: {
97 inherit name;
98 type = types.${name} or null;
99 wrapped = null;
100 payload = null;
101 binOp = a: b: null;
102 };
103
104 isOptionType = isType "option-type";
105 mkOptionType =
106 { # Human-readable representation of the type, should be equivalent to
107 # the type function name.
108 name
109 , # Description of the type, defined recursively by embedding the wrapped type if any.
110 description ? null
111 , # Function applied to each definition that should return true if
112 # its type-correct, false otherwise.
113 check ? (x: true)
114 , # Merge a list of definitions together into a single value.
115 # This function is called with two arguments: the location of
116 # the option in the configuration as a list of strings
117 # (e.g. ["boot" "loader "grub" "enable"]), and a list of
118 # definition values and locations (e.g. [ { file = "/foo.nix";
119 # value = 1; } { file = "/bar.nix"; value = 2 } ]).
120 merge ? mergeDefaultOption
121 , # Whether this type has a value representing nothingness. If it does,
122 # this should be a value of the form { value = <the nothing value>; }
123 # If it doesn't, this should be {}
124 # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`.
125 emptyValue ? {}
126 , # Return a flat list of sub-options. Used to generate
127 # documentation.
128 getSubOptions ? prefix: {}
129 , # List of modules if any, or null if none.
130 getSubModules ? null
131 , # Function for building the same option type with a different list of
132 # modules.
133 substSubModules ? m: null
134 , # Function that merge type declarations.
135 # internal, takes a functor as argument and returns the merged type.
136 # returning null means the type is not mergeable
137 typeMerge ? defaultTypeMerge functor
138 , # The type functor.
139 # internal, representation of the type as an attribute set.
140 # name: name of the type
141 # type: type function.
142 # wrapped: the type wrapped in case of compound types.
143 # payload: values of the type, two payloads of the same type must be
144 # combinable with the binOp binary operation.
145 # binOp: binary operation that merge two payloads of the same type.
146 functor ? defaultFunctor name
147 , # The deprecation message to display when this type is used by an option
148 # If null, the type isn't deprecated
149 deprecationMessage ? null
150 , # The types that occur in the definition of this type. This is used to
151 # issue deprecation warnings recursively. Can also be used to reuse
152 # nested types
153 nestedTypes ? {}
154 }:
155 { _type = "option-type";
156 inherit name check merge emptyValue getSubOptions getSubModules substSubModules typeMerge functor deprecationMessage nestedTypes;
157 description = if description == null then name else description;
158 };
159
160
161 # When adding new types don't forget to document them in
162 # nixos/doc/manual/development/option-types.xml!
163 types = rec {
164
165 anything = mkOptionType {
166 name = "anything";
167 description = "anything";
168 check = value: true;
169 merge = loc: defs:
170 let
171 getType = value:
172 if isAttrs value && isCoercibleToString value
173 then "stringCoercibleSet"
174 else builtins.typeOf value;
175
176 # Returns the common type of all definitions, throws an error if they
177 # don't have the same type
178 commonType = foldl' (type: def:
179 if getType def.value == type
180 then type
181 else throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}"
182 ) (getType (head defs).value) defs;
183
184 mergeFunction = {
185 # Recursively merge attribute sets
186 set = (attrsOf anything).merge;
187 # Safe and deterministic behavior for lists is to only accept one definition
188 # listOf only used to apply mkIf and co.
189 list =
190 if length defs > 1
191 then throw "The option `${showOption loc}' has conflicting definitions, in ${showFiles (getFiles defs)}."
192 else (listOf anything).merge;
193 # This is the type of packages, only accept a single definition
194 stringCoercibleSet = mergeOneOption;
195 lambda = loc: defs: arg: anything.merge
196 (loc ++ [ "<function body>" ])
197 (map (def: {
198 file = def.file;
199 value = def.value arg;
200 }) defs);
201 # Otherwise fall back to only allowing all equal definitions
202 }.${commonType} or mergeEqualOption;
203 in mergeFunction loc defs;
204 };
205
206 unspecified = mkOptionType {
207 name = "unspecified";
208 };
209
210 bool = mkOptionType {
211 name = "bool";
212 description = "boolean";
213 check = isBool;
214 merge = mergeEqualOption;
215 };
216
217 int = mkOptionType {
218 name = "int";
219 description = "signed integer";
220 check = isInt;
221 merge = mergeEqualOption;
222 };
223
224 # Specialized subdomains of int
225 ints =
226 let
227 betweenDesc = lowest: highest:
228 "${toString lowest} and ${toString highest} (both inclusive)";
229 between = lowest: highest:
230 assert lib.assertMsg (lowest <= highest)
231 "ints.between: lowest must be smaller than highest";
232 addCheck int (x: x >= lowest && x <= highest) // {
233 name = "intBetween";
234 description = "integer between ${betweenDesc lowest highest}";
235 };
236 ign = lowest: highest: name: docStart:
237 between lowest highest // {
238 inherit name;
239 description = docStart + "; between ${betweenDesc lowest highest}";
240 };
241 unsign = bit: range: ign 0 (range - 1)
242 "unsignedInt${toString bit}" "${toString bit} bit unsigned integer";
243 sign = bit: range: ign (0 - (range / 2)) (range / 2 - 1)
244 "signedInt${toString bit}" "${toString bit} bit signed integer";
245
246 in {
247 /* An int with a fixed range.
248 *
249 * Example:
250 * (ints.between 0 100).check (-1)
251 * => false
252 * (ints.between 0 100).check (101)
253 * => false
254 * (ints.between 0 0).check 0
255 * => true
256 */
257 inherit between;
258
259 unsigned = addCheck types.int (x: x >= 0) // {
260 name = "unsignedInt";
261 description = "unsigned integer, meaning >=0";
262 };
263 positive = addCheck types.int (x: x > 0) // {
264 name = "positiveInt";
265 description = "positive integer, meaning >0";
266 };
267 u8 = unsign 8 256;
268 u16 = unsign 16 65536;
269 # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808)
270 # the smallest int Nix accepts is -2^63 (-9223372036854775807)
271 u32 = unsign 32 4294967296;
272 # u64 = unsign 64 18446744073709551616;
273
274 s8 = sign 8 256;
275 s16 = sign 16 65536;
276 s32 = sign 32 4294967296;
277 };
278
279 # Alias of u16 for a port number
280 port = ints.u16;
281
282 float = mkOptionType {
283 name = "float";
284 description = "floating point number";
285 check = isFloat;
286 merge = mergeEqualOption;
287 };
288
289 str = mkOptionType {
290 name = "str";
291 description = "string";
292 check = isString;
293 merge = mergeEqualOption;
294 };
295
296 nonEmptyStr = mkOptionType {
297 name = "nonEmptyStr";
298 description = "non-empty string";
299 check = x: str.check x && builtins.match "[ \t\n]*" x == null;
300 inherit (str) merge;
301 };
302
303 strMatching = pattern: mkOptionType {
304 name = "strMatching ${escapeNixString pattern}";
305 description = "string matching the pattern ${pattern}";
306 check = x: str.check x && builtins.match pattern x != null;
307 inherit (str) merge;
308 };
309
310 # Merge multiple definitions by concatenating them (with the given
311 # separator between the values).
312 separatedString = sep: mkOptionType rec {
313 name = "separatedString";
314 description = if sep == ""
315 then "Concatenated string" # for types.string.
316 else "strings concatenated with ${builtins.toJSON sep}"
317 ;
318 check = isString;
319 merge = loc: defs: concatStringsSep sep (getValues defs);
320 functor = (defaultFunctor name) // {
321 payload = sep;
322 binOp = sepLhs: sepRhs:
323 if sepLhs == sepRhs then sepLhs
324 else null;
325 };
326 };
327
328 lines = separatedString "\n";
329 commas = separatedString ",";
330 envVar = separatedString ":";
331
332 # Deprecated; should not be used because it quietly concatenates
333 # strings, which is usually not what you want.
334 string = separatedString "" // {
335 name = "string";
336 deprecationMessage = "See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types.";
337 };
338
339 attrs = mkOptionType {
340 name = "attrs";
341 description = "attribute set";
342 check = isAttrs;
343 merge = loc: foldl' (res: def: res // def.value) {};
344 emptyValue = { value = {}; };
345 };
346
347 # derivation is a reserved keyword.
348 package = mkOptionType {
349 name = "package";
350 check = x: isDerivation x || isStorePath x;
351 merge = loc: defs:
352 let res = mergeOneOption loc defs;
353 in if isDerivation res then res else toDerivation res;
354 };
355
356 shellPackage = package // {
357 check = x: isDerivation x && hasAttr "shellPath" x;
358 };
359
360 path = mkOptionType {
361 name = "path";
362 check = x: isCoercibleToString x && builtins.substring 0 1 (toString x) == "/";
363 merge = mergeEqualOption;
364 };
365
366 listOf = elemType: mkOptionType rec {
367 name = "listOf";
368 description = "list of ${elemType.description}s";
369 check = isList;
370 merge = loc: defs:
371 map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def:
372 imap1 (m: def':
373 (mergeDefinitions
374 (loc ++ ["[definition ${toString n}-entry ${toString m}]"])
375 elemType
376 [{ inherit (def) file; value = def'; }]
377 ).optionalValue
378 ) def.value
379 ) defs)));
380 emptyValue = { value = {}; };
381 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
382 getSubModules = elemType.getSubModules;
383 substSubModules = m: listOf (elemType.substSubModules m);
384 functor = (defaultFunctor name) // { wrapped = elemType; };
385 nestedTypes.elemType = elemType;
386 };
387
388 nonEmptyListOf = elemType:
389 let list = addCheck (types.listOf elemType) (l: l != []);
390 in list // {
391 description = "non-empty " + list.description;
392 # Note: emptyValue is left as is, because another module may define an element.
393 };
394
395 attrsOf = elemType: mkOptionType rec {
396 name = "attrsOf";
397 description = "attribute set of ${elemType.description}s";
398 check = isAttrs;
399 merge = loc: defs:
400 mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
401 (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue
402 )
403 # Push down position info.
404 (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs)));
405 emptyValue = { value = {}; };
406 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
407 getSubModules = elemType.getSubModules;
408 substSubModules = m: attrsOf (elemType.substSubModules m);
409 functor = (defaultFunctor name) // { wrapped = elemType; };
410 nestedTypes.elemType = elemType;
411 };
412
413 # A version of attrsOf that's lazy in its values at the expense of
414 # conditional definitions not working properly. E.g. defining a value with
415 # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with
416 # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an
417 # error that it's not defined. Use only if conditional definitions don't make sense.
418 lazyAttrsOf = elemType: mkOptionType rec {
419 name = "lazyAttrsOf";
420 description = "lazy attribute set of ${elemType.description}s";
421 check = isAttrs;
422 merge = loc: defs:
423 zipAttrsWith (name: defs:
424 let merged = mergeDefinitions (loc ++ [name]) elemType defs;
425 # mergedValue will trigger an appropriate error when accessed
426 in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
427 )
428 # Push down position info.
429 (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs);
430 emptyValue = { value = {}; };
431 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
432 getSubModules = elemType.getSubModules;
433 substSubModules = m: lazyAttrsOf (elemType.substSubModules m);
434 functor = (defaultFunctor name) // { wrapped = elemType; };
435 nestedTypes.elemType = elemType;
436 };
437
438 # TODO: drop this in the future:
439 loaOf = elemType: types.attrsOf elemType // {
440 name = "loaOf";
441 deprecationMessage = "Mixing lists with attribute values is no longer"
442 + " possible; please use `types.attrsOf` instead. See"
443 + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation.";
444 nestedTypes.elemType = elemType;
445 };
446
447 # Value of given type but with no merging (i.e. `uniq list`s are not concatenated).
448 uniq = elemType: mkOptionType rec {
449 name = "uniq";
450 inherit (elemType) description check;
451 merge = mergeOneOption;
452 emptyValue = elemType.emptyValue;
453 getSubOptions = elemType.getSubOptions;
454 getSubModules = elemType.getSubModules;
455 substSubModules = m: uniq (elemType.substSubModules m);
456 functor = (defaultFunctor name) // { wrapped = elemType; };
457 nestedTypes.elemType = elemType;
458 };
459
460 # Null or value of ...
461 nullOr = elemType: mkOptionType rec {
462 name = "nullOr";
463 description = "null or ${elemType.description}";
464 check = x: x == null || elemType.check x;
465 merge = loc: defs:
466 let nrNulls = count (def: def.value == null) defs; in
467 if nrNulls == length defs then null
468 else if nrNulls != 0 then
469 throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}."
470 else elemType.merge loc defs;
471 emptyValue = { value = null; };
472 getSubOptions = elemType.getSubOptions;
473 getSubModules = elemType.getSubModules;
474 substSubModules = m: nullOr (elemType.substSubModules m);
475 functor = (defaultFunctor name) // { wrapped = elemType; };
476 nestedTypes.elemType = elemType;
477 };
478
479 functionTo = elemType: mkOptionType {
480 name = "functionTo";
481 description = "function that evaluates to a(n) ${elemType.name}";
482 check = isFunction;
483 merge = loc: defs:
484 fnArgs: (mergeDefinitions (loc ++ [ "[function body]" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue;
485 getSubOptions = elemType.getSubOptions;
486 getSubModules = elemType.getSubModules;
487 substSubModules = m: functionTo (elemType.substSubModules m);
488 };
489
490 # A submodule (like typed attribute set). See NixOS manual.
491 submodule = modules: submoduleWith {
492 shorthandOnlyDefinesConfig = true;
493 modules = toList modules;
494 };
495
496 submoduleWith =
497 { modules
498 , specialArgs ? {}
499 , shorthandOnlyDefinesConfig ? false
500 }@attrs:
501 let
502 inherit (lib.modules) evalModules;
503
504 coerce = unify: value: if isFunction value
505 then setFunctionArgs (args: unify (value args)) (functionArgs value)
506 else unify (if shorthandOnlyDefinesConfig then { config = value; } else value);
507
508 allModules = defs: imap1 (n: { value, file }:
509 if isAttrs value || isFunction value then
510 # Annotate the value with the location of its definition for better error messages
511 coerce (lib.modules.unifyModuleSyntax file "${toString file}-${toString n}") value
512 else value
513 ) defs;
514
515 base = evalModules {
516 inherit specialArgs;
517 modules = [{
518 # This is a work-around for the fact that some sub-modules,
519 # such as the one included in an attribute set, expects an "args"
520 # attribute to be given to the sub-module. As the option
521 # evaluation does not have any specific attribute name yet, we
522 # provide a default for the documentation and the freeform type.
523 #
524 # This is necessary as some option declaration might use the
525 # "name" attribute given as argument of the submodule and use it
526 # as the default of option declarations.
527 #
528 # We use lookalike unicode single angle quotation marks because
529 # of the docbook transformation the options receive. In all uses
530 # > and < wouldn't be encoded correctly so the encoded values
531 # would be used, and use of `<` and `>` would break the XML document.
532 # It shouldn't cause an issue since this is cosmetic for the manual.
533 _module.args.name = lib.mkOptionDefault "‹name›";
534 }] ++ modules;
535 };
536
537 freeformType = base._module.freeformType;
538
539 in
540 mkOptionType rec {
541 name = "submodule";
542 description = freeformType.description or name;
543 check = x: isAttrs x || isFunction x || path.check x;
544 merge = loc: defs:
545 (base.extendModules {
546 modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
547 prefix = loc;
548 }).config;
549 emptyValue = { value = {}; };
550 getSubOptions = prefix: (base.extendModules
551 { inherit prefix; }).options // optionalAttrs (freeformType != null) {
552 # Expose the sub options of the freeform type. Note that the option
553 # discovery doesn't care about the attribute name used here, so this
554 # is just to avoid conflicts with potential options from the submodule
555 _freeformOptions = freeformType.getSubOptions prefix;
556 };
557 getSubModules = modules;
558 substSubModules = m: submoduleWith (attrs // {
559 modules = m;
560 });
561 nestedTypes = lib.optionalAttrs (freeformType != null) {
562 freeformType = freeformType;
563 };
564 functor = defaultFunctor name // {
565 type = types.submoduleWith;
566 payload = {
567 modules = modules;
568 specialArgs = specialArgs;
569 shorthandOnlyDefinesConfig = shorthandOnlyDefinesConfig;
570 };
571 binOp = lhs: rhs: {
572 modules = lhs.modules ++ rhs.modules;
573 specialArgs =
574 let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs;
575 in if intersecting == {}
576 then lhs.specialArgs // rhs.specialArgs
577 else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\"";
578 shorthandOnlyDefinesConfig =
579 if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig
580 then lhs.shorthandOnlyDefinesConfig
581 else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values";
582 };
583 };
584 };
585
586 # A value from a set of allowed ones.
587 enum = values:
588 let
589 show = v:
590 if builtins.isString v then ''"${v}"''
591 else if builtins.isInt v then builtins.toString v
592 else if builtins.isBool v then boolToString v
593 else ''<${builtins.typeOf v}>'';
594 in
595 mkOptionType rec {
596 name = "enum";
597 description =
598 # Length 0 or 1 enums may occur in a design pattern with type merging
599 # where an "interface" module declares an empty enum and other modules
600 # provide implementations, each extending the enum with their own
601 # identifier.
602 if values == [] then
603 "impossible (empty enum)"
604 else if builtins.length values == 1 then
605 "value ${show (builtins.head values)} (singular enum)"
606 else
607 "one of ${concatMapStringsSep ", " show values}";
608 check = flip elem values;
609 merge = mergeEqualOption;
610 functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); };
611 };
612
613 # Either value of type `t1` or `t2`.
614 either = t1: t2: mkOptionType rec {
615 name = "either";
616 description = "${t1.description} or ${t2.description}";
617 check = x: t1.check x || t2.check x;
618 merge = loc: defs:
619 let
620 defList = map (d: d.value) defs;
621 in
622 if all (x: t1.check x) defList
623 then t1.merge loc defs
624 else if all (x: t2.check x) defList
625 then t2.merge loc defs
626 else mergeOneOption loc defs;
627 typeMerge = f':
628 let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor;
629 mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor;
630 in
631 if (name == f'.name) && (mt1 != null) && (mt2 != null)
632 then functor.type mt1 mt2
633 else null;
634 functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; };
635 nestedTypes.left = t1;
636 nestedTypes.right = t2;
637 };
638
639 # Any of the types in the given list
640 oneOf = ts:
641 let
642 head' = if ts == [] then throw "types.oneOf needs to get at least one type in its argument" else head ts;
643 tail' = tail ts;
644 in foldl' either head' tail';
645
646 # Either value of type `coercedType` or `finalType`, the former is
647 # converted to `finalType` using `coerceFunc`.
648 coercedTo = coercedType: coerceFunc: finalType:
649 assert lib.assertMsg (coercedType.getSubModules == null)
650 "coercedTo: coercedType must not have submodules (it’s a ${
651 coercedType.description})";
652 mkOptionType rec {
653 name = "coercedTo";
654 description = "${finalType.description} or ${coercedType.description} convertible to it";
655 check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
656 merge = loc: defs:
657 let
658 coerceVal = val:
659 if coercedType.check val then coerceFunc val
660 else val;
661 in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
662 emptyValue = finalType.emptyValue;
663 getSubOptions = finalType.getSubOptions;
664 getSubModules = finalType.getSubModules;
665 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
666 typeMerge = t1: t2: null;
667 functor = (defaultFunctor name) // { wrapped = finalType; };
668 nestedTypes.coercedType = coercedType;
669 nestedTypes.finalType = finalType;
670 };
671
672 # Obsolete alternative to configOf. It takes its option
673 # declarations from the ‘options’ attribute of containing option
674 # declaration.
675 optionSet = mkOptionType {
676 name = "optionSet";
677 description = "option set";
678 deprecationMessage = "Use `types.submodule' instead";
679 };
680 # Augment the given type with an additional type check function.
681 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
682
683 };
684};
685
686in outer_types // outer_types.types