about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/libexpr/common-opts.cc7
-rw-r--r--src/libexpr/download.cc235
-rw-r--r--src/libexpr/download.hh22
-rw-r--r--src/libexpr/eval.cc5
-rw-r--r--src/libexpr/parser.y20
-rw-r--r--src/libexpr/primops.cc75
-rw-r--r--src/libstore/build.cc41
-rw-r--r--src/libstore/globals.cc10
-rw-r--r--src/libstore/globals.hh8
-rw-r--r--src/libstore/local-store.cc3
-rw-r--r--src/libstore/local.mk1
-rw-r--r--src/libutil/util.cc40
-rw-r--r--src/libutil/util.hh7
-rw-r--r--src/nix-collect-garbage/local.mk7
-rw-r--r--src/nix-collect-garbage/nix-collect-garbage.cc91
-rw-r--r--src/nix-daemon/local.mk2
-rw-r--r--src/nix-env/nix-env.cc4
-rw-r--r--src/nix-env/profiles.cc11
-rw-r--r--src/nix-instantiate/nix-instantiate.cc16
-rw-r--r--src/nix-store/nix-store.cc11
20 files changed, 496 insertions, 120 deletions
diff --git a/src/libexpr/common-opts.cc b/src/libexpr/common-opts.cc
index c03d720bde82..13760490d9c4 100644
--- a/src/libexpr/common-opts.cc
+++ b/src/libexpr/common-opts.cc
@@ -1,5 +1,6 @@
 #include "common-opts.hh"
-#include "../libmain/shared.hh"
+#include "shared.hh"
+#include "download.hh"
 #include "util.hh"
 
 
