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