about summary refs log tree commit diff
diff options
context:
space:
mode:
authorShea Levy <shea@shealevy.com>2017-03-30T12·04-0400
committerShea Levy <shea@shealevy.com>2017-03-30T12·04-0400
commit0bb8db257d98a32abde759f4d07d28b5178bd3bf (patch)
treeea00a7b109da40a2a187f2bda4cde3f92a6d4818
parentc60715e937e3773bbb8a114fc9b9c6577f8c5cb5 (diff)
Add exec primop behind allow-unsafe-native-code-during-evaluation.
Execute a given program with the (optional) given arguments as the
user running the evaluation, parsing stdout as an expression to be
evaluated.

There are many use cases for nix that would benefit from being able to
run arbitrary code during evaluation, including but not limited to:

* Automatic git fetching to get a sha256 from a git revision
* git rev-parse HEAD
* Automatic extraction of information from build specifications from
  other tools, particularly language-specific package managers like
  cabal or npm
* Secrets decryption (e.g. with nixops)
* Private repository fetching

Ideally, we would add this functionality in a more principled way to
nix, but in the mean time 'builtins.exec' can be used to get these
tasks done.

The primop is only available when the
'allow-unsafe-native-code-during-evaluation' nix option is true. That
flag also enables the 'importNative' primop, which is strictly more
powerful but less convenient (since it requires compiling a plugin
against the running version of nix).
-rw-r--r--src/libexpr/primops.cc56
-rw-r--r--src/libstore/globals.cc4
-rw-r--r--src/libstore/globals.hh4
3 files changed, 59 insertions, 5 deletions
diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc
index 93097f3d1bf3..a98da737e4a4 100644
--- a/src/libexpr/primops.cc
+++ b/src/libexpr/primops.cc
@@ -178,6 +178,58 @@ static void prim_importNative(EvalState & state, const Pos & pos, Value * * args
 }
 
 
+/* Execute a program and parse its output */
+static void prim_exec(EvalState & state, const Pos & pos, Value * * args, Value & v)
+{
+    state.forceAttrs(*args[0], pos);
+    auto sProgram = state.symbols.create("program");
+    auto sArguments = state.symbols.create("arguments");
+    PathSet context;
+    string program;
+    bool programSet = false;
+    Strings commandArgs;
+    for (auto & attr : *args[0]->attrs) {
+        if (attr.name == sProgram) {
+            program = state.coerceToString(*attr.pos, *attr.value, context, false, false);
+            programSet = true;
+        } else if (attr.name == sArguments) {
+            state.forceList(*attr.value, *attr.pos);
+            auto elems = attr.value->listElems();
+            for (unsigned int i = 0; i < attr.value->listSize(); ++i) {
+                commandArgs.emplace_back(state.coerceToString(*attr.pos, *elems[i], context, false, false));
+            }
+        } else {
+            throw EvalError(format("unexpected attribute ‘%1%’ in argument to builtins.exec, at %2%")
+                % attr.name % pos);
+        }
+    }
+    if (!programSet) {
+        throw EvalError(format("attribute ‘programSet’ required, at %1%") % pos);
+    }
+    try {
+        state.realiseContext(context);
+    } catch (InvalidPathError & e) {
+        throw EvalError(format("cannot execute ‘%1%’, since path ‘%2%’ is not valid, at %3%")
+            % program % e.path % pos);
+    }
+
+    auto output = runProgram(program, true, commandArgs);
+    Expr * parsed;
+    try {
+        parsed = state.parseExprFromString(output, pos.file);
+    } catch (Error & e) {
+        e.addPrefix(format("While parsing the output from ‘%1%’, at %2%\n") % program % pos);
+        throw;
+    }
+    try {
+        state.eval(parsed, v);
+    } catch (Error & e) {
+        e.addPrefix(format("While evaluating the output from ‘%1%’, at %2%\n") % program % pos);
+        throw;
+    }
+}
+
+
 /* Return a string representing the type of the expression. */
 static void prim_typeOf(EvalState & state, const Pos & pos, Value * * args, Value & v)
 {
@@ -1903,8 +1955,10 @@ void EvalState::createBaseEnv()
     mkApp(v, *baseEnv.values[baseEnvDispl - 1], *v2);
     forceValue(v);
     addConstant("import", v);
-    if (settings.enableImportNative)
+    if (settings.enableNativeCode) {
         addPrimOp("__importNative", 2, prim_importNative);
+        addPrimOp("__exec", 1, prim_exec);
+    }
     addPrimOp("__typeOf", 1, prim_typeOf);
     addPrimOp("isNull", 1, prim_isNull);
     addPrimOp("__isFunction", 1, prim_isFunction);
diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc
index 012b3d5b8b98..8c900be77b8f 100644
--- a/src/libstore/globals.cc
+++ b/src/libstore/globals.cc
@@ -67,7 +67,7 @@ Settings::Settings()
     envKeepDerivations = false;
     lockCPU = getEnv("NIX_AFFINITY_HACK", "1") == "1";
     showTrace = false;
-    enableImportNative = false;
+    enableNativeCode = false;
     netrcFile = fmt("%s/%s", nixConfDir, "netrc");
     caFile = getEnv("NIX_SSL_CERT_FILE", getEnv("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"));
     enableImportFromDerivation = true;
@@ -179,7 +179,7 @@ void Settings::update()
     _get(envKeepDerivations, "env-keep-derivations");
     _get(sshSubstituterHosts, "ssh-substituter-hosts");
     _get(useSshSubstituter, "use-ssh-substituter");
-    _get(enableImportNative, "allow-unsafe-native-code-during-evaluation");
+    _get(enableNativeCode, "allow-unsafe-native-code-during-evaluation");
     _get(useCaseHack, "use-case-hack");
     _get(preBuildHook, "pre-build-hook");
     _get(keepGoing, "keep-going");
diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh
index 462721681912..ccec300f776e 100644
--- a/src/libstore/globals.hh
+++ b/src/libstore/globals.hh
@@ -181,8 +181,8 @@ struct Settings {
     /* Whether to show a stack trace if Nix evaluation fails. */
     bool showTrace;
 
-    /* Whether the importNative primop should be enabled */
-    bool enableImportNative;
+    /* Whether native-code enabling primops should be enabled */
+    bool enableNativeCode;
 
     /* The hook to run just before a build to set derivation-specific
        build settings */