@@ -53,7 +54,9 @@ bool parseSearchPathArg(Strings::iterator & i,
 
 Path lookupFileArg(EvalState & state, string s)
 {
-    if (s.size() > 2 && s.at(0) == '<' && s.at(s.size() - 1) == '>') {
+    if (isUri(s))
+        return downloadFileCached(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
diff --git a/src/libexpr/download.cc b/src/libexpr/download.cc
new file mode 100644
index 000000000000..18ab6fbcda73
--- /dev/null
+++ b/src/libexpr/download.cc
@@ -0,0 +1,235 @@
+#include "download.hh"
+#include "util.hh"
+#include "globals.hh"
+#include "hash.hh"
+#include "store-api.hh"
+
+#include <curl/curl.h>
+
+namespace nix {
+
+struct Curl
+{
+    CURL * curl;
+    string data;
+    string etag, status, expectedETag;
+
+    struct curl_slist * requestHeaders;
+
+    static size_t writeCallback(void * contents, size_t size, size_t nmemb, void * userp)
+    {
+        Curl & c(* (Curl *) userp);
+        size_t realSize = size * nmemb;
+        c.data.append((char *) contents, realSize);
+        return realSize;
+    }
+
+    static size_t headerCallback(void * contents, size_t size, size_t nmemb, void * userp)
+    {
+        Curl & c(* (Curl *) userp);
+        size_t realSize = size * nmemb;
+        string line = string((char *) contents, realSize);
+        printMsg(lvlVomit, format("got header: %1%") % trim(line));
+        if (line.compare(0, 5, "HTTP/") == 0) { // new response starts
+            c.etag = "";
+            auto ss = tokenizeString<vector<string>>(line, " ");
+            c.status = ss.size() >= 2 ? ss[1] : "";
+        } else {
+            auto i = line.find(':');
+            if (i != string::npos) {
+                string name = trim(string(line, 0, i));
+                if (name == "ETag") { // FIXME: case
+                    c.etag = trim(string(line, i + 1));
+                    /* Hack to work around a GitHub bug: it sends
+                       ETags, but ignores If-None-Match. So if we get
+                       the expected ETag on a 200 response, then shut
+                       down the connection because we already have the
+                       data. */
+                    printMsg(lvlDebug, format("got ETag: %1%") % c.etag);
+                    if (c.etag == c.expectedETag && c.status == "200") {
+                        printMsg(lvlDebug, format("shutting down on 200 HTTP response with expected ETag"));
+                        return 0;
+                    }
+                }
+            }
+        }
+        return realSize;
+    }
+
+    static int progressCallback(void * clientp, double dltotal, double dlnow, double ultotal, double ulnow)
+    {
+        return _isInterrupted;
+    }
+
+    Curl()
+    {
+        requestHeaders = 0;
+
+        curl = curl_easy_init();
+        if (!curl) throw Error("unable to initialize curl");
+
+        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+        curl_easy_setopt(curl, CURLOPT_CAINFO, getEnv("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt").c_str());
+        curl_easy_setopt(curl, CURLOPT_USERAGENT, ("Nix/" + nixVersion).c_str());
+        curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1);
+
+        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
+        curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) &curl);
+
+        curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback);
+        curl_easy_setopt(curl, CURLOPT_HEADERDATA, (void *) &curl);
+
+        curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressCallback);
+        curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
+    }
+
+    ~Curl()
+    {
+        if (curl) curl_easy_cleanup(curl);
+        if (requestHeaders) curl_slist_free_all(requestHeaders);
+    }
+
+    bool fetch(const string & url, const string & expectedETag = "")
+    {
+        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+
+        data.clear();
+
+        if (requestHeaders) {
+            curl_slist_free_all(requestHeaders);
+            requestHeaders = 0;
+        }
+
+        if (!expectedETag.empty()) {
+            this->expectedETag = expectedETag;
+            requestHeaders = curl_slist_append(requestHeaders, ("If-None-Match: " + expectedETag).c_str());
+        }
+
+        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, requestHeaders);
+
+        CURLcode res = curl_easy_perform(curl);
+        checkInterrupt();
+        if (res == CURLE_WRITE_ERROR && etag == expectedETag) return false;
+        if (res != CURLE_OK)
+            throw DownloadError(format("unable to download ‘%1%’: %2% (%3%)")
+                % url % curl_easy_strerror(res) % res);
+
+        long httpStatus = 0;
+        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus);
+        if (httpStatus == 304) return false;
+
+        return true;
+    }
+};
+
+
+DownloadResult downloadFile(string url, string expectedETag)
+{
+    DownloadResult res;
+    Curl curl;
+    if (curl.fetch(url, expectedETag)) {
+        res.cached = false;
+        res.data = curl.data;
+    } else
+        res.cached = true;
+    res.etag = curl.etag;
+    return res;
+}
+
+
+Path downloadFileCached(const string & url, bool unpack)
+{
+    Path cacheDir = getEnv("XDG_CACHE_HOME", getEnv("HOME", "") + "/.cache") + "/nix/tarballs";
+    createDirs(cacheDir);
+
+    string urlHash = printHash32(hashString(htSHA256, url));
+
+    Path dataFile = cacheDir + "/" + urlHash + ".info";
+    Path fileLink = cacheDir + "/" + urlHash + "-file";
+
+    Path storePath;
+
+    string expectedETag;
+
+    int ttl = settings.get("tarball-ttl", 60 * 60);
+    bool skip = false;
+
+    if (pathExists(fileLink) && pathExists(dataFile)) {
+        storePath = readLink(fileLink);
+        store->addTempRoot(storePath);
+        if (store->isValidPath(storePath)) {
+            auto ss = tokenizeString<vector<string>>(readFile(dataFile), "\n");
+            if (ss.size() >= 3 && ss[0] == url) {
+                time_t lastChecked;
+                if (string2Int(ss[2], lastChecked) && lastChecked + ttl >= time(0))
+                    skip = true;
+                else if (!ss[1].empty()) {
+                    printMsg(lvlDebug, format("verifying previous ETag ‘%1%’") % ss[1]);
+                    expectedETag = ss[1];
+                }
+            }
+        } else
+            storePath = "";
+    }
+
+    string name;
+    auto p = url.rfind('/');
+    if (p != string::npos) name = string(url, p + 1);
+
+    if (!skip) {
+
+        if (storePath.empty())
+            printMsg(lvlInfo, format("downloading ‘%1%’...") % url);
+        else
+            printMsg(lvlInfo, format("checking ‘%1%’...") % url);
+
+        try {
+            auto res = downloadFile(url, expectedETag);
+
+            if (!res.cached)
+                storePath = store->addTextToStore(name, res.data, PathSet(), false);
+
+            assert(!storePath.empty());
+            replaceSymlink(storePath, fileLink);
+
+            writeFile(dataFile, url + "\n" + res.etag + "\n" + int2String(time(0)) + "\n");
+        } catch (DownloadError & e) {
+            if (storePath.empty()) throw;
+            printMsg(lvlError, format("warning: %1%; using cached result") % e.msg());
+        }
+    }
+
+    if (unpack) {
+        Path unpackedLink = cacheDir + "/" + baseNameOf(storePath) + "-unpacked";
+        Path unpackedStorePath;
+        if (pathExists(unpackedLink)) {
+            unpackedStorePath = readLink(unpackedLink);
+            store->addTempRoot(unpackedStorePath);
+            if (!store->isValidPath(unpackedStorePath))
+                unpackedStorePath = "";
+        }
+        if (unpackedStorePath.empty()) {
+            printMsg(lvlInfo, format("unpacking ‘%1%’...") % url);
+            Path tmpDir = createTempDir();
+            AutoDelete autoDelete(tmpDir, true);
+            runProgram("tar", true, {"xf", storePath, "-C", tmpDir, "--strip-components", "1"}, "");
+            unpackedStorePath = store->addToStore(name, tmpDir, true, htSHA256, defaultPathFilter, false);
+        }
+        replaceSymlink(unpackedStorePath, unpackedLink);
+        return unpackedStorePath;
+    }
+
+    return storePath;
+}
+
+
+bool isUri(const string & s)
+{
+    size_t pos = s.find("://");
+    if (pos == string::npos) return false;
+    string scheme(s, 0, pos);
+    return scheme == "http" || scheme == "https";
+}
+
+
+}
diff --git a/src/libexpr/download.hh b/src/libexpr/download.hh
new file mode 100644
index 000000000000..28c9117e4227
--- /dev/null
+++ b/src/libexpr/download.hh
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "types.hh"
+#include <string>
+
+namespace nix {
+
+struct DownloadResult
+{
+    bool cached;
+    string data, etag;
+};
+
+DownloadResult downloadFile(string url, string expectedETag = "");
+
+Path downloadFileCached(const string & url, bool unpack);
+
+MakeError(DownloadError, Error)
+
+bool isUri(const string & s);
+
+}
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index d8a4193a8e64..301f991b7ab9 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -292,6 +292,11 @@ Path EvalState::checkSourcePath(const Path & path_)
         if (path == i.second || isInDir(path, i.second))
             return path;
 
