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 isAttrs
10 isBool
11 isDerivation
12 isFloat
13 isFunction
14 isInt
15 isList
16 isString
17 isStorePath
18 throwIf
19 toDerivation
20 toList
21 ;
22 inherit (lib.lists)
23 all
24 concatLists
25 count
26 elemAt
27 filter
28 foldl'
29 head
30 imap1
31 last
32 length
33 tail
34 ;
35 inherit (lib.attrsets)
36 attrNames
37 filterAttrs
38 hasAttr
39 mapAttrs
40 optionalAttrs
41 zipAttrsWith
42 ;
43 inherit (lib.options)
44 getFiles
45 getValues
46 mergeDefaultOption
47 mergeEqualOption
48 mergeOneOption
49 mergeUniqueOption
50 showFiles
51 showOption
52 ;
53 inherit (lib.strings)
54 concatMapStringsSep
55 concatStringsSep
56 escapeNixString
57 hasInfix
58 isStringLike
59 ;
60 inherit (lib.trivial)
61 boolToString
62 ;
63
64 inherit (lib.modules)
65 mergeDefinitions
66 fixupOptionType
67 mergeOptionDecls
68 ;
69
70 inAttrPosSuffix =
71 v: name:
72 let
73 pos = builtins.unsafeGetAttrPos name v;
74 in
75 if pos == null then "" else " at ${pos.file}:${toString pos.line}:${toString pos.column}";
76
77 # Internal functor to help for migrating functor.wrapped to functor.payload.elemType
78 # Note that individual attributes can be overriden if needed.
79 elemTypeFunctor =
80 name:
81 { elemType, ... }@payload:
82 {
83 inherit name payload;
84 wrappedDeprecationMessage = makeWrappedDeprecationMessage payload;
85 type = outer_types.types.${name};
86 binOp =
87 a: b:
88 let
89 merged = a.elemType.typeMerge b.elemType.functor;
90 in
91 if merged == null then null else { elemType = merged; };
92 };
93 makeWrappedDeprecationMessage =
94 payload:
95 { loc }:
96 lib.warn ''
97 The deprecated `${lib.optionalString (loc != null) "type."}functor.wrapped` attribute ${
98 lib.optionalString (loc != null) "of the option `${showOption loc}` "
99 }is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead.
100 '' payload.elemType;
101
102 outer_types = rec {
103 isType = type: x: (x._type or "") == type;
104
105 setType =
106 typeName: value:
107 value
108 // {
109 _type = typeName;
110 };
111
112 # Default type merging function
113 # takes two type functors and return the merged type
114 defaultTypeMerge =
115 f: f':
116 let
117 mergedWrapped = f.wrapped.typeMerge f'.wrapped.functor;
118 mergedPayload = f.binOp f.payload f'.payload;
119
120 hasPayload =
121 assert (f'.payload != null) == (f.payload != null);
122 f.payload != null;
123 hasWrapped =
124 assert (f'.wrapped != null) == (f.wrapped != null);
125 f.wrapped != null;
126
127 typeFromPayload = if mergedPayload == null then null else f.type mergedPayload;
128 typeFromWrapped = if mergedWrapped == null then null else f.type mergedWrapped;
129 in
130 # Abort early: cannot merge different types
131 if f.name != f'.name then
132 null
133 else
134
135 if hasPayload then
136 # Just return the payload if returning wrapped is deprecated
137 if f ? wrappedDeprecationMessage then
138 typeFromPayload
139 else if hasWrapped then
140 # Has both wrapped and payload
141 throw ''
142 Type ${f.name} defines both `functor.payload` and `functor.wrapped` at the same time, which is not supported.
143
144 Use either `functor.payload` or `functor.wrapped` but not both.
145
146 If your code worked before remove either `functor.wrapped` or `functor.payload` from the type definition.
147 ''
148 else
149 typeFromPayload
150 else if hasWrapped then
151 typeFromWrapped
152 else
153 f.type;
154
155 # Default type functor
156 defaultFunctor = name: {
157 inherit name;
158 type = types.${name} or null;
159 wrapped = null;
160 payload = null;
161 binOp = a: b: null;
162 };
163
164 isOptionType = isType "option-type";
165 mkOptionType =
166 {
167 # Human-readable representation of the type, should be equivalent to
168 # the type function name.
169 name,
170 # Description of the type, defined recursively by embedding the wrapped type if any.
171 description ? null,
172 # A hint for whether or not this description needs parentheses. Possible values:
173 # - "noun": a noun phrase
174 # Example description: "positive integer",
175 # - "conjunction": a phrase with a potentially ambiguous "or" connective
176 # Example description: "int or string"
177 # - "composite": a phrase with an "of" connective
178 # Example description: "list of string"
179 # - "nonRestrictiveClause": a noun followed by a comma and a clause
180 # Example description: "positive integer, meaning >0"
181 # See the `optionDescriptionPhrase` function.
182 descriptionClass ? null,
183 # DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING!
184 # Function applied to each definition that must return false when a definition
185 # does not match the type. It should not check more than the root of the value,
186 # because checking nested values reduces laziness, leading to unnecessary
187 # infinite recursions in the module system.
188 # Further checks of nested values should be performed by throwing in
189 # the merge function.
190 # Strict and deep type checking can be performed by calling lib.deepSeq on
191 # the merged value.
192 #
193 # See https://github.com/NixOS/nixpkgs/pull/6794 that introduced this change,
194 # https://github.com/NixOS/nixpkgs/pull/173568 and
195 # https://github.com/NixOS/nixpkgs/pull/168295 that attempted to revert this,
196 # https://github.com/NixOS/nixpkgs/issues/191124 and
197 # https://github.com/NixOS/nixos-search/issues/391 for what happens if you ignore
198 # this disclaimer.
199 check ? (x: true),
200 # Merge a list of definitions together into a single value.
201 # This function is called with two arguments: the location of
202 # the option in the configuration as a list of strings
203 # (e.g. ["boot" "loader "grub" "enable"]), and a list of
204 # definition values and locations (e.g. [ { file = "/foo.nix";
205 # value = 1; } { file = "/bar.nix"; value = 2 } ]).
206 merge ? mergeDefaultOption,
207 # Whether this type has a value representing nothingness. If it does,
208 # this should be a value of the form { value = <the nothing value>; }
209 # If it doesn't, this should be {}
210 # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`.
211 emptyValue ? { },
212 # Return a flat attrset of sub-options. Used to generate
213 # documentation.
214 getSubOptions ? prefix: { },
215 # List of modules if any, or null if none.
216 getSubModules ? null,
217 # Function for building the same option type with a different list of
218 # modules.
219 substSubModules ? m: null,
220 # Function that merge type declarations.
221 # internal, takes a functor as argument and returns the merged type.
222 # returning null means the type is not mergeable
223 typeMerge ? defaultTypeMerge functor,
224 # The type functor.
225 # internal, representation of the type as an attribute set.
226 # name: name of the type
227 # type: type function.
228 # wrapped: the type wrapped in case of compound types.
229 # payload: values of the type, two payloads of the same type must be
230 # combinable with the binOp binary operation.
231 # binOp: binary operation that merge two payloads of the same type.
232 functor ? defaultFunctor name,
233 # The deprecation message to display when this type is used by an option
234 # If null, the type isn't deprecated
235 deprecationMessage ? null,
236 # The types that occur in the definition of this type. This is used to
237 # issue deprecation warnings recursively. Can also be used to reuse
238 # nested types
239 nestedTypes ? { },
240 }:
241 {
242 _type = "option-type";
243 inherit
244 name
245 check
246 merge
247 emptyValue
248 getSubOptions
249 getSubModules
250 substSubModules
251 typeMerge
252 deprecationMessage
253 nestedTypes
254 descriptionClass
255 ;
256 functor =
257 if functor ? wrappedDeprecationMessage then
258 functor
259 // {
260 wrapped = functor.wrappedDeprecationMessage {
261 loc = null;
262 };
263 }
264 else
265 functor;
266 description = if description == null then name else description;
267 };
268
269 # optionDescriptionPhrase :: (str -> bool) -> optionType -> str
270 #
271 # Helper function for producing unambiguous but readable natural language
272 # descriptions of types.
273 #
274 # Parameters
275 #
276 # optionDescriptionPhase unparenthesize optionType
277 #
278 # `unparenthesize`: A function from descriptionClass string to boolean.
279 # It must return true when the class of phrase will fit unambiguously into
280 # the description of the caller.
281 #
282 # `optionType`: The option type to parenthesize or not.
283 # The option whose description we're returning.
284 #
285 # Return value
286 #
287 # The description of the `optionType`, with parentheses if there may be an
288 # ambiguity.
289 optionDescriptionPhrase =
290 unparenthesize: t:
291 if unparenthesize (t.descriptionClass or null) then t.description else "(${t.description})";
292
293 noCheckForDocsModule = {
294 # When generating documentation, our goal isn't to check anything.
295 # Quite the opposite in fact. Generating docs is somewhat of a
296 # challenge, evaluating modules in a *lacking* context. Anything
297 # that makes the docs avoid an error is a win.
298 config._module.check = lib.mkForce false;
299 _file = "<built-in module that disables checks for the purpose of documentation generation>";
300 };
301
302 # When adding new types don't forget to document them in
303 # nixos/doc/manual/development/option-types.section.md!
304 types = rec {
305
306 raw = mkOptionType {
307 name = "raw";
308 description = "raw value";
309 descriptionClass = "noun";
310 check = value: true;
311 merge = mergeOneOption;
312 };
313
314 anything = mkOptionType {
315 name = "anything";
316 description = "anything";
317 descriptionClass = "noun";
318 check = value: true;
319 merge =
320 loc: defs:
321 let
322 getType =
323 value: if isAttrs value && isStringLike value then "stringCoercibleSet" else builtins.typeOf value;
324
325 # Returns the common type of all definitions, throws an error if they
326 # don't have the same type
327 commonType = foldl' (
328 type: def:
329 if getType def.value == type then
330 type
331 else
332 throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}"
333 ) (getType (head defs).value) defs;
334
335 mergeFunction =
336 {
337 # Recursively merge attribute sets
338 set = (attrsOf anything).merge;
339 # This is the type of packages, only accept a single definition
340 stringCoercibleSet = mergeOneOption;
341 lambda =
342 loc: defs: arg:
343 anything.merge (loc ++ [ "<function body>" ]) (
344 map (def: {
345 file = def.file;
346 value = def.value arg;
347 }) defs
348 );
349 # Otherwise fall back to only allowing all equal definitions
350 }
351 .${commonType} or mergeEqualOption;
352 in
353 mergeFunction loc defs;
354 };
355
356 unspecified = mkOptionType {
357 name = "unspecified";
358 description = "unspecified value";
359 descriptionClass = "noun";
360 };
361
362 bool = mkOptionType {
363 name = "bool";
364 description = "boolean";
365 descriptionClass = "noun";
366 check = isBool;
367 merge = mergeEqualOption;
368 };
369
370 boolByOr = mkOptionType {
371 name = "boolByOr";
372 description = "boolean (merged using or)";
373 descriptionClass = "noun";
374 check = isBool;
375 merge =
376 loc: defs:
377 foldl' (
378 result: def:
379 # Under the assumption that .check always runs before merge, we can assume that all defs.*.value
380 # have been forced, and therefore we assume we don't introduce order-dependent strictness here
381 result || def.value
382 ) false defs;
383 };
384
385 int = mkOptionType {
386 name = "int";
387 description = "signed integer";
388 descriptionClass = "noun";
389 check = isInt;
390 merge = mergeEqualOption;
391 };
392
393 # Specialized subdomains of int
394 ints =
395 let
396 betweenDesc = lowest: highest: "${toString lowest} and ${toString highest} (both inclusive)";
397 between =
398 lowest: highest:
399 assert lib.assertMsg (lowest <= highest) "ints.between: lowest must be smaller than highest";
400 addCheck int (x: x >= lowest && x <= highest)
401 // {
402 name = "intBetween";
403 description = "integer between ${betweenDesc lowest highest}";
404 };
405 ign =
406 lowest: highest: name: docStart:
407 between lowest highest
408 // {
409 inherit name;
410 description = docStart + "; between ${betweenDesc lowest highest}";
411 };
412 unsign =
413 bit: range: ign 0 (range - 1) "unsignedInt${toString bit}" "${toString bit} bit unsigned integer";
414 sign =
415 bit: range:
416 ign (0 - (range / 2)) (
417 range / 2 - 1
418 ) "signedInt${toString bit}" "${toString bit} bit signed integer";
419
420 in
421 {
422 # TODO: Deduplicate with docs in nixos/doc/manual/development/option-types.section.md
423 /**
424 An int with a fixed range.
425
426 # Example
427 :::{.example}
428 ## `lib.types.ints.between` usage example
429
430 ```nix
431 (ints.between 0 100).check (-1)
432 => false
433 (ints.between 0 100).check (101)
434 => false
435 (ints.between 0 0).check 0
436 => true
437 ```
438
439 :::
440 */
441 inherit between;
442
443 unsigned = addCheck types.int (x: x >= 0) // {
444 name = "unsignedInt";
445 description = "unsigned integer, meaning >=0";
446 descriptionClass = "nonRestrictiveClause";
447 };
448 positive = addCheck types.int (x: x > 0) // {
449 name = "positiveInt";
450 description = "positive integer, meaning >0";
451 descriptionClass = "nonRestrictiveClause";
452 };
453 u8 = unsign 8 256;
454 u16 = unsign 16 65536;
455 # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808)
456 # the smallest int Nix accepts is -2^63 (-9223372036854775807)
457 u32 = unsign 32 4294967296;
458 # u64 = unsign 64 18446744073709551616;
459
460 s8 = sign 8 256;
461 s16 = sign 16 65536;
462 s32 = sign 32 4294967296;
463 };
464
465 # Alias of u16 for a port number
466 port = ints.u16;
467
468 float = mkOptionType {
469 name = "float";
470 description = "floating point number";
471 descriptionClass = "noun";
472 check = isFloat;
473 merge = mergeEqualOption;
474 };
475
476 number = either int float;
477
478 numbers =
479 let
480 betweenDesc =
481 lowest: highest: "${builtins.toJSON lowest} and ${builtins.toJSON highest} (both inclusive)";
482 in
483 {
484 between =
485 lowest: highest:
486 assert lib.assertMsg (lowest <= highest) "numbers.between: lowest must be smaller than highest";
487 addCheck number (x: x >= lowest && x <= highest)
488 // {
489 name = "numberBetween";
490 description = "integer or floating point number between ${betweenDesc lowest highest}";
491 };
492
493 nonnegative = addCheck number (x: x >= 0) // {
494 name = "numberNonnegative";
495 description = "nonnegative integer or floating point number, meaning >=0";
496 descriptionClass = "nonRestrictiveClause";
497 };
498 positive = addCheck number (x: x > 0) // {
499 name = "numberPositive";
500 description = "positive integer or floating point number, meaning >0";
501 descriptionClass = "nonRestrictiveClause";
502 };
503 };
504
505 str = mkOptionType {
506 name = "str";
507 description = "string";
508 descriptionClass = "noun";
509 check = isString;
510 merge = mergeEqualOption;
511 };
512
513 nonEmptyStr = mkOptionType {
514 name = "nonEmptyStr";
515 description = "non-empty string";
516 descriptionClass = "noun";
517 check = x: str.check x && builtins.match "[ \t\n]*" x == null;
518 inherit (str) merge;
519 };
520
521 # Allow a newline character at the end and trim it in the merge function.
522 singleLineStr =
523 let
524 inherit (strMatching "[^\n\r]*\n?") check merge;
525 in
526 mkOptionType {
527 name = "singleLineStr";
528 description = "(optionally newline-terminated) single-line string";
529 descriptionClass = "noun";
530 inherit check;
531 merge = loc: defs: lib.removeSuffix "\n" (merge loc defs);
532 };
533
534 strMatching =
535 pattern:
536 mkOptionType {
537 name = "strMatching ${escapeNixString pattern}";
538 description = "string matching the pattern ${pattern}";
539 descriptionClass = "noun";
540 check = x: str.check x && builtins.match pattern x != null;
541 inherit (str) merge;
542 functor = defaultFunctor "strMatching" // {
543 type = payload: strMatching payload.pattern;
544 payload = { inherit pattern; };
545 binOp = lhs: rhs: if lhs == rhs then lhs else null;
546 };
547 };
548
549 # Merge multiple definitions by concatenating them (with the given
550 # separator between the values).
551 separatedString =
552 sep:
553 mkOptionType rec {
554 name = "separatedString";
555 description =
556 if sep == "" then
557 "Concatenated string" # for types.string.
558 else
559 "strings concatenated with ${builtins.toJSON sep}";
560 descriptionClass = "noun";
561 check = isString;
562 merge = loc: defs: concatStringsSep sep (getValues defs);
563 functor = (defaultFunctor name) // {
564 payload = { inherit sep; };
565 type = payload: types.separatedString payload.sep;
566 binOp = lhs: rhs: if lhs.sep == rhs.sep then { inherit (lhs) sep; } else null;
567 };
568 };
569
570 lines = separatedString "\n";
571 commas = separatedString ",";
572 envVar = separatedString ":";
573
574 # Deprecated; should not be used because it quietly concatenates
575 # strings, which is usually not what you want.
576 # We use a lib.warn because `deprecationMessage` doesn't trigger in nested types such as `attrsOf string`
577 string =
578 lib.warn
579 "The type `types.string` is deprecated. See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types."
580 (
581 separatedString ""
582 // {
583 name = "string";
584 }
585 );
586
587 passwdEntry =
588 entryType:
589 addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str))
590 // {
591 name = "passwdEntry ${entryType.name}";
592 description = "${
593 optionDescriptionPhrase (class: class == "noun") entryType
594 }, not containing newlines or colons";
595 descriptionClass = "nonRestrictiveClause";
596 };
597
598 attrs = mkOptionType {
599 name = "attrs";
600 description = "attribute set";
601 check = isAttrs;
602 merge = loc: foldl' (res: def: res // def.value) { };
603 emptyValue = {
604 value = { };
605 };
606 };
607
608 # A package is a top-level store path (/nix/store/hash-name). This includes:
609 # - derivations
610 # - more generally, attribute sets with an `outPath` or `__toString` attribute
611 # pointing to a store path, e.g. flake inputs
612 # - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo)
613 # - hardcoded store path literals (/nix/store/hash-foo) or strings without context
614 # ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath.
615 # If you don't need a *top-level* store path, consider using pathInStore instead.
616 package = mkOptionType {
617 name = "package";
618 descriptionClass = "noun";
619 check = x: isDerivation x || isStorePath x;
620 merge =
621 loc: defs:
622 let
623 res = mergeOneOption loc defs;
624 in
625 if builtins.isPath res || (builtins.isString res && !builtins.hasContext res) then
626 toDerivation res
627 else
628 res;
629 };
630
631 shellPackage = package // {
632 check = x: isDerivation x && hasAttr "shellPath" x;
633 };
634
635 pkgs = addCheck (
636 unique { message = "A Nixpkgs pkgs set can not be merged with another pkgs set."; } attrs
637 // {
638 name = "pkgs";
639 descriptionClass = "noun";
640 description = "Nixpkgs package set";
641 }
642 ) (x: (x._type or null) == "pkgs");
643
644 path = pathWith {
645 absolute = true;
646 };
647
648 pathInStore = pathWith {
649 inStore = true;
650 };
651
652 pathWith =
653 {
654 inStore ? null,
655 absolute ? null,
656 }:
657 throwIf (inStore != null && absolute != null && inStore && !absolute)
658 "In pathWith, inStore means the path must be absolute"
659 mkOptionType
660 {
661 name = "path";
662 description = (
663 (if absolute == null then "" else (if absolute then "absolute " else "relative "))
664 + "path"
665 + (
666 if inStore == null then "" else (if inStore then " in the Nix store" else " not in the Nix store")
667 )
668 );
669 descriptionClass = "noun";
670
671 merge = mergeEqualOption;
672 functor = defaultFunctor "path" // {
673 type = pathWith;
674 payload = { inherit inStore absolute; };
675 binOp = lhs: rhs: if lhs == rhs then lhs else null;
676 };
677
678 check =
679 x:
680 let
681 isInStore = lib.path.hasStorePathPrefix (
682 if builtins.isPath x then
683 x
684 # Discarding string context is necessary to convert the value to
685 # a path and safe as the result is never used in any derivation.
686 else
687 /. + builtins.unsafeDiscardStringContext x
688 );
689 isAbsolute = builtins.substring 0 1 (toString x) == "/";
690 isExpectedType = (
691 if inStore == null || inStore then isStringLike x else isString x # Do not allow a true path, which could be copied to the store later on.
692 );
693 in
694 isExpectedType
695 && (inStore == null || inStore == isInStore)
696 && (absolute == null || absolute == isAbsolute);
697 };
698
699 listOf =
700 elemType:
701 mkOptionType rec {
702 name = "listOf";
703 description = "list of ${
704 optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType
705 }";
706 descriptionClass = "composite";
707 check = isList;
708 merge =
709 loc: defs:
710 map (x: x.value) (
711 filter (x: x ? value) (
712 concatLists (
713 imap1 (
714 n: def:
715 imap1 (
716 m: def':
717 (mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [
718 {
719 inherit (def) file;
720 value = def';
721 }
722 ]).optionalValue
723 ) def.value
724 ) defs
725 )
726 )
727 );
728 emptyValue = {
729 value = [ ];
730 };
731 getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" ]);
732 getSubModules = elemType.getSubModules;
733 substSubModules = m: listOf (elemType.substSubModules m);
734 functor = (elemTypeFunctor name { inherit elemType; }) // {
735 type = payload: types.listOf payload.elemType;
736 };
737 nestedTypes.elemType = elemType;
738 };
739
740 nonEmptyListOf =
741 elemType:
742 let
743 list = addCheck (types.listOf elemType) (l: l != [ ]);
744 in
745 list
746 // {
747 description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
748 emptyValue = { }; # no .value attr, meaning unset
749 substSubModules = m: nonEmptyListOf (elemType.substSubModules m);
750 };
751
752 attrsOf = elemType: attrsWith { inherit elemType; };
753
754 # A version of attrsOf that's lazy in its values at the expense of
755 # conditional definitions not working properly. E.g. defining a value with
756 # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with
757 # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an
758 # error that it's not defined. Use only if conditional definitions don't make sense.
759 lazyAttrsOf =
760 elemType:
761 attrsWith {
762 inherit elemType;
763 lazy = true;
764 };
765
766 # base type for lazyAttrsOf and attrsOf
767 attrsWith =
768 let
769 # Push down position info.
770 pushPositions = map (
771 def:
772 mapAttrs (n: v: {
773 inherit (def) file;
774 value = v;
775 }) def.value
776 );
777 binOp =
778 lhs: rhs:
779 let
780 elemType = lhs.elemType.typeMerge rhs.elemType.functor;
781 lazy = if lhs.lazy == rhs.lazy then lhs.lazy else null;
782 placeholder =
783 if lhs.placeholder == rhs.placeholder then
784 lhs.placeholder
785 else if lhs.placeholder == "name" then
786 rhs.placeholder
787 else if rhs.placeholder == "name" then
788 lhs.placeholder
789 else
790 null;
791 in
792 if elemType == null || lazy == null || placeholder == null then
793 null
794 else
795 {
796 inherit elemType lazy placeholder;
797 };
798 in
799 {
800 elemType,
801 lazy ? false,
802 placeholder ? "name",
803 }:
804 mkOptionType {
805 name = if lazy then "lazyAttrsOf" else "attrsOf";
806 description =
807 (if lazy then "lazy attribute set" else "attribute set")
808 + " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
809 descriptionClass = "composite";
810 check = isAttrs;
811 merge =
812 if lazy then
813 (
814 # Lazy merge Function
815 loc: defs:
816 zipAttrsWith
817 (
818 name: defs:
819 let
820 merged = mergeDefinitions (loc ++ [ name ]) elemType defs;
821 # mergedValue will trigger an appropriate error when accessed
822 in
823 merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
824 )
825 # Push down position info.
826 (pushPositions defs)
827 )
828 else
829 (
830 # Non-lazy merge Function
831 loc: defs:
832 mapAttrs (n: v: v.value) (
833 filterAttrs (n: v: v ? value) (
834 zipAttrsWith (name: defs: (mergeDefinitions (loc ++ [ name ]) elemType (defs)).optionalValue)
835 # Push down position info.
836 (pushPositions defs)
837 )
838 )
839 );
840 emptyValue = {
841 value = { };
842 };
843 getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<${placeholder}>" ]);
844 getSubModules = elemType.getSubModules;
845 substSubModules =
846 m:
847 attrsWith {
848 elemType = elemType.substSubModules m;
849 inherit lazy placeholder;
850 };
851 functor =
852 (elemTypeFunctor "attrsWith" {
853 inherit elemType lazy placeholder;
854 })
855 // {
856 # Custom type merging required because of the "placeholder" attribute
857 inherit binOp;
858 };
859 nestedTypes.elemType = elemType;
860 };
861
862 # TODO: deprecate this in the future:
863 loaOf =
864 elemType:
865 types.attrsOf elemType
866 // {
867 name = "loaOf";
868 deprecationMessage =
869 "Mixing lists with attribute values is no longer"
870 + " possible; please use `types.attrsOf` instead. See"
871 + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation.";
872 nestedTypes.elemType = elemType;
873 };
874
875 attrTag =
876 tags:
877 let
878 tags_ = tags;
879 in
880 let
881 tags = mapAttrs (
882 n: opt:
883 builtins.addErrorContext
884 "while checking that attrTag tag ${lib.strings.escapeNixIdentifier n} is an option with a type${inAttrPosSuffix tags_ n}"
885 (
886 throwIf (opt._type or null != "option")
887 "In attrTag, each tag value must be an option, but tag ${lib.strings.escapeNixIdentifier n} ${
888 if opt ? _type then
889 if opt._type == "option-type" then
890 "was a bare type, not wrapped in mkOption."
891 else
892 "was of type ${lib.strings.escapeNixString opt._type}."
893 else
894 "was not."
895 }"
896 opt
897 // {
898 declarations =
899 opt.declarations or (
900 let
901 pos = builtins.unsafeGetAttrPos n tags_;
902 in
903 if pos == null then [ ] else [ pos.file ]
904 );
905 declarationPositions =
906 opt.declarationPositions or (
907 let
908 pos = builtins.unsafeGetAttrPos n tags_;
909 in
910 if pos == null then [ ] else [ pos ]
911 );
912 }
913 )
914 ) tags_;
915 choicesStr = concatMapStringsSep ", " lib.strings.escapeNixIdentifier (attrNames tags);
916 in
917 mkOptionType {
918 name = "attrTag";
919 description = "attribute-tagged union";
920 descriptionClass = "noun";
921 getSubOptions =
922 prefix:
923 mapAttrs (tagName: tagOption: {
924 "${lib.showOption prefix}" = tagOption // {
925 loc = prefix ++ [ tagName ];
926 };
927 }) tags;
928 check = v: isAttrs v && length (attrNames v) == 1 && tags ? ${head (attrNames v)};
929 merge =
930 loc: defs:
931 let
932 choice = head (attrNames (head defs).value);
933 checkedValueDefs = map (
934 def:
935 assert (length (attrNames def.value)) == 1;
936 if (head (attrNames def.value)) != choice then
937 throw "The option `${showOption loc}` is defined both as `${choice}` and `${head (attrNames def.value)}`, in ${showFiles (getFiles defs)}."
938 else
939 {
940 inherit (def) file;
941 value = def.value.${choice};
942 }
943 ) defs;
944 in
945 if tags ? ${choice} then
946 {
947 ${choice} = (lib.modules.evalOptionValue (loc ++ [ choice ]) tags.${choice} checkedValueDefs).value;
948 }
949 else
950 throw "The option `${showOption loc}` is defined as ${lib.strings.escapeNixIdentifier choice}, but ${lib.strings.escapeNixIdentifier choice} is not among the valid choices (${choicesStr}). Value ${choice} was defined in ${showFiles (getFiles defs)}.";
951 nestedTypes = tags;
952 functor = defaultFunctor "attrTag" // {
953 type = { tags, ... }: types.attrTag tags;
954 payload = { inherit tags; };
955 binOp =
956 let
957 # Add metadata in the format that submodules work with
958 wrapOptionDecl = option: {
959 options = option;
960 _file = "<attrTag {...}>";
961 pos = null;
962 };
963 in
964 a: b: {
965 tags =
966 a.tags
967 // b.tags
968 // mapAttrs (
969 tagName: bOpt:
970 lib.mergeOptionDecls
971 # FIXME: loc is not accurate; should include prefix
972 # Fortunately, it's only used for error messages, where a "relative" location is kinda ok.
973 # It is also returned though, but use of the attribute seems rare?
974 [ tagName ]
975 [
976 (wrapOptionDecl a.tags.${tagName})
977 (wrapOptionDecl bOpt)
978 ]
979 // {
980 # mergeOptionDecls is not idempotent in these attrs:
981 declarations = a.tags.${tagName}.declarations ++ bOpt.declarations;
982 declarationPositions = a.tags.${tagName}.declarationPositions ++ bOpt.declarationPositions;
983 }
984 ) (builtins.intersectAttrs a.tags b.tags);
985 };
986 };
987 };
988
989 # A value produced by `lib.mkLuaInline`
990 luaInline = mkOptionType {
991 name = "luaInline";
992 description = "inline lua";
993 descriptionClass = "noun";
994 check = x: x._type or null == "lua-inline";
995 merge = mergeEqualOption;
996 };
997
998 uniq = unique { message = ""; };
999
1000 unique =
1001 { message }:
1002 type:
1003 mkOptionType rec {
1004 name = "unique";
1005 inherit (type) description descriptionClass check;
1006 merge = mergeUniqueOption {
1007 inherit message;
1008 inherit (type) merge;
1009 };
1010 emptyValue = type.emptyValue;
1011 getSubOptions = type.getSubOptions;
1012 getSubModules = type.getSubModules;
1013 substSubModules = m: uniq (type.substSubModules m);
1014 functor = elemTypeFunctor name { elemType = type; } // {
1015 type = payload: types.unique { inherit message; } payload.elemType;
1016 };
1017 nestedTypes.elemType = type;
1018 };
1019
1020 # Null or value of ...
1021 nullOr =
1022 elemType:
1023 mkOptionType rec {
1024 name = "nullOr";
1025 description = "null or ${
1026 optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType
1027 }";
1028 descriptionClass = "conjunction";
1029 check = x: x == null || elemType.check x;
1030 merge =
1031 loc: defs:
1032 let
1033 nrNulls = count (def: def.value == null) defs;
1034 in
1035 if nrNulls == length defs then
1036 null
1037 else if nrNulls != 0 then
1038 throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}."
1039 else
1040 elemType.merge loc defs;
1041 emptyValue = {
1042 value = null;
1043 };
1044 getSubOptions = elemType.getSubOptions;
1045 getSubModules = elemType.getSubModules;
1046 substSubModules = m: nullOr (elemType.substSubModules m);
1047 functor = (elemTypeFunctor name { inherit elemType; }) // {
1048 type = payload: types.nullOr payload.elemType;
1049 };
1050 nestedTypes.elemType = elemType;
1051 };
1052
1053 functionTo =
1054 elemType:
1055 mkOptionType {
1056 name = "functionTo";
1057 description = "function that evaluates to a(n) ${
1058 optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType
1059 }";
1060 descriptionClass = "composite";
1061 check = isFunction;
1062 merge = loc: defs: {
1063 # An argument attribute has a default when it has a default in all definitions
1064 __functionArgs = lib.zipAttrsWith (_: lib.all (x: x)) (
1065 lib.map (fn: lib.functionArgs fn.value) defs
1066 );
1067 __functor =
1068 _: callerArgs:
1069 (mergeDefinitions (loc ++ [ "<function body>" ]) elemType (
1070 map (fn: {
1071 inherit (fn) file;
1072 value = fn.value callerArgs;
1073 }) defs
1074 )).mergedValue;
1075 };
1076 getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]);
1077 getSubModules = elemType.getSubModules;
1078 substSubModules = m: functionTo (elemType.substSubModules m);
1079 functor = (elemTypeFunctor "functionTo" { inherit elemType; }) // {
1080 type = payload: types.functionTo payload.elemType;
1081 };
1082 nestedTypes.elemType = elemType;
1083 };
1084
1085 # A submodule (like typed attribute set). See NixOS manual.
1086 submodule =
1087 modules:
1088 submoduleWith {
1089 shorthandOnlyDefinesConfig = true;
1090 modules = toList modules;
1091 };
1092
1093 # A module to be imported in some other part of the configuration.
1094 deferredModule = deferredModuleWith { };
1095
1096 # A module to be imported in some other part of the configuration.
1097 # `staticModules`' options will be added to the documentation, unlike
1098 # options declared via `config`.
1099 deferredModuleWith =
1100 attrs@{
1101 staticModules ? [ ],
1102 }:
1103 mkOptionType {
1104 name = "deferredModule";
1105 description = "module";
1106 descriptionClass = "noun";
1107 check = x: isAttrs x || isFunction x || path.check x;
1108 merge = loc: defs: {
1109 imports =
1110 staticModules
1111 ++ map (
1112 def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value
1113 ) defs;
1114 };
1115 inherit (submoduleWith { modules = staticModules; })
1116 getSubOptions
1117 getSubModules
1118 ;
1119 substSubModules =
1120 m:
1121 deferredModuleWith (
1122 attrs
1123 // {
1124 staticModules = m;
1125 }
1126 );
1127 functor = defaultFunctor "deferredModuleWith" // {
1128 type = types.deferredModuleWith;
1129 payload = {
1130 inherit staticModules;
1131 };
1132 binOp = lhs: rhs: {
1133 staticModules = lhs.staticModules ++ rhs.staticModules;
1134 };
1135 };
1136 };
1137
1138 # The type of a type!
1139 optionType = mkOptionType {
1140 name = "optionType";
1141 description = "optionType";
1142 descriptionClass = "noun";
1143 check = value: value._type or null == "option-type";
1144 merge =
1145 loc: defs:
1146 if length defs == 1 then
1147 (head defs).value
1148 else
1149 let
1150 # Prepares the type definitions for mergeOptionDecls, which
1151 # annotates submodules types with file locations
1152 optionModules = map (
1153 { value, file }:
1154 {
1155 _file = file;
1156 # There's no way to merge types directly from the module system,
1157 # but we can cheat a bit by just declaring an option with the type
1158 options = lib.mkOption {
1159 type = value;
1160 };
1161 }
1162 ) defs;
1163 # Merges all the types into a single one, including submodule merging.
1164 # This also propagates file information to all submodules
1165 mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules);
1166 in
1167 mergedOption.type;
1168 };
1169
1170 submoduleWith =
1171 {
1172 modules,
1173 specialArgs ? { },
1174 shorthandOnlyDefinesConfig ? false,
1175 description ? null,
1176 class ? null,
1177 }@attrs:
1178 let
1179 inherit (lib.modules) evalModules;
1180
1181 allModules =
1182 defs:
1183 map (
1184 { value, file }:
1185 if isAttrs value && shorthandOnlyDefinesConfig then
1186 {
1187 _file = file;
1188 config = value;
1189 }
1190 else
1191 {
1192 _file = file;
1193 imports = [ value ];
1194 }
1195 ) defs;
1196
1197 base = evalModules {
1198 inherit class specialArgs;
1199 modules = [
1200 {
1201 # This is a work-around for the fact that some sub-modules,
1202 # such as the one included in an attribute set, expects an "args"
1203 # attribute to be given to the sub-module. As the option
1204 # evaluation does not have any specific attribute name yet, we
1205 # provide a default for the documentation and the freeform type.
1206 #
1207 # This is necessary as some option declaration might use the
1208 # "name" attribute given as argument of the submodule and use it
1209 # as the default of option declarations.
1210 #
1211 # We use lookalike unicode single angle quotation marks because
1212 # of the docbook transformation the options receive. In all uses
1213 # > and < wouldn't be encoded correctly so the encoded values
1214 # would be used, and use of `<` and `>` would break the XML document.
1215 # It shouldn't cause an issue since this is cosmetic for the manual.
1216 _module.args.name = lib.mkOptionDefault "‹name›";
1217 }
1218 ] ++ modules;
1219 };
1220
1221 freeformType = base._module.freeformType;
1222
1223 name = "submodule";
1224
1225 in
1226 mkOptionType {
1227 inherit name;
1228 description =
1229 if description != null then
1230 description
1231 else
1232 let
1233 docsEval = base.extendModules { modules = [ noCheckForDocsModule ]; };
1234 in
1235 docsEval._module.freeformType.description or name;
1236 check = x: isAttrs x || isFunction x || path.check x;
1237 merge =
1238 loc: defs:
1239 (base.extendModules {
1240 modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
1241 prefix = loc;
1242 }).config;
1243 emptyValue = {
1244 value = { };
1245 };
1246 getSubOptions =
1247 prefix:
1248 let
1249 docsEval = (
1250 base.extendModules {
1251 inherit prefix;
1252 modules = [ noCheckForDocsModule ];
1253 }
1254 );
1255 # Intentionally shadow the freeformType from the possibly *checked*
1256 # configuration. See `noCheckForDocsModule` comment.
1257 inherit (docsEval._module) freeformType;
1258 in
1259 docsEval.options
1260 // optionalAttrs (freeformType != null) {
1261 # Expose the sub options of the freeform type. Note that the option
1262 # discovery doesn't care about the attribute name used here, so this
1263 # is just to avoid conflicts with potential options from the submodule
1264 _freeformOptions = freeformType.getSubOptions prefix;
1265 };
1266 getSubModules = modules;
1267 substSubModules =
1268 m:
1269 submoduleWith (
1270 attrs
1271 // {
1272 modules = m;
1273 }
1274 );
1275 nestedTypes = lib.optionalAttrs (freeformType != null) {
1276 freeformType = freeformType;
1277 };
1278 functor = defaultFunctor name // {
1279 type = types.submoduleWith;
1280 payload = {
1281 inherit
1282 modules
1283 class
1284 specialArgs
1285 shorthandOnlyDefinesConfig
1286 description
1287 ;
1288 };
1289 binOp = lhs: rhs: {
1290 class =
1291 # `or null` was added for backwards compatibility only. `class` is
1292 # always set in the current version of the module system.
1293 if lhs.class or null == null then
1294 rhs.class or null
1295 else if rhs.class or null == null then
1296 lhs.class or null
1297 else if lhs.class or null == rhs.class then
1298 lhs.class or null
1299 else
1300 throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\".";
1301 modules = lhs.modules ++ rhs.modules;
1302 specialArgs =
1303 let
1304 intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs;
1305 in
1306 if intersecting == { } then
1307 lhs.specialArgs // rhs.specialArgs
1308 else
1309 throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\"";
1310 shorthandOnlyDefinesConfig =
1311 if lhs.shorthandOnlyDefinesConfig == null then
1312 rhs.shorthandOnlyDefinesConfig
1313 else if rhs.shorthandOnlyDefinesConfig == null then
1314 lhs.shorthandOnlyDefinesConfig
1315 else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig then
1316 lhs.shorthandOnlyDefinesConfig
1317 else
1318 throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values";
1319 description =
1320 if lhs.description == null then
1321 rhs.description
1322 else if rhs.description == null then
1323 lhs.description
1324 else if lhs.description == rhs.description then
1325 lhs.description
1326 else
1327 throw "A submoduleWith option is declared multiple times with conflicting descriptions";
1328 };
1329 };
1330 };
1331
1332 # A value from a set of allowed ones.
1333 enum =
1334 values:
1335 let
1336 inherit (lib.lists) unique;
1337 show =
1338 v:
1339 if builtins.isString v then
1340 ''"${v}"''
1341 else if builtins.isInt v then
1342 builtins.toString v
1343 else if builtins.isBool v then
1344 boolToString v
1345 else
1346 ''<${builtins.typeOf v}>'';
1347 in
1348 mkOptionType rec {
1349 name = "enum";
1350 description =
1351 # Length 0 or 1 enums may occur in a design pattern with type merging
1352 # where an "interface" module declares an empty enum and other modules
1353 # provide implementations, each extending the enum with their own
1354 # identifier.
1355 if values == [ ] then
1356 "impossible (empty enum)"
1357 else if builtins.length values == 1 then
1358 "value ${show (builtins.head values)} (singular enum)"
1359 else
1360 "one of ${concatMapStringsSep ", " show values}";
1361 descriptionClass = if builtins.length values < 2 then "noun" else "conjunction";
1362 check = flip elem values;
1363 merge = mergeEqualOption;
1364 functor = (defaultFunctor name) // {
1365 payload = { inherit values; };
1366 type = payload: types.enum payload.values;
1367 binOp = a: b: { values = unique (a.values ++ b.values); };
1368 };
1369 };
1370
1371 # Either value of type `t1` or `t2`.
1372 either =
1373 t1: t2:
1374 mkOptionType rec {
1375 name = "either";
1376 description =
1377 if t1.descriptionClass or null == "nonRestrictiveClause" then
1378 # Plain, but add comma
1379 "${t1.description}, or ${
1380 optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t2
1381 }"
1382 else
1383 "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${
1384 optionDescriptionPhrase (
1385 class: class == "noun" || class == "conjunction" || class == "composite"
1386 ) t2
1387 }";
1388 descriptionClass = "conjunction";
1389 check = x: t1.check x || t2.check x;
1390 merge =
1391 loc: defs:
1392 let
1393 defList = map (d: d.value) defs;
1394 in
1395 if all (x: t1.check x) defList then
1396 t1.merge loc defs
1397 else if all (x: t2.check x) defList then
1398 t2.merge loc defs
1399 else
1400 mergeOneOption loc defs;
1401 typeMerge =
1402 f':
1403 let
1404 mt1 = t1.typeMerge (elemAt f'.payload.elemType 0).functor;
1405 mt2 = t2.typeMerge (elemAt f'.payload.elemType 1).functor;
1406 in
1407 if (name == f'.name) && (mt1 != null) && (mt2 != null) then functor.type mt1 mt2 else null;
1408 functor = elemTypeFunctor name {
1409 elemType = [
1410 t1
1411 t2
1412 ];
1413 };
1414 nestedTypes.left = t1;
1415 nestedTypes.right = t2;
1416 };
1417
1418 # Any of the types in the given list
1419 oneOf =
1420 ts:
1421 let
1422 head' =
1423 if ts == [ ] then throw "types.oneOf needs to get at least one type in its argument" else head ts;
1424 tail' = tail ts;
1425 in
1426 foldl' either head' tail';
1427
1428 # Either value of type `coercedType` or `finalType`, the former is
1429 # converted to `finalType` using `coerceFunc`.
1430 coercedTo =
1431 coercedType: coerceFunc: finalType:
1432 assert lib.assertMsg (
1433 coercedType.getSubModules == null
1434 ) "coercedTo: coercedType must not have submodules (it’s a ${coercedType.description})";
1435 mkOptionType rec {
1436 name = "coercedTo";
1437 description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${
1438 optionDescriptionPhrase (class: class == "noun") coercedType
1439 } convertible to it";
1440 check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
1441 merge =
1442 loc: defs:
1443 let
1444 coerceVal = val: if coercedType.check val then coerceFunc val else val;
1445 in
1446 finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
1447 emptyValue = finalType.emptyValue;
1448 getSubOptions = finalType.getSubOptions;
1449 getSubModules = finalType.getSubModules;
1450 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
1451 typeMerge = t: null;
1452 functor = (defaultFunctor name) // {
1453 wrappedDeprecationMessage = makeWrappedDeprecationMessage { elemType = finalType; };
1454 };
1455 nestedTypes.coercedType = coercedType;
1456 nestedTypes.finalType = finalType;
1457 };
1458
1459 # Augment the given type with an additional type check function.
1460 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
1461
1462 };
1463
1464 /**
1465 Merges two option types together.
1466
1467 :::{.note}
1468 Uses the type merge function of the first type, to merge it with the second type.
1469
1470 Usually types can only be merged if they are of the same type
1471 :::
1472
1473 # Inputs
1474
1475 : `a` (option type): The first option type.
1476 : `b` (option type): The second option type.
1477
1478 # Returns
1479
1480 - The merged option type.
1481 - `{ _type = "merge-error"; error = "Cannot merge types"; }` if the types can't be merged.
1482
1483 # Examples
1484 :::{.example}
1485 ## `lib.types.mergeTypes` usage example
1486 ```nix
1487 let
1488 enumAB = lib.types.enum ["A" "B"];
1489 enumXY = lib.types.enum ["X" "Y"];
1490 # This operation could be notated as: [ A ] | [ B ] -> [ A B ]
1491 merged = lib.types.mergeTypes enumAB enumXY; # -> enum [ "A" "B" "X" "Y" ]
1492 in
1493 assert merged.check "A"; # true
1494 assert merged.check "B"; # true
1495 assert merged.check "X"; # true
1496 assert merged.check "Y"; # true
1497 merged.check "C" # false
1498 ```
1499 :::
1500 */
1501 mergeTypes =
1502 a: b:
1503 assert isOptionType a && isOptionType b;
1504 let
1505 merged = a.typeMerge b.functor;
1506 in
1507 if merged == null then setType "merge-error" { error = "Cannot merge types"; } else merged;
1508 };
1509
1510in
1511outer_types // outer_types.types