about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/libexpr/lexer.l16
-rw-r--r--src/libstore/build.cc125
-rw-r--r--src/libstore/derivations.cc2
-rw-r--r--src/libstore/gc.cc5
-rw-r--r--src/libstore/globals.hh5
-rw-r--r--src/libstore/nar-accessor.cc3
-rw-r--r--src/libstore/pathlocks.cc87
-rw-r--r--src/libstore/s3-binary-cache-store.cc29
-rw-r--r--src/libstore/store-api.hh15
-rw-r--r--src/libutil/hash.cc10
-rw-r--r--src/libutil/logging.hh1
-rw-r--r--src/libutil/util.hh51
-rwxr-xr-xsrc/nix-build/nix-build.cc30
-rw-r--r--src/nix-store/nix-store.cc9
-rw-r--r--src/nix-store/serve-protocol.hh2
15 files changed, 234 insertions, 156 deletions
diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l
index 3ac7ce723cb3..5b1ff0350cd1 100644
--- a/src/libexpr/lexer.l
+++ b/src/libexpr/lexer.l
@@ -87,8 +87,8 @@ static Expr * unescapeStr(SymbolTable & symbols, const char * s)
 ID          [a-zA-Z\_][a-zA-Z0-9\_\'\-]*
 INT         [0-9]+
 FLOAT       (([1-9][0-9]*\.[0-9]*)|(0?\.[0-9]+))([Ee][+-]?[0-9]+)?
-PATH        [a-zA-Z0-9\.\_\-\+]*(\/[a-zA-Z0-9\.\_\-\+]+)+
-HPATH       \~(\/[a-zA-Z0-9\.\_\-\+]+)+
+PATH        [a-zA-Z0-9\.\_\-\+]*(\/[a-zA-Z0-9\.\_\-\+]+)+\/?
+HPATH       \~(\/[a-zA-Z0-9\.\_\-\+]+)+\/?
 SPATH       \<[a-zA-Z0-9\.\_\-\+]+(\/[a-zA-Z0-9\.\_\-\+]+)*\>
 URI         [a-zA-Z][a-zA-Z0-9\+\-\.]*\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*\']+
 
@@ -182,8 +182,16 @@ or          { return OR_KW; }
 
 <INITIAL,INSIDE_DOLLAR_CURLY>{
 
-{PATH}      { yylval->path = strdup(yytext); return PATH; }
-{HPATH}     { yylval->path = strdup(yytext); return HPATH; }
+{PATH}      { if (yytext[yyleng-1] == '/')
+                  throw ParseError("path ‘%s’ has a trailing slash", yytext);
+              yylval->path = strdup(yytext);
+              return PATH;
+            }
+{HPATH}     { if (yytext[yyleng-1] == '/')
+                  throw ParseError("path ‘%s’ has a trailing slash", yytext);
+              yylval->path = strdup(yytext);
+              return HPATH;
+            }
 {SPATH}     { yylval->path = strdup(yytext); return SPATH; }
 {URI}       { yylval->uri = strdup(yytext); return URI; }
 
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
index ee266b7d3704..ca56d3ad925b 100644
--- a/src/libstore/build.cc
+++ b/src/libstore/build.cc
@@ -17,9 +17,9 @@
 #include <sstream>
 #include <thread>
 #include <future>
+#include <chrono>
 
 #include <limits.h>
-#include <time.h>
 #include <sys/time.h>
 #include <sys/wait.h>
 #include <sys/types.h>
@@ -187,6 +187,9 @@ bool CompareGoalPtrs::operator() (const GoalPtr & a, const GoalPtr & b) {
 }
 
 
+typedef std::chrono::time_point<std::chrono::steady_clock> steady_time_point;
+
+
 /* A mapping used to remember for each child process to what goal it
    belongs, and file descriptors for receiving log data and output
    path creation commands. */
@@ -197,8 +200,8 @@ struct Child
     set<int> fds;
     bool respectTimeouts;
     bool inBuildSlot;
-    time_t lastOutput; /* time we last got output on stdout/stderr */
-    time_t timeStarted;
+    steady_time_point lastOutput; /* time we last got output on stdout/stderr */
+    steady_time_point timeStarted;
 };
 
 
@@ -238,7 +241,7 @@ private:
     WeakGoals waitingForAWhile;
 
     /* Last time the goals in `waitingForAWhile' where woken up. */
-    time_t lastWokenUp;
+    steady_time_point lastWokenUp;
 
     /* Cache for pathContentsGood(). */
     std::map<Path, bool> pathContentsGoodCache;
@@ -1269,6 +1272,8 @@ void DerivationGoal::inputsRealised()
        build hook. */
     state = &DerivationGoal::tryToBuild;
     worker.wakeUp(shared_from_this());
+
+    result = BuildResult();
 }
 
 
