diff options
Diffstat (limited to 'lib/types.nix')
-rw-r--r-- | lib/types.nix | 312 |
1 files changed, 178 insertions, 134 deletions
diff --git a/lib/types.nix b/lib/types.nix index 1845b6ae339..a0be2ff3a45 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -1,12 +1,65 @@ # Definitions related to run-time type checking. Used in particular # to type-check NixOS configurations. { lib }: -with lib.lists; -with lib.attrsets; -with lib.options; -with lib.trivial; -with lib.strings; + let + inherit (lib) + elem + flip + functionArgs + isAttrs + isBool + isDerivation + isFloat + isFunction + isInt + isList + isString + isStorePath + setFunctionArgs + toDerivation + toList + ; + inherit (lib.lists) + all + concatLists + count + elemAt + filter + foldl' + head + imap1 + last + length + tail + unique + ; + inherit (lib.attrsets) + attrNames + filterAttrs + hasAttr + mapAttrs + optionalAttrs + zipAttrsWith + ; + inherit (lib.options) + getFiles + getValues + mergeDefaultOption + mergeEqualOption + mergeOneOption + showFiles + showOption + ; + inherit (lib.strings) + concatMapStringsSep + concatStringsSep + escapeNixString + isCoercibleToString + ; + inherit (lib.trivial) + boolToString + ; inherit (lib.modules) mergeDefinitions; outer_types = @@ -91,9 +144,16 @@ rec { # combinable with the binOp binary operation. # binOp: binary operation that merge two payloads of the same type. functor ? defaultFunctor name + , # The deprecation message to display when this type is used by an option + # If null, the type isn't deprecated + deprecationMessage ? null + , # The types that occur in the definition of this type. This is used to + # issue deprecation warnings recursively. Can also be used to reuse + # nested types + nestedTypes ? {} }: { _type = "option-type"; - inherit name check merge emptyValue getSubOptions getSubModules substSubModules typeMerge functor; + inherit name check merge emptyValue getSubOptions getSubModules substSubModules typeMerge functor deprecationMessage nestedTypes; description = if description == null then name else description; }; @@ -101,6 +161,42 @@ rec { # When adding new types don't forget to document them in # nixos/doc/manual/development/option-types.xml! types = rec { + + anything = mkOptionType { + name = "anything"; + description = "anything"; + check = value: true; + merge = loc: defs: + let + getType = value: + if isAttrs value && isCoercibleToString value + then "stringCoercibleSet" + else builtins.typeOf value; + + # Returns the common type of all definitions, throws an error if they + # don't have the same type + commonType = foldl' (type: def: + if getType def.value == type + then type + else throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}" + ) (getType (head defs).value) defs; + + mergeFunction = { + # Recursively merge attribute sets + set = (attrsOf anything).merge; + # Safe and deterministic behavior for lists is to only accept one definition + # listOf only used to apply mkIf and co. + list = + if length defs > 1 + then throw "The option `${showOption loc}' has conflicting definitions, in ${showFiles (getFiles defs)}." + else (listOf anything).merge; + # This is the type of packages, only accept a single definition + stringCoercibleSet = mergeOneOption; + # Otherwise fall back to only allowing all equal definitions + }.${commonType} or mergeEqualOption; + in mergeFunction loc defs; + }; + unspecified = mkOptionType { name = "unspecified"; }; @@ -164,14 +260,14 @@ rec { }; u8 = unsign 8 256; u16 = unsign 16 65536; - # the biggest int a 64-bit Nix accepts is 2^63 - 1 (9223372036854775808), for a 32-bit Nix it is 2^31 - 1 (2147483647) - # the smallest int a 64-bit Nix accepts is -2^63 (-9223372036854775807), for a 32-bit Nix it is -2^31 (-2147483648) - # u32 = unsign 32 4294967296; + # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808) + # the smallest int Nix accepts is -2^63 (-9223372036854775807) + u32 = unsign 32 4294967296; # u64 = unsign 64 18446744073709551616; s8 = sign 8 256; s16 = sign 16 65536; - # s32 = sign 32 4294967296; + s32 = sign 32 4294967296; }; # Alias of u16 for a port number @@ -191,6 +287,13 @@ rec { merge = mergeEqualOption; }; + nonEmptyStr = mkOptionType { + name = "nonEmptyStr"; + description = "non-empty string"; + check = x: str.check x && builtins.match "[ \t\n]*" x == null; + inherit (str) merge; + }; + strMatching = pattern: mkOptionType { name = "strMatching ${escapeNixString pattern}"; description = "string matching the pattern ${pattern}"; @@ -222,14 +325,16 @@ rec { # Deprecated; should not be used because it quietly concatenates # strings, which is usually not what you want. - string = warn "types.string is deprecated because it quietly concatenates strings" - (separatedString ""); + string = separatedString "" // { + name = "string"; + deprecationMessage = "See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types."; + }; attrs = mkOptionType { name = "attrs"; description = "attribute set"; check = isAttrs; - merge = loc: foldl' (res: def: mergeAttrs res def.value) {}; + merge = loc: foldl' (res: def: res // def.value) {}; emptyValue = { value = {}; }; }; @@ -243,7 +348,7 @@ rec { }; shellPackage = package // { - check = x: (package.check x) && (hasAttr "shellPath" x); + check = x: isDerivation x && hasAttr "shellPath" x; }; path = mkOptionType { @@ -252,30 +357,26 @@ rec { merge = mergeEqualOption; }; - # drop this in the future: - list = builtins.trace "`types.list` is deprecated; use `types.listOf` instead" types.listOf; - listOf = elemType: mkOptionType rec { name = "listOf"; description = "list of ${elemType.description}s"; check = isList; merge = loc: defs: map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def: - if isList def.value then - imap1 (m: def': - (mergeDefinitions - (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) - elemType - [{ inherit (def) file; value = def'; }] - ).optionalValue - ) def.value - else - throw "The option value `${showOption loc}` in `${def.file}` is not a list.") defs))); + imap1 (m: def': + (mergeDefinitions + (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) + elemType + [{ inherit (def) file; value = def'; }] + ).optionalValue + ) def.value + ) defs))); emptyValue = { value = {}; }; getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); getSubModules = elemType.getSubModules; substSubModules = m: listOf (elemType.substSubModules m); functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; }; nonEmptyListOf = elemType: @@ -300,6 +401,7 @@ rec { getSubModules = elemType.getSubModules; substSubModules = m: attrsOf (elemType.substSubModules m); functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; }; # A version of attrsOf that's lazy in its values at the expense of @@ -324,112 +426,17 @@ rec { getSubModules = elemType.getSubModules; substSubModules = m: lazyAttrsOf (elemType.substSubModules m); functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; }; - # List or attribute set of ... - loaOf = elemType: - let - convertAllLists = loc: defs: - let - padWidth = stringLength (toString (length defs)); - unnamedPrefix = i: "unnamed-" + fixedWidthNumber padWidth i + "."; - in - imap1 (i: convertIfList loc (unnamedPrefix i)) defs; - convertIfList = loc: unnamedPrefix: def: - if isList def.value then - let - padWidth = stringLength (toString (length def.value)); - unnamed = i: unnamedPrefix + fixedWidthNumber padWidth i; - anyString = placeholder "name"; - nameAttrs = [ - { path = [ "environment" "etc" ]; - name = "target"; - } - { path = [ "containers" anyString "bindMounts" ]; - name = "mountPoint"; - } - { path = [ "programs" "ssh" "knownHosts" ]; - # hostNames is actually a list so we would need to handle it only when singleton - name = "hostNames"; - } - { path = [ "fileSystems" ]; - name = "mountPoint"; - } - { path = [ "boot" "specialFileSystems" ]; - name = "mountPoint"; - } - { path = [ "services" "znapzend" "zetup" ]; - name = "dataset"; - } - { path = [ "services" "znapzend" "zetup" anyString "destinations" ]; - name = "label"; - } - { path = [ "services" "geoclue2" "appConfig" ]; - name = "desktopID"; - } - ]; - matched = let - equals = a: b: b == anyString || a == b; - fallback = { name = "name"; }; - in findFirst ({ path, ... }: all (v: v == true) (zipListsWith equals loc path)) fallback nameAttrs; - nameAttr = matched.name; - nameValueOld = value: - if isList value then - if length value > 0 then - "[ " + concatMapStringsSep " " escapeNixString value + " ]" - else - "[ ]" - else - escapeNixString value; - nameValueNew = value: unnamed: - if isList value then - if length value > 0 then - head value - else - unnamed - else - value; - res = - { inherit (def) file; - value = listToAttrs ( - imap1 (elemIdx: elem: - { name = nameValueNew (elem.${nameAttr} or (unnamed elemIdx)) (unnamed elemIdx); - value = elem; - }) def.value); - }; - option = concatStringsSep "." loc; - sample = take 3 def.value; - more = lib.optionalString (length def.value > 3) "... "; - list = concatMapStrings (x: ''{ ${nameAttr} = ${nameValueOld (x.${nameAttr} or "unnamed")}; ...} '') sample; - set = concatMapStrings (x: ''${nameValueNew (x.${nameAttr} or "unnamed") "unnamed"} = {...}; '') sample; - msg = '' - In file ${def.file} - a list is being assigned to the option config.${option}. - This will soon be an error as type loaOf is deprecated. - See https://github.com/NixOS/nixpkgs/pull/63103 for more information. - Do - ${option} = - { ${set}${more}} - instead of - ${option} = - [ ${list}${more}] - ''; - in - lib.warn msg res - else - def; - attrOnly = attrsOf elemType; - in mkOptionType rec { - name = "loaOf"; - description = "list or attribute set of ${elemType.description}s"; - check = x: isList x || isAttrs x; - merge = loc: defs: attrOnly.merge loc (convertAllLists loc defs); - emptyValue = { value = {}; }; - getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); - getSubModules = elemType.getSubModules; - substSubModules = m: loaOf (elemType.substSubModules m); - functor = (defaultFunctor name) // { wrapped = elemType; }; - }; + # TODO: drop this in the future: + loaOf = elemType: types.attrsOf elemType // { + name = "loaOf"; + deprecationMessage = "Mixing lists with attribute values is no longer" + + " possible; please use `types.attrsOf` instead. See" + + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation."; + nestedTypes.elemType = elemType; + }; # Value of given type but with no merging (i.e. `uniq list`s are not concatenated). uniq = elemType: mkOptionType rec { @@ -441,6 +448,7 @@ rec { getSubModules = elemType.getSubModules; substSubModules = m: uniq (elemType.substSubModules m); functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; }; # Null or value of ... @@ -459,6 +467,18 @@ rec { getSubModules = elemType.getSubModules; substSubModules = m: nullOr (elemType.substSubModules m); functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; + }; + + functionTo = elemType: mkOptionType { + name = "functionTo"; + description = "function that evaluates to a(n) ${elemType.name}"; + check = isFunction; + merge = loc: defs: + fnArgs: (mergeDefinitions (loc ++ [ "[function body]" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue; + getSubOptions = elemType.getSubOptions; + getSubModules = elemType.getSubModules; + substSubModules = m: functionTo (elemType.substSubModules m); }; # A submodule (like typed attribute set). See NixOS manual. @@ -522,11 +542,19 @@ rec { # would be used, and use of `<` and `>` would break the XML document. # It shouldn't cause an issue since this is cosmetic for the manual. args.name = "‹name›"; - }).options; + }).options // optionalAttrs (freeformType != null) { + # Expose the sub options of the freeform type. Note that the option + # discovery doesn't care about the attribute name used here, so this + # is just to avoid conflicts with potential options from the submodule + _freeformOptions = freeformType.getSubOptions prefix; + }; getSubModules = modules; substSubModules = m: submoduleWith (attrs // { modules = m; }); + nestedTypes = lib.optionalAttrs (freeformType != null) { + freeformType = freeformType; + }; functor = defaultFunctor name // { type = types.submoduleWith; payload = { @@ -555,11 +583,22 @@ rec { show = v: if builtins.isString v then ''"${v}"'' else if builtins.isInt v then builtins.toString v + else if builtins.isBool v then boolToString v else ''<${builtins.typeOf v}>''; in mkOptionType rec { name = "enum"; - description = "one of ${concatMapStringsSep ", " show values}"; + description = + # Length 0 or 1 enums may occur in a design pattern with type merging + # where an "interface" module declares an empty enum and other modules + # provide implementations, each extending the enum with their own + # identifier. + if values == [] then + "impossible (empty enum)" + else if builtins.length values == 1 then + "value ${show (builtins.head values)} (singular enum)" + else + "one of ${concatMapStringsSep ", " show values}"; check = flip elem values; merge = mergeEqualOption; functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; @@ -587,6 +626,8 @@ rec { then functor.type mt1 mt2 else null; functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; + nestedTypes.left = t1; + nestedTypes.right = t2; }; # Any of the types in the given list @@ -618,14 +659,17 @@ rec { substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); typeMerge = t1: t2: null; functor = (defaultFunctor name) // { wrapped = finalType; }; + nestedTypes.coercedType = coercedType; + nestedTypes.finalType = finalType; }; # Obsolete alternative to configOf. It takes its option # declarations from the ‘options’ attribute of containing option # declaration. optionSet = mkOptionType { - name = builtins.trace "types.optionSet is deprecated; use types.submodule instead" "optionSet"; + name = "optionSet"; description = "option set"; + deprecationMessage = "Use `types.submodule' instead"; }; # Augment the given type with an additional type check function. addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; |