Skip to content

Commit c47b105

Browse files
committed
Lazy trees v2
This adds a setting 'lazy-trees' that causes flake inputs to be "mounted" as virtual filesystems on top of /nix/store as random "virtual" store paths. Only when the store path is actually used as a dependency of a store derivation do we materialize ("devirtualize") the input by copying it to its content-addressed location in the store. String contexts determine when devirtualization happens. One wrinkle is that there are cases where we had store paths without proper contexts, in particular when the user does `toString <path>` (where <path> is a source tree in the Nix store) and passes the result to a derivation. This usage was always broken, since it can result in derivations that lack correct references. But to ensure that we don't change evaluation results, we introduce a new type of context that results in devirtualization but not in store references. We also now print a warning about this.
1 parent e5e5c81 commit c47b105

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+533
-133
lines changed

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-test-support/include/nix/expr/tests/value/context.hh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ struct Arbitrary<NixStringContextElem::DrvDeep> {
2323
static Gen<NixStringContextElem::DrvDeep> arbitrary();
2424
};
2525

26+
template<>
27+
struct Arbitrary<NixStringContextElem::Path> {
28+
static Gen<NixStringContextElem::Path> arbitrary();
29+
};
30+
2631
template<>
2732
struct Arbitrary<NixStringContextElem> {
2833
static Gen<NixStringContextElem> arbitrary();

src/libexpr-test-support/tests/value/context.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ Gen<NixStringContextElem::DrvDeep> Arbitrary<NixStringContextElem::DrvDeep>::arb
1515
});
1616
}
1717

