at 24.05-pre 35 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 toDerivation 19 toList 20 ; 21 inherit (lib.lists) 22 all 23 concatLists 24 count 25 elemAt 26 filter 27 foldl' 28 head 29 imap1 30 last 31 length 32 tail 33 ; 34 inherit (lib.attrsets) 35 attrNames 36 filterAttrs 37 hasAttr 38 mapAttrs 39 optionalAttrs 40 zipAttrsWith 41 ; 42 inherit (lib.options) 43 getFiles 44 getValues 45 mergeDefaultOption 46 mergeEqualOption 47 mergeOneOption 48 mergeUniqueOption 49 showFiles 50 showOption 51 ; 52 inherit (lib.strings) 53 concatMapStringsSep 54 concatStringsSep 55 escapeNixString 56 hasInfix 57 isStringLike 58 ; 59 inherit (lib.trivial) 60 boolToString 61 ; 62 63 inherit (lib.modules) 64 mergeDefinitions 65 fixupOptionType 66 mergeOptionDecls 67 ; 68 outer_types = 69rec { 70 isType = type: x: (x._type or "") == type; 71 72 setType = typeName: value: value // { 73 _type = typeName; 74 }; 75 76 77 # Default type merging function 78 # takes two type functors and return the merged type 79 defaultTypeMerge = f: f': 80 let wrapped = f.wrapped.typeMerge f'.wrapped.functor; 81 payload = f.binOp f.payload f'.payload; 82 in 83 # cannot merge different types 84 if f.name != f'.name 85 then null 86 # simple types 87 else if (f.wrapped == null && f'.wrapped == null) 88 && (f.payload == null && f'.payload == null) 89 then f.type 90 # composed types 91 else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null) 92 then f.type wrapped 93 # value types 94 else if (f.payload != null && f'.payload != null) && (payload != null) 95 then f.type payload 96 else null; 97 98 # Default type functor 99 defaultFunctor = name: { 100 inherit name; 101 type = types.${name} or null; 102 wrapped = null; 103 payload = null; 104 binOp = a: b: null; 105 }; 106 107 isOptionType = isType "option-type"; 108 mkOptionType = 109 { # Human-readable representation of the type, should be equivalent to 110 # the type function name. 111 name 112 , # Description of the type, defined recursively by embedding the wrapped type if any. 113 description ? null 114 # A hint for whether or not this description needs parentheses. Possible values: 115 # - "noun": a simple noun phrase such as "positive integer" 116 # - "conjunction": a phrase with a potentially ambiguous "or" connective. 117 # - "composite": a phrase with an "of" connective 118 # See the `optionDescriptionPhrase` function. 119 , descriptionClass ? null 120 , # DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING! 121 # Function applied to each definition that must return false when a definition 122 # does not match the type. It should not check more than the root of the value, 123 # because checking nested values reduces laziness, leading to unnecessary 124 # infinite recursions in the module system. 125 # Further checks of nested values should be performed by throwing in 126 # the merge function. 127 # Strict and deep type checking can be performed by calling lib.deepSeq on 128 # the merged value. 129 # 130 # See https://github.com/NixOS/nixpkgs/pull/6794 that introduced this change, 131 # https://github.com/NixOS/nixpkgs/pull/173568 and 132 # https://github.com/NixOS/nixpkgs/pull/168295 that attempted to revert this, 133 # https://github.com/NixOS/nixpkgs/issues/191124 and 134 # https://github.com/NixOS/nixos-search/issues/391 for what happens if you ignore 135 # this disclaimer. 136 check ? (x: true) 137 , # Merge a list of definitions together into a single value. 138 # This function is called with two arguments: the location of 139 # the option in the configuration as a list of strings 140 # (e.g. ["boot" "loader "grub" "enable"]), and a list of 141 # definition values and locations (e.g. [ { file = "/foo.nix"; 142 # value = 1; } { file = "/bar.nix"; value = 2 } ]). 143 merge ? mergeDefaultOption 144 , # Whether this type has a value representing nothingness. If it does, 145 # this should be a value of the form { value = <the nothing value>; } 146 # If it doesn't, this should be {} 147 # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`. 148 emptyValue ? {} 149 , # Return a flat list of sub-options. Used to generate 150 # documentation. 151 getSubOptions ? prefix: {} 152 , # List of modules if any, or null if none. 153 getSubModules ? null 154 , # Function for building the same option type with a different list of 155 # modules. 156 substSubModules ? m: null 157 , # Function that merge type declarations. 158 # internal, takes a functor as argument and returns the merged type. 159 # returning null means the type is not mergeable 160 typeMerge ? defaultTypeMerge functor 161 , # The type functor. 162 # internal, representation of the type as an attribute set. 163 # name: name of the type 164 # type: type function. 165 # wrapped: the type wrapped in case of compound types. 166 # payload: values of the type, two payloads of the same type must be 167 # combinable with the binOp binary operation. 168 # binOp: binary operation that merge two payloads of the same type. 169 functor ? defaultFunctor name 170 , # The deprecation message to display when this type is used by an option 171 # If null, the type isn't deprecated 172 deprecationMessage ? null 173 , # The types that occur in the definition of this type. This is used to 174 # issue deprecation warnings recursively. Can also be used to reuse 175 # nested types 176 nestedTypes ? {} 177 }: 178 { _type = "option-type"; 179 inherit 180 name check merge emptyValue getSubOptions getSubModules substSubModules 181 typeMerge functor deprecationMessage nestedTypes descriptionClass; 182 description = if description == null then name else description; 183 }; 184 185 # optionDescriptionPhrase :: (str -> bool) -> optionType -> str 186 # 187 # Helper function for producing unambiguous but readable natural language 188 # descriptions of types. 189 # 190 # Parameters 191 # 192 # optionDescriptionPhase unparenthesize optionType 193 # 194 # `unparenthesize`: A function from descriptionClass string to boolean. 195 # It must return true when the class of phrase will fit unambiguously into 196 # the description of the caller. 197 # 198 # `optionType`: The option type to parenthesize or not. 199 # The option whose description we're returning. 200 # 201 # Return value 202 # 203 # The description of the `optionType`, with parentheses if there may be an 204 # ambiguity. 205 optionDescriptionPhrase = unparenthesize: t: 206 if unparenthesize (t.descriptionClass or null) 207 then t.description 208 else "(${t.description})"; 209 210 # When adding new types don't forget to document them in 211 # nixos/doc/manual/development/option-types.xml! 212 types = rec { 213 214 raw = mkOptionType { 215 name = "raw"; 216 description = "raw value"; 217 descriptionClass = "noun"; 218 check = value: true; 219 merge = mergeOneOption; 220 }; 221 222 anything = mkOptionType { 223 name = "anything"; 224 description = "anything"; 225 descriptionClass = "noun"; 226 check = value: true; 227 merge = loc: defs: 228 let 229 getType = value: 230 if isAttrs value && isStringLike value 231 then "stringCoercibleSet" 232 else builtins.typeOf value; 233 234 # Returns the common type of all definitions, throws an error if they 235 # don't have the same type 236 commonType = foldl' (type: def: 237 if getType def.value == type 238 then type 239 else throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}" 240 ) (getType (head defs).value) defs; 241 242 mergeFunction = { 243 # Recursively merge attribute sets 244 set = (attrsOf anything).merge; 245 # Safe and deterministic behavior for lists is to only accept one definition 246 # listOf only used to apply mkIf and co. 247 list = 248 if length defs > 1 249 then throw "The option `${showOption loc}' has conflicting definitions, in ${showFiles (getFiles defs)}." 250 else (listOf anything).merge; 251 # This is the type of packages, only accept a single definition 252 stringCoercibleSet = mergeOneOption; 253 lambda = loc: defs: arg: anything.merge 254 (loc ++ [ "<function body>" ]) 255 (map (def: { 256 file = def.file; 257 value = def.value arg; 258 }) defs); 259 # Otherwise fall back to only allowing all equal definitions 260 }.${commonType} or mergeEqualOption; 261 in mergeFunction loc defs; 262 }; 263 264 unspecified = mkOptionType { 265 name = "unspecified"; 266 description = "unspecified value"; 267 descriptionClass = "noun"; 268 }; 269 270 bool = mkOptionType { 271 name = "bool"; 272 description = "boolean"; 273 descriptionClass = "noun"; 274 check = isBool; 275 merge = mergeEqualOption; 276 }; 277 278 int = mkOptionType { 279 name = "int"; 280 description = "signed integer"; 281 descriptionClass = "noun"; 282 check = isInt; 283 merge = mergeEqualOption; 284 }; 285 286 # Specialized subdomains of int 287 ints = 288 let 289 betweenDesc = lowest: highest: 290 "${toString lowest} and ${toString highest} (both inclusive)"; 291 between = lowest: highest: 292 assert lib.assertMsg (lowest <= highest) 293 "ints.between: lowest must be smaller than highest"; 294 addCheck int (x: x >= lowest && x <= highest) // { 295 name = "intBetween"; 296 description = "integer between ${betweenDesc lowest highest}"; 297 }; 298 ign = lowest: highest: name: docStart: 299 between lowest highest // { 300 inherit name; 301 description = docStart + "; between ${betweenDesc lowest highest}"; 302 }; 303 unsign = bit: range: ign 0 (range - 1) 304 "unsignedInt${toString bit}" "${toString bit} bit unsigned integer"; 305 sign = bit: range: ign (0 - (range / 2)) (range / 2 - 1) 306 "signedInt${toString bit}" "${toString bit} bit signed integer"; 307 308 in { 309 /* An int with a fixed range. 310 * 311 * Example: 312 * (ints.between 0 100).check (-1) 313 * => false 314 * (ints.between 0 100).check (101) 315 * => false 316 * (ints.between 0 0).check 0 317 * => true 318 */ 319 inherit between; 320 321 unsigned = addCheck types.int (x: x >= 0) // { 322 name = "unsignedInt"; 323 description = "unsigned integer, meaning >=0"; 324 }; 325 positive = addCheck types.int (x: x > 0) // { 326 name = "positiveInt"; 327 description = "positive integer, meaning >0"; 328 }; 329 u8 = unsign 8 256; 330 u16 = unsign 16 65536; 331 # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808) 332 # the smallest int Nix accepts is -2^63 (-9223372036854775807) 333 u32 = unsign 32 4294967296; 334 # u64 = unsign 64 18446744073709551616; 335 336 s8 = sign 8 256; 337 s16 = sign 16 65536; 338 s32 = sign 32 4294967296; 339 }; 340 341 # Alias of u16 for a port number 342 port = ints.u16; 343 344 float = mkOptionType { 345 name = "float"; 346 description = "floating point number"; 347 descriptionClass = "noun"; 348 check = isFloat; 349 merge = mergeEqualOption; 350 }; 351 352 number = either int float; 353 354 numbers = let 355 betweenDesc = lowest: highest: 356 "${builtins.toJSON lowest} and ${builtins.toJSON highest} (both inclusive)"; 357 in { 358 between = lowest: highest: 359 assert lib.assertMsg (lowest <= highest) 360 "numbers.between: lowest must be smaller than highest"; 361 addCheck number (x: x >= lowest && x <= highest) // { 362 name = "numberBetween"; 363 description = "integer or floating point number between ${betweenDesc lowest highest}"; 364 }; 365 366 nonnegative = addCheck number (x: x >= 0) // { 367 name = "numberNonnegative"; 368 description = "nonnegative integer or floating point number, meaning >=0"; 369 }; 370 positive = addCheck number (x: x > 0) // { 371 name = "numberPositive"; 372 description = "positive integer or floating point number, meaning >0"; 373 }; 374 }; 375 376 str = mkOptionType { 377 name = "str"; 378 description = "string"; 379 descriptionClass = "noun"; 380 check = isString; 381 merge = mergeEqualOption; 382 }; 383 384 nonEmptyStr = mkOptionType { 385 name = "nonEmptyStr"; 386 description = "non-empty string"; 387 descriptionClass = "noun"; 388 check = x: str.check x && builtins.match "[ \t\n]*" x == null; 389 inherit (str) merge; 390 }; 391 392 # Allow a newline character at the end and trim it in the merge function. 393 singleLineStr = 394 let 395 inherit (strMatching "[^\n\r]*\n?") check merge; 396 in 397 mkOptionType { 398 name = "singleLineStr"; 399 description = "(optionally newline-terminated) single-line string"; 400 descriptionClass = "noun"; 401 inherit check; 402 merge = loc: defs: 403 lib.removeSuffix "\n" (merge loc defs); 404 }; 405 406 strMatching = pattern: mkOptionType { 407 name = "strMatching ${escapeNixString pattern}"; 408 description = "string matching the pattern ${pattern}"; 409 descriptionClass = "noun"; 410 check = x: str.check x && builtins.match pattern x != null; 411 inherit (str) merge; 412 }; 413 414 # Merge multiple definitions by concatenating them (with the given 415 # separator between the values). 416 separatedString = sep: mkOptionType rec { 417 name = "separatedString"; 418 description = if sep == "" 419 then "Concatenated string" # for types.string. 420 else "strings concatenated with ${builtins.toJSON sep}" 421 ; 422 descriptionClass = "noun"; 423 check = isString; 424 merge = loc: defs: concatStringsSep sep (getValues defs); 425 functor = (defaultFunctor name) // { 426 payload = sep; 427 binOp = sepLhs: sepRhs: 428 if sepLhs == sepRhs then sepLhs 429 else null; 430 }; 431 }; 432 433 lines = separatedString "\n"; 434 commas = separatedString ","; 435 envVar = separatedString ":"; 436 437 # Deprecated; should not be used because it quietly concatenates 438 # strings, which is usually not what you want. 439 # We use a lib.warn because `deprecationMessage` doesn't trigger in nested types such as `attrsOf string` 440 string = lib.warn 441 "The type `types.string` is deprecated. See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types." 442 (separatedString "" // { 443 name = "string"; 444 }); 445 446 passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // { 447 name = "passwdEntry ${entryType.name}"; 448 description = "${optionDescriptionPhrase (class: class == "noun") entryType}, not containing newlines or colons"; 449 }; 450 451 attrs = mkOptionType { 452 name = "attrs"; 453 description = "attribute set"; 454 check = isAttrs; 455 merge = loc: foldl' (res: def: res // def.value) {}; 456 emptyValue = { value = {}; }; 457 }; 458 459 # A package is a top-level store path (/nix/store/hash-name). This includes: 460 # - derivations 461 # - more generally, attribute sets with an `outPath` or `__toString` attribute 462 # pointing to a store path, e.g. flake inputs 463 # - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo) 464 # - hardcoded store path literals (/nix/store/hash-foo) or strings without context 465 # ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath. 466 # If you don't need a *top-level* store path, consider using pathInStore instead. 467 package = mkOptionType { 468 name = "package"; 469 descriptionClass = "noun"; 470 check = x: isDerivation x || isStorePath x; 471 merge = loc: defs: 472 let res = mergeOneOption loc defs; 473 in if builtins.isPath res || (builtins.isString res && ! builtins.hasContext res) 474 then toDerivation res 475 else res; 476 }; 477 478 shellPackage = package // { 479 check = x: isDerivation x && hasAttr "shellPath" x; 480 }; 481 482 pkgs = addCheck 483 (unique { message = "A Nixpkgs pkgs set can not be merged with another pkgs set."; } attrs // { 484 name = "pkgs"; 485 descriptionClass = "noun"; 486 description = "Nixpkgs package set"; 487 }) 488 (x: (x._type or null) == "pkgs"); 489 490 path = mkOptionType { 491 name = "path"; 492 descriptionClass = "noun"; 493 check = x: isStringLike x && builtins.substring 0 1 (toString x) == "/"; 494 merge = mergeEqualOption; 495 }; 496 497 pathInStore = mkOptionType { 498 name = "pathInStore"; 499 description = "path in the Nix store"; 500 descriptionClass = "noun"; 501 check = x: isStringLike x && builtins.match "${builtins.storeDir}/[^.].*" (toString x) != null; 502 merge = mergeEqualOption; 503 }; 504 505 listOf = elemType: mkOptionType rec { 506 name = "listOf"; 507 description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; 508 descriptionClass = "composite"; 509 check = isList; 510 merge = loc: defs: 511 map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def: 512 imap1 (m: def': 513 (mergeDefinitions 514 (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) 515 elemType 516 [{ inherit (def) file; value = def'; }] 517 ).optionalValue 518 ) def.value 519 ) defs))); 520 emptyValue = { value = []; }; 521 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); 522 getSubModules = elemType.getSubModules; 523 substSubModules = m: listOf (elemType.substSubModules m); 524 functor = (defaultFunctor name) // { wrapped = elemType; }; 525 nestedTypes.elemType = elemType; 526 }; 527 528 nonEmptyListOf = elemType: 529 let list = addCheck (types.listOf elemType) (l: l != []); 530 in list // { 531 description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}"; 532 emptyValue = { }; # no .value attr, meaning unset 533 }; 534 535 attrsOf = elemType: mkOptionType rec { 536 name = "attrsOf"; 537 description = "attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; 538 descriptionClass = "composite"; 539 check = isAttrs; 540 merge = loc: defs: 541 mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: 542 (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue 543 ) 544 # Push down position info. 545 (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs))); 546 emptyValue = { value = {}; }; 547 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); 548 getSubModules = elemType.getSubModules; 549 substSubModules = m: attrsOf (elemType.substSubModules m); 550 functor = (defaultFunctor name) // { wrapped = elemType; }; 551 nestedTypes.elemType = elemType; 552 }; 553 554 # A version of attrsOf that's lazy in its values at the expense of 555 # conditional definitions not working properly. E.g. defining a value with 556 # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with 557 # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an 558 # error that it's not defined. Use only if conditional definitions don't make sense. 559 lazyAttrsOf = elemType: mkOptionType rec { 560 name = "lazyAttrsOf"; 561 description = "lazy attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; 562 descriptionClass = "composite"; 563 check = isAttrs; 564 merge = loc: defs: 565 zipAttrsWith (name: defs: 566 let merged = mergeDefinitions (loc ++ [name]) elemType defs; 567 # mergedValue will trigger an appropriate error when accessed 568 in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue 569 ) 570 # Push down position info. 571 (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs); 572 emptyValue = { value = {}; }; 573 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); 574 getSubModules = elemType.getSubModules; 575 substSubModules = m: lazyAttrsOf (elemType.substSubModules m); 576 functor = (defaultFunctor name) // { wrapped = elemType; }; 577 nestedTypes.elemType = elemType; 578 }; 579 580 # TODO: deprecate this in the future: 581 loaOf = elemType: types.attrsOf elemType // { 582 name = "loaOf"; 583 deprecationMessage = "Mixing lists with attribute values is no longer" 584 + " possible; please use `types.attrsOf` instead. See" 585 + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation."; 586 nestedTypes.elemType = elemType; 587 }; 588 589 # Value of given type but with no merging (i.e. `uniq list`s are not concatenated). 590 uniq = elemType: mkOptionType rec { 591 name = "uniq"; 592 inherit (elemType) description descriptionClass check; 593 merge = mergeOneOption; 594 emptyValue = elemType.emptyValue; 595 getSubOptions = elemType.getSubOptions; 596 getSubModules = elemType.getSubModules; 597 substSubModules = m: uniq (elemType.substSubModules m); 598 functor = (defaultFunctor name) // { wrapped = elemType; }; 599 nestedTypes.elemType = elemType; 600 }; 601 602 unique = { message }: type: mkOptionType rec { 603 name = "unique"; 604 inherit (type) description descriptionClass check; 605 merge = mergeUniqueOption { inherit message; }; 606 emptyValue = type.emptyValue; 607 getSubOptions = type.getSubOptions; 608 getSubModules = type.getSubModules; 609 substSubModules = m: uniq (type.substSubModules m); 610 functor = (defaultFunctor name) // { wrapped = type; }; 611 nestedTypes.elemType = type; 612 }; 613 614 # Null or value of ... 615 nullOr = elemType: mkOptionType rec { 616 name = "nullOr"; 617 description = "null or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType}"; 618 descriptionClass = "conjunction"; 619 check = x: x == null || elemType.check x; 620 merge = loc: defs: 621 let nrNulls = count (def: def.value == null) defs; in 622 if nrNulls == length defs then null 623 else if nrNulls != 0 then 624 throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." 625 else elemType.merge loc defs; 626 emptyValue = { value = null; }; 627 getSubOptions = elemType.getSubOptions; 628 getSubModules = elemType.getSubModules; 629 substSubModules = m: nullOr (elemType.substSubModules m); 630 functor = (defaultFunctor name) // { wrapped = elemType; }; 631 nestedTypes.elemType = elemType; 632 }; 633 634 functionTo = elemType: mkOptionType { 635 name = "functionTo"; 636 description = "function that evaluates to a(n) ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; 637 descriptionClass = "composite"; 638 check = isFunction; 639 merge = loc: defs: 640 fnArgs: (mergeDefinitions (loc ++ [ "<function body>" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue; 641 getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]); 642 getSubModules = elemType.getSubModules; 643 substSubModules = m: functionTo (elemType.substSubModules m); 644 functor = (defaultFunctor "functionTo") // { wrapped = elemType; }; 645 nestedTypes.elemType = elemType; 646 }; 647 648 # A submodule (like typed attribute set). See NixOS manual. 649 submodule = modules: submoduleWith { 650 shorthandOnlyDefinesConfig = true; 651 modules = toList modules; 652 }; 653 654 # A module to be imported in some other part of the configuration. 655 deferredModule = deferredModuleWith { }; 656 657 # A module to be imported in some other part of the configuration. 658 # `staticModules`' options will be added to the documentation, unlike 659 # options declared via `config`. 660 deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType { 661 name = "deferredModule"; 662 description = "module"; 663 descriptionClass = "noun"; 664 check = x: isAttrs x || isFunction x || path.check x; 665 merge = loc: defs: { 666 imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; 667 }; 668 inherit (submoduleWith { modules = staticModules; }) 669 getSubOptions 670 getSubModules; 671 substSubModules = m: deferredModuleWith (attrs // { 672 staticModules = m; 673 }); 674 functor = defaultFunctor "deferredModuleWith" // { 675 type = types.deferredModuleWith; 676 payload = { 677 inherit staticModules; 678 }; 679 binOp = lhs: rhs: { 680 staticModules = lhs.staticModules ++ rhs.staticModules; 681 }; 682 }; 683 }; 684 685 # The type of a type! 686 optionType = mkOptionType { 687 name = "optionType"; 688 description = "optionType"; 689 descriptionClass = "noun"; 690 check = value: value._type or null == "option-type"; 691 merge = loc: defs: 692 if length defs == 1 693 then (head defs).value 694 else let 695 # Prepares the type definitions for mergeOptionDecls, which 696 # annotates submodules types with file locations 697 optionModules = map ({ value, file }: 698 { 699 _file = file; 700 # There's no way to merge types directly from the module system, 701 # but we can cheat a bit by just declaring an option with the type 702 options = lib.mkOption { 703 type = value; 704 }; 705 } 706 ) defs; 707 # Merges all the types into a single one, including submodule merging. 708 # This also propagates file information to all submodules 709 mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules); 710 in mergedOption.type; 711 }; 712 713 submoduleWith = 714 { modules 715 , specialArgs ? {} 716 , shorthandOnlyDefinesConfig ? false 717 , description ? null 718 , class ? null 719 }@attrs: 720 let 721 inherit (lib.modules) evalModules; 722 723 allModules = defs: map ({ value, file }: 724 if isAttrs value && shorthandOnlyDefinesConfig 725 then { _file = file; config = value; } 726 else { _file = file; imports = [ value ]; } 727 ) defs; 728 729 base = evalModules { 730 inherit class specialArgs; 731 modules = [{ 732 # This is a work-around for the fact that some sub-modules, 733 # such as the one included in an attribute set, expects an "args" 734 # attribute to be given to the sub-module. As the option 735 # evaluation does not have any specific attribute name yet, we 736 # provide a default for the documentation and the freeform type. 737 # 738 # This is necessary as some option declaration might use the 739 # "name" attribute given as argument of the submodule and use it 740 # as the default of option declarations. 741 # 742 # We use lookalike unicode single angle quotation marks because 743 # of the docbook transformation the options receive. In all uses 744 # &gt; and &lt; wouldn't be encoded correctly so the encoded values 745 # would be used, and use of `<` and `>` would break the XML document. 746 # It shouldn't cause an issue since this is cosmetic for the manual. 747 _module.args.name = lib.mkOptionDefault "name"; 748 }] ++ modules; 749 }; 750 751 freeformType = base._module.freeformType; 752 753 name = "submodule"; 754 755 in 756 mkOptionType { 757 inherit name; 758 description = 759 if description != null then description 760 else freeformType.description or name; 761 check = x: isAttrs x || isFunction x || path.check x; 762 merge = loc: defs: 763 (base.extendModules { 764 modules = [ { _module.args.name = last loc; } ] ++ allModules defs; 765 prefix = loc; 766 }).config; 767 emptyValue = { value = {}; }; 768 getSubOptions = prefix: (base.extendModules 769 { inherit prefix; }).options // optionalAttrs (freeformType != null) { 770 # Expose the sub options of the freeform type. Note that the option 771 # discovery doesn't care about the attribute name used here, so this 772 # is just to avoid conflicts with potential options from the submodule 773 _freeformOptions = freeformType.getSubOptions prefix; 774 }; 775 getSubModules = modules; 776 substSubModules = m: submoduleWith (attrs // { 777 modules = m; 778 }); 779 nestedTypes = lib.optionalAttrs (freeformType != null) { 780 freeformType = freeformType; 781 }; 782 functor = defaultFunctor name // { 783 type = types.submoduleWith; 784 payload = { 785 inherit modules class specialArgs shorthandOnlyDefinesConfig description; 786 }; 787 binOp = lhs: rhs: { 788 class = 789 # `or null` was added for backwards compatibility only. `class` is 790 # always set in the current version of the module system. 791 if lhs.class or null == null then rhs.class or null 792 else if rhs.class or null == null then lhs.class or null 793 else if lhs.class or null == rhs.class then lhs.class or null 794 else throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\"."; 795 modules = lhs.modules ++ rhs.modules; 796 specialArgs = 797 let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs; 798 in if intersecting == {} 799 then lhs.specialArgs // rhs.specialArgs 800 else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\""; 801 shorthandOnlyDefinesConfig = 802 if lhs.shorthandOnlyDefinesConfig == null 803 then rhs.shorthandOnlyDefinesConfig 804 else if rhs.shorthandOnlyDefinesConfig == null 805 then lhs.shorthandOnlyDefinesConfig 806 else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig 807 then lhs.shorthandOnlyDefinesConfig 808 else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values"; 809 description = 810 if lhs.description == null 811 then rhs.description 812 else if rhs.description == null 813 then lhs.description 814 else if lhs.description == rhs.description 815 then lhs.description 816 else throw "A submoduleWith option is declared multiple times with conflicting descriptions"; 817 }; 818 }; 819 }; 820 821 # A value from a set of allowed ones. 822 enum = values: 823 let 824 inherit (lib.lists) unique; 825 show = v: 826 if builtins.isString v then ''"${v}"'' 827 else if builtins.isInt v then builtins.toString v 828 else if builtins.isBool v then boolToString v 829 else ''<${builtins.typeOf v}>''; 830 in 831 mkOptionType rec { 832 name = "enum"; 833 description = 834 # Length 0 or 1 enums may occur in a design pattern with type merging 835 # where an "interface" module declares an empty enum and other modules 836 # provide implementations, each extending the enum with their own 837 # identifier. 838 if values == [] then 839 "impossible (empty enum)" 840 else if builtins.length values == 1 then 841 "value ${show (builtins.head values)} (singular enum)" 842 else 843 "one of ${concatMapStringsSep ", " show values}"; 844 descriptionClass = 845 if builtins.length values < 2 846 then "noun" 847 else "conjunction"; 848 check = flip elem values; 849 merge = mergeEqualOption; 850 functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; 851 }; 852 853 # Either value of type `t1` or `t2`. 854 either = t1: t2: mkOptionType rec { 855 name = "either"; 856 description = "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite") t2}"; 857 descriptionClass = "conjunction"; 858 check = x: t1.check x || t2.check x; 859 merge = loc: defs: 860 let 861 defList = map (d: d.value) defs; 862 in 863 if all (x: t1.check x) defList 864 then t1.merge loc defs 865 else if all (x: t2.check x) defList 866 then t2.merge loc defs 867 else mergeOneOption loc defs; 868 typeMerge = f': 869 let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; 870 mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; 871 in 872 if (name == f'.name) && (mt1 != null) && (mt2 != null) 873 then functor.type mt1 mt2 874 else null; 875 functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; 876 nestedTypes.left = t1; 877 nestedTypes.right = t2; 878 }; 879 880 # Any of the types in the given list 881 oneOf = ts: 882 let 883 head' = if ts == [] then throw "types.oneOf needs to get at least one type in its argument" else head ts; 884 tail' = tail ts; 885 in foldl' either head' tail'; 886 887 # Either value of type `coercedType` or `finalType`, the former is 888 # converted to `finalType` using `coerceFunc`. 889 coercedTo = coercedType: coerceFunc: finalType: 890 assert lib.assertMsg (coercedType.getSubModules == null) 891 "coercedTo: coercedType must not have submodules (its a ${ 892 coercedType.description})"; 893 mkOptionType rec { 894 name = "coercedTo"; 895 description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${optionDescriptionPhrase (class: class == "noun") coercedType} convertible to it"; 896 check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x; 897 merge = loc: defs: 898 let 899 coerceVal = val: 900 if coercedType.check val then coerceFunc val 901 else val; 902 in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 903 emptyValue = finalType.emptyValue; 904 getSubOptions = finalType.getSubOptions; 905 getSubModules = finalType.getSubModules; 906 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 907 typeMerge = t1: t2: null; 908 functor = (defaultFunctor name) // { wrapped = finalType; }; 909 nestedTypes.coercedType = coercedType; 910 nestedTypes.finalType = finalType; 911 }; 912 913 # Augment the given type with an additional type check function. 914 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; 915 916 }; 917}; 918 919in outer_types // outer_types.types