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