at 18.03-beta 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 convertIfList = defIdx: def: 284 if isList def.value then 285 { inherit (def) file; 286 value = listToAttrs ( 287 imap1 (elemIdx: elem: 288 { name = elem.name or "unnamed-${toString defIdx}.${toString elemIdx}"; 289 value = elem; 290 }) def.value); 291 } 292 else 293 def; 294 listOnly = listOf elemType; 295 attrOnly = attrsOf elemType; 296 in mkOptionType rec { 297 name = "loaOf"; 298 description = "list or attribute set of ${elemType.description}s"; 299 check = x: isList x || isAttrs x; 300 merge = loc: defs: attrOnly.merge loc (imap1 convertIfList defs); 301 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); 302 getSubModules = elemType.getSubModules; 303 substSubModules = m: loaOf (elemType.substSubModules m); 304 functor = (defaultFunctor name) // { wrapped = elemType; }; 305 }; 306 307 # Value of given type but with no merging (i.e. `uniq list`s are not concatenated). 308 uniq = elemType: mkOptionType rec { 309 name = "uniq"; 310 inherit (elemType) description check; 311 merge = mergeOneOption; 312 getSubOptions = elemType.getSubOptions; 313 getSubModules = elemType.getSubModules; 314 substSubModules = m: uniq (elemType.substSubModules m); 315 functor = (defaultFunctor name) // { wrapped = elemType; }; 316 }; 317 318 # Null or value of ... 319 nullOr = elemType: mkOptionType rec { 320 name = "nullOr"; 321 description = "null or ${elemType.description}"; 322 check = x: x == null || elemType.check x; 323 merge = loc: defs: 324 let nrNulls = count (def: def.value == null) defs; in 325 if nrNulls == length defs then null 326 else if nrNulls != 0 then 327 throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." 328 else elemType.merge loc defs; 329 getSubOptions = elemType.getSubOptions; 330 getSubModules = elemType.getSubModules; 331 substSubModules = m: nullOr (elemType.substSubModules m); 332 functor = (defaultFunctor name) // { wrapped = elemType; }; 333 }; 334 335 # A submodule (like typed attribute set). See NixOS manual. 336 submodule = opts: 337 let 338 opts' = toList opts; 339 inherit (lib.modules) evalModules; 340 in 341 mkOptionType rec { 342 name = "submodule"; 343 check = x: isAttrs x || isFunction x; 344 merge = loc: defs: 345 let 346 coerce = def: if isFunction def then def else { config = def; }; 347 modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs; 348 in (evalModules { 349 inherit modules; 350 args.name = last loc; 351 prefix = loc; 352 }).config; 353 getSubOptions = prefix: (evalModules 354 { modules = opts'; inherit prefix; 355 # This is a work-around due to the fact that some sub-modules, 356 # such as the one included in an attribute set, expects a "args" 357 # attribute to be given to the sub-module. As the option 358 # evaluation does not have any specific attribute name, we 359 # provide a default one for the documentation. 360 # 361 # This is mandatory as some option declaration might use the 362 # "name" attribute given as argument of the submodule and use it 363 # as the default of option declarations. 364 args.name = "&lt;name&gt;"; 365 }).options; 366 getSubModules = opts'; 367 substSubModules = m: submodule m; 368 functor = (defaultFunctor name) // { 369 # Merging of submodules is done as part of mergeOptionDecls, as we have to annotate 370 # each submodule with its location. 371 payload = []; 372 binOp = lhs: rhs: []; 373 }; 374 }; 375 376 # A value from a set of allowed ones. 377 enum = values: 378 let 379 show = v: 380 if builtins.isString v then ''"${v}"'' 381 else if builtins.isInt v then builtins.toString v 382 else ''<${builtins.typeOf v}>''; 383 in 384 mkOptionType rec { 385 name = "enum"; 386 description = "one of ${concatMapStringsSep ", " show values}"; 387 check = flip elem values; 388 merge = mergeOneOption; 389 functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; 390 }; 391 392 # Either value of type `t1` or `t2`. 393 either = t1: t2: mkOptionType rec { 394 name = "either"; 395 description = "${t1.description} or ${t2.description}"; 396 check = x: t1.check x || t2.check x; 397 merge = loc: defs: 398 let 399 defList = map (d: d.value) defs; 400 in 401 if all (x: t1.check x) defList 402 then t1.merge loc defs 403 else if all (x: t2.check x) defList 404 then t2.merge loc defs 405 else mergeOneOption loc defs; 406 typeMerge = f': 407 let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; 408 mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; 409 in 410 if (name == f'.name) && (mt1 != null) && (mt2 != null) 411 then functor.type mt1 mt2 412 else null; 413 functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; 414 }; 415 416 # Either value of type `finalType` or `coercedType`, the latter is 417 # converted to `finalType` using `coerceFunc`. 418 coercedTo = coercedType: coerceFunc: finalType: 419 assert coercedType.getSubModules == null; 420 mkOptionType rec { 421 name = "coercedTo"; 422 description = "${finalType.description} or ${coercedType.description}"; 423 check = x: finalType.check x || coercedType.check x; 424 merge = loc: defs: 425 let 426 coerceVal = val: 427 if finalType.check val then val 428 else let 429 coerced = coerceFunc val; 430 in assert finalType.check coerced; coerced; 431 432 in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 433 getSubOptions = finalType.getSubOptions; 434 getSubModules = finalType.getSubModules; 435 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 436 typeMerge = t1: t2: null; 437 functor = (defaultFunctor name) // { wrapped = finalType; }; 438 }; 439 440 # Obsolete alternative to configOf. It takes its option 441 # declarations from the ‘options’ attribute of containing option 442 # declaration. 443 optionSet = mkOptionType { 444 name = builtins.trace "types.optionSet is deprecated; use types.submodule instead" "optionSet"; 445 description = "option set"; 446 }; 447 448 # Augment the given type with an additional type check function. 449 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; 450 451 }; 452}; 453 454in outer_types // outer_types.types