be324687d8f85aef9053ff429d376999b7173001
[buildfarm-client.git] / run_build.pl
1 #!/usr/bin/perl
2
3 =comment
4
5 Copyright (c) 2003-2010, Andrew Dunstan
6
7 See accompanying License file for license details
8
9 =cut 
10
11 ####################################################
12
13 =comment
14
15  NAME: run_build.pl - script to run exim buildfarm
16
17  SYNOPSIS:
18
19   run_build.pl [option ...] [branchname]
20
21  AUTHOR: Andrew Dunstan
22
23  DOCUMENTATION:
24
25   See http://wiki.exim.org/wiki/PostgreSQL_Buildfarm_Howto
26
27  REPOSITORY:
28
29   https://github.com/EximBuildFarm/client-code
30
31 =cut
32
33 ###################################################
34
35 use vars qw($VERSION); $VERSION = 'REL_0.1';
36
37 use strict;
38 use warnings;
39 use Config;
40 use Fcntl qw(:flock :seek);
41 use File::Path;
42 use File::Copy;
43 use File::Basename;
44 use File::Temp;
45 use File::Spec;
46 use IO::Handle;
47 use POSIX qw(:signal_h strftime);
48 use Data::Dumper;
49 use Cwd qw(abs_path getcwd);
50 use File::Find ();
51
52 # save a copy of the original enviroment for reporting
53 # save it early to reduce the risk of prior mangling
54 use vars qw($orig_env);
55
56 BEGIN
57 {
58     $orig_env = {};
59     while (my ($k,$v) = each %ENV)
60     {
61
62         # report all the keys but only values for whitelisted settings
63         # this is to stop leaking of things like passwords
64         $orig_env->{$k} =(
65             (
66                     $k =~ /^PG(?!PASSWORD)|MAKE|CC|CPP|FLAG|LIBRAR|INCLUDE/
67                   ||$k =~/^(HOME|LOGNAME|USER|PATH|SHELL)$/
68             )
69             ? $v
70             : 'xxxxxx'
71         );
72     }
73 }
74
75 use EximBuild::SCM;
76 use EximBuild::Options;
77 use EximBuild::WebTxn;
78
79 my %module_hooks;
80 my $orig_dir = getcwd();
81 push @INC, $orig_dir;
82
83 # make sure we exit nicely on any normal interrupt
84 # so the cleanup handler gets called.
85 # that lets us stop the db if it's running and
86 # remove the inst and exim directories
87 # so the next run can start clean.
88
89 foreach my $sig (qw(INT TERM HUP QUIT))
90 {
91     $SIG{$sig}=\&interrupt_exit;
92 }
93
94 # copy command line before processing - so we can later report it
95 # unmunged
96
97 my @invocation_args = (@ARGV);
98
99 # process the command line
100 EximBuild::Options::fetch_options();
101
102 die "only one of --from-source and --from-source-clean allowed"
103   if ($from_source && $from_source_clean);
104
105 die "only one of --skip-steps and --only-steps allowed"
106   if ($skip_steps && $only_steps);
107
108 $verbose=1 if (defined($verbose) && $verbose==0);
109 $verbose ||= 0; # stop complaints about undefined var in numeric comparison
110
111 if ($testmode)
112 {
113     $verbose=1 unless $verbose;
114     $forcerun = 1;
115     $nostatus = 1;
116     $nosend = 1;
117
118 }
119
120 use vars qw(%skip_steps %only_steps);
121 $skip_steps ||= "";
122 if ($skip_steps =~ /\S/)
123 {
124     %skip_steps = map {$_ => 1} split(/\s+/,$skip_steps);
125 }
126 $only_steps ||= "";
127 if ($only_steps =~ /\S/)
128 {
129     %only_steps = map {$_ => 1} split(/(\s+|[:,])/,$only_steps);
130 }
131
132 # Currently only specifying a branch is actually used.
133 # Specifying a different repo is just a wishlist item .
134 use vars qw($branch $repo);
135 my ($arg1,$arg2) = (shift,shift);
136 $branch = $arg2 ? $arg2 :
137           $arg1 ? $arg1 :
138           'HEAD';
139 $repo = $arg2 ? $arg1 : 'exim';
140 my $explicit_branch = $branch;
141
142 print_help() if ($help);
143
144 #
145 # process config file
146 #
147 require $buildconf;
148
149 # get the config data into some local variables
150 my (
151     $buildroot,$target,$animal, $print_success,
152     $aux_path,$trigger_exclude,$trigger_include,$secret,
153     $keep_errs,$force_every, $make, $optional_steps,
154     $use_vpath,$tar_log_cmd, $using_msvc, $extra_config,
155     $make_jobs, $core_file_glob
156   )
157   =@EximBuild::conf{
158     qw(build_root target animal print_success aux_path trigger_exclude
159       trigger_include secret keep_error_builds force_every make optional_steps
160       use_vpath tar_log_cmd using_msvc extra_config make_jobs core_file_glob)
161   };
162
163 #default is no parallel build
164 $make_jobs ||= 1;
165
166 # default core file pattern is Linux, which used to be hardcoded
167 $core_file_glob ||= 'core*';
168
169 # legacy name
170 if (defined($EximBuild::conf{trigger_filter}))
171 {
172     $trigger_exclude = $EximBuild::conf{trigger_filter};
173 }
174
175 my  $scm_timeout_secs = $EximBuild::conf{scm_timeout_secs}
176   || $EximBuild::conf{cvs_timeout_secs};
177
178 print scalar(localtime()),": buildfarm run for $animal:$branch starting\n"
179   if $verbose;
180
181 # Allow commandline overrides of conf variables
182 foreach my $arg ( @{$EximBuild::Options::overrides} )
183 {
184   if (my ($key,$val) = split '=', $arg)
185   {
186     $EximBuild::conf{$key} = $val;
187     printf "Commandline override: '$key' = '%s'\n", $EximBuild::conf{$key}
188       if $verbose;
189   }
190 }
191
192 if (ref($force_every) eq 'HASH')
193 {
194     $force_every = $force_every->{$branch} || $force_every->{default};
195 }
196
197 my $scm = new EximBuild::SCM \%EximBuild::conf;
198
199 my $buildport;
200
201 if (exists $EximBuild::conf{base_port})
202 {
203     $buildport = $EximBuild::conf{base_port};
204     if ($branch =~ /REL(\d+)_(\d+)/)
205     {
206         $buildport += (10 * ($1 - 7)) + $2;
207     }
208 }
209 else
210 {
211
212     # support for legacy config style
213     $buildport = $EximBuild::conf{branch_ports}->{$branch} || 5999;
214 }
215
216 $ENV{EXTRA_REGRESS_OPTS} = "--port=$buildport";
217
218 $tar_log_cmd ||= "tar -z -cf runlogs.tgz *.log";
219
220 my $logdirname = "lastrun-logs";
221
222 if ($from_source || $from_source_clean)
223 {
224     $from_source ||= $from_source_clean;
225     die "sourceroot $from_source not absolute"
226       unless $from_source =~ m!^/!;
227
228     # we need to know where the lock should go, so unless the path
229     # contains HEAD we require it to be specified.
230     die "must specify branch explicitly with from_source"
231       unless ($explicit_branch || $from_source =~ m!/HEAD/!);
232     $verbose ||= 1;
233     $nosend=1;
234     $nostatus=1;
235     $use_vpath = undef;
236     $logdirname = "fromsource-logs";
237 }
238
239 my @locales;
240 if ($branch eq 'HEAD' || $branch ge 'REL8_4')
241 {
242
243     # non-C locales are not regression-safe before 8.4
244     @locales = @{$EximBuild::conf{locales}} if exists $EximBuild::conf{locales};
245 }
246 unshift(@locales,'C') unless grep {$_ eq "C"} @locales;
247
248 # sanity checks
249 # several people have run into these
250
251 if ( `uname -s 2>&1 ` =~ /CYGWIN/i )
252 {
253     my @procs = `ps -ef`;
254     die "cygserver not running" unless(grep {/cygserver/} @procs);
255 }
256 my $ccachedir;
257 if ( $ccachedir = $EximBuild::conf{build_env}->{CCACHE_DIR} )
258 {
259
260     # ccache is smart enough to create what you tell it is the cache dir, but
261     # not smart enough to build the whole path. mkpath croaks on error, so
262     # we just let it.
263
264     mkpath $ccachedir;
265     $ccachedir = abs_path($ccachedir);
266 }
267
268 if ($^V lt v5.8.0)
269 {
270     die "no aux_path in config file" unless $aux_path;
271 }
272
273 die "cannot run as root/Administrator" unless ($using_msvc or $> > 0);
274
275 my $devnull = $using_msvc ? "nul" : "/dev/null";
276
277 if (!$from_source)
278 {
279     $scm->check_access($using_msvc);
280 }
281
282 my $st_prefix = "$animal.";
283
284 my $exim = $from_source  || $scm->get_build_path($use_vpath);
285
286 # set environment from config
287 while (my ($envkey,$envval) = each %{$EximBuild::conf{build_env}})
288 {
289     $ENV{$envkey}=$envval;
290 }
291
292 # change to buildroot for this branch or die
293 die "no buildroot" unless $buildroot;
294
295 unless ($buildroot =~ m!^/!
296     or($using_msvc and $buildroot =~ m![a-z]:[/\\]!i ))
297 {
298     die "buildroot $buildroot not absolute";
299 }
300
301 die "$buildroot does not exist or is not a directory" unless -d $buildroot;
302
303 chdir $buildroot || die "chdir to $buildroot: $!";
304
305 mkdir $branch unless -d $branch;
306 chdir $branch || die "chdir to $buildroot/$branch";
307
308 # rename legacy status files/directories
309 foreach my $oldfile (glob("last*"))
310 {
311     move $oldfile, "$st_prefix$oldfile";
312 }
313
314 my $branch_root = getcwd();
315
316 # make sure we are using GNU make
317 die "$make is not GNU Make - please fix config file"
318   unless check_make();
319
320 # set up modules
321 foreach my $module (@{$EximBuild::conf{modules}})
322 {
323
324     # fill in the name of the module here, so use double quotes
325     # so everything BUT the module name needs to be escaped
326     my $str = qq!
327          require EximBuild::Modules::$module; 
328          EximBuild::Modules::${module}::setup(
329               \$buildroot,
330               \$branch,
331               \\\%EximBuild::conf,
332               \$exim);
333     !;
334     eval $str;
335
336     # make errors fatal
337     die $@ if $@;
338 }
339
340 # acquire the lock
341
342 my $lockfile;
343 my $have_lock;
344
345 open($lockfile, ">builder.LCK") || die "opening lockfile: $!";
346
347 # only one builder at a time allowed per branch
348 # having another build running is not a failure, and so we do not output
349 # a failure message under this condition.
350 if ($from_source)
351 {
352     die "acquiring lock in $buildroot/$branch/builder.LCK"
353       unless flock($lockfile,LOCK_EX|LOCK_NB);
354 }
355 elsif ( !flock($lockfile,LOCK_EX|LOCK_NB) )
356 {
357     print "Another process holds the lock on "
358       ."$buildroot/$branch/builder.LCK. Exiting."
359       if ($verbose);
360     exit(0);
361 }
362
363 die "$buildroot/$branch has $exim or inst directories!"
364   if ((!$from_source && -d $exim) || -d "inst");
365
366 # we are OK to run if we get here
367 $have_lock = 1;
368
369 # check if file present for forced run
370 my $forcefile = $st_prefix . "force-one-run";
371 if (-e $forcefile)
372 {
373     $forcerun = 1;
374     unlink $forcefile;
375 }
376
377 # try to allow core files to be produced.
378 # another way would be for the calling environment
379 # to call ulimit. We do this in an eval so failure is
380 # not fatal.
381 eval{
382     require BSD::Resource;
383     BSD::Resource->import();
384
385     # explicit sub calls here. using & keeps compiler happy
386     my $coreok = setrlimit(&RLIMIT_CORE,&RLIM_INFINITY,&RLIM_INFINITY);
387     die "setrlimit" unless $coreok;
388 };
389 warn "failed to unlimit core size: $@" if $@ && $verbose > 1;
390
391 # the time we take the snapshot
392 my $now=time;
393 my $installdir = "$buildroot/$branch/inst";
394 my $dbstarted;
395
396 my $extraconf;
397
398 # cleanup handler for all exits
399 END
400 {
401
402     # clean up temp file
403     unlink $ENV{TEMP_CONFIG} if $extraconf;
404
405     # if we have the lock we must already be in the build root, so
406     # removing things there should be safe.
407     # there should only be anything to cleanup if we didn't have
408     # success.
409     if ( $have_lock && -d "$exim")
410     {
411         if ($dbstarted)
412         {
413             chdir $installdir;
414             system(qq{"bin/pg_ctl" -D data stop >$devnull 2>&1});
415             foreach my $loc (@locales)
416             {
417                 next unless -d "data-$loc";
418                 system(qq{"bin/pg_ctl" -D "data-$loc" stop >$devnull 2>&1});
419             }
420             chdir $branch_root;
421         }
422         if ( !$from_source && $keep_errs)
423         {
424             print "moving kept error trees\n" if $verbose;
425             my $timestr = strftime "%Y-%m-%d_%H-%M-%S", localtime($now);
426             unless (move("$exim", "eximkeep.$timestr"))
427             {
428                 print "error renaming '$exim' to 'eximkeep.$timestr': $!";
429             }
430             if (-d "inst")
431             {
432                 unless(move("inst", "instkeep.$timestr"))
433                 {
434                     print "error renaming 'inst' to 'instkeep.$timestr': $!";
435                 }
436             }
437         }
438         else
439         {
440             rmtree("inst") unless $keepall;
441             rmtree("$exim") unless ($from_source || $keepall);
442         }
443
444         # only keep the cache in cases of success
445         rmtree("$ccachedir") if $ccachedir;
446     }
447
448     # get the modules to clean up after themselves
449     process_module_hooks('cleanup');
450
451     if ($have_lock)
452     {
453         if ($use_vpath)
454         {
455
456             # vpath builds leave some stuff lying around in the
457             # source dir, unfortunately. This should clean it up.
458             $scm->cleanup();
459         }
460         close($lockfile);
461         unlink("builder.LCK");
462     }
463 }
464
465 # Prepend the DEFAULT settings (if any) to any settings for the
466 # branch. Since we're mangling this, deep clone $extra_config
467 # so the config object is kept as given. This is done using
468 # Dumper() because the MSys DTK perl doesn't have Storable. This
469 # is less efficient but it hardly matters here for this shallow
470 # structure.
471
472 $extra_config = eval Dumper($extra_config);
473
474 if ($extra_config &&  $extra_config->{DEFAULT})
475 {
476     if (!exists  $extra_config->{$branch})
477     {
478         $extra_config->{$branch} =      $extra_config->{DEFAULT};
479     }
480     else
481     {
482         unshift(@{$extra_config->{$branch}}, @{$extra_config->{DEFAULT}});
483     }
484 }
485
486 if ($extra_config && $extra_config->{$branch})
487 {
488     my $tmpname;
489     ($extraconf,$tmpname) =File::Temp::tempfile(
490         'buildfarm-XXXXXX',
491         DIR => File::Spec->tmpdir(),
492         UNLINK => 1
493     );
494     die 'no $tmpname!' unless $tmpname;
495     $ENV{TEMP_CONFIG} = $tmpname;
496     foreach my $line (@{$extra_config->{$branch}})
497     {
498         print $extraconf "$line\n";
499     }
500     autoflush $extraconf 1;
501 }
502
503 use vars qw($steps_completed);
504 $steps_completed = "";
505
506 my @changed_files;
507 my @changed_since_success;
508 my $last_config;
509 my $last_status;
510 my $last_run_snap;
511 my $last_success_snap;
512 my $current_config;
513 my $current_snap;
514 my @filtered_files;
515 my $savescmlog = "";
516
517 if ($from_source_clean)
518 {
519     print time_str(),"cleaning source in $exim ...\n";
520     clean_from_source();
521 }
522 elsif (!$from_source)
523 {
524
525     # see if we need to run the tests (i.e. if either something has changed or
526     # we have gone over the force_every heartbeat time)
527
528     print time_str(),"checking out source ...\n" if $verbose;
529
530     my $timeout_pid;
531
532     $timeout_pid = spawn(\&scm_timeout,$scm_timeout_secs)
533       if $scm_timeout_secs;
534
535     $savescmlog = $scm->checkout($branch);
536     $steps_completed = "SCM-checkout";
537
538     process_module_hooks('checkout',$savescmlog);
539
540     if ($timeout_pid)
541     {
542
543         # don't kill me, I finished in time
544         if (kill(SIGTERM, $timeout_pid))
545         {
546
547             # reap the zombie
548             waitpid($timeout_pid,0);
549         }
550     }
551
552     print time_str(),"checking if build run needed ...\n" if $verbose;
553
554     # transition to new time processing
555     unlink "last.success";
556
557     # get the timestamp data
558     $last_config = find_last('config') || 0;
559     $last_status = find_last('status') || 0;
560     $last_run_snap = find_last('run.snap');
561     $last_success_snap = find_last('success.snap');
562     $forcerun = 1 unless (defined($last_run_snap));
563
564     # If config file changed, force a rebuild
565     ($current_config) = (stat $orig_dir.'/'.$buildconf)[9];
566     if ($current_config > $last_config)
567     {
568       $last_status = 0;
569       set_last('config',$current_config) unless $nostatus;
570     }
571
572     # updated by find_changed to last mtime of any file in the repo
573     $current_snap=0;
574
575     # see if we need to force a build
576     $last_status = 0
577       if ( $last_status
578         && $force_every
579         &&$last_status+($force_every*3600) < $now);
580     $last_status = 0 if $forcerun;
581
582     # see what's changed since the last time we did work
583     $scm->find_changed(\$current_snap,$last_run_snap, $last_success_snap,
584         \@changed_files,\@changed_since_success);
585
586     #ignore changes to files specified by the trigger exclude filter, if any
587     if (defined($trigger_exclude))
588     {
589         @filtered_files = grep { !m[$trigger_exclude] } @changed_files;
590     }
591     else
592     {
593         @filtered_files = @changed_files;
594     }
595
596     #ignore changes to files NOT specified by the trigger include filter, if any
597     if (defined($trigger_include))
598     {
599         @filtered_files = grep { m[$trigger_include] } @filtered_files;
600     }
601
602     my $modules_need_run;
603
604     process_module_hooks('need-run',\$modules_need_run);
605
606     # if no build required do nothing
607     if ($last_status && !@filtered_files && !$modules_need_run)
608     {
609         print time_str(),
610           "No build required: last status = ",scalar(gmtime($last_status)),
611           " GMT, current snapshot = ",scalar(gmtime($current_snap))," GMT,",
612           " changed files = ",scalar(@filtered_files),"\n"
613           if $verbose;
614         rmtree("$exim");
615         exit 0;
616     }
617
618     # get version info on both changed files sets
619     # XXX modules support?
620
621     $scm->get_versions(\@changed_files);
622     $scm->get_versions(\@changed_since_success);
623
624 } # end of unless ($from_source)
625
626 cleanlogs();
627
628 writelog('SCM-checkout',$savescmlog) unless $from_source;
629 $scm->log_id() unless $from_source;
630
631 # copy/create according to vpath/scm settings
632
633 if ($use_vpath)
634 {
635     print time_str(),"creating vpath build dir $exim ...\n" if $verbose;
636     mkdir $exim || die "making $exim: $!";
637 }
638 elsif (!$from_source && $scm->copy_source_required())
639 {
640     print time_str(),"copying source to $exim ...\n" if $verbose;
641
642     $scm->copy_source($using_msvc);
643 }
644
645 process_module_hooks('setup-target');
646
647 # start working
648
649 set_last('status',$now) unless $nostatus;
650 set_last('run.snap',$current_snap) unless $nostatus;
651
652 my $started_times = 0;
653 print time_str(),"running configure ...\n" if $verbose;
654
655 # each of these routines will call send_result, which calls exit,
656 # on any error, so each step depends on success in the previous
657 # steps.
658 configure();
659
660 make();
661
662 display_features();
663
664 make_test() if (check_optional_step('make_test'));
665
666 make_doc() if (check_optional_step('build_docs'));
667
668 ##check_port_is_ok($buildport,'Post');
669
670 # if we get here everything went fine ...
671
672 my $saved_config = get_config_summary();
673
674 rmtree("inst"); # only keep failures
675 rmtree("$exim") unless ($from_source || $keepall);
676
677 print(time_str(),"OK\n") if $verbose;
678
679 send_result("OK");
680
681 exit;
682
683 ############## end of main program ###########################
684
685 sub print_help
686 {
687     print qq!
688 usage: $0 [options] [branch]
689
690  where options are one or more of:
691
692   --nosend                  = don't send results
693   --nostatus                = don't set status files
694   --force                   = force a build run (ignore status files)
695   --from-source=/path       = use source in path, not from SCM
696   or
697   --from-source-clean=/path = same as --from-source, run make distclean first
698   --config=/path/to/file    = alternative location for config file
699   --keepall                 = keep directories if an error occurs
700   --verbose[=n]             = verbosity (default 1) 2 or more = huge output.
701   --quiet                   = suppress normal error message 
702   --test                    = short for --nosend --nostatus --verbose --force
703   --skip-steps=list         = skip certain steps
704   --only-steps=list         = only do certain steps, not allowed with skip-steps
705                               lists can be comma, colon, or space separated
706
707 Default branch is HEAD. Usually only the --config option should be necessary.
708
709 !;
710     exit(0);
711 }
712
713 sub time_str
714 {
715     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
716     return sprintf("[%.2d:%.2d:%.2d] ",$hour, $min, $sec);
717 }
718
719 sub step_wanted
720 {
721     my $step = shift;
722     return $only_steps{$step} if $only_steps;
723     return !$skip_steps{$step} if $skip_steps;
724     return 1; # default is everything is wanted
725 }
726
727 sub register_module_hooks
728 {
729     my $who = shift;
730     my $what = shift;
731     while (my ($hook,$func) = each %$what)
732     {
733         $module_hooks{$hook} ||= [];
734         push(@{$module_hooks{$hook}},[$func,$who]);
735     }
736 }
737
738 sub process_module_hooks
739 {
740     my $hook = shift;
741
742     # pass remaining args (if any) to module func
743     foreach my $module (@{$module_hooks{$hook}})
744     {
745         my ($func,$module_instance) = @$module;
746         &$func($module_instance, @_);
747     }
748 }
749
750 sub check_optional_step
751 {
752     my $step = shift;
753     my $oconf;
754     my $shandle;
755
756     return undef unless ref($oconf = $optional_steps->{$step});
757     if ($oconf->{branches})
758     {
759         return undef unless grep {$_ eq $branch} @{$oconf->{branches}};
760     }
761
762     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =localtime(time);
763     return undef if (exists $oconf->{min_hour} &&  $hour < $oconf->{min_hour});
764     return undef if (exists $oconf->{max_hour} &&  $hour > $oconf->{max_hour});
765     return undef if (exists $oconf->{dow}
766         &&grep {$_ eq $wday} @{$oconf->{dow}});
767
768     my $last_step = $last_status = find_last("$step") || 0;
769
770     return undef unless ($forcerun ||
771                          time >$last_step + (3600 * $oconf->{min_hours_since}));
772     set_last("$step") unless $nostatus;
773
774     return 1;
775 }
776
777 sub clean_from_source
778 {
779     if (-e "$exim/GNUmakefile")
780     {
781
782         # fixme for MSVC
783         my @makeout = `cd $exim && $make distclean 2>&1`;
784         my $status = $? >>8;
785         writelog('distclean',\@makeout);
786         print "======== distclean log ===========\n",@makeout if ($verbose > 1);
787         send_result('distclean',$status,\@makeout) if $status;
788     }
789 }
790
791 sub interrupt_exit
792 {
793     my $signame = shift;
794     print "Exiting on signal $signame\n";
795     exit(1);
796 }
797
798 sub cleanlogs
799 {
800     my $lrname = $st_prefix . $logdirname;
801     rmtree("$lrname");
802     mkdir "$lrname" || die "can't make $lrname dir: $!";
803 }
804
805 sub writelog
806 {
807     my $stage = shift;
808     my $loglines = shift;
809     my $handle;
810     my $lrname = $st_prefix . $logdirname;
811     open($handle,">$lrname/$stage.log") || die $!;
812     print $handle @$loglines;
813     close($handle);
814 }
815
816 sub display_features
817 {
818     return unless step_wanted('features');
819     my @out = `cd $exim
820                src/build-*/exim -C test/confs/0000 -bV `;
821     my $status = $? >>8;
822     writelog('features',\@out);
823     print "======== features log ===========\n",@out if ($verbose > 1);
824     send_result('Features',$status,\@out) if $status;
825     $steps_completed .= " Features";
826 }
827     
828 sub check_make
829 {
830     my @out = `$make -v 2>&1`;
831     return undef unless ($? == 0 && grep {/GNU Make/} @out);
832     return 'OK';
833 }
834
835 sub make
836 {
837     return unless step_wanted('make');
838     print time_str(),"running make ...\n" if $verbose;
839     my $make_args = join(' ',$EximBuild::conf{make_args});
840     my (@makeout);
841     my $make_cmd = $make;
842     $make_cmd = "$make -j $make_jobs"
843       if ($make_jobs > 1 && ($branch eq 'HEAD' || $branch ge 'REL9_1'));
844     @makeout = `cd $exim/src && $make_cmd $make_args 2>&1`;
845     my $status = $? >>8;
846     writelog('make',\@makeout);
847     print "======== make log ===========\n",@makeout if ($verbose > 1);
848     send_result('Make',$status,\@makeout) if $status;
849     $steps_completed .= " Make";
850 }
851
852 sub make_doc
853 {
854     return unless step_wanted('build_docs');
855     print time_str(),"running make doc ...\n" if $verbose;
856
857     my (@makeout);
858     @makeout = `cd $exim/doc/doc-docbook/ && \
859                 EXIM_VER="4.82" $make everything 2>&1`;
860     my $status = $? >>8;
861     writelog('build_docs',\@makeout);
862     print "======== make doc log ===========\n",@makeout if ($verbose > 1);
863     send_result('Doc',$status,\@makeout) if $status;
864     $steps_completed .= " Doc";
865 }
866
867 sub get_stack_trace
868 {
869     my $bindir = shift;
870     my $pgdata = shift;
871
872     # no core = no result
873     my @cores = glob("$pgdata/$core_file_glob");
874     return () unless @cores;
875
876     # no gdb = no result
877     system "gdb --version > $devnull 2>&1";
878     my $status = $? >>8;
879     return () if $status;
880
881     my $cmdfile = "./gdbcmd";
882     my $handle;
883     open($handle, ">$cmdfile");
884     print $handle "bt\n";
885     close($handle);
886
887     my @trace;
888
889     foreach my $core (@cores)
890     {
891         my @onetrace = `gdb -x $cmdfile --batch $bindir/exim $core 2>&1`;
892         push(@trace,
893             "\n\n================== stack trace: $core ==================\n",
894             @onetrace);
895     }
896
897     unlink $cmdfile;
898
899     return @trace;
900 }
901
902 sub make_install_check
903 {
904     my $locale = shift;
905     return unless step_wanted('install-check');
906     print time_str(),"running make installcheck ($locale)...\n" if $verbose;
907
908     my @checklog;
909     unless ($using_msvc)
910     {
911         @checklog = `cd $exim/src/test/regress && $make installcheck 2>&1`;
912     }
913     else
914     {
915         chdir "$exim/src/tools/msvc";
916         @checklog = `perl vcregress.pl installcheck 2>&1`;
917         chdir $branch_root;
918     }
919     my $status = $? >>8;
920     my @logfiles =
921       ("$exim/src/test/regress/regression.diffs","$installdir/logfile");
922     foreach my $logfile(@logfiles)
923     {
924         next unless (-e $logfile );
925         push(@checklog,"\n\n================== $logfile ==================\n");
926         my $handle;
927         open($handle,$logfile);
928         while(<$handle>)
929         {
930             push(@checklog,$_);
931         }
932         close($handle);
933     }
934     if ($status)
935     {
936         my @trace =
937           get_stack_trace("$installdir/bin","$installdir/data-$locale");
938         push(@checklog,@trace);
939     }
940     writelog("install-check-$locale",\@checklog);
941     print "======== make installcheck log ===========\n",@checklog
942       if ($verbose > 1);
943     send_result("InstallCheck-$locale",$status,\@checklog) if $status;
944     $steps_completed .= " InstallCheck-$locale";
945 }
946
947 sub make_isolation_check
948 {
949     my $locale = shift;
950     return unless step_wanted('isolation-check');
951     my @makeout;
952     unless ($using_msvc)
953     {
954         my $cmd =
955           "cd $exim/src/test/isolation && $make NO_LOCALE=1 installcheck";
956         @makeout = `$cmd 2>&1`;
957     }
958     else
959     {
960         chdir "$exim/src/tools/msvc";
961         @makeout = `perl vcregress.pl isolationcheck 2>&1`;
962         chdir $branch_root;
963     }
964
965     my $status = $? >>8;
966
967     # get the log files and the regression diffs
968     my @logs = glob("$exim/src/test/isolation/log/*.log");
969     push(@logs,"$installdir/logfile");
970     unshift(@logs,"$exim/src/test/isolation/regression.diffs")
971       if (-e "$exim/src/test/isolation/regression.diffs");
972     foreach my $logfile (@logs)
973     {
974         push(@makeout,"\n\n================== $logfile ===================\n");
975         my $handle;
976         open($handle,$logfile);
977         while(<$handle>)
978         {
979             push(@makeout,$_);
980         }
981         close($handle);
982     }
983     if ($status)
984     {
985         my @trace =
986           get_stack_trace("$installdir/bin","$installdir/data-$locale");
987         push(@makeout,@trace);
988     }
989     writelog('isolation-check',\@makeout);
990     print "======== make isolation check logs ===========\n",@makeout
991       if ($verbose > 1);
992
993     send_result('IsolationCheck',$status,\@makeout) if $status;
994     $steps_completed .= " IsolationCheck";
995 }
996
997 sub make_test
998 {
999     return unless step_wanted('make_test');
1000     print time_str(),"running make test ...\n" if $verbose;
1001     my $tests_range = $EximBuild::conf{range_num_tests} || "1 4";
1002     my @makeout;
1003     @makeout =`(cd $exim/test
1004                 autoconf && ./configure && $make )2>&1 `;
1005     my $status = $? >>8;
1006     unless($status)
1007     {
1008       my @tmp = `(WORKDIR=\$PWD
1009                   cd $exim/test
1010                   ./runtest \$WORKDIR/$exim/src/build-*/exim -CONTINUE $tests_range )2>&1`;
1011       $status = $? >>8;
1012       push @makeout, @tmp;
1013       # Prepend the failed summary log outputs for ease of reading
1014       my $fail_summary = "$exim/test/failed-summary.log";
1015       if (-f $fail_summary)
1016       {
1017         @tmp = `cat $fail_summary`;
1018         push @tmp, "\n";
1019         unshift @makeout, @tmp;
1020         unshift @makeout, "Summary of failed tests:\n";
1021       }
1022     }
1023     writelog('make_test',\@makeout);
1024     print "======== make test logs ===========\n",@makeout
1025       if ($verbose > 1);
1026
1027     send_result('Test',$status,\@makeout) if $status;
1028     $steps_completed .= " Test";
1029 }
1030
1031 sub make_ecpg_check
1032 {
1033     return unless step_wanted('ecpg-check');
1034     my @makeout;
1035     my $ecpg_dir = "$exim/src/interfaces/ecpg";
1036     if ($using_msvc)
1037     {
1038         chdir "$exim/src/tools/msvc";
1039         @makeout = `perl vcregress.pl ecpgcheck 2>&1`;
1040         chdir $branch_root;
1041     }
1042     else
1043     {
1044         @makeout = `cd  $ecpg_dir && $make NO_LOCALE=1 check 2>&1`;
1045     }
1046     my $status = $? >>8;
1047
1048     # get the log files and the regression diffs
1049     my @logs = glob("$ecpg_dir/test/log/*.log");
1050     unshift(@logs,"$ecpg_dir/test/regression.diffs")
1051       if (-e "$ecpg_dir/test/regression.diffs");
1052     foreach my $logfile (@logs)
1053     {
1054         push(@makeout,"\n\n================== $logfile ===================\n");
1055         my $handle;
1056         open($handle,$logfile);
1057         while(<$handle>)
1058         {
1059             push(@makeout,$_);
1060         }
1061         close($handle);
1062     }
1063     if ($status)
1064     {
1065         my $base = "$ecpg_dir/test/regress/tmp_check";
1066         my @trace =
1067           get_stack_trace("$base/install$installdir/bin",       "$base/data");
1068         push(@makeout,@trace);
1069     }
1070     writelog('ecpg-check',\@makeout);
1071     print "======== make ecpg check logs ===========\n",@makeout
1072       if ($verbose > 1);
1073
1074     send_result('ECPG-Check',$status,\@makeout) if $status;
1075     $steps_completed .= " ECPG-Check";
1076 }
1077
1078 sub configure
1079 {
1080     return unless step_wanted('configure');
1081     print time_str(),"creating configuration ...\n" if $verbose;
1082
1083     my $env = $EximBuild::conf{makefile_set};
1084     my $add = $EximBuild::conf{makefile_add};
1085     my $features = $EximBuild::conf{makefile_regex};
1086
1087     my $envstr = "";
1088     while (my ($key,$val) = each %$env)
1089     {
1090         $envstr .= "$key='$val'\n";
1091     }
1092     while (my ($key,$val) = each %$add)
1093     {
1094         $envstr .= "$key+='$val'\n";
1095     }
1096
1097     my $conf_path = "src/src/EDITME";
1098     my $local_conf = "src/Local/Makefile";
1099     my @confout = `cd $exim; mkdir -p src/Local 2>&1`;
1100     my @tmp = `cd $exim && cp $conf_path $local_conf 2>&1`;
1101     my $status = $? >> 8;
1102     push @confout, @tmp;
1103     if ($status == 0)
1104     {
1105         # First, let's display some directory permissions in case
1106         # permissions are causing problems.
1107         my @dir = split('/',`pwd`);
1108         chomp(@dir);
1109         my $count = scalar @dir;
1110         my $loop = 1;
1111         my $dirs = '';
1112         while ($loop < $count)
1113         {
1114           my $dir = "";
1115           foreach my $i (0 .. $loop)
1116           {
1117             $dir .= $dir[$i].'/';
1118           }
1119           $dirs .= " $dir";
1120           $loop++;
1121         }
1122         @tmp = `echo "Verify Directory Permissions"
1123                 ls -ld $dirs`;
1124         push @confout, @tmp;
1125         # Build the config file from the EDITME template
1126         @tmp = `cd $exim && echo '$envstr' >> $local_conf`;
1127         push @confout, @tmp;
1128         my $exim_user = $EximBuild::conf{master_exim_user} || 'exim';
1129         @tmp = `echo "Hardcoded Exim user info:"; id $exim_user
1130           cd $exim && perl -pi -e 's/^EXIM_USER=.*/EXIM_USER=$exim_user/' $local_conf`;
1131         push @confout, @tmp;
1132         my $me = `whoami`; chomp $me;
1133         @tmp = `echo "Build Farm user info:"; id $me
1134           cd $exim && perl -pi -e 's/^# CONFIGURE_OWNER=\$/CONFIGURE_OWNER=$me/' $local_conf`;
1135         push @confout, @tmp;
1136         @tmp = `cd $exim && perl -pi -e 's/^# TRUSTED_CONFIG_LIST=/TRUSTED_CONFIG_LIST=/' $local_conf`;
1137         push @confout, @tmp;
1138         @tmp = `cd $exim && perl -pi -e 's/^# WHITELIST_D_MACROS=.*/WHITELIST_D_MACROS=DIR:EXIM_PATH:AA:ACL:ACLRCPT:ACL_MAIL:ACL_PREDATA:ACL_RCPT:AFFIX:ALLOW:ARG1:ARG2:AUTHF:AUTHS:AUTH_ID_DOMAIN:BAD:BANNER:BB:BR:BRB:CERT:COM:COMMAND_USER:CONNECTCOND:CONTROL:CREQCIP:CREQMAC:CRL:CSS:D6:DATA:DCF:DDF:DEFAULTDWC:DELAY:DETAILS:DRATELIMIT:DYNAMIC_OPTION:ELI:ERROR_DETAILS:ERT:FAKE:FALLBACK:FILTER:FILTER_PREPEND_HOME:FORBID:FORBID_SMTP_CODE:FUSER:HAI:HAP:HARDLIMIT:HEADER_LINE_MAXSIZE:HEADER_MAXSIZE:HELO_MSG:HL:HOSTS:HOSTS_AVOID_TLS:HOSTS_MAX_TRY:HVH:IFACE:IGNORE_QUOTA:INC:INSERT:IP1:IP2:LAST:LDAPSERVERS:LENCHECK:LIMIT:LIST:LOG_SELECTOR:LS:MAXNM:MESSAGE_LOGS:MSIZE:NOTDAEMON:ONCE:ONLY:OPT:OPTION:ORDER:PAH:PEX:PORT:PTBC:QDG:QOLL:QUOTA:QUOTA_FILECOUNT:QWM:RCPT_MSG:REMEMBER:REQUIRE:RETRY:RETRY1:RETRY2:RETURN:RETURN_ERROR_DETAILS:REWRITE:ROUTE_DATA:RRATELIMIT:RT:S:SELECTOR:SELF:SERVER:SERVERS:SREQCIP:SREQMAC:SRV:STD:STRICT:SUB:SUBMISSION_OPTIONS:TIMEOUTDEFER:TIMES:TRUSTED:TRYCLEAR:UL:USE_SENDER:UTF8:VALUE:WMF:X:Y/' $local_conf`;
1139         push @confout, @tmp;
1140         @tmp = `cd $exim && perl -pi -e 's/^EXIM_MONITOR=(.*)/# EXIM_MONITOR=\$1/' $local_conf`;
1141         push @confout, @tmp;
1142         for my $regex ( @$features )
1143         {
1144             @tmp = `cd $exim
1145                     perl -pi -e '$regex' $local_conf 2>&1
1146                     echo "Used regex: $regex" `;
1147             push @confout, @tmp;
1148         }
1149         # Add the final build file to the display output
1150         @tmp = `cd $exim
1151                 echo
1152                 echo "Contents of Local/Makefile:"
1153                 egrep '^[^#]' $local_conf `;
1154         push @confout, @tmp;
1155         # Build the config_opts array to send to the server
1156         chomp @tmp;
1157         my @config_opts = grep s/(?:LOOKUP_|EXPERIMENTAL_|USE_)(\S+)=.*/$1/,
1158                           @tmp;
1159         push @config_opts, grep s/^(?:EXIM_)(PERL|PYTHON)=.*/$1/,
1160                            @tmp;
1161         # OpenSSL doesn't have a specific USE flag
1162         push @config_opts, grep s/^(TLS_LIBS.*-l(ssl|crypto)).*/OPENSSL/,
1163                            @tmp;
1164         $EximBuild::conf{config_opts} = \@config_opts;
1165         
1166         # Does not matter what the Exim version is, as long as it is valid.
1167         my $exim_ver = $EximBuild::conf{exim_test_version} || '4.82';
1168         `cd $exim
1169          echo 'EXIM_RELEASE_VERSION="$exim_ver"' > src/src/version.sh
1170          echo 'EXIM_VARIANT_VERSION=""' >> src/src/version.sh
1171          echo 'EXIM_COMPILE_NUMBER="0"' >> src/src/version.sh`;
1172     }
1173    
1174     print "======== configure output ===========\n",@confout
1175       if ($verbose > 1);
1176
1177     writelog('configure',\@confout);
1178
1179     if ($status)
1180     {
1181         send_result('Configure',$status,\@confout);
1182     }
1183
1184     $steps_completed .= " Configure";
1185 }
1186
1187 sub find_last
1188 {
1189     my $which = shift;
1190     my $stname = $st_prefix . "last.$which";
1191     my $handle;
1192     open($handle,$stname) or return undef;
1193     my $time = <$handle>;
1194     close($handle);
1195     chomp $time;
1196     return $time + 0;
1197 }
1198
1199 sub set_last
1200 {
1201     my $which = shift;
1202     my $stname = $st_prefix . "last.$which";
1203     my $st_now = shift || time;
1204     my $handle;
1205     open($handle,">$stname") or die "opening $stname: $!";
1206     print $handle "$st_now\n";
1207     close($handle);
1208 }
1209
1210 sub send_result
1211 {
1212
1213     # clean up temp file
1214     $extraconf = undef;
1215
1216     my $stage = shift;
1217
1218     my $ts = $now || time;
1219     my $status=shift || 0;
1220     my $log = shift || [];
1221     print "======== log passed to send_result ===========\n",@$log
1222       if ($verbose > 1);
1223
1224     unshift(@$log,
1225         "Last file mtime in snapshot: ",
1226         scalar(gmtime($current_snap)),
1227         " GMT\n","===================================================\n")
1228       unless ($from_source || !$current_snap);
1229
1230     my $log_data = join("",@$log);
1231     my $confsum = "";
1232     my $changed_this_run = "";
1233     my $changed_since_success = "";
1234     $changed_this_run = join("!",@changed_files)
1235       if @changed_files;
1236     $changed_since_success = join("!",@changed_since_success)
1237       if ($stage ne 'OK' && @changed_since_success);
1238
1239     if ($stage eq 'OK')
1240     {
1241         $confsum= $saved_config;
1242     }
1243     elsif ($stage !~ /CVS|Git|SCM/ )
1244     {
1245         $confsum = get_config_summary();
1246     }
1247     else
1248     {
1249         $confsum = get_script_config_dump();
1250     }
1251
1252     my $savedata = Data::Dumper->Dump(
1253         [
1254             $changed_this_run, $changed_since_success, $branch, $status,$stage,
1255             $animal, $ts,$log_data, $confsum, $target, $verbose, $secret
1256         ],
1257         [
1258             qw(changed_this_run changed_since_success branch status stage
1259               animal ts log_data confsum target verbose secret)
1260         ]
1261     );
1262
1263     my $lrname = $st_prefix . $logdirname;
1264
1265     # might happen if there is a CVS failure and have never got further
1266     mkdir $lrname unless -d $lrname;
1267
1268     my $txfname = "$lrname/web-txn.data";
1269     my $txdhandle;
1270     open($txdhandle,">$txfname");
1271     print $txdhandle $savedata;
1272     close($txdhandle);
1273
1274     if ($nosend || $stage eq 'CVS' || $stage eq 'CVS-status' )
1275     {
1276         print "Branch: $branch\n";
1277         if ($stage eq 'OK')
1278         {
1279             print "All stages succeeded\n";
1280             set_last('success.snap',$current_snap) unless $nostatus;
1281             exit(0);
1282         }
1283         else
1284         {
1285             print "Stage $stage failed with status $status\n";
1286             exit(1);
1287         }
1288     }
1289
1290     if ($stage !~ /CVS|Git|SCM|Pre-run-port-check/ )
1291     {
1292
1293         my @logfiles = glob("$lrname/*.log");
1294         my %mtimes = map { $_ => (stat $_)[9] } @logfiles;
1295         @logfiles =
1296           map { basename $_ }( sort { $mtimes{$a} <=> $mtimes{$b} } @logfiles );
1297         my $logfiles = join(' ',@logfiles);
1298         $tar_log_cmd =~ s/\*\.log/$logfiles/;
1299         chdir($lrname);
1300         system("$tar_log_cmd 2>&1 ");
1301         chdir($branch_root);
1302
1303     }
1304     else
1305     {
1306
1307         # these would be from an earlier run, since we
1308         # do cleanlogs() after the cvs stage
1309         # so don't send them.
1310         unlink "$lrname/runlogs.tgz";
1311     }
1312
1313     my $txstatus;
1314
1315     # this should now only apply to older Msys installs. All others should
1316     # be running with perl >= 5.8 since that's required to build exim
1317     # anyway
1318     if (!$^V or $^V lt v5.8.0)
1319     {
1320
1321         unless (-x "$aux_path/run_web_txn.pl")
1322         {
1323             print "Could not locate $aux_path/run_web_txn.pl\n";
1324             exit(1);
1325         }
1326
1327         system("$aux_path/run_web_txn.pl $lrname");
1328         $txstatus = $? >> 8;
1329     }
1330     else
1331     {
1332         $txstatus = EximBuild::WebTxn::run_web_txn($lrname) ? 0 : 1;
1333
1334     }
1335
1336     if ($txstatus)
1337     {
1338         print "Web txn failed with status: $txstatus\n";
1339
1340         # if the web txn fails, restore the timestamps
1341         # so we try again the next time.
1342         set_last('status',$last_status) unless $nostatus;
1343         set_last('run.snap',$last_run_snap) unless $nostatus;
1344         exit($txstatus);
1345     }
1346
1347     unless ($stage eq 'OK' || $quiet)
1348     {
1349         print "BuildFarm member $animal failed on $branch stage $stage\n";
1350     }
1351
1352     #   print "Success!\n",$response->content
1353     #           if $print_success;
1354
1355     set_last('success.snap',$current_snap) if ($stage eq 'OK' && !$nostatus);
1356
1357     exit 0;
1358 }
1359
1360 sub get_config_summary
1361 {
1362     my $handle;
1363     my $config = "";
1364     # unless ($using_msvc)
1365     # {
1366     #     open($handle,"$exim/config.log") || return undef;
1367     #     my $start = undef;
1368     #     while (<$handle>)
1369     #     {
1370     #         if (!$start && /created by PostgreSQL configure/)
1371     #         {
1372     #             $start=1;
1373     #             s/It was/This file was/;
1374     #         }
1375     #         next unless $start;
1376     #         last if /Core tests/;
1377     #         next if /^\#/;
1378     #         next if /= <?unknown>?/;
1379
1380     #         # split up long configure line
1381     #         if (m!\$.*configure.*--with! && length > 70)
1382     #         {
1383     #             my $pos = index($_," ",70);
1384     #             substr($_,$pos+1,0,"\\\n        ") if ($pos > 0);
1385     #             $pos = index($_," ",140);
1386     #             substr($_,$pos+1,0,"\\\n        ") if ($pos > 0);
1387     #             $pos = index($_," ",210);
1388     #             substr($_,$pos+1,0,"\\\n        ") if ($pos > 0);
1389     #         }
1390     #         $config .= $_;
1391     #     }
1392     #     close($handle);
1393     #     $config .=
1394     #       "\n========================================================\n";
1395     # }
1396     $config .= get_script_config_dump();
1397     return $config;
1398 }
1399
1400 sub get_script_config_dump
1401 {
1402     my $conf = {
1403         %EximBuild::conf,  # shallow copy
1404         script_version => $VERSION,
1405         invocation_args => \@invocation_args,
1406         steps_completed => $steps_completed,
1407         orig_env => $orig_env,
1408     };
1409     delete $conf->{secret};
1410     $Data::Dumper::Sortkeys = 1;
1411     return  Data::Dumper->Dump([$conf],['Script_Config']);
1412 }
1413
1414 sub scm_timeout
1415 {
1416     my $wait_time = shift;
1417     my $who_to_kill = getpgrp(0);
1418     my $sig = SIGTERM;
1419     $sig = -$sig;
1420     print "waiting $wait_time secs to time out process $who_to_kill\n"
1421       if $verbose;
1422     foreach my $sig (qw(INT TERM HUP QUIT))
1423     {
1424         $SIG{$sig}='DEFAULT';
1425     }
1426     sleep($wait_time);
1427     $SIG{TERM} = 'IGNORE'; # so we don't kill ourself, we're exiting anyway
1428     # kill the whole process group
1429     unless (kill $sig,$who_to_kill)
1430     {
1431         print "scm timeout kill failed\n";
1432     }
1433 }
1434
1435 sub spawn
1436 {
1437     my $coderef = shift;
1438     my $pid = fork;
1439     if (defined($pid) && $pid == 0)
1440     {
1441         exit &$coderef(@_);
1442     }
1443     return $pid;
1444 }
1445