#!@PERL@ # # aeintegratq - aegis integration manager # Copyright (C) 2005, 2006, 2008 Peter Miller. # # Copyright (C) 1998-2006 Endocardial Solutions, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see # . # # # Automatic integration script for aegis # - Handles the normal stages of integration automatically. # # - Can do multiple integrations serially without resorting # to cron jobs at short intervals, although that works too. # # has options for # - controlling the order or subset of changes to integrate # - setting changes as "precious" so they just stop rather than fail # - picking up an integration which is part way through # # - will execute optional "hooks" at each stage, which may be used # for many purposes under control of the administrator. # # - Requires arch_hosts to perform integrations on/for other architectures # including integration host being different architecture than project # target architecture. # If arch_hosts not installed, will execute all locally which will # work fine for single architecture projects. # # - If aelogres exists, it will be called at build/test failure # to gather more sophisticated report of failure for email. # If not, report will contain a simple tail of the log. # # - uses sound_all_machines, if available, to make audio # announcement of pass/fail of each change. # # @configure_input@ # # Grab useful locations from configure $BinDir = "@bindir@"; # configure does not expand @comdir@ directly so use sharedstatedir $ComDir = "@sharedstatedir@"; # Configure additions? $TmpDir = "/var/tmp"; # base mail program that takes all info (to: subj: etc) on stdin $SendMail = "/usr/lib/sendmail"; # Define the preferred integration host # the aeib/aeipass take place there and may work better on the file server # the -ib and/or -ip options can specify a remote server if desired. $IntegrationHost = "_AEGIS_FILE_SERVER_"; $ProgramName = "aeintegratq"; require 5.004; use Getopt::Long; Getopt::Long::Configure( "no_ignore_case_always", "auto_abbrev" ); $ENV{'SHELL'} = "/bin/sh"; # set signal handlers so lock file is cleaned up on kill $SIG{'INT'} = \&cleanup_and_quit; $SIG{'QUIT'} = \&cleanup_and_quit; $SIG{'TERM'} = \&cleanup_and_quit; my %Option = ( ibserver => undef, ipserver => undef, Trace => 0, ); sub usage { warn <<"EO_USAGE"; # Usage: $ProgramName [options] project_name [name...] # Accepts options: # -h Help - show this information # -H Help - show this plus all helpful comment information # -a run on Any machine (normally only $IntegrationHost) # -s run remote operations via ssh (default rsh) # -n No action - just tell what would be done # -ib - Specify (remote) server on which ibegin be done # or -ib "" - request server be determined as host for project baseline # -ip - Specify (remote) server on which ipass be done # or -ip "" - request server be determined as host for project baseline # -display - use given X display # or -display "" - use display as exists in environment # -k Keep the scripts and report files # -K Keep the temp file even if integration passes # -l Loop to get more changes if available - stops when nothing done # -M Minimum -M 33,99 run given changes -minimum # -P Precious -P 33,99 do not actually fail - just report # -R Ready -R 29,45 specify order/subset # -S Pick up at stage (diff|build|test|integrate) # -c change_num - specify change to integrate at Stage # -p project_name - specify single project name # NOTE: if custom options such as -P -R -S -c -p are given # only a single project may be integrated since the # options would be meaningless to the next project given. EO_USAGE exit 1; } GetOptions( \%Option, "help", "Help", "any", "ibserver=s", "ipserver=s", "display=s", "ssh", "noaction", "keep_scripts", "Keep_temp", "loop", "Minimum=s", "Precious=s", "Ready=s", "Stage=s", "change=i", "project=s", "Trace=i", "verbose", ) || usage(); # if they asked for help - just do it if ( defined($Option{help}) ) { &usage; } if ( defined($Option{Help}) ) { system "sed -n -e 's/^ *#:#/ #/p' $0"; &usage; } $RemoteCommand = defined($Option{ssh}) ? "ssh" : "rsh"; # Convert the -P and -M options to a hash for future reference %Precious = (); %Minimums = (); %Ready = (); hash_option("-P", $Option{Precious}, \%Precious) if defined($Option{Precious}); hash_option("-M", $Option{Minimum}, \%Minimums) if defined($Option{Minimum}); hash_option("-R", $Option{Ready}, \%Ready) if defined($Option{Ready}); # allow -c to give a single change with familiar option hash_option("-c", $Option{change}, \%Ready) if defined($Option{change}); $HookPath = $ENV{'HOME'} . "/integration_hooks"; # define some locations for logging and such if ( defined( $ENV{'AEGIS_TEST_DIR'} ) ) { $LogFile = "$ENV{'AEGIS_TEST_DIR'}/integrate.log"; $HookPath = $ENV{'AEGIS_TEST_DIR'}; } elsif ( -d "$ComDir/integrations" ) { $LogFile = "$ComDir/integrations/integrate.log"; }else{ $LogFile = "$ENV{'HOME'}/integrate.log"; } $Q_status = 0; # unless/until something goes wrong # figure out who and where we are # $Integrator = (getpwuid($<))[0]; # Verify running on the chosen file server host, unless none defined unless( $IntegrationHost =~ /AEGIS_FILE_SERVER/ ) { chop($Host = `uname -n`); # # enforce $IntegrationHost unless only looking or -any option given # if ( ($Host ne "$IntegrationHost") && !(defined($Option{any}) || defined($Option{noaction})) ) { die "Integrations must be run on $IntegrationHost\n"; } } # #:# Set up the Project list. Usually just be one project name #:# If -p project is given we enforce only a single project for the run. #:# Otherwise multiple projects (or multiple occurances of a project) #:# may be given. #:# Multiple occurances may be useful if you want to start integrating #:# but may end more changes while integration is running. #:# When done with the current list, aeintegratq will look again, #:# as many times as the project name is given. #:# Note that any failure puts an end to the happy situation. # if ( defined($Option{project}) ) { @Projects = ( $Option{project} ); } else { @Projects = @ARGV; } # # set up stage name/status definitions # %Stages = ( "begin" => 1, "difference" => 2, "build" => 4, "test" => 8, "integrate" => 16 ); $StageStatus = 0; if ( defined($Option{Stage}) ) { if ( scalar(keys %Ready) ) # must provide -R n or -c n to specify { # what change to run using -S stage my $start = ''; $stat_bit = 1; # get stage names in order - to check or to diagnose errors my @names = sort {$Stages{$a} <=> $Stages{$b}} keys %Stages; foreach my $stage (@names) { if ( $stage =~ /$Option{Stage}/ ) { $start = $stage; last; } warn sprintf("skipping %-12s set %d\n", $stage, $stat_bit); $StageStatus |= $stat_bit; $stat_bit <<= 1; } if ( $start ) { warn "pre-setting starting stage to $start\n"; } else { warn "Huh? illegal -S $Option{Stage} given - no such stage\n"; warn "Legal names: ", join(',', @names), "\n"; exit 1; } } else { warn "-S stage requires -c change_number\n"; exit 1; } } # # # # set our base temp file name $Tmp = "$TmpDir/intq.$$"; # Set up some things needed for remote execution. # - a base script file name / increment $ScriptFile = "." . $Integrator . ".sh." . $$; $ScriptIncr = "aaa"; @ExecEnv = (); if ( scalar(@Projects) ) { # - a list of the relevant environment variables @ExecEnv = &sift_user_env; # A hash structure for change info needed # first 3 are used for any/all projects/changes # "dev_report" => "filename" # "test_report" => "filename" # "arch_report" => "filename" # rest are calcualted for each project/change # "developer => "who" # "logfile" => "path"; # "build_archs" => [ ] # "test_archs" => [ ] # "tests_req" => [ ] my %info = ( "dev_report" => &dev_rpt_file($Tmp), "test_report" => &test_rpt_file($Tmp), "arch_report" => &arch_rpt_file($Tmp), ); my( $project, $change ); my @projects = @Projects; my $q_size = 0; # OK lets process the given project[s] while(($Q_status == 0) && ($project = shift(@projects)) ) { $project = &canonical_name($project); $Q_status = 0; # want to integrate other projects after 1 fails # generate and store lock file name my @changes = &ready($project); $q_size += scalar(@changes); if( defined( $Option{verbose} ) ) { &write_log(sprintf("run %d changes in %s", scalar(@changes), $project)); } while(($Q_status == 0) && (scalar(@changes)) ) { my $entry = shift(@changes); my $flags; ($change, $flags) = split(/:/, $entry); chop(my $stage = `aesub -p $project -c $change \'\${state}\'`); if( ( ( $stage =~ /awaiting_integration/ ) ) || ( ( $stage =~ /being_integrated/ ) && $StageStatus ) ) { # get architecture lists, and check support for same if ( &check_arch_support($project, $change, \%info) ) { # set the name of the temp file to use $info{"logfile"} = "$Tmp.$project.$change"; # Get relevant info re: what must be done for this change # first the developer's name @tmplist = &aegis_report($project, $change, $info{"dev_report"}); $info{"developer"} = shift( @tmplist ); # And the test types required on the change @tmplist = &aegis_report($project, $change, $info{"test_report"}); @{ $info{"tests_req"} } = @tmplist; # Got all info now - go do it $Q_status = &integrate($project, $change, $flags, \%info); } } else { &write_log("Skipping $project $change $stage - bad state"); } } # if -loop was given and we are done with project list # - start over and run em again if( ( defined($Option{loop}) ) # -loop was given && ( $#projects < 0 ) # done with this list && ( $q_size > 0 ) # this queue did something && ( $Q_status == 0 ) # successfully ) { push(@projects, @Projects); $q_size = 0; # reset for next loop &write_log("Loop again for " . join(" ", @Projects) ); } } # clean up our report generation program files unlink $info{"dev_report"}, $info{"test_report"}, $info{"arch_report"} unless ($Option{keep_scripts}); # Now call the end/fail run hook in case we need to schedule # another run, clean up stuff, page someone, or whatever. # Pass project and change number, which are actually last ones # and may only matter to the .fail hook if ( $Q_status == 0 ) { $Q_status = &run_hooks($project, $change, "end"); } else { $Q_status = &run_hooks($project, $change, "fail"); } } else { &usage; } exit $Q_status; # process of integrating a single change in one project # these may, in future, be created as child processes to do # more than one at a time, in disparate projects of course.. # Return result of sub stages, or 1 if ib fails sub integrate { my($project, $change, $flags, $info) = @_; my($result, $intdir); &preview($project, $change, $flags, $info) if defined($Option{noaction}); if ( ($intdir = &integrate_begin($project, $change, $flags, $info)) && ($intdir ne "_failed_ib_") ) { if ( (($StageStatus & $Stages{"difference"}) || &difference($project, $change, $intdir, $info)) && (($StageStatus & $Stages{"build"}) || &build( $project, $change, $intdir, $info)) && (($StageStatus & $Stages{"test"}) || &test( $project, $change, $intdir, $info)) ) { $result = &pass_integration($project, $change, $info); } else { $result = &fail_integration($project, $change, $info); } } else { &write_log("FAILED integrate begin: $project $change"); $result = 1; } $result; } sub integrate_begin { my($project, $change, $flags, $info) = @_; my $intdir = undef; my $errors = 0; my $logf = $info->{"logfile"}; my $aecmd = "aegis -ib -p $project -c $change"; if ( $StageStatus & $Stages{"begin"} ) # picking up an open integration { # must find existing integration directory chop($intdir = `aegis -cd -p $project -c $change -terse`); if ( $intdir =~ m=/delta\d+= ) { &write_log("pick up integration of $project $change"); } else { &write_log("no $aecmd - cannot pick up"); $errors++; } } elsif ( &check_space($project) ) { # # log entries # &write_log("began integration of $project $change $flags"); # # try the pre_ib hook # $errors = &run_hooks($project, $change, "pre_ib"); unless( $errors ) { # run aegis -ib send errs to $logf # If option was given for begin server use it. # if( my $server = given_server("ibserver", $project) ) { $errors = &host_cmd("$aecmd $flags", $server, $logf); } else # run command locally { $errors = &system_cmd("$aecmd $flags", $logf); } # # If the ib failed there is nothing we can do # if( $errors ) { &write_log("Failed:$aecmd"); } else { # # get the actual integration directory. # Have to do this at the last minute because the # integration begin makes a new directory # that we do not know about until now # if ( $Option{noaction}) { $intdir = $ENV{'HOME'}; # to provide existing directory }else{ chop($intdir = `aegis -cd -p $project -c $change -ter`); } $errors = &run_hooks($project, $change, "ib"); if( $errors ) # if the hook finds errors, undo the ib { &system_cmd("aegis -ibu -p $project -c $change", $logf); } } } } else { # gripe &write_log("Failed aeib $project $change No Space"); $errors++; } $intdir = "_failed_ib_" if $errors; $intdir; } # # difference the change # sub difference { my($project, $change, $chdir, $info) = @_; my $errors = 0; my($arch, $log_message); my $aecmd = "aegis -diff -p $project -c $change"; $errors = &run_hooks($project, $change, "pre_d"); unless ( $errors ) { &write_log($aecmd); $errors = &system_cmd("cd $chdir && $aecmd -v -nolog", $info->{"logfile"}); if ( $errors ) { &write_log("Failed $aecmd"); } else { # on success run hooks if defined $errors = &run_hooks($project, $change, "d"); } } $errors == 0; } sub build { my($project, $change, $chdir, $info) = @_; my $arch; my $errors = 0; # log file for all output my $logf = $info->{"logfile"}; # list of architectures my @archs = @{ $info->{"build_archs"} }; if ( @archs ) { $errors = &run_hooks($project, $change, "pre_b"); } else # no architectures is an error { $errors = 1; } # run aegis -build on each - send errs to $logf - stop on first fail while( ($errors == 0) && ($arch = shift(@archs)) ) { chomp($arch); my $build_host = &find_host("-b", $arch); if ( $build_host ) # found one { my $aecmd = "aegis -build -p $project -c $change"; # log entries my $log_message = "$aecmd ($arch $build_host)"; $errors = &run_hooks($project, $change, "pre_" . $arch . "b"); &write_log($log_message); unless ( $errors ) { $errors = &host_cmd("cd $chdir;$aecmd -v -nolog", $build_host, $logf); } # allow one re-try in case of false failures # only with a strategy script if such a strategy is defined if ( $errors ) { my $strategy = $ENV{'HOME'} . "/strategy." . $project; if ( -x $strategy ) # a strategy is defined { my $retry = "with $strategy"; # run the strategy for whatever it does $errors = &host_cmd("cd $chdir;$strategy", $build_host, $logf); if ( $errors ) # strategy failed { &write_log("Oops $log_message $retry - failed"); } else { # log it then go ahead and run another build &write_log("Oops retry $log_message $retry"); $errors = &host_cmd("cd $chdir;$aecmd -v -nolog", $build_host, $logf); if ( $errors ) # still errors - too bad { &write_log("Failed retry $log_message $retry"); } else { &write_log("OK retry $log_message $retry"); } } } else # no strategy defined - just report failure { &write_log("Failed build $log_message"); } if ( $errors ) # still - nothing worked?, gather results { $errors = &gather_results($errors, $chdir, $logf); } } unless( $errors ) { $errors = &run_hooks($project, $change, $arch . "b"); } } else { &write_log("Error:No build host for $project $change $arch"); $errors++; } } # on success run hooks if defined unless ( $errors ) { $errors = &run_hooks($project, $change, "b"); } $errors == 0; } sub test { my($project, $change, $chdir, $info) = @_; my $errors; my $tests_run = 0; # log file for all output my $logf = $info->{"logfile"}; # list of architectures my @archs = @{ $info->{"test_archs"} }; if ( @archs ) { $errors = &run_hooks($project, $change, "pre_t"); } else # no architectures is an error { $errors = 1; } # stop on first failure while( ($errors == 0) && ($arch = shift(@archs)) ) { # log entries &write_log("testing $project $change for $arch"); my @test_types = @{ $info->{"tests_req"} }; $errors = ($#test_types < 0) ? 1 : 0; # high hopes if list worked while( ($errors == 0) && ($test = shift(@test_types)) ) { $test =~ s/;.*$//; my($test_type, $value) = split(/=/, $test); # # example output is: # test=true; # test_baseline=false; # regression_test=true; # if ( $value eq "true" ) { my($test_host, $test_args); $test_host = &find_host("-t", $arch); if ( $test_host ) # found one { $tests_run++; if ( $test_type eq "test" ) { $test_args = "-test"; } elsif ( $test_type eq "test_baseline" ) { $test_args = "-test -bl"; } elsif ( $test_type eq "regression_test" ) { $test_args = "-test -reg"; } else { $errors++; } if ( $errors ) { &write_log("Error: unknown test type:$test_type"); } else { my $aecmd = "aegis $test_args -p $project -c $change"; my $log_message = "$aecmd ($arch $test_host)"; &write_log($log_message); # KLUGE: Signal to some tests that this is not being # KLUGE: run from a console my $display = "INTEGRATE_SCRIPT"; # Allow a better value to be used if( defined($Option{display}) ) # -display given { if( $Option{display} ) # not empty { $display = $Option{display}; } else # use current value { # from environment OR base local display $display = $ENV{DISPLAY} || ":0.0"; } } # build up the command line to use. my $cmd = "cd $chdir && " . "DISPLAY=$display " . "$aecmd"; # Ok now run it $errors = &host_cmd($cmd, $test_host, $logf); if ( $errors ) { $errors = &gather_results($errors, $chdir, $logf); } } } else { &write_log("No test host for $project $change $arch"); $errors++; } } } } unless( $tests_run > 0 ) { &write_log("$project $change is exempt from all tests!"); } unless ( $errors ) { # on success run hooks if defined # Note that this hook could test for other kinds of test failures $errors = &run_hooks($project, $change, "t"); } $errors == 0; } sub pass_integration { my($project, $change, $info) = @_; my $errors = 0; # tempfile for all output my $logf = $info->{"logfile"}; my $ipass_log = $logf . ".ip"; my $aecmd = "aegis -ipass -p $project -c $change"; my $log_message = $aecmd; $errors = &run_hooks($project, $change, "pre_ip"); unless( $errors ) { # log entries &write_log($log_message); # # run aegis -ipass # If option was given for pass server use it. # if( my $server = given_server("ipserver", $project) ) { $errors = &host_cmd($aecmd, $server, $ipass_log); } else # run command locally { $errors = &system_cmd($aecmd, $ipass_log); } unless ( $errors ) { $errors = &run_hooks($project, $change, "ip"); } } if ( $errors ) { &write_log("Failed $log_message"); &mailFile($ipass_log, "ipass fail $project $change", $Integrator); &sound_off("fail", $project, $change, $info->{"developer"}); } else { my $xtras = $logf . ".*"; &sound_off("pass", $project, $change, $info->{"developer"}); system_cmd("rm -f $logf $xtras", "") unless ($Option{Keep_temp}); &write_log("completed integration of $project $change"); } $errors; } sub fail_integration { my($project, $change, $info) = @_; my $errors = 1; # the failure itself is an error to stop queue my $log_message; my $developer = $info->{"developer"}; # log file for all output my $logf = $info->{"logfile"}; my $failf = $logf . ".fail"; my $resf = $logf . ".res"; my $aecmd = "aegis -ifail -p $project -c $change"; # better subset of results may have been gathered by build/test # and should be mailed if available # If not no failf exists, so get the default info # if ( ! -s $failf ) { # subset of results logged by process &system_cmd("tail -20 $logf", "$failf"); } if ( defined($Precious{$change}) || defined($Precious{"all"}) ) { $who_to = $Integrator; # who to notify $log_message = "aegis -ifail (precious) $project $change"; } else { $who_to = $developer; &run_hooks($project, $change, "pre_if"); # to do what? # Now actually run aegis -ifail $errors = &system_cmd("cd /;$aecmd -f $failf", $resf); # Now a failure. If we actually failed the change, it is only # a failure if the ifail fails.... if ( $errors ) { $log_message = "Failed $aecmd"; &mailFile($resf, "ifail fail $project $change", $Integrator); } else { $log_message = "$aecmd"; } } # Now mail the digested results, either way &mailFile($failf, "Integration $project $change Failed", $who_to); # # log entries # &write_log($log_message . ", results mailed to $who_to"); # but it always sounds like a failure ;^) &sound_off("fail", $project, $change, $developer); warn "rats - results left in $logf\n"; &run_hooks($project, $change, "if"); # to notify or whatever # clean up rest unless keep option given unlink $failf unless ( $Option{keep_temp} ); $errors; } sub dev_rpt_file { my $rptf = shift; $rptf .= ".dev"; if ( open(RPT, "> $rptf") ) { print RPT <<'EO_RPT'; columns(80); auto cs; cs = project[project_name()].state.branch.change[change_number()]; auto developer; developer = "nobody"; auto h; for (h in cs.history) if (h.what == develop_end) developer = h.who; print(developer); EO_RPT close(RPT); } else { die "unable to open $rptf:$!\n"; } $rptf; } sub test_rpt_file { my $rptf = shift; $rptf .= ".test"; if ( open(RPT, "> $rptf") ) { print RPT <<'EO_RPT'; columns(80); auto cs; cs = project[project_name()].state.branch.change[change_number()]; print("test=" ## !cs.test_exempt ## ";"); print("test_baseline=" ## !cs.test_baseline_exempt ## ";"); print("regression_test=" ## !cs.regression_test_exempt ## ";"); EO_RPT close(RPT); } else { die "unable to open $rptf:$!\n"; } $rptf; } sub arch_rpt_file { my $rptf = shift; $rptf .= ".arch"; if ( open(RPT, "> $rptf") ) { print RPT <<'EO_RPT'; columns({ name = "Architecture\n----------"; right = 0; }); auto cs; cs = project[project_name()].state.branch.change[change_number()]; auto arch; if ( cs.config.build_covers_all_architectures ) { print("build_covers_all_architectures=true\n"); } else { print("build_covers_all_architectures=false\n"); } if ( cs.config.test_covers_all_architectures ) { print("test_covers_all_architectures=true\n"); } else { print("test_covers_all_architectures=false\n"); } for (arch in cs.architecture) print(arch); EO_RPT close(RPT); } else { die "unable to open $rptf:$!\n"; } $rptf; } sub ready { my $project = shift; # allow -R n,n2,n3 my @i; my @ready = (); # Get list of changes from aegis. May override with -R or -c option # but doing it first guarantees that automount is complete # to prevent error of delta directory being created root:root ownership # chomp(@i = `aegis -ibegin -list -p $project -terse`); if( scalar( keys %Ready ) ) # a subset was requested { # Need to also consider changes being integrated chomp(my @ci = `aegis -ipass -list -p $project -terse`); @i = sort { $Ready{$a} cmp $Ready{$b} } grep { defined($Ready{$_}) } @i, @ci; } # # this is where to check for requested -minimum integrations # and build the actual list - pushing mins to the end # while( my $n = pop(@i) ) { chomp($n); if ( defined($Minimums{$n}) ) { push(@ready, "$n:-minimum -v"); }else{ unshift(@ready, "$n:-v"); } } @ready; } sub mailFile { my($fname, $subj, $who) = @_; if ( $Option{noaction} ) { warn "mailing $fname -s $subj to $who\n"; } else { if ( open(DAT, "< $fname") ) { if ( open(MAIL, "| $SendMail -t") ) { print MAIL "To: $who\n"; print MAIL "Subject: $subj\n"; print MAIL "\n"; while() { print MAIL $_; } close(MAIL); } else { # bitch warn "Unable to open pipe to $SendMail:$!\n"; } close(DAT); } else { # bitch warn "Unable to open data file $fname:$!\n"; } } } # entry in the integration log as necessary sub write_log { my $msg = shift; chop(my $date = `date +"%d %b %T"`); if ( $Option{noaction} ) { warn "$date $msg\n"; } else { if ( open(LOG, ">> $LogFile") ) { print LOG "$date $Integrator $msg\n"; close(LOG); } else { warn "unable to open $LogFile:$!\n"; } } } sub system_cmd { my($cmdstring, $resfile) = @_; my $sysres; my $logfile = ($resfile =~ /\w+/) ? $resfile : "/dev/null"; if ( $Option{noaction} ) { $sysres = 0; # no action always succeeds warn " -sh- running:$cmdstring to:$logfile\n"; } else { $sysres = system("$cmdstring >> $logfile 2>&1"); if ( $sysres == 0xff00 ) { warn "command ($cmdstring) failed"; } elsif ( $sysres > 0x80 ) { $sysres >>= 8; } elsif ( $sysres & 0x80 ) { my $sig = $sysres & ~0x80; warn "command ($cmdstring) core signal $sig\n"; } } $sysres; } sub host_cmd { my($cmdstring, $host, $logfile) = @_; my $sysres; if ( $host eq "localhost" ) { $sysres = &system_cmd($cmdstring, $logfile); } elsif ( $Option{noaction} ) { $sysres = 0; # no action always succeeds my $info = $logfile ? "with $logfile" : "no-tmp"; warn " -$RemoteCommand-$host- $cmdstring $info\n"; $sysres = 0; } else { $ScriptIncr ++; my $script = $ScriptFile . $ScriptIncr; my $status = $script . ".status"; my $cwd; if ( open(SCRIPT, "> $script") ) { local($,) = "\n"; print SCRIPT "#!/bin/sh\n"; print SCRIPT @ExecEnv, "\n\n"; # exec cmdstring in a subshell # so the current directory stays current for the status file. print SCRIPT "( $cmdstring )\n"; print SCRIPT 'echo $? > ' . $status . "\n"; close(SCRIPT); chmod 0777, $script; chop($cwd = `pwd`); # the current directory $cwd =~ s:^/tmp_mnt/:/:; # trim the blanketty blank automount # preload the status file with non-success code # so that if the rsh|ssh itself fails result will be failure. my $rmtcmd = "echo 99 > $status;" . "$RemoteCommand $host 'cd " . $cwd . ";/bin/sh -c ./" . $script . "'"; if ( $logfile =~ /\w+/ ) { $rmtcmd .= " >> $logfile 2>&1"; } # Run the built up command string system("$rmtcmd"); # # pick up the status of the remote command # from the status file # chop($sysres = `cat $status`); # # And clean up droppings # unlink $script, $status unless ($Option{keep_temp}); } else { warn "Error - unable to open script file:$!\n"; $sysres = 2; } } $sysres; } # paths needed by remote execution stuff # perhaps later we will use the existing ENV and just # remove some cruft - hence the name. # sub sift_user_env { my @env = (); my %uniq = (); my %lang = ( "LANG" => "C", "LANGUAGE" => "en_US" ); # defaults my @path = (); # build a path that covers the system stuff on most system types # also deal with configuration directory which # might be different on host compared to target machine. # Most should be covered by /bin /usr/bin /usr/local/bin /opt/local/bin # If BinDir is one of those, it will come first and the dup eliminated my @paths = ( # the venerable (and preferable) $BinDir $BinDir, # common "prefix" directories "/usr/local/bin", "/opt/local/bin", "/usr/bin", "/bin", # basic system directories "/usr/sbin", "/usr/etc", # bsd tools on sgi|solaris|others "/usr/bsd", "/usr/ucb", # build tools on solaris "/usr/ccs/bin", ); # Take pains to eliminate dups foreach my $part (@paths) { push(@path, $part) unless defined( $uniq{$part} ); $uniq{$part}++; } push(@env, "PATH=" . join(":", @path) . ";export PATH"); push(@env, "SHELL=/bin/sh;export SHELL"); push(@env, "TMPDIR=$TmpDir;export TMPDIR"); push(@env, "TMP=$TmpDir;export TMP"); unless( defined( $ENV{'AEGIS_TEST_DIR'} ) ) { # Now take minimal basic locale environment from user - skip LC_* foreach my $ev (keys %ENV) { if( $ev =~ /^LANG/ ) { $lang{$ev} = $ENV{$ev}; } } foreach my $ev (keys %lang) { push(@env, sprintf("%s=%s;export %s", $ev, $lang{$ev}, $ev)); } } @env; } sub sound_off { my($type, $project, $change, $developer) = @_; # so we can personalize it my($sound, @sounds); # feature depends on existance of program "sound_all_machines" if ( -x "$BinDir/sound_all_machines" ) { my $utime = time(); # the unix time my $hour = (localtime($utime))[2]; # only make noise if time is 07:00 - 19:00 if ( ($hour >= 7) && ($hour <= 19) ) { # check for a personal one $sound = $ComDir . "/sounds/" . $developer . "_" . $type; # a set of numbered files is also allowed my $lsargs = $sound . ".[0-9]*"; @sounds = `/bin/ls -1 $lsargs 2> /dev/null`; my $limit = scalar(@sounds); if ( $limit > 0 ) # found a set of files { # pick one at random my $pid; chop($pid = `echo \$\$`); # try simple method - use next pid my $n = (($utime % $pid) % $limit); # and apply mod $sound .= ".$n"; } unless ( -s $sound ) # if not there then use old default { $sound = $ComDir . "/sounds/integration_" . $type; } &system_cmd("sound_all_machines $sound", ""); } } } sub check_space { my $project = shift; #stub - always OK now - check by size 1; } # Look for a "hook" under the integrators home directory (HookPath) # its name should be . # or aeintegratq.end or aeintegratq.fail # where project can be either an alias or a canonical name. # Try the argument name first which may be an alias. # Lacking that we get the canonical name and try it. # This will be helpful for projects with aliases, so we # don't have to maintain both the alias and the aegis name # in case aeintegratq is invoked with either form # Lacking the canonical name, strip the last branch component (if present) # and try the parent. It is more simple to use branches if we don't # have to duplicate any/all hooks for every one. sub run_hooks { my($project, $change, $stage) = @_; my $errors = 0; if ( -d $HookPath ) { my $hookfile; if ( $stage =~ /end|fail/ ) { $hookfile = "$HookPath/$ProgramName.$stage"; } else { $hookfile = "$HookPath/$project.$stage"; # If exists as given use it unless( -x $hookfile ) { # try a cononical name my $cname = &canonical_name($project); $hookfile = ''; while( $cname ) { $hookfile = "$HookPath/" . "$cname" . ".$stage"; if ( -x $hookfile ) { $cname = ''; # no more need to check } else { # attempt to strip last branch component if ( $cname =~ /(.+)\W\d+$/ ) { $cname = $1; } else { $hookfile = ''; # did not find one $cname = ''; # and nothing more to try } } } } } if ( $hookfile && -x $hookfile ) # came up with something usable { &write_log("run hook: $hookfile"); $errors = &system_cmd("$hookfile $project $change", ""); } } $errors; } sub aegis_report { my($project, $change, $rptfile) = @_; my $try = 3; # allow try up to 3 times my @result = (); my $bl_dir; chop($bl_dir = `aegis -cd -bl -ter -p $project`); # allow a couple retries to get the project directory mounted until( -s "$bl_dir/config" || ($try-- <= 0) ) { sleep(1); } if ( $try ) { chomp(@result = `aereport -p $project -c $change -f $rptfile -terse`); } @result; } # Use aesub to dereference project name # if alias it will return canonical name, otherwise same name # sub canonical_name { my $projname = shift; chop(my $canonical = `aesub -p $projname -bl \'\${project}\'`); $canonical; } # Find a host on which to run - use arch_hosts to ensure it is up/free # Also check for bogus names as a "belt and suspenders" paranoia. # # If arch_hosts not available, returns localhost so execution is local # If project requires multiple architectures, arch_hosts is Required sub find_host { my($job, $arch) = @_; my $host = ''; my $typeres = `/bin/sh -c \"type arch_hosts 2>/dev/null\"`; if ( ( $? == 0 ) && !defined($ENV{'AEGIS_TEST_DIR'} ) ) { my $arch_host_cmd = (split(/\s+/, $typeres))[2]; my $tries = 0; while( ($host eq '') && ($tries++ < 4) ) { chop($host = `$arch_host_cmd -f $job -q 1 -a $arch`); unless( ($? == 0) && (gethostbyname $host) ) { &write_log("find_host $job $arch failed<$host>"); $host = ''; } } } else { $host = "localhost"; } $host; } # Checks to see if a program called aelogres is available # if so runs it in the change directory to search aegis.log # Puts any such output in $logf.fail which will be used # for the ifail input. # If aelogres is not available, does nothing and the # ifail will use a default subset of data for message. # Note that we or the results sub gather_results { my($errors, $chdir, $logf) = @_; # If aelogres is available it can produce a better report # of the problem and/or report as errors things that aegis # would not, such as considering compiler warnings # if ( -x "$BinDir/aelogres" ) { my $failf = $logf . ".fail"; my $rescmd = "cd $chdir && " . "aelogres -i aegis.log"; $errors |= &system_cmd($rescmd, "$failf"); } $errors; } sub check_arch_support { my($project, $change, $info) = @_; my $ok = 0; # no go until we find out my($build_covers_all, $test_covers_all, @archlist); # get architecture of machine we are running on # must use aesub since projects can name architectures anything # and might even be changing the name in this change chop(my $hostarch = `aesub -p $project -c $change \'\${architecture}\' 2> /dev/null`); # hostarch will be empty if project does not define this archtecture # Which is a sure indication that arch_hosts is required. if ( $hostarch || -x "$BinDir/arch_hosts" ) { # get architecture list required for this change @archlist = &aegis_report($project, $change, $info->{"arch_report"}); # take off the first two entries # First is the "build_covers_all_architectures=(true|false)" $build_covers_all = shift(@archlist); # Second is the "test_covers_all_architectures=(true|false)" $test_covers_all = shift(@archlist); if ( @archlist ) { $ok = 1; # Now check each - even though it may be a list of 1 foreach my $arch (@archlist) { unless( ($arch eq $hostarch) || -x "$BinDir/arch_hosts" ) { $ok = 0; # bummer - can't do it } } } } if ( $ok ) { if ( $build_covers_all =~ /true/ ) # all in one go? { # Take the first architecture which will # allow project admins to control on which arch to build @{ $info->{"build_archs"} } = ( $archlist[0] ); } else { @{ $info->{"build_archs"} } = @archlist; } if ( $test_covers_all =~ /true/ ) # all in one go? { # Take the first architecture which will # allow project admins to control on which arch to build @{ $info->{"test_archs"} } = ( $archlist[0] ); } else { @{ $info->{"test_archs"} } = @archlist; } } else { warn "No architecture support for $project $change\n"; @{ $info->{"test_archs"} } = (); @{ $info->{"build_archs"} } = (); } $ok; } sub hash_option { my( $opt, $val, $href ) = @_; $val =~ tr/[A-Z]/[a-z]/; # in case all is given mixed case my @vals = split(/,/, $val); my $index = "aa"; # ascii index for easy sort # Store values in hash using $index to keep track of order foreach my $v (@vals) { if( $v =~ /all/ ) { $href->{$v} = "a"; } elsif( $v =~ /\d+/ ) { $href->{$v} = $index++; } } # Make sure the option had a legal argument given unless( scalar( keys %$href ) ) { warn "$opt requires keyword \"all\" or 1 or more change numbers\n\n"; &usage; } } # look at options to see if one was given for ibegin or ipass sub given_server { my($option, $project) = @_; my $server = undef; # was the option given at all if( defined($Option{$option}) ) { if( $Option{$option} ) # a name was given { $server = $Option{$option}; } else # empty string given - find the server { chop(my $baseline = `aesub -p $project -bl \'\${baseline}\'`); if( open(DF, "df -k $baseline |") ) { while() { if( my( $s ) = m=(\w[^:]+):/\w+= ) { $server = $s; last; } } close(DF); } else { warn "Unable to run df for $baseline:$!\n"; } } } return $server; } sub preview { my($project, $change, $flags, $info) = @_; printf("integrating %s %s with %s\n", $project, $change, $flags); printf("info contains:\n"); printf(" developer => %s\n", $info->{"developer"}); printf(" logfile => %s\n", $info->{"logfile"}); printf(" build_archs => %s\n", join(",", @{ $info->{"build_archs"} })); printf(" test_archs => %s\n", join(",", @{ $info->{"test_archs"} })); printf(" tests_req => %s\n", join(" ", @{ $info->{"tests_req"} })); } sub cleanup_and_quit { exit 0; } # EOF script/aeintegratq.in