at 25.11-pre 54 kB view raw
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 # &gt; and &lt; 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 (its 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