+    /* Hack to support the chroot dependencies of corepkgs (see
+       corepkgs/config.nix.in). */
+    if (path == settings.nixPrefix && isStorePath(settings.nixPrefix))
+        return path;
+
     throw RestrictedPathError(format("access to path ‘%1%’ is forbidden in restricted mode") % path_);
 }
 
diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y
index 664d6692f51e..26168b2ed420 100644
--- a/src/libexpr/parser.y
+++ b/src/libexpr/parser.y
@@ -527,6 +527,8 @@ formal
 #include <unistd.h>
 
 #include <eval.hh>
+#include <download.hh>
+#include <store-api.hh>
 
 
 namespace nix {
@@ -611,6 +613,9 @@ void EvalState::addToSearchPath(const string & s, bool warn)
         path = string(s, pos + 1);
     }
 
+    if (isUri(path))
+        path = downloadFileCached(path, true);
+
     path = absPath(path);
     if (pathExists(path)) {
         debug(format("adding path ‘%1%’ to the search path") % path);
@@ -629,16 +634,17 @@ Path EvalState::findFile(const string & path)
 
 Path EvalState::findFile(SearchPath & searchPath, const string & path, const Pos & pos)
 {
-    foreach (SearchPath::iterator, i, searchPath) {
+    for (auto & i : searchPath) {
+        assert(!isUri(i.second));
         Path res;
-        if (i->first.empty())
-            res = i->second + "/" + path;
+        if (i.first.empty())
+            res = i.second + "/" + path;
         else {
-            if (path.compare(0, i->first.size(), i->first) != 0 ||
-                (path.size() > i->first.size() && path[i->first.size()] != '/'))
+            if (path.compare(0, i.first.size(), i.first) != 0 ||
+                (path.size() > i.first.size() && path[i.first.size()] != '/'))
                 continue;
-            res = i->second +
-                (path.size() == i->first.size() ? "" : "/" + string(path, i->first.size()));
+            res = i.second +
+                (path.size() == i.first.size() ? "" : "/" + string(path, i.first.size()));
         }
         if (pathExists(res)) return canonPath(res);
     }
diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc
index e818496460ea..fe2f1b1e0ae1 100644
--- a/src/libexpr/primops.cc
+++ b/src/libexpr/primops.cc
@@ -9,6 +9,7 @@
 #include "json-to-value.hh"
 #include "names.hh"
 #include "eval-inline.hh"
+#include "download.hh"
 
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -18,8 +19,6 @@
 #include <cstring>
 #include <dlfcn.h>
 
-#include <curl/curl.h>
-
 
 namespace nix {
 
@@ -104,7 +103,7 @@ static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args
         }
         w.attrs->sort();
         Value fun;
-        state.evalFile(state.findFile("nix/imported-drv-to-derivation.nix"), fun);
+        state.evalFile(settings.nixDataDir + "/nix/corepkgs/imported-drv-to-derivation.nix", fun);
         state.forceFunction(fun, pos);
         mkApp(v, fun, w);
         state.forceAttrs(v, pos);
@@ -1486,53 +1485,6 @@ static void prim_compareVersions(EvalState & state, const Pos & pos, Value * * a
  *************************************************************/
 
 
-struct Curl
-{
-    CURL * curl;
-    string data;
-
-    static size_t writeCallback(void * contents, size_t size, size_t nmemb, void * userp)
-    {
-        Curl & c(* (Curl *) userp);
-        size_t realSize = size * nmemb;
-        c.data.append((char *) contents, realSize);
-        return realSize;
-    }
-
-    Curl()
-    {
-        curl = curl_easy_init();
-        if (!curl) throw Error("unable to initialize curl");
-
-        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-        curl_easy_setopt(curl, CURLOPT_CAINFO, getEnv("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt").c_str());
-        curl_easy_setopt(curl, CURLOPT_USERAGENT, ("Nix/" + nixVersion).c_str());
-
-        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
-        curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) &curl);
-    }
-
-    ~Curl()
-    {
-        if (curl) curl_easy_cleanup(curl);
-    }
-
-    string fetch(const string & url)
-    {
-        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
-
-        data.clear();
-
-        CURLcode res = curl_easy_perform(curl);
-        if (res != CURLE_OK)
-            throw Error(format("unable to download ‘%1%’: %2%")
-                % url % curl_easy_strerror(res));
-
-        return data;
-    }
-};
-
-
 void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
     const string & who, bool unpack)
 {
@@ -1560,25 +1512,7 @@ void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
     } else
         url = state.forceStringNoCtx(*args[0], pos);
 
-    // TODO: cache downloads.
-
-    Curl curl;
-    string data = curl.fetch(url);
-
-    string name;
-    string::size_type p = url.rfind('/');
-    if (p != string::npos) name = string(url, p + 1);
-
-    Path storePath = store->addTextToStore(name, data, PathSet(), state.repair);
-
-    if (unpack) {
-        Path tmpDir = createTempDir();
-        AutoDelete autoDelete(tmpDir, true);
-        runProgram("tar", true, {"xf", storePath, "-C", tmpDir, "--strip-components", "1"}, "");
-        storePath = store->addToStore(name, tmpDir, true, htSHA256, defaultPathFilter, state.repair);
-    }
-
-    mkString(v, storePath, singleton<PathSet>(storePath));
+    mkString(v, downloadFileCached(url, unpack), PathSet({url}));
 }
 
 
