#include "worker.hh" #include "substitution-goal.hh" #include "nar-info.hh" #include "signals.hh" #include "finally.hh" #include #include namespace nix { PathSubstitutionGoal::PathSubstitutionGoal( const StorePath & storePath, Worker & worker, bool isDependency, RepairFlag repair, std::optional ca ) : Goal(worker, isDependency) , storePath(storePath) , repair(repair) , ca(ca) { name = fmt("substitution of '%s'", worker.store.printStorePath(this->storePath)); trace("created"); maintainExpectedSubstitutions = worker.expectedSubstitutions.addTemporarily(1); } PathSubstitutionGoal::~PathSubstitutionGoal() { cleanup(); } Goal::WorkResult PathSubstitutionGoal::done( ExitCode result, BuildResult::Status status, std::optional errorMsg) { BuildResult buildResult{.status = status}; if (errorMsg) { debug(*errorMsg); buildResult.errorMsg = *errorMsg; } return WorkResult{result, std::move(buildResult)}; } kj::Promise> PathSubstitutionGoal::workImpl() noexcept try { trace("init"); worker.store.addTempRoot(storePath); /* If the path already exists we're done. */ if (!repair && worker.store.isValidPath(storePath)) { return {done(ecSuccess, BuildResult::AlreadyValid)}; } if (settings.readOnlyMode) throw Error("cannot substitute path '%s' - no write access to the Nix store", worker.store.printStorePath(storePath)); subs = settings.useSubstitutes ? getDefaultSubstituters() : std::list>(); return tryNext(); } catch (...) { return {std::current_exception()}; } kj::Promise> PathSubstitutionGoal::tryNext() noexcept try { trace("trying next substituter"); cleanup(); if (subs.size() == 0) { /* None left. Terminate this goal and let someone else deal with it. */ if (substituterFailed) { worker.failedSubstitutions++; } /* Hack: don't indicate failure if there were no substituters. In that case the calling derivation should just do a build. */ co_return done( substituterFailed ? ecFailed : ecNoSubstituters, BuildResult::NoSubstituters, fmt("path '%s' is required, but there is no substituter that can build it", worker.store.printStorePath(storePath))); } sub = subs.front(); subs.pop_front(); if (ca) { subPath = sub->makeFixedOutputPathFromCA( std::string { storePath.name() }, ContentAddressWithReferences::withoutRefs(*ca)); if (sub->storeDir == worker.store.storeDir) assert(subPath == storePath); } else if (sub->storeDir != worker.store.storeDir) { co_return co_await tryNext(); } do { try { // FIXME: make async info = sub->queryPathInfo(subPath ? *subPath : storePath); break; } catch (InvalidPath &) { } catch (SubstituterDisabled &) { if (!settings.tryFallback) { throw; } } catch (Error & e) { if (settings.tryFallback) { logError(e.info()); } else { throw; } } co_return co_await tryNext(); } while (false); if (info->path != storePath) { if (info->isContentAddressed(*sub) && info->references.empty()) { auto info2 = std::make_shared(*info); info2->path = storePath; info = info2; } else { printError("asked '%s' for '%s' but got '%s'", sub->getUri(), worker.store.printStorePath(storePath), sub->printStorePath(info->path)); co_return co_await tryNext(); } } /* Update the total expected download size. */ auto narInfo = std::dynamic_pointer_cast(info); maintainExpectedNar = worker.expectedNarSize.addTemporarily(info->narSize); maintainExpectedDownload = narInfo && narInfo->fileSize ? worker.expectedDownloadSize.addTemporarily(narInfo->fileSize) : nullptr; /* Bail out early if this substituter lacks a valid signature. LocalStore::addToStore() also checks for this, but only after we've downloaded the path. */ if (!sub->isTrusted && worker.store.pathInfoIsUntrusted(*info)) { warn("ignoring substitute for '%s' from '%s', as it's not signed by any of the keys in 'trusted-public-keys'", worker.store.printStorePath(storePath), sub->getUri()); co_return co_await tryNext(); } /* To maintain the closure invariant, we first have to realise the paths referenced by this one. */ kj::Vector>>> dependencies; for (auto & i : info->references) if (i != storePath) /* ignore self-references */ dependencies.add(worker.goalFactory().makePathSubstitutionGoal(i)); if (!dependencies.empty()) {/* to prevent hang (no wake-up event) */ (co_await waitForGoals(dependencies.releaseAsArray())).value(); } co_return co_await referencesValid(); } catch (...) { co_return result::failure(std::current_exception()); } kj::Promise> PathSubstitutionGoal::referencesValid() noexcept try { trace("all references realised"); if (nrFailed > 0) { return {done( nrNoSubstituters > 0 || nrIncompleteClosure > 0 ? ecIncompleteClosure : ecFailed, BuildResult::DependencyFailed, fmt("some references of path '%s' could not be realised", worker.store.printStorePath(storePath)))}; } for (auto & i : info->references) if (i != storePath) /* ignore self-references */ assert(worker.store.isValidPath(i)); return tryToRun(); } catch (...) { return {std::current_exception()}; } kj::Promise> PathSubstitutionGoal::tryToRun() noexcept try { trace("trying to run"); if (!slotToken.valid()) { slotToken = co_await worker.substitutions.acquire(); } maintainRunningSubstitutions = worker.runningSubstitutions.addTemporarily(1); auto pipe = kj::newPromiseAndCrossThreadFulfiller(); outPipe = kj::mv(pipe.fulfiller); thr = std::async(std::launch::async, [this]() { /* Wake up the worker loop when we're done. */ Finally updateStats([this]() { outPipe->fulfill(); }); auto & fetchPath = subPath ? *subPath : storePath; try { ReceiveInterrupts receiveInterrupts; Activity act(*logger, actSubstitute, Logger::Fields{worker.store.printStorePath(storePath), sub->getUri()}); PushActivity pact(act.id); copyStorePath( *sub, worker.store, fetchPath, repair, sub->isTrusted ? NoCheckSigs : CheckSigs ); } catch (const EndOfFile &) { throw EndOfFile( "NAR for '%s' fetched from '%s' is incomplete", sub->printStorePath(fetchPath), sub->getUri() ); } }); co_await pipe.promise; co_return co_await finished(); } catch (...) { co_return result::failure(std::current_exception()); } kj::Promise> PathSubstitutionGoal::finished() noexcept try { trace("substitute finished"); do { try { slotToken = {}; thr.get(); break; } catch (std::exception & e) { printError(e.what()); /* Cause the parent build to fail unless --fallback is given, or the substitute has disappeared. The latter case behaves the same as the substitute never having existed in the first place. */ try { throw; } catch (SubstituteGone &) { } catch (...) { substituterFailed = true; } } /* Try the next substitute. */ co_return co_await tryNext(); } while (false); worker.markContentsGood(storePath); printMsg(lvlChatty, "substitution of path '%s' succeeded", worker.store.printStorePath(storePath)); maintainRunningSubstitutions.reset(); maintainExpectedSubstitutions.reset(); worker.doneSubstitutions++; worker.doneDownloadSize += maintainExpectedDownload.delta(); maintainExpectedDownload.reset(); worker.doneNarSize += maintainExpectedNar.delta(); maintainExpectedNar.reset(); co_return done(ecSuccess, BuildResult::Substituted); } catch (...) { co_return result::failure(std::current_exception()); } void PathSubstitutionGoal::cleanup() { try { if (thr.valid()) { // FIXME: signal worker thread to quit. thr.get(); } } catch (...) { ignoreExceptionInDestructor(); } } }