at 17.09-beta 14 kB view raw
1# Definitions related to run-time type checking. Used in particular 2# to type-check NixOS configurations. 3 4with import ./lists.nix; 5with import ./attrsets.nix; 6with import ./options.nix; 7with import ./trivial.nix; 8with import ./strings.nix; 9let inherit (import ./modules.nix) mergeDefinitions filterOverrides; in 10 11rec { 12 13 isType = type: x: (x._type or "") == type; 14 15 setType = typeName: value: value // { 16 _type = typeName; 17 }; 18 19 20 # Default type merging function 21 # takes two type functors and return the merged type 22 defaultTypeMerge = f: f': 23 let wrapped = f.wrapped.typeMerge f'.wrapped.functor; 24 payload = f.binOp f.payload f'.payload; 25 in 26 # cannot merge different types 27 if f.name != f'.name 28 then null 29 # simple types 30 else if (f.wrapped == null && f'.wrapped == null) 31 && (f.payload == null && f'.payload == null) 32 then f.type 33 # composed types 34 else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null) 35 then f.type wrapped 36 # value types 37 else if (f.payload != null && f'.payload != null) && (payload != null) 38 then f.type payload 39 else null; 40 41 # Default type functor 42 defaultFunctor = name: { 43 inherit name; 44 type = types."${name}" or null; 45 wrapped = null; 46 payload = null; 47 binOp = a: b: null; 48 }; 49 50 isOptionType = isType "option-type"; 51 mkOptionType = 52 { # Human-readable representation of the type, should be equivalent to 53 # the type function name. 54 name 55 , # Description of the type, defined recursively by embedding the wrapped type if any. 56 description ? null 57 , # Function applied to each definition that should return true if 58 # its type-correct, false otherwise. 59 check ? (x: true) 60 , # Merge a list of definitions together into a single value. 61 # This function is called with two arguments: the location of 62 # the option in the configuration as a list of strings 63 # (e.g. ["boot" "loader "grub" "enable"]), and a list of 64 # definition values and locations (e.g. [ { file = "/foo.nix"; 65 # value = 1; } { file = "/bar.nix"; value = 2 } ]). 66 merge ? mergeDefaultOption 67 , # Return a flat list of sub-options. Used to generate 68 # documentation. 69 getSubOptions ? prefix: {} 70 , # List of modules if any, or null if none. 71 getSubModules ? null 72 , # Function for building the same option type with a different list of 73 # modules. 74 substSubModules ? m: null 75 , # Function that merge type declarations. 76 # internal, takes a functor as argument and returns the merged type. 77 # returning null means the type is not mergeable 78 typeMerge ? defaultTypeMerge functor 79 , # The type functor. 80 # internal, representation of the type as an attribute set. 81 # name: name of the type 82 # type: type function. 83 # wrapped: the type wrapped in case of compound types. 84 # payload: values of the type, two payloads of the same type must be 85 # combinable with the binOp binary operation. 86 # binOp: binary operation that merge two payloads of the same type. 87 functor ? defaultFunctor name 88 }: 89 { _type = "option-type"; 90 inherit name check merge getSubOptions getSubModules substSubModules typeMerge functor; 91 description = if description == null then name else description; 92 }; 93 94 95 # When adding new types don't forget to document them in 96 # nixos/doc/manual/development/option-types.xml! 97 types = rec { 98 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 = "integer"; 113 check = isInt; 114 merge = mergeOneOption; 115 }; 116 117 str = mkOptionType { 118 name = "str"; 119 description = "string"; 120 check = isString; 121 merge = mergeOneOption; 122 }; 123 124 # Merge multiple definitions by concatenating them (with the given 125 # separator between the values). 126 separatedString = sep: mkOptionType rec { 127 name = "separatedString"; 128 description = "string"; 129 check = isString; 130 merge = loc: defs: concatStringsSep sep (getValues defs); 131 functor = (defaultFunctor name) // { 132 payload = sep; 133 binOp = sepLhs: sepRhs: 134 if sepLhs == sepRhs then sepLhs 135 else null; 136 }; 137 }; 138 139 lines = separatedString "\n"; 140 commas = separatedString ","; 141 envVar = separatedString ":"; 142 143 # Deprecated; should not be used because it quietly concatenates 144 # strings, which is usually not what you want. 145 string = separatedString ""; 146 147 attrs = mkOptionType { 148 name = "attrs"; 149 description = "attribute set"; 150 check = isAttrs; 151 merge = loc: foldl' (res: def: mergeAttrs res def.value) {}; 152 }; 153 154 # derivation is a reserved keyword. 155 package = mkOptionType { 156 name = "package"; 157 check = x: isDerivation x || isStorePath x; 158 merge = loc: defs: 159 let res = mergeOneOption loc defs; 160 in if isDerivation res then res else toDerivation res; 161 }; 162 163 shellPackage = package // { 164 check = x: (package.check x) && (hasAttr "shellPath" x); 165 }; 166 167 path = mkOptionType { 168 name = "path"; 169 # Hacky: there is no ‘isPath’ primop. 170 check = x: builtins.substring 0 1 (toString x) == "/"; 171 merge = mergeOneOption; 172 }; 173 174 # drop this in the future: 175 list = builtins.trace "`types.list' is deprecated; use `types.listOf' instead" types.listOf; 176 177 listOf = elemType: mkOptionType rec { 178 name = "listOf"; 179 description = "list of ${elemType.description}s"; 180 check = isList; 181 merge = loc: defs: 182 map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def: 183 if isList def.value then 184 imap1 (m: def': 185 (mergeDefinitions 186 (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) 187 elemType 188 [{ inherit (def) file; value = def'; }] 189 ).optionalValue 190 ) def.value 191 else 192 throw "The option value `${showOption loc}' in `${def.file}' is not a list.") defs))); 193 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); 194 getSubModules = elemType.getSubModules; 195 substSubModules = m: listOf (elemType.substSubModules m); 196 functor = (defaultFunctor name) // { wrapped = elemType; }; 197 }; 198 199 attrsOf = elemType: mkOptionType rec { 200 name = "attrsOf"; 201 description = "attribute set of ${elemType.description}s"; 202 check = isAttrs; 203 merge = loc: defs: 204 mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: 205 (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue 206 ) 207 # Push down position info. 208 (map (def: listToAttrs (mapAttrsToList (n: def': 209 { name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs))); 210 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); 211 getSubModules = elemType.getSubModules; 212 substSubModules = m: attrsOf (elemType.substSubModules m); 213 functor = (defaultFunctor name) // { wrapped = elemType; }; 214 }; 215 216 # List or attribute set of ... 217 loaOf = elemType: 218 let 219 convertIfList = defIdx: def: 220 if isList def.value then 221 { inherit (def) file; 222 value = listToAttrs ( 223 imap1 (elemIdx: elem: 224 { name = elem.name or "unnamed-${toString defIdx}.${toString elemIdx}"; 225 value = elem; 226 }) def.value); 227 } 228 else 229 def; 230 listOnly = listOf elemType; 231 attrOnly = attrsOf elemType; 232 in mkOptionType rec { 233 name = "loaOf"; 234 description = "list or attribute set of ${elemType.description}s"; 235 check = x: isList x || isAttrs x; 236 merge = loc: defs: attrOnly.merge loc (imap1 convertIfList defs); 237 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); 238 getSubModules = elemType.getSubModules; 239 substSubModules = m: loaOf (elemType.substSubModules m); 240 functor = (defaultFunctor name) // { wrapped = elemType; }; 241 }; 242 243 # List or element of ... 244 loeOf = elemType: mkOptionType rec { 245 name = "loeOf"; 246 description = "element or list of ${elemType.description}s"; 247 check = x: isList x || elemType.check x; 248 merge = loc: defs: 249 let 250 defs' = filterOverrides defs; 251 res = (head defs').value; 252 in 253 if isList res then concatLists (getValues defs') 254 else if lessThan 1 (length defs') then 255 throw "The option `${showOption loc}' is defined multiple times, in ${showFiles (getFiles defs)}." 256 else if !isString res then 257 throw "The option `${showOption loc}' does not have a string value, in ${showFiles (getFiles defs)}." 258 else res; 259 functor = (defaultFunctor name) // { wrapped = elemType; }; 260 }; 261 262 # Value of given type but with no merging (i.e. `uniq list`s are not concatenated). 263 uniq = elemType: mkOptionType rec { 264 name = "uniq"; 265 inherit (elemType) description check; 266 merge = mergeOneOption; 267 getSubOptions = elemType.getSubOptions; 268 getSubModules = elemType.getSubModules; 269 substSubModules = m: uniq (elemType.substSubModules m); 270 functor = (defaultFunctor name) // { wrapped = elemType; }; 271 }; 272 273 # Null or value of ... 274 nullOr = elemType: mkOptionType rec { 275 name = "nullOr"; 276 description = "null or ${elemType.description}"; 277 check = x: x == null || elemType.check x; 278 merge = loc: defs: 279 let nrNulls = count (def: def.value == null) defs; in 280 if nrNulls == length defs then null 281 else if nrNulls != 0 then 282 throw "The option `${showOption loc}' is defined both null and not null, in ${showFiles (getFiles defs)}." 283 else elemType.merge loc defs; 284 getSubOptions = elemType.getSubOptions; 285 getSubModules = elemType.getSubModules; 286 substSubModules = m: nullOr (elemType.substSubModules m); 287 functor = (defaultFunctor name) // { wrapped = elemType; }; 288 }; 289 290 # A submodule (like typed attribute set). See NixOS manual. 291 submodule = opts: 292 let 293 opts' = toList opts; 294 inherit (import ./modules.nix) evalModules; 295 in 296 mkOptionType rec { 297 name = "submodule"; 298 check = x: isAttrs x || isFunction x; 299 merge = loc: defs: 300 let 301 coerce = def: if isFunction def then def else { config = def; }; 302 modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs; 303 in (evalModules { 304 inherit modules; 305 args.name = last loc; 306 prefix = loc; 307 }).config; 308 getSubOptions = prefix: (evalModules 309 { modules = opts'; inherit prefix; 310 # FIXME: hack to get shit to evaluate. 311 args = { name = ""; }; }).options; 312 getSubModules = opts'; 313 substSubModules = m: submodule m; 314 functor = (defaultFunctor name) // { 315 # Merging of submodules is done as part of mergeOptionDecls, as we have to annotate 316 # each submodule with its location. 317 payload = []; 318 binOp = lhs: rhs: []; 319 }; 320 }; 321 322 # A value from a set of allowed ones. 323 enum = values: 324 let 325 show = v: 326 if builtins.isString v then ''"${v}"'' 327 else if builtins.isInt v then builtins.toString v 328 else ''<${builtins.typeOf v}>''; 329 in 330 mkOptionType rec { 331 name = "enum"; 332 description = "one of ${concatMapStringsSep ", " show values}"; 333 check = flip elem values; 334 merge = mergeOneOption; 335 functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; 336 }; 337 338 # Either value of type `t1` or `t2`. 339 either = t1: t2: mkOptionType rec { 340 name = "either"; 341 description = "${t1.description} or ${t2.description}"; 342 check = x: t1.check x || t2.check x; 343 merge = loc: defs: 344 let 345 defList = map (d: d.value) defs; 346 in 347 if all (x: t1.check x) defList 348 then t1.merge loc defs 349 else if all (x: t2.check x) defList 350 then t2.merge loc defs 351 else mergeOneOption loc defs; 352 typeMerge = f': 353 let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; 354 mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; 355 in 356 if (name == f'.name) && (mt1 != null) && (mt2 != null) 357 then functor.type mt1 mt2 358 else null; 359 functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; 360 }; 361 362 # Either value of type `finalType` or `coercedType`, the latter is 363 # converted to `finalType` using `coerceFunc`. 364 coercedTo = coercedType: coerceFunc: finalType: 365 assert coercedType.getSubModules == null; 366 mkOptionType rec { 367 name = "coercedTo"; 368 description = "${finalType.description} or ${coercedType.description}"; 369 check = x: finalType.check x || coercedType.check x; 370 merge = loc: defs: 371 let 372 coerceVal = val: 373 if finalType.check val then val 374 else let 375 coerced = coerceFunc val; 376 in assert finalType.check coerced; coerced; 377 378 in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 379 getSubOptions = finalType.getSubOptions; 380 getSubModules = finalType.getSubModules; 381 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 382 typeMerge = t1: t2: null; 383 functor = (defaultFunctor name) // { wrapped = finalType; }; 384 }; 385 386 # Obsolete alternative to configOf. It takes its option 387 # declarations from the ‘options’ attribute of containing option 388 # declaration. 389 optionSet = mkOptionType { 390 name = builtins.trace "types.optionSet is deprecated; use types.submodule instead" "optionSet"; 391 description = "option set"; 392 }; 393 394 # Augment the given type with an additional type check function. 395 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; 396 397 }; 398 399}