@@ -1738,8 +1672,7 @@ void EvalState::createBaseEnv()
 
     /* Add a wrapper around the derivation primop that computes the
        `drvPath' and `outPath' attributes lazily. */
-    string path = findFile("nix/derivation.nix");
-    assert(!path.empty());
+    string path = settings.nixDataDir + "/nix/corepkgs/derivation.nix";
     sDerivationNix = symbols.create(path);
     evalFile(path, v);
     addConstant("derivation", v);
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
index 90bcccd243a7..50c59c1314d9 100644
--- a/src/libstore/build.cc
+++ b/src/libstore/build.cc
@@ -20,6 +20,7 @@
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <sys/utsname.h>
+#include <sys/select.h>
 #include <fcntl.h>
 #include <unistd.h>
 #include <errno.h>
@@ -1786,8 +1787,10 @@ void DerivationGoal::startBuilder()
     if (useChroot) {
 
         string defaultChrootDirs;
+#if CHROOT_ENABLED
         if (isInStore(BASH_PATH))
             defaultChrootDirs = "/bin/sh=" BASH_PATH;
+#endif
 
         /* Allow a user-configurable set of directories from the
            host file system. */
@@ -1895,7 +1898,7 @@ void DerivationGoal::startBuilder()
            build user. */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
         createDirs(chrootStoreDir);
-        chmod_(chrootStoreDir, 0730);
+        chmod_(chrootStoreDir, 01775);
 
         if (chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
             throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
@@ -1969,6 +1972,42 @@ void DerivationGoal::startBuilder()
                 }
     }
 