@@ -1342,6 +1347,7 @@ void DerivationGoal::tryToBuild()
             case rpAccept:
                 /* Yes, it has started doing so.  Wait until we get
                    EOF from the hook. */
+                result.startTime = time(0); // inexact
                 state = &DerivationGoal::buildDone;
                 return;
             case rpPostpone:
@@ -1418,6 +1424,9 @@ void DerivationGoal::buildDone()
 
     debug(format("builder process for ‘%1%’ finished") % drvPath);
 
+    result.timesBuilt++;
+    result.stopTime = time(0);
+
     /* So the child is gone now. */
     worker.childTerminated(this);
 
@@ -2101,6 +2110,8 @@ void DerivationGoal::startBuilder()
     /* Create a pipe to get the output of the builder. */
     builderOut.create();
 
+    result.startTime = time(0);
+
     /* Fork a child to build the package. */
 #if __linux__
     if (useChroot) {
@@ -2154,7 +2165,8 @@ void DerivationGoal::startBuilder()
                namespace, we can't drop additional groups; they will
                be mapped to nogroup in the child namespace. There does
                not seem to be a workaround for this. (But who can tell
-               from reading user_namespaces(7)?)*/
+               from reading user_namespaces(7)?)
+               See also https://lwn.net/Articles/621612/. */
             if (getuid() == 0 && setgroups(0, 0) == -1)
                 throw SysError("setgroups failed");
 
@@ -2324,6 +2336,7 @@ void DerivationGoal::runChild()
                 ss.push_back("/etc/nsswitch.conf");
                 ss.push_back("/etc/services");
                 ss.push_back("/etc/hosts");
+                ss.push_back("/var/run/nscd/socket");
             }
 
             for (auto & i : ss) dirsInChroot[i] = i;
@@ -2672,7 +2685,9 @@ void DerivationGoal::registerOutputs()
        outputs to allow hard links between outputs. */
     InodesSeen inodesSeen;
 
-    Path checkSuffix = "-check";
+    Path checkSuffix = ".check";
+    bool runDiffHook = settings.get("run-diff-hook", false);
+    bool keepPreviousRound = settings.keepFailed || runDiffHook;
 
     /* Check whether the output paths were created, and grep each
        output path to determine what other paths it references.  Also make all
@@ -2902,30 +2917,42 @@ void DerivationGoal::registerOutputs()
         assert(prevInfos.size() == infos.size());
         for (auto i = prevInfos.begin(), j = infos.begin(); i != prevInfos.end(); ++i, ++j)
             if (!(*i == *j)) {
+                result.isNonDeterministic = true;
                 Path prev = i->path + checkSuffix;
-                if (pathExists(prev))
-                    throw NotDeterministic(
-                        format("output ‘%1%’ of ‘%2%’ differs from ‘%3%’ from previous round")
-                        % i->path % drvPath % prev);
-                else
-                    throw NotDeterministic(
-                        format("output ‘%1%’ of ‘%2%’ differs from previous round")
-                        % i->path % drvPath);
+                bool prevExists = keepPreviousRound && pathExists(prev);
+                auto msg = prevExists
+                    ? fmt("output ‘%1%’ of ‘%2%’ differs from ‘%3%’ from previous round", i->path, drvPath, prev)
+                    : fmt("output ‘%1%’ of ‘%2%’ differs from previous round", i->path, drvPath);
+
+                auto diffHook = settings.get("diff-hook", std::string(""));
+                if (prevExists && diffHook != "" && runDiffHook) {
+                    try {
+                        auto diff = runProgram(diffHook, true, {prev, i->path});
+                        if (diff != "")
+                            printError(chomp(diff));
+                    } catch (Error & error) {
+                        printError("diff hook execution failed: %s", error.what());
+                    }
+                }
+
+                if (settings.get("enforce-determinism", true))
+                    throw NotDeterministic(msg);
+
+                printError(msg);
+                curRound = nrRounds; // we know enough, bail out early
             }
-        abort(); // shouldn't happen
     }
 
-    if (settings.keepFailed) {
+    /* If this is the first round of several, then move the output out
+       of the way. */
+    if (nrRounds > 1 && curRound == 1 && curRound < nrRounds && keepPreviousRound) {
         for (auto & i : drv->outputs) {
             Path prev = i.second.path + checkSuffix;
             deletePath(prev);
-            if (curRound < nrRounds) {
-                Path dst = i.second.path + checkSuffix;
-                if (rename(i.second.path.c_str(), dst.c_str()))
-                    throw SysError(format("renaming ‘%1%’ to ‘%2%’") % i.second.path % dst);
-            }
+            Path dst = i.second.path + checkSuffix;
+            if (rename(i.second.path.c_str(), dst.c_str()))
+                throw SysError(format("renaming ‘%1%’ to ‘%2%’") % i.second.path % dst);
         }
-
     }
 
     if (curRound < nrRounds) {
@@ -2933,6 +2960,15 @@ void DerivationGoal::registerOutputs()
         return;
     }
 
