1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-06-28 01:11:15 +02:00

Lazily copy trees to the store

We now mount lazy accessors on top of /nix/store without materializing
them, and only materialize them to the real store if needed (e.g. in
the `derivation` primop).
This commit is contained in:
Eelco Dolstra 2025-04-08 23:32:52 +02:00
parent c891554999
commit febd28db87
14 changed files with 122 additions and 50 deletions

View file

@ -57,7 +57,8 @@ std::optional<DerivedPathWithInfo> InstallableValue::trySinglePathToDerivedPaths
else if (v.type() == nString) { else if (v.type() == nString) {
return {{ return {{
.path = DerivedPath::fromSingle( .path = DerivedPath::fromSingle(
state->coerceToSingleDerivedPath(pos, v, errorCtx)), state->devirtualize(
state->coerceToSingleDerivedPath(pos, v, errorCtx))),
.info = make_ref<ExtraPathInfo>(), .info = make_ref<ExtraPathInfo>(),
}}; }};
} }

View file

@ -267,11 +267,9 @@ EvalState::EvalState(
auto accessor = getFSSourceAccessor(); auto accessor = getFSSourceAccessor();
auto realStoreDir = dirOf(store->toRealPath(StorePath::dummy)); auto realStoreDir = dirOf(store->toRealPath(StorePath::dummy));
if (settings.pureEval || store->storeDir != realStoreDir) { accessor = settings.pureEval
accessor = settings.pureEval ? storeFS.cast<SourceAccessor>()
? storeFS.cast<SourceAccessor>() : makeUnionSourceAccessor({accessor, storeFS});
: makeUnionSourceAccessor({accessor, storeFS});
}
/* Apply access control if needed. */ /* Apply access control if needed. */
if (settings.restrictEval || settings.pureEval) if (settings.restrictEval || settings.pureEval)

View file

@ -554,6 +554,18 @@ public:
std::optional<std::string> tryAttrsToString(const PosIdx pos, Value & v, std::optional<std::string> tryAttrsToString(const PosIdx pos, Value & v,
NixStringContext & context, bool coerceMore = false, bool copyToStore = true); NixStringContext & context, bool coerceMore = false, bool copyToStore = true);
StorePath devirtualize(
const StorePath & path,
StringMap * rewrites = nullptr);
SingleDerivedPath devirtualize(
const SingleDerivedPath & path,
StringMap * rewrites = nullptr);
std::string devirtualize(
std::string_view s,
const NixStringContext & context);
/** /**
* String coercion. * String coercion.
* *

View file

@ -1,5 +1,7 @@
#include "nix/store/store-api.hh" #include "nix/store/store-api.hh"
#include "nix/expr/eval.hh" #include "nix/expr/eval.hh"
#include "nix/util/mounted-source-accessor.hh"
#include "nix/fetchers/fetch-to-store.hh"
namespace nix { namespace nix {
@ -18,4 +20,36 @@ SourcePath EvalState::storePath(const StorePath & path)
return {rootFS, CanonPath{store->printStorePath(path)}}; return {rootFS, CanonPath{store->printStorePath(path)}};
} }
StorePath EvalState::devirtualize(const StorePath & path, StringMap * rewrites)
{
if (auto mount = storeFS->getMount(CanonPath(store->printStorePath(path)))) {
auto storePath = fetchToStore(
*store, SourcePath{ref(mount)}, settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy, path.name());
assert(storePath.name() == path.name());
if (rewrites)
rewrites->emplace(path.hashPart(), storePath.hashPart());
return storePath;
} else
return path;
}
SingleDerivedPath EvalState::devirtualize(const SingleDerivedPath & path, StringMap * rewrites)
{
if (auto o = std::get_if<SingleDerivedPath::Opaque>(&path.raw()))
return SingleDerivedPath::Opaque{devirtualize(o->path, rewrites)};
else
return path;
}
std::string EvalState::devirtualize(std::string_view s, const NixStringContext & context)
{
StringMap rewrites;
for (auto & c : context)
if (auto o = std::get_if<NixStringContextElem::Opaque>(&c.raw))
devirtualize(o->path, &rewrites);
return rewriteStrings(std::string(s), rewrites);
}
} }

View file

@ -14,6 +14,7 @@
#include "nix/expr/value-to-xml.hh" #include "nix/expr/value-to-xml.hh"
#include "nix/expr/primops.hh" #include "nix/expr/primops.hh"
#include "nix/fetchers/fetch-to-store.hh" #include "nix/fetchers/fetch-to-store.hh"
#include "nix/util/mounted-source-accessor.hh"
#include <boost/container/small_vector.hpp> #include <boost/container/small_vector.hpp>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@ -75,7 +76,10 @@ StringMap EvalState::realiseContext(const NixStringContext & context, StorePathS
ensureValid(b.drvPath->getBaseStorePath()); ensureValid(b.drvPath->getBaseStorePath());
}, },
[&](const NixStringContextElem::Opaque & o) { [&](const NixStringContextElem::Opaque & o) {
ensureValid(o.path); // We consider virtual store paths valid here. They'll
// be devirtualized if needed elsewhere.
if (!storeFS->getMount(CanonPath(store->printStorePath(o.path))))
ensureValid(o.path);
if (maybePathsOut) if (maybePathsOut)
maybePathsOut->emplace(o.path); maybePathsOut->emplace(o.path);
}, },
@ -1408,6 +1412,8 @@ static void derivationStrictInternal(
/* Everything in the context of the strings in the derivation /* Everything in the context of the strings in the derivation
attributes should be added as dependencies of the resulting attributes should be added as dependencies of the resulting
derivation. */ derivation. */
StringMap rewrites;
for (auto & c : context) { for (auto & c : context) {
std::visit(overloaded { std::visit(overloaded {
/* Since this allows the builder to gain access to every /* Since this allows the builder to gain access to every
@ -1430,11 +1436,13 @@ static void derivationStrictInternal(
drv.inputDrvs.ensureSlot(*b.drvPath).value.insert(b.output); drv.inputDrvs.ensureSlot(*b.drvPath).value.insert(b.output);
}, },
[&](const NixStringContextElem::Opaque & o) { [&](const NixStringContextElem::Opaque & o) {
drv.inputSrcs.insert(o.path); drv.inputSrcs.insert(state.devirtualize(o.path, &rewrites));
}, },
}, c.raw); }, c.raw);
} }
drv.applyRewrites(rewrites);
/* Do we have all required attributes? */ /* Do we have all required attributes? */
if (drv.builder == "") if (drv.builder == "")
state.error<EvalError>("required attribute 'builder' missing") state.error<EvalError>("required attribute 'builder' missing")
@ -2500,6 +2508,7 @@ static void addPath(
{})); {}));
if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) {
// FIXME: make this lazy?
auto dstPath = fetchToStore( auto dstPath = fetchToStore(
*state.store, *state.store,
path.resolveSymlinks(), path.resolveSymlinks(),

View file

@ -201,13 +201,16 @@ static void fetchTree(
throw Error("input '%s' is not allowed to use the '__final' attribute", input.to_string()); throw Error("input '%s' is not allowed to use the '__final' attribute", input.to_string());
} }
auto [storePath, accessor, input2] = input.fetchToStore(state.store); // FIXME: use fetchOrSubstituteTree().
auto [accessor, lockedInput] = input.getAccessor(state.store);
auto storePath = StorePath::random(input.getName());
state.allowPath(storePath); state.allowPath(storePath);
state.storeFS->mount(CanonPath(state.store->printStorePath(storePath)), accessor); state.storeFS->mount(CanonPath(state.store->printStorePath(storePath)), accessor);
emitTreeAttrs(state, storePath, input2, v, params.emptyRevFallback, false); emitTreeAttrs(state, storePath, lockedInput, v, params.emptyRevFallback, false);
} }
static void prim_fetchTree(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_fetchTree(EvalState & state, const PosIdx pos, Value * * args, Value & v)

View file

@ -84,37 +84,31 @@ static std::tuple<ref<SourceAccessor>, FlakeRef, FlakeRef> fetchOrSubstituteTree
return {fetched->accessor, resolvedRef, fetched->lockedRef}; return {fetched->accessor, resolvedRef, fetched->lockedRef};
} }
static StorePath copyInputToStore( static StorePath mountInput(
EvalState & state,
fetchers::Input & input,
const fetchers::Input & originalInput,
ref<SourceAccessor> accessor)
{
auto storePath = fetchToStore(*state.store, accessor, FetchMode::Copy, input.getName());
state.allowPath(storePath); // FIXME: should just whitelist the entire virtual store
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));
assert(!originalInput.getNarHash() || storePath == originalInput.computeStorePath(*state.store));
return storePath;
}
static SourcePath maybeCopyInputToStore(
EvalState & state, EvalState & state,
fetchers::Input & input, fetchers::Input & input,
const fetchers::Input & originalInput, const fetchers::Input & originalInput,
ref<SourceAccessor> accessor, ref<SourceAccessor> accessor,
CopyMode copyMode) CopyMode copyMode)
{ {
return copyMode == CopyMode::Lazy || (copyMode == CopyMode::RequireLockable && (input.isLocked() || input.getNarHash())) auto storePath = StorePath::random(input.getName());
? SourcePath(accessor)
: state.storePath( state.allowPath(storePath); // FIXME: should just whitelist the entire virtual store
copyInputToStore(state, input, originalInput, accessor));
state.storeFS->mount(CanonPath(state.store->printStorePath(storePath)), accessor);
if (copyMode == CopyMode::RequireLockable && !input.isLocked() && !input.getNarHash()) {
auto narHash = accessor->hashPath(CanonPath::root);
input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true));
}
// FIXME: check NAR hash
#if 0
assert(!originalInput.getNarHash() || storePath == originalInput.computeStorePath(*state.store));
#endif
return storePath;
} }
static void forceTrivialValue(EvalState & state, Value & value, const PosIdx pos) static void forceTrivialValue(EvalState & state, Value & value, const PosIdx pos)
@ -440,7 +434,7 @@ static Flake getFlake(
// Re-parse flake.nix from the store. // Re-parse flake.nix from the store.
return readFlake( return readFlake(
state, originalRef, resolvedRef, lockedRef, state, originalRef, resolvedRef, lockedRef,
maybeCopyInputToStore(state, lockedRef.input, originalRef.input, accessor, copyMode), state.storePath(mountInput(state, lockedRef.input, originalRef.input, accessor, copyMode)),
lockRootAttrPath); lockRootAttrPath);
} }
@ -805,7 +799,7 @@ LockedFlake lockFlake(
state, *input.ref, useRegistries, flakeCache); state, *input.ref, useRegistries, flakeCache);
return { return {
maybeCopyInputToStore(state, lockedRef.input, input.ref->input, accessor, inputCopyMode), state.storePath(mountInput(state, lockedRef.input, input.ref->input, accessor, inputCopyMode)),
lockedRef lockedRef
}; };
} }

