Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore input substitution #11701

Merged
merged 17 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/libexpr/call-flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ lockFileStr:
# unlocked trees.
overrides:

# This is `prim_fetchFinalTree`.
fetchTreeFinal:

let

lockFile = builtins.fromJSON lockFileStr;
Expand Down Expand Up @@ -44,7 +47,8 @@ let
overrides.${key}.sourceInfo
else
# FIXME: remove obsolete node.info.
fetchTree (node.info or {} // removeAttrs node.locked ["dir"]);
# Note: lock file entries are always final.
fetchTreeFinal (node.info or {} // removeAttrs node.locked ["dir"]);

subdir = overrides.${key}.dir or node.locked.dir or "";

Expand Down
12 changes: 9 additions & 3 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -510,9 +510,15 @@ Value * EvalState::addPrimOp(PrimOp && primOp)

Value * v = allocValue();
v->mkPrimOp(new PrimOp(primOp));
staticBaseEnv->vars.emplace_back(envName, baseEnvDispl);
baseEnv.values[baseEnvDispl++] = v;
baseEnv.values[0]->payload.attrs->push_back(Attr(symbols.create(primOp.name), v));

if (primOp.internal)
internalPrimOps.emplace(primOp.name, v);
else {
staticBaseEnv->vars.emplace_back(envName, baseEnvDispl);
baseEnv.values[baseEnvDispl++] = v;
baseEnv.values[0]->payload.attrs->push_back(Attr(symbols.create(primOp.name), v));
}

return v;
}

Expand Down
10 changes: 10 additions & 0 deletions src/libexpr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ struct PrimOp
*/
std::optional<ExperimentalFeature> experimentalFeature;

/**
* If true, this primop is not exposed to the user.
*/
bool internal = false;

/**
* Validity check to be performed by functions that introduce primops,
* such as RegisterPrimOp() and Value::mkPrimOp().
Expand Down Expand Up @@ -591,6 +596,11 @@ public:
*/
std::shared_ptr<StaticEnv> staticBaseEnv; // !!! should be private

/**
* Internal primops not exposed to the user.
*/
std::unordered_map<std::string, Value *, std::hash<std::string>, std::equal_to<std::string>, traceable_allocator<std::pair<const std::string, Value *>>> internalPrimOps;

/**
* Name and documentation about every constant.
*
Expand Down
20 changes: 20 additions & 0 deletions src/libexpr/primops/fetchTree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ struct FetchTreeParams {
bool emptyRevFallback = false;
bool allowNameArgument = false;
bool isFetchGit = false;
bool isFinal = false;
};

static void fetchTree(
Expand Down Expand Up @@ -195,6 +196,13 @@ static void fetchTree(

state.checkURI(input.toURLString());

if (params.isFinal) {
input.attrs.insert_or_assign("__final", Explicit<bool>(true));
} else {
if (input.isFinal())
throw Error("input '%s' is not allowed to use the '__final' attribute", input.to_string());
}

auto [storePath, input2] = input.fetchToStore(state.store);

state.allowPath(storePath);
Expand Down Expand Up @@ -431,6 +439,18 @@ static RegisterPrimOp primop_fetchTree({
.experimentalFeature = Xp::FetchTree,
});

void prim_fetchFinalTree(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
fetchTree(state, pos, args, v, {.isFinal = true});
}

static RegisterPrimOp primop_fetchFinalTree({
.name = "fetchFinalTree",
.args = {"input"},
.fun = prim_fetchFinalTree,
.internal = true,
});

Comment on lines +447 to +453
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static RegisterPrimOp primop_fetchFinalTree({
.name = "fetchFinalTree",
.args = {"input"},
.fun = prim_fetchFinalTree,
.internal = true,
});

Couldn't we construct the primop on the fly when loading call-flake.nix?
That way we don't need internal and internalPrimOps.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seemed tricky since this happens in libfetcher rather than libexpr, so it's not clear how to construct a singleton value. Or we could allocate a new PrimOp every time, but that would leak.

static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v,
const std::string & who, bool unpack, std::string name)
{
Expand Down
119 changes: 95 additions & 24 deletions src/libfetchers/fetchers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "source-path.hh"
#include "fetch-to-store.hh"
#include "json-utils.hh"
#include "store-path-accessor.hh"

#include <nlohmann/json.hpp>

Expand Down Expand Up @@ -100,7 +101,7 @@ Input Input::fromAttrs(const Settings & settings, Attrs && attrs)
auto allowedAttrs = inputScheme->allowedAttrs();

for (auto & [name, _] : attrs)
if (name != "type" && allowedAttrs.count(name) == 0)
if (name != "type" && name != "__final" && allowedAttrs.count(name) == 0)
throw Error("input attribute '%s' not supported by scheme '%s'", name, schemeName);

auto res = inputScheme->inputFromAttrs(settings, attrs);
Expand Down Expand Up @@ -145,6 +146,11 @@ bool Input::isLocked() const
return scheme && scheme->isLocked(*this);
}

bool Input::isFinal() const
{
return maybeGetBoolAttr(attrs, "__final").value_or(false);
}

Attrs Input::toAttrs() const
{
return attrs;
Expand Down Expand Up @@ -172,16 +178,24 @@ std::pair<StorePath, Input> Input::fetchToStore(ref<Store> store) const

auto [storePath, input] = [&]() -> std::pair<StorePath, Input> {
try {
auto [accessor, final] = getAccessorUnchecked(store);
auto [accessor, result] = getAccessorUnchecked(store);

auto storePath = nix::fetchToStore(*store, SourcePath(accessor), FetchMode::Copy, final.getName());
auto storePath = nix::fetchToStore(*store, SourcePath(accessor), FetchMode::Copy, result.getName());

auto narHash = store->queryPathInfo(storePath)->narHash;
final.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true));
result.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true));

scheme->checkLocks(*this, final);
// FIXME: we would like to mark inputs as final in
// getAccessorUnchecked(), but then we can't add
// narHash. Or maybe narHash should be excluded from the
// concept of "final" inputs?
result.attrs.insert_or_assign("__final", Explicit<bool>(true));

return {storePath, final};
assert(result.isFinal());

checkLocks(*this, result);

return {storePath, result};
} catch (Error & e) {
e.addTrace({}, "while fetching the input '%s'", to_string());
throw;
Expand All @@ -191,46 +205,73 @@ std::pair<StorePath, Input> Input::fetchToStore(ref<Store> store) const
return {std::move(storePath), input};
}

void InputScheme::checkLocks(const Input & specified, const Input & final) const
{
void Input::checkLocks(Input specified, Input & result)
{
/* If the original input is final, then we just return the
original attributes, dropping any new fields returned by the
fetcher. However, any fields that are in both the specified and
result input must be identical. */
if (specified.isFinal()) {

/* Backwards compatibility hack: we had some lock files in the
past that 'narHash' fields with incorrect base-64
formatting (lacking the trailing '=', e.g. 'sha256-ri...Mw'
instead of ''sha256-ri...Mw='). So fix that. */
if (auto prevNarHash = specified.getNarHash())
specified.attrs.insert_or_assign("narHash", prevNarHash->to_string(HashFormat::SRI, true));

for (auto & field : specified.attrs) {
auto field2 = result.attrs.find(field.first);
if (field2 != result.attrs.end() && field.second != field2->second)
throw Error("mismatch in field '%s' of input '%s', got '%s'",
field.first,
attrsToJSON(specified.attrs),
attrsToJSON(result.attrs));
}

result.attrs = specified.attrs;

return;
}

if (auto prevNarHash = specified.getNarHash()) {
if (final.getNarHash() != prevNarHash) {
if (final.getNarHash())
if (result.getNarHash() != prevNarHash) {
if (result.getNarHash())
throw Error((unsigned int) 102, "NAR hash mismatch in input '%s', expected '%s' but got '%s'",
specified.to_string(), prevNarHash->to_string(HashFormat::SRI, true), final.getNarHash()->to_string(HashFormat::SRI, true));
specified.to_string(), prevNarHash->to_string(HashFormat::SRI, true), result.getNarHash()->to_string(HashFormat::SRI, true));
else
throw Error((unsigned int) 102, "NAR hash mismatch in input '%s', expected '%s' but got none",
specified.to_string(), prevNarHash->to_string(HashFormat::SRI, true));
}
}

if (auto prevLastModified = specified.getLastModified()) {
if (final.getLastModified() != prevLastModified)
throw Error("'lastModified' attribute mismatch in input '%s', expected %d",
final.to_string(), *prevLastModified);
if (result.getLastModified() != prevLastModified)
throw Error("'lastModified' attribute mismatch in input '%s', expected %d, got %d",
result.to_string(), *prevLastModified, result.getLastModified().value_or(-1));
}

if (auto prevRev = specified.getRev()) {
if (final.getRev() != prevRev)
if (result.getRev() != prevRev)
throw Error("'rev' attribute mismatch in input '%s', expected %s",
final.to_string(), prevRev->gitRev());
result.to_string(), prevRev->gitRev());
}

