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.wip/yesman
parent
f4dadc5df8
commit
0e333688ce
@ -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"]; |
||||
|
||||
in |
||||
if isModule m then |
||||
{ key = "<unknown location>"; } // m |
||||
else |
||||
{ key = "<unknown location>"; |
||||
imports = (m.imports or []) ++ getImportedPaths; |
||||
config = getConfig; |
||||
} // ( |
||||
if getImportedSets != [] then |
||||
assert length getImportedSets == 1; |
||||
{ options = head getImportedSets; } |
||||
else |
||||
{} |
||||
); |
||||
|
||||
|
||||
unifyOptionModule = {key ? "<unknown location>"}: name: index: m: (args: |
||||
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 |
||||
module = lib.applyIfFunction m args; |
||||
key_ = rec { |
||||
file = key; |
||||
option = name; |
||||
number = index; |
||||
outPath = key; |
||||
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 |
||||
builtins.genericClosure { |
||||
startSet = toClosureList modules; |
||||
operator = m: toClosureList m.imports; |
||||
}; |
||||
in if lib.isModule module then |
||||
{ key = key_; } // module |
||||
else |
||||
{ key = key_; options = module; } |
||||
); |
||||
|
||||
/* 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 |
||||
{ inherit key; |
||||
imports = m.imports or []; |
||||
options = m.options or {}; |
||||
config = m.config or {}; |
||||
} |
||||
else |
||||
{ inherit key; |
||||
imports = m.require or []; |
||||
options = {}; |
||||
config = m; |
||||
}; |
||||
|
||||
moduleClosure = initModules: args: |
||||
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 |
||||
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}:<import-${toString index}>"; |
||||
|
||||
topLevel = { |
||||
key = "<top-level>"; |
||||
# 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 |
||||
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 |
||||
else |
||||
value |
||||
) module; |
||||
is transformed into |
||||
|
||||
[ { boot = set1; } { boot = mkIf cond set2; services mkIf cond set3; } ]. |
||||
|
||||
# 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 = {}; }) ] |
||||
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 |
||||
[ module ]; |
||||
|
||||
|
||||
# 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); |
||||
|
||||
|
||||
evalDefinitions = opt: values: |
||||
if opt.type.delayOnGlobalEval or false then |
||||
map (delayPropertiesWithIter opt.type.iter opt.name) |
||||
(evalLocalProperties values) |
||||
# 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 |
||||
[ ] |
||||
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 |
||||
); |
||||
[ 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, |
||||
|
||||
modulesNames = modules: |
||||
lib.concatMap (m: [] |
||||
++ optionals (m ? options) (lib.attrNames m.options) |
||||
++ optionals (m ? config) (lib.attrNames m.config) |
||||
) modules; |
||||
[ (mkOverride 10 "a") (mkOverride 20 "b") "z" (mkOverride 10 "d") ] |
||||
|
||||
|
||||
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; |
||||
|
||||
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 "<unknown location>" |
||||
) 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: |
||||
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; |
||||
|
||||
/* 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. |
||||
|
||||
} |
||||
|
@ -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; |
||||
}; |
||||
|
||||
} |
Loading…
Reference in new issue