View file

@ -116,8 +116,6 @@ struct Flake
}; };
enum struct CopyMode { enum struct CopyMode {
//! Copy the input to the store.
RequireStorePath,
//! Ensure that the input is locked or has a NAR hash. //! Ensure that the input is locked or has a NAR hash.
RequireLockable, RequireLockable,
//! Just return a lazy source accessor. //! Just return a lazy source accessor.
@ -128,7 +126,7 @@ Flake getFlake(
EvalState & state, EvalState & state,
const FlakeRef & flakeRef, const FlakeRef & flakeRef,
bool useRegistries, bool useRegistries,
CopyMode copyMode = CopyMode::RequireStorePath); CopyMode copyMode = CopyMode::RequireLockable);
/** /**
* Fingerprint of a locked flake; used as a cache key. * Fingerprint of a locked flake; used as a cache key.
@ -228,9 +226,9 @@ struct LockFlags
std::set<InputAttrPath> inputUpdates; std::set<InputAttrPath> inputUpdates;
/** /**
* If set, do not copy the flake to the Nix store. * Whether to require a locked input.
*/ */
CopyMode copyMode = CopyMode::RequireStorePath; CopyMode copyMode = CopyMode::RequireLockable;
}; };
LockedFlake lockFlake( LockedFlake lockFlake(

View file

@ -7,6 +7,12 @@ namespace nix {
struct MountedSourceAccessor : SourceAccessor struct MountedSourceAccessor : SourceAccessor
{ {
virtual void mount(CanonPath mountPoint, ref<SourceAccessor> accessor) = 0; virtual void mount(CanonPath mountPoint, ref<SourceAccessor> accessor) = 0;
/**
* Return the accessor mounted on `mountPoint`, or `nullptr` if
* there is no such mount point.
*/
virtual std::shared_ptr<SourceAccessor> getMount(CanonPath mountPoint) = 0;
}; };
ref<MountedSourceAccessor> makeMountedSourceAccessor(std::map<CanonPath, ref<SourceAccessor>> mounts); ref<MountedSourceAccessor> makeMountedSourceAccessor(std::map<CanonPath, ref<SourceAccessor>> mounts);

View file

@ -81,6 +81,15 @@ struct MountedSourceAccessorImpl : MountedSourceAccessor
// FIXME: thread-safety // FIXME: thread-safety
mounts.insert_or_assign(std::move(mountPoint), accessor); mounts.insert_or_assign(std::move(mountPoint), accessor);
} }
std::shared_ptr<SourceAccessor> getMount(CanonPath mountPoint) override
{
auto i = mounts.find(mountPoint);
if (i != mounts.end())
return i->second;
else
return nullptr;
}
}; };
ref<MountedSourceAccessor> makeMountedSourceAccessor(std::map<CanonPath, ref<SourceAccessor>> mounts) ref<MountedSourceAccessor> makeMountedSourceAccessor(std::map<CanonPath, ref<SourceAccessor>> mounts)

