Skip to content

Commit df93fa8

Browse files
authored
Merge pull request #27 from DeterminateSystems/lazy-trees-v2
Lazy trees v2
2 parents 8713cd6 + f6ad629 commit df93fa8

31 files changed

Lines changed: 438 additions & 158 deletions

.github/workflows/ci.yml

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,30 @@ jobs:
9191
flake_regressions:
9292
if: github.event_name == 'merge_group'
9393
needs: build_x86_64-linux
94-
runs-on: blacksmith-32vcpu-ubuntu-2204
94+
runs-on: namespace-profile-x86-32cpu-64gb
95+
steps:
96+
- name: Checkout nix
97+
uses: actions/checkout@v4
98+
- name: Checkout flake-regressions
99+
uses: actions/checkout@v4
100+
with:
101+
repository: DeterminateSystems/flake-regressions
102+
path: flake-regressions
103+
- name: Checkout flake-regressions-data
104+
uses: actions/checkout@v4
105+
with:
106+
repository: DeterminateSystems/flake-regressions-data
107+
path: flake-regressions/tests
108+
- uses: DeterminateSystems/nix-installer-action@main
109+
with:
110+
determinate: true
111+
- uses: DeterminateSystems/flakehub-cache-action@main
112+
- run: nix build -L --out-link ./new-nix && PATH=$(pwd)/new-nix/bin:$PATH PARALLEL="-P 50%" flake-regressions/eval-all.sh
113+
114+
flake_regressions_lazy:
115+
if: github.event_name == 'merge_group'
116+
needs: build_x86_64-linux
117+
runs-on: namespace-profile-x86-32cpu-64gb
95118
steps:
96119
- name: Checkout nix
97120
uses: actions/checkout@v4
@@ -109,7 +132,7 @@ jobs:
109132
with:
110133
determinate: true
111134
- uses: DeterminateSystems/flakehub-cache-action@main
112-
- run: nix build -L --out-link ./new-nix && PATH=$(pwd)/new-nix/bin:$PATH MAX_FLAKES=50 flake-regressions/eval-all.sh
135+
- run: nix build -L --out-link ./new-nix && PATH=$(pwd)/new-nix/bin:$PATH PARALLEL="-P 50%" NIX_CONFIG="lazy-trees = true" flake-regressions/eval-all.sh
113136

114137
manual:
115138
if: github.event_name != 'merge_group'

src/libcmd/installable-value.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ std::optional<DerivedPathWithInfo> InstallableValue::trySinglePathToDerivedPaths
5757
else if (v.type() == nString) {
5858
return {{
5959
.path = DerivedPath::fromSingle(
60-
state->coerceToSingleDerivedPath(pos, v, errorCtx)),
60+
state->devirtualize(
61+
state->coerceToSingleDerivedPath(pos, v, errorCtx))),
6162
.info = make_ref<ExtraPathInfo>(),
6263
}};
6364
}

src/libexpr/eval.cc

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "nix/util/url.hh"
2121
#include "nix/fetchers/fetch-to-store.hh"
2222
#include "nix/fetchers/tarball.hh"
23+
#include "nix/fetchers/input-cache.hh"
2324

2425
#include "parser-tab.hh"
2526

