* Made the build hook mechanism more efficient. Rather than starting
the hook every time we want to ask whether we can run a remote build (which can be very often), we now reuse a hook process for answering those queries until it accepts a build. So if there are N derivations to be built, at most N hooks will be started.
This commit is contained in:
parent
1a396f3789
commit
e437b08250
|
@ -31,57 +31,20 @@ $ENV{"DISPLAY"} = "";
|
||||||
$ENV{"SSH_ASKPASS"} = "";
|
$ENV{"SSH_ASKPASS"} = "";
|
||||||
|
|
||||||
|
|
||||||
my $loadIncreased = 0;
|
|
||||||
|
|
||||||
my ($amWilling, $localSystem, $neededSystem, $drvPath, $maxSilentTime) = @ARGV;
|
|
||||||
$maxSilentTime = 0 unless defined $maxSilentTime;
|
|
||||||
|
|
||||||
sub sendReply {
|
sub sendReply {
|
||||||
my $reply = shift;
|
my $reply = shift;
|
||||||
print STDERR "# $reply\n";
|
print STDERR "# $reply\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
sub decline {
|
|
||||||
sendReply "decline";
|
# Initialisation.
|
||||||
exit 0;
|
my $loadIncreased = 0;
|
||||||
}
|
|
||||||
|
my ($localSystem, $maxSilentTime) = @ARGV;
|
||||||
|
$maxSilentTime = 0 unless defined $maxSilentTime;
|
||||||
|
|
||||||
my $currentLoad = $ENV{"NIX_CURRENT_LOAD"};
|
my $currentLoad = $ENV{"NIX_CURRENT_LOAD"};
|
||||||
decline unless defined $currentLoad;
|
|
||||||
mkdir $currentLoad, 0777 or die unless -d $currentLoad;
|
|
||||||
|
|
||||||
my $conf = $ENV{"NIX_REMOTE_SYSTEMS"};
|
my $conf = $ENV{"NIX_REMOTE_SYSTEMS"};
|
||||||
decline if !defined $conf || ! -e $conf;
|
|
||||||
|
|
||||||
my $canBuildLocally = $amWilling && ($localSystem eq $neededSystem);
|
|
||||||
|
|
||||||
|
|
||||||
# Read the list of machines.
|
|
||||||
my @machines;
|
|
||||||
open CONF, "< $conf" or die;
|
|
||||||
|
|
||||||
while (<CONF>) {
|
|
||||||
chomp;
|
|
||||||
s/\#.*$//g;
|
|
||||||
next if /^\s*$/;
|
|
||||||
/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\d+)(\s+([0-9\.]+))?\s*$/ or die;
|
|
||||||
push @machines,
|
|
||||||
{ hostName => $1
|
|
||||||
, systemTypes => [split(/,/, $2)]
|
|
||||||
, sshKeys => $3
|
|
||||||
, maxJobs => $4
|
|
||||||
, speedFactor => 1.0 * ($6 || 1)
|
|
||||||
, enabled => 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
close CONF;
|
|
||||||
|
|
||||||
|
|
||||||
# Acquire the exclusive lock on $currentLoad/main-lock.
|
|
||||||
my $mainLock = "$currentLoad/main-lock";
|
|
||||||
open MAINLOCK, ">>$mainLock" or die;
|
|
||||||
flock(MAINLOCK, LOCK_EX) or die;
|
|
||||||
|
|
||||||
|
|
||||||
sub openSlotLock {
|
sub openSlotLock {
|
||||||
|
@ -93,109 +56,145 @@ sub openSlotLock {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
my $hostName;
|
# Read the list of machines.
|
||||||
my $slotLock;
|
my @machines;
|
||||||
|
if (defined $conf && -e $conf) {
|
||||||
|
open CONF, "< $conf" or die;
|
||||||
|
while (<CONF>) {
|
||||||
|
chomp;
|
||||||
|
s/\#.*$//g;
|
||||||
|
next if /^\s*$/;
|
||||||
|
/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\d+)(\s+([0-9\.]+))?\s*$/ or die;
|
||||||
|
push @machines,
|
||||||
|
{ hostName => $1
|
||||||
|
, systemTypes => [split(/,/, $2)]
|
||||||
|
, sshKeys => $3
|
||||||
|
, maxJobs => $4
|
||||||
|
, speedFactor => 1.0 * ($6 || 1)
|
||||||
|
, enabled => 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
close CONF;
|
||||||
|
}
|
||||||
|
|
||||||
while (1) {
|
|
||||||
|
|
||||||
# Find all machine that can execute this build, i.e., that support
|
|
||||||
# builds for the given platform and are not at their job limit.
|
|
||||||
my $rightType = 0;
|
|
||||||
my @available = ();
|
|
||||||
LOOP: foreach my $cur (@machines) {
|
|
||||||
if ($cur->{enabled} && grep { $neededSystem eq $_ } @{$cur->{systemTypes}}) {
|
|
||||||
$rightType = 1;
|
|
||||||
|
|
||||||
# We have a machine of the right type. Determine the load on
|
# Wait for the calling process to ask us whether we can build some derivation.
|
||||||
# the machine.
|
my ($drvPath, $hostName, $slotLock);
|
||||||
my $slot = 0;
|
|
||||||
my $load = 0;
|
REQ: while (1) {
|
||||||
my $free;
|
$_ = <STDIN> || exit 0;
|
||||||
while ($slot < $cur->{maxJobs}) {
|
my ($amWilling, $neededSystem);
|
||||||
my $slotLock = openSlotLock($cur, $slot);
|
($amWilling, $neededSystem, $drvPath) = split;
|
||||||
if (flock($slotLock, LOCK_EX | LOCK_NB)) {
|
|
||||||
$free = $slot unless defined $free;
|
my $canBuildLocally = $amWilling && ($localSystem eq $neededSystem);
|
||||||
flock($slotLock, LOCK_UN) or die;
|
|
||||||
} else {
|
if (!defined $currentLoad) {
|
||||||
$load++;
|
sendReply "decline";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Acquire the exclusive lock on $currentLoad/main-lock.
|
||||||
|
mkdir $currentLoad, 0777 or die unless -d $currentLoad;
|
||||||
|
my $mainLock = "$currentLoad/main-lock";
|
||||||
|
open MAINLOCK, ">>$mainLock" or die;
|
||||||
|
flock(MAINLOCK, LOCK_EX) or die;
|
||||||
|
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
# Find all machine that can execute this build, i.e., that
|
||||||
|
# support builds for the given platform and are not at their
|
||||||
|
# job limit.
|
||||||
|
my $rightType = 0;
|
||||||
|
my @available = ();
|
||||||
|
LOOP: foreach my $cur (@machines) {
|
||||||
|
if ($cur->{enabled} && grep { $neededSystem eq $_ } @{$cur->{systemTypes}}) {
|
||||||
|
$rightType = 1;
|
||||||
|
|
||||||
|
# We have a machine of the right type. Determine the load on
|
||||||
|
# the machine.
|
||||||
|
my $slot = 0;
|
||||||
|
my $load = 0;
|
||||||
|
my $free;
|
||||||
|
while ($slot < $cur->{maxJobs}) {
|
||||||
|
my $slotLock = openSlotLock($cur, $slot);
|
||||||
|
if (flock($slotLock, LOCK_EX | LOCK_NB)) {
|
||||||
|
$free = $slot unless defined $free;
|
||||||
|
flock($slotLock, LOCK_UN) or die;
|
||||||
|
} else {
|
||||||
|
$load++;
|
||||||
|
}
|
||||||
|
close $slotLock;
|
||||||
|
$slot++;
|
||||||
}
|
}
|
||||||
close $slotLock;
|
|
||||||
$slot++;
|
push @available, { machine => $cur, load => $load, free => $free }
|
||||||
|
if $load < $cur->{maxJobs};
|
||||||
}
|
}
|
||||||
|
|
||||||
push @available, { machine => $cur, load => $load, free => $free }
|
|
||||||
if $load < $cur->{maxJobs};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (defined $ENV{NIX_DEBUG_HOOK}) {
|
if (defined $ENV{NIX_DEBUG_HOOK}) {
|
||||||
print STDERR "load on " . $_->{machine}->{hostName} . " = " . $_->{load} . "\n"
|
print STDERR "load on " . $_->{machine}->{hostName} . " = " . $_->{load} . "\n"
|
||||||
foreach @available;
|
foreach @available;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Didn't find any available machine? Then decline or postpone.
|
|
||||||
if (scalar @available == 0) {
|
|
||||||
# Postpone if we have a machine of the right type, except if the
|
|
||||||
# local system can and wants to do the build.
|
|
||||||
if ($rightType && !$canBuildLocally) {
|
|
||||||
sendReply "postpone";
|
|
||||||
exit 0;
|
|
||||||
} else {
|
|
||||||
decline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Didn't find any available machine? Then decline or postpone.
|
||||||
|
if (scalar @available == 0) {
|
||||||
|
# Postpone if we have a machine of the right type, except
|
||||||
|
# if the local system can and wants to do the build.
|
||||||
|
if ($rightType && !$canBuildLocally) {
|
||||||
|
sendReply "postpone";
|
||||||
|
} else {
|
||||||
|
sendReply "decline";
|
||||||
|
}
|
||||||
|
close MAINLOCK;
|
||||||
|
next REQ;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Prioritise the available machines as follows:
|
||||||
|
# - First by load divided by speed factor, rounded to the nearest
|
||||||
|
# integer. This causes fast machines to be preferred over slow
|
||||||
|
# machines with similar loads.
|
||||||
|
# - Then by speed factor.
|
||||||
|
# - Finally by load.
|
||||||
|
sub lf { my $x = shift; return int($x->{load} / $x->{machine}->{speedFactor} + 0.4999); }
|
||||||
|
@available = sort
|
||||||
|
{ lf($a) <=> lf($b)
|
||||||
|
|| $b->{machine}->{speedFactor} <=> $a->{machine}->{speedFactor}
|
||||||
|
|| $a->{load} <=> $b->{load}
|
||||||
|
} @available;
|
||||||
|
|
||||||
|
|
||||||
|
# Select the best available machine and lock a free slot.
|
||||||
|
my $selected = $available[0];
|
||||||
|
my $machine = $selected->{machine};
|
||||||
|
|
||||||
|
$slotLock = openSlotLock($machine, $selected->{free});
|
||||||
|
flock($slotLock, LOCK_EX | LOCK_NB) or die;
|
||||||
|
utime undef, undef, $slotLock;
|
||||||
|
|
||||||
|
close MAINLOCK;
|
||||||
|
|
||||||
|
|
||||||
|
# Connect to the selected machine.
|
||||||
|
@sshOpts = ("-i", $machine->{sshKeys}, "-x");
|
||||||
|
$hostName = $machine->{hostName};
|
||||||
|
last REQ if openSSHConnection $hostName;
|
||||||
|
|
||||||
|
warn "unable to open SSH connection to $hostName, trying other available machines...\n";
|
||||||
|
$machine->{enabled} = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Prioritise the available machines as follows:
|
|
||||||
# - First by load divided by speed factor, rounded to the nearest
|
|
||||||
# integer. This causes fast machines to be preferred over slow
|
|
||||||
# machines with similar loads.
|
|
||||||
# - Then by speed factor.
|
|
||||||
# - Finally by load.
|
|
||||||
sub lf { my $x = shift; return int($x->{load} / $x->{machine}->{speedFactor} + 0.4999); }
|
|
||||||
@available = sort
|
|
||||||
{ lf($a) <=> lf($b)
|
|
||||||
|| $b->{machine}->{speedFactor} <=> $a->{machine}->{speedFactor}
|
|
||||||
|| $a->{load} <=> $b->{load}
|
|
||||||
} @available;
|
|
||||||
|
|
||||||
|
|
||||||
# Select the best available machine and lock a free slot.
|
|
||||||
my $selected = $available[0];
|
|
||||||
my $machine = $selected->{machine};
|
|
||||||
|
|
||||||
$slotLock = openSlotLock($machine, $selected->{free});
|
|
||||||
flock($slotLock, LOCK_EX | LOCK_NB) or die;
|
|
||||||
utime undef, undef, $slotLock;
|
|
||||||
|
|
||||||
close MAINLOCK;
|
|
||||||
|
|
||||||
|
|
||||||
# Connect to the selected machine.
|
|
||||||
@sshOpts = ("-i", $machine->{sshKeys}, "-x");
|
|
||||||
$hostName = $machine->{hostName};
|
|
||||||
last if openSSHConnection $hostName;
|
|
||||||
|
|
||||||
warn "unable to open SSH connection to $hostName, trying other available machines...\n";
|
|
||||||
$machine->{enabled} = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Tell Nix we've accepted the build.
|
# Tell Nix we've accepted the build.
|
||||||
sendReply "accept";
|
|
||||||
my $x = <STDIN>;
|
|
||||||
chomp $x;
|
|
||||||
|
|
||||||
if ($x ne "okay") {
|
|
||||||
exit 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
print STDERR "building `$drvPath' on `$hostName'\n";
|
print STDERR "building `$drvPath' on `$hostName'\n";
|
||||||
|
sendReply "accept";
|
||||||
|
my @inputs = split /\s/, readline(STDIN);
|
||||||
|
my @outputs = split /\s/, readline(STDIN);
|
||||||
|
|
||||||
my @inputs = split /\s/, do { local $/; local @ARGV = "inputs"; <> };
|
|
||||||
my @outputs = split /\s/, do { local $/; local @ARGV = "outputs"; <> };
|
|
||||||
|
|
||||||
my $maybeSign = "";
|
my $maybeSign = "";
|
||||||
$maybeSign = "--sign" if -e "/nix/etc/nix/signing-key.sec";
|
$maybeSign = "--sign" if -e "/nix/etc/nix/signing-key.sec";
|
||||||
|
@ -238,7 +237,7 @@ if (system("ssh $hostName @sshOpts -tt 'nix-store -r $drvPath $buildFlags > /dev
|
||||||
exit $res;
|
exit $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
print "build of `$drvPath' on `$hostName' succeeded\n";
|
#print "build of `$drvPath' on `$hostName' succeeded\n";
|
||||||
|
|
||||||
|
|
||||||
# Copy the output from the build machine.
|
# Copy the output from the build machine.
|
||||||
|
@ -246,7 +245,8 @@ foreach my $output (@outputs) {
|
||||||
my $maybeSignRemote = "";
|
my $maybeSignRemote = "";
|
||||||
$maybeSignRemote = "--sign" if $UID != 0;
|
$maybeSignRemote = "--sign" if $UID != 0;
|
||||||
|
|
||||||
system("ssh $hostName @sshOpts 'nix-store --export $maybeSignRemote $output' | @bindir@/nix-store --import > /dev/null") == 0
|
system("ssh $hostName @sshOpts 'nix-store --export $maybeSignRemote $output'" .
|
||||||
|
"| NIX_HELD_LOCKS=$output @bindir@/nix-store --import > /dev/null") == 0
|
||||||
or die "cannot copy $output from $hostName: $?";
|
or die "cannot copy $output from $hostName: $?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ static const uid_t rootUserId = 0;
|
||||||
|
|
||||||
/* Forward definition. */
|
/* Forward definition. */
|
||||||
class Worker;
|
class Worker;
|
||||||
|
class HookInstance;
|
||||||
|
|
||||||
|
|
||||||
/* A pointer to a goal. */
|
/* A pointer to a goal. */
|
||||||
|
@ -215,6 +216,8 @@ public:
|
||||||
|
|
||||||
LocalStore & store;
|
LocalStore & store;
|
||||||
|
|
||||||
|
boost::shared_ptr<HookInstance> hook;
|
||||||
|
|
||||||
Worker(LocalStore & store);
|
Worker(LocalStore & store);
|
||||||
~Worker();
|
~Worker();
|
||||||
|
|
||||||
|
@ -615,6 +618,94 @@ void deletePathWrapped(const Path & path)
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
struct HookInstance
|
||||||
|
{
|
||||||
|
/* Pipes for talking to the build hook. */
|
||||||
|
Pipe toHook;
|
||||||
|
|
||||||
|
/* Pipe for the hook's standard output/error. */
|
||||||
|
Pipe fromHook;
|
||||||
|
|
||||||
|
/* The process ID of the hook. */
|
||||||
|
Pid pid;
|
||||||
|
|
||||||
|
HookInstance();
|
||||||
|
|
||||||
|
~HookInstance();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
HookInstance::HookInstance()
|
||||||
|
{
|
||||||
|
debug("starting build hook");
|
||||||
|
|
||||||
|
Path buildHook = absPath(getEnv("NIX_BUILD_HOOK"));
|
||||||
|
|
||||||
|
/* Create a pipe to get the output of the child. */
|
||||||
|
fromHook.create();
|
||||||
|
|
||||||
|
/* Create the communication pipes. */
|
||||||
|
toHook.create();
|
||||||
|
|
||||||
|
/* Fork the hook. */
|
||||||
|
pid = fork();
|
||||||
|
switch (pid) {
|
||||||
|
|
||||||
|
case -1:
|
||||||
|
throw SysError("unable to fork");
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
try { /* child */
|
||||||
|
|
||||||
|
commonChildInit(fromHook);
|
||||||
|
|
||||||
|
if (chdir("/") == -1) throw SysError("changing into `/");
|
||||||
|
|
||||||
|
/* Dup the communication pipes. */
|
||||||
|
toHook.writeSide.close();
|
||||||
|
if (dup2(toHook.readSide, STDIN_FILENO) == -1)
|
||||||
|
throw SysError("dupping to-hook read side");
|
||||||
|
|
||||||
|
execl(buildHook.c_str(), buildHook.c_str(), thisSystem.c_str(),
|
||||||
|
(format("%1%") % maxSilentTime).str().c_str(), NULL);
|
||||||
|
|
||||||
|
throw SysError(format("executing `%1%'") % buildHook);
|
||||||
|
|
||||||
|
} catch (std::exception & e) {
|
||||||
|
std::cerr << format("build hook error: %1%") % e.what() << std::endl;
|
||||||
|
}
|
||||||
|
quickExit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* parent */
|
||||||
|
pid.setSeparatePG(true);
|
||||||
|
pid.setKillSignal(SIGTERM);
|
||||||
|
fromHook.writeSide.close();
|
||||||
|
toHook.readSide.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
HookInstance::~HookInstance()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
/* Cleanly shut down the hook by closing its stdin if it's not
|
||||||
|
already building. Otherwise pid's destructor will kill
|
||||||
|
it. */
|
||||||
|
if (pid != -1 && toHook.writeSide != -1) {
|
||||||
|
toHook.writeSide.close();
|
||||||
|
pid.wait(true);
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
ignoreException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum {rpAccept, rpDecline, rpPostpone} HookReply;
|
||||||
|
|
||||||
class DerivationGoal : public Goal
|
class DerivationGoal : public Goal
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
@ -651,11 +742,8 @@ private:
|
||||||
/* Pipe for the builder's standard output/error. */
|
/* Pipe for the builder's standard output/error. */
|
||||||
Pipe logPipe;
|
Pipe logPipe;
|
||||||
|
|
||||||
/* Whether we're building using a build hook. */
|
/* The build hook. */
|
||||||
bool usingBuildHook;
|
boost::shared_ptr<HookInstance> hook;
|
||||||
|
|
||||||
/* Pipes for talking to the build hook (if any). */
|
|
||||||
Pipe toHook;
|
|
||||||
|
|
||||||
/* Whether we're currently doing a chroot build. */
|
/* Whether we're currently doing a chroot build. */
|
||||||
bool useChroot;
|
bool useChroot;
|
||||||
|
@ -694,12 +782,8 @@ private:
|
||||||
void buildDone();
|
void buildDone();
|
||||||
|
|
||||||
/* Is the build hook willing to perform the build? */
|
/* Is the build hook willing to perform the build? */
|
||||||
typedef enum {rpAccept, rpDecline, rpPostpone} HookReply;
|
|
||||||
HookReply tryBuildHook();
|
HookReply tryBuildHook();
|
||||||
|
|
||||||
/* Synchronously wait for a build hook to finish. */
|
|
||||||
void terminateBuildHook(bool kill = false);
|
|
||||||
|
|
||||||
/* Start building a derivation. */
|
/* Start building a derivation. */
|
||||||
void startBuilder();
|
void startBuilder();
|
||||||
|
|
||||||
|
@ -711,10 +795,6 @@ private:
|
||||||
/* Open a log file and a pipe to it. */
|
/* Open a log file and a pipe to it. */
|
||||||
Path openLogFile();
|
Path openLogFile();
|
||||||
|
|
||||||
/* Common initialisation to be performed in child processes (i.e.,
|
|
||||||
both in builders and in build hooks). */
|
|
||||||
void initChild();
|
|
||||||
|
|
||||||
/* Delete the temporary directory, if we have one. */
|
/* Delete the temporary directory, if we have one. */
|
||||||
void deleteTmpDir(bool force);
|
void deleteTmpDir(bool force);
|
||||||
|
|
||||||
|
@ -742,6 +822,7 @@ DerivationGoal::DerivationGoal(const Path & drvPath, Worker & worker)
|
||||||
trace("created");
|
trace("created");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
DerivationGoal::~DerivationGoal()
|
DerivationGoal::~DerivationGoal()
|
||||||
{
|
{
|
||||||
/* Careful: we should never ever throw an exception from a
|
/* Careful: we should never ever throw an exception from a
|
||||||
|
@ -754,6 +835,7 @@ DerivationGoal::~DerivationGoal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void DerivationGoal::killChild()
|
void DerivationGoal::killChild()
|
||||||
{
|
{
|
||||||
if (pid != -1) {
|
if (pid != -1) {
|
||||||
|
@ -778,6 +860,8 @@ void DerivationGoal::killChild()
|
||||||
|
|
||||||
assert(pid == -1);
|
assert(pid == -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hook.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1048,7 +1132,6 @@ void DerivationGoal::tryToBuild()
|
||||||
|
|
||||||
/* Is the build hook willing to accept this job? */
|
/* Is the build hook willing to accept this job? */
|
||||||
if (!preferLocalBuild) {
|
if (!preferLocalBuild) {
|
||||||
usingBuildHook = true;
|
|
||||||
switch (tryBuildHook()) {
|
switch (tryBuildHook()) {
|
||||||
case rpAccept:
|
case rpAccept:
|
||||||
/* Yes, it has started doing so. Wait until we get
|
/* Yes, it has started doing so. Wait until we get
|
||||||
|
@ -1056,7 +1139,8 @@ void DerivationGoal::tryToBuild()
|
||||||
state = &DerivationGoal::buildDone;
|
state = &DerivationGoal::buildDone;
|
||||||
return;
|
return;
|
||||||
case rpPostpone:
|
case rpPostpone:
|
||||||
/* Not now; wait until at least one child finishes. */
|
/* Not now; wait until at least one child finishes or
|
||||||
|
the wake-up timeout expires. */
|
||||||
worker.waitForAWhile(shared_from_this());
|
worker.waitForAWhile(shared_from_this());
|
||||||
outputLocks.unlock();
|
outputLocks.unlock();
|
||||||
return;
|
return;
|
||||||
|
@ -1066,8 +1150,6 @@ void DerivationGoal::tryToBuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
usingBuildHook = false;
|
|
||||||
|
|
||||||
/* Make sure that we are allowed to start a build. If this
|
/* Make sure that we are allowed to start a build. If this
|
||||||
derivation prefers to be done locally, do it even if
|
derivation prefers to be done locally, do it even if
|
||||||
maxBuildJobs is 0. */
|
maxBuildJobs is 0. */
|
||||||
|
@ -1108,10 +1190,17 @@ void DerivationGoal::buildDone()
|
||||||
to have terminated. In fact, the builder could also have
|
to have terminated. In fact, the builder could also have
|
||||||
simply have closed its end of the pipe --- just don't do that
|
simply have closed its end of the pipe --- just don't do that
|
||||||
:-) */
|
:-) */
|
||||||
/* !!! this could block! security problem! solution: kill the
|
int status;
|
||||||
child */
|
pid_t savedPid;
|
||||||
pid_t savedPid = pid;
|
if (hook) {
|
||||||
int status = pid.wait(true);
|
savedPid = hook->pid;
|
||||||
|
status = hook->pid.wait(true);
|
||||||
|
} else {
|
||||||
|
/* !!! this could block! security problem! solution: kill the
|
||||||
|
child */
|
||||||
|
savedPid = pid;
|
||||||
|
status = pid.wait(true);
|
||||||
|
}
|
||||||
|
|
||||||
debug(format("builder process for `%1%' finished") % drvPath);
|
debug(format("builder process for `%1%' finished") % drvPath);
|
||||||
|
|
||||||
|
@ -1192,11 +1281,11 @@ void DerivationGoal::buildDone()
|
||||||
/* When using a build hook, the hook will return a remote
|
/* When using a build hook, the hook will return a remote
|
||||||
build failure using exit code 100. Anything else is a hook
|
build failure using exit code 100. Anything else is a hook
|
||||||
problem. */
|
problem. */
|
||||||
bool hookError = usingBuildHook &&
|
bool hookError = hook &&
|
||||||
(!WIFEXITED(status) || WEXITSTATUS(status) != 100);
|
(!WIFEXITED(status) || WEXITSTATUS(status) != 100);
|
||||||
|
|
||||||
if (printBuildTrace) {
|
if (printBuildTrace) {
|
||||||
if (usingBuildHook && hookError)
|
if (hook && hookError)
|
||||||
printMsg(lvlError, format("@ hook-failed %1% %2% %3% %4%")
|
printMsg(lvlError, format("@ hook-failed %1% %2% %3% %4%")
|
||||||
% drvPath % drv.outputs["out"].path % status % e.msg());
|
% drvPath % drv.outputs["out"].path % status % e.msg());
|
||||||
else
|
else
|
||||||
|
@ -1231,162 +1320,74 @@ void DerivationGoal::buildDone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
DerivationGoal::HookReply DerivationGoal::tryBuildHook()
|
HookReply DerivationGoal::tryBuildHook()
|
||||||
{
|
{
|
||||||
if (!useBuildHook) return rpDecline;
|
if (!useBuildHook || getEnv("NIX_BUILD_HOOK") == "") return rpDecline;
|
||||||
Path buildHook = getEnv("NIX_BUILD_HOOK");
|
|
||||||
if (buildHook == "") return rpDecline;
|
|
||||||
buildHook = absPath(buildHook);
|
|
||||||
|
|
||||||
/* Create a directory where we will store files used for
|
if (!worker.hook)
|
||||||
communication between us and the build hook. */
|
worker.hook = boost::shared_ptr<HookInstance>(new HookInstance);
|
||||||
tmpDir = createTempDir();
|
|
||||||
|
|
||||||
/* Create the log file and pipe. */
|
writeLine(worker.hook->toHook.writeSide, (format("%1% %2% %3%") %
|
||||||
Path logFile = openLogFile();
|
(worker.getNrLocalBuilds() < maxBuildJobs ? "1" : "0") % drv.platform % drvPath).str());
|
||||||
|
|
||||||
/* Create the communication pipes. */
|
|
||||||
toHook.create();
|
|
||||||
|
|
||||||
/* Fork the hook. */
|
|
||||||
pid = fork();
|
|
||||||
switch (pid) {
|
|
||||||
|
|
||||||
case -1:
|
|
||||||
throw SysError("unable to fork");
|
|
||||||
|
|
||||||
case 0:
|
|
||||||
try { /* child */
|
|
||||||
|
|
||||||
initChild();
|
|
||||||
|
|
||||||
string s;
|
|
||||||
foreach (DerivationOutputs::const_iterator, i, drv.outputs)
|
|
||||||
s += i->second.path + " ";
|
|
||||||
if (setenv("NIX_HELD_LOCKS", s.c_str(), 1))
|
|
||||||
throw SysError("setting an environment variable");
|
|
||||||
|
|
||||||
execl(buildHook.c_str(), buildHook.c_str(),
|
|
||||||
(worker.getNrLocalBuilds() < maxBuildJobs ? (string) "1" : "0").c_str(),
|
|
||||||
thisSystem.c_str(),
|
|
||||||
drv.platform.c_str(),
|
|
||||||
drvPath.c_str(),
|
|
||||||
(format("%1%") % maxSilentTime).str().c_str(),
|
|
||||||
NULL);
|
|
||||||
|
|
||||||
throw SysError(format("executing `%1%'") % buildHook);
|
|
||||||
|
|
||||||
} catch (std::exception & e) {
|
|
||||||
std::cerr << format("build hook error: %1%") % e.what() << std::endl;
|
|
||||||
}
|
|
||||||
quickExit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* parent */
|
|
||||||
pid.setSeparatePG(true);
|
|
||||||
pid.setKillSignal(SIGTERM);
|
|
||||||
logPipe.writeSide.close();
|
|
||||||
worker.childStarted(shared_from_this(),
|
|
||||||
pid, singleton<set<int> >(logPipe.readSide), false, false);
|
|
||||||
|
|
||||||
toHook.readSide.close();
|
|
||||||
|
|
||||||
/* Read the first line of input, which should be a word indicating
|
/* Read the first line of input, which should be a word indicating
|
||||||
whether the hook wishes to perform the build. */
|
whether the hook wishes to perform the build. */
|
||||||
string reply;
|
string reply;
|
||||||
try {
|
while (true) {
|
||||||
while (true) {
|
string s = readLine(worker.hook->fromHook.readSide);
|
||||||
string s = readLine(logPipe.readSide);
|
if (string(s, 0, 2) == "# ") {
|
||||||
if (string(s, 0, 2) == "# ") {
|
reply = string(s, 2);
|
||||||
reply = string(s, 2);
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
handleChildOutput(logPipe.readSide, s + "\n");
|
|
||||||
}
|
}
|
||||||
} catch (Error & e) {
|
handleChildOutput(worker.hook->fromHook.readSide, s + "\n");
|
||||||
terminateBuildHook(true);
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(format("hook reply is `%1%'") % reply);
|
debug(format("hook reply is `%1%'") % reply);
|
||||||
|
|
||||||
if (reply == "decline" || reply == "postpone") {
|
if (reply == "decline" || reply == "postpone")
|
||||||
/* Clean up the child. !!! hacky / should verify */
|
|
||||||
terminateBuildHook();
|
|
||||||
return reply == "decline" ? rpDecline : rpPostpone;
|
return reply == "decline" ? rpDecline : rpPostpone;
|
||||||
}
|
else if (reply != "accept")
|
||||||
|
throw Error(format("bad hook reply `%1%'") % reply);
|
||||||
|
|
||||||
else if (reply == "accept") {
|
printMsg(lvlTalkative, format("using hook to build path(s) %1%")
|
||||||
|
% showPaths(outputPaths(drv.outputs)));
|
||||||
|
|
||||||
printMsg(lvlInfo, format("using hook to build path(s) %1%")
|
hook = worker.hook;
|
||||||
% showPaths(outputPaths(drv.outputs)));
|
worker.hook.reset();
|
||||||
|
|
||||||
/* Write the information that the hook needs to perform the
|
/* Tell the hook all the inputs that have to be copied to the
|
||||||
build, i.e., the set of input paths, the set of output
|
remote system. This unfortunately has to contain the entire
|
||||||
paths, and the references (pointer graph) in the input
|
derivation closure to ensure that the validity invariant holds
|
||||||
paths. */
|
on the remote system. (I.e., it's unfortunate that we have to
|
||||||
|
list it since the remote system *probably* already has it.) */
|
||||||
|
PathSet allInputs;
|
||||||
|
allInputs.insert(inputPaths.begin(), inputPaths.end());
|
||||||
|
computeFSClosure(drvPath, allInputs);
|
||||||
|
|
||||||
Path inputListFN = tmpDir + "/inputs";
|
string s;
|
||||||
Path outputListFN = tmpDir + "/outputs";
|
foreach (PathSet::iterator, i, allInputs) s += *i + " ";
|
||||||
Path referencesFN = tmpDir + "/references";
|
writeLine(hook->toHook.writeSide, s);
|
||||||
|
|
||||||
/* The `inputs' file lists all inputs that have to be copied
|
/* Tell the hooks the outputs that have to be copied back from the
|
||||||
to the remote system. This unfortunately has to contain
|
remote system. */
|
||||||
the entire derivation closure to ensure that the validity
|
s = "";
|
||||||
invariant holds on the remote system. (I.e., it's
|
foreach (DerivationOutputs::iterator, i, drv.outputs)
|
||||||
unfortunate that we have to list it since the remote system
|
s += i->second.path + " ";
|
||||||
*probably* already has it.) */
|
writeLine(hook->toHook.writeSide, s);
|
||||||
PathSet allInputs;
|
|
||||||
allInputs.insert(inputPaths.begin(), inputPaths.end());
|
|
||||||
computeFSClosure(drvPath, allInputs);
|
|
||||||
|
|
||||||
string s;
|
hook->toHook.writeSide.close();
|
||||||
foreach (PathSet::iterator, i, allInputs) s += *i + "\n";
|
|
||||||
|
|
||||||
writeFile(inputListFN, s);
|
/* Create the log file and pipe. */
|
||||||
|
Path logFile = openLogFile();
|
||||||
|
|
||||||
/* The `outputs' file lists all outputs that have to be copied
|
worker.childStarted(shared_from_this(),
|
||||||
from the remote system. */
|
hook->pid, singleton<set<int> >(hook->fromHook.readSide), false, false);
|
||||||
s = "";
|
|
||||||
foreach (DerivationOutputs::iterator, i, drv.outputs)
|
|
||||||
s += i->second.path + "\n";
|
|
||||||
writeFile(outputListFN, s);
|
|
||||||
|
|
||||||
/* The `references' file has exactly the format accepted by
|
if (printBuildTrace)
|
||||||
`nix-store --register-validity'. */
|
printMsg(lvlError, format("@ build-started %1% %2% %3% %4%")
|
||||||
writeFile(referencesFN,
|
% drvPath % drv.outputs["out"].path % drv.platform % logFile);
|
||||||
makeValidityRegistration(allInputs, true, false));
|
|
||||||
|
|
||||||
/* Tell the hook to proceed. */
|
return rpAccept;
|
||||||
writeLine(toHook.writeSide, "okay");
|
|
||||||
toHook.writeSide.close();
|
|
||||||
|
|
||||||
if (printBuildTrace)
|
|
||||||
printMsg(lvlError, format("@ build-started %1% %2% %3% %4%")
|
|
||||||
% drvPath % drv.outputs["out"].path % drv.platform % logFile);
|
|
||||||
|
|
||||||
return rpAccept;
|
|
||||||
}
|
|
||||||
|
|
||||||
else throw Error(format("bad hook reply `%1%'") % reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void DerivationGoal::terminateBuildHook(bool kill)
|
|
||||||
{
|
|
||||||
debug("terminating build hook");
|
|
||||||
pid_t savedPid = pid;
|
|
||||||
if (kill)
|
|
||||||
pid.kill();
|
|
||||||
else
|
|
||||||
pid.wait(true);
|
|
||||||
/* `false' means don't wake up waiting goals, since we want to
|
|
||||||
keep this build slot ourselves. */
|
|
||||||
worker.childTerminated(savedPid, false);
|
|
||||||
toHook.writeSide.close();
|
|
||||||
fdLogFile.close();
|
|
||||||
logPipe.readSide.close();
|
|
||||||
deleteTmpDir(true); /* get rid of the hook's temporary directory */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1667,9 +1668,12 @@ void DerivationGoal::startBuilder()
|
||||||
printMsg(lvlChatty, format("executing builder `%1%'") %
|
printMsg(lvlChatty, format("executing builder `%1%'") %
|
||||||
drv.builder);
|
drv.builder);
|
||||||
|
|
||||||
/* Create the log file and pipe. */
|
/* Create the log file. */
|
||||||
Path logFile = openLogFile();
|
Path logFile = openLogFile();
|
||||||
|
|
||||||
|
/* Create a pipe to get the output of the child. */
|
||||||
|
logPipe.create();
|
||||||
|
|
||||||
/* Fork a child to build the package. Note that while we
|
/* Fork a child to build the package. Note that while we
|
||||||
currently use forks to run and wait for the children, it
|
currently use forks to run and wait for the children, it
|
||||||
shouldn't be hard to use threads for this on systems where
|
shouldn't be hard to use threads for this on systems where
|
||||||
|
@ -1710,18 +1714,23 @@ void DerivationGoal::startBuilder()
|
||||||
throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
|
throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Do the chroot(). initChild() will do a chdir() to
|
/* Do the chroot(). Below we do a chdir() to the
|
||||||
the temporary build directory to make sure the
|
temporary build directory to make sure the current
|
||||||
current directory is in the chroot. (Actually the
|
directory is in the chroot. (Actually the order
|
||||||
order doesn't matter, since due to the bind mount
|
doesn't matter, since due to the bind mount tmpDir
|
||||||
tmpDir and tmpRootDit/tmpDir are the same
|
and tmpRootDit/tmpDir are the same directories.) */
|
||||||
directories.) */
|
|
||||||
if (chroot(chrootRootDir.c_str()) == -1)
|
if (chroot(chrootRootDir.c_str()) == -1)
|
||||||
throw SysError(format("cannot change root directory to `%1%'") % chrootRootDir);
|
throw SysError(format("cannot change root directory to `%1%'") % chrootRootDir);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
initChild();
|
commonChildInit(logPipe);
|
||||||
|
|
||||||
|
if (chdir(tmpDir.c_str()) == -1)
|
||||||
|
throw SysError(format("changing into `%1%'") % tmpDir);
|
||||||
|
|
||||||
|
/* Close all other file descriptors. */
|
||||||
|
closeMostFDs(set<int>());
|
||||||
|
|
||||||
#ifdef CAN_DO_LINUX32_BUILDS
|
#ifdef CAN_DO_LINUX32_BUILDS
|
||||||
if (drv.platform == "i686-linux" && thisSystem == "x86_64-linux") {
|
if (drv.platform == "i686-linux" && thisSystem == "x86_64-linux") {
|
||||||
|
@ -1742,10 +1751,10 @@ void DerivationGoal::startBuilder()
|
||||||
|
|
||||||
/* If we are running in `build-users' mode, then switch to
|
/* If we are running in `build-users' mode, then switch to
|
||||||
the user we allocated above. Make sure that we drop
|
the user we allocated above. Make sure that we drop
|
||||||
all root privileges. Note that initChild() above has
|
all root privileges. Note that above we have closed
|
||||||
closed all file descriptors except std*, so that's
|
all file descriptors except std*, so that's safe. Also
|
||||||
safe. Also note that setuid() when run as root sets
|
note that setuid() when run as root sets the real,
|
||||||
the real, effective and saved UIDs. */
|
effective and saved UIDs. */
|
||||||
if (buildUser.enabled()) {
|
if (buildUser.enabled()) {
|
||||||
printMsg(lvlChatty, format("switching to user `%1%'") % buildUser.getUser());
|
printMsg(lvlChatty, format("switching to user `%1%'") % buildUser.getUser());
|
||||||
|
|
||||||
|
@ -1838,7 +1847,7 @@ void DerivationGoal::computeClosure()
|
||||||
/* When using a build hook, the build hook can register the output
|
/* When using a build hook, the build hook can register the output
|
||||||
as valid (by doing `nix-store --import'). If so we don't have
|
as valid (by doing `nix-store --import'). If so we don't have
|
||||||
to do anything here. */
|
to do anything here. */
|
||||||
if (usingBuildHook) {
|
if (hook) {
|
||||||
bool allValid = true;
|
bool allValid = true;
|
||||||
foreach (DerivationOutputs::iterator, i, drv.outputs)
|
foreach (DerivationOutputs::iterator, i, drv.outputs)
|
||||||
if (!worker.store.isValidPath(i->second.path)) allValid = false;
|
if (!worker.store.isValidPath(i->second.path)) allValid = false;
|
||||||
|
@ -1966,32 +1975,10 @@ Path DerivationGoal::openLogFile()
|
||||||
if (fdLogFile == -1)
|
if (fdLogFile == -1)
|
||||||
throw SysError(format("creating log file `%1%'") % logFileName);
|
throw SysError(format("creating log file `%1%'") % logFileName);
|
||||||
|
|
||||||
/* Create a pipe to get the output of the child. */
|
|
||||||
logPipe.create();
|
|
||||||
|
|
||||||
return logFileName;
|
return logFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void DerivationGoal::initChild()
|
|
||||||
{
|
|
||||||
commonChildInit(logPipe);
|
|
||||||
|
|
||||||
if (chdir(tmpDir.c_str()) == -1)
|
|
||||||
throw SysError(format("changing into `%1%'") % tmpDir);
|
|
||||||
|
|
||||||
/* When running a hook, dup the communication pipes. */
|
|
||||||
if (usingBuildHook) {
|
|
||||||
toHook.writeSide.close();
|
|
||||||
if (dup2(toHook.readSide, STDIN_FILENO) == -1)
|
|
||||||
throw SysError("dupping to-hook read side");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Close all other file descriptors. */
|
|
||||||
closeMostFDs(set<int>());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void DerivationGoal::deleteTmpDir(bool force)
|
void DerivationGoal::deleteTmpDir(bool force)
|
||||||
{
|
{
|
||||||
if (tmpDir != "") {
|
if (tmpDir != "") {
|
||||||
|
@ -2011,19 +1998,16 @@ void DerivationGoal::deleteTmpDir(bool force)
|
||||||
|
|
||||||
void DerivationGoal::handleChildOutput(int fd, const string & data)
|
void DerivationGoal::handleChildOutput(int fd, const string & data)
|
||||||
{
|
{
|
||||||
if (fd == logPipe.readSide) {
|
if (verbosity >= buildVerbosity)
|
||||||
if (verbosity >= buildVerbosity)
|
writeToStderr((unsigned char *) data.c_str(), data.size());
|
||||||
writeToStderr((unsigned char *) data.c_str(), data.size());
|
if (fdLogFile != -1)
|
||||||
writeFull(fdLogFile, (unsigned char *) data.c_str(), data.size());
|
writeFull(fdLogFile, (unsigned char *) data.c_str(), data.size());
|
||||||
}
|
|
||||||
|
|
||||||
else abort();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void DerivationGoal::handleEOF(int fd)
|
void DerivationGoal::handleEOF(int fd)
|
||||||
{
|
{
|
||||||
if (fd == logPipe.readSide) worker.wakeUp(shared_from_this());
|
worker.wakeUp(shared_from_this());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,20 +2,22 @@
|
||||||
|
|
||||||
#set -x
|
#set -x
|
||||||
|
|
||||||
drv=$4
|
while read x y drv rest; do
|
||||||
|
|
||||||
echo "HOOK for $drv" >&2
|
echo "HOOK for $drv" >&2
|
||||||
|
|
||||||
outPath=`sed 's/Derive(\[("out",\"\([^\"]*\)\".*/\1/' $drv`
|
outPath=`sed 's/Derive(\[("out",\"\([^\"]*\)\".*/\1/' $drv`
|
||||||
|
|
||||||
echo "output path is $outPath" >&2
|
echo "output path is $outPath" >&2
|
||||||
|
|
||||||
if `echo $outPath | grep -q input-1`; then
|
if `echo $outPath | grep -q input-1`; then
|
||||||
echo "# accept" >&2
|
echo "# accept" >&2
|
||||||
read x
|
read inputs
|
||||||
echo "got $x"
|
read outputs
|
||||||
mkdir $outPath
|
mkdir $outPath
|
||||||
echo "BAR" > $outPath/foo
|
echo "BAR" > $outPath/foo
|
||||||
else
|
else
|
||||||
echo "# decline" >&2
|
echo "# decline" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
done
|
Loading…
Reference in a new issue