at 23.05-pre 34 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 isCoercibleToString 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 rec { 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 && isCoercibleToString 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 string = separatedString "" // { 440 name = "string"; 441 deprecationMessage = "See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types."; 442 }; 443 444 passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // { 445 name = "passwdEntry ${entryType.name}"; 446 description = "${optionDescriptionPhrase (class: class == "noun") entryType}, not containing newlines or colons"; 447 }; 448 449 attrs = mkOptionType { 450 name = "attrs"; 451 description = "attribute set"; 452 check = isAttrs; 453 merge = loc: foldl' (res: def: res // def.value) {}; 454 emptyValue = { value = {}; }; 455 }; 456 457 # A package is a top-level store path (/nix/store/hash-name). This includes: 458 # - derivations 459 # - more generally, attribute sets with an `outPath` or `__toString` attribute 460 # pointing to a store path, e.g. flake inputs 461 # - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo) 462 # - hardcoded store path literals (/nix/store/hash-foo) or strings without context 463 # ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath. 464 package = mkOptionType { 465 name = "package"; 466 descriptionClass = "noun"; 467 check = x: isDerivation x || isStorePath x; 468 merge = loc: defs: 469 let res = mergeOneOption loc defs; 470 in if builtins.isPath res || (builtins.isString res && ! builtins.hasContext res) 471 then toDerivation res 472 else res; 473 }; 474 475 shellPackage = package // { 476 check = x: isDerivation x && hasAttr "shellPath" x; 477 }; 478 479 path = mkOptionType { 480 name = "path"; 481 descriptionClass = "noun"; 482 check = x: isCoercibleToString x && builtins.substring 0 1 (toString x) == "/"; 483 merge = mergeEqualOption; 484 }; 485 486 listOf = elemType: mkOptionType rec { 487 name = "listOf"; 488 description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; 489 descriptionClass = "composite"; 490 check = isList; 491 merge = loc: defs: 492 map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def: 493 imap1 (m: def': 494 (mergeDefinitions 495 (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) 496 elemType 497 [{ inherit (def) file; value = def'; }] 498 ).optionalValue 499 ) def.value 500 ) defs))); 501 emptyValue = { value = []; }; 502 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); 503 getSubModules = elemType.getSubModules; 504 substSubModules = m: listOf (elemType.substSubModules m); 505 functor = (defaultFunctor name) // { wrapped = elemType; }; 506 nestedTypes.elemType = elemType; 507 }; 508 509 nonEmptyListOf = elemType: 510 let list = addCheck (types.listOf elemType) (l: l != []); 511 in list // { 512 description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}"; 513 emptyValue = { }; # no .value attr, meaning unset 514 }; 515 516 attrsOf = elemType: mkOptionType rec { 517 name = "attrsOf"; 518 description = "attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; 519 descriptionClass = "composite"; 520 check = isAttrs; 521 merge = loc: defs: 522 mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: 523 (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue 524 ) 525 # Push down position info. 526 (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs))); 527 emptyValue = { value = {}; }; 528 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); 529 getSubModules = elemType.getSubModules; 530 substSubModules = m: attrsOf (elemType.substSubModules m); 531 functor = (defaultFunctor name) // { wrapped = elemType; }; 532 nestedTypes.elemType = elemType; 533 }; 534 535 # A version of attrsOf that's lazy in its values at the expense of 536 # conditional definitions not working properly. E.g. defining a value with 537 # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with 538 # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an 539 # error that it's not defined. Use only if conditional definitions don't make sense. 540 lazyAttrsOf = elemType: mkOptionType rec { 541 name = "lazyAttrsOf"; 542 description = "lazy attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; 543 descriptionClass = "composite"; 544 check = isAttrs; 545 merge = loc: defs: 546 zipAttrsWith (name: defs: 547 let merged = mergeDefinitions (loc ++ [name]) elemType defs; 548 # mergedValue will trigger an appropriate error when accessed 549 in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue 550 ) 551 # Push down position info. 552 (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs); 553 emptyValue = { value = {}; }; 554 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); 555 getSubModules = elemType.getSubModules; 556 substSubModules = m: lazyAttrsOf (elemType.substSubModules m); 557 functor = (defaultFunctor name) // { wrapped = elemType; }; 558 nestedTypes.elemType = elemType; 559 }; 560 561 # TODO: drop this in the future: 562 loaOf = elemType: types.attrsOf elemType // { 563 name = "loaOf"; 564 deprecationMessage = "Mixing lists with attribute values is no longer" 565 + " possible; please use `types.attrsOf` instead. See" 566 + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation."; 567 nestedTypes.elemType = elemType; 568 }; 569 570 # Value of given type but with no merging (i.e. `uniq list`s are not concatenated). 571 uniq = elemType: mkOptionType rec { 572 name = "uniq"; 573 inherit (elemType) description descriptionClass check; 574 merge = mergeOneOption; 575 emptyValue = elemType.emptyValue; 576 getSubOptions = elemType.getSubOptions; 577 getSubModules = elemType.getSubModules; 578 substSubModules = m: uniq (elemType.substSubModules m); 579 functor = (defaultFunctor name) // { wrapped = elemType; }; 580 nestedTypes.elemType = elemType; 581 }; 582 583 unique = { message }: type: mkOptionType rec { 584 name = "unique"; 585 inherit (type) description descriptionClass check; 586 merge = mergeUniqueOption { inherit message; }; 587 emptyValue = type.emptyValue; 588 getSubOptions = type.getSubOptions; 589 getSubModules = type.getSubModules; 590 substSubModules = m: uniq (type.substSubModules m); 591 functor = (defaultFunctor name) // { wrapped = type; }; 592 nestedTypes.elemType = type; 593 }; 594 595 # Null or value of ... 596 nullOr = elemType: mkOptionType rec { 597 name = "nullOr"; 598 description = "null or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType}"; 599 descriptionClass = "conjunction"; 600 check = x: x == null || elemType.check x; 601 merge = loc: defs: 602 let nrNulls = count (def: def.value == null) defs; in 603 if nrNulls == length defs then null 604 else if nrNulls != 0 then 605 throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." 606 else elemType.merge loc defs; 607 emptyValue = { value = null; }; 608 getSubOptions = elemType.getSubOptions; 609 getSubModules = elemType.getSubModules; 610 substSubModules = m: nullOr (elemType.substSubModules m); 611 functor = (defaultFunctor name) // { wrapped = elemType; }; 612 nestedTypes.elemType = elemType; 613 }; 614 615 functionTo = elemType: mkOptionType { 616 name = "functionTo"; 617 description = "function that evaluates to a(n) ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; 618 descriptionClass = "composite"; 619 check = isFunction; 620 merge = loc: defs: 621 fnArgs: (mergeDefinitions (loc ++ [ "<function body>" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue; 622 getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]); 623 getSubModules = elemType.getSubModules; 624 substSubModules = m: functionTo (elemType.substSubModules m); 625 functor = (defaultFunctor "functionTo") // { wrapped = elemType; }; 626 nestedTypes.elemType = elemType; 627 }; 628 629 # A submodule (like typed attribute set). See NixOS manual. 630 submodule = modules: submoduleWith { 631 shorthandOnlyDefinesConfig = true; 632 modules = toList modules; 633 }; 634 635 # A module to be imported in some other part of the configuration. 636 deferredModule = deferredModuleWith { }; 637 638 # A module to be imported in some other part of the configuration. 639 # `staticModules`' options will be added to the documentation, unlike 640 # options declared via `config`. 641 deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType { 642 name = "deferredModule"; 643 description = "module"; 644 descriptionClass = "noun"; 645 check = x: isAttrs x || isFunction x || path.check x; 646 merge = loc: defs: { 647 imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; 648 }; 649 inherit (submoduleWith { modules = staticModules; }) 650 getSubOptions 651 getSubModules; 652 substSubModules = m: deferredModuleWith (attrs // { 653 staticModules = m; 654 }); 655 functor = defaultFunctor "deferredModuleWith" // { 656 type = types.deferredModuleWith; 657 payload = { 658 inherit staticModules; 659 }; 660 binOp = lhs: rhs: { 661 staticModules = lhs.staticModules ++ rhs.staticModules; 662 }; 663 }; 664 }; 665 666 # The type of a type! 667 optionType = mkOptionType { 668 name = "optionType"; 669 description = "optionType"; 670 descriptionClass = "noun"; 671 check = value: value._type or null == "option-type"; 672 merge = loc: defs: 673 if length defs == 1 674 then (head defs).value 675 else let 676 # Prepares the type definitions for mergeOptionDecls, which 677 # annotates submodules types with file locations 678 optionModules = map ({ value, file }: 679 { 680 _file = file; 681 # There's no way to merge types directly from the module system, 682 # but we can cheat a bit by just declaring an option with the type 683 options = lib.mkOption { 684 type = value; 685 }; 686 } 687 ) defs; 688 # Merges all the types into a single one, including submodule merging. 689 # This also propagates file information to all submodules 690 mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules); 691 in mergedOption.type; 692 }; 693 694 submoduleWith = 695 { modules 696 , specialArgs ? {} 697 , shorthandOnlyDefinesConfig ? false 698 , description ? null 699 }@attrs: 700 let 701 inherit (lib.modules) evalModules; 702 703 allModules = defs: map ({ value, file }: 704 if isAttrs value && shorthandOnlyDefinesConfig 705 then { _file = file; config = value; } 706 else { _file = file; imports = [ value ]; } 707 ) defs; 708 709 base = evalModules { 710 inherit specialArgs; 711 modules = [{ 712 # This is a work-around for the fact that some sub-modules, 713 # such as the one included in an attribute set, expects an "args" 714 # attribute to be given to the sub-module. As the option 715 # evaluation does not have any specific attribute name yet, we 716 # provide a default for the documentation and the freeform type. 717 # 718 # This is necessary as some option declaration might use the 719 # "name" attribute given as argument of the submodule and use it 720 # as the default of option declarations. 721 # 722 # We use lookalike unicode single angle quotation marks because 723 # of the docbook transformation the options receive. In all uses 724 # &gt; and &lt; wouldn't be encoded correctly so the encoded values 725 # would be used, and use of `<` and `>` would break the XML document. 726 # It shouldn't cause an issue since this is cosmetic for the manual. 727 _module.args.name = lib.mkOptionDefault "name"; 728 }] ++ modules; 729 }; 730 731 freeformType = base._module.freeformType; 732 733 name = "submodule"; 734 735 in 736 mkOptionType { 737 inherit name; 738 description = 739 if description != null then description 740 else freeformType.description or name; 741 check = x: isAttrs x || isFunction x || path.check x; 742 merge = loc: defs: 743 (base.extendModules { 744 modules = [ { _module.args.name = last loc; } ] ++ allModules defs; 745 prefix = loc; 746 }).config; 747 emptyValue = { value = {}; }; 748 getSubOptions = prefix: (base.extendModules 749 { inherit prefix; }).options // optionalAttrs (freeformType != null) { 750 # Expose the sub options of the freeform type. Note that the option 751 # discovery doesn't care about the attribute name used here, so this 752 # is just to avoid conflicts with potential options from the submodule 753 _freeformOptions = freeformType.getSubOptions prefix; 754 }; 755 getSubModules = modules; 756 substSubModules = m: submoduleWith (attrs // { 757 modules = m; 758 }); 759 nestedTypes = lib.optionalAttrs (freeformType != null) { 760 freeformType = freeformType; 761 }; 762 functor = defaultFunctor name // { 763 type = types.submoduleWith; 764 payload = { 765 inherit modules specialArgs shorthandOnlyDefinesConfig description; 766 }; 767 binOp = lhs: rhs: { 768 modules = lhs.modules ++ rhs.modules; 769 specialArgs = 770 let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs; 771 in if intersecting == {} 772 then lhs.specialArgs // rhs.specialArgs 773 else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\""; 774 shorthandOnlyDefinesConfig = 775 if lhs.shorthandOnlyDefinesConfig == null 776 then rhs.shorthandOnlyDefinesConfig 777 else if rhs.shorthandOnlyDefinesConfig == null 778 then lhs.shorthandOnlyDefinesConfig 779 else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig 780 then lhs.shorthandOnlyDefinesConfig 781 else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values"; 782 description = 783 if lhs.description == null 784 then rhs.description 785 else if rhs.description == null 786 then lhs.description 787 else if lhs.description == rhs.description 788 then lhs.description 789 else throw "A submoduleWith option is declared multiple times with conflicting descriptions"; 790 }; 791 }; 792 }; 793 794 # A value from a set of allowed ones. 795 enum = values: 796 let 797 inherit (lib.lists) unique; 798 show = v: 799 if builtins.isString v then ''"${v}"'' 800 else if builtins.isInt v then builtins.toString v 801 else if builtins.isBool v then boolToString v 802 else ''<${builtins.typeOf v}>''; 803 in 804 mkOptionType rec { 805 name = "enum"; 806 description = 807 # Length 0 or 1 enums may occur in a design pattern with type merging 808 # where an "interface" module declares an empty enum and other modules 809 # provide implementations, each extending the enum with their own 810 # identifier. 811 if values == [] then 812 "impossible (empty enum)" 813 else if builtins.length values == 1 then 814 "value ${show (builtins.head values)} (singular enum)" 815 else 816 "one of ${concatMapStringsSep ", " show values}"; 817 descriptionClass = 818 if builtins.length values < 2 819 then "noun" 820 else "conjunction"; 821 check = flip elem values; 822 merge = mergeEqualOption; 823 functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; 824 }; 825 826 # Either value of type `t1` or `t2`. 827 either = t1: t2: mkOptionType rec { 828 name = "either"; 829 description = "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite") t2}"; 830 descriptionClass = "conjunction"; 831 check = x: t1.check x || t2.check x; 832 merge = loc: defs: 833 let 834 defList = map (d: d.value) defs; 835 in 836 if all (x: t1.check x) defList 837 then t1.merge loc defs 838 else if all (x: t2.check x) defList 839 then t2.merge loc defs 840 else mergeOneOption loc defs; 841 typeMerge = f': 842 let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; 843 mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; 844 in 845 if (name == f'.name) && (mt1 != null) && (mt2 != null) 846 then functor.type mt1 mt2 847 else null; 848 functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; 849 nestedTypes.left = t1; 850 nestedTypes.right = t2; 851 }; 852 853 # Any of the types in the given list 854 oneOf = ts: 855 let 856 head' = if ts == [] then throw "types.oneOf needs to get at least one type in its argument" else head ts; 857 tail' = tail ts; 858 in foldl' either head' tail'; 859 860 # Either value of type `coercedType` or `finalType`, the former is 861 # converted to `finalType` using `coerceFunc`. 862 coercedTo = coercedType: coerceFunc: finalType: 863 assert lib.assertMsg (coercedType.getSubModules == null) 864 "coercedTo: coercedType must not have submodules (its a ${ 865 coercedType.description})"; 866 mkOptionType rec { 867 name = "coercedTo"; 868 description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${optionDescriptionPhrase (class: class == "noun") coercedType} convertible to it"; 869 check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x; 870 merge = loc: defs: 871 let 872 coerceVal = val: 873 if coercedType.check val then coerceFunc val 874 else val; 875 in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 876 emptyValue = finalType.emptyValue; 877 getSubOptions = finalType.getSubOptions; 878 getSubModules = finalType.getSubModules; 879 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 880 typeMerge = t1: t2: null; 881 functor = (defaultFunctor name) // { wrapped = finalType; }; 882 nestedTypes.coercedType = coercedType; 883 nestedTypes.finalType = finalType; 884 }; 885 886 # Augment the given type with an additional type check function. 887 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; 888 889 }; 890}; 891 892in outer_types // outer_types.types