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