diff options
Diffstat (limited to 'src/nix/develop.cc')
-rw-r--r-- | src/nix/develop.cc | 491 |
1 files changed, 352 insertions, 139 deletions
diff --git a/src/nix/develop.cc b/src/nix/develop.cc index eb93f56fc..c20b9f272 100644 --- a/src/nix/develop.cc +++ b/src/nix/develop.cc @@ -3,97 +3,166 @@ #include "common-args.hh" #include "shared.hh" #include "store-api.hh" +#include "path-with-outputs.hh" #include "derivations.hh" #include "affinity.hh" #include "progress-bar.hh" +#include "run.hh" -#include <regex> +#include <memory> +#include <nlohmann/json.hpp> using namespace nix; -struct Var +struct DevelopSettings : Config { - bool exported = true; - bool associative = false; - std::string value; // quoted string or array + Setting<std::string> bashPrompt{this, "", "bash-prompt", + "The bash prompt (`PS1`) in `nix develop` shells."}; + + Setting<std::string> bashPromptSuffix{this, "", "bash-prompt-suffix", + "Suffix appended to the `PS1` environment variable in `nix develop` shells."}; }; +static DevelopSettings developSettings; + +static GlobalConfig::Register rDevelopSettings(&developSettings); + struct BuildEnvironment { - std::map<std::string, Var> env; - std::string bashFunctions; -}; + struct String + { + bool exported; + std::string value; -BuildEnvironment readEnvironment(const Path & path) -{ - BuildEnvironment res; + bool operator == (const String & other) const + { + return exported == other.exported && value == other.value; + } + }; - std::set<std::string> exported; + using Array = std::vector<std::string>; - debug("reading environment file '%s'", path); + using Associative = std::map<std::string, std::string>; - auto file = readFile(path); + using Value = std::variant<String, Array, Associative>; - auto pos = file.cbegin(); + std::map<std::string, Value> vars; + std::map<std::string, std::string> bashFunctions; - static std::string varNameRegex = - R"re((?:[a-zA-Z_][a-zA-Z0-9_]*))re"; + static BuildEnvironment fromJSON(std::string_view in) + { + BuildEnvironment res; - static std::regex declareRegex( - "^declare -x (" + varNameRegex + ")" + - R"re((?:="((?:[^"\\]|\\.)*)")?\n)re"); + std::set<std::string> exported; - static std::string simpleStringRegex = - R"re((?:[a-zA-Z0-9_/:\.\-\+=]*))re"; + auto json = nlohmann::json::parse(in); - static std::string quotedStringRegex = - R"re((?:\$?'(?:[^'\\]|\\[abeEfnrtv\\'"?])*'))re"; + for (auto & [name, info] : json["variables"].items()) { + std::string type = info["type"]; + if (type == "var" || type == "exported") + res.vars.insert({name, BuildEnvironment::String { .exported = type == "exported", .value = info["value"] }}); + else if (type == "array") + res.vars.insert({name, (Array) info["value"]}); + else if (type == "associative") + res.vars.insert({name, (Associative) info["value"]}); + } - static std::string indexedArrayRegex = - R"re((?:\(( *\[[0-9]+\]="(?:[^"\\]|\\.)*")*\)))re"; + for (auto & [name, def] : json["bashFunctions"].items()) { + res.bashFunctions.insert({name, def}); + } - static std::regex varRegex( - "^(" + varNameRegex + ")=(" + simpleStringRegex + "|" + quotedStringRegex + "|" + indexedArrayRegex + ")\n"); + return res; + } - /* 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"); + std::string toJSON() const + { + auto res = nlohmann::json::object(); + + auto vars2 = nlohmann::json::object(); + for (auto & [name, value] : vars) { + auto info = nlohmann::json::object(); + if (auto str = std::get_if<String>(&value)) { + info["type"] = str->exported ? "exported" : "var"; + info["value"] = str->value; + } + else if (auto arr = std::get_if<Array>(&value)) { + info["type"] = "array"; + info["value"] = *arr; + } + else if (auto arr = std::get_if<Associative>(&value)) { + info["type"] = "associative"; + info["value"] = *arr; + } + vars2[name] = std::move(info); + } + res["variables"] = std::move(vars2); - static std::regex functionRegex( - "^" + varNameRegex + " \\(\\) *\n"); + res["bashFunctions"] = bashFunctions; - while (pos != file.end()) { + auto json = res.dump(); - std::smatch match; + assert(BuildEnvironment::fromJSON(json) == *this); - if (std::regex_search(pos, file.cend(), match, declareRegex)) { - pos = match[0].second; - exported.insert(match[1]); - } + return json; + } - 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] }}); + void toBash(std::ostream & out, const std::set<std::string> & ignoreVars) const + { + for (auto & [name, value] : vars) { + if (!ignoreVars.count(name)) { + if (auto str = std::get_if<String>(&value)) { + out << fmt("%s=%s\n", name, shellEscape(str->value)); + if (str->exported) + out << fmt("export %s\n", name); + } + else if (auto arr = std::get_if<Array>(&value)) { + out << "declare -a " << name << "=("; + for (auto & s : *arr) + out << shellEscape(s) << " "; + out << ")\n"; + } + else if (auto arr = std::get_if<Associative>(&value)) { + out << "declare -A " << name << "=("; + for (auto & [n, v] : *arr) + out << "[" << shellEscape(n) << "]=" << shellEscape(v) << " "; + out << ")\n"; + } + } } - 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] }}); + for (auto & [name, def] : bashFunctions) { + out << name << " ()\n{\n" << def << "}\n"; } + } - else if (std::regex_search(pos, file.cend(), match, functionRegex)) { - res.bashFunctions = std::string(pos, file.cend()); - break; - } + static std::string getString(const Value & value) + { + if (auto str = std::get_if<String>(&value)) + return str->value; + else + throw Error("bash variable is not a string"); + } - else throw Error("shell environment '%s' has unexpected line '%s'", - path, file.substr(pos - file.cbegin(), 60)); + static Array getStrings(const Value & value) + { + if (auto str = std::get_if<String>(&value)) + return tokenizeString<Array>(str->value); + else if (auto arr = std::get_if<Array>(&value)) { + return *arr; + } else if (auto assoc = std::get_if<Associative>(&value)) { + Array assocKeys; + std::for_each(assoc->begin(), assoc->end(), [&](auto & n) { assocKeys.push_back(n.first); }); + return assocKeys; + } + else + throw Error("bash variable is not a string or array"); } - return res; -} + bool operator == (const BuildEnvironment & other) const + { + return vars == other.vars && bashFunctions == other.bashFunctions; + } +}; const static std::string getEnvSh = #include "get-env.sh.gen.hh" @@ -104,15 +173,15 @@ const static std::string getEnvSh = 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> store, const StorePath & drvPath) +static StorePath getDerivationEnvironment(ref<Store> store, ref<Store> evalStore, const StorePath & drvPath) { - auto drv = store->derivationFromPath(drvPath); + auto drv = evalStore->derivationFromPath(drvPath); auto builder = baseNameOf(drv.builder); if (builder != "bash") throw Error("'nix develop' only works on derivations that use 'bash' as their builder"); - auto getEnvShPath = store->addTextToStore("get-env.sh", getEnvSh, {}); + auto getEnvShPath = evalStore->addTextToStore("get-env.sh", getEnvSh, {}); drv.args = {store->printStorePath(getEnvShPath)}; @@ -124,42 +193,57 @@ StorePath getDerivationEnvironment(ref<Store> store, const StorePath & drvPath) /* 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.name += "-env"; 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 { .path = shellOutPath }); - drv.env["out"] = store->printStorePath(shellOutPath); - auto shellDrvPath2 = writeDerivation(store, drv, drvName); + if (settings.isExperimentalFeatureEnabled("ca-derivations")) { + for (auto & output : drv.outputs) { + output.second = { + .output = DerivationOutputDeferred{}, + }; + drv.env[output.first] = hashPlaceholder(output.first); + } + } else { + for (auto & output : drv.outputs) { + output.second = { .output = DerivationOutputInputAddressed { .path = StorePath::dummy } }; + drv.env[output.first] = ""; + } + Hash h = std::get<0>(hashDerivationModulo(*evalStore, drv, true)); - /* Build the derivation. */ - store->buildPaths({{shellDrvPath2}}); + for (auto & output : drv.outputs) { + auto outPath = store->makeOutputPath(output.first, h, drv.name); + output.second = { .output = DerivationOutputInputAddressed { .path = outPath } }; + drv.env[output.first] = store->printStorePath(outPath); + } + } - assert(store->isValidPath(shellOutPath)); + auto shellDrvPath = writeDerivation(*evalStore, drv); - return shellOutPath; + /* Build the derivation. */ + store->buildPaths({DerivedPath::Built{shellDrvPath}}, bmNormal, evalStore); + + for (auto & [_0, optPath] : evalStore->queryPartialDerivationOutputMap(shellDrvPath)) { + assert(optPath); + auto & outPath = *optPath; + assert(store->isValidPath(outPath)); + auto outPathS = store->toRealPath(outPath); + if (lstat(outPathS).st_size) + return outPath; + } + + throw Error("get-env.sh failed to produce an environment"); } struct Common : InstallableCommand, MixProfile { - std::set<string> ignoreVars{ + std::set<std::string> ignoreVars{ "BASHOPTS", - "EUID", "HOME", // FIXME: don't ignore in pure mode? "NIX_BUILD_TOP", "NIX_ENFORCE_PURITY", "NIX_LOG_FD", + "NIX_REMOTE", "PPID", - "PWD", "SHELLOPTS", - "SHLVL", "SSL_CERT_FILE", // FIXME: only want to ignore /no-cert-file.crt "TEMP", "TEMPDIR", @@ -170,35 +254,85 @@ struct Common : InstallableCommand, MixProfile "UID", }; - void makeRcScript(const BuildEnvironment & buildEnvironment, std::ostream & out) + std::vector<std::pair<std::string, std::string>> redirects; + + Common() { + addFlag({ + .longName = "redirect", + .description = "Redirect a store path to a mutable location.", + .labels = {"installable", "outputs-dir"}, + .handler = {[&](std::string installable, std::string outputsDir) { + redirects.push_back({installable, outputsDir}); + }} + }); + } + + std::string makeRcScript( + ref<Store> store, + const BuildEnvironment & buildEnvironment, + const Path & outputsDir = absPath(".") + "/outputs") + { + std::ostringstream 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); - } - } - } + buildEnvironment.toBash(out, ignoreVars); 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"; + out << "export NIX_BUILD_TOP=\"$(mktemp -d -t nix-shell.XXXXXX)\"\n"; for (auto & i : {"TMP", "TMPDIR", "TEMP", "TEMPDIR"}) out << fmt("export %s=\"$NIX_BUILD_TOP\"\n", i); out << "eval \"$shellHook\"\n"; + + auto script = out.str(); + + /* Substitute occurrences of output paths. */ + auto outputs = buildEnvironment.vars.find("outputs"); + assert(outputs != buildEnvironment.vars.end()); + + // FIXME: properly unquote 'outputs'. + StringMap rewrites; + for (auto & outputName : BuildEnvironment::getStrings(outputs->second)) { + auto from = buildEnvironment.vars.find(outputName); + assert(from != buildEnvironment.vars.end()); + // FIXME: unquote + rewrites.insert({BuildEnvironment::getString(from->second), outputsDir + "/" + outputName}); + } + + /* Substitute redirects. */ + for (auto & [installable_, dir_] : redirects) { + auto dir = absPath(dir_); + auto installable = parseInstallable(store, installable_); + auto builtPaths = toStorePaths( + getEvalStore(), store, Realise::Nothing, OperateOn::Output, {installable}); + for (auto & path: builtPaths) { + auto from = store->printStorePath(path); + if (script.find(from) == std::string::npos) + warn("'%s' (path '%s') is not used by this build environment", installable->what(), from); + else { + printInfo("redirecting '%s' to '%s'", from, dir); + rewrites.insert({from, dir}); + } + } + } + + return rewriteStrings(script, rewrites); + } + + Strings getDefaultFlakeAttrPaths() override + { + return {"devShell." + settings.thisSystem.get(), "defaultPackage." + settings.thisSystem.get()}; + } + Strings getDefaultFlakeAttrPathPrefixes() override + { + auto res = SourceExprCommand::getDefaultFlakeAttrPathPrefixes(); + res.emplace_front("devShells." + settings.thisSystem.get() + "."); + return res; } StorePath getShellOutPath(ref<Store> store) @@ -215,7 +349,7 @@ struct Common : InstallableCommand, MixProfile auto & drvPath = *drvs.begin(); - return getDerivationEnvironment(store, drvPath); + return getDerivationEnvironment(store, getEvalStore(), drvPath); } } @@ -227,26 +361,66 @@ struct Common : InstallableCommand, MixProfile updateProfile(shellOutPath); - return {readEnvironment(strPath), strPath}; + debug("reading environment file '%s'", strPath); + + return {BuildEnvironment::fromJSON(readFile(store->toRealPath(shellOutPath))), strPath}; } }; struct CmdDevelop : Common, MixEnvironment { std::vector<std::string> command; + std::optional<std::string> phase; CmdDevelop() { addFlag({ .longName = "command", .shortName = 'c', - .description = "command and arguments to be executed insted of an interactive shell", + .description = "Instead of starting an interactive shell, start the specified command and arguments.", .labels = {"command", "args"}, .handler = {[&](std::vector<std::string> ss) { if (ss.empty()) throw UsageError("--command requires at least one argument"); command = ss; }} }); + + addFlag({ + .longName = "phase", + .description = "The stdenv phase to run (e.g. `build` or `configure`).", + .labels = {"phase-name"}, + .handler = {&phase}, + }); + + addFlag({ + .longName = "configure", + .description = "Run the `configure` phase.", + .handler = {&phase, {"configure"}}, + }); + + addFlag({ + .longName = "build", + .description = "Run the `build` phase.", + .handler = {&phase, {"build"}}, + }); + + addFlag({ + .longName = "check", + .description = "Run the `check` phase.", + .handler = {&phase, {"check"}}, + }); + + addFlag({ + .longName = "install", + .description = "Run the `install` phase.", + .handler = {&phase, {"install"}}, + }); + + addFlag({ + .longName = "installcheck", + .description = "Run the `installcheck` phase.", + .handler = {&phase, {"installCheck"}}, + }); } std::string description() override @@ -254,22 +428,11 @@ struct CmdDevelop : Common, MixEnvironment return "run a bash shell that provides the build environment of a derivation"; } - Examples examples() override + std::string doc() override { - return { - Example{ - "To get the build environment of GNU hello:", - "nix develop nixpkgs.hello" - }, - Example{ - "To store the build environment in a profile:", - "nix develop --profile /tmp/my-shell nixpkgs.hello" - }, - Example{ - "To use a build environment previously recorded in a profile:", - "nix develop /tmp/my-shell" - }, - }; + return + #include "develop.md" + ; } void run(ref<Store> store) override @@ -278,54 +441,101 @@ struct CmdDevelop : Common, MixEnvironment auto [rcFileFd, rcFilePath] = createTempFile("nix-shell"); - std::ostringstream ss; - makeRcScript(buildEnvironment, ss); + auto script = makeRcScript(store, buildEnvironment); + + if (verbosity >= lvlDebug) + script += "set -x\n"; - ss << fmt("rm -f '%s'\n", rcFilePath); + script += fmt("command rm -f '%s'\n", rcFilePath); - if (!command.empty()) { + if (phase) { + if (!command.empty()) + throw UsageError("you cannot use both '--command' and '--phase'"); + // FIXME: foundMakefile is set by buildPhase, need to get + // rid of that. + script += fmt("foundMakefile=1\n"); + script += fmt("runHook %1%Phase\n", *phase); + } + + else if (!command.empty()) { std::vector<std::string> args; for (auto s : command) args.push_back(shellEscape(s)); - ss << fmt("exec %s\n", concatStringsSep(" ", args)); + script += fmt("exec %s\n", concatStringsSep(" ", args)); } - writeFull(rcFileFd.get(), ss.str()); - - stopProgressBar(); + else { + script = "[ -n \"$PS1\" ] && [ -e ~/.bashrc ] && source ~/.bashrc;\n" + script; + if (developSettings.bashPrompt != "") + script += fmt("[ -n \"$PS1\" ] && PS1=%s;\n", shellEscape(developSettings.bashPrompt)); + if (developSettings.bashPromptSuffix != "") + script += fmt("[ -n \"$PS1\" ] && PS1+=%s;\n", shellEscape(developSettings.bashPromptSuffix)); + } - auto shell = getEnv("SHELL").value_or("bash"); + writeFull(rcFileFd.get(), script); setEnviron(); // prevent garbage collection until shell exits setenv("NIX_GCROOT", gcroot.data(), 1); - auto args = Strings{std::string(baseNameOf(shell)), "--rcfile", rcFilePath}; + Path shell = "bash"; + + try { + auto state = getEvalState(); - restoreAffinity(); - restoreSignals(); + auto nixpkgsLockFlags = lockFlags; + nixpkgsLockFlags.inputOverrides = {}; + nixpkgsLockFlags.inputUpdates = {}; - execvp(shell.c_str(), stringsToCharPtrs(args).data()); + auto bashInstallable = std::make_shared<InstallableFlake>( + this, + state, + installable->nixpkgsFlakeRef(), + Strings{"bashInteractive"}, + Strings{"legacyPackages." + settings.thisSystem.get() + "."}, + nixpkgsLockFlags); + + shell = store->printStorePath( + toStorePath(getEvalStore(), store, Realise::Outputs, OperateOn::Output, bashInstallable)) + "/bin/bash"; + } catch (Error &) { + ignoreException(); + } + + // If running a phase or single command, don't want an interactive shell running after + // Ctrl-C, so don't pass --rcfile + auto args = phase || !command.empty() ? Strings{std::string(baseNameOf(shell)), rcFilePath} + : Strings{std::string(baseNameOf(shell)), "--rcfile", rcFilePath}; + + // Need to chdir since phases assume in flake directory + if (phase) { + // chdir if installable is a flake of type git+file or path + auto installableFlake = std::dynamic_pointer_cast<InstallableFlake>(installable); + if (installableFlake) { + auto sourcePath = installableFlake->getLockedFlake()->flake.resolvedRef.input.getSourcePath(); + if (sourcePath) { + if (chdir(sourcePath->c_str()) == -1) { + throw SysError("chdir to '%s' failed", *sourcePath); + } + } + } + } - throw SysError("executing shell '%s'", shell); + runProgramInStore(store, shell, args); } }; -struct CmdPrintDevEnv : Common +struct CmdPrintDevEnv : Common, MixJSON { 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 + std::string doc() override { - return { - Example{ - "To apply the build environment of GNU hello to the current shell:", - ". <(nix print-dev-env nixpkgs.hello)" - }, - }; + return + #include "print-dev-env.md" + ; } Category category() override { return catUtility; } @@ -336,9 +546,12 @@ struct CmdPrintDevEnv : Common stopProgressBar(); - makeRcScript(buildEnvironment, std::cout); + logger->writeToStdout( + json + ? buildEnvironment.toJSON() + : makeRcScript(store, buildEnvironment)); } }; -static auto r1 = registerCommand<CmdPrintDevEnv>("print-dev-env"); -static auto r2 = registerCommand<CmdDevelop>("develop"); +static auto rCmdPrintDevEnv = registerCommand<CmdPrintDevEnv>("print-dev-env"); +static auto rCmdDevelop = registerCommand<CmdDevelop>("develop"); |