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