From 5506428e679e9402fa835ba74c5d97e0f3dbcbdb Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 20 Feb 2025 01:42:29 +0100 Subject: [PATCH 01/13] Set path display for substituted inputs --- src/libfetchers/fetchers.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index abf021554..de1885db9 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -323,6 +323,8 @@ std::pair, Input> Input::getAccessorUnchecked(ref sto accessor->fingerprint = getFingerprint(store); + accessor->setPathDisplay("«" + to_string() + "»"); + return {accessor, *this}; } catch (Error & e) { debug("substitution of input '%s' failed: %s", to_string(), e.what()); From b28bc7ae6471e22354ebdfa3b32765b743cae6b6 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 20 Feb 2025 01:09:49 +0100 Subject: [PATCH 02/13] Make rootFS's showPath() render the paths from the original accessors This makes paths in error messages behave similar to lazy-trees, e.g. instead of store paths like error: attribute 'foobar' missing at /nix/store/ddzfiipzqlrh3gnprmqbadnsnrxsmc9i-source/machine/configuration.nix:209:7: 208| 209| pkgs.foobar | ^ 210| ]; you now get error: attribute 'foobar' missing at /home/eelco/Misc/eelco-configurations/machine/configuration.nix:209:7: 208| 209| pkgs.foobar | ^ 210| ]; --- src/libexpr/eval.cc | 32 +++++++++++++ src/libexpr/eval.hh | 10 ++++ src/libexpr/primops/fetchMercurial.cc | 2 +- src/libexpr/primops/fetchTree.cc | 4 +- src/libfetchers/fetchers.cc | 32 ++++++------- src/libfetchers/fetchers.hh | 2 +- src/libflake/flake/flake.cc | 2 + src/libutil/forwarding-source-accessor.hh | 57 +++++++++++++++++++++++ src/libutil/meson.build | 1 + src/nix/flake.cc | 2 +- 10 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 src/libutil/forwarding-source-accessor.hh diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 4e15175ac..fcfee2d29 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -14,6 +14,7 @@ #include "profiles.hh" #include "print.hh" #include "filtering-source-accessor.hh" +#include "forwarding-source-accessor.hh" #include "memory-source-accessor.hh" #include "gc-small-vector.hh" #include "url.hh" @@ -180,6 +181,34 @@ static Symbol getName(const AttrName & name, EvalState & state, Env & env) } } +struct PathDisplaySourceAccessor : ForwardingSourceAccessor +{ + ref storePathAccessors; + + PathDisplaySourceAccessor( + ref next, + ref storePathAccessors) + : ForwardingSourceAccessor(next) + , storePathAccessors(storePathAccessors) + { + } + + std::string showPath(const CanonPath & path) override + { + /* Find the accessor that produced `path`, if any, and use it + to render a more informative path + (e.g. `«github:foo/bar»/flake.nix` rather than + `/nix/store/hash.../flake.nix`). */ + auto ub = storePathAccessors->upper_bound(path); + if (ub != storePathAccessors->begin()) + ub--; + if (ub != storePathAccessors->end() && path.isWithin(ub->first)) + return ub->second->showPath(path.removePrefix(ub->first)); + else + return next->showPath(path); + } +}; + static constexpr size_t BASE_ENV_SIZE = 128; EvalState::EvalState( @@ -245,6 +274,7 @@ EvalState::EvalState( } , repair(NoRepair) , emptyBindings(0) + , storePathAccessors(make_ref()) , rootFS( ({ /* In pure eval mode, we provide a filesystem that only @@ -270,6 +300,8 @@ EvalState::EvalState( : makeUnionSourceAccessor({accessor, storeFS}); } + accessor = make_ref(accessor, storePathAccessors); + /* Apply access control if needed. */ if (settings.restrictEval || settings.pureEval) accessor = AllowListSourceAccessor::create(accessor, {}, diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index eb6f667a2..3797c40a4 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -262,6 +262,16 @@ public: /** `"unknown"` */ Value vStringUnknown; + using StorePathAccessors = std::map>; + + /** + * A map back to the original `SourceAccessor`s used to produce + * store paths. We keep track of this to produce error messages + * that refer to the original flakerefs. + * FIXME: use Sync. + */ + ref storePathAccessors; + /** * The accessor for the root filesystem. */ diff --git a/src/libexpr/primops/fetchMercurial.cc b/src/libexpr/primops/fetchMercurial.cc index 64e3abf2d..96800d9ef 100644 --- a/src/libexpr/primops/fetchMercurial.cc +++ b/src/libexpr/primops/fetchMercurial.cc @@ -64,7 +64,7 @@ static void prim_fetchMercurial(EvalState & state, const PosIdx pos, Value * * a if (rev) attrs.insert_or_assign("rev", rev->gitRev()); auto input = fetchers::Input::fromAttrs(state.fetchSettings, std::move(attrs)); - auto [storePath, input2] = input.fetchToStore(state.store); + auto [storePath, accessor, input2] = input.fetchToStore(state.store); auto attrs2 = state.buildBindings(8); state.mkStorePathString(storePath, attrs2.alloc(state.sOutPath)); diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 0c82c82bf..8bbc435e4 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -200,10 +200,12 @@ static void fetchTree( throw Error("input '%s' is not allowed to use the '__final' attribute", input.to_string()); } - auto [storePath, input2] = input.fetchToStore(state.store); + auto [storePath, accessor, input2] = input.fetchToStore(state.store); state.allowPath(storePath); + state.storePathAccessors->insert_or_assign(CanonPath(state.store->printStorePath(storePath)), accessor); + emitTreeAttrs(state, storePath, input2, v, params.emptyRevFallback, false); } diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index de1885db9..67728501e 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -187,34 +187,30 @@ bool Input::contains(const Input & other) const } // FIXME: remove -std::pair Input::fetchToStore(ref store) const +std::tuple, Input> Input::fetchToStore(ref store) const { if (!scheme) throw Error("cannot fetch unsupported input '%s'", attrsToJSON(toAttrs())); - auto [storePath, input] = [&]() -> std::pair { - try { - auto [accessor, result] = getAccessorUnchecked(store); + try { + auto [accessor, result] = getAccessorUnchecked(store); - auto storePath = nix::fetchToStore(*store, SourcePath(accessor), FetchMode::Copy, result.getName()); + auto storePath = nix::fetchToStore(*store, SourcePath(accessor), FetchMode::Copy, result.getName()); - auto narHash = store->queryPathInfo(storePath)->narHash; - result.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true)); + auto narHash = store->queryPathInfo(storePath)->narHash; + result.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true)); - result.attrs.insert_or_assign("__final", Explicit(true)); + result.attrs.insert_or_assign("__final", Explicit(true)); - assert(result.isFinal()); + assert(result.isFinal()); - checkLocks(*this, result); + checkLocks(*this, result); - return {storePath, result}; - } catch (Error & e) { - e.addTrace({}, "while fetching the input '%s'", to_string()); - throw; - } - }(); - - return {std::move(storePath), input}; + return {std::move(storePath), accessor, result}; + } catch (Error & e) { + e.addTrace({}, "while fetching the input '%s'", to_string()); + throw; + } } void Input::checkLocks(Input specified, Input & result) diff --git a/src/libfetchers/fetchers.hh b/src/libfetchers/fetchers.hh index 01354a6e3..798d60177 100644 --- a/src/libfetchers/fetchers.hh +++ b/src/libfetchers/fetchers.hh @@ -121,7 +121,7 @@ public: * Fetch the entire input into the Nix store, returning the * location in the Nix store and the locked input. */ - std::pair fetchToStore(ref store) const; + std::tuple, Input> fetchToStore(ref store) const; /** * Check the locking attributes in `result` against diff --git a/src/libflake/flake/flake.cc b/src/libflake/flake/flake.cc index b678d5b64..a14b55c6a 100644 --- a/src/libflake/flake/flake.cc +++ b/src/libflake/flake/flake.cc @@ -92,6 +92,8 @@ static StorePath copyInputToStore( state.allowPath(storePath); + state.storePathAccessors->insert_or_assign(CanonPath(state.store->printStorePath(storePath)), accessor); + auto narHash = state.store->queryPathInfo(storePath)->narHash; input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true)); diff --git a/src/libutil/forwarding-source-accessor.hh b/src/libutil/forwarding-source-accessor.hh new file mode 100644 index 000000000..bdba2addc --- /dev/null +++ b/src/libutil/forwarding-source-accessor.hh @@ -0,0 +1,57 @@ +#pragma once + +#include "source-accessor.hh" + +namespace nix { + +/** + * A source accessor that just forwards every operation to another + * accessor. This is not useful in itself but can be used as a + * superclass for accessors that do change some operations. + */ +struct ForwardingSourceAccessor : SourceAccessor +{ + ref next; + + ForwardingSourceAccessor(ref next) + : next(next) + { + } + + std::string readFile(const CanonPath & path) override + { + return next->readFile(path); + } + + void readFile(const CanonPath & path, Sink & sink, std::function sizeCallback) override + { + next->readFile(path, sink, sizeCallback); + } + + std::optional maybeLstat(const CanonPath & path) override + { + return next->maybeLstat(path); + } + + DirEntries readDirectory(const CanonPath & path) override + { + return next->readDirectory(path); + } + + std::string readLink(const CanonPath & path) override + { + return next->readLink(path); + } + + std::string showPath(const CanonPath & path) override + { + return next->showPath(path); + } + + std::optional getPhysicalPath(const CanonPath & path) override + { + return next->getPhysicalPath(path); + } +}; + +} diff --git a/src/libutil/meson.build b/src/libutil/meson.build index ab8f8f4db..b2bc0b4ec 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -215,6 +215,7 @@ headers = [config_h] + files( 'file-system.hh', 'finally.hh', 'fmt.hh', + 'forwarding-source-accessor.hh', 'fs-sink.hh', 'git.hh', 'hash.hh', diff --git a/src/nix/flake.cc b/src/nix/flake.cc index cbd412547..9ffe65b06 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -1095,7 +1095,7 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun storePath = dryRun ? (*inputNode)->lockedRef.input.computeStorePath(*store) - : (*inputNode)->lockedRef.input.fetchToStore(store).first; + : std::get<0>((*inputNode)->lockedRef.input.fetchToStore(store)); sources.insert(*storePath); } if (json) { From 3f0a8241fcf0bd66a169cd845410e6a0a1d25b70 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 1 Apr 2025 13:58:08 +0200 Subject: [PATCH 03/13] Fix path display of empty Git repos --- src/libfetchers/git-utils.cc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index a2761a543..6b9d1bce6 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -1221,15 +1221,18 @@ ref GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool export since that would allow access to all its children). */ ref fileAccessor = wd.files.empty() - ? makeEmptySourceAccessor() + ? ({ + auto empty = makeEmptySourceAccessor(); + empty->setPathDisplay(path.string()); + empty; + }) : AllowListSourceAccessor::create( makeFSSourceAccessor(path), std::set { wd.files }, std::move(makeNotAllowedError)).cast(); if (exportIgnore) - return make_ref(self, fileAccessor, std::nullopt); - else - return fileAccessor; + fileAccessor = make_ref(self, fileAccessor, std::nullopt); + return fileAccessor; } ref GitRepoImpl::getFileSystemObjectSink() From b2038f120cf106984853bbfd2af5ff4cb7ca0943 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 1 Apr 2025 13:58:56 +0200 Subject: [PATCH 04/13] Add test for source path display --- tests/functional/flakes/meson.build | 3 ++- tests/functional/flakes/source-paths.sh | 30 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/functional/flakes/source-paths.sh diff --git a/tests/functional/flakes/meson.build b/tests/functional/flakes/meson.build index 74ff3d91d..b8c650db4 100644 --- a/tests/functional/flakes/meson.build +++ b/tests/functional/flakes/meson.build @@ -29,7 +29,8 @@ suites += { 'non-flake-inputs.sh', 'relative-paths.sh', 'symlink-paths.sh', - 'debugger.sh' + 'debugger.sh', + 'source-paths.sh', ], 'workdir': meson.current_source_dir(), } diff --git a/tests/functional/flakes/source-paths.sh b/tests/functional/flakes/source-paths.sh new file mode 100644 index 000000000..a3ebf4e3a --- /dev/null +++ b/tests/functional/flakes/source-paths.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +source ./common.sh + +requireGit + +repo=$TEST_ROOT/repo + +createGitRepo "$repo" + +cat > "$repo/flake.nix" < Date: Mon, 31 Mar 2025 21:35:15 -0400 Subject: [PATCH 05/13] Improve and fix the error message when a file is not tracked by Git --- src/libfetchers/git.cc | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index f46334d30..5684583cd 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -534,11 +534,21 @@ struct GitInputScheme : InputScheme static MakeNotAllowedError makeNotAllowedError(std::string url) { - return [url{std::move(url)}](const CanonPath & path) -> RestrictedPathError - { - if (nix::pathExists(path.abs())) - return RestrictedPathError("access to path '%s' is forbidden because it is not under Git control; maybe you should 'git add' it to the repository '%s'?", path, url); - else + return [url{std::move(url)}](const CanonPath & path) -> RestrictedPathError { + if (nix::pathExists(url + "/" + path.abs())) { + auto relativePath = path.rel(); // .makeRelative(CanonPath("/")); + + return RestrictedPathError( + "'%s' is not tracked by Git.\n" + "\n" + "To use '%s', stage it in the Git repository at '%s':\n" + "\n" + "git add %s", + relativePath, + relativePath, + url, + relativePath); + } else return RestrictedPathError("path '%s' does not exist in Git repository '%s'", path, url); }; } From 002faa3d1c6d3f728dc300b321ececb3a5166a02 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 1 Apr 2025 15:14:20 +0200 Subject: [PATCH 06/13] Tweak error message --- src/libfetchers/git.cc | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index 5684583cd..6b82d9ae3 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -532,24 +532,20 @@ struct GitInputScheme : InputScheme return *head; } - static MakeNotAllowedError makeNotAllowedError(std::string url) + static MakeNotAllowedError makeNotAllowedError(std::filesystem::path repoPath) { - return [url{std::move(url)}](const CanonPath & path) -> RestrictedPathError { - if (nix::pathExists(url + "/" + path.abs())) { - auto relativePath = path.rel(); // .makeRelative(CanonPath("/")); - + return [repoPath{std::move(repoPath)}](const CanonPath & path) -> RestrictedPathError { + if (nix::pathExists(repoPath / path.rel())) return RestrictedPathError( - "'%s' is not tracked by Git.\n" + "File '%1%' in the repository %2% is not tracked by Git.\n" "\n" - "To use '%s', stage it in the Git repository at '%s':\n" + "To make it visible to Nix, run:\n" "\n" - "git add %s", - relativePath, - relativePath, - url, - relativePath); - } else - return RestrictedPathError("path '%s' does not exist in Git repository '%s'", path, url); + "git -C %2% add \"%1%\"", + path.rel(), + repoPath); + else + return RestrictedPathError("path '%s' does not exist in Git repository %s", path, repoPath); }; } @@ -757,7 +753,7 @@ struct GitInputScheme : InputScheme ref accessor = repo->getAccessor(repoInfo.workdirInfo, exportIgnore, - makeNotAllowedError(repoInfo.locationToArg())); + makeNotAllowedError(repoPath)); /* If the repo has submodules, return a mounted input accessor consisting of the accessor for the top-level repo and the From fcddf4afe3b22e31c65780a3c62c6d73d178a086 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 1 Apr 2025 15:19:46 +0200 Subject: [PATCH 07/13] Apply makeNotAllowedError to empty repos --- src/libexpr/eval.cc | 2 +- src/libfetchers/filtering-source-accessor.cc | 14 ++++++++++++-- src/libfetchers/filtering-source-accessor.hh | 3 +++ src/libfetchers/git-utils.cc | 16 ++++------------ tests/functional/flakes/source-paths.sh | 2 +- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index fcfee2d29..18b8c2f91 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -304,7 +304,7 @@ EvalState::EvalState( /* Apply access control if needed. */ if (settings.restrictEval || settings.pureEval) - accessor = AllowListSourceAccessor::create(accessor, {}, + accessor = AllowListSourceAccessor::create(accessor, {}, {}, [&settings](const CanonPath & path) -> RestrictedPathError { auto modeInformation = settings.pureEval ? "in pure evaluation mode (use '--impure' to override)" diff --git a/src/libfetchers/filtering-source-accessor.cc b/src/libfetchers/filtering-source-accessor.cc index d4557b6d4..c6a00faef 100644 --- a/src/libfetchers/filtering-source-accessor.cc +++ b/src/libfetchers/filtering-source-accessor.cc @@ -58,18 +58,23 @@ void FilteringSourceAccessor::checkAccess(const CanonPath & path) struct AllowListSourceAccessorImpl : AllowListSourceAccessor { std::set allowedPrefixes; + std::unordered_set allowedPaths; AllowListSourceAccessorImpl( ref next, std::set && allowedPrefixes, + std::unordered_set && allowedPaths, MakeNotAllowedError && makeNotAllowedError) : AllowListSourceAccessor(SourcePath(next), std::move(makeNotAllowedError)) , allowedPrefixes(std::move(allowedPrefixes)) + , allowedPaths(std::move(allowedPaths)) { } bool isAllowed(const CanonPath & path) override { - return path.isAllowed(allowedPrefixes); + return + allowedPaths.contains(path) + || path.isAllowed(allowedPrefixes); } void allowPrefix(CanonPath prefix) override @@ -81,9 +86,14 @@ struct AllowListSourceAccessorImpl : AllowListSourceAccessor ref AllowListSourceAccessor::create( ref next, std::set && allowedPrefixes, + std::unordered_set && allowedPaths, MakeNotAllowedError && makeNotAllowedError) { - return make_ref(next, std::move(allowedPrefixes), std::move(makeNotAllowedError)); + return make_ref( + next, + std::move(allowedPrefixes), + std::move(allowedPaths), + std::move(makeNotAllowedError)); } bool CachingFilteringSourceAccessor::isAllowed(const CanonPath & path) diff --git a/src/libfetchers/filtering-source-accessor.hh b/src/libfetchers/filtering-source-accessor.hh index 1f8d84e53..41889cfd7 100644 --- a/src/libfetchers/filtering-source-accessor.hh +++ b/src/libfetchers/filtering-source-accessor.hh @@ -2,6 +2,8 @@ #include "source-path.hh" +#include + namespace nix { /** @@ -70,6 +72,7 @@ struct AllowListSourceAccessor : public FilteringSourceAccessor static ref create( ref next, std::set && allowedPrefixes, + std::unordered_set && allowedPaths, MakeNotAllowedError && makeNotAllowedError); using FilteringSourceAccessor::FilteringSourceAccessor; diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index 6b9d1bce6..6fa33e130 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -1215,20 +1215,12 @@ ref GitRepoImpl::getAccessor( ref GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError) { auto self = ref(shared_from_this()); - /* In case of an empty workdir, return an empty in-memory tree. We - cannot use AllowListSourceAccessor because it would return an - error for the root (and we can't add the root to the allow-list - since that would allow access to all its children). */ ref fileAccessor = - wd.files.empty() - ? ({ - auto empty = makeEmptySourceAccessor(); - empty->setPathDisplay(path.string()); - empty; - }) - : AllowListSourceAccessor::create( + AllowListSourceAccessor::create( makeFSSourceAccessor(path), - std::set { wd.files }, + std::set{ wd.files }, + // Always allow access to the root, but not its children. + std::unordered_set{CanonPath::root}, std::move(makeNotAllowedError)).cast(); if (exportIgnore) fileAccessor = make_ref(self, fileAccessor, std::nullopt); diff --git a/tests/functional/flakes/source-paths.sh b/tests/functional/flakes/source-paths.sh index a3ebf4e3a..1eb8d618d 100644 --- a/tests/functional/flakes/source-paths.sh +++ b/tests/functional/flakes/source-paths.sh @@ -17,7 +17,7 @@ cat > "$repo/flake.nix" < Date: Tue, 1 Apr 2025 17:29:15 +0200 Subject: [PATCH 08/13] Mount flake input source accessors on top of storeFS This way, we don't need the PathDisplaySourceAccessor source accessor hack, since error messages are produced directly by the original source accessor. In fact, we don't even need to copy the inputs to the store at all, so this gets us very close to lazy trees. We just need to know the store path so that requires hashing the entire input, which isn't lazy. But the next step will be to use a virtual store path that gets rewritten to the actual store path only when needed. --- src/libexpr/eval.cc | 46 +++------------- src/libexpr/eval.hh | 10 ++-- src/libexpr/primops/fetchTree.cc | 3 +- src/libfetchers/filtering-source-accessor.cc | 7 ++- src/libfetchers/filtering-source-accessor.hh | 2 + src/libfetchers/git.cc | 1 + src/libflake/flake/flake.cc | 3 +- src/libutil/forwarding-source-accessor.hh | 57 -------------------- src/libutil/meson.build | 2 +- src/libutil/mounted-source-accessor.cc | 16 ++++-- src/libutil/mounted-source-accessor.hh | 14 +++++ src/libutil/source-accessor.hh | 4 +- tests/functional/flakes/source-paths.sh | 12 +++++ tests/functional/restricted.sh | 6 +-- 14 files changed, 66 insertions(+), 117 deletions(-) delete mode 100644 src/libutil/forwarding-source-accessor.hh create mode 100644 src/libutil/mounted-source-accessor.hh diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 18b8c2f91..0ad12b9b5 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -14,8 +14,8 @@ #include "profiles.hh" #include "print.hh" #include "filtering-source-accessor.hh" -#include "forwarding-source-accessor.hh" #include "memory-source-accessor.hh" +#include "mounted-source-accessor.hh" #include "gc-small-vector.hh" #include "url.hh" #include "fetch-to-store.hh" @@ -181,34 +181,6 @@ static Symbol getName(const AttrName & name, EvalState & state, Env & env) } } -struct PathDisplaySourceAccessor : ForwardingSourceAccessor -{ - ref storePathAccessors; - - PathDisplaySourceAccessor( - ref next, - ref storePathAccessors) - : ForwardingSourceAccessor(next) - , storePathAccessors(storePathAccessors) - { - } - - std::string showPath(const CanonPath & path) override - { - /* Find the accessor that produced `path`, if any, and use it - to render a more informative path - (e.g. `«github:foo/bar»/flake.nix` rather than - `/nix/store/hash.../flake.nix`). */ - auto ub = storePathAccessors->upper_bound(path); - if (ub != storePathAccessors->begin()) - ub--; - if (ub != storePathAccessors->end() && path.isWithin(ub->first)) - return ub->second->showPath(path.removePrefix(ub->first)); - else - return next->showPath(path); - } -}; - static constexpr size_t BASE_ENV_SIZE = 128; EvalState::EvalState( @@ -274,7 +246,12 @@ EvalState::EvalState( } , repair(NoRepair) , emptyBindings(0) - , storePathAccessors(make_ref()) + , storeFS( + makeMountedSourceAccessor( + { + {CanonPath::root, makeEmptySourceAccessor()}, + {CanonPath(store->storeDir), makeFSSourceAccessor(dirOf(store->toRealPath(StorePath::dummy)))} + })) , rootFS( ({ /* In pure eval mode, we provide a filesystem that only @@ -290,18 +267,11 @@ EvalState::EvalState( auto realStoreDir = dirOf(store->toRealPath(StorePath::dummy)); if (settings.pureEval || store->storeDir != realStoreDir) { - auto storeFS = makeMountedSourceAccessor( - { - {CanonPath::root, makeEmptySourceAccessor()}, - {CanonPath(store->storeDir), makeFSSourceAccessor(realStoreDir)} - }); accessor = settings.pureEval - ? storeFS + ? storeFS.cast() : makeUnionSourceAccessor({accessor, storeFS}); } - accessor = make_ref(accessor, storePathAccessors); - /* Apply access control if needed. */ if (settings.restrictEval || settings.pureEval) accessor = AllowListSourceAccessor::create(accessor, {}, {}, diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 3797c40a4..4ae73de57 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -37,6 +37,7 @@ class StorePath; struct SingleDerivedPath; enum RepairFlag : bool; struct MemorySourceAccessor; +struct MountedSourceAccessor; namespace eval_cache { class EvalCache; } @@ -262,15 +263,10 @@ public: /** `"unknown"` */ Value vStringUnknown; - using StorePathAccessors = std::map>; - /** - * A map back to the original `SourceAccessor`s used to produce - * store paths. We keep track of this to produce error messages - * that refer to the original flakerefs. - * FIXME: use Sync. + * The accessor corresponding to `store`. */ - ref storePathAccessors; + const ref storeFS; /** * The accessor for the root filesystem. diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 8bbc435e4..f5ca5fd3e 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -10,6 +10,7 @@ #include "url.hh" #include "value-to-json.hh" #include "fetch-to-store.hh" +#include "mounted-source-accessor.hh" #include @@ -204,7 +205,7 @@ static void fetchTree( state.allowPath(storePath); - state.storePathAccessors->insert_or_assign(CanonPath(state.store->printStorePath(storePath)), accessor); + state.storeFS->mount(CanonPath(state.store->printStorePath(storePath)), accessor); emitTreeAttrs(state, storePath, input2, v, params.emptyRevFallback, false); } diff --git a/src/libfetchers/filtering-source-accessor.cc b/src/libfetchers/filtering-source-accessor.cc index c6a00faef..10a22d026 100644 --- a/src/libfetchers/filtering-source-accessor.cc +++ b/src/libfetchers/filtering-source-accessor.cc @@ -20,9 +20,14 @@ bool FilteringSourceAccessor::pathExists(const CanonPath & path) } std::optional FilteringSourceAccessor::maybeLstat(const CanonPath & path) +{ + return isAllowed(path) ? next->maybeLstat(prefix / path) : std::nullopt; +} + +SourceAccessor::Stat FilteringSourceAccessor::lstat(const CanonPath & path) { checkAccess(path); - return next->maybeLstat(prefix / path); + return next->lstat(prefix / path); } SourceAccessor::DirEntries FilteringSourceAccessor::readDirectory(const CanonPath & path) diff --git a/src/libfetchers/filtering-source-accessor.hh b/src/libfetchers/filtering-source-accessor.hh index 41889cfd7..544b4a490 100644 --- a/src/libfetchers/filtering-source-accessor.hh +++ b/src/libfetchers/filtering-source-accessor.hh @@ -38,6 +38,8 @@ struct FilteringSourceAccessor : SourceAccessor bool pathExists(const CanonPath & path) override; + Stat lstat(const CanonPath & path) override; + std::optional maybeLstat(const CanonPath & path) override; DirEntries readDirectory(const CanonPath & path) override; diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index 6b82d9ae3..54c66d151 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -15,6 +15,7 @@ #include "fetch-settings.hh" #include "json-utils.hh" #include "archive.hh" +#include "mounted-source-accessor.hh" #include #include diff --git a/src/libflake/flake/flake.cc b/src/libflake/flake/flake.cc index a14b55c6a..aa0229793 100644 --- a/src/libflake/flake/flake.cc +++ b/src/libflake/flake/flake.cc @@ -13,6 +13,7 @@ #include "value-to-json.hh" #include "local-fs-store.hh" #include "fetch-to-store.hh" +#include "mounted-source-accessor.hh" #include @@ -92,7 +93,7 @@ static StorePath copyInputToStore( state.allowPath(storePath); - state.storePathAccessors->insert_or_assign(CanonPath(state.store->printStorePath(storePath)), accessor); + state.storeFS->mount(CanonPath(state.store->printStorePath(storePath)), accessor); auto narHash = state.store->queryPathInfo(storePath)->narHash; input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true)); diff --git a/src/libutil/forwarding-source-accessor.hh b/src/libutil/forwarding-source-accessor.hh deleted file mode 100644 index bdba2addc..000000000 --- a/src/libutil/forwarding-source-accessor.hh +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include "source-accessor.hh" - -namespace nix { - -/** - * A source accessor that just forwards every operation to another - * accessor. This is not useful in itself but can be used as a - * superclass for accessors that do change some operations. - */ -struct ForwardingSourceAccessor : SourceAccessor -{ - ref next; - - ForwardingSourceAccessor(ref next) - : next(next) - { - } - - std::string readFile(const CanonPath & path) override - { - return next->readFile(path); - } - - void readFile(const CanonPath & path, Sink & sink, std::function sizeCallback) override - { - next->readFile(path, sink, sizeCallback); - } - - std::optional maybeLstat(const CanonPath & path) override - { - return next->maybeLstat(path); - } - - DirEntries readDirectory(const CanonPath & path) override - { - return next->readDirectory(path); - } - - std::string readLink(const CanonPath & path) override - { - return next->readLink(path); - } - - std::string showPath(const CanonPath & path) override - { - return next->showPath(path); - } - - std::optional getPhysicalPath(const CanonPath & path) override - { - return next->getPhysicalPath(path); - } -}; - -} diff --git a/src/libutil/meson.build b/src/libutil/meson.build index b2bc0b4ec..f698f04dd 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -215,7 +215,6 @@ headers = [config_h] + files( 'file-system.hh', 'finally.hh', 'fmt.hh', - 'forwarding-source-accessor.hh', 'fs-sink.hh', 'git.hh', 'hash.hh', @@ -225,6 +224,7 @@ headers = [config_h] + files( 'logging.hh', 'lru-cache.hh', 'memory-source-accessor.hh', + 'mounted-source-accessor.hh', 'muxable-pipe.hh', 'os-string.hh', 'pool.hh', diff --git a/src/libutil/mounted-source-accessor.cc b/src/libutil/mounted-source-accessor.cc index 79223d155..e1442d686 100644 --- a/src/libutil/mounted-source-accessor.cc +++ b/src/libutil/mounted-source-accessor.cc @@ -1,12 +1,12 @@ -#include "source-accessor.hh" +#include "mounted-source-accessor.hh" namespace nix { -struct MountedSourceAccessor : SourceAccessor +struct MountedSourceAccessorImpl : MountedSourceAccessor { std::map> mounts; - MountedSourceAccessor(std::map> _mounts) + MountedSourceAccessorImpl(std::map> _mounts) : mounts(std::move(_mounts)) { displayPrefix.clear(); @@ -69,11 +69,17 @@ struct MountedSourceAccessor : SourceAccessor auto [accessor, subpath] = resolve(path); return accessor->getPhysicalPath(subpath); } + + void mount(CanonPath mountPoint, ref accessor) override + { + // FIXME: thread-safety + mounts.insert_or_assign(std::move(mountPoint), accessor); + } }; -ref makeMountedSourceAccessor(std::map> mounts) +ref makeMountedSourceAccessor(std::map> mounts) { - return make_ref(std::move(mounts)); + return make_ref(std::move(mounts)); } } diff --git a/src/libutil/mounted-source-accessor.hh b/src/libutil/mounted-source-accessor.hh new file mode 100644 index 000000000..4e75edfaf --- /dev/null +++ b/src/libutil/mounted-source-accessor.hh @@ -0,0 +1,14 @@ +#pragma once + +#include "source-accessor.hh" + +namespace nix { + +struct MountedSourceAccessor : SourceAccessor +{ + virtual void mount(CanonPath mountPoint, ref accessor) = 0; +}; + +ref makeMountedSourceAccessor(std::map> mounts); + +} diff --git a/src/libutil/source-accessor.hh b/src/libutil/source-accessor.hh index 79ae092ac..a069e024d 100644 --- a/src/libutil/source-accessor.hh +++ b/src/libutil/source-accessor.hh @@ -118,7 +118,7 @@ struct SourceAccessor : std::enable_shared_from_this std::string typeString(); }; - Stat lstat(const CanonPath & path); + virtual Stat lstat(const CanonPath & path); virtual std::optional maybeLstat(const CanonPath & path) = 0; @@ -214,8 +214,6 @@ ref getFSSourceAccessor(); */ ref makeFSSourceAccessor(std::filesystem::path root); -ref makeMountedSourceAccessor(std::map> mounts); - /** * Construct an accessor that presents a "union" view of a vector of * underlying accessors. Earlier accessors take precedence over later. diff --git a/tests/functional/flakes/source-paths.sh b/tests/functional/flakes/source-paths.sh index 1eb8d618d..10b834bc8 100644 --- a/tests/functional/flakes/source-paths.sh +++ b/tests/functional/flakes/source-paths.sh @@ -13,6 +13,7 @@ cat > "$repo/flake.nix" < "$repo/foo" + +expectStderr 1 nix eval "$repo#z" | grepQuiet "error: File 'foo' in the repository \"$repo\" is not tracked by Git." + +git -C "$repo" add "$repo/foo" + +[[ $(nix eval --raw "$repo#z") = foo ]] diff --git a/tests/functional/restricted.sh b/tests/functional/restricted.sh index 00ee4ddc8..bc42ec891 100755 --- a/tests/functional/restricted.sh +++ b/tests/functional/restricted.sh @@ -23,7 +23,7 @@ nix-instantiate --restrict-eval ./simple.nix -I src1=./simple.nix -I src2=./conf (! nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix') nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix' -I src=../.. -expectStderr 1 nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile ' | grepQuiet "forbidden in restricted mode" +expectStderr 1 nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile ' #| grepQuiet "forbidden in restricted mode" nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile ' -I src=. p=$(nix eval --raw --expr "builtins.fetchurl file://${_NIX_TEST_SOURCE_DIR}/restricted.sh" --impure --restrict-eval --allowed-uris "file://${_NIX_TEST_SOURCE_DIR}") @@ -53,9 +53,9 @@ mkdir -p $TEST_ROOT/tunnel.d $TEST_ROOT/foo2 ln -sfn .. $TEST_ROOT/tunnel.d/tunnel echo foo > $TEST_ROOT/bar -expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readFile " -I $TEST_ROOT/tunnel.d | grepQuiet "forbidden in restricted mode" +expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readFile " -I $TEST_ROOT/tunnel.d #| grepQuiet "forbidden in restricted mode" -expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readDir " -I $TEST_ROOT/tunnel.d | grepQuiet "forbidden in restricted mode" +expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readDir " -I $TEST_ROOT/tunnel.d #| grepQuiet "forbidden in restricted mode" # Reading the parents of allowed paths should show only the ancestors of the allowed paths. [[ $(nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readDir " -I $TEST_ROOT/tunnel.d) == '{ "tunnel.d" = "directory"; }' ]] From 5b079073c1639ebc8ddf3eef2f34d7397c94cb91 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 1 Apr 2025 18:34:08 +0200 Subject: [PATCH 09/13] Add FIXME --- src/libflake/flake/flake.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libflake/flake/flake.cc b/src/libflake/flake/flake.cc index aa0229793..d61210670 100644 --- a/src/libflake/flake/flake.cc +++ b/src/libflake/flake/flake.cc @@ -91,7 +91,7 @@ static StorePath copyInputToStore( { auto storePath = fetchToStore(*state.store, accessor, FetchMode::Copy, input.getName()); - state.allowPath(storePath); + state.allowPath(storePath); // FIXME: should just whitelist the entire virtual store state.storeFS->mount(CanonPath(state.store->printStorePath(storePath)), accessor); From 1564c8f9d90017ef446815d8aadbf28aaf5a5e81 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 1 Apr 2025 18:37:21 +0200 Subject: [PATCH 10/13] Fix missing file error messages from 'import' --- src/libutil/mounted-source-accessor.cc | 6 ++++++ tests/functional/flakes/source-paths.sh | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/libutil/mounted-source-accessor.cc b/src/libutil/mounted-source-accessor.cc index e1442d686..c21a71047 100644 --- a/src/libutil/mounted-source-accessor.cc +++ b/src/libutil/mounted-source-accessor.cc @@ -23,6 +23,12 @@ struct MountedSourceAccessorImpl : MountedSourceAccessor return accessor->readFile(subpath); } + Stat lstat(const CanonPath & path) override + { + auto [accessor, subpath] = resolve(path); + return accessor->lstat(subpath); + } + std::optional maybeLstat(const CanonPath & path) override { auto [accessor, subpath] = resolve(path); diff --git a/tests/functional/flakes/source-paths.sh b/tests/functional/flakes/source-paths.sh index 10b834bc8..e82d27c81 100644 --- a/tests/functional/flakes/source-paths.sh +++ b/tests/functional/flakes/source-paths.sh @@ -14,6 +14,8 @@ cat > "$repo/flake.nix" < "$repo/foo" +echo 123 > "$repo/foo" expectStderr 1 nix eval "$repo#z" | grepQuiet "error: File 'foo' in the repository \"$repo\" is not tracked by Git." +expectStderr 1 nix eval "$repo#a" | grepQuiet "error: File 'foo' in the repository \"$repo\" is not tracked by Git." git -C "$repo" add "$repo/foo" -[[ $(nix eval --raw "$repo#z") = foo ]] +[[ $(nix eval --raw "$repo#z") = 123 ]] + +expectStderr 1 nix eval "$repo#b" | grepQuiet "error: path '/dir' does not exist in Git repository \"$repo\"" + +mkdir -p $repo/dir +echo 456 > $repo/dir/default.nix + +expectStderr 1 nix eval "$repo#b" | grepQuiet "error: File 'dir' in the repository \"$repo\" is not tracked by Git." + +git -C "$repo" add "$repo/dir/default.nix" + +[[ $(nix eval "$repo#b") = 456 ]] From 4e0346dcc15d7ffd8795e6364e2b81f29412f201 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 1 Apr 2025 20:46:26 +0200 Subject: [PATCH 11/13] Restore 'forbidden in restricted mode' errors --- src/libexpr/eval.cc | 10 ++++++++++ tests/functional/restricted.sh | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 0ad12b9b5..9b9aabf7e 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -3073,6 +3073,11 @@ SourcePath EvalState::findFile(const LookupPath & lookupPath, const std::string_ auto res = (r / CanonPath(suffix)).resolveSymlinks(); if (res.pathExists()) return res; + + // Backward compatibility hack: throw an exception if access + // to this path is not allowed. + if (auto accessor = res.accessor.dynamic_pointer_cast()) + accessor->checkAccess(res.path); } if (hasPrefix(path, "nix/")) @@ -3143,6 +3148,11 @@ std::optional EvalState::resolveLookupPathPath(const LookupPath::Pat if (path.resolveSymlinks().pathExists()) return finish(std::move(path)); else { + // Backward compatibility hack: throw an exception if access + // to this path is not allowed. + if (auto accessor = path.accessor.dynamic_pointer_cast()) + accessor->checkAccess(path.path); + logWarning({ .msg = HintFmt("Nix search path entry '%1%' does not exist, ignoring", value) }); diff --git a/tests/functional/restricted.sh b/tests/functional/restricted.sh index bc42ec891..00ee4ddc8 100755 --- a/tests/functional/restricted.sh +++ b/tests/functional/restricted.sh @@ -23,7 +23,7 @@ nix-instantiate --restrict-eval ./simple.nix -I src1=./simple.nix -I src2=./conf (! nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix') nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix' -I src=../.. -expectStderr 1 nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile ' #| grepQuiet "forbidden in restricted mode" +expectStderr 1 nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile ' | grepQuiet "forbidden in restricted mode" nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile ' -I src=. p=$(nix eval --raw --expr "builtins.fetchurl file://${_NIX_TEST_SOURCE_DIR}/restricted.sh" --impure --restrict-eval --allowed-uris "file://${_NIX_TEST_SOURCE_DIR}") @@ -53,9 +53,9 @@ mkdir -p $TEST_ROOT/tunnel.d $TEST_ROOT/foo2 ln -sfn .. $TEST_ROOT/tunnel.d/tunnel echo foo > $TEST_ROOT/bar -expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readFile " -I $TEST_ROOT/tunnel.d #| grepQuiet "forbidden in restricted mode" +expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readFile " -I $TEST_ROOT/tunnel.d | grepQuiet "forbidden in restricted mode" -expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readDir " -I $TEST_ROOT/tunnel.d #| grepQuiet "forbidden in restricted mode" +expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readDir " -I $TEST_ROOT/tunnel.d | grepQuiet "forbidden in restricted mode" # Reading the parents of allowed paths should show only the ancestors of the allowed paths. [[ $(nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readDir " -I $TEST_ROOT/tunnel.d) == '{ "tunnel.d" = "directory"; }' ]] From 25262931711b64b3e5c1067a66b8f6b15872e61d Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 1 Apr 2025 20:52:27 +0200 Subject: [PATCH 12/13] shellcheck --- tests/functional/flakes/source-paths.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/flakes/source-paths.sh b/tests/functional/flakes/source-paths.sh index e82d27c81..5318806ce 100644 --- a/tests/functional/flakes/source-paths.sh +++ b/tests/functional/flakes/source-paths.sh @@ -47,8 +47,8 @@ git -C "$repo" add "$repo/foo" expectStderr 1 nix eval "$repo#b" | grepQuiet "error: path '/dir' does not exist in Git repository \"$repo\"" -mkdir -p $repo/dir -echo 456 > $repo/dir/default.nix +mkdir -p "$repo/dir" +echo 456 > "$repo/dir/default.nix" expectStderr 1 nix eval "$repo#b" | grepQuiet "error: File 'dir' in the repository \"$repo\" is not tracked by Git." From fb7bcdd5543c7deb06cb2e65edd8ca6c895716ec Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 1 Apr 2025 22:56:14 +0200 Subject: [PATCH 13/13] Make Git error messages more consistent --- src/libfetchers/git.cc | 4 ++-- tests/functional/flakes/source-paths.sh | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index 54c66d151..e182740d6 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -538,7 +538,7 @@ struct GitInputScheme : InputScheme return [repoPath{std::move(repoPath)}](const CanonPath & path) -> RestrictedPathError { if (nix::pathExists(repoPath / path.rel())) return RestrictedPathError( - "File '%1%' in the repository %2% is not tracked by Git.\n" + "Path '%1%' in the repository %2% is not tracked by Git.\n" "\n" "To make it visible to Nix, run:\n" "\n" @@ -546,7 +546,7 @@ struct GitInputScheme : InputScheme path.rel(), repoPath); else - return RestrictedPathError("path '%s' does not exist in Git repository %s", path, repoPath); + return RestrictedPathError("Path '%s' does not exist in Git repository %s.", path.rel(), repoPath); }; } diff --git a/tests/functional/flakes/source-paths.sh b/tests/functional/flakes/source-paths.sh index 5318806ce..3aa3683c2 100644 --- a/tests/functional/flakes/source-paths.sh +++ b/tests/functional/flakes/source-paths.sh @@ -20,7 +20,7 @@ cat > "$repo/flake.nix" < "$repo/foo" -expectStderr 1 nix eval "$repo#z" | grepQuiet "error: File 'foo' in the repository \"$repo\" is not tracked by Git." -expectStderr 1 nix eval "$repo#a" | grepQuiet "error: File 'foo' in the repository \"$repo\" is not tracked by Git." +expectStderr 1 nix eval "$repo#z" | grepQuiet "error: Path 'foo' in the repository \"$repo\" is not tracked by Git." +expectStderr 1 nix eval "$repo#a" | grepQuiet "error: Path 'foo' in the repository \"$repo\" is not tracked by Git." git -C "$repo" add "$repo/foo" [[ $(nix eval --raw "$repo#z") = 123 ]] -expectStderr 1 nix eval "$repo#b" | grepQuiet "error: path '/dir' does not exist in Git repository \"$repo\"" +expectStderr 1 nix eval "$repo#b" | grepQuiet "error: Path 'dir' does not exist in Git repository \"$repo\"." mkdir -p "$repo/dir" echo 456 > "$repo/dir/default.nix" -expectStderr 1 nix eval "$repo#b" | grepQuiet "error: File 'dir' in the repository \"$repo\" is not tracked by Git." +expectStderr 1 nix eval "$repo#b" | grepQuiet "error: Path 'dir' in the repository \"$repo\" is not tracked by Git." git -C "$repo" add "$repo/dir/default.nix"