aboutsummaryrefslogtreecommitdiff
path: root/src/nix/develop.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/nix/develop.cc')
-rw-r--r--src/nix/develop.cc491
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");