9255c48a06
This makes the relationship between property types clearer, and more importantly will let option types parameterized by other option types reuse the code for delegated type checking and merging.
403 lines
16 KiB
Nix
403 lines
16 KiB
Nix
with import ./lists.nix;
|
||
with import ./trivial.nix;
|
||
with import ./attrsets.nix;
|
||
with import ./options.nix;
|
||
with import ./debug.nix;
|
||
with import ./types.nix;
|
||
|
||
rec {
|
||
|
||
/* 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, prefix ? [], args ? {}, check ? true }:
|
||
let
|
||
args' = args // { lib = import ./.; } // 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 prefix (reverseList closed);
|
||
# Traverse options and extract the option values into the final
|
||
# config set. At the same time, check whether all option
|
||
# definitions have matching declarations.
|
||
config = yieldConfig prefix options;
|
||
yieldConfig = prefix: set:
|
||
let res = removeAttrs (mapAttrs (n: v:
|
||
if isOption v then v.value
|
||
else yieldConfig (prefix ++ [n]) v) set) ["_definedNames"];
|
||
in
|
||
if check && set ? _definedNames then
|
||
fold (m: res:
|
||
fold (name: res:
|
||
if set ? ${name} then res else throw "The option `${showOption (prefix ++ [name])}' defined in `${m.file}' does not exist.")
|
||
res m.names)
|
||
res set._definedNames
|
||
else
|
||
res;
|
||
result = { inherit options config; };
|
||
in result;
|
||
|
||
/* Close a set of modules under the ‘imports’ relation. */
|
||
closeModules = modules: args:
|
||
let
|
||
toClosureList = file: parentKey: imap (n: x:
|
||
if isAttrs x || isFunction x then
|
||
unifyModuleSyntax file "${parentKey}:anon-${toString n}" (applyIfFunction x args)
|
||
else
|
||
unifyModuleSyntax (toString x) (toString x) (applyIfFunction (import x) args));
|
||
in
|
||
builtins.genericClosure {
|
||
startSet = toClosureList unknownModule "" modules;
|
||
operator = m: toClosureList m.file m.key m.imports;
|
||
};
|
||
|
||
/* Massage a module into canonical form, that is, a set consisting
|
||
of ‘options’, ‘config’ and ‘imports’ attributes. */
|
||
unifyModuleSyntax = file: key: m:
|
||
if m ? config || m ? options then
|
||
let badAttrs = removeAttrs m ["imports" "options" "config" "key" "_file"]; in
|
||
if badAttrs != {} then
|
||
throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by assignments to the top-level attributes `config' or `options'."
|
||
else
|
||
{ file = m._file or file;
|
||
key = toString m.key or key;
|
||
imports = m.imports or [];
|
||
options = m.options or {};
|
||
config = m.config or {};
|
||
}
|
||
else
|
||
{ file = m._file or file;
|
||
key = toString m.key or key;
|
||
imports = m.require or [] ++ m.imports or [];
|
||
options = {};
|
||
config = removeAttrs m ["key" "_file" "require" "imports"];
|
||
};
|
||
|
||
applyIfFunction = f: arg: if 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 = prefix: modules:
|
||
mergeModules' prefix modules
|
||
(concatMap (m: map (config: { inherit (m) file; inherit config; }) (pushDownProperties m.config)) modules);
|
||
|
||
mergeModules' = prefix: options: configs:
|
||
listToAttrs (map (name: {
|
||
# We're descending into attribute ‘name’.
|
||
inherit name;
|
||
value =
|
||
let
|
||
loc = prefix ++ [name];
|
||
# Get all submodules that declare ‘name’.
|
||
decls = concatLists (map (m:
|
||
if m.options ? ${name}
|
||
then [ { inherit (m) file; options = m.options.${name}; } ]
|
||
else []
|
||
) options);
|
||
# Get all submodules that define ‘name’.
|
||
defns = concatLists (map (m:
|
||
if m.config ? ${name}
|
||
then map (config: { inherit (m) file; inherit config; })
|
||
(pushDownProperties m.config.${name})
|
||
else []
|
||
) configs);
|
||
nrOptions = count (m: isOption m.options) decls;
|
||
# Extract the definitions for this loc
|
||
defns' = map (m: { inherit (m) file; value = m.config.${name}; })
|
||
(filter (m: m.config ? ${name}) configs);
|
||
in
|
||
if nrOptions == length decls then
|
||
let opt = fixupOptionType loc (mergeOptionDecls loc decls);
|
||
in evalOptionValue loc opt defns'
|
||
else if nrOptions != 0 then
|
||
let
|
||
firstOption = findFirst (m: isOption m.options) "" decls;
|
||
firstNonOption = findFirst (m: !isOption m.options) "" decls;
|
||
in
|
||
throw "The option `${showOption loc}' in `${firstOption.file}' is a prefix of options in `${firstNonOption.file}'."
|
||
else
|
||
mergeModules' loc decls defns;
|
||
}) (concatMap (m: attrNames m.options) options))
|
||
// { _definedNames = map (m: { inherit (m) file; names = attrNames m.config; }) configs; };
|
||
|
||
/* 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’).
|
||
|
||
'loc' is the list of attribute names where the option is located.
|
||
|
||
'opts' is a list of modules. Each module has an options attribute which
|
||
correspond to the definition of 'loc' in 'opt.file'. */
|
||
mergeOptionDecls = loc: opts:
|
||
fold (opt: res:
|
||
if opt.options ? default && res ? default ||
|
||
opt.options ? example && res ? example ||
|
||
opt.options ? description && res ? description ||
|
||
opt.options ? apply && res ? apply ||
|
||
# Accept to merge options which have identical types.
|
||
opt.options ? type && res ? type && opt.options.type.name != res.type.name
|
||
then
|
||
throw "The option `${showOption loc}' in `${opt.file}' is already declared in ${showFiles res.declarations}."
|
||
else
|
||
let
|
||
/* Add the modules of the current option to the list of modules
|
||
already collected. The options attribute except either a list of
|
||
submodules or a submodule. For each submodule, we add the file of the
|
||
current option declaration as the file use for the submodule. If the
|
||
submodule defines any filename, then we ignore the enclosing option file. */
|
||
options' = toList opt.options.options;
|
||
addModuleFile = m:
|
||
if isFunction m then args: { _file = opt.file; } // (m args)
|
||
else { _file = opt.file; } // m;
|
||
coerceOption = file: opt:
|
||
if isFunction opt then args: { _file = file; } // (opt args)
|
||
else { _file = file; options = opt; };
|
||
getSubModules = opt.options.type.getSubModules or null;
|
||
submodules =
|
||
if getSubModules != null then map addModuleFile getSubModules ++ res.options
|
||
else if opt.options ? options then map (coerceOption opt.file) options' ++ res.options
|
||
else res.options;
|
||
in opt.options // res //
|
||
{ declarations = [opt.file] ++ res.declarations;
|
||
options = submodules;
|
||
}
|
||
) { inherit loc; declarations = []; options = []; } opts;
|
||
|
||
/* Merge all the definitions of an option to produce the final
|
||
config value. */
|
||
evalOptionValue = loc: opt: defs:
|
||
let
|
||
# Add in the default value for this option, if any.
|
||
defs' = (optional (opt ? default)
|
||
{ file = head opt.declarations; value = mkOptionDefault opt.default; }) ++ defs;
|
||
# Handle properties, check types, and merge everything together
|
||
inherit (mergeDefinitions loc opt.type defs') defsFinal mergedValue;
|
||
files = map (def: def.file) defsFinal;
|
||
merged =
|
||
if defsFinal == [] then
|
||
throw "The option `${showOption loc}' is used but not defined."
|
||
else
|
||
mergedValue;
|
||
# 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 //
|
||
{ value = addErrorContext "while evaluating the option `${showOption loc}':" value;
|
||
definitions = map (def: def.value) defsFinal;
|
||
isDefined = defsFinal != [];
|
||
inherit files;
|
||
};
|
||
|
||
# Merge definitions of a value of a given type
|
||
mergeDefinitions = loc: type: defs: rec {
|
||
defsFinal =
|
||
let
|
||
# Process mkMerge and mkIf properties
|
||
discharged = concatMap (m:
|
||
map (value: { inherit (m) file; inherit value; }) (dischargeProperties m.value)
|
||
) defs;
|
||
|
||
# Process mkOverride properties
|
||
overridden = filterOverrides discharged;
|
||
|
||
# Sort mkOrder properties
|
||
sorted =
|
||
# Avoid sorting if we don't have to.
|
||
if any (def: def.value._type or "" == "order") overridden
|
||
then sortProperties overridden
|
||
else overridden;
|
||
in sorted;
|
||
|
||
# Type-check the remaining definitions, and merge them
|
||
mergedValue = fold (def: res:
|
||
if type.check def.value then res
|
||
else throw "The option value `${showOption loc}' in `${def.file}' is not a ${type.name}.")
|
||
(type.merge loc defsFinal) defsFinal;
|
||
};
|
||
|
||
/* Given a config set, expand mkMerge properties, and push down the
|
||
other 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; }) ]
|
||
|
||
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 if cfg._type or "" == "override" then
|
||
map (mapAttrs (n: v: mkOverride cfg.priority v)) (pushDownProperties cfg.content)
|
||
else # FIXME: handle mkOrder?
|
||
[ 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
|
||
[ def ];
|
||
|
||
/* Given a list of config values, 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,
|
||
|
||
[ { file = "/1"; value = mkOverride 10 "a"; }
|
||
{ file = "/2"; value = mkOverride 20 "b"; }
|
||
{ file = "/3"; value = "z"; }
|
||
{ file = "/4"; value = mkOverride 10 "d"; }
|
||
]
|
||
|
||
yields
|
||
|
||
[ { file = "/1"; value = "a"; }
|
||
{ file = "/4"; value = "d"; }
|
||
]
|
||
|
||
Note that "z" has the default priority 100.
|
||
*/
|
||
filterOverrides = defs:
|
||
let
|
||
defaultPrio = 100;
|
||
getPrio = def: if def.value._type or "" == "override" then def.value.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.value._type or "" == "override" then def // { value = def.value.content; } else def;
|
||
in concatMap (def: if getPrio def == highestPrio then [(strip def)] else []) defs;
|
||
|
||
/* Sort a list of properties. The sort priority of a property is
|
||
1000 by default, but can be overriden by wrapping the property
|
||
using mkOrder. */
|
||
sortProperties = defs:
|
||
let
|
||
strip = def:
|
||
if def.value._type or "" == "order"
|
||
then def // { value = def.value.content; inherit (def.value) priority; }
|
||
else def;
|
||
defs' = map strip defs;
|
||
compare = a: b: (a.priority or 1000) < (b.priority or 1000);
|
||
in sort compare defs';
|
||
|
||
/* Hack for backward compatibility: convert options of type
|
||
optionSet to options of type submodule. FIXME: remove
|
||
eventually. */
|
||
fixupOptionType = loc: opt:
|
||
let
|
||
options = opt.options or
|
||
(throw "Option `${showOption loc'}' has type optionSet but has no option attribute, in ${showFiles opt.declarations}.");
|
||
f = tp:
|
||
if tp.name == "option set" || tp.name == "submodule" then
|
||
throw "The option ${showOption loc} uses submodules without a wrapping type, in ${showFiles opt.declarations}."
|
||
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
|
||
if opt.type.getSubModules or null == null
|
||
then opt // { type = f (opt.type or types.unspecified); }
|
||
else opt // { type = opt.type.substSubModules opt.options; options = []; };
|
||
|
||
|
||
/* 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; # priority of option defaults
|
||
mkDefault = mkOverride 1000; # used in config sections of non-user modules to set a default
|
||
mkForce = mkOverride 50;
|
||
mkVMOverride = mkOverride 10; # used by ‘nixos-rebuild build-vm’
|
||
|
||
mkStrict = builtins.trace "`mkStrict' is obsolete; use `mkOverride 0' instead." (mkOverride 0);
|
||
|
||
mkFixStrictness = id; # obsolete, no-op
|
||
|
||
mkOrder = priority: content:
|
||
{ _type = "order";
|
||
inherit priority content;
|
||
};
|
||
|
||
mkBefore = mkOrder 500;
|
||
mkAfter = mkOrder 1500;
|
||
|
||
# Convenient property used to transfer all definitions and their
|
||
# properties from one option to another. This property is useful for
|
||
# renaming options, and also for including properties from another module
|
||
# system, including sub-modules.
|
||
#
|
||
# { config, options, ... }:
|
||
#
|
||
# {
|
||
# # 'bar' might not always be defined in the current module-set.
|
||
# config.foo.enable = mkAliasDefinitions (options.bar.enable or {});
|
||
#
|
||
# # 'barbaz' has to be defined in the current module-set.
|
||
# config.foobar.paths = mkAliasDefinitions options.barbaz.paths;
|
||
# }
|
||
#
|
||
# Note, this is different than taking the value of the option and using it
|
||
# as a definition, as the new definition will not keep the mkOverride /
|
||
# mkDefault properties of the previous option.
|
||
#
|
||
mkAliasDefinitions = mkAliasAndWrapDefinitions id;
|
||
mkAliasAndWrapDefinitions = wrap: option:
|
||
mkMerge
|
||
(optional (isOption option && option.isDefined)
|
||
(wrap (mkMerge option.definitions)));
|
||
|
||
|
||
/* Compatibility. */
|
||
fixMergeModules = modules: args: evalModules { inherit modules args; check = false; };
|
||
|
||
}
|