724221cebd23c8fdc083e85eedb29cc3124251e2
[pspp] / build-pspp
1 #! /usr/bin/env perl
2
3 use Getopt::Long qw(:config bundling no_ignore_case);
4 use POSIX;
5 use Cwd;
6
7 use strict;
8 use warnings;
9
10 my $help = 0;
11 my $build_binary = 1;
12 my $batch = 0;
13 GetOptions ("h|help" => \$help,
14             "binary!" => \$build_binary,
15             "batch!" => \$batch);
16
17 usage () if $help;
18
19 die "$0: exactly one or two nonoption arguments are required\n"
20   if @ARGV != 1 && @ARGV != 2;
21
22 my $builder = `hostname`;
23 chomp $builder;
24
25 # Select build number.
26 my $build_number = POSIX::strftime("%Y%m%d%H%M%S", localtime);
27 print "$build_number\n" if $batch;
28
29 my $topdir = getcwd ();
30
31 # Create build directory.
32 my $builddir = "builds/$build_number";
33 mkdir "builds" or die "builds: mkdir: $!\n" if ! -d "builds";
34 mkdir $builddir or die "$builddir: mkdir: $!\n";
35
36 our $resultsdir = "$builddir/results";
37 mkdir $resultsdir or die "$resultsdir: mkdir: $!\n";
38 mkdir "$resultsdir/vars" or die "$resultsdir/vars: mkdir: $!\n";
39
40 my $logfile = "$resultsdir/LOG";
41 open (LOG, '>', $logfile) or die "creating $logfile failed: $!\n";
42
43 set_var ("builder", $builder);
44 set_var ("build_number", $build_number);
45
46 sub start_step {
47     my ($msg) = @_;
48     print LOG "\f\n$msg\n";
49     print "$msg\n" unless $batch;
50 }
51
52 sub set_var {
53     my ($var, $value) = @_;
54     open (VAR, '>', "$resultsdir/vars/$var")
55       or die "creating $resultsdir/$var failed: $!\n";
56     print VAR "$value\n";
57     close VAR;
58     print LOG "$var=$value\n";
59     print "\t$var=$value\n" unless $batch;
60 }
61
62 sub save_result {
63     my ($src, $rm_src) = @_;
64     my ($dst) = $src;
65     $dst =~ s(^.*/)();
66     $dst = "$resultsdir/$dst";
67
68     start_step ("Saving $src");
69     run ("cp -R $src $dst");
70
71     if (defined ($rm_src) && $rm_src) {
72         run ("rm $src");
73     }
74
75     return $dst;
76 }
77
78 sub save_result_if_exists {
79     my ($src, $rm_src) = @_;
80     if (-e $src) {
81         save_result (@_);
82     } else {
83         start_step ("$src does not exist, cannot save");
84     }
85 }
86
87 my $tarball;
88 if (@ARGV == 2) {
89     my ($repo, $branch) = @ARGV;
90
91     # Fetch branch
92     start_step ("Fetch $repo, branch $branch");
93     run ("git fetch $repo +$branch:buildtmp/$$/pspp");
94
95     # Get revision number.
96     my $revision = `git rev-parse buildtmp/$$/pspp`;
97     chomp $revision;
98     set_var ("pspp_commit", $revision);
99     my $abbrev_commit = substr ($revision, 0, 6);
100
101     # Extract source.
102     start_step ("Extract branch into $builddir/pspp$build_number");
103     run ("git archive --format=tar --prefix=pspp$build_number/ buildtmp/$$/pspp | (cd $builddir && tar xf -)");
104     run ("git branch -D buildtmp/$$/pspp");
105
106     # Extract version number.
107     start_step ("Extract repository version number");
108     my $trace = `cd $builddir/pspp$build_number && autoconf -t AC_INIT`;
109     chomp $trace;
110     my ($file, $line, $macro, $package, $repo_version, @rest)
111       = split (':', $trace);
112     set_var ("repo_version", $repo_version);
113
114     # Is this a "gnits" mode tree?
115     start_step ("Checking Automake mode");
116     open (MAKEFILE_AM, '<', "$builddir/pspp$build_number/Makefile.am");
117     my $am_mode = "gnu";
118     while (<MAKEFILE_AM>) {
119         if (/gnits/) {
120             $am_mode = "gnits";
121             last;
122         }
123     }
124     close (MAKEFILE_AM);
125     set_var ("am_mode", $am_mode);
126
127     # Generate version number for build.
128     # We want to append -g012345, but if we're in Gnits mode and the
129     # version number already has a hyphen, we have to omit it.
130     start_step ("Generate build version number");
131     my $version = $repo_version;
132     $version .= '-' unless $version =~ /-/;
133     $version .= "g$abbrev_commit";
134     set_var ("version", $version);
135
136     # Append -g012345 to configure.ac version number.
137     start_step ("Updating version number in $file");
138     my $fullname = "$builddir/pspp$build_number/$file";
139     open (OLDFILE, '<', $fullname)
140       or die "opening $fullname failed: $!\n";
141     open (NEWFILE, '>', "$fullname.new")
142       or die "creating $fullname.new failed: $!\n";
143     while (<OLDFILE>) {
144         if ($. != $line) {
145             print NEWFILE $_;
146         } else {
147             print NEWFILE "AC_INIT([$package], [$version]";
148             print NEWFILE ", [$_]" foreach @rest;
149             print NEWFILE ")\n";
150         }
151     }
152     close (NEWFILE);
153     close (OLDFILE);
154     rename ("$fullname.new", $fullname)
155       or die "rename $fullname.new to $fullname failed: $!\n";
156
157     # Add note to beginning of NEWS (otherwise "make dist" fails).
158     start_step ("Updating NEWS");
159     $fullname = "$builddir/pspp$build_number/NEWS";
160     open (OLDFILE, '<', $fullname)
161       or die "opening $fullname failed: $!\n";
162     open (NEWFILE, '>', "$fullname.new")
163       or die "creating $fullname.new failed: $!\n";
164     my $found_changes = 0;
165     while (<OLDFILE>) {
166         if (!$found_changes && /^Changes/) {
167             $found_changes = 1;
168             print NEWFILE <<EOF;
169 Changes from $repo_version to $version:
170
171  * Built automatically from commit $revision
172    in branch $branch by builder $builder
173
174 EOF
175         }
176         print NEWFILE $_;
177     }
178     close (NEWFILE);
179     close (OLDFILE);
180     rename ("$fullname.new", $fullname)
181       or die "rename $fullname.new to $fullname failed: $!\n";
182
183     # Get Gnulib commit number.
184     start_step ("Reading README.Git to find Gnulib commit number");
185     my $gnulib_commit;
186     $fullname = "$builddir/pspp$build_number/README.Git";
187     open (README_GIT, '<', $fullname)
188       or die "opening $fullname failed: $!\n";
189     while (<README_GIT>) {
190         ($gnulib_commit) = /^\s+commit ([0-9a-fA-F]{8,})/ and last;
191     }
192     die "$fullname does not specify a Git commit number\n"
193       if !defined ($gnulib_commit);
194     set_var ("gnulib_commit", $gnulib_commit);
195
196     # If we don't already have that Gnulib commit, update Gnulib.
197     `git rev-parse $gnulib_commit`;
198     if ($? != 0) {
199         start_step ("Updating Gnulib to obtain commit");
200         run ("git fetch gnulib");
201     }
202
203     # Extract gnulib source.
204     start_step ("Extract Gnulib source");
205     run ("git archive --format=tar --prefix=gnulib/ $gnulib_commit | (cd $builddir && tar xf -)");
206
207     # Bootstrap.
208     start_step ("Bootstrap (make -f Smake)");
209     run ("cd $builddir/pspp$build_number && make -f Smake", "bootstrap");
210
211     # Configure.
212     start_step ("Configure source");
213     run ("cd $builddir/pspp$build_number && mkdir _build && cd _build && ../configure", "configure");
214
215     # Distribute.
216     start_step ("Make source tarball");
217     run ("cd $builddir/pspp$build_number/_build && make dist", "dist");
218     my $tarname = "pspp-$version.tar.gz";
219     $tarball = save_result ("$builddir/pspp$build_number/_build/$tarname", 1);
220
221     # Build user manual
222     start_step ("Build user manual");
223     run ("cd $builddir/pspp$build_number && cp _build/doc/*.texi doc/");
224     run ("cd $builddir/pspp$build_number && GENDOCS_TEMPLATE_DIR=$topdir $topdir/gendocs.sh -s doc/pspp.texinfo -o $topdir/$builddir/results/user-manual --email bug-gnu-pspp\@gnu.org pspp \"GNU PSPP User Manual\"", "user-manual");
225
226     # Build developer's guide
227     start_step ("Build developers guide");
228     run ("cd $builddir/pspp$build_number && GENDOCS_TEMPLATE_DIR=$topdir $topdir/gendocs.sh -s doc/pspp-dev.texinfo -o $topdir/$builddir/results/dev-guide --email bug-gnu-pspp\@gnu.org pspp-dev \"GNU PSPP Developers Guide\"", "dev-guide");
229 } else {
230     $tarball = $ARGV[0];
231 }
232
233 if ($build_binary) {
234     start_step ("Determining $tarball target directory");
235     my $sample_filename = `zcat $tarball | tar tf - | head -1`;
236     my ($tarball_dir) = $sample_filename =~ m%^(?:[./])*([^/]+)/%;
237     set_var ("dist_dir", $tarball_dir);
238
239     start_step ("Extracting $tarball into $builddir/$tarball_dir");
240     run ("zcat $tarball | (cd $builddir && tar xf -)");
241
242     start_step ("Extracting tar version");
243     my ($version) = `cd $builddir/$tarball_dir && ./configure --version | head -1`
244       =~ /configure (\S+)$/;
245     set_var ("dist_version", $version);
246     my ($binary_version) = "$version-$builder-build$build_number";
247     set_var ("binary_version", $binary_version);
248
249     start_step ("Configuring");
250     run ("chmod -R a-w $builddir/$tarball_dir");
251     run ("chmod u+w $builddir/$tarball_dir");
252     run ("mkdir $builddir/$tarball_dir/_build");
253     run ("chmod a-w $builddir/$tarball_dir");
254     my $ok = try_run ("cd $builddir/$tarball_dir/_build && ../configure --enable-relocatable --prefix=''", "bin-configure");
255     for my $basename ("config.h", "config.log") {
256         save_result_if_exists ("$builddir/$tarball_dir/_build/$basename");
257     }
258     fail () if !$ok;
259
260     start_step ("Build");
261     run ("cd $builddir/$tarball_dir/_build && make", "build");
262
263     start_step ("Install");
264     run ("cd $builddir/$tarball_dir/_build && make install DESTDIR=\$PWD/pspp-$binary_version", "install");
265
266     start_step ("Make binary distribution");
267     run ("cd $builddir/$tarball_dir/_build && tar cfz pspp-$binary_version.tar.gz pspp-$binary_version");
268     save_result ("$builddir/$tarball_dir/_build/pspp-$binary_version.tar.gz", 1);
269
270     start_step ("Check");
271     $ok = try_run ("cd $builddir/$tarball_dir/_build && make check", "check");
272     for my $basename ("tests/testsuite.log", "tests/testsuite.dir") {
273         save_result_if_exists ("$builddir/$tarball_dir/_build/$basename");
274     }
275     fail () if !$ok;
276
277     start_step ("Uninstall");
278     run ("cd $builddir/$tarball_dir/_build && make uninstall DESTDIR=\$PWD/pspp-$binary_version", "uninstall");
279
280     start_step ("Check uninstall");
281     run ("cd $builddir/$tarball_dir/_build && make distuninstallcheck distuninstallcheck_dir=\$PWD/pspp-$binary_version", "distuninstallcheck");
282
283     # distcleancheck
284 }
285
286 start_step ("Success");
287
288 sub usage {
289     print <<EOF;
290 $0, for building and testing PSPP
291 usage: $0 [OPTIONS] [TARBALL | REPO REFSPEC]
292 where TARBALL is the name of a tarball produced by "make dist"
293    or REPO and REFSPEC are a Git repo and refspec (e.g. branch) to clone.
294
295 Options:
296   --help            Print this usage message and exit
297   --no-binary       Build source tarballs but no binaries.
298   --batch           Do not print progress to stdout.
299 EOF
300     exit(0);
301 }
302
303 sub run {
304     fail () if !try_run (@_);
305 }
306
307 sub try_run {
308     my ($command, $id) = @_;
309
310     print LOG "$command\n";
311
312     my $pid = open (COMMAND, '-|');
313     if (!defined ($pid)) {
314         die "fork failed: $!\n";
315     } elsif (!$pid) {
316         dup2 (1, 2);
317         exec ($command);
318         die "$command: exec failed: $!\n";
319     }
320
321     my ($start) = time ();
322     my ($est_time) = (defined ($id) ? read_timing ($id) : 0);
323
324     local ($|) = 1;
325     my $lines = 0;
326     while (<COMMAND>) {
327         print LOG $_;
328
329         my $elapsed = time () - $start;
330         $progress = sprintf "%d lines logged, %d s elapsed",
331           $lines++, $elapsed;
332         if ($est_time > 0) {
333             my $left = $est_time - $elapsed;
334             if ($left > 0) {
335                 $progress .= sprintf ", ETA %d s", $left;
336             }
337         }
338         print "\r$progress", " " x (79 - length ($progress)), "\r"
339           unless $batch;
340     }
341     close (COMMAND);
342     print "\r", " " x 79, "\r" unless $batch;
343
344     write_timing ($id, time () - $start) if defined ($id);
345
346     return 1 if !$?;
347
348     if ($? & 127) {
349         printf STDERR "%s: child died with signal %d, %s coredump\n",
350           $command, ($? & 127),  ($? & 128) ? 'with' : 'without';
351     } else {
352         printf STDERR "%s: child exited with value %d\n", $command, $? >> 8;
353     }
354     return 0;
355 }
356
357 sub read_timing {
358     my ($id) = @_;
359     open (TIMINGS, "<", "$topdir/timings") or return 0;
360     while (<TIMINGS>) {
361         chomp;
362         my ($key, $value) = /^([^=]+)=(.*)/ or next;
363         return $value if $key eq $id;
364     }
365     close (TIMINGS);
366     return 0;
367 }
368
369 sub write_timing {
370     my ($id, $time) = @_;
371
372     open (NEWTIMINGS, ">", "$topdir/timings.tmp$$") or return;
373
374     if (open (OLDTIMINGS, "<", "$topdir/timings")) {
375         while (<OLDTIMINGS>) {
376             if (my ($key, $value) = /^([^=]+)=(.*)/) {
377                 next if $key eq $id;
378             }
379             print NEWTIMINGS $_;
380         }
381         close (OLDTIMINGS);
382     }
383
384     print NEWTIMINGS "$id=$time\n";
385     close (NEWTIMINGS);
386
387     rename ("$topdir/timings.tmp$$", "$topdir/timings");
388 }
389
390 sub fail {
391     die "Build failed, refer to:\n\t$topdir/$logfile\nfor details.\n";
392 }