@@ -266,12 +267,9 @@ EvalState::EvalState(
266267
/nix/store while using a chroot store. */
267268
auto accessor = getFSSourceAccessor();
268269

269-
auto realStoreDir = dirOf(store->toRealPath(StorePath::dummy));
270-
if (settings.pureEval || store->storeDir != realStoreDir) {
271-
accessor = settings.pureEval
272-
? storeFS.cast<SourceAccessor>()
273-
: makeUnionSourceAccessor({accessor, storeFS});
274-
}
270+
accessor = settings.pureEval
271+
? storeFS.cast<SourceAccessor>()
272+
: makeUnionSourceAccessor({accessor, storeFS});
275273

276274
/* Apply access control if needed. */
277275
if (settings.restrictEval || settings.pureEval)
@@ -293,6 +291,7 @@ EvalState::EvalState(
293291
)}
294292
, store(store)
295293
, buildStore(buildStore ? buildStore : store)
294+
, inputCache(fetchers::InputCache::create())
296295
, debugRepl(nullptr)
297296
, debugStop(false)
298297
, trylevel(0)
@@ -949,7 +948,16 @@ void EvalState::mkPos(Value & v, PosIdx p)
949948
auto origin = positions.originOf(p);
950949
if (auto path = std::get_if<SourcePath>(&origin)) {
951950
auto attrs = buildBindings(3);
952-
attrs.alloc(sFile).mkString(path->path.abs());
951+
if (path->accessor == rootFS && store->isInStore(path->path.abs()))
952+
// FIXME: only do this for virtual store paths?
953+
attrs.alloc(sFile).mkString(path->path.abs(),
954+
{
955+
NixStringContextElem::Opaque{
956+
.path = store->toStorePath(path->path.abs()).first
957+
}
958+
});
959+
else
960+
attrs.alloc(sFile).mkString(path->path.abs());
953961
makePositionThunks(*this, p, attrs.alloc(sLine), attrs.alloc(sColumn));
954962
v.mkAttrs(attrs);
955963
} else
@@ -1135,6 +1143,7 @@ void EvalState::resetFileCache()
11351143
{
11361144
fileEvalCache.clear();
11371145
fileParseCache.clear();
1146+
inputCache->clear();
11381147
}
11391148

11401149

@@ -2317,6 +2326,9 @@ BackedStringView EvalState::coerceToString(
23172326
}
23182327

23192328
if (v.type() == nPath) {
2329+
// FIXME: instead of copying the path to the store, we could
2330+
// return a virtual store path that lazily copies the path to
2331+
// the store in devirtualize().
23202332
return
23212333
!canonicalizePath && !copyToStore
23222334
? // FIXME: hack to preserve path literals that end in a
@@ -2406,7 +2418,7 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat
24062418
*store,
24072419
path.resolveSymlinks(SymlinkResolution::Ancestors),
24082420
settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy,
2409-
path.baseName(),
2421+
computeBaseName(path),
24102422
ContentAddressMethod::Raw::NixArchive,
24112423
nullptr,
24122424
repair);
@@ -2461,7 +2473,7 @@ StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, NixStringCon
24612473
auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned();
24622474
if (auto storePath = store->maybeParseStorePath(path))
24632475
return *storePath;
2464-
error<EvalError>("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow();
2476+
error<EvalError>("cannot coerce '%s' to a store path because it is not a subpath of the Nix store", path).withTrace(pos, errorCtx).debugThrow();
24652477
}
24662478

24672479

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ struct EvalSettings : Config
247247
248248
This option can be enabled by setting `NIX_ABORT_ON_WARN=1` in the environment.
249249
)"};
250+
251+
Setting<bool> lazyTrees{this, false, "lazy-trees",
252+
R"(
253+
If set to true, flakes and trees fetched by [`builtins.fetchTree`](@docroot@/language/builtins.md#builtins-fetchTree) are only copied to the Nix store when they're used as a dependency of a derivation. This avoids copying (potentially large) source trees unnecessarily.
254+
)"};
250255
};
251256

252257
/**

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

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ namespace nix {
3333
constexpr size_t maxPrimOpArity = 8;
3434

3535
class Store;
36-
namespace fetchers { struct Settings; }
36+
namespace fetchers {
37+
struct Settings;
38+
struct InputCache;
39+
struct Input;
40+
}
3741
struct EvalSettings;
3842
class EvalState;
3943
class StorePath;
@@ -301,6 +305,8 @@ public:
301305

302306
RootValue vImportedDrvToDerivation = nullptr;
303307

308+
ref<fetchers::InputCache> inputCache;
309+
304310
/**
305311
* Debugger
306312
*/
@@ -445,6 +451,15 @@ public:
445451

446452
void checkURI(const std::string & uri);
447453

454+
/**
455+
* Mount an input on the Nix store.
456+
*/
457+
StorePath mountInput(
458+
fetchers::Input & input,
459+
const fetchers::Input & originalInput,
460+
ref<SourceAccessor> accessor,
461+
bool requireLockable);
462+
448463
/**
449464
* Parse a Nix expression from the specified file.
450465
*/
@@ -554,6 +569,18 @@ public:
554569
std::optional<std::string> tryAttrsToString(const PosIdx pos, Value & v,
555570
NixStringContext & context, bool coerceMore = false, bool copyToStore = true);
556571

