#!/usr/bin/perl -w # $OpenBSD: dpb,v 1.13 2005/03/03 21:08:03 sturm Exp $ # Copyright (c) 2004 Nikolay Sturm . # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE OPENBSD PROJECT AND CONTRIBUTORS # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OPENBSD # PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # use strict; use File::Temp; use Getopt::Std; our $MAKE = "/usr/bin/make"; our $MAKEFLAGS = "BATCH=Yes BIN_PACKAGES=Yes BULK=Yes TRUST_PACKAGES=Yes"; our $PORTSDIR = $ENV{'PORTSDIR'} || "/usr/ports"; our $TMPDIR = $ENV{'PKG_TMPDIR'} || '/var/tmp'; our @SSH = ("/usr/bin/ssh", "-n"); # -A : specify architecture of build hosts # -b: force creation of dependency file # -c: clean build, i.e. pkg_delete * before building a port # -d: debug run, don't actually build any packages # -e: perform expensive operations to get full dependency information in # order to optimize build order by package importance # -F : one host per line # -L : use instead of $PORTSDIR/logs/$ARCH # -S : use instead of all ports # -s: build all packages in cwd # -T : use instead of a temporary one # -t : use this timeout instead of the default our ($opt_A, $opt_b, $opt_c, $opt_d, $opt_e, $opt_F, $opt_L, $opt_S, $opt_s, $opt_T, $opt_t); getopts('A:bcdeF:L:S:sT:t:'); our $ARCH; if (defined $opt_A) { $ARCH = $opt_A; } else { chomp($ARCH = `/usr/bin/arch -s`); } our $HOSTS_FILE = $opt_F || "$PORTSDIR/infrastructure/db/hosts-$ARCH"; our $LOGGER = "$PORTSDIR/infrastructure/build/portslogger "; $LOGGER .= $opt_L || "$PORTSDIR/logs/$ARCH"; our $TIMEOUT = $opt_t || 60; push @SSH, "-o ConnectTimeout=$TIMEOUT"; # per slave pid: node, port and retval our $SLAVES = {}; # per port: slave pid our $PORTS = {}; # dependency lists our %depend_on = (); our %prereqs_of = (); # per node: fifo, host, pid, shell our $NODES = {}; # per host: down, nodes, ssh_master our $HOSTS = {}; our $FREE_NODES = {}; our $CHECK_HOSTS; our $check_host_ctr = 0; our @dead_slaves = (); sub child_handler() { while ((my $child = waitpid(-1,1)) > 0) { my $sig = ($? && 255); my $retval = ($? >> 8); # host/session died, retry build $retval = 255 if $sig > 0 and $retval == 0; if (defined $SLAVES->{$child}) { $SLAVES->{$child}{retval} = $retval; push(@dead_slaves, $child); } elsif (exists $CHECK_HOSTS->{$child}) { $CHECK_HOSTS->{$child} = $retval; } } } sub term_handler() { local $SIG{CHLD} = "IGNORE"; local $SIG{HUP} = "IGNORE"; local $SIG{INT} = "IGNORE"; local $SIG{TERM} = "IGNORE"; foreach my $pid (keys %{$CHECK_HOSTS}, keys %{$SLAVES}) { kill INT => $pid; } clean_up(1); } sub my_exec($$) { my ($args, $out) = @_; $SIG{HUP} = "DEFAULT"; $SIG{INT} = "DEFAULT"; $SIG{TERM} = "DEFAULT"; open STDOUT, '>', "$out" or die "Cannot redirect STDOUT: $!"; open STDERR, ">&STDOUT" or die "Cannot redirect STDERR: $!"; exec @{$args}; die "Cannot @{$args}: $!"; } sub reap_slaves() { while (my $c = pop @dead_slaves) { update_after_slave($c); } } sub mark_host_down($) { my $host = shift; warn "*** lost $host\n"; $HOSTS->{$host}{down} = 1; foreach my $node (@{$HOSTS->{$host}{nodes}}) { my $nodepid = $NODES->{$node}{pid}; if (defined $nodepid) { kill KILL => $nodepid ; child_handler(); reap_slaves(); } mark_node_down($node); } kill_ssh_master($host); } sub mark_node_down($) { my $node = shift; my $nodepid = $NODES->{$node}{pid}; # active node delete $NODES->{$node}{pid}; delete $SLAVES->{$nodepid} if defined $nodepid; # free node delete $FREE_NODES->{$node}; } sub mark_node_free($) { my $node = shift; $FREE_NODES->{$node} = 1; } sub kill_ssh_master($) { my $host = shift; my $ssh_mpid = $HOSTS->{$host}{ssh_master}; if (defined $ssh_mpid) { kill INT => $ssh_mpid; delete $HOSTS->{$host}{ssh_master}; } } sub start_ssh_master($) { my $host = shift; my $pid = fork(); die "fork: $!" unless defined $pid; if ($pid > 0) { # parent while (not -e "$TMPDIR/ssh-$host") { # child died? return if ((my $child = waitpid($pid, 1)) > 0); sleep 1; } $HOSTS->{$host}{ssh_master} = $pid; } else { # child my @args = (@SSH, "-N", "-M", "-S", "$TMPDIR/ssh-$host", "$host"); my_exec(\@args, "/dev/null"); } } sub clear_lock($) { my $fullport = shift; my ($port, $t) = split /,/, $fullport; my $flavor = ""; if (defined $t and not $t =~ /^-/) { $flavor = "FLAVOR=$t"; } my $lockdir = `cd ${PORTSDIR}/$port && $flavor make show=LOCKDIR`; chomp $lockdir; return if $lockdir eq ""; my $lockname = `cd ${PORTSDIR}/$port && $flavor make show=_LOCKNAME`; chomp $lockname; return if $lockname eq ""; system("/bin/rm -f $lockdir/$lockname.lock"); } sub clear_packages($) { my $host = shift; my $pid = fork(); die "fork: $!" unless defined $pid; if ($pid > 0) { # parent # just wait for pkg_delete to finish, we don't care # about anything else waitpid($pid, 0); } else { # child # beware of format, might not work with /bin/sh -c otherwise my @args = (@{$NODES->{"$host%0"}{shell}}, "/usr/bin/sudo /usr/sbin/pkg_delete -c -q /var/db/pkg/*"); my_exec(\@args, "/dev/null"); } } sub check_host($) { my $host = shift; my $pid = fork(); die "fork: $!" unless defined $pid; if ($pid > 0) { # parent my $begin = time(); $CHECK_HOSTS->{$pid} = undef; child_handler(); while (not defined $CHECK_HOSTS->{$pid}) { # give ssh a chance to timeout by itself if ($begin + $TIMEOUT + 2 > time()) { sleep(1); } else { # ssh did not terminate in time, kill it kill INT => $pid; return -1; } child_handler(); } return $CHECK_HOSTS->{$pid}; } else { # child my @args = (@SSH, "$host", "exit 0"); my_exec(\@args, "/dev/null"); } } sub check_hosts() { # don't check hosts in debug mode and only every so often # in regular mode return if defined $opt_d or ($check_host_ctr++ % 60 != 0); foreach my $host (keys %{$HOSTS}) { next if ($host =~ /^localhost/); my $retval = check_host($host); if ($retval == 0 and $HOSTS->{$host}{down}) { warn "*** $host is back\n"; start_ssh_master($host); foreach my $node (@{$HOSTS->{$host}{nodes}}) { mark_node_free($node); } delete $HOSTS->{$host}{down}; } elsif ($retval !=0 and $HOSTS->{$host}{down}) { # host is still down, do nothing } elsif ($retval == 0 and not $HOSTS->{$host}{down}) { # host is still up start_ssh_master($host) if not -e "$TMPDIR/ssh-$host"; } elsif ($retval != 0 and not $HOSTS->{$host}{down}) { # host is down mark_host_down($host); } } } sub update_after_slave($) { my $pid = shift; return unless defined $SLAVES->{$pid}; my $node = $SLAVES->{$pid}{node}; my $port = $SLAVES->{$pid}{port}; my $retval = $SLAVES->{$pid}{retval}; delete $SLAVES->{$pid}; delete $NODES->{$node}{pid}; if ($retval == 0) { print "<== built $port\n"; update_prereqs_of($port); delete $prereqs_of{$port}; } elsif ($retval == 1) { warn "*** failure building $port\n"; remove_port($port); } elsif ($retval == 255) { warn "*** build was killed, retrying $port later\n"; clear_lock($port); } else { warn "*** unexpected return code $retval from $node " . "for $port\n"; remove_port($port); } delete $PORTS->{$port}; mark_node_free($node); } sub find_free_node() { while (1) { child_handler(); reap_slaves(); check_hosts(); foreach my $n (keys %{$FREE_NODES}) { if (defined $FREE_NODES->{$n}) { delete $FREE_NODES->{$n}; return $n; } } sleep(1); } } # we failed to build $port, thus no $dep can be build (recursive) # if $dep is not build, it no longer depends on $pre sub remove_port($); sub remove_port($) { my $port = shift; for (my $i = 0; $i <= $#{$depend_on{$port}}; $i++) { my $dep = ${$depend_on{$port}}[$i]; foreach my $pre (@{$prereqs_of{$dep}}) { next if $pre eq $dep; remove_from_list(\@{$depend_on{$pre}}, $dep); $i-- if $pre eq $port; } remove_port($dep) unless $dep eq $port; } warn "*** will not build $port\n"; delete $prereqs_of{$port}; delete $depend_on{$port}; } # generate full dependency information # This is computational intensive! sub push_dep($$); sub push_dep($$) { my ($a, $b) = @_; foreach my $depends_on_a (@{$depend_on{$a}}) { foreach my $prereq_of_b (@{$prereqs_of{$b}}) { my $gotcha = 0; foreach my $p (@{$prereqs_of{$depends_on_a}}) { if ($p eq $prereq_of_b) { $gotcha = 1; last; } } next unless $gotcha == 0; push_dep($depends_on_a, $prereq_of_b) unless $depends_on_a eq $a and $prereq_of_b eq $b; } } push(@{$prereqs_of{$a}}, $b); push(@{$depend_on{$b}}, $a); } sub parse_dependency_file() { open(my $fh, "sort -u $opt_T |") or die "Could not open $opt_T: $!"; while (<$fh>) { chomp; my ($a, $b) = split /\s+/; # ensure $a and $b are really port specs and not gibberish # category/{subcategory/}*port{\,flavor}*{\,-subpackage} my $borked = 0; foreach my $p ($a, $b) { if (not defined $p or $p eq "") { warn "*** empty port spec in deplist\n"; $borked = 1; last; } if (not $p =~ /(\w*\/)+[-.\w]+(,\w+)*(,-[\w]+)*/) { warn "*** broken deplist entry: $p\n"; $borked = 1; } } next if $borked == 1; # ensure every port depends on itself, needed by build logic # ports depending on the key $depend_on{$a} = [$a] unless defined $depend_on{$a}; $depend_on{$b} = [$b] unless defined $depend_on{$b}; # ports, the key depends on $prereqs_of{$a} = [$a] unless defined $prereqs_of{$a}; $prereqs_of{$b} = [$b] unless defined $prereqs_of{$b}; if ($a ne $b) { if (defined $opt_e) { push_dep($a, $b); } else { push(@{$prereqs_of{$a}}, $b); push(@{$depend_on{$b}}, $a); } } } close($fh); } sub parse_hosts_file() { my $sysctl = "/sbin/sysctl -n hw.ncpu"; open(my $fh, "<", $HOSTS_FILE) or die "Could not open $HOSTS_FILE: $!\n"; while (<$fh>) { chomp; my $host = $_; my ($ncpu, $local); if (/^localhost/) { $ncpu = `$sysctl`; $local = 1; } else { $ncpu = `@SSH $host $sysctl`; $local = 0 } if (not defined $ncpu or $ncpu eq "") { warn "*** host $host does not answer, not using it\n"; next; } elsif ($ncpu > 1 and defined $opt_c) { $ncpu = 1; warn "*** only using one node on $host due to '-c'\n"; } $HOSTS->{$host}{nodes} = (); for (my $i = 0; $i < $ncpu; $i++) { my $node = "$host%$i"; push @{$HOSTS->{$host}{nodes}}, $node; mark_node_free($node); $NODES->{$node}{fifo} = "$TMPDIR/dpb-$node.log"; $NODES->{$node}{host} = $host; if ($local == 1) { @{$NODES->{$node}{shell}} = ("/bin/sh", "-c"); } else { @{$NODES->{$node}{shell}} = (@SSH, "-S", "$TMPDIR/ssh-$host", "$host"); } } } close($fh); } sub build_package($$$$) { my ($node, $port, $flavor, $fullport) = @_; my $pid = fork(); die "fork: $!" unless defined $pid; if ($pid > 0) { # parent $SLAVES->{$pid} = {}; $SLAVES->{$pid}{node} = $node; $SLAVES->{$pid}{port} = $fullport; $SLAVES->{$pid}{retval} = undef; $PORTS->{$fullport} = $pid; $NODES->{$node}{pid} = $pid; return; } else { # child $SIG{HUP} = "DEFAULT"; $SIG{INT} = "DEFAULT"; $SIG{TERM} = "DEFAULT"; $0 = "dpb [slave] - $port"; print "==> building $port"; print ", FLAVOR \"$flavor\"" if defined $flavor; print " on $node\n"; if (defined $opt_d) { sleep(1); } else { my $arg = "cd $PORTSDIR/$port && "; $arg .= "FLAVOR=\"$flavor\" " if defined $flavor; # if we lost contact to the node, a build of this # port might still be running; cleaning kills it $arg .= "$MAKE $MAKEFLAGS clean package"; my @args = (@{$NODES->{$node}{shell}}, $arg); start_logger($node); clear_packages($NODES->{$node}{host}) if defined $opt_c; my_exec(\@args, "$NODES->{$node}{fifo}"); } exit 0; } } sub update_prereqs_of($) { my $port = shift; return unless defined @{$depend_on{$port}}; # remove $port from lists of prerequisites foreach my $depending (@{$depend_on{$port}}) { next unless defined @{$prereqs_of{$depending}}; remove_from_list(\@{$prereqs_of{$depending}}, $port); } } sub remove_from_list(\@$) { my ($list, $entry) = @_; for (my $i = 0; $i <= $#{$list}; $i++) { if (${$list}[$i] eq $entry) { splice(@{$list}, $i, 1); $i--; } } } sub start_logger() { my $node = shift; my $fifo = $NODES->{$node}{fifo}; unless (-p $fifo) { system("mkfifo $fifo") and die "Cannot create $fifo: $!"; } my $pid = fork(); die "fork: $!" unless defined $pid; if ($pid > 0) { # parent return; } else { # child # dies on its own on EOF my @args = ("$LOGGER < $fifo"); my_exec(\@args, "/dev/null"); } } sub clean_up($) { # only remove self generated dependency file unlink($opt_T) if ref $opt_T; foreach my $node (keys %{$NODES}) { kill_ssh_master($NODES->{$node}{host}); unlink($NODES->{$node}{fifo}) if -p $NODES->{$node}{fifo}; # is there still anything building on this node? my $pid = $NODES->{$node}{pid}; next if not defined $pid; my $port = $SLAVES->{$pid}{port}; local $SIG{CHLD} = "DEFAULT"; clear_lock($port); } exit(shift); } # MAIN $SIG{HUP} = \&term_handler; $SIG{INT} = \&term_handler; $SIG{TERM} = \&term_handler; $0 = "dpb [master]"; # collect dependency data if (defined $opt_b or not defined $opt_T or not -f $opt_T) { my $arg = "cd $PORTSDIR && $MAKE "; unless (defined $opt_T) { $opt_T = new File::Temp( TEMPLATE => 'all-depends.XXXXXXXXXX', DIR => $TMPDIR, UNLINK => 0 ); } if (defined $opt_s and defined $opt_S) { die "-s and -S are mutually exclusive!"; } elsif (defined $opt_s) { # cwd is somewhere in the ports tree, just build from here $arg = "$MAKE "; } elsif (defined $opt_S) { die "SUBDIRLIST $opt_S not found!" unless (-f $opt_S); $arg .= "SUBDIRLIST=$opt_S "; } $arg .= "ECHO_MSG=: all-dir-depends > $opt_T"; print "==> creating dependency file\n"; system($arg) and die "$MAKE all-dir-depends: $!"; } else { print "==> using dependency file $opt_T\n"; } parse_dependency_file(); parse_hosts_file(); check_hosts(); my @keys_prereqs = (keys %prereqs_of); my @keys_PORTS = (); do { # sort ports by their importance, i.e. by the number of other # ports depending on them foreach my $k (sort {$#{$depend_on{$b}} <=> $#{$depend_on{$a}}} @keys_prereqs) { # only compile ports that don't have unbuilt dependencies if ($#{$prereqs_of{$k}} == 0) { my $node = find_free_node(); my ($port, $flavor); my @spec = split(/,/, $k); # do not try to build multiple SUBPACKAGEs of the same # port in parallel my $build_conflict = 0; $port = $spec[0]; # a build for a different subpackage might be running foreach (keys %{$PORTS}) { my @key_spec = split(/,/); if ($port eq $key_spec[0]) { $build_conflict = 1; } } for (my $i = 1; $i <= $#spec; $i++) { if (not $spec[$i] =~ /^-/ and defined $flavor) { $flavor = join(" ", $flavor, $spec[$i]); } elsif (not $spec[$i] =~ /^-/) { $flavor = $spec[$i]; } } if ($build_conflict == 0) { build_package($node, $port, $flavor, $k); } else { mark_node_free($node); next; } last; } } check_hosts(); child_handler(); reap_slaves(); # create new key set, taking currently building ports into account @keys_PORTS = (keys %{$PORTS}); @keys_prereqs = (); foreach my $k (keys %prereqs_of) { push(@keys_prereqs, $k) unless defined $PORTS->{$k}; } sleep(1); } while ($#keys_prereqs >= 0 or $#keys_PORTS >= 0); print "==> done, cleaning up\n"; clean_up(0);