diff options
Diffstat (limited to 'src/libstore/build.cc')
-rw-r--r-- | src/libstore/build.cc | 1758 |
1 files changed, 1758 insertions, 0 deletions
diff --git a/src/libstore/build.cc b/src/libstore/build.cc new file mode 100644 index 000000000..60e72c9dc --- /dev/null +++ b/src/libstore/build.cc @@ -0,0 +1,1758 @@ +#include <map> +#include <boost/shared_ptr.hpp> +#include <boost/weak_ptr.hpp> +#include <boost/enable_shared_from_this.hpp> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> + +#include "build.hh" +#include "references.hh" +#include "pathlocks.hh" +#include "globals.hh" + + +/* !!! TODO storeExprFromPath shouldn't be used here */ + + +static string pathNullDevice = "/dev/null"; + + +/* Forward definition. */ +class Worker; + + +/* A pointer to a goal. */ +class Goal; +typedef shared_ptr<Goal> GoalPtr; +typedef weak_ptr<Goal> WeakGoalPtr; + +/* Set of goals. */ +typedef set<GoalPtr> Goals; +typedef set<WeakGoalPtr> WeakGoals; + +/* A map of paths to goals (and the other way around). */ +typedef map<Path, WeakGoalPtr> WeakGoalMap; + + + +class Goal : public enable_shared_from_this<Goal> +{ +protected: + + /* Backlink to the worker. */ + Worker & worker; + + /* Goals that this goal is waiting for. */ + Goals waitees; + + /* Goals waiting for this one to finish. Must use weak pointers + here to prevent cycles. */ + WeakGoals waiters; + + /* Number of goals we are/were waiting for that have failed. */ + unsigned int nrFailed; + + /* Whether amDone() has been called. */ + bool done; + + + Goal(Worker & worker) : worker(worker) + { + done = false; + nrFailed = 0; + } + + virtual ~Goal() + { + printMsg(lvlVomit, "goal destroyed"); + } + +public: + virtual void work() = 0; + + virtual string name() = 0; + + void addWaitee(GoalPtr waitee); + + virtual void waiteeDone(GoalPtr waitee, bool success); + + virtual void writeLog(int fd, const unsigned char * buf, size_t count) + { + abort(); + } + + void trace(const format & f); + +protected: + void amDone(bool success = true); +}; + + +/* A mapping used to remember for each child process to what goal it + belongs, and a file descriptor for receiving log data. */ +struct Child +{ + WeakGoalPtr goal; + int fdOutput; + bool inBuildSlot; +}; + +typedef map<pid_t, Child> Children; + + +/* The worker class. */ +class Worker +{ +private: + + /* Note: the worker should only have strong pointers to the + top-level goals. */ + + /* The top-level goals of the worker. */ + Goals topGoals; + + /* Goals that are ready to do some work. */ + WeakGoals awake; + + /* Goals waiting for a build slot. */ + WeakGoals wantingToBuild; + + /* Child processes currently running. */ + Children children; + + /* Number of build slots occupied. Not all child processes + (namely build hooks) count as occupied build slots. */ + unsigned int nrChildren; + + /* Maps used to prevent multiple instantiations of a goal for the + same expression / path. */ + WeakGoalMap derivationGoals; + WeakGoalMap substitutionGoals; + +public: + + Worker(); + ~Worker(); + + /* Make a goal (with caching). */ + GoalPtr makeDerivationGoal(const Path & drvPath); + GoalPtr makeSubstitutionGoal(const Path & storePath); + + /* Remove a dead goal. */ + void removeGoal(GoalPtr goal); + + /* Wake up a goal (i.e., there is something for it to do). */ + void wakeUp(GoalPtr goal); + + /* Can we start another child process? */ + bool canBuildMore(); + + /* Registers / unregisters a running child process. */ + void childStarted(GoalPtr goal, pid_t pid, int fdOutput, + bool inBuildSlot); + void childTerminated(pid_t pid, bool wakeSleepers = true); + + /* Add a goal to the set of goals waiting for a build slot. */ + void waitForBuildSlot(GoalPtr goal, bool reallyWait = false); + + /* Loop until the specified top-level goal has finished. Returns + true if it has finished succesfully. */ + bool run(const Goals & topGoals); + + /* Wait for input to become available. */ + void waitForInput(); +}; + + +class SubstError : public Error +{ +public: + SubstError(const format & f) : Error(f) { }; +}; + + +class BuildError : public Error +{ +public: + BuildError(const format & f) : Error(f) { }; +}; + + + +////////////////////////////////////////////////////////////////////// + + +void Goal::addWaitee(GoalPtr waitee) +{ + waitees.insert(waitee); + waitee->waiters.insert(shared_from_this()); +} + + +void Goal::waiteeDone(GoalPtr waitee, bool success) +{ + assert(waitees.find(waitee) != waitees.end()); + waitees.erase(waitee); + + if (!success) ++nrFailed; + + if (waitees.empty() || (!success && !keepGoing)) { + + /* If we failed and keepGoing is not set, we remove all + remaining waitees. */ + for (Goals::iterator i = waitees.begin(); i != waitees.end(); ++i) { + GoalPtr goal = *i; + WeakGoals waiters2; + for (WeakGoals::iterator j = goal->waiters.begin(); + j != goal->waiters.end(); ++j) + if (j->lock() != shared_from_this()) + waiters2.insert(*j); + goal->waiters = waiters2; + } + waitees.clear(); + + worker.wakeUp(shared_from_this()); + } +} + + +void Goal::amDone(bool success) +{ + trace("done"); + assert(!done); + done = true; + for (WeakGoals::iterator i = waiters.begin(); i != waiters.end(); ++i) { + GoalPtr goal = i->lock(); + if (goal) goal->waiteeDone(shared_from_this(), success); + } + waiters.clear(); + worker.removeGoal(shared_from_this()); +} + + +void Goal::trace(const format & f) +{ + debug(format("%1%: %2%") % name() % f); +} + + + +////////////////////////////////////////////////////////////////////// + + +/* Common initialisation performed in child processes. */ +void commonChildInit(Pipe & logPipe) +{ + /* Put the child in a separate process group so that it doesn't + receive terminal signals. */ + if (setpgid(0, 0) == -1) + throw SysError(format("setting process group")); + + /* Dup the write side of the logger pipe into stderr. */ + if (dup2(logPipe.writeSide, STDERR_FILENO) == -1) + throw SysError("cannot pipe standard error into log file"); + logPipe.readSide.close(); + + /* Dup stderr to stdin. */ + if (dup2(STDERR_FILENO, STDOUT_FILENO) == -1) + throw SysError("cannot dup stderr into stdout"); + + /* Reroute stdin to /dev/null. */ + int fdDevNull = open(pathNullDevice.c_str(), O_RDWR); + if (fdDevNull == -1) + throw SysError(format("cannot open `%1%'") % pathNullDevice); + if (dup2(fdDevNull, STDIN_FILENO) == -1) + throw SysError("cannot dup null device into stdin"); +} + + +/* Convert a string list to an array of char pointers. Careful: the + string list should outlive the array. */ +const char * * strings2CharPtrs(const Strings & ss) +{ + const char * * arr = new const char * [ss.size() + 1]; + const char * * p = arr; + for (Strings::const_iterator i = ss.begin(); i != ss.end(); ++i) + *p++ = i->c_str(); + *p = 0; + return arr; +} + + + +////////////////////////////////////////////////////////////////////// + + +class DerivationGoal : public Goal +{ +private: + /* The path of the derivation store expression. */ + Path drvPath; + + /* The derivation store expression stored at drvPath. */ + Derivation drv; + + /* The remainder is state held during the build. */ + + /* Locks on the output paths. */ + PathLocks outputLocks; + + /* All input paths (that is, the union of FS closures of the + immediate input paths). */ + PathSet inputPaths; + + /* Referenceable paths (i.e., input and output paths). */ + PathSet allPaths; + + /* The process ID of the builder. */ + Pid pid; + + /* The temporary directory. */ + Path tmpDir; + + /* File descriptor for the log file. */ + AutoCloseFD fdLogFile; + + /* Pipe for the builder's standard output/error. */ + Pipe logPipe; + + /* Pipes for talking to the build hook (if any). */ + Pipe toHook; + Pipe fromHook; + + typedef void (DerivationGoal::*GoalState)(); + GoalState state; + +public: + DerivationGoal(const Path & drvPath, Worker & worker); + ~DerivationGoal(); + + void work(); + +private: + /* The states. */ + void init(); + void haveStoreExpr(); + void inputsRealised(); + void tryToBuild(); + void buildDone(); + + /* Is the build hook willing to perform the build? */ + typedef enum {rpAccept, rpDecline, rpPostpone, rpDone} HookReply; + HookReply tryBuildHook(); + + /* Synchronously wait for a build hook to finish. */ + void terminateBuildHook(); + + /* Acquires locks on the output paths and gathers information + about the build (e.g., the input closures). During this + process its possible that we find out that the build is + unnecessary, in which case we return false (this is not an + error condition!). */ + bool prepareBuild(); + + /* Start building a derivation. */ + void startBuilder(); + + /* Must be called after the output paths have become valid (either + due to a successful build or hook, or because they already + were). */ + void computeClosure(); + + /* Open a log file and a pipe to it. */ + void openLogFile(); + + /* Common initialisation to be performed in child processes (i.e., + both in builders and in build hooks. */ + void initChild(); + + /* Delete the temporary directory, if we have one. */ + void deleteTmpDir(bool force); + + /* Callback used by the worker to write to the log. */ + void writeLog(int fd, const unsigned char * buf, size_t count); + + /* Return true iff all output paths are valid. */ + bool allOutputsValid(); + + string name(); +}; + + +DerivationGoal::DerivationGoal(const Path & drvPath, Worker & worker) + : Goal(worker) +{ + this->drvPath = drvPath; + state = &DerivationGoal::init; +} + + +DerivationGoal::~DerivationGoal() +{ + if (pid != -1) worker.childTerminated(pid); + + /* Careful: we should never ever throw an exception from a + destructor. */ + try { + deleteTmpDir(false); + } catch (Error & e) { + printMsg(lvlError, format("error (ignored): %1%") % e.msg()); + } +} + + +void DerivationGoal::work() +{ + (this->*state)(); +} + + +void DerivationGoal::init() +{ + trace("init"); + + /* The first thing to do is to make sure that the store expression + exists. If it doesn't, it may be created through a + substitute. */ + addWaitee(worker.makeSubstitutionGoal(drvPath)); + + state = &DerivationGoal::haveStoreExpr; +} + + +void DerivationGoal::haveStoreExpr() +{ + trace("loading derivation"); + + if (nrFailed != 0) { + printMsg(lvlError, + format("cannot build missing derivation `%1%'") + % drvPath); + amDone(false); + return; + } + + assert(isValidPath(drvPath)); + + /* Get the derivation. */ + drv = derivationFromPath(drvPath); + + /* If all the outputs already exist, then we're done. */ + if (allOutputsValid()) { + amDone(true); + return; + } + + /* Inputs must be built before we can build this goal. */ + for (PathSet::iterator i = drv.inputDrvs.begin(); + i != drv.inputDrvs.end(); ++i) + addWaitee(worker.makeDerivationGoal(*i)); + + for (PathSet::iterator i = drv.inputSrcs.begin(); + i != drv.inputSrcs.end(); ++i) + addWaitee(worker.makeSubstitutionGoal(*i)); + + state = &DerivationGoal::inputsRealised; +} + + +void DerivationGoal::inputsRealised() +{ + trace("all inputs realised"); + + if (nrFailed != 0) { + printMsg(lvlError, + format("cannot build derivation `%1%': " + "%2% inputs could not be realised") + % drvPath % nrFailed); + amDone(false); + return; + } + + /* Okay, try to build. Note that here we don't wait for a build + slot to become available, since we don't need one if there is a + build hook. */ + state = &DerivationGoal::tryToBuild; + worker.wakeUp(shared_from_this()); +} + + +void DerivationGoal::tryToBuild() +{ + trace("trying to build"); + + try { + + /* Is the build hook willing to accept this job? */ + switch (tryBuildHook()) { + case rpAccept: + /* Yes, it has started doing so. Wait until we get + EOF from the hook. */ + state = &DerivationGoal::buildDone; + return; + case rpPostpone: + /* Not now; wait until at least one child finishes. */ + worker.waitForBuildSlot(shared_from_this(), true); + return; + case rpDecline: + /* We should do it ourselves. */ + break; + case rpDone: + /* Somebody else did it. */ + amDone(); + return; + } + + /* Make sure that we are allowed to start a build. */ + if (!worker.canBuildMore()) { + worker.waitForBuildSlot(shared_from_this()); + return; + } + + /* Acquire locks and such. If we then see that the build has + been done by somebody else, we're done. */ + if (!prepareBuild()) { + amDone(); + return; + } + + /* Okay, we have to build. */ + startBuilder(); + + } catch (BuildError & e) { + printMsg(lvlError, e.msg()); + amDone(false); + return; + } + + /* This state will be reached when we get EOF on the child's + log pipe. */ + state = &DerivationGoal::buildDone; +} + + +void DerivationGoal::buildDone() +{ + trace("build done"); + + /* Since we got an EOF on the logger pipe, the builder is presumed + to have terminated. In fact, the builder could also have + simply have closed its end of the pipe --- just don't do that + :-) */ + /* !!! this could block! */ + pid_t savedPid = pid; + int status = pid.wait(true); + + /* So the child is gone now. */ + worker.childTerminated(savedPid); + + /* Close the read side of the logger pipe. */ + logPipe.readSide.close(); + + /* Close the log file. */ + fdLogFile.close(); + + debug(format("builder process for `%1%' finished") % drvPath); + + /* Check the exit status. */ + if (!statusOk(status)) { + deleteTmpDir(false); + printMsg(lvlError, format("builder for `%1%' %2%") + % drvPath % statusToString(status)); + amDone(false); + return; + } + + deleteTmpDir(true); + + /* Compute the FS closure of the outputs and register them as + being valid. */ + try { + computeClosure(); + } catch (BuildError & e) { + printMsg(lvlError, e.msg()); + amDone(false); + return; + } + + amDone(); +} + + +static string readLine(int fd) +{ + string s; + while (1) { + char ch; + ssize_t rd = read(fd, &ch, 1); + if (rd == -1) { + if (errno != EINTR) + throw SysError("reading a line"); + } else if (rd == 0) + throw Error("unexpected EOF reading a line"); + else { + if (ch == '\n') return s; + s += ch; + } + } +} + + +static void writeLine(int fd, string s) +{ + s += '\n'; + writeFull(fd, (const unsigned char *) s.c_str(), s.size()); +} + + +/* !!! ugly hack */ +static void drain(int fd) +{ + unsigned char buffer[1024]; + while (1) { + ssize_t rd = read(fd, buffer, sizeof buffer); + if (rd == -1) { + if (errno != EINTR) + throw SysError("draining"); + } else if (rd == 0) break; + else writeFull(STDERR_FILENO, buffer, rd); + } +} + + +PathSet outputPaths(const DerivationOutputs & outputs) +{ + PathSet paths; + for (DerivationOutputs::const_iterator i = outputs.begin(); + i != outputs.end(); ++i) + paths.insert(i->second.path); + return paths; +} + + +string showPaths(const PathSet & paths) +{ + string s; + for (PathSet::const_iterator i = paths.begin(); + i != paths.end(); ++i) + { + if (s.size() != 0) s += ", "; + s += "`" + *i + "'"; + } + return s; +} + + +DerivationGoal::HookReply DerivationGoal::tryBuildHook() +{ + Path buildHook = getEnv("NIX_BUILD_HOOK"); + if (buildHook == "") return rpDecline; + buildHook = absPath(buildHook); + + /* Create a directory where we will store files used for + communication between us and the build hook. */ + tmpDir = createTempDir(); + + /* Create the log file and pipe. */ + openLogFile(); + + /* Create the communication pipes. */ + toHook.create(); + fromHook.create(); + + /* Fork the hook. */ + pid = fork(); + switch (pid) { + + case -1: + throw SysError("unable to fork"); + + case 0: + try { /* child */ + + initChild(); + + execl(buildHook.c_str(), buildHook.c_str(), + (worker.canBuildMore() ? (string) "1" : "0").c_str(), + thisSystem.c_str(), + drv.platform.c_str(), + drvPath.c_str(), 0); + + throw SysError(format("executing `%1%'") % buildHook); + + } catch (exception & e) { + cerr << format("build error: %1%\n") % e.what(); + } + _exit(1); + } + + /* parent */ + logPipe.writeSide.close(); + worker.childStarted(shared_from_this(), + pid, logPipe.readSide, false); + + fromHook.writeSide.close(); + toHook.readSide.close(); + + /* Read the first line of input, which should be a word indicating + whether the hook wishes to perform the build. !!! potential + for deadlock here: we should also read from the child's logger + pipe. */ + string reply; + try { + reply = readLine(fromHook.readSide); + } catch (Error & e) { + drain(logPipe.readSide); + throw; + } + + debug(format("hook reply is `%1%'") % reply); + + if (reply == "decline" || reply == "postpone") { + /* Clean up the child. !!! hacky / should verify */ + drain(logPipe.readSide); + terminateBuildHook(); + return reply == "decline" ? rpDecline : rpPostpone; + } + + else if (reply == "accept") { + + /* Acquire locks and such. If we then see that the output + paths are now valid, we're done. */ + if (!prepareBuild()) { + /* Tell the hook to exit. */ + writeLine(toHook.writeSide, "cancel"); + terminateBuildHook(); + return rpDone; + } + + printMsg(lvlInfo, format("running hook to build path(s) %1%") + % showPaths(outputPaths(drv.outputs))); + + /* Write the information that the hook needs to perform the + build, i.e., the set of input paths (including closure + expressions), the set of output paths, and [!!!]. */ + + Path inputListFN = tmpDir + "/inputs"; + Path outputListFN = tmpDir + "/outputs"; + + string s; + for (PathSet::iterator i = inputPaths.begin(); + i != inputPaths.end(); ++i) + s += *i + "\n"; + writeStringToFile(inputListFN, s); + + s = ""; + for (DerivationOutputs::iterator i = drv.outputs.begin(); + i != drv.outputs.end(); ++i) + s += i->second.path + "\n"; + writeStringToFile(outputListFN, s); + + /* Tell the hook to proceed. */ + writeLine(toHook.writeSide, "okay"); + + return rpAccept; + } + + else throw Error(format("bad hook reply `%1%'") % reply); +} + + +void DerivationGoal::terminateBuildHook() +{ + /* !!! drain stdout of hook */ + debug("terminating build hook"); + pid_t savedPid = pid; + pid.wait(true); + worker.childTerminated(savedPid, false); + fromHook.readSide.close(); + toHook.writeSide.close(); + fdLogFile.close(); + logPipe.readSide.close(); + deleteTmpDir(true); /* get rid of the hook's temporary directory */ +} + + +bool DerivationGoal::prepareBuild() +{ + /* Obtain locks on all output paths. The locks are automatically + released when we exit this function or Nix crashes. */ + /* !!! BUG: this could block, which is not allowed. */ + outputLocks.lockPaths(outputPaths(drv.outputs)); + + /* Now check again whether the outputs are valid. This is because + another process may have started building in parallel. After + it has finished and released the locks, we can (and should) + reuse its results. (Strictly speaking the first check can be + omitted, but that would be less efficient.) Note that since we + now hold the locks on the output paths, no other process can + build this expression, so no further checks are necessary. */ + if (allOutputsValid()) { + debug(format("skipping build of derivation `%1%', someone beat us to it") + % drvPath); + outputLocks.setDeletion(true); + return false; + } + + /* Gather information necessary for computing the closure and/or + running the build hook. */ + + /* The outputs are referenceable paths. */ + for (DerivationOutputs::iterator i = drv.outputs.begin(); + i != drv.outputs.end(); ++i) + { + debug(format("building path `%1%'") % i->second.path); + allPaths.insert(i->second.path); + } + + /* Determine the full set of input paths. */ + + /* First, the input derivations. */ + for (PathSet::iterator i = drv.inputDrvs.begin(); + i != drv.inputDrvs.end(); ++i) + { + /* Add all the output closures of the input derivation `*i' as + input paths. !!! there should be a way to indicate + specific outputs. */ + /* !!! is `*i' present? */ + assert(isValidPath(*i)); + Derivation inDrv = derivationFromPath(*i); + for (DerivationOutputs::iterator j = inDrv.outputs.begin(); + j != inDrv.outputs.end(); ++j) + computeFSClosure(j->second.path, inputPaths); + } + + for (PathSet::iterator i = inputPaths.begin(); i != inputPaths.end(); ++i) + debug(format("INPUT %1%") % *i); + + allPaths.insert(inputPaths.begin(), inputPaths.end()); + + /* Second, the input sources. */ + for (PathSet::iterator i = drv.inputSrcs.begin(); + i != drv.inputSrcs.end(); ++i) + computeFSClosure(*i, inputPaths); + + /* We can skip running the builder if all output paths are already + valid. */ + bool fastBuild = true; + for (DerivationOutputs::iterator i = drv.outputs.begin(); + i != drv.outputs.end(); ++i) + if (!isValidPath(i->second.path)) { + fastBuild = false; + break; + } + + if (fastBuild) { + printMsg(lvlChatty, format("skipping build; output paths already exist")); + computeClosure(); + return false; + } + + return true; +} + + +void DerivationGoal::startBuilder() +{ + startNest(nest, lvlInfo, + format("building path(s) %1%") % showPaths(outputPaths(drv.outputs))) + + /* Right platform? */ + if (drv.platform != thisSystem) + throw BuildError( + format("a `%1%' is required to build `%3%', but I am a `%2%'") + % drv.platform % thisSystem % drvPath); + + /* If any of the outputs already exist but are not registered, + delete them. */ + for (DerivationOutputs::iterator i = drv.outputs.begin(); + i != drv.outputs.end(); ++i) + { + Path path = i->second.path; + if (isValidPath(path)) + throw Error(format("obstructed build: path `%1%' exists") % path); + if (pathExists(path)) { + debug(format("removing unregistered path `%1%'") % path); + deletePath(path); + } + } + + /* Construct the environment passed to the builder. */ + typedef map<string, string> Environment; + Environment env; + + /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when + PATH is not set. We don't want this, so we fill it in with some dummy + value. */ + env["PATH"] = "/path-not-set"; + + /* Set HOME to a non-existing path to prevent certain programs from using + /etc/passwd (or NIS, or whatever) to locate the home directory (for + example, wget looks for ~/.wgetrc). I.e., these tools use /etc/passwd + if HOME is not set, but they will just assume that the settings file + they are looking for does not exist if HOME is set but points to some + non-existing path. */ + env["HOME"] = "/homeless-shelter"; + + /* Tell the builder where the Nix store is. Usually they + shouldn't care, but this is useful for purity checking (e.g., + the compiler or linker might only want to accept paths to files + in the store or in the build directory). */ + env["NIX_STORE"] = nixStore; + + /* Add all bindings specified in the derivation expression. */ + for (StringPairs::iterator i = drv.env.begin(); + i != drv.env.end(); ++i) + env[i->first] = i->second; + + /* Create a temporary directory where the build will take + place. */ + tmpDir = createTempDir(); + + /* For convenience, set an environment pointing to the top build + directory. */ + env["NIX_BUILD_TOP"] = tmpDir; + + /* Also set TMPDIR and variants to point to this directory. */ + env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDir; + + /* Run the builder. */ + printMsg(lvlChatty, format("executing builder `%1%'") % + drv.builder); + + /* Create the log file and pipe. */ + openLogFile(); + + /* Fork a child to build the package. Note that while we + currently use forks to run and wait for the children, it + shouldn't be hard to use threads for this on systems where + fork() is unavailable or inefficient. */ + pid = fork(); + switch (pid) { + + case -1: + throw SysError("unable to fork"); + + case 0: + + /* Warning: in the child we should absolutely not make any + Berkeley DB calls! */ + + try { /* child */ + + initChild(); + + /* Fill in the arguments. */ + Strings args(drv.args); + args.push_front(baseNameOf(drv.builder)); + const char * * argArr = strings2CharPtrs(args); + + /* Fill in the environment. */ + Strings envStrs; + for (Environment::const_iterator i = env.begin(); + i != env.end(); ++i) + envStrs.push_back(i->first + "=" + i->second); + const char * * envArr = strings2CharPtrs(envStrs); + + /* Execute the program. This should not return. */ + execve(drv.builder.c_str(), + (char * *) argArr, (char * *) envArr); + + throw SysError(format("executing `%1%'") + % drv.builder); + + } catch (exception & e) { + cerr << format("build error: %1%\n") % e.what(); + } + _exit(1); + } + + /* parent */ + pid.setSeparatePG(true); + logPipe.writeSide.close(); + worker.childStarted(shared_from_this(), + pid, logPipe.readSide, true); +} + + +void DerivationGoal::computeClosure() +{ + startNest(nest, lvlTalkative, + format("determining closure for `%1%'") % drvPath); + + map<Path, PathSet> allReferences; + map<Path, Hash> contentHashes; + + /* Check whether the output paths were created, and grep each + output path to determine what other paths it references. Also make all + output paths read-only. */ + for (DerivationOutputs::iterator i = drv.outputs.begin(); + i != drv.outputs.end(); ++i) + { + Path path = i->second.path; + if (!pathExists(path)) { + throw BuildError( + format("builder for `%1%' failed to produce output path `%2%'") + % drvPath % path); + } + + /* Check that fixed-output derivations produced the right + outputs (i.e., the content hash should match the specified + hash). */ + if (i->second.hash != "") { + HashType ht = parseHashType(i->second.hashAlgo); + if (ht == htUnknown) + throw Error(format("unknown hash algorithm `%1%'") % i->second.hashAlgo); + Hash h = parseHash(ht, i->second.hash); + Hash h2 = hashFile(ht, path); + if (h != h2) + throw Error( + format("output path `%1% should have %2% hash `%3%', instead has `%4%'") + % path % i->second.hashAlgo % printHash(h) % printHash(h2)); + + /* Also, the output path should be a regular file withouth + execute permission. */ + struct stat st; + if (lstat(path.c_str(), &st)) + throw SysError(format("getting attributes of path `%1%'") % path); + if (!S_ISREG(st.st_mode) || (st.st_mode & S_IXUSR) != 0) + throw Error( + format("output path `%1% should be a non-executable regular file") + % path); + } + + canonicalisePathMetaData(path); + + /* For this output path, find the references to other paths contained + in it. */ + PathSet references; + if (!pathExists(path + "/nix-support/no-scan")) { + startNest(nest2, lvlChatty, + format("scanning for store references in `%1%'") % path); + Paths references2; + references2 = filterReferences(path, + Paths(allPaths.begin(), allPaths.end())); + references = PathSet(references2.begin(), references2.end()); + + /* For debugging, print out the referenced and + unreferenced paths. */ + for (PathSet::iterator i = inputPaths.begin(); + i != inputPaths.end(); ++i) + { + PathSet::iterator j = references.find(*i); + if (j == references.end()) + debug(format("unreferenced input: `%1%'") % *i); + else + debug(format("referenced input: `%1%'") % *i); + } + + nest2.close(); + } + + allReferences[path] = references; + + /* Hash the contents of the path. The hash is stored in the + database so that we can verify later on whether nobody has + messed with the store. !!! inefficient: it would be nice + if we could combine this with filterReferences(). */ + contentHashes[path] = hashPath(htSHA256, path); + } + + /* Register each output path as valid, and register the sets of + paths referenced by each of them. This is wrapped in one + database transaction to ensure that if we crash, either + everything is registered or nothing is. This is for + recoverability: unregistered paths in the store can be deleted + arbitrarily, while registered paths can only be deleted by + running the garbage collector. + + The reason that we do the transaction here and not on the fly + while we are scanning (above) is so that we don't hold database + locks for too long. */ + Transaction txn; + createStoreTransaction(txn); + for (DerivationOutputs::iterator i = drv.outputs.begin(); + i != drv.outputs.end(); ++i) + { + registerValidPath(txn, i->second.path, + contentHashes[i->second.path]); + setReferences(txn, i->second.path, + allReferences[i->second.path]); + } + txn.commit(); + + /* It is now safe to delete the lock files, since all future + lockers will see that the output paths are valid; they will not + create new lock files with the same names as the old (unlinked) + lock files. */ + outputLocks.setDeletion(true); +} + + +void DerivationGoal::openLogFile() +{ + /* Create a log file. */ + Path logFileName = nixLogDir + "/" + baseNameOf(drvPath); + fdLogFile = open(logFileName.c_str(), + O_CREAT | O_WRONLY | O_TRUNC, 0666); + if (fdLogFile == -1) + throw SysError(format("creating log file `%1%'") % logFileName); + + /* Create a pipe to get the output of the child. */ + logPipe.create(); +} + + +void DerivationGoal::initChild() +{ + commonChildInit(logPipe); + + if (chdir(tmpDir.c_str()) == -1) + throw SysError(format("changing into `%1%'") % tmpDir); + + /* When running a hook, dup the communication pipes. */ + bool inHook = fromHook.writeSide.isOpen(); + if (inHook) { + fromHook.readSide.close(); + if (dup2(fromHook.writeSide, 3) == -1) + throw SysError("dupping from-hook write side"); + + toHook.writeSide.close(); + if (dup2(toHook.readSide, 4) == -1) + throw SysError("dupping to-hook read side"); + } + + /* Close all other file descriptors. */ + int maxFD = 0; + maxFD = sysconf(_SC_OPEN_MAX); + for (int fd = 0; fd < maxFD; ++fd) + if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO + && (!inHook || (fd != 3 && fd != 4))) + close(fd); /* ignore result */ +} + + +void DerivationGoal::deleteTmpDir(bool force) +{ + if (tmpDir != "") { + if (keepFailed && !force) + printMsg(lvlError, + format("builder for `%1%' failed; keeping build directory `%2%'") + % drvPath % tmpDir); + else + deletePath(tmpDir); + tmpDir = ""; + } +} + + +void DerivationGoal::writeLog(int fd, + const unsigned char * buf, size_t count) +{ + assert(fd == logPipe.readSide); + writeFull(fdLogFile, buf, count); +} + + +bool DerivationGoal::allOutputsValid() +{ + unsigned int nrValid = 0; + for (DerivationOutputs::iterator i = drv.outputs.begin(); + i != drv.outputs.end(); ++i) + if (isValidPath(i->second.path)) nrValid++; + + if (nrValid != 0) { + if (nrValid == drv.outputs.size()) return true; + throw Error( + format("derivation `%1%' is blocked by its output paths") + % drvPath); + } + + return false; +} + + +string DerivationGoal::name() +{ + return (format("building of `%1%'") % drvPath).str(); +} + + + +////////////////////////////////////////////////////////////////////// + + +class SubstitutionGoal : public Goal +{ +private: + /* The store path that should be realised through a substitute. */ + Path storePath; + + /* The remaining substitutes for this path. */ + Substitutes subs; + + /* The current substitute. */ + Substitute sub; + + /* Pipe for the substitute's standard output/error. */ + Pipe logPipe; + + /* The process ID of the builder. */ + Pid pid; + + /* Lock on the store path. */ + shared_ptr<PathLocks> outputLock; + + typedef void (SubstitutionGoal::*GoalState)(); + GoalState state; + +public: + SubstitutionGoal(const Path & storePath, Worker & worker); + ~SubstitutionGoal(); + + void work(); + + /* The states. */ + void init(); + void tryNext(); + void tryToRun(); + void finished(); + + /* Callback used by the worker to write to the log. */ + void writeLog(int fd, const unsigned char * buf, size_t count); + + string name(); +}; + + +SubstitutionGoal::SubstitutionGoal(const Path & storePath, Worker & worker) + : Goal(worker) +{ + this->storePath = storePath; + state = &SubstitutionGoal::init; +} + + +SubstitutionGoal::~SubstitutionGoal() +{ + if (pid != -1) worker.childTerminated(pid); +} + + +void SubstitutionGoal::work() +{ + (this->*state)(); +} + + +void SubstitutionGoal::init() +{ + trace("init"); + + /* If the path already exists we're done. */ + if (isValidPath(storePath)) { + amDone(); + return; + } + + /* !!! build the outgoing references of this path first to + maintain the closure invariant! */ + + /* Otherwise, get the substitutes. */ + subs = querySubstitutes(storePath); + + /* Try the first one. */ + tryNext(); +} + + +void SubstitutionGoal::tryNext() +{ + trace("trying next substitute"); + + if (subs.size() == 0) { + /* None left. Terminate this goal and let someone else deal + with it. */ + printMsg(lvlError, + format("path `%1%' is required, but it has no (remaining) substitutes") + % storePath); + amDone(false); + return; + } + sub = subs.front(); + subs.pop_front(); + + /* Wait until we can run the substitute program. */ + state = &SubstitutionGoal::tryToRun; + worker.waitForBuildSlot(shared_from_this()); +} + + +void SubstitutionGoal::tryToRun() +{ + trace("trying to run"); + + /* Make sure that we are allowed to start a build. */ + if (!worker.canBuildMore()) { + worker.waitForBuildSlot(shared_from_this()); + return; + } + + /* Acquire a lock on the output path. */ + PathSet lockPath; + lockPath.insert(storePath); + outputLock = shared_ptr<PathLocks>(new PathLocks); + outputLock->lockPaths(lockPath); + + /* Check again whether the path is invalid. */ + if (isValidPath(storePath)) { + debug(format("store path `%1%' has become valid") % storePath); + outputLock->setDeletion(true); + amDone(); + return; + } + + printMsg(lvlInfo, + format("substituting path `%1%' using substituter `%2%'") + % storePath % sub.program); + + logPipe.create(); + + /* Remove the (stale) output path if it exists. */ + if (pathExists(storePath)) + deletePath(storePath); + + /* Fork the substitute program. */ + pid = fork(); + switch (pid) { + + case -1: + throw SysError("unable to fork"); + + case 0: + try { /* child */ + + logPipe.readSide.close(); + + /* !!! close other handles */ + + commonChildInit(logPipe); + + /* Fill in the arguments. */ + Strings args(sub.args); + args.push_front(storePath); + args.push_front(baseNameOf(sub.program)); + const char * * argArr = strings2CharPtrs(args); + + execv(sub.program.c_str(), (char * *) argArr); + + throw SysError(format("executing `%1%'") % sub.program); + + } catch (exception & e) { + cerr << format("substitute error: %1%\n") % e.what(); + } + _exit(1); + } + + /* parent */ + pid.setSeparatePG(true); + logPipe.writeSide.close(); + worker.childStarted(shared_from_this(), + pid, logPipe.readSide, true); + + state = &SubstitutionGoal::finished; +} + + +void SubstitutionGoal::finished() +{ + trace("substitute finished"); + + /* Since we got an EOF on the logger pipe, the substitute is + presumed to have terminated. */ + /* !!! this could block! */ + pid_t savedPid = pid; + int status = pid.wait(true); + + /* So the child is gone now. */ + worker.childTerminated(savedPid); + + /* Close the read side of the logger pipe. */ + logPipe.readSide.close(); + + debug(format("substitute for `%1%' finished") % storePath); + + /* Check the exit status and the build result. */ + try { + + if (!statusOk(status)) + throw SubstError(format("builder for `%1%' %2%") + % storePath % statusToString(status)); + + if (!pathExists(storePath)) + throw SubstError( + format("substitute did not produce path `%1%'") + % storePath); + + } catch (SubstError & e) { + + printMsg(lvlInfo, + format("substitution of path `%1%' using substituter `%2%' failed: %3%") + % storePath % sub.program % e.msg()); + + /* Try the next substitute. */ + state = &SubstitutionGoal::tryNext; + worker.wakeUp(shared_from_this()); + return; + } + + canonicalisePathMetaData(storePath); + + Hash contentHash = hashPath(htSHA256, storePath); + + Transaction txn; + createStoreTransaction(txn); + registerValidPath(txn, storePath, contentHash); + txn.commit(); + + outputLock->setDeletion(true); + + printMsg(lvlChatty, + format("substitution of path `%1%' succeeded") % storePath); + + amDone(); +} + + +void SubstitutionGoal::writeLog(int fd, + const unsigned char * buf, size_t count) +{ + assert(fd == logPipe.readSide); + /* Don't write substitution output to a log file for now. We + probably should, though. */ +} + + +string SubstitutionGoal::name() +{ + return (format("substitution of `%1%'") % storePath).str(); +} + + + +////////////////////////////////////////////////////////////////////// + + +/* A fake goal used to receive notification of success or failure of + other goals. */ +class PseudoGoal : public Goal +{ +private: + bool success; + +public: + PseudoGoal(Worker & worker) : Goal(worker) + { + success = true; + } + + void work() + { + abort(); + } + + void waiteeDone(GoalPtr waitee, bool success) + { + if (!success) this->success = false; + } + + bool isOkay() + { + return success; + } + + string name() + { + return "pseudo-goal"; + } +}; + + + +////////////////////////////////////////////////////////////////////// + + +static bool working = false; + + +Worker::Worker() +{ + /* Debugging: prevent recursive workers. */ + if (working) abort(); + working = true; + nrChildren = 0; +} + + +Worker::~Worker() +{ + working = false; + + /* Explicitly get rid of all strong pointers now. After this all + goals that refer to this worker should be gone. (Otherwise we + are in trouble, since goals may call childTerminated() etc. in + their destructors). */ + topGoals.clear(); +} + + +template<class T> +static GoalPtr addGoal(const Path & path, + Worker & worker, WeakGoalMap & goalMap) +{ + GoalPtr goal = goalMap[path].lock(); + if (!goal) { + goal = GoalPtr(new T(path, worker)); + goalMap[path] = goal; + worker.wakeUp(goal); + } + return goal; +} + + +GoalPtr Worker::makeDerivationGoal(const Path & nePath) +{ + return addGoal<DerivationGoal>(nePath, *this, derivationGoals); +} + + +GoalPtr Worker::makeSubstitutionGoal(const Path & storePath) +{ + return addGoal<SubstitutionGoal>(storePath, *this, substitutionGoals); +} + + +static void removeGoal(GoalPtr goal, WeakGoalMap & goalMap) +{ + /* !!! For now we just let dead goals accumulate. We should + probably periodically sweep the goalMap to remove dead + goals. */ +} + + +void Worker::removeGoal(GoalPtr goal) +{ + topGoals.erase(goal); + ::removeGoal(goal, derivationGoals); + ::removeGoal(goal, substitutionGoals); +} + + +void Worker::wakeUp(GoalPtr goal) +{ + goal->trace("woken up"); + awake.insert(goal); +} + + +bool Worker::canBuildMore() +{ + return nrChildren < maxBuildJobs; +} + + +void Worker::childStarted(GoalPtr goal, + pid_t pid, int fdOutput, bool inBuildSlot) +{ + Child child; + child.goal = goal; + child.fdOutput = fdOutput; + child.inBuildSlot = inBuildSlot; + children[pid] = child; + if (inBuildSlot) nrChildren++; +} + + +void Worker::childTerminated(pid_t pid, bool wakeSleepers) +{ + Children::iterator i = children.find(pid); + assert(i != children.end()); + + if (i->second.inBuildSlot) { + assert(nrChildren > 0); + nrChildren--; + } + + children.erase(pid); + + if (wakeSleepers) { + + /* Wake up goals waiting for a build slot. */ + for (WeakGoals::iterator i = wantingToBuild.begin(); + i != wantingToBuild.end(); ++i) + { + GoalPtr goal = i->lock(); + if (goal) wakeUp(goal); + } + + wantingToBuild.clear(); + + } +} + + +void Worker::waitForBuildSlot(GoalPtr goal, bool reallyWait) +{ + debug("wait for build slot"); + if (reallyWait && children.size() == 0) + throw Error("waiting for a build slot, yet there are no children - " + "maybe the build hook gave an inappropriate `postpone' reply?"); + if (!reallyWait && canBuildMore()) + wakeUp(goal); /* we can do it right away */ + else + wantingToBuild.insert(goal); +} + + +bool Worker::run(const Goals & _topGoals) +{ + /* Wrap the specified top-level goal in a pseudo-goal so that we + can check whether it succeeded. */ + shared_ptr<PseudoGoal> pseudo(new PseudoGoal(*this)); + for (Goals::iterator i = _topGoals.begin(); + i != _topGoals.end(); ++i) + { + assert(*i); + pseudo->addWaitee(*i); + topGoals.insert(*i); + } + + startNest(nest, lvlDebug, format("entered goal loop")); + + while (1) { + + checkInterrupt(); + + /* Call every wake goal. */ + while (!awake.empty()) { + WeakGoals awake2(awake); + awake.clear(); + for (WeakGoals::iterator i = awake2.begin(); i != awake2.end(); ++i) { + checkInterrupt(); + GoalPtr goal = i->lock(); + if (goal) goal->work(); + } + } + + if (topGoals.empty()) break; + + /* !!! not when we're polling */ + assert(!children.empty()); + + /* Wait for input. */ + waitForInput(); + } + + /* If --keep-going is not set, it's possible that the main goal + exited while some of its subgoals were still active. But if + --keep-going *is* set, then they must all be finished now. */ + assert(!keepGoing || awake.empty()); + assert(!keepGoing || wantingToBuild.empty()); + assert(!keepGoing || children.empty()); + + return pseudo->isOkay(); +} + + +void Worker::waitForInput() +{ + printMsg(lvlVomit, "waiting for children"); + + /* Process log output from the children. We also use this to + detect child termination: if we get EOF on the logger pipe of a + build, we assume that the builder has terminated. */ + + /* Use select() to wait for the input side of any logger pipe to + become `available'. Note that `available' (i.e., non-blocking) + includes EOF. */ + fd_set fds; + FD_ZERO(&fds); + int fdMax = 0; + for (Children::iterator i = children.begin(); + i != children.end(); ++i) + { + int fd = i->second.fdOutput; + FD_SET(fd, &fds); + if (fd >= fdMax) fdMax = fd + 1; + } + + if (select(fdMax, &fds, 0, 0, 0) == -1) { + if (errno == EINTR) return; + throw SysError("waiting for input"); + } + + /* Process all available file descriptors. */ + for (Children::iterator i = children.begin(); + i != children.end(); ++i) + { + checkInterrupt(); + GoalPtr goal = i->second.goal.lock(); + assert(goal); + int fd = i->second.fdOutput; + if (FD_ISSET(fd, &fds)) { + unsigned char buffer[4096]; + ssize_t rd = read(fd, buffer, sizeof(buffer)); + if (rd == -1) { + if (errno != EINTR) + throw SysError(format("reading from %1%") + % goal->name()); + } else if (rd == 0) { + debug(format("%1%: got EOF") % goal->name()); + wakeUp(goal); + } else { + printMsg(lvlVomit, format("%1%: read %2% bytes") + % goal->name() % rd); + goal->writeLog(fd, buffer, (size_t) rd); + if (verbosity >= buildVerbosity) + writeFull(STDERR_FILENO, buffer, rd); + } + } + } +} + + +////////////////////////////////////////////////////////////////////// + + +void buildDerivations(const PathSet & drvPaths) +{ + startNest(nest, lvlDebug, + format("building %1%") % showPaths(drvPaths)); + + Worker worker; + + Goals goals; + for (PathSet::const_iterator i = drvPaths.begin(); + i != drvPaths.end(); ++i) + goals.insert(worker.makeDerivationGoal(*i)); + + if (!worker.run(goals)) + throw Error(format("build failed")); +} + + +void ensurePath(const Path & path) +{ + /* If the path is already valid, we're done. */ + if (isValidPath(path)) return; + + Worker worker; + Goals goals; + goals.insert(worker.makeSubstitutionGoal(path)); + if (!worker.run(goals)) + throw Error(format("path `%1%' does not exist and cannot be created") % path); +} |