if (auto prevRevCount = specified.getRevCount()) {
if (final.getRevCount() != prevRevCount)
if (result.getRevCount() != prevRevCount)
throw Error("'revCount' attribute mismatch in input '%s', expected %d",
final.to_string(), *prevRevCount);
result.to_string(), *prevRevCount);
}
}

std::pair<ref<SourceAccessor>, Input> Input::getAccessor(ref<Store> store) const
{
try {
auto [accessor, final] = getAccessorUnchecked(store);
auto [accessor, result] = getAccessorUnchecked(store);

scheme->checkLocks(*this, final);
checkLocks(*this, result);

return {accessor, std::move(final)};
return {accessor, std::move(result)};
} catch (Error & e) {
e.addTrace({}, "while fetching the input '%s'", to_string());
throw;
Expand All @@ -244,12 +285,42 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(ref<Store> sto
if (!scheme)
throw Error("cannot fetch unsupported input '%s'", attrsToJSON(toAttrs()));

auto [accessor, final] = scheme->getAccessor(store, *this);
/* The tree may already be in the Nix store, or it could be
substituted (which is often faster than fetching from the
original source). So check that. We only do this for final
inputs, otherwise there is a risk that we don't return the
same attributes (like `lastModified`) that the "real" fetcher
would return.

FIXME: add a setting to disable this.
edolstra marked this conversation as resolved.
Show resolved Hide resolved
FIXME: substituting may be slower than fetching normally,
e.g. for fetchers like Git that are incremental!
*/
if (isFinal() && getNarHash()) {
try {
auto storePath = computeStorePath(*store);

store->ensurePath(storePath);

debug("using substituted/cached input '%s' in '%s'",
to_string(), store->printStorePath(storePath));

auto accessor = makeStorePathAccessor(store, storePath);

accessor->fingerprint = scheme->getFingerprint(store, *this);

return {accessor, *this};
} catch (Error & e) {
debug("substitution of input '%s' failed: %s", to_string(), e.what());
}
}

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

assert(!accessor->fingerprint);
accessor->fingerprint = scheme->getFingerprint(store, final);
accessor->fingerprint = scheme->getFingerprint(store, result);

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

Input Input::applyOverrides(
Expand Down
56 changes: 33 additions & 23 deletions src/libfetchers/fetchers.hh
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,30 @@ public:
Attrs toAttrs() const;

/**
* Check whether this is a "direct" input, that is, not
* Return whether this is a "direct" input, that is, not
* one that goes through a registry.
*/
bool isDirect() const;

/**
* Check whether this is a "locked" input, that is,
* one that contains a commit hash or content hash.
* Return whether this is a "locked" input, that is, it has
* attributes like a Git revision or NAR hash that uniquely
* identify its contents.
*/
bool isLocked() const;

/**
* Return whether this is a "final" input, meaning that fetching
* it will not add, remove or change any attributes. (See
* `checkLocks()` for the semantics.) Only "final" inputs can be
* substituted from a binary cache.
*
* The "final" state is denoted by the presence of an attribute
* `__final = true`. This attribute is currently undocumented and
* for internal use only.
*/
bool isFinal() const;

bool operator ==(const Input & other) const noexcept;

bool contains(const Input & other) const;
Expand All @@ -99,6 +112,19 @@ public:
*/
std::pair<StorePath, Input> fetchToStore(ref<Store> store) const;

/**
* Check the locking attributes in `result` against
* `specified`. E.g. if `specified` has a `rev` attribute, then
* `result` must have the same `rev` attribute. Throw an exception
* if there is a mismatch.
*
* If `specified` is marked final (i.e. has the `__final`
* attribute), then the intersection of attributes in `specified`
* and `result` must be equal, and `final.attrs` is set to
* `specified.attrs` (i.e. we discard any new attributes).
*/
static void checkLocks(Input specified, Input & result);

/**
* Return a `SourceAccessor` that allows access to files in the
* input without copying it to the store. Also return a possibly
Expand Down Expand Up @@ -144,6 +170,10 @@ public:
/**
* For locked inputs, return a string that uniquely specifies the
* content of the input (typically a commit hash or content hash).
*
* Only known-equivalent inputs should return the same fingerprint.
*
* This is not a stable identifier between Nix versions, but not guaranteed to change either.
*/
std::optional<std::string> getFingerprint(ref<Store> store) const;
};
Expand Down Expand Up @@ -215,31 +245,11 @@ struct InputScheme
virtual bool isDirect(const Input & input) const
{ return true; }

/**
* A sufficiently unique string that can be used as a cache key to identify the `input`.
*
* Only known-equivalent inputs should return the same fingerprint.
*
* This is not a stable identifier between Nix versions, but not guaranteed to change either.
*/
virtual std::optional<std::string> getFingerprint(ref<Store> store, const Input & input) const
{ return std::nullopt; }

/**
* Return `true` if this input is considered "locked", i.e. it has
* attributes like a Git revision or NAR hash that uniquely
* identify its contents.
*/
virtual bool isLocked(const Input & input) const
{ return false; }

/**
* Check the locking attributes in `final` against
* `specified`. E.g. if `specified` has a `rev` attribute, then
* `final` must have the same `rev` attribute. Throw an exception
* if there is a mismatch.
*/
virtual void checkLocks(const Input & specified, const Input & final) const;
};

void registerInputScheme(std::shared_ptr<InputScheme> && fetcher);
Expand Down
Loading
Loading