#include "eval.hh" #include "command.hh" #include "common-args.hh" #include "shared.hh" #include "store-api.hh" #include "derivations.hh" #include "affinity.hh" #include "progress-bar.hh" #include using namespace nix; struct Var { bool exported = true; bool associative = false; std::string value; // quoted string or array }; struct BuildEnvironment { std::map env; std::string bashFunctions; }; BuildEnvironment readEnvironment(const Path & path) { BuildEnvironment res; std::set exported; debug("reading environment file '%s'", path); auto file = readFile(path); auto pos = file.cbegin(); static std::string varNameRegex = R"re((?:[a-zA-Z_][a-zA-Z0-9_]*))re"; static std::regex declareRegex( "^declare -x (" + varNameRegex + ")" + R"re((?:="((?:[^"\\]|\\.)*)")?\n)re"); static std::string simpleStringRegex = R"re((?:[a-zA-Z0-9_/:\.\-\+=]*))re"; static std::string quotedStringRegex = R"re((?:\$?'(?:[^'\\]|\\[abeEfnrtv\\'"?])*'))re"; static std::string indexedArrayRegex = R"re((?:\(( *\[[0-9]+]="(?:[^"\\]|\\.)*")**\)))re"; static std::regex varRegex( "^(" + varNameRegex + ")=(" + simpleStringRegex + "|" + quotedStringRegex + "|" + indexedArrayRegex + ")\n"); /* Note: we distinguish between an indexed and associative array using the space before the closing parenthesis. Will undoubtedly regret this some day. */ static std::regex assocArrayRegex( "^(" + varNameRegex + ")=" + R"re((?:\(( *\[[^\]]+\]="(?:[^"\\]|\\.)*")* *\)))re" + "\n"); static std::regex functionRegex( "^" + varNameRegex + " \\(\\) *\n"); while (pos != file.end()) { std::smatch match; if (std::regex_search(pos, file.cend(), match, declareRegex)) { pos = match[0].second; exported.insert(match[1]); } else if (std::regex_search(pos, file.cend(), match, varRegex)) { pos = match[0].second; res.env.insert({match[1], Var { .exported = exported.count(match[1]) > 0, .value = match[2] }}); } else if (std::regex_search(pos, file.cend(), match, assocArrayRegex)) { pos = match[0].second; res.env.insert({match[1], Var { .associative = true, .value = match[2] }}); } else if (std::regex_search(pos, file.cend(), match, functionRegex)) { res.bashFunctions = std::string(pos, file.cend()); break; } else throw Error("shell environment '%s' has unexpected line '%s'", path, file.substr(pos - file.cbegin(), 60)); } return res; } const static std::string getEnvSh = #include "get-env.sh.gen.hh" ; /* Given an existing derivation, return the shell environment as initialised by stdenv's setup script. We do this by building a modified derivation with the same dependencies and nearly the same initial environment variables, that just writes the resulting environment to a file and exits. */ StorePath getDerivationEnvironment(ref store, const StorePath & drvPath) { auto drv = store->derivationFromPath(drvPath); auto builder = baseNameOf(drv.builder); if (builder != "bash") throw Error("'nix dev-shell' only works on derivations that use 'bash' as their builder"); auto getEnvShPath = store->addTextToStore("get-env.sh", getEnvSh, {}); drv.args = {store->printStorePath(getEnvShPath)}; /* Remove derivation checks. */ drv.env.erase("allowedReferences"); drv.env.erase("allowedRequisites"); drv.env.erase("disallowedReferences"); drv.env.erase("disallowedRequisites"); /* Rehash and write the derivation. FIXME: would be nice to use 'buildDerivation', but that's privileged. */ auto drvName = std::string(drvPath.name()); assert(hasSuffix(drvName, ".drv")); drvName.resize(drvName.size() - 4); drvName += "-env"; for (auto & output : drv.outputs) drv.env.erase(output.first); drv.env["out"] = ""; drv.env["outputs"] = "out"; drv.inputSrcs.insert(std::move(getEnvShPath)); Hash h = hashDerivationModulo(*store, drv, true); auto shellOutPath = store->makeOutputPath("out", h, drvName); drv.outputs.insert_or_assign("out", DerivationOutput(shellOutPath.clone(), "", "")); drv.env["out"] = store->printStorePath(shellOutPath); auto shellDrvPath2 = writeDerivation(store, drv, drvName); /* Build the derivation. */ store->buildPaths({shellDrvPath2}); assert(store->isValidPath(shellOutPath)); return shellOutPath; } struct Common : InstallableCommand, MixProfile { std::set ignoreVars{ "BASHOPTS", "EUID", "HOME", // FIXME: don't ignore in pure mode? "NIX_BUILD_TOP", "NIX_ENFORCE_PURITY", "NIX_LOG_FD", "PPID", "PWD", "SHELLOPTS", "SHLVL", "SSL_CERT_FILE", // FIXME: only want to ignore /no-cert-file.crt "TEMP", "TEMPDIR", "TERM", "TMP", "TMPDIR", "TZ", "UID", }; void makeRcScript(const BuildEnvironment & buildEnvironment, std::ostream & out) { out << "unset shellHook\n"; out << "nix_saved_PATH=\"$PATH\"\n"; for (auto & i : buildEnvironment.env) { if (!ignoreVars.count(i.first) && !hasPrefix(i.first, "BASH_")) { if (i.second.associative) out << fmt("declare -A %s=(%s)\n", i.first, i.second.value); else { out << fmt("%s=%s\n", i.first, i.second.value); if (i.second.exported) out << fmt("export %s\n", i.first); } } } out << "PATH=\"$PATH:$nix_saved_PATH\"\n"; out << buildEnvironment.bashFunctions << "\n"; // FIXME: set outputs out << "export NIX_BUILD_TOP=\"$(mktemp -d --tmpdir nix-shell.XXXXXX)\"\n"; for (auto & i : {"TMP", "TMPDIR", "TEMP", "TEMPDIR"}) out << fmt("export %s=\"$NIX_BUILD_TOP\"\n", i); out << "eval \"$shellHook\"\n"; } StorePath getShellOutPath(ref store) { auto path = installable->getStorePath(); if (path && hasSuffix(path->to_string(), "-env")) return path->clone(); else { auto drvs = toDerivations(store, {installable}); if (drvs.size() != 1) throw Error("'%s' needs to evaluate to a single derivation, but it evaluated to %d derivations", installable->what(), drvs.size()); auto & drvPath = *drvs.begin(); return getDerivationEnvironment(store, drvPath); } } std::pair getBuildEnvironment(ref store) { auto shellOutPath = getShellOutPath(store); auto strPath = store->printStorePath(shellOutPath); updateProfile(shellOutPath); return {readEnvironment(strPath), strPath}; } }; struct CmdDevShell : Common, MixEnvironment { std::vector command; CmdDevShell() { addFlag({ .longName = "command", .shortName = 'c', .description = "command and arguments to be executed insted of an interactive shell", .labels = {"command", "args"}, .handler = {[&](std::vector ss) { if (ss.empty()) throw UsageError("--command requires at least one argument"); command = ss; }} }); } std::string description() override { return "run a bash shell that provides the build environment of a derivation"; } Examples examples() override { return { Example{ "To get the build environment of GNU hello:", "nix dev-shell nixpkgs.hello" }, Example{ "To store the build environment in a profile:", "nix dev-shell --profile /tmp/my-shell nixpkgs.hello" }, Example{ "To use a build environment previously recorded in a profile:", "nix dev-shell /tmp/my-shell" }, }; } void run(ref store) override { auto [buildEnvironment, gcroot] = getBuildEnvironment(store); auto [rcFileFd, rcFilePath] = createTempFile("nix-shell"); std::ostringstream ss; makeRcScript(buildEnvironment, ss); ss << fmt("rm -f '%s'\n", rcFilePath); if (!command.empty()) { std::vector args; for (auto s : command) args.push_back(shellEscape(s)); ss << fmt("exec %s\n", concatStringsSep(" ", args)); } writeFull(rcFileFd.get(), ss.str()); stopProgressBar(); auto shell = getEnv("SHELL").value_or("bash"); setEnviron(); // prevent garbage collection until shell exits setenv("NIX_GCROOT", gcroot.data(), 1); auto args = Strings{std::string(baseNameOf(shell)), "--rcfile", rcFilePath}; restoreAffinity(); restoreSignals(); execvp(shell.c_str(), stringsToCharPtrs(args).data()); throw SysError("executing shell '%s'", shell); } }; struct CmdPrintDevEnv : Common { std::string description() override { return "print shell code that can be sourced by bash to reproduce the build environment of a derivation"; } Examples examples() override { return { Example{ "To apply the build environment of GNU hello to the current shell:", ". <(nix print-dev-env nixpkgs.hello)" }, }; } void run(ref store) override { auto buildEnvironment = getBuildEnvironment(store).first; stopProgressBar(); makeRcScript(buildEnvironment, std::cout); } }; static auto r1 = registerCommand("print-dev-env"); static auto r2 = registerCommand("dev-shell");