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