about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/build-remote/build-remote.cc105
-rw-r--r--src/libexpr/common-eval-args.cc57
-rw-r--r--src/libexpr/common-eval-args.hh26
-rw-r--r--src/libexpr/common-opts.cc67
-rw-r--r--src/libexpr/common-opts.hh20
-rw-r--r--src/libexpr/eval.cc22
-rw-r--r--src/libexpr/eval.hh2
-rw-r--r--src/libexpr/lexer.l2
-rw-r--r--src/libexpr/parser.y2
-rw-r--r--src/libexpr/primops.cc19
-rw-r--r--src/libexpr/primops/fetchgit.cc113
-rw-r--r--src/libexpr/primops/fetchgit.hh15
-rw-r--r--src/libmain/common-args.cc28
-rw-r--r--src/libmain/shared.cc123
-rw-r--r--src/libmain/shared.hh14
-rw-r--r--src/libstore/binary-cache-store.cc72
-rw-r--r--src/libstore/binary-cache-store.hh1
-rw-r--r--src/libstore/build.cc423
-rw-r--r--src/libstore/download.cc4
-rw-r--r--src/libstore/globals.cc13
-rw-r--r--src/libstore/globals.hh19
-rw-r--r--src/libstore/machines.cc30
-rw-r--r--src/libstore/remote-fs-accessor.cc34
-rw-r--r--src/libstore/remote-fs-accessor.hh12
-rw-r--r--src/libstore/remote-store.cc2
-rw-r--r--src/libstore/store-api.cc24
-rw-r--r--src/libstore/store-api.hh5
-rw-r--r--src/libutil/args.cc18
-rw-r--r--src/libutil/args.hh68
-rw-r--r--src/libutil/config.cc6
-rw-r--r--src/libutil/logging.cc130
-rw-r--r--src/libutil/logging.hh6
-rw-r--r--src/libutil/serialise.hh22
-rw-r--r--src/libutil/util.cc10
-rw-r--r--src/libutil/util.hh6
-rwxr-xr-xsrc/nix-build/nix-build.cc32
-rw-r--r--src/nix-daemon/nix-daemon.cc2
-rw-r--r--src/nix-env/nix-env.cc22
-rw-r--r--src/nix-instantiate/nix-instantiate.cc22
-rw-r--r--src/nix-prefetch-url/nix-prefetch-url.cc21
-rw-r--r--src/nix-store/nix-store.cc11
-rw-r--r--src/nix/command.cc11
-rw-r--r--src/nix/command.hh12
-rw-r--r--src/nix/copy.cc12
-rw-r--r--src/nix/hash.cc14
-rw-r--r--src/nix/installables.cc26
-rw-r--r--src/nix/main.cc36
-rw-r--r--src/nix/progress-bar.cc7
-rw-r--r--src/nix/repl.cc13
-rw-r--r--src/nix/run.cc11
-rw-r--r--src/nix/search.cc4
-rw-r--r--src/nix/sigs.cc9
-rw-r--r--src/nix/verify.cc2
53 files changed, 1053 insertions, 734 deletions
diff --git a/src/build-remote/build-remote.cc b/src/build-remote/build-remote.cc
index 4b7a24d03e9a..6e05e165545d 100644
--- a/src/build-remote/build-remote.cc
+++ b/src/build-remote/build-remote.cc
@@ -16,6 +16,7 @@
 #include "serialise.hh"
 #include "store-api.hh"
 #include "derivations.hh"
+#include "local-store.hh"
 
 using namespace nix;
 using std::cin;
@@ -41,24 +42,33 @@ int main (int argc, char * * argv)
     return handleExceptions(argv[0], [&]() {
         initNix();
 
+        logger = makeJSONLogger(*logger);
+
         /* Ensure we don't get any SSH passphrase or host key popups. */
         unsetenv("DISPLAY");
         unsetenv("SSH_ASKPASS");
 
-        if (argc != 6)
+        if (argc != 2)
             throw UsageError("called without required arguments");
 
-        auto store = openStore();
+        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);
+        }
+
+        settings.maxBuildJobs.set("1"); // hack to make tests with local?root= work
 
-        auto localSystem = argv[1];
-        settings.maxSilentTime = std::stoll(argv[2]);
-        settings.buildTimeout = std::stoll(argv[3]);
-        verbosity = (Verbosity) std::stoll(argv[4]);
-        settings.builders = argv[5];
+        auto store = openStore().cast<LocalStore>();
 
         /* It would be more appropriate to use $XDG_RUNTIME_DIR, since
            that gets cleared on reboot, but it wouldn't work on macOS. */
-        currentLoad = settings.nixStateDir + "/current-load";
+        currentLoad = store->stateDir + "/current-load";
 
         std::shared_ptr<Store> sshStore;
         AutoCloseFD bestSlotLock;
@@ -73,18 +83,20 @@ int main (int argc, char * * argv)
 
         string drvPath;
         string storeUri;
