diff options
Diffstat (limited to 'src/libexpr/flake/flake.cc')
-rw-r--r-- | src/libexpr/flake/flake.cc | 802 |
1 files changed, 802 insertions, 0 deletions
diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc new file mode 100644 index 000000000..a30637262 --- /dev/null +++ b/src/libexpr/flake/flake.cc @@ -0,0 +1,802 @@ +#include "flake.hh" +#include "lockfile.hh" +#include "primops.hh" +#include "eval-inline.hh" +#include "store-api.hh" +#include "fetchers/fetchers.hh" +#include "finally.hh" + +#include <iostream> +#include <ctime> +#include <iomanip> + +namespace nix { + +using namespace flake; + +namespace flake { + +/* If 'allowLookup' is true, then resolve 'flakeRef' using the + registries. */ +static FlakeRef maybeLookupFlake( + ref<Store> store, + const FlakeRef & flakeRef, + bool allowLookup) +{ + if (!flakeRef.input->isDirect()) { + if (allowLookup) + return flakeRef.resolve(store); + else + throw Error("'%s' is an indirect flake reference, but registry lookups are not allowed", flakeRef); + } else + return flakeRef; +} + +typedef std::vector<std::pair<FlakeRef, FlakeRef>> FlakeCache; + +static FlakeRef lookupInFlakeCache( + const FlakeCache & flakeCache, + const FlakeRef & flakeRef) +{ + // FIXME: inefficient. + for (auto & i : flakeCache) { + if (flakeRef == i.first) { + debug("mapping '%s' to previously seen input '%s' -> '%s", + flakeRef, i.first, i.second); + return i.second; + } + } + + return flakeRef; +} + +static std::pair<fetchers::Tree, FlakeRef> fetchOrSubstituteTree( + EvalState & state, + const FlakeRef & originalRef, + std::optional<TreeInfo> treeInfo, + bool allowLookup, + FlakeCache & flakeCache) +{ + /* 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. */ + if (treeInfo && originalRef.input->isDirect() && originalRef.input->isImmutable()) { + try { + auto storePath = treeInfo->computeStorePath(*state.store); + + state.store->ensurePath(storePath); + + debug("using substituted/cached input '%s' in '%s'", + originalRef, state.store->printStorePath(storePath)); + + auto actualPath = state.store->toRealPath(storePath); + + if (state.allowedPaths) + state.allowedPaths->insert(actualPath); + + return { + Tree { + .actualPath = actualPath, + .storePath = std::move(storePath), + .info = *treeInfo, + }, + originalRef + }; + } catch (Error & e) { + debug("substitution of input '%s' failed: %s", originalRef, e.what()); + } + } + + auto resolvedRef = lookupInFlakeCache(flakeCache, + maybeLookupFlake(state.store, + lookupInFlakeCache(flakeCache, originalRef), allowLookup)); + + auto [tree, lockedRef] = resolvedRef.fetchTree(state.store); + + debug("got tree '%s' from '%s'", + state.store->printStorePath(tree.storePath), lockedRef); + + flakeCache.push_back({originalRef, lockedRef}); + flakeCache.push_back({resolvedRef, lockedRef}); + + if (state.allowedPaths) + state.allowedPaths->insert(tree.actualPath); + + if (treeInfo) + assert(tree.storePath == treeInfo->computeStorePath(*state.store)); + + return {std::move(tree), lockedRef}; +} + +static void expectType(EvalState & state, ValueType type, + Value & value, const Pos & pos) +{ + if (value.type == tThunk && value.isTrivial()) + state.forceValue(value, pos); + if (value.type != type) + throw Error("expected %s but got %s at %s", + showType(type), showType(value.type), pos); +} + +static std::map<FlakeId, FlakeInput> parseFlakeInputs( + EvalState & state, Value * value, const Pos & pos); + +static FlakeInput parseFlakeInput(EvalState & state, + const std::string & inputName, Value * value, const Pos & pos) +{ + expectType(state, tAttrs, *value, pos); + + FlakeInput input { + .ref = FlakeRef::fromAttrs({{"type", "indirect"}, {"id", inputName}}) + }; + + auto sInputs = state.symbols.create("inputs"); + auto sUrl = state.symbols.create("url"); + auto sUri = state.symbols.create("uri"); // FIXME: remove soon + auto sFlake = state.symbols.create("flake"); + auto sFollows = state.symbols.create("follows"); + + fetchers::Input::Attrs attrs; + std::optional<std::string> url; + + for (Attr attr : *(value->attrs)) { + try { + if (attr.name == sUrl || attr.name == sUri) { + expectType(state, tString, *attr.value, *attr.pos); + url = attr.value->string.s; + attrs.emplace("url", *url); + } else if (attr.name == sFlake) { + expectType(state, tBool, *attr.value, *attr.pos); + input.isFlake = attr.value->boolean; + } else if (attr.name == sInputs) { + input.overrides = parseFlakeInputs(state, attr.value, *attr.pos); + } else if (attr.name == sFollows) { + expectType(state, tString, *attr.value, *attr.pos); + input.follows = parseInputPath(attr.value->string.s); + } else { + state.forceValue(*attr.value); + if (attr.value->type == tString) + attrs.emplace(attr.name, attr.value->string.s); + else + throw Error("unsupported attribute type"); + } + } catch (Error & e) { + e.addPrefix(fmt("in flake attribute '%s' at '%s':\n", attr.name, *attr.pos)); + throw; + } + } + + if (attrs.count("type")) + try { + input.ref = FlakeRef::fromAttrs(attrs); + } catch (Error & e) { + e.addPrefix(fmt("in flake input at '%s':\n", pos)); + throw; + } + else { + attrs.erase("url"); + if (!attrs.empty()) + throw Error("unexpected flake input attribute '%s', at %s", attrs.begin()->first, pos); + if (url) + input.ref = parseFlakeRef(*url); + } + + return input; +} + +static std::map<FlakeId, FlakeInput> parseFlakeInputs( + EvalState & state, Value * value, const Pos & pos) +{ + std::map<FlakeId, FlakeInput> inputs; + + expectType(state, tAttrs, *value, pos); + + for (Attr & inputAttr : *(*value).attrs) { + inputs.emplace(inputAttr.name, + parseFlakeInput(state, + inputAttr.name, + inputAttr.value, + *inputAttr.pos)); + } + + return inputs; +} + +static Flake getFlake( + EvalState & state, + const FlakeRef & originalRef, + std::optional<TreeInfo> treeInfo, + bool allowLookup, + FlakeCache & flakeCache) +{ + auto [sourceInfo, lockedRef] = fetchOrSubstituteTree( + state, originalRef, treeInfo, allowLookup, flakeCache); + + // Guard against symlink attacks. + auto flakeFile = canonPath(sourceInfo.actualPath + "/" + lockedRef.subdir + "/flake.nix"); + if (!isInDir(flakeFile, sourceInfo.actualPath)) + throw Error("'flake.nix' file of flake '%s' escapes from '%s'", + lockedRef, state.store->printStorePath(sourceInfo.storePath)); + + Flake flake { + .originalRef = originalRef, + .lockedRef = lockedRef, + .sourceInfo = std::make_shared<fetchers::Tree>(std::move(sourceInfo)) + }; + + if (!pathExists(flakeFile)) + throw Error("source tree referenced by '%s' does not contain a '%s/flake.nix' file", lockedRef, lockedRef.subdir); + + Value vInfo; + state.evalFile(flakeFile, vInfo, true); // FIXME: symlink attack + + expectType(state, tAttrs, vInfo, Pos(state.symbols.create(flakeFile), 0, 0)); + + auto sEdition = state.symbols.create("edition"); + auto sEpoch = state.symbols.create("epoch"); // FIXME: remove soon + + auto edition = vInfo.attrs->get(sEdition); + if (!edition) + edition = vInfo.attrs->get(sEpoch); + + if (edition) { + expectType(state, tInt, *edition->value, *edition->pos); + flake.edition = edition->value->integer; + if (flake.edition > 201909) + throw Error("flake '%s' requires unsupported edition %d; please upgrade Nix", lockedRef, flake.edition); + if (flake.edition < 201909) + throw Error("flake '%s' has illegal edition %d", lockedRef, flake.edition); + } else + throw Error("flake '%s' lacks attribute 'edition'", lockedRef); + + if (auto description = vInfo.attrs->get(state.sDescription)) { + expectType(state, tString, *description->value, *description->pos); + flake.description = description->value->string.s; + } + + auto sInputs = state.symbols.create("inputs"); + + if (auto inputs = vInfo.attrs->get(sInputs)) + flake.inputs = parseFlakeInputs(state, inputs->value, *inputs->pos); + + auto sOutputs = state.symbols.create("outputs"); + + if (auto outputs = vInfo.attrs->get(sOutputs)) { + expectType(state, tLambda, *outputs->value, *outputs->pos); + flake.vOutputs = outputs->value; + + if (flake.vOutputs->lambda.fun->matchAttrs) { + for (auto & formal : flake.vOutputs->lambda.fun->formals->formals) { + if (formal.name != state.sSelf) + flake.inputs.emplace(formal.name, FlakeInput { + .ref = parseFlakeRef(formal.name) + }); + } + } + + } else + throw Error("flake '%s' lacks attribute 'outputs'", lockedRef); + + for (auto & attr : *vInfo.attrs) { + if (attr.name != sEdition && + attr.name != sEpoch && + attr.name != state.sDescription && + attr.name != sInputs && + attr.name != sOutputs) + throw Error("flake '%s' has an unsupported attribute '%s', at %s", + lockedRef, attr.name, *attr.pos); + } + + return flake; +} + +Flake getFlake(EvalState & state, const FlakeRef & originalRef, bool allowLookup) +{ + FlakeCache flakeCache; + return getFlake(state, originalRef, {}, allowLookup, flakeCache); +} + +static void flattenLockFile( + const LockedInputs & inputs, + const InputPath & prefix, + std::map<InputPath, const LockedInput *> & res) +{ + for (auto &[id, input] : inputs.inputs) { + auto inputPath(prefix); + inputPath.push_back(id); + res.emplace(inputPath, &input); + flattenLockFile(input, inputPath, res); + } +} + +static std::string diffLockFiles(const LockedInputs & oldLocks, const LockedInputs & newLocks) +{ + std::map<InputPath, const LockedInput *> oldFlat, newFlat; + flattenLockFile(oldLocks, {}, oldFlat); + flattenLockFile(newLocks, {}, newFlat); + + auto i = oldFlat.begin(); + auto j = newFlat.begin(); + std::string res; + + while (i != oldFlat.end() || j != newFlat.end()) { + if (j != newFlat.end() && (i == oldFlat.end() || i->first > j->first)) { + res += fmt("* Added '%s': '%s'\n", concatStringsSep("/", j->first), j->second->lockedRef); + ++j; + } else if (i != oldFlat.end() && (j == newFlat.end() || i->first < j->first)) { + res += fmt("* Removed '%s'\n", concatStringsSep("/", i->first)); + ++i; + } else { + if (!(i->second->lockedRef == j->second->lockedRef)) { + assert(i->second->lockedRef.to_string() != j->second->lockedRef.to_string()); + res += fmt("* Updated '%s': '%s' -> '%s'\n", + concatStringsSep("/", i->first), + i->second->lockedRef, + j->second->lockedRef); + } + ++i; + ++j; + } + } + + return res; +} + +/* Compute an in-memory lock file for the specified top-level flake, + and optionally write it to file, it the flake is writable. */ +LockedFlake lockFlake( + EvalState & state, + const FlakeRef & topRef, + const LockFlags & lockFlags) +{ + settings.requireExperimentalFeature("flakes"); + + FlakeCache flakeCache; + + auto flake = getFlake(state, topRef, {}, lockFlags.useRegistries, flakeCache); + + LockFile oldLockFile; + + if (!lockFlags.recreateLockFile) { + // FIXME: symlink attack + oldLockFile = LockFile::read( + flake.sourceInfo->actualPath + "/" + flake.lockedRef.subdir + "/flake.lock"); + } + + debug("old lock file: %s", oldLockFile); + + LockFile newLockFile, prevLockFile; + std::vector<InputPath> prevUnresolved; + + // FIXME: check whether all overrides are used. + std::map<InputPath, FlakeInput> overrides; + + for (auto & i : lockFlags.inputOverrides) + overrides.insert_or_assign(i.first, FlakeInput { .ref = i.second }); + + /* Compute the new lock file. This is dones as a fixpoint + iteration: we repeat until the new lock file no longer changes + and there are no unresolved "follows" inputs. */ + while (true) { + std::vector<InputPath> unresolved; + + /* Recurse into the flake inputs. */ + std::function<void( + const FlakeInputs & flakeInputs, + const LockedInputs & oldLocks, + LockedInputs & newLocks, + const InputPath & inputPathPrefix)> + updateLocks; + + std::vector<FlakeRef> parents; + + updateLocks = [&]( + const FlakeInputs & flakeInputs, + const LockedInputs & oldLocks, + LockedInputs & newLocks, + const InputPath & inputPathPrefix) + { + /* Get the overrides (i.e. attributes of the form + 'inputs.nixops.inputs.nixpkgs.url = ...'). */ + for (auto & [id, input] : flake.inputs) { + for (auto & [idOverride, inputOverride] : input.overrides) { + auto inputPath(inputPathPrefix); + inputPath.push_back(id); + inputPath.push_back(idOverride); + overrides.insert_or_assign(inputPath, inputOverride); + } + } + + /* Go over the flake inputs, resolve/fetch them if + necessary (i.e. if they're new or the flakeref changed + from what's in the lock file). */ + for (auto & [id, input2] : flakeInputs) { + auto inputPath(inputPathPrefix); + inputPath.push_back(id); + auto inputPathS = concatStringsSep("/", inputPath); + + /* Do we have an override for this input from one of + the ancestors? */ + auto i = overrides.find(inputPath); + bool hasOverride = i != overrides.end(); + auto & input = hasOverride ? i->second : input2; + + if (input.follows) { + /* This is a "follows" input + (i.e. 'inputs.nixpkgs.follows = + "dwarffs/nixpkgs"). Resolve the source and copy + its inputs. Note that the source is normally + relative to the current node of the lock file + (e.g. "dwarffs/nixpkgs" refers to the nixpkgs + input of the dwarffs input of the root flake), + but if it's from an override, it's relative to + the *root* of the lock file. */ + auto follows = (hasOverride ? newLockFile : newLocks).findInput(*input.follows); + if (follows) + newLocks.inputs.insert_or_assign(id, **follows); + else + /* We haven't processed the source of the + "follows" yet (e.g. "dwarffs/nixpkgs"). So + we'll need another round of the fixpoint + iteration. */ + unresolved.push_back(inputPath); + continue; + } + + /* Do we have an entry in the existing lock file? And + we don't have a --update-input flag for this + input? */ + auto oldLock = + lockFlags.inputUpdates.count(inputPath) + ? oldLocks.inputs.end() + : oldLocks.inputs.find(id); + + if (oldLock != oldLocks.inputs.end() && oldLock->second.originalRef == input.ref && !hasOverride) { + /* Copy the input from the old lock file if its + flakeref didn't change and there is no override + from a higher level flake. */ + newLocks.inputs.insert_or_assign(id, oldLock->second); + + /* If we have an --update-input flag for an input + of this input, then we must fetch the flake to + to update it. */ + auto lb = lockFlags.inputUpdates.lower_bound(inputPath); + + auto hasChildUpdate = + lb != lockFlags.inputUpdates.end() + && lb->size() > inputPath.size() + && std::equal(inputPath.begin(), inputPath.end(), lb->begin()); + + if (hasChildUpdate) { + auto inputFlake = getFlake( + state, oldLock->second.lockedRef, oldLock->second.info, false, flakeCache); + + updateLocks(inputFlake.inputs, + (const LockedInputs &) oldLock->second, + newLocks.inputs.find(id)->second, + inputPath); + + } else { + /* No need to fetch this flake, we can be + lazy. However there may be new overrides on + the inputs of this flake, so we need to + check those. */ + FlakeInputs fakeInputs; + + for (auto & i : oldLock->second.inputs) + fakeInputs.emplace(i.first, FlakeInput { .ref = i.second.originalRef }); + + updateLocks(fakeInputs, + oldLock->second, + newLocks.inputs.find(id)->second, + inputPath); + } + + } else { + /* We need to update/create a new lock file + entry. So fetch the flake/non-flake. */ + + if (!lockFlags.allowMutable && !input.ref.input->isImmutable()) + throw Error("cannot update flake input '%s' in pure mode", inputPathS); + + if (input.isFlake) { + auto inputFlake = getFlake(state, input.ref, {}, lockFlags.useRegistries, flakeCache); + + newLocks.inputs.insert_or_assign(id, + LockedInput(inputFlake.lockedRef, inputFlake.originalRef, inputFlake.sourceInfo->info)); + + /* Recursively process the inputs of this + flake. Also, unless we already have this + flake in the top-level lock file, use this + flake's own lock file. */ + + /* Guard against circular flake imports. */ + for (auto & parent : parents) + if (parent == input.ref) + throw Error("found circular import of flake '%s'", parent); + parents.push_back(input.ref); + Finally cleanup([&]() { parents.pop_back(); }); + + updateLocks(inputFlake.inputs, + oldLock != oldLocks.inputs.end() + ? (const LockedInputs &) oldLock->second + : LockFile::read( + inputFlake.sourceInfo->actualPath + "/" + inputFlake.lockedRef.subdir + "/flake.lock"), + newLocks.inputs.find(id)->second, + inputPath); + } + + else { + auto [sourceInfo, lockedRef] = fetchOrSubstituteTree( + state, input.ref, {}, lockFlags.useRegistries, flakeCache); + newLocks.inputs.insert_or_assign(id, + LockedInput(lockedRef, input.ref, sourceInfo.info)); + } + } + } + }; + + updateLocks(flake.inputs, oldLockFile, newLockFile, {}); + + /* Check if there is a cycle in the "follows" inputs. */ + if (!unresolved.empty() && unresolved == prevUnresolved) { + std::vector<std::string> ss; + for (auto & i : unresolved) + ss.push_back(concatStringsSep("/", i)); + throw Error("cycle or missing input detected in flake inputs: %s", concatStringsSep(", ", ss)); + } + + std::swap(unresolved, prevUnresolved); + + /* Done with the fixpoint iteration? */ + if (newLockFile == prevLockFile) break; + prevLockFile = newLockFile; + }; + + debug("new lock file: %s", newLockFile); + + /* Check whether we need to / can write the new lock file. */ + if (!(newLockFile == oldLockFile)) { + + auto diff = diffLockFiles(oldLockFile, newLockFile); + + if (!(oldLockFile == LockFile())) + printInfo("inputs of flake '%s' changed:\n%s", topRef, chomp(diff)); + + if (lockFlags.writeLockFile) { + if (auto sourcePath = topRef.input->getSourcePath()) { + if (!newLockFile.isImmutable()) { + if (settings.warnDirty) + warn("will not write lock file of flake '%s' because it has a mutable input", topRef); + } else { + if (!lockFlags.updateLockFile) + throw Error("flake '%s' requires lock file changes but they're not allowed due to '--no-update-lock-file'", topRef); + + auto relPath = (topRef.subdir == "" ? "" : topRef.subdir + "/") + "flake.lock"; + + auto path = *sourcePath + "/" + relPath; + + bool lockFileExists = pathExists(path); + + if (lockFileExists) + warn("updating lock file '%s'", path); + else + warn("creating lock file '%s'", path); + + newLockFile.write(path); + + topRef.input->markChangedFile( + (topRef.subdir == "" ? "" : topRef.subdir + "/") + "flake.lock", + lockFlags.commitLockFile + ? std::optional<std::string>(fmt("%s: %s\n\nFlake input changes:\n\n%s", + relPath, lockFileExists ? "Update" : "Add", diff)) + : std::nullopt); + + /* Rewriting the lockfile changed the top-level + repo, so we should re-read it. FIXME: we could + also just clear the 'rev' field... */ + auto prevLockedRef = flake.lockedRef; + FlakeCache dummyCache; + flake = getFlake(state, topRef, {}, lockFlags.useRegistries, dummyCache); + + if (lockFlags.commitLockFile && + flake.lockedRef.input->getRev() && + prevLockedRef.input->getRev() != flake.lockedRef.input->getRev()) + warn("committed new revision '%s'", flake.lockedRef.input->getRev()->gitRev()); + + /* Make sure that we picked up the change, + i.e. the tree should usually be dirty + now. Corner case: we could have reverted from a + dirty to a clean tree! */ + if (flake.lockedRef.input == prevLockedRef.input + && !flake.lockedRef.input->isImmutable()) + throw Error("'%s' did not change after I updated its 'flake.lock' file; is 'flake.lock' under version control?", flake.originalRef); + } + } else + throw Error("cannot write modified lock file of flake '%s' (use '--no-write-lock-file' to ignore)", topRef); + } else + warn("not writing modified lock file of flake '%s'", topRef); + } + + return LockedFlake { .flake = std::move(flake), .lockFile = std::move(newLockFile) }; +} + +static void emitSourceInfoAttrs( + EvalState & state, + const FlakeRef & flakeRef, + const fetchers::Tree & sourceInfo, + Value & vAttrs) +{ + assert(state.store->isValidPath(sourceInfo.storePath)); + auto pathS = state.store->printStorePath(sourceInfo.storePath); + mkString(*state.allocAttr(vAttrs, state.sOutPath), pathS, {pathS}); + + assert(sourceInfo.info.narHash); + mkString(*state.allocAttr(vAttrs, state.symbols.create("narHash")), + sourceInfo.info.narHash.to_string(SRI)); + + if (auto rev = flakeRef.input->getRev()) { + mkString(*state.allocAttr(vAttrs, state.symbols.create("rev")), + rev->gitRev()); + mkString(*state.allocAttr(vAttrs, state.symbols.create("shortRev")), + rev->gitShortRev()); + } + + if (sourceInfo.info.revCount) + mkInt(*state.allocAttr(vAttrs, state.symbols.create("revCount")), *sourceInfo.info.revCount); + + if (sourceInfo.info.lastModified) + mkString(*state.allocAttr(vAttrs, state.symbols.create("lastModified")), + fmt("%s", std::put_time(std::gmtime(&*sourceInfo.info.lastModified), "%Y%m%d%H%M%S"))); +} + +struct LazyInput +{ + bool isFlake; + LockedInput lockedInput; +}; + +/* Helper primop to make callFlake (below) fetch/call its inputs + lazily. Note that this primop cannot be called by user code since + it doesn't appear in 'builtins'. */ +static void prim_callFlake(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + auto lazyInput = (LazyInput *) args[0]->attrs; + + if (lazyInput->isFlake) { + FlakeCache flakeCache; + auto flake = getFlake(state, lazyInput->lockedInput.lockedRef, lazyInput->lockedInput.info, false, flakeCache); + + if (flake.sourceInfo->info.narHash != lazyInput->lockedInput.info.narHash) + throw Error("the content hash of flake '%s' (%s) doesn't match the hash recorded in the referring lock file (%s)", + lazyInput->lockedInput.lockedRef, + flake.sourceInfo->info.narHash.to_string(SRI), + lazyInput->lockedInput.info.narHash.to_string(SRI)); + + // FIXME: check all the other attributes in lockedInput.info + // once we've dropped support for lock file version 4. + + assert(flake.sourceInfo->storePath == lazyInput->lockedInput.computeStorePath(*state.store)); + + callFlake(state, flake, lazyInput->lockedInput, v); + } else { + FlakeCache flakeCache; + auto [sourceInfo, lockedRef] = fetchOrSubstituteTree( + state, lazyInput->lockedInput.lockedRef, {}, false, flakeCache); + + if (sourceInfo.info.narHash != lazyInput->lockedInput.info.narHash) + throw Error("the content hash of repository '%s' (%s) doesn't match the hash recorded in the referring lock file (%s)", + lazyInput->lockedInput.lockedRef, + sourceInfo.info.narHash.to_string(SRI), + lazyInput->lockedInput.info.narHash.to_string(SRI)); + + // FIXME: check all the other attributes in lockedInput.info + // once we've dropped support for lock file version 4. + + assert(sourceInfo.storePath == lazyInput->lockedInput.computeStorePath(*state.store)); + + state.mkAttrs(v, 8); + + assert(state.store->isValidPath(sourceInfo.storePath)); + + auto pathS = state.store->printStorePath(sourceInfo.storePath); + + mkString(*state.allocAttr(v, state.sOutPath), pathS, {pathS}); + + emitSourceInfoAttrs(state, lockedRef, sourceInfo, v); + + v.attrs->sort(); + } +} + +void callFlake(EvalState & state, + const Flake & flake, + const LockedInputs & lockedInputs, + Value & vResFinal) +{ + auto & vRes = *state.allocValue(); + auto & vInputs = *state.allocValue(); + + state.mkAttrs(vInputs, flake.inputs.size() + 1); + + for (auto & [inputId, input] : flake.inputs) { + auto vFlake = state.allocAttr(vInputs, inputId); + auto vPrimOp = state.allocValue(); + static auto primOp = new PrimOp(prim_callFlake, 1, state.symbols.create("callFlake")); + vPrimOp->type = tPrimOp; + vPrimOp->primOp = primOp; + auto vArg = state.allocValue(); + vArg->type = tNull; + auto lockedInput = lockedInputs.inputs.find(inputId); + assert(lockedInput != lockedInputs.inputs.end()); + // FIXME: leak + vArg->attrs = (Bindings *) new LazyInput{input.isFlake, lockedInput->second}; + mkApp(*vFlake, *vPrimOp, *vArg); + } + + auto & vSourceInfo = *state.allocValue(); + state.mkAttrs(vSourceInfo, 8); + emitSourceInfoAttrs(state, flake.lockedRef, *flake.sourceInfo, vSourceInfo); + vSourceInfo.attrs->sort(); + + vInputs.attrs->push_back(Attr(state.sSelf, &vRes)); + + vInputs.attrs->sort(); + + /* For convenience, put the outputs directly in the result, so you + can refer to an output of an input as 'inputs.foo.bar' rather + than 'inputs.foo.outputs.bar'. */ + auto vCall = *state.allocValue(); + state.eval(state.parseExprFromString( + "outputsFun: inputs: sourceInfo: let outputs = outputsFun inputs; in " + "outputs // sourceInfo // { inherit inputs; inherit outputs; inherit sourceInfo; }", "/"), vCall); + + auto vCall2 = *state.allocValue(); + auto vCall3 = *state.allocValue(); + state.callFunction(vCall, *flake.vOutputs, vCall2, noPos); + state.callFunction(vCall2, vInputs, vCall3, noPos); + state.callFunction(vCall3, vSourceInfo, vRes, noPos); + + vResFinal = vRes; +} + +void callFlake(EvalState & state, + const LockedFlake & lockedFlake, + Value & v) +{ + callFlake(state, lockedFlake.flake, lockedFlake.lockFile, v); +} + +// This function is exposed to be used in nix files. +static void prim_getFlake(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + callFlake(state, + lockFlake(state, parseFlakeRef(state.forceStringNoCtx(*args[0], pos)), + LockFlags { + .updateLockFile = false, + .useRegistries = !evalSettings.pureEval, + .allowMutable = !evalSettings.pureEval, + }), + v); +} + +static RegisterPrimOp r2("getFlake", 1, prim_getFlake); + +} + +Fingerprint LockedFlake::getFingerprint() const +{ + // FIXME: as an optimization, if the flake contains a lock file + // and we haven't changed it, then it's sufficient to use + // flake.sourceInfo.storePath for the fingerprint. + return hashString(htSHA256, + fmt("%s;%d;%d;%s", + flake.sourceInfo->storePath.to_string(), + flake.sourceInfo->info.revCount.value_or(0), + flake.sourceInfo->info.lastModified.value_or(0), + lockFile)); +} + +Flake::~Flake() { } + +} |