+    if (settings.preBuildHook != "") {
+        printMsg(lvlChatty, format("executing pre-build hook ‘%1%’")
+            % settings.preBuildHook);
+        auto args = useChroot ? Strings({drvPath, chrootRootDir}) :
+            Strings({ drvPath });
+        enum BuildHookState {
+            stBegin,
+            stExtraChrootDirs
+        };
+        auto state = stBegin;
+        auto lines = runProgram(settings.preBuildHook, false, args);
+        auto lastPos = std::string::size_type{0};
+        for (auto nlPos = lines.find('\n'); nlPos != string::npos;
+                nlPos = lines.find('\n', lastPos)) {
+            auto line = std::string{lines, lastPos, nlPos};
+            lastPos = nlPos + 1;
+            if (state == stBegin) {
+                if (line == "extra-chroot-dirs") {
+                    state = stExtraChrootDirs;
+                } else {
+                    throw Error(format("unknown pre-build hook command ‘%1%’")
+                        % line);
+                }
+            } else if (state == stExtraChrootDirs) {
+                if (line == "") {
+                    state = stBegin;
+                } else {
+                    auto p = line.find('=');
+                    if (p == string::npos)
+                        dirsInChroot[line] = line;
+                    else
+                        dirsInChroot[string(line, 0, p)] = string(line, p + 1);
+                }
+            }
+        }
+    }
 
     /* Run the builder. */
     printMsg(lvlChatty, format("executing builder ‘%1%’") % drv.builder);
diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc
index e382b3aac03a..d5615d93c7d0 100644
--- a/src/libstore/globals.cc
+++ b/src/libstore/globals.cc
@@ -67,6 +67,7 @@ Settings::Settings()
 
 void Settings::processEnvironment()
 {
+    nixPrefix = NIX_PREFIX;
     nixStore = canonPath(getEnv("NIX_STORE_DIR", getEnv("NIX_STORE", NIX_STORE_DIR)));
     nixDataDir = canonPath(getEnv("NIX_DATA_DIR", NIX_DATA_DIR));
     nixLogDir = canonPath(getEnv("NIX_LOG_DIR", NIX_LOG_DIR));
@@ -143,6 +144,14 @@ bool Settings::get(const string & name, bool def)
 }
 
 