-        for (string line; getline(cin, line);) {
-            auto tokens = tokenizeString<std::vector<string>>(line);
-            auto sz = tokens.size();
-            if (sz != 3 && sz != 4)
-                throw Error("invalid build hook line '%1%'", line);
-            auto amWilling = tokens[0] == "1";
-            auto neededSystem = tokens[1];
-            drvPath = tokens[2];
-            auto requiredFeatures = sz == 3 ?
-                std::set<string>{} :
-                tokenizeString<std::set<string>>(tokens[3], ",");
-            auto canBuildLocally = amWilling && (neededSystem == localSystem);
+
+        while (true) {
+
+            try {
+                auto s = readString(source);
+                if (s != "try") return;
+            } catch (EndOfFile &) { return; }
+
+            auto amWilling = readInt(source);
+            auto neededSystem = readString(source);
+            source >> drvPath;
+            auto requiredFeatures = readStrings<std::set<std::string>>(source);
+
+            auto canBuildLocally = amWilling && (neededSystem == settings.thisSystem);
 
             /* Error ignored here, will be caught later */
             mkdir(currentLoad.c_str(), 0777);
@@ -99,7 +111,7 @@ int main (int argc, char * * argv)
                 Machine * bestMachine = nullptr;
                 unsigned long long bestLoad = 0;
                 for (auto & m : machines) {
-                    debug("considering building on '%s'", m.storeUri);
+                    debug("considering building on remote machine '%s'", m.storeUri);
 
                     if (m.enabled && std::find(m.systemTypes.begin(),
                             m.systemTypes.end(),
@@ -162,9 +174,15 @@ int main (int argc, char * * argv)
 
                 try {
 
-                    Store::Params storeParams{{"max-connections", "1"}, {"log-fd", "4"}};
-                    if (bestMachine->sshKey != "")
-                        storeParams["ssh-key"] = bestMachine->sshKey;
+                    Activity act(*logger, lvlTalkative, actUnknown, fmt("connecting to '%s'", bestMachine->storeUri));
+
+                    Store::Params storeParams;
+                    if (hasPrefix(storeUri, "ssh://")) {
+                        storeParams["max-connections"] ="1";
+                        storeParams["log-fd"] = "4";
+                        if (bestMachine->sshKey != "")
+                            storeParams["ssh-key"] = bestMachine->sshKey;
+                    }
 
                     sshStore = openStore(bestMachine->storeUri, storeParams);
                     sshStore->connect();
@@ -182,32 +200,34 @@ int main (int argc, char * * argv)
         }
 
 connected:
-        std::cerr << "# accept\n";
-        string line;
-        if (!getline(cin, line))
-            throw Error("hook caller didn't send inputs");
+        std::cerr << "# accept\n" << storeUri << "\n";
 
-        auto inputs = tokenizeString<PathSet>(line);
-        if (!getline(cin, line))
-            throw Error("hook caller didn't send outputs");
-
-        auto outputs = tokenizeString<PathSet>(line);
+        auto inputs = readStrings<PathSet>(source);
+        auto outputs = readStrings<PathSet>(source);
 
         AutoCloseFD uploadLock = openLockFile(currentLoad + "/" + escapeUri(storeUri) + ".upload-lock", true);
 
-        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);
-        copyPaths(store, ref<Store>(sshStore), inputs, NoRepair, NoCheckSigs);
+        {
+            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);
+        }
+
+        {
+            Activity act(*logger, lvlTalkative, actUnknown, fmt("copying dependencies to '%s'", storeUri));
+            copyPaths(store, ref<Store>(sshStore), inputs, NoRepair, NoCheckSigs);
+        }
+
         uploadLock = -1;
 
-        BasicDerivation drv(readDerivation(drvPath));
+        BasicDerivation drv(readDerivation(store->realStoreDir + "/" + baseNameOf(drvPath)));
         drv.inputSrcs = inputs;
 
-        printError("building '%s' on '%s'", drvPath, storeUri);
         auto result = sshStore->buildDerivation(drvPath, drv);
 
         if (!result.success())
@@ -218,6 +238,7 @@ connected:
             if (!store->isValidPath(path)) missing.insert(path);
 
         if (!missing.empty()) {
+            Activity act(*logger, lvlTalkative, actUnknown, fmt("copying outputs from '%s'", storeUri));
             setenv("NIX_HELD_LOCKS", concatStringsSep(" ", missing).c_str(), 1); /* FIXME: ugly */
             copyPaths(ref<Store>(sshStore), store, missing, NoRepair, NoCheckSigs);
         }
diff --git a/src/libexpr/common-eval-args.cc b/src/libexpr/common-eval-args.cc
new file mode 100644
index 000000000000..3e0c78f280f7
--- /dev/null
+++ b/src/libexpr/common-eval-args.cc
@@ -0,0 +1,57 @@
+#include "common-eval-args.hh"
+#include "shared.hh"
+#include "download.hh"
+#include "util.hh"
+#include "eval.hh"
+
+namespace nix {
+
+MixEvalArgs::MixEvalArgs()
+{
+    mkFlag()
+        .longName("arg")
+        .description("argument to be passed to Nix functions")
+        .labels({"name", "expr"})
+        .handler([&](std::vector<std::string> ss) { autoArgs[ss[0]] = 'E' + ss[1]; });
+
+    mkFlag()
+        .longName("argstr")
+        .description("string-valued argument to be passed to Nix functions")
+        .labels({"name", "string"})
+        .handler([&](std::vector<std::string> ss) { autoArgs[ss[0]] = 'S' + ss[1]; });
+
+    mkFlag()
+        .shortName('I')
+        .longName("include")
+        .description("add a path to the list of locations used to look up <...> file names")
+        .label("path")
+        .handler([&](std::string s) { searchPath.push_back(s); });
+}
+
+Bindings * MixEvalArgs::getAutoArgs(EvalState & state)
+{
+    Bindings * res = state.allocBindings(autoArgs.size());
+    for (auto & i : autoArgs) {
+        Value * v = state.allocValue();
+        if (i.second[0] == 'E')
+            state.mkThunk_(*v, state.parseExprFromString(string(i.second, 1), absPath(".")));
+        else
+            mkString(*v, string(i.second, 1));
+        res->push_back(Attr(state.symbols.create(i.first), v));
+    }
+    res->sort();
+    return res;
+}
+
+Path lookupFileArg(EvalState & state, string s)
+{
+    if (isUri(s))
+        return getDownloader()->downloadCached(state.store, s, true);
+    else if (s.size() > 2 && s.at(0) == '<' && s.at(s.size() - 1) == '>') {
+        Path p = s.substr(1, s.size() - 2);
+        return state.findFile(p);
+    } else
+        return absPath(s);
+}
+
+}
diff --git a/src/libexpr/common-eval-args.hh b/src/libexpr/common-eval-args.hh
new file mode 100644
index 000000000000..09fa406b2cdc
--- /dev/null
+++ b/src/libexpr/common-eval-args.hh
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "args.hh"
+
+namespace nix {
+
+class Store;
+class EvalState;
+struct Bindings;
+
+struct MixEvalArgs : virtual Args
+{
+    MixEvalArgs();
+
+    Bindings * getAutoArgs(EvalState & state);
+
+    Strings searchPath;
+
+private:
+
+    std::map<std::string, std::string> autoArgs;
+};
+
+Path lookupFileArg(EvalState & state, string s);
+
+}
diff --git a/src/libexpr/common-opts.cc b/src/libexpr/common-opts.cc
deleted file mode 100644
index 6b31961d345b..000000000000
--- a/src/libexpr/common-opts.cc
+++ /dev/null
@@ -1,67 +0,0 @@
-#include "common-opts.hh"
-#include "shared.hh"
-#include "download.hh"
-#include "util.hh"
-
-
-namespace nix {
-
-
-bool parseAutoArgs(Strings::iterator & i,
-    const Strings::iterator & argsEnd, std::map<string, string> & res)
-{
-    string arg = *i;
-    if (arg != "--arg" && arg != "--argstr") return false;
-
-    UsageError error(format("'%1%' requires two arguments") % arg);
-
-    if (++i == argsEnd) throw error;
-    string name = *i;
-    if (++i == argsEnd) throw error;
-    string value = *i;
-
-    res[name] = (arg == "--arg" ? 'E' : 'S') + value;
-
-    return true;
-}
-
-
-Bindings * evalAutoArgs(EvalState & state, std::map<string, string> & in)
-{
-    Bindings * res = state.allocBindings(in.size());
-    for (auto & i : in) {
-        Value * v = state.allocValue();
-        if (i.second[0] == 'E')
-            state.mkThunk_(*v, state.parseExprFromString(string(i.second, 1), absPath(".")));
-        else
-            mkString(*v, string(i.second, 1));
-        res->push_back(Attr(state.symbols.create(i.first), v));
-    }
-    res->sort();
-    return res;
-}
-
-
-bool parseSearchPathArg(Strings::iterator & i,
-    const Strings::iterator & argsEnd, Strings & searchPath)
-{
-    if (*i != "-I") return false;
-    if (++i == argsEnd) throw UsageError("'-I' requires an argument");
-    searchPath.push_back(*i);
-    return true;
-}
-
-
-Path lookupFileArg(EvalState & state, string s)
-{
-    if (isUri(s))
-        return getDownloader()->downloadCached(state.store, s, true);
-    else if (s.size() > 2 && s.at(0) == '<' && s.at(s.size() - 1) == '>') {
-        Path p = s.substr(1, s.size() - 2);
-        return state.findFile(p);
-    } else
-        return absPath(s);
-}
-
-
-}
diff --git a/src/libexpr/common-opts.hh b/src/libexpr/common-opts.hh
deleted file mode 100644
index cb2732d6fe7e..000000000000
--- a/src/libexpr/common-opts.hh
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma once
-
-#include "eval.hh"
-
-namespace nix {
-
-class Store;
-
-/* Some common option parsing between nix-env and nix-instantiate. */
-bool parseAutoArgs(Strings::iterator & i,
-    const Strings::iterator & argsEnd, std::map<string, string> & res);
-
-Bindings * evalAutoArgs(EvalState & state, std::map<string, string> & in);
-
-bool parseSearchPathArg(Strings::iterator & i,
-    const Strings::iterator & argsEnd, Strings & searchPath);
-
-Path lookupFileArg(EvalState & state, string s);
-
-}
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 78f6b0010523..63de2d60a147 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -149,7 +149,7 @@ string showType(const Value & v)
     switch (v.type) {
         case tInt: return "an integer";
         case tBool: return "a boolean";
-        case tString: return "a string";
+        case tString: return v.string.context ? "a string with context" : "a string";
         case tPath: return "a path";
         case tNull: return "null";
         case tAttrs: return "a set";
@@ -355,6 +355,26 @@ Path EvalState::checkSourcePath(const Path & path_)
 }
 
 
+void EvalState::checkURI(const std::string & uri)
+{
+    if (!restricted) return;
+
+    /* 'uri' should be equal to a prefix, or in a subdirectory of a
+       prefix. Thus, the prefix https://github.co does not permit
+       access to https://github.com. Note: this allows 'http://' and
+       'https://' as prefixes for any http/https URI. */
+    for (auto & prefix : settings.allowedUris.get())
+        if (uri == prefix ||
+            (uri.size() > prefix.size()
+            && prefix.size() > 0
+            && hasPrefix(uri, prefix)
+            && (prefix[prefix.size() - 1] == '/' || uri[prefix.size()] == '/')))
+            return;
+
+    throw RestrictedPathError("access to URI '%s' is forbidden in restricted mode", uri);
+}
+
+
 void EvalState::addConstant(const string & name, Value & v)
 {
     Value * v2 = allocValue();
diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh
index 04a36b14cefa..f0ab1435bff3 100644
--- a/src/libexpr/eval.hh
+++ b/src/libexpr/eval.hh
@@ -110,6 +110,8 @@ public:
 
     Path checkSourcePath(const Path & path);
 
+    void checkURI(const std::string & uri);
+
     /* Parse a Nix expression from the specified file. */
     Expr * parseExprFromFile(const Path & path);
     Expr * parseExprFromFile(const Path & path, StaticEnv & staticEnv);
diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l
index 28a0a6a87896..828356bbf447 100644
--- a/src/libexpr/lexer.l
+++ b/src/libexpr/lexer.l
@@ -90,7 +90,7 @@ 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\.\_\-\+]+)+\/?
 SPATH       \<[a-zA-Z0-9\.\_\-\+]+(\/[a-zA-Z0-9\.\_\-\+]+)*\>
-URI         [a-zA-Z][a-zA-Z0-9\+\-\.]*\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*\']+
+URI         [a-zA-Z][a-zA-Z0-9\+\-\.]*\:\/\/[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*\']+|channel\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*\']+
 
 
 %%
diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y
index 669312bb7cff..eee31522830f 100644
--- a/src/libexpr/parser.y
+++ b/src/libexpr/parser.y
@@ -667,7 +667,7 @@ std::pair<bool, std::string> EvalState::resolveSearchPathElem(const SearchPathEl
         try {
             if (hasPrefix(elem.second, "git://") || hasSuffix(elem.second, ".git"))
                 // FIXME: support specifying revision/branch
-                res = { true, exportGit(store, elem.second, "master") };
+                res = { true, exportGit(store, elem.second, "master").storePath };
             else
                 res = { true, getDownloader()->downloadCached(store, elem.second, true) };
         } catch (DownloadError & e) {
diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc
index fcd3f8efee3f..cd0dfbc03e94 100644
--- a/src/libexpr/primops.cc
+++ b/src/libexpr/primops.cc
@@ -713,7 +713,7 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
         if (outputHashRecursive) outputHashAlgo = "r:" + outputHashAlgo;
 
         Path outPath = state.store->makeFixedOutputPath(outputHashRecursive, h, drvName);
-        drv.env["out"] = outPath;
+        if (!jsonObject) drv.env["out"] = outPath;
         drv.outputs["out"] = DerivationOutput(outPath, outputHashAlgo, *outputHash);
     }
 
@@ -724,7 +724,7 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
            an empty value.  This ensures that changes in the set of
            output names do get reflected in the hash. */
         for (auto & i : outputs) {
-            drv.env[i] = "";
+            if (!jsonObject) drv.env[i] = "";
             drv.outputs[i] = DerivationOutput("", "", "");
         }
 
@@ -735,7 +735,7 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
         for (auto & i : drv.outputs)
             if (i.second.path == "") {
                 Path outPath = state.store->makeOutputPath(i.first, h, drvName);
-                drv.env[i.first] = outPath;
+                if (!jsonObject) drv.env[i.first] = outPath;
                 i.second.path = outPath;
             }
     }
@@ -1907,11 +1907,11 @@ static void prim_compareVersions(EvalState & state, const Pos & pos, Value * * a
 
 
 void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
-    const string & who, bool unpack)
+    const string & who, bool unpack, const std::string & defaultName)
 {
     string url;
     Hash expectedHash;
-    string name;
+    string name = defaultName;
 
     state.forceValue(*args[0]);
 
@@ -1937,8 +1937,7 @@ void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
     } else
         url = state.forceStringNoCtx(*args[0], pos);
 
-    if (state.restricted && !expectedHash)
-        throw Error(format("'%1%' is not allowed in restricted mode") % who);
+    state.checkURI(url);
 
     Path res = getDownloader()->downloadCached(state.store, url, unpack, name, expectedHash);
     mkString(v, res, PathSet({res}));
@@ -1947,13 +1946,13 @@ void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
 
 static void prim_fetchurl(EvalState & state, const Pos & pos, Value * * args, Value & v)
 {
-    fetch(state, pos, args, v, "fetchurl", false);
+    fetch(state, pos, args, v, "fetchurl", false, "");
 }
 
 
 static void prim_fetchTarball(EvalState & state, const Pos & pos, Value * * args, Value & v)
 {
-    fetch(state, pos, args, v, "fetchTarball", true);
+    fetch(state, pos, args, v, "fetchTarball", true, "source");
 }
 
 
@@ -2008,7 +2007,7 @@ void EvalState::createBaseEnv()
        language feature gets added.  It's not necessary to increase it
        when primops get added, because you can just use `builtins ?
        primOp' to check. */
-    mkInt(v, 4);
+    mkInt(v, 5);
     addConstant("__langVersion", v);
 
     // Miscellaneous
diff --git a/src/libexpr/primops/fetchgit.cc b/src/libexpr/primops/fetchgit.cc
index e16c8235378d..4af5301247bc 100644
--- a/src/libexpr/primops/fetchgit.cc
+++ b/src/libexpr/primops/fetchgit.cc
@@ -1,3 +1,4 @@
+#include "fetchgit.hh"
 #include "primops.hh"
 #include "eval-inline.hh"
 #include "download.hh"
@@ -8,25 +9,22 @@
 
 #include <regex>
 
+#include <nlohmann/json.hpp>
+
+using namespace std::string_literals;
+
 namespace nix {
 
-Path exportGit(ref<Store> store, const std::string & uri,
-    const std::string & ref, const std::string & rev)
+GitInfo exportGit(ref<Store> store, const std::string & uri,
+    const std::string & ref, const std::string & rev,
+    const std::string & name)
 {
-    if (!isUri(uri))
-        throw EvalError(format("'%s' is not a valid URI") % uri);
-
     if (rev != "") {
         std::regex revRegex("^[0-9a-fA-F]{40}$");
         if (!std::regex_match(rev, revRegex))
             throw Error("invalid Git revision '%s'", rev);
     }
 
-    // FIXME: too restrictive, but better safe than sorry.
-    std::regex refRegex("^[0-9a-zA-Z][0-9a-zA-Z.-]+$");
-    if (!std::regex_match(ref, refRegex))
-        throw Error("invalid Git ref '%s'", ref);
-
     Path cacheDir = getCacheDir() + "/nix/git";
 
     if (!pathExists(cacheDir)) {
@@ -34,8 +32,6 @@ Path exportGit(ref<Store> store, const std::string & uri,
         runProgram("git", true, { "init", "--bare", cacheDir });
     }
 
-    //Activity act(*logger, lvlInfo, format("fetching Git repository '%s'") % uri);
-
     std::string localRef = hashString(htSHA256, fmt("%s-%s", uri, ref)).to_string(Base32, false);
 
     Path localRefFile = cacheDir + "/refs/heads/" + localRef;
@@ -47,7 +43,11 @@ Path exportGit(ref<Store> store, const std::string & uri,
     if (stat(localRefFile.c_str(), &st) != 0 ||
         st.st_mtime < now - settings.tarballTtl)
     {
-        runProgram("git", true, { "-C", cacheDir, "fetch", "--force", uri, ref + ":" + localRef });
+        Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Git repository '%s'", uri));
+
+        // FIXME: git stderr messes up our progress indicator, so
+        // we're using --quiet for now. Should process its stderr.
+        runProgram("git", true, { "-C", cacheDir, "fetch", "--quiet", "--force", "--", uri, ref + ":" + localRef });
 
         struct timeval times[2];
         times[0].tv_sec = now;
@@ -59,46 +59,65 @@ Path exportGit(ref<Store> store, const std::string & uri,
     }
 
     // FIXME: check whether rev is an ancestor of ref.
-    std::string commitHash =
-        rev != "" ? rev : chomp(readFile(localRefFile));
+    GitInfo gitInfo;
+    gitInfo.rev = rev != "" ? rev : chomp(readFile(localRefFile));
+    gitInfo.shortRev = std::string(gitInfo.rev, 0, 7);
 
-    printTalkative("using revision %s of repo '%s'", uri, commitHash);
+    printTalkative("using revision %s of repo '%s'", uri, gitInfo.rev);
 
-    Path storeLink = cacheDir + "/" + commitHash + ".link";
+    std::string storeLinkName = hashString(htSHA512, name + std::string("\0"s) + gitInfo.rev).to_string(Base32, false);
+    Path storeLink = cacheDir + "/" + storeLinkName + ".link";
     PathLocks storeLinkLock({storeLink}, fmt("waiting for lock on '%1%'...", storeLink));
 
-    if (pathExists(storeLink)) {
-        auto storePath = readLink(storeLink);
-        store->addTempRoot(storePath);
-        if (store->isValidPath(storePath)) {
-            return storePath;
+    try {
+        // FIXME: doesn't handle empty lines
+        auto json = nlohmann::json::parse(readFile(storeLink));
+
+        assert(json["name"] == name && json["rev"] == gitInfo.rev);
+
+        gitInfo.storePath = json["storePath"];
+
+        if (store->isValidPath(gitInfo.storePath)) {
+            gitInfo.revCount = json["revCount"];
+            return gitInfo;
         }
+
+    } catch (SysError & e) {
+        if (e.errNo != ENOENT) throw;
     }
 
     // FIXME: should pipe this, or find some better way to extract a
     // revision.
-    auto tar = runProgram("git", true, { "-C", cacheDir, "archive", commitHash });
+    auto tar = runProgram("git", true, { "-C", cacheDir, "archive", gitInfo.rev });
 
     Path tmpDir = createTempDir();
     AutoDelete delTmpDir(tmpDir, true);
 
     runProgram("tar", true, { "x", "-C", tmpDir }, tar);
 
-    auto storePath = store->addToStore("git-export", tmpDir);
+    gitInfo.storePath = store->addToStore(name, tmpDir);
+
+    gitInfo.revCount = std::stoull(runProgram("git", true, { "-C", cacheDir, "rev-list", "--count", gitInfo.rev }));
+
+    nlohmann::json json;
+    json["storePath"] = gitInfo.storePath;
+    json["uri"] = uri;
+    json["name"] = name;
+    json["rev"] = gitInfo.rev;
+    json["revCount"] = gitInfo.revCount;
 
-    replaceSymlink(storePath, storeLink);
+    writeFile(storeLink, json.dump());
 
-    return storePath;
+    return gitInfo;
 }
 
-static void prim_fetchgit(EvalState & state, const Pos & pos, Value * * args, Value & v)
+static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Value & v)
 {
-    // FIXME: cut&paste from fetch().
-    if (state.restricted) throw Error("'fetchgit' is not allowed in restricted mode");
-
     std::string url;
     std::string ref = "master";
     std::string rev;
+    std::string name = "source";
+    PathSet context;
 
     state.forceValue(*args[0]);
 
@@ -107,31 +126,41 @@ static void prim_fetchgit(EvalState & state, const Pos & pos, Value * * args, Va
         state.forceAttrs(*args[0], pos);
 
         for (auto & attr : *args[0]->attrs) {
-            string name(attr.name);
-            if (name == "url") {
-                PathSet context;
+            string n(attr.name);
+            if (n == "url")
                 url = state.coerceToString(*attr.pos, *attr.value, context, false, false);
-                if (hasPrefix(url, "/")) url = "file://" + url;
-            }
-            else if (name == "ref")
+            else if (n == "ref")
                 ref = state.forceStringNoCtx(*attr.value, *attr.pos);
-            else if (name == "rev")
+            else if (n == "rev")
                 rev = state.forceStringNoCtx(*attr.value, *attr.pos);
+            else if (n == "name")
+                name = state.forceStringNoCtx(*attr.value, *attr.pos);
             else
-                throw EvalError("unsupported argument '%s' to 'fetchgit', at %s", attr.name, *attr.pos);
+                throw EvalError("unsupported argument '%s' to 'fetchGit', at %s", attr.name, *attr.pos);
         }
 
         if (url.empty())
             throw EvalError(format("'url' argument required, at %1%") % pos);
 
     } else
-        url = state.forceStringNoCtx(*args[0], pos);
+        url = state.coerceToString(pos, *args[0], context, false, false);
+
+    if (hasPrefix(url, "/")) url = "file://" + url;
+
+    // FIXME: git externals probably can be used to bypass the URI
+    // whitelist. Ah well.
+    state.checkURI(url);
 
-    Path storePath = exportGit(state.store, url, ref, rev);
+    auto gitInfo = exportGit(state.store, url, ref, rev, name);
 
-    mkString(v, storePath, PathSet({storePath}));
+    state.mkAttrs(v, 8);
+    mkString(*state.allocAttr(v, state.sOutPath), gitInfo.storePath, PathSet({gitInfo.storePath}));
+    mkString(*state.allocAttr(v, state.symbols.create("rev")), gitInfo.rev);
+    mkString(*state.allocAttr(v, state.symbols.create("shortRev")), gitInfo.shortRev);
+    mkInt(*state.allocAttr(v, state.symbols.create("revCount")), gitInfo.revCount);
+    v.attrs->sort();
 }
 
-static RegisterPrimOp r("__fetchgit", 1, prim_fetchgit);
+static RegisterPrimOp r("fetchGit", 1, prim_fetchGit);
 
 }
diff --git a/src/libexpr/primops/fetchgit.hh b/src/libexpr/primops/fetchgit.hh
index ff228f3b3c6a..056b6fcbe78d 100644
--- a/src/libexpr/primops/fetchgit.hh
+++ b/src/libexpr/primops/fetchgit.hh
@@ -2,13 +2,22 @@
 
 #include <string>
 
-#include "ref.hh"
+#include "util.hh"
 
 namespace nix {
 
 class Store;
 
-Path exportGit(ref<Store> store, const std::string & uri,
-    const std::string & ref, const std::string & rev = "");
+struct GitInfo
+{
+    Path storePath;
+    std::string rev;
+    std::string shortRev;
+    uint64_t revCount = 0;
+};
+
+GitInfo exportGit(ref<Store> store, const std::string & uri,
+    const std::string & ref, const std::string & rev = "",
+    const std::string & name = "");
 
 }
diff --git a/src/libmain/common-args.cc b/src/libmain/common-args.cc
index 3fa42c2aafa9..ea27aaa35e03 100644
--- a/src/libmain/common-args.cc
+++ b/src/libmain/common-args.cc
@@ -6,28 +6,30 @@ namespace nix {
 MixCommonArgs::MixCommonArgs(const string & programName)
     : programName(programName)
 {
-    mkFlag('v', "verbose", "increase verbosity level", []() {
-        verbosity = (Verbosity) (verbosity + 1);
-    });
+    mkFlag()
+        .longName("verbose")
+        .shortName('v')
+        .description("increase verbosity level")
+        .handler([]() { verbosity = (Verbosity) (verbosity + 1); });
 
-    mkFlag(0, "quiet", "decrease verbosity level", []() {
-        verbosity = verbosity > lvlError ? (Verbosity) (verbosity - 1) : lvlError;
-    });
+    mkFlag()
+        .longName("quiet")
+        .description("decrease verbosity level")
+        .handler([]() { verbosity = verbosity > lvlError ? (Verbosity) (verbosity - 1) : lvlError; });
 
-    mkFlag(0, "debug", "enable debug output", []() {
-        verbosity = lvlDebug;
-    });
+    mkFlag()
+        .longName("debug")
+        .description("enable debug output")
+        .handler([]() { verbosity = lvlDebug; });
 
     mkFlag()
         .longName("option")
         .labels({"name", "value"})
         .description("set a Nix configuration option (overriding nix.conf)")
         .arity(2)
-        .handler([](Strings ss) {
-            auto name = ss.front(); ss.pop_front();
-            auto value = ss.front();
+        .handler([](std::vector<std::string> ss) {
             try {
-                settings.set(name, value);
+                settings.set(ss[0], ss[1]);
             } catch (UsageError & e) {
                 warn(e.what());
             }
diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc
index 6393d80bbf00..0f599f388585 100644
--- a/src/libmain/shared.cc
+++ b/src/libmain/shared.cc
@@ -1,4 +1,3 @@
-#include "common-args.hh"
 #include "globals.hh"
 #include "shared.hh"
 #include "store-api.hh"
@@ -149,74 +148,84 @@ void initNix()
 }
 
 
-struct LegacyArgs : public MixCommonArgs
+LegacyArgs::LegacyArgs(const std::string & programName,
+    std::function<bool(Strings::iterator & arg, const Strings::iterator & end)> parseArg)
+    : MixCommonArgs(programName), parseArg(parseArg)
 {
-    std::function<bool(Strings::iterator & arg, const Strings::iterator & end)> parseArg;
-
-    LegacyArgs(const std::string & programName,
-        std::function<bool(Strings::iterator & arg, const Strings::iterator & end)> parseArg)
-        : MixCommonArgs(programName), parseArg(parseArg)
-    {
-        mkFlag('Q', "no-build-output", "do not show build output",
-            &settings.verboseBuild, false);
-
-        mkFlag('K', "keep-failed", "keep temporary directories of failed builds",
-            &(bool&) settings.keepFailed);
-
-        mkFlag('k', "keep-going", "keep going after a build fails",
-            &(bool&) settings.keepGoing);
+    mkFlag()
+        .longName("no-build-output")
+        .shortName('Q')
+        .description("do not show build output")
+        .set(&settings.verboseBuild, false);
+
+    mkFlag()
+        .longName("keep-failed")
+        .shortName('K')
+        .description("keep temporary directories of failed builds")
+        .set(&(bool&) settings.keepFailed, true);
+
+    mkFlag()
+        .longName("keep-going")
+        .shortName('k')
+        .description("keep going after a build fails")
+        .set(&(bool&) settings.keepGoing, true);
+
+    mkFlag()
+        .longName("fallback")
+        .description("build from source if substitution fails")
+        .set(&(bool&) settings.tryFallback, true);
+
+    mkFlag1('j', "max-jobs", "jobs", "maximum number of parallel builds", [=](std::string s) {
+        settings.set("max-jobs", s);
+    });
 
-        mkFlag(0, "fallback", "build from source if substitution fails", []() {
-            settings.tryFallback = true;
+    auto intSettingAlias = [&](char shortName, const std::string & longName,
+        const std::string & description, const std::string & dest) {
+        mkFlag<unsigned int>(shortName, longName, description, [=](unsigned int n) {
+            settings.set(dest, std::to_string(n));
         });
+    };
 
-        mkFlag1('j', "max-jobs", "jobs", "maximum number of parallel builds", [=](std::string s) {
-            settings.set("max-jobs", s);
-        });
+    intSettingAlias(0, "cores", "maximum number of CPU cores to use inside a build", "cores");
+    intSettingAlias(0, "max-silent-time", "number of seconds of silence before a build is killed", "max-silent-time");
+    intSettingAlias(0, "timeout", "number of seconds before a build is killed", "timeout");
 
-        auto intSettingAlias = [&](char shortName, const std::string & longName,
-            const std::string & description, const std::string & dest) {
-            mkFlag<unsigned int>(shortName, longName, description, [=](unsigned int n) {
-                settings.set(dest, std::to_string(n));
-            });
-        };
+    mkFlag(0, "readonly-mode", "do not write to the Nix store",
+        &settings.readOnlyMode);
 
-        intSettingAlias(0, "cores", "maximum number of CPU cores to use inside a build", "cores");
-        intSettingAlias(0, "max-silent-time", "number of seconds of silence before a build is killed", "max-silent-time");
-        intSettingAlias(0, "timeout", "number of seconds before a build is killed", "timeout");
+    mkFlag(0, "show-trace", "show Nix expression stack trace in evaluation errors",
+        &settings.showTrace);
 
-        mkFlag(0, "readonly-mode", "do not write to the Nix store",
-            &settings.readOnlyMode);
+    mkFlag(0, "no-gc-warning", "disable warning about not using '--add-root'",
+        &gcWarning, false);
 
-        mkFlag(0, "no-build-hook", "disable use of the build hook mechanism",
-            &(bool&) settings.useBuildHook, false);
+    mkFlag()
+        .longName("store")
+        .label("store-uri")
+        .description("URI of the Nix store to use")
+        .dest(&(std::string&) settings.storeUri);
+}
 
-        mkFlag(0, "show-trace", "show Nix expression stack trace in evaluation errors",
-            &settings.showTrace);
 
-        mkFlag(0, "no-gc-warning", "disable warning about not using '--add-root'",
-            &gcWarning, false);
-    }
+bool LegacyArgs::processFlag(Strings::iterator & pos, Strings::iterator end)
+{
+    if (MixCommonArgs::processFlag(pos, end)) return true;
+    bool res = parseArg(pos, end);
+    if (res) ++pos;
+    return res;
+}
 
-    bool processFlag(Strings::iterator & pos, Strings::iterator end) override
-    {
-        if (MixCommonArgs::processFlag(pos, end)) return true;
-        bool res = parseArg(pos, end);
-        if (res) ++pos;
-        return res;
-    }
 
-    bool processArgs(const Strings & args, bool finish) override
-    {
-        if (args.empty()) return true;
-        assert(args.size() == 1);
-        Strings ss(args);
-        auto pos = ss.begin();
-        if (!parseArg(pos, ss.end()))
-            throw UsageError(format("unexpected argument '%1%'") % args.front());
-        return true;
-    }
-};
+bool LegacyArgs::processArgs(const Strings & args, bool finish)
+{
+    if (args.empty()) return true;
+    assert(args.size() == 1);
+    Strings ss(args);
+    auto pos = ss.begin();
+    if (!parseArg(pos, ss.end()))
+        throw UsageError(format("unexpected argument '%1%'") % args.front());
+    return true;
+}
 
 
 void parseCmdLine(int argc, char * * argv,
diff --git a/src/libmain/shared.hh b/src/libmain/shared.hh
index 2a1e42dd9774..9219dbed8325 100644
--- a/src/libmain/shared.hh
+++ b/src/libmain/shared.hh
@@ -2,6 +2,7 @@
 
 #include "util.hh"
 #include "args.hh"
+#include "common-args.hh"
 
 #include <signal.h>
 
@@ -69,6 +70,19 @@ template<class N> N getIntArg(const string & opt,
 }
 
 
+struct LegacyArgs : public MixCommonArgs
+{
+    std::function<bool(Strings::iterator & arg, const Strings::iterator & end)> parseArg;
+
+    LegacyArgs(const std::string & programName,
+        std::function<bool(Strings::iterator & arg, const Strings::iterator & end)> parseArg);
+
+    bool processFlag(Strings::iterator & pos, Strings::iterator end) override;
+
+    bool processArgs(const Strings & args, bool finish) override;
+};
+
+
 /* Show the manual page for the specified program. */
 void showManPage(const string & name);
 
diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc
index 556fa3d59355..67607ab3d43a 100644
--- a/src/libstore/binary-cache-store.cc
+++ b/src/libstore/binary-cache-store.cc
@@ -17,66 +17,6 @@
 
 namespace nix {
 
-/* Given requests for a path /nix/store/<x>/<y>, this accessor will
-   first download the NAR for /nix/store/<x> from the binary cache,
-   build a NAR accessor for that NAR, and use that to access <y>. */
-struct BinaryCacheStoreAccessor : public FSAccessor
-{
-    ref<BinaryCacheStore> store;
-
-    std::map<Path, ref<FSAccessor>> nars;
-
-    BinaryCacheStoreAccessor(ref<BinaryCacheStore> store)
-        : store(store)
-    {
-    }
-
-    std::pair<ref<FSAccessor>, Path> fetch(const Path & path_)
-    {
-        auto path = canonPath(path_);
-
-        auto storePath = store->toStorePath(path);
-        std::string restPath = std::string(path, storePath.size());
-
-        if (!store->isValidPath(storePath))
-            throw InvalidPath(format("path '%1%' is not a valid store path") % storePath);
-
-        auto i = nars.find(storePath);
-        if (i != nars.end()) return {i->second, restPath};
-
-        StringSink sink;
-        store->narFromPath(storePath, sink);
-
-        auto accessor = makeNarAccessor(sink.s);
-        nars.emplace(storePath, accessor);
-        return {accessor, restPath};
-    }
-
-    Stat stat(const Path & path) override
-    {
-        auto res = fetch(path);
-        return res.first->stat(res.second);
-    }
-
-    StringSet readDirectory(const Path & path) override
-    {
-        auto res = fetch(path);
-        return res.first->readDirectory(res.second);
-    }
-
-    std::string readFile(const Path & path) override
-    {
-        auto res = fetch(path);
-        return res.first->readFile(res.second);
-    }
-
-    std::string readLink(const Path & path) override
-    {
-        auto res = fetch(path);
-        return res.first->readLink(res.second);
-    }
-};
-
 BinaryCacheStore::BinaryCacheStore(const Params & params)
     : Store(params)
 {
@@ -161,7 +101,7 @@ void BinaryCacheStore::addToStore(const ValidPathInfo & info, const ref<std::str
     if (info.narHash && info.narHash != narInfo->narHash)
         throw Error(format("refusing to copy corrupted path '%1%' to binary cache") % info.path);
 
-    auto accessor_ = std::dynamic_pointer_cast<BinaryCacheStoreAccessor>(accessor);
+    auto accessor_ = std::dynamic_pointer_cast<RemoteFSAccessor>(accessor);
 
     /* Optionally write a JSON file containing a listing of the
        contents of the NAR. */
@@ -174,8 +114,10 @@ void BinaryCacheStore::addToStore(const ValidPathInfo & info, const ref<std::str
 
             auto narAccessor = makeNarAccessor(nar);
 
-            if (accessor_)
+            if (accessor_) {
                 accessor_->nars.emplace(info.path, narAccessor);
+                accessor_->addToCache(info.path, *nar);
+            }
 
             std::function<void(const Path &, JSONPlaceholder &)> recurse;
 
@@ -220,8 +162,10 @@ void BinaryCacheStore::addToStore(const ValidPathInfo & info, const ref<std::str
     }
 
     else {
-        if (accessor_)
+        if (accessor_) {
             accessor_->nars.emplace(info.path, makeNarAccessor(nar));
+            accessor_->addToCache(info.path, *nar);
+        }
     }
 
     /* Compress the NAR. */
@@ -379,7 +323,7 @@ Path BinaryCacheStore::addTextToStore(const string & name, const string & s,
 
 ref<FSAccessor> BinaryCacheStore::getFSAccessor()
 {
-    return make_ref<RemoteFSAccessor>(ref<Store>(shared_from_this()));
+    return make_ref<RemoteFSAccessor>(ref<Store>(shared_from_this()), localNarCache);
 }
 
 std::shared_ptr<std::string> BinaryCacheStore::getBuildLog(const Path & path)
diff --git a/src/libstore/binary-cache-store.hh b/src/libstore/binary-cache-store.hh
index f9c1c2cbe8a8..d3b0e0bd9332 100644
--- a/src/libstore/binary-cache-store.hh
+++ b/src/libstore/binary-cache-store.hh
@@ -18,6 +18,7 @@ public:
     const Setting<std::string> compression{this, "xz", "compression", "NAR compression method ('xz', 'bzip2', or 'none')"};
     const Setting<bool> writeNARListing{this, false, "write-nar-listing", "whether to write a JSON file listing the files in each NAR"};
     const Setting<Path> secretKeyFile{this, "", "secret-key", "path to secret key used to sign the binary cache"};
+    const Setting<Path> localNarCache{this, "", "local-nar-cache", "path to a local cache of NARs"};
 
 private:
 
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
index 9069d9b06e08..061682377257 100644
--- a/src/libstore/build.cc
+++ b/src/libstore/build.cc
@@ -18,6 +18,7 @@
 #include <thread>
 #include <future>
 #include <chrono>
+#include <regex>
 
 #include <limits.h>
 #include <sys/time.h>
@@ -274,6 +275,10 @@ public:
     uint64_t expectedNarSize = 0;
     uint64_t doneNarSize = 0;
 
+    /* Whether to ask the build hook if it can build a derivation. If
+       it answers with "decline-permanently", we don't try again. */
+    bool tryBuildHook = true;
+
     Worker(LocalStore & store);
     ~Worker();
 
@@ -606,6 +611,10 @@ struct HookInstance
     /* The process ID of the hook. */
     Pid pid;
 
+    FdSink sink;
+
+    std::map<ActivityId, Activity> activities;
+
     HookInstance();
 
     ~HookInstance();
@@ -642,11 +651,7 @@ HookInstance::HookInstance()
 
         Strings args = {
             baseNameOf(settings.buildHook),
-            settings.thisSystem,
-            std::to_string(settings.maxSilentTime),
-            std::to_string(settings.buildTimeout),
             std::to_string(verbosity),
-            settings.builders
         };
 
         execv(settings.buildHook.get().c_str(), stringsToCharPtrs(args).data());
@@ -657,6 +662,11 @@ HookInstance::HookInstance()
     pid.setSeparatePG(true);
     fromHook.writeSide = -1;
     toHook.readSide = -1;
+
+    sink = FdSink(toHook.writeSide.get());
+    for (auto & setting : settings.getSettings())
+        sink << 1 << setting.first << setting.second;
+    sink << 0;
 }
 
 
@@ -762,6 +772,8 @@ private:
     std::string currentLogLine;
     size_t currentLogLinePos = 0; // to handle carriage return
 
+    std::string currentHookLine;
+
     /* Pipe for the builder's standard output/error. */
     Pipe builderOut;
 
@@ -843,6 +855,9 @@ private:
 
     std::map<ActivityId, Activity> builderActivities;
 
+    /* The remote machine on which we're building. */
+    std::string machineName;
+
 public:
     DerivationGoal(const Path & drvPath, const StringSet & wantedOutputs,
         Worker & worker, BuildMode buildMode = bmNormal);
@@ -899,9 +914,6 @@ private:
     /* Make a file owned by the builder. */
     void chownToBuilder(const Path & path);
 
-    /* Handle the exportReferencesGraph attribute. */
-    void doExportReferencesGraph();
-
     /* Run the builder's process. */
     void runChild();
 
@@ -1390,8 +1402,15 @@ void DerivationGoal::tryToBuild()
     bool buildLocally = buildMode != bmNormal || drv->willBuildLocally();
 
     auto started = [&]() {
-        act = std::make_unique<Activity>(*logger, lvlInfo, actBuild,
-            fmt("building '%s'", drvPath), Logger::Fields{drvPath});
+        auto msg = fmt(
+            buildMode == bmRepair ? "repairing outputs of '%s'" :
+            buildMode == bmCheck ? "checking outputs of '%s'" :
+            nrRounds > 1 ? "building '%s' (round %d/%d)" :
+            "building '%s'", drvPath, curRound, nrRounds);
+        fmt("building '%s'", drvPath);
+        if (hook) msg += fmt(" on '%s'", machineName);
+        act = std::make_unique<Activity>(*logger, lvlInfo, actBuild, msg,
+            Logger::Fields{drvPath, hook ? machineName : "", curRound, nrRounds});
         mcRunningBuilds = std::make_unique<MaintainCount<uint64_t>>(worker.runningBuilds);
         worker.updateProgress();
     };
@@ -1619,7 +1638,7 @@ void DerivationGoal::buildDone()
 
 HookReply DerivationGoal::tryBuildHook()
 {
-    if (!settings.useBuildHook || !useDerivation) return rpDecline;
+    if (!worker.tryBuildHook || !useDerivation) return rpDecline;
 
     if (!worker.hook)
         worker.hook = std::make_unique<HookInstance>();
@@ -1633,21 +1652,29 @@ HookReply DerivationGoal::tryBuildHook()
         for (auto & i : features) checkStoreName(i); /* !!! abuse */
 
         /* Send the request to the hook. */
-        writeLine(worker.hook->toHook.writeSide.get(), (format("%1% %2% %3% %4%")
-                % (worker.getNrLocalBuilds() < settings.maxBuildJobs ? "1" : "0")
-                % drv->platform % drvPath % concatStringsSep(",", features)).str());
+        worker.hook->sink
+            << "try"
+            << (worker.getNrLocalBuilds() < settings.maxBuildJobs ? 1 : 0)
+            << drv->platform
+            << drvPath
+            << features;
+        worker.hook->sink.flush();
 
         /* Read the first line of input, which should be a word indicating
            whether the hook wishes to perform the build. */
         string reply;
         while (true) {
             string s = readLine(worker.hook->fromHook.readSide.get());
-            if (string(s, 0, 2) == "# ") {
+            if (handleJSONLogMessage(s, worker.act, worker.hook->activities, true))
+                ;
+            else if (string(s, 0, 2) == "# ") {
                 reply = string(s, 2);
                 break;
             }
-            s += "\n";
-            writeToStderr(s);
+            else {
+                s += "\n";
+                writeToStderr(s);
+            }
         }
 
         debug(format("hook reply is '%1%'") % reply);
@@ -1655,7 +1682,7 @@ HookReply DerivationGoal::tryBuildHook()
         if (reply == "decline")
             return rpDecline;
         else if (reply == "decline-permanently") {
-            settings.useBuildHook = false;
+            worker.tryBuildHook = false;
             worker.hook = 0;
             return rpDecline;
         }
@@ -1674,18 +1701,19 @@ HookReply DerivationGoal::tryBuildHook()
             throw;
     }
 
-    printMsg(lvlTalkative, format("using hook to build path(s) %1%") % showPaths(missingPaths));
-
     hook = std::move(worker.hook);
 
+    machineName = readLine(hook->fromHook.readSide.get());
+
     /* Tell the hook all the inputs that have to be copied to the
        remote system. */
-    writeLine(hook->toHook.writeSide.get(), concatStringsSep(" ", inputPaths));
+    hook->sink << inputPaths;
 
     /* Tell the hooks the missing outputs that have to be copied back
        from the remote system. */
-    writeLine(hook->toHook.writeSide.get(), concatStringsSep(" ", missingPaths));
+    hook->sink << missingPaths;
 
+    hook->sink = FdSink();
     hook->toHook.writeSide = -1;
 
     /* Create the log file and pipe. */
@@ -1714,16 +1742,44 @@ int childEntry(void * arg)
 }
 
 
-void DerivationGoal::startBuilder()
+PathSet exportReferences(Store & store, PathSet storePaths)
 {
-    auto f = format(
-        buildMode == bmRepair ? "repairing path(s) %1%" :
-        buildMode == bmCheck ? "checking path(s) %1%" :
-        nrRounds > 1 ? "building path(s) %1% (round %2%/%3%)" :
-        "building path(s) %1%");
-    f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
-    printInfo(f % showPaths(missingPaths) % curRound % nrRounds);
+    PathSet paths;
+
+    for (auto storePath : storePaths) {
 
+        /* Check that the store path is valid. */
+        if (!store.isInStore(storePath))
+            throw BuildError(format("'exportReferencesGraph' contains a non-store path '%1%'")
+                % storePath);
+        storePath = store.toStorePath(storePath);
+        if (!store.isValidPath(storePath))
+            throw BuildError(format("'exportReferencesGraph' contains an invalid path '%1%'")
+                % storePath);
+
+        store.computeFSClosure(storePath, paths);
+    }
+
+    /* If there are derivations in the graph, then include their
+       outputs as well.  This is useful if you want to do things
+       like passing all build-time dependencies of some path to a
+       derivation that builds a NixOS DVD image. */
+    PathSet paths2(paths);
+
+    for (auto & j : paths2) {
+        if (isDerivation(j)) {
+            Derivation drv = store.derivationFromPath(j);
+            for (auto & k : drv.outputs)
+                store.computeFSClosure(k.second.path, paths);
+        }
+    }
+
+    return paths;
+}
+
+
+void DerivationGoal::startBuilder()
+{
     /* Right platform? */
     if (!drv->canBuildLocally()) {
         throw Error(
@@ -1797,7 +1853,29 @@ void DerivationGoal::startBuilder()
     writeStructuredAttrs();
 
     /* Handle exportReferencesGraph(), if set. */
-    doExportReferencesGraph();
+    if (!drv->env.count("__json")) {
+        /* The `exportReferencesGraph' feature allows the references graph
+           to be passed to a builder.  This attribute should be a list of
+           pairs [name1 path1 name2 path2 ...].  The references graph of
+           each `pathN' will be stored in a text file `nameN' in the
+           temporary build directory.  The text files have the format used
+           by `nix-store --register-validity'.  However, the deriver
+           fields are left empty. */
+        string s = get(drv->env, "exportReferencesGraph");
+        Strings ss = tokenizeString<Strings>(s);
+        if (ss.size() % 2 != 0)
+            throw BuildError(format("odd number of tokens in 'exportReferencesGraph': '%1%'") % s);
+        for (Strings::iterator i = ss.begin(); i != ss.end(); ) {
+            string fileName = *i++;
+            checkStoreName(fileName); /* !!! abuse of this function */
+            Path storePath = *i++;
+
+            /* Write closure info to <fileName>. */
+            writeFile(tmpDir + "/" + fileName,
+                worker.store.makeValidityRegistration(
+                    exportReferences(worker.store, {storePath}), false, false));
+        }
+    }
 
     if (useChroot) {
 
@@ -2266,88 +2344,127 @@ void DerivationGoal::initEnv()
 }
 
 
+static std::regex shVarName("[A-Za-z_][A-Za-z0-9_]*");
+
+
 void DerivationGoal::writeStructuredAttrs()
 {
-    auto json = drv->env.find("__json");
-    if (json == drv->env.end()) return;
+    auto jsonAttr = drv->env.find("__json");
+    if (jsonAttr == drv->env.end()) return;
 
-    writeFile(tmpDir + "/.attrs.json", rewriteStrings(json->second, inputRewrites));
-}
+    try {
 
+        auto jsonStr = rewriteStrings(jsonAttr->second, inputRewrites);
 
-void DerivationGoal::chownToBuilder(const Path & path)
-{
-    if (!buildUser) return;
-    if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1)
-        throw SysError(format("cannot change ownership of '%1%'") % path);
-}
+        auto json = nlohmann::json::parse(jsonStr);
+
+        /* Add an "outputs" object containing the output paths. */
+        nlohmann::json outputs;
+        for (auto & i : drv->outputs)
+            outputs[i.first] = rewriteStrings(i.second.path, inputRewrites);
+        json["outputs"] = outputs;
+
+        /* Handle exportReferencesGraph. */
+        auto e = json.find("exportReferencesGraph");
+        if (e != json.end() && e->is_object()) {
+            for (auto i = e->begin(); i != e->end(); ++i) {
+                std::ostringstream str;
+                {
+                    JSONPlaceholder jsonRoot(str, true);
+                    PathSet storePaths;
+                    for (auto & p : *i)
+                        storePaths.insert(p.get<std::string>());
+                    worker.store.pathInfoToJSON(jsonRoot,
+                        exportReferences(worker.store, storePaths), false, true);
+                }
+                json[i.key()] = nlohmann::json::parse(str.str()); // urgh
+            }
+        }
 
+        writeFile(tmpDir + "/.attrs.json", json.dump());
 
-void DerivationGoal::doExportReferencesGraph()
-{
-    /* The `exportReferencesGraph' feature allows the references graph
-       to be passed to a builder.  This attribute should be a list of
-       pairs [name1 path1 name2 path2 ...].  The references graph of
-       each `pathN' will be stored in a text file `nameN' in the
-       temporary build directory.  The text files have the format used
-       by `nix-store --register-validity'.  However, the deriver
-       fields are left empty. */
-    string s = get(drv->env, "exportReferencesGraph");
-    Strings ss = tokenizeString<Strings>(s);
-    if (ss.size() % 2 != 0)
-        throw BuildError(format("odd number of tokens in 'exportReferencesGraph': '%1%'") % s);
-    for (Strings::iterator i = ss.begin(); i != ss.end(); ) {
-        string fileName = *i++;
-        checkStoreName(fileName); /* !!! abuse of this function */
+        /* As a convenience to bash scripts, write a shell file that
+           maps all attributes that are representable in bash -
+           namely, strings, integers, nulls, Booleans, and arrays and
+           objects consisting entirely of those values. (So nested
+           arrays or objects are not supported.) */
 
-        /* Check that the store path is valid. */
-        Path storePath = *i++;
-        if (!worker.store.isInStore(storePath))
-            throw BuildError(format("'exportReferencesGraph' contains a non-store path '%1%'")
-                % storePath);
-        storePath = worker.store.toStorePath(storePath);
-        if (!worker.store.isValidPath(storePath))
-            throw BuildError(format("'exportReferencesGraph' contains an invalid path '%1%'")
-                % storePath);
+        auto handleSimpleType = [](const nlohmann::json & value) -> std::experimental::optional<std::string> {
+            if (value.is_string())
+                return shellEscape(value);
 
-        /* If there are derivations in the graph, then include their
-           outputs as well.  This is useful if you want to do things
-           like passing all build-time dependencies of some path to a
-           derivation that builds a NixOS DVD image. */
-        PathSet paths, paths2;
-        worker.store.computeFSClosure(storePath, paths);
-        paths2 = paths;
-
-        for (auto & j : paths2) {
-            if (isDerivation(j)) {
-                Derivation drv = worker.store.derivationFromPath(j);
-                for (auto & k : drv.outputs)
-                    worker.store.computeFSClosure(k.second.path, paths);
+            if (value.is_number()) {
+                auto f = value.get<float>();
+                if (std::ceil(f) == f)
+                    return std::to_string(value.get<int>());
             }
-        }
 
-        if (!drv->env.count("__json")) {
+            if (value.is_null())
+                return std::string("''");
 
-            /* Write closure info to <fileName>. */
-            writeFile(tmpDir + "/" + fileName,
-                worker.store.makeValidityRegistration(paths, false, false));
+            if (value.is_boolean())
+                return value.get<bool>() ? std::string("1") : std::string("");
 
-        } else {
+            return {};
+        };
 
-            /* Write a more comprehensive JSON serialisation to
-               <fileName>. */
-            std::ostringstream str;
-            {
-                JSONPlaceholder jsonRoot(str, true);
-                worker.store.pathInfoToJSON(jsonRoot, paths, false, true);
+        std::string jsonSh;
+
+        for (auto i = json.begin(); i != json.end(); ++i) {
+
+            if (!std::regex_match(i.key(), shVarName)) continue;
+
+            auto & value = i.value();
+
+            auto s = handleSimpleType(value);
+            if (s)
+                jsonSh += fmt("declare %s=%s\n", i.key(), *s);
+
+            else if (value.is_array()) {
+                std::string s2;
+                bool good = true;
+
+                for (auto i = value.begin(); i != value.end(); ++i) {
+                    auto s3 = handleSimpleType(i.value());
+                    if (!s3) { good = false; break; }
+                    s2 += *s3; s2 += ' ';
+                }
+
+                if (good)
+                    jsonSh += fmt("declare -a %s=(%s)\n", i.key(), s2);
             }
-            writeFile(tmpDir + "/" + fileName, str.str());
 
+            else if (value.is_object()) {
+                std::string s2;
+                bool good = true;
+
+                for (auto i = value.begin(); i != value.end(); ++i) {
+                    auto s3 = handleSimpleType(i.value());
+                    if (!s3) { good = false; break; }
+                    s2 += fmt("[%s]=%s ", shellEscape(i.key()), *s3);
+                }
+
+                if (good)
+                    jsonSh += fmt("declare -A %s=(%s)\n", i.key(), s2);
+            }
         }
+
+        writeFile(tmpDir + "/.attrs.sh", jsonSh);
+
+    } catch (std::exception & e) {
+        throw Error("cannot process __json attribute of '%s': %s", drvPath, e.what());
     }
 }
 
 
+void DerivationGoal::chownToBuilder(const Path & path)
+{
+    if (!buildUser) return;
+    if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1)
+        throw SysError(format("cannot change ownership of '%1%'") % path);
+}
+
+
 void setupSeccomp()
 {
 #if __linux__
@@ -2402,64 +2519,6 @@ void setupSeccomp()
 }
 
 
-struct BuilderLogger : Logger
-{
-    Logger & prevLogger;
-
-    BuilderLogger(Logger & prevLogger) : prevLogger(prevLogger) { }
-
-    void addFields(nlohmann::json & json, const Fields & fields)
-    {
-        if (fields.empty()) return;
-        auto & arr = json["fields"] = nlohmann::json::array();
-        for (auto & f : fields)
-            if (f.type == Logger::Field::tInt)
-                arr.push_back(f.i);
-            else if (f.type == Logger::Field::tString)
-                arr.push_back(f.s);
-            else
-                abort();
-    }
-
-    void log(Verbosity lvl, const FormatOrString & fs) override
-    {
-        prevLogger.log(lvl, fs);
-    }
-
-    void startActivity(ActivityId act, Verbosity lvl, ActivityType type,
-        const std::string & s, const Fields & fields, ActivityId parent) override
-    {
-        nlohmann::json json;
-        json["action"] = "start";
-        json["id"] = act;
-        json["level"] = lvl;
-        json["type"] = type;
-        json["text"] = s;
-        addFields(json, fields);
-        // FIXME: handle parent
-        log(lvlError, "@nix " + json.dump());
-    }
-
-    void stopActivity(ActivityId act) override
-    {
-        nlohmann::json json;
-        json["action"] = "stop";
-        json["id"] = act;
-        log(lvlError, "@nix " + json.dump());
-    }
-
-    void result(ActivityId act, ResultType type, const Fields & fields) override
-    {
-        nlohmann::json json;
-        json["action"] = "result";
-        json["id"] = act;
-        json["type"] = type;
-        addFields(json, fields);
-        log(lvlError, "@nix " + json.dump());
-    }
-};
-
-
 void DerivationGoal::runChild()
 {
     /* Warning: in the child we should absolutely not make any SQLite
@@ -2868,7 +2927,7 @@ void DerivationGoal::runChild()
         /* Execute the program.  This should not return. */
         if (drv->isBuiltin()) {
             try {
-                logger = new BuilderLogger(*logger);
+                logger = makeJSONLogger(*logger);
                 if (drv->builder == "builtin:fetchurl")
                     builtinFetchurl(*drv, netrcData);
                 else
@@ -3309,8 +3368,14 @@ void DerivationGoal::handleChildOutput(int fd, const string & data)
         if (logSink) (*logSink)(data);
     }
 
-    if (hook && fd == hook->fromHook.readSide.get())
-        printError(chomp(data));
+    if (hook && fd == hook->fromHook.readSide.get()) {
+        for (auto c : data)
+            if (c == '\n') {
+                handleJSONLogMessage(currentHookLine, worker.act, hook->activities, true);
+                currentHookLine.clear();
+            } else
+                currentHookLine += c;
+    }
 }
 
 
@@ -3321,56 +3386,10 @@ void DerivationGoal::handleEOF(int fd)
 }
 
 
-static Logger::Fields getFields(nlohmann::json & json)
-{
-    Logger::Fields fields;
-    for (auto & f : json) {
-        if (f.type() == nlohmann::json::value_t::number_unsigned)
-            fields.emplace_back(Logger::Field(f.get<uint64_t>()));
-        else if (f.type() == nlohmann::json::value_t::string)
-            fields.emplace_back(Logger::Field(f.get<std::string>()));
-        else throw Error("unsupported JSON type %d", (int) f.type());
-    }
-    return fields;
-}
-
-
 void DerivationGoal::flushLine()
 {
-    if (hasPrefix(currentLogLine, "@nix ")) {
-
-        try {
-            auto json = nlohmann::json::parse(std::string(currentLogLine, 5));
-
-            std::string action = json["action"];
-
-            if (action == "start") {
-                auto type = (ActivityType) json["type"];
-                if (type == actDownload)
-                    builderActivities.emplace(std::piecewise_construct,
-                        std::forward_as_tuple(json["id"]),
-                        std::forward_as_tuple(*logger, (Verbosity) json["level"], type,
-                            json["text"], getFields(json["fields"]), act->id));
-            }
-
-            else if (action == "stop")
-                builderActivities.erase((ActivityId) json["id"]);
-
-            else if (action == "result") {
-                auto i = builderActivities.find((ActivityId) json["id"]);
-                if (i != builderActivities.end())
-                    i->second.result((ResultType) json["type"], getFields(json["fields"]));
-            }
-
-            else if (action == "setPhase") {
-                std::string phase = json["phase"];
-                act->result(resSetPhase, phase);
-            }
-
-        } catch (std::exception & e) {
-            printError("bad log message from builder: %s", e.what());
-        }
-    }
+    if (handleJSONLogMessage(currentLogLine, *act, builderActivities, false))
+        ;
 
     else {
         if (settings.verboseBuild &&
@@ -3683,8 +3702,6 @@ void SubstitutionGoal::tryToRun()
         return;
     }
 
-    printInfo(format("fetching path '%1%'...") % storePath);
-
     maintainRunningSubstitutions = std::make_unique<MaintainCount<uint64_t>>(worker.runningSubstitutions);
     worker.updateProgress();
 
@@ -3992,7 +4009,7 @@ void Worker::run(const Goals & _topGoals)
         else {
             if (awake.empty() && 0 == settings.maxBuildJobs) throw Error(
                 "unable to start any build; either increase '--max-jobs' "
-                "or enable distributed builds");
+                "or enable remote builds");
             assert(!awake.empty());
         }
     }
diff --git a/src/libstore/download.cc b/src/libstore/download.cc
index 608b8fd399b4..579a5e8c1b59 100644
--- a/src/libstore/download.cc
+++ b/src/libstore/download.cc
@@ -23,6 +23,8 @@
 #include <cmath>
 #include <random>
 
+using namespace std::string_literals;
+
 namespace nix {
 
 double getTime()
@@ -604,7 +606,7 @@ Path Downloader::downloadCached(ref<Store> store, const string & url_, bool unpa
     Path cacheDir = getCacheDir() + "/nix/tarballs";
     createDirs(cacheDir);
 
-    string urlHash = hashString(htSHA256, url).to_string(Base32, false);
+    string urlHash = hashString(htSHA256, name + std::string("\0"s) + url).to_string(Base32, false);
 
     Path dataFile = cacheDir + "/" + urlHash + ".info";
     Path fileLink = cacheDir + "/" + urlHash + "-file";
diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc
index 7da4bce87753..4fa02f92085a 100644
--- a/src/libstore/globals.cc
+++ b/src/libstore/globals.cc
@@ -53,7 +53,12 @@ Settings::Settings()
 
     /* Backwards compatibility. */
     auto s = getEnv("NIX_REMOTE_SYSTEMS");
-    if (s != "") builderFiles = tokenizeString<Strings>(s, ":");
+    if (s != "") {
+        Strings ss;
+        for (auto & p : tokenizeString<Strings>(s, ":"))
+            ss.push_back("@" + p);
+        builders = concatStringsSep(" ", ss);
+    }
 
 #if defined(__linux__) && defined(SANDBOX_SHELL)
     sandboxPaths = tokenizeString<StringSet>("/bin/sh=" SANDBOX_SHELL);
@@ -111,17 +116,17 @@ template<> void BaseSetting<SandboxMode>::convertToArg(Args & args, const std::s
     args.mkFlag()
         .longName(name)
         .description("Enable sandboxing.")
-        .handler([=](Strings ss) { value = smEnabled; })
+        .handler([=](std::vector<std::string> ss) { value = smEnabled; })
         .category(category);
     args.mkFlag()
         .longName("no-" + name)
         .description("Disable sandboxing.")
-        .handler([=](Strings ss) { value = smDisabled; })
+        .handler([=](std::vector<std::string> ss) { value = smDisabled; })
         .category(category);
     args.mkFlag()
         .longName("relaxed-" + name)
         .description("Enable sandboxing, but allow builds to disable it.")
-        .handler([=](Strings ss) { value = smRelaxed; })
+        .handler([=](std::vector<std::string> ss) { value = smRelaxed; })
         .category(category);
 }
 
diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh
index 264e82a16e20..a4aa842d70fd 100644
--- a/src/libstore/globals.hh
+++ b/src/libstore/globals.hh
@@ -2,6 +2,7 @@
 
 #include "types.hh"
 #include "config.hh"
+#include "util.hh"
 
 #include <map>
 #include <limits>
@@ -84,6 +85,9 @@ public:
     /* File name of the socket the daemon listens to.  */
     Path nixDaemonSocketFile;
 
+    Setting<std::string> storeUri{this, getEnv("NIX_REMOTE", "auto"), "store",
+        "The default Nix store to use."};
+
     Setting<bool> keepFailed{this, false, "keep-failed",
         "Whether to keep temporary directories of failed builds."};
 
@@ -128,19 +132,12 @@ public:
         "The maximum duration in seconds that a builder can run. "
         "0 means infinity.", {"build-timeout"}};
 
-    Setting<bool> useBuildHook{this, true, "remote-builds",
-        "Whether to use build hooks (for distributed builds)."};
-
     PathSetting buildHook{this, true, nixLibexecDir + "/nix/build-remote", "build-hook",
         "The path of the helper program that executes builds to remote machines."};
 
-    Setting<std::string> builders{this, "", "builders",
+    Setting<std::string> builders{this, "@" + nixConfDir + "/machines", "builders",
         "A semicolon-separated list of build machines, in the format of nix.machines."};
 
-    Setting<Strings> builderFiles{this,
-        {nixConfDir + "/machines"}, "builder-files",
-        "A list of files specifying build machines."};
-
     Setting<off_t> reservedSize{this, 8 * 1024 * 1024, "gc-reserved-space",
         "Amount of reserved disk space for the garbage collector."};
 
@@ -228,7 +225,7 @@ public:
 
     Setting<bool> restrictEval{this, false, "restrict-eval",
         "Whether to restrict file system access to paths in $NIX_PATH, "
-        "and to disallow fetching files from the network."};
+        "and network access to the URI prefixes listed in 'allowed-uris'."};
 
     Setting<size_t> buildRepeat{this, 0, "repeat",
         "The number of times to repeat a build in order to verify determinism.",
@@ -274,7 +271,7 @@ public:
         "Number of parallel HTTP connections.",
         {"binary-caches-parallel-connections"}};
 
-    Setting<bool> enableHttp2{this, true, "enable-http2",
+    Setting<bool> enableHttp2{this, true, "http2",
         "Whether to enable HTTP/2 support."};
 
     Setting<unsigned int> tarballTtl{this, 60 * 60, "tarball-ttl",
@@ -356,6 +353,8 @@ public:
     Setting<uint64_t> maxFree{this, std::numeric_limits<uint64_t>::max(), "max-free",
         "Stop deleting garbage when free disk space is above the specified amount."};
 
+    Setting<Strings> allowedUris{this, {}, "allowed-uris",
+        "Prefixes of URIs that builtin functions such as fetchurl and fetchGit are allowed to fetch."};
 };
 
 
diff --git a/src/libstore/machines.cc b/src/libstore/machines.cc
index 076c3cab3e90..edd03d147832 100644
--- a/src/libstore/machines.cc
+++ b/src/libstore/machines.cc
@@ -17,7 +17,11 @@ Machine::Machine(decltype(storeUri) storeUri,
     storeUri(
         // Backwards compatibility: if the URI is a hostname,
         // prepend ssh://.
-        storeUri.find("://") != std::string::npos || hasPrefix(storeUri, "local") || hasPrefix(storeUri, "remote") || hasPrefix(storeUri, "auto")
+        storeUri.find("://") != std::string::npos
+        || hasPrefix(storeUri, "local")
+        || hasPrefix(storeUri, "remote")
+        || hasPrefix(storeUri, "auto")
+        || hasPrefix(storeUri, "/")
         ? storeUri
         : "ssh://" + storeUri),
     systemTypes(systemTypes),
@@ -47,9 +51,22 @@ bool Machine::mandatoryMet(const std::set<string> & features) const {
 void parseMachines(const std::string & s, Machines & machines)
 {
     for (auto line : tokenizeString<std::vector<string>>(s, "\n;")) {
-        chomp(line);
+        trim(line);
         line.erase(std::find(line.begin(), line.end(), '#'), line.end());
         if (line.empty()) continue;
+
+        if (line[0] == '@') {
+            auto file = trim(std::string(line, 1));
+            try {
+                parseMachines(readFile(file), machines);
+            } catch (const SysError & e) {
+                if (e.errNo != ENOENT)
+                    throw;
+                debug("cannot find machines file '%s'", file);
+            }
+            continue;
+        }
+
         auto tokens = tokenizeString<std::vector<string>>(line);
         auto sz = tokens.size();
         if (sz < 1)
@@ -74,15 +91,6 @@ Machines getMachines()
 {
     Machines machines;
 
-    for (auto & file : settings.builderFiles.get()) {
-        try {
-            parseMachines(readFile(file), machines);
-        } catch (const SysError & e) {
-            if (e.errNo != ENOENT)
-                throw;
-        }
-    }
-
     parseMachines(settings.builders, machines);
 
     return machines;
diff --git a/src/libstore/remote-fs-accessor.cc b/src/libstore/remote-fs-accessor.cc
index 098151f8c0f6..ba9620a175bb 100644
--- a/src/libstore/remote-fs-accessor.cc
+++ b/src/libstore/remote-fs-accessor.cc
@@ -3,10 +3,29 @@
 
 namespace nix {
 
-
-RemoteFSAccessor::RemoteFSAccessor(ref<Store> store)
+RemoteFSAccessor::RemoteFSAccessor(ref<Store> store, const Path & cacheDir)
     : store(store)
+    , cacheDir(cacheDir)
 {
+    if (cacheDir != "")
+        createDirs(cacheDir);
+}
+
+Path RemoteFSAccessor::makeCacheFile(const Path & storePath)
+{
+    assert(cacheDir != "");
+    return fmt("%s/%s.nar", cacheDir, storePathToHash(storePath));
+}
+
+void RemoteFSAccessor::addToCache(const Path & storePath, const std::string & nar)
+{
+    try {
+        if (cacheDir == "") return;
+        /* FIXME: do this asynchronously. */
+        writeFile(makeCacheFile(storePath), nar);
+    } catch (...) {
+        ignoreException();
+    }
 }
 
 std::pair<ref<FSAccessor>, Path> RemoteFSAccessor::fetch(const Path & path_)
@@ -23,7 +42,16 @@ std::pair<ref<FSAccessor>, Path> RemoteFSAccessor::fetch(const Path & path_)
     if (i != nars.end()) return {i->second, restPath};
 
     StringSink sink;
-    store->narFromPath(storePath, sink);
+
+    try {
+        if (cacheDir != "")
+            *sink.s = nix::readFile(makeCacheFile(storePath));
+    } catch (SysError &) { }
+
+    if (sink.s->empty()) {
+        store->narFromPath(storePath, sink);
+        addToCache(storePath, *sink.s);
+    }
 
     auto accessor = makeNarAccessor(sink.s);
     nars.emplace(storePath, accessor);
diff --git a/src/libstore/remote-fs-accessor.hh b/src/libstore/remote-fs-accessor.hh
index 28f36c8296e1..2a3fc01eff58 100644
--- a/src/libstore/remote-fs-accessor.hh
+++ b/src/libstore/remote-fs-accessor.hh
@@ -12,10 +12,20 @@ class RemoteFSAccessor : public FSAccessor
 
     std::map<Path, ref<FSAccessor>> nars;
 
+    Path cacheDir;
+
     std::pair<ref<FSAccessor>, Path> fetch(const Path & path_);
+
+    friend class BinaryCacheStore;
+
+    Path makeCacheFile(const Path & storePath);
+
+    void addToCache(const Path & storePath, const std::string & nar);
+
 public:
 
-    RemoteFSAccessor(ref<Store> store);
+    RemoteFSAccessor(ref<Store> store,
+        const /* FIXME: use std::optional */ Path & cacheDir = "");
 
     Stat stat(const Path & path) override;
 
diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc
index b9076c0474d6..77b41b6bf8a8 100644
--- a/src/libstore/remote-store.cc
+++ b/src/libstore/remote-store.cc
@@ -166,7 +166,7 @@ void RemoteStore::setOptions(Connection & conn)
        << verbosity
        << settings.maxBuildJobs
        << settings.maxSilentTime
-       << settings.useBuildHook
+       << true
        << (settings.verboseBuild ? lvlError : lvlVomit)
        << 0 // obsolete log type
        << 0 /* obsolete print build trace */
diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc
index fa6ade75002a..c57e42fec00d 100644
--- a/src/libstore/store-api.cc
+++ b/src/libstore/store-api.cc
@@ -565,8 +565,16 @@ void Store::buildPaths(const PathSet & paths, BuildMode buildMode)
 void copyStorePath(ref<Store> srcStore, ref<Store> dstStore,
     const Path & storePath, RepairFlag repair, CheckSigsFlag checkSigs)
 {
-    Activity act(*logger, lvlInfo, actCopyPath, fmt("copying path '%s'", storePath),
-        {storePath, srcStore->getUri(), dstStore->getUri()});
+    auto srcUri = srcStore->getUri();
+    auto dstUri = dstStore->getUri();
+
+    Activity act(*logger, lvlInfo, actCopyPath,
+        srcUri == "local"
+          ? fmt("copying path '%s' to '%s'", storePath, dstUri)
+          : dstUri == "local"
+            ? fmt("copying path '%s' from '%s'", storePath, srcUri)
+            : fmt("copying path '%s' from '%s' to '%s'", storePath, srcUri, dstUri),
+        {storePath, srcUri, dstUri});
     PushActivity pact(act.id);
 
     auto info = srcStore->queryPathInfo(storePath);
@@ -619,6 +627,8 @@ void copyPaths(ref<Store> srcStore, ref<Store> dstStore, const PathSet & storePa
     for (auto & path : storePaths)
         if (!valid.count(path)) missing.insert(path);
 
+    if (missing.empty()) return;
+
     Activity act(*logger, lvlInfo, actCopyPaths, fmt("copying %d paths", missing.size()));
 
     std::atomic<size_t> nrDone{0};
@@ -833,7 +843,7 @@ StoreType getStoreType(const std::string & uri, const std::string & stateDir)
 {
     if (uri == "daemon") {
         return tDaemon;
-    } else if (uri == "local") {
+    } else if (uri == "local" || hasPrefix(uri, "/")) {
         return tLocal;
     } else if (uri == "" || uri == "auto") {
         if (access(stateDir.c_str(), R_OK | W_OK) == 0)
@@ -855,8 +865,12 @@ static RegisterStoreImplementation regStore([](
     switch (getStoreType(uri, get(params, "state", settings.nixStateDir))) {
         case tDaemon:
             return std::shared_ptr<Store>(std::make_shared<UDSRemoteStore>(params));
-        case tLocal:
-            return std::shared_ptr<Store>(std::make_shared<LocalStore>(params));
+        case tLocal: {
+            Store::Params params2 = params;
+            if (hasPrefix(uri, "/"))
+                params2["root"] = uri;
+            return std::shared_ptr<Store>(std::make_shared<LocalStore>(params2));
+        }
         default:
             return nullptr;
     }
diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh
index 5f3d8c7b9529..d1e1b5d6f452 100644
--- a/src/libstore/store-api.hh
+++ b/src/libstore/store-api.hh
@@ -716,7 +716,7 @@ void removeTempRoots();
    You can pass parameters to the store implementation by appending
    ‘?key=value&key=value&...’ to the URI.
 */
-ref<Store> openStore(const std::string & uri = getEnv("NIX_REMOTE"),
+ref<Store> openStore(const std::string & uri = settings.storeUri.get(),
     const Store::Params & extraParams = Store::Params());
 
 
@@ -727,7 +727,8 @@ enum StoreType {
 };
 
 
-StoreType getStoreType(const std::string & uri = getEnv("NIX_REMOTE"), const std::string & stateDir = settings.nixStateDir);
+StoreType getStoreType(const std::string & uri = settings.storeUri.get(),
+    const std::string & stateDir = settings.nixStateDir);
 
 /* Return the default substituter stores, defined by the
    ‘substituters’ option and various legacy options like
diff --git a/src/libutil/args.cc b/src/libutil/args.cc
index d17a1e7a9abb..7af2a1bf731a 100644
--- a/src/libutil/args.cc
+++ b/src/libutil/args.cc
@@ -100,7 +100,7 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end)
 
     auto process = [&](const std::string & name, const Flag & flag) -> bool {
         ++pos;
-        Strings args;
+        std::vector<std::string> args;
         for (size_t n = 0 ; n < flag.arity; ++n) {
             if (pos == end) {
                 if (flag.arity == ArityAny) break;
@@ -109,7 +109,7 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end)
             }
             args.push_back(*pos++);
         }
-        flag.handler(args);
+        flag.handler(std::move(args));
         return true;
     };
 
@@ -144,7 +144,9 @@ bool Args::processArgs(const Strings & args, bool finish)
     if ((exp.arity == 0 && finish) ||
         (exp.arity > 0 && args.size() == exp.arity))
     {
-        exp.handler(args);
+        std::vector<std::string> ss;
+        for (auto & s : args) ss.push_back(s);
+        exp.handler(std::move(ss));
         expectedArgs.pop_front();
         res = true;
     }
@@ -155,13 +157,17 @@ bool Args::processArgs(const Strings & args, bool finish)
     return res;
 }
 
-void Args::mkHashTypeFlag(const std::string & name, HashType * ht)
+Args::FlagMaker & Args::FlagMaker::mkHashTypeFlag(HashType * ht)
 {
-    mkFlag1(0, name, "TYPE", "hash algorithm ('md5', 'sha1', 'sha256', or 'sha512')", [=](std::string s) {
+    arity(1);
+    label("type");
+    description("hash algorithm ('md5', 'sha1', 'sha256', or 'sha512')");
+    handler([ht](std::string s) {
         *ht = parseHashType(s);
         if (*ht == htUnknown)
-            throw UsageError(format("unknown hash type '%1%'") % s);
+            throw UsageError("unknown hash type '%1%'", s);
     });
+    return *this;
 }
 
 Strings argvToStrings(int argc, char * * argv)
diff --git a/src/libutil/args.hh b/src/libutil/args.hh
index 37e31825ab37..ad5fcca39418 100644
--- a/src/libutil/args.hh
+++ b/src/libutil/args.hh
@@ -37,7 +37,7 @@ protected:
         std::string description;
         Strings labels;
         size_t arity = 0;
-        std::function<void(Strings)> handler;
+        std::function<void(std::vector<std::string>)> handler;
         std::string category;
     };
 
@@ -54,7 +54,7 @@ protected:
         std::string label;
         size_t arity; // 0 = any
         bool optional;
-        std::function<void(Strings)> handler;
+        std::function<void(std::vector<std::string>)> handler;
     };
 
     std::list<ExpectedArg> expectedArgs;
@@ -76,24 +76,35 @@ public:
         FlagMaker & longName(const std::string & s) { flag->longName = s; return *this; };
         FlagMaker & shortName(char s) { flag->shortName = s; return *this; };
         FlagMaker & description(const std::string & s) { flag->description = s; return *this; };
-        FlagMaker & labels(const Strings & ls) { flag->labels = ls; return *this; };
+        FlagMaker & label(const std::string & l) { flag->arity = 1; flag->labels = {l}; return *this; };
+        FlagMaker & labels(const Strings & ls) { flag->arity = ls.size(); flag->labels = ls; return *this; };
         FlagMaker & arity(size_t arity) { flag->arity = arity; return *this; };
-        FlagMaker & handler(std::function<void(Strings)> handler) { flag->handler = handler; return *this; };
+        FlagMaker & handler(std::function<void(std::vector<std::string>)> handler) { flag->handler = handler; return *this; };
+        FlagMaker & handler(std::function<void()> handler) { flag->handler = [handler](std::vector<std::string>) { handler(); }; return *this; };
+        FlagMaker & handler(std::function<void(std::string)> handler) {
+            flag->arity = 1;
+            flag->handler = [handler](std::vector<std::string> ss) { handler(std::move(ss[0])); };
+            return *this;
+        };
         FlagMaker & category(const std::string & s) { flag->category = s; return *this; };
 
         template<class T>
-        FlagMaker & dest(T * dest) {
+        FlagMaker & dest(T * dest)
+        {
             flag->arity = 1;
-            flag->handler = [=](Strings ss) { *dest = ss.front(); };
+            flag->handler = [=](std::vector<std::string> ss) { *dest = ss[0]; };
             return *this;
         };
 
         template<class T>
-        FlagMaker & set(T * dest, const T & val) {
+        FlagMaker & set(T * dest, const T & val)
+        {
             flag->arity = 0;
-            flag->handler = [=](Strings ss) { *dest = val; };
+            flag->handler = [=](std::vector<std::string> ss) { *dest = val; };
             return *this;
         };
+
+        FlagMaker & mkHashTypeFlag(HashType * ht);
     };
 
     FlagMaker mkFlag();
@@ -101,16 +112,6 @@ public:
     /* Helper functions for constructing flags / positional
        arguments. */
 
-    void mkFlag(char shortName, const std::string & longName,
-        const std::string & description, std::function<void()> fun)
-    {
-        mkFlag()
-            .shortName(shortName)
-            .longName(longName)
-            .description(description)
-            .handler(std::bind(fun));
-    }
-
     void mkFlag1(char shortName, const std::string & longName,
         const std::string & label, const std::string & description,
         std::function<void(std::string)> fun)
@@ -121,7 +122,7 @@ public:
             .labels({label})
             .description(description)
             .arity(1)
-            .handler([=](Strings ss) { fun(ss.front()); });
+            .handler([=](std::vector<std::string> ss) { fun(ss[0]); });
     }
 
     void mkFlag(char shortName, const std::string & name,
@@ -130,17 +131,6 @@ public:
         mkFlag(shortName, name, description, dest, true);
     }
 
-    void mkFlag(char shortName, const std::string & longName,
-        const std::string & label, const std::string & description,
-        string * dest)
-    {
-        mkFlag1(shortName, longName, label, description, [=](std::string s) {
-            *dest = s;
-        });
-    }
-
-    void mkHashTypeFlag(const std::string & name, HashType * ht);
-
     template<class T>
     void mkFlag(char shortName, const std::string & longName, const std::string & description,
         T * dest, const T & value)
@@ -149,7 +139,7 @@ public:
             .shortName(shortName)
             .longName(longName)
             .description(description)
-            .handler([=](Strings ss) { *dest = value; });
+            .handler([=](std::vector<std::string> ss) { *dest = value; });
     }
 
     template<class I>
@@ -171,10 +161,10 @@ public:
             .labels({"N"})
             .description(description)
             .arity(1)
-            .handler([=](Strings ss) {
+            .handler([=](std::vector<std::string> ss) {
                 I n;
-                if (!string2Int(ss.front(), n))
-                    throw UsageError(format("flag '--%1%' requires a integer argument") % longName);
+                if (!string2Int(ss[0], n))
+                    throw UsageError("flag '--%s' requires a integer argument", longName);
                 fun(n);
             });
     }
@@ -182,16 +172,16 @@ public:
     /* Expect a string argument. */
     void expectArg(const std::string & label, string * dest, bool optional = false)
     {
-        expectedArgs.push_back(ExpectedArg{label, 1, optional, [=](Strings ss) {
-            *dest = ss.front();
+        expectedArgs.push_back(ExpectedArg{label, 1, optional, [=](std::vector<std::string> ss) {
+            *dest = ss[0];
         }});
     }
 
     /* Expect 0 or more arguments. */
-    void expectArgs(const std::string & label, Strings * dest)
+    void expectArgs(const std::string & label, std::vector<std::string> * dest)
     {
-        expectedArgs.push_back(ExpectedArg{label, 0, false, [=](Strings ss) {
-            *dest = ss;
+        expectedArgs.push_back(ExpectedArg{label, 0, false, [=](std::vector<std::string> ss) {
+            *dest = std::move(ss);
         }});
     }
 
diff --git a/src/libutil/config.cc b/src/libutil/config.cc
index 27157a83178a..14c4cca031bb 100644
--- a/src/libutil/config.cc
+++ b/src/libutil/config.cc
@@ -152,7 +152,7 @@ void BaseSetting<T>::convertToArg(Args & args, const std::string & category)
         .longName(name)
         .description(description)
         .arity(1)
-        .handler([=](Strings ss) { set(*ss.begin()); })
+        .handler([=](std::vector<std::string> ss) { set(ss[0]); })
         .category(category);
 }
 
@@ -201,12 +201,12 @@ template<> void BaseSetting<bool>::convertToArg(Args & args, const std::string &
     args.mkFlag()
         .longName(name)
         .description(description)
-        .handler([=](Strings ss) { value = true; })
+        .handler([=](std::vector<std::string> ss) { value = true; })
         .category(category);
     args.mkFlag()
         .longName("no-" + name)
         .description(description)
-        .handler([=](Strings ss) { value = false; })
+        .handler([=](std::vector<std::string> ss) { value = false; })
         .category(category);
 }
 
diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc
index e38a460537ea..011155871122 100644
--- a/src/libutil/logging.cc
+++ b/src/libutil/logging.cc
@@ -2,6 +2,7 @@
 #include "util.hh"
 
 #include <atomic>
+#include <nlohmann/json.hpp>
 
 namespace nix {
 
@@ -90,4 +91,133 @@ Activity::Activity(Logger & logger, Verbosity lvl, ActivityType type,
     logger.startActivity(id, lvl, type, s, fields, parent);
 }
 
+struct JSONLogger : Logger
+{
+    Logger & prevLogger;
+
+    JSONLogger(Logger & prevLogger) : prevLogger(prevLogger) { }
+
+    void addFields(nlohmann::json & json, const Fields & fields)
+    {
+        if (fields.empty()) return;
+        auto & arr = json["fields"] = nlohmann::json::array();
+        for (auto & f : fields)
+            if (f.type == Logger::Field::tInt)
+                arr.push_back(f.i);
+            else if (f.type == Logger::Field::tString)
+                arr.push_back(f.s);
+            else
+                abort();
+    }
+
+    void write(const nlohmann::json & json)
+    {
+        prevLogger.log(lvlError, "@nix " + json.dump());
+    }
+
+    void log(Verbosity lvl, const FormatOrString & fs) override
+    {
+        nlohmann::json json;
+        json["action"] = "msg";
+        json["level"] = lvl;
+        json["msg"] = fs.s;
+        write(json);
+    }
+
+    void startActivity(ActivityId act, Verbosity lvl, ActivityType type,
+        const std::string & s, const Fields & fields, ActivityId parent) override
+    {
+        nlohmann::json json;
+        json["action"] = "start";
+        json["id"] = act;
+        json["level"] = lvl;
+        json["type"] = type;
+        json["text"] = s;
+        addFields(json, fields);
+        // FIXME: handle parent
+        write(json);
+    }
+
+    void stopActivity(ActivityId act) override
+    {
+        nlohmann::json json;
+        json["action"] = "stop";
+        json["id"] = act;
+        write(json);
+    }
+
+    void result(ActivityId act, ResultType type, const Fields & fields) override
+    {
+        nlohmann::json json;
+        json["action"] = "result";
+        json["id"] = act;
+        json["type"] = type;
+        addFields(json, fields);
+        write(json);
+    }
+};
+
+Logger * makeJSONLogger(Logger & prevLogger)
+{
+    return new JSONLogger(prevLogger);
+}
+
+static Logger::Fields getFields(nlohmann::json & json)
+{
+    Logger::Fields fields;
+    for (auto & f : json) {
+        if (f.type() == nlohmann::json::value_t::number_unsigned)
+            fields.emplace_back(Logger::Field(f.get<uint64_t>()));
+        else if (f.type() == nlohmann::json::value_t::string)
+            fields.emplace_back(Logger::Field(f.get<std::string>()));
+        else throw Error("unsupported JSON type %d", (int) f.type());
+    }
+    return fields;
+}
+
+bool handleJSONLogMessage(const std::string & msg,
+    const Activity & act, std::map<ActivityId, Activity> & activities, bool trusted)
+{
+    if (!hasPrefix(msg, "@nix ")) return false;
+
+    try {
+        auto json = nlohmann::json::parse(std::string(msg, 5));
+
+        std::string action = json["action"];
+
+        if (action == "start") {
+            auto type = (ActivityType) json["type"];
+            if (trusted || type == actDownload)
+                activities.emplace(std::piecewise_construct,
+                    std::forward_as_tuple(json["id"]),
+                    std::forward_as_tuple(*logger, (Verbosity) json["level"], type,
+                        json["text"], getFields(json["fields"]), act.id));
+        }
+
+        else if (action == "stop")
+            activities.erase((ActivityId) json["id"]);
+
+        else if (action == "result") {
+            auto i = activities.find((ActivityId) json["id"]);
+            if (i != activities.end())
+                i->second.result((ResultType) json["type"], getFields(json["fields"]));
+        }
+
+        else if (action == "setPhase") {
+            std::string phase = json["phase"];
+            act.result(resSetPhase, phase);
+        }
+
+        else if (action == "msg") {
+            std::string msg = json["msg"];
+            logger->log((Verbosity) json["level"], msg);
+        }
+
+    } catch (std::exception & e) {
+        printError("bad log message from builder: %s", e.what());
+    }
+
+    return true;
+}
+
 }
diff --git a/src/libutil/logging.hh b/src/libutil/logging.hh
index 21898c03a276..677aa4daec4d 100644
--- a/src/libutil/logging.hh
+++ b/src/libutil/logging.hh
@@ -130,6 +130,12 @@ extern Logger * logger;
 
 Logger * makeDefaultLogger();
 
+Logger * makeJSONLogger(Logger & prevLogger);
+
+bool handleJSONLogMessage(const std::string & msg,
+    const Activity & act, std::map<ActivityId, Activity> & activities,
+    bool trusted);
+
 extern Verbosity verbosity; /* suppress msgs > this */
 
 /* Print a message if the current log level is at least the specified
diff --git a/src/libutil/serialise.hh b/src/libutil/serialise.hh
index 70b193941638..2ea5b6354ee9 100644
--- a/src/libutil/serialise.hh
+++ b/src/libutil/serialise.hh
@@ -92,7 +92,17 @@ struct FdSink : BufferedSink
     FdSink() : fd(-1) { }
     FdSink(int fd) : fd(fd) { }
     FdSink(FdSink&&) = default;
-    FdSink& operator=(FdSink&&) = default;
+
+    FdSink& operator=(FdSink && s)
+    {
+        flush();
+        fd = s.fd;
+        s.fd = -1;
+        warn = s.warn;
+        written = s.written;
+        return *this;
+    }
+
     ~FdSink();
 
     void write(const unsigned char * data, size_t len) override;
@@ -112,6 +122,16 @@ struct FdSource : BufferedSource
 
     FdSource() : fd(-1) { }
     FdSource(int fd) : fd(fd) { }
+    FdSource(FdSource&&) = default;
+
+    FdSource& operator=(FdSource && s)
+    {
+        fd = s.fd;
+        s.fd = -1;
+        read = s.read;
+        return *this;
+    }
+
     size_t readUnbuffered(unsigned char * data, size_t len) override;
     bool good() override;
 private:
diff --git a/src/libutil/util.cc b/src/libutil/util.cc
index 3c98a61f9e50..9346d5dc4cf8 100644
--- a/src/libutil/util.cc
+++ b/src/libutil/util.cc
@@ -1142,6 +1142,16 @@ std::string toLower(const std::string & s)
 }
 
 
+std::string shellEscape(const std::string & s)
+{
+    std::string r = "'";
+    for (auto & i : s)
+        if (i == '\'') r += "'\\''"; else r += i;
+    r += '\'';
+    return r;
+}
+
+
 void ignoreException()
 {
     try {
diff --git a/src/libutil/util.hh b/src/libutil/util.hh
index 6a66576e96ce..fccf5d854800 100644
--- a/src/libutil/util.hh
+++ b/src/libutil/util.hh
@@ -352,10 +352,8 @@ bool hasSuffix(const string & s, const string & suffix);
 std::string toLower(const std::string & s);
 
 
-/* Escape a string that contains octal-encoded escape codes such as
-   used in /etc/fstab and /proc/mounts (e.g. "foo\040bar" decodes to
-   "foo bar"). */
-string decodeOctalEscaped(const string & s);
+/* Escape a string as a shell word. */
+std::string shellEscape(const std::string & s);
 
 
 /* Exception handling in destructors: print an error message, then
diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc
index a3d3c8007be6..21b0a18dd887 100755
--- a/src/nix-build/nix-build.cc
+++ b/src/nix-build/nix-build.cc
@@ -14,7 +14,7 @@
 #include "eval.hh"
 #include "eval-inline.hh"
 #include "get-drvs.hh"
-#include "common-opts.hh"
+#include "common-eval-args.hh"
 #include "attr-path.hh"
 
 using namespace nix;
@@ -80,8 +80,6 @@ void mainWrapped(int argc, char * * argv)
     auto interactive = isatty(STDIN_FILENO) && isatty(STDERR_FILENO);
     Strings attrPaths;
     Strings left;
-    Strings searchPath;
-    std::map<string, string> autoArgs_;
     RepairFlag repair = NoRepair;
     Path gcRoot;
     BuildMode buildMode = bmNormal;
@@ -129,7 +127,12 @@ void mainWrapped(int argc, char * * argv)
         } catch (SysError &) { }
     }
 
-    parseCmdLine(myName, args, [&](Strings::iterator & arg, const Strings::iterator & end) {
+    struct MyArgs : LegacyArgs, MixEvalArgs
+    {
+        using LegacyArgs::LegacyArgs;
+    };
+
+    MyArgs myArgs(myName, [&](Strings::iterator & arg, const Strings::iterator & end) {
         if (*arg == "--help") {
             deletePath(tmpDir);
             showManPage(myName);
@@ -153,12 +156,6 @@ void mainWrapped(int argc, char * * argv)
         else if (*arg == "--out-link" || *arg == "-o")
             outLink = getArg(*arg, arg, end);
 
-        else if (parseAutoArgs(arg, end, autoArgs_))
-            ;
-
-        else if (parseSearchPathArg(arg, end, searchPath))
-            ;
-
         else if (*arg == "--add-root")
             gcRoot = getArg(*arg, arg, end);
 
@@ -170,6 +167,9 @@ void mainWrapped(int argc, char * * argv)
             buildMode = bmRepair;
         }
 
+        else if (*arg == "--hash")
+            buildMode = bmHash;
+
         else if (*arg == "--run-env") // obsolete
             runEnv = true;
 
@@ -199,10 +199,6 @@ void mainWrapped(int argc, char * * argv)
             interactive = false;
             auto execArgs = "";
 
-            auto shellEscape = [](const string & s) {
-                return "'" + std::regex_replace(s, std::regex("'"), "'\\''") + "'";
-            };
-
             // Überhack to support Perl. Perl examines the shebang and
             // executes it unless it contains the string "perl" or "indir",
             // or (undocumented) argv[0] does not contain "perl". Exploit
@@ -237,15 +233,17 @@ void mainWrapped(int argc, char * * argv)
         return true;
     });
 
+    myArgs.parseCmdline(args);
+
     if (packages && fromArgs)
         throw UsageError("'-p' and '-E' are mutually exclusive");
 
     auto store = openStore();
 
-    EvalState state(searchPath, store);
+    EvalState state(myArgs.searchPath, store);
     state.repair = repair;
 
-    Bindings & autoArgs(*evalAutoArgs(state, autoArgs_));
+    Bindings & autoArgs = *myArgs.getAutoArgs(state);
 
     if (packages) {
         std::ostringstream joined;
@@ -278,7 +276,7 @@ void mainWrapped(int argc, char * * argv)
                 /* If we're in a #! script, interpret filenames
                    relative to the script. */
                 exprs.push_back(state.parseExprFromFile(resolveExprPath(lookupFileArg(state,
-                    inShebang && !packages ? absPath(i, dirOf(script)) : i))));
+                    inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i))));
         }
 
     /* Evaluate them into derivations. */
diff --git a/src/nix-daemon/nix-daemon.cc b/src/nix-daemon/nix-daemon.cc
index dbf301a91533..5629cc64b96e 100644
--- a/src/nix-daemon/nix-daemon.cc
+++ b/src/nix-daemon/nix-daemon.cc
@@ -509,7 +509,7 @@ static void performOp(TunnelLogger * logger, ref<LocalStore> store,
         verbosity = (Verbosity) readInt(from);
         settings.maxBuildJobs.assign(readInt(from));
         settings.maxSilentTime = readInt(from);
-        settings.useBuildHook = readInt(from) != 0;
+        readInt(from); // obsolete useBuildHook
         settings.verboseBuild = lvlError == (Verbosity) readInt(from);
         readInt(from); // obsolete logType
         readInt(from); // obsolete printBuildTrace
diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc
index 94fbc09f6642..016caf6d2346 100644
--- a/src/nix-env/nix-env.cc
+++ b/src/nix-env/nix-env.cc
@@ -1,5 +1,5 @@
 #include "attr-path.hh"
-#include "common-opts.hh"
+#include "common-eval-args.hh"
 #include "derivations.hh"
 #include "eval.hh"
 #include "get-drvs.hh"
@@ -1309,8 +1309,7 @@ int main(int argc, char * * argv)
         initNix();
         initGC();
 
-        Strings opFlags, opArgs, searchPath;
-        std::map<string, string> autoArgs_;
+        Strings opFlags, opArgs;
         Operation op = 0;
         RepairFlag repair = NoRepair;
         string file;
@@ -1326,7 +1325,12 @@ int main(int argc, char * * argv)
         globals.removeAll = false;
         globals.prebuiltOnly = false;
 
-        parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
+        struct MyArgs : LegacyArgs, MixEvalArgs
+        {
+            using LegacyArgs::LegacyArgs;
+        };
+
+        MyArgs myArgs(baseNameOf(argv[0]), [&](Strings::iterator & arg, const Strings::iterator & end) {
             Operation oldOp = op;
 
             if (*arg == "--help")
@@ -1335,10 +1339,6 @@ int main(int argc, char * * argv)
                 op = opVersion;
             else if (*arg == "--install" || *arg == "-i")
                 op = opInstall;
-            else if (parseAutoArgs(arg, end, autoArgs_))
-                ;
-            else if (parseSearchPathArg(arg, end, searchPath))
-                ;
             else if (*arg == "--force-name") // undocumented flag for nix-install-package
                 globals.forceName = getArg(*arg, arg, end);
             else if (*arg == "--uninstall" || *arg == "-e")
@@ -1391,17 +1391,19 @@ int main(int argc, char * * argv)
             return true;
         });
 
+        myArgs.parseCmdline(argvToStrings(argc, argv));
+
         if (!op) throw UsageError("no operation specified");
 
         auto store = openStore();
 
-        globals.state = std::shared_ptr<EvalState>(new EvalState(searchPath, store));
+        globals.state = std::shared_ptr<EvalState>(new EvalState(myArgs.searchPath, store));
         globals.state->repair = repair;
 
         if (file != "")
             globals.instSource.nixExprPath = lookupFileArg(*globals.state, file);
 
-        globals.instSource.autoArgs = evalAutoArgs(*globals.state, autoArgs_);
+        globals.instSource.autoArgs = myArgs.getAutoArgs(*globals.state);
 
         if (globals.profile == "")
             globals.profile = getEnv("NIX_PROFILE", "");
diff --git a/src/nix-instantiate/nix-instantiate.cc b/src/nix-instantiate/nix-instantiate.cc
index 2498df0f0d72..55ac007e8682 100644
--- a/src/nix-instantiate/nix-instantiate.cc
+++ b/src/nix-instantiate/nix-instantiate.cc
@@ -8,7 +8,7 @@
 #include "value-to-json.hh"
 #include "util.hh"
 #include "store-api.hh"
-#include "common-opts.hh"
+#include "common-eval-args.hh"
 
 #include <map>
 #include <iostream>
@@ -89,7 +89,7 @@ int main(int argc, char * * argv)
         initNix();
         initGC();
 
-        Strings files, searchPath;
+        Strings files;
         bool readStdin = false;
         bool fromArgs = false;
         bool findFile = false;
@@ -100,10 +100,14 @@ int main(int argc, char * * argv)
         bool strict = false;
         Strings attrPaths;
         bool wantsReadWrite = false;
-        std::map<string, string> autoArgs_;
         RepairFlag repair = NoRepair;
 
-        parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
+        struct MyArgs : LegacyArgs, MixEvalArgs
+        {
+            using LegacyArgs::LegacyArgs;
+        };
+
+        MyArgs myArgs(baseNameOf(argv[0]), [&](Strings::iterator & arg, const Strings::iterator & end) {
             if (*arg == "--help")
                 showManPage("nix-instantiate");
             else if (*arg == "--version")
@@ -122,10 +126,6 @@ int main(int argc, char * * argv)
                 findFile = true;
             else if (*arg == "--attr" || *arg == "-A")
                 attrPaths.push_back(getArg(*arg, arg, end));
-            else if (parseAutoArgs(arg, end, autoArgs_))
-                ;
-            else if (parseSearchPathArg(arg, end, searchPath))
-                ;
             else if (*arg == "--add-root")
                 gcRoot = getArg(*arg, arg, end);
             else if (*arg == "--indirect")
@@ -149,15 +149,17 @@ int main(int argc, char * * argv)
             return true;
         });
 
+        myArgs.parseCmdline(argvToStrings(argc, argv));
+
         if (evalOnly && !wantsReadWrite)
             settings.readOnlyMode = true;
 
         auto store = openStore();
 
-        EvalState state(searchPath, store);
+        EvalState state(myArgs.searchPath, store);
         state.repair = repair;
 
-        Bindings & autoArgs(*evalAutoArgs(state, autoArgs_));
+        Bindings & autoArgs = *myArgs.getAutoArgs(state);
 
         if (attrPaths.empty()) attrPaths = {""};
 
diff --git a/src/nix-prefetch-url/nix-prefetch-url.cc b/src/nix-prefetch-url/nix-prefetch-url.cc
index 7e62a033b458..fef3eaa45538 100644
--- a/src/nix-prefetch-url/nix-prefetch-url.cc
+++ b/src/nix-prefetch-url/nix-prefetch-url.cc
@@ -4,7 +4,7 @@
 #include "store-api.hh"
 #include "eval.hh"
 #include "eval-inline.hh"
-#include "common-opts.hh"
+#include "common-eval-args.hh"
 #include "attr-path.hh"
 
 #include <iostream>
@@ -48,15 +48,18 @@ int main(int argc, char * * argv)
 
         HashType ht = htSHA256;
         std::vector<string> args;
-        Strings searchPath;
         bool printPath = getEnv("PRINT_PATH") != "";
         bool fromExpr = false;
         string attrPath;
-        std::map<string, string> autoArgs_;
         bool unpack = false;
         string name;
 
-        parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
+        struct MyArgs : LegacyArgs, MixEvalArgs
+        {
+            using LegacyArgs::LegacyArgs;
+        };
+
+        MyArgs myArgs(baseNameOf(argv[0]), [&](Strings::iterator & arg, const Strings::iterator & end) {
             if (*arg == "--help")
                 showManPage("nix-prefetch-url");
             else if (*arg == "--version")
@@ -77,10 +80,6 @@ int main(int argc, char * * argv)
                 unpack = true;
             else if (*arg == "--name")
                 name = getArg(*arg, arg, end);
-            else if (parseAutoArgs(arg, end, autoArgs_))
-                ;
-            else if (parseSearchPathArg(arg, end, searchPath))
-                ;
             else if (*arg != "" && arg->at(0) == '-')
                 return false;
             else
@@ -88,13 +87,15 @@ int main(int argc, char * * argv)
             return true;
         });
 
+        myArgs.parseCmdline(argvToStrings(argc, argv));
+
         if (args.size() > 2)
             throw UsageError("too many arguments");
 
         auto store = openStore();
-        EvalState state(searchPath, store);
+        EvalState state(myArgs.searchPath, store);
 
-        Bindings & autoArgs(*evalAutoArgs(state, autoArgs_));
+        Bindings & autoArgs = *myArgs.getAutoArgs(state);
 
         /* If -A is given, get the URI from the specified Nix
            expression. */
diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc
index 85bcbc22e9db..f6f276dd1798 100644
--- a/src/nix-store/nix-store.cc
+++ b/src/nix-store/nix-store.cc
@@ -440,15 +440,6 @@ static void opQuery(Strings opFlags, Strings opArgs)
 }
 
 
-static string shellEscape(const string & s)
-{
-    string r;
-    for (auto & i : s)
-        if (i == '\'') r += "'\\''"; else r += i;
-    return r;
-}
-
-
 static void opPrintEnv(Strings opFlags, Strings opArgs)
 {
     if (!opFlags.empty()) throw UsageError("unknown flag");
@@ -460,7 +451,7 @@ static void opPrintEnv(Strings opFlags, Strings opArgs)
     /* Print each environment variable in the derivation in a format
        that can be sourced by the shell. */
     for (auto & i : drv.env)
-        cout << format("export %1%; %1%='%2%'\n") % i.first % shellEscape(i.second);
+        cout << format("export %1%; %1%=%2%\n") % i.first % shellEscape(i.second);
 
     /* Also output the arguments.  This doesn't preserve whitespace in
        arguments. */
diff --git a/src/nix/command.cc b/src/nix/command.cc
index 0f6bb294b38c..1e6f0d2bb75d 100644
--- a/src/nix/command.cc
+++ b/src/nix/command.cc
@@ -24,11 +24,11 @@ void Command::printHelp(const string & programName, std::ostream & out)
 MultiCommand::MultiCommand(const Commands & _commands)
     : commands(_commands)
 {
-    expectedArgs.push_back(ExpectedArg{"command", 1, true, [=](Strings ss) {
+    expectedArgs.push_back(ExpectedArg{"command", 1, true, [=](std::vector<std::string> ss) {
         assert(!command);
-        auto i = commands.find(ss.front());
+        auto i = commands.find(ss[0]);
         if (i == commands.end())
-            throw UsageError(format("'%1%' is not a recognised command") % ss.front());
+            throw UsageError("'%s' is not a recognised command", ss[0]);
         command = i->second;
     }});
 }
@@ -78,9 +78,6 @@ bool MultiCommand::processArgs(const Strings & args, bool finish)
 
 StoreCommand::StoreCommand()
 {
-    storeUri = getEnv("NIX_REMOTE");
-
-    mkFlag(0, "store", "store-uri", "URI of the Nix store to use", &storeUri);
 }
 
 ref<Store> StoreCommand::getStore()
@@ -92,7 +89,7 @@ ref<Store> StoreCommand::getStore()
 
 ref<Store> StoreCommand::createStore()
 {
-    return openStore(storeUri);
+    return openStore();
 }
 
 void StoreCommand::run()
diff --git a/src/nix/command.hh b/src/nix/command.hh
index bf897f620db6..daa3b3fa7030 100644
--- a/src/nix/command.hh
+++ b/src/nix/command.hh
@@ -1,10 +1,12 @@
 #pragma once
 
 #include "args.hh"
+#include "common-eval-args.hh"
 
 namespace nix {
 
 struct Value;
+struct Bindings;
 class EvalState;
 
 /* A command is an argument parser that can be executed by calling its
@@ -33,7 +35,6 @@ class Store;
 /* A command that require a Nix store. */
 struct StoreCommand : virtual Command
 {
-    std::string storeUri;
     StoreCommand();
     void run() override;
     ref<Store> getStore();
@@ -69,14 +70,11 @@ struct Installable
     }
 };
 
-struct SourceExprCommand : virtual Args, StoreCommand
+struct SourceExprCommand : virtual Args, StoreCommand, MixEvalArgs
 {
     Path file;
 
-    SourceExprCommand()
-    {
-        mkFlag('f', "file", "file", "evaluate FILE rather than the default", &file);
-    }
+    SourceExprCommand();
 
     /* Return a value representing the Nix expression from which we
        are installing. This is either the file specified by ‘--file’,
@@ -112,7 +110,7 @@ struct InstallablesCommand : virtual Args, SourceExprCommand
 
 private:
 
-    Strings _installables;
+    std::vector<std::string> _installables;
 };
 
 struct InstallableCommand : virtual Args, SourceExprCommand
diff --git a/src/nix/copy.cc b/src/nix/copy.cc
index 071ac3890aa9..2ddea9e70a6a 100644
--- a/src/nix/copy.cc
+++ b/src/nix/copy.cc
@@ -19,8 +19,16 @@ struct CmdCopy : StorePathsCommand
     CmdCopy()
         : StorePathsCommand(true)
     {
-        mkFlag(0, "from", "store-uri", "URI of the source Nix store", &srcUri);
-        mkFlag(0, "to", "store-uri", "URI of the destination Nix store", &dstUri);
+        mkFlag()
+            .longName("from")
+            .labels({"store-uri"})
+            .description("URI of the source Nix store")
+            .dest(&srcUri);
+        mkFlag()
+            .longName("to")
+            .labels({"store-uri"})
+            .description("URI of the destination Nix store")
+            .dest(&dstUri);
 
         mkFlag()
             .longName("no-check-sigs")
diff --git a/src/nix/hash.cc b/src/nix/hash.cc
index 923dabb103d3..64062fb97955 100644
--- a/src/nix/hash.cc
+++ b/src/nix/hash.cc
@@ -12,14 +12,16 @@ struct CmdHash : Command
     Base base = Base16;
     bool truncate = false;
     HashType ht = htSHA512;
-    Strings paths;
+    std::vector<std::string> paths;
 
     CmdHash(Mode mode) : mode(mode)
     {
         mkFlag(0, "base64", "print hash in base-64", &base, Base64);
         mkFlag(0, "base32", "print hash in base-32 (Nix-specific)", &base, Base32);
         mkFlag(0, "base16", "print hash in base-16", &base, Base16);
-        mkHashTypeFlag("type", &ht);
+        mkFlag()
+            .longName("type")
+            .mkHashTypeFlag(&ht);
         expectArgs("paths", &paths);
     }
 
@@ -53,11 +55,13 @@ struct CmdToBase : Command
 {
     Base base;
     HashType ht = htSHA512;
-    Strings args;
+    std::vector<std::string> args;
 
     CmdToBase(Base base) : base(base)
     {
-        mkHashTypeFlag("type", &ht);
+        mkFlag()
+            .longName("type")
+            .mkHashTypeFlag(&ht);
         expectArgs("strings", &args);
     }
 
@@ -95,7 +99,7 @@ static int compatNixHash(int argc, char * * argv)
     bool base32 = false;
     bool truncate = false;
     enum { opHash, opTo32, opTo16 } op = opHash;
-    Strings ss;
+    std::vector<std::string> ss;
 
     parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
         if (*arg == "--help")
diff --git a/src/nix/installables.cc b/src/nix/installables.cc
index c83d6316d3f3..ae93c4ef649e 100644
--- a/src/nix/installables.cc
+++ b/src/nix/installables.cc
@@ -1,6 +1,6 @@
 #include "command.hh"
 #include "attr-path.hh"
-#include "common-opts.hh"
+#include "common-eval-args.hh"
 #include "derivations.hh"
 #include "eval-inline.hh"
 #include "eval.hh"
@@ -12,6 +12,16 @@
 
 namespace nix {
 
+SourceExprCommand::SourceExprCommand()
+{
+    mkFlag()
+        .shortName('f')
+        .longName("file")
+        .label("file")
+        .description("evaluate FILE rather than the default")
+        .dest(&file);
+}
+
 Value * SourceExprCommand::getSourceExpr(EvalState & state)
 {
     if (vSourceExpr) return vSourceExpr;
@@ -66,7 +76,7 @@ Value * SourceExprCommand::getSourceExpr(EvalState & state)
 ref<EvalState> SourceExprCommand::getEvalState()
 {
     if (!evalState)
-        evalState = std::make_shared<EvalState>(Strings{}, getStore());
+        evalState = std::make_shared<EvalState>(searchPath, getStore());
     return ref<EvalState>(evalState);
 }
 
@@ -120,9 +130,7 @@ struct InstallableValue : Installable
 
         auto v = toValue(*state);
 
-        // FIXME
-        std::map<string, string> autoArgs_;
-        Bindings & autoArgs(*evalAutoArgs(*state, autoArgs_));
+        Bindings & autoArgs = *cmd.getAutoArgs(*state);
 
         DrvInfos drvs;
         getDerivations(*state, *v, "", autoArgs, drvs, false);
@@ -187,9 +195,7 @@ struct InstallableAttrPath : InstallableValue
     {
         auto source = cmd.getSourceExpr(state);
 
-        // FIXME
-        std::map<string, string> autoArgs_;
-        Bindings & autoArgs(*evalAutoArgs(state, autoArgs_));
+        Bindings & autoArgs = *cmd.getAutoArgs(state);
 
         Value * v = findAlongAttrPath(state, attrPath, autoArgs, *source);
         state.forceValue(*v);
@@ -203,14 +209,14 @@ std::string attrRegex = R"([A-Za-z_][A-Za-z0-9-_+]*)";
 static std::regex attrPathRegex(fmt(R"(%1%(\.%1%)*)", attrRegex));
 
 static std::vector<std::shared_ptr<Installable>> parseInstallables(
-    SourceExprCommand & cmd, ref<Store> store, Strings ss, bool useDefaultInstallables)
+    SourceExprCommand & cmd, ref<Store> store, std::vector<std::string> ss, bool useDefaultInstallables)
 {
     std::vector<std::shared_ptr<Installable>> result;
 
     if (ss.empty() && useDefaultInstallables) {
         if (cmd.file == "")
             cmd.file = ".";
-        ss = Strings{""};
+        ss = {""};
     }
 
     for (auto & s : ss) {
diff --git a/src/nix/main.cc b/src/nix/main.cc
index ec9b58b20fe8..060402cd08d5 100644
--- a/src/nix/main.cc
+++ b/src/nix/main.cc
@@ -20,19 +20,29 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs
 {
     NixArgs() : MultiCommand(*RegisterCommand::commands), MixCommonArgs("nix")
     {
-        mkFlag('h', "help", "show usage information", [&]() { showHelpAndExit(); });
-
-        mkFlag(0, "help-config", "show configuration options", [=]() {
-            std::cout << "The following configuration options are available:\n\n";
-            Table2 tbl;
-            for (const auto & s : settings._getSettings())
-                if (!s.second.isAlias)
-                    tbl.emplace_back(s.first, s.second.setting->description);
-            printTable(std::cout, tbl);
-            throw Exit();
-        });
-
-        mkFlag(0, "version", "show version information", std::bind(printVersion, programName));
+        mkFlag()
+            .longName("help")
+            .shortName('h')
+            .description("show usage information")
+            .handler([&]() { showHelpAndExit(); });
+
+        mkFlag()
+            .longName("help-config")
+            .description("show configuration options")
+            .handler([&]() {
+                std::cout << "The following configuration options are available:\n\n";
+                Table2 tbl;
+                for (const auto & s : settings._getSettings())
+                    if (!s.second.isAlias)
+                        tbl.emplace_back(s.first, s.second.setting->description);
+                printTable(std::cout, tbl);
+                throw Exit();
+            });
+
+        mkFlag()
+            .longName("version")
+            .description("show version information")
+            .handler([&]() { printVersion(programName); });
 
         std::string cat = "config";
         settings.convertToArgs(*this, cat);
diff --git a/src/nix/progress-bar.cc b/src/nix/progress-bar.cc
index 76138b2cce28..fb9955190b40 100644
--- a/src/nix/progress-bar.cc
+++ b/src/nix/progress-bar.cc
@@ -156,6 +156,13 @@ public:
             if (hasSuffix(name, ".drv"))
                 name.resize(name.size() - 4);
             i->s = fmt("building " ANSI_BOLD "%s" ANSI_NORMAL, name);
+            auto machineName = getS(fields, 1);
+            if (machineName != "")
+                i->s += fmt(" on " ANSI_BOLD "%s" ANSI_NORMAL, machineName);
+            auto curRound = getI(fields, 2);
+            auto nrRounds = getI(fields, 3);
+            if (nrRounds != 1)
+                i->s += fmt(" (round %d/%d)", curRound, nrRounds);
         }
 
         if (type == actSubstitute) {
diff --git a/src/nix/repl.cc b/src/nix/repl.cc
index 781b4463e54a..28a8ebc8c499 100644
--- a/src/nix/repl.cc
+++ b/src/nix/repl.cc
@@ -7,7 +7,7 @@
 #include "eval.hh"
 #include "eval-inline.hh"
 #include "store-api.hh"
-#include "common-opts.hh"
+#include "common-eval-args.hh"
 #include "get-drvs.hh"
 #include "derivations.hh"
 #include "affinity.hh"
@@ -44,7 +44,7 @@ struct NixRepl
 
     NixRepl(const Strings & searchPath, nix::ref<Store> store);
     ~NixRepl();
-    void mainLoop(const Strings & files);
+    void mainLoop(const std::vector<std::string> & files);
     StringSet completePrefix(string prefix);
     bool getLine(string & input, const std::string &prompt);
     Path getDerivationPath(Value & v);
@@ -131,7 +131,7 @@ static void completionCallback(const char * s, linenoiseCompletions *lc)
 }
 
 
-void NixRepl::mainLoop(const Strings & files)
+void NixRepl::mainLoop(const std::vector<std::string> & files)
 {
     string error = ANSI_RED "error:" ANSI_NORMAL " ";
     std::cout << "Welcome to Nix version " << nixVersion << ". Type :? for help." << std::endl << std::endl;
@@ -664,9 +664,9 @@ std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int m
     return str;
 }
 
-struct CmdRepl : StoreCommand
+struct CmdRepl : StoreCommand, MixEvalArgs
 {
-    Strings files;
+    std::vector<std::string> files;
 
     CmdRepl()
     {
@@ -682,8 +682,7 @@ struct CmdRepl : StoreCommand
 
     void run(ref<Store> store) override
     {
-        // FIXME: pass searchPath
-        NixRepl repl({}, openStore());
+        NixRepl repl(searchPath, openStore());
         repl.mainLoop(files);
     }
 };
diff --git a/src/nix/run.cc b/src/nix/run.cc
index 2f93ca351502..6657a86314bf 100644
--- a/src/nix/run.cc
+++ b/src/nix/run.cc
@@ -20,7 +20,7 @@ extern char * * environ;
 
 struct CmdRun : InstallablesCommand
 {
-    Strings command = { "bash" };
+    std::vector<std::string> command = { "bash" };
     StringSet keep, unset;
     bool ignoreEnvironment = false;
 
@@ -32,7 +32,7 @@ struct CmdRun : InstallablesCommand
             .description("command and arguments to be executed; defaults to 'bash'")
             .arity(ArityAny)
             .labels({"command", "args"})
-            .handler([&](Strings ss) {
+            .handler([&](std::vector<std::string> ss) {
                 if (ss.empty()) throw UsageError("--command requires at least one argument");
                 command = ss;
             });
@@ -49,7 +49,7 @@ struct CmdRun : InstallablesCommand
             .description("keep specified environment variable")
             .arity(1)
             .labels({"name"})
-            .handler([&](Strings ss) { keep.insert(ss.front()); });
+            .handler([&](std::vector<std::string> ss) { keep.insert(ss.front()); });
 
         mkFlag()
             .longName("unset")
@@ -57,7 +57,7 @@ struct CmdRun : InstallablesCommand
             .description("unset specified environment variable")
             .arity(1)
             .labels({"name"})
-            .handler([&](Strings ss) { unset.insert(ss.front()); });
+            .handler([&](std::vector<std::string> ss) { unset.insert(ss.front()); });
     }
 
     std::string name() override
@@ -126,7 +126,8 @@ struct CmdRun : InstallablesCommand
         setenv("PATH", concatStringsSep(":", unixPath).c_str(), 1);
 
         std::string cmd = *command.begin();
-        Strings args = command;
+        Strings args;
+        for (auto & arg : command) args.push_back(arg);
 
         stopProgressBar();
 
diff --git a/src/nix/search.cc b/src/nix/search.cc
index 9476b79fbc1b..f458367dcb55 100644
--- a/src/nix/search.cc
+++ b/src/nix/search.cc
@@ -38,12 +38,12 @@ struct CmdSearch : SourceExprCommand, MixJSON
             .longName("update-cache")
             .shortName('u')
             .description("update the package search cache")
-            .handler([&](Strings ss) { writeCache = true; useCache = false; });
+            .handler([&]() { writeCache = true; useCache = false; });
 
         mkFlag()
             .longName("no-cache")
             .description("do not use or update the package search cache")
-            .handler([&](Strings ss) { writeCache = false; useCache = false; });
+            .handler([&]() { writeCache = false; useCache = false; });
     }
 
     std::string name() override
diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc
index 992ff742835e..b1825c412c2d 100644
--- a/src/nix/sigs.cc
+++ b/src/nix/sigs.cc
@@ -19,7 +19,7 @@ struct CmdCopySigs : StorePathsCommand
             .labels({"store-uri"})
             .description("use signatures from specified store")
             .arity(1)
-            .handler([&](Strings ss) { substituterUris.push_back(ss.front()); });
+            .handler([&](std::vector<std::string> ss) { substituterUris.push_back(ss[0]); });
     }
 
     std::string name() override
@@ -101,7 +101,12 @@ struct CmdSignPaths : StorePathsCommand
 
     CmdSignPaths()
     {
-        mkFlag('k', "key-file", {"file"}, "file containing the secret signing key", &secretKeyFile);
+        mkFlag()
+            .shortName('k')
+            .longName("key-file")
+            .label("file")
+            .description("file containing the secret signing key")
+            .dest(&secretKeyFile);
     }
 
     std::string name() override
diff --git a/src/nix/verify.cc b/src/nix/verify.cc
index 4913d990097d..6540208a8a2c 100644
--- a/src/nix/verify.cc
+++ b/src/nix/verify.cc
@@ -25,7 +25,7 @@ struct CmdVerify : StorePathsCommand
             .labels({"store-uri"})
             .description("use signatures from specified store")
             .arity(1)
-            .handler([&](Strings ss) { substituterUris.push_back(ss.front()); });
+            .handler([&](std::vector<std::string> ss) { substituterUris.push_back(ss[0]); });
         mkIntFlag('n', "sigs-needed", "require that each path has at least N valid signatures", &sigsNeeded);
     }