From 0e333688cea468a28516bf6935648c03ed62a7bb Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 28 Oct 2013 00:56:22 +0100 Subject: [PATCH] Big cleanup of the NixOS module system The major changes are: * The evaluation is now driven by the declared options. In particular, this fixes the long-standing problem with lack of laziness of disabled option definitions. Thus, a configuration like config = mkIf false { environment.systemPackages = throw "bla"; }; will now evaluate without throwing an error. This also improves performance since we're not evaluating unused option definitions. * The implementation of properties is greatly simplified. * There is a new type constructor "submodule" that replaces "optionSet". Unlike "optionSet", "submodule" gets its option declarations as an argument, making it more like "listOf" and other type constructors. A typical use is: foo = mkOption { type = type.attrsOf (type.submodule ( { config, ... }: { bar = mkOption { ... }; xyzzy = mkOption { ... }; })); }; Existing uses of "optionSet" are automatically mapped to "submodule". * Modules are now checked for unsupported attributes: you get an error if a module contains an attribute other than "config", "options" or "imports". * The new implementation is faster and uses much less memory. --- lib/default.nix | 5 +- lib/lists.nix | 5 +- lib/modules.nix | 580 +++++++----------- lib/options.nix | 46 +- lib/properties.nix | 464 -------------- lib/types.nix | 144 ++--- nixos/lib/eval-config.nix | 13 +- nixos/modules/config/shells-environment.nix | 9 +- .../system/activation/activation-script.nix | 2 +- 9 files changed, 294 insertions(+), 974 deletions(-) delete mode 100644 lib/properties.nix diff --git a/lib/default.nix b/lib/default.nix index 033269e6b60..fc92e04503b 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -8,7 +8,6 @@ let sources = import ./sources.nix; modules = import ./modules.nix; options = import ./options.nix; - properties = import ./properties.nix; types = import ./types.nix; meta = import ./meta.nix; debug = import ./debug.nix; @@ -21,13 +20,13 @@ let in { inherit trivial lists strings stringsWithDeps attrsets sources options - properties modules types meta debug maintainers licenses platforms systems; + modules types meta debug maintainers licenses platforms systems; # Pull in some builtins not included elsewhere. inherit (builtins) pathExists readFile; } # !!! don't include everything at top-level; perhaps only the most # commonly used functions. // trivial // lists // strings // stringsWithDeps // attrsets // sources - // properties // options // types // meta // debug // misc // modules + // options // types // meta // debug // misc // modules // systems // customisation diff --git a/lib/lists.nix b/lib/lists.nix index 262a529b2b6..eb7e6baf5ed 100644 --- a/lib/lists.nix +++ b/lib/lists.nix @@ -165,10 +165,11 @@ in rec { zipLists = zipListsWith (fst: snd: { inherit fst snd; }); - - # Reverse the order of the elements of a list. + + # Reverse the order of the elements of a list. FIXME: O(n^2)! reverseList = fold (e: acc: acc ++ [ e ]) []; + # Sort a list based on a comparator function which compares two # elements and returns true if the first argument is strictly below # the second argument. The returned list is sorted in an increasing diff --git a/lib/modules.nix b/lib/modules.nix index f914947e784..f5a82d7d8e9 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1,379 +1,245 @@ -# NixOS module handling. - -let lib = import ./default.nix; in - -with { inherit (builtins) head; }; -with import ./trivial.nix; -with import ./lists.nix; -with import ./misc.nix; -with import ./attrsets.nix; -with import ./options.nix; -with import ./properties.nix; +with import ./.. {}; +with lib; rec { - # Unfortunately this can also be a string. - isPath = x: !( - builtins.isFunction x - || builtins.isAttrs x - || builtins.isInt x - || builtins.isBool x - || builtins.isList x - ); - - - importIfPath = path: - if isPath path then - import path - else - path; - - - applyIfFunction = f: arg: - if builtins.isFunction f then - f arg - else - f; - - - isModule = m: - (m ? config && isAttrs m.config && ! isOption m.config) - || (m ? options && isAttrs m.options && ! isOption m.options); - - - # Convert module to a set which has imports / options and config - # attributes. - unifyModuleSyntax = m: + /* Evaluate a set of modules. The result is a set of two + attributes: ‘options’: the nested set of all option declarations, + and ‘config’: the nested set of all option values. */ + evalModules = modules: args: let - delayedModule = delayProperties m; - - getImports = - toList (rmProperties (delayedModule.require or [])); - getImportedPaths = filter isPath getImports; - getImportedSets = filter (x: !isPath x) getImports; - - getConfig = - removeAttrs delayedModule ["require" "key" "imports"]; + args' = args // result; + closed = closeModules modules args'; + # Note: the list of modules is reversed to maintain backward + # compatibility with the old module system. Not sure if this is + # the most sensible policy. + options = mergeModules (reverseList closed); + config = yieldConfig options; + yieldConfig = mapAttrs (n: v: if isOption v then v.value else yieldConfig v); + result = { inherit options config; }; + in result; + /* Close a set of modules under the ‘imports’ relation. */ + closeModules = modules: args: + let + coerceToModule = n: x: + if isAttrs x || builtins.isFunction x then + unifyModuleSyntax "anon-${toString n}" (applyIfFunction x args) + else + unifyModuleSyntax (toString x) (applyIfFunction (import x) args); + toClosureList = imap (path: coerceToModule path); in - if isModule m then - { key = ""; } // m + builtins.genericClosure { + startSet = toClosureList modules; + operator = m: toClosureList m.imports; + }; + + /* Massage a module into canonical form, that is, a set consisting + of ‘options’, ‘config’ and ‘imports’ attributes. */ + unifyModuleSyntax = key: m: + if m ? config || m ? options || m ? imports then + let badAttrs = removeAttrs m ["imports" "options" "config"]; in + if badAttrs != {} then + throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. ${builtins.toXML m} " else - { key = ""; - imports = (m.imports or []) ++ getImportedPaths; - config = getConfig; - } // ( - if getImportedSets != [] then - assert length getImportedSets == 1; - { options = head getImportedSets; } + { inherit key; + imports = m.imports or []; + options = m.options or {}; + config = m.config or {}; + } + else + { inherit key; + imports = m.require or []; + options = {}; + config = m; + }; + + applyIfFunction = f: arg: if builtins.isFunction f then f arg else f; + + /* Merge a list of modules. This will recurse over the option + declarations in all modules, combining them into a single set. + At the same time, for each option declaration, it will merge the + corresponding option definitions in all machines, returning them + in the ‘value’ attribute of each option. */ + mergeModules = modules: + mergeModules' [] (map (m: m.options) modules) (concatMap (m: pushDownProperties m.config) modules); + + mergeModules' = loc: options: configs: + zipAttrsWith (name: vals: + let loc' = loc ++ [name]; in + if all isOption vals then + let opt = fixupOptionType loc' (mergeOptionDecls loc' vals); + in evalOptionValue loc' opt (catAttrs name configs) + else if any isOption vals then + throw "There are options with the prefix `${showOption loc'}', which is itself an option." + else + mergeModules' loc' vals (concatMap pushDownProperties (catAttrs name configs)) + ) options; + + /* Merge multiple option declarations into a single declaration. In + general, there should be only one declaration of each option. + The exception is the ‘options’ attribute, which specifies + sub-options. These can be specified multiple times to allow one + module to add sub-options to an option declared somewhere else + (e.g. multiple modules define sub-options for ‘fileSystems’). */ + mergeOptionDecls = loc: opts: + fold (opt1: opt2: + if opt1 ? default && opt2 ? default || + opt1 ? example && opt2 ? example || + opt1 ? description && opt2 ? description || + opt1 ? merge && opt2 ? merge || + opt1 ? apply && opt2 ? apply || + opt1 ? type && opt2 ? type + then + throw "Conflicting declarations of the option `${showOption loc}'." + else + opt1 // opt2 + // optionalAttrs (opt1 ? options && opt2 ? options) + { options = [ opt1.options opt2.options ]; } + ) {} opts; + + /* Merge all the definitions of an option to produce the final + config value. */ + evalOptionValue = loc: opt: defs: + let + # Process mkMerge and mkIf properties. + defs' = concatMap dischargeProperties defs; + # Process mkOverride properties, adding in the default + # value specified in the option declaration (if any). + defsFinal = filterOverrides (optional (opt ? default) (mkOptionDefault opt.default) ++ defs'); + # Type-check the remaining definitions, and merge them + # if possible. + merged = + if defsFinal == [] then + throw "The option `${showOption loc}' is used but not defined." + else + if all opt.type.check defsFinal then + opt.type.merge defsFinal + #throw "The option `${showOption loc}' has multiple values (with no way to merge them)." else - {} - ); - - - unifyOptionModule = {key ? ""}: name: index: m: (args: - let - module = lib.applyIfFunction m args; - key_ = rec { - file = key; - option = name; - number = index; - outPath = key; - }; - in if lib.isModule module then - { key = key_; } // module - else - { key = key_; options = module; } - ); - - - moduleClosure = initModules: args: - let - moduleImport = origin: index: m: - let m' = applyIfFunction (importIfPath m) args; - in (unifyModuleSyntax m') // { - # used by generic closure to avoid duplicated imports. - key = - if isPath m then m - else m'.key or (newModuleName origin index); - }; - - getImports = m: m.imports or []; - - newModuleName = origin: index: - "${origin.key}:"; - - topLevel = { - key = ""; + throw "A value of the option `${showOption loc}' has a bad type."; + # Finally, apply the ‘apply’ function to the merged + # value. This allows options to yield a value computed + # from the definitions. + value = (opt.apply or id) merged; + in opt // + { inherit value; + definitions = defsFinal; + isDefined = defsFinal != []; }; - in - (lazyGenericClosure { - startSet = imap (moduleImport topLevel) initModules; - operator = m: imap (moduleImport m) (getImports m); - }); + /* Given a config set, expand mkMerge properties, and push down the + mkIf properties into the children. The result is a list of + config sets that do not have properties at top-level. For + example, + mkMerge [ { boot = set1; } (mkIf cond { boot = set2; services = set3; }) ] - moduleApply = funs: module: - lib.mapAttrs (name: value: - if builtins.hasAttr name funs then - let fun = lib.getAttr name funs; in - fun value + is transformed into + + [ { boot = set1; } { boot = mkIf cond set2; services mkIf cond set3; } ]. + + This transform is the critical step that allows mkIf conditions + to refer to the full configuration without creating an infinite + recursion. + */ + pushDownProperties = cfg: + if cfg._type or "" == "merge" then + concatMap pushDownProperties cfg.contents + else if cfg._type or "" == "if" then + map (mapAttrs (n: v: mkIf cfg.condition v)) (pushDownProperties cfg.content) + else + # FIXME: handle mkOverride? + [ cfg ]; + + /* Given a config value, expand mkMerge properties, and discharge + any mkIf conditions. That is, this is the place where mkIf + conditions are actually evaluated. The result is a list of + config values. For example, ‘mkIf false x’ yields ‘[]’, + ‘mkIf true x’ yields ‘[x]’, and + + mkMerge [ 1 (mkIf true 2) (mkIf true (mkIf false 3)) ] + + yields ‘[ 1 2 ]’. + */ + dischargeProperties = def: + if def._type or "" == "merge" then + concatMap dischargeProperties def.contents + else if def._type or "" == "if" then + if def.condition then + dischargeProperties def.content else - value - ) module; - - - # Handle mkMerge function left behind after a delay property. - moduleFlattenMerge = module: - if module ? config && - isProperty module.config && - isMerge module.config.property - then - (map (cfg: { key = module.key; config = cfg; }) module.config.content) - ++ [ (module // { config = {}; }) ] + [ ] else - [ module ]; + [ def ]; + /* Given a list of config value, process the mkOverride properties, + that is, return the values that have the highest (that + is,. numerically lowest) priority, and strip the mkOverride + properties. For example, - # Handle mkMerge attributes which are left behind by previous delay - # properties and convert them into a list of modules. Delay properties - # inside the config attribute of a module and create a second module if a - # mkMerge attribute was left behind. - # - # Module -> [ Module ] - delayModule = module: - map (moduleApply { config = delayProperties; }) (moduleFlattenMerge module); + [ (mkOverride 10 "a") (mkOverride 20 "b") "z" (mkOverride 10 "d") ] - - evalDefinitions = opt: values: - if opt.type.delayOnGlobalEval or false then - map (delayPropertiesWithIter opt.type.iter opt.name) - (evalLocalProperties values) - else - evalProperties values; - - - selectModule = name: m: - { inherit (m) key; - } // ( - if m ? options && builtins.hasAttr name m.options then - { options = lib.getAttr name m.options; } - else {} - ) // ( - if m ? config && builtins.hasAttr name m.config then - { config = lib.getAttr name m.config; } - else {} - ); - - filterModules = name: modules: - filter (m: m ? config || m ? options) ( - map (selectModule name) modules - ); - - - modulesNames = modules: - lib.concatMap (m: [] - ++ optionals (m ? options) (lib.attrNames m.options) - ++ optionals (m ? config) (lib.attrNames m.config) - ) modules; - - - moduleZip = funs: modules: - lib.mapAttrs (name: fun: - fun (catAttrs name modules) - ) funs; - - - moduleMerge = path: modules_: + yields ‘[ "a" "d" ]’. Note that "z" has the default priority 100. + */ + filterOverrides = defs: let - addName = name: - if path == "" then name else path + "." + name; + defaultPrio = 100; + getPrio = def: if def._type or "" == "override" then def.priority else defaultPrio; + min = x: y: if x < y then x else y; + highestPrio = fold (def: prio: min (getPrio def) prio) 9999 defs; + strip = def: if def._type or "" == "override" then def.content else def; + in concatMap (def: if getPrio def == highestPrio then [(strip def)] else []) defs; - modules = concatLists (map delayModule modules_); - - modulesOf = name: filterModules name modules; - declarationsOf = name: filter (m: m ? options) (modulesOf name); - definitionsOf = name: filter (m: m ? config ) (modulesOf name); - - recurseInto = name: - moduleMerge (addName name) (modulesOf name); - - recurseForOption = name: modules: args: - moduleMerge name ( - moduleClosure modules args - ); - - errorSource = modules: - "The error may come from the following files:\n" + ( - lib.concatStringsSep "\n" ( - map (m: - if m ? key then toString m.key else "" - ) modules - ) - ); - - eol = "\n"; - - allNames = modulesNames modules; - - getResults = m: - let fetchResult = s: mapAttrs (n: v: v.result) s; in { - options = fetchResult m.options; - config = fetchResult m.config; - }; - - endRecursion = { options = {}; config = {}; }; - - in if modules == [] then endRecursion else - getResults (fix (crossResults: moduleZip { - options = lib.zipWithNames allNames (name: values: rec { - config = lib.getAttr name crossResults.config; - - declarations = declarationsOf name; - declarationSources = - map (m: { - source = m.key; - }) declarations; - - hasOptions = values != []; - isOption = any lib.isOption values; - - decls = # add location to sub-module options. - map (m: - mapSubOptions - (unifyOptionModule {inherit (m) key;} name) - m.options - ) declarations; - - decl = - lib.addErrorContext "${eol - }while enhancing option `${addName name}':${eol - }${errorSource declarations}${eol - }" ( - addOptionMakeUp - { name = addName name; recurseInto = recurseForOption; } - (mergeOptionDecls decls) - ); - - value = decl // (with config; { - inherit (config) isNotDefined; - isDefined = ! isNotDefined; - declarations = declarationSources; - definitions = definitionSources; - config = strictResult; - }); - - recurse = (recurseInto name).options; - - result = - if isOption then value - else if !hasOptions then {} - else if all isAttrs values then recurse - else - throw "${eol - }Unexpected type where option declarations are expected.${eol - }${errorSource declarations}${eol - }"; - - }); - - config = lib.zipWithNames allNames (name: values_: rec { - option = lib.getAttr name crossResults.options; - - definitions = definitionsOf name; - definitionSources = - map (m: { - source = m.key; - value = m.config; - }) definitions; - - values = values_ ++ - optionals (option.isOption && option.decl ? extraConfigs) - option.decl.extraConfigs; - - defs = evalDefinitions option.decl values; - - isNotDefined = defs == []; - - value = - lib.addErrorContext "${eol - }while evaluating the option `${addName name}':${eol - }${errorSource (modulesOf name)}${eol - }" ( - let opt = option.decl; in - opt.apply ( - if isNotDefined then - opt.default or (throw "Option `${addName name}' not defined and does not have a default value.") - else opt.merge defs - ) - ); - - strictResult = builtins.tryEval (builtins.toXML value); - - recurse = (recurseInto name).config; - - configIsAnOption = v: isOption (rmProperties v); - errConfigIsAnOption = - let badModules = filter (m: configIsAnOption m.config) definitions; in - "${eol - }Option ${addName name} is defined in the configuration section.${eol - }${errorSource badModules}${eol - }"; - - errDefinedWithoutDeclaration = - let badModules = definitions; in - "${eol - }Option '${addName name}' defined without option declaration.${eol - }${errorSource badModules}${eol - }"; - - result = - if option.isOption then value - else if !option.hasOptions then throw errDefinedWithoutDeclaration - else if any configIsAnOption values then throw errConfigIsAnOption - else if all isAttrs values then recurse - # plain value during the traversal - else throw errDefinedWithoutDeclaration; - - }); - } modules)); - - - fixMergeModules = initModules: {...}@args: - lib.fix (result: - # This trick avoids an infinite loop because names of attribute - # are know and it is not required to evaluate the result of - # moduleMerge to know which attributes are present as arguments. - let module = { inherit (result) options config; }; in - moduleMerge "" ( - moduleClosure initModules (module // args) - ) - ); - - - # Visit all definitions to raise errors related to undeclared options. - checkModule = path: {config, options, ...}@m: + /* Hack for backward compatibility: convert options of type + optionSet to configOf. FIXME: remove eventually. */ + fixupOptionType = loc: opt: let - eol = "\n"; - addName = name: - if path == "" then name else path + "." + name; - in - if lib.isOption options then - if options ? options then - options.type.fold - (cfg: res: res && checkModule (options.type.docPath path) cfg._args) - true config - else - true - else if isAttrs options && lib.attrNames m.options != [] then - all (name: - lib.addErrorContext "${eol - }while checking the attribute `${addName name}':${eol - }" (checkModule (addName name) (selectModule name m)) - ) (lib.attrNames m.config) - else - builtins.trace "try to evaluate config ${lib.showVal config}." - false; + options' = opt.options or + (throw "Option `${showOption loc'}' has type optionSet but has no option attribute."); + coerce = x: + if builtins.isFunction x then x + else { config, ... }: { options = x; }; + options = map coerce (flatten options'); + f = tp: + if tp.name == "option set" then types.submodule options + else if tp.name == "attribute set of option sets" then types.attrsOf (types.submodule options) + else if tp.name == "list or attribute set of option sets" then types.loaOf (types.submodule options) + else if tp.name == "list of option sets" then types.listOf (types.submodule options) + else if tp.name == "null or option set" then types.nullOr (types.submodule options) + else tp; + in opt // { type = f (opt.type or types.unspecified); }; + + + /* Properties. */ + + mkIf = condition: content: + { _type = "if"; + inherit condition content; + }; + + mkAssert = assertion: message: content: + mkIf + (if assertion then true else throw "\nFailed assertion: ${message}") + content; + + mkMerge = contents: + { _type = "merge"; + inherit contents; + }; + + mkOverride = priority: content: + { _type = "override"; + inherit priority content; + }; + + mkOptionDefault = mkOverride 1001; + mkDefault = mkOverride 1000; + mkForce = mkOverride 50; + + mkFixStrictness = id; # obsolete, no-op + + # FIXME: Add mkOrder back in. It's not currently used anywhere in + # NixOS, but it should be useful. } diff --git a/lib/options.nix b/lib/options.nix index b6a88008bb7..0fa702a616c 100644 --- a/lib/options.nix +++ b/lib/options.nix @@ -2,12 +2,10 @@ let lib = import ./default.nix; in -with { inherit (builtins) head length; }; with import ./trivial.nix; with import ./lists.nix; with import ./misc.nix; with import ./attrsets.nix; -with import ./properties.nix; rec { @@ -137,46 +135,6 @@ rec { handleOptionSets ]; - # Merge a list of options containning different field. This is useful to - # separate the merge & apply fields from the interface. - mergeOptionDecls = opts: - if opts == [] then {} - else if length opts == 1 then - let opt = head opts; in - if opt ? options then - opt // { options = toList opt.options; } - else - opt - else - fold (opt1: opt2: - lib.addErrorContext "opt1 = ${lib.showVal opt1}\nopt2 = ${lib.showVal opt2}" ( - # You cannot merge if two options have the same field. - assert opt1 ? default -> ! opt2 ? default; - assert opt1 ? example -> ! opt2 ? example; - assert opt1 ? description -> ! opt2 ? description; - assert opt1 ? merge -> ! opt2 ? merge; - assert opt1 ? apply -> ! opt2 ? apply; - assert opt1 ? type -> ! opt2 ? type; - opt1 // opt2 - // optionalAttrs (opt1 ? options || opt2 ? options) { - options = - (toList (opt1.options or [])) - ++ (toList (opt2.options or [])); - } - // optionalAttrs (opt1 ? extraConfigs || opt2 ? extraConfigs) { - extraConfigs = opt1.extraConfigs or [] ++ opt2.extraConfigs or []; - } - // optionalAttrs (opt1 ? extraArgs || opt2 ? extraArgs) { - extraArgs = opt1.extraArgs or {} // opt2.extraArgs or {}; - } - // optionalAttrs (opt1 ? individualExtraArgs || opt2 ? individualExtraArgs) { - individualExtraArgs = zipAttrsWith (name: values: - if length values == 1 then head values else (head values // (head (tail values))) - ) [ (opt1.individualExtraArgs or {}) (opt2.individualExtraArgs or {}) ]; - } - )) {} opts; - - # !!! This function will be removed because this can be done with the # multiple option declarations. addDefaultOptionValues = defs: opts: opts // @@ -285,6 +243,7 @@ rec { else []; in + # FIXME: expensive (O(n^2) [ docOption ] ++ subOptions ++ rest ) [] options; @@ -308,4 +267,7 @@ rec { literalExample = text: { _type = "literalExample"; inherit text; }; + /* Helper functions. */ + showOption = concatStringsSep "."; + } diff --git a/lib/properties.nix b/lib/properties.nix deleted file mode 100644 index 22aa8d891d8..00000000000 --- a/lib/properties.nix +++ /dev/null @@ -1,464 +0,0 @@ -# Nixpkgs/NixOS properties. Generalize the problem of delayable (not yet -# evaluable) properties like mkIf. - -let lib = import ./default.nix; in - -with { inherit (builtins) head tail; }; -with import ./trivial.nix; -with import ./lists.nix; -with import ./misc.nix; -with import ./attrsets.nix; - -rec { - - inherit (lib) isType; - - # Tell that nothing is defined. When properties are evaluated, this type - # is used to remove an entry. Thus if your property evaluation semantic - # implies that you have to mute the content of an attribute, then your - # property should produce this value. - isNotdef = isType "notdef"; - mkNotdef = {_type = "notdef";}; - - # General property type, it has a property attribute and a content - # attribute. The property attribute refers to an attribute set which - # contains a _type attribute and a list of functions which are used to - # evaluate this property. The content attribute is used to stack properties - # on top of each other. - # - # The optional functions which may be contained in the property attribute - # are: - # - onDelay: run on a copied property. - # - onGlobalDelay: run on all copied properties. - # - onEval: run on an evaluated property. - # - onGlobalEval: run on a list of property stack on top of their values. - isProperty = isType "property"; - mkProperty = p@{property, content, ...}: p // { - _type = "property"; - }; - - # Go through the stack of properties and apply the function `op' on all - # property and call the function `nul' on the final value which is not a - # property. The stack is traversed in reversed order. The `op' function - # should expect a property with a content which have been modified. - # - # Warning: The `op' function expects only one argument in order to avoid - # calls to mkProperties as the argument is already a valid property which - # contains the result of the folding inside the content attribute. - foldProperty = op: nul: attrs: - if isProperty attrs then - op (attrs // { - content = foldProperty op nul attrs.content; - }) - else - nul attrs; - - # Simple function which can be used as the `op' argument of the - # foldProperty function. Properties that you don't want to handle can be - # ignored with the `id' function. `isSearched' is a function which should - # check the type of a property and return a boolean value. `thenFun' and - # `elseFun' are functions which behave as the `op' argument of the - # foldProperty function. - foldFilter = isSearched: thenFun: elseFun: attrs: - if isSearched attrs.property then - thenFun attrs - else - elseFun attrs; - - - # Move properties from the current attribute set to the attribute - # contained in this attribute set. This trigger property handlers called - # `onDelay' and `onGlobalDelay'. - delayPropertiesWithIter = iter: path: attrs: - let cleanAttrs = rmProperties attrs; in - if isProperty attrs then - iter (a: v: - lib.addErrorContext "while moving properties on the attribute `${a}':" ( - triggerPropertiesGlobalDelay a ( - triggerPropertiesDelay a ( - copyProperties attrs v - )))) path cleanAttrs - else - attrs; - - delayProperties = # implicit attrs argument. - let - # mapAttrs except that it also recurse into potential mkMerge - # functions. This may cause a strictness issue because looking the - # type of a string implies evaluating it. - iter = fun: path: value: - lib.mapAttrs (attr: val: - if isProperty val && isMerge val.property then - val // { content = map (fun attr) val.content; } - else - fun attr val - ) value; - in - delayPropertiesWithIter iter ""; - - # Call onDelay functions. - triggerPropertiesDelay = name: attrs: - let - callOnDelay = p@{property, ...}: - if property ? onDelay then - property.onDelay name p - else - p; - in - foldProperty callOnDelay id attrs; - - # Call onGlobalDelay functions. - triggerPropertiesGlobalDelay = name: attrs: - let - globalDelayFuns = uniqListExt { - getter = property: property._type; - inputList = foldProperty (p@{property, content, ...}: - if property ? onGlobalDelay then - [ property ] ++ content - else - content - ) (a: []) attrs; - }; - - callOnGlobalDelay = property: content: - property.onGlobalDelay name content; - in - fold callOnGlobalDelay attrs globalDelayFuns; - - # Expect a list of values which may have properties and return the same - # list of values where all properties have been evaluated and where all - # ignored values are removed. This trigger property handlers called - # `onEval' and `onGlobalEval'. - evalProperties = valList: - if valList != [] then - filter (x: !isNotdef x) ( - triggerPropertiesGlobalEval ( - evalLocalProperties valList - ) - ) - else - valList; - - evalLocalProperties = valList: - filter (x: !isNotdef x) ( - map triggerPropertiesEval valList - ); - - # Call onEval function - triggerPropertiesEval = val: - foldProperty (p@{property, ...}: - if property ? onEval then - property.onEval p - else - p - ) id val; - - # Call onGlobalEval function - triggerPropertiesGlobalEval = valList: - let - globalEvalFuns = uniqListExt { - getter = property: property._type; - inputList = - fold (attrs: list: - foldProperty (p@{property, content, ...}: - if property ? onGlobalEval then - [ property ] ++ content - else - content - ) (a: list) attrs - ) [] valList; - }; - - callOnGlobalEval = property: valList: property.onGlobalEval valList; - in - fold callOnGlobalEval valList globalEvalFuns; - - # Remove all properties on top of a value and return the value. - rmProperties = - foldProperty (p@{content, ...}: content) id; - - # Copy properties defined on a value on another value. - copyProperties = attrs: newAttrs: - foldProperty id (x: newAttrs) attrs; - - /* Merge. */ - - # Create "merge" statement which is skipped by the delayProperty function - # and interpreted by the underlying system using properties (modules). - - # Create a "Merge" property which only contains a condition. - isMerge = isType "merge"; - mkMerge = content: mkProperty { - property = { - _type = "merge"; - onDelay = name: val: throw "mkMerge is not the first of the list of properties."; - onEval = val: throw "mkMerge is not allowed on option definitions."; - }; - inherit content; - }; - - /* If. ThenElse. Always. */ - - # create "if" statement that can be delayed on sets until a "then-else" or - # "always" set is reached. When an always set is reached the condition - # is ignore. - - # Create a "If" property which only contains a condition. - isIf = isType "if"; - mkIf = condition: content: mkProperty { - property = { - _type = "if"; - onGlobalDelay = onIfGlobalDelay; - onEval = onIfEval; - inherit condition; - }; - inherit content; - }; - - mkAssert = assertion: message: content: - mkIf - (if assertion then true else throw "\nFailed assertion: ${message}") - content; - - # Evaluate the "If" statements when either "ThenElse" or "Always" - # statement is encountered. Otherwise it removes multiple If statements and - # replaces them by one "If" statement where the condition is the list of all - # conditions joined with a "and" operation. - onIfGlobalDelay = name: content: - let - # extract if statements and non-if statements and repectively put them - # in the attribute list and attrs. - ifProps = - foldProperty - (foldFilter (p: isIf p) - # then, push the condition inside the list list - (p@{property, content, ...}: - { inherit (content) attrs; - list = [property] ++ content.list; - } - ) - # otherwise, add the propertie. - (p@{property, content, ...}: - { inherit (content) list; - attrs = p // { content = content.attrs; }; - } - ) - ) - (attrs: { list = []; inherit attrs; }) - content; - - # compute the list of if statements. - evalIf = content: condition: list: - if list == [] then - mkIf condition content - else - let p = head list; in - evalIf content (condition && p.condition) (tail list); - in - evalIf ifProps.attrs true ifProps.list; - - # Evaluate the condition of the "If" statement to either get the value or - # to ignore the value. - onIfEval = p@{property, content, ...}: - if property.condition then - content - else - mkNotdef; - - /* mkOverride */ - - # Create an "Override" statement which allow the user to define - # priorities between values. The default priority is 100. The lowest - # priorities are kept. The template argument must reproduce the same - # attribute set hierarchy to override leaves of the hierarchy. - isOverride = isType "override"; - mkOverrideTemplate = priority: template: content: mkProperty { - property = { - _type = "override"; - onDelay = onOverrideDelay; - onGlobalEval = onOverrideGlobalEval; - inherit priority template; - }; - inherit content; - }; - - # Like mkOverrideTemplate, but without the template argument. - mkOverride = priority: content: mkOverrideTemplate priority {} content; - - # Sugar to override the default value of the option by making a new - # default value based on the configuration. - mkDefaultValue = mkOverride 1000; - mkDefault = mkOverride 1000; - mkForce = mkOverride 50; - mkStrict = mkOverride 0; - - # Make the template traversal in function of the property traversal. If - # the template define a non-empty attribute set, then the property is - # copied only on all mentionned attributes inside the template. - # Otherwise, the property is kept on all sub-attribute definitions. - onOverrideDelay = name: p@{property, content, ...}: - let inherit (property) template; in - if isAttrs template && template != {} then - if hasAttr name template then - p // { - property = p.property // { - template = builtins.getAttr name template; - }; - } - # Do not override the attribute \name\ - else - content - # Override values defined inside the attribute \name\. - else - p; - - # Keep values having lowest priority numbers only throwing away those having - # a higher priority assigned. - onOverrideGlobalEval = valList: - let - defaultPrio = 100; - - inherit (builtins) lessThan; - - getPrioVal = - foldProperty - (foldFilter isOverride - (p@{property, content, ...}: - if content ? priority && lessThan content.priority property.priority then - content - else - content // { - inherit (property) priority; - } - ) - (p@{property, content, ...}: - content // { - value = p // { content = content.value; }; - } - ) - ) (value: { inherit value; }); - - addDefaultPrio = x: - if x ? priority then x - else x // { priority = defaultPrio; }; - - prioValList = map (x: addDefaultPrio (getPrioVal x)) valList; - - higherPrio = - if prioValList == [] then - defaultPrio - else - fold (x: min: - if lessThan x.priority min then - x.priority - else - min - ) (head prioValList).priority (tail prioValList); - in - map (x: - if x.priority == higherPrio then - x.value - else - mkNotdef - ) prioValList; - - /* mkOrder */ - - # Order definitions based on there index value. This property is useful - # when the result of the merge function depends on the order on the - # initial list. (e.g. concatStrings) Definitions are ordered based on - # their rank. The lowest ranked definition would be the first to element - # of the list used by the merge function. And the highest ranked - # definition would be the last. Definitions which does not have any rank - # value have the default rank of 100. - isOrder = isType "order"; - mkOrder = rank: content: mkProperty { - property = { - _type = "order"; - onGlobalEval = onOrderGlobalEval; - inherit rank; - }; - inherit content; - }; - - mkHeader = mkOrder 10; - mkFooter = mkOrder 1000; - - # Fetch the rank of each definition (add the default rank is none) and - # sort them based on their ranking. - onOrderGlobalEval = valList: - let - defaultRank = 100; - - inherit (builtins) lessThan; - - getRankVal = - foldProperty - (foldFilter isOrder - (p@{property, content, ...}: - if content ? rank then - content - else - content // { - inherit (property) rank; - } - ) - (p@{property, content, ...}: - content // { - value = p // { content = content.value; }; - } - ) - ) (value: { inherit value; }); - - addDefaultRank = x: - if x ? rank then x - else x // { rank = defaultRank; }; - - rankValList = map (x: addDefaultRank (getRankVal x)) valList; - - cmp = x: y: - builtins.lessThan x.rank y.rank; - in - map (x: x.value) (sort cmp rankValList); - - /* mkFixStrictness */ - - # This is a hack used to restore laziness on some option definitions. - # Some option definitions are evaluated when they are not used. This - # error is caused by the strictness of type checking builtins. Builtins - # like 'isAttrs' are too strict because they have to evaluate their - # arguments to check if the type is correct. This evaluation, cause the - # strictness of properties. - # - # Properties can be stacked on top of each other. The stackability of - # properties on top of the option definition is nice for user manipulation - # but require to check if the content of the property is not another - # property. Such testing implies to verify if this is an attribute set - # and if it possess the type 'property'. (see isProperty & typeOf/isType) - # - # To avoid strict evaluation of option definitions, 'mkFixStrictness' is - # introduced. This property protects an option definition by replacing - # the base of the stack of properties by 'mkNotDef', when this property is - # evaluated it returns the original definition. - # - # This property is useful over any elements which depends on options which - # are raising errors when they get evaluated without the proper settings. - # - # Plain list and attribute set are lazy structures, which means that the - # container gets evaluated but not the content. Thus, using this property - # on top of plain list or attribute set is pointless. - # - # This is a Hack, you should avoid it! - - # This property has a long name because you should avoid it. - isFixStrictness = attrs: (typeOf attrs) == "fix-strictness"; - mkFixStrictness = value: - mkProperty { - property = { - _type = "fix-strictness"; - onEval = p: value; - }; - content = mkNotdef; - }; - -} diff --git a/lib/types.nix b/lib/types.nix index 156d72ac5e7..0545cd6a3c2 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -3,10 +3,11 @@ let lib = import ./default.nix; in -with import ./lists.nix; -with import ./attrsets.nix; -with import ./options.nix; -with import ./trivial.nix; +with lib.lists; +with lib.attrsets; +with lib.options; +with lib.trivial; +with lib.modules; rec { @@ -20,48 +21,43 @@ rec { # name (name of the type) - # check (check the config value. Before returning false it should trace the bad value eg using traceValIfNot) + # check (check the config value) # merge (default merge function) - # iter (iterate on all elements contained in this type) - # fold (fold all elements contained in this type) - # hasOptions (boolean: whatever this option contains an option set) - # delayOnGlobalEval (boolean: should properties go through the evaluation of this option) # docPath (path concatenated to the option name contained in the option set) isOptionType = isType "option-type"; mkOptionType = { name , check ? (x: true) , merge ? mergeDefaultOption - # Handle complex structure types. - , iter ? (f: path: v: f path v) - , fold ? (op: nul: v: op v nul) + , merge' ? args: merge , docPath ? lib.id - # If the type can contains option sets. - , hasOptions ? false - , delayOnGlobalEval ? false }: { _type = "option-type"; - inherit name check merge iter fold docPath hasOptions delayOnGlobalEval; + inherit name check merge merge' docPath; }; types = rec { + unspecified = mkOptionType { + name = "unspecified"; + }; + bool = mkOptionType { name = "boolean"; - check = lib.traceValIfNot builtins.isBool; + check = builtins.isBool; merge = fold lib.or false; }; int = mkOptionType { name = "integer"; - check = lib.traceValIfNot builtins.isInt; + check = builtins.isInt; }; string = mkOptionType { name = "string"; - check = lib.traceValIfNot builtins.isString; + check = builtins.isString; merge = lib.concatStrings; }; @@ -69,7 +65,7 @@ rec { # configuration file contents. lines = mkOptionType { name = "string"; - check = lib.traceValIfNot builtins.isString; + check = builtins.isString; merge = lib.concatStringsSep "\n"; }; @@ -81,48 +77,37 @@ rec { attrs = mkOptionType { name = "attribute set"; - check = lib.traceValIfNot isAttrs; + check = isAttrs; merge = fold lib.mergeAttrs {}; }; # derivation is a reserved keyword. package = mkOptionType { name = "derivation"; - check = lib.traceValIfNot isDerivation; + check = isDerivation; }; path = mkOptionType { name = "path"; # Hacky: there is no ‘isPath’ primop. - check = lib.traceValIfNot (x: builtins.unsafeDiscardStringContext (builtins.substring 0 1 (toString x)) == "/"); + check = x: builtins.unsafeDiscardStringContext (builtins.substring 0 1 (toString x)) == "/"; }; # drop this in the future: - list = builtins.trace "types.list is deprecated, use types.listOf instead" types.listOf; + list = builtins.trace "types.list is deprecated; use types.listOf instead" types.listOf; - listOf = elemType: mkOptionType { + listOf = elemType: mkOptionType { name = "list of ${elemType.name}s"; - check = value: lib.traceValIfNot isList value && all elemType.check value; - merge = concatLists; - iter = f: path: list: map (elemType.iter f (path + ".*")) list; - fold = op: nul: list: lib.fold (e: l: elemType.fold op l e) nul list; + check = value: isList value && all elemType.check value; + merge = defs: map (def: elemType.merge [def]) (concatLists defs); docPath = path: elemType.docPath (path + ".*"); - inherit (elemType) hasOptions; - - # You cannot define multiple configurations of one entity, therefore - # no reason justify to delay properties inside list elements. - delayOnGlobalEval = false; }; attrsOf = elemType: mkOptionType { name = "attribute set of ${elemType.name}s"; - check = x: lib.traceValIfNot isAttrs x - && all elemType.check (lib.attrValues x); - merge = lib.zipAttrsWith (name: elemType.merge); - iter = f: path: set: lib.mapAttrs (name: elemType.iter f (path + "." + name)) set; - fold = op: nul: set: fold (e: l: elemType.fold op l e) nul (lib.attrValues set); + check = x: isAttrs x && all elemType.check (lib.attrValues x); + merge = lib.zipAttrsWith (name: elemType.merge' { inherit name; }); docPath = path: elemType.docPath (path + "."); - inherit (elemType) hasOptions delayOnGlobalEval; }; # List or attribute set of ... @@ -143,26 +128,13 @@ rec { check = x: if isList x then listOnly.check x else if isAttrs x then attrOnly.check x - else lib.traceValIfNot (x: false) x; - ## The merge function returns an attribute set - merge = defs: - attrOnly.merge (imap convertIfList defs); - iter = f: path: def: - if isList def then listOnly.iter f path def - else if isAttrs def then attrOnly.iter f path def - else throw "Unexpected value"; - fold = op: nul: def: - if isList def then listOnly.fold op nul def - else if isAttrs def then attrOnly.fold op nul def - else throw "Unexpected value"; - + else false; + merge = defs: attrOnly.merge (imap convertIfList defs); docPath = path: elemType.docPath (path + "."); - inherit (elemType) hasOptions delayOnGlobalEval; - } - ; + }; uniq = elemType: mkOptionType { - inherit (elemType) name check iter fold docPath hasOptions; + inherit (elemType) name check docPath; merge = list: if length list == 1 then head list @@ -171,54 +143,46 @@ rec { }; none = elemType: mkOptionType { - inherit (elemType) name check iter fold docPath hasOptions; + inherit (elemType) name check docPath; merge = list: throw "No definitions are allowed for this option."; }; nullOr = elemType: mkOptionType { - inherit (elemType) name merge docPath hasOptions; + inherit (elemType) docPath; + name = "null or ${elemType.name}"; check = x: builtins.isNull x || elemType.check x; - iter = f: path: v: if v == null then v else elemType.iter f path v; - fold = op: nul: v: if v == null then nul else elemType.fold op nul v; + merge = defs: + if all isNull defs then null + else if any isNull defs then + throw "Some but not all values are null." + else elemType.merge defs; }; functionTo = elemType: mkOptionType { name = "function that evaluates to a(n) ${elemType.name}"; - check = lib.traceValIfNot builtins.isFunction; + check = builtins.isFunction; merge = fns: args: elemType.merge (map (fn: fn args) fns); - # These are guesses, I don't fully understand iter, fold, delayOnGlobalEval - iter = f: path: v: - args: elemType.iter f path (v args); - fold = op: nul: v: - args: elemType.fold op nul (v args); - inherit (elemType) delayOnGlobalEval; - hasOptions = false; }; - # usually used with listOf, attrsOf, loaOf like this: - # users = mkOption { - # type = loaOf optionSet; - # - # # you can omit the list if there is one element only - # options = [ { - # name = mkOption { - # description = "name of the user" - # ... - # }; - # # more options here - # } { more options } ]; - # } - # TODO: !!! document passing options as an argument to optionSet, - # deprecate the current approach. - optionSet = mkOptionType { - name = "option set"; - # merge is done in "options.nix > addOptionMakeUp > handleOptionSets" - merge = lib.id; + submodule = opts: mkOptionType rec { + name = "submodule"; check = x: isAttrs x || builtins.isFunction x; - hasOptions = true; - delayOnGlobalEval = true; + # FIXME: make error messages include the parent attrpath. + merge = merge' {}; + merge' = args: defs: + let + coerce = def: if builtins.isFunction def then def else { config = def; }; + modules = (toList opts) ++ map coerce defs; + in (evalModules modules args).config; + }; + + # Obsolete alternative to configOf. It takes its option + # declarations from the ‘options’ attribute of containing option + # declaration. + optionSet = mkOptionType { + name = /* builtins.trace "types.optionSet is deprecated; use types.submodule instead" */ "option set"; }; }; diff --git a/nixos/lib/eval-config.nix b/nixos/lib/eval-config.nix index cd543c958ff..119bba78ff1 100644 --- a/nixos/lib/eval-config.nix +++ b/nixos/lib/eval-config.nix @@ -19,11 +19,9 @@ rec { # Merge the option definitions in all modules, forming the full # system configuration. It's not checked for undeclared options. systemModule = - pkgs.lib.fixMergeModules configComponents extraArgs; + pkgs.lib.evalModules configComponents extraArgs; - optionDefinitions = systemModule.config; - optionDeclarations = systemModule.options; - inherit (systemModule) options; + config = systemModule.config; # These are the extra arguments passed to every module. In # particular, Nixpkgs is passed through the "pkgs" argument. @@ -56,16 +54,11 @@ rec { # define nixpkgs.config, so it's pointless to evaluate them. baseModules = [ ../modules/misc/nixpkgs.nix ]; pkgs = import ./nixpkgs.nix { system = system_; config = {}; }; - }).optionDefinitions.nixpkgs; + }).config.nixpkgs; in { inherit system; inherit (nixpkgsOptions) config; }); - # Optionally check wether all config values have corresponding - # option declarations. - config = - assert optionDefinitions.environment.checkConfigurationOptions -> pkgs.lib.checkModule "" systemModule; - systemModule.config; } diff --git a/nixos/modules/config/shells-environment.nix b/nixos/modules/config/shells-environment.nix index 4f7447f435b..5b8b6bc600c 100644 --- a/nixos/modules/config/shells-environment.nix +++ b/nixos/modules/config/shells-environment.nix @@ -26,11 +26,10 @@ in type = types.attrsOf (mkOptionType { name = "a string or a list of strings"; merge = xs: - let xs' = evalProperties xs; in - if isList (head xs') then concatLists xs' - else if builtins.lessThan 1 (length xs') then abort "variable in ‘environment.variables’ has multiple values" - else if !builtins.isString (head xs') then abort "variable in ‘environment.variables’ does not have a string value" - else head xs'; + if isList (head xs) then concatLists xs + else if builtins.lessThan 1 (length xs) then abort "variable in ‘environment.variables’ has multiple values" + else if !builtins.isString (head xs) then abort "variable in ‘environment.variables’ does not have a string value" + else head xs; }); apply = mapAttrs (n: v: if isList v then concatStringsSep ":" v else v); }; diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix index ff3c844030b..b502484a520 100644 --- a/nixos/modules/system/activation/activation-script.nix +++ b/nixos/modules/system/activation/activation-script.nix @@ -52,7 +52,7 @@ in idempotent and fast. ''; - merge = mergeTypedOption "script" builtins.isAttrs (fold mergeAttrs {}); + type = types.attrsOf types.unspecified; # FIXME apply = set: { script =