+int Settings::get(const string & name, int def)
+{
+    int res = def;
+    _get(res, name);
+    return res;
+}
+
+
 void Settings::update()
 {
     _get(tryFallback, "build-fallback");
@@ -173,6 +182,7 @@ void Settings::update()
     _get(logServers, "log-servers");
     _get(enableImportNative, "allow-unsafe-native-code-during-evaluation");
     _get(useCaseHack, "use-case-hack");
+    _get(preBuildHook, "pre-build-hook");
 
     string subs = getEnv("NIX_SUBSTITUTERS", "default");
     if (subs == "default") {
diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh
index 0230a540e655..60b11afe6088 100644
--- a/src/libstore/globals.hh
+++ b/src/libstore/globals.hh
@@ -27,6 +27,8 @@ struct Settings {
 
     bool get(const string & name, bool def);
 
+    int get(const string & name, int def);
+
     void update();
 
     string pack();
@@ -40,6 +42,8 @@ struct Settings {
 
     Path nixDataDir; /* !!! fix */
 
+    Path nixPrefix;
+
     /* The directory where we log various operations. */
     Path nixLogDir;
 
@@ -202,6 +206,10 @@ struct Settings {
     /* Whether the importNative primop should be enabled */
     bool enableImportNative;
 
+    /* The hook to run just before a build to set derivation-specific
+       build settings */
+    Path preBuildHook;
+
 private:
     SettingsMap settings, overrides;
 
diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc
index 3ec467649d3b..1dcd7228f347 100644
--- a/src/libstore/local-store.cc
+++ b/src/libstore/local-store.cc
@@ -13,6 +13,7 @@
 
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <sys/select.h>
 #include <sys/time.h>
 #include <unistd.h>
 #include <utime.h>
@@ -256,7 +257,7 @@ LocalStore::LocalStore(bool reserveSpace)
         if (chmod(perUserDir.c_str(), 01777) == -1)
             throw SysError(format("could not set permissions on ‘%1%’ to 1777") % perUserDir);
 
-        mode_t perm = 01735;
+        mode_t perm = 01775;
 
         struct group * gr = getgrnam(settings.buildUsersGroup.c_str());
         if (!gr)
diff --git a/src/libstore/local.mk b/src/libstore/local.mk
index 78b4d0fd4b94..771c06753a65 100644
--- a/src/libstore/local.mk
+++ b/src/libstore/local.mk
@@ -15,6 +15,7 @@ ifeq ($(OS), SunOS)
 endif
 
 libstore_CXXFLAGS = \
+ -DNIX_PREFIX=\"$(prefix)\" \
  -DNIX_STORE_DIR=\"$(storedir)\" \
  -DNIX_DATA_DIR=\"$(datadir)\" \
  -DNIX_STATE_DIR=\"$(localstatedir)/nix\" \
diff --git a/src/libutil/util.cc b/src/libutil/util.cc
index be0a9bf317d1..5cda9a0677b5 100644
--- a/src/libutil/util.cc
+++ b/src/libutil/util.cc
@@ -413,6 +413,17 @@ void createSymlink(const Path & target, const Path & link)
 }
 
 
+void replaceSymlink(const Path & target, const Path & link)
+{
+    Path tmp = canonPath(dirOf(link) + "/.new_" + baseNameOf(link));
+
+    createSymlink(target, tmp);
+
+    if (rename(tmp.c_str(), link.c_str()) != 0)
+        throw SysError(format("renaming ‘%1%’ to ‘%2%’") % tmp % link);
+}
+
+
 LogType logType = ltPretty;
 Verbosity verbosity = lvlInfo;
 
@@ -931,16 +942,16 @@ string runProgram(Path program, bool searchPath, const Strings & args,
     checkInterrupt();
 
     /* Create a pipe. */
-    Pipe stdout, stdin;
-    stdout.create();
-    if (!input.empty()) stdin.create();
+    Pipe out, in;
+    out.create();
+    if (!input.empty()) in.create();
 
     /* Fork. */
     Pid pid = startProcess([&]() {
-        if (dup2(stdout.writeSide, STDOUT_FILENO) == -1)
+        if (dup2(out.writeSide, STDOUT_FILENO) == -1)
             throw SysError("dupping stdout");
         if (!input.empty()) {
-            if (dup2(stdin.readSide, STDIN_FILENO) == -1)
+            if (dup2(in.readSide, STDIN_FILENO) == -1)
                 throw SysError("dupping stdin");
         }
 
@@ -956,16 +967,16 @@ string runProgram(Path program, bool searchPath, const Strings & args,
         throw SysError(format("executing ‘%1%’") % program);
     });
 
-    stdout.writeSide.close();
+    out.writeSide.close();
 
     /* FIXME: This can deadlock if the input is too long. */
     if (!input.empty()) {
-        stdin.readSide.close();
-        writeFull(stdin.writeSide, input);
-        stdin.writeSide.close();
+        in.readSide.close();
+        writeFull(in.writeSide, input);
+        in.writeSide.close();
     }
 
-    string result = drainFD(stdout.readSide);
+    string result = drainFD(out.readSide);
 
     /* Wait for the child to finish. */
     int status = pid.wait(true);
@@ -1076,6 +1087,15 @@ string chomp(const string & s)
 }
 
 
+string trim(const string & s, const string & whitespace)
+{
+    auto i = s.find_first_not_of(whitespace);
+    if (i == string::npos) return "";
+    auto j = s.find_last_not_of(whitespace);
+    return string(s, i, j == string::npos ? j : j - i + 1);
+}
+
+
 string statusToString(int status)
 {
     if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
diff --git a/src/libutil/util.hh b/src/libutil/util.hh
index 20330fb7699e..7d20351eec2e 100644
--- a/src/libutil/util.hh
+++ b/src/libutil/util.hh
@@ -110,6 +110,9 @@ Paths createDirs(const Path & path);
 /* Create a symlink. */
 void createSymlink(const Path & target, const Path & link);
 
+/* Atomically create or replace a symlink. */
+void replaceSymlink(const Path & target, const Path & link);
+
 
 template<class T, class A>
 T singleton(const A & a)
@@ -334,6 +337,10 @@ string concatStringsSep(const string & sep, const StringSet & ss);
 string chomp(const string & s);
 
 
+/* Remove whitespace from the start and end of a string. */
+string trim(const string & s, const string & whitespace = " \n\r\t");
+
+
 /* Convert the exit status of a child as returned by wait() into an
    error string. */
 string statusToString(int status);
diff --git a/src/nix-collect-garbage/local.mk b/src/nix-collect-garbage/local.mk
new file mode 100644
index 000000000000..02d14cf62199
--- /dev/null
+++ b/src/nix-collect-garbage/local.mk
@@ -0,0 +1,7 @@
+programs += nix-collect-garbage
+
+nix-collect-garbage_DIR := $(d)
+
+nix-collect-garbage_SOURCES := $(d)/nix-collect-garbage.cc
+
+nix-collect-garbage_LIBS = libmain libstore libutil libformat
diff --git a/src/nix-collect-garbage/nix-collect-garbage.cc b/src/nix-collect-garbage/nix-collect-garbage.cc
new file mode 100644
index 000000000000..ae75fd62103c
--- /dev/null
+++ b/src/nix-collect-garbage/nix-collect-garbage.cc
@@ -0,0 +1,91 @@
+#include "hash.hh"
+#include "shared.hh"
+#include "globals.hh"
+
+#include <iostream>
+
+using namespace nix;
+
+std::string gen = "old";
+bool dryRun = false;
+
+void runProgramSimple(Path program, const Strings & args)
+{
+    checkInterrupt();
+
+    /* Fork. */
+    Pid pid = startProcess([&]() {
+        Strings args_(args);
+        args_.push_front(program);
+        auto cargs = stringsToCharPtrs(args_);
+
+        execv(program.c_str(), (char * *) &cargs[0]);
+
+        throw SysError(format("executing ‘%1%’") % program);
+    });
+
+    pid.wait(true);
+}
+
+
+/* If `-d' was specified, remove all old generations of all profiles.
+ * Of course, this makes rollbacks to before this point in time
+ * impossible. */
+
+void removeOldGenerations(std::string dir)
+{
+    for (auto & i : readDirectory(dir)) {
+        checkInterrupt();
+
+        auto path = dir + "/" + i.name; 
+        auto type = getFileType(path);
+
+        if (type == DT_LNK) {
+            auto link = readLink(path);
+            if (link.find("link") != string::npos) {
+                printMsg(lvlInfo, format("removing old generations of profile %1%") % path);
+
+                auto args = Strings{"-p", path, "--delete-generations", gen};
+                if (dryRun) {
+                    args.push_back("--dry-run");
+                }
+                runProgramSimple(settings.nixBinDir + "/nix-env", args);
+            }
+        } else if (type == DT_DIR) {
+            removeOldGenerations(path);
+        }
+    }
+}
+
+int main(int argc, char * * argv)
+{
+    bool removeOld = false;
+    Strings extraArgs;
+
+    return handleExceptions(argv[0], [&]() {
+        initNix();
+
+        parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
+            if (*arg == "--help")
+                showManPage("nix-collect-garbage");
+            else if (*arg == "--version")
+                printVersion("nix-collect-garbage");
+            else if (*arg == "--delete-old" || *arg == "-d") removeOld = true;
+            else if (*arg == "--delete-older-than") {
+                removeOld = true;
+                gen = getArg(*arg, arg, end);
+            }
+            else if (*arg == "--dry-run") dryRun = true;
+            else
+                extraArgs.push_back(*arg);
+            return true;
+        });
+
+        auto profilesDir = settings.nixStateDir + "/profiles";
+        if (removeOld) removeOldGenerations(profilesDir);
+
+        // Run the actual garbage collector.
+        if (!dryRun) runProgramSimple(settings.nixBinDir + "/nix-store", Strings{"--gc"});
+    });
+}
+
diff --git a/src/nix-daemon/local.mk b/src/nix-daemon/local.mk
index e5538bada0b2..5a4474465b3c 100644
--- a/src/nix-daemon/local.mk
+++ b/src/nix-daemon/local.mk
@@ -11,5 +11,3 @@ nix-daemon_LDFLAGS = -pthread
 ifeq ($(OS), SunOS)
         nix-daemon_LDFLAGS += -lsocket
 endif
-
-$(eval $(call install-symlink, nix-daemon, $(bindir)/nix-worker))
diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc
index 10b95dad168c..5cf41e844e83 100644
--- a/src/nix-env/nix-env.cc
+++ b/src/nix-env/nix-env.cc
@@ -1423,6 +1423,8 @@ int main(int argc, char * * argv)
 
         if (!op) throw UsageError("no operation specified");
 
+        store = openStore();
+
         globals.state = std::shared_ptr<EvalState>(new EvalState(searchPath));
         globals.state->repair = repair;
 
@@ -1441,8 +1443,6 @@ int main(int argc, char * * argv)
                 : canonPath(settings.nixStateDir + "/profiles/default");
         }
 
-        store = openStore();
-
         op(globals, opFlags, opArgs);
 
         globals.state->printStats();
diff --git a/src/nix-env/profiles.cc b/src/nix-env/profiles.cc
index cedefb157a4b..5b7a533df290 100644
--- a/src/nix-env/profiles.cc
+++ b/src/nix-env/profiles.cc
@@ -134,16 +134,7 @@ void switchLink(Path link, Path target)
     /* Hacky. */
     if (dirOf(target) == dirOf(link)) target = baseNameOf(target);
 
-    Path tmp = canonPath(dirOf(link) + "/.new_" + baseNameOf(link));
-    createSymlink(target, tmp);
-    /* The rename() system call is supposed to be essentially atomic
-       on Unix.  That is, if we have links `current -> X' and
-       `new_current -> Y', and we rename new_current to current, a
-       process accessing current will see X or Y, but never a
-       file-not-found or other error condition.  This is sufficient to
-       atomically switch user environments. */
-    if (rename(tmp.c_str(), link.c_str()) != 0)
-        throw SysError(format("renaming ‘%1%’ to ‘%2%’") % tmp % link);
+    replaceSymlink(target, link);
 }
 
 
diff --git a/src/nix-instantiate/nix-instantiate.cc b/src/nix-instantiate/nix-instantiate.cc
index 5abaa617d245..bea04180e513 100644
--- a/src/nix-instantiate/nix-instantiate.cc
+++ b/src/nix-instantiate/nix-instantiate.cc
@@ -155,14 +155,16 @@ int main(int argc, char * * argv)
             return true;
         });
 