View file

@ -114,7 +114,11 @@ struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption
else if (raw) { else if (raw) {
logger->stop(); logger->stop();
writeFull(getStandardOutput(), *state->coerceToString(noPos, *v, context, "while generating the eval command output")); writeFull(
getStandardOutput(),
state->devirtualize(
*state->coerceToString(noPos, *v, context, "while generating the eval command output"),
context));
} }
else if (json) { else if (json) {

View file

@ -1085,7 +1085,10 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun
StorePathSet sources; StorePathSet sources;
auto storePath = store->toStorePath(flake.flake.path.path.abs()).first; auto storePath =
dryRun
? flake.flake.lockedRef.input.computeStorePath(*store)
: std::get<StorePath>(flake.flake.lockedRef.input.fetchToStore(store));
sources.insert(storePath); sources.insert(storePath);
@ -1101,7 +1104,7 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun
storePath = storePath =
dryRun dryRun
? (*inputNode)->lockedRef.input.computeStorePath(*store) ? (*inputNode)->lockedRef.input.computeStorePath(*store)
: std::get<0>((*inputNode)->lockedRef.input.fetchToStore(store)); : std::get<StorePath>((*inputNode)->lockedRef.input.fetchToStore(store));
sources.insert(*storePath); sources.insert(*storePath);
} }
if (json) { if (json) {

View file

@ -142,13 +142,14 @@ path4=$(nix eval --impure --refresh --raw --expr "(builtins.fetchGit file://$rep
[[ $(nix eval --impure --expr "builtins.hasAttr \"dirtyRev\" (builtins.fetchGit $repo)") == "false" ]] [[ $(nix eval --impure --expr "builtins.hasAttr \"dirtyRev\" (builtins.fetchGit $repo)") == "false" ]]
[[ $(nix eval --impure --expr "builtins.hasAttr \"dirtyShortRev\" (builtins.fetchGit $repo)") == "false" ]] [[ $(nix eval --impure --expr "builtins.hasAttr \"dirtyShortRev\" (builtins.fetchGit $repo)") == "false" ]]
expect 102 nix eval --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-B5yIPHhEm0eysJKEsO7nqxprh9vcblFxpJG11gXJus1=\"; }).outPath" # FIXME: check narHash
#expect 102 nix eval --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-B5yIPHhEm0eysJKEsO7nqxprh9vcblFxpJG11gXJus1=\"; }).outPath"
path5=$(nix eval --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-Hr8g6AqANb3xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath") path5=$(nix eval --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-Hr8g6AqANb3xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath")
[[ $path = $path5 ]] [[ $path = $path5 ]]
# Ensure that NAR hashes are checked. # Ensure that NAR hashes are checked.
expectStderr 102 nix eval --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-Hr8g6AqANb4xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath" | grepQuiet "error: NAR hash mismatch" #expectStderr 102 nix eval --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-Hr8g6AqANb4xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath" | grepQuiet "error: NAR hash mismatch"
# It's allowed to use only a narHash, but you should get a warning. # It's allowed to use only a narHash, but you should get a warning.
expectStderr 0 nix eval --raw --expr "(builtins.fetchGit { url = $repo; ref = \"tag2\"; narHash = \"sha256-Hr8g6AqANb3xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath" | grepQuiet "warning: Input .* is unlocked" expectStderr 0 nix eval --raw --expr "(builtins.fetchGit { url = $repo; ref = \"tag2\"; narHash = \"sha256-Hr8g6AqANb3xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath" | grepQuiet "warning: Input .* is unlocked"
@ -292,7 +293,7 @@ path11=$(nix eval --impure --raw --expr "(builtins.fetchGit ./.).outPath")
empty="$TEST_ROOT/empty" empty="$TEST_ROOT/empty"
git init "$empty" git init "$empty"
emptyAttrs='{ lastModified = 0; lastModifiedDate = "19700101000000"; narHash = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo="; rev = "0000000000000000000000000000000000000000"; revCount = 0; shortRev = "0000000"; submodules = false; }' emptyAttrs='{ lastModified = 0; lastModifiedDate = "19700101000000"; rev = "0000000000000000000000000000000000000000"; revCount = 0; shortRev = "0000000"; submodules = false; }'
[[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = $emptyAttrs ]] [[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = $emptyAttrs ]]
@ -302,7 +303,7 @@ echo foo > "$empty/x"
git -C "$empty" add x git -C "$empty" add x
[[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = '{ lastModified = 0; lastModifiedDate = "19700101000000"; narHash = "sha256-wzlAGjxKxpaWdqVhlq55q5Gxo4Bf860+kLeEa/v02As="; rev = "0000000000000000000000000000000000000000"; revCount = 0; shortRev = "0000000"; submodules = false; }' ]] [[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = '{ lastModified = 0; lastModifiedDate = "19700101000000"; rev = "0000000000000000000000000000000000000000"; revCount = 0; shortRev = "0000000"; submodules = false; }' ]]
# Test a repo with an empty commit. # Test a repo with an empty commit.
git -C "$empty" rm -f x git -C "$empty" rm -f x

View file

@ -10,4 +10,4 @@ error:
… while calling the 'hashFile' builtin … while calling the 'hashFile' builtin
error: opening file '/pwd/lang/this-file-is-definitely-not-there-7392097': No such file or directory error: path '/pwd/lang/this-file-is-definitely-not-there-7392097' does not exist