at 18.09-beta 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: 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 float = mkOptionType rec { 171 name = "float"; 172 description = "floating point number"; 173 check = isFloat; 174 merge = mergeOneOption; 175 }; 176 177 str = mkOptionType { 178 name = "str"; 179 description = "string"; 180 check = isString; 181 merge = mergeOneOption; 182 }; 183 184 strMatching = pattern: mkOptionType { 185 name = "strMatching ${escapeNixString pattern}"; 186 description = "string matching the pattern ${pattern}"; 187 check = x: str.check x && builtins.match pattern x != null; 188 inherit (str) merge; 189 }; 190 191 # Merge multiple definitions by concatenating them (with the given 192 # separator between the values). 193 separatedString = sep: mkOptionType rec { 194 name = "separatedString"; 195 description = "string"; 196 check = isString; 197 merge = loc: defs: concatStringsSep sep (getValues defs); 198 functor = (defaultFunctor name) // { 199 payload = sep; 200 binOp = sepLhs: sepRhs: 201 if sepLhs == sepRhs then sepLhs 202 else null; 203 }; 204 }; 205 206 lines = separatedString "\n"; 207 commas = separatedString ","; 208 envVar = separatedString ":"; 209 210 # Deprecated; should not be used because it quietly concatenates 211 # strings, which is usually not what you want. 212 string = separatedString ""; 213 214 attrs = mkOptionType { 215 name = "attrs"; 216 description = "attribute set"; 217 check = isAttrs; 218 merge = loc: foldl' (res: def: mergeAttrs res def.value) {}; 219 }; 220 221 # derivation is a reserved keyword. 222 package = mkOptionType { 223 name = "package"; 224 check = x: isDerivation x || isStorePath x; 225 merge = loc: defs: 226 let res = mergeOneOption loc defs; 227 in if isDerivation res then res else toDerivation res; 228 }; 229 230 shellPackage = package // { 231 check = x: (package.check x) && (hasAttr "shellPath" x); 232 }; 233 234 path = mkOptionType { 235 name = "path"; 236 # Hacky: there is no ‘isPath’ primop. 237 check = x: builtins.substring 0 1 (toString x) == "/"; 238 merge = mergeOneOption; 239 }; 240 241 # drop this in the future: 242 list = builtins.trace "`types.list` is deprecated; use `types.listOf` instead" types.listOf; 243 244 listOf = elemType: mkOptionType rec { 245 name = "listOf"; 246 description = "list of ${elemType.description}s"; 247 check = isList; 248 merge = loc: defs: 249 map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def: 250 if isList def.value then 251 imap1 (m: def': 252 (mergeDefinitions 253 (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) 254 elemType 255 [{ inherit (def) file; value = def'; }] 256 ).optionalValue 257 ) def.value 258 else 259 throw "The option value `${showOption loc}` in `${def.file}` is not a list.") defs))); 260 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); 261 getSubModules = elemType.getSubModules; 262 substSubModules = m: listOf (elemType.substSubModules m); 263 functor = (defaultFunctor name) // { wrapped = elemType; }; 264 }; 265 266 nonEmptyListOf = elemType: 267 let list = addCheck (types.listOf elemType) (l: l != []); 268 in list // { description = "non-empty " + list.description; }; 269 270 attrsOf = elemType: mkOptionType rec { 271 name = "attrsOf"; 272 description = "attribute set of ${elemType.description}s"; 273 check = isAttrs; 274 merge = loc: defs: 275 mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: 276 (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue 277 ) 278 # Push down position info. 279 (map (def: listToAttrs (mapAttrsToList (n: def': 280 { name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs))); 281 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); 282 getSubModules = elemType.getSubModules; 283 substSubModules = m: attrsOf (elemType.substSubModules m); 284 functor = (defaultFunctor name) // { wrapped = elemType; }; 285 }; 286 287 # List or attribute set of ... 288 loaOf = elemType: 289 let 290 convertAllLists = defs: 291 let 292 padWidth = stringLength (toString (length defs)); 293 unnamedPrefix = i: "unnamed-" + fixedWidthNumber padWidth i + "."; 294 in 295 imap1 (i: convertIfList (unnamedPrefix i)) defs; 296 297 convertIfList = unnamedPrefix: def: 298 if isList def.value then 299 let 300 padWidth = stringLength (toString (length def.value)); 301 unnamed = i: unnamedPrefix + fixedWidthNumber padWidth i; 302 in 303 { inherit (def) file; 304 value = listToAttrs ( 305 imap1 (elemIdx: elem: 306 { name = elem.name or (unnamed elemIdx); 307 value = elem; 308 }) def.value); 309 } 310 else 311 def; 312 attrOnly = attrsOf elemType; 313 in mkOptionType rec { 314 name = "loaOf"; 315 description = "list or attribute set of ${elemType.description}s"; 316 check = x: isList x || isAttrs x; 317 merge = loc: defs: attrOnly.merge loc (convertAllLists defs); 318 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); 319 getSubModules = elemType.getSubModules; 320 substSubModules = m: loaOf (elemType.substSubModules m); 321 functor = (defaultFunctor name) // { wrapped = elemType; }; 322 }; 323 324 # Value of given type but with no merging (i.e. `uniq list`s are not concatenated). 325 uniq = elemType: mkOptionType rec { 326 name = "uniq"; 327 inherit (elemType) description check; 328 merge = mergeOneOption; 329 getSubOptions = elemType.getSubOptions; 330 getSubModules = elemType.getSubModules; 331 substSubModules = m: uniq (elemType.substSubModules m); 332 functor = (defaultFunctor name) // { wrapped = elemType; }; 333 }; 334 335 # Null or value of ... 336 nullOr = elemType: mkOptionType rec { 337 name = "nullOr"; 338 description = "null or ${elemType.description}"; 339 check = x: x == null || elemType.check x; 340 merge = loc: defs: 341 let nrNulls = count (def: def.value == null) defs; in 342 if nrNulls == length defs then null 343 else if nrNulls != 0 then 344 throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." 345 else elemType.merge loc defs; 346 getSubOptions = elemType.getSubOptions; 347 getSubModules = elemType.getSubModules; 348 substSubModules = m: nullOr (elemType.substSubModules m); 349 functor = (defaultFunctor name) // { wrapped = elemType; }; 350 }; 351 352 # A submodule (like typed attribute set). See NixOS manual. 353 submodule = opts: 354 let 355 opts' = toList opts; 356 inherit (lib.modules) evalModules; 357 in 358 mkOptionType rec { 359 name = "submodule"; 360 check = x: isAttrs x || isFunction x; 361 merge = loc: defs: 362 let 363 coerce = def: if isFunction def then def else { config = def; }; 364 modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs; 365 in (evalModules { 366 inherit modules; 367 args.name = last loc; 368 prefix = loc; 369 }).config; 370 getSubOptions = prefix: (evalModules 371 { modules = opts'; inherit prefix; 372 # This is a work-around due to the fact that some sub-modules, 373 # such as the one included in an attribute set, expects a "args" 374 # attribute to be given to the sub-module. As the option 375 # evaluation does not have any specific attribute name, we 376 # provide a default one for the documentation. 377 # 378 # This is mandatory as some option declaration might use the 379 # "name" attribute given as argument of the submodule and use it 380 # as the default of option declarations. 381 # 382 # Using lookalike unicode single angle quotation marks because 383 # of the docbook transformation the options receive. In all uses 384 # &gt; and &lt; wouldn't be encoded correctly so the encoded values 385 # would be used, and use of `<` and `>` would break the XML document. 386 # It shouldn't cause an issue since this is cosmetic for the manual. 387 args.name = "name"; 388 }).options; 389 getSubModules = opts'; 390 substSubModules = m: submodule m; 391 functor = (defaultFunctor name) // { 392 # Merging of submodules is done as part of mergeOptionDecls, as we have to annotate 393 # each submodule with its location. 394 payload = []; 395 binOp = lhs: rhs: []; 396 }; 397 }; 398 399 # A value from a set of allowed ones. 400 enum = values: 401 let 402 show = v: 403 if builtins.isString v then ''"${v}"'' 404 else if builtins.isInt v then builtins.toString v 405 else ''<${builtins.typeOf v}>''; 406 in 407 mkOptionType rec { 408 name = "enum"; 409 description = "one of ${concatMapStringsSep ", " show values}"; 410 check = flip elem values; 411 merge = mergeOneOption; 412 functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; 413 }; 414 415 # Either value of type `t1` or `t2`. 416 either = t1: t2: mkOptionType rec { 417 name = "either"; 418 description = "${t1.description} or ${t2.description}"; 419 check = x: t1.check x || t2.check x; 420 merge = loc: defs: 421 let 422 defList = map (d: d.value) defs; 423 in 424 if all (x: t1.check x) defList 425 then t1.merge loc defs 426 else if all (x: t2.check x) defList 427 then t2.merge loc defs 428 else mergeOneOption loc defs; 429 typeMerge = f': 430 let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; 431 mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; 432 in 433 if (name == f'.name) && (mt1 != null) && (mt2 != null) 434 then functor.type mt1 mt2 435 else null; 436 functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; 437 }; 438 439 # Either value of type `finalType` or `coercedType`, the latter is 440 # converted to `finalType` using `coerceFunc`. 441 coercedTo = coercedType: coerceFunc: finalType: 442 assert coercedType.getSubModules == null; 443 mkOptionType rec { 444 name = "coercedTo"; 445 description = "${finalType.description} or ${coercedType.description} convertible to it"; 446 check = x: finalType.check x || (coercedType.check x && finalType.check (coerceFunc x)); 447 merge = loc: defs: 448 let 449 coerceVal = val: 450 if finalType.check val then val 451 else coerceFunc val; 452 in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 453 getSubOptions = finalType.getSubOptions; 454 getSubModules = finalType.getSubModules; 455 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 456 typeMerge = t1: t2: null; 457 functor = (defaultFunctor name) // { wrapped = finalType; }; 458 }; 459 460 # Obsolete alternative to configOf. It takes its option 461 # declarations from the ‘options’ attribute of containing option 462 # declaration. 463 optionSet = mkOptionType { 464 name = builtins.trace "types.optionSet is deprecated; use types.submodule instead" "optionSet"; 465 description = "option set"; 466 }; 467 468 # Augment the given type with an additional type check function. 469 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; 470 471 }; 472}; 473 474in outer_types // outer_types.types