aboutsummaryrefslogtreecommitdiff
path: root/src/legacy/build-remote.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/legacy/build-remote.cc')
-rw-r--r--src/legacy/build-remote.cc396
1 files changed, 396 insertions, 0 deletions
diff --git a/src/legacy/build-remote.cc b/src/legacy/build-remote.cc
new file mode 100644
index 000000000..62ceef283
--- /dev/null
+++ b/src/legacy/build-remote.cc
@@ -0,0 +1,396 @@
+#include <algorithm>
+#include <set>
+#include <memory>
+#include <tuple>
+#if __APPLE__
+#include <sys/time.h>
+#endif
+
+#include "machines.hh"
+#include "shared.hh"
+#include "pathlocks.hh"
+#include "globals.hh"
+#include "serialise.hh"
+#include "build-result.hh"
+#include "store-api.hh"
+#include "derivations.hh"
+#include "strings.hh"
+#include "local-store.hh"
+#include "legacy.hh"
+#include "experimental-features.hh"
+#include "hash.hh"
+#include "build-remote.hh"
+
+namespace nix {
+
+static void handleAlarm(int sig) {
+}
+
+std::string escapeUri(std::string uri)
+{
+ std::replace(uri.begin(), uri.end(), '/', '_');
+ return uri;
+}
+
+static std::string currentLoad;
+
+static std::string makeLockFilename(const std::string & storeUri) {
+ // We include 48 bytes of escaped URI to give an idea of what the lock
+ // is on, then 16 bytes of hash to disambiguate.
+ // This avoids issues with the escaped URI being very long and causing
+ // path too long errors, while also avoiding any possibility of collision
+ // caused by simple truncation.
+ auto hash = hashString(HashType::SHA256, storeUri).to_string(Base::Base32, false);
+ return escapeUri(storeUri).substr(0, 48) + "-" + hash.substr(0, 16);
+}
+
+static AutoCloseFD openSlotLock(const Machine & m, uint64_t slot)
+{
+ return openLockFile(fmt("%s/%s-%d", currentLoad, makeLockFilename(m.storeUri), slot), true);
+}
+
+static bool allSupportedLocally(Store & store, const std::set<std::string>& requiredFeatures) {
+ for (auto & feature : requiredFeatures)
+ if (!store.systemFeatures.get().count(feature)) return false;
+ return true;
+}
+
+static int main_build_remote(int argc, char * * argv)
+{
+ {
+ logger = makeJSONLogger(*logger);
+
+ /* Ensure we don't get any SSH passphrase or host key popups. */
+ unsetenv("DISPLAY");
+ unsetenv("SSH_ASKPASS");
+
+ /* If we ever use the common args framework, make sure to
+ remove initPlugins below and initialize settings first.
+ */
+ if (argc != 2)
+ throw UsageError("called without required arguments");
+
+ verbosity = (Verbosity) std::stoll(argv[1]);
+
+ FdSource source(STDIN_FILENO);
+
+ /* Read the parent's settings. */
+ while (readInt(source)) {
+ auto name = readString(source);
+ auto value = readString(source);
+ settings.set(name, value);
+ }
+
+ auto maxBuildJobs = settings.maxBuildJobs;
+ settings.maxBuildJobs.set("1"); // hack to make tests with local?root= work
+
+ initPlugins();
+
+ auto store = openStore();
+
+ /* It would be more appropriate to use $XDG_RUNTIME_DIR, since
+ that gets cleared on reboot, but it wouldn't work on macOS. */
+ auto currentLoadName = "/current-load";
+ if (auto localStore = store.dynamic_pointer_cast<LocalFSStore>())
+ currentLoad = std::string { localStore->stateDir } + currentLoadName;
+ else
+ currentLoad = settings.nixStateDir + currentLoadName;
+
+ std::shared_ptr<Store> sshStore;
+ AutoCloseFD bestSlotLock;
+
+ auto machines = getMachines();
+ debug("got %d remote builders", machines.size());
+
+ if (machines.empty()) {
+ std::cerr << "# decline-permanently\n";
+ return 0;
+ }
+
+ std::optional<StorePath> drvPath;
+ std::string storeUri;
+
+ while (true) {
+
+ try {
+ auto s = readString(source);
+ if (s != "try") return 0;
+ } catch (EndOfFile &) { return 0; }
+
+ auto amWilling = readInt(source);
+ auto neededSystem = readString(source);
+ drvPath = store->parseStorePath(readString(source));
+ auto requiredFeatures = readStrings<std::set<std::string>>(source);
+
+ /* It would be possible to build locally after some builds clear out,
+ so don't show the warning now: */
+ bool couldBuildLocally = maxBuildJobs > 0
+ && ( neededSystem == settings.thisSystem
+ || settings.extraPlatforms.get().count(neededSystem) > 0)
+ && allSupportedLocally(*store, requiredFeatures);
+ /* It's possible to build this locally right now: */
+ bool canBuildLocally = amWilling && couldBuildLocally;
+
+ /* Error ignored here, will be caught later */
+ mkdir(currentLoad.c_str(), 0777);
+
+ while (true) {
+ bestSlotLock.reset();
+ AutoCloseFD lock = openLockFile(currentLoad + "/main-lock", true);
+ lockFile(lock.get(), ltWrite, true);
+
+ bool rightType = false;
+
+ Machine * bestMachine = nullptr;
+ uint64_t bestLoad = 0;
+ for (auto & m : machines) {
+ debug("considering building on remote machine '%s'", m.storeUri);
+
+ if (m.enabled &&
+ m.systemSupported(neededSystem) &&
+ m.allSupported(requiredFeatures) &&
+ m.mandatoryMet(requiredFeatures))
+ {
+ rightType = true;
+ AutoCloseFD free;
+ uint64_t load = 0;
+ for (uint64_t slot = 0; slot < m.maxJobs; ++slot) {
+ auto slotLock = openSlotLock(m, slot);
+ if (lockFile(slotLock.get(), ltWrite, false)) {
+ if (!free) {
+ free = std::move(slotLock);
+ }
+ } else {
+ ++load;
+ }
+ }
+ if (!free) {
+ continue;
+ }
+ bool best = false;
+ if (!bestSlotLock) {
+ best = true;
+ } else if (load / m.speedFactor < bestLoad / bestMachine->speedFactor) {
+ best = true;
+ } else if (load / m.speedFactor == bestLoad / bestMachine->speedFactor) {
+ if (m.speedFactor > bestMachine->speedFactor) {
+ best = true;
+ } else if (m.speedFactor == bestMachine->speedFactor) {
+ if (load < bestLoad) {
+ best = true;
+ }
+ }
+ }
+ if (best) {
+ bestLoad = load;
+ bestSlotLock = std::move(free);
+ bestMachine = &m;
+ }
+ }
+ }
+
+ if (!bestSlotLock) {
+ if (rightType && !canBuildLocally)
+ std::cerr << "# postpone\n";
+ else
+ {
+ // add the template values.
+ std::string drvstr;
+ if (drvPath.has_value())
+ drvstr = drvPath->to_string();
+ else
+ drvstr = "<unknown>";
+
+ std::string machinesFormatted;
+
+ for (auto & m : machines) {
+ machinesFormatted += HintFmt(
+ "\n([%s], %s, [%s], [%s])",
+ concatStringsSep<StringSet>(", ", m.systemTypes),
+ m.maxJobs,
+ concatStringsSep<StringSet>(", ", m.supportedFeatures),
+ concatStringsSep<StringSet>(", ", m.mandatoryFeatures)
+ ).str();
+ }
+
+ auto error = HintFmt(
+ "Failed to find a machine for remote build!\n"
+ "derivation: %s\n"
+ "required (system, features): (%s, [%s])\n"
+ "%s available machines:\n"
+ "(systems, maxjobs, supportedFeatures, mandatoryFeatures)%s",
+ drvstr,
+ neededSystem,
+ concatStringsSep<StringSet>(", ", requiredFeatures),
+ machines.size(),
+ Uncolored(machinesFormatted)
+ );
+
+ printMsg(couldBuildLocally ? lvlChatty : lvlWarn, error.str());
+
+ std::cerr << "# decline\n";
+ }
+ break;
+ }
+
+#if __APPLE__
+ futimes(bestSlotLock.get(), nullptr);
+#else
+ futimens(bestSlotLock.get(), nullptr);
+#endif
+
+ lock.reset();
+
+ try {
+
+ Activity act(*logger, lvlTalkative, actUnknown, fmt("connecting to '%s'", bestMachine->storeUri));
+
+ sshStore = bestMachine->openStore();
+ sshStore->connect();
+ storeUri = bestMachine->storeUri;
+
+ } catch (std::exception & e) {
+ auto msg = chomp(drainFD(5, false));
+ printError("cannot build on '%s': %s%s",
+ bestMachine->storeUri, e.what(),
+ msg.empty() ? "" : ": " + msg);
+ bestMachine->enabled = false;
+ continue;
+ }
+
+ goto connected;
+ }
+ }
+
+connected:
+ close(5);
+
+ assert(sshStore);
+
+ std::cerr << "# accept\n" << storeUri << "\n";
+
+ auto inputs = readStrings<PathSet>(source);
+ auto wantedOutputs = readStrings<StringSet>(source);
+
+ auto lockFileName = currentLoad + "/" + makeLockFilename(storeUri) + ".upload-lock";
+
+ AutoCloseFD uploadLock = openLockFile(lockFileName, true);
+
+ {
+ Activity act(*logger, lvlTalkative, actUnknown, fmt("waiting for the upload lock to '%s'", storeUri));
+
+ auto old = signal(SIGALRM, handleAlarm);
+ alarm(15 * 60);
+ if (!lockFile(uploadLock.get(), ltWrite, true))
+ printError("somebody is hogging the upload lock for '%s', continuing...");
+ alarm(0);
+ signal(SIGALRM, old);
+ }
+
+ auto substitute = settings.buildersUseSubstitutes ? Substitute : NoSubstitute;
+
+ {
+ Activity act(*logger, lvlTalkative, actUnknown, fmt("copying dependencies to '%s'", storeUri));
+ copyPaths(*store, *sshStore, store->parseStorePathSet(inputs), NoRepair, NoCheckSigs, substitute);
+ }
+
+ uploadLock.reset();
+
+ auto drv = store->readDerivation(*drvPath);
+
+ std::optional<BuildResult> optResult;
+
+ // If we don't know whether we are trusted (e.g. `ssh://`
+ // stores), we assume we are. This is necessary for backwards
+ // compat.
+ bool trustedOrLegacy = ({
+ std::optional trusted = sshStore->isTrustedClient();
+ !trusted || *trusted;
+ });
+
+ // See the very large comment in `case WorkerProto::Op::BuildDerivation:` in
+ // `src/libstore/daemon.cc` that explains the trust model here.
+ //
+ // This condition mirrors that: that code enforces the "rules" outlined there;
+ // we do the best we can given those "rules".
+ if (trustedOrLegacy || drv.type().isCA()) {
+ // Hijack the inputs paths of the derivation to include all
+ // the paths that come from the `inputDrvs` set. We don’t do
+ // that for the derivations whose `inputDrvs` is empty
+ // because:
+ //
+ // 1. It’s not needed
+ //
+ // 2. Changing the `inputSrcs` set changes the associated
+ // output ids, which break CA derivations
+ if (!drv.inputDrvs.map.empty())
+ drv.inputSrcs = store->parseStorePathSet(inputs);
+ optResult = sshStore->buildDerivation(*drvPath, (const BasicDerivation &) drv);
+ auto & result = *optResult;
+ if (!result.success())
+ throw Error("build of '%s' on '%s' failed: %s", store->printStorePath(*drvPath), storeUri, result.errorMsg);
+ } else {
+ copyClosure(*store, *sshStore, StorePathSet {*drvPath}, NoRepair, NoCheckSigs, substitute);
+ auto res = sshStore->buildPathsWithResults({
+ DerivedPath::Built {
+ .drvPath = makeConstantStorePathRef(*drvPath),
+ .outputs = OutputsSpec::All {},
+ }
+ });
+ // One path to build should produce exactly one build result
+ assert(res.size() == 1);
+ optResult = std::move(res[0]);
+ }
+
+
+ auto outputHashes = staticOutputHashes(*store, drv);
+ std::set<Realisation> missingRealisations;
+ StorePathSet missingPaths;
+ if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations) && !drv.type().hasKnownOutputPaths()) {
+ for (auto & outputName : wantedOutputs) {
+ auto thisOutputHash = outputHashes.at(outputName);
+ auto thisOutputId = DrvOutput{ thisOutputHash, outputName };
+ if (!store->queryRealisation(thisOutputId)) {
+ debug("missing output %s", outputName);
+ assert(optResult);
+ auto & result = *optResult;
+ auto i = result.builtOutputs.find(outputName);
+ assert(i != result.builtOutputs.end());
+ auto & newRealisation = i->second;
+ missingRealisations.insert(newRealisation);
+ missingPaths.insert(newRealisation.outPath);
+ }
+ }
+ } else {
+ auto outputPaths = drv.outputsAndOptPaths(*store);
+ for (auto & [outputName, hopefullyOutputPath] : outputPaths) {
+ assert(hopefullyOutputPath.second);
+ if (!store->isValidPath(*hopefullyOutputPath.second))
+ missingPaths.insert(*hopefullyOutputPath.second);
+ }
+ }
+
+ if (!missingPaths.empty()) {
+ Activity act(*logger, lvlTalkative, actUnknown, fmt("copying outputs from '%s'", storeUri));
+ if (auto localStore = store.dynamic_pointer_cast<LocalStore>())
+ for (auto & path : missingPaths)
+ localStore->locksHeld.insert(store->printStorePath(path)); /* FIXME: ugly */
+ copyPaths(*sshStore, *store, missingPaths, NoRepair, NoCheckSigs, NoSubstitute);
+ }
+ // XXX: Should be done as part of `copyPaths`
+ for (auto & realisation : missingRealisations) {
+ // Should hold, because if the feature isn't enabled the set
+ // of missing realisations should be empty
+ experimentalFeatureSettings.require(Xp::CaDerivations);
+ store->registerDrvOutput(realisation);
+ }
+
+ return 0;
+ }
+}
+
+void registerBuildRemote() {
+ LegacyCommands::add("build-remote", main_build_remote);
+}
+
+}