Skip to content

Commit 1e70955

Browse files
authored
Merge pull request #14050 from NixOS/fix-fetch-to-store-caching
Fix fetchToStore caching
2 parents 35b3557 + 6721ef5 commit 1e70955

File tree

21 files changed

+189
-93
lines changed

21 files changed

+189
-93
lines changed

src/libexpr/eval.cc

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -236,24 +236,17 @@ EvalState::EvalState(
236236
{CanonPath(store->storeDir), store->getFSAccessor(settings.pureEval)},
237237
}))
238238
, rootFS([&] {
239-
auto accessor = [&]() -> decltype(rootFS) {
240-
/* In pure eval mode, we provide a filesystem that only
241-
contains the Nix store. */
242-
if (settings.pureEval)
243-
return storeFS;
244-
245-
/* If we have a chroot store and pure eval is not enabled,
246-
use a union accessor to make the chroot store available
247-
at its logical location while still having the underlying
248-
directory available. This is necessary for instance if
249-
we're evaluating a file from the physical /nix/store
250-
while using a chroot store. */
251-
auto realStoreDir = dirOf(store->toRealPath(StorePath::dummy));
252-
if (store->storeDir != realStoreDir)
253-
return makeUnionSourceAccessor({getFSSourceAccessor(), storeFS});
254-
255-
return getFSSourceAccessor();
256-
}();
239+
/* In pure eval mode, we provide a filesystem that only
240+
contains the Nix store.
241+
242+
Otherwise, use a union accessor to make the augmented store
243+
available at its logical location while still having the
244+
underlying directory available. This is necessary for
245+
instance if we're evaluating a file from the physical
246+
/nix/store while using a chroot store, and also for lazy
247+
mounted fetchTree. */
248+
auto accessor = settings.pureEval ? storeFS.cast<SourceAccessor>()
249+
: makeUnionSourceAccessor({getFSSourceAccessor(), storeFS});
257250

258251
/* Apply access control if needed. */
259252
if (settings.restrictEval || settings.pureEval)
@@ -3133,6 +3126,11 @@ SourcePath EvalState::findFile(const LookupPath & lookupPath, const std::string_
31333126
auto res = (r / CanonPath(suffix)).resolveSymlinks();
31343127
if (res.pathExists())
31353128
return res;
3129+
3130+
// Backward compatibility hack: throw an exception if access
3131+
// to this path is not allowed.
3132+
if (auto accessor = res.accessor.dynamic_pointer_cast<FilteringSourceAccessor>())
3133+
accessor->checkAccess(res.path);
31363134
}
31373135

31383136
if (hasPrefix(path, "nix/"))
@@ -3199,6 +3197,11 @@ std::optional<SourcePath> EvalState::resolveLookupPathPath(const LookupPath::Pat
31993197
if (path.resolveSymlinks().pathExists())
32003198
return finish(std::move(path));
32013199
else {
3200+
// Backward compatibility hack: throw an exception if access
3201+
// to this path is not allowed.
3202+
if (auto accessor = path.accessor.dynamic_pointer_cast<FilteringSourceAccessor>())
3203+
accessor->checkAccess(path.path);
3204+
32023205
logWarning({.msg = HintFmt("Nix search path entry '%1%' does not exist, ignoring", value)});
32033206
}
32043207
}

src/libexpr/include/nix/expr/eval.hh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Store;
4242
namespace fetchers {
4343
struct Settings;
4444
struct InputCache;
45+
struct Input;
4546
} // namespace fetchers
4647
struct EvalSettings;
4748
class EvalState;
@@ -575,6 +576,11 @@ public:
575576

576577
void checkURI(const std::string & uri);
577578

579+
/**
580+
* Mount an input on the Nix store.
581+
*/
582+
StorePath mountInput(fetchers::Input & input, const fetchers::Input & originalInput, ref<SourceAccessor> accessor);
583+
578584
/**
579585
* Parse a Nix expression from the specified file.
580586
*/

src/libexpr/paths.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "nix/store/store-api.hh"
22
#include "nix/expr/eval.hh"
3+
#include "nix/util/mounted-source-accessor.hh"
4+
#include "nix/fetchers/fetch-to-store.hh"
35

46
namespace nix {
57

@@ -18,4 +20,27 @@ SourcePath EvalState::storePath(const StorePath & path)
1820
return {rootFS, CanonPath{store->printStorePath(path)}};
1921
}
2022

23+
StorePath
24+
EvalState::mountInput(fetchers::Input & input, const fetchers::Input & originalInput, ref<SourceAccessor> accessor)
25+
{
26+
auto storePath = fetchToStore(fetchSettings, *store, accessor, FetchMode::Copy, input.getName());
27+
28+
allowPath(storePath); // FIXME: should just whitelist the entire virtual store
29+
30+
storeFS->mount(CanonPath(store->printStorePath(storePath)), accessor);
31+
32+
auto narHash = store->queryPathInfo(storePath)->narHash;
33+
input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true));
34+
35+
if (originalInput.getNarHash() && narHash != *originalInput.getNarHash())
36+
throw Error(
37+
(unsigned int) 102,
38+
"NAR hash mismatch in input '%s', expected '%s' but got '%s'",
39+
originalInput.to_string(),
40+
narHash.to_string(HashFormat::SRI, true),
41+
originalInput.getNarHash()->to_string(HashFormat::SRI, true));
42+
43+
return storePath;
44+
}
45+
2146
} // namespace nix