572+
StorePath devirtualize(
573+
const StorePath & path,
574+
StringMap * rewrites = nullptr);
575+
576+
SingleDerivedPath devirtualize(
577+
const SingleDerivedPath & path,
578+
StringMap * rewrites = nullptr);
579+
580+
std::string devirtualize(
581+
std::string_view s,
582+
const NixStringContext & context);
583+
557584
/**
558585
* String coercion.
559586
*
@@ -569,6 +596,19 @@ public:
569596

570597
StorePath copyPathToStore(NixStringContext & context, const SourcePath & path);
571598

599+
600+
/**
601+
* Compute the base name for a `SourcePath`. For non-store paths,
602+
* this is just `SourcePath::baseName()`. But for store paths, for
603+
* backwards compatibility, it needs to be `<hash>-source`,
604+
* i.e. as if the path were copied to the Nix store. This results
605+
* in a "double-copied" store path like
606+
* `/nix/store/<hash1>-<hash2>-source`. We don't need to
607+
* materialize /nix/store/<hash2>-source though. Still, this
608+
* requires reading/hashing the path twice.
609+
*/
610+
std::string computeBaseName(const SourcePath & path);
611+
572612
/**
573613
* Path coercion.
574614
*

src/libexpr/include/nix/expr/print-ambiguous.hh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ namespace nix {
1515
* See: https://github.com/NixOS/nix/issues/9730
1616
*/
1717
void printAmbiguous(
18-
Value &v,
19-
const SymbolTable &symbols,
20-
std::ostream &str,
21-
std::set<const void *> *seen,
18+
EvalState & state,
19+
Value & v,
20+
std::ostream & str,
21+
std::set<const void *> * seen,
2222
int depth);
2323

2424
}

src/libexpr/paths.cc

Lines changed: 79 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,81 @@ SourcePath EvalState::storePath(const StorePath & path)
1820
return {rootFS, CanonPath{store->printStorePath(path)}};
1921
}
2022

23+
StorePath EvalState::devirtualize(const StorePath & path, StringMap * rewrites)
24+
{
25+
if (auto mount = storeFS->getMount(CanonPath(store->printStorePath(path)))) {
26+
auto storePath = fetchToStore(
27+
*store, SourcePath{ref(mount)}, settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy, path.name());
28+
assert(storePath.name() == path.name());
29+
if (rewrites)
30+
rewrites->emplace(path.hashPart(), storePath.hashPart());
31+
return storePath;
32+
} else
33+
return path;
34+
}
35+
36+
SingleDerivedPath EvalState::devirtualize(const SingleDerivedPath & path, StringMap * rewrites)
37+
{
38+
if (auto o = std::get_if<SingleDerivedPath::Opaque>(&path.raw()))
39+
return SingleDerivedPath::Opaque{devirtualize(o->path, rewrites)};
40+
else
41+
return path;
42+
}
43+
44+
std::string EvalState::devirtualize(std::string_view s, const NixStringContext & context)
45+
{
46+
StringMap rewrites;
47+
48+
for (auto & c : context)
49+
if (auto o = std::get_if<NixStringContextElem::Opaque>(&c.raw))
50+
devirtualize(o->path, &rewrites);
51+
52+
return rewriteStrings(std::string(s), rewrites);
53+
}
54+
55+
std::string EvalState::computeBaseName(const SourcePath & path)
56+
{
57+
if (path.accessor == rootFS) {
58+
if (auto storePath = store->maybeParseStorePath(path.path.abs())) {
59+
warn(
60+
"Performing inefficient double copy of path '%s' to the store. "
61+
"This can typically be avoided by rewriting an attribute like `src = ./.` "
62+
"to `src = builtins.path { path = ./.; name = \"source\"; }`.",
63+
path);
64+
return std::string(fetchToStore(*store, path, FetchMode::DryRun, storePath->name()).to_string());
65+
}
66+
}
67+
return std::string(path.baseName());
68+
}
69+
70+
StorePath EvalState::mountInput(
71+
fetchers::Input & input, const fetchers::Input & originalInput, ref<SourceAccessor> accessor, bool requireLockable)
72+
{
73+
auto storePath = settings.lazyTrees ? StorePath::random(input.getName())
74+
: fetchToStore(*store, accessor, FetchMode::Copy, input.getName());
75+
76+
allowPath(storePath); // FIXME: should just whitelist the entire virtual store
77+
78+
storeFS->mount(CanonPath(store->printStorePath(storePath)), accessor);
79+
80+
if (requireLockable && !input.isLocked() && !input.getNarHash()) {
81+
auto narHash = accessor->hashPath(CanonPath::root);
82+
input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true));
83+
}
84+
85+
// FIXME: what to do with the NAR hash in lazy mode?
86+
if (!settings.lazyTrees && originalInput.getNarHash()) {
87+
auto expected = originalInput.computeStorePath(*store);
88+
if (storePath != expected)
89+
throw Error(
90+
(unsigned int) 102,
91+
"NAR hash mismatch in input '%s', expected '%s' but got '%s'",
92+
originalInput.to_string(),
93+
store->printStorePath(storePath),
94+
store->printStorePath(expected));
95+
}
96+
97+
return storePath;
98+
}
99+
21100
}

