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