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