about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--corepkgs/fetchurl.nix6
-rw-r--r--doc/manual/command-ref/conf-file.xml8
-rw-r--r--doc/manual/command-ref/opt-common-syn.xml1
-rw-r--r--doc/manual/command-ref/opt-common.xml7
-rw-r--r--mk/tests.mk2
-rw-r--r--release.nix2
-rw-r--r--scripts/nix-profile.sh.in2
-rw-r--r--src/libexpr/eval.cc65
-rw-r--r--src/libexpr/eval.hh8
-rw-r--r--src/libexpr/primops.cc46
-rw-r--r--src/libexpr/primops/fetchGit.cc15
-rw-r--r--src/libexpr/primops/fetchMercurial.cc6
-rw-r--r--src/libmain/shared.cc3
-rw-r--r--src/libstore/build.cc9
-rw-r--r--src/libstore/globals.hh6
-rw-r--r--src/libutil/util.cc6
-rw-r--r--src/libutil/util.hh6
-rwxr-xr-xsrc/nix-build/nix-build.cc4
-rw-r--r--src/nix-instantiate/nix-instantiate.cc2
-rw-r--r--src/nix/eval.cc28
-rw-r--r--src/nix/installables.cc6
-rw-r--r--src/nix/log.cc1
-rw-r--r--tests/fetchGit.sh7
-rw-r--r--tests/fetchMercurial.sh7
-rw-r--r--tests/local.mk3
-rw-r--r--tests/pure-eval.nix3
-rw-r--r--tests/pure-eval.sh18
-rw-r--r--tests/restricted.nix1
-rw-r--r--tests/restricted.sh10
29 files changed, 199 insertions, 89 deletions
diff --git a/corepkgs/fetchurl.nix b/corepkgs/fetchurl.nix
index e135b947fdbb..0ce1bab112f3 100644
--- a/corepkgs/fetchurl.nix
+++ b/corepkgs/fetchurl.nix
@@ -1,4 +1,4 @@
-{ system ? builtins.currentSystem
+{ system ? "" # obsolete
 , url
 , md5 ? "", sha1 ? "", sha256 ? "", sha512 ? ""
 , outputHash ?
@@ -17,7 +17,9 @@ derivation {
   inherit outputHashAlgo outputHash;
   outputHashMode = if unpack || executable then "recursive" else "flat";
 
-  inherit name system url executable unpack;
+  inherit name url executable unpack;
+
+  system = "builtin";
 
   # No need to double the amount of network traffic
   preferLocalBuild = true;
diff --git a/doc/manual/command-ref/conf-file.xml b/doc/manual/command-ref/conf-file.xml
index 2ddca991f19c..fff7994f28df 100644
--- a/doc/manual/command-ref/conf-file.xml
+++ b/doc/manual/command-ref/conf-file.xml
@@ -734,6 +734,14 @@ builtins.fetchurl {
   </varlistentry>
 
 
+  <varlistentry xml:id="conf-show-trace"><term><literal>show-trace</literal></term>
+
+    <listitem><para>Causes Nix to print out a stack trace in case of Nix
+    expression evaluation errors.</para></listitem>
+
+  </varlistentry>
+
+
 </variablelist>
 
 </para>
diff --git a/doc/manual/command-ref/opt-common-syn.xml b/doc/manual/command-ref/opt-common-syn.xml
index 3aff4e1b6357..168bef080f4f 100644
--- a/doc/manual/command-ref/opt-common-syn.xml
+++ b/doc/manual/command-ref/opt-common-syn.xml
@@ -47,7 +47,6 @@
 </arg>
 <arg><option>--fallback</option></arg>
 <arg><option>--readonly-mode</option></arg>
-<arg><option>--show-trace</option></arg>
 <arg>
   <option>-I</option>
   <replaceable>path</replaceable>
diff --git a/doc/manual/command-ref/opt-common.xml b/doc/manual/command-ref/opt-common.xml
index 32d53c753a22..bcb60b30125c 100644
--- a/doc/manual/command-ref/opt-common.xml
+++ b/doc/manual/command-ref/opt-common.xml
@@ -301,13 +301,6 @@
 
 </varlistentry>
 
-<varlistentry><term><option>--show-trace</option></term>
-
-  <listitem><para>Causes Nix to print out a stack trace in case of Nix
-  expression evaluation errors.</para></listitem>
-
-</varlistentry>
-
 
 <varlistentry xml:id="opt-I"><term><option>-I</option> <replaceable>path</replaceable></term>
 
diff --git a/mk/tests.mk b/mk/tests.mk
index e353d46a0d02..70c30661b95f 100644
--- a/mk/tests.mk
+++ b/mk/tests.mk
@@ -39,7 +39,7 @@ installcheck:
 	  echo "$${red}$$failed out of $$total tests failed $$normal"; \
 	  exit 1; \
 	else \
-		echo "$${green}All tests succeeded"; \
+		echo "$${green}All tests succeeded$$normal"; \
 	fi
 
 .PHONY: check installcheck
diff --git a/release.nix b/release.nix
index 30bec2a6e3e1..04f1f836743b 100644
--- a/release.nix
+++ b/release.nix
@@ -6,7 +6,7 @@
 
 let
 
-  pkgs = import nixpkgs {};
+  pkgs = import nixpkgs { system = "x86_64-linux"; };
 
   jobs = rec {
 
diff --git a/scripts/nix-profile.sh.in b/scripts/nix-profile.sh.in
index 450d683c7f44..a5f52274fc70 100644
--- a/scripts/nix-profile.sh.in
+++ b/scripts/nix-profile.sh.in
@@ -75,7 +75,7 @@ if [ -n "$HOME" ] && [ -n "$USER" ]; then
         export NIX_SSL_CERT_FILE="$NIX_LINK/etc/ca-bundle.crt"
     fi
 
-    if [ -n ${MANPATH} ]; then
+    if [ -n "${MANPATH}" ]; then
         export MANPATH="$NIX_LINK/share/man:$MANPATH"
     fi
 
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 087a95ddef8c..f8685e010e15 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -300,16 +300,25 @@ EvalState::EvalState(const Strings & _searchPath, ref<Store> store)
 {
     countCalls = getEnv("NIX_COUNT_CALLS", "0") != "0";
 
-    restricted = settings.restrictEval;
-
     assert(gcInitialised);
 
     /* Initialise the Nix expression search path. */
-    Strings paths = parseNixPath(getEnv("NIX_PATH", ""));
-    for (auto & i : _searchPath) addToSearchPath(i);
-    for (auto & i : paths) addToSearchPath(i);
+    if (!settings.pureEval) {
+        Strings paths = parseNixPath(getEnv("NIX_PATH", ""));
+        for (auto & i : _searchPath) addToSearchPath(i);
+        for (auto & i : paths) addToSearchPath(i);
+    }
     addToSearchPath("nix=" + settings.nixDataDir + "/nix/corepkgs");
 
+    if (settings.restrictEval || settings.pureEval) {
+        allowedPaths = PathSet();
+        for (auto & i : searchPath) {
+            auto r = resolveSearchPathElem(i);
+            if (!r.first) continue;
+            allowedPaths->insert(r.second);
+        }
+    }
+
     clearValue(vEmptySet);
     vEmptySet.type = tAttrs;
     vEmptySet.attrs = allocBindings(0);
@@ -326,38 +335,39 @@ EvalState::~EvalState()
 
 Path EvalState::checkSourcePath(const Path & path_)
 {
-    if (!restricted) return path_;
+    if (!allowedPaths) return path_;
+
+    auto doThrow = [&]() [[noreturn]] {
+        throw RestrictedPathError("access to path '%1%' is forbidden in restricted mode", path_);
+    };
+
+    bool found = false;
+
+    for (auto & i : *allowedPaths) {
+        if (isDirOrInDir(path_, i)) {
+            found = true;
+            break;
+        }
+    }
+
+    if (!found) doThrow();
 
     /* Resolve symlinks. */
     debug(format("checking access to '%s'") % path_);
     Path path = canonPath(path_, true);
 
-    for (auto & i : searchPath) {
-        auto r = resolveSearchPathElem(i);
-        if (!r.first) continue;
-        if (path == r.second || isInDir(path, r.second))
+    for (auto & i : *allowedPaths) {
+        if (isDirOrInDir(path, i))
             return path;
     }
 
-    /* To support import-from-derivation, allow access to anything in
-       the store. FIXME: only allow access to paths that have been
-       constructed by this evaluation. */
-    if (store->isInStore(path)) return path;
-
-#if 0
-    /* Hack to support the chroot dependencies of corepkgs (see
-       corepkgs/config.nix.in). */
-    if (path == settings.nixPrefix && isStorePath(settings.nixPrefix))
-        return path;
-#endif
-
-    throw RestrictedPathError(format("access to path '%1%' is forbidden in restricted mode") % path_);
+    doThrow();
 }
 
 
 void EvalState::checkURI(const std::string & uri)
 {
-    if (!restricted) return;
+    if (!settings.restrictEval) return;
 
     /* 'uri' should be equal to a prefix, or in a subdirectory of a
        prefix. Thus, the prefix https://github.co does not permit
@@ -396,7 +406,7 @@ void EvalState::addConstant(const string & name, Value & v)
 }
 
 
-void EvalState::addPrimOp(const string & name,
+Value * EvalState::addPrimOp(const string & name,
     unsigned int arity, PrimOpFun primOp)
 {
     Value * v = allocValue();
@@ -407,6 +417,7 @@ void EvalState::addPrimOp(const string & name,
     staticBaseEnv.vars[symbols.create(name)] = baseEnvDispl;
     baseEnv.values[baseEnvDispl++] = v;
     baseEnv.values[0]->attrs->push_back(Attr(sym, v));
+    return v;
 }
 
 
@@ -659,8 +670,10 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env)
 }
 
 
-void EvalState::evalFile(const Path & path, Value & v)
+void EvalState::evalFile(const Path & path_, Value & v)
 {
+    auto path = checkSourcePath(path_);
+
     FileEvalCache::iterator i;
     if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) {
         v = i->second;
diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh
index cc971ae80a43..9e3d30d95f49 100644
--- a/src/libexpr/eval.hh
+++ b/src/libexpr/eval.hh
@@ -76,9 +76,9 @@ public:
        already exist there. */
     RepairFlag repair;
 
-    /* If set, don't allow access to files outside of the Nix search
-       path or to environment variables. */
-    bool restricted;
+    /* The allowed filesystem paths in restricted or pure evaluation
+       mode. */
+    std::experimental::optional<PathSet> allowedPaths;
 
     Value vEmptySet;
 
@@ -212,7 +212,7 @@ private:
 
     void addConstant(const string & name, Value & v);
 
-    void addPrimOp(const string & name,
+    Value * addPrimOp(const string & name,
         unsigned int arity, PrimOpFun primOp);
 
 public:
diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc
index 98fe2199e9f5..e90a1da25e0b 100644
--- a/src/libexpr/primops.cc
+++ b/src/libexpr/primops.cc
@@ -39,7 +39,7 @@ std::pair<string, string> decodeContext(const string & s)
         size_t index = s.find("!", 1);
         return std::pair<string, string>(string(s, index + 1), string(s, 1, index - 1));
     } else
-        return std::pair<string, string>(s.at(0) == '/' ? s: string(s, 1), "");
+        return std::pair<string, string>(s.at(0) == '/' ? s : string(s, 1), "");
 }
 
 
@@ -439,7 +439,7 @@ static void prim_tryEval(EvalState & state, const Pos & pos, Value * * args, Val
 static void prim_getEnv(EvalState & state, const Pos & pos, Value * * args, Value & v)
 {
     string name = state.forceStringNoCtx(*args[0], pos);
-    mkString(v, state.restricted ? "" : getEnv(name));
+    mkString(v, settings.restrictEval || settings.pureEval ? "" : getEnv(name));
 }
 
 
@@ -1929,7 +1929,14 @@ void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
 
     state.checkURI(url);
 
+    if (settings.pureEval && !expectedHash)
+        throw Error("in pure evaluation mode, '%s' requires a 'sha256' argument", who);
+
     Path res = getDownloader()->downloadCached(state.store, url, unpack, name, expectedHash);
+
+    if (state.allowedPaths)
+        state.allowedPaths->insert(res);
+
     mkString(v, res, PathSet({res}));
 }
 
@@ -1981,11 +1988,28 @@ void EvalState::createBaseEnv()
     mkNull(v);
     addConstant("null", v);
 
-    mkInt(v, time(0));
-    addConstant("__currentTime", v);
+    auto vThrow = addPrimOp("throw", 1, prim_throw);
 
-    mkString(v, settings.thisSystem);
-    addConstant("__currentSystem", v);
+    auto addPurityError = [&](const std::string & name) {
+        Value * v2 = allocValue();
+        mkString(*v2, fmt("'%s' is not allowed in pure evaluation mode", name));
+        mkApp(v, *vThrow, *v2);
+        addConstant(name, v);
+    };
+
+    if (settings.pureEval)
+        addPurityError("__currentTime");
+    else {
+        mkInt(v, time(0));
+        addConstant("__currentTime", v);
+    }
+
+    if (settings.pureEval)
+        addPurityError("__currentSystem");
+    else {
+        mkString(v, settings.thisSystem);
+        addConstant("__currentSystem", v);
+    }
 
     mkString(v, nixVersion);
     addConstant("__nixVersion", v);
@@ -2001,10 +2025,10 @@ void EvalState::createBaseEnv()
     addConstant("__langVersion", v);
 
     // Miscellaneous
-    addPrimOp("scopedImport", 2, prim_scopedImport);
+    auto vScopedImport = addPrimOp("scopedImport", 2, prim_scopedImport);
     Value * v2 = allocValue();
     mkAttrs(*v2, 0);
-    mkApp(v, *baseEnv.values[baseEnvDispl - 1], *v2);
+    mkApp(v, *vScopedImport, *v2);
     forceValue(v);
     addConstant("import", v);
     if (settings.enableNativeCode) {
@@ -2020,7 +2044,6 @@ void EvalState::createBaseEnv()
     addPrimOp("__isBool", 1, prim_isBool);
     addPrimOp("__genericClosure", 1, prim_genericClosure);
     addPrimOp("abort", 1, prim_abort);
-    addPrimOp("throw", 1, prim_throw);
     addPrimOp("__addErrorContext", 2, prim_addErrorContext);
     addPrimOp("__tryEval", 1, prim_tryEval);
     addPrimOp("__getEnv", 1, prim_getEnv);
@@ -2035,7 +2058,10 @@ void EvalState::createBaseEnv()
 
     // Paths
     addPrimOp("__toPath", 1, prim_toPath);
-    addPrimOp("__storePath", 1, prim_storePath);
+    if (settings.pureEval)
+        addPurityError("__storePath");
+    else
+        addPrimOp("__storePath", 1, prim_storePath);
     addPrimOp("__pathExists", 1, prim_pathExists);
     addPrimOp("baseNameOf", 1, prim_baseNameOf);
     addPrimOp("dirOf", 1, prim_dirOf);
diff --git a/src/libexpr/primops/fetchGit.cc b/src/libexpr/primops/fetchGit.cc
index fb664cffb5b7..2e3e2634db8f 100644
--- a/src/libexpr/primops/fetchGit.cc
+++ b/src/libexpr/primops/fetchGit.cc
@@ -22,10 +22,15 @@ struct GitInfo
     uint64_t revCount = 0;
 };
 
+std::regex revRegex("^[0-9a-fA-F]{40}$");
+
 GitInfo exportGit(ref<Store> store, const std::string & uri,
     std::experimental::optional<std::string> ref, std::string rev,
     const std::string & name)
 {
+    if (settings.pureEval && rev == "")
+        throw Error("in pure evaluation mode, 'fetchGit' requires a Git revision");
+
     if (!ref && rev == "" && hasPrefix(uri, "/") && pathExists(uri + "/.git")) {
 
         bool clean = true;
@@ -76,11 +81,8 @@ GitInfo exportGit(ref<Store> store, const std::string & uri,
 
     if (!ref) ref = "master"s;
 
-    if (rev != "") {
-        std::regex revRegex("^[0-9a-fA-F]{40}$");
-        if (!std::regex_match(rev, revRegex))
-            throw Error("invalid Git revision '%s'", rev);
-    }
+    if (rev != "" && !std::regex_match(rev, revRegex))
+        throw Error("invalid Git revision '%s'", rev);
 
     Path cacheDir = getCacheDir() + "/nix/git";
 
@@ -231,6 +233,9 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va
     mkString(*state.allocAttr(v, state.symbols.create("shortRev")), gitInfo.shortRev);
     mkInt(*state.allocAttr(v, state.symbols.create("revCount")), gitInfo.revCount);
     v.attrs->sort();
+
+    if (state.allowedPaths)
+        state.allowedPaths->insert(gitInfo.storePath);
 }
 
 static RegisterPrimOp r("fetchGit", 1, prim_fetchGit);
diff --git a/src/libexpr/primops/fetchMercurial.cc b/src/libexpr/primops/fetchMercurial.cc
index a317476c5829..5517d83df824 100644
--- a/src/libexpr/primops/fetchMercurial.cc
+++ b/src/libexpr/primops/fetchMercurial.cc
@@ -27,6 +27,9 @@ std::regex commitHashRegex("^[0-9a-fA-F]{40}$");
 HgInfo exportMercurial(ref<Store> store, const std::string & uri,
     std::string rev, const std::string & name)
 {
+    if (settings.pureEval && rev == "")
+        throw Error("in pure evaluation mode, 'fetchMercurial' requires a Mercurial revision");
+
     if (rev == "" && hasPrefix(uri, "/") && pathExists(uri + "/.hg")) {
 
         bool clean = runProgram("hg", true, { "status", "-R", uri, "--modified", "--added", "--removed" }) == "";
@@ -196,6 +199,9 @@ static void prim_fetchMercurial(EvalState & state, const Pos & pos, Value * * ar
     mkString(*state.allocAttr(v, state.symbols.create("shortRev")), std::string(hgInfo.rev, 0, 12));
     mkInt(*state.allocAttr(v, state.symbols.create("revCount")), hgInfo.revCount);
     v.attrs->sort();
+
+    if (state.allowedPaths)
+        state.allowedPaths->insert(hgInfo.storePath);
 }
 
 static RegisterPrimOp r("fetchMercurial", 1, prim_fetchMercurial);
diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc
index 85d3c077ba5e..90a4867163df 100644
--- a/src/libmain/shared.cc
+++ b/src/libmain/shared.cc
@@ -193,9 +193,6 @@ LegacyArgs::LegacyArgs(const std::string & programName,
     mkFlag(0, "readonly-mode", "do not write to the Nix store",
         &settings.readOnlyMode);
 
-    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);
 
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
index d4bd650baf22..523d737d9bf8 100644
--- a/src/libstore/build.cc
+++ b/src/libstore/build.cc
@@ -1810,8 +1810,13 @@ void DerivationGoal::startBuilder()
             useChroot = !fixedOutput && get(drv->env, "__noChroot") != "1";
     }
 
-    if (worker.store.storeDir != worker.store.realStoreDir)
-        useChroot = true;
+    if (worker.store.storeDir != worker.store.realStoreDir) {
+        #if __linux__
+            useChroot = true;
+        #else
+            throw Error("building using a diverted store is not supported on this platform");
+        #endif
+    }
 
     /* If `build-users-group' is not empty, then we have to build as
        one of the members of that group. */
diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh
index af72f7b1e35d..1e50e2d13e93 100644
--- a/src/libstore/globals.hh
+++ b/src/libstore/globals.hh
@@ -211,7 +211,8 @@ public:
     bool lockCPU;
 
     /* Whether to show a stack trace if Nix evaluation fails. */
-    bool showTrace = false;
+    Setting<bool> showTrace{this, false, "show-trace",
+        "Whether to show a stack trace on evaluation errors."};
 
     Setting<bool> enableNativeCode{this, false, "allow-unsafe-native-code-during-evaluation",
         "Whether builtin functions that allow executing native code should be enabled."};
@@ -232,6 +233,9 @@ public:
         "Whether to restrict file system access to paths in $NIX_PATH, "
         "and network access to the URI prefixes listed in 'allowed-uris'."};
 
+    Setting<bool> pureEval{this, false, "pure-eval",
+        "Whether to restrict file system and network access to files specified by cryptographic hash."};
+
     Setting<size_t> buildRepeat{this, 0, "repeat",
         "The number of times to repeat a build in order to verify determinism.",
         {"build-repeat"}};
diff --git a/src/libutil/util.cc b/src/libutil/util.cc
index 197df0c44aa0..272997397794 100644
--- a/src/libutil/util.cc
+++ b/src/libutil/util.cc
@@ -192,6 +192,12 @@ bool isInDir(const Path & path, const Path & dir)
 }
 
 
+bool isDirOrInDir(const Path & path, const Path & dir)
+{
+    return path == dir or isInDir(path, dir);
+}
+
+
 struct stat lstat(const Path & path)
 {
     struct stat st;
diff --git a/src/libutil/util.hh b/src/libutil/util.hh
index a3494e09b09b..75eb9751524e 100644
--- a/src/libutil/util.hh
+++ b/src/libutil/util.hh
@@ -53,10 +53,12 @@ Path dirOf(const Path & path);
    following the final `/'. */
 string baseNameOf(const Path & path);
 
-/* Check whether a given path is a descendant of the given
-   directory. */
+/* Check whether 'path' is a descendant of 'dir'. */
 bool isInDir(const Path & path, const Path & dir);
 
+/* Check whether 'path' is equal to 'dir' or a descendant of 'dir'. */
+bool isDirOrInDir(const Path & path, const Path & dir);
+
 /* Get status of `path'. */
 struct stat lstat(const Path & path);
 
diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc
index 58366daa6e86..1b249427537d 100755
--- a/src/nix-build/nix-build.cc
+++ b/src/nix-build/nix-build.cc
@@ -279,8 +279,8 @@ void mainWrapped(int argc, char * * argv)
             else
                 /* If we're in a #! script, interpret filenames
                    relative to the script. */
-                exprs.push_back(state.parseExprFromFile(resolveExprPath(lookupFileArg(state,
-                    inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i))));
+                exprs.push_back(state.parseExprFromFile(resolveExprPath(state.checkSourcePath(lookupFileArg(state,
+                    inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i)))));
         }
 
     /* Evaluate them into derivations. */
diff --git a/src/nix-instantiate/nix-instantiate.cc b/src/nix-instantiate/nix-instantiate.cc
index 55ac007e8682..e05040a42deb 100644
--- a/src/nix-instantiate/nix-instantiate.cc
+++ b/src/nix-instantiate/nix-instantiate.cc
@@ -182,7 +182,7 @@ int main(int argc, char * * argv)
         for (auto & i : files) {
             Expr * e = fromArgs
                 ? state.parseExprFromString(i, absPath("."))
-                : state.parseExprFromFile(resolveExprPath(lookupFileArg(state, i)));
+                : state.parseExprFromFile(resolveExprPath(state.checkSourcePath(lookupFileArg(state, i))));
             processExpr(state, attrPaths, parseOnly, strict, autoArgs,
                 evalOnly, outputKind, xmlOutputSourceLocation, e);
         }
diff --git a/src/nix/eval.cc b/src/nix/eval.cc
index 0fbeca1c121d..b7058361cbec 100644
--- a/src/nix/eval.cc
+++ b/src/nix/eval.cc
@@ -5,10 +5,11 @@
 #include "eval.hh"
 #include "json.hh"
 #include "value-to-json.hh"
+#include "progress-bar.hh"
 
 using namespace nix;
 
-struct CmdEval : MixJSON, InstallablesCommand
+struct CmdEval : MixJSON, InstallableCommand
 {
     bool raw = false;
 
@@ -56,20 +57,19 @@ struct CmdEval : MixJSON, InstallablesCommand
 
         auto state = getEvalState();
 
-        auto jsonOut = json ? std::make_unique<JSONList>(std::cout) : nullptr;
+        auto v = installable->toValue(*state);
+        PathSet context;
 
-        for (auto & i : installables) {
-            auto v = i->toValue(*state);
-            PathSet context;
-            if (raw) {
-                std::cout << state->coerceToString(noPos, *v, context);
-            } else if (json) {
-                auto jsonElem = jsonOut->placeholder();
-                printValueAsJSON(*state, true, *v, jsonElem, context);
-            } else {
-                state->forceValueDeep(*v);
-                std::cout << *v << "\n";
-            }
+        stopProgressBar();
+
+        if (raw) {
+            std::cout << state->coerceToString(noPos, *v, context);
+        } else if (json) {
+            JSONPlaceholder jsonOut(std::cout);
+            printValueAsJSON(*state, true, *v, jsonOut, context);
+        } else {
+            state->forceValueDeep(*v);
+            std::cout << *v << "\n";
         }
     }
 };
diff --git a/src/nix/installables.cc b/src/nix/installables.cc
index ae93c4ef649e..c3b06c22eba8 100644
--- a/src/nix/installables.cc
+++ b/src/nix/installables.cc
@@ -30,10 +30,8 @@ Value * SourceExprCommand::getSourceExpr(EvalState & state)
 
     vSourceExpr = state.allocValue();
 
-    if (file != "") {
-        Expr * e = state.parseExprFromFile(resolveExprPath(lookupFileArg(state, file)));
-        state.eval(e, *vSourceExpr);
-    }
+    if (file != "")
+        state.evalFile(lookupFileArg(state, file), *vSourceExpr);
 
     else {
 
diff --git a/src/nix/log.cc b/src/nix/log.cc
index 966ad8b65087..f07ec4e93a16 100644
--- a/src/nix/log.cc
+++ b/src/nix/log.cc
@@ -50,6 +50,7 @@ struct CmdLog : InstallableCommand
 
         auto b = installable->toBuildable();
 
+        RunPager pager;
         for (auto & sub : subs) {
             auto log = b.drvPath != "" ? sub->getBuildLog(b.drvPath) : nullptr;
             for (auto & output : b.outputs) {
diff --git a/tests/fetchGit.sh b/tests/fetchGit.sh
index b556fe594ce4..530ac7bb813c 100644
--- a/tests/fetchGit.sh
+++ b/tests/fetchGit.sh
@@ -29,10 +29,17 @@ rev2=$(git -C $repo rev-parse HEAD)
 path=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
 [[ $(cat $path/hello) = world ]]
 
+# In pure eval mode, fetchGit without a revision should fail.
+[[ $(nix eval --raw "(builtins.readFile (fetchGit file://$repo + \"/hello\"))") = world ]]
+(! nix eval --pure-eval --raw "(builtins.readFile (fetchGit file://$repo + \"/hello\"))")
+
 # Fetch using an explicit revision hash.
 path2=$(nix eval --raw "(builtins.fetchGit { url = file://$repo; rev = \"$rev2\"; }).outPath")
 [[ $path = $path2 ]]
 
+# In pure eval mode, fetchGit with a revision should succeed.
+[[ $(nix eval --pure-eval --raw "(builtins.readFile (fetchGit { url = file://$repo; rev = \"$rev2\"; } + \"/hello\"))") = world ]]
+
 # Fetch again. This should be cached.
 mv $repo ${repo}-tmp
 path2=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
diff --git a/tests/fetchMercurial.sh b/tests/fetchMercurial.sh
index 271350ecd171..4088dbd39796 100644
--- a/tests/fetchMercurial.sh
+++ b/tests/fetchMercurial.sh
@@ -29,10 +29,17 @@ rev2=$(hg log --cwd $repo -r tip --template '{node}')
 path=$(nix eval --raw "(builtins.fetchMercurial file://$repo).outPath")
 [[ $(cat $path/hello) = world ]]
 
+# In pure eval mode, fetchGit without a revision should fail.
+[[ $(nix eval --raw "(builtins.readFile (fetchMercurial file://$repo + \"/hello\"))") = world ]]
+(! nix eval --pure-eval --raw "(builtins.readFile (fetchMercurial file://$repo + \"/hello\"))")
+
 # Fetch using an explicit revision hash.
 path2=$(nix eval --raw "(builtins.fetchMercurial { url = file://$repo; rev = \"$rev2\"; }).outPath")
 [[ $path = $path2 ]]
 
+# In pure eval mode, fetchGit with a revision should succeed.
+[[ $(nix eval --pure-eval --raw "(builtins.readFile (fetchMercurial { url = file://$repo; rev = \"$rev2\"; } + \"/hello\"))") = world ]]
+
 # Fetch again. This should be cached.
 mv $repo ${repo}-tmp
 path2=$(nix eval --raw "(builtins.fetchMercurial file://$repo).outPath")
diff --git a/tests/local.mk b/tests/local.mk
index 83154228e999..82502a8e5f0d 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -20,7 +20,8 @@ nix_tests = \
   fetchMercurial.sh \
   signing.sh \
   run.sh \
-  brotli.sh
+  brotli.sh \
+  pure-eval.sh
   # parallel.sh
 
 install-tests += $(foreach x, $(nix_tests), tests/$(x))
diff --git a/tests/pure-eval.nix b/tests/pure-eval.nix
new file mode 100644
index 000000000000..ed25b3d45637
--- /dev/null
+++ b/tests/pure-eval.nix
@@ -0,0 +1,3 @@
+{
+  x = 123;
+}
diff --git a/tests/pure-eval.sh b/tests/pure-eval.sh
new file mode 100644
index 000000000000..49c8564487c3
--- /dev/null
+++ b/tests/pure-eval.sh
@@ -0,0 +1,18 @@
+source common.sh
+
+clearStore
+
+nix eval --pure-eval '(assert 1 + 2 == 3; true)'
+
+[[ $(nix eval '(builtins.readFile ./pure-eval.sh)') =~ clearStore ]]
+
+(! nix eval --pure-eval '(builtins.readFile ./pure-eval.sh)')
+
+(! nix eval --pure-eval '(builtins.currentTime)')
+(! nix eval --pure-eval '(builtins.currentSystem)')
+
+(! nix-instantiate --pure-eval ./simple.nix)
+
+[[ $(nix eval "((import (builtins.fetchurl { url = file://$(pwd)/pure-eval.nix; })).x)") == 123 ]]
+(! nix eval --pure-eval "((import (builtins.fetchurl { url = file://$(pwd)/pure-eval.nix; })).x)")
+nix eval --pure-eval "((import (builtins.fetchurl { url = file://$(pwd)/pure-eval.nix; sha256 = \"$(nix hash-file pure-eval.nix --type sha256)\"; })).x)"
diff --git a/tests/restricted.nix b/tests/restricted.nix
new file mode 100644
index 000000000000..e0ef5840209c
--- /dev/null
+++ b/tests/restricted.nix
@@ -0,0 +1 @@
+1 + 2
diff --git a/tests/restricted.sh b/tests/restricted.sh
index c063c8693d55..6c0392facf31 100644
--- a/tests/restricted.sh
+++ b/tests/restricted.sh
@@ -3,7 +3,8 @@ source common.sh
 clearStore
 
 nix-instantiate --restrict-eval --eval -E '1 + 2'
-(! nix-instantiate --restrict-eval ./simple.nix)
+(! nix-instantiate --restrict-eval ./restricted.nix)
+(! nix-instantiate --eval --restrict-eval <(echo '1 + 2'))
 nix-instantiate --restrict-eval ./simple.nix -I src=.
 nix-instantiate --restrict-eval ./simple.nix -I src1=simple.nix -I src2=config.nix -I src3=./simple.builder.sh
 
@@ -28,3 +29,10 @@ nix eval --raw "(builtins.fetchurl file://$(pwd)/restricted.sh)" --restrict-eval
 (! nix eval --raw "(builtins.fetchurl https://github.com/NixOS/patchelf/archive/master.tar.gz)" --restrict-eval)
 (! nix eval --raw "(builtins.fetchTarball https://github.com/NixOS/patchelf/archive/master.tar.gz)" --restrict-eval)
 (! nix eval --raw "(fetchGit git://github.com/NixOS/patchelf.git)" --restrict-eval)
+
+ln -sfn $(pwd)/restricted.nix $TEST_ROOT/restricted.nix
+[[ $(nix-instantiate --eval $TEST_ROOT/restricted.nix) == 3 ]]
+(! nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix)
+(! nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I $TEST_ROOT)
+(! nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I .)
+nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I $TEST_ROOT -I .