src/libexpr/primops/fetchTree.cc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "nix/util/url.hh"
1111
#include "nix/expr/value-to-json.hh"
1212
#include "nix/fetchers/fetch-to-store.hh"
13+
#include "nix/fetchers/input-cache.hh"
1314

1415
#include <nlohmann/json.hpp>
1516

@@ -218,11 +219,11 @@ static void fetchTree(
218219
throw Error("input '%s' is not allowed to use the '__final' attribute", input.to_string());
219220
}
220221

221-
auto [storePath, input2] = input.fetchToStore(state.store);
222+
auto cachedInput = state.inputCache->getAccessor(state.store, input, fetchers::UseRegistries::No);
222223

223-
state.allowPath(storePath);
224+
auto storePath = state.mountInput(cachedInput.lockedInput, input, cachedInput.accessor);
224225

225-
emitTreeAttrs(state, storePath, input2, v, params.emptyRevFallback, false);
226+
emitTreeAttrs(state, storePath, cachedInput.lockedInput, v, params.emptyRevFallback, false);
226227
}
227228

228229
static void prim_fetchTree(EvalState & state, const PosIdx pos, Value ** args, Value & v)

src/libfetchers/fetch-to-store.cc

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "nix/fetchers/fetch-to-store.hh"
22
#include "nix/fetchers/fetchers.hh"
33
#include "nix/fetchers/fetch-settings.hh"
4+
#include "nix/util/environment-variables.hh"
45

