diff options
author | Katharina Fey <kookie@spacekookie.de> | 2020-02-03 09:26:35 +0100 |
---|---|---|
committer | Katharina Fey <kookie@spacekookie.de> | 2020-02-03 09:26:35 +0100 |
commit | 899a451e08f7d6d2c8214d119c2a0316849a0ed4 (patch) | |
tree | 5e72a7288b7d2b33fead36fbfe91a02a48ff7fef /nixpkgs/lib | |
parent | 5962418b6543dfb3ca34965c0fa16dd77543801b (diff) | |
parent | a21c2fa3ea2b88e698db6fc151d9c7259ae14d96 (diff) |
Merge commit 'a21c2fa3ea2b88e698db6fc151d9c7259ae14d96'
Diffstat (limited to 'nixpkgs/lib')
-rw-r--r-- | nixpkgs/lib/attrsets.nix | 2 | ||||
-rw-r--r-- | nixpkgs/lib/cli.nix | 83 | ||||
-rw-r--r-- | nixpkgs/lib/default.nix | 7 | ||||
-rw-r--r-- | nixpkgs/lib/generators.nix | 3 | ||||
-rw-r--r-- | nixpkgs/lib/licenses.nix | 5 | ||||
-rw-r--r-- | nixpkgs/lib/modules.nix | 40 | ||||
-rw-r--r-- | nixpkgs/lib/sources.nix | 37 | ||||
-rw-r--r-- | nixpkgs/lib/systems/examples.nix | 16 | ||||
-rw-r--r-- | nixpkgs/lib/tests/misc.nix | 37 | ||||
-rwxr-xr-x | nixpkgs/lib/tests/modules.sh | 17 | ||||
-rw-r--r-- | nixpkgs/lib/tests/modules/attrsOf-conditional-check.nix | 7 | ||||
-rw-r--r-- | nixpkgs/lib/tests/modules/attrsOf-lazy-check.nix | 7 | ||||
-rw-r--r-- | nixpkgs/lib/tests/modules/declare-attrsOf.nix | 6 | ||||
-rw-r--r-- | nixpkgs/lib/tests/modules/declare-lazyAttrsOf.nix | 6 | ||||
-rw-r--r-- | nixpkgs/lib/tests/modules/import-from-store.nix | 11 | ||||
-rw-r--r-- | nixpkgs/lib/trivial.nix | 2 | ||||
-rw-r--r-- | nixpkgs/lib/types.nix | 122 |
17 files changed, 351 insertions, 57 deletions
diff --git a/nixpkgs/lib/attrsets.nix b/nixpkgs/lib/attrsets.nix index 086c3d746fc..32994432d53 100644 --- a/nixpkgs/lib/attrsets.nix +++ b/nixpkgs/lib/attrsets.nix @@ -60,7 +60,7 @@ rec { [ { name = head attrPath; value = setAttrByPath (tail attrPath) value; } ]; - /* Like `getAttrPath' without a default value. If it doesn't find the + /* Like `attrByPath' without a default value. If it doesn't find the path it will throw. Example: diff --git a/nixpkgs/lib/cli.nix b/nixpkgs/lib/cli.nix new file mode 100644 index 00000000000..c96d4dbb043 --- /dev/null +++ b/nixpkgs/lib/cli.nix @@ -0,0 +1,83 @@ +{ lib }: + +rec { + /* Automatically convert an attribute set to command-line options. + + This helps protect against malformed command lines and also to reduce + boilerplate related to command-line construction for simple use cases. + + `toGNUCommandLine` returns a list of nix strings. + `toGNUCommandLineShell` returns an escaped shell string. + + Example: + cli.toGNUCommandLine {} { + data = builtins.toJSON { id = 0; }; + X = "PUT"; + retry = 3; + retry-delay = null; + url = [ "https://example.com/foo" "https://example.com/bar" ]; + silent = false; + verbose = true; + } + => [ + "-X" "PUT" + "--data" "{\"id\":0}" + "--retry" "3" + "--url" "https://example.com/foo" + "--url" "https://example.com/bar" + "--verbose" + ] + + cli.toGNUCommandLineShell {} { + data = builtins.toJSON { id = 0; }; + X = "PUT"; + retry = 3; + retry-delay = null; + url = [ "https://example.com/foo" "https://example.com/bar" ]; + silent = false; + verbose = true; + } + => "'-X' 'PUT' '--data' '{\"id\":0}' '--retry' '3' '--url' 'https://example.com/foo' '--url' 'https://example.com/bar' '--verbose'"; + */ + toGNUCommandLineShell = + options: attrs: lib.escapeShellArgs (toGNUCommandLine options attrs); + + toGNUCommandLine = { + # how to string-format the option name; + # by default one character is a short option (`-`), + # more than one characters a long option (`--`). + mkOptionName ? + k: if builtins.stringLength k == 1 + then "-${k}" + else "--${k}", + + # how to format a boolean value to a command list; + # by default itās a flag option + # (only the option name if true, left out completely if false). + mkBool ? k: v: lib.optional v (mkOptionName k), + + # how to format a list value to a command list; + # by default the option name is repeated for each value + # and `mkOption` is applied to the values themselves. + mkList ? k: v: lib.concatMap (mkOption k) v, + + # how to format any remaining value to a command list; + # on the toplevel, booleans and lists are handled by `mkBool` and `mkList`, + # though they can still appear as values of a list. + # By default, everything is printed verbatim and complex types + # are forbidden (lists, attrsets, functions). `null` values are omitted. + mkOption ? + k: v: if v == null + then [] + else [ (mkOptionName k) (lib.generators.mkValueStringDefault {} v) ] + }: + options: + let + render = k: v: + if builtins.isBool v then mkBool k v + else if builtins.isList v then mkList k v + else mkOption k v; + + in + builtins.concatLists (lib.mapAttrsToList render options); +} diff --git a/nixpkgs/lib/default.nix b/nixpkgs/lib/default.nix index 9f7a088d792..d2fe018aa6a 100644 --- a/nixpkgs/lib/default.nix +++ b/nixpkgs/lib/default.nix @@ -37,10 +37,13 @@ let licenses = callLibs ./licenses.nix; systems = callLibs ./systems; + # serialization + cli = callLibs ./cli.nix; + generators = callLibs ./generators.nix; + # misc asserts = callLibs ./asserts.nix; debug = callLibs ./debug.nix; - generators = callLibs ./generators.nix; misc = callLibs ./deprecated.nix; # domain-specific @@ -100,7 +103,7 @@ let inherit (sources) pathType pathIsDirectory cleanSourceFilter cleanSource sourceByRegex sourceFilesBySuffices commitIdFromGitRepo cleanSourceWith pathHasContext - canCleanSource; + canCleanSource pathIsRegularFile pathIsGitRepo; inherit (modules) evalModules unifyModuleSyntax applyIfFunction mergeModules mergeModules' mergeOptionDecls evalOptionValue mergeDefinitions diff --git a/nixpkgs/lib/generators.nix b/nixpkgs/lib/generators.nix index a71654bec6c..a64e94bd5cb 100644 --- a/nixpkgs/lib/generators.nix +++ b/nixpkgs/lib/generators.nix @@ -46,7 +46,10 @@ rec { else if isList v then err "lists" v # same as for lists, might want to replace else if isAttrs v then err "attrsets" v + # functions canāt be printed of course else if isFunction v then err "functions" v + # letās not talk about floats. There is no sensible `toString` for them. + else if isFloat v then err "floats" v else err "this value is" (toString v); diff --git a/nixpkgs/lib/licenses.nix b/nixpkgs/lib/licenses.nix index 986b7fa1fdd..e2f94e565ce 100644 --- a/nixpkgs/lib/licenses.nix +++ b/nixpkgs/lib/licenses.nix @@ -536,11 +536,6 @@ lib.mapAttrs (n: v: v // { shortName = n; }) { fullName = "University of Illinois/NCSA Open Source License"; }; - notion_lgpl = { - url = "https://raw.githubusercontent.com/raboof/notion/master/LICENSE"; - fullName = "Notion modified LGPL"; - }; - nposl3 = spdx { spdxId = "NPOSL-3.0"; fullName = "Non-Profit Open Software License 3.0"; diff --git a/nixpkgs/lib/modules.nix b/nixpkgs/lib/modules.nix index 38d6ac8cd91..2b1faf4f0c2 100644 --- a/nixpkgs/lib/modules.nix +++ b/nixpkgs/lib/modules.nix @@ -41,7 +41,13 @@ rec { options = { _module.args = mkOption { - type = types.attrsOf types.unspecified; + # Because things like `mkIf` are entirely useless for + # `_module.args` (because there's no way modules can check which + # arguments were passed), we'll use `lazyAttrsOf` which drops + # support for that, in turn it's lazy in its values. This means e.g. + # a `_module.args.pkgs = import (fetchTarball { ... }) {}` won't + # start a download when `pkgs` wasn't evaluated. + type = types.lazyAttrsOf types.unspecified; internal = true; description = "Arguments passed to each module."; }; @@ -151,8 +157,8 @@ rec { filterModules = modulesPath: { disabled, modules }: let moduleKey = m: if isString m then toString modulesPath + "/" + m else toString m; - disabledKeys = listToAttrs (map (k: nameValuePair (moduleKey k) null) disabled); - keyFilter = filter (attrs: ! disabledKeys ? ${attrs.key}); + disabledKeys = map moduleKey disabled; + keyFilter = filter (attrs: ! elem attrs.key disabledKeys); in map (attrs: attrs.module) (builtins.genericClosure { startSet = keyFilter modules; operator = attrs: keyFilter attrs.modules; @@ -365,16 +371,9 @@ rec { else mergeDefinitions loc opt.type defs'; - - # The value with a check that it is defined - valueDefined = if res.isDefined then res.mergedValue else - # (nixos-option detects this specific error message and gives it special - # handling. If changed here, please change it there too.) - throw "The option `${showOption loc}' is used but not defined."; - # Apply the 'apply' function to the merged value. This allows options to # yield a value computed from the definitions - value = if opt ? apply then opt.apply valueDefined else valueDefined; + value = if opt ? apply then opt.apply res.mergedValue else res.mergedValue; in opt // { value = builtins.addErrorContext "while evaluating the option `${showOption loc}':" value; @@ -408,11 +407,17 @@ rec { }; defsFinal = defsFinal'.values; - # Type-check the remaining definitions, and merge them. - mergedValue = foldl' (res: def: - if type.check def.value then res - else throw "The option value `${showOption loc}' in `${def.file}' is not of type `${type.description}'.") - (type.merge loc defsFinal) defsFinal; + # Type-check the remaining definitions, and merge them. Or throw if no definitions. + mergedValue = + if isDefined then + foldl' (res: def: + if type.check def.value then res + else throw "The option value `${showOption loc}' in `${def.file}' is not of type `${type.description}'." + ) (type.merge loc defsFinal) defsFinal + else + # (nixos-option detects this specific error message and gives it special + # handling. If changed here, please change it there too.) + throw "The option `${showOption loc}' is used but not defined."; isDefined = defsFinal != []; @@ -759,12 +764,15 @@ rec { fromOpt = getAttrFromPath from options; toOf = attrByPath to (abort "Renaming error: option `${showOption to}' does not exist."); + toType = let opt = attrByPath to {} options; in opt.type or null; in { options = setAttrByPath from (mkOption { inherit visible; description = "Alias of <option>${showOption to}</option>."; apply = x: use (toOf config); + } // optionalAttrs (toType != null) { + type = toType; }); config = mkMerge [ { diff --git a/nixpkgs/lib/sources.nix b/nixpkgs/lib/sources.nix index 51bcf5559e3..05519c3e392 100644 --- a/nixpkgs/lib/sources.nix +++ b/nixpkgs/lib/sources.nix @@ -9,6 +9,9 @@ rec { # Returns true if the path exists and is a directory, false otherwise pathIsDirectory = p: if builtins.pathExists p then (pathType p) == "directory" else false; + # Returns true if the path exists and is a regular file, false otherwise + pathIsRegularFile = p: if builtins.pathExists p then (pathType p) == "regular" else false; + # Bring in a path as a source, filtering out all Subversion and CVS # directories, as well as backup files (*~). cleanSourceFilter = name: type: let baseName = baseNameOf (toString name); in ! ( @@ -102,6 +105,7 @@ rec { in type == "directory" || lib.any (ext: lib.hasSuffix ext base) exts; in cleanSourceWith { inherit filter; src = path; }; + pathIsGitRepo = path: (builtins.tryEval (commitIdFromGitRepo path)).success; # Get the commit id of a git repo # Example: commitIdFromGitRepo <nixpkgs/.git> @@ -110,24 +114,45 @@ rec { with builtins; let fileName = toString path + "/" + file; packedRefsName = toString path + "/packed-refs"; - in if lib.pathExists fileName + absolutePath = base: path: + if lib.hasPrefix "/" path + then path + else toString (/. + "${base}/${path}"); + in if pathIsRegularFile path + # Resolve git worktrees. See gitrepository-layout(5) + then + let m = match "^gitdir: (.*)$" (lib.fileContents path); + in if m == null + then throw ("File contains no gitdir reference: " + path) + else + let gitDir = absolutePath (dirOf path) (lib.head m); + commonDir' = if pathIsRegularFile "${gitDir}/commondir" + then lib.fileContents "${gitDir}/commondir" + else gitDir; + commonDir = absolutePath gitDir commonDir'; + refFile = lib.removePrefix "${commonDir}/" "${gitDir}/${file}"; + in readCommitFromFile refFile commonDir + + else if pathIsRegularFile fileName + # Sometimes git stores the commitId directly in the file but + # sometimes it stores something like: Ā«ref: refs/heads/branch-nameĀ» then let fileContent = lib.fileContents fileName; - # Sometimes git stores the commitId directly in the file but - # sometimes it stores something like: Ā«ref: refs/heads/branch-nameĀ» matchRef = match "^ref: (.*)$" fileContent; - in if matchRef == null + in if matchRef == null then fileContent else readCommitFromFile (lib.head matchRef) path + + else if pathIsRegularFile packedRefsName # Sometimes, the file isn't there at all and has been packed away in the # packed-refs file, so we have to grep through it: - else if lib.pathExists packedRefsName then let fileContent = readFile packedRefsName; matchRef = match (".*\n([^\n ]*) " + file + "\n.*") fileContent; - in if matchRef == null + in if matchRef == null then throw ("Could not find " + file + " in " + packedRefsName) else lib.head matchRef + else throw ("Not a .git directory: " + path); in readCommitFromFile "HEAD"; diff --git a/nixpkgs/lib/systems/examples.nix b/nixpkgs/lib/systems/examples.nix index cb8bc3de6c4..19b3790ecbe 100644 --- a/nixpkgs/lib/systems/examples.nix +++ b/nixpkgs/lib/systems/examples.nix @@ -170,8 +170,8 @@ rec { iphone64 = { config = "aarch64-apple-ios"; # config = "aarch64-apple-darwin14"; - sdkVer = "10.2"; - xcodeVer = "8.2"; + sdkVer = "12.4"; + xcodeVer = "10.3"; xcodePlatform = "iPhoneOS"; useiOSPrebuilt = true; platform = {}; @@ -180,8 +180,8 @@ rec { iphone32 = { config = "armv7a-apple-ios"; # config = "arm-apple-darwin10"; - sdkVer = "10.2"; - xcodeVer = "8.2"; + sdkVer = "12.4"; + xcodeVer = "10.3"; xcodePlatform = "iPhoneOS"; useiOSPrebuilt = true; platform = {}; @@ -190,8 +190,8 @@ rec { iphone64-simulator = { config = "x86_64-apple-ios"; # config = "x86_64-apple-darwin14"; - sdkVer = "10.2"; - xcodeVer = "8.2"; + sdkVer = "12.4"; + xcodeVer = "10.3"; xcodePlatform = "iPhoneSimulator"; useiOSPrebuilt = true; platform = {}; @@ -200,8 +200,8 @@ rec { iphone32-simulator = { config = "i686-apple-ios"; # config = "i386-apple-darwin11"; - sdkVer = "10.2"; - xcodeVer = "8.2"; + sdkVer = "12.4"; + xcodeVer = "10.3"; xcodePlatform = "iPhoneSimulator"; useiOSPrebuilt = true; platform = {}; diff --git a/nixpkgs/lib/tests/misc.nix b/nixpkgs/lib/tests/misc.nix index b064faa1e1b..59ed1e507e2 100644 --- a/nixpkgs/lib/tests/misc.nix +++ b/nixpkgs/lib/tests/misc.nix @@ -441,4 +441,41 @@ runTests { expected = "Ā«fooĀ»"; }; + +# CLI + + testToGNUCommandLine = { + expr = cli.toGNUCommandLine {} { + data = builtins.toJSON { id = 0; }; + X = "PUT"; + retry = 3; + retry-delay = null; + url = [ "https://example.com/foo" "https://example.com/bar" ]; + silent = false; + verbose = true; + }; + + expected = [ + "-X" "PUT" + "--data" "{\"id\":0}" + "--retry" "3" + "--url" "https://example.com/foo" + "--url" "https://example.com/bar" + "--verbose" + ]; + }; + + testToGNUCommandLineShell = { + expr = cli.toGNUCommandLineShell {} { + data = builtins.toJSON { id = 0; }; + X = "PUT"; + retry = 3; + retry-delay = null; + url = [ "https://example.com/foo" "https://example.com/bar" ]; + silent = false; + verbose = true; + }; + + expected = "'-X' 'PUT' '--data' '{\"id\":0}' '--retry' '3' '--url' 'https://example.com/foo' '--url' 'https://example.com/bar' '--verbose'"; + }; } diff --git a/nixpkgs/lib/tests/modules.sh b/nixpkgs/lib/tests/modules.sh index 2997fb1ada1..8cd632a439c 100755 --- a/nixpkgs/lib/tests/modules.sh +++ b/nixpkgs/lib/tests/modules.sh @@ -12,7 +12,7 @@ evalConfig() { local attr=$1 shift; local script="import ./default.nix { modules = [ $@ ];}" - nix-instantiate --timeout 1 -E "$script" -A "$attr" --eval-only --show-trace + nix-instantiate --timeout 1 -E "$script" -A "$attr" --eval-only --show-trace --read-write-mode } reportFailure() { @@ -174,8 +174,7 @@ checkConfigOutput "true" config.submodule.inner ./declare-submoduleWith-modules. checkConfigOutput "true" config.submodule.outer ./declare-submoduleWith-modules.nix ## Paths should be allowed as values and work as expected -# Temporarily disabled until https://github.com/NixOS/nixpkgs/pull/76861 -#checkConfigOutput "true" config.submodule.enable ./declare-submoduleWith-path.nix +checkConfigOutput "true" config.submodule.enable ./declare-submoduleWith-path.nix # Check that disabledModules works recursively and correctly checkConfigOutput "true" config.enable ./disable-recursive/main.nix @@ -183,6 +182,18 @@ checkConfigOutput "true" config.enable ./disable-recursive/{main.nix,disable-foo checkConfigOutput "true" config.enable ./disable-recursive/{main.nix,disable-bar.nix} checkConfigError 'The option .* defined in .* does not exist' config.enable ./disable-recursive/{main.nix,disable-foo.nix,disable-bar.nix} +# Check that imports can depend on derivations +checkConfigOutput "true" config.enable ./import-from-store.nix + +# Check attrsOf and lazyAttrsOf. Only lazyAttrsOf should be lazy, and only +# attrsOf should work with conditional definitions +# In addition, lazyAttrsOf should honor an options emptyValue +checkConfigError "is not lazy" config.isLazy ./declare-attrsOf.nix ./attrsOf-lazy-check.nix +checkConfigOutput "true" config.isLazy ./declare-lazyAttrsOf.nix ./attrsOf-lazy-check.nix +checkConfigOutput "true" config.conditionalWorks ./declare-attrsOf.nix ./attrsOf-conditional-check.nix +checkConfigOutput "false" config.conditionalWorks ./declare-lazyAttrsOf.nix ./attrsOf-conditional-check.nix +checkConfigOutput "empty" config.value.foo ./declare-lazyAttrsOf.nix ./attrsOf-conditional-check.nix + cat <<EOF ====== module tests ====== $pass Pass diff --git a/nixpkgs/lib/tests/modules/attrsOf-conditional-check.nix b/nixpkgs/lib/tests/modules/attrsOf-conditional-check.nix new file mode 100644 index 00000000000..0f00ebca155 --- /dev/null +++ b/nixpkgs/lib/tests/modules/attrsOf-conditional-check.nix @@ -0,0 +1,7 @@ +{ lib, config, ... }: { + options.conditionalWorks = lib.mkOption { + default = ! config.value ? foo; + }; + + config.value.foo = lib.mkIf false "should not be defined"; +} diff --git a/nixpkgs/lib/tests/modules/attrsOf-lazy-check.nix b/nixpkgs/lib/tests/modules/attrsOf-lazy-check.nix new file mode 100644 index 00000000000..ec5b418b15a --- /dev/null +++ b/nixpkgs/lib/tests/modules/attrsOf-lazy-check.nix @@ -0,0 +1,7 @@ +{ lib, config, ... }: { + options.isLazy = lib.mkOption { + default = ! config.value ? foo; + }; + + config.value.bar = throw "is not lazy"; +} diff --git a/nixpkgs/lib/tests/modules/declare-attrsOf.nix b/nixpkgs/lib/tests/modules/declare-attrsOf.nix new file mode 100644 index 00000000000..b3999de7e5f --- /dev/null +++ b/nixpkgs/lib/tests/modules/declare-attrsOf.nix @@ -0,0 +1,6 @@ +{ lib, ... }: { + options.value = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = {}; + }; +} diff --git a/nixpkgs/lib/tests/modules/declare-lazyAttrsOf.nix b/nixpkgs/lib/tests/modules/declare-lazyAttrsOf.nix new file mode 100644 index 00000000000..1d9fec25f90 --- /dev/null +++ b/nixpkgs/lib/tests/modules/declare-lazyAttrsOf.nix @@ -0,0 +1,6 @@ +{ lib, ... }: { + options.value = lib.mkOption { + type = lib.types.lazyAttrsOf (lib.types.str // { emptyValue.value = "empty"; }); + default = {}; + }; +} diff --git a/nixpkgs/lib/tests/modules/import-from-store.nix b/nixpkgs/lib/tests/modules/import-from-store.nix new file mode 100644 index 00000000000..f5af22432ce --- /dev/null +++ b/nixpkgs/lib/tests/modules/import-from-store.nix @@ -0,0 +1,11 @@ +{ lib, ... }: +{ + + imports = [ + "${builtins.toFile "drv" "{}"}" + ./declare-enable.nix + ./define-enable.nix + ]; + +} + diff --git a/nixpkgs/lib/trivial.nix b/nixpkgs/lib/trivial.nix index 3a25e31fb05..a281cd70fb0 100644 --- a/nixpkgs/lib/trivial.nix +++ b/nixpkgs/lib/trivial.nix @@ -191,7 +191,7 @@ rec { let revisionFile = "${toString ./..}/.git-revision"; gitRepo = "${toString ./..}/.git"; - in if lib.pathIsDirectory gitRepo + in if lib.pathIsGitRepo gitRepo then lib.commitIdFromGitRepo gitRepo else if lib.pathExists revisionFile then lib.fileContents revisionFile else default; diff --git a/nixpkgs/lib/types.nix b/nixpkgs/lib/types.nix index 4872a676657..6fd6de7e1fd 100644 --- a/nixpkgs/lib/types.nix +++ b/nixpkgs/lib/types.nix @@ -65,6 +65,11 @@ rec { # definition values and locations (e.g. [ { file = "/foo.nix"; # value = 1; } { file = "/bar.nix"; value = 2 } ]). merge ? mergeDefaultOption + , # Whether this type has a value representing nothingness. If it does, + # this should be a value of the form { value = <the nothing value>; } + # If it doesn't, this should be {} + # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`. + emptyValue ? {} , # Return a flat list of sub-options. Used to generate # documentation. getSubOptions ? prefix: {} @@ -88,7 +93,7 @@ rec { functor ? defaultFunctor name }: { _type = "option-type"; - inherit name check merge getSubOptions getSubModules substSubModules typeMerge functor; + inherit name check merge emptyValue getSubOptions getSubModules substSubModules typeMerge functor; description = if description == null then name else description; }; @@ -225,6 +230,7 @@ rec { description = "attribute set"; check = isAttrs; merge = loc: foldl' (res: def: mergeAttrs res def.value) {}; + emptyValue = { value = {}; }; }; # derivation is a reserved keyword. @@ -265,6 +271,7 @@ rec { ) def.value else throw "The option value `${showOption loc}` in `${def.file}` is not a list.") defs))); + emptyValue = { value = {}; }; getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); getSubModules = elemType.getSubModules; substSubModules = m: listOf (elemType.substSubModules m); @@ -273,7 +280,10 @@ rec { nonEmptyListOf = elemType: let list = addCheck (types.listOf elemType) (l: l != []); - in list // { description = "non-empty " + list.description; }; + in list // { + description = "non-empty " + list.description; + # Note: emptyValue is left as is, because another module may define an element. + }; attrsOf = elemType: mkOptionType rec { name = "attrsOf"; @@ -285,12 +295,37 @@ rec { ) # Push down position info. (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs))); + emptyValue = { value = {}; }; getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); getSubModules = elemType.getSubModules; substSubModules = m: attrsOf (elemType.substSubModules m); functor = (defaultFunctor name) // { wrapped = elemType; }; }; + # A version of attrsOf that's lazy in its values at the expense of + # conditional definitions not working properly. E.g. defining a value with + # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with + # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an + # error that it's not defined. Use only if conditional definitions don't make sense. + lazyAttrsOf = elemType: mkOptionType rec { + name = "lazyAttrsOf"; + description = "lazy attribute set of ${elemType.description}s"; + check = isAttrs; + merge = loc: defs: + zipAttrsWith (name: defs: + let merged = mergeDefinitions (loc ++ [name]) elemType defs; + # mergedValue will trigger an appropriate error when accessed + in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue + ) + # Push down position info. + (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs); + emptyValue = { value = {}; }; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); + getSubModules = elemType.getSubModules; + substSubModules = m: lazyAttrsOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; + }; + # List or attribute set of ... loaOf = elemType: let @@ -305,29 +340,79 @@ rec { 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 = elem.name or (unnamed elemIdx); + { name = nameValueNew (elem.${nameAttr} or (unnamed elemIdx)) (unnamed elemIdx); value = elem; }) def.value); }; option = concatStringsSep "." loc; sample = take 3 def.value; - list = concatMapStrings (x: ''{ name = "${x.name or "unnamed"}"; ...} '') sample; - set = concatMapStrings (x: ''${x.name or "unnamed"} = {...}; '') sample; + 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://git.io/fj2zm for more information. + See https://github.com/NixOS/nixpkgs/pull/63103 for more information. Do ${option} = - { ${set}...} + { ${set}${more}} instead of ${option} = - [ ${list}...] + [ ${list}${more}] ''; in lib.warn msg res @@ -339,6 +424,7 @@ rec { 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); @@ -350,6 +436,7 @@ rec { name = "uniq"; inherit (elemType) description check; merge = mergeOneOption; + emptyValue = elemType.emptyValue; getSubOptions = elemType.getSubOptions; getSubModules = elemType.getSubModules; substSubModules = m: uniq (elemType.substSubModules m); @@ -367,6 +454,7 @@ rec { else if nrNulls != 0 then throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." else elemType.merge loc defs; + emptyValue = { value = null; }; getSubOptions = elemType.getSubOptions; getSubModules = elemType.getSubModules; substSubModules = m: nullOr (elemType.substSubModules m); @@ -392,14 +480,16 @@ rec { else unify (if shorthandOnlyDefinesConfig then { config = value; } else value); allModules = defs: modules ++ imap1 (n: { value, file }: - # Annotate the value with the location of its definition for better error messages - coerce (lib.modules.unifyModuleSyntax file "${toString file}-${toString n}") value + if isAttrs value || isFunction value then + # Annotate the value with the location of its definition for better error messages + coerce (lib.modules.unifyModuleSyntax file "${toString file}-${toString n}") value + else value ) defs; in mkOptionType rec { name = "submodule"; - check = x: isAttrs x || isFunction x; + check = x: isAttrs x || isFunction x || path.check x; merge = loc: defs: (evalModules { modules = allModules defs; @@ -407,6 +497,7 @@ rec { args.name = last loc; prefix = loc; }).config; + emptyValue = { value = {}; }; getSubOptions = prefix: (evalModules { inherit modules prefix specialArgs; # This is a work-around due to the fact that some sub-modules, @@ -499,7 +590,7 @@ rec { tail' = tail ts; in foldl' either head' tail'; - # Either value of type `finalType` or `coercedType`, the latter is + # Either value of type `coercedType` or `finalType`, the former is # converted to `finalType` using `coerceFunc`. coercedTo = coercedType: coerceFunc: finalType: assert lib.assertMsg (coercedType.getSubModules == null) @@ -508,13 +599,14 @@ rec { mkOptionType rec { name = "coercedTo"; description = "${finalType.description} or ${coercedType.description} convertible to it"; - check = x: finalType.check x || (coercedType.check x && finalType.check (coerceFunc x)); + check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x; merge = loc: defs: let coerceVal = val: - if finalType.check val then val - else coerceFunc val; + if coercedType.check val then coerceFunc val + else val; in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); + emptyValue = finalType.emptyValue; getSubOptions = finalType.getSubOptions; getSubModules = finalType.getSubModules; substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); |