Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at release-18.03 465 lines 17 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 filterOverrides; 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: assert lowest <= highest; 123 addCheck int (x: x >= lowest && x <= highest) // { 124 name = "intBetween"; 125 description = "integer between ${betweenDesc lowest highest}"; 126 }; 127 ign = lowest: highest: name: docStart: 128 between lowest highest // { 129 inherit name; 130 description = docStart + "; between ${betweenDesc lowest highest}"; 131 }; 132 unsign = bit: range: ign 0 (range - 1) 133 "unsignedInt${toString bit}" "${toString bit} bit unsigned integer"; 134 sign = bit: range: ign (0 - (range / 2)) (range / 2 - 1) 135 "signedInt${toString bit}" "${toString bit} bit signed integer"; 136 137 in rec { 138 /* An int with a fixed range. 139 * 140 * Example: 141 * (ints.between 0 100).check (-1) 142 * => false 143 * (ints.between 0 100).check (101) 144 * => false 145 * (ints.between 0 0).check 0 146 * => true 147 */ 148 inherit between; 149 150 unsigned = addCheck types.int (x: x >= 0) // { 151 name = "unsignedInt"; 152 description = "unsigned integer, meaning >=0"; 153 }; 154 positive = addCheck types.int (x: x > 0) // { 155 name = "positiveInt"; 156 description = "positive integer, meaning >0"; 157 }; 158 u8 = unsign 8 256; 159 u16 = unsign 16 65536; 160 # the biggest int a 64-bit Nix accepts is 2^63 - 1 (9223372036854775808), for a 32-bit Nix it is 2^31 - 1 (2147483647) 161 # the smallest int a 64-bit Nix accepts is -2^63 (-9223372036854775807), for a 32-bit Nix it is -2^31 (-2147483648) 162 # u32 = unsign 32 4294967296; 163 # u64 = unsign 64 18446744073709551616; 164 165 s8 = sign 8 256; 166 s16 = sign 16 65536; 167 # s32 = sign 32 4294967296; 168 }; 169 170 str = mkOptionType { 171 name = "str"; 172 description = "string"; 173 check = isString; 174 merge = mergeOneOption; 175 }; 176 177 strMatching = pattern: mkOptionType { 178 name = "strMatching ${escapeNixString pattern}"; 179 description = "string matching the pattern ${pattern}"; 180 check = x: str.check x && builtins.match pattern x != null; 181 inherit (str) merge; 182 }; 183 184 # Merge multiple definitions by concatenating them (with the given 185 # separator between the values). 186 separatedString = sep: mkOptionType rec { 187 name = "separatedString"; 188 description = "string"; 189 check = isString; 190 merge = loc: defs: concatStringsSep sep (getValues defs); 191 functor = (defaultFunctor name) // { 192 payload = sep; 193 binOp = sepLhs: sepRhs: 194 if sepLhs == sepRhs then sepLhs 195 else null; 196 }; 197 }; 198 199 lines = separatedString "\n"; 200 commas = separatedString ","; 201 envVar = separatedString ":"; 202 203 # Deprecated; should not be used because it quietly concatenates 204 # strings, which is usually not what you want. 205 string = separatedString ""; 206 207 attrs = mkOptionType { 208 name = "attrs"; 209 description = "attribute set"; 210 check = isAttrs; 211 merge = loc: foldl' (res: def: mergeAttrs res def.value) {}; 212 }; 213 214 # derivation is a reserved keyword. 215 package = mkOptionType { 216 name = "package"; 217 check = x: isDerivation x || isStorePath x; 218 merge = loc: defs: 219 let res = mergeOneOption loc defs; 220 in if isDerivation res then res else toDerivation res; 221 }; 222 223 shellPackage = package // { 224 check = x: (package.check x) && (hasAttr "shellPath" x); 225 }; 226 227 path = mkOptionType { 228 name = "path"; 229 # Hacky: there is no ‘isPath’ primop. 230 check = x: builtins.substring 0 1 (toString x) == "/"; 231 merge = mergeOneOption; 232 }; 233 234 # drop this in the future: 235 list = builtins.trace "`types.list` is deprecated; use `types.listOf` instead" types.listOf; 236 237 listOf = elemType: mkOptionType rec { 238 name = "listOf"; 239 description = "list of ${elemType.description}s"; 240 check = isList; 241 merge = loc: defs: 242 map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def: 243 if isList def.value then 244 imap1 (m: def': 245 (mergeDefinitions 246 (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) 247 elemType 248 [{ inherit (def) file; value = def'; }] 249 ).optionalValue 250 ) def.value 251 else 252 throw "The option value `${showOption loc}` in `${def.file}` is not a list.") defs))); 253 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); 254 getSubModules = elemType.getSubModules; 255 substSubModules = m: listOf (elemType.substSubModules m); 256 functor = (defaultFunctor name) // { wrapped = elemType; }; 257 }; 258 259 nonEmptyListOf = elemType: 260 let list = addCheck (types.listOf elemType) (l: l != []); 261 in list // { description = "non-empty " + list.description; }; 262 263 attrsOf = elemType: mkOptionType rec { 264 name = "attrsOf"; 265 description = "attribute set of ${elemType.description}s"; 266 check = isAttrs; 267 merge = loc: defs: 268 mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: 269 (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue 270 ) 271 # Push down position info. 272 (map (def: listToAttrs (mapAttrsToList (n: def': 273 { name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs))); 274 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); 275 getSubModules = elemType.getSubModules; 276 substSubModules = m: attrsOf (elemType.substSubModules m); 277 functor = (defaultFunctor name) // { wrapped = elemType; }; 278 }; 279 280 # List or attribute set of ... 281 loaOf = elemType: 282 let 283 convertAllLists = defs: 284 let 285 padWidth = stringLength (toString (length defs)); 286 unnamedPrefix = i: "unnamed-" + fixedWidthNumber padWidth i + "."; 287 in 288 imap1 (i: convertIfList (unnamedPrefix i)) defs; 289 290 convertIfList = unnamedPrefix: def: 291 if isList def.value then 292 let 293 padWidth = stringLength (toString (length def.value)); 294 unnamed = i: unnamedPrefix + fixedWidthNumber padWidth i; 295 in 296 { inherit (def) file; 297 value = listToAttrs ( 298 imap1 (elemIdx: elem: 299 { name = elem.name or (unnamed elemIdx); 300 value = elem; 301 }) def.value); 302 } 303 else 304 def; 305 listOnly = listOf elemType; 306 attrOnly = attrsOf elemType; 307 in mkOptionType rec { 308 name = "loaOf"; 309 description = "list or attribute set of ${elemType.description}s"; 310 check = x: isList x || isAttrs x; 311 merge = loc: defs: attrOnly.merge loc (convertAllLists defs); 312 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); 313 getSubModules = elemType.getSubModules; 314 substSubModules = m: loaOf (elemType.substSubModules m); 315 functor = (defaultFunctor name) // { wrapped = elemType; }; 316 }; 317 318 # Value of given type but with no merging (i.e. `uniq list`s are not concatenated). 319 uniq = elemType: mkOptionType rec { 320 name = "uniq"; 321 inherit (elemType) description check; 322 merge = mergeOneOption; 323 getSubOptions = elemType.getSubOptions; 324 getSubModules = elemType.getSubModules; 325 substSubModules = m: uniq (elemType.substSubModules m); 326 functor = (defaultFunctor name) // { wrapped = elemType; }; 327 }; 328 329 # Null or value of ... 330 nullOr = elemType: mkOptionType rec { 331 name = "nullOr"; 332 description = "null or ${elemType.description}"; 333 check = x: x == null || elemType.check x; 334 merge = loc: defs: 335 let nrNulls = count (def: def.value == null) defs; in 336 if nrNulls == length defs then null 337 else if nrNulls != 0 then 338 throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." 339 else elemType.merge loc defs; 340 getSubOptions = elemType.getSubOptions; 341 getSubModules = elemType.getSubModules; 342 substSubModules = m: nullOr (elemType.substSubModules m); 343 functor = (defaultFunctor name) // { wrapped = elemType; }; 344 }; 345 346 # A submodule (like typed attribute set). See NixOS manual. 347 submodule = opts: 348 let 349 opts' = toList opts; 350 inherit (lib.modules) evalModules; 351 in 352 mkOptionType rec { 353 name = "submodule"; 354 check = x: isAttrs x || isFunction x; 355 merge = loc: defs: 356 let 357 coerce = def: if isFunction def then def else { config = def; }; 358 modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs; 359 in (evalModules { 360 inherit modules; 361 args.name = last loc; 362 prefix = loc; 363 }).config; 364 getSubOptions = prefix: (evalModules 365 { modules = opts'; inherit prefix; 366 # This is a work-around due to the fact that some sub-modules, 367 # such as the one included in an attribute set, expects a "args" 368 # attribute to be given to the sub-module. As the option 369 # evaluation does not have any specific attribute name, we 370 # provide a default one for the documentation. 371 # 372 # This is mandatory as some option declaration might use the 373 # "name" attribute given as argument of the submodule and use it 374 # as the default of option declarations. 375 args.name = "&lt;name&gt;"; 376 }).options; 377 getSubModules = opts'; 378 substSubModules = m: submodule m; 379 functor = (defaultFunctor name) // { 380 # Merging of submodules is done as part of mergeOptionDecls, as we have to annotate 381 # each submodule with its location. 382 payload = []; 383 binOp = lhs: rhs: []; 384 }; 385 }; 386 387 # A value from a set of allowed ones. 388 enum = values: 389 let 390 show = v: 391 if builtins.isString v then ''"${v}"'' 392 else if builtins.isInt v then builtins.toString v 393 else ''<${builtins.typeOf v}>''; 394 in 395 mkOptionType rec { 396 name = "enum"; 397 description = "one of ${concatMapStringsSep ", " show values}"; 398 check = flip elem values; 399 merge = mergeOneOption; 400 functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; 401 }; 402 403 # Either value of type `t1` or `t2`. 404 either = t1: t2: mkOptionType rec { 405 name = "either"; 406 description = "${t1.description} or ${t2.description}"; 407 check = x: t1.check x || t2.check x; 408 merge = loc: defs: 409 let 410 defList = map (d: d.value) defs; 411 in 412 if all (x: t1.check x) defList 413 then t1.merge loc defs 414 else if all (x: t2.check x) defList 415 then t2.merge loc defs 416 else mergeOneOption loc defs; 417 typeMerge = f': 418 let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; 419 mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; 420 in 421 if (name == f'.name) && (mt1 != null) && (mt2 != null) 422 then functor.type mt1 mt2 423 else null; 424 functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; 425 }; 426 427 # Either value of type `finalType` or `coercedType`, the latter is 428 # converted to `finalType` using `coerceFunc`. 429 coercedTo = coercedType: coerceFunc: finalType: 430 assert coercedType.getSubModules == null; 431 mkOptionType rec { 432 name = "coercedTo"; 433 description = "${finalType.description} or ${coercedType.description}"; 434 check = x: finalType.check x || coercedType.check x; 435 merge = loc: defs: 436 let 437 coerceVal = val: 438 if finalType.check val then val 439 else let 440 coerced = coerceFunc val; 441 in assert finalType.check coerced; coerced; 442 443 in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 444 getSubOptions = finalType.getSubOptions; 445 getSubModules = finalType.getSubModules; 446 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 447 typeMerge = t1: t2: null; 448 functor = (defaultFunctor name) // { wrapped = finalType; }; 449 }; 450 451 # Obsolete alternative to configOf. It takes its option 452 # declarations from the ‘options’ attribute of containing option 453 # declaration. 454 optionSet = mkOptionType { 455 name = builtins.trace "types.optionSet is deprecated; use types.submodule instead" "optionSet"; 456 description = "option set"; 457 }; 458 459 # Augment the given type with an additional type check function. 460 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; 461 462 }; 463}; 464 465in outer_types // outer_types.types