lix/src/nix-build/nix-build.cc
Eelco Dolstra a7e55151a8
Fix #1314
Also, make nix-shell respect --option. (Previously it only passed it
along to nix-instantiate and nix-build.)
2017-05-24 11:33:42 +02:00

531 lines
19 KiB
C++
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <cstring>
#include <fstream>
#include <iostream>
#include <regex>
#include <sstream>
#include <vector>
#include <unistd.h>
#include "store-api.hh"
#include "globals.hh"
#include "derivations.hh"
#include "affinity.hh"
#include "util.hh"
#include "shared.hh"
using namespace nix;
using namespace std::string_literals;
extern char * * environ;
/* Recreate the effect of the perl shellwords function, breaking up a
* string into arguments like a shell word, including escapes
*/
std::vector<string> shellwords(const string & s)
{
std::regex whitespace("^(\\s+).*");
auto begin = s.cbegin();
std::vector<string> res;
std::string cur;
enum state {
sBegin,
sQuote
};
state st = sBegin;
auto it = begin;
for (; it != s.cend(); ++it) {
if (st == sBegin) {
std::smatch match;
if (regex_search(it, s.cend(), match, whitespace)) {
cur.append(begin, it);
res.push_back(cur);
cur.clear();
it = match[1].second;
begin = it;
}
}
switch (*it) {
case '"':
cur.append(begin, it);
begin = it + 1;
st = st == sBegin ? sQuote : sBegin;
break;
case '\\':
/* perl shellwords mostly just treats the next char as part of the string with no special processing */
cur.append(begin, it);
begin = ++it;
break;
}
}
cur.append(begin, it);
if (!cur.empty()) res.push_back(cur);
return res;
}
static void maybePrintExecError(ExecError & e)
{
if (WIFEXITED(e.status))
throw Exit(WEXITSTATUS(e.status));
else
throw e;
}
int main(int argc, char ** argv)
{
return handleExceptions(argv[0], [&]() {
initNix();
auto store = openStore();
auto dryRun = false;
auto verbose = false;
auto runEnv = std::regex_search(argv[0], std::regex("nix-shell$"));
auto pure = false;
auto fromArgs = false;
auto packages = false;
// Same condition as bash uses for interactive shells
auto interactive = isatty(STDIN_FILENO) && isatty(STDERR_FILENO);
Strings instArgs;
Strings buildArgs;
Strings exprs;
auto shell = getEnv("SHELL", "/bin/sh");
std::string envCommand; // interactive shell
Strings envExclude;
auto myName = runEnv ? "nix-shell" : "nix-build";
auto inShebang = false;
std::string script;
std::vector<string> savedArgs;
AutoDelete tmpDir(createTempDir("", myName));
std::string outLink = "./result";
auto drvLink = (Path) tmpDir + "/derivation";
std::vector<string> args;
for (int i = 1; i < argc; ++i)
args.push_back(argv[i]);
// Heuristic to see if we're invoked as a shebang script, namely, if we
// have a single argument, it's the name of an executable file, and it
// starts with "#!".
if (runEnv && argc > 1 && !std::regex_search(argv[1], std::regex("nix-shell"))) {
script = argv[1];
if (access(script.c_str(), F_OK) == 0 && access(script.c_str(), X_OK) == 0) {
auto lines = tokenizeString<Strings>(readFile(script), "\n");
if (std::regex_search(lines.front(), std::regex("^#!"))) {
lines.pop_front();
inShebang = true;
for (int i = 2; i < argc; ++i)
savedArgs.push_back(argv[i]);
args.clear();
for (auto line : lines) {
line = chomp(line);
std::smatch match;
if (std::regex_match(line, match, std::regex("^#!\\s*nix-shell (.*)$")))
for (const auto & word : shellwords(match[1].str()))
args.push_back(word);
}
}
}
}
for (size_t n = 0; n < args.size(); ++n) {
auto arg = args[n];
if (arg == "--help") {
deletePath(tmpDir);
showManPage(myName);
}
else if (arg == "--version")
printVersion(myName);
else if (arg == "--add-drv-link") {
drvLink = "./derivation";
}
else if (arg == "--no-out-link" || arg == "--no-link") {
outLink = (Path) tmpDir + "/result";
}
else if (arg == "--drv-link") {
n++;
if (n >= args.size()) {
throw UsageError("--drv-link requires an argument");
}
drvLink = args[n];
}
else if (arg == "--out-link" || arg == "-o") {
n++;
if (n >= args.size()) {
throw UsageError(format("%1% requires an argument") % arg);
}
outLink = args[n];
}
else if (arg == "--attr" || arg == "-A" || arg == "-I") {
n++;
if (n >= args.size()) {
throw UsageError(format("%1% requires an argument") % arg);
}
instArgs.push_back(arg);
instArgs.push_back(args[n]);
}
else if (arg == "--arg" || arg == "--argstr") {
if (n + 2 >= args.size()) {
throw UsageError(format("%1% requires two arguments") % arg);
}
instArgs.push_back(arg);
instArgs.push_back(args[n + 1]);
instArgs.push_back(args[n + 2]);
n += 2;
}
else if (arg == "--option") {
if (n + 2 >= args.size()) {
throw UsageError(format("%1% requires two arguments") % arg);
}
instArgs.push_back(arg);
instArgs.push_back(args[n + 1]);
instArgs.push_back(args[n + 2]);
buildArgs.push_back(arg);
buildArgs.push_back(args[n + 1]);
buildArgs.push_back(args[n + 2]);
settings.set(args[n + 1], args[n + 2]);
n += 2;
}
else if (arg == "--max-jobs" || arg == "-j" || arg == "--max-silent-time" || arg == "--cores" || arg == "--timeout" || arg == "--add-root") {
n++;
if (n >= args.size()) {
throw UsageError(format("%1% requires an argument") % arg);
}
buildArgs.push_back(arg);
buildArgs.push_back(args[n]);
}
else if (arg == "--dry-run") {
buildArgs.push_back("--dry-run");
dryRun = true;
}
else if (arg == "--show-trace") {
instArgs.push_back(arg);
}
else if (arg == "-") {
exprs = Strings{"-"};
}
else if (arg == "--verbose" || (arg.size() >= 2 && arg.substr(0, 2) == "-v")) {
buildArgs.push_back(arg);
instArgs.push_back(arg);
verbose = true;
}
else if (arg == "--quiet" || arg == "--repair") {
buildArgs.push_back(arg);
instArgs.push_back(arg);
}
else if (arg == "--check") {
buildArgs.push_back(arg);
}
else if (arg == "--run-env") { // obsolete
runEnv = true;
}
else if (arg == "--command" || arg == "--run") {
n++;
if (n >= args.size()) {
throw UsageError(format("%1% requires an argument") % arg);
}
envCommand = args[n] + "\nexit";
if (arg == "--run")
interactive = false;
}
else if (arg == "--exclude") {
n++;
if (n >= args.size()) {
throw UsageError(format("%1% requires an argument") % arg);
}
envExclude.push_back(args[n]);
}
else if (arg == "--pure") { pure = true; }
else if (arg == "--impure") { pure = false; }
else if (arg == "--expr" || arg == "-E") {
fromArgs = true;
instArgs.push_back("--expr");
}
else if (arg == "--packages" || arg == "-p") {
packages = true;
}
else if (inShebang && arg == "-i") {
n++;
if (n >= args.size()) {
throw UsageError(format("%1% requires an argument") % arg);
}
interactive = false;
auto interpreter = args[n];
auto execArgs = "";
auto shellEscape = [](const string & s) {
return "'" + std::regex_replace(s, std::regex("'"), "'\\''") + "'";
};
// Überhack to support Perl. Perl examines the shebang and
// executes it unless it contains the string "perl" or "indir",
// or (undocumented) argv[0] does not contain "perl". Exploit
// the latter by doing "exec -a".
if (std::regex_search(interpreter, std::regex("perl")))
execArgs = "-a PERL";
std::ostringstream joined;
for (const auto & i : savedArgs)
joined << shellEscape(i) << ' ';
if (std::regex_search(interpreter, std::regex("ruby"))) {
// Hack for Ruby. Ruby also examines the shebang. It tries to
// read the shebang to understand which packages to read from. Since
// this is handled via nix-shell -p, we wrap our ruby script execution
// in ruby -e 'load' which ignores the shebangs.
envCommand = (format("exec %1% %2% -e 'load(\"%3%\") -- %4%") % execArgs % interpreter % script % joined.str()).str();
} else {
envCommand = (format("exec %1% %2% %3% %4%") % execArgs % interpreter % script % joined.str()).str();
}
}
else if (!arg.empty() && arg[0] == '-') {
buildArgs.push_back(arg);
}
else if (arg == "-Q" || arg == "--no-build-output") {
buildArgs.push_back(arg);
instArgs.push_back(arg);
}
else {
exprs.push_back(arg);
}
}
if (packages && fromArgs) {
throw UsageError("-p and -E are mutually exclusive");
}
if (packages) {
instArgs.push_back("--expr");
std::ostringstream joined;
joined << "with import <nixpkgs> { }; (pkgs.runCommandCC or pkgs.runCommand) \"shell\" { buildInputs = [ ";
for (const auto & i : exprs)
joined << '(' << i << ") ";
joined << "]; } \"\"";
exprs = Strings{joined.str()};
} else if (!fromArgs) {
if (exprs.empty() && runEnv && access("shell.nix", F_OK) == 0)
exprs.push_back("shell.nix");
if (exprs.empty())
exprs.push_back("default.nix");
}
if (runEnv)
setenv("IN_NIX_SHELL", pure ? "pure" : "impure", 1);
for (auto & expr : exprs) {
// Instantiate.
std::vector<string> drvPaths;
if (!std::regex_match(expr, std::regex("^/.*\\.drv$"))) {
// If we're in a #! script, interpret filenames relative to the
// script.
if (inShebang && !packages)
expr = absPath(expr, dirOf(script));
Strings instantiateArgs{"--add-root", drvLink, "--indirect"};
for (const auto & arg : instArgs)
instantiateArgs.push_back(arg);
instantiateArgs.push_back(expr);
try {
auto instOutput = runProgram(settings.nixBinDir + "/nix-instantiate", false, instantiateArgs);
drvPaths = tokenizeString<std::vector<string>>(instOutput);
} catch (ExecError & e) {
maybePrintExecError(e);
}
} else {
drvPaths.push_back(expr);
}
if (runEnv) {
if (drvPaths.size() != 1)
throw UsageError("a single derivation is required");
auto drvPath = drvPaths[0];
drvPath = drvPath.substr(0, drvPath.find_first_of('!'));
if (isLink(drvPath))
drvPath = readLink(drvPath);
auto drv = store->derivationFromPath(drvPath);
// Build or fetch all dependencies of the derivation.
Strings nixStoreArgs{"-r", "--no-output", "--no-gc-warning"};
for (const auto & arg : buildArgs)
nixStoreArgs.push_back(arg);
for (const auto & input : drv.inputDrvs)
if (std::all_of(envExclude.cbegin(), envExclude.cend(), [&](const string & exclude) { return !std::regex_search(input.first, std::regex(exclude)); }))
nixStoreArgs.push_back(input.first);
for (const auto & src : drv.inputSrcs)
nixStoreArgs.push_back(src);
try {
runProgram(settings.nixBinDir + "/nix-store", false, nixStoreArgs);
} catch (ExecError & e) {
maybePrintExecError(e);
}
// Set the environment.
auto env = getEnv();
auto tmp = getEnv("TMPDIR", getEnv("XDG_RUNTIME_DIR", "/tmp"));
if (pure) {
std::set<string> keepVars{"HOME", "USER", "LOGNAME", "DISPLAY", "PATH", "TERM", "IN_NIX_SHELL", "TZ", "PAGER", "NIX_BUILD_SHELL", "SHLVL"};
decltype(env) newEnv;
for (auto & i : env)
if (keepVars.count(i.first))
newEnv.emplace(i);
env = newEnv;
// NixOS hack: prevent /etc/bashrc from sourcing /etc/profile.
env["__ETC_PROFILE_SOURCED"] = "1";
}
env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmp;
env["NIX_STORE"] = store->storeDir;
env["NIX_BUILD_CORES"] = std::to_string(settings.buildCores);
auto passAsFile = tokenizeString<StringSet>(get(drv.env, "passAsFile", ""));
bool keepTmp = false;
int fileNr = 0;
for (auto & var : drv.env)
if (passAsFile.count(var.first)) {
keepTmp = true;
string fn = ".attr-" + std::to_string(fileNr++);
Path p = (Path) tmpDir + "/" + fn;
writeFile(p, var.second);
env[var.first + "Path"] = p;
} else
env[var.first] = var.second;
restoreAffinity();
// Run a shell using the derivation's environment. For
// convenience, source $stdenv/setup to setup additional
// environment variables and shell functions. Also don't lose
// the current $PATH directories.
auto rcfile = (Path) tmpDir + "/rc";
writeFile(rcfile, fmt(
(keepTmp ? "" : "rm -rf '%1%'; "s) +
"[ -n \"$PS1\" ] && [ -e ~/.bashrc ] && source ~/.bashrc; "
"%2%"
"dontAddDisableDepTrack=1; "
"[ -e $stdenv/setup ] && source $stdenv/setup; "
"%3%"
"set +e; "
R"s([ -n "$PS1" ] && PS1='\n\[\033[1;32m\][nix-shell:\w]\$\[\033[0m\] '; )s"
"if [ \"$(type -t runHook)\" = function ]; then runHook shellHook; fi; "
"unset NIX_ENFORCE_PURITY; "
"unset NIX_INDENT_MAKE; "
"shopt -u nullglob; "
"unset TZ; %4%"
"%5%",
(Path) tmpDir,
(pure ? "" : "p=$PATH; "),
(pure ? "" : "PATH=$PATH:$p; unset p; "),
(getenv("TZ") ? (string("export TZ='") + getenv("TZ") + "'; ") : ""),
envCommand));
Strings envStrs;
for (auto & i : env)
envStrs.push_back(i.first + "=" + i.second);
auto args = interactive
? Strings{"bash", "--rcfile", rcfile}
: Strings{"bash", rcfile};
auto envPtrs = stringsToCharPtrs(envStrs);
auto shell = getEnv("NIX_BUILD_SHELL", "bash");
environ = envPtrs.data();
auto argPtrs = stringsToCharPtrs(args);
restoreSignals();
execvp(shell.c_str(), argPtrs.data());
throw SysError("executing shell %s", shell);
}
// Ugly hackery to make "nix-build -A foo.all" produce symlinks
// ./result, ./result-dev, and so on, rather than ./result,
// ./result-2-dev, and so on. This combines multiple derivation
// paths into one "/nix/store/drv-path!out1,out2,..." argument.
std::string prevDrvPath;
Strings drvPaths2;
for (const auto & drvPath : drvPaths) {
auto p = drvPath;
std::string output = "out";
std::smatch match;
if (std::regex_match(drvPath, match, std::regex("(.*)!(.*)"))) {
p = match[1].str();
output = match[2].str();
}
auto target = readLink(p);
if (verbose)
std::cerr << "derivation is " << target << '\n';
if (target == prevDrvPath) {
auto last = drvPaths2.back();
drvPaths2.pop_back();
drvPaths2.push_back(last + "," + output);
} else {
drvPaths2.push_back(target + "!" + output);
prevDrvPath = target;
}
}
// Build.
Strings outPaths;
Strings nixStoreArgs{"--add-root", outLink, "--indirect", "-r"};
for (const auto & arg : buildArgs)
nixStoreArgs.push_back(arg);
for (const auto & path : drvPaths2)
nixStoreArgs.push_back(path);
std::string nixStoreRes;
try {
nixStoreRes = runProgram(settings.nixBinDir + "/nix-store", false, nixStoreArgs);
} catch (ExecError & e) {
maybePrintExecError(e);
}
for (const auto & outpath : tokenizeString<std::vector<string>>(nixStoreRes))
outPaths.push_back(chomp(outpath));
if (dryRun)
continue;
for (const auto & outPath : outPaths)
std::cout << readLink(outPath) << '\n';
}
});
}