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