at 18.03-beta 27 kB view raw
1{ lib }: 2 3with lib.lists; 4with lib.strings; 5with lib.trivial; 6with lib.attrsets; 7with lib.options; 8with lib.debug; 9with lib.types; 10 11rec { 12 13 /* Evaluate a set of modules. The result is a set of two 14 attributes: options: the nested set of all option declarations, 15 and config: the nested set of all option values. 16 !!! Please think twice before adding to this argument list! The more 17 that is specified here instead of in the modules themselves the harder 18 it is to transparently move a set of modules to be a submodule of another 19 config (as the proper arguments need to be replicated at each call to 20 evalModules) and the less declarative the module set is. */ 21 evalModules = { modules 22 , prefix ? [] 23 , # This should only be used for special arguments that need to be evaluated 24 # when resolving module structure (like in imports). For everything else, 25 # there's _module.args. If specialArgs.modulesPath is defined it will be 26 # used as the base path for disabledModules. 27 specialArgs ? {} 28 , # This would be remove in the future, Prefer _module.args option instead. 29 args ? {} 30 , # This would be remove in the future, Prefer _module.check option instead. 31 check ? true 32 }: 33 let 34 # This internal module declare internal options under the `_module' 35 # attribute. These options are fragile, as they are used by the 36 # module system to change the interpretation of modules. 37 internalModule = rec { 38 _file = ./modules.nix; 39 40 key = _file; 41 42 options = { 43 _module.args = mkOption { 44 type = types.attrsOf types.unspecified; 45 internal = true; 46 description = "Arguments passed to each module."; 47 }; 48 49 _module.check = mkOption { 50 type = types.bool; 51 internal = true; 52 default = check; 53 description = "Whether to check whether all option definitions have matching declarations."; 54 }; 55 }; 56 57 config = { 58 _module.args = args; 59 }; 60 }; 61 62 closed = closeModules (modules ++ [ internalModule ]) ({ inherit config options; lib = import ./.; } // specialArgs); 63 64 options = mergeModules prefix (reverseList (filterModules (specialArgs.modulesPath or "") closed)); 65 66 # Traverse options and extract the option values into the final 67 # config set. At the same time, check whether all option 68 # definitions have matching declarations. 69 # !!! _module.check's value can't depend on any other config values 70 # without an infinite recursion. One way around this is to make the 71 # 'config' passed around to the modules be unconditionally unchecked, 72 # and only do the check in 'result'. 73 config = yieldConfig prefix options; 74 yieldConfig = prefix: set: 75 let res = removeAttrs (mapAttrs (n: v: 76 if isOption v then v.value 77 else yieldConfig (prefix ++ [n]) v) set) ["_definedNames"]; 78 in 79 if options._module.check.value && set ? _definedNames then 80 foldl' (res: m: 81 foldl' (res: name: 82 if set ? ${name} then res else throw "The option `${showOption (prefix ++ [name])}' defined in `${m.file}' does not exist.") 83 res m.names) 84 res set._definedNames 85 else 86 res; 87 result = { inherit options config; }; 88 in result; 89 90 91 # Filter disabled modules. Modules can be disabled allowing 92 # their implementation to be replaced. 93 filterModules = modulesPath: modules: 94 let 95 moduleKey = m: if isString m then toString modulesPath + "/" + m else toString m; 96 disabledKeys = map moduleKey (concatMap (m: m.disabledModules) modules); 97 in 98 filter (m: !(elem m.key disabledKeys)) modules; 99 100 /* Close a set of modules under the imports relation. */ 101 closeModules = modules: args: 102 let 103 toClosureList = file: parentKey: imap1 (n: x: 104 if isAttrs x || isFunction x then 105 let key = "${parentKey}:anon-${toString n}"; in 106 unifyModuleSyntax file key (unpackSubmodule (applyIfFunction key) x args) 107 else 108 let file = toString x; key = toString x; in 109 unifyModuleSyntax file key (applyIfFunction key (import x) args)); 110 in 111 builtins.genericClosure { 112 startSet = toClosureList unknownModule "" modules; 113 operator = m: toClosureList m.file m.key m.imports; 114 }; 115 116 /* Massage a module into canonical form, that is, a set consisting 117 of options, config and imports attributes. */ 118 unifyModuleSyntax = file: key: m: 119 let metaSet = if m ? meta 120 then { meta = m.meta; } 121 else {}; 122 in 123 if m ? config || m ? options then 124 let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta"]; in 125 if badAttrs != {} then 126 throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by assignments to the top-level attributes `config' or `options'." 127 else 128 { file = m._file or file; 129 key = toString m.key or key; 130 disabledModules = m.disabledModules or []; 131 imports = m.imports or []; 132 options = m.options or {}; 133 config = mkMerge [ (m.config or {}) metaSet ]; 134 } 135 else 136 { file = m._file or file; 137 key = toString m.key or key; 138 disabledModules = m.disabledModules or []; 139 imports = m.require or [] ++ m.imports or []; 140 options = {}; 141 config = mkMerge [ (removeAttrs m ["_file" "key" "disabledModules" "require" "imports"]) metaSet ]; 142 }; 143 144 applyIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then 145 let 146 # Module arguments are resolved in a strict manner when attribute set 147 # deconstruction is used. As the arguments are now defined with the 148 # config._module.args option, the strictness used on the attribute 149 # set argument would cause an infinite loop, if the result of the 150 # option is given as argument. 151 # 152 # To work-around the strictness issue on the deconstruction of the 153 # attributes set argument, we create a new attribute set which is 154 # constructed to satisfy the expected set of attributes. Thus calling 155 # a module will resolve strictly the attributes used as argument but 156 # not their values. The values are forwarding the result of the 157 # evaluation of the option. 158 requiredArgs = builtins.attrNames (lib.functionArgs f); 159 context = name: ''while evaluating the module argument `${name}' in "${key}":''; 160 extraArgs = builtins.listToAttrs (map (name: { 161 inherit name; 162 value = addErrorContext (context name) 163 (args.${name} or config._module.args.${name}); 164 }) requiredArgs); 165 166 # Note: we append in the opposite order such that we can add an error 167 # context on the explicited arguments of "args" too. This update 168 # operator is used to make the "args@{ ... }: with args.lib;" notation 169 # works. 170 in f (args // extraArgs) 171 else 172 f; 173 174 /* We have to pack and unpack submodules. We cannot wrap the expected 175 result of the function as we would no longer be able to list the arguments 176 of the submodule. (see applyIfFunction) */ 177 unpackSubmodule = unpack: m: args: 178 if isType "submodule" m then 179 { _file = m.file; } // (unpack m.submodule args) 180 else unpack m args; 181 182 packSubmodule = file: m: 183 { _type = "submodule"; file = file; submodule = m; }; 184 185 /* Merge a list of modules. This will recurse over the option 186 declarations in all modules, combining them into a single set. 187 At the same time, for each option declaration, it will merge the 188 corresponding option definitions in all machines, returning them 189 in the value attribute of each option. */ 190 mergeModules = prefix: modules: 191 mergeModules' prefix modules 192 (concatMap (m: map (config: { inherit (m) file; inherit config; }) (pushDownProperties m.config)) modules); 193 194 mergeModules' = prefix: options: configs: 195 listToAttrs (map (name: { 196 # We're descending into attribute ‘name’. 197 inherit name; 198 value = 199 let 200 loc = prefix ++ [name]; 201 # Get all submodules that declare ‘name’. 202 decls = concatMap (m: 203 if m.options ? ${name} 204 then [ { inherit (m) file; options = m.options.${name}; } ] 205 else [] 206 ) options; 207 # Get all submodules that define ‘name’. 208 defns = concatMap (m: 209 if m.config ? ${name} 210 then map (config: { inherit (m) file; inherit config; }) 211 (pushDownProperties m.config.${name}) 212 else [] 213 ) configs; 214 nrOptions = count (m: isOption m.options) decls; 215 # Extract the definitions for this loc 216 defns' = map (m: { inherit (m) file; value = m.config.${name}; }) 217 (filter (m: m.config ? ${name}) configs); 218 in 219 if nrOptions == length decls then 220 let opt = fixupOptionType loc (mergeOptionDecls loc decls); 221 in evalOptionValue loc opt defns' 222 else if nrOptions != 0 then 223 let 224 firstOption = findFirst (m: isOption m.options) "" decls; 225 firstNonOption = findFirst (m: !isOption m.options) "" decls; 226 in 227 throw "The option `${showOption loc}' in `${firstOption.file}' is a prefix of options in `${firstNonOption.file}'." 228 else 229 mergeModules' loc decls defns; 230 }) (concatMap (m: attrNames m.options) options)) 231 // { _definedNames = map (m: { inherit (m) file; names = attrNames m.config; }) configs; }; 232 233 /* Merge multiple option declarations into a single declaration. In 234 general, there should be only one declaration of each option. 235 The exception is the options attribute, which specifies 236 sub-options. These can be specified multiple times to allow one 237 module to add sub-options to an option declared somewhere else 238 (e.g. multiple modules define sub-options for fileSystems). 239 240 'loc' is the list of attribute names where the option is located. 241 242 'opts' is a list of modules. Each module has an options attribute which 243 correspond to the definition of 'loc' in 'opt.file'. */ 244 mergeOptionDecls = loc: opts: 245 foldl' (res: opt: 246 let t = res.type; 247 t' = opt.options.type; 248 mergedType = t.typeMerge t'.functor; 249 typesMergeable = mergedType != null; 250 typeSet = if (bothHave "type") && typesMergeable 251 then { type = mergedType; } 252 else {}; 253 bothHave = k: opt.options ? ${k} && res ? ${k}; 254 in 255 if bothHave "default" || 256 bothHave "example" || 257 bothHave "description" || 258 bothHave "apply" || 259 (bothHave "type" && (! typesMergeable)) 260 then 261 throw "The option `${showOption loc}' in `${opt.file}' is already declared in ${showFiles res.declarations}." 262 else 263 let 264 /* Add the modules of the current option to the list of modules 265 already collected. The options attribute except either a list of 266 submodules or a submodule. For each submodule, we add the file of the 267 current option declaration as the file use for the submodule. If the 268 submodule defines any filename, then we ignore the enclosing option file. */ 269 options' = toList opt.options.options; 270 coerceOption = file: opt: 271 if isFunction opt then packSubmodule file opt 272 else packSubmodule file { options = opt; }; 273 getSubModules = opt.options.type.getSubModules or null; 274 submodules = 275 if getSubModules != null then map (packSubmodule opt.file) getSubModules ++ res.options 276 else if opt.options ? options then map (coerceOption opt.file) options' ++ res.options 277 else res.options; 278 in opt.options // res // 279 { declarations = res.declarations ++ [opt.file]; 280 options = submodules; 281 } // typeSet 282 ) { inherit loc; declarations = []; options = []; } opts; 283 284 /* Merge all the definitions of an option to produce the final 285 config value. */ 286 evalOptionValue = loc: opt: defs: 287 let 288 # Add in the default value for this option, if any. 289 defs' = 290 (optional (opt ? default) 291 { file = head opt.declarations; value = mkOptionDefault opt.default; }) ++ defs; 292 293 # Handle properties, check types, and merge everything together. 294 res = 295 if opt.readOnly or false && length defs' > 1 then 296 throw "The option `${showOption loc}' is read-only, but it's set multiple times." 297 else 298 mergeDefinitions loc opt.type defs'; 299 300 # Check whether the option is defined, and apply the ‘apply’ 301 # function to the merged value. This allows options to yield a 302 # value computed from the definitions. 303 value = 304 if !res.isDefined then 305 throw "The option `${showOption loc}' is used but not defined." 306 else if opt ? apply then 307 opt.apply res.mergedValue 308 else 309 res.mergedValue; 310 311 in opt // 312 { value = addErrorContext "while evaluating the option `${showOption loc}':" value; 313 definitions = map (def: def.value) res.defsFinal; 314 files = map (def: def.file) res.defsFinal; 315 inherit (res) isDefined; 316 }; 317 318 # Merge definitions of a value of a given type. 319 mergeDefinitions = loc: type: defs: rec { 320 defsFinal = 321 let 322 # Process mkMerge and mkIf properties. 323 defs' = concatMap (m: 324 map (value: { inherit (m) file; inherit value; }) (dischargeProperties m.value) 325 ) defs; 326 327 # Process mkOverride properties. 328 defs'' = filterOverrides defs'; 329 330 # Sort mkOrder properties. 331 defs''' = 332 # Avoid sorting if we don't have to. 333 if any (def: def.value._type or "" == "order") defs'' 334 then sortProperties defs'' 335 else defs''; 336 in defs'''; 337 338 # Type-check the remaining definitions, and merge them. 339 mergedValue = foldl' (res: def: 340 if type.check def.value then res 341 else throw "The option value `${showOption loc}' in `${def.file}' is not of type `${type.description}'.") 342 (type.merge loc defsFinal) defsFinal; 343 344 isDefined = defsFinal != []; 345 346 optionalValue = 347 if isDefined then { value = mergedValue; } 348 else {}; 349 }; 350 351 /* Given a config set, expand mkMerge properties, and push down the 352 other properties into the children. The result is a list of 353 config sets that do not have properties at top-level. For 354 example, 355 356 mkMerge [ { boot = set1; } (mkIf cond { boot = set2; services = set3; }) ] 357 358 is transformed into 359 360 [ { boot = set1; } { boot = mkIf cond set2; services = mkIf cond set3; } ]. 361 362 This transform is the critical step that allows mkIf conditions 363 to refer to the full configuration without creating an infinite 364 recursion. 365 */ 366 pushDownProperties = cfg: 367 if cfg._type or "" == "merge" then 368 concatMap pushDownProperties cfg.contents 369 else if cfg._type or "" == "if" then 370 map (mapAttrs (n: v: mkIf cfg.condition v)) (pushDownProperties cfg.content) 371 else if cfg._type or "" == "override" then 372 map (mapAttrs (n: v: mkOverride cfg.priority v)) (pushDownProperties cfg.content) 373 else # FIXME: handle mkOrder? 374 [ cfg ]; 375 376 /* Given a config value, expand mkMerge properties, and discharge 377 any mkIf conditions. That is, this is the place where mkIf 378 conditions are actually evaluated. The result is a list of 379 config values. For example, mkIf false x yields [], 380 mkIf true x yields [x], and 381 382 mkMerge [ 1 (mkIf true 2) (mkIf true (mkIf false 3)) ] 383 384 yields [ 1 2 ]. 385 */ 386 dischargeProperties = def: 387 if def._type or "" == "merge" then 388 concatMap dischargeProperties def.contents 389 else if def._type or "" == "if" then 390 if isBool def.condition then 391 if def.condition then 392 dischargeProperties def.content 393 else 394 [ ] 395 else 396 throw "mkIf called with a non-Boolean condition" 397 else 398 [ def ]; 399 400 /* Given a list of config values, process the mkOverride properties, 401 that is, return the values that have the highest (that is, 402 numerically lowest) priority, and strip the mkOverride 403 properties. For example, 404 405 [ { file = "/1"; value = mkOverride 10 "a"; } 406 { file = "/2"; value = mkOverride 20 "b"; } 407 { file = "/3"; value = "z"; } 408 { file = "/4"; value = mkOverride 10 "d"; } 409 ] 410 411 yields 412 413 [ { file = "/1"; value = "a"; } 414 { file = "/4"; value = "d"; } 415 ] 416 417 Note that "z" has the default priority 100. 418 */ 419 filterOverrides = defs: 420 let 421 defaultPrio = 100; 422 getPrio = def: if def.value._type or "" == "override" then def.value.priority else defaultPrio; 423 highestPrio = foldl' (prio: def: min (getPrio def) prio) 9999 defs; 424 strip = def: if def.value._type or "" == "override" then def // { value = def.value.content; } else def; 425 in concatMap (def: if getPrio def == highestPrio then [(strip def)] else []) defs; 426 427 /* Sort a list of properties. The sort priority of a property is 428 1000 by default, but can be overridden by wrapping the property 429 using mkOrder. */ 430 sortProperties = defs: 431 let 432 strip = def: 433 if def.value._type or "" == "order" 434 then def // { value = def.value.content; inherit (def.value) priority; } 435 else def; 436 defs' = map strip defs; 437 compare = a: b: (a.priority or 1000) < (b.priority or 1000); 438 in sort compare defs'; 439 440 /* Hack for backward compatibility: convert options of type 441 optionSet to options of type submodule. FIXME: remove 442 eventually. */ 443 fixupOptionType = loc: opt: 444 let 445 options = opt.options or 446 (throw "Option `${showOption loc'}' has type optionSet but has no option attribute, in ${showFiles opt.declarations}."); 447 f = tp: 448 let optionSetIn = type: (tp.name == type) && (tp.functor.wrapped.name == "optionSet"); 449 in 450 if tp.name == "option set" || tp.name == "submodule" then 451 throw "The option ${showOption loc} uses submodules without a wrapping type, in ${showFiles opt.declarations}." 452 else if optionSetIn "attrsOf" then types.attrsOf (types.submodule options) 453 else if optionSetIn "loaOf" then types.loaOf (types.submodule options) 454 else if optionSetIn "listOf" then types.listOf (types.submodule options) 455 else if optionSetIn "nullOr" then types.nullOr (types.submodule options) 456 else tp; 457 in 458 if opt.type.getSubModules or null == null 459 then opt // { type = f (opt.type or types.unspecified); } 460 else opt // { type = opt.type.substSubModules opt.options; options = []; }; 461 462 463 /* Properties. */ 464 465 mkIf = condition: content: 466 { _type = "if"; 467 inherit condition content; 468 }; 469 470 mkAssert = assertion: message: content: 471 mkIf 472 (if assertion then true else throw "\nFailed assertion: ${message}") 473 content; 474 475 mkMerge = contents: 476 { _type = "merge"; 477 inherit contents; 478 }; 479 480 mkOverride = priority: content: 481 { _type = "override"; 482 inherit priority content; 483 }; 484 485 mkOptionDefault = mkOverride 1001; # priority of option defaults 486 mkDefault = mkOverride 1000; # used in config sections of non-user modules to set a default 487 mkForce = mkOverride 50; 488 mkVMOverride = mkOverride 10; # used by ‘nixos-rebuild build-vm’ 489 490 mkStrict = builtins.trace "`mkStrict' is obsolete; use `mkOverride 0' instead." (mkOverride 0); 491 492 mkFixStrictness = id; # obsolete, no-op 493 494 mkOrder = priority: content: 495 { _type = "order"; 496 inherit priority content; 497 }; 498 499 mkBefore = mkOrder 500; 500 mkAfter = mkOrder 1500; 501 502 503 # Convenient property used to transfer all definitions and their 504 # properties from one option to another. This property is useful for 505 # renaming options, and also for including properties from another module 506 # system, including sub-modules. 507 # 508 # { config, options, ... }: 509 # 510 # { 511 # # 'bar' might not always be defined in the current module-set. 512 # config.foo.enable = mkAliasDefinitions (options.bar.enable or {}); 513 # 514 # # 'barbaz' has to be defined in the current module-set. 515 # config.foobar.paths = mkAliasDefinitions options.barbaz.paths; 516 # } 517 # 518 # Note, this is different than taking the value of the option and using it 519 # as a definition, as the new definition will not keep the mkOverride / 520 # mkDefault properties of the previous option. 521 # 522 mkAliasDefinitions = mkAliasAndWrapDefinitions id; 523 mkAliasAndWrapDefinitions = wrap: option: 524 mkMerge 525 (optional (isOption option && option.isDefined) 526 (wrap (mkMerge option.definitions))); 527 528 529 /* Compatibility. */ 530 fixMergeModules = modules: args: evalModules { inherit modules args; check = false; }; 531 532 533 /* Return a module that causes a warning to be shown if the 534 specified option is defined. For example, 535 536 mkRemovedOptionModule [ "boot" "loader" "grub" "bootDevice" ] "<replacement instructions>" 537 538 causes a warning if the user defines boot.loader.grub.bootDevice. 539 540 replacementInstructions is a string that provides instructions on 541 how to achieve the same functionality without the removed option, 542 or alternatively a reasoning why the functionality is not needed. 543 replacementInstructions SHOULD be provided! 544 */ 545 mkRemovedOptionModule = optionName: replacementInstructions: 546 { options, ... }: 547 { options = setAttrByPath optionName (mkOption { 548 visible = false; 549 }); 550 config.warnings = 551 let opt = getAttrFromPath optionName options; in 552 optional opt.isDefined '' 553 The option definition `${showOption optionName}' in ${showFiles opt.files} no longer has any effect; please remove it. 554 ${replacementInstructions}''; 555 }; 556 557 /* Return a module that causes a warning to be shown if the 558 specified "from" option is defined; the defined value is however 559 forwarded to the "to" option. This can be used to rename options 560 while providing backward compatibility. For example, 561 562 mkRenamedOptionModule [ "boot" "copyKernels" ] [ "boot" "loader" "grub" "copyKernels" ] 563 564 forwards any definitions of boot.copyKernels to 565 boot.loader.grub.copyKernels while printing a warning. 566 */ 567 mkRenamedOptionModule = from: to: doRename { 568 inherit from to; 569 visible = false; 570 warn = true; 571 use = builtins.trace "Obsolete option `${showOption from}' is used. It was renamed to `${showOption to}'."; 572 }; 573 574 /* Return a module that causes a warning to be shown if any of the "from" 575 option is defined; the defined values can be used in the "mergeFn" to set 576 the "to" value. 577 This function can be used to merge multiple options into one that has a 578 different type. 579 580 "mergeFn" takes the module "config" as a parameter and must return a value 581 of "to" option type. 582 583 mkMergedOptionModule 584 [ [ "a" "b" "c" ] 585 [ "d" "e" "f" ] ] 586 [ "x" "y" "z" ] 587 (config: 588 let value = p: getAttrFromPath p config; 589 in 590 if (value [ "a" "b" "c" ]) == true then "foo" 591 else if (value [ "d" "e" "f" ]) == true then "bar" 592 else "baz") 593 594 - options.a.b.c is a removed boolean option 595 - options.d.e.f is a removed boolean option 596 - options.x.y.z is a new str option that combines a.b.c and d.e.f 597 functionality 598 599 This show a warning if any a.b.c or d.e.f is set, and set the value of 600 x.y.z to the result of the merge function 601 */ 602 mkMergedOptionModule = from: to: mergeFn: 603 { config, options, ... }: 604 { 605 options = foldl recursiveUpdate {} (map (path: setAttrByPath path (mkOption { 606 visible = false; 607 # To use the value in mergeFn without triggering errors 608 default = "_mkMergedOptionModule"; 609 })) from); 610 611 config = { 612 warnings = filter (x: x != "") (map (f: 613 let val = getAttrFromPath f config; 614 opt = getAttrFromPath f options; 615 in 616 optionalString 617 (val != "_mkMergedOptionModule") 618 "The option `${showOption f}' defined in ${showFiles opt.files} has been changed to `${showOption to}' that has a different type. Please read `${showOption to}' documentation and update your configuration accordingly." 619 ) from); 620 } // setAttrByPath to (mkMerge 621 (optional 622 (any (f: (getAttrFromPath f config) != "_mkMergedOptionModule") from) 623 (mergeFn config))); 624 }; 625 626 /* Single "from" version of mkMergedOptionModule. 627 Return a module that causes a warning to be shown if the "from" option is 628 defined; the defined value can be used in the "mergeFn" to set the "to" 629 value. 630 This function can be used to change an option into another that has a 631 different type. 632 633 "mergeFn" takes the module "config" as a parameter and must return a value of 634 "to" option type. 635 636 mkChangedOptionModule [ "a" "b" "c" ] [ "x" "y" "z" ] 637 (config: 638 let value = getAttrFromPath [ "a" "b" "c" ] config; 639 in 640 if value > 100 then "high" 641 else "normal") 642 643 - options.a.b.c is a removed int option 644 - options.x.y.z is a new str option that supersedes a.b.c 645 646 This show a warning if a.b.c is set, and set the value of x.y.z to the 647 result of the change function 648 */ 649 mkChangedOptionModule = from: to: changeFn: 650 mkMergedOptionModule [ from ] to changeFn; 651 652 /* Like mkRenamedOptionModule, but doesn't show a warning. */ 653 mkAliasOptionModule = from: to: doRename { 654 inherit from to; 655 visible = true; 656 warn = false; 657 use = id; 658 }; 659 660 doRename = { from, to, visible, warn, use }: 661 let 662 toOf = attrByPath to 663 (abort "Renaming error: option `${showOption to}' does not exists."); 664 in 665 { config, options, ... }: 666 { options = setAttrByPath from (mkOption { 667 description = "Alias of <option>${showOption to}</option>."; 668 apply = x: use (toOf config); 669 }); 670 config = { 671 warnings = 672 let opt = getAttrFromPath from options; in 673 optional (warn && opt.isDefined) 674 "The option `${showOption from}' defined in ${showFiles opt.files} has been renamed to `${showOption to}'."; 675 } // setAttrByPath to (mkAliasDefinitions (getAttrFromPath from options)); 676 }; 677 678}