Show correct position info for errors in submodules

E.g.

  The unique option `fileSystems./.device' is defined multiple times, in `/etc/nixos/configuration.nix' and `/etc/nixos/foo.nix'.

This requires passing file/value tuples to the merge functions.
This commit is contained in:
Eelco Dolstra 2013-10-30 14:21:41 +01:00
parent 4680af6a93
commit 800f9c2037
6 changed files with 88 additions and 93 deletions

View file

@ -17,7 +17,7 @@ rec {
# Traverse options and extract the option values into the final # Traverse options and extract the option values into the final
# config set. At the same time, check whether all option # config set. At the same time, check whether all option
# definitions have matching declarations. # definitions have matching declarations.
config = yieldConfig [] options; config = yieldConfig prefix options;
yieldConfig = prefix: set: yieldConfig = prefix: set:
let res = removeAttrs (mapAttrs (n: v: let res = removeAttrs (mapAttrs (n: v:
if isOption v then v.value if isOption v then v.value
@ -52,22 +52,22 @@ rec {
of options, config and imports attributes. */ of options, config and imports attributes. */
unifyModuleSyntax = file: key: m: unifyModuleSyntax = file: key: m:
if m ? config || m ? options then if m ? config || m ? options then
let badAttrs = removeAttrs m ["imports" "options" "config" "key"]; in let badAttrs = removeAttrs m ["imports" "options" "config" "key" "_file"]; in
if badAttrs != {} then if badAttrs != {} then
throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'." throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'."
else else
{ inherit file; { file = m._file or file;
key = toString m.key or key; key = toString m.key or key;
imports = m.imports or []; imports = m.imports or [];
options = m.options or {}; options = m.options or {};
config = m.config or {}; config = m.config or {};
} }
else else
{ inherit file; { file = m._file or file;
key = toString m.key or key; key = toString m.key or key;
imports = m.require or [] ++ m.imports or []; imports = m.require or [] ++ m.imports or [];
options = {}; options = {};
config = removeAttrs m ["key" "require" "imports"]; config = removeAttrs m ["key" "_file" "require" "imports"];
}; };
applyIfFunction = f: arg: if builtins.isFunction f then f arg else f; applyIfFunction = f: arg: if builtins.isFunction f then f arg else f;
@ -151,7 +151,7 @@ rec {
let let
# Process mkOverride properties, adding in the default # Process mkOverride properties, adding in the default
# value specified in the option declaration (if any). # value specified in the option declaration (if any).
defsFinal = filterOverrides' defsFinal = filterOverrides
((if opt ? default then [{ file = head opt.declarations; value = mkOptionDefault opt.default; }] else []) ++ defs); ((if opt ? default then [{ file = head opt.declarations; value = mkOptionDefault opt.default; }] else []) ++ defs);
files = map (def: def.file) defsFinal; files = map (def: def.file) defsFinal;
# Type-check the remaining definitions, and merge them if # Type-check the remaining definitions, and merge them if
@ -163,7 +163,7 @@ rec {
fold (def: res: fold (def: res:
if opt.type.check def.value then 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}.") else throw "The option value `${showOption loc}' in `${def.file}' is not a ${opt.type.name}.")
(opt.type.merge { prefix = loc; inherit files; } (map (m: m.value) defsFinal)) defsFinal; (opt.type.merge loc defsFinal) defsFinal;
# Finally, apply the apply function to the merged # Finally, apply the apply function to the merged
# value. This allows options to yield a value computed # value. This allows options to yield a value computed
# from the definitions. # from the definitions.
@ -240,7 +240,7 @@ rec {
Note that "z" has the default priority 100. Note that "z" has the default priority 100.
*/ */
filterOverrides' = defs: filterOverrides = defs:
let let
defaultPrio = 100; defaultPrio = 100;
getPrio = def: if def.value._type or "" == "override" then def.value.priority else defaultPrio; getPrio = def: if def.value._type or "" == "override" then def.value.priority else defaultPrio;
@ -249,9 +249,6 @@ rec {
strip = def: if def.value._type or "" == "override" then def // { value = def.value.content; } else def; 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; in concatMap (def: if getPrio def == highestPrio then [(strip def)] else []) defs;
/* For use in options like environment.variables. */
filterOverrides = defs: map (def: def.value) (filterOverrides' (map (def: { value = def; }) defs));
/* Hack for backward compatibility: convert options of type /* Hack for backward compatibility: convert options of type
optionSet to configOf. FIXME: remove eventually. */ optionSet to configOf. FIXME: remove eventually. */
fixupOptionType = loc: opt: fixupOptionType = loc: opt:

View file

@ -30,36 +30,39 @@ rec {
type = lib.types.bool; type = lib.types.bool;
}; };
mergeDefaultOption = args: list: mergeDefaultOption = loc: defs:
let list = getValues defs; in
if length list == 1 then head list if length list == 1 then head list
else if all builtins.isFunction list then x: mergeDefaultOption args (map (f: f x) list) else if all builtins.isFunction list then x: mergeDefaultOption loc (map (f: f x) list)
else if all isList list then concatLists list else if all isList list then concatLists list
else if all isAttrs list then fold lib.mergeAttrs {} list else if all isAttrs list then fold lib.mergeAttrs {} list
else if all builtins.isBool list then fold lib.or false list else if all builtins.isBool list then fold lib.or false list
else if all builtins.isString list then lib.concatStrings list else if all builtins.isString list then lib.concatStrings list
else if all builtins.isInt list && all (x: x == head list) list then head list else if all builtins.isInt list && all (x: x == head list) list then head list
else throw "Cannot merge definitions of `${showOption args.prefix}' given in ${showFiles args.files}."; else throw "Cannot merge definitions of `${showOption loc}' given in ${showFiles (getFiles defs)}.";
/* Obsolete, will remove soon. Specify an option type or apply /* Obsolete, will remove soon. Specify an option type or apply
function instead. */ function instead. */
mergeTypedOption = typeName: predicate: merge: args: list: mergeTypedOption = typeName: predicate: merge: loc: list:
if all predicate list then merge list let list' = map (x: x.value) list; in
else throw "Expect a ${typeName}."; if all predicate list then merge list'
else throw "Expected a ${typeName}.";
mergeEnableOption = mergeTypedOption "boolean" mergeEnableOption = mergeTypedOption "boolean"
(x: true == x || false == x) (fold lib.or false); (x: true == x || false == x) (fold lib.or false);
mergeListOption = mergeTypedOption "list" isList concatLists; mergeListOption = mergeTypedOption "list" isList concatLists;
mergeStringOption = mergeTypedOption "string" mergeStringOption = mergeTypedOption "string" builtins.isString lib.concatStrings;
(x: if builtins ? isString then builtins.isString x else x + "")
lib.concatStrings;
mergeOneOption = args: list: mergeOneOption = loc: defs:
if list == [] then abort "This case should never happen." if defs == [] then abort "This case should never happen."
else if length list != 1 then else if length defs != 1 then
throw "The unique option `${showOption args.prefix}' is defined multiple times, in ${showFiles args.files}." throw "The unique option `${showOption loc}' is defined multiple times, in ${showFiles (getFiles defs)}."
else head list; else (head defs).value;
getValues = map (x: x.value);
getFiles = map (x: x.file);
# Generate documentation template from the list of option declaration like # Generate documentation template from the list of option declaration like

View file

@ -27,6 +27,11 @@ rec {
# its type-correct, false otherwise. # its type-correct, false otherwise.
check ? (x: true) check ? (x: true)
, # Merge a list of definitions together into a single value. , # Merge a list of definitions together into a single value.
# This function is called with two arguments: the location of
# the option in the configuration as a list of strings
# (e.g. ["boot" "loader "grub" "enable"]), and a list of
# definition values and locations (e.g. [ { file = "/foo.nix";
# value = 1; } { file = "/bar.nix"; value = 2 } ]).
merge ? mergeDefaultOption merge ? mergeDefaultOption
, # Return a flat list of sub-options. Used to generate , # Return a flat list of sub-options. Used to generate
# documentation. # documentation.
@ -46,12 +51,13 @@ rec {
bool = mkOptionType { bool = mkOptionType {
name = "boolean"; name = "boolean";
check = builtins.isBool; check = builtins.isBool;
merge = args: fold lib.or false; merge = loc: fold (x: lib.or x.value) false;
}; };
int = mkOptionType { int = mkOptionType {
name = "integer"; name = "integer";
check = builtins.isInt; check = builtins.isInt;
merge = mergeOneOption;
}; };
str = mkOptionType { str = mkOptionType {
@ -60,38 +66,26 @@ rec {
merge = mergeOneOption; merge = mergeOneOption;
}; };
# Merge multiple definitions by concatenating them (with the given
# separator between the values).
separatedString = sep: mkOptionType {
name = "string";
check = builtins.isString;
merge = loc: defs: lib.concatStringsSep sep (getValues defs);
};
lines = separatedString "\n";
commas = separatedString ",";
envVar = separatedString ":";
# Deprecated; should not be used because it quietly concatenates # Deprecated; should not be used because it quietly concatenates
# strings, which is usually not what you want. # strings, which is usually not what you want.
string = mkOptionType { string = separatedString "";
name = "string";
check = builtins.isString;
merge = args: lib.concatStrings;
};
# Like string, but add newlines between every value. Useful for
# configuration file contents.
lines = mkOptionType {
name = "string";
check = builtins.isString;
merge = args: lib.concatStringsSep "\n";
};
commas = mkOptionType {
name = "string";
check = builtins.isString;
merge = args: lib.concatStringsSep ",";
};
envVar = mkOptionType {
name = "environment variable";
inherit (string) check;
merge = args: lib.concatStringsSep ":";
};
attrs = mkOptionType { attrs = mkOptionType {
name = "attribute set"; name = "attribute set";
check = isAttrs; check = isAttrs;
merge = args: fold lib.mergeAttrs {}; merge = loc: fold (def: lib.mergeAttrs def.value) {};
}; };
# derivation is a reserved keyword. # derivation is a reserved keyword.
@ -114,15 +108,21 @@ rec {
listOf = elemType: mkOptionType { listOf = elemType: mkOptionType {
name = "list of ${elemType.name}s"; name = "list of ${elemType.name}s";
check = value: isList value && all elemType.check value; check = value: isList value && all elemType.check value;
merge = args: defs: imap (n: def: elemType.merge (addToPrefix args (toString n)) [def]) (concatLists defs); 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);
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
}; };
attrsOf = elemType: mkOptionType { attrsOf = elemType: mkOptionType {
name = "attribute set of ${elemType.name}s"; name = "attribute set of ${elemType.name}s";
check = x: isAttrs x && all elemType.check (lib.attrValues x); check = x: isAttrs x && all elemType.check (lib.attrValues x);
merge = args: lib.zipAttrsWith (name: merge = loc: defs:
elemType.merge (addToPrefix (args // { inherit name; }) name)); zipAttrsWith (name: elemType.merge (loc ++ [name]))
# Push down position info.
(map (def: listToAttrs (mapAttrsToList (n: def':
{ name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs);
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
}; };
@ -130,22 +130,25 @@ rec {
loaOf = elemType: loaOf = elemType:
let let
convertIfList = defIdx: def: convertIfList = defIdx: def:
if isList def then if isList def.value then
listToAttrs ( { inherit (def) file;
flip imap def (elemIdx: elem: value = listToAttrs (
{ name = "unnamed-${toString defIdx}.${toString elemIdx}"; value = elem; })) imap (elemIdx: elem:
{ name = "unnamed-${toString defIdx}.${toString elemIdx}";
value = elem;
}) def.value);
}
else else
def; def;
listOnly = listOf elemType; listOnly = listOf elemType;
attrOnly = attrsOf elemType; attrOnly = attrsOf elemType;
in mkOptionType { in mkOptionType {
name = "list or attribute set of ${elemType.name}s"; name = "list or attribute set of ${elemType.name}s";
check = x: check = x:
if isList x then listOnly.check x if isList x then listOnly.check x
else if isAttrs x then attrOnly.check x else if isAttrs x then attrOnly.check x
else false; else false;
merge = args: defs: attrOnly.merge args (imap convertIfList defs); merge = loc: defs: attrOnly.merge loc (imap convertIfList defs);
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]);
}; };
@ -155,29 +158,23 @@ rec {
getSubOptions = elemType.getSubOptions; getSubOptions = elemType.getSubOptions;
}; };
none = elemType: mkOptionType {
inherit (elemType) name check;
merge = args: list:
throw "No definitions are allowed for the option `${showOption args.prefix}'.";
getSubOptions = elemType.getSubOptions;
};
nullOr = elemType: mkOptionType { nullOr = elemType: mkOptionType {
name = "null or ${elemType.name}"; name = "null or ${elemType.name}";
check = x: builtins.isNull x || elemType.check x; check = x: builtins.isNull x || elemType.check x;
merge = args: defs: merge = loc: defs:
if all isNull defs then null let nrNulls = count (def: isNull def.value) defs; in
else if any isNull defs then if nrNulls == length defs then null
throw "The option `${showOption args.prefix}' is defined both null and not null, in ${showFiles args.files}." else if nrNulls != 0 then
else elemType.merge args defs; throw "The option `${showOption loc}' is defined both null and not null, in ${showFiles (getFiles defs)}."
else elemType.merge loc defs;
getSubOptions = elemType.getSubOptions; getSubOptions = elemType.getSubOptions;
}; };
functionTo = elemType: mkOptionType { functionTo = elemType: mkOptionType {
name = "function that evaluates to a(n) ${elemType.name}"; name = "function that evaluates to a(n) ${elemType.name}";
check = builtins.isFunction; check = builtins.isFunction;
merge = args: fns: merge = loc: defs:
fnArgs: elemType.merge args (map (fn: fn fnArgs) fns); fnArgs: elemType.merge loc (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs);
getSubOptions = elemType.getSubOptions; getSubOptions = elemType.getSubOptions;
}; };
@ -186,11 +183,11 @@ rec {
mkOptionType rec { mkOptionType rec {
name = "submodule"; name = "submodule";
check = x: isAttrs x || builtins.isFunction x; check = x: isAttrs x || builtins.isFunction x;
merge = args: defs: merge = loc: defs:
let let
coerce = def: if builtins.isFunction def then def else { config = def; }; coerce = def: if builtins.isFunction def then def else { config = def; };
modules = opts' ++ map coerce defs; modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs;
in (evalModules { inherit modules args; prefix = args.prefix; }).config; in (evalModules { inherit modules; args.name = last loc; prefix = loc; }).config;
getSubOptions = prefix: (evalModules getSubOptions = prefix: (evalModules
{ modules = opts'; inherit prefix; { modules = opts'; inherit prefix;
# FIXME: hack to get shit to evaluate. # FIXME: hack to get shit to evaluate.
@ -206,8 +203,4 @@ rec {
}; };
/* Helper function. */
addToPrefix = args: name: args // { prefix = args.prefix ++ [name]; };
} }

View file

@ -25,15 +25,17 @@ in
''; '';
type = types.attrsOf (mkOptionType { type = types.attrsOf (mkOptionType {
name = "a string or a list of strings"; name = "a string or a list of strings";
merge = args: xs: merge = loc: defs:
let xs' = filterOverrides xs; in let
if isList (head xs') then concatLists xs' defs' = filterOverrides defs;
else if builtins.lessThan 1 (length xs') then res = (head defs').value;
# Don't show location info here, since it's too general. in
throw "The option `${showOption args.prefix}' is defined multiple times." if isList res then concatLists (getValues defs')
else if !builtins.isString (head xs') then else if builtins.lessThan 1 (length defs') then
throw "The option `${showOption args.prefix}' does not have a string value." throw "The option `${showOption loc}' is defined multiple times, in ${showFiles (getFiles defs)}."
else head xs'; else if !builtins.isString res then
throw "The option `${showOption loc}' does not have a string value, in ${showFiles (getFiles defs)}."
else res;
}); });
apply = mapAttrs (n: v: if isList v then concatStringsSep ":" v else v); apply = mapAttrs (n: v: if isList v then concatStringsSep ":" v else v);
}; };

View file

@ -7,7 +7,7 @@ let
sysctlOption = mkOptionType { sysctlOption = mkOptionType {
name = "sysctl option value"; name = "sysctl option value";
check = x: builtins.isBool x || builtins.isString x || builtins.isInt x; check = x: builtins.isBool x || builtins.isString x || builtins.isInt x;
merge = args: xs: last xs; # FIXME: hacky way to allow overriding in configuration.nix. merge = args: defs: (last defs).value; # FIXME: hacky way to allow overriding in configuration.nix.
}; };
in in

View file

@ -26,7 +26,7 @@ let
configType = mkOptionType { configType = mkOptionType {
name = "nixpkgs config"; name = "nixpkgs config";
check = traceValIfNot isConfig; check = traceValIfNot isConfig;
merge = args: fold mergeConfig {}; merge = args: fold (def: mergeConfig def.value) {};
}; };
in in