diff options
104 files changed, 2485 insertions, 1692 deletions
@@ -1 +1 @@ -2.13.0 +2.14.0
\ No newline at end of file diff --git a/doc/manual/book.toml b/doc/manual/book.toml index 46ced7ff7..73fb7e75e 100644 --- a/doc/manual/book.toml +++ b/doc/manual/book.toml @@ -10,3 +10,12 @@ git-repository-url = "https://github.com/NixOS/nix" [preprocessor.anchors] renderers = ["html"] command = "jq --from-file doc/manual/anchors.jq" + +[output.linkcheck] +# no Internet during the build (in the sandbox) +follow-web-links = false + +# mdbook-linkcheck does not understand [foo]{#bar} style links, resulting in +# excessive "Potential incomplete link" warnings. No other kind of warning was +# produced at the time of writing. +warning-policy = "ignore" diff --git a/doc/manual/local.mk b/doc/manual/local.mk index c0f69e00f..190f0258a 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -50,11 +50,16 @@ $(d)/src/SUMMARY.md: $(d)/src/SUMMARY.md.in $(d)/src/command-ref/new-cli $(d)/src/command-ref/new-cli: $(d)/nix.json $(d)/generate-manpage.nix $(bindir)/nix @rm -rf $@ - $(trace-gen) $(nix-eval) --write-to $@ --expr 'import doc/manual/generate-manpage.nix { toplevel = builtins.readFile $<; }' + $(trace-gen) $(nix-eval) --write-to $@.tmp --expr 'import doc/manual/generate-manpage.nix { toplevel = builtins.readFile $<; }' + # @docroot@: https://nixos.org/manual/nix/unstable/contributing/hacking.html#docroot-variable + $(trace-gen) sed -i $@.tmp/*.md -e 's^@docroot@^../..^g' + @mv $@.tmp $@ $(d)/src/command-ref/conf-file.md: $(d)/conf-file.json $(d)/generate-options.nix $(d)/src/command-ref/conf-file-prefix.md $(bindir)/nix @cat doc/manual/src/command-ref/conf-file-prefix.md > $@.tmp - $(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-options.nix (builtins.fromJSON (builtins.readFile $<))' >> $@.tmp + # @docroot@: https://nixos.org/manual/nix/unstable/contributing/hacking.html#docroot-variable + $(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-options.nix (builtins.fromJSON (builtins.readFile $<))' \ + | sed -e 's^@docroot@^..^g'>> $@.tmp @mv $@.tmp $@ $(d)/nix.json: $(bindir)/nix @@ -67,7 +72,9 @@ $(d)/conf-file.json: $(bindir)/nix $(d)/src/language/builtins.md: $(d)/builtins.json $(d)/generate-builtins.nix $(d)/src/language/builtins-prefix.md $(bindir)/nix @cat doc/manual/src/language/builtins-prefix.md > $@.tmp - $(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-builtins.nix (builtins.fromJSON (builtins.readFile $<))' >> $@.tmp + # @docroot@: https://nixos.org/manual/nix/unstable/contributing/hacking.html#docroot-variable + $(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-builtins.nix (builtins.fromJSON (builtins.readFile $<))' \ + | sed -e 's^@docroot@^..^g' >> $@.tmp @cat doc/manual/src/language/builtins-suffix.md >> $@.tmp @mv $@.tmp $@ @@ -102,6 +109,12 @@ doc/manual/generated/man1/nix3-manpages: $(d)/src/command-ref/new-cli @touch $@ $(docdir)/manual/index.html: $(MANUAL_SRCS) $(d)/book.toml $(d)/anchors.jq $(d)/custom.css $(d)/src/SUMMARY.md $(d)/src/command-ref/new-cli $(d)/src/command-ref/conf-file.md $(d)/src/language/builtins.md - $(trace-gen) RUST_LOG=warn mdbook build doc/manual -d $(DESTDIR)$(docdir)/manual + $(trace-gen) \ + set -euo pipefail; \ + RUST_LOG=warn mdbook build doc/manual -d $(DESTDIR)$(docdir)/manual.tmp 2>&1 \ + | { grep -Fv "because fragment resolution isn't implemented" || :; } + @rm -rf $(DESTDIR)$(docdir)/manual + @mv $(DESTDIR)$(docdir)/manual.tmp/html $(DESTDIR)$(docdir)/manual + @rm -rf $(DESTDIR)$(docdir)/manual.tmp endif diff --git a/doc/manual/src/SUMMARY.md.in b/doc/manual/src/SUMMARY.md.in index 4f1fc34ce..b1c551969 100644 --- a/doc/manual/src/SUMMARY.md.in +++ b/doc/manual/src/SUMMARY.md.in @@ -67,6 +67,7 @@ - [CLI guideline](contributing/cli-guideline.md) - [Release Notes](release-notes/release-notes.md) - [Release X.Y (202?-??-??)](release-notes/rl-next.md) + - [Release 2.13 (2023-01-17)](release-notes/rl-2.13.md) - [Release 2.12 (2022-12-06)](release-notes/rl-2.12.md) - [Release 2.11 (2022-08-25)](release-notes/rl-2.11.md) - [Release 2.10 (2022-07-11)](release-notes/rl-2.10.md) diff --git a/doc/manual/src/architecture/architecture.md b/doc/manual/src/architecture/architecture.md index 2d1b26558..e51958052 100644 --- a/doc/manual/src/architecture/architecture.md +++ b/doc/manual/src/architecture/architecture.md @@ -68,7 +68,7 @@ It can also execute build plans to produce new data, which are made available to A build plan itself is a series of *build tasks*, together with their build inputs. > **Important** -> A build task in Nix is called [derivation](../glossary#gloss-derivation). +> A build task in Nix is called [derivation](../glossary.md#gloss-derivation). Each build task has a special build input executed as *build instructions* in order to perform the build. The result of a build task can be input to another build task. diff --git a/doc/manual/src/command-ref/env-common.md b/doc/manual/src/command-ref/env-common.md index 5845bdc43..bb85a6b07 100644 --- a/doc/manual/src/command-ref/env-common.md +++ b/doc/manual/src/command-ref/env-common.md @@ -11,7 +11,7 @@ Most Nix commands interpret the following environment variables: expressions using [paths](../language/values.md#type-path) enclosed in angle brackets (i.e., `<path>`), e.g. `/home/eelco/Dev:/etc/nixos`. It can be extended using the - [`-I` option](./opt-common#opt-I). + [`-I` option](./opt-common.md#opt-I). - [`NIX_IGNORE_SYMLINK_STORE`]{#env-NIX_IGNORE_SYMLINK_STORE}\ Normally, the Nix store directory (typically `/nix/store`) is not diff --git a/doc/manual/src/command-ref/nix-copy-closure.md b/doc/manual/src/command-ref/nix-copy-closure.md index 83e8a2936..cd8e351bb 100644 --- a/doc/manual/src/command-ref/nix-copy-closure.md +++ b/doc/manual/src/command-ref/nix-copy-closure.md @@ -49,7 +49,7 @@ authentication, you can avoid typing the passphrase with `ssh-agent`. - `--include-outputs`\ Also copy the outputs of [store derivation]s included in the closure. - [store derivation]: ../../glossary.md#gloss-store-derivation + [store derivation]: ../glossary.md#gloss-store-derivation - `--use-substitutes` / `-s`\ Attempt to download missing paths on the target machine using Nix’s diff --git a/doc/manual/src/command-ref/nix-store.md b/doc/manual/src/command-ref/nix-store.md index 6d0e02ca5..f6017481c 100644 --- a/doc/manual/src/command-ref/nix-store.md +++ b/doc/manual/src/command-ref/nix-store.md @@ -66,11 +66,11 @@ The operation `--realise` essentially “builds” the specified store paths. Realisation is a somewhat overloaded term: - If the store path is a *derivation*, realisation ensures that the - output paths of the derivation are [valid](../glossary.md) (i.e., + output paths of the derivation are [valid] (i.e., the output path and its closure exist in the file system). This can be done in several ways. First, it is possible that the outputs are already valid, in which case we are done - immediately. Otherwise, there may be [substitutes](../glossary.md) + immediately. Otherwise, there may be [substitutes] that produce the outputs (e.g., by downloading them). Finally, the outputs can be produced by running the build task described by the derivation. @@ -82,6 +82,9 @@ paths. Realisation is a somewhat overloaded term: produced through substitutes. If there are no (successful) substitutes, realisation fails. +[valid]: ../glossary.md#validity +[substitutes]: ../glossary.md#substitute + The output path of each derivation is printed on standard output. (For non-derivations argument, the argument itself is printed.) @@ -295,8 +298,8 @@ error: cannot delete path `/nix/store/zq0h41l75vlb4z45kzgjjmsjxvcv1qk7-mesa-6.4' ## Description -The operation `--query` displays various bits of information about the -store paths . The queries are described below. At most one query can be +The operation `--query` displays information about [store path]s. +The queries are described below. At most one query can be specified. The default query is `--outputs`. The paths *paths* may also be symlinks from outside of the Nix store, to @@ -316,12 +319,12 @@ symlink. ## Queries - `--outputs`\ - Prints out the [output paths](../glossary.md) of the store + Prints out the [output path]s of the store derivations *paths*. These are the paths that will be produced when the derivation is built. - `--requisites`; `-R`\ - Prints out the [closure](../glossary.md) of the store path *paths*. + Prints out the [closure] of the given *paths*. This query has one option: @@ -338,10 +341,12 @@ symlink. derivation and specifying the option `--include-outputs`. - `--references`\ - Prints the set of [references](../glossary.md) of the store paths + Prints the set of [references]s of the store paths *paths*, that is, their immediate dependencies. (For *all* dependencies, use `--requisites`.) + [reference]: ../glossary.md#gloss-reference + - `--referrers`\ Prints the set of *referrers* of the store paths *paths*, that is, the store paths currently existing in the Nix store that refer to @@ -356,11 +361,13 @@ symlink. in the Nix store that are dependent on *paths*. - `--deriver`; `-d`\ - Prints the [deriver](../glossary.md) of the store paths *paths*. If + Prints the [deriver] of the store paths *paths*. If the path has no deriver (e.g., if it is a source file), or if the deriver is not known (e.g., in the case of a binary-only deployment), the string `unknown-deriver` is printed. + [deriver]: ../glossary.md#gloss-deriver + - `--graph`\ Prints the references graph of the store paths *paths* in the format of the `dot` tool of AT\&T's [Graphviz diff --git a/doc/manual/src/contributing/hacking.md b/doc/manual/src/contributing/hacking.md index c9da1962f..aeb0d41b3 100644 --- a/doc/manual/src/contributing/hacking.md +++ b/doc/manual/src/contributing/hacking.md @@ -249,3 +249,36 @@ search/replaced in it for each new build. The installer now supports a `--tarball-url-prefix` flag which _may_ have solved this need? --> + +### Checking links in the manual + +The build checks for broken internal links. +This happens late in the process, so `nix build` is not suitable for iterating. +To build the manual incrementally, run: + +```console +make html -j $NIX_BUILD_CORES +``` + +In order to reflect changes to the [Makefile], clear all generated files before re-building: + +[Makefile]: https://github.com/NixOS/nix/blob/master/doc/manual/local.mk + +```console +rm $(git ls-files doc/manual/ -o | grep -F '.md') && rmdir doc/manual/src/command-ref/new-cli && make html -j $NIX_BUILD_CORES +``` + +[`mdbook-linkcheck`] does not implement checking [URI fragments] yet. + +[`mdbook-linkcheck`]: https://github.com/Michael-F-Bryan/mdbook-linkcheck +[URI fragments]: https://en.m.wikipedia.org/wiki/URI_fragment + +#### `@docroot@` variable + +`@docroot@` provides a base path for links that occur in reusable snippets or other documentation that doesn't have a base path of its own. + +If a broken link occurs in a snippet that was inserted into multiple generated files in different directories, use `@docroot@` to reference the `doc/manual/src` directory. + +If the `@docroot@` literal appears in an error message from the `mdbook-linkcheck` tool, the `@docroot@` replacement needs to be applied to the generated source file that mentions it. +See existing `@docroot@` logic in the [Makefile]. +Regular markdown files used for the manual have a base path of their own and they can use relative paths instead of `@docroot@`. diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index e63f6becc..5a49af8dc 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -19,6 +19,17 @@ [store derivation]: #gloss-store-derivation + - [realise]{#gloss-realise}, realisation\ + Ensure a [store path] is [valid][validity]. + + This means either running the `builder` executable as specified in the corresponding [derivation] or fetching a pre-built [store object] from a [substituter]. + + See [`nix-build`](./command-ref/nix-build.md) and [`nix-store --realise`](./command-ref/nix-store.md#operation---realise). + + See [`nix build`](./command-ref/new-cli/nix3-build.md) (experimental). + + [realise]: #gloss-realise + - [content-addressed derivation]{#gloss-content-addressed-derivation}\ A derivation which has the [`__contentAddressed`](./language/advanced-attributes.md#adv-attr-__contentAddressed) @@ -101,6 +112,8 @@ copy store objects it doesn't have. For details, see the [`substituters` option](./command-ref/conf-file.md#conf-substituters). + [substituter]: #gloss-substituter + - [purity]{#gloss-purity}\ The assumption that equal Nix derivations when run always produce the same output. This cannot be guaranteed in general (e.g., a @@ -149,13 +162,15 @@ [output path]: #gloss-output-path - [deriver]{#gloss-deriver}\ - The deriver of an *output path* is the store - derivation that built it. + The [store derivation] that produced an [output path]. - [validity]{#gloss-validity}\ - A store path is considered *valid* if it exists in the file system, - is listed in the Nix database as being valid, and if all paths in - its closure are also valid. + A store path is valid if all [store object]s in its [closure] can be read from the [store]. + + For a local store, this means: + - The store path leads to an existing [store object] in that [store]. + - The store path is listed in the Nix database as being valid. + - All paths in the store path's [closure] are valid. - [user environment]{#gloss-user-env}\ An automatically generated store object that consists of a set of diff --git a/doc/manual/src/language/index.md b/doc/manual/src/language/index.md index db34fde75..31300631c 100644 --- a/doc/manual/src/language/index.md +++ b/doc/manual/src/language/index.md @@ -191,12 +191,12 @@ This is an incomplete overview of language features, by example. <tr> <td> - <nixpkgs> + `<nixpkgs>` </td> <td> - Search path. Value determined by [`$NIX_PATH` environment variable](../command-ref/env-common.md#env-NIX_PATH). + Search path for Nix files. Value determined by [`$NIX_PATH` environment variable](../command-ref/env-common.md#env-NIX_PATH). </td> </tr> diff --git a/doc/manual/src/language/operators.md b/doc/manual/src/language/operators.md index 797f13bd3..1f918bd4d 100644 --- a/doc/manual/src/language/operators.md +++ b/doc/manual/src/language/operators.md @@ -24,7 +24,7 @@ | [Equality] | *expr* `==` *expr* | none | 11 | | Inequality | *expr* `!=` *expr* | none | 11 | | Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 | -| Logical disjunction (`OR`) | *bool* `||` *bool* | left | 13 | +| Logical disjunction (`OR`) | *bool* `\|\|` *bool* | left | 13 | | [Logical implication] | *bool* `->` *bool* | none | 14 | [string]: ./values.md#type-string @@ -120,12 +120,12 @@ The result is a string. ## Update -> *attrset1* + *attrset2* +> *attrset1* // *attrset2* Update [attribute set] *attrset1* with names and values from *attrset2*. -The returned attribute set will have of all the attributes in *e1* and *e2*. -If an attribute name is present in both, the attribute value from the former is taken. +The returned attribute set will have of all the attributes in *attrset1* and *attrset2*. +If an attribute name is present in both, the attribute value from the latter is taken. [Update]: #update diff --git a/doc/manual/src/package-management/binary-cache-substituter.md b/doc/manual/src/package-management/binary-cache-substituter.md index ef738794b..5befad9f8 100644 --- a/doc/manual/src/package-management/binary-cache-substituter.md +++ b/doc/manual/src/package-management/binary-cache-substituter.md @@ -32,13 +32,13 @@ which should print something like: Priority: 30 On the client side, you can tell Nix to use your binary cache using -`--option extra-binary-caches`, e.g.: +`--substituters`, e.g.: ```console -$ nix-env -iA nixpkgs.firefox --option extra-binary-caches http://avalon:8080/ +$ nix-env -iA nixpkgs.firefox --substituters http://avalon:8080/ ``` -The option `extra-binary-caches` tells Nix to use this binary cache in +The option `substituters` tells Nix to use this binary cache in addition to your default caches, such as <https://cache.nixos.org>. Thus, for any path in the closure of Firefox, Nix will first check if the path is available on the server `avalon` or another binary caches. @@ -47,4 +47,4 @@ If not, it will fall back to building from source. You can also tell Nix to always use your binary cache by adding a line to the `nix.conf` configuration file like this: - binary-caches = http://avalon:8080/ https://cache.nixos.org/ + substituters = http://avalon:8080/ https://cache.nixos.org/ diff --git a/doc/manual/src/release-notes/rl-2.13.md b/doc/manual/src/release-notes/rl-2.13.md new file mode 100644 index 000000000..2b79620be --- /dev/null +++ b/doc/manual/src/release-notes/rl-2.13.md @@ -0,0 +1,40 @@ +# Release 2.13 (2023-01-17) + +* The `repeat` and `enforce-determinism` options have been removed + since they had been broken under many circumstances for a long time. + +* You can now use [flake references] in the [old command line interface], e.g. + + [flake references]: ../command-ref/new-cli/nix3-flake.md#flake-references + [old command line interface]: ../command-ref/main-commands.md + + ```shell-session + # nix-build flake:nixpkgs -A hello + # nix-build -I nixpkgs=flake:github:NixOS/nixpkgs/nixos-22.05 \ + '<nixpkgs>' -A hello + # NIX_PATH=nixpkgs=flake:nixpkgs nix-build '<nixpkgs>' -A hello + ``` + +* Instead of "antiquotation", the more common term [string interpolation](../language/string-interpolation.md) is now used consistently. + Historical release notes were not changed. + +* Allow explicitly selecting outputs in a store derivation installable, just like we can do with other sorts of installables. + For example, + ```shell-session + # nix-build /nix/store/gzaflydcr6sb3567hap9q6srzx8ggdgg-glibc-2.33-78.drv^dev + ``` + now works just as + ```shell-session + # nix-build glibc^dev + ``` + does already. + +* On Linux, `nix develop` now sets the + [*personality*](https://man7.org/linux/man-pages/man2/personality.2.html) + for the development shell in the same way as the actual build of the + derivation. This makes shells for `i686-linux` derivations work + correctly on `x86_64-linux`. + +* You can now disable the global flake registry by setting the `flake-registry` + configuration option to an empty string. The same can be achieved at runtime with + `--flake-registry ""`. diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index 9f491efc8..78ae99f4b 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -1,23 +1,2 @@ # Release X.Y (202?-??-??) -* The `repeat` and `enforce-determinism` options have been removed - since they had been broken under many circumstances for a long time. - -* You can now use [flake references] in the [old command line interface], e.g. - - [flake references]: ../command-ref/new-cli/nix3-flake.md#flake-references - [old command line interface]: ../command-ref/main-commands.md - - ``` - # nix-build flake:nixpkgs -A hello - # nix-build -I nixpkgs=flake:github:NixOS/nixpkgs/nixos-22.05 \ - '<nixpkgs>' -A hello - # NIX_PATH=nixpkgs=flake:nixpkgs nix-build '<nixpkgs>' -A hello - ``` - -* Instead of "antiquotation", the more common term [string interpolation](../language/string-interpolation.md) is now used consistently. - Historical release notes were not changed. - -* Error traces have been reworked to provide detailed explanations and more - accurate error locations. A short excerpt of the trace is now shown by - default when an error occurs. @@ -96,6 +96,7 @@ buildPackages.flex (lib.getBin buildPackages.lowdown-nix) buildPackages.mdbook + buildPackages.mdbook-linkcheck buildPackages.autoconf-archive buildPackages.autoreconfHook buildPackages.pkg-config @@ -531,6 +532,12 @@ mkdir $out ''; + tests.nixpkgsLibTests = + nixpkgs.lib.genAttrs systems (system: + import (nixpkgs + "/lib/tests/release.nix") + { pkgs = nixpkgsFor.${system}; } + ); + metrics.nixpkgs = import "${nixpkgs-regression}/pkgs/top-level/metrics.nix" { pkgs = nixpkgsFor.x86_64-linux; nixpkgs = nixpkgs-regression; @@ -561,6 +568,7 @@ binaryTarball = self.hydraJobs.binaryTarball.${system}; perlBindings = self.hydraJobs.perlBindings.${system}; installTests = self.hydraJobs.installTests.${system}; + nixpkgsLibTests = self.hydraJobs.tests.nixpkgsLibTests.${system}; } // (nixpkgs.lib.optionalAttrs (builtins.elem system linux64BitSystems)) { dockerImage = self.hydraJobs.dockerImage.${system}; }); diff --git a/maintainers/README.md b/maintainers/README.md index 60768db0a..08d197c1b 100644 --- a/maintainers/README.md +++ b/maintainers/README.md @@ -36,17 +36,45 @@ Issues on the board progress through the following states: - No Status - Team members can add pull requests or issues to discuss or review together. - During the discussion meeting, the team triages new items. + To be considered, issues and pull requests must have a high-level description to provide the whole team with the necessary context at a glance. + + On every meeting, at least one item from each of the following categories is inspected: + + 1. [critical](https://github.com/NixOS/nix/labels/critical) + 2. [security](https://github.com/NixOS/nix/labels/security) + 3. [regression](https://github.com/NixOS/nix/labels/regression) + 4. [bug](https://github.com/NixOS/nix/issues?q=is%3Aopen+label%3Abug+sort%3Areactions-%2B1-desc) + + - [oldest pull requests](https://github.com/NixOS/nix/pulls?q=is%3Apr+is%3Aopen+sort%3Acreated-asc) + - [most popular pull requests](https://github.com/NixOS/nix/pulls?q=is%3Apr+is%3Aopen+sort%3Areactions-%2B1-desc) + - [oldest issues](https://github.com/NixOS/nix/issues?q=is%3Aissue+is%3Aopen+sort%3Acreated-asc) + - [most popular issues](https://github.com/NixOS/nix/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc) + + Team members can also add pull requests or issues they would like the whole team to consider. + If there is disagreement on the general idea behind an issue or pull request, it is moved to _To discuss_, otherwise to _In review_. - To discuss - Pull requests and issues that are important and controversial are discussed by the team during discussion meetings. + Pull requests and issues that are deemed important and controversial are discussed by the team during discussion meetings. This may be where the merit of the change itself or the implementation strategy is contested by a team member. + As a general guideline, the order of items is determined as follows: + + - Prioritise pull requests over issues + + Contributors who took the time to implement concrete change proposals should not wait indefinitely. + + - Prioritise fixing bugs over documentation, improvements or new features + + The team values stability and accessibility higher than raw functionality. + + - Interleave issues and PRs + + This way issues without attempts at a solution get a chance to get addressed. + - In review Pull requests in this column are reviewed together during work meetings. diff --git a/scripts/install-systemd-multi-user.sh b/scripts/install-systemd-multi-user.sh index 62397127a..7dd567747 100755 --- a/scripts/install-systemd-multi-user.sh +++ b/scripts/install-systemd-multi-user.sh @@ -24,12 +24,17 @@ $1 EOF } +escape_systemd_env() { + temp_var="${1//\'/\\\'}" + echo "${temp_var//\%/%%}" +} + # Gather all non-empty proxy environment variables into a string create_systemd_proxy_env() { vars="http_proxy https_proxy ftp_proxy no_proxy HTTP_PROXY HTTPS_PROXY FTP_PROXY NO_PROXY" for v in $vars; do if [ "x${!v:-}" != "x" ]; then - echo "Environment=${v}=${!v}" + echo "Environment=${v}=$(escape_systemd_env ${!v})" fi done } diff --git a/src/libcmd/common-eval-args.cc b/src/libcmd/common-eval-args.cc index 0e321e5e4..908127b4d 100644 --- a/src/libcmd/common-eval-args.cc +++ b/src/libcmd/common-eval-args.cc @@ -34,8 +34,8 @@ MixEvalArgs::MixEvalArgs() .shortName = 'I', .description = R"( Add *path* to the Nix search path. The Nix search path is - initialized from the colon-separated [`NIX_PATH`](./env-common.md#env-NIX_PATH) environment - variable, and is used to look up the location of Nix expressions using [paths](../language/values.md#type-path) enclosed in angle + initialized from the colon-separated [`NIX_PATH`](@docroot@/command-ref/env-common.md#env-NIX_PATH) environment + variable, and is used to look up the location of Nix expressions using [paths](@docroot@/language/values.md#type-path) enclosed in angle brackets (i.e., `<nixpkgs>`). For instance, passing diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index c0db2a715..60d6e9dc0 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -1,5 +1,6 @@ #include "globals.hh" #include "installables.hh" +#include "outputs-spec.hh" #include "util.hh" #include "command.hh" #include "attr-path.hh" @@ -401,18 +402,6 @@ struct InstallableStorePath : Installable ref<Store> store; DerivedPath req; - InstallableStorePath(ref<Store> store, StorePath && storePath) - : store(store), - req(storePath.isDerivation() - ? (DerivedPath) DerivedPath::Built { - .drvPath = std::move(storePath), - .outputs = {}, - } - : (DerivedPath) DerivedPath::Opaque { - .path = std::move(storePath), - }) - { } - InstallableStorePath(ref<Store> store, DerivedPath && req) : store(store), req(std::move(req)) { } @@ -445,19 +434,19 @@ struct InstallableAttrPath : InstallableValue SourceExprCommand & cmd; RootValue v; std::string attrPath; - OutputsSpec outputsSpec; + ExtendedOutputsSpec extendedOutputsSpec; InstallableAttrPath( ref<EvalState> state, SourceExprCommand & cmd, Value * v, const std::string & attrPath, - OutputsSpec outputsSpec) + ExtendedOutputsSpec extendedOutputsSpec) : InstallableValue(state) , cmd(cmd) , v(allocRootValue(v)) , attrPath(attrPath) - , outputsSpec(std::move(outputsSpec)) + , extendedOutputsSpec(std::move(extendedOutputsSpec)) { } std::string what() const override { return attrPath; } @@ -480,30 +469,39 @@ struct InstallableAttrPath : InstallableValue // Backward compatibility hack: group results by drvPath. This // helps keep .all output together. - std::map<StorePath, DerivedPath::Built> byDrvPath; + std::map<StorePath, OutputsSpec> byDrvPath; for (auto & drvInfo : drvInfos) { auto drvPath = drvInfo.queryDrvPath(); if (!drvPath) throw Error("'%s' is not a derivation", what()); - std::set<std::string> outputsToInstall; - - if (auto outputNames = std::get_if<OutputNames>(&outputsSpec)) - outputsToInstall = *outputNames; - else - for (auto & output : drvInfo.queryOutputs(false, std::get_if<DefaultOutputs>(&outputsSpec))) - outputsToInstall.insert(output.first); + auto newOutputs = std::visit(overloaded { + [&](const ExtendedOutputsSpec::Default & d) -> OutputsSpec { + std::set<std::string> outputsToInstall; + for (auto & output : drvInfo.queryOutputs(false, true)) + outputsToInstall.insert(output.first); + return OutputsSpec::Names { std::move(outputsToInstall) }; + }, + [&](const ExtendedOutputsSpec::Explicit & e) -> OutputsSpec { + return e; + }, + }, extendedOutputsSpec.raw()); - auto derivedPath = byDrvPath.emplace(*drvPath, DerivedPath::Built { .drvPath = *drvPath }).first; + auto [iter, didInsert] = byDrvPath.emplace(*drvPath, newOutputs); - for (auto & output : outputsToInstall) - derivedPath->second.outputs.insert(output); + if (!didInsert) + iter->second = iter->second.union_(newOutputs); } DerivedPathsWithInfo res; - for (auto & [_, info] : byDrvPath) - res.push_back({ .path = { info } }); + for (auto & [drvPath, outputs] : byDrvPath) + res.push_back({ + .path = DerivedPath::Built { + .drvPath = drvPath, + .outputs = outputs, + }, + }); return res; } @@ -556,7 +554,7 @@ ref<eval_cache::EvalCache> openEvalCache( auto vFlake = state.allocValue(); flake::callFlake(state, *lockedFlake, *vFlake); - state.forceAttrs(*vFlake, noPos, "while parsing cached flake data"); + state.forceAttrs(*vFlake, noPos); auto aOutputs = vFlake->attrs->get(state.symbols.create("outputs")); assert(aOutputs); @@ -580,7 +578,7 @@ InstallableFlake::InstallableFlake( ref<EvalState> state, FlakeRef && flakeRef, std::string_view fragment, - OutputsSpec outputsSpec, + ExtendedOutputsSpec extendedOutputsSpec, Strings attrPaths, Strings prefixes, const flake::LockFlags & lockFlags) @@ -588,7 +586,7 @@ InstallableFlake::InstallableFlake( flakeRef(flakeRef), attrPaths(fragment == "" ? attrPaths : Strings{(std::string) fragment}), prefixes(fragment == "" ? Strings{} : prefixes), - outputsSpec(std::move(outputsSpec)), + extendedOutputsSpec(std::move(extendedOutputsSpec)), lockFlags(lockFlags) { if (cmd && cmd->getAutoArgs(*state)->size()) @@ -620,7 +618,7 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths() else if (v.type() == nString) { PathSet context; - auto s = state->forceString(v, context, noPos, fmt("while evaluating the flake output attribute '%s'", attrPath)); + auto s = state->forceString(v, context, noPos); auto storePath = state->store->maybeParseStorePath(s); if (storePath && context.count(std::string(s))) { return {{ @@ -638,48 +636,47 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths() auto drvPath = attr->forceDerivation(); - std::set<std::string> outputsToInstall; std::optional<NixInt> priority; - if (auto aOutputSpecified = attr->maybeGetAttr(state->sOutputSpecified)) { - if (aOutputSpecified->getBool()) { - if (auto aOutputName = attr->maybeGetAttr("outputName")) - outputsToInstall = { aOutputName->getString() }; - } - } - - else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) { - if (auto aOutputsToInstall = aMeta->maybeGetAttr("outputsToInstall")) - for (auto & s : aOutputsToInstall->getListOfStrings()) - outputsToInstall.insert(s); + if (attr->maybeGetAttr(state->sOutputSpecified)) { + } else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) { if (auto aPriority = aMeta->maybeGetAttr("priority")) priority = aPriority->getInt(); } - if (outputsToInstall.empty() || std::get_if<AllOutputs>(&outputsSpec)) { - outputsToInstall.clear(); - if (auto aOutputs = attr->maybeGetAttr(state->sOutputs)) - for (auto & s : aOutputs->getListOfStrings()) - outputsToInstall.insert(s); - } - - if (outputsToInstall.empty()) - outputsToInstall.insert("out"); - - if (auto outputNames = std::get_if<OutputNames>(&outputsSpec)) - outputsToInstall = *outputNames; - return {{ .path = DerivedPath::Built { .drvPath = std::move(drvPath), - .outputs = std::move(outputsToInstall), + .outputs = std::visit(overloaded { + [&](const ExtendedOutputsSpec::Default & d) -> OutputsSpec { + std::set<std::string> outputsToInstall; + if (auto aOutputSpecified = attr->maybeGetAttr(state->sOutputSpecified)) { + if (aOutputSpecified->getBool()) { + if (auto aOutputName = attr->maybeGetAttr("outputName")) + outputsToInstall = { aOutputName->getString() }; + } + } else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) { + if (auto aOutputsToInstall = aMeta->maybeGetAttr("outputsToInstall")) + for (auto & s : aOutputsToInstall->getListOfStrings()) + outputsToInstall.insert(s); + } + + if (outputsToInstall.empty()) + outputsToInstall.insert("out"); + + return OutputsSpec::Names { std::move(outputsToInstall) }; + }, + [&](const ExtendedOutputsSpec::Explicit & e) -> OutputsSpec { + return e; + }, + }, extendedOutputsSpec.raw()), }, .info = { .priority = priority, .originalRef = flakeRef, .resolvedRef = getLockedFlake()->flake.lockedRef, .attrPath = attrPath, - .outputsSpec = outputsSpec, + .extendedOutputsSpec = extendedOutputsSpec, } }}; } @@ -796,12 +793,12 @@ std::vector<std::shared_ptr<Installable>> SourceExprCommand::parseInstallables( } for (auto & s : ss) { - auto [prefix, outputsSpec] = parseOutputsSpec(s); + auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(s); result.push_back( std::make_shared<InstallableAttrPath>( state, *this, vFile, - prefix == "." ? "" : prefix, - outputsSpec)); + prefix == "." ? "" : std::string { prefix }, + extendedOutputsSpec)); } } else { @@ -809,24 +806,46 @@ std::vector<std::shared_ptr<Installable>> SourceExprCommand::parseInstallables( for (auto & s : ss) { std::exception_ptr ex; - auto found = s.rfind('^'); - if (found != std::string::npos) { - try { - result.push_back(std::make_shared<InstallableStorePath>( - store, - DerivedPath::Built::parse(*store, s.substr(0, found), s.substr(found + 1)))); - continue; - } catch (BadStorePath &) { - } catch (...) { - if (!ex) - ex = std::current_exception(); - } - } + auto [prefix_, extendedOutputsSpec_] = ExtendedOutputsSpec::parse(s); + // To avoid clang's pedantry + auto prefix = std::move(prefix_); + auto extendedOutputsSpec = std::move(extendedOutputsSpec_); - found = s.find('/'); + auto found = prefix.find('/'); if (found != std::string::npos) { try { - result.push_back(std::make_shared<InstallableStorePath>(store, store->followLinksToStorePath(s))); + auto derivedPath = std::visit(overloaded { + // If the user did not use ^, we treat the output more liberally. + [&](const ExtendedOutputsSpec::Default &) -> DerivedPath { + // First, we accept a symlink chain or an actual store path. + auto storePath = store->followLinksToStorePath(prefix); + // Second, we see if the store path ends in `.drv` to decide what sort + // of derived path they want. + // + // This handling predates the `^` syntax. The `^*` in + // `/nix/store/hash-foo.drv^*` unambiguously means "do the + // `DerivedPath::Built` case", so plain `/nix/store/hash-foo.drv` could + // also unambiguously mean "do the DerivedPath::Opaque` case". + // + // Issue #7261 tracks reconsidering this `.drv` dispatching. + return storePath.isDerivation() + ? (DerivedPath) DerivedPath::Built { + .drvPath = std::move(storePath), + .outputs = OutputsSpec::All {}, + } + : (DerivedPath) DerivedPath::Opaque { + .path = std::move(storePath), + }; + }, + // If the user did use ^, we just do exactly what is written. + [&](const ExtendedOutputsSpec::Explicit & outputSpec) -> DerivedPath { + return DerivedPath::Built { + .drvPath = store->parseStorePath(prefix), + .outputs = outputSpec, + }; + }, + }, extendedOutputsSpec.raw()); + result.push_back(std::make_shared<InstallableStorePath>(store, std::move(derivedPath))); continue; } catch (BadStorePath &) { } catch (...) { @@ -836,13 +855,13 @@ std::vector<std::shared_ptr<Installable>> SourceExprCommand::parseInstallables( } try { - auto [flakeRef, fragment, outputsSpec] = parseFlakeRefWithFragmentAndOutputsSpec(s, absPath(".")); + auto [flakeRef, fragment] = parseFlakeRefWithFragment(std::string { prefix }, absPath(".")); result.push_back(std::make_shared<InstallableFlake>( this, getEvalState(), std::move(flakeRef), fragment, - outputsSpec, + extendedOutputsSpec, getDefaultFlakeAttrPaths(), getDefaultFlakeAttrPathPrefixes(), lockFlags)); @@ -917,32 +936,7 @@ std::vector<std::pair<std::shared_ptr<Installable>, BuiltPathWithResult>> Instal for (auto & aux : backmap[path]) { std::visit(overloaded { [&](const DerivedPath::Built & bfd) { - OutputPathMap outputs; - auto drv = evalStore->readDerivation(bfd.drvPath); - auto outputHashes = staticOutputHashes(*evalStore, drv); // FIXME: expensive - auto drvOutputs = drv.outputsAndOptPaths(*store); - for (auto & output : bfd.outputs) { - auto outputHash = get(outputHashes, output); - if (!outputHash) - throw Error( - "the derivation '%s' doesn't have an output named '%s'", - store->printStorePath(bfd.drvPath), output); - if (settings.isExperimentalFeatureEnabled(Xp::CaDerivations)) { - DrvOutput outputId { *outputHash, output }; - auto realisation = store->queryRealisation(outputId); - if (!realisation) - throw MissingRealisation(outputId); - outputs.insert_or_assign(output, realisation->outPath); - } else { - // If ca-derivations isn't enabled, assume that - // the output path is statically known. - auto drvOutput = get(drvOutputs, output); - assert(drvOutput); - assert(drvOutput->second); - outputs.insert_or_assign( - output, *drvOutput->second); - } - } + auto outputs = resolveDerivedPath(*store, bfd, &*evalStore); res.push_back({aux.installable, { .path = BuiltPath::Built { bfd.drvPath, outputs }, .info = aux.info}}); diff --git a/src/libcmd/installables.hh b/src/libcmd/installables.hh index 250cf7e83..3d12639b0 100644 --- a/src/libcmd/installables.hh +++ b/src/libcmd/installables.hh @@ -2,7 +2,7 @@ #include "util.hh" #include "path.hh" -#include "path-with-outputs.hh" +#include "outputs-spec.hh" #include "derived-path.hh" #include "eval.hh" #include "store-api.hh" @@ -20,7 +20,7 @@ namespace eval_cache { class EvalCache; class AttrCursor; } struct App { - std::vector<StorePathWithOutputs> context; + std::vector<DerivedPath> context; Path program; // FIXME: add args, sandbox settings, metadata, ... }; @@ -59,7 +59,7 @@ struct ExtraPathInfo std::optional<FlakeRef> resolvedRef; std::optional<std::string> attrPath; // FIXME: merge with DerivedPath's 'outputs' field? - std::optional<OutputsSpec> outputsSpec; + std::optional<ExtendedOutputsSpec> extendedOutputsSpec; }; /* A derived path with any additional info that commands might @@ -169,7 +169,7 @@ struct InstallableFlake : InstallableValue FlakeRef flakeRef; Strings attrPaths; Strings prefixes; - OutputsSpec outputsSpec; + ExtendedOutputsSpec extendedOutputsSpec; const flake::LockFlags & lockFlags; mutable std::shared_ptr<flake::LockedFlake> _lockedFlake; @@ -178,7 +178,7 @@ struct InstallableFlake : InstallableValue ref<EvalState> state, FlakeRef && flakeRef, std::string_view fragment, - OutputsSpec outputsSpec, + ExtendedOutputsSpec extendedOutputsSpec, Strings attrPaths, Strings prefixes, const flake::LockFlags & lockFlags); diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index 71a7e079a..b7f691808 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -397,7 +397,7 @@ StringSet NixRepl::completePrefix(const std::string & prefix) Expr * e = parseString(expr); Value v; e->eval(*state, *env, v); - state->forceAttrs(v, noPos, "nevermind, it is ignored anyway"); + state->forceAttrs(v, noPos); for (auto & i : *v.attrs) { std::string_view name = state->symbols[i.name]; @@ -590,7 +590,7 @@ bool NixRepl::processLine(std::string line) const auto [path, line] = [&] () -> std::pair<Path, uint32_t> { if (v.type() == nPath || v.type() == nString) { PathSet context; - auto path = state->coerceToPath(noPos, v, context, "while evaluating the filename to edit"); + auto path = state->coerceToPath(noPos, v, context); return {path, 0}; } else if (v.isLambda()) { auto pos = state->positions[v.lambda.fun->pos]; @@ -641,7 +641,12 @@ bool NixRepl::processLine(std::string line) Path drvPathRaw = state->store->printStorePath(drvPath); if (command == ":b" || command == ":bl") { - state->store->buildPaths({DerivedPath::Built{drvPath}}); + state->store->buildPaths({ + DerivedPath::Built { + .drvPath = drvPath, + .outputs = OutputsSpec::All { }, + }, + }); auto drv = state->store->readDerivation(drvPath); logger->cout("\nThis derivation produced the following outputs:"); for (auto & [outputName, outputPath] : state->store->queryDerivationOutputMap(drvPath)) { @@ -834,7 +839,7 @@ void NixRepl::loadFiles() void NixRepl::addAttrsToScope(Value & attrs) { - state->forceAttrs(attrs, [&]() { return attrs.determinePos(noPos); }, "while evaluating an attribute set to be merged in the global scope"); + state->forceAttrs(attrs, [&]() { return attrs.determinePos(noPos); }); if (displ + attrs.attrs->size() >= envSize) throw Error("environment full; cannot add more variables"); @@ -939,7 +944,7 @@ std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int m Bindings::iterator i = v.attrs->find(state->sDrvPath); PathSet context; if (i != v.attrs->end()) - str << state->store->printStorePath(state->coerceToStorePath(i->pos, *i->value, context, "while evaluating the drvPath of a derivation")); + str << state->store->printStorePath(state->coerceToStorePath(i->pos, *i->value, context)); else str << "???"; str << "»"; diff --git a/src/libexpr/attr-path.cc b/src/libexpr/attr-path.cc index 7c0705091..94ab60f9a 100644 --- a/src/libexpr/attr-path.cc +++ b/src/libexpr/attr-path.cc @@ -118,7 +118,7 @@ std::pair<std::string, uint32_t> findPackageFilename(EvalState & state, Value & // FIXME: is it possible to extract the Pos object instead of doing this // toString + parsing? - auto pos = state.forceString(*v2, noPos, "while evaluating the 'meta.position' attribute of a derivation"); + auto pos = state.forceString(*v2); auto colon = pos.rfind(':'); if (colon == std::string::npos) diff --git a/src/libexpr/eval-cache.cc b/src/libexpr/eval-cache.cc index afe575fee..f8c4275a1 100644 --- a/src/libexpr/eval-cache.cc +++ b/src/libexpr/eval-cache.cc @@ -300,7 +300,7 @@ struct AttrDb NixStringContext context; if (!queryAttribute.isNull(3)) for (auto & s : tokenizeString<std::vector<std::string>>(queryAttribute.getStr(3), ";")) - context.push_back(decodeContext(cfg, s)); + context.push_back(NixStringContextElem::parse(cfg, s)); return {{rowId, string_t{queryAttribute.getStr(2), context}}}; } case AttrType::Bool: @@ -385,7 +385,7 @@ Value & AttrCursor::getValue() if (!_value) { if (parent) { auto & vParent = parent->first->getValue(); - root->state.forceAttrs(vParent, noPos, "while searching for an attribute"); + root->state.forceAttrs(vParent, noPos); auto attr = vParent.attrs->get(parent->second); if (!attr) throw Error("attribute '%s' is unexpectedly missing", getAttrPathStr()); @@ -571,14 +571,14 @@ std::string AttrCursor::getString() debug("using cached string attribute '%s'", getAttrPathStr()); return s->first; } else - root->state.error("'%s' is not a string", getAttrPathStr()).debugThrow<TypeError>(); + root->state.debugThrowLastTrace(TypeError("'%s' is not a string", getAttrPathStr())); } } auto & v = forceValue(); if (v.type() != nString && v.type() != nPath) - root->state.error("'%s' is not a string but %s", getAttrPathStr()).debugThrow<TypeError>(); + root->state.debugThrowLastTrace(TypeError("'%s' is not a string but %s", getAttrPathStr(), showType(v.type()))); return v.type() == nString ? v.string.s : v.path; } @@ -592,7 +592,18 @@ string_t AttrCursor::getStringWithContext() if (auto s = std::get_if<string_t>(&cachedValue->second)) { bool valid = true; for (auto & c : s->second) { - if (!root->state.store->isValidPath(c.first)) { + const StorePath & path = std::visit(overloaded { + [&](const NixStringContextElem::DrvDeep & d) -> const StorePath & { + return d.drvPath; + }, + [&](const NixStringContextElem::Built & b) -> const StorePath & { + return b.drvPath; + }, + [&](const NixStringContextElem::Opaque & o) -> const StorePath & { + return o.path; + }, + }, c.raw()); + if (!root->state.store->isValidPath(path)) { valid = false; break; } @@ -602,7 +613,7 @@ string_t AttrCursor::getStringWithContext() return *s; } } else - root->state.error("'%s' is not a string", getAttrPathStr()).debugThrow<TypeError>(); + root->state.debugThrowLastTrace(TypeError("'%s' is not a string", getAttrPathStr())); } } @@ -613,7 +624,7 @@ string_t AttrCursor::getStringWithContext() else if (v.type() == nPath) return {v.path, {}}; else - root->state.error("'%s' is not a string but %s", getAttrPathStr()).debugThrow<TypeError>(); + root->state.debugThrowLastTrace(TypeError("'%s' is not a string but %s", getAttrPathStr(), showType(v.type()))); } bool AttrCursor::getBool() @@ -626,14 +637,14 @@ bool AttrCursor::getBool() debug("using cached Boolean attribute '%s'", getAttrPathStr()); return *b; } else - root->state.error("'%s' is not a Boolean", getAttrPathStr()).debugThrow<TypeError>(); + root->state.debugThrowLastTrace(TypeError("'%s' is not a Boolean", getAttrPathStr())); } } auto & v = forceValue(); if (v.type() != nBool) - root->state.error("'%s' is not a Boolean", getAttrPathStr()).debugThrow<TypeError>(); + root->state.debugThrowLastTrace(TypeError("'%s' is not a Boolean", getAttrPathStr())); return v.boolean; } @@ -685,7 +696,7 @@ std::vector<std::string> AttrCursor::getListOfStrings() std::vector<std::string> res; for (auto & elem : v.listItems()) - res.push_back(std::string(root->state.forceStringNoCtx(*elem, noPos, "while evaluating an attribute for caching"))); + res.push_back(std::string(root->state.forceStringNoCtx(*elem))); if (root->db) cachedValue = {root->db->setListOfStrings(getKey(), res), res}; @@ -703,14 +714,14 @@ std::vector<Symbol> AttrCursor::getAttrs() debug("using cached attrset attribute '%s'", getAttrPathStr()); return *attrs; } else - root->state.error("'%s' is not an attribute set", getAttrPathStr()).debugThrow<TypeError>(); + root->state.debugThrowLastTrace(TypeError("'%s' is not an attribute set", getAttrPathStr())); } } auto & v = forceValue(); if (v.type() != nAttrs) - root->state.error("'%s' is not an attribute set", getAttrPathStr()).debugThrow<TypeError>(); + root->state.debugThrowLastTrace(TypeError("'%s' is not an attribute set", getAttrPathStr())); std::vector<Symbol> attrs; for (auto & attr : *getValue().attrs) diff --git a/src/libexpr/eval-inline.hh b/src/libexpr/eval-inline.hh index f0da688db..f2f4ba725 100644 --- a/src/libexpr/eval-inline.hh +++ b/src/libexpr/eval-inline.hh @@ -103,36 +103,33 @@ void EvalState::forceValue(Value & v, Callable getPos) else if (v.isApp()) callFunction(*v.app.left, *v.app.right, v, noPos); else if (v.isBlackhole()) - error("infinite recursion encountered").atPos(getPos()).template debugThrow<EvalError>(); + throwEvalError(getPos(), "infinite recursion encountered"); } [[gnu::always_inline]] -inline void EvalState::forceAttrs(Value & v, const PosIdx pos, std::string_view errorCtx) +inline void EvalState::forceAttrs(Value & v, const PosIdx pos) { - forceAttrs(v, [&]() { return pos; }, errorCtx); + forceAttrs(v, [&]() { return pos; }); } template <typename Callable> [[gnu::always_inline]] -inline void EvalState::forceAttrs(Value & v, Callable getPos, std::string_view errorCtx) +inline void EvalState::forceAttrs(Value & v, Callable getPos) { - forceValue(v, noPos); - if (v.type() != nAttrs) { - PosIdx pos = getPos(); - error("value is %1% while a set was expected", showType(v)).withTrace(pos, errorCtx).debugThrow<TypeError>(); - } + forceValue(v, getPos); + if (v.type() != nAttrs) + throwTypeError(getPos(), "value is %1% while a set was expected", v); } [[gnu::always_inline]] -inline void EvalState::forceList(Value & v, const PosIdx pos, std::string_view errorCtx) +inline void EvalState::forceList(Value & v, const PosIdx pos) { - forceValue(v, noPos); - if (!v.isList()) { - error("value is %1% while a list was expected", showType(v)).withTrace(pos, errorCtx).debugThrow<TypeError>(); - } + forceValue(v, pos); + if (!v.isList()) + throwTypeError(pos, "value is %1% while a list was expected", v); } diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 978b0f0e2..9bc20a502 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -318,7 +318,7 @@ static Symbol getName(const AttrName & name, EvalState & state, Env & env) } else { Value nameValue; name.expr->eval(state, env, nameValue); - state.forceStringNoCtx(nameValue, noPos, "while evaluating an attribute name"); + state.forceStringNoCtx(nameValue); return state.symbols.create(nameValue.string.s); } } @@ -414,44 +414,6 @@ static Strings parseNixPath(const std::string & s) return res; } -ErrorBuilder & ErrorBuilder::atPos(PosIdx pos) -{ - info.errPos = state.positions[pos]; - return *this; -} - -ErrorBuilder & ErrorBuilder::withTrace(PosIdx pos, const std::string_view text) -{ - info.traces.push_front(Trace{ .pos = state.positions[pos], .hint = hintformat(std::string(text)), .frame = false }); - return *this; -} - -ErrorBuilder & ErrorBuilder::withFrameTrace(PosIdx pos, const std::string_view text) -{ - info.traces.push_front(Trace{ .pos = state.positions[pos], .hint = hintformat(std::string(text)), .frame = true }); - return *this; -} - -ErrorBuilder & ErrorBuilder::withSuggestions(Suggestions & s) -{ - info.suggestions = s; - return *this; -} - -ErrorBuilder & ErrorBuilder::withFrame(const Env & env, const Expr & expr) -{ - // NOTE: This is abusing side-effects. - // TODO: check compatibility with nested debugger calls. - state.debugTraces.push_front(DebugTrace { - .pos = nullptr, - .expr = expr, - .env = env, - .hint = hintformat("Fake frame for debugging purposes"), - .isError = true - }); - return *this; -} - EvalState::EvalState( const Strings & _searchPath, @@ -684,7 +646,25 @@ void EvalState::addConstant(const std::string & name, Value * v) Value * EvalState::addPrimOp(const std::string & name, size_t arity, PrimOpFun primOp) { - return addPrimOp(PrimOp { .fun = primOp, .arity = arity, .name = name }); + auto name2 = name.substr(0, 2) == "__" ? name.substr(2) : name; + auto sym = symbols.create(name2); + + /* Hack to make constants lazy: turn them into a application of + the primop to a dummy value. */ + if (arity == 0) { + auto vPrimOp = allocValue(); + vPrimOp->mkPrimOp(new PrimOp { .fun = primOp, .arity = 1, .name = name2 }); + Value v; + v.mkApp(vPrimOp, vPrimOp); + return addConstant(name, v); + } + + Value * v = allocValue(); + v->mkPrimOp(new PrimOp { .fun = primOp, .arity = arity, .name = name2 }); + staticBaseEnv->vars.emplace_back(symbols.create(name), baseEnvDispl); + baseEnv.values[baseEnvDispl++] = v; + baseEnv.values[0]->attrs->push_back(Attr(sym, v)); + return v; } @@ -862,14 +842,176 @@ void EvalState::runDebugRepl(const Error * error, const Env & env, const Expr & } } +/* Every "format" object (even temporary) takes up a few hundred bytes + of stack space, which is a real killer in the recursive + evaluator. So here are some helper functions for throwing + exceptions. */ +void EvalState::throwEvalError(const PosIdx pos, const char * s, Env & env, Expr & expr) +{ + debugThrow(EvalError({ + .msg = hintfmt(s), + .errPos = positions[pos] + }), env, expr); +} + +void EvalState::throwEvalError(const PosIdx pos, const char * s) +{ + debugThrowLastTrace(EvalError({ + .msg = hintfmt(s), + .errPos = positions[pos] + })); +} + +void EvalState::throwEvalError(const char * s, const std::string & s2) +{ + debugThrowLastTrace(EvalError(s, s2)); +} + +void EvalState::throwEvalError(const PosIdx pos, const Suggestions & suggestions, const char * s, + const std::string & s2, Env & env, Expr & expr) +{ + debugThrow(EvalError(ErrorInfo{ + .msg = hintfmt(s, s2), + .errPos = positions[pos], + .suggestions = suggestions, + }), env, expr); +} + +void EvalState::throwEvalError(const PosIdx pos, const char * s, const std::string & s2) +{ + debugThrowLastTrace(EvalError({ + .msg = hintfmt(s, s2), + .errPos = positions[pos] + })); +} + +void EvalState::throwEvalError(const PosIdx pos, const char * s, const std::string & s2, Env & env, Expr & expr) +{ + debugThrow(EvalError({ + .msg = hintfmt(s, s2), + .errPos = positions[pos] + }), env, expr); +} + +void EvalState::throwEvalError(const char * s, const std::string & s2, + const std::string & s3) +{ + debugThrowLastTrace(EvalError({ + .msg = hintfmt(s, s2, s3), + .errPos = positions[noPos] + })); +} + +void EvalState::throwEvalError(const PosIdx pos, const char * s, const std::string & s2, + const std::string & s3) +{ + debugThrowLastTrace(EvalError({ + .msg = hintfmt(s, s2, s3), + .errPos = positions[pos] + })); +} + +void EvalState::throwEvalError(const PosIdx pos, const char * s, const std::string & s2, + const std::string & s3, Env & env, Expr & expr) +{ + debugThrow(EvalError({ + .msg = hintfmt(s, s2, s3), + .errPos = positions[pos] + }), env, expr); +} + +void EvalState::throwEvalError(const PosIdx p1, const char * s, const Symbol sym, const PosIdx p2, Env & env, Expr & expr) +{ + // p1 is where the error occurred; p2 is a position mentioned in the message. + debugThrow(EvalError({ + .msg = hintfmt(s, symbols[sym], positions[p2]), + .errPos = positions[p1] + }), env, expr); +} + +void EvalState::throwTypeError(const PosIdx pos, const char * s, const Value & v) +{ + debugThrowLastTrace(TypeError({ + .msg = hintfmt(s, showType(v)), + .errPos = positions[pos] + })); +} + +void EvalState::throwTypeError(const PosIdx pos, const char * s, const Value & v, Env & env, Expr & expr) +{ + debugThrow(TypeError({ + .msg = hintfmt(s, showType(v)), + .errPos = positions[pos] + }), env, expr); +} + +void EvalState::throwTypeError(const PosIdx pos, const char * s) +{ + debugThrowLastTrace(TypeError({ + .msg = hintfmt(s), + .errPos = positions[pos] + })); +} + +void EvalState::throwTypeError(const PosIdx pos, const char * s, const ExprLambda & fun, + const Symbol s2, Env & env, Expr &expr) +{ + debugThrow(TypeError({ + .msg = hintfmt(s, fun.showNamePos(*this), symbols[s2]), + .errPos = positions[pos] + }), env, expr); +} + +void EvalState::throwTypeError(const PosIdx pos, const Suggestions & suggestions, const char * s, + const ExprLambda & fun, const Symbol s2, Env & env, Expr &expr) +{ + debugThrow(TypeError(ErrorInfo { + .msg = hintfmt(s, fun.showNamePos(*this), symbols[s2]), + .errPos = positions[pos], + .suggestions = suggestions, + }), env, expr); +} + +void EvalState::throwTypeError(const char * s, const Value & v, Env & env, Expr &expr) +{ + debugThrow(TypeError({ + .msg = hintfmt(s, showType(v)), + .errPos = positions[expr.getPos()], + }), env, expr); +} + +void EvalState::throwAssertionError(const PosIdx pos, const char * s, const std::string & s1, Env & env, Expr &expr) +{ + debugThrow(AssertionError({ + .msg = hintfmt(s, s1), + .errPos = positions[pos] + }), env, expr); +} + +void EvalState::throwUndefinedVarError(const PosIdx pos, const char * s, const std::string & s1, Env & env, Expr &expr) +{ + debugThrow(UndefinedVarError({ + .msg = hintfmt(s, s1), + .errPos = positions[pos] + }), env, expr); +} + +void EvalState::throwMissingArgumentError(const PosIdx pos, const char * s, const std::string & s1, Env & env, Expr &expr) +{ + debugThrow(MissingArgumentError({ + .msg = hintfmt(s, s1), + .errPos = positions[pos] + }), env, expr); +} + void EvalState::addErrorTrace(Error & e, const char * s, const std::string & s2) const { e.addTrace(nullptr, s, s2); } -void EvalState::addErrorTrace(Error & e, const PosIdx pos, const char * s, const std::string & s2, bool frame) const +void EvalState::addErrorTrace(Error & e, const PosIdx pos, const char * s, const std::string & s2) const { - e.addTrace(positions[pos], hintfmt(s, s2), frame); + e.addTrace(positions[pos], s, s2); } static std::unique_ptr<DebugTraceStacker> makeDebugTraceStacker( @@ -946,7 +1088,7 @@ inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval) if (env->type == Env::HasWithExpr) { if (noEval) return 0; Value * v = allocValue(); - evalAttrs(*env->up, (Expr *) env->values[0], *v, noPos, "<borked>"); + evalAttrs(*env->up, (Expr *) env->values[0], *v); env->values[0] = v; env->type = Env::HasWithAttrs; } @@ -956,7 +1098,7 @@ inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval) return j->value; } if (!env->prevWith) - error("undefined variable '%1%'", symbols[var.name]).atPos(var.pos).withFrame(*env, var).debugThrow<UndefinedVarError>(); + throwUndefinedVarError(var.pos, "undefined variable '%1%'", symbols[var.name], *env, const_cast<ExprVar&>(var)); for (size_t l = env->prevWith; l; --l, env = env->up) ; } } @@ -1106,7 +1248,7 @@ void EvalState::cacheFile( // computation. if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e))) - error("file '%s' must be an attribute set", path).debugThrow<EvalError>(); + throw EvalError("file '%s' must be an attribute set", path); eval(e, v); } catch (Error & e) { addErrorTrace(e, "while evaluating the file '%1%':", resolvedPath); @@ -1124,31 +1266,31 @@ void EvalState::eval(Expr * e, Value & v) } -inline bool EvalState::evalBool(Env & env, Expr * e, const PosIdx pos, std::string_view errorCtx) +inline bool EvalState::evalBool(Env & env, Expr * e) { - try { - Value v; - e->eval(*this, env, v); - if (v.type() != nBool) - error("value is %1% while a Boolean was expected", showType(v)).withFrame(env, *e).debugThrow<TypeError>(); - return v.boolean; - } catch (Error & e) { - e.addTrace(positions[pos], errorCtx); - throw; - } + Value v; + e->eval(*this, env, v); + if (v.type() != nBool) + throwTypeError(noPos, "value is %1% while a Boolean was expected", v, env, *e); + return v.boolean; } -inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v, const PosIdx pos, std::string_view errorCtx) +inline bool EvalState::evalBool(Env & env, Expr * e, const PosIdx pos) { - try { - e->eval(*this, env, v); - if (v.type() != nAttrs) - error("value is %1% while a set was expected", showType(v)).withFrame(env, *e).debugThrow<TypeError>(); - } catch (Error & e) { - e.addTrace(positions[pos], errorCtx); - throw; - } + Value v; + e->eval(*this, env, v); + if (v.type() != nBool) + throwTypeError(pos, "value is %1% while a Boolean was expected", v, env, *e); + return v.boolean; +} + + +inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v) +{ + e->eval(*this, env, v); + if (v.type() != nAttrs) + throwTypeError(noPos, "value is %1% while a set was expected", v, env, *e); } @@ -1221,7 +1363,7 @@ void ExprAttrs::eval(EvalState & state, Env & env, Value & v) Hence we need __overrides.) */ if (hasOverrides) { Value * vOverrides = (*v.attrs)[overrides->second.displ].value; - state.forceAttrs(*vOverrides, [&]() { return vOverrides->determinePos(noPos); }, "while evaluating the `__overrides` attribute"); + state.forceAttrs(*vOverrides, [&]() { return vOverrides->determinePos(noPos); }); Bindings * newBnds = state.allocBindings(v.attrs->capacity() + vOverrides->attrs->size()); for (auto & i : *v.attrs) newBnds->push_back(i); @@ -1249,11 +1391,11 @@ void ExprAttrs::eval(EvalState & state, Env & env, Value & v) state.forceValue(nameVal, i.pos); if (nameVal.type() == nNull) continue; - state.forceStringNoCtx(nameVal, i.pos, "while evaluating the name of a dynamic attribute"); + state.forceStringNoCtx(nameVal); auto nameSym = state.symbols.create(nameVal.string.s); Bindings::iterator j = v.attrs->find(nameSym); if (j != v.attrs->end()) - state.error("dynamic attribute '%1%' already defined at %2%", state.symbols[nameSym], state.positions[j->pos]).atPos(i.pos).withFrame(env, *this).debugThrow<EvalError>(); + state.throwEvalError(i.pos, "dynamic attribute '%1%' already defined at %2%", nameSym, j->pos, env, *this); i.valueExpr->setName(nameSym); /* Keep sorted order so find can catch duplicates */ @@ -1350,14 +1492,15 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v) return; } } else { - state.forceAttrs(*vAttrs, pos, "while selecting an attribute"); + state.forceAttrs(*vAttrs, pos); if ((j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) { std::set<std::string> allAttrNames; for (auto & attr : *vAttrs->attrs) allAttrNames.insert(state.symbols[attr.name]); - auto suggestions = Suggestions::bestMatches(allAttrNames, state.symbols[name]); - state.error("attribute '%1%' missing", state.symbols[name]) - .atPos(pos).withSuggestions(suggestions).withFrame(env, *this).debugThrow<EvalError>(); + state.throwEvalError( + pos, + Suggestions::bestMatches(allAttrNames, state.symbols[name]), + "attribute '%1%' missing", state.symbols[name], env, *this); } } vAttrs = j->value; @@ -1452,12 +1595,7 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & if (!lambda.hasFormals()) env2.values[displ++] = args[0]; else { - try { - forceAttrs(*args[0], lambda.pos, "while evaluating the value passed for the lambda argument"); - } catch (Error & e) { - if (pos) e.addTrace(positions[pos], "from call site"); - throw; - } + forceAttrs(*args[0], pos); if (lambda.arg) env2.values[displ++] = args[0]; @@ -1469,15 +1607,8 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & for (auto & i : lambda.formals->formals) { auto j = args[0]->attrs->get(i.name); if (!j) { - if (!i.def) { - error("function '%1%' called without required argument '%2%'", - (lambda.name ? std::string(symbols[lambda.name]) : "anonymous lambda"), - symbols[i.name]) - .atPos(lambda.pos) - .withTrace(pos, "from call site") - .withFrame(*fun.lambda.env, lambda) - .debugThrow<TypeError>(); - } + if (!i.def) throwTypeError(pos, "%1% called without required argument '%2%'", + lambda, i.name, *fun.lambda.env, lambda); env2.values[displ++] = i.def->maybeThunk(*this, env2); } else { attrsUsed++; @@ -1495,15 +1626,11 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & std::set<std::string> formalNames; for (auto & formal : lambda.formals->formals) formalNames.insert(symbols[formal.name]); - auto suggestions = Suggestions::bestMatches(formalNames, symbols[i.name]); - error("function '%1%' called with unexpected argument '%2%'", - (lambda.name ? std::string(symbols[lambda.name]) : "anonymous lambda"), - symbols[i.name]) - .atPos(lambda.pos) - .withTrace(pos, "from call site") - .withSuggestions(suggestions) - .withFrame(*fun.lambda.env, lambda) - .debugThrow<TypeError>(); + throwTypeError( + pos, + Suggestions::bestMatches(formalNames, symbols[i.name]), + "%1% called with unexpected argument '%2%'", + lambda, i.name, *fun.lambda.env, lambda); } abort(); // can't happen } @@ -1526,15 +1653,11 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & lambda.body->eval(*this, env2, vCur); } catch (Error & e) { if (loggerSettings.showTrace.get()) { - addErrorTrace( - e, - lambda.pos, - "while calling %s", - lambda.name - ? concatStrings("'", symbols[lambda.name], "'") - : "anonymous lambda", - true); - if (pos) addErrorTrace(e, pos, "from call site%s", "", true); + addErrorTrace(e, lambda.pos, "while calling %s", + (lambda.name + ? concatStrings("'", symbols[lambda.name], "'") + : "anonymous lambda")); + addErrorTrace(e, pos, "while evaluating call site%s", ""); } throw; } @@ -1553,17 +1676,9 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & return; } else { /* We have all the arguments, so call the primop. */ - auto name = vCur.primOp->name; - nrPrimOpCalls++; - if (countCalls) primOpCalls[name]++; - - try { - vCur.primOp->fun(*this, noPos, args, vCur); - } catch (Error & e) { - addErrorTrace(e, pos, "while calling the '%1%' builtin", name); - throw; - } + if (countCalls) primOpCalls[vCur.primOp->name]++; + vCur.primOp->fun(*this, pos, args, vCur); nrArgs -= argsLeft; args += argsLeft; @@ -1598,20 +1713,9 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & for (size_t i = 0; i < argsLeft; ++i) vArgs[argsDone + i] = args[i]; - auto name = primOp->primOp->name; nrPrimOpCalls++; - if (countCalls) primOpCalls[name]++; - - try { - // TODO: - // 1. Unify this and above code. Heavily redundant. - // 2. Create a fake env (arg1, arg2, etc.) and a fake expr (arg1: arg2: etc: builtins.name arg1 arg2 etc) - // so the debugger allows to inspect the wrong parameters passed to the builtin. - primOp->primOp->fun(*this, noPos, vArgs, vCur); - } catch (Error & e) { - addErrorTrace(e, pos, "while calling the '%1%' builtin", name); - throw; - } + if (countCalls) primOpCalls[primOp->primOp->name]++; + primOp->primOp->fun(*this, pos, vArgs, vCur); nrArgs -= argsLeft; args += argsLeft; @@ -1624,18 +1728,14 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & heap-allocate a copy and use that instead. */ Value * args2[] = {allocValue(), args[0]}; *args2[0] = vCur; - try { - callFunction(*functor->value, 2, args2, vCur, functor->pos); - } catch (Error & e) { - e.addTrace(positions[pos], "while calling a functor (an attribute set with a '__functor' attribute)"); - throw; - } + /* !!! Should we use the attr pos here? */ + callFunction(*functor->value, 2, args2, vCur, pos); nrArgs--; args++; } else - error("attempt to call something which is not a function but %1%", showType(vCur)).atPos(pos).debugThrow<TypeError>(); + throwTypeError(pos, "attempt to call something which is not a function but %1%", vCur); } vRes = vCur; @@ -1699,12 +1799,13 @@ void EvalState::autoCallFunction(Bindings & args, Value & fun, Value & res) if (j != args.end()) { attrs.insert(*j); } else if (!i.def) { - error(R"(cannot evaluate a function that has an argument without a value ('%1%') + throwMissingArgumentError(i.pos, R"(cannot evaluate a function that has an argument without a value ('%1%') + Nix attempted to evaluate a function as a top level expression; in this case it must have its arguments supplied either by default values, or passed explicitly with '--arg' or '--argstr'. See -https://nixos.org/manual/nix/stable/language/constructs.html#functions.)", symbols[i.name]) - .atPos(i.pos).withFrame(*fun.lambda.env, *fun.lambda.fun).debugThrow<MissingArgumentError>(); +https://nixos.org/manual/nix/stable/language/constructs.html#functions.)", symbols[i.name], + *fun.lambda.env, *fun.lambda.fun); } } } @@ -1727,17 +1828,16 @@ void ExprWith::eval(EvalState & state, Env & env, Value & v) void ExprIf::eval(EvalState & state, Env & env, Value & v) { - // We cheat in the parser, and pass the position of the condition as the position of the if itself. - (state.evalBool(env, cond, pos, "while evaluating a branch condition") ? then : else_)->eval(state, env, v); + (state.evalBool(env, cond, pos) ? then : else_)->eval(state, env, v); } void ExprAssert::eval(EvalState & state, Env & env, Value & v) { - if (!state.evalBool(env, cond, pos, "in the condition of the assert statement")) { + if (!state.evalBool(env, cond, pos)) { std::ostringstream out; cond->show(state.symbols, out); - state.error("assertion '%1%' failed", out.str()).atPos(pos).withFrame(env, *this).debugThrow<AssertionError>(); + state.throwAssertionError(pos, "assertion '%1%' failed", out.str(), env, *this); } body->eval(state, env, v); } @@ -1745,7 +1845,7 @@ void ExprAssert::eval(EvalState & state, Env & env, Value & v) void ExprOpNot::eval(EvalState & state, Env & env, Value & v) { - v.mkBool(!state.evalBool(env, e, noPos, "in the argument of the not operator")); // XXX: FIXME: ! + v.mkBool(!state.evalBool(env, e)); } @@ -1753,7 +1853,7 @@ void ExprOpEq::eval(EvalState & state, Env & env, Value & v) { Value v1; e1->eval(state, env, v1); Value v2; e2->eval(state, env, v2); - v.mkBool(state.eqValues(v1, v2, pos, "while testing two values for equality")); + v.mkBool(state.eqValues(v1, v2)); } @@ -1761,33 +1861,33 @@ void ExprOpNEq::eval(EvalState & state, Env & env, Value & v) { Value v1; e1->eval(state, env, v1); Value v2; e2->eval(state, env, v2); - v.mkBool(!state.eqValues(v1, v2, pos, "while testing two values for inequality")); + v.mkBool(!state.eqValues(v1, v2)); } void ExprOpAnd::eval(EvalState & state, Env & env, Value & v) { - v.mkBool(state.evalBool(env, e1, pos, "in the left operand of the AND (&&) operator") && state.evalBool(env, e2, pos, "in the right operand of the AND (&&) operator")); + v.mkBool(state.evalBool(env, e1, pos) && state.evalBool(env, e2, pos)); } void ExprOpOr::eval(EvalState & state, Env & env, Value & v) { - v.mkBool(state.evalBool(env, e1, pos, "in the left operand of the OR (||) operator") || state.evalBool(env, e2, pos, "in the right operand of the OR (||) operator")); + v.mkBool(state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos)); } void ExprOpImpl::eval(EvalState & state, Env & env, Value & v) { - v.mkBool(!state.evalBool(env, e1, pos, "in the left operand of the IMPL (->) operator") || state.evalBool(env, e2, pos, "in the right operand of the IMPL (->) operator")); + v.mkBool(!state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos)); } void ExprOpUpdate::eval(EvalState & state, Env & env, Value & v) { Value v1, v2; - state.evalAttrs(env, e1, v1, pos, "in the left operand of the update (//) operator"); - state.evalAttrs(env, e2, v2, pos, "in the right operand of the update (//) operator"); + state.evalAttrs(env, e1, v1); + state.evalAttrs(env, e2, v2); state.nrOpUpdates++; @@ -1826,18 +1926,18 @@ void ExprOpConcatLists::eval(EvalState & state, Env & env, Value & v) Value v1; e1->eval(state, env, v1); Value v2; e2->eval(state, env, v2); Value * lists[2] = { &v1, &v2 }; - state.concatLists(v, 2, lists, pos, "while evaluating one of the elements to concatenate"); + state.concatLists(v, 2, lists, pos); } -void EvalState::concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos, std::string_view errorCtx) +void EvalState::concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos) { nrListConcats++; Value * nonEmpty = 0; size_t len = 0; for (size_t n = 0; n < nrLists; ++n) { - forceList(*lists[n], pos, errorCtx); + forceList(*lists[n], pos); auto l = lists[n]->listSize(); len += l; if (l) nonEmpty = lists[n]; @@ -1914,20 +2014,20 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) nf = n; nf += vTmp.fpoint; } else - state.error("cannot add %1% to an integer", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow<EvalError>(); + state.throwEvalError(i_pos, "cannot add %1% to an integer", showType(vTmp), env, *this); } else if (firstType == nFloat) { if (vTmp.type() == nInt) { nf += vTmp.integer; } else if (vTmp.type() == nFloat) { nf += vTmp.fpoint; } else - state.error("cannot add %1% to a float", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow<EvalError>(); + state.throwEvalError(i_pos, "cannot add %1% to a float", showType(vTmp), env, *this); } else { if (s.empty()) s.reserve(es->size()); /* skip canonization of first path, which would only be not canonized in the first place if it's coming from a ./${foo} type path */ - auto part = state.coerceToString(i_pos, vTmp, context, false, firstType == nString, !first, "while evaluating a path segment"); + auto part = state.coerceToString(i_pos, vTmp, context, false, firstType == nString, !first); sSize += part->size(); s.emplace_back(std::move(part)); } @@ -1941,7 +2041,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) v.mkFloat(nf); else if (firstType == nPath) { if (!context.empty()) - state.error("a string that refers to a store path cannot be appended to a path").atPos(pos).withFrame(env, *this).debugThrow<EvalError>(); + state.throwEvalError(pos, "a string that refers to a store path cannot be appended to a path", env, *this); v.mkPath(canonPath(str())); } else v.mkStringMove(c_str(), context); @@ -1991,47 +2091,33 @@ void EvalState::forceValueDeep(Value & v) } -NixInt EvalState::forceInt(Value & v, const PosIdx pos, std::string_view errorCtx) +NixInt EvalState::forceInt(Value & v, const PosIdx pos) { - try { - forceValue(v, pos); - if (v.type() != nInt) - error("value is %1% while an integer was expected", showType(v)).debugThrow<TypeError>(); - return v.integer; - } catch (Error & e) { - e.addTrace(positions[pos], errorCtx); - throw; - } + forceValue(v, pos); + if (v.type() != nInt) + throwTypeError(pos, "value is %1% while an integer was expected", v); + + return v.integer; } -NixFloat EvalState::forceFloat(Value & v, const PosIdx pos, std::string_view errorCtx) +NixFloat EvalState::forceFloat(Value & v, const PosIdx pos) { - try { - forceValue(v, pos); - if (v.type() == nInt) - return v.integer; - else if (v.type() != nFloat) - error("value is %1% while a float was expected", showType(v)).debugThrow<TypeError>(); - return v.fpoint; - } catch (Error & e) { - e.addTrace(positions[pos], errorCtx); - throw; - } + forceValue(v, pos); + if (v.type() == nInt) + return v.integer; + else if (v.type() != nFloat) + throwTypeError(pos, "value is %1% while a float was expected", v); + return v.fpoint; } -bool EvalState::forceBool(Value & v, const PosIdx pos, std::string_view errorCtx) +bool EvalState::forceBool(Value & v, const PosIdx pos) { - try { - forceValue(v, pos); - if (v.type() != nBool) - error("value is %1% while a Boolean was expected", showType(v)).debugThrow<TypeError>(); - return v.boolean; - } catch (Error & e) { - e.addTrace(positions[pos], errorCtx); - throw; - } + forceValue(v, pos); + if (v.type() != nBool) + throwTypeError(pos, "value is %1% while a Boolean was expected", v); + return v.boolean; } @@ -2041,51 +2127,21 @@ bool EvalState::isFunctor(Value & fun) } -void EvalState::forceFunction(Value & v, const PosIdx pos, std::string_view errorCtx) +void EvalState::forceFunction(Value & v, const PosIdx pos) { - try { - forceValue(v, pos); - if (v.type() != nFunction && !isFunctor(v)) - error("value is %1% while a function was expected", showType(v)).debugThrow<TypeError>(); - } catch (Error & e) { - e.addTrace(positions[pos], errorCtx); - throw; - } + forceValue(v, pos); + if (v.type() != nFunction && !isFunctor(v)) + throwTypeError(pos, "value is %1% while a function was expected", v); } -std::string_view EvalState::forceString(Value & v, const PosIdx pos, std::string_view errorCtx) +std::string_view EvalState::forceString(Value & v, const PosIdx pos) { - try { - forceValue(v, pos); - if (v.type() != nString) - error("value is %1% while a string was expected", showType(v)).debugThrow<TypeError>(); - return v.string.s; - } catch (Error & e) { - e.addTrace(positions[pos], errorCtx); - throw; + forceValue(v, pos); + if (v.type() != nString) { + throwTypeError(pos, "value is %1% while a string was expected", v); } -} - - -/* Decode a context string ‘!<name>!<path>’ into a pair <path, - name>. */ -NixStringContextElem decodeContext(const Store & store, std::string_view s) -{ - if (s.at(0) == '!') { - size_t index = s.find("!", 1); - return { - store.parseStorePath(s.substr(index + 1)), - std::string(s.substr(1, index - 1)), - }; - } else - return { - store.parseStorePath( - s.at(0) == '/' - ? s - : s.substr(1)), - "", - }; + return v.string.s; } @@ -2103,24 +2159,29 @@ NixStringContext Value::getContext(const Store & store) assert(internalType == tString); if (string.context) for (const char * * p = string.context; *p; ++p) - res.push_back(decodeContext(store, *p)); + res.push_back(NixStringContextElem::parse(store, *p)); return res; } -std::string_view EvalState::forceString(Value & v, PathSet & context, const PosIdx pos, std::string_view errorCtx) +std::string_view EvalState::forceString(Value & v, PathSet & context, const PosIdx pos) { - auto s = forceString(v, pos, errorCtx); + auto s = forceString(v, pos); copyContext(v, context); return s; } -std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos, std::string_view errorCtx) +std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos) { - auto s = forceString(v, pos, errorCtx); + auto s = forceString(v, pos); if (v.string.context) { - error("the string '%1%' is not allowed to refer to a store path (such as '%2%')", v.string.s, v.string.context[0]).withTrace(pos, errorCtx).debugThrow<EvalError>(); + if (pos) + throwEvalError(pos, "the string '%1%' is not allowed to refer to a store path (such as '%2%')", + v.string.s, v.string.context[0]); + else + throwEvalError("the string '%1%' is not allowed to refer to a store path (such as '%2%')", + v.string.s, v.string.context[0]); } return s; } @@ -2144,15 +2205,14 @@ std::optional<std::string> EvalState::tryAttrsToString(const PosIdx pos, Value & if (i != v.attrs->end()) { Value v1; callFunction(*i->value, v, v1, pos); - return coerceToString(pos, v1, context, coerceMore, copyToStore, - "while evaluating the result of the `toString` attribute").toOwned(); + return coerceToString(pos, v1, context, coerceMore, copyToStore).toOwned(); } return {}; } BackedStringView EvalState::coerceToString(const PosIdx pos, Value & v, PathSet & context, - bool coerceMore, bool copyToStore, bool canonicalizePath, std::string_view errorCtx) + bool coerceMore, bool copyToStore, bool canonicalizePath) { forceValue(v, pos); @@ -2176,12 +2236,12 @@ BackedStringView EvalState::coerceToString(const PosIdx pos, Value & v, PathSet return std::move(*maybeString); auto i = v.attrs->find(sOutPath); if (i == v.attrs->end()) - error("cannot coerce a set to a string", showType(v)).withTrace(pos, errorCtx).debugThrow<TypeError>(); - return coerceToString(pos, *i->value, context, coerceMore, copyToStore, canonicalizePath, errorCtx); + throwTypeError(pos, "cannot coerce a set to a string"); + return coerceToString(pos, *i->value, context, coerceMore, copyToStore); } if (v.type() == nExternal) - return v.external->coerceToString(positions[pos], context, coerceMore, copyToStore, errorCtx); + return v.external->coerceToString(positions[pos], context, coerceMore, copyToStore); if (coerceMore) { /* Note that `false' is represented as an empty string for @@ -2195,13 +2255,7 @@ BackedStringView EvalState::coerceToString(const PosIdx pos, Value & v, PathSet if (v.isList()) { std::string result; for (auto [n, v2] : enumerate(v.listItems())) { - try { - result += *coerceToString(noPos, *v2, context, coerceMore, copyToStore, canonicalizePath, - "while evaluating one element of the list"); - } catch (Error & e) { - e.addTrace(positions[pos], errorCtx); - throw; - } + result += *coerceToString(pos, *v2, context, coerceMore, copyToStore); if (n < v.listSize() - 1 /* !!! not quite correct */ && (!v2->isList() || v2->listSize() != 0)) @@ -2211,14 +2265,14 @@ BackedStringView EvalState::coerceToString(const PosIdx pos, Value & v, PathSet } } - error("cannot coerce %1% to a string", showType(v)).withTrace(pos, errorCtx).debugThrow<TypeError>(); + throwTypeError(pos, "cannot coerce %1% to a string", v); } StorePath EvalState::copyPathToStore(PathSet & context, const Path & path) { if (nix::isDerivation(path)) - error("file names are not allowed to end in '%1%'", drvExtension).debugThrow<EvalError>(); + throwEvalError("file names are not allowed to end in '%1%'", drvExtension); auto dstPath = [&]() -> StorePath { @@ -2239,25 +2293,28 @@ StorePath EvalState::copyPathToStore(PathSet & context, const Path & path) } -Path EvalState::coerceToPath(const PosIdx pos, Value & v, PathSet & context, std::string_view errorCtx) +Path EvalState::coerceToPath(const PosIdx pos, Value & v, PathSet & context) { - auto path = coerceToString(pos, v, context, false, false, true, errorCtx).toOwned(); + auto path = coerceToString(pos, v, context, false, false).toOwned(); if (path == "" || path[0] != '/') - error("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow<EvalError>(); + throwEvalError(pos, "string '%1%' doesn't represent an absolute path", path); return path; } -StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, PathSet & context, std::string_view errorCtx) +StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, PathSet & context) { - auto path = coerceToString(pos, v, context, false, false, true, errorCtx).toOwned(); + auto path = coerceToString(pos, v, context, false, false).toOwned(); if (auto storePath = store->maybeParseStorePath(path)) return *storePath; - error("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow<EvalError>(); + throw EvalError({ + .msg = hintfmt("path '%1%' is not in the Nix store", path), + .errPos = positions[pos] + }); } -bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx) +bool EvalState::eqValues(Value & v1, Value & v2) { forceValue(v1, noPos); forceValue(v2, noPos); @@ -2277,6 +2334,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v if (v1.type() != v2.type()) return false; switch (v1.type()) { + case nInt: return v1.integer == v2.integer; @@ -2295,7 +2353,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v case nList: if (v1.listSize() != v2.listSize()) return false; for (size_t n = 0; n < v1.listSize(); ++n) - if (!eqValues(*v1.listElems()[n], *v2.listElems()[n], pos, errorCtx)) return false; + if (!eqValues(*v1.listElems()[n], *v2.listElems()[n])) return false; return true; case nAttrs: { @@ -2305,7 +2363,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v Bindings::iterator i = v1.attrs->find(sOutPath); Bindings::iterator j = v2.attrs->find(sOutPath); if (i != v1.attrs->end() && j != v2.attrs->end()) - return eqValues(*i->value, *j->value, pos, errorCtx); + return eqValues(*i->value, *j->value); } if (v1.attrs->size() != v2.attrs->size()) return false; @@ -2313,7 +2371,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v /* Otherwise, compare the attributes one by one. */ Bindings::iterator i, j; for (i = v1.attrs->begin(), j = v2.attrs->begin(); i != v1.attrs->end(); ++i, ++j) - if (i->name != j->name || !eqValues(*i->value, *j->value, pos, errorCtx)) + if (i->name != j->name || !eqValues(*i->value, *j->value)) return false; return true; @@ -2330,7 +2388,9 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v return v1.fpoint == v2.fpoint; default: - error("cannot compare %1% with %2%", showType(v1), showType(v2)).withTrace(pos, errorCtx).debugThrow<EvalError>(); + throwEvalError("cannot compare %1% with %2%", + showType(v1), + showType(v2)); } } @@ -2454,13 +2514,12 @@ void EvalState::printStats() } -std::string ExternalValueBase::coerceToString(const Pos & pos, PathSet & context, bool copyMore, bool copyToStore, std::string_view errorCtx) const +std::string ExternalValueBase::coerceToString(const Pos & pos, PathSet & context, bool copyMore, bool copyToStore) const { - auto e = TypeError({ - .msg = hintfmt("cannot coerce %1% to a string", showType()) + throw TypeError({ + .msg = hintfmt("cannot coerce %1% to a string", showType()), + .errPos = pos }); - e.addTrace(pos, errorCtx); - throw e; } diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 4e0c4db95..df6ac431d 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -86,43 +86,6 @@ struct DebugTrace { void debugError(Error * e, Env & env, Expr & expr); -class ErrorBuilder -{ - private: - EvalState & state; - ErrorInfo info; - - ErrorBuilder(EvalState & s, ErrorInfo && i): state(s), info(i) { } - - public: - template<typename... Args> - [[nodiscard, gnu::noinline]] - static ErrorBuilder * create(EvalState & s, const Args & ... args) - { - return new ErrorBuilder(s, ErrorInfo { .msg = hintfmt(args...) }); - } - - [[nodiscard, gnu::noinline]] - ErrorBuilder & atPos(PosIdx pos); - - [[nodiscard, gnu::noinline]] - ErrorBuilder & withTrace(PosIdx pos, const std::string_view text); - - [[nodiscard, gnu::noinline]] - ErrorBuilder & withFrameTrace(PosIdx pos, const std::string_view text); - - [[nodiscard, gnu::noinline]] - ErrorBuilder & withSuggestions(Suggestions & s); - - [[nodiscard, gnu::noinline]] - ErrorBuilder & withFrame(const Env & e, const Expr & ex); - - template<class ErrorType> - [[gnu::noinline, gnu::noreturn]] - void debugThrow(); -}; - - class EvalState : public std::enable_shared_from_this<EvalState> { public: @@ -182,35 +145,29 @@ public: template<class E> [[gnu::noinline, gnu::noreturn]] - void debugThrowLastTrace(E && error) + void debugThrow(E && error, const Env & env, const Expr & expr) { - debugThrow(error, nullptr, nullptr); + if (debugRepl) + runDebugRepl(&error, env, expr); + + throw std::move(error); } template<class E> [[gnu::noinline, gnu::noreturn]] - void debugThrow(E && error, const Env * env, const Expr * expr) + void debugThrowLastTrace(E && e) { - if (debugRepl && ((env && expr) || !debugTraces.empty())) { - if (!env || !expr) { - const DebugTrace & last = debugTraces.front(); - env = &last.env; - expr = &last.expr; - } - runDebugRepl(&error, *env, *expr); + // Call this in the situation where Expr and Env are inaccessible. + // The debugger will start in the last context that's in the + // DebugTrace stack. + if (debugRepl && !debugTraces.empty()) { + const DebugTrace & last = debugTraces.front(); + runDebugRepl(&e, last.env, last.expr); } - throw std::move(error); + throw std::move(e); } - ErrorBuilder * errorBuilder; - - template<typename... Args> - [[nodiscard, gnu::noinline]] - ErrorBuilder & error(const Args & ... args) { - errorBuilder = ErrorBuilder::create(*this, args...); - return *errorBuilder; - } private: SrcToStore srcToStore; @@ -325,8 +282,8 @@ public: /* Evaluation the expression, then verify that it has the expected type. */ inline bool evalBool(Env & env, Expr * e); - inline bool evalBool(Env & env, Expr * e, const PosIdx pos, std::string_view errorCtx); - inline void evalAttrs(Env & env, Expr * e, Value & v, const PosIdx pos, std::string_view errorCtx); + inline bool evalBool(Env & env, Expr * e, const PosIdx pos); + inline void evalAttrs(Env & env, Expr * e, Value & v); /* If `v' is a thunk, enter it and overwrite `v' with the result of the evaluation of the thunk. If `v' is a delayed function @@ -342,25 +299,89 @@ public: void forceValueDeep(Value & v); /* Force `v', and then verify that it has the expected type. */ - NixInt forceInt(Value & v, const PosIdx pos, std::string_view errorCtx); - NixFloat forceFloat(Value & v, const PosIdx pos, std::string_view errorCtx); - bool forceBool(Value & v, const PosIdx pos, std::string_view errorCtx); + NixInt forceInt(Value & v, const PosIdx pos); + NixFloat forceFloat(Value & v, const PosIdx pos); + bool forceBool(Value & v, const PosIdx pos); - void forceAttrs(Value & v, const PosIdx pos, std::string_view errorCtx); + void forceAttrs(Value & v, const PosIdx pos); template <typename Callable> - inline void forceAttrs(Value & v, Callable getPos, std::string_view errorCtx); + inline void forceAttrs(Value & v, Callable getPos); - inline void forceList(Value & v, const PosIdx pos, std::string_view errorCtx); - void forceFunction(Value & v, const PosIdx pos, std::string_view errorCtx); // either lambda or primop - std::string_view forceString(Value & v, const PosIdx pos, std::string_view errorCtx); - std::string_view forceString(Value & v, PathSet & context, const PosIdx pos, std::string_view errorCtx); - std::string_view forceStringNoCtx(Value & v, const PosIdx pos, std::string_view errorCtx); + inline void forceList(Value & v, const PosIdx pos); + void forceFunction(Value & v, const PosIdx pos); // either lambda or primop + std::string_view forceString(Value & v, const PosIdx pos = noPos); + std::string_view forceString(Value & v, PathSet & context, const PosIdx pos = noPos); + std::string_view forceStringNoCtx(Value & v, const PosIdx pos = noPos); + + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const PosIdx pos, const char * s); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const PosIdx pos, const char * s, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const char * s, const std::string & s2); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const PosIdx pos, const char * s, const std::string & s2); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const char * s, const std::string & s2, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const PosIdx pos, const char * s, const std::string & s2, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const char * s, const std::string & s2, const std::string & s3, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const PosIdx pos, const char * s, const std::string & s2, const std::string & s3, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const PosIdx pos, const char * s, const std::string & s2, const std::string & s3); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const char * s, const std::string & s2, const std::string & s3); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const PosIdx pos, const Suggestions & suggestions, const char * s, const std::string & s2, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwEvalError(const PosIdx p1, const char * s, const Symbol sym, const PosIdx p2, + Env & env, Expr & expr); + + [[gnu::noinline, gnu::noreturn]] + void throwTypeError(const PosIdx pos, const char * s, const Value & v); + [[gnu::noinline, gnu::noreturn]] + void throwTypeError(const PosIdx pos, const char * s, const Value & v, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwTypeError(const PosIdx pos, const char * s); + [[gnu::noinline, gnu::noreturn]] + void throwTypeError(const PosIdx pos, const char * s, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwTypeError(const PosIdx pos, const char * s, const ExprLambda & fun, const Symbol s2, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwTypeError(const PosIdx pos, const Suggestions & suggestions, const char * s, const ExprLambda & fun, const Symbol s2, + Env & env, Expr & expr); + [[gnu::noinline, gnu::noreturn]] + void throwTypeError(const char * s, const Value & v, + Env & env, Expr & expr); + + [[gnu::noinline, gnu::noreturn]] + void throwAssertionError(const PosIdx pos, const char * s, const std::string & s1, + Env & env, Expr & expr); + + [[gnu::noinline, gnu::noreturn]] + void throwUndefinedVarError(const PosIdx pos, const char * s, const std::string & s1, + Env & env, Expr & expr); + + [[gnu::noinline, gnu::noreturn]] + void throwMissingArgumentError(const PosIdx pos, const char * s, const std::string & s1, + Env & env, Expr & expr); [[gnu::noinline]] void addErrorTrace(Error & e, const char * s, const std::string & s2) const; [[gnu::noinline]] - void addErrorTrace(Error & e, const PosIdx pos, const char * s, const std::string & s2, bool frame = false) const; + void addErrorTrace(Error & e, const PosIdx pos, const char * s, const std::string & s2) const; public: /* Return true iff the value `v' denotes a derivation (i.e. a @@ -376,18 +397,17 @@ public: referenced paths are copied to the Nix store as a side effect. */ BackedStringView coerceToString(const PosIdx pos, Value & v, PathSet & context, bool coerceMore = false, bool copyToStore = true, - bool canonicalizePath = true, - std::string_view errorCtx = ""); + bool canonicalizePath = true); StorePath copyPathToStore(PathSet & context, const Path & path); /* Path coercion. Converts strings, paths and derivations to a path. The result is guaranteed to be a canonicalised, absolute path. Nothing is copied to the store. */ - Path coerceToPath(const PosIdx pos, Value & v, PathSet & context, std::string_view errorCtx); + Path coerceToPath(const PosIdx pos, Value & v, PathSet & context); /* Like coerceToPath, but the result must be a store path. */ - StorePath coerceToStorePath(const PosIdx pos, Value & v, PathSet & context, std::string_view errorCtx); + StorePath coerceToStorePath(const PosIdx pos, Value & v, PathSet & context); public: @@ -447,7 +467,7 @@ public: /* Do a deep equality test between two values. That is, list elements and attributes are compared recursively. */ - bool eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx); + bool eqValues(Value & v1, Value & v2); bool isFunctor(Value & fun); @@ -482,7 +502,7 @@ public: void mkThunk_(Value & v, Expr * expr); void mkPos(Value & v, PosIdx pos); - void concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos, std::string_view errorCtx); + void concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos); /* Print statistics. */ void printStats(); @@ -551,10 +571,6 @@ struct DebugTraceStacker { std::string_view showType(ValueType type); std::string showType(const Value & v); -/* Decode a context string ‘!<name>!<path>’ into a pair <path, - name>. */ -NixStringContextElem decodeContext(const Store & store, std::string_view s); - /* If `path' refers to a directory, then append "/default.nix". */ Path resolveExprPath(Path path); @@ -649,13 +665,6 @@ extern EvalSettings evalSettings; static const std::string corepkgsPrefix{"/__corepkgs__/"}; -template<class ErrorType> -void ErrorBuilder::debugThrow() -{ - // NOTE: We always use the -LastTrace version as we push the new trace in withFrame() - state.debugThrowLastTrace(ErrorType(info)); -} - } #include "eval-inline.hh" diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index fc4be5678..105d32467 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -259,28 +259,28 @@ static Flake getFlake( if (setting.value->type() == nString) flake.config.settings.emplace( state.symbols[setting.name], - std::string(state.forceStringNoCtx(*setting.value, setting.pos, ""))); + std::string(state.forceStringNoCtx(*setting.value, setting.pos))); else if (setting.value->type() == nPath) { PathSet emptyContext = {}; flake.config.settings.emplace( state.symbols[setting.name], - state.coerceToString(setting.pos, *setting.value, emptyContext, false, true, true, "") .toOwned()); + state.coerceToString(setting.pos, *setting.value, emptyContext, false, true, true) .toOwned()); } else if (setting.value->type() == nInt) flake.config.settings.emplace( state.symbols[setting.name], - state.forceInt(*setting.value, setting.pos, "")); + state.forceInt(*setting.value, setting.pos)); else if (setting.value->type() == nBool) flake.config.settings.emplace( state.symbols[setting.name], - Explicit<bool> { state.forceBool(*setting.value, setting.pos, "") }); + Explicit<bool> { state.forceBool(*setting.value, setting.pos) }); else if (setting.value->type() == nList) { std::vector<std::string> ss; for (auto elem : setting.value->listItems()) { if (elem->type() != nString) throw TypeError("list element in flake configuration setting '%s' is %s while a string is expected", state.symbols[setting.name], showType(*setting.value)); - ss.emplace_back(state.forceStringNoCtx(*elem, setting.pos, "")); + ss.emplace_back(state.forceStringNoCtx(*elem, setting.pos)); } flake.config.settings.emplace(state.symbols[setting.name], ss); } @@ -741,7 +741,7 @@ void callFlake(EvalState & state, static void prim_getFlake(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - std::string flakeRefS(state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.getFlake")); + std::string flakeRefS(state.forceStringNoCtx(*args[0], pos)); auto flakeRef = parseFlakeRef(flakeRefS, {}, true); if (evalSettings.pureEval && !flakeRef.input.isLocked()) throw Error("cannot call 'getFlake' on unlocked flake reference '%s', at %s (use --impure to override)", flakeRefS, state.positions[pos]); diff --git a/src/libexpr/flake/flakeref.cc b/src/libexpr/flake/flakeref.cc index eede493f8..08adbe0c9 100644 --- a/src/libexpr/flake/flakeref.cc +++ b/src/libexpr/flake/flakeref.cc @@ -238,15 +238,15 @@ std::pair<fetchers::Tree, FlakeRef> FlakeRef::fetchTree(ref<Store> store) const return {std::move(tree), FlakeRef(std::move(lockedInput), subdir)}; } -std::tuple<FlakeRef, std::string, OutputsSpec> parseFlakeRefWithFragmentAndOutputsSpec( +std::tuple<FlakeRef, std::string, ExtendedOutputsSpec> parseFlakeRefWithFragmentAndExtendedOutputsSpec( const std::string & url, const std::optional<Path> & baseDir, bool allowMissing, bool isFlake) { - auto [prefix, outputsSpec] = parseOutputsSpec(url); - auto [flakeRef, fragment] = parseFlakeRefWithFragment(prefix, baseDir, allowMissing, isFlake); - return {std::move(flakeRef), fragment, outputsSpec}; + auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(url); + auto [flakeRef, fragment] = parseFlakeRefWithFragment(std::string { prefix }, baseDir, allowMissing, isFlake); + return {std::move(flakeRef), fragment, extendedOutputsSpec}; } } diff --git a/src/libexpr/flake/flakeref.hh b/src/libexpr/flake/flakeref.hh index a36d852a8..c4142fc20 100644 --- a/src/libexpr/flake/flakeref.hh +++ b/src/libexpr/flake/flakeref.hh @@ -3,7 +3,7 @@ #include "types.hh" #include "hash.hh" #include "fetchers.hh" -#include "path-with-outputs.hh" +#include "outputs-spec.hh" #include <variant> @@ -80,7 +80,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment( std::optional<std::pair<FlakeRef, std::string>> maybeParseFlakeRefWithFragment( const std::string & url, const std::optional<Path> & baseDir = {}); -std::tuple<FlakeRef, std::string, OutputsSpec> parseFlakeRefWithFragmentAndOutputsSpec( +std::tuple<FlakeRef, std::string, ExtendedOutputsSpec> parseFlakeRefWithFragmentAndExtendedOutputsSpec( const std::string & url, const std::optional<Path> & baseDir = {}, bool allowMissing = false, diff --git a/src/libexpr/get-drvs.cc b/src/libexpr/get-drvs.cc index 1602fbffb..5ad5d1fd4 100644 --- a/src/libexpr/get-drvs.cc +++ b/src/libexpr/get-drvs.cc @@ -51,7 +51,7 @@ std::string DrvInfo::queryName() const if (name == "" && attrs) { auto i = attrs->find(state->sName); if (i == attrs->end()) throw TypeError("derivation name missing"); - name = state->forceStringNoCtx(*i->value, noPos, "while evaluating the 'name' attribute of a derivation"); + name = state->forceStringNoCtx(*i->value); } return name; } @@ -61,7 +61,7 @@ std::string DrvInfo::querySystem() const { if (system == "" && attrs) { auto i = attrs->find(state->sSystem); - system = i == attrs->end() ? "unknown" : state->forceStringNoCtx(*i->value, i->pos, "while evaluating the 'system' attribute of a derivation"); + system = i == attrs->end() ? "unknown" : state->forceStringNoCtx(*i->value, i->pos); } return system; } @@ -75,7 +75,7 @@ std::optional<StorePath> DrvInfo::queryDrvPath() const if (i == attrs->end()) drvPath = {std::nullopt}; else - drvPath = {state->coerceToStorePath(i->pos, *i->value, context, "while evaluating the 'drvPath' attribute of a derivation")}; + drvPath = {state->coerceToStorePath(i->pos, *i->value, context)}; } return drvPath.value_or(std::nullopt); } @@ -95,7 +95,7 @@ StorePath DrvInfo::queryOutPath() const Bindings::iterator i = attrs->find(state->sOutPath); PathSet context; if (i != attrs->end()) - outPath = state->coerceToStorePath(i->pos, *i->value, context, "while evaluating the output path of a derivation"); + outPath = state->coerceToStorePath(i->pos, *i->value, context); } if (!outPath) throw UnimplementedError("CA derivations are not yet supported"); @@ -109,23 +109,23 @@ DrvInfo::Outputs DrvInfo::queryOutputs(bool withPaths, bool onlyOutputsToInstall /* Get the ‘outputs’ list. */ Bindings::iterator i; if (attrs && (i = attrs->find(state->sOutputs)) != attrs->end()) { - state->forceList(*i->value, i->pos, "while evaluating the 'outputs' attribute of a derivation"); + state->forceList(*i->value, i->pos); /* For each output... */ for (auto elem : i->value->listItems()) { - std::string output(state->forceStringNoCtx(*elem, i->pos, "while evaluating the name of an output of a derivation")); + std::string output(state->forceStringNoCtx(*elem, i->pos)); if (withPaths) { /* Evaluate the corresponding set. */ Bindings::iterator out = attrs->find(state->symbols.create(output)); if (out == attrs->end()) continue; // FIXME: throw error? - state->forceAttrs(*out->value, i->pos, "while evaluating an output of a derivation"); + state->forceAttrs(*out->value, i->pos); /* And evaluate its ‘outPath’ attribute. */ Bindings::iterator outPath = out->value->attrs->find(state->sOutPath); if (outPath == out->value->attrs->end()) continue; // FIXME: throw error? PathSet context; - outputs.emplace(output, state->coerceToStorePath(outPath->pos, *outPath->value, context, "while evaluating an output path of a derivation")); + outputs.emplace(output, state->coerceToStorePath(outPath->pos, *outPath->value, context)); } else outputs.emplace(output, std::nullopt); } @@ -137,7 +137,7 @@ DrvInfo::Outputs DrvInfo::queryOutputs(bool withPaths, bool onlyOutputsToInstall return outputs; Bindings::iterator i; - if (attrs && (i = attrs->find(state->sOutputSpecified)) != attrs->end() && state->forceBool(*i->value, i->pos, "while evaluating the 'outputSpecified' attribute of a derivation")) { + if (attrs && (i = attrs->find(state->sOutputSpecified)) != attrs->end() && state->forceBool(*i->value, i->pos)) { Outputs result; auto out = outputs.find(queryOutputName()); if (out == outputs.end()) @@ -169,7 +169,7 @@ std::string DrvInfo::queryOutputName() const { if (outputName == "" && attrs) { Bindings::iterator i = attrs->find(state->sOutputName); - outputName = i != attrs->end() ? state->forceStringNoCtx(*i->value, noPos, "while evaluating the output name of a derivation") : ""; + outputName = i != attrs->end() ? state->forceStringNoCtx(*i->value) : ""; } return outputName; } @@ -181,7 +181,7 @@ Bindings * DrvInfo::getMeta() if (!attrs) return 0; Bindings::iterator a = attrs->find(state->sMeta); if (a == attrs->end()) return 0; - state->forceAttrs(*a->value, a->pos, "while evaluating the 'meta' attribute of a derivation"); + state->forceAttrs(*a->value, a->pos); meta = a->value->attrs; return meta; } @@ -382,7 +382,7 @@ static void getDerivations(EvalState & state, Value & vIn, `recurseForDerivations = true' attribute. */ if (i->value->type() == nAttrs) { Bindings::iterator j = i->value->attrs->find(state.sRecurseForDerivations); - if (j != i->value->attrs->end() && state.forceBool(*j->value, j->pos, "while evaluating the attribute `recurseForDerivations`")) + if (j != i->value->attrs->end() && state.forceBool(*j->value, j->pos)) getDerivations(state, *i->value, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); } } diff --git a/src/libexpr/local.mk b/src/libexpr/local.mk index 016631647..2171e769b 100644 --- a/src/libexpr/local.mk +++ b/src/libexpr/local.mk @@ -6,6 +6,7 @@ libexpr_DIR := $(d) libexpr_SOURCES := \ $(wildcard $(d)/*.cc) \ + $(wildcard $(d)/value/*.cc) \ $(wildcard $(d)/primops/*.cc) \ $(wildcard $(d)/flake/*.cc) \ $(d)/lexer-tab.cc \ @@ -37,6 +38,8 @@ clean-files += $(d)/parser-tab.cc $(d)/parser-tab.hh $(d)/lexer-tab.cc $(d)/lexe $(eval $(call install-file-in, $(d)/nix-expr.pc, $(libdir)/pkgconfig, 0644)) +$(foreach i, $(wildcard src/libexpr/value/*.hh), \ + $(eval $(call install-file-in, $(i), $(includedir)/nix/value, 0644))) $(foreach i, $(wildcard src/libexpr/flake/*.hh), \ $(eval $(call install-file-in, $(i), $(includedir)/nix/flake, 0644))) diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index ffe67f97d..ac7ce021e 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -8,6 +8,7 @@ #include "error.hh" #include "chunked-vector.hh" + namespace nix { diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index ffb364a90..e07909f8e 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -400,21 +400,21 @@ expr_op | '-' expr_op %prec NEGATE { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__sub")), {new ExprInt(0), $2}); } | expr_op EQ expr_op { $$ = new ExprOpEq($1, $3); } | expr_op NEQ expr_op { $$ = new ExprOpNEq($1, $3); } - | expr_op '<' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__lessThan")), {$1, $3}); } - | expr_op LEQ expr_op { $$ = new ExprOpNot(new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__lessThan")), {$3, $1})); } - | expr_op '>' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__lessThan")), {$3, $1}); } - | expr_op GEQ expr_op { $$ = new ExprOpNot(new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__lessThan")), {$1, $3})); } - | expr_op AND expr_op { $$ = new ExprOpAnd(makeCurPos(@2, data), $1, $3); } - | expr_op OR expr_op { $$ = new ExprOpOr(makeCurPos(@2, data), $1, $3); } - | expr_op IMPL expr_op { $$ = new ExprOpImpl(makeCurPos(@2, data), $1, $3); } - | expr_op UPDATE expr_op { $$ = new ExprOpUpdate(makeCurPos(@2, data), $1, $3); } + | expr_op '<' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$1, $3}); } + | expr_op LEQ expr_op { $$ = new ExprOpNot(new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$3, $1})); } + | expr_op '>' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$3, $1}); } + | expr_op GEQ expr_op { $$ = new ExprOpNot(new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$1, $3})); } + | expr_op AND expr_op { $$ = new ExprOpAnd(CUR_POS, $1, $3); } + | expr_op OR expr_op { $$ = new ExprOpOr(CUR_POS, $1, $3); } + | expr_op IMPL expr_op { $$ = new ExprOpImpl(CUR_POS, $1, $3); } + | expr_op UPDATE expr_op { $$ = new ExprOpUpdate(CUR_POS, $1, $3); } | expr_op '?' attrpath { $$ = new ExprOpHasAttr($1, *$3); } | expr_op '+' expr_op - { $$ = new ExprConcatStrings(makeCurPos(@2, data), false, new std::vector<std::pair<PosIdx, Expr *> >({{makeCurPos(@1, data), $1}, {makeCurPos(@3, data), $3}})); } - | expr_op '-' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__sub")), {$1, $3}); } - | expr_op '*' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__mul")), {$1, $3}); } - | expr_op '/' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__div")), {$1, $3}); } - | expr_op CONCAT expr_op { $$ = new ExprOpConcatLists(makeCurPos(@2, data), $1, $3); } + { $$ = new ExprConcatStrings(CUR_POS, false, new std::vector<std::pair<PosIdx, Expr *>>({{makeCurPos(@1, data), $1}, {makeCurPos(@3, data), $3}})); } + | expr_op '-' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__sub")), {$1, $3}); } + | expr_op '*' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__mul")), {$1, $3}); } + | expr_op '/' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__div")), {$1, $3}); } + | expr_op CONCAT expr_op { $$ = new ExprOpConcatLists(CUR_POS, $1, $3); } | expr_app ; @@ -782,13 +782,13 @@ Path EvalState::findFile(SearchPath & searchPath, const std::string_view path, c if (hasPrefix(path, "nix/")) return concatStrings(corepkgsPrefix, path.substr(4)); - debugThrow(ThrownError({ + debugThrowLastTrace(ThrownError({ .msg = hintfmt(evalSettings.pureEval ? "cannot look up '<%s>' in pure evaluation mode (use '--impure' to override)" : "file '%s' was not found in the Nix search path (add it using $NIX_PATH or -I)", path), .errPos = positions[pos] - }), 0, 0); + })); } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 9ef91cbc5..080892cbd 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -43,16 +43,32 @@ StringMap EvalState::realiseContext(const PathSet & context) std::vector<DerivedPath::Built> drvs; StringMap res; - for (auto & i : context) { - auto [ctx, outputName] = decodeContext(*store, i); - auto ctxS = store->printStorePath(ctx); - if (!store->isValidPath(ctx)) - debugThrowLastTrace(InvalidPathError(store->printStorePath(ctx))); - if (!outputName.empty() && ctx.isDerivation()) { - drvs.push_back({ctx, {outputName}}); - } else { - res.insert_or_assign(ctxS, ctxS); - } + for (auto & c_ : context) { + auto ensureValid = [&](const StorePath & p) { + if (!store->isValidPath(p)) + debugThrowLastTrace(InvalidPathError(store->printStorePath(p))); + }; + auto c = NixStringContextElem::parse(*store, c_); + std::visit(overloaded { + [&](const NixStringContextElem::Built & b) { + drvs.push_back(DerivedPath::Built { + .drvPath = b.drvPath, + .outputs = OutputsSpec::Names { b.output }, + }); + ensureValid(b.drvPath); + }, + [&](const NixStringContextElem::Opaque & o) { + auto ctxS = store->printStorePath(o.path); + res.insert_or_assign(ctxS, ctxS); + ensureValid(o.path); + }, + [&](const NixStringContextElem::DrvDeep & d) { + /* Treat same as Opaque */ + auto ctxS = store->printStorePath(d.drvPath); + res.insert_or_assign(ctxS, ctxS); + ensureValid(d.drvPath); + }, + }, c.raw()); } if (drvs.empty()) return {}; @@ -68,16 +84,12 @@ StringMap EvalState::realiseContext(const PathSet & context) store->buildPaths(buildReqs); /* Get all the output paths corresponding to the placeholders we had */ - for (auto & [drvPath, outputs] : drvs) { - const auto outputPaths = store->queryDerivationOutputMap(drvPath); - for (auto & outputName : outputs) { - auto outputPath = get(outputPaths, outputName); - if (!outputPath) - debugThrowLastTrace(Error("derivation '%s' does not have an output named '%s'", - store->printStorePath(drvPath), outputName)); + for (auto & drv : drvs) { + auto outputs = resolveDerivedPath(*store, drv); + for (auto & [outputName, outputPath] : outputs) { res.insert_or_assign( - downstreamPlaceholder(*store, drvPath, outputName), - store->printStorePath(*outputPath) + downstreamPlaceholder(*store, drv.drvPath, outputName), + store->printStorePath(outputPath) ); } } @@ -102,7 +114,15 @@ static Path realisePath(EvalState & state, const PosIdx pos, Value & v, const Re { PathSet context; - auto path = state.coerceToPath(noPos, v, context, "while realising the context of a path"); + auto path = [&]() + { + try { + return state.coerceToPath(pos, v, context); + } catch (Error & e) { + e.addTrace(state.positions[pos], "while realising the context of a path"); + throw; + } + }(); try { StringMap rewrites = state.realiseContext(context); @@ -189,9 +209,9 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v , "/"), **state.vImportedDrvToDerivation); } - state.forceFunction(**state.vImportedDrvToDerivation, pos, "while evaluating imported-drv-to-derivation.nix.gen.hh"); + state.forceFunction(**state.vImportedDrvToDerivation, pos); v.mkApp(*state.vImportedDrvToDerivation, w); - state.forceAttrs(v, pos, "while calling imported-drv-to-derivation.nix.gen.hh"); + state.forceAttrs(v, pos); } else if (path == corepkgsPrefix + "fetchurl.nix") { @@ -204,7 +224,7 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v if (!vScope) state.evalFile(path, v); else { - state.forceAttrs(*vScope, pos, "while evaluating the first argument passed to builtins.scopedImport"); + state.forceAttrs(*vScope, pos); Env * env = &state.allocEnv(vScope->attrs->size()); env->up = &state.baseEnv; @@ -240,6 +260,7 @@ static RegisterPrimOp primop_scopedImport(RegisterPrimOp::Info { static RegisterPrimOp primop_import({ .name = "import", .args = {"path"}, + // TODO turn "normal path values" into link below .doc = R"( Load, parse and return the Nix expression in the file *path*. If *path* is a directory, the file ` default.nix ` in that directory @@ -253,7 +274,7 @@ static RegisterPrimOp primop_import({ > > Unlike some languages, `import` is a regular function in Nix. > Paths using the angle bracket syntax (e.g., `import` *\<foo\>*) - > are [normal path values](language-values.md). + > are normal [path values](@docroot@/language/values.md#type-path). A Nix expression loaded by `import` must not contain any *free variables* (identifiers that are not defined in the Nix expression @@ -308,7 +329,7 @@ void prim_importNative(EvalState & state, const PosIdx pos, Value * * args, Valu { auto path = realisePath(state, pos, *args[0]); - std::string sym(state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.importNative")); + std::string sym(state.forceStringNoCtx(*args[1], pos)); void *handle = dlopen(path.c_str(), RTLD_LAZY | RTLD_LOCAL); if (!handle) @@ -333,7 +354,7 @@ void prim_importNative(EvalState & state, const PosIdx pos, Value * * args, Valu /* Execute a program and parse its output */ void prim_exec(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.exec"); + state.forceList(*args[0], pos); auto elems = args[0]->listElems(); auto count = args[0]->listSize(); if (count == 0) @@ -342,12 +363,10 @@ void prim_exec(EvalState & state, const PosIdx pos, Value * * args, Value & v) .errPos = state.positions[pos] })); PathSet context; - auto program = state.coerceToString(pos, *elems[0], context, false, false, - "while evaluating the first element of the argument passed to builtins.exec").toOwned(); + auto program = state.coerceToString(pos, *elems[0], context, false, false).toOwned(); Strings commandArgs; for (unsigned int i = 1; i < args[0]->listSize(); ++i) { - commandArgs.push_back(state.coerceToString(pos, *elems[i], context, false, false, - "while evaluating an element of the argument passed to builtins.exec").toOwned()); + commandArgs.push_back(state.coerceToString(pos, *elems[i], context, false, false).toOwned()); } try { auto _ = state.realiseContext(context); // FIXME: Handle CA derivations @@ -364,17 +383,18 @@ void prim_exec(EvalState & state, const PosIdx pos, Value * * args, Value & v) try { parsed = state.parseExprFromString(std::move(output), "/"); } catch (Error & e) { - e.addTrace(state.positions[pos], "while parsing the output from '%1%'", program); + e.addTrace(state.positions[pos], "While parsing the output from '%1%'", program); throw; } try { state.eval(parsed, v); } catch (Error & e) { - e.addTrace(state.positions[pos], "while evaluating the output from '%1%'", program); + e.addTrace(state.positions[pos], "While evaluating the output from '%1%'", program); throw; } } + /* Return a string representing the type of the expression. */ static void prim_typeOf(EvalState & state, const PosIdx pos, Value * * args, Value & v) { @@ -525,68 +545,42 @@ static RegisterPrimOp primop_isPath({ .fun = prim_isPath, }); -template<typename Callable> - static inline void withExceptionContext(Trace trace, Callable&& func) -{ - try - { - func(); - } - catch(Error & e) - { - e.pushTrace(trace); - throw; - } -} - struct CompareValues { EvalState & state; - const PosIdx pos; - const std::string_view errorCtx; - CompareValues(EvalState & state, const PosIdx pos, const std::string_view && errorCtx) : state(state), pos(pos), errorCtx(errorCtx) { }; + CompareValues(EvalState & state) : state(state) { }; bool operator () (Value * v1, Value * v2) const { - return (*this)(v1, v2, errorCtx); - } - - bool operator () (Value * v1, Value * v2, std::string_view errorCtx) const - { - try { - if (v1->type() == nFloat && v2->type() == nInt) - return v1->fpoint < v2->integer; - if (v1->type() == nInt && v2->type() == nFloat) - return v1->integer < v2->fpoint; - if (v1->type() != v2->type()) - state.error("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow<EvalError>(); - switch (v1->type()) { - case nInt: - return v1->integer < v2->integer; - case nFloat: - return v1->fpoint < v2->fpoint; - case nString: - return strcmp(v1->string.s, v2->string.s) < 0; - case nPath: - return strcmp(v1->path, v2->path) < 0; - case nList: - // Lexicographic comparison - for (size_t i = 0;; i++) { - if (i == v2->listSize()) { - return false; - } else if (i == v1->listSize()) { - return true; - } else if (!state.eqValues(*v1->listElems()[i], *v2->listElems()[i], pos, errorCtx)) { - return (*this)(v1->listElems()[i], v2->listElems()[i], "while comparing two list elements"); - } + if (v1->type() == nFloat && v2->type() == nInt) + return v1->fpoint < v2->integer; + if (v1->type() == nInt && v2->type() == nFloat) + return v1->integer < v2->fpoint; + if (v1->type() != v2->type()) + state.debugThrowLastTrace(EvalError("cannot compare %1% with %2%", showType(*v1), showType(*v2))); + switch (v1->type()) { + case nInt: + return v1->integer < v2->integer; + case nFloat: + return v1->fpoint < v2->fpoint; + case nString: + return strcmp(v1->string.s, v2->string.s) < 0; + case nPath: + return strcmp(v1->path, v2->path) < 0; + case nList: + // Lexicographic comparison + for (size_t i = 0;; i++) { + if (i == v2->listSize()) { + return false; + } else if (i == v1->listSize()) { + return true; + } else if (!state.eqValues(*v1->listElems()[i], *v2->listElems()[i])) { + return (*this)(v1->listElems()[i], v2->listElems()[i]); } - default: - state.error("cannot compare %s with %s; values of that type are incomparable", showType(*v1), showType(*v2)).debugThrow<EvalError>(); - } - } catch (Error & e) { - e.addTrace(nullptr, errorCtx); - throw; + } + default: + state.debugThrowLastTrace(EvalError("cannot compare %1% with %2%", showType(*v1), showType(*v2))); } } }; @@ -601,75 +595,105 @@ typedef std::list<Value *> ValueList; static Bindings::iterator getAttr( EvalState & state, + std::string_view funcName, Symbol attrSym, Bindings * attrSet, - std::string_view errorCtx) + const PosIdx pos) { Bindings::iterator value = attrSet->find(attrSym); if (value == attrSet->end()) { - throw TypeError({ - .msg = hintfmt("attribute '%s' missing %s", state.symbols[attrSym], normaltxt(errorCtx)), - .errPos = state.positions[attrSet->pos], - }); - // TODO XXX - // Adding another trace for the function name to make it clear - // which call received wrong arguments. - //e.addTrace(state.positions[pos], hintfmt("while invoking '%s'", funcName)); - //state.debugThrowLastTrace(e); + hintformat errorMsg = hintfmt( + "attribute '%s' missing for call to '%s'", + state.symbols[attrSym], + funcName + ); + + auto aPos = attrSet->pos; + if (!aPos) { + state.debugThrowLastTrace(TypeError({ + .msg = errorMsg, + .errPos = state.positions[pos], + })); + } else { + auto e = TypeError({ + .msg = errorMsg, + .errPos = state.positions[aPos], + }); + + // Adding another trace for the function name to make it clear + // which call received wrong arguments. + e.addTrace(state.positions[pos], hintfmt("while invoking '%s'", funcName)); + state.debugThrowLastTrace(e); + } } + return value; } static void prim_genericClosure(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceAttrs(*args[0], noPos, "while evaluating the first argument passed to builtins.genericClosure"); + state.forceAttrs(*args[0], pos); /* Get the start set. */ - Bindings::iterator startSet = getAttr(state, state.sStartSet, args[0]->attrs, "in the attrset passed as argument to builtins.genericClosure"); + Bindings::iterator startSet = getAttr( + state, + "genericClosure", + state.sStartSet, + args[0]->attrs, + pos + ); - state.forceList(*startSet->value, noPos, "while evaluating the 'startSet' attribute passed as argument to builtins.genericClosure"); + state.forceList(*startSet->value, pos); ValueList workSet; for (auto elem : startSet->value->listItems()) workSet.push_back(elem); - if (startSet->value->listSize() == 0) { - v = *startSet->value; - return; - } - /* Get the operator. */ - Bindings::iterator op = getAttr(state, state.sOperator, args[0]->attrs, "in the attrset passed as argument to builtins.genericClosure"); - state.forceFunction(*op->value, noPos, "while evaluating the 'operator' attribute passed as argument to builtins.genericClosure"); + Bindings::iterator op = getAttr( + state, + "genericClosure", + state.sOperator, + args[0]->attrs, + pos + ); - /* Construct the closure by applying the operator to elements of + state.forceValue(*op->value, pos); + + /* Construct the closure by applying the operator to element of `workSet', adding the result to `workSet', continuing until no new elements are found. */ ValueList res; // `doneKeys' doesn't need to be a GC root, because its values are // reachable from res. - auto cmp = CompareValues(state, noPos, "while comparing the `key` attributes of two genericClosure elements"); + auto cmp = CompareValues(state); std::set<Value *, decltype(cmp)> doneKeys(cmp); while (!workSet.empty()) { Value * e = *(workSet.begin()); workSet.pop_front(); - state.forceAttrs(*e, noPos, "while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure"); + state.forceAttrs(*e, pos); - Bindings::iterator key = getAttr(state, state.sKey, e->attrs, "in one of the attrsets generated by (or initially passed to) builtins.genericClosure"); - state.forceValue(*key->value, noPos); + Bindings::iterator key = + e->attrs->find(state.sKey); + if (key == e->attrs->end()) + state.debugThrowLastTrace(EvalError({ + .msg = hintfmt("attribute 'key' required"), + .errPos = state.positions[pos] + })); + state.forceValue(*key->value, pos); if (!doneKeys.insert(key->value).second) continue; res.push_back(e); /* Call the `operator' function with `e' as argument. */ - Value newElements; - state.callFunction(*op->value, 1, &e, newElements, noPos); - state.forceList(newElements, noPos, "while evaluating the return value of the `operator` passed to builtins.genericClosure"); + Value call; + call.mkApp(op->value, e); + state.forceList(call, pos); /* Add the values returned by the operator to the work set. */ - for (auto elem : newElements.listItems()) { - state.forceValue(*elem, noPos); // "while evaluating one one of the elements returned by the `operator` passed to builtins.genericClosure"); + for (auto elem : call.listItems()) { + state.forceValue(*elem, pos); workSet.push_back(elem); } } @@ -737,7 +761,7 @@ static RegisterPrimOp primop_break({ throw Error(ErrorInfo{ .level = lvlInfo, .msg = hintfmt("quit the debugger"), - .errPos = nullptr, + .errPos = state.positions[noPos], }); } } @@ -756,8 +780,7 @@ static RegisterPrimOp primop_abort({ .fun = [](EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - auto s = state.coerceToString(pos, *args[0], context, - "while evaluating the error message passed to builtins.abort").toOwned(); + auto s = state.coerceToString(pos, *args[0], context).toOwned(); state.debugThrowLastTrace(Abort("evaluation aborted with the following error message: '%1%'", s)); } }); @@ -775,8 +798,7 @@ static RegisterPrimOp primop_throw({ .fun = [](EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - auto s = state.coerceToString(pos, *args[0], context, - "while evaluating the error message passed to builtin.throw").toOwned(); + auto s = state.coerceToString(pos, *args[0], context).toOwned(); state.debugThrowLastTrace(ThrownError(s)); } }); @@ -788,8 +810,7 @@ static void prim_addErrorContext(EvalState & state, const PosIdx pos, Value * * v = *args[1]; } catch (Error & e) { PathSet context; - e.addTrace(nullptr, state.coerceToString(pos, *args[0], context, - "while evaluating the error message passed to builtins.addErrorContext").toOwned()); + e.addTrace(nullptr, state.coerceToString(pos, *args[0], context).toOwned()); throw; } } @@ -802,8 +823,7 @@ static RegisterPrimOp primop_addErrorContext(RegisterPrimOp::Info { static void prim_ceil(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto value = state.forceFloat(*args[0], args[0]->determinePos(pos), - "while evaluating the first argument passed to builtins.ceil"); + auto value = state.forceFloat(*args[0], args[0]->determinePos(pos)); v.mkInt(ceil(value)); } @@ -822,7 +842,7 @@ static RegisterPrimOp primop_ceil({ static void prim_floor(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto value = state.forceFloat(*args[0], args[0]->determinePos(pos), "while evaluating the first argument passed to builtins.floor"); + auto value = state.forceFloat(*args[0], args[0]->determinePos(pos)); v.mkInt(floor(value)); } @@ -896,7 +916,7 @@ static RegisterPrimOp primop_tryEval({ /* Return an environment variable. Use with care. */ static void prim_getEnv(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - std::string name(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.getEnv")); + std::string name(state.forceStringNoCtx(*args[0], pos)); v.mkString(evalSettings.restrictEval || evalSettings.pureEval ? "" : getEnv(name).value_or("")); } @@ -1004,15 +1024,21 @@ static void prim_second(EvalState & state, const PosIdx pos, Value * * args, Val static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * args, Value & v) { using nlohmann::json; - state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.derivationStrict"); + state.forceAttrs(*args[0], pos); /* Figure out the name first (for stack backtraces). */ - Bindings::iterator attr = getAttr(state, state.sName, args[0]->attrs, "in the attrset passed as argument to builtins.derivationStrict"); + Bindings::iterator attr = getAttr( + state, + "derivationStrict", + state.sName, + args[0]->attrs, + pos + ); std::string drvName; const auto posDrvName = attr->pos; try { - drvName = state.forceStringNoCtx(*attr->value, pos, "while evaluating the `name` attribute passed to builtins.derivationStrict"); + drvName = state.forceStringNoCtx(*attr->value, pos); } catch (Error & e) { e.addTrace(state.positions[posDrvName], "while evaluating the derivation attribute 'name'"); throw; @@ -1021,14 +1047,14 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * /* Check whether attributes should be passed as a JSON file. */ std::optional<json> jsonObject; attr = args[0]->attrs->find(state.sStructuredAttrs); - if (attr != args[0]->attrs->end() && state.forceBool(*attr->value, pos, "while evaluating the `__structuredAttrs` attribute passed to builtins.derivationStrict")) + if (attr != args[0]->attrs->end() && state.forceBool(*attr->value, pos)) jsonObject = json::object(); /* Check whether null attributes should be ignored. */ bool ignoreNulls = false; attr = args[0]->attrs->find(state.sIgnoreNulls); if (attr != args[0]->attrs->end()) - ignoreNulls = state.forceBool(*attr->value, pos, "while evaluating the `__ignoreNulls` attribute passed to builtins.derivationStrict"); + ignoreNulls = state.forceBool(*attr->value, pos); /* Build the derivation expression by processing the attributes. */ Derivation drv; @@ -1095,15 +1121,13 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * } if (i->name == state.sContentAddressed) { - contentAddressed = state.forceBool(*i->value, pos, - "while evaluating the `__contentAddressed` attribute passed to builtins.derivationStrict"); + contentAddressed = state.forceBool(*i->value, pos); if (contentAddressed) settings.requireExperimentalFeature(Xp::CaDerivations); } else if (i->name == state.sImpure) { - isImpure = state.forceBool(*i->value, pos, - "while evaluating the 'impure' attribute passed to builtins.derivationStrict"); + isImpure = state.forceBool(*i->value, pos); if (isImpure) settings.requireExperimentalFeature(Xp::ImpureDerivations); } @@ -1111,11 +1135,9 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * /* The `args' attribute is special: it supplies the command-line arguments to the builder. */ else if (i->name == state.sArgs) { - state.forceList(*i->value, pos, - "while evaluating the `args` attribute passed to builtins.derivationStrict"); + state.forceList(*i->value, pos); for (auto elem : i->value->listItems()) { - auto s = state.coerceToString(posDrvName, *elem, context, true, - "while evaluating an element of the `args` argument passed to builtins.derivationStrict").toOwned(); + auto s = state.coerceToString(posDrvName, *elem, context, true).toOwned(); drv.args.push_back(s); } } @@ -1131,26 +1153,26 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * (*jsonObject)[key] = printValueAsJSON(state, true, *i->value, pos, context); if (i->name == state.sBuilder) - drv.builder = state.forceString(*i->value, context, posDrvName, "while evaluating the `builder` attribute passed to builtins.derivationStrict"); + drv.builder = state.forceString(*i->value, context, posDrvName); else if (i->name == state.sSystem) - drv.platform = state.forceStringNoCtx(*i->value, posDrvName, "while evaluating the `system` attribute passed to builtins.derivationStrict"); + drv.platform = state.forceStringNoCtx(*i->value, posDrvName); else if (i->name == state.sOutputHash) - outputHash = state.forceStringNoCtx(*i->value, posDrvName, "while evaluating the `outputHash` attribute passed to builtins.derivationStrict"); + outputHash = state.forceStringNoCtx(*i->value, posDrvName); else if (i->name == state.sOutputHashAlgo) - outputHashAlgo = state.forceStringNoCtx(*i->value, posDrvName, "while evaluating the `outputHashAlgo` attribute passed to builtins.derivationStrict"); + outputHashAlgo = state.forceStringNoCtx(*i->value, posDrvName); else if (i->name == state.sOutputHashMode) - handleHashMode(state.forceStringNoCtx(*i->value, posDrvName, "while evaluating the `outputHashMode` attribute passed to builtins.derivationStrict")); + handleHashMode(state.forceStringNoCtx(*i->value, posDrvName)); else if (i->name == state.sOutputs) { /* Require ‘outputs’ to be a list of strings. */ - state.forceList(*i->value, posDrvName, "while evaluating the `outputs` attribute passed to builtins.derivationStrict"); + state.forceList(*i->value, posDrvName); Strings ss; for (auto elem : i->value->listItems()) - ss.emplace_back(state.forceStringNoCtx(*elem, posDrvName, "while evaluating an element of the `outputs` attribute passed to builtins.derivationStrict")); + ss.emplace_back(state.forceStringNoCtx(*elem, posDrvName)); handleOutputs(ss); } } else { - auto s = state.coerceToString(i->pos, *i->value, context, true, "while evaluating an attribute passed to builtins.derivationStrict").toOwned(); + auto s = state.coerceToString(i->pos, *i->value, context, true).toOwned(); drv.env.emplace(key, s); if (i->name == state.sBuilder) drv.builder = std::move(s); else if (i->name == state.sSystem) drv.platform = std::move(s); @@ -1164,9 +1186,9 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * } } catch (Error & e) { - e.addTrace(nullptr, - hintfmt("while evaluating the attribute '%1%' of the derivation '%2%'", key, drvName), - true); + e.addTrace(state.positions[posDrvName], + "while evaluating the attribute '%1%' of the derivation '%2%'", + key, drvName); throw; } } @@ -1179,35 +1201,31 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * /* Everything in the context of the strings in the derivation attributes should be added as dependencies of the resulting derivation. */ - for (auto & path : context) { - - /* Paths marked with `=' denote that the path of a derivation - is explicitly passed to the builder. Since that allows the - builder to gain access to every path in the dependency - graph of the derivation (including all outputs), all paths - in the graph must be added to this derivation's list of - inputs to ensure that they are available when the builder - runs. */ - if (path.at(0) == '=') { - /* !!! This doesn't work if readOnlyMode is set. */ - StorePathSet refs; - state.store->computeFSClosure(state.store->parseStorePath(std::string_view(path).substr(1)), refs); - for (auto & j : refs) { - drv.inputSrcs.insert(j); - if (j.isDerivation()) - drv.inputDrvs[j] = state.store->readDerivation(j).outputNames(); - } - } - - /* Handle derivation outputs of the form ‘!<name>!<path>’. */ - else if (path.at(0) == '!') { - auto ctx = decodeContext(*state.store, path); - drv.inputDrvs[ctx.first].insert(ctx.second); - } - - /* Otherwise it's a source file. */ - else - drv.inputSrcs.insert(state.store->parseStorePath(path)); + for (auto & c_ : context) { + auto c = NixStringContextElem::parse(*state.store, c_); + std::visit(overloaded { + /* Since this allows the builder to gain access to every + path in the dependency graph of the derivation (including + all outputs), all paths in the graph must be added to + this derivation's list of inputs to ensure that they are + available when the builder runs. */ + [&](const NixStringContextElem::DrvDeep & d) { + /* !!! This doesn't work if readOnlyMode is set. */ + StorePathSet refs; + state.store->computeFSClosure(d.drvPath, refs); + for (auto & j : refs) { + drv.inputSrcs.insert(j); + if (j.isDerivation()) + drv.inputDrvs[j] = state.store->readDerivation(j).outputNames(); + } + }, + [&](const NixStringContextElem::Built & b) { + drv.inputDrvs[b.drvPath].insert(b.output); + }, + [&](const NixStringContextElem::Opaque & o) { + drv.inputSrcs.insert(o.path); + }, + }, c.raw()); } /* Do we have all required attributes? */ @@ -1358,7 +1376,7 @@ static RegisterPrimOp primop_derivationStrict(RegisterPrimOp::Info { ‘out’. */ static void prim_placeholder(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - v.mkString(hashPlaceholder(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.placeholder"))); + v.mkString(hashPlaceholder(state.forceStringNoCtx(*args[0], pos))); } static RegisterPrimOp primop_placeholder({ @@ -1382,7 +1400,7 @@ static RegisterPrimOp primop_placeholder({ static void prim_toPath(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - Path path = state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.toPath"); + Path path = state.coerceToPath(pos, *args[0], context); v.mkString(canonPath(path), context); } @@ -1413,7 +1431,7 @@ static void prim_storePath(EvalState & state, const PosIdx pos, Value * * args, })); PathSet context; - Path path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.storePath")); + Path path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context)); /* Resolve symlinks in ‘path’, unless ‘path’ itself is a symlink directly in the store. The latter condition is necessary so e.g. nix-push does the right thing. */ @@ -1483,7 +1501,7 @@ static RegisterPrimOp primop_pathExists({ static void prim_baseNameOf(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - v.mkString(baseNameOf(*state.coerceToString(pos, *args[0], context, false, false, "while evaluating the first argument passed to builtins.baseNameOf")), context); + v.mkString(baseNameOf(*state.coerceToString(pos, *args[0], context, false, false)), context); } static RegisterPrimOp primop_baseNameOf({ @@ -1503,7 +1521,7 @@ static RegisterPrimOp primop_baseNameOf({ static void prim_dirOf(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - auto path = state.coerceToString(pos, *args[0], context, false, false, "while evaluating the first argument passed to builtins.dirOf"); + auto path = state.coerceToString(pos, *args[0], context, false, false); auto dir = dirOf(*path); if (args[0]->type() == nPath) v.mkPath(dir); else v.mkString(dir, context); } @@ -1554,23 +1572,28 @@ static RegisterPrimOp primop_readFile({ which are desugared to 'findFile __nixPath "x"'. */ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.findFile"); + state.forceList(*args[0], pos); SearchPath searchPath; for (auto v2 : args[0]->listItems()) { - state.forceAttrs(*v2, pos, "while evaluating an element of the list passed to builtins.findFile"); + state.forceAttrs(*v2, pos); std::string prefix; Bindings::iterator i = v2->attrs->find(state.sPrefix); if (i != v2->attrs->end()) - prefix = state.forceStringNoCtx(*i->value, pos, "while evaluating the `prefix` attribute of an element of the list passed to builtins.findFile"); + prefix = state.forceStringNoCtx(*i->value, pos); - i = getAttr(state, state.sPath, v2->attrs, "in an element of the __nixPath"); + i = getAttr( + state, + "findFile", + state.sPath, + v2->attrs, + pos + ); PathSet context; - auto path = state.coerceToString(pos, *i->value, context, false, false, - "while evaluating the `path` attribute of an element of the list passed to builtins.findFile").toOwned(); + auto path = state.coerceToString(pos, *i->value, context, false, false).toOwned(); try { auto rewrites = state.realiseContext(context); @@ -1585,7 +1608,7 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V searchPath.emplace_back(prefix, path); } - auto path = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.findFile"); + auto path = state.forceStringNoCtx(*args[1], pos); v.mkPath(state.checkSourcePath(state.findFile(searchPath, path, pos))); } @@ -1599,7 +1622,7 @@ static RegisterPrimOp primop_findFile(RegisterPrimOp::Info { /* Return the cryptographic hash of a file in base-16. */ static void prim_hashFile(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto type = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.hashFile"); + auto type = state.forceStringNoCtx(*args[0], pos); std::optional<HashType> ht = parseHashType(type); if (!ht) state.debugThrowLastTrace(Error({ @@ -1806,7 +1829,7 @@ static RegisterPrimOp primop_toJSON({ /* Parse a JSON string to a value. */ static void prim_fromJSON(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto s = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.fromJSON"); + auto s = state.forceStringNoCtx(*args[0], pos); try { parseJSON(state, s, v); } catch (JSONParseError &e) { @@ -1835,8 +1858,8 @@ static RegisterPrimOp primop_fromJSON({ static void prim_toFile(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - std::string name(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.toFile")); - std::string contents(state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.toFile")); + std::string name(state.forceStringNoCtx(*args[0], pos)); + std::string contents(state.forceString(*args[1], context, pos)); StorePathSet refs; @@ -1872,8 +1895,7 @@ static RegisterPrimOp primop_toFile({ path. The file has suffix *name*. This file can be used as an input to derivations. One application is to write builders “inline”. For instance, the following Nix expression combines the - [Nix expression for GNU Hello](expression-syntax.md) and its - [build script](build-script.md) into one file: + Nix expression for GNU Hello and its build script into one file: ```nix { stdenv, fetchurl, perl }: @@ -1917,7 +1939,7 @@ static RegisterPrimOp primop_toFile({ ``` Note that `${configFile}` is a - [string interpolation](language/values.md#type-string), so the result of the + [string interpolation](@docroot@/language/values.md#type-string), so the result of the expression `configFile` (i.e., a path like `/nix/store/m7p7jfny445k...-foo.conf`) will be spliced into the resulting string. @@ -1994,7 +2016,7 @@ static void addPath( Value res; state.callFunction(*filterFun, 2, args, res, pos); - return state.forceBool(res, pos, "while evaluating the return value of the path filter function"); + return state.forceBool(res, pos); }) : defaultPathFilter; std::optional<StorePath> expectedStorePath; @@ -2020,8 +2042,17 @@ static void addPath( static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - Path path = state.coerceToPath(pos, *args[1], context, "while evaluating the second argument (the path to filter) passed to builtins.filterSource"); - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource"); + Path path = state.coerceToPath(pos, *args[1], context); + + state.forceValue(*args[0], pos); + if (args[0]->type() != nFunction) + state.debugThrowLastTrace(TypeError({ + .msg = hintfmt( + "first argument in call to 'filterSource' is not a function but %1%", + showType(*args[0])), + .errPos = state.positions[pos] + })); + addPath(state, pos, std::string(baseNameOf(path)), path, args[0], FileIngestionMethod::Recursive, std::nullopt, v, context); } @@ -2082,7 +2113,7 @@ static RegisterPrimOp primop_filterSource({ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.path"); + state.forceAttrs(*args[0], pos); Path path; std::string name; Value * filterFun = nullptr; @@ -2093,15 +2124,16 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value for (auto & attr : *args[0]->attrs) { auto n = state.symbols[attr.name]; if (n == "path") - path = state.coerceToPath(attr.pos, *attr.value, context, "while evaluating the `path` attribute passed to builtins.path"); + path = state.coerceToPath(attr.pos, *attr.value, context); else if (attr.name == state.sName) - name = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `name` attribute passed to builtins.path"); - else if (n == "filter") - state.forceFunction(*(filterFun = attr.value), attr.pos, "while evaluating the `filter` parameter passed to builtins.path"); - else if (n == "recursive") - method = FileIngestionMethod { state.forceBool(*attr.value, attr.pos, "while evaluating the `recursive` attribute passed to builtins.path") }; + name = state.forceStringNoCtx(*attr.value, attr.pos); + else if (n == "filter") { + state.forceValue(*attr.value, pos); + filterFun = attr.value; + } else if (n == "recursive") + method = FileIngestionMethod { state.forceBool(*attr.value, attr.pos) }; else if (n == "sha256") - expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `sha256` attribute passed to builtins.path"), htSHA256); + expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos), htSHA256); else state.debugThrowLastTrace(EvalError({ .msg = hintfmt("unsupported argument '%1%' to 'addPath'", state.symbols[attr.name]), @@ -2110,7 +2142,7 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value } if (path.empty()) state.debugThrowLastTrace(EvalError({ - .msg = hintfmt("missing required 'path' attribute in the first argument to builtins.path"), + .msg = hintfmt("'path' required"), .errPos = state.positions[pos] })); if (name.empty()) @@ -2164,7 +2196,7 @@ static RegisterPrimOp primop_path({ strings. */ static void prim_attrNames(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.attrNames"); + state.forceAttrs(*args[0], pos); state.mkList(v, args[0]->attrs->size()); @@ -2191,7 +2223,7 @@ static RegisterPrimOp primop_attrNames({ order as attrNames. */ static void prim_attrValues(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.attrValues"); + state.forceAttrs(*args[0], pos); state.mkList(v, args[0]->attrs->size()); @@ -2223,13 +2255,14 @@ static RegisterPrimOp primop_attrValues({ /* Dynamic version of the `.' operator. */ void prim_getAttr(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto attr = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.getAttr"); - state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.getAttr"); + auto attr = state.forceStringNoCtx(*args[0], pos); + state.forceAttrs(*args[1], pos); Bindings::iterator i = getAttr( state, + "getAttr", state.symbols.create(attr), args[1]->attrs, - "in the attribute set under consideration" + pos ); // !!! add to stack trace? if (state.countCalls && i->pos) state.attrSelects[i->pos]++; @@ -2252,8 +2285,8 @@ static RegisterPrimOp primop_getAttr({ /* Return position information of the specified attribute. */ static void prim_unsafeGetAttrPos(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto attr = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.unsafeGetAttrPos"); - state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.unsafeGetAttrPos"); + auto attr = state.forceStringNoCtx(*args[0], pos); + state.forceAttrs(*args[1], pos); Bindings::iterator i = args[1]->attrs->find(state.symbols.create(attr)); if (i == args[1]->attrs->end()) v.mkNull(); @@ -2270,8 +2303,8 @@ static RegisterPrimOp primop_unsafeGetAttrPos(RegisterPrimOp::Info { /* Dynamic version of the `?' operator. */ static void prim_hasAttr(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto attr = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.hasAttr"); - state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.hasAttr"); + auto attr = state.forceStringNoCtx(*args[0], pos); + state.forceAttrs(*args[1], pos); v.mkBool(args[1]->attrs->find(state.symbols.create(attr)) != args[1]->attrs->end()); } @@ -2304,8 +2337,8 @@ static RegisterPrimOp primop_isAttrs({ static void prim_removeAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceAttrs(*args[0], pos, "while evaluating the first argument passed to builtins.removeAttrs"); - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.removeAttrs"); + state.forceAttrs(*args[0], pos); + state.forceList(*args[1], pos); /* Get the attribute names to be removed. We keep them as Attrs instead of Symbols so std::set_difference @@ -2313,7 +2346,7 @@ static void prim_removeAttrs(EvalState & state, const PosIdx pos, Value * * args boost::container::small_vector<Attr, 64> names; names.reserve(args[1]->listSize()); for (auto elem : args[1]->listItems()) { - state.forceStringNoCtx(*elem, pos, "while evaluating the values of the second argument passed to builtins.removeAttrs"); + state.forceStringNoCtx(*elem, pos); names.emplace_back(state.symbols.create(elem->string.s), nullptr); } std::sort(names.begin(), names.end()); @@ -2352,22 +2385,34 @@ static RegisterPrimOp primop_removeAttrs({ name, the first takes precedence. */ static void prim_listToAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[0], pos, "while evaluating the argument passed to builtins.listToAttrs"); + state.forceList(*args[0], pos); auto attrs = state.buildBindings(args[0]->listSize()); std::set<Symbol> seen; for (auto v2 : args[0]->listItems()) { - state.forceAttrs(*v2, pos, "while evaluating an element of the list passed to builtins.listToAttrs"); + state.forceAttrs(*v2, pos); - Bindings::iterator j = getAttr(state, state.sName, v2->attrs, "in a {name=...; value=...;} pair"); + Bindings::iterator j = getAttr( + state, + "listToAttrs", + state.sName, + v2->attrs, + pos + ); - auto name = state.forceStringNoCtx(*j->value, j->pos, "while evaluating the `name` attribute of an element of the list passed to builtins.listToAttrs"); + auto name = state.forceStringNoCtx(*j->value, j->pos); auto sym = state.symbols.create(name); if (seen.insert(sym).second) { - Bindings::iterator j2 = getAttr(state, state.sValue, v2->attrs, "in a {name=...; value=...;} pair"); + Bindings::iterator j2 = getAttr( + state, + "listToAttrs", + state.sValue, + v2->attrs, + pos + ); attrs.insert(sym, j2->value, j2->pos); } } @@ -2408,8 +2453,8 @@ static RegisterPrimOp primop_listToAttrs({ static void prim_intersectAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceAttrs(*args[0], pos, "while evaluating the first argument passed to builtins.intersectAttrs"); - state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.intersectAttrs"); + state.forceAttrs(*args[0], pos); + state.forceAttrs(*args[1], pos); Bindings &left = *args[0]->attrs; Bindings &right = *args[1]->attrs; @@ -2486,14 +2531,14 @@ static RegisterPrimOp primop_intersectAttrs({ static void prim_catAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto attrName = state.symbols.create(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.catAttrs")); - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.catAttrs"); + auto attrName = state.symbols.create(state.forceStringNoCtx(*args[0], pos)); + state.forceList(*args[1], pos); Value * res[args[1]->listSize()]; unsigned int found = 0; for (auto v2 : args[1]->listItems()) { - state.forceAttrs(*v2, pos, "while evaluating an element in the list passed as second argument to builtins.catAttrs"); + state.forceAttrs(*v2, pos); Bindings::iterator i = v2->attrs->find(attrName); if (i != v2->attrs->end()) res[found++] = i->value; @@ -2566,7 +2611,7 @@ static RegisterPrimOp primop_functionArgs({ /* */ static void prim_mapAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.mapAttrs"); + state.forceAttrs(*args[1], pos); auto attrs = state.buildBindings(args[1]->attrs->size()); @@ -2607,15 +2652,15 @@ static void prim_zipAttrsWith(EvalState & state, const PosIdx pos, Value * * arg std::map<Symbol, std::pair<size_t, Value * *>> attrsSeen; - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.zipAttrsWith"); - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.zipAttrsWith"); + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); const auto listSize = args[1]->listSize(); const auto listElems = args[1]->listElems(); for (unsigned int n = 0; n < listSize; ++n) { Value * vElem = listElems[n]; try { - state.forceAttrs(*vElem, noPos, "while evaluating a value of the list passed as second argument to builtins.zipAttrsWith"); + state.forceAttrs(*vElem, noPos); for (auto & attr : *vElem->attrs) attrsSeen[attr.name].first++; } catch (TypeError & e) { @@ -2705,7 +2750,7 @@ static RegisterPrimOp primop_isList({ static void elemAt(EvalState & state, const PosIdx pos, Value & list, int n, Value & v) { - state.forceList(list, pos, "while evaluating the first argument passed to builtins.elemAt"); + state.forceList(list, pos); if (n < 0 || (unsigned int) n >= list.listSize()) state.debugThrowLastTrace(Error({ .msg = hintfmt("list index %1% is out of bounds", n), @@ -2718,7 +2763,7 @@ static void elemAt(EvalState & state, const PosIdx pos, Value & list, int n, Val /* Return the n-1'th element of a list. */ static void prim_elemAt(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - elemAt(state, pos, *args[0], state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.elemAt"), v); + elemAt(state, pos, *args[0], state.forceInt(*args[1], pos), v); } static RegisterPrimOp primop_elemAt({ @@ -2753,7 +2798,7 @@ static RegisterPrimOp primop_head({ don't want to use it! */ static void prim_tail(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.tail"); + state.forceList(*args[0], pos); if (args[0]->listSize() == 0) state.debugThrowLastTrace(Error({ .msg = hintfmt("'tail' called on an empty list"), @@ -2784,16 +2829,10 @@ static RegisterPrimOp primop_tail({ /* Apply a function to every element of a list. */ static void prim_map(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.map"); - - if (args[1]->listSize() == 0) { - v = *args[1]; - return; - } - - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.map"); + state.forceList(*args[1], pos); state.mkList(v, args[1]->listSize()); + for (unsigned int n = 0; n < v.listSize(); ++n) (v.listElems()[n] = state.allocValue())->mkApp( args[0], args[1]->listElems()[n]); @@ -2820,14 +2859,8 @@ static RegisterPrimOp primop_map({ returns true. */ static void prim_filter(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.filter"); - - if (args[1]->listSize() == 0) { - v = *args[1]; - return; - } - - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filter"); + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); // FIXME: putting this on the stack is risky. Value * vs[args[1]->listSize()]; @@ -2837,7 +2870,7 @@ static void prim_filter(EvalState & state, const PosIdx pos, Value * * args, Val for (unsigned int n = 0; n < args[1]->listSize(); ++n) { Value res; state.callFunction(*args[0], *args[1]->listElems()[n], res, noPos); - if (state.forceBool(res, pos, "while evaluating the return value of the filtering function passed to builtins.filter")) + if (state.forceBool(res, pos)) vs[k++] = args[1]->listElems()[n]; else same = false; @@ -2865,9 +2898,9 @@ static RegisterPrimOp primop_filter({ static void prim_elem(EvalState & state, const PosIdx pos, Value * * args, Value & v) { bool res = false; - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.elem"); + state.forceList(*args[1], pos); for (auto elem : args[1]->listItems()) - if (state.eqValues(*args[0], *elem, pos, "while searching for the presence of the given element in the list")) { + if (state.eqValues(*args[0], *elem)) { res = true; break; } @@ -2887,8 +2920,8 @@ static RegisterPrimOp primop_elem({ /* Concatenate a list of lists. */ static void prim_concatLists(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.concatLists"); - state.concatLists(v, args[0]->listSize(), args[0]->listElems(), pos, "while evaluating a value of the list passed to builtins.concatLists"); + state.forceList(*args[0], pos); + state.concatLists(v, args[0]->listSize(), args[0]->listElems(), pos); } static RegisterPrimOp primop_concatLists({ @@ -2903,7 +2936,7 @@ static RegisterPrimOp primop_concatLists({ /* Return the length of a list. This is an O(1) time operation. */ static void prim_length(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.length"); + state.forceList(*args[0], pos); v.mkInt(args[0]->listSize()); } @@ -2920,8 +2953,8 @@ static RegisterPrimOp primop_length({ right. The operator is applied strictly. */ static void prim_foldlStrict(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.foldlStrict"); - state.forceList(*args[2], pos, "while evaluating the third argument passed to builtins.foldlStrict"); + state.forceFunction(*args[0], pos); + state.forceList(*args[2], pos); if (args[2]->listSize()) { Value * vCur = args[1]; @@ -2953,13 +2986,13 @@ static RegisterPrimOp primop_foldlStrict({ static void anyOrAll(bool any, EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceFunction(*args[0], pos, std::string("while evaluating the first argument passed to builtins.") + (any ? "any" : "all")); - state.forceList(*args[1], pos, std::string("while evaluating the second argument passed to builtins.") + (any ? "any" : "all")); + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); Value vTmp; for (auto elem : args[1]->listItems()) { state.callFunction(*args[0], *elem, vTmp, pos); - bool res = state.forceBool(vTmp, pos, std::string("while evaluating the return value of the function passed to builtins.") + (any ? "any" : "all")); + bool res = state.forceBool(vTmp, pos); if (res == any) { v.mkBool(any); return; @@ -3002,7 +3035,7 @@ static RegisterPrimOp primop_all({ static void prim_genList(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto len = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.genList"); + auto len = state.forceInt(*args[1], pos); if (len < 0) state.debugThrowLastTrace(EvalError({ @@ -3040,16 +3073,10 @@ static void prim_lessThan(EvalState & state, const PosIdx pos, Value * * args, V static void prim_sort(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.sort"); + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); auto len = args[1]->listSize(); - if (len == 0) { - v = *args[1]; - return; - } - - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.sort"); - state.mkList(v, len); for (unsigned int n = 0; n < len; ++n) { state.forceValue(*args[1]->listElems()[n], pos); @@ -3060,12 +3087,12 @@ static void prim_sort(EvalState & state, const PosIdx pos, Value * * args, Value /* Optimization: if the comparator is lessThan, bypass callFunction. */ if (args[0]->isPrimOp() && args[0]->primOp->fun == prim_lessThan) - return CompareValues(state, noPos, "while evaluating the ordering function passed to builtins.sort")(a, b); + return CompareValues(state)(a, b); Value * vs[] = {a, b}; Value vBool; - state.callFunction(*args[0], 2, vs, vBool, noPos); - return state.forceBool(vBool, pos, "while evaluating the return value of the sorting function passed to builtins.sort"); + state.callFunction(*args[0], 2, vs, vBool, pos); + return state.forceBool(vBool, pos); }; /* FIXME: std::sort can segfault if the comparator is not a strict @@ -3097,8 +3124,8 @@ static RegisterPrimOp primop_sort({ static void prim_partition(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.partition"); - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.partition"); + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); auto len = args[1]->listSize(); @@ -3109,7 +3136,7 @@ static void prim_partition(EvalState & state, const PosIdx pos, Value * * args, state.forceValue(*vElem, pos); Value res; state.callFunction(*args[0], *vElem, res, pos); - if (state.forceBool(res, pos, "while evaluating the return value of the partition function passed to builtins.partition")) + if (state.forceBool(res, pos)) right.push_back(vElem); else wrong.push_back(vElem); @@ -3157,15 +3184,15 @@ static RegisterPrimOp primop_partition({ static void prim_groupBy(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.groupBy"); - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.groupBy"); + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); ValueVectorMap attrs; for (auto vElem : args[1]->listItems()) { Value res; state.callFunction(*args[0], *vElem, res, pos); - auto name = state.forceStringNoCtx(res, pos, "while evaluating the return value of the grouping function passed to builtins.groupBy"); + auto name = state.forceStringNoCtx(res, pos); auto sym = state.symbols.create(name); auto vector = attrs.try_emplace(sym, ValueVector()).first; vector->second.push_back(vElem); @@ -3209,8 +3236,8 @@ static RegisterPrimOp primop_groupBy({ static void prim_concatMap(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.concatMap"); - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.concatMap"); + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); auto nrLists = args[1]->listSize(); Value lists[nrLists]; @@ -3220,7 +3247,7 @@ static void prim_concatMap(EvalState & state, const PosIdx pos, Value * * args, Value * vElem = args[1]->listElems()[n]; state.callFunction(*args[0], *vElem, lists[n], pos); try { - state.forceList(lists[n], lists[n].determinePos(args[0]->determinePos(pos)), "while evaluating the return value of the function passed to buitlins.concatMap"); + state.forceList(lists[n], lists[n].determinePos(args[0]->determinePos(pos))); } catch (TypeError &e) { e.addTrace(state.positions[pos], hintfmt("while invoking '%s'", "concatMap")); state.debugThrowLastTrace(e); @@ -3259,11 +3286,9 @@ static void prim_add(EvalState & state, const PosIdx pos, Value * * args, Value state.forceValue(*args[0], pos); state.forceValue(*args[1], pos); if (args[0]->type() == nFloat || args[1]->type() == nFloat) - v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the addition") - + state.forceFloat(*args[1], pos, "while evaluating the second argument of the addition")); + v.mkFloat(state.forceFloat(*args[0], pos) + state.forceFloat(*args[1], pos)); else - v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the addition") - + state.forceInt(*args[1], pos, "while evaluating the second argument of the addition")); + v.mkInt(state.forceInt(*args[0], pos) + state.forceInt(*args[1], pos)); } static RegisterPrimOp primop_add({ @@ -3280,11 +3305,9 @@ static void prim_sub(EvalState & state, const PosIdx pos, Value * * args, Value state.forceValue(*args[0], pos); state.forceValue(*args[1], pos); if (args[0]->type() == nFloat || args[1]->type() == nFloat) - v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the subtraction") - - state.forceFloat(*args[1], pos, "while evaluating the second argument of the subtraction")); + v.mkFloat(state.forceFloat(*args[0], pos) - state.forceFloat(*args[1], pos)); else - v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the subtraction") - - state.forceInt(*args[1], pos, "while evaluating the second argument of the subtraction")); + v.mkInt(state.forceInt(*args[0], pos) - state.forceInt(*args[1], pos)); } static RegisterPrimOp primop_sub({ @@ -3301,11 +3324,9 @@ static void prim_mul(EvalState & state, const PosIdx pos, Value * * args, Value state.forceValue(*args[0], pos); state.forceValue(*args[1], pos); if (args[0]->type() == nFloat || args[1]->type() == nFloat) - v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first of the multiplication") - * state.forceFloat(*args[1], pos, "while evaluating the second argument of the multiplication")); + v.mkFloat(state.forceFloat(*args[0], pos) * state.forceFloat(*args[1], pos)); else - v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the multiplication") - * state.forceInt(*args[1], pos, "while evaluating the second argument of the multiplication")); + v.mkInt(state.forceInt(*args[0], pos) * state.forceInt(*args[1], pos)); } static RegisterPrimOp primop_mul({ @@ -3322,7 +3343,7 @@ static void prim_div(EvalState & state, const PosIdx pos, Value * * args, Value state.forceValue(*args[0], pos); state.forceValue(*args[1], pos); - NixFloat f2 = state.forceFloat(*args[1], pos, "while evaluating the second operand of the division"); + NixFloat f2 = state.forceFloat(*args[1], pos); if (f2 == 0) state.debugThrowLastTrace(EvalError({ .msg = hintfmt("division by zero"), @@ -3330,10 +3351,10 @@ static void prim_div(EvalState & state, const PosIdx pos, Value * * args, Value })); if (args[0]->type() == nFloat || args[1]->type() == nFloat) { - v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first operand of the division") / f2); + v.mkFloat(state.forceFloat(*args[0], pos) / state.forceFloat(*args[1], pos)); } else { - NixInt i1 = state.forceInt(*args[0], pos, "while evaluating the first operand of the division"); - NixInt i2 = state.forceInt(*args[1], pos, "while evaluating the second operand of the division"); + NixInt i1 = state.forceInt(*args[0], pos); + NixInt i2 = state.forceInt(*args[1], pos); /* Avoid division overflow as it might raise SIGFPE. */ if (i1 == std::numeric_limits<NixInt>::min() && i2 == -1) state.debugThrowLastTrace(EvalError({ @@ -3356,8 +3377,7 @@ static RegisterPrimOp primop_div({ static void prim_bitAnd(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitAnd") - & state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitAnd")); + v.mkInt(state.forceInt(*args[0], pos) & state.forceInt(*args[1], pos)); } static RegisterPrimOp primop_bitAnd({ @@ -3371,8 +3391,7 @@ static RegisterPrimOp primop_bitAnd({ static void prim_bitOr(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitOr") - | state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitOr")); + v.mkInt(state.forceInt(*args[0], pos) | state.forceInt(*args[1], pos)); } static RegisterPrimOp primop_bitOr({ @@ -3386,8 +3405,7 @@ static RegisterPrimOp primop_bitOr({ static void prim_bitXor(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitXor") - ^ state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitXor")); + v.mkInt(state.forceInt(*args[0], pos) ^ state.forceInt(*args[1], pos)); } static RegisterPrimOp primop_bitXor({ @@ -3403,8 +3421,7 @@ static void prim_lessThan(EvalState & state, const PosIdx pos, Value * * args, V { state.forceValue(*args[0], pos); state.forceValue(*args[1], pos); - // pos is exact here, no need for a message. - CompareValues comp(state, pos, ""); + CompareValues comp{state}; v.mkBool(comp(args[0], args[1])); } @@ -3431,7 +3448,7 @@ static RegisterPrimOp primop_lessThan({ static void prim_toString(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - auto s = state.coerceToString(pos, *args[0], context, true, false, "while evaluating the first argument passed to builtins.toString"); + auto s = state.coerceToString(pos, *args[0], context, true, false); v.mkString(*s, context); } @@ -3465,10 +3482,10 @@ static RegisterPrimOp primop_toString({ non-negative. */ static void prim_substring(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - int start = state.forceInt(*args[0], pos, "while evaluating the first argument (the start offset) passed to builtins.substring"); - int len = state.forceInt(*args[1], pos, "while evaluating the second argument (the substring length) passed to builtins.substring"); + int start = state.forceInt(*args[0], pos); + int len = state.forceInt(*args[1], pos); PathSet context; - auto s = state.coerceToString(pos, *args[2], context, "while evaluating the third argument (the string) passed to builtins.substring"); + auto s = state.coerceToString(pos, *args[2], context); if (start < 0) state.debugThrowLastTrace(EvalError({ @@ -3502,7 +3519,7 @@ static RegisterPrimOp primop_substring({ static void prim_stringLength(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.stringLength"); + auto s = state.coerceToString(pos, *args[0], context); v.mkInt(s->size()); } @@ -3519,7 +3536,7 @@ static RegisterPrimOp primop_stringLength({ /* Return the cryptographic hash of a string in base-16. */ static void prim_hashString(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto type = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.hashString"); + auto type = state.forceStringNoCtx(*args[0], pos); std::optional<HashType> ht = parseHashType(type); if (!ht) state.debugThrowLastTrace(Error({ @@ -3528,7 +3545,7 @@ static void prim_hashString(EvalState & state, const PosIdx pos, Value * * args, })); PathSet context; // discarded - auto s = state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.hashString"); + auto s = state.forceString(*args[1], context, pos); v.mkString(hashString(*ht, s).to_string(Base16, false)); } @@ -3567,14 +3584,14 @@ std::shared_ptr<RegexCache> makeRegexCache() void prim_match(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto re = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.match"); + auto re = state.forceStringNoCtx(*args[0], pos); try { auto regex = state.regexCache->get(re); PathSet context; - const auto str = state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.match"); + const auto str = state.forceString(*args[1], context, pos); std::cmatch match; if (!std::regex_match(str.begin(), str.end(), match, regex)) { @@ -3647,14 +3664,14 @@ static RegisterPrimOp primop_match({ non-matching parts interleaved by the lists of the matching groups. */ void prim_split(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto re = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.split"); + auto re = state.forceStringNoCtx(*args[0], pos); try { auto regex = state.regexCache->get(re); PathSet context; - const auto str = state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.split"); + const auto str = state.forceString(*args[1], context, pos); auto begin = std::cregex_iterator(str.begin(), str.end(), regex); auto end = std::cregex_iterator(); @@ -3752,8 +3769,8 @@ static void prim_concatStringsSep(EvalState & state, const PosIdx pos, Value * * { PathSet context; - auto sep = state.forceString(*args[0], context, pos, "while evaluating the first argument (the separator string) passed to builtins.concatStringsSep"); - state.forceList(*args[1], pos, "while evaluating the second argument (the list of strings to concat) passed to builtins.concatStringsSep"); + auto sep = state.forceString(*args[0], context, pos); + state.forceList(*args[1], pos); std::string res; res.reserve((args[1]->listSize() + 32) * sep.size()); @@ -3761,7 +3778,7 @@ static void prim_concatStringsSep(EvalState & state, const PosIdx pos, Value * * for (auto elem : args[1]->listItems()) { if (first) first = false; else res += sep; - res += *state.coerceToString(pos, *elem, context, "while evaluating one element of the list of strings to concat passed to builtins.concatStringsSep"); + res += *state.coerceToString(pos, *elem, context); } v.mkString(res, context); @@ -3780,8 +3797,8 @@ static RegisterPrimOp primop_concatStringsSep({ static void prim_replaceStrings(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.replaceStrings"); - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.replaceStrings"); + state.forceList(*args[0], pos); + state.forceList(*args[1], pos); if (args[0]->listSize() != args[1]->listSize()) state.debugThrowLastTrace(EvalError({ .msg = hintfmt("'from' and 'to' arguments to 'replaceStrings' have different lengths"), @@ -3791,18 +3808,18 @@ static void prim_replaceStrings(EvalState & state, const PosIdx pos, Value * * a std::vector<std::string> from; from.reserve(args[0]->listSize()); for (auto elem : args[0]->listItems()) - from.emplace_back(state.forceString(*elem, pos, "while evaluating one of the strings to replace in builtins.replaceStrings")); + from.emplace_back(state.forceString(*elem, pos)); std::vector<std::pair<std::string, PathSet>> to; to.reserve(args[1]->listSize()); for (auto elem : args[1]->listItems()) { PathSet ctx; - auto s = state.forceString(*elem, ctx, pos, "while evaluating one of the replacement strings of builtins.replaceStrings"); + auto s = state.forceString(*elem, ctx, pos); to.emplace_back(s, std::move(ctx)); } PathSet context; - auto s = state.forceString(*args[2], context, pos, "while evaluating the third argument passed to builtins.replaceStrings"); + auto s = state.forceString(*args[2], context, pos); std::string res; // Loops one past last character to handle the case where 'from' contains an empty string. @@ -3860,7 +3877,7 @@ static RegisterPrimOp primop_replaceStrings({ static void prim_parseDrvName(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto name = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.parseDrvName"); + auto name = state.forceStringNoCtx(*args[0], pos); DrvName parsed(name); auto attrs = state.buildBindings(2); attrs.alloc(state.sName).mkString(parsed.name); @@ -3884,8 +3901,8 @@ static RegisterPrimOp primop_parseDrvName({ static void prim_compareVersions(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto version1 = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.compareVersions"); - auto version2 = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.compareVersions"); + auto version1 = state.forceStringNoCtx(*args[0], pos); + auto version2 = state.forceStringNoCtx(*args[1], pos); v.mkInt(compareVersions(version1, version2)); } @@ -3904,7 +3921,7 @@ static RegisterPrimOp primop_compareVersions({ static void prim_splitVersion(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - auto version = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.splitVersion"); + auto version = state.forceStringNoCtx(*args[0], pos); auto iter = version.cbegin(); Strings components; while (iter != version.cend()) { diff --git a/src/libexpr/primops/context.cc b/src/libexpr/primops/context.cc index 9fae0b14d..4b7357495 100644 --- a/src/libexpr/primops/context.cc +++ b/src/libexpr/primops/context.cc @@ -8,7 +8,7 @@ namespace nix { static void prim_unsafeDiscardStringContext(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.unsafeDiscardStringContext"); + auto s = state.coerceToString(pos, *args[0], context); v.mkString(*s); } @@ -18,7 +18,7 @@ static RegisterPrimOp primop_unsafeDiscardStringContext("__unsafeDiscardStringCo static void prim_hasContext(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - state.forceString(*args[0], context, pos, "while evaluating the argument passed to builtins.hasContext"); + state.forceString(*args[0], context, pos); v.mkBool(!context.empty()); } @@ -34,11 +34,18 @@ static RegisterPrimOp primop_hasContext("__hasContext", 1, prim_hasContext); static void prim_unsafeDiscardOutputDependency(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.unsafeDiscardOutputDependency"); + auto s = state.coerceToString(pos, *args[0], context); PathSet context2; - for (auto & p : context) - context2.insert(p.at(0) == '=' ? std::string(p, 1) : p); + for (auto && p : context) { + auto c = NixStringContextElem::parse(*state.store, p); + if (auto * ptr = std::get_if<NixStringContextElem::DrvDeep>(&c)) { + context2.emplace(state.store->printStorePath(ptr->drvPath)); + } else { + /* Can reuse original item */ + context2.emplace(std::move(p)); + } + } v.mkString(*s, context2); } @@ -73,35 +80,23 @@ static void prim_getContext(EvalState & state, const PosIdx pos, Value * * args, Strings outputs; }; PathSet context; - state.forceString(*args[0], context, pos, "while evaluating the argument passed to builtins.getContext"); - auto contextInfos = std::map<Path, ContextInfo>(); + state.forceString(*args[0], context, pos); + auto contextInfos = std::map<StorePath, ContextInfo>(); for (const auto & p : context) { Path drv; std::string output; - const Path * path = &p; - if (p.at(0) == '=') { - drv = std::string(p, 1); - path = &drv; - } else if (p.at(0) == '!') { - NixStringContextElem ctx = decodeContext(*state.store, p); - drv = state.store->printStorePath(ctx.first); - output = ctx.second; - path = &drv; - } - auto isPath = drv.empty(); - auto isAllOutputs = (!drv.empty()) && output.empty(); - - auto iter = contextInfos.find(*path); - if (iter == contextInfos.end()) { - contextInfos.emplace(*path, ContextInfo{isPath, isAllOutputs, output.empty() ? Strings{} : Strings{std::move(output)}}); - } else { - if (isPath) - iter->second.path = true; - else if (isAllOutputs) - iter->second.allOutputs = true; - else - iter->second.outputs.emplace_back(std::move(output)); - } + NixStringContextElem ctx = NixStringContextElem::parse(*state.store, p); + std::visit(overloaded { + [&](NixStringContextElem::DrvDeep & d) { + contextInfos[d.drvPath].allOutputs = true; + }, + [&](NixStringContextElem::Built & b) { + contextInfos[b.drvPath].outputs.emplace_back(std::move(output)); + }, + [&](NixStringContextElem::Opaque & o) { + contextInfos[o.path].path = true; + }, + }, ctx.raw()); } auto attrs = state.buildBindings(contextInfos.size()); @@ -120,7 +115,7 @@ static void prim_getContext(EvalState & state, const PosIdx pos, Value * * args, for (const auto & [i, output] : enumerate(info.second.outputs)) (outputsVal.listElems()[i] = state.allocValue())->mkString(output); } - attrs.alloc(info.first).mkAttrs(infoAttrs); + attrs.alloc(state.store->printStorePath(info.first)).mkAttrs(infoAttrs); } v.mkAttrs(attrs); @@ -137,9 +132,9 @@ static RegisterPrimOp primop_getContext("__getContext", 1, prim_getContext); static void prim_appendContext(EvalState & state, const PosIdx pos, Value * * args, Value & v) { PathSet context; - auto orig = state.forceString(*args[0], context, noPos, "while evaluating the first argument passed to builtins.appendContext"); + auto orig = state.forceString(*args[0], context, pos); - state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.appendContext"); + state.forceAttrs(*args[1], pos); auto sPath = state.symbols.create("path"); auto sAllOutputs = state.symbols.create("allOutputs"); @@ -147,24 +142,24 @@ static void prim_appendContext(EvalState & state, const PosIdx pos, Value * * ar const auto & name = state.symbols[i.name]; if (!state.store->isStorePath(name)) throw EvalError({ - .msg = hintfmt("context key '%s' is not a store path", name), + .msg = hintfmt("Context key '%s' is not a store path", name), .errPos = state.positions[i.pos] }); if (!settings.readOnlyMode) state.store->ensurePath(state.store->parseStorePath(name)); - state.forceAttrs(*i.value, i.pos, "while evaluating the value of a string context"); + state.forceAttrs(*i.value, i.pos); auto iter = i.value->attrs->find(sPath); if (iter != i.value->attrs->end()) { - if (state.forceBool(*iter->value, iter->pos, "while evaluating the `path` attribute of a string context")) + if (state.forceBool(*iter->value, iter->pos)) context.emplace(name); } iter = i.value->attrs->find(sAllOutputs); if (iter != i.value->attrs->end()) { - if (state.forceBool(*iter->value, iter->pos, "while evaluating the `allOutputs` attribute of a string context")) { + if (state.forceBool(*iter->value, iter->pos)) { if (!isDerivation(name)) { throw EvalError({ - .msg = hintfmt("tried to add all-outputs context of %s, which is not a derivation, to a string", name), + .msg = hintfmt("Tried to add all-outputs context of %s, which is not a derivation, to a string", name), .errPos = state.positions[i.pos] }); } @@ -174,15 +169,15 @@ static void prim_appendContext(EvalState & state, const PosIdx pos, Value * * ar iter = i.value->attrs->find(state.sOutputs); if (iter != i.value->attrs->end()) { - state.forceList(*iter->value, iter->pos, "while evaluating the `outputs` attribute of a string context"); + state.forceList(*iter->value, iter->pos); if (iter->value->listSize() && !isDerivation(name)) { throw EvalError({ - .msg = hintfmt("tried to add derivation output context of %s, which is not a derivation, to a string", name), + .msg = hintfmt("Tried to add derivation output context of %s, which is not a derivation, to a string", name), .errPos = state.positions[i.pos] }); } for (auto elem : iter->value->listItems()) { - auto outputName = state.forceStringNoCtx(*elem, iter->pos, "while evaluating an output name within a string context"); + auto outputName = state.forceStringNoCtx(*elem, iter->pos); context.insert(concatStrings("!", outputName, "!", name)); } } diff --git a/src/libexpr/primops/fetchClosure.cc b/src/libexpr/primops/fetchClosure.cc index 0dfa97fa3..662c9652e 100644 --- a/src/libexpr/primops/fetchClosure.cc +++ b/src/libexpr/primops/fetchClosure.cc @@ -7,7 +7,7 @@ namespace nix { static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.fetchClosure"); + state.forceAttrs(*args[0], pos); std::optional<std::string> fromStoreUrl; std::optional<StorePath> fromPath; @@ -19,8 +19,7 @@ static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * arg if (attrName == "fromPath") { PathSet context; - fromPath = state.coerceToStorePath(attr.pos, *attr.value, context, - "while evaluating the 'fromPath' attribute passed to builtins.fetchClosure"); + fromPath = state.coerceToStorePath(attr.pos, *attr.value, context); } else if (attrName == "toPath") { @@ -28,14 +27,12 @@ static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * arg toCA = true; if (attr.value->type() != nString || attr.value->string.s != std::string("")) { PathSet context; - toPath = state.coerceToStorePath(attr.pos, *attr.value, context, - "while evaluating the 'toPath' attribute passed to builtins.fetchClosure"); + toPath = state.coerceToStorePath(attr.pos, *attr.value, context); } } else if (attrName == "fromStore") - fromStoreUrl = state.forceStringNoCtx(*attr.value, attr.pos, - "while evaluating the 'fromStore' attribute passed to builtins.fetchClosure"); + fromStoreUrl = state.forceStringNoCtx(*attr.value, attr.pos); else throw Error({ diff --git a/src/libexpr/primops/fetchMercurial.cc b/src/libexpr/primops/fetchMercurial.cc index c9c93bdba..249c0934e 100644 --- a/src/libexpr/primops/fetchMercurial.cc +++ b/src/libexpr/primops/fetchMercurial.cc @@ -19,21 +19,23 @@ static void prim_fetchMercurial(EvalState & state, const PosIdx pos, Value * * a if (args[0]->type() == nAttrs) { + state.forceAttrs(*args[0], pos); + for (auto & attr : *args[0]->attrs) { std::string_view n(state.symbols[attr.name]); if (n == "url") - url = state.coerceToString(attr.pos, *attr.value, context, false, false, "while evaluating the `url` attribute passed to builtins.fetchMercurial").toOwned(); + url = state.coerceToString(attr.pos, *attr.value, context, false, false).toOwned(); else if (n == "rev") { // Ugly: unlike fetchGit, here the "rev" attribute can // be both a revision or a branch/tag name. - auto value = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `rev` attribute passed to builtins.fetchMercurial"); + auto value = state.forceStringNoCtx(*attr.value, attr.pos); if (std::regex_match(value.begin(), value.end(), revRegex)) rev = Hash::parseAny(value, htSHA1); else ref = value; } else if (n == "name") - name = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `name` attribute passed to builtins.fetchMercurial"); + name = state.forceStringNoCtx(*attr.value, attr.pos); else throw EvalError({ .msg = hintfmt("unsupported argument '%s' to 'fetchMercurial'", state.symbols[attr.name]), @@ -48,7 +50,7 @@ static void prim_fetchMercurial(EvalState & state, const PosIdx pos, Value * * a }); } else - url = state.coerceToString(pos, *args[0], context, false, false, "while evaluating the first argument passed to builtins.fetchMercurial").toOwned(); + url = state.coerceToString(pos, *args[0], context, false, false).toOwned(); // FIXME: git externals probably can be used to bypass the URI // whitelist. Ah well. diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 1fb480089..fb392a6e8 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -102,7 +102,7 @@ static void fetchTree( state.forceValue(*args[0], pos); if (args[0]->type() == nAttrs) { - state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.fetchTree"); + state.forceAttrs(*args[0], pos); fetchers::Attrs attrs; @@ -112,7 +112,7 @@ static void fetchTree( .msg = hintfmt("unexpected attribute 'type'"), .errPos = state.positions[pos] })); - type = state.forceStringNoCtx(*aType->value, aType->pos, "while evaluating the `type` attribute passed to builtins.fetchTree"); + type = state.forceStringNoCtx(*aType->value, aType->pos); } else if (!type) state.debugThrowLastTrace(EvalError({ .msg = hintfmt("attribute 'type' is missing in call to 'fetchTree'"), @@ -125,7 +125,7 @@ static void fetchTree( if (attr.name == state.sType) continue; state.forceValue(*attr.value, attr.pos); if (attr.value->type() == nPath || attr.value->type() == nString) { - auto s = state.coerceToString(attr.pos, *attr.value, context, false, false, "").toOwned(); + auto s = state.coerceToString(attr.pos, *attr.value, context, false, false).toOwned(); attrs.emplace(state.symbols[attr.name], state.symbols[attr.name] == "url" ? type == "git" @@ -151,7 +151,7 @@ static void fetchTree( input = fetchers::Input::fromAttrs(std::move(attrs)); } else { - auto url = state.coerceToString(pos, *args[0], context, false, false, "while evaluating the first argument passed to the fetcher").toOwned(); + auto url = state.coerceToString(pos, *args[0], context, false, false).toOwned(); if (type == "git") { fetchers::Attrs attrs; @@ -195,14 +195,16 @@ static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v if (args[0]->type() == nAttrs) { + state.forceAttrs(*args[0], pos); + for (auto & attr : *args[0]->attrs) { std::string_view n(state.symbols[attr.name]); if (n == "url") - url = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the url we should fetch"); + url = state.forceStringNoCtx(*attr.value, attr.pos); else if (n == "sha256") - expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the sha256 of the content we should fetch"), htSHA256); + expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos), htSHA256); else if (n == "name") - name = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the name of the content we should fetch"); + name = state.forceStringNoCtx(*attr.value, attr.pos); else state.debugThrowLastTrace(EvalError({ .msg = hintfmt("unsupported argument '%s' to '%s'", n, who), @@ -216,7 +218,10 @@ static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v .errPos = state.positions[pos] })); } else - url = state.forceStringNoCtx(*args[0], pos, "while evaluating the url we should fetch"); + url = state.forceStringNoCtx(*args[0], pos); + + if (who == "fetchTarball") + url = evalSettings.resolvePseudoUrl(*url); state.checkURI(*url); diff --git a/src/libexpr/primops/fromTOML.cc b/src/libexpr/primops/fromTOML.cc index 8a5231781..9753e2ac9 100644 --- a/src/libexpr/primops/fromTOML.cc +++ b/src/libexpr/primops/fromTOML.cc @@ -7,7 +7,7 @@ namespace nix { static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, Value & val) { - auto toml = state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.fromTOML"); + auto toml = state.forceStringNoCtx(*args[0], pos); std::istringstream tomlStream(std::string{toml}); diff --git a/src/libexpr/tests/error_traces.cc b/src/libexpr/tests/error_traces.cc deleted file mode 100644 index 8741ecdd2..000000000 --- a/src/libexpr/tests/error_traces.cc +++ /dev/null @@ -1,94 +0,0 @@ -#include <gmock/gmock.h> -#include <gtest/gtest.h> - -#include "libexprtests.hh" - -namespace nix { - - using namespace testing; - - // Testing eval of PrimOp's - class ErrorTraceTest : public LibExprTest { }; - -#define ASSERT_TRACE1(args, type, message) \ - ASSERT_THROW( \ - try { \ - eval("builtins." args); \ - } catch (BaseError & e) { \ - ASSERT_EQ(PrintToString(e.info().msg), \ - PrintToString(message)); \ - auto trace = e.info().traces.rbegin(); \ - ASSERT_EQ(PrintToString(trace->hint), \ - PrintToString(hintfmt("while calling the '%s' builtin", "genericClosure"))); \ - throw; \ - } \ - , type \ - ) - -#define ASSERT_TRACE2(args, type, message, context) \ - ASSERT_THROW( \ - try { \ - eval("builtins." args); \ - } catch (BaseError & e) { \ - ASSERT_EQ(PrintToString(e.info().msg), \ - PrintToString(message)); \ - auto trace = e.info().traces.rbegin(); \ - ASSERT_EQ(PrintToString(trace->hint), \ - PrintToString(context)); \ - ++trace; \ - ASSERT_EQ(PrintToString(trace->hint), \ - PrintToString(hintfmt("while calling the '%s' builtin", "genericClosure"))); \ - throw; \ - } \ - , type \ - ) - - TEST_F(ErrorTraceTest, genericClosure) { \ - ASSERT_TRACE2("genericClosure 1", - TypeError, - hintfmt("value is %s while a set was expected", "an integer"), - hintfmt("while evaluating the first argument passed to builtins.genericClosure")); - - ASSERT_TRACE1("genericClosure {}", - TypeError, - hintfmt("attribute '%s' missing %s", "startSet", normaltxt("in the attrset passed as argument to builtins.genericClosure"))); - - ASSERT_TRACE2("genericClosure { startSet = 1; }", - TypeError, - hintfmt("value is %s while a list was expected", "an integer"), - hintfmt("while evaluating the 'startSet' attribute passed as argument to builtins.genericClosure")); - - // Okay: "genericClosure { startSet = []; }" - - ASSERT_TRACE2("genericClosure { startSet = [{ key = 1;}]; operator = true; }", - TypeError, - hintfmt("value is %s while a function was expected", "a Boolean"), - hintfmt("while evaluating the 'operator' attribute passed as argument to builtins.genericClosure")); - - ASSERT_TRACE2("genericClosure { startSet = [{ key = 1;}]; operator = item: true; }", - TypeError, - hintfmt("value is %s while a list was expected", "a Boolean"), - hintfmt("while evaluating the return value of the `operator` passed to builtins.genericClosure")); // TODO: inconsistent naming - - ASSERT_TRACE2("genericClosure { startSet = [{ key = 1;}]; operator = item: [ true ]; }", - TypeError, - hintfmt("value is %s while a set was expected", "a Boolean"), - hintfmt("while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure")); - - ASSERT_TRACE1("genericClosure { startSet = [{ key = 1;}]; operator = item: [ {} ]; }", - TypeError, - hintfmt("attribute '%s' missing %s", "key", normaltxt("in one of the attrsets generated by (or initially passed to) builtins.genericClosure"))); - - ASSERT_TRACE2("genericClosure { startSet = [{ key = 1;}]; operator = item: [{ key = ''a''; }]; }", - EvalError, - hintfmt("cannot compare %s with %s", "a string", "an integer"), - hintfmt("while comparing the `key` attributes of two genericClosure elements")); - - ASSERT_TRACE2("genericClosure { startSet = [ true ]; operator = item: [{ key = ''a''; }]; }", - TypeError, - hintfmt("value is %s while a set was expected", "a Boolean"), - hintfmt("while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure")); - - } - -} /* namespace nix */ diff --git a/src/libexpr/tests/local.mk b/src/libexpr/tests/local.mk index b95980cab..e483575a4 100644 --- a/src/libexpr/tests/local.mk +++ b/src/libexpr/tests/local.mk @@ -6,7 +6,9 @@ libexpr-tests_DIR := $(d) libexpr-tests_INSTALL_DIR := -libexpr-tests_SOURCES := $(wildcard $(d)/*.cc) +libexpr-tests_SOURCES := \ + $(wildcard $(d)/*.cc) \ + $(wildcard $(d)/value/*.cc) libexpr-tests_CXXFLAGS += -I src/libexpr -I src/libutil -I src/libstore -I src/libexpr/tests diff --git a/src/libexpr/tests/primops.cc b/src/libexpr/tests/primops.cc index 9cdcf64a1..bcdc7086b 100644 --- a/src/libexpr/tests/primops.cc +++ b/src/libexpr/tests/primops.cc @@ -823,10 +823,4 @@ namespace nix { for (const auto [n, elem] : enumerate(v.listItems())) ASSERT_THAT(*elem, IsStringEq(expected[n])); } - - TEST_F(PrimOpTest, genericClosure_not_strict) { - // Operator should not be used when startSet is empty - auto v = eval("builtins.genericClosure { startSet = []; }"); - ASSERT_THAT(v, IsListOfSize(0)); - } } /* namespace nix */ diff --git a/src/libexpr/tests/value/context.cc b/src/libexpr/tests/value/context.cc new file mode 100644 index 000000000..d5c9d3bce --- /dev/null +++ b/src/libexpr/tests/value/context.cc @@ -0,0 +1,72 @@ +#include "value/context.hh" + +#include "libexprtests.hh" + +namespace nix { + +// Testing of trivial expressions +struct NixStringContextElemTest : public LibExprTest { + const Store & store() const { + return *LibExprTest::store; + } +}; + +TEST_F(NixStringContextElemTest, empty_invalid) { + EXPECT_THROW( + NixStringContextElem::parse(store(), ""), + BadNixStringContextElem); +} + +TEST_F(NixStringContextElemTest, single_bang_invalid) { + EXPECT_THROW( + NixStringContextElem::parse(store(), "!"), + BadNixStringContextElem); +} + +TEST_F(NixStringContextElemTest, double_bang_invalid) { + EXPECT_THROW( + NixStringContextElem::parse(store(), "!!/"), + BadStorePath); +} + +TEST_F(NixStringContextElemTest, eq_slash_invalid) { + EXPECT_THROW( + NixStringContextElem::parse(store(), "=/"), + BadStorePath); +} + +TEST_F(NixStringContextElemTest, slash_invalid) { + EXPECT_THROW( + NixStringContextElem::parse(store(), "/"), + BadStorePath); +} + +TEST_F(NixStringContextElemTest, opaque) { + std::string_view opaque = "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x"; + auto elem = NixStringContextElem::parse(store(), opaque); + auto * p = std::get_if<NixStringContextElem::Opaque>(&elem); + ASSERT_TRUE(p); + ASSERT_EQ(p->path, store().parseStorePath(opaque)); + ASSERT_EQ(elem.to_string(store()), opaque); +} + +TEST_F(NixStringContextElemTest, drvDeep) { + std::string_view drvDeep = "=/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv"; + auto elem = NixStringContextElem::parse(store(), drvDeep); + auto * p = std::get_if<NixStringContextElem::DrvDeep>(&elem); + ASSERT_TRUE(p); + ASSERT_EQ(p->drvPath, store().parseStorePath(drvDeep.substr(1))); + ASSERT_EQ(elem.to_string(store()), drvDeep); +} + +TEST_F(NixStringContextElemTest, built) { + std::string_view built = "!foo!/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv"; + auto elem = NixStringContextElem::parse(store(), built); + auto * p = std::get_if<NixStringContextElem::Built>(&elem); + ASSERT_TRUE(p); + ASSERT_EQ(p->output, "foo"); + ASSERT_EQ(p->drvPath, store().parseStorePath(built.substr(5))); + ASSERT_EQ(elem.to_string(store()), built); +} + +} diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh index f57597cff..508dbe218 100644 --- a/src/libexpr/value.hh +++ b/src/libexpr/value.hh @@ -3,6 +3,7 @@ #include <cassert> #include "symbol-table.hh" +#include "value/context.hh" #if HAVE_BOEHMGC #include <gc/gc_allocator.h> @@ -67,8 +68,6 @@ class XMLWriter; typedef int64_t NixInt; typedef double NixFloat; -typedef std::pair<StorePath, std::string> NixStringContextElem; -typedef std::vector<NixStringContextElem> NixStringContext; /* External values must descend from ExternalValueBase, so that * type-agnostic nix functions (e.g. showType) can be implemented @@ -90,7 +89,7 @@ class ExternalValueBase /* Coerce the value to a string. Defaults to uncoercable, i.e. throws an * error. */ - virtual std::string coerceToString(const Pos & pos, PathSet & context, bool copyMore, bool copyToStore, std::string_view errorCtx) const; + virtual std::string coerceToString(const Pos & pos, PathSet & context, bool copyMore, bool copyToStore) const; /* Compare to another value of the same type. Defaults to uncomparable, * i.e. always false. diff --git a/src/libexpr/value/context.cc b/src/libexpr/value/context.cc new file mode 100644 index 000000000..61d9c53df --- /dev/null +++ b/src/libexpr/value/context.cc @@ -0,0 +1,67 @@ +#include "value/context.hh" +#include "store-api.hh" + +#include <optional> + +namespace nix { + +NixStringContextElem NixStringContextElem::parse(const Store & store, std::string_view s0) +{ + std::string_view s = s0; + + if (s.size() == 0) { + throw BadNixStringContextElem(s0, + "String context element should never be an empty string"); + } + switch (s.at(0)) { + case '!': { + s = s.substr(1); // advance string to parse after first ! + size_t index = s.find("!"); + // This makes index + 1 safe. Index can be the length (one after index + // of last character), so given any valid character index --- a + // successful find --- we can add one. + if (index == std::string_view::npos) { + throw BadNixStringContextElem(s0, + "String content element beginning with '!' should have a second '!'"); + } + return NixStringContextElem::Built { + .drvPath = store.parseStorePath(s.substr(index + 1)), + .output = std::string(s.substr(0, index)), + }; + } + case '=': { + return NixStringContextElem::DrvDeep { + .drvPath = store.parseStorePath(s.substr(1)), + }; + } + default: { + return NixStringContextElem::Opaque { + .path = store.parseStorePath(s), + }; + } + } +} + +std::string NixStringContextElem::to_string(const Store & store) const { + return std::visit(overloaded { + [&](const NixStringContextElem::Built & b) { + std::string res; + res += '!'; + res += b.output; + res += '!'; + res += store.printStorePath(b.drvPath); + return res; + }, + [&](const NixStringContextElem::DrvDeep & d) { + std::string res; + res += '='; + res += store.printStorePath(d.drvPath); + return res; + }, + [&](const NixStringContextElem::Opaque & o) { + return store.printStorePath(o.path); + }, + }, raw()); +} + +} diff --git a/src/libexpr/value/context.hh b/src/libexpr/value/context.hh new file mode 100644 index 000000000..d8008c436 --- /dev/null +++ b/src/libexpr/value/context.hh @@ -0,0 +1,90 @@ +#pragma once + +#include "util.hh" +#include "path.hh" + +#include <optional> + +#include <nlohmann/json_fwd.hpp> + +namespace nix { + +class BadNixStringContextElem : public Error +{ +public: + std::string_view raw; + + template<typename... Args> + BadNixStringContextElem(std::string_view raw_, const Args & ... args) + : Error("") + { + raw = raw_; + auto hf = hintfmt(args...); + err.msg = hintfmt("Bad String Context element: %1%: %2%", normaltxt(hf.str()), raw); + } +}; + +class Store; + +/* Plain opaque path to some store object. + + Encoded as just the path: ‘<path>’. +*/ +struct NixStringContextElem_Opaque { + StorePath path; +}; + +/* Path to a derivation and its entire build closure. + + The path doesn't just refer to derivation itself and its closure, but + also all outputs of all derivations in that closure (including the + root derivation). + + Encoded in the form ‘=<drvPath>’. +*/ +struct NixStringContextElem_DrvDeep { + StorePath drvPath; +}; + +/* Derivation output. + + Encoded in the form ‘!<output>!<drvPath>’. +*/ +struct NixStringContextElem_Built { + StorePath drvPath; + std::string output; +}; + +using _NixStringContextElem_Raw = std::variant< + NixStringContextElem_Opaque, + NixStringContextElem_DrvDeep, + NixStringContextElem_Built +>; + +struct NixStringContextElem : _NixStringContextElem_Raw { + using Raw = _NixStringContextElem_Raw; + using Raw::Raw; + + using Opaque = NixStringContextElem_Opaque; + using DrvDeep = NixStringContextElem_DrvDeep; + using Built = NixStringContextElem_Built; + + inline const Raw & raw() const { + return static_cast<const Raw &>(*this); + } + inline Raw & raw() { + return static_cast<Raw &>(*this); + } + + /* Decode a context string, one of: + - ‘<path>’ + - ‘=<path>’ + - ‘!<name>!<path>’ + */ + static NixStringContextElem parse(const Store & store, std::string_view s); + std::string to_string(const Store & store) const; +}; + +typedef std::vector<NixStringContextElem> NixStringContext; + +} diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index d4871a8e2..6d4365652 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -404,6 +404,8 @@ int handleExceptions(const std::string & programName, std::function<void()> fun) return 1; } catch (BaseError & e) { logError(e.info()); + if (e.hasTrace() && !loggerSettings.showTrace.get()) + printError("(use '--show-trace' to show detailed location information)"); return e.status; } catch (std::bad_alloc & e) { printError(error + "out of memory"); diff --git a/src/libmain/shared.hh b/src/libmain/shared.hh index 3c37fd627..1715374a6 100644 --- a/src/libmain/shared.hh +++ b/src/libmain/shared.hh @@ -39,7 +39,6 @@ void printVersion(const std::string & programName); void printGCWarning(); class Store; -struct StorePathWithOutputs; void printMissing( ref<Store> store, diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc index 149d414d3..05bf0501d 100644 --- a/src/libstore/binary-cache-store.cc +++ b/src/libstore/binary-cache-store.cc @@ -502,22 +502,9 @@ void BinaryCacheStore::addSignatures(const StorePath & storePath, const StringSe writeNarInfo(narInfo); } -std::optional<std::string> BinaryCacheStore::getBuildLog(const StorePath & path) +std::optional<std::string> BinaryCacheStore::getBuildLogExact(const StorePath & path) { - auto drvPath = path; - - if (!path.isDerivation()) { - try { - auto info = queryPathInfo(path); - // FIXME: add a "Log" field to .narinfo - if (!info->deriver) return std::nullopt; - drvPath = *info->deriver; - } catch (InvalidPath &) { - return std::nullopt; - } - } - - auto logPath = "log/" + std::string(baseNameOf(printStorePath(drvPath))); + auto logPath = "log/" + std::string(baseNameOf(printStorePath(path))); debug("fetching build log from binary cache '%s/%s'", getUri(), logPath); diff --git a/src/libstore/binary-cache-store.hh b/src/libstore/binary-cache-store.hh index 8c82e2387..abd92a83c 100644 --- a/src/libstore/binary-cache-store.hh +++ b/src/libstore/binary-cache-store.hh @@ -129,7 +129,7 @@ public: void addSignatures(const StorePath & storePath, const StringSet & sigs) override; - std::optional<std::string> getBuildLog(const StorePath & path) override; + std::optional<std::string> getBuildLogExact(const StorePath & path) override; void addBuildLog(const StorePath & drvPath, std::string_view log) override; diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 5e86b5269..2021d0023 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -63,7 +63,7 @@ namespace nix { DerivationGoal::DerivationGoal(const StorePath & drvPath, - const StringSet & wantedOutputs, Worker & worker, BuildMode buildMode) + const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) : Goal(worker, DerivedPath::Built { .drvPath = drvPath, .outputs = wantedOutputs }) , useDerivation(true) , drvPath(drvPath) @@ -82,7 +82,7 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, DerivationGoal::DerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, - const StringSet & wantedOutputs, Worker & worker, BuildMode buildMode) + const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) : Goal(worker, DerivedPath::Built { .drvPath = drvPath, .outputs = wantedOutputs }) , useDerivation(false) , drvPath(drvPath) @@ -142,18 +142,12 @@ void DerivationGoal::work() (this->*state)(); } -void DerivationGoal::addWantedOutputs(const StringSet & outputs) +void DerivationGoal::addWantedOutputs(const OutputsSpec & outputs) { - /* If we already want all outputs, there is nothing to do. */ - if (wantedOutputs.empty()) return; - - if (outputs.empty()) { - wantedOutputs.clear(); + auto newWanted = wantedOutputs.union_(outputs); + if (!newWanted.isSubsetOf(wantedOutputs)) needRestart = true; - } else - for (auto & i : outputs) - if (wantedOutputs.insert(i).second) - needRestart = true; + wantedOutputs = newWanted; } @@ -390,7 +384,7 @@ void DerivationGoal::repairClosure() auto outputs = queryDerivationOutputMap(); StorePathSet outputClosure; for (auto & i : outputs) { - if (!wantOutput(i.first, wantedOutputs)) continue; + if (!wantedOutputs.contains(i.first)) continue; worker.store.computeFSClosure(i.second, outputClosure); } @@ -422,7 +416,7 @@ void DerivationGoal::repairClosure() if (drvPath2 == outputsToDrv.end()) addWaitee(upcast_goal(worker.makePathSubstitutionGoal(i, Repair))); else - addWaitee(worker.makeDerivationGoal(drvPath2->second, StringSet(), bmRepair)); + addWaitee(worker.makeDerivationGoal(drvPath2->second, OutputsSpec::All(), bmRepair)); } if (waitees.empty()) { @@ -991,10 +985,15 @@ void DerivationGoal::resolvedFinished() StorePathSet outputPaths; - // `wantedOutputs` might be empty, which means “all the outputs” - auto realWantedOutputs = wantedOutputs; - if (realWantedOutputs.empty()) - realWantedOutputs = resolvedDrv.outputNames(); + // `wantedOutputs` might merely indicate “all the outputs” + auto realWantedOutputs = std::visit(overloaded { + [&](const OutputsSpec::All &) { + return resolvedDrv.outputNames(); + }, + [&](const OutputsSpec::Names & names) { + return static_cast<std::set<std::string>>(names); + }, + }, wantedOutputs.raw()); for (auto & wantedOutput : realWantedOutputs) { auto initialOutput = get(initialOutputs, wantedOutput); @@ -1322,7 +1321,14 @@ std::pair<bool, DrvOutputs> DerivationGoal::checkPathValidity() if (!drv->type().isPure()) return { false, {} }; bool checkHash = buildMode == bmRepair; - auto wantedOutputsLeft = wantedOutputs; + auto wantedOutputsLeft = std::visit(overloaded { + [&](const OutputsSpec::All &) { + return StringSet {}; + }, + [&](const OutputsSpec::Names & names) { + return static_cast<StringSet>(names); + }, + }, wantedOutputs.raw()); DrvOutputs validOutputs; for (auto & i : queryPartialDerivationOutputMap()) { @@ -1331,7 +1337,7 @@ std::pair<bool, DrvOutputs> DerivationGoal::checkPathValidity() // this is an invalid output, gets catched with (!wantedOutputsLeft.empty()) continue; auto & info = *initialOutput; - info.wanted = wantOutput(i.first, wantedOutputs); + info.wanted = wantedOutputs.contains(i.first); if (info.wanted) wantedOutputsLeft.erase(i.first); if (i.second) { @@ -1369,7 +1375,7 @@ std::pair<bool, DrvOutputs> DerivationGoal::checkPathValidity() validOutputs.emplace(drvOutput, Realisation { drvOutput, info.known->path }); } - // If we requested all the outputs via the empty set, we are always fine. + // If we requested all the outputs, we are always fine. // If we requested specific elements, the loop above removes all the valid // ones, so any that are left must be invalid. if (!wantedOutputsLeft.empty()) diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh index d33e04cbc..707e38b4b 100644 --- a/src/libstore/build/derivation-goal.hh +++ b/src/libstore/build/derivation-goal.hh @@ -2,6 +2,7 @@ #include "parsed-derivations.hh" #include "lock.hh" +#include "outputs-spec.hh" #include "store-api.hh" #include "pathlocks.hh" #include "goal.hh" @@ -55,7 +56,7 @@ struct DerivationGoal : public Goal /* The specific outputs that we need to build. Empty means all of them. */ - StringSet wantedOutputs; + OutputsSpec wantedOutputs; /* Mapping from input derivations + output names to actual store paths. This is filled in by waiteeDone() as each dependency @@ -128,10 +129,10 @@ struct DerivationGoal : public Goal std::string machineName; DerivationGoal(const StorePath & drvPath, - const StringSet & wantedOutputs, Worker & worker, + const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode = bmNormal); DerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, - const StringSet & wantedOutputs, Worker & worker, + const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode = bmNormal); virtual ~DerivationGoal(); @@ -142,7 +143,7 @@ struct DerivationGoal : public Goal void work() override; /* Add wanted outputs to an already existing derivation goal. */ - void addWantedOutputs(const StringSet & outputs); + void addWantedOutputs(const OutputsSpec & outputs); /* The states. */ void getDerivation(); diff --git a/src/libstore/build/entry-points.cc b/src/libstore/build/entry-points.cc index e1b80165e..2925fe3ca 100644 --- a/src/libstore/build/entry-points.cc +++ b/src/libstore/build/entry-points.cc @@ -80,7 +80,7 @@ BuildResult Store::buildDerivation(const StorePath & drvPath, const BasicDerivat BuildMode buildMode) { Worker worker(*this, *this); - auto goal = worker.makeBasicDerivationGoal(drvPath, drv, {}, buildMode); + auto goal = worker.makeBasicDerivationGoal(drvPath, drv, OutputsSpec::All {}, buildMode); try { worker.run(Goals{goal}); @@ -89,7 +89,10 @@ BuildResult Store::buildDerivation(const StorePath & drvPath, const BasicDerivat return BuildResult { .status = BuildResult::MiscFailure, .errorMsg = e.msg(), - .path = DerivedPath::Built { .drvPath = drvPath }, + .path = DerivedPath::Built { + .drvPath = drvPath, + .outputs = OutputsSpec::All { }, + }, }; }; } @@ -130,7 +133,8 @@ void LocalStore::repairPath(const StorePath & path) auto info = queryPathInfo(path); if (info->deriver && isValidPath(*info->deriver)) { goals.clear(); - goals.insert(worker.makeDerivationGoal(*info->deriver, StringSet(), bmRepair)); + // FIXME: Should just build the specific output we need. + goals.insert(worker.makeDerivationGoal(*info->deriver, OutputsSpec::All { }, bmRepair)); worker.run(goals); } else throw Error(worker.exitStatus(), "cannot repair path '%s'", printStorePath(path)); diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 488e06d8c..9ab9b17fe 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -1459,7 +1459,7 @@ struct RestrictedStore : public virtual RestrictedStoreConfig, public virtual Lo unknown, downloadSize, narSize); } - virtual std::optional<std::string> getBuildLog(const StorePath & path) override + virtual std::optional<std::string> getBuildLogExact(const StorePath & path) override { return std::nullopt; } virtual void addBuildLog(const StorePath & path, std::string_view log) override @@ -2735,7 +2735,7 @@ DrvOutputs LocalDerivationGoal::registerOutputs() signRealisation(thisRealisation); worker.store.registerDrvOutput(thisRealisation); } - if (wantOutput(outputName, wantedOutputs)) + if (wantedOutputs.contains(outputName)) builtOutputs.emplace(thisRealisation.id, thisRealisation); } diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index b192fbc77..b94fb8416 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -42,7 +42,7 @@ Worker::~Worker() std::shared_ptr<DerivationGoal> Worker::makeDerivationGoalCommon( const StorePath & drvPath, - const StringSet & wantedOutputs, + const OutputsSpec & wantedOutputs, std::function<std::shared_ptr<DerivationGoal>()> mkDrvGoal) { std::weak_ptr<DerivationGoal> & goal_weak = derivationGoals[drvPath]; @@ -59,7 +59,7 @@ std::shared_ptr<DerivationGoal> Worker::makeDerivationGoalCommon( std::shared_ptr<DerivationGoal> Worker::makeDerivationGoal(const StorePath & drvPath, - const StringSet & wantedOutputs, BuildMode buildMode) + const OutputsSpec & wantedOutputs, BuildMode buildMode) { return makeDerivationGoalCommon(drvPath, wantedOutputs, [&]() -> std::shared_ptr<DerivationGoal> { return !dynamic_cast<LocalStore *>(&store) @@ -70,7 +70,7 @@ std::shared_ptr<DerivationGoal> Worker::makeDerivationGoal(const StorePath & drv std::shared_ptr<DerivationGoal> Worker::makeBasicDerivationGoal(const StorePath & drvPath, - const BasicDerivation & drv, const StringSet & wantedOutputs, BuildMode buildMode) + const BasicDerivation & drv, const OutputsSpec & wantedOutputs, BuildMode buildMode) { return makeDerivationGoalCommon(drvPath, wantedOutputs, [&]() -> std::shared_ptr<DerivationGoal> { return !dynamic_cast<LocalStore *>(&store) diff --git a/src/libstore/build/worker.hh b/src/libstore/build/worker.hh index a1e036a96..6d68d3cf1 100644 --- a/src/libstore/build/worker.hh +++ b/src/libstore/build/worker.hh @@ -140,15 +140,15 @@ public: /* derivation goal */ private: std::shared_ptr<DerivationGoal> makeDerivationGoalCommon( - const StorePath & drvPath, const StringSet & wantedOutputs, + const StorePath & drvPath, const OutputsSpec & wantedOutputs, std::function<std::shared_ptr<DerivationGoal>()> mkDrvGoal); public: std::shared_ptr<DerivationGoal> makeDerivationGoal( const StorePath & drvPath, - const StringSet & wantedOutputs, BuildMode buildMode = bmNormal); + const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal); std::shared_ptr<DerivationGoal> makeBasicDerivationGoal( const StorePath & drvPath, const BasicDerivation & drv, - const StringSet & wantedOutputs, BuildMode buildMode = bmNormal); + const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal); /* substitution goal */ std::shared_ptr<PathSubstitutionGoal> makePathSubstitutionGoal(const StorePath & storePath, RepairFlag repair = NoRepair, std::optional<ContentAddress> ca = std::nullopt); diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index 42a53912e..cf18724ef 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -688,12 +688,6 @@ std::map<std::string, Hash> staticOutputHashes(Store & store, const Derivation & } -bool wantOutput(const std::string & output, const std::set<std::string> & wanted) -{ - return wanted.empty() || wanted.find(output) != wanted.end(); -} - - static DerivationOutput readDerivationOutput(Source & in, const Store & store) { const auto pathS = readString(in); diff --git a/src/libstore/derivations.hh b/src/libstore/derivations.hh index f3cd87fb1..7ee3ded6a 100644 --- a/src/libstore/derivations.hh +++ b/src/libstore/derivations.hh @@ -294,8 +294,6 @@ typedef std::map<StorePath, DrvHash> DrvHashes; // FIXME: global, though at least thread-safe. extern Sync<DrvHashes> drvHashes; -bool wantOutput(const std::string & output, const std::set<std::string> & wanted); - struct Source; struct Sink; diff --git a/src/libstore/derived-path.cc b/src/libstore/derived-path.cc index 3fa5ae4f7..e0d86a42f 100644 --- a/src/libstore/derived-path.cc +++ b/src/libstore/derived-path.cc @@ -19,11 +19,11 @@ nlohmann::json DerivedPath::Built::toJSON(ref<Store> store) const { res["drvPath"] = store->printStorePath(drvPath); // Fallback for the input-addressed derivation case: We expect to always be // able to print the output paths, so let’s do it - const auto knownOutputs = store->queryPartialDerivationOutputMap(drvPath); - for (const auto & output : outputs) { - auto knownOutput = get(knownOutputs, output); - if (knownOutput && *knownOutput) - res["outputs"][output] = store->printStorePath(**knownOutput); + const auto outputMap = store->queryPartialDerivationOutputMap(drvPath); + for (const auto & [output, outputPathOpt] : outputMap) { + if (!outputs.contains(output)) continue; + if (outputPathOpt) + res["outputs"][output] = store->printStorePath(*outputPathOpt); else res["outputs"][output] = nullptr; } @@ -63,7 +63,7 @@ std::string DerivedPath::Built::to_string(const Store & store) const { return store.printStorePath(drvPath) + "!" - + (outputs.empty() ? std::string { "*" } : concatStringsSep(",", outputs)); + + outputs.to_string(); } std::string DerivedPath::to_string(const Store & store) const @@ -81,15 +81,10 @@ DerivedPath::Opaque DerivedPath::Opaque::parse(const Store & store, std::string_ DerivedPath::Built DerivedPath::Built::parse(const Store & store, std::string_view drvS, std::string_view outputsS) { - auto drvPath = store.parseStorePath(drvS); - std::set<std::string> outputs; - if (outputsS != "*") { - outputs = tokenizeString<std::set<std::string>>(outputsS, ","); - if (outputs.empty()) - throw Error( - "Explicit list of wanted outputs '%s' must not be empty. Consider using '*' as a wildcard meaning all outputs if no output in particular is wanted.", outputsS); - } - return {drvPath, outputs}; + return { + .drvPath = store.parseStorePath(drvS), + .outputs = OutputsSpec::parse(outputsS), + }; } DerivedPath DerivedPath::parse(const Store & store, std::string_view s) diff --git a/src/libstore/derived-path.hh b/src/libstore/derived-path.hh index 706e5dcb4..4edff7467 100644 --- a/src/libstore/derived-path.hh +++ b/src/libstore/derived-path.hh @@ -3,6 +3,7 @@ #include "util.hh" #include "path.hh" #include "realisation.hh" +#include "outputs-spec.hh" #include <optional> @@ -44,7 +45,7 @@ struct DerivedPathOpaque { */ struct DerivedPathBuilt { StorePath drvPath; - std::set<std::string> outputs; + OutputsSpec outputs; std::string to_string(const Store & store) const; static DerivedPathBuilt parse(const Store & store, std::string_view, std::string_view); diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index f026c8808..7111def92 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -676,7 +676,7 @@ public: - the store object is signed by one of the [`trusted-public-keys`](#conf-trusted-public-keys) - the substituter is in the [`trusted-substituters`](#conf-trusted-substituters) list - the [`require-sigs`](#conf-require-sigs) option has been set to `false` - - the store object is [output-addressed](glossary.md#gloss-output-addressed-store-object) + - the store object is [output-addressed](@docroot@/glossary.md#gloss-output-addressed-store-object) )", {"binary-caches"}}; diff --git a/src/libstore/legacy-ssh-store.cc b/src/libstore/legacy-ssh-store.cc index 4d398b21d..e1a4e13a3 100644 --- a/src/libstore/legacy-ssh-store.cc +++ b/src/libstore/legacy-ssh-store.cc @@ -279,7 +279,12 @@ public: conn->to.flush(); - BuildResult status { .path = DerivedPath::Built { .drvPath = drvPath } }; + BuildResult status { + .path = DerivedPath::Built { + .drvPath = drvPath, + .outputs = OutputsSpec::All { }, + }, + }; status.status = (BuildResult::Status) readInt(conn->from); conn->from >> status.errorMsg; diff --git a/src/libstore/local-fs-store.cc b/src/libstore/local-fs-store.cc index c5ae7536f..b224fc3e9 100644 --- a/src/libstore/local-fs-store.cc +++ b/src/libstore/local-fs-store.cc @@ -87,20 +87,8 @@ void LocalFSStore::narFromPath(const StorePath & path, Sink & sink) const std::string LocalFSStore::drvsLogDir = "drvs"; -std::optional<std::string> LocalFSStore::getBuildLog(const StorePath & path_) +std::optional<std::string> LocalFSStore::getBuildLogExact(const StorePath & path) { - auto path = path_; - - if (!path.isDerivation()) { - try { - auto info = queryPathInfo(path); - if (!info->deriver) return std::nullopt; - path = *info->deriver; - } catch (InvalidPath &) { - return std::nullopt; - } - } - auto baseName = path.to_string(); for (int j = 0; j < 2; j++) { diff --git a/src/libstore/local-fs-store.hh b/src/libstore/local-fs-store.hh index e6fb3201a..947707341 100644 --- a/src/libstore/local-fs-store.hh +++ b/src/libstore/local-fs-store.hh @@ -50,7 +50,7 @@ public: return getRealStoreDir() + "/" + std::string(storePath, storeDir.size() + 1); } - std::optional<std::string> getBuildLog(const StorePath & path) override; + std::optional<std::string> getBuildLogExact(const StorePath & path) override; }; diff --git a/src/libstore/log-store.cc b/src/libstore/log-store.cc new file mode 100644 index 000000000..8a26832ab --- /dev/null +++ b/src/libstore/log-store.cc @@ -0,0 +1,12 @@ +#include "log-store.hh" + +namespace nix { + +std::optional<std::string> LogStore::getBuildLog(const StorePath & path) { + auto maybePath = getBuildDerivationPath(path); + if (!maybePath) + return std::nullopt; + return getBuildLogExact(maybePath.value()); +} + +} diff --git a/src/libstore/log-store.hh b/src/libstore/log-store.hh index ff1b92e17..e4d95bab6 100644 --- a/src/libstore/log-store.hh +++ b/src/libstore/log-store.hh @@ -11,7 +11,9 @@ struct LogStore : public virtual Store /* Return the build log of the specified store path, if available, or null otherwise. */ - virtual std::optional<std::string> getBuildLog(const StorePath & path) = 0; + std::optional<std::string> getBuildLog(const StorePath & path); + + virtual std::optional<std::string> getBuildLogExact(const StorePath & path) = 0; virtual void addBuildLog(const StorePath & path, std::string_view log) = 0; diff --git a/src/libstore/misc.cc b/src/libstore/misc.cc index fb985c97b..b28768459 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -185,7 +185,7 @@ void Store::queryMissing(const std::vector<DerivedPath> & targets, knownOutputPaths = false; break; } - if (wantOutput(outputName, bfd.outputs) && !isValidPath(*pathOpt)) + if (bfd.outputs.contains(outputName) && !isValidPath(*pathOpt)) invalid.insert(*pathOpt); } if (knownOutputPaths && invalid.empty()) return; @@ -301,4 +301,47 @@ std::map<DrvOutput, StorePath> drvOutputReferences( return drvOutputReferences(Realisation::closure(store, inputRealisations), info->references); } +OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, Store * evalStore_) +{ + auto & evalStore = evalStore_ ? *evalStore_ : store; + + OutputPathMap outputs; + auto drv = evalStore.readDerivation(bfd.drvPath); + auto outputHashes = staticOutputHashes(store, drv); + auto drvOutputs = drv.outputsAndOptPaths(store); + auto outputNames = std::visit(overloaded { + [&](const OutputsSpec::All &) { + StringSet names; + for (auto & [outputName, _] : drv.outputs) + names.insert(outputName); + return names; + }, + [&](const OutputsSpec::Names & names) { + return static_cast<std::set<std::string>>(names); + }, + }, bfd.outputs.raw()); + for (auto & output : outputNames) { + auto outputHash = get(outputHashes, output); + if (!outputHash) + throw Error( + "the derivation '%s' doesn't have an output named '%s'", + store.printStorePath(bfd.drvPath), output); + if (settings.isExperimentalFeatureEnabled(Xp::CaDerivations)) { + DrvOutput outputId { *outputHash, output }; + auto realisation = store.queryRealisation(outputId); + if (!realisation) + throw MissingRealisation(outputId); + outputs.insert_or_assign(output, realisation->outPath); + } else { + // If ca-derivations isn't enabled, assume that + // the output path is statically known. + auto drvOutput = get(drvOutputs, output); + assert(drvOutput); + assert(drvOutput->second); + outputs.insert_or_assign(output, *drvOutput->second); + } + } + return outputs; +} + } diff --git a/src/libstore/outputs-spec.cc b/src/libstore/outputs-spec.cc new file mode 100644 index 000000000..096443cb2 --- /dev/null +++ b/src/libstore/outputs-spec.cc @@ -0,0 +1,190 @@ +#include "util.hh" +#include "outputs-spec.hh" +#include "nlohmann/json.hpp" + +#include <regex> + +namespace nix { + +bool OutputsSpec::contains(const std::string & outputName) const +{ + return std::visit(overloaded { + [&](const OutputsSpec::All &) { + return true; + }, + [&](const OutputsSpec::Names & outputNames) { + return outputNames.count(outputName) > 0; + }, + }, raw()); +} + + +std::optional<OutputsSpec> OutputsSpec::parseOpt(std::string_view s) +{ + // See checkName() for valid output name characters. + static std::regex regex(R"((\*)|([a-zA-Z\+\-\._\?=]+(,[a-zA-Z\+\-\._\?=]+)*))"); + + std::smatch match; + std::string s2 { s }; // until some improves std::regex + if (!std::regex_match(s2, match, regex)) + return std::nullopt; + + if (match[1].matched) + return { OutputsSpec::All {} }; + + if (match[2].matched) + return OutputsSpec::Names { tokenizeString<StringSet>(match[2].str(), ",") }; + + assert(false); +} + + +OutputsSpec OutputsSpec::parse(std::string_view s) +{ + std::optional spec = parseOpt(s); + if (!spec) + throw Error("invalid outputs specifier '%s'", s); + return *spec; +} + + +std::optional<std::pair<std::string_view, ExtendedOutputsSpec>> ExtendedOutputsSpec::parseOpt(std::string_view s) +{ + auto found = s.rfind('^'); + + if (found == std::string::npos) + return std::pair { s, ExtendedOutputsSpec::Default {} }; + + auto specOpt = OutputsSpec::parseOpt(s.substr(found + 1)); + if (!specOpt) + return std::nullopt; + return std::pair { s.substr(0, found), ExtendedOutputsSpec::Explicit { *std::move(specOpt) } }; +} + + +std::pair<std::string_view, ExtendedOutputsSpec> ExtendedOutputsSpec::parse(std::string_view s) +{ + std::optional spec = parseOpt(s); + if (!spec) + throw Error("invalid extended outputs specifier '%s'", s); + return *spec; +} + + +std::string OutputsSpec::to_string() const +{ + return std::visit(overloaded { + [&](const OutputsSpec::All &) -> std::string { + return "*"; + }, + [&](const OutputsSpec::Names & outputNames) -> std::string { + return concatStringsSep(",", outputNames); + }, + }, raw()); +} + + +std::string ExtendedOutputsSpec::to_string() const +{ + return std::visit(overloaded { + [&](const ExtendedOutputsSpec::Default &) -> std::string { + return ""; + }, + [&](const ExtendedOutputsSpec::Explicit & outputSpec) -> std::string { + return "^" + outputSpec.to_string(); + }, + }, raw()); +} + + +OutputsSpec OutputsSpec::union_(const OutputsSpec & that) const +{ + return std::visit(overloaded { + [&](const OutputsSpec::All &) -> OutputsSpec { + return OutputsSpec::All { }; + }, + [&](const OutputsSpec::Names & theseNames) -> OutputsSpec { + return std::visit(overloaded { + [&](const OutputsSpec::All &) -> OutputsSpec { + return OutputsSpec::All {}; + }, + [&](const OutputsSpec::Names & thoseNames) -> OutputsSpec { + OutputsSpec::Names ret = theseNames; + ret.insert(thoseNames.begin(), thoseNames.end()); + return ret; + }, + }, that.raw()); + }, + }, raw()); +} + + +bool OutputsSpec::isSubsetOf(const OutputsSpec & that) const +{ + return std::visit(overloaded { + [&](const OutputsSpec::All &) { + return true; + }, + [&](const OutputsSpec::Names & thoseNames) { + return std::visit(overloaded { + [&](const OutputsSpec::All &) { + return false; + }, + [&](const OutputsSpec::Names & theseNames) { + bool ret = true; + for (auto & o : theseNames) + if (thoseNames.count(o) == 0) + ret = false; + return ret; + }, + }, raw()); + }, + }, that.raw()); +} + +} + +namespace nlohmann { + +using namespace nix; + +OutputsSpec adl_serializer<OutputsSpec>::from_json(const json & json) { + auto names = json.get<StringSet>(); + if (names == StringSet({"*"})) + return OutputsSpec::All {}; + else + return OutputsSpec::Names { std::move(names) }; +} + +void adl_serializer<OutputsSpec>::to_json(json & json, OutputsSpec t) { + std::visit(overloaded { + [&](const OutputsSpec::All &) { + json = std::vector<std::string>({"*"}); + }, + [&](const OutputsSpec::Names & names) { + json = names; + }, + }, t.raw()); +} + + +ExtendedOutputsSpec adl_serializer<ExtendedOutputsSpec>::from_json(const json & json) { + if (json.is_null()) + return ExtendedOutputsSpec::Default {}; + else { + return ExtendedOutputsSpec::Explicit { json.get<OutputsSpec>() }; + } +} + +void adl_serializer<ExtendedOutputsSpec>::to_json(json & json, ExtendedOutputsSpec t) { + std::visit(overloaded { + [&](const ExtendedOutputsSpec::Default &) { + json = nullptr; + }, + [&](const ExtendedOutputsSpec::Explicit & e) { + adl_serializer<OutputsSpec>::to_json(json, e); + }, + }, t.raw()); +} + +} diff --git a/src/libstore/outputs-spec.hh b/src/libstore/outputs-spec.hh new file mode 100644 index 000000000..46bc35ebc --- /dev/null +++ b/src/libstore/outputs-spec.hh @@ -0,0 +1,95 @@ +#pragma once + +#include <cassert> +#include <optional> +#include <set> +#include <variant> + +#include "json-impls.hh" + +namespace nix { + +struct OutputNames : std::set<std::string> { + using std::set<std::string>::set; + + /* These need to be "inherited manually" */ + + OutputNames(const std::set<std::string> & s) + : std::set<std::string>(s) + { assert(!empty()); } + + OutputNames(std::set<std::string> && s) + : std::set<std::string>(s) + { assert(!empty()); } + + /* This set should always be non-empty, so we delete this + constructor in order make creating empty ones by mistake harder. + */ + OutputNames() = delete; +}; + +struct AllOutputs : std::monostate { }; + +typedef std::variant<AllOutputs, OutputNames> _OutputsSpecRaw; + +struct OutputsSpec : _OutputsSpecRaw { + using Raw = _OutputsSpecRaw; + using Raw::Raw; + + /* Force choosing a variant */ + OutputsSpec() = delete; + + using Names = OutputNames; + using All = AllOutputs; + + inline const Raw & raw() const { + return static_cast<const Raw &>(*this); + } + + inline Raw & raw() { + return static_cast<Raw &>(*this); + } + + bool contains(const std::string & output) const; + + /* Create a new OutputsSpec which is the union of this and that. */ + OutputsSpec union_(const OutputsSpec & that) const; + + /* Whether this OutputsSpec is a subset of that. */ + bool isSubsetOf(const OutputsSpec & outputs) const; + + /* Parse a string of the form 'output1,...outputN' or + '*', returning the outputs spec. */ + static OutputsSpec parse(std::string_view s); + static std::optional<OutputsSpec> parseOpt(std::string_view s); + + std::string to_string() const; +}; + +struct DefaultOutputs : std::monostate { }; + +typedef std::variant<DefaultOutputs, OutputsSpec> _ExtendedOutputsSpecRaw; + +struct ExtendedOutputsSpec : _ExtendedOutputsSpecRaw { + using Raw = _ExtendedOutputsSpecRaw; + using Raw::Raw; + + using Default = DefaultOutputs; + using Explicit = OutputsSpec; + + inline const Raw & raw() const { + return static_cast<const Raw &>(*this); + } + + /* Parse a string of the form 'prefix^output1,...outputN' or + 'prefix^*', returning the prefix and the extended outputs spec. */ + static std::pair<std::string_view, ExtendedOutputsSpec> parse(std::string_view s); + static std::optional<std::pair<std::string_view, ExtendedOutputsSpec>> parseOpt(std::string_view s); + + std::string to_string() const; +}; + +} + +JSON_IMPL(OutputsSpec) +JSON_IMPL(ExtendedOutputsSpec) diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index fda55b2b6..bd55a9d06 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -3,6 +3,80 @@ namespace nix { +std::string ValidPathInfo::fingerprint(const Store & store) const +{ + if (narSize == 0) + throw Error("cannot calculate fingerprint of path '%s' because its size is not known", + store.printStorePath(path)); + return + "1;" + store.printStorePath(path) + ";" + + narHash.to_string(Base32, true) + ";" + + std::to_string(narSize) + ";" + + concatStringsSep(",", store.printStorePathSet(references)); +} + + +void ValidPathInfo::sign(const Store & store, const SecretKey & secretKey) +{ + sigs.insert(secretKey.signDetached(fingerprint(store))); +} + + +bool ValidPathInfo::isContentAddressed(const Store & store) const +{ + if (! ca) return false; + + auto caPath = std::visit(overloaded { + [&](const TextHash & th) { + return store.makeTextPath(path.name(), th.hash, references); + }, + [&](const FixedOutputHash & fsh) { + auto refs = references; + bool hasSelfReference = false; + if (refs.count(path)) { + hasSelfReference = true; + refs.erase(path); + } + return store.makeFixedOutputPath(fsh.method, fsh.hash, path.name(), refs, hasSelfReference); + } + }, *ca); + + bool res = caPath == path; + + if (!res) + printError("warning: path '%s' claims to be content-addressed but isn't", store.printStorePath(path)); + + return res; +} + + +size_t ValidPathInfo::checkSignatures(const Store & store, const PublicKeys & publicKeys) const +{ + if (isContentAddressed(store)) return maxSigs; + + size_t good = 0; + for (auto & sig : sigs) + if (checkSignature(store, publicKeys, sig)) + good++; + return good; +} + + +bool ValidPathInfo::checkSignature(const Store & store, const PublicKeys & publicKeys, const std::string & sig) const +{ + return verifyDetached(fingerprint(store), sig, publicKeys); +} + + +Strings ValidPathInfo::shortRefs() const +{ + Strings refs; + for (auto & r : references) + refs.push_back(std::string(r.to_string())); + return refs; +} + + ValidPathInfo ValidPathInfo::read(Source & source, const Store & store, unsigned int format) { return read(source, store, format, store.parseStorePath(readString(source))); @@ -24,6 +98,7 @@ ValidPathInfo ValidPathInfo::read(Source & source, const Store & store, unsigned return info; } + void ValidPathInfo::write( Sink & sink, const Store & store, diff --git a/src/libstore/path-with-outputs.cc b/src/libstore/path-with-outputs.cc index d6d67ea05..869b490ad 100644 --- a/src/libstore/path-with-outputs.cc +++ b/src/libstore/path-with-outputs.cc @@ -1,6 +1,5 @@ #include "path-with-outputs.hh" #include "store-api.hh" -#include "nlohmann/json.hpp" #include <regex> @@ -16,10 +15,14 @@ std::string StorePathWithOutputs::to_string(const Store & store) const DerivedPath StorePathWithOutputs::toDerivedPath() const { - if (!outputs.empty() || path.isDerivation()) - return DerivedPath::Built { path, outputs }; - else + if (!outputs.empty()) { + return DerivedPath::Built { path, OutputsSpec::Names { outputs } }; + } else if (path.isDerivation()) { + assert(outputs.empty()); + return DerivedPath::Built { path, OutputsSpec::All { } }; + } else { return DerivedPath::Opaque { path }; + } } @@ -42,7 +45,18 @@ std::variant<StorePathWithOutputs, StorePath> StorePathWithOutputs::tryFromDeriv return StorePathWithOutputs { bo.path }; }, [&](const DerivedPath::Built & bfd) -> std::variant<StorePathWithOutputs, StorePath> { - return StorePathWithOutputs { bfd.drvPath, bfd.outputs }; + return StorePathWithOutputs { + .path = bfd.drvPath, + // Use legacy encoding of wildcard as empty set + .outputs = std::visit(overloaded { + [&](const OutputsSpec::All &) -> StringSet { + return {}; + }, + [&](const OutputsSpec::Names & outputs) { + return static_cast<StringSet>(outputs); + }, + }, bfd.outputs.raw()), + }; }, }, p.raw()); } @@ -53,8 +67,8 @@ std::pair<std::string_view, StringSet> parsePathWithOutputs(std::string_view s) size_t n = s.find("!"); return n == s.npos ? std::make_pair(s, std::set<std::string>()) - : std::make_pair(((std::string_view) s).substr(0, n), - tokenizeString<std::set<std::string>>(((std::string_view) s).substr(n + 1), ",")); + : std::make_pair(s.substr(0, n), + tokenizeString<std::set<std::string>>(s.substr(n + 1), ",")); } @@ -71,57 +85,4 @@ StorePathWithOutputs followLinksToStorePathWithOutputs(const Store & store, std: return StorePathWithOutputs { store.followLinksToStorePath(path), std::move(outputs) }; } -std::pair<std::string, OutputsSpec> parseOutputsSpec(const std::string & s) -{ - static std::regex regex(R"((.*)\^((\*)|([a-z]+(,[a-z]+)*)))"); - - std::smatch match; - if (!std::regex_match(s, match, regex)) - return {s, DefaultOutputs()}; - - if (match[3].matched) - return {match[1], AllOutputs()}; - - return {match[1], tokenizeString<OutputNames>(match[4].str(), ",")}; -} - -std::string printOutputsSpec(const OutputsSpec & outputsSpec) -{ - if (std::get_if<DefaultOutputs>(&outputsSpec)) - return ""; - - if (std::get_if<AllOutputs>(&outputsSpec)) - return "^*"; - - if (auto outputNames = std::get_if<OutputNames>(&outputsSpec)) - return "^" + concatStringsSep(",", *outputNames); - - assert(false); -} - -void to_json(nlohmann::json & json, const OutputsSpec & outputsSpec) -{ - if (std::get_if<DefaultOutputs>(&outputsSpec)) - json = nullptr; - - else if (std::get_if<AllOutputs>(&outputsSpec)) - json = std::vector<std::string>({"*"}); - - else if (auto outputNames = std::get_if<OutputNames>(&outputsSpec)) - json = *outputNames; -} - -void from_json(const nlohmann::json & json, OutputsSpec & outputsSpec) -{ - if (json.is_null()) - outputsSpec = DefaultOutputs(); - else { - auto names = json.get<OutputNames>(); - if (names == OutputNames({"*"})) - outputsSpec = AllOutputs(); - else - outputsSpec = names; - } -} - } diff --git a/src/libstore/path-with-outputs.hh b/src/libstore/path-with-outputs.hh index 0cb5eb223..5d25656a5 100644 --- a/src/libstore/path-with-outputs.hh +++ b/src/libstore/path-with-outputs.hh @@ -1,13 +1,17 @@ #pragma once -#include <variant> - #include "path.hh" #include "derived-path.hh" -#include "nlohmann/json_fwd.hpp" namespace nix { +/* This is a deprecated old type just for use by the old CLI, and older + versions of the RPC protocols. In new code don't use it; you want + `DerivedPath` instead. + + `DerivedPath` is better because it handles more cases, and does so more + explicitly without devious punning tricks. +*/ struct StorePathWithOutputs { StorePath path; @@ -33,25 +37,4 @@ StorePathWithOutputs parsePathWithOutputs(const Store & store, std::string_view StorePathWithOutputs followLinksToStorePathWithOutputs(const Store & store, std::string_view pathWithOutputs); -typedef std::set<std::string> OutputNames; - -struct AllOutputs { - bool operator < (const AllOutputs & _) const { return false; } -}; - -struct DefaultOutputs { - bool operator < (const DefaultOutputs & _) const { return false; } -}; - -typedef std::variant<DefaultOutputs, AllOutputs, OutputNames> OutputsSpec; - -/* Parse a string of the form 'prefix^output1,...outputN' or - 'prefix^*', returning the prefix and the outputs spec. */ -std::pair<std::string, OutputsSpec> parseOutputsSpec(const std::string & s); - -std::string printOutputsSpec(const OutputsSpec & outputsSpec); - -void to_json(nlohmann::json &, const OutputsSpec &); -void from_json(const nlohmann::json &, OutputsSpec &); - } diff --git a/src/libstore/path.hh b/src/libstore/path.hh index 77fd0f8dc..0694b4c18 100644 --- a/src/libstore/path.hh +++ b/src/libstore/path.hh @@ -64,7 +64,6 @@ public: typedef std::set<StorePath> StorePathSet; typedef std::vector<StorePath> StorePaths; -typedef std::map<std::string, StorePath> OutputPathMap; typedef std::map<StorePath, std::optional<ContentAddress>> StorePathCAMap; diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index ccf7d7e8b..ff57a77ca 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -867,8 +867,8 @@ std::vector<BuildResult> RemoteStore::buildPathsWithResults( OutputPathMap outputs; auto drv = evalStore->readDerivation(bfd.drvPath); const auto outputHashes = staticOutputHashes(*evalStore, drv); // FIXME: expensive - const auto drvOutputs = drv.outputsAndOptPaths(*this); - for (auto & output : bfd.outputs) { + auto built = resolveDerivedPath(*this, bfd, &*evalStore); + for (auto & [output, outputPath] : built) { auto outputHash = get(outputHashes, output); if (!outputHash) throw Error( @@ -882,16 +882,11 @@ std::vector<BuildResult> RemoteStore::buildPathsWithResults( throw MissingRealisation(outputId); res.builtOutputs.emplace(realisation->id, *realisation); } else { - // If ca-derivations isn't enabled, assume that - // the output path is statically known. - const auto drvOutput = get(drvOutputs, output); - assert(drvOutput); - assert(drvOutput->second); res.builtOutputs.emplace( outputId, Realisation { .id = outputId, - .outPath = *drvOutput->second, + .outPath = outputPath, }); } } @@ -915,7 +910,12 @@ BuildResult RemoteStore::buildDerivation(const StorePath & drvPath, const BasicD writeDerivation(conn->to, *this, drv); conn->to << buildMode; conn.processStderr(); - BuildResult res { .path = DerivedPath::Built { .drvPath = drvPath } }; + BuildResult res { + .path = DerivedPath::Built { + .drvPath = drvPath, + .outputs = OutputsSpec::All { }, + }, + }; res.status = (BuildResult::Status) readInt(conn->from); conn->from >> res.errorMsg; if (GET_PROTOCOL_MINOR(conn->daemonVersion) >= 29) { diff --git a/src/libstore/ssh-store.cc b/src/libstore/ssh-store.cc index 62daa838c..a1d4daafd 100644 --- a/src/libstore/ssh-store.cc +++ b/src/libstore/ssh-store.cc @@ -53,8 +53,8 @@ public: { return false; } // FIXME extend daemon protocol, move implementation to RemoteStore - std::optional<std::string> getBuildLog(const StorePath & path) override - { unsupported("getBuildLog"); } + std::optional<std::string> getBuildLogExact(const StorePath & path) override + { unsupported("getBuildLogExact"); } private: diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 426230ca5..5130409d4 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -1210,79 +1210,6 @@ std::string showPaths(const PathSet & paths) } -std::string ValidPathInfo::fingerprint(const Store & store) const -{ - if (narSize == 0) - throw Error("cannot calculate fingerprint of path '%s' because its size is not known", - store.printStorePath(path)); - return - "1;" + store.printStorePath(path) + ";" - + narHash.to_string(Base32, true) + ";" - + std::to_string(narSize) + ";" - + concatStringsSep(",", store.printStorePathSet(references)); -} - - -void ValidPathInfo::sign(const Store & store, const SecretKey & secretKey) -{ - sigs.insert(secretKey.signDetached(fingerprint(store))); -} - -bool ValidPathInfo::isContentAddressed(const Store & store) const -{ - if (! ca) return false; - - auto caPath = std::visit(overloaded { - [&](const TextHash & th) { - return store.makeTextPath(path.name(), th.hash, references); - }, - [&](const FixedOutputHash & fsh) { - auto refs = references; - bool hasSelfReference = false; - if (refs.count(path)) { - hasSelfReference = true; - refs.erase(path); - } - return store.makeFixedOutputPath(fsh.method, fsh.hash, path.name(), refs, hasSelfReference); - } - }, *ca); - - bool res = caPath == path; - - if (!res) - printError("warning: path '%s' claims to be content-addressed but isn't", store.printStorePath(path)); - - return res; -} - - -size_t ValidPathInfo::checkSignatures(const Store & store, const PublicKeys & publicKeys) const -{ - if (isContentAddressed(store)) return maxSigs; - - size_t good = 0; - for (auto & sig : sigs) - if (checkSignature(store, publicKeys, sig)) - good++; - return good; -} - - -bool ValidPathInfo::checkSignature(const Store & store, const PublicKeys & publicKeys, const std::string & sig) const -{ - return verifyDetached(fingerprint(store), sig, publicKeys); -} - - -Strings ValidPathInfo::shortRefs() const -{ - Strings refs; - for (auto & r : references) - refs.push_back(std::string(r.to_string())); - return refs; -} - - Derivation Store::derivationFromPath(const StorePath & drvPath) { ensurePath(drvPath); @@ -1301,6 +1228,34 @@ Derivation readDerivationCommon(Store& store, const StorePath& drvPath, bool req } } +std::optional<StorePath> Store::getBuildDerivationPath(const StorePath & path) +{ + + if (!path.isDerivation()) { + try { + auto info = queryPathInfo(path); + if (!info->deriver) return std::nullopt; + return *info->deriver; + } catch (InvalidPath &) { + return std::nullopt; + } + } + + if (!settings.isExperimentalFeatureEnabled(Xp::CaDerivations) || !isValidPath(path)) + return path; + + auto drv = readDerivation(path); + if (!drv.type().hasKnownOutputPaths()) { + // The build log is actually attached to the corresponding + // resolved derivation, so we need to get it first + auto resolvedDrv = drv.tryResolve(*this); + if (resolvedDrv) + return writeDerivation(*this, *resolvedDrv, NoRepair, true); + } + + return path; +} + Derivation Store::readDerivation(const StorePath & drvPath) { return readDerivationCommon(*this, drvPath, true); } diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 4a88d7216..9eab4b4e5 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -71,6 +71,9 @@ class NarInfoDiskCache; class Store; +typedef std::map<std::string, StorePath> OutputPathMap; + + enum CheckSigsFlag : bool { NoCheckSigs = false, CheckSigs = true }; enum SubstituteFlag : bool { NoSubstitute = false, Substitute = true }; enum AllowInvalidFlag : bool { DisallowInvalid = false, AllowInvalid = true }; @@ -120,6 +123,8 @@ public: typedef std::map<std::string, std::string> Params; + + protected: struct PathInfoCacheValue { @@ -618,6 +623,13 @@ public: */ StorePathSet exportReferences(const StorePathSet & storePaths, const StorePathSet & inputPaths); + /** + * Given a store path, return the realisation actually used in the realisation of this path: + * - If the path is a content-addressed derivation, try to resolve it + * - Otherwise, find one of its derivers + */ + std::optional<StorePath> getBuildDerivationPath(const StorePath &); + /* Hack to allow long-running processes like hydra-queue-runner to occasionally flush their path info cache. */ void clearPathInfoCache() @@ -719,6 +731,11 @@ void copyClosure( void removeTempRoots(); +/* Resolve the derived path completely, failing if any derivation output + is unknown. */ +OutputPathMap resolveDerivedPath(Store &, const DerivedPath::Built &, Store * evalStore = nullptr); + + /* Return a Store object to access the Nix store denoted by ‘uri’ (slight misnomer...). Supported values are: diff --git a/src/libstore/tests/outputs-spec.cc b/src/libstore/tests/outputs-spec.cc new file mode 100644 index 000000000..836ba7e82 --- /dev/null +++ b/src/libstore/tests/outputs-spec.cc @@ -0,0 +1,194 @@ +#include "outputs-spec.hh" + +#include <nlohmann/json.hpp> +#include <gtest/gtest.h> + +namespace nix { + +#ifndef NDEBUG +TEST(OutputsSpec, no_empty_names) { + ASSERT_DEATH(OutputsSpec::Names { std::set<std::string> { } }, ""); +} +#endif + +#define TEST_DONT_PARSE(NAME, STR) \ + TEST(OutputsSpec, bad_ ## NAME) { \ + std::optional OutputsSpecOpt = \ + OutputsSpec::parseOpt(STR); \ + ASSERT_FALSE(OutputsSpecOpt); \ + } + +TEST_DONT_PARSE(empty, "") +TEST_DONT_PARSE(garbage, "&*()") +TEST_DONT_PARSE(double_star, "**") +TEST_DONT_PARSE(star_first, "*,foo") +TEST_DONT_PARSE(star_second, "foo,*") + +#undef TEST_DONT_PARSE + +TEST(OutputsSpec, all) { + std::string_view str = "*"; + OutputsSpec expected = OutputsSpec::All { }; + ASSERT_EQ(OutputsSpec::parse(str), expected); + ASSERT_EQ(expected.to_string(), str); +} + +TEST(OutputsSpec, names_out) { + std::string_view str = "out"; + OutputsSpec expected = OutputsSpec::Names { "out" }; + ASSERT_EQ(OutputsSpec::parse(str), expected); + ASSERT_EQ(expected.to_string(), str); +} + +TEST(OutputsSpec, names_underscore) { + std::string_view str = "a_b"; + OutputsSpec expected = OutputsSpec::Names { "a_b" }; + ASSERT_EQ(OutputsSpec::parse(str), expected); + ASSERT_EQ(expected.to_string(), str); +} + +TEST(OutputsSpec, names_out_bin) { + OutputsSpec expected = OutputsSpec::Names { "out", "bin" }; + ASSERT_EQ(OutputsSpec::parse("out,bin"), expected); + // N.B. This normalization is OK. + ASSERT_EQ(expected.to_string(), "bin,out"); +} + +#define TEST_SUBSET(X, THIS, THAT) \ + X((OutputsSpec { THIS }).isSubsetOf(THAT)); + +TEST(OutputsSpec, subsets_all_all) { + TEST_SUBSET(ASSERT_TRUE, OutputsSpec::All { }, OutputsSpec::All { }); +} + +TEST(OutputsSpec, subsets_names_all) { + TEST_SUBSET(ASSERT_TRUE, OutputsSpec::Names { "a" }, OutputsSpec::All { }); +} + +TEST(OutputsSpec, subsets_names_names_eq) { + TEST_SUBSET(ASSERT_TRUE, OutputsSpec::Names { "a" }, OutputsSpec::Names { "a" }); +} + +TEST(OutputsSpec, subsets_names_names_noneq) { + TEST_SUBSET(ASSERT_TRUE, OutputsSpec::Names { "a" }, (OutputsSpec::Names { "a", "b" })); +} + +TEST(OutputsSpec, not_subsets_all_names) { + TEST_SUBSET(ASSERT_FALSE, OutputsSpec::All { }, OutputsSpec::Names { "a" }); +} + +TEST(OutputsSpec, not_subsets_names_names) { + TEST_SUBSET(ASSERT_FALSE, (OutputsSpec::Names { "a", "b" }), (OutputsSpec::Names { "a" })); +} + +#undef TEST_SUBSET + +#define TEST_UNION(RES, THIS, THAT) \ + ASSERT_EQ(OutputsSpec { RES }, (OutputsSpec { THIS }).union_(THAT)); + +TEST(OutputsSpec, union_all_all) { + TEST_UNION(OutputsSpec::All { }, OutputsSpec::All { }, OutputsSpec::All { }); +} + +TEST(OutputsSpec, union_all_names) { + TEST_UNION(OutputsSpec::All { }, OutputsSpec::All { }, OutputsSpec::Names { "a" }); +} + +TEST(OutputsSpec, union_names_all) { + TEST_UNION(OutputsSpec::All { }, OutputsSpec::Names { "a" }, OutputsSpec::All { }); +} + +TEST(OutputsSpec, union_names_names) { + TEST_UNION((OutputsSpec::Names { "a", "b" }), OutputsSpec::Names { "a" }, OutputsSpec::Names { "b" }); +} + +#undef TEST_UNION + +#define TEST_DONT_PARSE(NAME, STR) \ + TEST(ExtendedOutputsSpec, bad_ ## NAME) { \ + std::optional extendedOutputsSpecOpt = \ + ExtendedOutputsSpec::parseOpt(STR); \ + ASSERT_FALSE(extendedOutputsSpecOpt); \ + } + +TEST_DONT_PARSE(carot_empty, "^") +TEST_DONT_PARSE(prefix_carot_empty, "foo^") +TEST_DONT_PARSE(garbage, "^&*()") +TEST_DONT_PARSE(double_star, "^**") +TEST_DONT_PARSE(star_first, "^*,foo") +TEST_DONT_PARSE(star_second, "^foo,*") + +#undef TEST_DONT_PARSE + +TEST(ExtendedOutputsSpec, defeault) { + std::string_view str = "foo"; + auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(str); + ASSERT_EQ(prefix, "foo"); + ExtendedOutputsSpec expected = ExtendedOutputsSpec::Default { }; + ASSERT_EQ(extendedOutputsSpec, expected); + ASSERT_EQ(std::string { prefix } + expected.to_string(), str); +} + +TEST(ExtendedOutputsSpec, all) { + std::string_view str = "foo^*"; + auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(str); + ASSERT_EQ(prefix, "foo"); + ExtendedOutputsSpec expected = OutputsSpec::All { }; + ASSERT_EQ(extendedOutputsSpec, expected); + ASSERT_EQ(std::string { prefix } + expected.to_string(), str); +} + +TEST(ExtendedOutputsSpec, out) { + std::string_view str = "foo^out"; + auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(str); + ASSERT_EQ(prefix, "foo"); + ExtendedOutputsSpec expected = OutputsSpec::Names { "out" }; + ASSERT_EQ(extendedOutputsSpec, expected); + ASSERT_EQ(std::string { prefix } + expected.to_string(), str); +} + +TEST(ExtendedOutputsSpec, out_bin) { + auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^out,bin"); + ASSERT_EQ(prefix, "foo"); + ExtendedOutputsSpec expected = OutputsSpec::Names { "out", "bin" }; + ASSERT_EQ(extendedOutputsSpec, expected); + ASSERT_EQ(std::string { prefix } + expected.to_string(), "foo^bin,out"); +} + +TEST(ExtendedOutputsSpec, many_carrot) { + auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^bar^out,bin"); + ASSERT_EQ(prefix, "foo^bar"); + ExtendedOutputsSpec expected = OutputsSpec::Names { "out", "bin" }; + ASSERT_EQ(extendedOutputsSpec, expected); + ASSERT_EQ(std::string { prefix } + expected.to_string(), "foo^bar^bin,out"); +} + + +#define TEST_JSON(TYPE, NAME, STR, VAL) \ + \ + TEST(TYPE, NAME ## _to_json) { \ + using nlohmann::literals::operator "" _json; \ + ASSERT_EQ( \ + STR ## _json, \ + ((nlohmann::json) TYPE { VAL })); \ + } \ + \ + TEST(TYPE, NAME ## _from_json) { \ + using nlohmann::literals::operator "" _json; \ + ASSERT_EQ( \ + TYPE { VAL }, \ + (STR ## _json).get<TYPE>()); \ + } + +TEST_JSON(OutputsSpec, all, R"(["*"])", OutputsSpec::All { }) +TEST_JSON(OutputsSpec, name, R"(["a"])", OutputsSpec::Names { "a" }) +TEST_JSON(OutputsSpec, names, R"(["a","b"])", (OutputsSpec::Names { "a", "b" })) + +TEST_JSON(ExtendedOutputsSpec, def, R"(null)", ExtendedOutputsSpec::Default { }) +TEST_JSON(ExtendedOutputsSpec, all, R"(["*"])", ExtendedOutputsSpec::Explicit { OutputsSpec::All { } }) +TEST_JSON(ExtendedOutputsSpec, name, R"(["a"])", ExtendedOutputsSpec::Explicit { OutputsSpec::Names { "a" } }) +TEST_JSON(ExtendedOutputsSpec, names, R"(["a","b"])", (ExtendedOutputsSpec::Explicit { OutputsSpec::Names { "a", "b" } })) + +#undef TEST_JSON + +} diff --git a/src/libstore/tests/path-with-outputs.cc b/src/libstore/tests/path-with-outputs.cc deleted file mode 100644 index 350ea7ffd..000000000 --- a/src/libstore/tests/path-with-outputs.cc +++ /dev/null @@ -1,46 +0,0 @@ -#include "path-with-outputs.hh" - -#include <gtest/gtest.h> - -namespace nix { - -TEST(parseOutputsSpec, basic) -{ - { - auto [prefix, outputsSpec] = parseOutputsSpec("foo"); - ASSERT_EQ(prefix, "foo"); - ASSERT_TRUE(std::get_if<DefaultOutputs>(&outputsSpec)); - } - - { - auto [prefix, outputsSpec] = parseOutputsSpec("foo^*"); - ASSERT_EQ(prefix, "foo"); - ASSERT_TRUE(std::get_if<AllOutputs>(&outputsSpec)); - } - - { - auto [prefix, outputsSpec] = parseOutputsSpec("foo^out"); - ASSERT_EQ(prefix, "foo"); - ASSERT_TRUE(std::get<OutputNames>(outputsSpec) == OutputNames({"out"})); - } - - { - auto [prefix, outputsSpec] = parseOutputsSpec("foo^out,bin"); - ASSERT_EQ(prefix, "foo"); - ASSERT_TRUE(std::get<OutputNames>(outputsSpec) == OutputNames({"out", "bin"})); - } - - { - auto [prefix, outputsSpec] = parseOutputsSpec("foo^bar^out,bin"); - ASSERT_EQ(prefix, "foo^bar"); - ASSERT_TRUE(std::get<OutputNames>(outputsSpec) == OutputNames({"out", "bin"})); - } - - { - auto [prefix, outputsSpec] = parseOutputsSpec("foo^&*()"); - ASSERT_EQ(prefix, "foo^&*()"); - ASSERT_TRUE(std::get_if<DefaultOutputs>(&outputsSpec)); - } -} - -} diff --git a/src/libutil/error.cc b/src/libutil/error.cc index e4f0d4677..1a1aecea5 100644 --- a/src/libutil/error.cc +++ b/src/libutil/error.cc @@ -9,9 +9,9 @@ namespace nix { const std::string nativeSystem = SYSTEM; -void BaseError::addTrace(std::shared_ptr<AbstractPos> && e, hintformat hint, bool frame) +void BaseError::addTrace(std::shared_ptr<AbstractPos> && e, hintformat hint) { - err.traces.push_front(Trace { .pos = std::move(e), .hint = hint, .frame = frame }); + err.traces.push_front(Trace { .pos = std::move(e), .hint = hint }); } // c++ std::exception descendants must have a 'const char* what()' function. @@ -200,125 +200,13 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s auto noSource = ANSI_ITALIC " (source not available)" ANSI_NORMAL "\n"; - /* - * Traces - * ------ - * - * The semantics of traces is a bit weird. We have only one option to - * print them and to make them verbose (--show-trace). In the code they - * are always collected, but they are not printed by default. The code - * also collects more traces when the option is on. This means that there - * is no way to print the simplified traces at all. - * - * I (layus) designed the code to attach positions to a restricted set of - * messages. This means that we have a lot of traces with no position at - * all, including most of the base error messages. For example "type - * error: found a string while a set was expected" has no position, but - * will come with several traces detailing it's precise relation to the - * closest know position. This makes erroring without printing traces - * quite useless. - * - * This is why I introduced the idea to always print a few traces on - * error. The number 3 is quite arbitrary, and was selected so as not to - * clutter the console on error. For the same reason, a trace with an - * error position takes more space, and counts as two traces towards the - * limit. - * - * The rest is truncated, unless --show-trace is passed. This preserves - * the same bad semantics of --show-trace to both show the trace and - * augment it with new data. Not too sure what is the best course of - * action. - * - * The issue is that it is fundamentally hard to provide a trace for a - * lazy language. The trace will only cover the current spine of the - * evaluation, missing things that have been evaluated before. For - * example, most type errors are hard to inspect because there is not - * trace for the faulty value. These errors should really print the faulty - * value itself. - * - * In function calls, the --show-trace flag triggers extra traces for each - * function invocation. These work as scopes, allowing to follow the - * current spine of the evaluation graph. Without that flag, the error - * trace should restrict itself to a restricted prefix of that trace, - * until the first scope. If we ever get to such a precise error - * reporting, there would be no need to add an arbitrary limit here. We - * could always print the full trace, and it would just be small without - * the flag. - * - * One idea I had is for XxxError.addTrace() to perform nothing if one - * scope has already been traced. Alternatively, we could stop here when - * we encounter such a scope instead of after an arbitrary number of - * traces. This however requires to augment traces with the notion of - * "scope". - * - * This is particularly visible in code like evalAttrs(...) where we have - * to make a decision between the two following options. - * - * ``` long traces - * inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v, const Pos & pos, std::string_view errorCtx) - * { - * try { - * e->eval(*this, env, v); - * if (v.type() != nAttrs) - * throwTypeError("value is %1% while a set was expected", v); - * } catch (Error & e) { - * e.addTrace(pos, errorCtx); - * throw; - * } - * } - * ``` - * - * ``` short traces - * inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v, const Pos & pos, std::string_view errorCtx) - * { - * e->eval(*this, env, v); - * try { - * if (v.type() != nAttrs) - * throwTypeError("value is %1% while a set was expected", v); - * } catch (Error & e) { - * e.addTrace(pos, errorCtx); - * throw; - * } - * } - * ``` - * - * The second example can be rewritten more concisely, but kept in this - * form to highlight the symmetry. The first option adds more information, - * because whatever caused an error down the line, in the generic eval - * function, will get annotated with the code location that uses and - * required it. The second option is less verbose, but does not provide - * any context at all as to where and why a failing value was required. - * - * Scopes would fix that, by adding context only when --show-trace is - * passed, and keeping the trace terse otherwise. - * - */ - - // Enough indent to align with with the `... ` - // prepended to each element of the trace - auto ellipsisIndent = " "; - - bool frameOnly = false; - if (!einfo.traces.empty()) { - size_t count = 0; + // traces + if (showTrace && !einfo.traces.empty()) { for (const auto & trace : einfo.traces) { - if (!showTrace && count > 3) { - oss << "\n" << ANSI_WARNING "(stack trace truncated; use '--show-trace' to show the full trace)" ANSI_NORMAL << "\n"; - break; - } - - if (trace.hint.str().empty()) continue; - if (frameOnly && !trace.frame) continue; - - count++; - frameOnly = trace.frame; - oss << "\n" << "… " << trace.hint.str() << "\n"; if (trace.pos) { - count++; - - oss << "\n" << ellipsisIndent << ANSI_BLUE << "at " ANSI_WARNING << *trace.pos << ANSI_NORMAL << ":"; + oss << "\n" << ANSI_BLUE << "at " ANSI_WARNING << *trace.pos << ANSI_NORMAL << ":"; if (auto loc = trace.pos->getCodeLines()) { oss << "\n"; diff --git a/src/libutil/error.hh b/src/libutil/error.hh index 7d236028c..c3bb8c0df 100644 --- a/src/libutil/error.hh +++ b/src/libutil/error.hh @@ -86,7 +86,6 @@ void printCodeLines(std::ostream & out, struct Trace { std::shared_ptr<AbstractPos> pos; hintformat hint; - bool frame; }; struct ErrorInfo { @@ -115,8 +114,6 @@ protected: public: unsigned int status = 1; // exit status - BaseError(const BaseError &) = default; - template<typename... Args> BaseError(unsigned int status, const Args & ... args) : err { .level = lvlError, .msg = hintfmt(args...) } @@ -155,22 +152,15 @@ public: const std::string & msg() const { return calcWhat(); } const ErrorInfo & info() const { calcWhat(); return err; } - void pushTrace(Trace trace) - { - err.traces.push_front(trace); - } - template<typename... Args> - void addTrace(std::shared_ptr<AbstractPos> && e, std::string_view fs, const Args & ... args) + void addTrace(std::shared_ptr<AbstractPos> && e, const std::string & fs, const Args & ... args) { - addTrace(std::move(e), hintfmt(std::string(fs), args...)); + addTrace(std::move(e), hintfmt(fs, args...)); } - void addTrace(std::shared_ptr<AbstractPos> && e, hintformat hint, bool frame = false); + void addTrace(std::shared_ptr<AbstractPos> && e, hintformat hint); bool hasTrace() const { return !err.traces.empty(); } - - const ErrorInfo & info() { return err; }; }; #define MakeError(newClass, superClass) \ diff --git a/src/libutil/json-impls.hh b/src/libutil/json-impls.hh new file mode 100644 index 000000000..bd75748ad --- /dev/null +++ b/src/libutil/json-impls.hh @@ -0,0 +1,14 @@ +#pragma once + +#include "nlohmann/json_fwd.hpp" + +// Following https://github.com/nlohmann/json#how-can-i-use-get-for-non-default-constructiblenon-copyable-types +#define JSON_IMPL(TYPE) \ + namespace nlohmann { \ + using namespace nix; \ + template <> \ + struct adl_serializer<TYPE> { \ + static TYPE from_json(const json & json); \ + static void to_json(json & json, TYPE t); \ + }; \ + } diff --git a/src/libutil/monitor-fd.hh b/src/libutil/monitor-fd.hh index 5ee0b88ef..9518cf8aa 100644 --- a/src/libutil/monitor-fd.hh +++ b/src/libutil/monitor-fd.hh @@ -22,27 +22,38 @@ public: { thread = std::thread([fd]() { while (true) { - /* Wait indefinitely until a POLLHUP occurs. */ - struct pollfd fds[1]; - fds[0].fd = fd; - /* This shouldn't be necessary, but macOS doesn't seem to - like a zeroed out events field. - See rdar://37537852. - */ - fds[0].events = POLLHUP; - auto count = poll(fds, 1, -1); - if (count == -1) abort(); // can't happen - /* This shouldn't happen, but can on macOS due to a bug. - See rdar://37550628. - - This may eventually need a delay or further - coordination with the main thread if spinning proves - too harmful. - */ - if (count == 0) continue; - assert(fds[0].revents & POLLHUP); - triggerInterrupt(); - break; + /* Wait indefinitely until a POLLHUP occurs. */ + struct pollfd fds[1]; + fds[0].fd = fd; + /* Polling for no specific events (i.e. just waiting + for an error/hangup) doesn't work on macOS + anymore. So wait for read events and ignore + them. */ + fds[0].events = + #ifdef __APPLE__ + POLLRDNORM + #else + 0 + #endif + ; + auto count = poll(fds, 1, -1); + if (count == -1) abort(); // can't happen + /* This shouldn't happen, but can on macOS due to a bug. + See rdar://37550628. + + This may eventually need a delay or further + coordination with the main thread if spinning proves + too harmful. + */ + if (count == 0) continue; + if (fds[0].revents & POLLHUP) { + triggerInterrupt(); + break; + } + /* This will only happen on macOS. We sleep a bit to + avoid waking up too often if the client is sending + input. */ + sleep(1); } }); }; diff --git a/src/libutil/tests/tests.cc b/src/libutil/tests/tests.cc index 6e325db98..250e83a38 100644 --- a/src/libutil/tests/tests.cc +++ b/src/libutil/tests/tests.cc @@ -312,6 +312,42 @@ namespace nix { } /* ---------------------------------------------------------------------------- + * getLine + * --------------------------------------------------------------------------*/ + + TEST(getLine, all) { + { + auto [line, rest] = getLine("foo\nbar\nxyzzy"); + ASSERT_EQ(line, "foo"); + ASSERT_EQ(rest, "bar\nxyzzy"); + } + + { + auto [line, rest] = getLine("foo\r\nbar\r\nxyzzy"); + ASSERT_EQ(line, "foo"); + ASSERT_EQ(rest, "bar\r\nxyzzy"); + } + + { + auto [line, rest] = getLine("foo\n"); + ASSERT_EQ(line, "foo"); + ASSERT_EQ(rest, ""); + } + + { + auto [line, rest] = getLine("foo"); + ASSERT_EQ(line, "foo"); + ASSERT_EQ(rest, ""); + } + + { + auto [line, rest] = getLine(""); + ASSERT_EQ(line, ""); + ASSERT_EQ(rest, ""); + } + } + + /* ---------------------------------------------------------------------------- * toLower * --------------------------------------------------------------------------*/ diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index adcaab686..049838bb1 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -397,7 +397,7 @@ static void main_nix_build(int argc, char * * argv) auto bashDrv = drv->requireDrvPath(); pathsToBuild.push_back(DerivedPath::Built { .drvPath = bashDrv, - .outputs = {"out"}, + .outputs = OutputsSpec::Names {"out"}, }); pathsToCopy.insert(bashDrv); shellDrv = bashDrv; @@ -421,7 +421,7 @@ static void main_nix_build(int argc, char * * argv) { pathsToBuild.push_back(DerivedPath::Built { .drvPath = inputDrv, - .outputs = inputOutputs + .outputs = OutputsSpec::Names { inputOutputs }, }); pathsToCopy.insert(inputDrv); } @@ -591,7 +591,7 @@ static void main_nix_build(int argc, char * * argv) if (outputName == "") throw Error("derivation '%s' lacks an 'outputName' attribute", store->printStorePath(drvPath)); - pathsToBuild.push_back(DerivedPath::Built{drvPath, {outputName}}); + pathsToBuild.push_back(DerivedPath::Built{drvPath, OutputsSpec::Names{outputName}}); pathsToBuildOrdered.push_back({drvPath, {outputName}}); drvsToCopy.insert(drvPath); diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index 31823a966..406e548c0 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -478,9 +478,14 @@ static void printMissing(EvalState & state, DrvInfos & elems) std::vector<DerivedPath> targets; for (auto & i : elems) if (auto drvPath = i.queryDrvPath()) - targets.push_back(DerivedPath::Built{*drvPath}); + targets.push_back(DerivedPath::Built{ + .drvPath = *drvPath, + .outputs = OutputsSpec::All { }, + }); else - targets.push_back(DerivedPath::Opaque{i.queryOutPath()}); + targets.push_back(DerivedPath::Opaque{ + .path = i.queryOutPath(), + }); printMissing(state.store, targets); } @@ -751,8 +756,13 @@ static void opSet(Globals & globals, Strings opFlags, Strings opArgs) auto drvPath = drv.queryDrvPath(); std::vector<DerivedPath> paths { drvPath - ? (DerivedPath) (DerivedPath::Built { *drvPath }) - : (DerivedPath) (DerivedPath::Opaque { drv.queryOutPath() }), + ? (DerivedPath) (DerivedPath::Built { + .drvPath = *drvPath, + .outputs = OutputsSpec::All { }, + }) + : (DerivedPath) (DerivedPath::Opaque { + .path = drv.queryOutPath(), + }), }; printMissing(globals.state->store, paths); if (globals.dryRun) return; diff --git a/src/nix-env/user-env.cc b/src/nix-env/user-env.cc index cad7f9c88..4b1202be3 100644 --- a/src/nix-env/user-env.cc +++ b/src/nix-env/user-env.cc @@ -134,9 +134,9 @@ bool createUserEnv(EvalState & state, DrvInfos & elems, state.forceValue(topLevel, [&]() { return topLevel.determinePos(noPos); }); PathSet context; Attr & aDrvPath(*topLevel.attrs->find(state.sDrvPath)); - auto topLevelDrv = state.coerceToStorePath(aDrvPath.pos, *aDrvPath.value, context, ""); + auto topLevelDrv = state.coerceToStorePath(aDrvPath.pos, *aDrvPath.value, context); Attr & aOutPath(*topLevel.attrs->find(state.sOutPath)); - auto topLevelOut = state.coerceToStorePath(aOutPath.pos, *aOutPath.value, context, ""); + auto topLevelOut = state.coerceToStorePath(aOutPath.pos, *aOutPath.value, context); /* Realise the resulting store expression. */ debug("building user environment"); diff --git a/src/nix/app.cc b/src/nix/app.cc index a8d7e115b..08cd0ccd4 100644 --- a/src/nix/app.cc +++ b/src/nix/app.cc @@ -79,9 +79,29 @@ UnresolvedApp Installable::toApp(EvalState & state) if (type == "app") { auto [program, context] = cursor->getAttr("program")->getStringWithContext(); - std::vector<StorePathWithOutputs> context2; - for (auto & [path, name] : context) - context2.push_back({path, {name}}); + std::vector<DerivedPath> context2; + for (auto & c : context) { + context2.emplace_back(std::visit(overloaded { + [&](const NixStringContextElem::DrvDeep & d) -> DerivedPath { + /* We want all outputs of the drv */ + return DerivedPath::Built { + .drvPath = d.drvPath, + .outputs = OutputsSpec::All {}, + }; + }, + [&](const NixStringContextElem::Built & b) -> DerivedPath { + return DerivedPath::Built { + .drvPath = b.drvPath, + .outputs = OutputsSpec::Names { b.output }, + }; + }, + [&](const NixStringContextElem::Opaque & o) -> DerivedPath { + return DerivedPath::Opaque { + .path = o.path, + }; + }, + }, c.raw())); + } return UnresolvedApp{App { .context = std::move(context2), @@ -105,7 +125,10 @@ UnresolvedApp Installable::toApp(EvalState & state) : DrvName(name).name; auto program = outPath + "/bin/" + mainProgram; return UnresolvedApp { App { - .context = { { drvPath, {outputName} } }, + .context = { DerivedPath::Built { + .drvPath = drvPath, + .outputs = OutputsSpec::Names { outputName }, + } }, .program = program, }}; } @@ -123,7 +146,7 @@ App UnresolvedApp::resolve(ref<Store> evalStore, ref<Store> store) for (auto & ctxElt : unresolved.context) installableContext.push_back( - std::make_shared<InstallableDerivedPath>(store, ctxElt.toDerivedPath())); + std::make_shared<InstallableDerivedPath>(store, ctxElt)); auto builtContext = Installable::build(evalStore, store, Realise::Outputs, installableContext); res.program = resolveString(*store, unresolved.program, builtContext); diff --git a/src/nix/bundle.cc b/src/nix/bundle.cc index 74a7973b0..26db08d80 100644 --- a/src/nix/bundle.cc +++ b/src/nix/bundle.cc @@ -75,10 +75,10 @@ struct CmdBundle : InstallableCommand auto val = installable->toValue(*evalState).first; - auto [bundlerFlakeRef, bundlerName, outputsSpec] = parseFlakeRefWithFragmentAndOutputsSpec(bundler, absPath(".")); + auto [bundlerFlakeRef, bundlerName, extendedOutputsSpec] = parseFlakeRefWithFragmentAndExtendedOutputsSpec(bundler, absPath(".")); const flake::LockFlags lockFlags{ .writeLockFile = false }; InstallableFlake bundler{this, - evalState, std::move(bundlerFlakeRef), bundlerName, outputsSpec, + evalState, std::move(bundlerFlakeRef), bundlerName, extendedOutputsSpec, {"bundlers." + settings.thisSystem.get() + ".default", "defaultBundler." + settings.thisSystem.get() }, @@ -97,15 +97,20 @@ struct CmdBundle : InstallableCommand throw Error("the bundler '%s' does not produce a derivation", bundler.what()); PathSet context2; - auto drvPath = evalState->coerceToStorePath(attr1->pos, *attr1->value, context2, ""); + auto drvPath = evalState->coerceToStorePath(attr1->pos, *attr1->value, context2); auto attr2 = vRes->attrs->get(evalState->sOutPath); if (!attr2) throw Error("the bundler '%s' does not produce a derivation", bundler.what()); - auto outPath = evalState->coerceToStorePath(attr2->pos, *attr2->value, context2, ""); + auto outPath = evalState->coerceToStorePath(attr2->pos, *attr2->value, context2); - store->buildPaths({ DerivedPath::Built { drvPath } }); + store->buildPaths({ + DerivedPath::Built { + .drvPath = drvPath, + .outputs = OutputsSpec::All { }, + }, + }); auto outPathS = store->printStorePath(outPath); @@ -113,7 +118,7 @@ struct CmdBundle : InstallableCommand auto * attr = vRes->attrs->get(evalState->sName); if (!attr) throw Error("attribute 'name' missing"); - outLink = evalState->forceStringNoCtx(*attr->value, attr->pos, ""); + outLink = evalState->forceStringNoCtx(*attr->value, attr->pos); } // TODO: will crash if not a localFSStore? diff --git a/src/nix/develop.cc b/src/nix/develop.cc index 1d90d1dac..16bbd8613 100644 --- a/src/nix/develop.cc +++ b/src/nix/develop.cc @@ -3,7 +3,7 @@ #include "common-args.hh" #include "shared.hh" #include "store-api.hh" -#include "path-with-outputs.hh" +#include "outputs-spec.hh" #include "derivations.hh" #include "progress-bar.hh" #include "run.hh" @@ -232,7 +232,12 @@ static StorePath getDerivationEnvironment(ref<Store> store, ref<Store> evalStore auto shellDrvPath = writeDerivation(*evalStore, drv); /* Build the derivation. */ - store->buildPaths({DerivedPath::Built{shellDrvPath}}, bmNormal, evalStore); + store->buildPaths( + { DerivedPath::Built { + .drvPath = shellDrvPath, + .outputs = OutputsSpec::All { }, + }}, + bmNormal, evalStore); for (auto & [_0, optPath] : evalStore->queryPartialDerivationOutputMap(shellDrvPath)) { assert(optPath); diff --git a/src/nix/eval.cc b/src/nix/eval.cc index ccee074e9..ba82b5772 100644 --- a/src/nix/eval.cc +++ b/src/nix/eval.cc @@ -112,7 +112,7 @@ struct CmdEval : MixJSON, InstallableCommand else if (raw) { stopProgressBar(); - std::cout << *state->coerceToString(noPos, *v, context, "while generating the eval command output"); + std::cout << *state->coerceToString(noPos, *v, context); } else if (json) { diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 9b4cdf35a..17ebf12eb 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -7,7 +7,7 @@ #include "get-drvs.hh" #include "store-api.hh" #include "derivations.hh" -#include "path-with-outputs.hh" +#include "outputs-spec.hh" #include "attr-path.hh" #include "fetchers.hh" #include "registry.hh" @@ -126,12 +126,12 @@ static void enumerateOutputs(EvalState & state, Value & vFlake, std::function<void(const std::string & name, Value & vProvide, const PosIdx pos)> callback) { auto pos = vFlake.determinePos(noPos); - state.forceAttrs(vFlake, pos, "while evaluating a flake to get its outputs"); + state.forceAttrs(vFlake, pos); auto aOutputs = vFlake.attrs->get(state.symbols.create("outputs")); assert(aOutputs); - state.forceAttrs(*aOutputs->value, pos, "while evaluating the outputs of a flake"); + state.forceAttrs(*aOutputs->value, pos); auto sHydraJobs = state.symbols.create("hydraJobs"); @@ -348,7 +348,7 @@ struct CmdFlakeCheck : FlakeCommand // FIXME auto app = App(*state, v); for (auto & i : app.context) { - auto [drvPathS, outputName] = decodeContext(i); + auto [drvPathS, outputName] = NixStringContextElem::parse(i); store->parseStorePath(drvPathS); } #endif @@ -391,13 +391,13 @@ struct CmdFlakeCheck : FlakeCommand checkHydraJobs = [&](const std::string & attrPath, Value & v, const PosIdx pos) { try { - state->forceAttrs(v, pos, ""); + state->forceAttrs(v, pos); if (state->isDerivation(v)) throw Error("jobset should not be a derivation at top-level"); for (auto & attr : *v.attrs) { - state->forceAttrs(*attr.value, attr.pos, ""); + state->forceAttrs(*attr.value, attr.pos); auto attrPath2 = concatStrings(attrPath, ".", state->symbols[attr.name]); if (state->isDerivation(*attr.value)) { Activity act(*logger, lvlChatty, actUnknown, @@ -419,7 +419,7 @@ struct CmdFlakeCheck : FlakeCommand fmt("checking NixOS configuration '%s'", attrPath)); Bindings & bindings(*state->allocBindings(0)); auto vToplevel = findAlongAttrPath(*state, "config.system.build.toplevel", bindings, v).first; - state->forceValue(*vToplevel, pos); + state->forceAttrs(*vToplevel, pos); if (!state->isDerivation(*vToplevel)) throw Error("attribute 'config.system.build.toplevel' is not a derivation"); } catch (Error & e) { @@ -433,12 +433,12 @@ struct CmdFlakeCheck : FlakeCommand Activity act(*logger, lvlChatty, actUnknown, fmt("checking template '%s'", attrPath)); - state->forceAttrs(v, pos, ""); + state->forceAttrs(v, pos); if (auto attr = v.attrs->get(state->symbols.create("path"))) { if (attr->name == state->symbols.create("path")) { PathSet context; - auto path = state->coerceToPath(attr->pos, *attr->value, context, ""); + auto path = state->coerceToPath(attr->pos, *attr->value, context); if (!store->isInStore(path)) throw Error("template '%s' has a bad 'path' attribute"); // TODO: recursively check the flake in 'path'. @@ -447,7 +447,7 @@ struct CmdFlakeCheck : FlakeCommand throw Error("template '%s' lacks attribute 'path'", attrPath); if (auto attr = v.attrs->get(state->symbols.create("description"))) - state->forceStringNoCtx(*attr->value, attr->pos, ""); + state->forceStringNoCtx(*attr->value, attr->pos); else throw Error("template '%s' lacks attribute 'description'", attrPath); @@ -504,23 +504,27 @@ struct CmdFlakeCheck : FlakeCommand warn("flake output attribute '%s' is deprecated; use '%s' instead", name, replacement); if (name == "checks") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) { const auto & attr_name = state->symbols[attr.name]; checkSystemName(attr_name, attr.pos); - state->forceAttrs(*attr.value, attr.pos, ""); + state->forceAttrs(*attr.value, attr.pos); for (auto & attr2 : *attr.value->attrs) { auto drvPath = checkDerivation( fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]), *attr2.value, attr2.pos); - if (drvPath && attr_name == settings.thisSystem.get()) - drvPaths.push_back(DerivedPath::Built{*drvPath}); + if (drvPath && attr_name == settings.thisSystem.get()) { + drvPaths.push_back(DerivedPath::Built { + .drvPath = *drvPath, + .outputs = OutputsSpec::All { }, + }); + } } } } else if (name == "formatter") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) { const auto & attr_name = state->symbols[attr.name]; checkSystemName(attr_name, attr.pos); @@ -531,11 +535,11 @@ struct CmdFlakeCheck : FlakeCommand } else if (name == "packages" || name == "devShells") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) { const auto & attr_name = state->symbols[attr.name]; checkSystemName(attr_name, attr.pos); - state->forceAttrs(*attr.value, attr.pos, ""); + state->forceAttrs(*attr.value, attr.pos); for (auto & attr2 : *attr.value->attrs) checkDerivation( fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]), @@ -544,11 +548,11 @@ struct CmdFlakeCheck : FlakeCommand } else if (name == "apps") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) { const auto & attr_name = state->symbols[attr.name]; checkSystemName(attr_name, attr.pos); - state->forceAttrs(*attr.value, attr.pos, ""); + state->forceAttrs(*attr.value, attr.pos); for (auto & attr2 : *attr.value->attrs) checkApp( fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]), @@ -557,7 +561,7 @@ struct CmdFlakeCheck : FlakeCommand } else if (name == "defaultPackage" || name == "devShell") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) { const auto & attr_name = state->symbols[attr.name]; checkSystemName(attr_name, attr.pos); @@ -568,7 +572,7 @@ struct CmdFlakeCheck : FlakeCommand } else if (name == "defaultApp") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) { const auto & attr_name = state->symbols[attr.name]; checkSystemName(attr_name, attr.pos); @@ -579,7 +583,7 @@ struct CmdFlakeCheck : FlakeCommand } else if (name == "legacyPackages") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) { checkSystemName(state->symbols[attr.name], attr.pos); // FIXME: do getDerivations? @@ -590,7 +594,7 @@ struct CmdFlakeCheck : FlakeCommand checkOverlay(name, vOutput, pos); else if (name == "overlays") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) checkOverlay(fmt("%s.%s", name, state->symbols[attr.name]), *attr.value, attr.pos); @@ -600,14 +604,14 @@ struct CmdFlakeCheck : FlakeCommand checkModule(name, vOutput, pos); else if (name == "nixosModules") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) checkModule(fmt("%s.%s", name, state->symbols[attr.name]), *attr.value, attr.pos); } else if (name == "nixosConfigurations") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) checkNixOSConfiguration(fmt("%s.%s", name, state->symbols[attr.name]), *attr.value, attr.pos); @@ -620,14 +624,14 @@ struct CmdFlakeCheck : FlakeCommand checkTemplate(name, vOutput, pos); else if (name == "templates") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) checkTemplate(fmt("%s.%s", name, state->symbols[attr.name]), *attr.value, attr.pos); } else if (name == "defaultBundler") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) { const auto & attr_name = state->symbols[attr.name]; checkSystemName(attr_name, attr.pos); @@ -638,11 +642,11 @@ struct CmdFlakeCheck : FlakeCommand } else if (name == "bundlers") { - state->forceAttrs(vOutput, pos, ""); + state->forceAttrs(vOutput, pos); for (auto & attr : *vOutput.attrs) { const auto & attr_name = state->symbols[attr.name]; checkSystemName(attr_name, attr.pos); - state->forceAttrs(*attr.value, attr.pos, ""); + state->forceAttrs(*attr.value, attr.pos); for (auto & attr2 : *attr.value->attrs) { checkBundler( fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]), @@ -651,6 +655,19 @@ struct CmdFlakeCheck : FlakeCommand } } + else if ( + name == "lib" + || name == "darwinConfigurations" + || name == "darwinModules" + || name == "flakeModule" + || name == "flakeModules" + || name == "herculesCI" + || name == "homeConfigurations" + || name == "nixopsConfigurations" + ) + // Known but unchecked community attribute + ; + else warn("unknown flake output '%s'", name); diff --git a/src/nix/main.cc b/src/nix/main.cc index d3d2f5b16..2c6309c81 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -199,7 +199,7 @@ static void showHelp(std::vector<std::string> subcommand, MultiCommand & topleve if (!attr) throw UsageError("Nix has no subcommand '%s'", concatStringsSep("", subcommand)); - auto markdown = state.forceString(*attr->value, noPos, "while evaluating the lowdown help text"); + auto markdown = state.forceString(*attr->value); RunPager pager; std::cout << renderMarkdownToTerminal(markdown) << "\n"; diff --git a/src/nix/prefetch.cc b/src/nix/prefetch.cc index fc3823406..ce3288dc1 100644 --- a/src/nix/prefetch.cc +++ b/src/nix/prefetch.cc @@ -28,17 +28,17 @@ std::string resolveMirrorUrl(EvalState & state, const std::string & url) Value vMirrors; // FIXME: use nixpkgs flake state.eval(state.parseExprFromString("import <nixpkgs/pkgs/build-support/fetchurl/mirrors.nix>", "."), vMirrors); - state.forceAttrs(vMirrors, noPos, "while evaluating the set of all mirrors"); + state.forceAttrs(vMirrors, noPos); auto mirrorList = vMirrors.attrs->find(state.symbols.create(mirrorName)); if (mirrorList == vMirrors.attrs->end()) throw Error("unknown mirror name '%s'", mirrorName); - state.forceList(*mirrorList->value, noPos, "while evaluating one mirror configuration"); + state.forceList(*mirrorList->value, noPos); if (mirrorList->value->listSize() < 1) throw Error("mirror URL '%s' did not expand to anything", url); - std::string mirror(state.forceString(*mirrorList->value->listElems()[0], noPos, "while evaluating the first available mirror")); + std::string mirror(state.forceString(*mirrorList->value->listElems()[0])); return mirror + (hasSuffix(mirror, "/") ? "" : "/") + s.substr(p + 1); } @@ -196,29 +196,29 @@ static int main_nix_prefetch_url(int argc, char * * argv) Value vRoot; state->evalFile(path, vRoot); Value & v(*findAlongAttrPath(*state, attrPath, autoArgs, vRoot).first); - state->forceAttrs(v, noPos, "while evaluating the source attribute to prefetch"); + state->forceAttrs(v, noPos); /* Extract the URL. */ auto * attr = v.attrs->get(state->symbols.create("urls")); if (!attr) throw Error("attribute 'urls' missing"); - state->forceList(*attr->value, noPos, "while evaluating the urls to prefetch"); + state->forceList(*attr->value, noPos); if (attr->value->listSize() < 1) throw Error("'urls' list is empty"); - url = state->forceString(*attr->value->listElems()[0], noPos, "while evaluating the first url from the urls list"); + url = state->forceString(*attr->value->listElems()[0]); /* Extract the hash mode. */ auto attr2 = v.attrs->get(state->symbols.create("outputHashMode")); if (!attr2) printInfo("warning: this does not look like a fetchurl call"); else - unpack = state->forceString(*attr2->value, noPos, "while evaluating the outputHashMode of the source to prefetch") == "recursive"; + unpack = state->forceString(*attr2->value) == "recursive"; /* Extract the name. */ if (!name) { auto attr3 = v.attrs->get(state->symbols.create("name")); if (!attr3) - name = state->forceString(*attr3->value, noPos, "while evaluating the name of the source to prefetch"); + name = state->forceString(*attr3->value); } } diff --git a/src/nix/profile.cc b/src/nix/profile.cc index 22ee51ab9..32364e720 100644 --- a/src/nix/profile.cc +++ b/src/nix/profile.cc @@ -22,7 +22,7 @@ struct ProfileElementSource // FIXME: record original attrpath. FlakeRef resolvedRef; std::string attrPath; - OutputsSpec outputs; + ExtendedOutputsSpec outputs; bool operator < (const ProfileElementSource & other) const { @@ -44,7 +44,7 @@ struct ProfileElement std::string describe() const { if (source) - return fmt("%s#%s%s", source->originalRef, source->attrPath, printOutputsSpec(source->outputs)); + return fmt("%s#%s%s", source->originalRef, source->attrPath, source->outputs.to_string()); StringSet names; for (auto & path : storePaths) names.insert(DrvName(path.name()).name); @@ -126,7 +126,7 @@ struct ProfileManifest parseFlakeRef(e[sOriginalUrl]), parseFlakeRef(e[sUrl]), e["attrPath"], - e["outputs"].get<OutputsSpec>() + e["outputs"].get<ExtendedOutputsSpec>() }; } elements.emplace_back(std::move(element)); @@ -308,12 +308,12 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile auto & [res, info] = builtPaths[installable.get()]; - if (info.originalRef && info.resolvedRef && info.attrPath && info.outputsSpec) { + if (info.originalRef && info.resolvedRef && info.attrPath && info.extendedOutputsSpec) { element.source = ProfileElementSource { .originalRef = *info.originalRef, .resolvedRef = *info.resolvedRef, .attrPath = *info.attrPath, - .outputs = *info.outputsSpec, + .outputs = *info.extendedOutputsSpec, }; } @@ -497,7 +497,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf .originalRef = installable->flakeRef, .resolvedRef = *info.resolvedRef, .attrPath = *info.attrPath, - .outputs = installable->outputsSpec, + .outputs = installable->extendedOutputsSpec, }; installables.push_back(installable); @@ -553,8 +553,8 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro for (size_t i = 0; i < manifest.elements.size(); ++i) { auto & element(manifest.elements[i]); logger->cout("%d %s %s %s", i, - element.source ? element.source->originalRef.to_string() + "#" + element.source->attrPath + printOutputsSpec(element.source->outputs) : "-", - element.source ? element.source->resolvedRef.to_string() + "#" + element.source->attrPath + printOutputsSpec(element.source->outputs) : "-", + element.source ? element.source->originalRef.to_string() + "#" + element.source->attrPath + element.source->outputs.to_string() : "-", + element.source ? element.source->resolvedRef.to_string() + "#" + element.source->attrPath + element.source->outputs.to_string() : "-", concatStringsSep(" ", store->printStorePathSet(element.storePaths))); } } diff --git a/src/nix/upgrade-nix.cc b/src/nix/upgrade-nix.cc index 17796d6b8..2d2453395 100644 --- a/src/nix/upgrade-nix.cc +++ b/src/nix/upgrade-nix.cc @@ -144,7 +144,7 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand Bindings & bindings(*state->allocBindings(0)); auto v2 = findAlongAttrPath(*state, settings.thisSystem, bindings, *v).first; - return store->parseStorePath(state->forceString(*v2, noPos, "while evaluating the path tho latest nix version")); + return store->parseStorePath(state->forceString(*v2)); } }; diff --git a/tests/build-hook-ca-floating.nix b/tests/build-hook-ca-floating.nix index 67295985f..dfdbb82da 100644 --- a/tests/build-hook-ca-floating.nix +++ b/tests/build-hook-ca-floating.nix @@ -1,53 +1,6 @@ { busybox }: -with import ./config.nix; - -let - - mkDerivation = args: - derivation ({ - inherit system; - builder = busybox; - args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" "if [ -e .attrs.sh ]; then source .attrs.sh; fi; eval \"$buildCommand\"")]; - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; - __contentAddressed = true; - } // removeAttrs args ["builder" "meta"]) - // { meta = args.meta or {}; }; - - input1 = mkDerivation { - shell = busybox; - name = "build-remote-input-1"; - buildCommand = "echo FOO > $out"; - requiredSystemFeatures = ["foo"]; - }; - - input2 = mkDerivation { - shell = busybox; - name = "build-remote-input-2"; - buildCommand = "echo BAR > $out"; - requiredSystemFeatures = ["bar"]; - }; - - input3 = mkDerivation { - shell = busybox; - name = "build-remote-input-3"; - buildCommand = '' - read x < ${input2} - echo $x BAZ > $out - ''; - requiredSystemFeatures = ["baz"]; - }; - -in - - mkDerivation { - shell = busybox; - name = "build-remote"; - buildCommand = - '' - read x < ${input1} - read y < ${input3} - echo "$x $y" > $out - ''; - } +import ./build-hook.nix { + inherit busybox; + contentAddressed = true; +} diff --git a/tests/build-hook.nix b/tests/build-hook.nix index 643334caa..7effd7903 100644 --- a/tests/build-hook.nix +++ b/tests/build-hook.nix @@ -1,15 +1,22 @@ -{ busybox }: +{ busybox, contentAddressed ? false }: with import ./config.nix; let + caArgs = if contentAddressed then { + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + __contentAddressed = true; + } else {}; + mkDerivation = args: derivation ({ inherit system; builder = busybox; args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" "if [ -e .attrs.sh ]; then source .attrs.sh; fi; eval \"$buildCommand\"")]; - } // removeAttrs args ["builder" "meta" "passthru"]) + } // removeAttrs args ["builder" "meta" "passthru"] + // caArgs) // { meta = args.meta or {}; passthru = args.passthru or {}; }; input1 = mkDerivation { diff --git a/tests/build-remote.sh b/tests/build-remote.sh index e73c37ea4..25a482003 100644 --- a/tests/build-remote.sh +++ b/tests/build-remote.sh @@ -63,12 +63,9 @@ nix path-info --store $TEST_ROOT/machine3 --all \ | grep builder-build-remote-input-3.sh -# Temporarily disabled because of https://github.com/NixOS/nix/issues/6209 -if [[ -z "$CONTENT_ADDRESSED" ]]; then - for i in input1 input3; do - nix log --store $TEST_ROOT/machine0 --file "$file" --arg busybox $busybox passthru."$i" | grep hi-$i - done -fi +for i in input1 input3; do +nix log --store $TEST_ROOT/machine0 --file "$file" --arg busybox $busybox passthru."$i" | grep hi-$i +done # Behavior of keep-failed out="$(nix-build 2>&1 failing.nix \ diff --git a/tests/build.sh b/tests/build.sh index 036fb037e..a00fb5232 100644 --- a/tests/build.sh +++ b/tests/build.sh @@ -42,20 +42,21 @@ nix build -f multiple-outputs.nix --json 'a^*' --no-link | jq --exit-status ' nix build -f multiple-outputs.nix --json e --no-link | jq --exit-status ' (.[0] | (.drvPath | match(".*multiple-outputs-e.drv")) and - (.outputs | keys == ["a", "b"])) + (.outputs | keys == ["a_a", "b"])) ' # But not when it's overriden. -nix build -f multiple-outputs.nix --json e^a --no-link | jq --exit-status ' +nix build -f multiple-outputs.nix --json e^a_a --no-link +nix build -f multiple-outputs.nix --json e^a_a --no-link | jq --exit-status ' (.[0] | (.drvPath | match(".*multiple-outputs-e.drv")) and - (.outputs | keys == ["a"])) + (.outputs | keys == ["a_a"])) ' nix build -f multiple-outputs.nix --json 'e^*' --no-link | jq --exit-status ' (.[0] | (.drvPath | match(".*multiple-outputs-e.drv")) and - (.outputs | keys == ["a", "b", "c"])) + (.outputs | keys == ["a_a", "b", "c"])) ' # Test building from raw store path to drv not expression. @@ -88,7 +89,7 @@ nix build "$drv^first,second" --no-link --json | jq --exit-status ' (.outputs | (keys | length == 2) and (.first | match(".*multiple-outputs-a-first")) and - (.second | match(".*multiple-outputs-a-second")))) + (.second | match(".*multiple-outputs-a-second")))) ' nix build "$drv^*" --no-link --json | jq --exit-status ' @@ -97,14 +98,14 @@ nix build "$drv^*" --no-link --json | jq --exit-status ' (.outputs | (keys | length == 2) and (.first | match(".*multiple-outputs-a-first")) and - (.second | match(".*multiple-outputs-a-second")))) + (.second | match(".*multiple-outputs-a-second")))) ' # Make sure that `--impure` works (regression test for https://github.com/NixOS/nix/issues/6488) nix build --impure -f multiple-outputs.nix --json e --no-link | jq --exit-status ' (.[0] | (.drvPath | match(".*multiple-outputs-e.drv")) and - (.outputs | keys == ["a", "b"])) + (.outputs | keys == ["a_a", "b"])) ' testNormalization () { diff --git a/tests/multiple-outputs.nix b/tests/multiple-outputs.nix index 1429bc648..413d392e4 100644 --- a/tests/multiple-outputs.nix +++ b/tests/multiple-outputs.nix @@ -91,9 +91,9 @@ rec { e = mkDerivation { name = "multiple-outputs-e"; - outputs = [ "a" "b" "c" ]; - meta.outputsToInstall = [ "a" "b" ]; - buildCommand = "mkdir $a $b $c"; + outputs = [ "a_a" "b" "c" ]; + meta.outputsToInstall = [ "a_a" "b" ]; + buildCommand = "mkdir $a_a $b $c"; }; independent = mkDerivation { @@ -117,4 +117,14 @@ rec { ''; }; + invalid-output-name-1 = mkDerivation { + name = "invalid-output-name-1"; + outputs = [ "out/"]; + }; + + invalid-output-name-2 = mkDerivation { + name = "invalid-output-name-2"; + outputs = [ "x" "foo$"]; + }; + } diff --git a/tests/multiple-outputs.sh b/tests/multiple-outputs.sh index 0d45ad35b..66be6fa64 100644 --- a/tests/multiple-outputs.sh +++ b/tests/multiple-outputs.sh @@ -83,3 +83,6 @@ nix-store --gc --keep-derivations --keep-outputs nix-store --gc --print-roots rm -rf $NIX_STORE_DIR/.links rmdir $NIX_STORE_DIR + +nix build -f multiple-outputs.nix invalid-output-name-1 2>&1 | grep 'contains illegal character' +nix build -f multiple-outputs.nix invalid-output-name-2 2>&1 | grep 'contains illegal character' diff --git a/tests/plugins/local.mk b/tests/plugins/local.mk index 82ad99402..8182a6a83 100644 --- a/tests/plugins/local.mk +++ b/tests/plugins/local.mk @@ -8,4 +8,4 @@ libplugintest_ALLOW_UNDEFINED := 1 libplugintest_EXCLUDE_FROM_LIBRARY_LIST := 1 -libplugintest_CXXFLAGS := -I src/libutil -I src/libexpr +libplugintest_CXXFLAGS := -I src/libutil -I src/libstore -I src/libexpr |