Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at release-19.03 482 lines 18 kB view raw
1# Definitions related to run-time type checking. Used in particular 2# to type-check NixOS configurations. 3{ lib }: 4with lib.lists; 5with lib.attrsets; 6with lib.options; 7with lib.trivial; 8with lib.strings; 9let 10 11 inherit (lib.modules) mergeDefinitions; 12 outer_types = 13rec { 14 isType = type: x: (x._type or "") == type; 15 16 setType = typeName: value: value // { 17 _type = typeName; 18 }; 19 20 21 # Default type merging function 22 # takes two type functors and return the merged type 23 defaultTypeMerge = f: f': 24 let wrapped = f.wrapped.typeMerge f'.wrapped.functor; 25 payload = f.binOp f.payload f'.payload; 26 in 27 # cannot merge different types 28 if f.name != f'.name 29 then null 30 # simple types 31 else if (f.wrapped == null && f'.wrapped == null) 32 && (f.payload == null && f'.payload == null) 33 then f.type 34 # composed types 35 else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null) 36 then f.type wrapped 37 # value types 38 else if (f.payload != null && f'.payload != null) && (payload != null) 39 then f.type payload 40 else null; 41 42 # Default type functor 43 defaultFunctor = name: { 44 inherit name; 45 type = types."${name}" or null; 46 wrapped = null; 47 payload = null; 48 binOp = a: b: null; 49 }; 50 51 isOptionType = isType "option-type"; 52 mkOptionType = 53 { # Human-readable representation of the type, should be equivalent to 54 # the type function name. 55 name 56 , # Description of the type, defined recursively by embedding the wrapped type if any. 57 description ? null 58 , # Function applied to each definition that should return true if 59 # its type-correct, false otherwise. 60 check ? (x: true) 61 , # Merge a list of definitions together into a single value. 62 # This function is called with two arguments: the location of 63 # the option in the configuration as a list of strings 64 # (e.g. ["boot" "loader "grub" "enable"]), and a list of 65 # definition values and locations (e.g. [ { file = "/foo.nix"; 66 # value = 1; } { file = "/bar.nix"; value = 2 } ]). 67 merge ? mergeDefaultOption 68 , # Return a flat list of sub-options. Used to generate 69 # documentation. 70 getSubOptions ? prefix: {} 71 , # List of modules if any, or null if none. 72 getSubModules ? null 73 , # Function for building the same option type with a different list of 74 # modules. 75 substSubModules ? m: null 76 , # Function that merge type declarations. 77 # internal, takes a functor as argument and returns the merged type. 78 # returning null means the type is not mergeable 79 typeMerge ? defaultTypeMerge functor 80 , # The type functor. 81 # internal, representation of the type as an attribute set. 82 # name: name of the type 83 # type: type function. 84 # wrapped: the type wrapped in case of compound types. 85 # payload: values of the type, two payloads of the same type must be 86 # combinable with the binOp binary operation. 87 # binOp: binary operation that merge two payloads of the same type. 88 functor ? defaultFunctor name 89 }: 90 { _type = "option-type"; 91 inherit name check merge getSubOptions getSubModules substSubModules typeMerge functor; 92 description = if description == null then name else description; 93 }; 94 95 96 # When adding new types don't forget to document them in 97 # nixos/doc/manual/development/option-types.xml! 98 types = rec { 99 unspecified = mkOptionType { 100 name = "unspecified"; 101 }; 102 103 bool = mkOptionType { 104 name = "bool"; 105 description = "boolean"; 106 check = isBool; 107 merge = mergeEqualOption; 108 }; 109 110 int = mkOptionType rec { 111 name = "int"; 112 description = "signed integer"; 113 check = isInt; 114 merge = mergeOneOption; 115 }; 116 117 # Specialized subdomains of int 118 ints = 119 let 120 betweenDesc = lowest: highest: 121 "${toString lowest} and ${toString highest} (both inclusive)"; 122 between = lowest: highest: 123 assert lib.assertMsg (lowest <= highest) 124 "ints.between: lowest must be smaller than highest"; 125 addCheck int (x: x >= lowest && x <= highest) // { 126 name = "intBetween"; 127 description = "integer between ${betweenDesc lowest highest}"; 128 }; 129 ign = lowest: highest: name: docStart: 130 between lowest highest // { 131 inherit name; 132 description = docStart + "; between ${betweenDesc lowest highest}"; 133 }; 134 unsign = bit: range: ign 0 (range - 1) 135 "unsignedInt${toString bit}" "${toString bit} bit unsigned integer"; 136 sign = bit: range: ign (0 - (range / 2)) (range / 2 - 1) 137 "signedInt${toString bit}" "${toString bit} bit signed integer"; 138 139 in rec { 140 /* An int with a fixed range. 141 * 142 * Example: 143 * (ints.between 0 100).check (-1) 144 * => false 145 * (ints.between 0 100).check (101) 146 * => false 147 * (ints.between 0 0).check 0 148 * => true 149 */ 150 inherit between; 151 152 unsigned = addCheck types.int (x: x >= 0) // { 153 name = "unsignedInt"; 154 description = "unsigned integer, meaning >=0"; 155 }; 156 positive = addCheck types.int (x: x > 0) // { 157 name = "positiveInt"; 158 description = "positive integer, meaning >0"; 159 }; 160 u8 = unsign 8 256; 161 u16 = unsign 16 65536; 162 # the biggest int a 64-bit Nix accepts is 2^63 - 1 (9223372036854775808), for a 32-bit Nix it is 2^31 - 1 (2147483647) 163 # the smallest int a 64-bit Nix accepts is -2^63 (-9223372036854775807), for a 32-bit Nix it is -2^31 (-2147483648) 164 # u32 = unsign 32 4294967296; 165 # u64 = unsign 64 18446744073709551616; 166 167 s8 = sign 8 256; 168 s16 = sign 16 65536; 169 # s32 = sign 32 4294967296; 170 }; 171 172 # Alias of u16 for a port number 173 port = ints.u16; 174 175 float = mkOptionType rec { 176 name = "float"; 177 description = "floating point number"; 178 check = isFloat; 179 merge = mergeOneOption; 180 }; 181 182 str = mkOptionType { 183 name = "str"; 184 description = "string"; 185 check = isString; 186 merge = mergeOneOption; 187 }; 188 189 strMatching = pattern: mkOptionType { 190 name = "strMatching ${escapeNixString pattern}"; 191 description = "string matching the pattern ${pattern}"; 192 check = x: str.check x && builtins.match pattern x != null; 193 inherit (str) merge; 194 }; 195 196 # Merge multiple definitions by concatenating them (with the given 197 # separator between the values). 198 separatedString = sep: mkOptionType rec { 199 name = "separatedString"; 200 description = if sep == "" 201 then "Concatenated string" # for types.string. 202 else "strings concatenated with ${builtins.toJSON sep}" 203 ; 204 check = isString; 205 merge = loc: defs: concatStringsSep sep (getValues defs); 206 functor = (defaultFunctor name) // { 207 payload = sep; 208 binOp = sepLhs: sepRhs: 209 if sepLhs == sepRhs then sepLhs 210 else null; 211 }; 212 }; 213 214 lines = separatedString "\n"; 215 commas = separatedString ","; 216 envVar = separatedString ":"; 217 218 # Deprecated; should not be used because it quietly concatenates 219 # strings, which is usually not what you want. 220 string = separatedString ""; 221 222 attrs = mkOptionType { 223 name = "attrs"; 224 description = "attribute set"; 225 check = isAttrs; 226 merge = loc: foldl' (res: def: mergeAttrs res def.value) {}; 227 }; 228 229 # derivation is a reserved keyword. 230 package = mkOptionType { 231 name = "package"; 232 check = x: isDerivation x || isStorePath x; 233 merge = loc: defs: 234 let res = mergeOneOption loc defs; 235 in if isDerivation res then res else toDerivation res; 236 }; 237 238 shellPackage = package // { 239 check = x: (package.check x) && (hasAttr "shellPath" x); 240 }; 241 242 path = mkOptionType { 243 name = "path"; 244 # Hacky: there is no ‘isPath’ primop. 245 check = x: builtins.substring 0 1 (toString x) == "/"; 246 merge = mergeOneOption; 247 }; 248 249 # drop this in the future: 250 list = builtins.trace "`types.list` is deprecated; use `types.listOf` instead" types.listOf; 251 252 listOf = elemType: mkOptionType rec { 253 name = "listOf"; 254 description = "list of ${elemType.description}s"; 255 check = isList; 256 merge = loc: defs: 257 map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def: 258 if isList def.value then 259 imap1 (m: def': 260 (mergeDefinitions 261 (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) 262 elemType 263 [{ inherit (def) file; value = def'; }] 264 ).optionalValue 265 ) def.value 266 else 267 throw "The option value `${showOption loc}` in `${def.file}` is not a list.") defs))); 268 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); 269 getSubModules = elemType.getSubModules; 270 substSubModules = m: listOf (elemType.substSubModules m); 271 functor = (defaultFunctor name) // { wrapped = elemType; }; 272 }; 273 274 nonEmptyListOf = elemType: 275 let list = addCheck (types.listOf elemType) (l: l != []); 276 in list // { description = "non-empty " + list.description; }; 277 278 attrsOf = elemType: mkOptionType rec { 279 name = "attrsOf"; 280 description = "attribute set of ${elemType.description}s"; 281 check = isAttrs; 282 merge = loc: defs: 283 mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: 284 (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue 285 ) 286 # Push down position info. 287 (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs))); 288 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); 289 getSubModules = elemType.getSubModules; 290 substSubModules = m: attrsOf (elemType.substSubModules m); 291 functor = (defaultFunctor name) // { wrapped = elemType; }; 292 }; 293 294 # List or attribute set of ... 295 loaOf = elemType: 296 let 297 convertAllLists = defs: 298 let 299 padWidth = stringLength (toString (length defs)); 300 unnamedPrefix = i: "unnamed-" + fixedWidthNumber padWidth i + "."; 301 in 302 imap1 (i: convertIfList (unnamedPrefix i)) defs; 303 304 convertIfList = unnamedPrefix: def: 305 if isList def.value then 306 let 307 padWidth = stringLength (toString (length def.value)); 308 unnamed = i: unnamedPrefix + fixedWidthNumber padWidth i; 309 in 310 { inherit (def) file; 311 value = listToAttrs ( 312 imap1 (elemIdx: elem: 313 { name = elem.name or (unnamed elemIdx); 314 value = elem; 315 }) def.value); 316 } 317 else 318 def; 319 attrOnly = attrsOf elemType; 320 in mkOptionType rec { 321 name = "loaOf"; 322 description = "list or attribute set of ${elemType.description}s"; 323 check = x: isList x || isAttrs x; 324 merge = loc: defs: attrOnly.merge loc (convertAllLists defs); 325 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); 326 getSubModules = elemType.getSubModules; 327 substSubModules = m: loaOf (elemType.substSubModules m); 328 functor = (defaultFunctor name) // { wrapped = elemType; }; 329 }; 330 331 # Value of given type but with no merging (i.e. `uniq list`s are not concatenated). 332 uniq = elemType: mkOptionType rec { 333 name = "uniq"; 334 inherit (elemType) description check; 335 merge = mergeOneOption; 336 getSubOptions = elemType.getSubOptions; 337 getSubModules = elemType.getSubModules; 338 substSubModules = m: uniq (elemType.substSubModules m); 339 functor = (defaultFunctor name) // { wrapped = elemType; }; 340 }; 341 342 # Null or value of ... 343 nullOr = elemType: mkOptionType rec { 344 name = "nullOr"; 345 description = "null or ${elemType.description}"; 346 check = x: x == null || elemType.check x; 347 merge = loc: defs: 348 let nrNulls = count (def: def.value == null) defs; in 349 if nrNulls == length defs then null 350 else if nrNulls != 0 then 351 throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." 352 else elemType.merge loc defs; 353 getSubOptions = elemType.getSubOptions; 354 getSubModules = elemType.getSubModules; 355 substSubModules = m: nullOr (elemType.substSubModules m); 356 functor = (defaultFunctor name) // { wrapped = elemType; }; 357 }; 358 359 # A submodule (like typed attribute set). See NixOS manual. 360 submodule = opts: 361 let 362 opts' = toList opts; 363 inherit (lib.modules) evalModules; 364 in 365 mkOptionType rec { 366 name = "submodule"; 367 check = x: isAttrs x || isFunction x; 368 merge = loc: defs: 369 let 370 coerce = def: if isFunction def then def else { config = def; }; 371 modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs; 372 in (evalModules { 373 inherit modules; 374 args.name = last loc; 375 prefix = loc; 376 }).config; 377 getSubOptions = prefix: (evalModules 378 { modules = opts'; inherit prefix; 379 # This is a work-around due to the fact that some sub-modules, 380 # such as the one included in an attribute set, expects a "args" 381 # attribute to be given to the sub-module. As the option 382 # evaluation does not have any specific attribute name, we 383 # provide a default one for the documentation. 384 # 385 # This is mandatory as some option declaration might use the 386 # "name" attribute given as argument of the submodule and use it 387 # as the default of option declarations. 388 # 389 # Using lookalike unicode single angle quotation marks because 390 # of the docbook transformation the options receive. In all uses 391 # &gt; and &lt; wouldn't be encoded correctly so the encoded values 392 # would be used, and use of `<` and `>` would break the XML document. 393 # It shouldn't cause an issue since this is cosmetic for the manual. 394 args.name = "name"; 395 }).options; 396 getSubModules = opts'; 397 substSubModules = m: submodule m; 398 functor = (defaultFunctor name) // { 399 # Merging of submodules is done as part of mergeOptionDecls, as we have to annotate 400 # each submodule with its location. 401 payload = []; 402 binOp = lhs: rhs: []; 403 }; 404 }; 405 406 # A value from a set of allowed ones. 407 enum = values: 408 let 409 show = v: 410 if builtins.isString v then ''"${v}"'' 411 else if builtins.isInt v then builtins.toString v 412 else ''<${builtins.typeOf v}>''; 413 in 414 mkOptionType rec { 415 name = "enum"; 416 description = "one of ${concatMapStringsSep ", " show values}"; 417 check = flip elem values; 418 merge = mergeOneOption; 419 functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; 420 }; 421 422 # Either value of type `t1` or `t2`. 423 either = t1: t2: mkOptionType rec { 424 name = "either"; 425 description = "${t1.description} or ${t2.description}"; 426 check = x: t1.check x || t2.check x; 427 merge = loc: defs: 428 let 429 defList = map (d: d.value) defs; 430 in 431 if all (x: t1.check x) defList 432 then t1.merge loc defs 433 else if all (x: t2.check x) defList 434 then t2.merge loc defs 435 else mergeOneOption loc defs; 436 typeMerge = f': 437 let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; 438 mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; 439 in 440 if (name == f'.name) && (mt1 != null) && (mt2 != null) 441 then functor.type mt1 mt2 442 else null; 443 functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; 444 }; 445 446 # Either value of type `finalType` or `coercedType`, the latter is 447 # converted to `finalType` using `coerceFunc`. 448 coercedTo = coercedType: coerceFunc: finalType: 449 assert lib.assertMsg (coercedType.getSubModules == null) 450 "coercedTo: coercedType must not have submodules (its a ${ 451 coercedType.description})"; 452 mkOptionType rec { 453 name = "coercedTo"; 454 description = "${finalType.description} or ${coercedType.description} convertible to it"; 455 check = x: finalType.check x || (coercedType.check x && finalType.check (coerceFunc x)); 456 merge = loc: defs: 457 let 458 coerceVal = val: 459 if finalType.check val then val 460 else coerceFunc val; 461 in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 462 getSubOptions = finalType.getSubOptions; 463 getSubModules = finalType.getSubModules; 464 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 465 typeMerge = t1: t2: null; 466 functor = (defaultFunctor name) // { wrapped = finalType; }; 467 }; 468 469 # Obsolete alternative to configOf. It takes its option 470 # declarations from the ‘options’ attribute of containing option 471 # declaration. 472 optionSet = mkOptionType { 473 name = builtins.trace "types.optionSet is deprecated; use types.submodule instead" "optionSet"; 474 description = "option set"; 475 }; 476 # Augment the given type with an additional type check function. 477 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; 478 479 }; 480}; 481 482in outer_types // outer_types.types