Show precise error messages in option merge failures

For instance, if time.timeZone is defined multiple times, you now get
the error message:

  error: user-thrown exception: The unique option `time.timeZone' is defined multiple times, in `/etc/nixos/configurations/misc/eelco/x11vnc.nix' and `/etc/nixos/configuration.nix'.

while previously you got:

  error: user-thrown exception: Multiple definitions of string. Only one is allowed for this option.

and only an inspection of the stack trace gave a clue as to what
option caused the problem.
This commit is contained in:
Eelco Dolstra 2013-10-28 19:48:30 +01:00
parent dbefab9cf4
commit 73f32d0375
6 changed files with 58 additions and 56 deletions

View File

@ -128,7 +128,7 @@ rec {
opt.options ? apply && res ? apply || opt.options ? apply && res ? apply ||
opt.options ? type && res ? type opt.options ? type && res ? type
then then
throw "The option `${showOption loc}' in `${opt.file}' is already declared in ${concatStringsSep " and " (map (d: "`${d}'") res.declarations)}." throw "The option `${showOption loc}' in `${opt.file}' is already declared in ${showFiles res.declarations}."
else else
opt.options // res // opt.options // res //
{ declarations = [opt.file] ++ res.declarations; { declarations = [opt.file] ++ res.declarations;
@ -153,7 +153,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; } (map (m: m.value) defsFinal)) defsFinal; (opt.type.merge { prefix = loc; files = map (m: m.file) defsFinal; } (map (m: m.value) 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.

View File

@ -35,7 +35,7 @@ rec {
addDefaultOptionValues = defs: opts: opts // addDefaultOptionValues = defs: opts: opts //
builtins.listToAttrs (map (defName: builtins.listToAttrs (map (defName:
{ name = defName; { name = defName;
value = value =
let let
defValue = builtins.getAttr defName defs; defValue = builtins.getAttr defName defs;
optValue = builtins.getAttr defName opts; optValue = builtins.getAttr defName opts;
@ -49,27 +49,26 @@ rec {
else else
# `defValue' is an attribute set containing options. # `defValue' is an attribute set containing options.
# So recurse. # So recurse.
if hasAttr defName opts && isAttrs optValue if hasAttr defName opts && isAttrs optValue
then addDefaultOptionValues defValue optValue then addDefaultOptionValues defValue optValue
else addDefaultOptionValues defValue {}; else addDefaultOptionValues defValue {};
} }
) (attrNames defs)); ) (attrNames defs));
mergeDefaultOption = list: mergeDefaultOption = args: list:
if length list == 1 then head list if length list == 1 then head list
else if all builtins.isFunction list then x: mergeDefaultOption (map (f: f x) list) else if all builtins.isFunction list then x: mergeDefaultOption args (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 else if all builtins.isInt list && all (x: x == head list) list then head list
then head list else throw "Cannot merge definitions of `${showOption args.prefix}' given in ${showFiles args.files}.";
else throw "Cannot merge values.";
/* 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: list: mergeTypedOption = typeName: predicate: merge: args: list:
if all predicate list then merge list if all predicate list then merge list
else throw "Expect a ${typeName}."; else throw "Expect a ${typeName}.";
@ -82,9 +81,10 @@ rec {
(x: if builtins ? isString then builtins.isString x else x + "") (x: if builtins ? isString then builtins.isString x else x + "")
lib.concatStrings; lib.concatStrings;
mergeOneOption = list: mergeOneOption = args: list:
if list == [] then abort "This case should never happen." if list == [] then abort "This case should never happen."
else if length list != 1 then throw "Multiple definitions. Only one is allowed for this option." else if length list != 1 then
throw "The unique option `${showOption args.prefix}' is defined multiple times, in ${showFiles args.files}."
else head list; else head list;
@ -135,6 +135,7 @@ rec {
/* Helper functions. */ /* Helper functions. */
showOption = concatStringsSep "."; showOption = concatStringsSep ".";
showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files);
unknownModule = "<unknown-file>"; unknownModule = "<unknown-file>";
} }

View File

@ -19,27 +19,24 @@ rec {
}; };
# name (name of the type)
# check (check the config value)
# merge (default merge function)
# getSubOptions (returns sub-options for manual generation)
isOptionType = isType "option-type"; isOptionType = isType "option-type";
mkOptionType = mkOptionType =
{ name { # Human-readable representation of the type.
, check ? (x: true) name
, merge ? mergeDefaultOption , # Function applied to each definition that should return true if
, merge' ? args: merge # its type-correct, false otherwise.
, getSubOptions ? prefix: {} check ? (x: true)
, # Merge a list of definitions together into a single value.
merge ? mergeDefaultOption
, # Return a flat list of sub-options. Used to generate
# documentation.
getSubOptions ? prefix: {}
}: }:
{ _type = "option-type"; { _type = "option-type";
inherit name check merge merge' getSubOptions; inherit name check merge getSubOptions;
}; };
addToPrefix = args: name: args // { prefix = args.prefix ++ [name]; };
types = rec { types = rec {
unspecified = mkOptionType { unspecified = mkOptionType {
@ -49,7 +46,7 @@ rec {
bool = mkOptionType { bool = mkOptionType {
name = "boolean"; name = "boolean";
check = builtins.isBool; check = builtins.isBool;
merge = fold lib.or false; merge = args: fold lib.or false;
}; };
int = mkOptionType { int = mkOptionType {
@ -60,7 +57,7 @@ rec {
string = mkOptionType { string = mkOptionType {
name = "string"; name = "string";
check = builtins.isString; check = builtins.isString;
merge = lib.concatStrings; merge = args: lib.concatStrings;
}; };
# Like string, but add newlines between every value. Useful for # Like string, but add newlines between every value. Useful for
@ -68,54 +65,56 @@ rec {
lines = mkOptionType { lines = mkOptionType {
name = "string"; name = "string";
check = builtins.isString; check = builtins.isString;
merge = lib.concatStringsSep "\n"; merge = args: lib.concatStringsSep "\n";
}; };
commas = mkOptionType { commas = mkOptionType {
name = "string"; name = "string";
check = builtins.isString; check = builtins.isString;
merge = lib.concatStringsSep ","; merge = args: lib.concatStringsSep ",";
}; };
envVar = mkOptionType { envVar = mkOptionType {
name = "environment variable"; name = "environment variable";
inherit (string) check; inherit (string) check;
merge = lib.concatStringsSep ":"; merge = args: lib.concatStringsSep ":";
}; };
attrs = mkOptionType { attrs = mkOptionType {
name = "attribute set"; name = "attribute set";
check = isAttrs; check = isAttrs;
merge = fold lib.mergeAttrs {}; merge = args: fold lib.mergeAttrs {};
}; };
# derivation is a reserved keyword. # derivation is a reserved keyword.
package = mkOptionType { package = mkOptionType {
name = "derivation"; name = "derivation";
check = isDerivation; check = isDerivation;
merge = mergeOneOption;
}; };
path = mkOptionType { path = mkOptionType {
name = "path"; name = "path";
# Hacky: there is no isPath primop. # Hacky: there is no isPath primop.
check = x: builtins.unsafeDiscardStringContext (builtins.substring 0 1 (toString x)) == "/"; check = x: builtins.unsafeDiscardStringContext (builtins.substring 0 1 (toString x)) == "/";
merge = mergeOneOption;
}; };
# drop this in the future: # drop this in the future:
list = builtins.trace "types.list is deprecated; use types.listOf instead" types.listOf; list = builtins.trace "`types.list' is deprecated; use `types.listOf' instead" types.listOf;
listOf = elemType: mkOptionType { listOf = elemType: mkOptionType {
name = "list of ${elemType.name}s"; 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 = args: defs: imap (n: def: elemType.merge (addToPrefix args (toString n)) [def]) (concatLists 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 = args: lib.zipAttrsWith (name:
elemType.merge' (addToPrefix (args // { inherit name; }) name)); elemType.merge (addToPrefix (args // { inherit name; }) name));
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
}; };
@ -138,43 +137,39 @@ rec {
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 = args: defs: attrOnly.merge args (imap convertIfList defs);
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]);
}; };
uniq = elemType: mkOptionType { uniq = elemType: mkOptionType {
inherit (elemType) name check; inherit (elemType) name check;
merge = list: merge = mergeOneOption;
if length list == 1 then
head list
else
throw "Multiple definitions of ${elemType.name}. Only one is allowed for this option.";
getSubOptions = elemType.getSubOptions; getSubOptions = elemType.getSubOptions;
}; };
none = elemType: mkOptionType { none = elemType: mkOptionType {
inherit (elemType) name check; inherit (elemType) name check;
merge = list: merge = args: list:
throw "No definitions are allowed for this option."; throw "No definitions are allowed for the option `${showOption args.prefix}'.";
getSubOptions = elemType.getSubOptions; 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 = args: defs:
if all isNull defs then null if all isNull defs then null
else if any isNull defs then else if any isNull defs then
throw "Some but not all values are null." throw "The option `${showOption args.prefix}' is defined both null and not null, in ${showFiles args.files}."
else elemType.merge' args defs; else elemType.merge args 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 = args: fns:
fnArgs: elemType.merge' args (map (fn: fn fnArgs) fns); fnArgs: elemType.merge args (map (fn: fn fnArgs) fns);
getSubOptions = elemType.getSubOptions; getSubOptions = elemType.getSubOptions;
}; };
@ -183,8 +178,7 @@ rec {
mkOptionType rec { mkOptionType rec {
name = "submodule"; name = "submodule";
check = x: isAttrs x || builtins.isFunction x; check = x: isAttrs x || builtins.isFunction x;
merge = merge' {}; merge = args: defs:
merge' = args: 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 coerce defs;
@ -204,4 +198,8 @@ rec {
}; };
/* Helper function. */
addToPrefix = args: name: args // { prefix = args.prefix ++ [name]; };
} }

View File

@ -25,11 +25,14 @@ 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 = xs: merge = args: xs:
let xs' = filterOverrides xs; in let xs' = filterOverrides xs; in
if isList (head xs') then concatLists xs' if isList (head xs') then concatLists xs'
else if builtins.lessThan 1 (length xs') then abort "variable in environment.variables has multiple values" else if builtins.lessThan 1 (length xs') then
else if !builtins.isString (head xs') then abort "variable in environment.variables does not have a string value" # Don't show location info here, since it's too general.
throw "The option `${showOption args.prefix}' is defined multiple times."
else if !builtins.isString (head xs') then
throw "The option `${showOption args.prefix}' does not have a string value."
else head xs'; else head xs';
}); });
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 = xs: last xs; # FIXME: hacky way to allow overriding in configuration.nix. merge = args: xs: last xs; # 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 = fold mergeConfig {}; merge = args: fold mergeConfig {};
}; };
in in