56
namespace nix {
67

@@ -27,14 +28,22 @@ StorePath fetchToStore(
2728

2829
std::optional<fetchers::Cache::Key> cacheKey;
2930

30-
if (!filter && path.accessor->fingerprint) {
31-
cacheKey = makeFetchToStoreCacheKey(std::string{name}, *path.accessor->fingerprint, method, path.path.abs());
31+
auto [subpath, fingerprint] = filter ? std::pair<CanonPath, std::optional<std::string>>{path.path, std::nullopt}
32+
: path.accessor->getFingerprint(path.path);
33+
34+
if (fingerprint) {
35+
cacheKey = makeFetchToStoreCacheKey(std::string{name}, *fingerprint, method, subpath.abs());
3236
if (auto res = settings.getCache()->lookupStorePath(*cacheKey, store)) {
3337
debug("store path cache hit for '%s'", path);
3438
return res->storePath;
3539
}
36-
} else
40+
} else {
41+
static auto barf = getEnv("_NIX_TEST_BARF_ON_UNCACHEABLE").value_or("") == "1";
42+
if (barf && !filter)
43+
throw Error("source path '%s' is uncacheable (filter=%d)", path, (bool) filter);
44+
// FIXME: could still provide in-memory caching keyed on `SourcePath`.
3745
debug("source path '%s' is uncacheable", path);
46+
}
3847

3948
Activity act(
4049
*logger,

src/libfetchers/fetchers.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,10 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(ref<Store> sto
356356

357357
auto [accessor, result] = scheme->getAccessor(store, *this);
358358

359-
assert(!accessor->fingerprint);
360-
accessor->fingerprint = result.getFingerprint(store);
359+
if (!accessor->fingerprint)
360+
accessor->fingerprint = result.getFingerprint(store);
361+
else
362+
result.cachedFingerprint = accessor->fingerprint;
361363

362364
return {accessor, std::move(result)};
363365
}

src/libfetchers/filtering-source-accessor.cc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,26 @@ std::string FilteringSourceAccessor::readFile(const CanonPath & path)
1616
return next->readFile(prefix / path);
1717
}
1818

19+
void FilteringSourceAccessor::readFile(const CanonPath & path, Sink & sink, std::function<void(uint64_t)> sizeCallback)
20+
{
21+
checkAccess(path);
22+
return next->readFile(prefix / path, sink, sizeCallback);
23+
}
24+
1925
bool FilteringSourceAccessor::pathExists(const CanonPath & path)
2026
{
2127
return isAllowed(path) && next->pathExists(prefix / path);
2228
}
2329

2430
std::optional<SourceAccessor::Stat> FilteringSourceAccessor::maybeLstat(const CanonPath & path)
31+
{
32+
return isAllowed(path) ? next->maybeLstat(prefix / path) : std::nullopt;
33+
}
34+
35+
SourceAccessor::Stat FilteringSourceAccessor::lstat(const CanonPath & path)
2536
{
2637
checkAccess(path);
27-
return next->maybeLstat(prefix / path);
38+
return next->lstat(prefix / path);
2839
}
2940

3041
SourceAccessor::DirEntries FilteringSourceAccessor::readDirectory(const CanonPath & path)
@@ -49,6 +60,13 @@ std::string FilteringSourceAccessor::showPath(const CanonPath & path)
4960
return displayPrefix + next->showPath(prefix / path) + displaySuffix;
5061
}
5162

63+
std::pair<CanonPath, std::optional<std::string>> FilteringSourceAccessor::getFingerprint(const CanonPath & path)
64+
{
65+
if (fingerprint)
66+
return {path, fingerprint};
67+
return next->getFingerprint(prefix / path);
68+
}
69+
5270
void FilteringSourceAccessor::checkAccess(const CanonPath & path)
5371
{
5472
if (!isAllowed(path))

src/libfetchers/git.cc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -893,8 +893,7 @@ struct GitInputScheme : InputScheme
893893
return makeFingerprint(*rev);
894894
else {
895895
auto repoInfo = getRepoInfo(input);
896-
if (auto repoPath = repoInfo.getPath();
897-
repoPath && repoInfo.workdirInfo.headRev && repoInfo.workdirInfo.submodules.empty()) {
896+
if (auto repoPath = repoInfo.getPath(); repoPath && repoInfo.workdirInfo.submodules.empty()) {
898897
/* Calculate a fingerprint that takes into account the
899898
deleted and modified/added files. */
900899
HashSink hashSink{HashAlgorithm::SHA512};
@@ -907,7 +906,7 @@ struct GitInputScheme : InputScheme
907906
writeString("deleted:", hashSink);
908907
writeString(file.abs(), hashSink);
909908
}
910-
return makeFingerprint(*repoInfo.workdirInfo.headRev)
909+
return makeFingerprint(repoInfo.workdirInfo.headRev.value_or(nullRev))
911910
+ ";d=" + hashSink.finish().hash.to_string(HashFormat::Base16, false);
912911
}
913912
return std::nullopt;

src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@ struct FilteringSourceAccessor : SourceAccessor
3636

3737
std::string readFile(const CanonPath & path) override;
3838

39+
void readFile(const CanonPath & path, Sink & sink, std::function<void(uint64_t)> sizeCallback) override;
40+
3941
bool pathExists(const CanonPath & path) override;
4042

43+
Stat lstat(const CanonPath & path) override;
44+
4145
std::optional<Stat> maybeLstat(const CanonPath & path) override;
4246

4347
DirEntries readDirectory(const CanonPath & path) override;
@@ -46,6 +50,8 @@ struct FilteringSourceAccessor : SourceAccessor
4650

4751
std::string showPath(const CanonPath & path) override;
4852

53+
std::pair<CanonPath, std::optional<std::string>> getFingerprint(const CanonPath & path) override;
54+
4955
/**
5056
* Call `makeNotAllowedError` to throw a `RestrictedPathError`
5157
* exception if `isAllowed()` returns `false` for `path`.

src/libfetchers/path.cc

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,6 @@ struct PathInputScheme : InputScheme
123123

124124
auto absPath = getAbsPath(input);
125125

126-
Activity act(*logger, lvlTalkative, actUnknown, fmt("copying %s to the store", absPath));
127-
128126
// FIXME: check whether access to 'path' is allowed.
129127
auto storePath = store->maybeParseStorePath(absPath.string());
130128

@@ -133,43 +131,33 @@ struct PathInputScheme : InputScheme
133131

134132
time_t mtime = 0;
135133
if (!storePath || storePath->name() != "source" || !store->isValidPath(*storePath)) {
134+
Activity act(*logger, lvlTalkative, actUnknown, fmt("copying %s to the store", absPath));
136135
// FIXME: try to substitute storePath.
137136
auto src = sinkToSource(
138137
[&](Sink & sink) { mtime = dumpPathAndGetMtime(absPath.string(), sink, defaultPathFilter); });
139138
storePath = store->addToStoreFromDump(*src, "source");
140139
}
141140

142-
// To avoid copying the path again to the /nix/store, we need to add a cache entry.
143-
ContentAddressMethod method = ContentAddressMethod::Raw::NixArchive;
144-
auto fp = getFingerprint(store, input);
145-
if (fp) {
146-
auto cacheKey = makeFetchToStoreCacheKey(input.getName(), *fp, method, "/");
147-
input.settings->getCache()->upsert(cacheKey, *store, {}, *storePath);
148-
}
141+
auto accessor = ref{store->getFSAccessor(*storePath)};
142+
143+
// To prevent `fetchToStore()` copying the path again to Nix
144+
// store, pre-create an entry in the fetcher cache.
145+
auto info = store->queryPathInfo(*storePath);
146+
accessor->fingerprint =
147+
fmt("path:%s", store->queryPathInfo(*storePath)->narHash.to_string(HashFormat::SRI, true));
148+
input.settings->getCache()->upsert(
149+
makeFetchToStoreCacheKey(
150+
input.getName(), *accessor->fingerprint, ContentAddressMethod::Raw::NixArchive, "/"),
151+
*store,
152+
{},
153+
*storePath);
149154

150155
/* Trust the lastModified value supplied by the user, if
151156
any. It's not a "secure" attribute so we don't care. */
152157
if (!input.getLastModified())
153158
input.attrs.insert_or_assign("lastModified", uint64_t(mtime));
154159

155-
return {ref{store->getFSAccessor(*storePath)}, std::move(input)};
156-
}
157-
158-
std::optional<std::string> getFingerprint(ref<Store> store, const Input & input) const override
159-
{
160-
if (isRelative(input))
161-
return std::nullopt;
162-
163-
/* If this path is in the Nix store, use the hash of the
164-
store object and the subpath. */
165-
auto path = getAbsPath(input);
166-
try {
167-
auto [storePath, subPath] = store->toStorePath(path.string());
168-
auto info = store->queryPathInfo(storePath);
169-
return fmt("path:%s:%s", info->narHash.to_string(HashFormat::Base16, false), subPath);
170-
} catch (Error &) {
171-
return std::nullopt;
172-
}
160+
return {accessor, std::move(input)};
173161
}
174162

175163
std::optional<ExperimentalFeature> experimentalFeature() const override

0 commit comments

Comments
 (0)