src/libexpr/primops.cc

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "nix/expr/value-to-xml.hh"
1515
#include "nix/expr/primops.hh"
1616
#include "nix/fetchers/fetch-to-store.hh"
17+
#include "nix/util/mounted-source-accessor.hh"
1718

1819
#include <boost/container/small_vector.hpp>
1920
#include <nlohmann/json.hpp>
@@ -75,7 +76,10 @@ StringMap EvalState::realiseContext(const NixStringContext & context, StorePathS
7576
ensureValid(b.drvPath->getBaseStorePath());
7677
},
7778
[&](const NixStringContextElem::Opaque & o) {
78-
ensureValid(o.path);
79+
// We consider virtual store paths valid here. They'll
80+
// be devirtualized if needed elsewhere.
81+
if (!storeFS->getMount(CanonPath(store->printStorePath(o.path))))
82+
ensureValid(o.path);
7983
if (maybePathsOut)
8084
maybePathsOut->emplace(o.path);
8185
},
@@ -1408,6 +1412,8 @@ static void derivationStrictInternal(
14081412
/* Everything in the context of the strings in the derivation
14091413
attributes should be added as dependencies of the resulting
14101414
derivation. */
1415+
StringMap rewrites;
1416+
14111417
for (auto & c : context) {
14121418
std::visit(overloaded {
14131419
/* Since this allows the builder to gain access to every
@@ -1430,11 +1436,13 @@ static void derivationStrictInternal(
14301436
drv.inputDrvs.ensureSlot(*b.drvPath).value.insert(b.output);
14311437
},
14321438
[&](const NixStringContextElem::Opaque & o) {
1433-
drv.inputSrcs.insert(o.path);
1439+
drv.inputSrcs.insert(state.devirtualize(o.path, &rewrites));
14341440
},
14351441
}, c.raw);
14361442
}
14371443

1444+
drv.applyRewrites(rewrites);
1445+
14381446
/* Do we have all required attributes? */
14391447
if (drv.builder == "")
14401448
state.error<EvalError>("required attribute 'builder' missing")
@@ -2500,6 +2508,7 @@ static void addPath(
25002508
{}));
25012509

25022510
if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) {
2511+
// FIXME: make this lazy?
25032512
auto dstPath = fetchToStore(
25042513
*state.store,
25052514
path.resolveSymlinks(),
@@ -2530,7 +2539,7 @@ static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * arg
25302539
"while evaluating the second argument (the path to filter) passed to 'builtins.filterSource'");
25312540
state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource");
25322541

2533-
addPath(state, pos, path.baseName(), path, args[0], ContentAddressMethod::Raw::NixArchive, std::nullopt, v, context);
2542+
addPath(state, pos, state.computeBaseName(path), path, args[0], ContentAddressMethod::Raw::NixArchive, std::nullopt, v, context);
25342543
}
25352544

25362545
static RegisterPrimOp primop_filterSource({

0 commit comments

Comments
 (0)