18+
Gen<NixStringContextElem::Path> Arbitrary<NixStringContextElem::Path>::arbitrary()
19+
{
20+
return gen::map(gen::arbitrary<StorePath>(), [](StorePath storePath) {
21+
return NixStringContextElem::Path{
22+
.storePath = storePath,
23+
};
24+
});
25+
}
26+
1827
Gen<NixStringContextElem> Arbitrary<NixStringContextElem>::arbitrary()
1928
{
2029
return gen::mapcat(
@@ -30,6 +39,9 @@ Gen<NixStringContextElem> Arbitrary<NixStringContextElem>::arbitrary()
3039
case 2:
3140
return gen::map(
3241
gen::arbitrary<NixStringContextElem::Built>(), [](NixStringContextElem a) { return a; });
42+
case 3:
43+
return gen::map(
44+
gen::arbitrary<NixStringContextElem::Path>(), [](NixStringContextElem a) { return a; });
3345
default:
3446
assert(false);
3547
}

src/libexpr/eval-cache.cc

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -618,18 +618,21 @@ string_t AttrCursor::getStringWithContext()
618618
if (auto s = std::get_if<string_t>(&cachedValue->second)) {
619619
bool valid = true;
620620
for (auto & c : s->second) {
621-
const StorePath & path = std::visit(overloaded {
622-
[&](const NixStringContextElem::DrvDeep & d) -> const StorePath & {
623-
return d.drvPath;
621+
const StorePath * path = std::visit(overloaded {
622+
[&](const NixStringContextElem::DrvDeep & d) -> const StorePath * {
623+
return &d.drvPath;
624624
},
625-
[&](const NixStringContextElem::Built & b) -> const StorePath & {
626-
return b.drvPath->getBaseStorePath();
625+
[&](const NixStringContextElem::Built & b) -> const StorePath * {
626+
return &b.drvPath->getBaseStorePath();
627627
},
628-
[&](const NixStringContextElem::Opaque & o) -> const StorePath & {
629-
return o.path;
628+
[&](const NixStringContextElem::Opaque & o) -> const StorePath * {
629+
return &o.path;
630+
},
631+
[&](const NixStringContextElem::Path & p) -> const StorePath * {
632+
return nullptr;
630633
},
631634
}, c.raw);
632-
if (!root->state.store->isValidPath(path)) {
635+
if (!path || !root->state.store->isValidPath(*path)) {
633636
valid = false;
634637
break;
635638
}

src/libexpr/eval.cc

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "nix/expr/print.hh"
1616
#include "nix/fetchers/filtering-source-accessor.hh"
1717
#include "nix/util/memory-source-accessor.hh"
18+
#include "nix/util/mounted-source-accessor.hh"
1819
#include "nix/expr/gc-small-vector.hh"
1920
#include "nix/util/url.hh"
2021
#include "nix/fetchers/fetch-to-store.hh"
@@ -269,7 +270,7 @@ EvalState::EvalState(
269270
exception, and make union source accessor
270271
catch it, so we don't need to do this hack.
271272
*/
272-
{CanonPath(store->storeDir), store->getFSAccessor(settings.pureEval)},
273+
{CanonPath(store->storeDir), makeFSSourceAccessor(dirOf(store->toRealPath(StorePath::dummy)))}
273274
}))
274275
, rootFS(
275276
({
@@ -284,12 +285,9 @@ EvalState::EvalState(
284285
/nix/store while using a chroot store. */
285286
auto accessor = getFSSourceAccessor();
286287

287-
auto realStoreDir = dirOf(store->toRealPath(StorePath::dummy));
288-
if (settings.pureEval || store->storeDir != realStoreDir) {
289-
accessor = settings.pureEval
290-
? storeFS
291-
: makeUnionSourceAccessor({accessor, storeFS});
292-
}
288+
accessor = settings.pureEval
289+
? storeFS.cast<SourceAccessor>()
290+
: makeUnionSourceAccessor({accessor, storeFS});
293291

294292
/* Apply access control if needed. */
295293
if (settings.restrictEval || settings.pureEval)
@@ -968,7 +966,16 @@ void EvalState::mkPos(Value & v, PosIdx p)
968966
auto origin = positions.originOf(p);
969967
if (auto path = std::get_if<SourcePath>(&origin)) {
970968
auto attrs = buildBindings(3);
971-
attrs.alloc(sFile).mkString(path->path.abs());
969+
if (path->accessor == rootFS && store->isInStore(path->path.abs()))
970+
// FIXME: only do this for virtual store paths?
971+
attrs.alloc(sFile).mkString(path->path.abs(),
972+
{
973+
NixStringContextElem::Path{
974+
.storePath = store->toStorePath(path->path.abs()).first
975+
}
976+
});
977+
else
978+
attrs.alloc(sFile).mkString(path->path.abs());
972979
makePositionThunks(*this, p, attrs.alloc(sLine), attrs.alloc(sColumn));
973980
v.mkAttrs(attrs);
974981
} else
@@ -2089,7 +2096,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
20892096
else if (firstType == nFloat)
20902097
v.mkFloat(nf);
20912098
else if (firstType == nPath) {
2092-
if (!context.empty())
2099+
if (hasContext(context))
20932100
state.error<EvalError>("a string that refers to a store path cannot be appended to a path").atPos(pos).withFrame(env, *this).debugThrow();
20942101
v.mkPath(state.rootPath(CanonPath(str())));
20952102
} else
@@ -2288,7 +2295,10 @@ std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos, std::s
22882295
{
22892296
auto s = forceString(v, pos, errorCtx);
22902297
if (v.context()) {
2291-
error<EvalError>("the string '%1%' is not allowed to refer to a store path (such as '%2%')", v.string_view(), v.context()[0]).withTrace(pos, errorCtx).debugThrow();
2298+
NixStringContext context;
2299+
copyContext(v, context);
2300+
if (hasContext(context))
2301+
error<EvalError>("the string '%1%' is not allowed to refer to a store path (such as '%2%')", v.string_view(), v.context()[0]).withTrace(pos, errorCtx).debugThrow();
22922302
}
22932303
return s;
22942304
}
@@ -2337,14 +2347,26 @@ BackedStringView EvalState::coerceToString(
23372347
}
23382348

23392349
if (v.type() == nPath) {
2350+
// FIXME: instead of copying the path to the store, we could
2351+
// return a virtual store path that lazily copies the path to
2352+
// the store in devirtualize().
23402353
return
23412354
!canonicalizePath && !copyToStore
23422355
? // FIXME: hack to preserve path literals that end in a
23432356
// slash, as in /foo/${x}.
23442357
v.payload.path.path
23452358
: copyToStore
23462359
? store->printStorePath(copyPathToStore(context, v.path()))
2347-
: std::string(v.path().path.abs());
2360+
: ({
2361+
auto path = v.path();
2362+
if (path.accessor == rootFS && store->isInStore(path.path.abs())) {
2363+
context.insert(
2364+
NixStringContextElem::Path{
2365+
.storePath = store->toStorePath(path.path.abs()).first
2366+
});
2367+
}
2368+
std::string(path.path.abs());
2369+
});
23482370
}
23492371

23502372
if (v.type() == nAttrs) {
@@ -2426,7 +2448,7 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat
24262448
*store,
24272449
path.resolveSymlinks(SymlinkResolution::Ancestors),
24282450
settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy,
2429-
path.baseName(),
2451+
computeBaseName(path),
24302452
ContentAddressMethod::Raw::NixArchive,
24312453
nullptr,
24322454
repair);
@@ -2481,7 +2503,7 @@ StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, NixStringCon
24812503
auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned();
24822504
if (auto storePath = store->maybeParseStorePath(path))
24832505
return *storePath;
2484-
error<EvalError>("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow();
2506+
error<EvalError>("cannot coerce '%s' to a store path because it is not a subpath of the Nix store", path).withTrace(pos, errorCtx).debugThrow();
24852507
}
24862508

24872509

@@ -2507,6 +2529,11 @@ std::pair<SingleDerivedPath, std::string_view> EvalState::coerceToSingleDerivedP
25072529
[&](NixStringContextElem::Built && b) -> SingleDerivedPath {
25082530
return std::move(b);
25092531
},
2532+
[&](NixStringContextElem::Path && p) -> SingleDerivedPath {
2533+
error<EvalError>(
2534+
"string '%s' has no context",
2535+
s).withTrace(pos, errorCtx).debugThrow();
2536+
},
25102537
}, ((NixStringContextElem &&) *context.begin()).raw);
25112538
return {
25122539
std::move(derivedPath),
@@ -3090,6 +3117,11 @@ SourcePath EvalState::findFile(const LookupPath & lookupPath, const std::string_
30903117

30913118
auto res = (r / CanonPath(suffix)).resolveSymlinks();
30923119
if (res.pathExists()) return res;
3120+
3121+
// Backward compatibility hack: throw an exception if access
3122+
// to this path is not allowed.
3123+
if (auto accessor = res.accessor.dynamic_pointer_cast<FilteringSourceAccessor>())
3124+
accessor->checkAccess(res.path);
30933125
}
30943126

30953127
if (hasPrefix(path, "nix/"))
@@ -3160,6 +3192,11 @@ std::optional<SourcePath> EvalState::resolveLookupPathPath(const LookupPath::Pat
31603192
if (path.resolveSymlinks().pathExists())
31613193
return finish(std::move(path));
31623194
else {
3195+
// Backward compatibility hack: throw an exception if access
3196+
// to this path is not allowed.
3197+
if (auto accessor = path.accessor.dynamic_pointer_cast<FilteringSourceAccessor>())
3198+
accessor->checkAccess(path.path);
3199+
31633200
logWarning({
31643201
.msg = HintFmt("Nix search path entry '%1%' does not exist, ignoring", value)
31653202
});

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: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ class Store;
3636
namespace fetchers {
3737
struct Settings;
3838
struct InputCache;
39+
struct Input;
3940
}
4041
struct EvalSettings;
4142
class EvalState;
4243
class StorePath;
4344
struct SingleDerivedPath;
4445
enum RepairFlag : bool;
4546
struct MemorySourceAccessor;
47+
struct MountedSourceAccessor;
4648
namespace eval_cache {
4749
class EvalCache;
4850
}
@@ -271,7 +273,7 @@ public:
271273
/**
272274
* The accessor corresponding to `store`.
273275
*/
274-
const ref<SourceAccessor> storeFS;
276+
const ref<MountedSourceAccessor> storeFS;
275277

276278
/**
277279
* The accessor for the root filesystem.
@@ -449,6 +451,15 @@ public:
449451

450452
void checkURI(const std::string & uri);
451453

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+
452463
/**
453464
* Parse a Nix expression from the specified file.
454465
*/
@@ -558,6 +569,18 @@ public:
558569
std::optional<std::string> tryAttrsToString(const PosIdx pos, Value & v,
559570
NixStringContext & context, bool coerceMore = false, bool copyToStore = true);
560571

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+
561584
/**
562585
* String coercion.
563586
*
@@ -573,6 +596,19 @@ public:
573596

574597
StorePath copyPathToStore(NixStringContext & context, const SourcePath & path);
575598

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+
576612
/**
577613
* Path coercion.
578614
*

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/include/nix/expr/value/context.hh

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,35 @@ struct NixStringContextElem {
5454
*/
5555
using Built = SingleDerivedPath::Built;
5656

57+
/**
58+
* A store path that will not result in a store reference when
59+
* used in a derivation or toFile.
60+
*
61+
* When you apply `builtins.toString` to a path value representing
62+
* a path in the Nix store (as is the case with flake inputs),
63+
* historically you got a string without context
64+
* (e.g. `/nix/store/...-source`). This is broken, since it allows
65+
* you to pass a store path to a derivation/toFile without a
66+
* proper store reference. This is especially a problem with lazy
67+
* trees, since the store path is a virtual path that doesn't
68+
* exist.
69+
*
70+
* For backwards compatibility, and to warn users about this
71+
* unsafe use of `toString`, we keep track of such strings as a
72+
* special type of context.
73+
*/
74+
struct Path
75+
{
76+
StorePath storePath;
77+
78+
GENERATE_CMP(Path, me->storePath);
79+
};
80+
5781
using Raw = std::variant<
5882
Opaque,
5983
DrvDeep,
60-
Built
84+
Built,
85+
Path
6186
>;
6287

6388
Raw raw;
@@ -82,4 +107,10 @@ struct NixStringContextElem {
82107

83108
typedef std::set<NixStringContextElem> NixStringContext;
84109

110+
/**
111+
* Returns false if `context` has no elements other than
112+
* `NixStringContextElem::Path`.
113+
*/
114+
bool hasContext(const NixStringContext & context);
115+
85116
}

0 commit comments

Comments
 (0)