Files
lix/lix/libexpr/primops/fetchClosure.cc
T
eldritch horrors 80654b84b6 libexpr: de-ptr-ize Value references
thunk values are shareable, and we can represent invalid/uninitialized
values with a special bit pattern that makes no sense otherwise. there
is no need to keep allocating values on the heap, instead we can treat
values like reference-counted smart pointers to heap objects, which in
turn lets us save a lot of allocations and, ultimately, gc heap space.

compared to our baseline (main of 2025-09-27) we save 15%+ memory on a
system rebuild and 17% on nix search. eval time regresses by ~3% for a
system rebuild, while nix search is 7% faster. further optimization is
probably possible (but for now this will just have to be good enough).

Change-Id: Ib6c47acdbe2fac4f76a83c2269f16f30ef66b2e1
2025-10-05 16:23:05 +02:00

202 lines
7.8 KiB
C++

#include "lix/libexpr/eval.hh"
#include "lix/libexpr/extra-primops.hh"
#include "lix/libstore/store-api.hh"
#include "lix/libstore/make-content-addressed.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/url.hh"
namespace nix {
/**
* Handler for the content addressed case.
*
* @param state Evaluator state and store to write to.
* @param fromStore Store containing the path to rewrite.
* @param fromPath Source path to be rewritten.
* @param toPathMaybe Path to write the rewritten path to. If empty, the error shows the actual path.
* @param v Return `Value`
*/
static void runFetchClosureWithRewrite(EvalState & state, Store & fromStore, const StorePath & fromPath, const std::optional<StorePath> & toPathMaybe, Value &v) {
// establish toPath or throw
if (!toPathMaybe || !state.aio.blockOn(state.ctx.store->isValidPath(*toPathMaybe))) {
auto rewrittenPath =
state.aio.blockOn(makeContentAddressed(fromStore, *state.ctx.store, fromPath));
if (toPathMaybe && *toPathMaybe != rewrittenPath)
throw Error({
.msg = HintFmt("rewriting '%s' to content-addressed form yielded '%s', while '%s' was expected",
state.ctx.store->printStorePath(fromPath),
state.ctx.store->printStorePath(rewrittenPath),
state.ctx.store->printStorePath(*toPathMaybe)),
});
if (!toPathMaybe)
throw Error({
.msg = HintFmt(
"rewriting '%s' to content-addressed form yielded '%s'\n"
"Use this value for the 'toPath' attribute passed to 'fetchClosure'",
state.ctx.store->printStorePath(fromPath),
state.ctx.store->printStorePath(rewrittenPath)),
});
}
auto toPath = *toPathMaybe;
// check and return
auto resultInfo = state.aio.blockOn(state.ctx.store->queryPathInfo(toPath));
if (!resultInfo->isContentAddressed(*state.ctx.store)) {
// We don't perform the rewriting when outPath already exists, as an optimisation.
// However, we can quickly detect a mistake if the toPath is input addressed.
throw Error({
.msg = HintFmt(
"The 'toPath' value '%s' is input-addressed, so it can't possibly be the result of rewriting to a content-addressed path.\n\n"
"Set 'toPath' to an empty string to make Lix report the correct content-addressed path.",
state.ctx.store->printStorePath(toPath)),
});
}
state.ctx.paths.mkStorePathString(toPath, v);
}
/**
* Fetch the closure and make sure it's content addressed.
*/
static void runFetchClosureWithContentAddressedPath(EvalState & state, Store & fromStore, const StorePath & fromPath, Value & v) {
if (!state.aio.blockOn(state.ctx.store->isValidPath(fromPath)))
state.aio.blockOn(copyClosure(fromStore, *state.ctx.store, RealisedPath::Set { fromPath }));
auto info = state.aio.blockOn(state.ctx.store->queryPathInfo(fromPath));
if (!info->isContentAddressed(*state.ctx.store)) {
throw Error({
.msg = HintFmt(
"The 'fromPath' value '%s' is input-addressed, but 'inputAddressed' is set to 'false' (default).\n\n"
"If you do intend to fetch an input-addressed store path, add\n\n"
" inputAddressed = true;\n\n"
"to the 'fetchClosure' arguments.\n\n"
"Note that to ensure authenticity input-addressed store paths, users must configure a trusted binary cache public key on their systems. This is not needed for content-addressed paths.",
state.ctx.store->printStorePath(fromPath)),
});
}
state.ctx.paths.mkStorePathString(fromPath, v);
}
/**
* Fetch the closure and make sure it's input addressed.
*/
static void runFetchClosureWithInputAddressedPath(EvalState & state, Store & fromStore, const StorePath & fromPath, Value & v) {
if (!state.aio.blockOn(state.ctx.store->isValidPath(fromPath)))
state.aio.blockOn(copyClosure(fromStore, *state.ctx.store, RealisedPath::Set { fromPath }));
auto info = state.aio.blockOn(state.ctx.store->queryPathInfo(fromPath));
if (info->isContentAddressed(*state.ctx.store)) {
throw Error({
.msg = HintFmt(
"The store object referred to by 'fromPath' at '%s' is not input-addressed, but 'inputAddressed' is set to 'true'.\n\n"
"Remove the 'inputAddressed' attribute (it defaults to 'false') to expect 'fromPath' to be content-addressed",
state.ctx.store->printStorePath(fromPath)),
});
}
state.ctx.paths.mkStorePathString(fromPath, v);
}
typedef std::optional<StorePath> StorePathOrGap;
void prim_fetchClosure(EvalState & state, Value * * args, Value & v)
{
state.forceAttrs(*args[0], noPos, "while evaluating the argument passed to builtins.fetchClosure");
std::optional<std::string> fromStoreUrl;
std::optional<StorePath> fromPath;
std::optional<StorePathOrGap> toPath;
std::optional<bool> inputAddressedMaybe;
for (auto & attr : *args[0]->attrs()) {
const auto & attrName = state.ctx.symbols[attr.name];
auto attrHint = [&]() -> std::string {
return "while evaluating the '" + attrName + "' attribute passed to builtins.fetchClosure";
};
if (attrName == "fromPath") {
NixStringContext context;
fromPath = state.coerceToStorePath(attr.pos, attr.value, context, attrHint());
}
else if (attrName == "toPath") {
state.forceValue(attr.value, attr.pos);
bool isEmptyString = attr.value.type() == nString && attr.value.str().empty();
if (isEmptyString) {
toPath = StorePathOrGap {};
}
else {
NixStringContext context;
toPath = state.coerceToStorePath(attr.pos, attr.value, context, attrHint());
}
}
else if (attrName == "fromStore")
fromStoreUrl = state.forceStringNoCtx(attr.value, attr.pos, attrHint());
else if (attrName == "inputAddressed")
inputAddressedMaybe = state.forceBool(attr.value, attr.pos, attrHint());
else
throw Error({
.msg = HintFmt("attribute '%s' isn't supported in call to 'fetchClosure'", attrName),
});
}
if (!fromPath)
throw Error({
.msg = HintFmt("attribute '%s' is missing in call to 'fetchClosure'", "fromPath"),
});
bool inputAddressed = inputAddressedMaybe.value_or(false);
if (inputAddressed) {
if (toPath)
throw Error({
.msg = HintFmt("attribute '%s' is set to true, but '%s' is also set. Please remove one of them",
"inputAddressed",
"toPath"),
});
}
if (!fromStoreUrl)
throw Error({
.msg = HintFmt("attribute '%s' is missing in call to 'fetchClosure'", "fromStore"),
});
auto parsedURL = parseURL(*fromStoreUrl);
if (parsedURL.scheme != "http" &&
parsedURL.scheme != "https" &&
!(getEnv("_NIX_IN_TEST").has_value() && parsedURL.scheme == "file"))
throw Error({
.msg = HintFmt("'fetchClosure' only supports http:// and https:// stores"),
});
if (!parsedURL.query.empty())
throw Error({
.msg = HintFmt("'fetchClosure' does not support URL query parameters (in '%s')", *fromStoreUrl),
});
auto fromStore = state.aio.blockOn(openStore(parsedURL.to_string()));
if (toPath)
runFetchClosureWithRewrite(state, *fromStore, *fromPath, *toPath, v);
else if (inputAddressed)
runFetchClosureWithInputAddressedPath(state, *fromStore, *fromPath, v);
else
runFetchClosureWithContentAddressedPath(state, *fromStore, *fromPath, v);
}
}