Merge pull request #6794 from nbp/module-system-tweaks

Module system improvements
This commit is contained in:
Nicolas B. Pierron 2015-04-03 14:22:33 -07:00
commit 889f72bad4
20 changed files with 280 additions and 123 deletions

View File

@ -9,25 +9,69 @@ 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 }:
and config: the nested set of all option values.
!!! Please think twice before adding to this argument list! The more
that is specified here instead of in the modules themselves the harder
it is to transparently move a set of modules to be a submodule of another
config (as the proper arguments need to be replicated at each call to
evalModules) and the less declarative the module set is. */
evalModules = { modules
, prefix ? []
, # This would be remove in the future, Prefer _module.args option instead.
args ? {}
, # This would be remove in the future, Prefer _module.check option instead.
check ? true
}:
let
args' = args // { lib = import ./.; } // result;
closed = closeModules modules args';
# This internal module declare internal options under the `_module'
# attribute. These options are fragile, as they are used by the
# module system to change the interpretation of modules.
internalModule = rec {
_file = ./modules.nix;
key = _file;
options = {
_module.args = mkOption {
type = types.attrsOf types.unspecified;
internal = true;
description = "Arguments passed to each module.";
};
_module.check = mkOption {
type = types.uniq types.bool;
internal = true;
default = check;
description = "Whether to check whether all option definitions have matching declarations.";
};
};
config = {
_module.args = args;
};
};
closed = closeModules (modules ++ [ internalModule ]) { inherit config options; lib = import ./.; };
# 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.
# !!! _module.check's value can't depend on any other config values
# without an infinite recursion. One way around this is to make the
# 'config' passed around to the modules be unconditionally unchecked,
# and only do the check in 'result'.
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
if options._module.check.value && 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.")
@ -43,7 +87,7 @@ rec {
let
toClosureList = file: parentKey: imap (n: x:
if isAttrs x || isFunction x then
unifyModuleSyntax file "${parentKey}:anon-${toString n}" (applyIfFunction x args)
unifyModuleSyntax file "${parentKey}:anon-${toString n}" (unpackSubmodule applyIfFunction x args)
else
unifyModuleSyntax (toString x) (toString x) (applyIfFunction (import x) args));
in
@ -74,7 +118,39 @@ rec {
config = removeAttrs m ["key" "_file" "require" "imports"];
};
applyIfFunction = f: arg: if isFunction f then f arg else f;
applyIfFunction = f: arg@{ config, options, lib }: if isFunction f then
let
# Module arguments are resolved in a strict manner when attribute set
# deconstruction is used. As the arguments are now defined with the
# config._module.args option, the strictness used on the attribute
# set argument would cause an infinite loop, if the result of the
# option is given as argument.
#
# To work-around the strictness issue on the deconstruction of the
# attributes set argument, we create a new attribute set which is
# constructed to satisfy the expected set of attributes. Thus calling
# a module will resolve strictly the attributes used as argument but
# not their values. The values are forwarding the result of the
# evaluation of the option.
requiredArgs = builtins.attrNames (builtins.functionArgs f);
extraArgs = builtins.listToAttrs (map (name: {
inherit name;
value = config._module.args.${name};
}) requiredArgs);
in f (extraArgs // arg)
else
f;
/* We have to pack and unpack submodules. We cannot wrap the expected
result of the function as we would no longer be able to list the arguments
of the submodule. (see applyIfFunction) */
unpackSubmodule = unpack: m: args:
if isType "submodule" m then
{ _file = m.file; } // (unpack m.submodule args)
else unpack m args;
packSubmodule = file: m:
{ _type = "submodule"; file = file; submodule = m; };
/* Merge a list of modules. This will recurse over the option
declarations in all modules, combining them into a single set.
@ -106,12 +182,9 @@ rec {
else []
) configs);
nrOptions = count (m: isOption m.options) decls;
# Process mkMerge and mkIf properties.
defns' = concatMap (m:
if m.config ? ${name}
then map (m': { inherit (m) file; value = m'; }) (dischargeProperties m.config.${name})
else []
) configs;
# 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);
@ -156,15 +229,12 @@ rec {
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; };
if isFunction opt then packSubmodule file opt
else packSubmodule file { options = opt; };
getSubModules = opt.options.type.getSubModules or null;
submodules =
if getSubModules != null then map addModuleFile getSubModules ++ res.options
if getSubModules != null then map (packSubmodule opt.file) getSubModules ++ res.options
else if opt.options ? options then map (coerceOption opt.file) options' ++ res.options
else res.options;
in opt.options // res //
@ -177,27 +247,17 @@ rec {
config value. */
evalOptionValue = loc: opt: defs:
let
# Process mkOverride properties, adding in the default
# value specified in the option declaration (if any).
defsFinal' = filterOverrides
((if opt ? default then [{ file = head opt.declarations; value = mkOptionDefault opt.default; }] else []) ++ defs);
# Sort mkOrder properties.
defsFinal =
# Avoid sorting if we don't have to.
if any (def: def.value._type or "" == "order") defsFinal'
then sortProperties defsFinal'
else defsFinal';
# 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') isDefined defsFinal mergedValue;
files = map (def: def.file) defsFinal;
# 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
fold (def: res:
if opt.type.check def.value then res
else throw "The option value `${showOption loc}' in `${def.file}' is not a ${opt.type.name}.")
(opt.type.merge loc defsFinal) defsFinal;
if isDefined then mergedValue
else throw "The option `${showOption loc}' is used but not defined.";
# Finally, apply the apply function to the merged
# value. This allows options to yield a value computed
# from the definitions.
@ -205,10 +265,42 @@ rec {
in opt //
{ value = addErrorContext "while evaluating the option `${showOption loc}':" value;
definitions = map (def: def.value) defsFinal;
isDefined = defsFinal != [];
inherit files;
inherit isDefined files;
};
# Merge definitions of a value of a given type
mergeDefinitions = loc: type: defs: rec {
defsFinal =
let
# Process mkMerge and mkIf properties
processIfAndMerge = defs: concatMap (m:
map (value: { inherit (m) file; inherit value; }) (dischargeProperties m.value)
) defs;
# Process mkOverride properties
processOverride = defs: filterOverrides defs;
# Sort mkOrder properties
processOrder = defs:
# Avoid sorting if we don't have to.
if any (def: def.value._type or "" == "order") defs
then sortProperties defs
else defs;
in
processOrder (processOverride (processIfAndMerge defs));
# 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;
isDefined = defsFinal != [];
optionalValue =
if isDefined then { value = mergedValue; }
else {};
};
/* 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

View File

@ -57,13 +57,17 @@ checkConfigError() {
fi
}
# Check boolean option.
checkConfigOutput "false" config.enable ./declare-enable.nix
checkConfigError 'The option .* defined in .* does not exist.' config.enable ./define-enable.nix
# Check mkForce without submodules.
set -- config.enable ./declare-enable.nix ./define-enable.nix
checkConfigOutput "true" "$@"
checkConfigOutput "false" "$@" ./define-force-enable.nix
checkConfigOutput "false" "$@" ./define-enable-force.nix
# Check mkForce with option and submodules.
checkConfigError 'attribute .*foo.* .* not found' config.loaOfSub.foo.enable ./declare-loaOfSub-any-enable.nix
checkConfigOutput 'false' config.loaOfSub.foo.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo.nix
set -- config.loaOfSub.foo.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo-enable.nix
@ -73,6 +77,7 @@ checkConfigOutput 'false' "$@" ./define-loaOfSub-force-foo-enable.nix
checkConfigOutput 'false' "$@" ./define-loaOfSub-foo-force-enable.nix
checkConfigOutput 'false' "$@" ./define-loaOfSub-foo-enable-force.nix
# Check overriding effect of mkForce on submodule definitions.
checkConfigError 'attribute .*bar.* .* not found' config.loaOfSub.bar.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo.nix
checkConfigOutput 'false' config.loaOfSub.bar.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo.nix ./define-loaOfSub-bar.nix
set -- config.loaOfSub.bar.enable ./declare-loaOfSub-any-enable.nix ./define-loaOfSub-foo.nix ./define-loaOfSub-bar-enable.nix
@ -82,6 +87,26 @@ checkConfigError 'attribute .*bar.* .* not found' "$@" ./define-loaOfSub-force-f
checkConfigOutput 'true' "$@" ./define-loaOfSub-foo-force-enable.nix
checkConfigOutput 'true' "$@" ./define-loaOfSub-foo-enable-force.nix
# Check mkIf with submodules.
checkConfigError 'attribute .*foo.* .* not found' config.loaOfSub.foo.enable ./declare-enable.nix ./declare-loaOfSub-any-enable.nix
set -- config.loaOfSub.foo.enable ./declare-enable.nix ./declare-loaOfSub-any-enable.nix
checkConfigError 'attribute .*foo.* .* not found' "$@" ./define-if-loaOfSub-foo-enable.nix
checkConfigError 'attribute .*foo.* .* not found' "$@" ./define-loaOfSub-if-foo-enable.nix
checkConfigError 'attribute .*foo.* .* not found' "$@" ./define-loaOfSub-foo-if-enable.nix
checkConfigOutput 'false' "$@" ./define-loaOfSub-foo-enable-if.nix
checkConfigOutput 'true' "$@" ./define-enable.nix ./define-if-loaOfSub-foo-enable.nix
checkConfigOutput 'true' "$@" ./define-enable.nix ./define-loaOfSub-if-foo-enable.nix
checkConfigOutput 'true' "$@" ./define-enable.nix ./define-loaOfSub-foo-if-enable.nix
checkConfigOutput 'true' "$@" ./define-enable.nix ./define-loaOfSub-foo-enable-if.nix
# Check _module.args.
checkConfigOutput "true" config.enable ./declare-enable.nix ./custom-arg-define-enable.nix
# Check _module.check.
set -- config.enable ./declare-enable.nix ./define-enable.nix ./define-loaOfSub-foo.nix
checkConfigError 'The option .* defined in .* does not exist.' "$@"
checkConfigOutput "true" "$@" ./define-module-check.nix
cat <<EOF
====== module tests ======
$pass Pass

View File

@ -0,0 +1,8 @@
{ lib, custom, ... }:
{
config = {
_module.args.custom = true;
enable = custom;
};
}

View File

@ -0,0 +1,5 @@
{ config, lib, ... }:
lib.mkIf config.enable {
loaOfSub.foo.enable = true;
}

View File

@ -0,0 +1,5 @@
{ config, lib, ... }:
{
loaOfSub.foo.enable = lib.mkIf config.enable true;
}

View File

@ -0,0 +1,7 @@
{ config, lib, ... }:
{
loaOfSub.foo = lib.mkIf config.enable {
enable = true;
};
}

View File

@ -0,0 +1,7 @@
{ config, lib, ... }:
{
loaOfSub = lib.mkIf config.enable {
foo.enable = true;
};
}

View File

@ -0,0 +1,3 @@
{
_module.check = false;
}

View File

@ -6,6 +6,7 @@ with import ./attrsets.nix;
with import ./options.nix;
with import ./trivial.nix;
with import ./strings.nix;
with {inherit (import ./modules.nix) mergeDefinitions; };
rec {
@ -109,11 +110,15 @@ rec {
listOf = elemType: mkOptionType {
name = "list of ${elemType.name}s";
check = value: isList value && all elemType.check value;
check = isList;
merge = loc: defs:
concatLists (imap (n: def: imap (m: def':
elemType.merge (loc ++ ["[${toString n}-${toString m}]"])
[{ inherit (def) file; value = def'; }]) def.value) defs);
map (x: x.value) (filter (x: x ? value) (concatLists (imap (n: def: imap (m: def':
(mergeDefinitions
(loc ++ ["[definition ${toString n}-entry ${toString m}]"])
elemType
[{ inherit (def) file; value = def'; }]
).optionalValue
) def.value) defs)));
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
getSubModules = elemType.getSubModules;
substSubModules = m: listOf (elemType.substSubModules m);
@ -121,12 +126,14 @@ rec {
attrsOf = elemType: mkOptionType {
name = "attribute set of ${elemType.name}s";
check = x: isAttrs x && all elemType.check (attrValues x);
check = isAttrs;
merge = loc: defs:
zipAttrsWith (name: elemType.merge (loc ++ [name]))
mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
(mergeDefinitions (loc ++ [name]) elemType defs).optionalValue
)
# Push down position info.
(map (def: listToAttrs (mapAttrsToList (n: def':
{ name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs);
{ name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs)));
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
getSubModules = elemType.getSubModules;
substSubModules = m: attrsOf (elemType.substSubModules m);
@ -150,10 +157,7 @@ rec {
attrOnly = attrsOf elemType;
in mkOptionType {
name = "list or attribute set of ${elemType.name}s";
check = x:
if isList x then listOnly.check x
else if isAttrs x then attrOnly.check x
else false;
check = x: isList x || isAttrs x;
merge = loc: defs: attrOnly.merge loc (imap convertIfList defs);
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]);
getSubModules = elemType.getSubModules;
@ -194,7 +198,11 @@ rec {
let
coerce = def: if isFunction def then def else { config = def; };
modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs;
in (evalModules { inherit modules; args.name = last loc; prefix = loc; }).config;
in (evalModules {
inherit modules;
args.name = last loc;
prefix = loc;
}).config;
getSubOptions = prefix: (evalModules
{ modules = opts'; inherit prefix;
# FIXME: hack to get shit to evaluate.

View File

@ -2,27 +2,51 @@
# configuration object (`config') from which we can retrieve option
# values.
{ system ? builtins.currentSystem
, pkgs ? null
, baseModules ? import ../modules/module-list.nix
, extraArgs ? {}
# !!! Please think twice before adding to this argument list!
# Ideally eval-config.nix would be an extremely thin wrapper
# around lib.evalModules, so that modular systems that have nixos configs
# as subcomponents (e.g. the container feature, or nixops if network
# expressions are ever made modular at the top level) can just use
# types.submodule instead of using eval-config.nix
{ # !!! system can be set modularly, would be nice to remove
system ? builtins.currentSystem
, # !!! is this argument needed any more? The pkgs argument can
# be set modularly anyway.
pkgs ? null
, # !!! what do we gain by making this configurable?
baseModules ? import ../modules/module-list.nix
, # !!! See comment about args in lib/modules.nix
extraArgs ? {}
, modules
, check ? true
, # !!! See comment about check in lib/modules.nix
check ? true
, prefix ? []
, lib ? import ../../lib
}:
let extraArgs_ = extraArgs; pkgs_ = pkgs; system_ = system;
extraModules = let e = builtins.getEnv "NIXOS_EXTRA_MODULE_PATH";
in if e == "" then [] else [(import (builtins.toPath e))];
in
let
pkgsModule = rec {
_file = ./eval-config.nix;
key = _file;
config = {
nixpkgs.system = lib.mkDefault system_;
_module.args.pkgs = lib.mkIf (pkgs_ != null) (lib.mkForce pkgs_);
};
};
in rec {
# Merge the option definitions in all modules, forming the full
# system configuration.
inherit (pkgs.lib.evalModules {
inherit prefix;
modules = modules ++ extraModules ++ baseModules;
inherit (lib.evalModules {
inherit prefix check;
modules = modules ++ extraModules ++ baseModules ++ [ pkgsModule ];
args = extraArgs;
check = check && options.environment.checkConfigurationOptions.value;
}) config options;
# These are the extra arguments passed to every module. In
@ -33,40 +57,8 @@ in rec {
# the 64-bit package anyway. However, it would be cleaner to respect
# nixpkgs.config here.
extraArgs = extraArgs_ // {
inherit pkgs modules baseModules;
modulesPath = ../modules;
pkgs_i686 = import ./nixpkgs.nix { system = "i686-linux"; config.allowUnfree = true; };
utils = import ./utils.nix pkgs;
inherit modules baseModules;
};
# Import Nixpkgs, allowing the NixOS option nixpkgs.config to
# specify the Nixpkgs configuration (e.g., to set package options
# such as firefox.enableGeckoMediaPlayer, or to apply global
# overrides such as changing GCC throughout the system), and the
# option nixpkgs.system to override the platform type. This is
# tricky, because we have to prevent an infinite recursion: "pkgs"
# is passed as an argument to NixOS modules, but the value of "pkgs"
# depends on config.nixpkgs.config, which we get from the modules.
# So we call ourselves here with "pkgs" explicitly set to an
# instance that doesn't depend on nixpkgs.config.
pkgs =
if pkgs_ != null
then pkgs_
else import ./nixpkgs.nix (
let
system = if nixpkgsOptions.system != "" then nixpkgsOptions.system else system_;
nixpkgsOptions = (import ./eval-config.nix {
inherit system extraArgs modules prefix;
# For efficiency, leave out most NixOS modules; they don't
# define nixpkgs.config, so it's pointless to evaluate them.
baseModules = [ ../modules/misc/nixpkgs.nix ../modules/config/no-x-libs.nix ];
pkgs = import ./nixpkgs.nix { system = system_; config = {}; };
check = false;
}).config.nixpkgs;
in
{
inherit system;
inherit (nixpkgsOptions) config;
});
inherit (config._module.args) pkgs;
}

View File

@ -1,6 +1,6 @@
{ config, pkgs, ... }:
{ config, pkgs, lib, ... }:
with pkgs.lib;
with lib;
let fcBool = x: if x then "<bool>true</bool>" else "<bool>false</bool>";
in

View File

@ -1,15 +0,0 @@
{ lib, ... }:
with lib;
{
options = {
environment.checkConfigurationOptions = mkOption {
type = types.bool;
default = true;
description = ''
Whether to check the validity of the entire configuration.
'';
};
};
}

View File

@ -0,0 +1,14 @@
{ lib, pkgs, config, ... }:
{
_module.args = {
modulesPath = ../.;
pkgs_i686 = import ../../lib/nixpkgs.nix {
system = "i686-linux";
config.allowUnfree = true;
};
utils = import ../../lib/utils.nix pkgs;
};
}

View File

@ -60,6 +60,7 @@ in
nixpkgs.system = mkOption {
type = types.str;
default = builtins.currentSystem;
description = ''
Specifies the Nix platform type for which NixOS should be built.
If unset, it defaults to the platform type of your host system.
@ -71,6 +72,10 @@ in
};
config = {
nixpkgs.system = mkDefault pkgs.stdenv.system;
_module.args.pkgs = import ../../lib/nixpkgs.nix {
system = config.nixpkgs.system;
inherit (config.nixpkgs) config;
};
};
}

View File

@ -44,8 +44,8 @@
./installer/tools/nixos-checkout.nix
./installer/tools/tools.nix
./misc/assertions.nix
./misc/check-config.nix
./misc/crashdump.nix
./misc/extra-arguments.nix
./misc/ids.nix
./misc/lib.nix
./misc/locate.nix

View File

@ -135,6 +135,8 @@ in zipModules ([]
++ obsolete [ "services" "mysql55" ] [ "services" "mysql" ]
++ obsolete [ "environment" "checkConfigurationOptions" ] [ "_module" "check" ]
# XBMC
++ obsolete [ "services" "xserver" "windowManager" "xbmc" ] [ "services" "xserver" "desktopManager" "kodi" ]
++ obsolete [ "services" "xserver" "desktopManager" "xbmc" ] [ "services" "xserver" "desktopManager" "kodi" ]

View File

@ -6,8 +6,9 @@
with lib;
let
parentConfig = config;
pamOpts = args: {
pamOpts = { config, name, ... }: let cfg = config; in let config = parentConfig; in {
options = {
@ -180,8 +181,8 @@ let
};
config = let cfg = args.config; in {
name = mkDefault args.name;
config = {
name = mkDefault name;
setLoginUid = mkDefault cfg.startSession;
limits = mkDefault config.security.pam.loginLimits;

View File

@ -3,7 +3,7 @@
# of the virtual consoles. The latter is useful for the installation
# CD.
{ config, lib, pkgs, baseModules, ... } @ extraArgs:
{ config, lib, pkgs, baseModules, ... }:
with lib;
@ -18,7 +18,7 @@ let
eval = evalModules {
modules = [ versionModule ] ++ baseModules;
args = (removeAttrs extraArgs ["config" "options"]) // { modules = [ ]; };
args = (config._module.args) // { modules = [ ]; };
};
manual = import ../../../doc/manual {

View File

@ -1,7 +1,7 @@
{ config, lib, pkgs, utils, ... }:
with lib;
with utils;
with lib;
with import ./systemd-unit-options.nix { inherit config lib; };
let

View File

@ -1,5 +1,3 @@
{ config, pkgs, modulesPath, ... }:
{
imports = [ "${modulesPath}/virtualisation/amazon-image.nix" ];
imports = [ ./amazon-image.nix ];
}