+    /* Remove the .check directories if we're done. FIXME: keep them
+       if the result was not determistic? */
+    if (curRound == nrRounds) {
+        for (auto & i : drv->outputs) {
+            Path prev = i.second.path + checkSuffix;
+            deletePath(prev);
+        }
+    }
+
     /* Register each output path as valid, and register the sets of
        paths referenced by each of them.  If there are cycles in the
        outputs, this will fail. */
@@ -3043,7 +3079,8 @@ void DerivationGoal::handleEOF(int fd)
 
 void DerivationGoal::flushLine()
 {
-    if (settings.verboseBuild)
+    if (settings.verboseBuild &&
+        (settings.printRepeatedBuilds || curRound == 1))
         printError(filterANSIEscapes(currentLogLine, true));
     else {
         logTail.push_back(currentLogLine);
@@ -3385,7 +3422,7 @@ Worker::Worker(LocalStore & store)
     if (working) abort();
     working = true;
     nrLocalBuilds = 0;
-    lastWokenUp = 0;
+    lastWokenUp = steady_time_point::min();
     permanentFailure = false;
     timedOut = false;
 }
@@ -3494,7 +3531,7 @@ void Worker::childStarted(GoalPtr goal, const set<int> & fds,
     child.goal = goal;
     child.goal2 = goal.get();
     child.fds = fds;
-    child.timeStarted = child.lastOutput = time(0);
+    child.timeStarted = child.lastOutput = steady_time_point::clock::now();
     child.inBuildSlot = inBuildSlot;
     child.respectTimeouts = respectTimeouts;
     children.emplace_back(child);
@@ -3613,35 +3650,38 @@ void Worker::waitForInput()
     bool useTimeout = false;
     struct timeval timeout;
     timeout.tv_usec = 0;
-    time_t before = time(0);
+    auto before = steady_time_point::clock::now();
 
     /* If we're monitoring for silence on stdout/stderr, or if there
        is a build timeout, then wait for input until the first
        deadline for any child. */
-    assert(sizeof(time_t) >= sizeof(long));
-    time_t nearest = LONG_MAX; // nearest deadline
+    auto nearest = steady_time_point::max(); // nearest deadline
     for (auto & i : children) {
         if (!i.respectTimeouts) continue;
         if (settings.maxSilentTime != 0)
-            nearest = std::min(nearest, i.lastOutput + settings.maxSilentTime);
+            nearest = std::min(nearest, i.lastOutput + std::chrono::seconds(settings.maxSilentTime));
         if (settings.buildTimeout != 0)
-            nearest = std::min(nearest, i.timeStarted + settings.buildTimeout);
+            nearest = std::min(nearest, i.timeStarted + std::chrono::seconds(settings.buildTimeout));
     }
-    if (nearest != LONG_MAX) {
-        timeout.tv_sec = std::max((time_t) 1, nearest - before);
+    if (nearest != steady_time_point::max()) {
+        timeout.tv_sec = std::max(1L, (long) std::chrono::duration_cast<std::chrono::seconds>(nearest - before).count());
         useTimeout = true;
-        printMsg(lvlVomit, format("sleeping %1% seconds") % timeout.tv_sec);
     }
 
     /* If we are polling goals that are waiting for a lock, then wake
        up after a few seconds at most. */
     if (!waitingForAWhile.empty()) {
         useTimeout = true;
-        if (lastWokenUp == 0)
+        if (lastWokenUp == steady_time_point::min())
             printError("waiting for locks or build slots...");
-        if (lastWokenUp == 0 || lastWokenUp > before) lastWokenUp = before;
-        timeout.tv_sec = std::max((time_t) 1, (time_t) (lastWokenUp + settings.pollInterval - before));
-    } else lastWokenUp = 0;
+        if (lastWokenUp == steady_time_point::min() || lastWokenUp > before) lastWokenUp = before;
+        timeout.tv_sec = std::max(1L,
+            (long) std::chrono::duration_cast<std::chrono::seconds>(
+                lastWokenUp + std::chrono::seconds(settings.pollInterval) - before).count());
+    } else lastWokenUp = steady_time_point::min();
+
+    if (useTimeout)
+        vomit("sleeping %d seconds", timeout.tv_sec);
 
     /* Use select() to wait for the input side of any logger pipe to
        become `available'.  Note that `available' (i.e., non-blocking)
@@ -3661,7 +3701,7 @@ void Worker::waitForInput()
         throw SysError("waiting for input");
     }
 
-    time_t after = time(0);
+    auto after = steady_time_point::clock::now();
 
     /* Process all available file descriptors. */
     decltype(children)::iterator i;
@@ -3699,7 +3739,7 @@ void Worker::waitForInput()
         if (goal->getExitCode() == Goal::ecBusy &&
             settings.maxSilentTime != 0 &&
             j->respectTimeouts &&
-            after - j->lastOutput >= (time_t) settings.maxSilentTime)
+            after - j->lastOutput >= std::chrono::seconds(settings.maxSilentTime))
         {
             printError(
                 format("%1% timed out after %2% seconds of silence")
@@ -3710,7 +3750,7 @@ void Worker::waitForInput()
         else if (goal->getExitCode() == Goal::ecBusy &&
             settings.buildTimeout != 0 &&
             j->respectTimeouts &&
-            after - j->timeStarted >= (time_t) settings.buildTimeout)
+            after - j->timeStarted >= std::chrono::seconds(settings.buildTimeout))
         {
             printError(
                 format("%1% timed out after %2% seconds")
@@ -3719,7 +3759,7 @@ void Worker::waitForInput()
         }
     }
 
-    if (!waitingForAWhile.empty() && lastWokenUp + (time_t) settings.pollInterval <= after) {
+    if (!waitingForAWhile.empty() && lastWokenUp + std::chrono::seconds(settings.pollInterval) <= after) {
         lastWokenUp = after;
         for (auto & i : waitingForAWhile) {
             GoalPtr goal = i.lock();
@@ -3781,12 +3821,13 @@ void LocalStore::buildPaths(const PathSet & drvPaths, BuildMode buildMode)
     worker.run(goals);
 
     PathSet failed;
-    for (auto & i : goals)
+    for (auto & i : goals) {
         if (i->getExitCode() != Goal::ecSuccess) {
             DerivationGoal * i2 = dynamic_cast<DerivationGoal *>(i.get());
             if (i2) failed.insert(i2->getDrvPath());
             else failed.insert(dynamic_cast<SubstitutionGoal *>(i.get())->getStorePath());
         }
+    }
 
     if (!failed.empty())
         throw Error(worker.exitStatus(), "build of %s failed", showPaths(failed));
diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc
index 5562d46892a8..d934bda38225 100644
--- a/src/libstore/derivations.cc
+++ b/src/libstore/derivations.cc
@@ -155,7 +155,7 @@ static StringSet parseStrings(std::istream & str, bool arePaths)
 static Derivation parseDerivation(const string & s)
 {
     Derivation drv;
-    istringstream_nocopy str(s);
+    std::istringstream str(s);
     expect(str, "Derive([");
 
     /* Parse the list of outputs. */
diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc
index ae03604faf98..f8c4a07238c7 100644
--- a/src/libstore/gc.cc
+++ b/src/libstore/gc.cc
@@ -621,6 +621,11 @@ void LocalStore::tryToDelete(GCState & state, const Path & path)
         /* Don't delete .chroot directories for derivations that are
            currently being built. */
         if (isActiveTempFile(state, path, ".chroot")) return;
+
+        /* Don't delete .check directories for derivations that are
+           currently being built, because we may need to run
+           diff-hook. */
+        if (isActiveTempFile(state, path, ".check")) return;
     }
 
     PathSet visited;
diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh
index 3194193bc842..a423b4e5c0f4 100644
--- a/src/libstore/globals.hh
+++ b/src/libstore/globals.hh
@@ -149,6 +149,11 @@ struct Settings {
        before being killed (0 means no limit). */
     unsigned long maxLogSize;
 
+    /* When build-repeat > 0 and verboseBuild == true, whether to
+       print repeated builds (i.e. builds other than the first one) to
+       stderr. Hack to prevent Hydra logs from being polluted. */
+    bool printRepeatedBuilds = true;
+
     /* How often (in seconds) to poll for locks. */
     unsigned int pollInterval;
 
diff --git a/src/libstore/nar-accessor.cc b/src/libstore/nar-accessor.cc
index ded19c05d2cd..4cb5de7449ea 100644
--- a/src/libstore/nar-accessor.cc
+++ b/src/libstore/nar-accessor.cc
@@ -52,8 +52,9 @@ struct NarIndexer : ParseSink, StringSource
     void preallocateContents(unsigned long long size) override
     {
         currentStart = string(s, pos, 16);
+        assert(size <= std::numeric_limits<size_t>::max());
         members.emplace(currentPath,
-            NarMember{FSAccessor::Type::tRegular, isExec, pos, size});
+            NarMember{FSAccessor::Type::tRegular, isExec, pos, (size_t) size});
     }
 
     void receiveContents(unsigned char * data, unsigned int len) override
diff --git a/src/libstore/pathlocks.cc b/src/libstore/pathlocks.cc
index 8788ee1649fb..fecd636877af 100644
--- a/src/libstore/pathlocks.cc
+++ b/src/libstore/pathlocks.cc
@@ -1,5 +1,6 @@
 #include "pathlocks.hh"
 #include "util.hh"
+#include "sync.hh"
 
 #include <cerrno>
 #include <cstdlib>
@@ -72,7 +73,7 @@ bool lockFile(int fd, LockType lockType, bool wait)
    close a descriptor, the previous lock will be closed as well.  And
    there is no way to query whether we already have a lock (F_GETLK
    only works on locks held by other processes). */
-static StringSet lockedPaths; /* !!! not thread-safe */
+static Sync<StringSet> lockedPaths_;
 
 
 PathLocks::PathLocks()
@@ -108,49 +109,60 @@ bool PathLocks::lockPaths(const PathSet & _paths,
 
         debug(format("locking path ‘%1%’") % path);
 
-        if (lockedPaths.find(lockPath) != lockedPaths.end())
-            throw Error("deadlock: trying to re-acquire self-held lock");
+        {
+            auto lockedPaths(lockedPaths_.lock());
+            if (lockedPaths->count(lockPath))
+                throw Error("deadlock: trying to re-acquire self-held lock ‘%s’", lockPath);
+            lockedPaths->insert(lockPath);
+        }
+
+        try {
 
-        AutoCloseFD fd;
+            AutoCloseFD fd;
 
-        while (1) {
+            while (1) {
 
-            /* Open/create the lock file. */
-            fd = openLockFile(lockPath, true);
+                /* Open/create the lock file. */
+                fd = openLockFile(lockPath, true);
 
-            /* Acquire an exclusive lock. */
-            if (!lockFile(fd.get(), ltWrite, false)) {
-                if (wait) {
-                    if (waitMsg != "") printError(waitMsg);
-                    lockFile(fd.get(), ltWrite, true);
-                } else {
-                    /* Failed to lock this path; release all other
-                       locks. */
-                    unlock();
-                    return false;
+                /* Acquire an exclusive lock. */
+                if (!lockFile(fd.get(), ltWrite, false)) {
+                    if (wait) {
+                        if (waitMsg != "") printError(waitMsg);
+                        lockFile(fd.get(), ltWrite, true);
+                    } else {
+                        /* Failed to lock this path; release all other
+                           locks. */
+                        unlock();
+                        return false;
+                    }
                 }
+
+                debug(format("lock acquired on ‘%1%’") % lockPath);
+
+                /* Check that the lock file hasn't become stale (i.e.,
+                   hasn't been unlinked). */
+                struct stat st;
+                if (fstat(fd.get(), &st) == -1)
+                    throw SysError(format("statting lock file ‘%1%’") % lockPath);
+                if (st.st_size != 0)
+                    /* This lock file has been unlinked, so we're holding
+                       a lock on a deleted file.  This means that other
+                       processes may create and acquire a lock on
+                       `lockPath', and proceed.  So we must retry. */
+                    debug(format("open lock file ‘%1%’ has become stale") % lockPath);
+                else
+                    break;
             }
 
-            debug(format("lock acquired on ‘%1%’") % lockPath);
-
-            /* Check that the lock file hasn't become stale (i.e.,
-               hasn't been unlinked). */
-            struct stat st;
-            if (fstat(fd.get(), &st) == -1)
-                throw SysError(format("statting lock file ‘%1%’") % lockPath);
-            if (st.st_size != 0)
-                /* This lock file has been unlinked, so we're holding
-                   a lock on a deleted file.  This means that other
-                   processes may create and acquire a lock on
-                   `lockPath', and proceed.  So we must retry. */
-                debug(format("open lock file ‘%1%’ has become stale") % lockPath);
-            else
-                break;
+            /* Use borrow so that the descriptor isn't closed. */
+            fds.push_back(FDPair(fd.release(), lockPath));
+
+        } catch (...) {
+            lockedPaths_.lock()->erase(lockPath);
+            throw;
         }
 
-        /* Use borrow so that the descriptor isn't closed. */
-        fds.push_back(FDPair(fd.release(), lockPath));
-        lockedPaths.insert(lockPath);
     }
 
     return true;
@@ -172,7 +184,8 @@ void PathLocks::unlock()
     for (auto & i : fds) {
         if (deletePaths) deleteLockFile(i.second, i.first);
 
-        lockedPaths.erase(i.second);
+        lockedPaths_.lock()->erase(i.second);
+
         if (close(i.first) == -1)
             printError(
                 format("error (ignored): cannot close lock file on ‘%1%’") % i.second);
@@ -193,7 +206,7 @@ void PathLocks::setDeletion(bool deletePaths)
 bool pathIsLockedByMe(const Path & path)
 {
     Path lockPath = path + ".lock";
-    return lockedPaths.find(lockPath) != lockedPaths.end();
+    return lockedPaths_.lock()->count(lockPath);
 }
 
 
diff --git a/src/libstore/s3-binary-cache-store.cc b/src/libstore/s3-binary-cache-store.cc
index 1bc8576a8aef..ccb71f1eefe5 100644
--- a/src/libstore/s3-binary-cache-store.cc
+++ b/src/libstore/s3-binary-cache-store.cc
@@ -1,23 +1,34 @@
 #include "config.h"
 
 #if ENABLE_S3
+#if __linux__
 
 #include "s3-binary-cache-store.hh"
 #include "nar-info.hh"
 #include "nar-info-disk-cache.hh"
 #include "globals.hh"
 
+#include <aws/core/Aws.h>
 #include <aws/core/client/ClientConfiguration.h>
 #include <aws/s3/S3Client.h>
 #include <aws/s3/model/CreateBucketRequest.h>
 #include <aws/s3/model/GetBucketLocationRequest.h>
 #include <aws/s3/model/GetObjectRequest.h>
 #include <aws/s3/model/HeadObjectRequest.h>
-#include <aws/s3/model/PutObjectRequest.h>
 #include <aws/s3/model/ListObjectsRequest.h>
+#include <aws/s3/model/PutObjectRequest.h>
 
 namespace nix {
 
+struct istringstream_nocopy : public std::stringstream
+{
+    istringstream_nocopy(const std::string & s)
+    {
+        rdbuf()->pubsetbuf(
+            (char *) s.data(), s.size());
+    }
+};
+
 struct S3Error : public Error
 {
     Aws::S3::S3Errors err;
@@ -37,6 +48,20 @@ R && checkAws(const FormatOrString & fs, Aws::Utils::Outcome<R, E> && outcome)
     return outcome.GetResultWithOwnership();
 }
 
+static void initAWS()
+{
+    static std::once_flag flag;
+    std::call_once(flag, []() {
+        Aws::SDKOptions options;
+
+        /* We install our own OpenSSL locking function (see
+           shared.cc), so don't let aws-sdk-cpp override it. */
+        options.cryptoOptions.initAndCleanupOpenSSL = false;
+
+        Aws::InitAPI(options);
+    });
+}
+
 struct S3BinaryCacheStoreImpl : public S3BinaryCacheStore
 {
     std::string bucketName;
@@ -63,6 +88,7 @@ struct S3BinaryCacheStoreImpl : public S3BinaryCacheStore
 
     ref<Aws::Client::ClientConfiguration> makeConfig()
     {
+        initAWS();
         auto res = make_ref<Aws::Client::ClientConfiguration>();
         res->region = Aws::Region::US_EAST_1; // FIXME: make configurable
         res->requestTimeoutMs = 600 * 1000;
@@ -260,3 +286,4 @@ static RegisterStoreImplementation regStore([](
 }
 
 #endif
+#endif
diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh
index 2ea74d90e78e..789526cc2b70 100644
--- a/src/libstore/store-api.hh
+++ b/src/libstore/store-api.hh
@@ -208,7 +208,20 @@ struct BuildResult
         NotDeterministic,
     } status = MiscFailure;
     std::string errorMsg;
-    //time_t startTime = 0, stopTime = 0;
+
+    /* How many times this build was performed. */
+    unsigned int timesBuilt = 0;
+
+    /* If timesBuilt > 1, whether some builds did not produce the same
+       result. (Note that 'isNonDeterministic = false' does not mean
+       the build is deterministic, just that we don't have evidence of
+       non-determinism.) */
+    bool isNonDeterministic = false;
+
+    /* The start/stop times of the build (or one of the rounds, if it
+       was repeated). */
+    time_t startTime = 0, stopTime = 0;
+
     bool success() {
         return status == Built || status == Substituted || status == AlreadyValid;
     }
diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc
index 49e781980f3a..aa50fceb9e3e 100644
--- a/src/libutil/hash.cc
+++ b/src/libutil/hash.cc
@@ -106,7 +106,7 @@ Hash parseHash(HashType ht, const string & s)
         string s2(s, i * 2, 2);
         if (!isxdigit(s2[0]) || !isxdigit(s2[1]))
             throw BadHash(format("invalid hash ‘%1%’") % s);
-        istringstream_nocopy str(s2);
+        std::istringstream str(s2);
         int n;
         str >> std::hex >> n;
         hash.hash[i] = n;
@@ -165,7 +165,13 @@ Hash parseHash32(HashType ht, const string & s)
         unsigned int i = b / 8;
         unsigned int j = b % 8;
         hash.hash[i] |= digit << j;
-        if (i < hash.hashSize - 1) hash.hash[i + 1] |= digit >> (8 - j);
+
+        if (i < hash.hashSize - 1) {
+            hash.hash[i + 1] |= digit >> (8 - j);
+        } else {
+            if (digit >> (8 - j))
+                throw BadHash(format("invalid base-32 hash ‘%1%’") % s);
+        }
     }
 
     return hash;
diff --git a/src/libutil/logging.hh b/src/libutil/logging.hh
index ba99a81c3826..3e6c4b54853c 100644
--- a/src/libutil/logging.hh
+++ b/src/libutil/logging.hh
@@ -79,6 +79,7 @@ extern Verbosity verbosity; /* suppress msgs > this */
 #define printError(args...) printMsg(lvlError, args)
 #define printInfo(args...) printMsg(lvlInfo, args)
 #define debug(args...) printMsg(lvlDebug, args)
+#define vomit(args...) printMsg(lvlVomit, args)
 
 void warnOnce(bool & haveWarned, const FormatOrString & fs);
 
diff --git a/src/libutil/util.hh b/src/libutil/util.hh
index 1ede48a65ff2..50b96f7ed92c 100644
--- a/src/libutil/util.hh
+++ b/src/libutil/util.hh
@@ -431,55 +431,4 @@ void callSuccess(
 }
 
 
-/* A variant of std::istringstream that doesn't copy its string
-   argument. This is useful for large strings. The caller must ensure
-   that the string object is not destroyed while it's referenced by
-   this object. */
-class istringbuf_nocopy : public std::streambuf
-{
-    const std::string & s;
-    decltype(s.size()) off;
-    decltype(s.size()) size;
-public:
-    istringbuf_nocopy(const std::string & s) : s{s}, off{0}, size{s.size()}
-    {
-    }
-
-private:
-    int_type underflow()
-    {
-      if (off == size)
-          return traits_type::eof();
-      return traits_type::to_int_type(s[off]);
-    }
-
-    int_type uflow()
-    {
-        if (off == size)
-            return traits_type::eof();
-        return traits_type::to_int_type(s[off++]);
-    }
-
-    int_type pbackfail(int_type ch)
-    {
-        if (off == 0 || (ch != traits_type::eof() && ch != s[off - 1]))
-            return traits_type::eof();
-
-        return traits_type::to_int_type(s[--off]);
-    }
-
-    std::streamsize showmanyc()
-    {
-        return size - off;
-    }
-};
-
-
-struct istringstream_nocopy : public std::iostream
-{
-    istringbuf_nocopy buf;
-    istringstream_nocopy(const std::string & s) : std::iostream(&buf), buf(s) {};
-};
-
-
 }
diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc
index 08c6793577a4..71ef5af86af9 100755
--- a/src/nix-build/nix-build.cc
+++ b/src/nix-build/nix-build.cc
@@ -81,7 +81,8 @@ int main(int argc, char ** argv)
         auto pure = false;
         auto fromArgs = false;
         auto packages = false;
-        auto interactive = true;
+        // Same condition as bash uses for interactive shells
+        auto interactive = isatty(STDIN_FILENO) && isatty(STDERR_FILENO);
 
         Strings instArgs;
         Strings buildArgs;
@@ -105,6 +106,7 @@ int main(int argc, char ** argv)
         std::vector<string> args;
         for (int i = 1; i < argc; ++i)
             args.push_back(argv[i]);
+
         // Heuristic to see if we're invoked as a shebang script, namely, if we
         // have a single argument, it's the name of an executable file, and it
         // starts with "#!".
@@ -115,9 +117,9 @@ int main(int argc, char ** argv)
                 if (std::regex_search(lines.front(), std::regex("^#!"))) {
                     lines.pop_front();
                     inShebang = true;
-                    for (int i = 2; i < argc - 1; ++i)
+                    for (int i = 2; i < argc; ++i)
                         savedArgs.push_back(argv[i]);
-                    std::vector<string> args;
+                    args.clear();
                     for (auto line : lines) {
                         line = chomp(line);
                         std::smatch match;
@@ -276,6 +278,7 @@ int main(int argc, char ** argv)
                 if (n >= args.size()) {
                     throw UsageError(format("%1% requires an argument") % arg);
                 }
+                interactive = false;
                 auto interpreter = args[n];
                 auto execArgs = "";
 
@@ -287,9 +290,8 @@ int main(int argc, char ** argv)
                 // executes it unless it contains the string "perl" or "indir",
                 // or (undocumented) argv[0] does not contain "perl". Exploit
                 // the latter by doing "exec -a".
-                if (std::regex_search(interpreter, std::regex("perl"))) {
-                        execArgs = "-a PERL";
-                }
+                if (std::regex_search(interpreter, std::regex("perl")))
+                    execArgs = "-a PERL";
 
                 std::ostringstream joined;
                 for (const auto & i : savedArgs)
@@ -300,7 +302,6 @@ int main(int argc, char ** argv)
                     // read the shebang to understand which packages to read from. Since
                     // this is handled via nix-shell -p, we wrap our ruby script execution
                     // in ruby -e 'load' which ignores the shebangs.
-
                     envCommand = (format("exec %1% %2% -e 'load(\"%3%\") -- %4%") % execArgs % interpreter % script % joined.str()).str();
                 } else {
                     envCommand = (format("exec %1% %2% %3% %4%") % execArgs % interpreter % script % joined.str()).str();
@@ -420,7 +421,7 @@ int main(int argc, char ** argv)
                 // environment variables and shell functions.  Also don't lose
                 // the current $PATH directories.
                 auto rcfile = (Path) tmpDir + "/rc";
-                writeFile(rcfile, (format(
+                writeFile(rcfile, fmt(
                         "rm -rf '%1%'; "
                         "[ -n \"$PS1\" ] && [ -e ~/.bashrc ] && source ~/.bashrc; "
                         "%2%"
@@ -434,13 +435,12 @@ int main(int argc, char ** argv)
                         "unset NIX_INDENT_MAKE; "
                         "shopt -u nullglob; "
                         "unset TZ; %4%"
-                        "%5%"
-                        )
-                        % (Path) tmpDir
-                        % (pure ? "" : "p=$PATH; ")
-                        % (pure ? "" : "PATH=$PATH:$p; unset p; ")
-                        % (getenv("TZ") ? (string("export TZ='") + getenv("TZ") + "'; ") : "")
-                        % envCommand).str());
+                        "%5%",
+                        (Path) tmpDir,
+                        (pure ? "" : "p=$PATH; "),
+                        (pure ? "" : "PATH=$PATH:$p; unset p; "),
+                        (getenv("TZ") ? (string("export TZ='") + getenv("TZ") + "'; ") : ""),
+                        envCommand));
 
                 Strings envStrs;
                 for (auto & i : env)
diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc
index 63e20a8c77a7..c1e6afef0e50 100644
--- a/src/nix-store/nix-store.cc
+++ b/src/nix-store/nix-store.cc
@@ -840,6 +840,12 @@ static void opServe(Strings opFlags, Strings opArgs)
         settings.buildTimeout = readInt(in);
         if (GET_PROTOCOL_MINOR(clientVersion) >= 2)
             settings.maxLogSize = readInt(in);
+        if (GET_PROTOCOL_MINOR(clientVersion) >= 3) {
+            settings.set("build-repeat", std::to_string(readInt(in)));
+            settings.set("enforce-determinism", readInt(in) != 0 ? "true" : "false");
+            settings.set("run-diff-hook", "true");
+        }
+        settings.printRepeatedBuilds = false;
     };
 
     while (true) {
@@ -955,6 +961,9 @@ static void opServe(Strings opFlags, Strings opArgs)
 
                 out << status.status << status.errorMsg;
 
+                if (GET_PROTOCOL_MINOR(clientVersion) >= 3)
+                    out << status.timesBuilt << status.isNonDeterministic << status.startTime << status.stopTime;
+
                 break;
             }
 
diff --git a/src/nix-store/serve-protocol.hh b/src/nix-store/serve-protocol.hh
index c4e2a370300b..f8cc9a4b6ebe 100644
--- a/src/nix-store/serve-protocol.hh
+++ b/src/nix-store/serve-protocol.hh
@@ -5,7 +5,7 @@ namespace nix {
 #define SERVE_MAGIC_1 0x390c9deb
 #define SERVE_MAGIC_2 0x5452eecb
 
-#define SERVE_PROTOCOL_VERSION 0x202
+#define SERVE_PROTOCOL_VERSION 0x203
 #define GET_PROTOCOL_MAJOR(x) ((x) & 0xff00)
 #define GET_PROTOCOL_MINOR(x) ((x) & 0x00ff)