Skip to content
Merged
5 changes: 3 additions & 2 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,9 @@ EvalState::EvalState(
instance if we're evaluating a file from the physical
/nix/store while using a chroot store, and also for lazy
mounted fetchTree. */
auto accessor = settings.pureEval ? storeFS.cast<SourceAccessor>()
: makeUnionSourceAccessor({getFSSourceAccessor(), storeFS});
auto accessor = settings.pureEval
? storeFS.cast<SourceAccessor>()
: makeUnionSourceAccessor({getFSSourceAccessor(), storeFS}, storeFS.cast<SourceAccessor>());

/* Apply access control if needed. */
if (settings.restrictEval || settings.pureEval)
Expand Down
29 changes: 4 additions & 25 deletions src/libexpr/primops/fetchTree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -427,38 +427,17 @@ static void fetch(
.atPos(pos)
.debugThrow();

// early exit if pinned and already in the store
if (expectedHash && expectedHash->algo == HashAlgorithm::SHA256) {
auto expectedPath = state.store->makeFixedOutputPath(
name,
FixedOutputInfo{
.method = unpack ? FileIngestionMethod::NixArchive : FileIngestionMethod::Flat,
.hash = *expectedHash,
.references = {}});

// Try to get the path from the local store or substituters
try {
state.store->ensurePath(expectedPath);
debug("using substituted/cached path '%s' for '%s'", state.store->printStorePath(expectedPath), *url);
state.allowAndSetStorePathString(expectedPath, v);
return;
} catch (Error & e) {
debug(
"substitution of '%s' failed, will try to download: %s",
state.store->printStorePath(expectedPath),
e.what());
// Fall through to download
}
}

if (unpack) {
auto attrs = fetchers::Attrs{
{"type", "tarball"},
{"url", *url},
{"name", name},
};
if (expectedHash)
if (expectedHash) {
attrs.emplace("narHash", expectedHash->to_string(HashFormat::SRI, true));
// Mark as final to allow the tree to be substituted.
attrs.emplace("__final", Explicit<bool>{true});
}
auto input = fetchers::Input::fromAttrs(state.fetchSettings, std::move(attrs));
auto cachedInput =
state.inputCache->getAccessor(state.fetchSettings, *state.store, input, fetchers::UseRegistries::No);
Expand Down
17 changes: 1 addition & 16 deletions src/libfetchers/fetchers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -299,21 +299,6 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessor(const Settings & settin
}
}

/**
* Helper class that ensures that paths in substituted source trees
* are rendered as `«input»/path` rather than
* `«input»/nix/store/<hash>-source/path`.
*/
struct SubstitutedSourceAccessor : ForwardingSourceAccessor
{
using ForwardingSourceAccessor::ForwardingSourceAccessor;

std::string showPath(const CanonPath & path) override
{
return displayPrefix + path.abs() + displaySuffix;
}
};

std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(const Settings & settings, Store & store) const
{
// FIXME: cache the accessor
Expand All @@ -326,7 +311,7 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(const Settings
storePath = computeStorePath(store);

auto makeStoreAccessor = [&]() -> std::pair<ref<SourceAccessor>, Input> {
auto accessor = make_ref<SubstitutedSourceAccessor>(store.requireStoreObjectAccessor(*storePath));
auto accessor = store.requireStoreObjectAccessor(*storePath);

// FIXME: use the NAR hash for fingerprinting Git trees since it may have a .gitattributes file and we don't
// know if we used `git archive` or libgit2 to fetch it.
Expand Down
2 changes: 1 addition & 1 deletion src/libfetchers/git-utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,7 @@ struct GitSourceAccessor : SourceAccessor
{
auto entry = lookup(state, path);
if (!entry)
throw Error("'%s' does not exist", showPath(path));
throw FileNotFound("path '%s' does not exist", showPath(path));
return entry;
}

Expand Down
2 changes: 1 addition & 1 deletion src/libutil-tests/source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ TEST_F(FSSourceAccessorTest, works)
{
auto accessor = makeFSSourceAccessor(tmpDir / "nonexistent");
EXPECT_FALSE(accessor->maybeLstat(CanonPath::root));
EXPECT_THROW(accessor->readFile(CanonPath::root), SystemError);
EXPECT_THROW(accessor->readFile(CanonPath::root), FileNotFound);
}

{
Expand Down
40 changes: 40 additions & 0 deletions src/libutil-tests/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,44 @@ TEST(getOr, getFromContainer)
ASSERT_EQ(getOr(s, "one", "nope"), expected);
}

/* ----------------------------------------------------------------------------
* markLast
* --------------------------------------------------------------------------*/

TEST(markLast, empty)
{
std::vector<int> v;
std::vector<std::pair<bool, int>> result;
for (auto [last, x] : markLast(v))
result.emplace_back(last, x);
ASSERT_TRUE(result.empty());
}

TEST(markLast, singleElement)
{
std::vector<int> v{42};
std::vector<std::pair<bool, int>> result;
for (auto [last, x] : markLast(v))
result.emplace_back(last, x);
ASSERT_EQ(result, (std::vector<std::pair<bool, int>>{{true, 42}}));
}

TEST(markLast, multipleElements)
{
std::vector<int> v{10, 20, 30};
std::vector<std::pair<bool, int>> result;
for (auto [last, x] : markLast(v))
result.emplace_back(last, x);
ASSERT_EQ(result, (std::vector<std::pair<bool, int>>{{false, 10}, {false, 20}, {true, 30}}));
}

TEST(markLast, mutateThroughReference)
{
std::vector<int> v{1, 2, 3};
for (auto && [last, x] : markLast(v))
if (last)
x = 99;
ASSERT_EQ(v, (std::vector<int>{1, 2, 99}));
}

} // namespace nix
3 changes: 2 additions & 1 deletion src/libutil/include/nix/util/source-accessor.hh
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ ref<SourceAccessor> makeFSSourceAccessor(std::filesystem::path root, bool trackL
* Construct an accessor that presents a "union" view of a vector of
* underlying accessors. Earlier accessors take precedence over later.
*/
ref<SourceAccessor> makeUnionSourceAccessor(std::vector<ref<SourceAccessor>> && accessors);
ref<SourceAccessor>
makeUnionSourceAccessor(std::vector<ref<SourceAccessor>> && accessors, std::shared_ptr<SourceAccessor> displayAccessor);

} // namespace nix
14 changes: 14 additions & 0 deletions src/libutil/include/nix/util/util.hh
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,20 @@ constexpr auto enumerate(R && range)
return std::views::zip(std::views::iota(size_t{0}), std::forward<R>(range));
}

/**
* An iterator adapter that enumerates the elements of a range,
* pairing each element with a boolean indicating whether it is the
* last element.
*/
template<std::ranges::viewable_range R>
requires std::ranges::sized_range<R>
constexpr auto markLast(R && range)
{
auto n = std::ranges::size(range);
return std::views::zip(
std::views::iota(size_t{1}) | std::views::transform([n](size_t i) { return i == n; }), std::forward<R>(range));
}

/**
* C++17 std::visit boilerplate
*/
Expand Down
6 changes: 3 additions & 3 deletions src/libutil/memory-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ void MemorySourceAccessor::readFile(const CanonPath & path, Sink & sink, fun<voi
{
auto * f = open(path, std::nullopt);
if (!f)
throw FileNotFound("file '%s' does not exist", showPath(path));
throw FileNotFound("path '%s' does not exist", showPath(path));
if (auto * r = std::get_if<File::Regular>(&f->raw)) {
sizeCallback(r->contents.size());
StringSource source{r->contents};
Expand Down Expand Up @@ -107,7 +107,7 @@ MemorySourceAccessor::DirEntries MemorySourceAccessor::readDirectory(const Canon
{
auto * f = open(path, std::nullopt);
if (!f)
throw FileNotFound("file '%s' does not exist", showPath(path));
throw FileNotFound("path '%s' does not exist", showPath(path));
if (auto * d = std::get_if<File::Directory>(&f->raw)) {
DirEntries res;
for (auto & [name, file] : d->entries)
Expand All @@ -122,7 +122,7 @@ std::string MemorySourceAccessor::readLink(const CanonPath & path)
{
auto * f = open(path, std::nullopt);
if (!f)
throw FileNotFound("file '%s' does not exist", showPath(path));
throw FileNotFound("path '%s' does not exist", showPath(path));
if (auto * s = std::get_if<File::Symlink>(&f->raw))
return s->target;
else
Expand Down
8 changes: 6 additions & 2 deletions src/libutil/posix-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ void PosixSourceAccessor::readFile(const CanonPath & path, Sink & sink, fun<void
| O_NOFOLLOW | O_CLOEXEC
#endif
));
if (!fd)
throw SysError("opening file '%1%'", ap.string());
if (!fd) {
if (errno == ENOENT)
throw FileNotFound("path '%1%' does not exist", showPath(path));
else
throw SysError("opening file '%1%'", showPath(path));
}

auto size = getFileSize(fd.get());

Expand Down
23 changes: 12 additions & 11 deletions src/libutil/union-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ namespace nix {
struct UnionSourceAccessor : SourceAccessor
{
std::vector<ref<SourceAccessor>> accessors;
std::shared_ptr<SourceAccessor> displayAccessor;

UnionSourceAccessor(std::vector<ref<SourceAccessor>> _accessors)
UnionSourceAccessor(std::vector<ref<SourceAccessor>> _accessors, std::shared_ptr<SourceAccessor> _displayAccessor)
: accessors(std::move(_accessors))
, displayAccessor(std::move(_displayAccessor))
{
displayPrefix.clear();
}

void readFile(const CanonPath & path, Sink & sink, fun<void(uint64_t)> sizeCallback) override
{
for (auto & accessor : accessors) {
auto st = accessor->maybeLstat(path);
if (st) {
for (const auto & [last, accessor] : markLast(accessors))
if (last || accessor->maybeLstat(path)) {
accessor->readFile(path, sink, sizeCallback);
return;
}
}
throw FileNotFound("path '%s' does not exist", showPath(path));
}

Expand Down Expand Up @@ -54,16 +54,16 @@ struct UnionSourceAccessor : SourceAccessor

std::string readLink(const CanonPath & path) override
{
for (auto & accessor : accessors) {
auto st = accessor->maybeLstat(path);
if (st)
for (const auto & [last, accessor] : markLast(accessors))
if (last || accessor->maybeLstat(path))
return accessor->readLink(path);
}
throw FileNotFound("path '%s' does not exist", showPath(path));
}

std::string showPath(const CanonPath & path) override
{
if (displayAccessor)
return displayAccessor->showPath(path);
for (auto & accessor : accessors)
return accessor->showPath(path);
return SourceAccessor::showPath(path);
Expand Down Expand Up @@ -108,9 +108,10 @@ struct UnionSourceAccessor : SourceAccessor
}
};

ref<SourceAccessor> makeUnionSourceAccessor(std::vector<ref<SourceAccessor>> && accessors)
ref<SourceAccessor>
makeUnionSourceAccessor(std::vector<ref<SourceAccessor>> && accessors, std::shared_ptr<SourceAccessor> displayAccessor)
{
return make_ref<UnionSourceAccessor>(std::move(accessors));
return make_ref<UnionSourceAccessor>(std::move(accessors), displayAccessor);
}

} // namespace nix
10 changes: 9 additions & 1 deletion tests/functional/flakes/source-paths.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ git -C "$repo" commit -a -m foo
expectStderr 1 nix eval "git+file://$repo?ref=master#y" | grepQuiet "at «git+file://$repo?rev=.*»/flake.nix:"

expectStderr 1 nix eval "$repo#z" | grepQuiet "error: Path 'foo' does not exist in Git repository \"$repo\"."
expectStderr 1 nix eval "git+file://$repo?ref=master#z" | grepQuiet "error: '«git+file://$repo?rev=.*»/foo' does not exist"
expectStderr 1 nix eval "git+file://$repo?ref=master#z" | grepQuiet "error: path '«git+file://$repo?rev=.*»/foo' does not exist"
expectStderr 1 nix eval "$repo#a" | grepQuiet "error: Path 'foo' does not exist in Git repository \"$repo\"."

echo 123 > "$repo/foo"
Expand All @@ -55,3 +55,11 @@ expectStderr 1 nix eval "$repo#b" | grepQuiet "error: Path 'dir' in the reposito
git -C "$repo" add "$repo/dir/default.nix"

[[ $(nix eval "$repo#b") = 456 ]]

# Check that error messages in impure mode correctly refer to the original path, not a store path.
expectStderr 1 nix eval --expr "builtins.readFile ((builtins.fetchTree { type = \"git\"; url = \"file://$repo\"; }) + \"/README.md\")" --impure | grepQuiet "Path 'README.md' does not exist in Git repository \"$repo\"."

# This should still be the case if the input is in the store.
narHash=$(nix flake prefetch --json "$repo" | jq -r .locked.narHash)
expectStderr 1 nix eval --expr "builtins.readFile ((builtins.fetchTree { type = \"git\"; url = \"file://$repo\"; narHash = \"$narHash\"; }) + \"/README.md\")" --impure | grepQuiet "path '.*git+file://$repo.*/README.md' does not exist"
expectStderr 1 nix eval --expr "builtins.readFile ((builtins.fetchTree { type = \"git\"; url = \"file://$repo\"; narHash = \"$narHash\"; }) + \"/README.md\")" | grepQuiet "path '.*git+file://$repo.*/README.md' does not exist"
Loading