+        if (evalOnly && !wantsReadWrite)
+            settings.readOnlyMode = true;
+
+        store = openStore();
+
         EvalState state(searchPath);
         state.repair = repair;
 
         Bindings & autoArgs(*evalAutoArgs(state, autoArgs_));
 
-        if (evalOnly && !wantsReadWrite)
-            settings.readOnlyMode = true;
-
         if (attrPaths.empty()) attrPaths.push_back("");
 
         if (findFile) {
@@ -174,8 +176,6 @@ int main(int argc, char * * argv)
             return;
         }
 
-        store = openStore();
-
         if (readStdin) {
             Expr * e = parseStdin(state);
             processExpr(state, attrPaths, parseOnly, strict, autoArgs,
@@ -183,10 +183,10 @@ int main(int argc, char * * argv)
         } else if (files.empty() && !fromArgs)
             files.push_back("./default.nix");
 
-        foreach (Strings::iterator, i, files) {
+        for (auto & i : files) {
             Expr * e = fromArgs
-                ? state.parseExprFromString(*i, absPath("."))
-                : state.parseExprFromFile(resolveExprPath(lookupFileArg(state, *i)));
+                ? state.parseExprFromString(i, absPath("."))
+                : state.parseExprFromFile(resolveExprPath(lookupFileArg(state, i)));
             processExpr(state, attrPaths, parseOnly, strict, autoArgs,
                 evalOnly, outputKind, xmlOutputSourceLocation, e);
         }
diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc
index 60080f253a22..506c1a397b55 100644
--- a/src/nix-store/nix-store.cc
+++ b/src/nix-store/nix-store.cc
@@ -49,12 +49,11 @@ LocalStore & ensureLocalStore()
 
 static Path useDeriver(Path path)
 {
-    if (!isDerivation(path)) {
-        path = store->queryDeriver(path);
-        if (path == "")
-            throw Error(format("deriver of path ‘%1%’ is not known") % path);
-    }
-    return path;
+    if (isDerivation(path)) return path;
+    Path drvPath = store->queryDeriver(path);
+    if (drvPath == "")
+        throw Error(format("deriver of path ‘%1%’ is not known") % path);
+    return drvPath;
 }