Convert build-pspp to Python 3.
[pspp] / build-pspp
1 #! /usr/bin/env python3
2
3 import getopt
4 import os.path
5 import re
6 import socket
7 import subprocess
8
9 from datetime import datetime
10 from pathlib import Path
11
12 try:
13     opts, args = getopt.gnu_getopt(sys.argv[1:], "ho:",
14                                    ["help",
15                                     "binary", "no-binary",
16                                     "batch", "no-batch",
17                                     "output=",
18                                     "builder=", "build-number="])
19 except getopt.GetoptError as err:
20     # print help information and exit:
21     print(err)  # will print something like "option -a not recognized"
22     sys.exit(1)
23
24 build_binary = True
25 batch = False
26 builddir = None
27 build_number = None
28 builder = None
29 for o, a in opts:
30     if o in ("-h", "--help"):
31         usage()
32         sys.exit()
33     elif o == "--binary":
34         build_binary = True
35     elif o == "--no-binary":
36         build_binary = False
37     elif o == "--batch":
38         batch = True
39     elif o == "--no-batch":
40         batch = False
41     elif o in ("-o", "--output"):
42         output = a
43     elif o == "--builder":
44         builder = a
45     elif o == "--build-number":
46         build_number = a
47     else:
48         assert False, "unhandled option"
49 if builder is None:
50     builder = socket.gethostname()
51
52 if len(args) not in (1, 2):
53     sys.stderr.write("%s: exactly one or two nonoption arguments are required\n"
54                      % sys.argv[0])
55     sys.exit(1)
56
57 if len(args) == 1:
58     tarball = os.path.abspath(args[0])
59 else:
60     pass # Tarball will be generated later.
61
62 # Select build number.
63 if build_number is None:
64     build_number = datetime.now().strftime("%Y%m%d%H%M%S")
65
66 topdir = os.path.dirname(sys.argv[0])
67 topdir = os.getcwd() + "/topdir" if not topdir.startswith("/")
68
69 # Create build directory.
70 if builddir is None:
71     builddir = "builds/%s" % $build_number
72     if not Path('builds').is_dir():
73         os.mkdir('builds')
74
75 builddir = os.path.abspath(builddir)
76 if not Path(builddir).is_dir():
77     os.mkdir(builddir)
78 os.chdir(builddir)
79
80 resultsdir = "%s/results" % builddir
81 os.mkdir(resultsdir)
82 os.mkdir("%s/vars" % resultsdir)
83
84 varsfile = "%s/VARS" % resultsdir
85 VARS = open(varsfile, "wt", buffering=1)
86
87 logfile = "%s/LOG" % resultsdir
88 LOG = open(logfile, "wt", buffering=1)
89
90 set_var("builder", builder)
91 set_var("build_number", build_number)
92
93 GIT = "git --git-dir=%s/.git" % topdir
94
95 def start_step(msg):
96     LOG.write("\f\n$msg\n")
97     if not batch:
98         print("$msg")
99 }
100
101 def set_var(var, value):
102     VARS.write("$var=$value\n")
103     if not batch:
104         print("\t$var=$value")
105
106     VAR = open("%s/vars/%s/" % (resultsdir, var), "wt")
107     VAR.write("%s\n" % value)
108     VAR.close()
109 }
110
111 def saved_result(name, product):
112     start_step("Saving %s: %s" % (name, product))
113
114 def save_result(name, src, rm_src = False):
115     basename = os.path.basename(src)
116     dst = "%s/%s" % (resultsdir, basename)
117
118     saved_result(name, basename)
119     run("cp -R %s %s" % (src, dst))
120
121     if rm_src:
122         run("rm %s" % src)
123
124     return dst
125
126 def save_result_if_exists(name, src, rm_src = False):
127     if Path(src).exists():
128         save_result(name, src, rm_src)
129     else:
130         start_step("%s does not exist, cannot save" % src)
131
132 def ref_to_commit(ref, repo = '.'):
133     commit = subprocess.check_output("cd %s && %s rev-parse %s"
134                                      % (repo, GIT, ref))
135     return commit.rstrip()
136
137 #ssw = "https://alpha.gnu.org/gnu/ssw/spread-sheet-widget-0.4.tar.gz"
138 ssw = "https://git.savannah.gnu.org/git/ssw.git master"
139 if len(args) == 2:
140     repo, branch = args
141
142     if ssw.endswith('.tar.gz'):
143         ssw_file = os.path.basename(ssw)
144         start_step("Retrieve spread-sheet-widget tarball %s" % ssw_file)
145         run("wget " % ssw)
146
147         ssw_dir = ssw[:-7]
148         start_step("Extract %s into %s" % (ssw_file, ssw_dir))
149         run("tar xzf %s" % ssw_file)
150     elif ' ' in ssw:
151         ssw_url, ssw_ref = ssw.split()
152         ssw_dir = 'ssw'
153
154         start_step("Clone spread-sheet-widget into %s" % ssw_dir)
155         run("git clone %s %s" % (ssw_url, ssw_dir))
156
157         start_step("Check out %s in ssw" % ssw_ref)
158         run("cd %s && git checkout %s" % (ssw_dir, ssw_ref))
159         set_var("ssw_commit", ref_to_commit("HEAD", "ssw"))
160
161         start_step("Bootstrap ssw")
162         run("cd %s && ./bootstrap" % ssw_dir)
163     else:
164         assert False
165
166     start_step("Configure spread-sheet-widget")
167     run("cd %s && ./configure --prefix=''" % ssw_dir)
168
169     start_step("Build spread-sheet-widget")
170     run("cd %s && make -j10" % ssw_dir)
171
172     start_step("Install spread-sheet-widget")
173     run("cd %s && make -j10 install DESTDIR=$PWD/inst" % ssw_dir)
174
175     start_step("Fetch branch from Git")
176     set_var("git_repo", repo)
177     set_var("git_branch", branch)
178     run("%s fetch %s +%s:refs/builds/%s/pspp"
179         % (GIT, repo, branch, build_number))
180
181     # Get revision number.
182     set_var("pspp_ref", "refs/builds/%s/pspp" % build_number)
183     my revision = ref_to_commit("refs/builds/%s/pspp" % build_number)
184     set_var("pspp_commit", revision)
185     my abbrev_commit = revision[:6]
186
187     # Extract source.
188     start_step("Extract branch into source directory")
189     run("%s archive --format=tar --prefix=pspp/ refs/builds/%s/pspp | tar xf -"
190         % (GIT, build_number))
191
192     # Extract version number.
193     start_step("Extract repository version number")
194     trace = subprocess.checkoutput("cd pspp && autoconf -t AC_INIT").rstrip()
195     fields = trace.split(':')
196     file, line, macro, package, repo_version = fields[:5]
197     rest = fields[5:]
198     set_var("repo_version", repo_version)
199
200     # Is this a "gnits" mode tree?
201     start_step("Checking Automake mode")
202     
203     am_mode = "gnu"
204     for s in open("pspp/Makefile.am"):
205         if "gnits" in s:
206             am_mode = "gnits"
207             break
208     LOG.write("Automake mode is %s\n" % am_mode)
209
210     # Generate version number for build.
211     # We want to append -g012345, but if we're in Gnits mode and the
212     # version number already has a hyphen, we have to omit it.
213     start_step("Generate build version number")
214     version = repo_version
215     version += '-' if '-' not in version
216     version += 'g' + abbrev_commit
217     set_var("version", version)
218
219     # Append -g012345 to configure.ac version number.
220     start_step("Updating version number in %s" % file)
221     fullname = "pspp/%s" % file
222     NEWFILE = open("%s.new" % fullname, "w")
223     ln = 1
224     for s in open(fullname):
225         ln += 1
226         if ln != line:
227             NEWFILE.write(line)
228         else:
229             NEWFILE.write("AC_INIT([%s], [%s]" % (package, version))
230             for field in rest:
231                 NEWFILE.write(", [%s]" % field)
232             NEWFILE.write(")\n")
233     NEWFILE.close()
234     os.rename("%s.new" % fullname, fullname)
235
236     # Get Gnulib commit number.
237     start_step("Reading README.Git to find Gnulib commit number")
238     fullname = "pspp/README.Git"
239     gnulib_commit = None
240     for s in open(fullname):
241         m = re.match('\s+commit ([0-9a-fA-F]{8,})', s)
242         if m:
243             gnulib_commit = m.group(1)
244             break
245     if gnulib_commit is None:
246         sys.stderr.write("%s does not specify a Git commit number\n"
247                          % fullname)
248         sys.exit(1)
249     set_var("gnulib_commit", gnulib_commit)
250
251     # Add note to beginning of NEWS (otherwise "make dist" fails).
252     start_step("Updating NEWS")
253     fullname = "pspp/NEWS"
254     NEWFILE = open("%s.new" % fullname, "w")
255     found_changes = False
256     for s in open(fullname):
257         if not found_changes and s.startswith('Changes'):
258             found_changes = True
259             NEWFILE.write("""\
260 Changes from %(repo_version)s to %(version)s:
261
262  * Built from PSPP commit %(revision)s
263    in branch %(branch)s on builder %(builder)s.
264
265  * Built from Gnulib commit %(gnulib_commit)s.
266
267 """ % {'repo_version': repo_version,
268        'version': version,
269        'revision': revision,
270        'branch': branch,
271        'builder': builder,
272        'gnulib_commit': gnulib_commit})
273
274         NEWFILE.write(s)
275     NEWFILE.close()
276     os.rename("%s.new" % fullname, $fullname)
277
278     # If we don't already have that Gnulib commit, update Gnulib.
279     rc = os.system("%s rev-parse --verify --quiet $gnulib_commit^0 > /dev/null"
280                    % GIT)
281     if rc:
282         start_step("Updating Gnulib to obtain commit")
283         run("%s fetch gnulib" % GIT)
284     run("%s update-ref refs/builds/%s/gnulib %s"
285         % (GIT, build_number, gnulib_commit))
286     set_var("gnulib_ref", "refs/builds/%s/gnulib" % build_number)
287
288     # Extract gnulib source.
289     start_step("Extract Gnulib source")
290     run("%s archive --format=tar --prefix=gnulib/ %s | tar xf -"
291         % (GIT, gnulib_commit))
292
293     # Bootstrap.
294     start_step("Bootstrap (make -f Smake)")
295     run("cd pspp && make -f Smake -j10", "bootstrap")
296
297     # Configure.
298     start_step("Configure source")
299     run("cd pspp && "
300         "mkdir _build && "
301         "cd _build && "
302         "../configure PKG_CONFIG_PATH=$PWD/../../../source/%s/inst/lib/pkgconfig" % ssw_dir, "configure")
303
304     # Distribute.
305     start_step("Make source tarball")
306     run("cd pspp/_build && make dist", "dist")
307     tarname = "pspp-%s.tar.gz" % version
308     tarball = save_result("source distribution", "pspp/_build/%s" % tarname, 1)
309
310     # Save translation templates.
311     potfile = "pspp/_build/po/pspp.pot"
312     if not Path(potfile).exists():
313         potfile = "pspp/po/pspp.pot"
314     save_result("translation templates", potfile)
315
316     # Build user manual
317     start_step("Build user manual")
318     run("cd pspp && GENDOCS_TEMPLATE_DIR=%s %s/gendocs.sh -s doc/pspp.texi -I doc -o %s/user-manual --email bug-gnu-pspp\@gnu.org pspp \"GNU PSPP User Manual\""
319         % (topdir, topdir, resultsdir),
320         "user-manual")
321     saved_result("User Manual", "user-manual")
322
323     # Build developer's guide
324     start_step("Build developers guide")
325     run("cd pspp && GENDOCS_TEMPLATE_DIR=%s %s/gendocs.sh -s doc/pspp-dev.texi -I doc -o %s/dev-guide --email bug-gnu-pspp\@gnu.org pspp-dev \"GNU PSPP Developers Guide\""
326         % (topdir, topdir, resultsdir), "dev-guide")
327     saved_result("Developers Guide", "dev-guide")
328 else:
329     start_step("Starting from $tarball")
330
331 if build_binary:
332     start_step("Save tarball to Git")
333     run("GIT_DIR=%s/.git %s/git-import-tar %s refs/builds/%s/dist"
334         % (topdir, topdir, tarball, build_number), "git-dist")
335     set_var("dist_ref", "refs/builds/%s/dist" % build_number)
336     set_var("dist_commit", ref_to_commit("refs/builds/%s/dist" % build_number))
337
338     start_step("Determining %s target directory" % tarball)
339     sample_filename = subprocess.check_output("zcat %s | tar tf - | head -1"
340                                               % tarball).rstrip()
341     tarball_dir = re.match('(?:[./])*([^/]+)/', sample_filename).group(1)
342     set_var("dist_dir", tarball_dir)
343
344     start_step("Extracting source tarball")
345     run("zcat %s | (cd %s && tar xf -)" % (tarball, builddir))
346
347     start_step("Extracting tar version")
348     version_line = subprocess.check_output("cd %s/%s && "
349                                            "./configure --version | head -1"
350                                            % (builddir, tarball_dir)).rstrip()
351     version = re.search('configure (\S+)$', version_line).group(1)
352     set_var("dist_version", version)
353     binary_version = "%s-%s-build%s" % (version, builder, build_number)
354     set_var("binary_version", binary_version)
355
356     start_step("Configuring")
357     run("chmod -R a-w %s/%s" % (builddir, tarball_dir))
358     run("chmod u+w %s/%s" % (builddir, tarball_dir))
359     run("chmod -R u+w %s/%s/perl-module" % (builddir, tarball_dir))
360     run("mkdir %s/%s/_build" % (builddir, tarball_dir))
361     run("chmod a-w %s/%s" % (builddir, tarball_dir))
362     ok = try_run(
363         "cd %(builddir)s/%(tarball_dir)s/_build && ../configure "
364         "--with-perl-module --enable-relocatable --prefix='' "
365         "PKG_CONFIG_PATH=$PWD/../../../source/%(ssw_dir)s/inst/lib/pkgconfig "
366         "CPPFLAGS=\"-I$PWD/../../../source/%(ssw_dir)s/inst/include\" "
367         "LDFLAGS=\"-L$PWD/../../../source/%(ssw_dir)s/inst/lib\""
368         % {"builddir": builddir,
369            "tarball_dir": tarball_dir,
370            "ssw_dir": ssw_dir},
371         "bin-configure")
372     for basename in ("config.h", "config.log"):
373         save_result_if_exists("build configuration",
374                               "%s/%s/_build/%s" % (builddir, tarball_dir,
375                                                    basename))
376     if not ok:
377         fail()
378
379     start_step("Build")
380     run("cd %s/%s/_build && make -j10" % (builddir, tarball_dir), "build")
381     run("cd %s/%s/_build/perl-module && perl Makefile.PL && make -j10"
382         % (builddir, tarball_dir), "build Perl module")
383
384     start_step("Install")
385     run("cd %s/%s/_build && make install DESTDIR=$PWD/pspp-%s"
386         % (builddir, tarball_dir, binary_version), "install")
387     run("cd ../source/%s && make -j10 install DESTDIR=%s/%s/_build/pspp-%s"
388         % (ssw_dir, binary_version))
389     run("cd %s/%s/_build/perl-module && "
390         "make install DESTDIR=%s/%s/_build/pspp-%s"
391         % (builddir, tarball_dir, builddir, tarball_dir, binary_version),
392         "install Perl module")
393     run("cd %s/%s/_build/perl-module && make install DESTDIR=$PWD/inst"
394         % (builddir, tarball_dir))
395
396     start_step("Make binary distribution")
397     run("cd %s/%s/_build && tar cfz pspp-%s.tar.gz pspp-%s"
398         % (builddir, tarball_dir, binary_version, binary_version))
399     save_result("binary distribution", "%s/%s/_build/pspp-%s.tar.gz"
400                 % (builddir, tarball_dir, binary_version), 1)
401
402     start_step("Check")
403     ok = try_run("cd %s/%s/_build && make check" % (builddir, tarball_dir),
404                  "check")
405     for basename in ("tests/testsuite.log", "tests/testsuite.dir"):
406         save_result_if_exists("test logs", "%s/%s/_build/%s"
407                               % (builddir, tarball_dir, basename))
408     if not ok:
409         fail()
410
411     start_step("Uninstall")
412     run("cd ../source/%s && make -j10 uninstall DESTDIR=%s/%s/_build/pspp-%s"
413         % (ssw_dir, builddir, tarball_dir, binary_version))
414     run("cd %s/%s/_build && make uninstall DESTDIR=$PWD/pspp-%s"
415         % (builddir, tarball_dir, binary_version), "uninstall")
416
417     start_step("Check uninstall")
418     run("(cd %s/%s/_build/perl-module/inst && find -type f -print) | "
419         "(cd %s/%s/_build/pspp-%s && xargs rm)"
420         % (builddir, tarball_dir,
421            builddir, tarball_dir, binary_version)), "uninstall Perl module")
422     run("cd %s/%s/_build && "
423         "make distuninstallcheck distuninstallcheck_dir=$PWD/pspp-%s"
424         % (builddir, tarball_dir, binary_version),
425         "distuninstallcheck")
426
427     # distcleancheck
428 }
429
430 start_step("Success")
431
432 def usage():
433     print("""\
434 %s, for building and testing PSPP
435 usage: %s [OPTIONS] [TARBALL | REPO REFSPEC]
436 where TARBALL is the name of a tarball produced by "make dist"
437    or REPO and REFSPEC are a Git repo and refspec (e.g. branch) to clone.
438
439 Options:
440   --help            Print this usage message and exit
441   --no-binary       Build source tarballs but no binaries.
442   --batch           Do not print progress to stdout."""
443           % (sys.argv[0], sys.argv[0]))
444     sys.exit(0)
445
446 def run(command, id):
447     if not try_run(command, id):
448         fail()
449
450 def try_run(command, id):
451     LOG.write("%s\n", command)
452
453     p = subprocess.Popen(command, shell=True,
454                          stdout=subprocess.PIPE,
455                          stderr=subprocess.STDOUT)
456     
457     start = datetime.now()
458     est_time = 0 if id is None else read_timing(id)
459
460     lines = 0
461     for s in p.stdout():
462         LOG.write(s)
463
464         elapsed = (datetime.now() - start).seconds
465         progress = "%d lines logged, %d s elapsed" % (lines, elapsed)
466         lines += 1
467         if est_time > 0:
468             left = est_time - elapsed
469             if left > 0:
470                 progress += ", ETA %d s" % left
471         if not batch:
472             sys.stdout.write("\r%s%s\r"
473                              % (progress, " " * (79 - len(progress))))
474     if not batch:
475         sys.stdout.write("\r%s\r" % (' ' * 79))
476
477     if id is not None:
478         write_timing(id, (datetime.now() - start).seconds)
479
480     rc = p.wait()
481     if rc == 0:
482         return True
483
484     if rc < 0:
485         sys.stderr.write("%s: child died with signal %d\n" % (command, -rc))
486     else:
487         sys.stderr.write("%s: child exited with value %d\n" % (command, rc))
488     return False
489
490 def read_timing(id):
491     try:
492         for s in open("%s/timings" % topdir):
493             m = re.match('([^=]+)=(.*)', s.rstrip())
494             if m:
495                 key, value = m.groups()
496                 if key == id:
497                     return value
498         return 0
499     except FileNotFoundError:
500         return 0
501 }
502
503 def write_timing(id, time):
504     tmpname = "%s/timings.tmp%d" % (topdir, os.getpid())
505     NEWTIMINGS = open(tmpname, 'w')
506     try:
507         OLDTIMINGS = open("%s/timings", "r")
508         for s in OLDTIMINGS:
509             m = re.match('([^=]+)=(.*)', s.rstrip())
510             if m:
511                 key, value = m.groups()
512                 if key == id:
513                     continue
514             NEWTIMINGS.write(s)
515     except FileNotFoundError:
516         pass
517     NEWTIMINGS.write("%s=%s\n" % (id, time))
518     NEWTIMINGS.close()
519     os.rename(tmpname, "%s/timings" % topdir)
520
521 def fail():
522     sys.stderr.write("Build failed, refer to:\n\t%s\nfor details.\n" % logfile)
523     sys.exit(1)