dacd116f8e61729827757bd2ac26de306c3a04b1
[external/glibc.git] / scripts / build-many-glibcs.py
1 #!/usr/bin/python3
2 # Build many configurations of glibc.
3 # Copyright (C) 2016-2019 Free Software Foundation, Inc.
4 # This file is part of the GNU C Library.
5 #
6 # The GNU C Library is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public
8 # License as published by the Free Software Foundation; either
9 # version 2.1 of the License, or (at your option) any later version.
10 #
11 # The GNU C Library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # Lesser General Public License for more details.
15 #
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with the GNU C Library; if not, see
18 # <http://www.gnu.org/licenses/>.
19
20 """Build many configurations of glibc.
21
22 This script takes as arguments a directory name (containing a src
23 subdirectory with sources of the relevant toolchain components) and a
24 description of what to do: 'checkout', to check out sources into that
25 directory, 'bot-cycle', to run a series of checkout and build steps,
26 'bot', to run 'bot-cycle' repeatedly, 'host-libraries', to build
27 libraries required by the toolchain, 'compilers', to build
28 cross-compilers for various configurations, or 'glibcs', to build
29 glibc for various configurations and run the compilation parts of the
30 testsuite.  Subsequent arguments name the versions of components to
31 check out (<component>-<version), for 'checkout', or, for actions
32 other than 'checkout' and 'bot-cycle', name configurations for which
33 compilers or glibc are to be built.
34
35 """
36
37 import argparse
38 import datetime
39 import email.mime.text
40 import email.utils
41 import json
42 import os
43 import re
44 import shutil
45 import smtplib
46 import stat
47 import subprocess
48 import sys
49 import time
50 import urllib.request
51
52 try:
53     subprocess.run
54 except:
55     class _CompletedProcess:
56         def __init__(self, args, returncode, stdout=None, stderr=None):
57             self.args = args
58             self.returncode = returncode
59             self.stdout = stdout
60             self.stderr = stderr
61
62     def _run(*popenargs, input=None, timeout=None, check=False, **kwargs):
63         assert(timeout is None)
64         with subprocess.Popen(*popenargs, **kwargs) as process:
65             try:
66                 stdout, stderr = process.communicate(input)
67             except:
68                 process.kill()
69                 process.wait()
70                 raise
71             returncode = process.poll()
72             if check and returncode:
73                 raise subprocess.CalledProcessError(returncode, popenargs)
74         return _CompletedProcess(popenargs, returncode, stdout, stderr)
75
76     subprocess.run = _run
77
78
79 class Context(object):
80     """The global state associated with builds in a given directory."""
81
82     def __init__(self, topdir, parallelism, keep, replace_sources, strip,
83                  full_gcc, action):
84         """Initialize the context."""
85         self.topdir = topdir
86         self.parallelism = parallelism
87         self.keep = keep
88         self.replace_sources = replace_sources
89         self.strip = strip
90         self.full_gcc = full_gcc
91         self.srcdir = os.path.join(topdir, 'src')
92         self.versions_json = os.path.join(self.srcdir, 'versions.json')
93         self.build_state_json = os.path.join(topdir, 'build-state.json')
94         self.bot_config_json = os.path.join(topdir, 'bot-config.json')
95         self.installdir = os.path.join(topdir, 'install')
96         self.host_libraries_installdir = os.path.join(self.installdir,
97                                                       'host-libraries')
98         self.builddir = os.path.join(topdir, 'build')
99         self.logsdir = os.path.join(topdir, 'logs')
100         self.logsdir_old = os.path.join(topdir, 'logs-old')
101         self.makefile = os.path.join(self.builddir, 'Makefile')
102         self.wrapper = os.path.join(self.builddir, 'wrapper')
103         self.save_logs = os.path.join(self.builddir, 'save-logs')
104         self.script_text = self.get_script_text()
105         if action != 'checkout':
106             self.build_triplet = self.get_build_triplet()
107             self.glibc_version = self.get_glibc_version()
108         self.configs = {}
109         self.glibc_configs = {}
110         self.makefile_pieces = ['.PHONY: all\n']
111         self.add_all_configs()
112         self.load_versions_json()
113         self.load_build_state_json()
114         self.status_log_list = []
115         self.email_warning = False
116
117     def get_script_text(self):
118         """Return the text of this script."""
119         with open(sys.argv[0], 'r') as f:
120             return f.read()
121
122     def exec_self(self):
123         """Re-execute this script with the same arguments."""
124         sys.stdout.flush()
125         os.execv(sys.executable, [sys.executable] + sys.argv)
126
127     def get_build_triplet(self):
128         """Determine the build triplet with config.guess."""
129         config_guess = os.path.join(self.component_srcdir('gcc'),
130                                     'config.guess')
131         cg_out = subprocess.run([config_guess], stdout=subprocess.PIPE,
132                                 check=True, universal_newlines=True).stdout
133         return cg_out.rstrip()
134
135     def get_glibc_version(self):
136         """Determine the glibc version number (major.minor)."""
137         version_h = os.path.join(self.component_srcdir('glibc'), 'version.h')
138         with open(version_h, 'r') as f:
139             lines = f.readlines()
140         starttext = '#define VERSION "'
141         for l in lines:
142             if l.startswith(starttext):
143                 l = l[len(starttext):]
144                 l = l.rstrip('"\n')
145                 m = re.fullmatch('([0-9]+)\.([0-9]+)[.0-9]*', l)
146                 return '%s.%s' % m.group(1, 2)
147         print('error: could not determine glibc version')
148         exit(1)
149
150     def add_all_configs(self):
151         """Add all known glibc build configurations."""
152         self.add_config(arch='aarch64',
153                         os_name='linux-gnu',
154                         extra_glibcs=[{'variant': 'disable-multi-arch',
155                                        'cfg': ['--disable-multi-arch']}])
156         self.add_config(arch='aarch64_be',
157                         os_name='linux-gnu')
158         self.add_config(arch='alpha',
159                         os_name='linux-gnu')
160         self.add_config(arch='arm',
161                         os_name='linux-gnueabi',
162                         extra_glibcs=[{'variant': 'v4t',
163                                        'ccopts': '-march=armv4t'}])
164         self.add_config(arch='armeb',
165                         os_name='linux-gnueabi')
166         self.add_config(arch='armeb',
167                         os_name='linux-gnueabi',
168                         variant='be8',
169                         gcc_cfg=['--with-arch=armv7-a'])
170         self.add_config(arch='arm',
171                         os_name='linux-gnueabihf',
172                         gcc_cfg=['--with-float=hard', '--with-cpu=arm926ej-s'],
173                         extra_glibcs=[{'variant': 'v7a',
174                                        'ccopts': '-march=armv7-a -mfpu=vfpv3'},
175                                       {'variant': 'v7a-disable-multi-arch',
176                                        'ccopts': '-march=armv7-a -mfpu=vfpv3',
177                                        'cfg': ['--disable-multi-arch']}])
178         self.add_config(arch='armeb',
179                         os_name='linux-gnueabihf',
180                         gcc_cfg=['--with-float=hard', '--with-cpu=arm926ej-s'])
181         self.add_config(arch='armeb',
182                         os_name='linux-gnueabihf',
183                         variant='be8',
184                         gcc_cfg=['--with-float=hard', '--with-arch=armv7-a',
185                                  '--with-fpu=vfpv3'])
186         self.add_config(arch='csky',
187                         os_name='linux-gnuabiv2',
188                         variant='soft',
189                         gcc_cfg=['--disable-multilib'])
190         self.add_config(arch='csky',
191                         os_name='linux-gnuabiv2',
192                         gcc_cfg=['--with-float=hard', '--disable-multilib'])
193         self.add_config(arch='hppa',
194                         os_name='linux-gnu')
195         self.add_config(arch='i686',
196                         os_name='gnu')
197         self.add_config(arch='ia64',
198                         os_name='linux-gnu',
199                         first_gcc_cfg=['--with-system-libunwind'])
200         self.add_config(arch='m68k',
201                         os_name='linux-gnu',
202                         gcc_cfg=['--disable-multilib'])
203         self.add_config(arch='m68k',
204                         os_name='linux-gnu',
205                         variant='coldfire',
206                         gcc_cfg=['--with-arch=cf', '--disable-multilib'])
207         self.add_config(arch='m68k',
208                         os_name='linux-gnu',
209                         variant='coldfire-soft',
210                         gcc_cfg=['--with-arch=cf', '--with-cpu=54455',
211                                  '--disable-multilib'])
212         self.add_config(arch='microblaze',
213                         os_name='linux-gnu',
214                         gcc_cfg=['--disable-multilib'])
215         self.add_config(arch='microblazeel',
216                         os_name='linux-gnu',
217                         gcc_cfg=['--disable-multilib'])
218         self.add_config(arch='mips64',
219                         os_name='linux-gnu',
220                         gcc_cfg=['--with-mips-plt'],
221                         glibcs=[{'variant': 'n32'},
222                                 {'arch': 'mips',
223                                  'ccopts': '-mabi=32'},
224                                 {'variant': 'n64',
225                                  'ccopts': '-mabi=64'}])
226         self.add_config(arch='mips64',
227                         os_name='linux-gnu',
228                         variant='soft',
229                         gcc_cfg=['--with-mips-plt', '--with-float=soft'],
230                         glibcs=[{'variant': 'n32-soft'},
231                                 {'variant': 'soft',
232                                  'arch': 'mips',
233                                  'ccopts': '-mabi=32'},
234                                 {'variant': 'n64-soft',
235                                  'ccopts': '-mabi=64'}])
236         self.add_config(arch='mips64',
237                         os_name='linux-gnu',
238                         variant='nan2008',
239                         gcc_cfg=['--with-mips-plt', '--with-nan=2008',
240                                  '--with-arch-64=mips64r2',
241                                  '--with-arch-32=mips32r2'],
242                         glibcs=[{'variant': 'n32-nan2008'},
243                                 {'variant': 'nan2008',
244                                  'arch': 'mips',
245                                  'ccopts': '-mabi=32'},
246                                 {'variant': 'n64-nan2008',
247                                  'ccopts': '-mabi=64'}])
248         self.add_config(arch='mips64',
249                         os_name='linux-gnu',
250                         variant='nan2008-soft',
251                         gcc_cfg=['--with-mips-plt', '--with-nan=2008',
252                                  '--with-arch-64=mips64r2',
253                                  '--with-arch-32=mips32r2',
254                                  '--with-float=soft'],
255                         glibcs=[{'variant': 'n32-nan2008-soft'},
256                                 {'variant': 'nan2008-soft',
257                                  'arch': 'mips',
258                                  'ccopts': '-mabi=32'},
259                                 {'variant': 'n64-nan2008-soft',
260                                  'ccopts': '-mabi=64'}])
261         self.add_config(arch='mips64el',
262                         os_name='linux-gnu',
263                         gcc_cfg=['--with-mips-plt'],
264                         glibcs=[{'variant': 'n32'},
265                                 {'arch': 'mipsel',
266                                  'ccopts': '-mabi=32'},
267                                 {'variant': 'n64',
268                                  'ccopts': '-mabi=64'}])
269         self.add_config(arch='mips64el',
270                         os_name='linux-gnu',
271                         variant='soft',
272                         gcc_cfg=['--with-mips-plt', '--with-float=soft'],
273                         glibcs=[{'variant': 'n32-soft'},
274                                 {'variant': 'soft',
275                                  'arch': 'mipsel',
276                                  'ccopts': '-mabi=32'},
277                                 {'variant': 'n64-soft',
278                                  'ccopts': '-mabi=64'}])
279         self.add_config(arch='mips64el',
280                         os_name='linux-gnu',
281                         variant='nan2008',
282                         gcc_cfg=['--with-mips-plt', '--with-nan=2008',
283                                  '--with-arch-64=mips64r2',
284                                  '--with-arch-32=mips32r2'],
285                         glibcs=[{'variant': 'n32-nan2008'},
286                                 {'variant': 'nan2008',
287                                  'arch': 'mipsel',
288                                  'ccopts': '-mabi=32'},
289                                 {'variant': 'n64-nan2008',
290                                  'ccopts': '-mabi=64'}])
291         self.add_config(arch='mips64el',
292                         os_name='linux-gnu',
293                         variant='nan2008-soft',
294                         gcc_cfg=['--with-mips-plt', '--with-nan=2008',
295                                  '--with-arch-64=mips64r2',
296                                  '--with-arch-32=mips32r2',
297                                  '--with-float=soft'],
298                         glibcs=[{'variant': 'n32-nan2008-soft'},
299                                 {'variant': 'nan2008-soft',
300                                  'arch': 'mipsel',
301                                  'ccopts': '-mabi=32'},
302                                 {'variant': 'n64-nan2008-soft',
303                                  'ccopts': '-mabi=64'}])
304         self.add_config(arch='nios2',
305                         os_name='linux-gnu')
306         self.add_config(arch='powerpc',
307                         os_name='linux-gnu',
308                         gcc_cfg=['--disable-multilib', '--enable-secureplt'],
309                         extra_glibcs=[{'variant': 'power4',
310                                        'ccopts': '-mcpu=power4',
311                                        'cfg': ['--with-cpu=power4']}])
312         self.add_config(arch='powerpc',
313                         os_name='linux-gnu',
314                         variant='soft',
315                         gcc_cfg=['--disable-multilib', '--with-float=soft',
316                                  '--enable-secureplt'])
317         self.add_config(arch='powerpc64',
318                         os_name='linux-gnu',
319                         gcc_cfg=['--disable-multilib', '--enable-secureplt'])
320         self.add_config(arch='powerpc64le',
321                         os_name='linux-gnu',
322                         gcc_cfg=['--disable-multilib', '--enable-secureplt'])
323         self.add_config(arch='riscv64',
324                         os_name='linux-gnu',
325                         variant='rv64imac-lp64',
326                         gcc_cfg=['--with-arch=rv64imac', '--with-abi=lp64',
327                                  '--disable-multilib'])
328         self.add_config(arch='riscv64',
329                         os_name='linux-gnu',
330                         variant='rv64imafdc-lp64',
331                         gcc_cfg=['--with-arch=rv64imafdc', '--with-abi=lp64',
332                                  '--disable-multilib'])
333         self.add_config(arch='riscv64',
334                         os_name='linux-gnu',
335                         variant='rv64imafdc-lp64d',
336                         gcc_cfg=['--with-arch=rv64imafdc', '--with-abi=lp64d',
337                                  '--disable-multilib'])
338         self.add_config(arch='s390x',
339                         os_name='linux-gnu',
340                         glibcs=[{},
341                                 {'arch': 's390', 'ccopts': '-m31'}])
342         self.add_config(arch='sh3',
343                         os_name='linux-gnu')
344         self.add_config(arch='sh3eb',
345                         os_name='linux-gnu')
346         self.add_config(arch='sh4',
347                         os_name='linux-gnu')
348         self.add_config(arch='sh4eb',
349                         os_name='linux-gnu')
350         self.add_config(arch='sh4',
351                         os_name='linux-gnu',
352                         variant='soft',
353                         gcc_cfg=['--without-fp'])
354         self.add_config(arch='sh4eb',
355                         os_name='linux-gnu',
356                         variant='soft',
357                         gcc_cfg=['--without-fp'])
358         self.add_config(arch='sparc64',
359                         os_name='linux-gnu',
360                         glibcs=[{},
361                                 {'arch': 'sparcv9',
362                                  'ccopts': '-m32 -mlong-double-128'}],
363                         extra_glibcs=[{'variant': 'disable-multi-arch',
364                                        'cfg': ['--disable-multi-arch']},
365                                       {'variant': 'disable-multi-arch',
366                                        'arch': 'sparcv9',
367                                        'ccopts': '-m32 -mlong-double-128',
368                                        'cfg': ['--disable-multi-arch']}])
369         self.add_config(arch='x86_64',
370                         os_name='linux-gnu',
371                         gcc_cfg=['--with-multilib-list=m64,m32,mx32'],
372                         glibcs=[{},
373                                 {'variant': 'x32', 'ccopts': '-mx32'},
374                                 {'arch': 'i686', 'ccopts': '-m32 -march=i686'}],
375                         extra_glibcs=[{'variant': 'disable-multi-arch',
376                                        'cfg': ['--disable-multi-arch']},
377                                       {'variant': 'enable-obsolete',
378                                        'cfg': ['--enable-obsolete-rpc',
379                                                '--enable-obsolete-nsl']},
380                                       {'variant': 'static-pie',
381                                        'cfg': ['--enable-static-pie']},
382                                       {'variant': 'x32-static-pie',
383                                        'ccopts': '-mx32',
384                                        'cfg': ['--enable-static-pie']},
385                                       {'variant': 'static-pie',
386                                        'arch': 'i686',
387                                        'ccopts': '-m32 -march=i686',
388                                        'cfg': ['--enable-static-pie']},
389                                       {'variant': 'disable-multi-arch',
390                                        'arch': 'i686',
391                                        'ccopts': '-m32 -march=i686',
392                                        'cfg': ['--disable-multi-arch']},
393                                       {'variant': 'enable-obsolete',
394                                        'arch': 'i686',
395                                        'ccopts': '-m32 -march=i686',
396                                        'cfg': ['--enable-obsolete-rpc',
397                                                '--enable-obsolete-nsl']},
398                                       {'arch': 'i486',
399                                        'ccopts': '-m32 -march=i486'},
400                                       {'arch': 'i586',
401                                        'ccopts': '-m32 -march=i586'}])
402
403     def add_config(self, **args):
404         """Add an individual build configuration."""
405         cfg = Config(self, **args)
406         if cfg.name in self.configs:
407             print('error: duplicate config %s' % cfg.name)
408             exit(1)
409         self.configs[cfg.name] = cfg
410         for c in cfg.all_glibcs:
411             if c.name in self.glibc_configs:
412                 print('error: duplicate glibc config %s' % c.name)
413                 exit(1)
414             self.glibc_configs[c.name] = c
415
416     def component_srcdir(self, component):
417         """Return the source directory for a given component, e.g. gcc."""
418         return os.path.join(self.srcdir, component)
419
420     def component_builddir(self, action, config, component, subconfig=None):
421         """Return the directory to use for a build."""
422         if config is None:
423             # Host libraries.
424             assert subconfig is None
425             return os.path.join(self.builddir, action, component)
426         if subconfig is None:
427             return os.path.join(self.builddir, action, config, component)
428         else:
429             # glibc build as part of compiler build.
430             return os.path.join(self.builddir, action, config, component,
431                                 subconfig)
432
433     def compiler_installdir(self, config):
434         """Return the directory in which to install a compiler."""
435         return os.path.join(self.installdir, 'compilers', config)
436
437     def compiler_bindir(self, config):
438         """Return the directory in which to find compiler binaries."""
439         return os.path.join(self.compiler_installdir(config), 'bin')
440
441     def compiler_sysroot(self, config):
442         """Return the sysroot directory for a compiler."""
443         return os.path.join(self.compiler_installdir(config), 'sysroot')
444
445     def glibc_installdir(self, config):
446         """Return the directory in which to install glibc."""
447         return os.path.join(self.installdir, 'glibcs', config)
448
449     def run_builds(self, action, configs):
450         """Run the requested builds."""
451         if action == 'checkout':
452             self.checkout(configs)
453             return
454         if action == 'bot-cycle':
455             if configs:
456                 print('error: configurations specified for bot-cycle')
457                 exit(1)
458             self.bot_cycle()
459             return
460         if action == 'bot':
461             if configs:
462                 print('error: configurations specified for bot')
463                 exit(1)
464             self.bot()
465             return
466         if action == 'host-libraries' and configs:
467             print('error: configurations specified for host-libraries')
468             exit(1)
469         self.clear_last_build_state(action)
470         build_time = datetime.datetime.utcnow()
471         if action == 'host-libraries':
472             build_components = ('gmp', 'mpfr', 'mpc')
473             old_components = ()
474             old_versions = {}
475             self.build_host_libraries()
476         elif action == 'compilers':
477             build_components = ('binutils', 'gcc', 'glibc', 'linux', 'mig',
478                                 'gnumach', 'hurd')
479             old_components = ('gmp', 'mpfr', 'mpc')
480             old_versions = self.build_state['host-libraries']['build-versions']
481             self.build_compilers(configs)
482         else:
483             build_components = ('glibc',)
484             old_components = ('gmp', 'mpfr', 'mpc', 'binutils', 'gcc', 'linux',
485                               'mig', 'gnumach', 'hurd')
486             old_versions = self.build_state['compilers']['build-versions']
487             self.build_glibcs(configs)
488         self.write_files()
489         self.do_build()
490         if configs:
491             # Partial build, do not update stored state.
492             return
493         build_versions = {}
494         for k in build_components:
495             if k in self.versions:
496                 build_versions[k] = {'version': self.versions[k]['version'],
497                                      'revision': self.versions[k]['revision']}
498         for k in old_components:
499             if k in old_versions:
500                 build_versions[k] = {'version': old_versions[k]['version'],
501                                      'revision': old_versions[k]['revision']}
502         self.update_build_state(action, build_time, build_versions)
503
504     @staticmethod
505     def remove_dirs(*args):
506         """Remove directories and their contents if they exist."""
507         for dir in args:
508             shutil.rmtree(dir, ignore_errors=True)
509
510     @staticmethod
511     def remove_recreate_dirs(*args):
512         """Remove directories if they exist, and create them as empty."""
513         Context.remove_dirs(*args)
514         for dir in args:
515             os.makedirs(dir, exist_ok=True)
516
517     def add_makefile_cmdlist(self, target, cmdlist, logsdir):
518         """Add makefile text for a list of commands."""
519         commands = cmdlist.makefile_commands(self.wrapper, logsdir)
520         self.makefile_pieces.append('all: %s\n.PHONY: %s\n%s:\n%s\n' %
521                                     (target, target, target, commands))
522         self.status_log_list.extend(cmdlist.status_logs(logsdir))
523
524     def write_files(self):
525         """Write out the Makefile and wrapper script."""
526         mftext = ''.join(self.makefile_pieces)
527         with open(self.makefile, 'w') as f:
528             f.write(mftext)
529         wrapper_text = (
530             '#!/bin/sh\n'
531             'prev_base=$1\n'
532             'this_base=$2\n'
533             'desc=$3\n'
534             'dir=$4\n'
535             'path=$5\n'
536             'shift 5\n'
537             'prev_status=$prev_base-status.txt\n'
538             'this_status=$this_base-status.txt\n'
539             'this_log=$this_base-log.txt\n'
540             'date > "$this_log"\n'
541             'echo >> "$this_log"\n'
542             'echo "Description: $desc" >> "$this_log"\n'
543             'printf "%s" "Command:" >> "$this_log"\n'
544             'for word in "$@"; do\n'
545             '  if expr "$word" : "[]+,./0-9@A-Z_a-z-]\\\\{1,\\\\}\\$" > /dev/null; then\n'
546             '    printf " %s" "$word"\n'
547             '  else\n'
548             '    printf " \'"\n'
549             '    printf "%s" "$word" | sed -e "s/\'/\'\\\\\\\\\'\'/"\n'
550             '    printf "\'"\n'
551             '  fi\n'
552             'done >> "$this_log"\n'
553             'echo >> "$this_log"\n'
554             'echo "Directory: $dir" >> "$this_log"\n'
555             'echo "Path addition: $path" >> "$this_log"\n'
556             'echo >> "$this_log"\n'
557             'record_status ()\n'
558             '{\n'
559             '  echo >> "$this_log"\n'
560             '  echo "$1: $desc" > "$this_status"\n'
561             '  echo "$1: $desc" >> "$this_log"\n'
562             '  echo >> "$this_log"\n'
563             '  date >> "$this_log"\n'
564             '  echo "$1: $desc"\n'
565             '  exit 0\n'
566             '}\n'
567             'check_error ()\n'
568             '{\n'
569             '  if [ "$1" != "0" ]; then\n'
570             '    record_status FAIL\n'
571             '  fi\n'
572             '}\n'
573             'if [ "$prev_base" ] && ! grep -q "^PASS" "$prev_status"; then\n'
574             '    record_status UNRESOLVED\n'
575             'fi\n'
576             'if [ "$dir" ]; then\n'
577             '  cd "$dir"\n'
578             '  check_error "$?"\n'
579             'fi\n'
580             'if [ "$path" ]; then\n'
581             '  PATH=$path:$PATH\n'
582             'fi\n'
583             '"$@" < /dev/null >> "$this_log" 2>&1\n'
584             'check_error "$?"\n'
585             'record_status PASS\n')
586         with open(self.wrapper, 'w') as f:
587             f.write(wrapper_text)
588         # Mode 0o755.
589         mode_exec = (stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|
590                      stat.S_IROTH|stat.S_IXOTH)
591         os.chmod(self.wrapper, mode_exec)
592         save_logs_text = (
593             '#!/bin/sh\n'
594             'if ! [ -f tests.sum ]; then\n'
595             '  echo "No test summary available."\n'
596             '  exit 0\n'
597             'fi\n'
598             'save_file ()\n'
599             '{\n'
600             '  echo "Contents of $1:"\n'
601             '  echo\n'
602             '  cat "$1"\n'
603             '  echo\n'
604             '  echo "End of contents of $1."\n'
605             '  echo\n'
606             '}\n'
607             'save_file tests.sum\n'
608             'non_pass_tests=$(grep -v "^PASS: " tests.sum | sed -e "s/^PASS: //")\n'
609             'for t in $non_pass_tests; do\n'
610             '  if [ -f "$t.out" ]; then\n'
611             '    save_file "$t.out"\n'
612             '  fi\n'
613             'done\n')
614         with open(self.save_logs, 'w') as f:
615             f.write(save_logs_text)
616         os.chmod(self.save_logs, mode_exec)
617
618     def do_build(self):
619         """Do the actual build."""
620         cmd = ['make', '-j%d' % self.parallelism]
621         subprocess.run(cmd, cwd=self.builddir, check=True)
622
623     def build_host_libraries(self):
624         """Build the host libraries."""
625         installdir = self.host_libraries_installdir
626         builddir = os.path.join(self.builddir, 'host-libraries')
627         logsdir = os.path.join(self.logsdir, 'host-libraries')
628         self.remove_recreate_dirs(installdir, builddir, logsdir)
629         cmdlist = CommandList('host-libraries', self.keep)
630         self.build_host_library(cmdlist, 'gmp')
631         self.build_host_library(cmdlist, 'mpfr',
632                                 ['--with-gmp=%s' % installdir])
633         self.build_host_library(cmdlist, 'mpc',
634                                 ['--with-gmp=%s' % installdir,
635                                 '--with-mpfr=%s' % installdir])
636         cmdlist.add_command('done', ['touch', os.path.join(installdir, 'ok')])
637         self.add_makefile_cmdlist('host-libraries', cmdlist, logsdir)
638
639     def build_host_library(self, cmdlist, lib, extra_opts=None):
640         """Build one host library."""
641         srcdir = self.component_srcdir(lib)
642         builddir = self.component_builddir('host-libraries', None, lib)
643         installdir = self.host_libraries_installdir
644         cmdlist.push_subdesc(lib)
645         cmdlist.create_use_dir(builddir)
646         cfg_cmd = [os.path.join(srcdir, 'configure'),
647                    '--prefix=%s' % installdir,
648                    '--disable-shared']
649         if extra_opts:
650             cfg_cmd.extend (extra_opts)
651         cmdlist.add_command('configure', cfg_cmd)
652         cmdlist.add_command('build', ['make'])
653         cmdlist.add_command('check', ['make', 'check'])
654         cmdlist.add_command('install', ['make', 'install'])
655         cmdlist.cleanup_dir()
656         cmdlist.pop_subdesc()
657
658     def build_compilers(self, configs):
659         """Build the compilers."""
660         if not configs:
661             self.remove_dirs(os.path.join(self.builddir, 'compilers'))
662             self.remove_dirs(os.path.join(self.installdir, 'compilers'))
663             self.remove_dirs(os.path.join(self.logsdir, 'compilers'))
664             configs = sorted(self.configs.keys())
665         for c in configs:
666             self.configs[c].build()
667
668     def build_glibcs(self, configs):
669         """Build the glibcs."""
670         if not configs:
671             self.remove_dirs(os.path.join(self.builddir, 'glibcs'))
672             self.remove_dirs(os.path.join(self.installdir, 'glibcs'))
673             self.remove_dirs(os.path.join(self.logsdir, 'glibcs'))
674             configs = sorted(self.glibc_configs.keys())
675         for c in configs:
676             self.glibc_configs[c].build()
677
678     def load_versions_json(self):
679         """Load information about source directory versions."""
680         if not os.access(self.versions_json, os.F_OK):
681             self.versions = {}
682             return
683         with open(self.versions_json, 'r') as f:
684             self.versions = json.load(f)
685
686     def store_json(self, data, filename):
687         """Store information in a JSON file."""
688         filename_tmp = filename + '.tmp'
689         with open(filename_tmp, 'w') as f:
690             json.dump(data, f, indent=2, sort_keys=True)
691         os.rename(filename_tmp, filename)
692
693     def store_versions_json(self):
694         """Store information about source directory versions."""
695         self.store_json(self.versions, self.versions_json)
696
697     def set_component_version(self, component, version, explicit, revision):
698         """Set the version information for a component."""
699         self.versions[component] = {'version': version,
700                                     'explicit': explicit,
701                                     'revision': revision}
702         self.store_versions_json()
703
704     def checkout(self, versions):
705         """Check out the desired component versions."""
706         default_versions = {'binutils': 'vcs-2.32',
707                             'gcc': 'vcs-9',
708                             'glibc': 'vcs-mainline',
709                             'gmp': '6.1.2',
710                             'linux': '5.0',
711                             'mpc': '1.1.0',
712                             'mpfr': '4.0.2',
713                             'mig': 'vcs-mainline',
714                             'gnumach': 'vcs-mainline',
715                             'hurd': 'vcs-mainline'}
716         use_versions = {}
717         explicit_versions = {}
718         for v in versions:
719             found_v = False
720             for k in default_versions.keys():
721                 kx = k + '-'
722                 if v.startswith(kx):
723                     vx = v[len(kx):]
724                     if k in use_versions:
725                         print('error: multiple versions for %s' % k)
726                         exit(1)
727                     use_versions[k] = vx
728                     explicit_versions[k] = True
729                     found_v = True
730                     break
731             if not found_v:
732                 print('error: unknown component in %s' % v)
733                 exit(1)
734         for k in default_versions.keys():
735             if k not in use_versions:
736                 if k in self.versions and self.versions[k]['explicit']:
737                     use_versions[k] = self.versions[k]['version']
738                     explicit_versions[k] = True
739                 else:
740                     use_versions[k] = default_versions[k]
741                     explicit_versions[k] = False
742         os.makedirs(self.srcdir, exist_ok=True)
743         for k in sorted(default_versions.keys()):
744             update = os.access(self.component_srcdir(k), os.F_OK)
745             v = use_versions[k]
746             if (update and
747                 k in self.versions and
748                 v != self.versions[k]['version']):
749                 if not self.replace_sources:
750                     print('error: version of %s has changed from %s to %s, '
751                           'use --replace-sources to check out again' %
752                           (k, self.versions[k]['version'], v))
753                     exit(1)
754                 shutil.rmtree(self.component_srcdir(k))
755                 update = False
756             if v.startswith('vcs-'):
757                 revision = self.checkout_vcs(k, v[4:], update)
758             else:
759                 self.checkout_tar(k, v, update)
760                 revision = v
761             self.set_component_version(k, v, explicit_versions[k], revision)
762         if self.get_script_text() != self.script_text:
763             # Rerun the checkout process in case the updated script
764             # uses different default versions or new components.
765             self.exec_self()
766
767     def checkout_vcs(self, component, version, update):
768         """Check out the given version of the given component from version
769         control.  Return a revision identifier."""
770         if component == 'binutils':
771             git_url = 'git://sourceware.org/git/binutils-gdb.git'
772             if version == 'mainline':
773                 git_branch = 'master'
774             else:
775                 trans = str.maketrans({'.': '_'})
776                 git_branch = 'binutils-%s-branch' % version.translate(trans)
777             return self.git_checkout(component, git_url, git_branch, update)
778         elif component == 'gcc':
779             if version == 'mainline':
780                 branch = 'trunk'
781             else:
782                 trans = str.maketrans({'.': '_'})
783                 branch = 'branches/gcc-%s-branch' % version.translate(trans)
784             svn_url = 'svn://gcc.gnu.org/svn/gcc/%s' % branch
785             return self.gcc_checkout(svn_url, update)
786         elif component == 'glibc':
787             git_url = 'git://sourceware.org/git/glibc.git'
788             if version == 'mainline':
789                 git_branch = 'master'
790             else:
791                 git_branch = 'release/%s/master' % version
792             r = self.git_checkout(component, git_url, git_branch, update)
793             self.fix_glibc_timestamps()
794             return r
795         elif component == 'gnumach':
796             git_url = 'git://git.savannah.gnu.org/hurd/gnumach.git'
797             git_branch = 'master'
798             r = self.git_checkout(component, git_url, git_branch, update)
799             subprocess.run(['autoreconf', '-i'],
800                            cwd=self.component_srcdir(component), check=True)
801             return r
802         elif component == 'mig':
803             git_url = 'git://git.savannah.gnu.org/hurd/mig.git'
804             git_branch = 'master'
805             r = self.git_checkout(component, git_url, git_branch, update)
806             subprocess.run(['autoreconf', '-i'],
807                            cwd=self.component_srcdir(component), check=True)
808             return r
809         elif component == 'hurd':
810             git_url = 'git://git.savannah.gnu.org/hurd/hurd.git'
811             git_branch = 'master'
812             r = self.git_checkout(component, git_url, git_branch, update)
813             subprocess.run(['autoconf'],
814                            cwd=self.component_srcdir(component), check=True)
815             return r
816         else:
817             print('error: component %s coming from VCS' % component)
818             exit(1)
819
820     def git_checkout(self, component, git_url, git_branch, update):
821         """Check out a component from git.  Return a commit identifier."""
822         if update:
823             subprocess.run(['git', 'remote', 'prune', 'origin'],
824                            cwd=self.component_srcdir(component), check=True)
825             if self.replace_sources:
826                 subprocess.run(['git', 'clean', '-dxfq'],
827                                cwd=self.component_srcdir(component), check=True)
828             subprocess.run(['git', 'pull', '-q'],
829                            cwd=self.component_srcdir(component), check=True)
830         else:
831             subprocess.run(['git', 'clone', '-q', '-b', git_branch, git_url,
832                             self.component_srcdir(component)], check=True)
833         r = subprocess.run(['git', 'rev-parse', 'HEAD'],
834                            cwd=self.component_srcdir(component),
835                            stdout=subprocess.PIPE,
836                            check=True, universal_newlines=True).stdout
837         return r.rstrip()
838
839     def fix_glibc_timestamps(self):
840         """Fix timestamps in a glibc checkout."""
841         # Ensure that builds do not try to regenerate generated files
842         # in the source tree.
843         srcdir = self.component_srcdir('glibc')
844         # These files have Makefile dependencies to regenerate them in
845         # the source tree that may be active during a normal build.
846         # Some other files have such dependencies but do not need to
847         # be touched because nothing in a build depends on the files
848         # in question.
849         for f in ('sysdeps/gnu/errlist.c',
850                   'sysdeps/mach/hurd/bits/errno.h',
851                   'sysdeps/sparc/sparc32/rem.S',
852                   'sysdeps/sparc/sparc32/sdiv.S',
853                   'sysdeps/sparc/sparc32/udiv.S',
854                   'sysdeps/sparc/sparc32/urem.S'):
855             to_touch = os.path.join(srcdir, f)
856             subprocess.run(['touch', '-c', to_touch], check=True)
857         for dirpath, dirnames, filenames in os.walk(srcdir):
858             for f in filenames:
859                 if (f == 'configure' or
860                     f == 'preconfigure' or
861                     f.endswith('-kw.h')):
862                     to_touch = os.path.join(dirpath, f)
863                     subprocess.run(['touch', to_touch], check=True)
864
865     def gcc_checkout(self, svn_url, update):
866         """Check out GCC from SVN.  Return the revision number."""
867         if not update:
868             subprocess.run(['svn', 'co', '-q', svn_url,
869                             self.component_srcdir('gcc')], check=True)
870         subprocess.run(['contrib/gcc_update', '--silent'],
871                        cwd=self.component_srcdir('gcc'), check=True)
872         r = subprocess.run(['svnversion', self.component_srcdir('gcc')],
873                            stdout=subprocess.PIPE,
874                            check=True, universal_newlines=True).stdout
875         return r.rstrip()
876
877     def checkout_tar(self, component, version, update):
878         """Check out the given version of the given component from a
879         tarball."""
880         if update:
881             return
882         url_map = {'binutils': 'https://ftp.gnu.org/gnu/binutils/binutils-%(version)s.tar.bz2',
883                    'gcc': 'https://ftp.gnu.org/gnu/gcc/gcc-%(version)s/gcc-%(version)s.tar.gz',
884                    'gmp': 'https://ftp.gnu.org/gnu/gmp/gmp-%(version)s.tar.xz',
885                    'linux': 'https://www.kernel.org/pub/linux/kernel/v%(major)s.x/linux-%(version)s.tar.xz',
886                    'mpc': 'https://ftp.gnu.org/gnu/mpc/mpc-%(version)s.tar.gz',
887                    'mpfr': 'https://ftp.gnu.org/gnu/mpfr/mpfr-%(version)s.tar.xz',
888                    'mig': 'https://ftp.gnu.org/gnu/mig/mig-%(version)s.tar.bz2',
889                    'gnumach': 'https://ftp.gnu.org/gnu/gnumach/gnumach-%(version)s.tar.bz2',
890                    'hurd': 'https://ftp.gnu.org/gnu/hurd/hurd-%(version)s.tar.bz2'}
891         if component not in url_map:
892             print('error: component %s coming from tarball' % component)
893             exit(1)
894         version_major = version.split('.')[0]
895         url = url_map[component] % {'version': version, 'major': version_major}
896         filename = os.path.join(self.srcdir, url.split('/')[-1])
897         response = urllib.request.urlopen(url)
898         data = response.read()
899         with open(filename, 'wb') as f:
900             f.write(data)
901         subprocess.run(['tar', '-C', self.srcdir, '-x', '-f', filename],
902                        check=True)
903         os.rename(os.path.join(self.srcdir, '%s-%s' % (component, version)),
904                   self.component_srcdir(component))
905         os.remove(filename)
906
907     def load_build_state_json(self):
908         """Load information about the state of previous builds."""
909         if os.access(self.build_state_json, os.F_OK):
910             with open(self.build_state_json, 'r') as f:
911                 self.build_state = json.load(f)
912         else:
913             self.build_state = {}
914         for k in ('host-libraries', 'compilers', 'glibcs'):
915             if k not in self.build_state:
916                 self.build_state[k] = {}
917             if 'build-time' not in self.build_state[k]:
918                 self.build_state[k]['build-time'] = ''
919             if 'build-versions' not in self.build_state[k]:
920                 self.build_state[k]['build-versions'] = {}
921             if 'build-results' not in self.build_state[k]:
922                 self.build_state[k]['build-results'] = {}
923             if 'result-changes' not in self.build_state[k]:
924                 self.build_state[k]['result-changes'] = {}
925             if 'ever-passed' not in self.build_state[k]:
926                 self.build_state[k]['ever-passed'] = []
927
928     def store_build_state_json(self):
929         """Store information about the state of previous builds."""
930         self.store_json(self.build_state, self.build_state_json)
931
932     def clear_last_build_state(self, action):
933         """Clear information about the state of part of the build."""
934         # We clear the last build time and versions when starting a
935         # new build.  The results of the last build are kept around,
936         # as comparison is still meaningful if this build is aborted
937         # and a new one started.
938         self.build_state[action]['build-time'] = ''
939         self.build_state[action]['build-versions'] = {}
940         self.store_build_state_json()
941
942     def update_build_state(self, action, build_time, build_versions):
943         """Update the build state after a build."""
944         build_time = build_time.replace(microsecond=0)
945         self.build_state[action]['build-time'] = str(build_time)
946         self.build_state[action]['build-versions'] = build_versions
947         build_results = {}
948         for log in self.status_log_list:
949             with open(log, 'r') as f:
950                 log_text = f.read()
951             log_text = log_text.rstrip()
952             m = re.fullmatch('([A-Z]+): (.*)', log_text)
953             result = m.group(1)
954             test_name = m.group(2)
955             assert test_name not in build_results
956             build_results[test_name] = result
957         old_build_results = self.build_state[action]['build-results']
958         self.build_state[action]['build-results'] = build_results
959         result_changes = {}
960         all_tests = set(old_build_results.keys()) | set(build_results.keys())
961         for t in all_tests:
962             if t in old_build_results:
963                 old_res = old_build_results[t]
964             else:
965                 old_res = '(New test)'
966             if t in build_results:
967                 new_res = build_results[t]
968             else:
969                 new_res = '(Test removed)'
970             if old_res != new_res:
971                 result_changes[t] = '%s -> %s' % (old_res, new_res)
972         self.build_state[action]['result-changes'] = result_changes
973         old_ever_passed = {t for t in self.build_state[action]['ever-passed']
974                            if t in build_results}
975         new_passes = {t for t in build_results if build_results[t] == 'PASS'}
976         self.build_state[action]['ever-passed'] = sorted(old_ever_passed |
977                                                          new_passes)
978         self.store_build_state_json()
979
980     def load_bot_config_json(self):
981         """Load bot configuration."""
982         with open(self.bot_config_json, 'r') as f:
983             self.bot_config = json.load(f)
984
985     def part_build_old(self, action, delay):
986         """Return whether the last build for a given action was at least a
987         given number of seconds ago, or does not have a time recorded."""
988         old_time_str = self.build_state[action]['build-time']
989         if not old_time_str:
990             return True
991         old_time = datetime.datetime.strptime(old_time_str,
992                                               '%Y-%m-%d %H:%M:%S')
993         new_time = datetime.datetime.utcnow()
994         delta = new_time - old_time
995         return delta.total_seconds() >= delay
996
997     def bot_cycle(self):
998         """Run a single round of checkout and builds."""
999         print('Bot cycle starting %s.' % str(datetime.datetime.utcnow()))
1000         self.load_bot_config_json()
1001         actions = ('host-libraries', 'compilers', 'glibcs')
1002         self.bot_run_self(['--replace-sources'], 'checkout')
1003         self.load_versions_json()
1004         if self.get_script_text() != self.script_text:
1005             print('Script changed, re-execing.')
1006             # On script change, all parts of the build should be rerun.
1007             for a in actions:
1008                 self.clear_last_build_state(a)
1009             self.exec_self()
1010         check_components = {'host-libraries': ('gmp', 'mpfr', 'mpc'),
1011                             'compilers': ('binutils', 'gcc', 'glibc', 'linux',
1012                                           'mig', 'gnumach', 'hurd'),
1013                             'glibcs': ('glibc',)}
1014         must_build = {}
1015         for a in actions:
1016             build_vers = self.build_state[a]['build-versions']
1017             must_build[a] = False
1018             if not self.build_state[a]['build-time']:
1019                 must_build[a] = True
1020             old_vers = {}
1021             new_vers = {}
1022             for c in check_components[a]:
1023                 if c in build_vers:
1024                     old_vers[c] = build_vers[c]
1025                 new_vers[c] = {'version': self.versions[c]['version'],
1026                                'revision': self.versions[c]['revision']}
1027             if new_vers == old_vers:
1028                 print('Versions for %s unchanged.' % a)
1029             else:
1030                 print('Versions changed or rebuild forced for %s.' % a)
1031                 if a == 'compilers' and not self.part_build_old(
1032                         a, self.bot_config['compilers-rebuild-delay']):
1033                     print('Not requiring rebuild of compilers this soon.')
1034                 else:
1035                     must_build[a] = True
1036         if must_build['host-libraries']:
1037             must_build['compilers'] = True
1038         if must_build['compilers']:
1039             must_build['glibcs'] = True
1040         for a in actions:
1041             if must_build[a]:
1042                 print('Must rebuild %s.' % a)
1043                 self.clear_last_build_state(a)
1044             else:
1045                 print('No need to rebuild %s.' % a)
1046         if os.access(self.logsdir, os.F_OK):
1047             shutil.rmtree(self.logsdir_old, ignore_errors=True)
1048             shutil.copytree(self.logsdir, self.logsdir_old)
1049         for a in actions:
1050             if must_build[a]:
1051                 build_time = datetime.datetime.utcnow()
1052                 print('Rebuilding %s at %s.' % (a, str(build_time)))
1053                 self.bot_run_self([], a)
1054                 self.load_build_state_json()
1055                 self.bot_build_mail(a, build_time)
1056         print('Bot cycle done at %s.' % str(datetime.datetime.utcnow()))
1057
1058     def bot_build_mail(self, action, build_time):
1059         """Send email with the results of a build."""
1060         if not ('email-from' in self.bot_config and
1061                 'email-server' in self.bot_config and
1062                 'email-subject' in self.bot_config and
1063                 'email-to' in self.bot_config):
1064             if not self.email_warning:
1065                 print("Email not configured, not sending.")
1066                 self.email_warning = True
1067             return
1068
1069         build_time = build_time.replace(microsecond=0)
1070         subject = (self.bot_config['email-subject'] %
1071                    {'action': action,
1072                     'build-time': str(build_time)})
1073         results = self.build_state[action]['build-results']
1074         changes = self.build_state[action]['result-changes']
1075         ever_passed = set(self.build_state[action]['ever-passed'])
1076         versions = self.build_state[action]['build-versions']
1077         new_regressions = {k for k in changes if changes[k] == 'PASS -> FAIL'}
1078         all_regressions = {k for k in ever_passed if results[k] == 'FAIL'}
1079         all_fails = {k for k in results if results[k] == 'FAIL'}
1080         if new_regressions:
1081             new_reg_list = sorted(['FAIL: %s' % k for k in new_regressions])
1082             new_reg_text = ('New regressions:\n\n%s\n\n' %
1083                             '\n'.join(new_reg_list))
1084         else:
1085             new_reg_text = ''
1086         if all_regressions:
1087             all_reg_list = sorted(['FAIL: %s' % k for k in all_regressions])
1088             all_reg_text = ('All regressions:\n\n%s\n\n' %
1089                             '\n'.join(all_reg_list))
1090         else:
1091             all_reg_text = ''
1092         if all_fails:
1093             all_fail_list = sorted(['FAIL: %s' % k for k in all_fails])
1094             all_fail_text = ('All failures:\n\n%s\n\n' %
1095                              '\n'.join(all_fail_list))
1096         else:
1097             all_fail_text = ''
1098         if changes:
1099             changes_list = sorted(changes.keys())
1100             changes_list = ['%s: %s' % (changes[k], k) for k in changes_list]
1101             changes_text = ('All changed results:\n\n%s\n\n' %
1102                             '\n'.join(changes_list))
1103         else:
1104             changes_text = ''
1105         results_text = (new_reg_text + all_reg_text + all_fail_text +
1106                         changes_text)
1107         if not results_text:
1108             results_text = 'Clean build with unchanged results.\n\n'
1109         versions_list = sorted(versions.keys())
1110         versions_list = ['%s: %s (%s)' % (k, versions[k]['version'],
1111                                           versions[k]['revision'])
1112                          for k in versions_list]
1113         versions_text = ('Component versions for this build:\n\n%s\n' %
1114                          '\n'.join(versions_list))
1115         body_text = results_text + versions_text
1116         msg = email.mime.text.MIMEText(body_text)
1117         msg['Subject'] = subject
1118         msg['From'] = self.bot_config['email-from']
1119         msg['To'] = self.bot_config['email-to']
1120         msg['Message-ID'] = email.utils.make_msgid()
1121         msg['Date'] = email.utils.format_datetime(datetime.datetime.utcnow())
1122         with smtplib.SMTP(self.bot_config['email-server']) as s:
1123             s.send_message(msg)
1124
1125     def bot_run_self(self, opts, action, check=True):
1126         """Run a copy of this script with given options."""
1127         cmd = [sys.executable, sys.argv[0], '--keep=none',
1128                '-j%d' % self.parallelism]
1129         if self.full_gcc:
1130             cmd.append('--full-gcc')
1131         cmd.extend(opts)
1132         cmd.extend([self.topdir, action])
1133         sys.stdout.flush()
1134         subprocess.run(cmd, check=check)
1135
1136     def bot(self):
1137         """Run repeated rounds of checkout and builds."""
1138         while True:
1139             self.load_bot_config_json()
1140             if not self.bot_config['run']:
1141                 print('Bot exiting by request.')
1142                 exit(0)
1143             self.bot_run_self([], 'bot-cycle', check=False)
1144             self.load_bot_config_json()
1145             if not self.bot_config['run']:
1146                 print('Bot exiting by request.')
1147                 exit(0)
1148             time.sleep(self.bot_config['delay'])
1149             if self.get_script_text() != self.script_text:
1150                 print('Script changed, bot re-execing.')
1151                 self.exec_self()
1152
1153
1154 class Config(object):
1155     """A configuration for building a compiler and associated libraries."""
1156
1157     def __init__(self, ctx, arch, os_name, variant=None, gcc_cfg=None,
1158                  first_gcc_cfg=None, glibcs=None, extra_glibcs=None):
1159         """Initialize a Config object."""
1160         self.ctx = ctx
1161         self.arch = arch
1162         self.os = os_name
1163         self.variant = variant
1164         if variant is None:
1165             self.name = '%s-%s' % (arch, os_name)
1166         else:
1167             self.name = '%s-%s-%s' % (arch, os_name, variant)
1168         self.triplet = '%s-glibc-%s' % (arch, os_name)
1169         if gcc_cfg is None:
1170             self.gcc_cfg = []
1171         else:
1172             self.gcc_cfg = gcc_cfg
1173         if first_gcc_cfg is None:
1174             self.first_gcc_cfg = []
1175         else:
1176             self.first_gcc_cfg = first_gcc_cfg
1177         if glibcs is None:
1178             glibcs = [{'variant': variant}]
1179         if extra_glibcs is None:
1180             extra_glibcs = []
1181         glibcs = [Glibc(self, **g) for g in glibcs]
1182         extra_glibcs = [Glibc(self, **g) for g in extra_glibcs]
1183         self.all_glibcs = glibcs + extra_glibcs
1184         self.compiler_glibcs = glibcs
1185         self.installdir = ctx.compiler_installdir(self.name)
1186         self.bindir = ctx.compiler_bindir(self.name)
1187         self.sysroot = ctx.compiler_sysroot(self.name)
1188         self.builddir = os.path.join(ctx.builddir, 'compilers', self.name)
1189         self.logsdir = os.path.join(ctx.logsdir, 'compilers', self.name)
1190
1191     def component_builddir(self, component):
1192         """Return the directory to use for a (non-glibc) build."""
1193         return self.ctx.component_builddir('compilers', self.name, component)
1194
1195     def build(self):
1196         """Generate commands to build this compiler."""
1197         self.ctx.remove_recreate_dirs(self.installdir, self.builddir,
1198                                       self.logsdir)
1199         cmdlist = CommandList('compilers-%s' % self.name, self.ctx.keep)
1200         cmdlist.add_command('check-host-libraries',
1201                             ['test', '-f',
1202                              os.path.join(self.ctx.host_libraries_installdir,
1203                                           'ok')])
1204         cmdlist.use_path(self.bindir)
1205         self.build_cross_tool(cmdlist, 'binutils', 'binutils',
1206                               ['--disable-gdb',
1207                                '--disable-libdecnumber',
1208                                '--disable-readline',
1209                                '--disable-sim'])
1210         if self.os.startswith('linux'):
1211             self.install_linux_headers(cmdlist)
1212         self.build_gcc(cmdlist, True)
1213         if self.os == 'gnu':
1214             self.install_gnumach_headers(cmdlist)
1215             self.build_cross_tool(cmdlist, 'mig', 'mig')
1216             self.install_hurd_headers(cmdlist)
1217         for g in self.compiler_glibcs:
1218             cmdlist.push_subdesc('glibc')
1219             cmdlist.push_subdesc(g.name)
1220             g.build_glibc(cmdlist, True)
1221             cmdlist.pop_subdesc()
1222             cmdlist.pop_subdesc()
1223         self.build_gcc(cmdlist, False)
1224         cmdlist.add_command('done', ['touch',
1225                                      os.path.join(self.installdir, 'ok')])
1226         self.ctx.add_makefile_cmdlist('compilers-%s' % self.name, cmdlist,
1227                                       self.logsdir)
1228
1229     def build_cross_tool(self, cmdlist, tool_src, tool_build, extra_opts=None):
1230         """Build one cross tool."""
1231         srcdir = self.ctx.component_srcdir(tool_src)
1232         builddir = self.component_builddir(tool_build)
1233         cmdlist.push_subdesc(tool_build)
1234         cmdlist.create_use_dir(builddir)
1235         cfg_cmd = [os.path.join(srcdir, 'configure'),
1236                    '--prefix=%s' % self.installdir,
1237                    '--build=%s' % self.ctx.build_triplet,
1238                    '--host=%s' % self.ctx.build_triplet,
1239                    '--target=%s' % self.triplet,
1240                    '--with-sysroot=%s' % self.sysroot]
1241         if extra_opts:
1242             cfg_cmd.extend(extra_opts)
1243         cmdlist.add_command('configure', cfg_cmd)
1244         cmdlist.add_command('build', ['make'])
1245         # Parallel "make install" for GCC has race conditions that can
1246         # cause it to fail; see
1247         # <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=42980>.  Such
1248         # problems are not known for binutils, but doing the
1249         # installation in parallel within a particular toolchain build
1250         # (as opposed to installation of one toolchain from
1251         # build-many-glibcs.py running in parallel to the installation
1252         # of other toolchains being built) is not known to be
1253         # significantly beneficial, so it is simplest just to disable
1254         # parallel install for cross tools here.
1255         cmdlist.add_command('install', ['make', '-j1', 'install'])
1256         cmdlist.cleanup_dir()
1257         cmdlist.pop_subdesc()
1258
1259     def install_linux_headers(self, cmdlist):
1260         """Install Linux kernel headers."""
1261         arch_map = {'aarch64': 'arm64',
1262                     'alpha': 'alpha',
1263                     'arm': 'arm',
1264                     'csky': 'csky',
1265                     'hppa': 'parisc',
1266                     'i486': 'x86',
1267                     'i586': 'x86',
1268                     'i686': 'x86',
1269                     'i786': 'x86',
1270                     'ia64': 'ia64',
1271                     'm68k': 'm68k',
1272                     'microblaze': 'microblaze',
1273                     'mips': 'mips',
1274                     'nios2': 'nios2',
1275                     'powerpc': 'powerpc',
1276                     's390': 's390',
1277                     'riscv32': 'riscv',
1278                     'riscv64': 'riscv',
1279                     'sh': 'sh',
1280                     'sparc': 'sparc',
1281                     'x86_64': 'x86'}
1282         linux_arch = None
1283         for k in arch_map:
1284             if self.arch.startswith(k):
1285                 linux_arch = arch_map[k]
1286                 break
1287         assert linux_arch is not None
1288         srcdir = self.ctx.component_srcdir('linux')
1289         builddir = self.component_builddir('linux')
1290         headers_dir = os.path.join(self.sysroot, 'usr')
1291         cmdlist.push_subdesc('linux')
1292         cmdlist.create_use_dir(builddir)
1293         cmdlist.add_command('install-headers',
1294                             ['make', '-C', srcdir, 'O=%s' % builddir,
1295                              'ARCH=%s' % linux_arch,
1296                              'INSTALL_HDR_PATH=%s' % headers_dir,
1297                              'headers_install'])
1298         cmdlist.cleanup_dir()
1299         cmdlist.pop_subdesc()
1300
1301     def install_gnumach_headers(self, cmdlist):
1302         """Install GNU Mach headers."""
1303         srcdir = self.ctx.component_srcdir('gnumach')
1304         builddir = self.component_builddir('gnumach')
1305         cmdlist.push_subdesc('gnumach')
1306         cmdlist.create_use_dir(builddir)
1307         cmdlist.add_command('configure',
1308                             [os.path.join(srcdir, 'configure'),
1309                              '--build=%s' % self.ctx.build_triplet,
1310                              '--host=%s' % self.triplet,
1311                              '--prefix=',
1312                              'CC=%s-gcc -nostdlib' % self.triplet])
1313         cmdlist.add_command('install', ['make', 'DESTDIR=%s' % self.sysroot,
1314                                         'install-data'])
1315         cmdlist.cleanup_dir()
1316         cmdlist.pop_subdesc()
1317
1318     def install_hurd_headers(self, cmdlist):
1319         """Install Hurd headers."""
1320         srcdir = self.ctx.component_srcdir('hurd')
1321         builddir = self.component_builddir('hurd')
1322         cmdlist.push_subdesc('hurd')
1323         cmdlist.create_use_dir(builddir)
1324         cmdlist.add_command('configure',
1325                             [os.path.join(srcdir, 'configure'),
1326                              '--build=%s' % self.ctx.build_triplet,
1327                              '--host=%s' % self.triplet,
1328                              '--prefix=',
1329                              '--disable-profile', '--without-parted',
1330                              'CC=%s-gcc -nostdlib' % self.triplet])
1331         cmdlist.add_command('install', ['make', 'prefix=%s' % self.sysroot,
1332                                         'no_deps=t', 'install-headers'])
1333         cmdlist.cleanup_dir()
1334         cmdlist.pop_subdesc()
1335
1336     def build_gcc(self, cmdlist, bootstrap):
1337         """Build GCC."""
1338         # libssp is of little relevance with glibc's own stack
1339         # checking support.  libcilkrts does not support GNU/Hurd (and
1340         # has been removed in GCC 8, so --disable-libcilkrts can be
1341         # removed once glibc no longer supports building with older
1342         # GCC versions).
1343         cfg_opts = list(self.gcc_cfg)
1344         cfg_opts += ['--disable-libssp', '--disable-libcilkrts']
1345         host_libs = self.ctx.host_libraries_installdir
1346         cfg_opts += ['--with-gmp=%s' % host_libs,
1347                      '--with-mpfr=%s' % host_libs,
1348                      '--with-mpc=%s' % host_libs]
1349         if bootstrap:
1350             tool_build = 'gcc-first'
1351             # Building a static-only, C-only compiler that is
1352             # sufficient to build glibc.  Various libraries and
1353             # features that may require libc headers must be disabled.
1354             # When configuring with a sysroot, --with-newlib is
1355             # required to define inhibit_libc (to stop some parts of
1356             # libgcc including libc headers); --without-headers is not
1357             # sufficient.
1358             cfg_opts += ['--enable-languages=c', '--disable-shared',
1359                          '--disable-threads',
1360                          '--disable-libatomic',
1361                          '--disable-decimal-float',
1362                          '--disable-libffi',
1363                          '--disable-libgomp',
1364                          '--disable-libitm',
1365                          '--disable-libmpx',
1366                          '--disable-libquadmath',
1367                          '--disable-libsanitizer',
1368                          '--without-headers', '--with-newlib',
1369                          '--with-glibc-version=%s' % self.ctx.glibc_version
1370                          ]
1371             cfg_opts += self.first_gcc_cfg
1372         else:
1373             tool_build = 'gcc'
1374             # libsanitizer commonly breaks because of glibc header
1375             # changes, or on unusual targets.
1376             if not self.ctx.full_gcc:
1377                 cfg_opts += ['--disable-libsanitizer']
1378             langs = 'all' if self.ctx.full_gcc else 'c,c++'
1379             cfg_opts += ['--enable-languages=%s' % langs,
1380                          '--enable-shared', '--enable-threads']
1381         self.build_cross_tool(cmdlist, 'gcc', tool_build, cfg_opts)
1382
1383
1384 class Glibc(object):
1385     """A configuration for building glibc."""
1386
1387     def __init__(self, compiler, arch=None, os_name=None, variant=None,
1388                  cfg=None, ccopts=None):
1389         """Initialize a Glibc object."""
1390         self.ctx = compiler.ctx
1391         self.compiler = compiler
1392         if arch is None:
1393             self.arch = compiler.arch
1394         else:
1395             self.arch = arch
1396         if os_name is None:
1397             self.os = compiler.os
1398         else:
1399             self.os = os_name
1400         self.variant = variant
1401         if variant is None:
1402             self.name = '%s-%s' % (self.arch, self.os)
1403         else:
1404             self.name = '%s-%s-%s' % (self.arch, self.os, variant)
1405         self.triplet = '%s-glibc-%s' % (self.arch, self.os)
1406         if cfg is None:
1407             self.cfg = []
1408         else:
1409             self.cfg = cfg
1410         self.ccopts = ccopts
1411
1412     def tool_name(self, tool):
1413         """Return the name of a cross-compilation tool."""
1414         ctool = '%s-%s' % (self.compiler.triplet, tool)
1415         if self.ccopts and (tool == 'gcc' or tool == 'g++'):
1416             ctool = '%s %s' % (ctool, self.ccopts)
1417         return ctool
1418
1419     def build(self):
1420         """Generate commands to build this glibc."""
1421         builddir = self.ctx.component_builddir('glibcs', self.name, 'glibc')
1422         installdir = self.ctx.glibc_installdir(self.name)
1423         logsdir = os.path.join(self.ctx.logsdir, 'glibcs', self.name)
1424         self.ctx.remove_recreate_dirs(installdir, builddir, logsdir)
1425         cmdlist = CommandList('glibcs-%s' % self.name, self.ctx.keep)
1426         cmdlist.add_command('check-compilers',
1427                             ['test', '-f',
1428                              os.path.join(self.compiler.installdir, 'ok')])
1429         cmdlist.use_path(self.compiler.bindir)
1430         self.build_glibc(cmdlist, False)
1431         self.ctx.add_makefile_cmdlist('glibcs-%s' % self.name, cmdlist,
1432                                       logsdir)
1433
1434     def build_glibc(self, cmdlist, for_compiler):
1435         """Generate commands to build this glibc, either as part of a compiler
1436         build or with the bootstrapped compiler (and in the latter case, run
1437         tests as well)."""
1438         srcdir = self.ctx.component_srcdir('glibc')
1439         if for_compiler:
1440             builddir = self.ctx.component_builddir('compilers',
1441                                                    self.compiler.name, 'glibc',
1442                                                    self.name)
1443             installdir = self.compiler.sysroot
1444         else:
1445             builddir = self.ctx.component_builddir('glibcs', self.name,
1446                                                    'glibc')
1447             installdir = self.ctx.glibc_installdir(self.name)
1448         cmdlist.create_use_dir(builddir)
1449         use_usr = self.os != 'gnu'
1450         prefix = '/usr' if use_usr else ''
1451         cfg_cmd = [os.path.join(srcdir, 'configure'),
1452                    '--prefix=%s' % prefix,
1453                    '--enable-profile',
1454                    '--build=%s' % self.ctx.build_triplet,
1455                    '--host=%s' % self.triplet,
1456                    'CC=%s' % self.tool_name('gcc'),
1457                    'CXX=%s' % self.tool_name('g++'),
1458                    'AR=%s' % self.tool_name('ar'),
1459                    'AS=%s' % self.tool_name('as'),
1460                    'LD=%s' % self.tool_name('ld'),
1461                    'NM=%s' % self.tool_name('nm'),
1462                    'OBJCOPY=%s' % self.tool_name('objcopy'),
1463                    'OBJDUMP=%s' % self.tool_name('objdump'),
1464                    'RANLIB=%s' % self.tool_name('ranlib'),
1465                    'READELF=%s' % self.tool_name('readelf'),
1466                    'STRIP=%s' % self.tool_name('strip')]
1467         if self.os == 'gnu':
1468             cfg_cmd += ['MIG=%s' % self.tool_name('mig')]
1469         cfg_cmd += self.cfg
1470         cmdlist.add_command('configure', cfg_cmd)
1471         cmdlist.add_command('build', ['make'])
1472         cmdlist.add_command('install', ['make', 'install',
1473                                         'install_root=%s' % installdir])
1474         # GCC uses paths such as lib/../lib64, so make sure lib
1475         # directories always exist.
1476         mkdir_cmd = ['mkdir', '-p',
1477                      os.path.join(installdir, 'lib')]
1478         if use_usr:
1479             mkdir_cmd += [os.path.join(installdir, 'usr', 'lib')]
1480         cmdlist.add_command('mkdir-lib', mkdir_cmd)
1481         if not for_compiler:
1482             if self.ctx.strip:
1483                 cmdlist.add_command('strip',
1484                                     ['sh', '-c',
1485                                      ('%s $(find %s/lib* -name "*.so")' %
1486                                       (self.tool_name('strip'), installdir))])
1487             cmdlist.add_command('check', ['make', 'check'])
1488             cmdlist.add_command('save-logs', [self.ctx.save_logs],
1489                                 always_run=True)
1490         cmdlist.cleanup_dir()
1491
1492
1493 class Command(object):
1494     """A command run in the build process."""
1495
1496     def __init__(self, desc, num, dir, path, command, always_run=False):
1497         """Initialize a Command object."""
1498         self.dir = dir
1499         self.path = path
1500         self.desc = desc
1501         trans = str.maketrans({' ': '-'})
1502         self.logbase = '%03d-%s' % (num, desc.translate(trans))
1503         self.command = command
1504         self.always_run = always_run
1505
1506     @staticmethod
1507     def shell_make_quote_string(s):
1508         """Given a string not containing a newline, quote it for use by the
1509         shell and make."""
1510         assert '\n' not in s
1511         if re.fullmatch('[]+,./0-9@A-Z_a-z-]+', s):
1512             return s
1513         strans = str.maketrans({"'": "'\\''"})
1514         s = "'%s'" % s.translate(strans)
1515         mtrans = str.maketrans({'$': '$$'})
1516         return s.translate(mtrans)
1517
1518     @staticmethod
1519     def shell_make_quote_list(l, translate_make):
1520         """Given a list of strings not containing newlines, quote them for use
1521         by the shell and make, returning a single string.  If translate_make
1522         is true and the first string is 'make', change it to $(MAKE)."""
1523         l = [Command.shell_make_quote_string(s) for s in l]
1524         if translate_make and l[0] == 'make':
1525             l[0] = '$(MAKE)'
1526         return ' '.join(l)
1527
1528     def shell_make_quote(self):
1529         """Return this command quoted for the shell and make."""
1530         return self.shell_make_quote_list(self.command, True)
1531
1532
1533 class CommandList(object):
1534     """A list of commands run in the build process."""
1535
1536     def __init__(self, desc, keep):
1537         """Initialize a CommandList object."""
1538         self.cmdlist = []
1539         self.dir = None
1540         self.path = None
1541         self.desc = [desc]
1542         self.keep = keep
1543
1544     def desc_txt(self, desc):
1545         """Return the description to use for a command."""
1546         return '%s %s' % (' '.join(self.desc), desc)
1547
1548     def use_dir(self, dir):
1549         """Set the default directory for subsequent commands."""
1550         self.dir = dir
1551
1552     def use_path(self, path):
1553         """Set a directory to be prepended to the PATH for subsequent
1554         commands."""
1555         self.path = path
1556
1557     def push_subdesc(self, subdesc):
1558         """Set the default subdescription for subsequent commands (e.g., the
1559         name of a component being built, within the series of commands
1560         building it)."""
1561         self.desc.append(subdesc)
1562
1563     def pop_subdesc(self):
1564         """Pop a subdescription from the list of descriptions."""
1565         self.desc.pop()
1566
1567     def create_use_dir(self, dir):
1568         """Remove and recreate a directory and use it for subsequent
1569         commands."""
1570         self.add_command_dir('rm', None, ['rm', '-rf', dir])
1571         self.add_command_dir('mkdir', None, ['mkdir', '-p', dir])
1572         self.use_dir(dir)
1573
1574     def add_command_dir(self, desc, dir, command, always_run=False):
1575         """Add a command to run in a given directory."""
1576         cmd = Command(self.desc_txt(desc), len(self.cmdlist), dir, self.path,
1577                       command, always_run)
1578         self.cmdlist.append(cmd)
1579
1580     def add_command(self, desc, command, always_run=False):
1581         """Add a command to run in the default directory."""
1582         cmd = Command(self.desc_txt(desc), len(self.cmdlist), self.dir,
1583                       self.path, command, always_run)
1584         self.cmdlist.append(cmd)
1585
1586     def cleanup_dir(self, desc='cleanup', dir=None):
1587         """Clean up a build directory.  If no directory is specified, the
1588         default directory is cleaned up and ceases to be the default
1589         directory."""
1590         if dir is None:
1591             dir = self.dir
1592             self.use_dir(None)
1593         if self.keep != 'all':
1594             self.add_command_dir(desc, None, ['rm', '-rf', dir],
1595                                  always_run=(self.keep == 'none'))
1596
1597     def makefile_commands(self, wrapper, logsdir):
1598         """Return the sequence of commands in the form of text for a Makefile.
1599         The given wrapper script takes arguments: base of logs for
1600         previous command, or empty; base of logs for this command;
1601         description; directory; PATH addition; the command itself."""
1602         # prev_base is the base of the name for logs of the previous
1603         # command that is not always-run (that is, a build command,
1604         # whose failure should stop subsequent build commands from
1605         # being run, as opposed to a cleanup command, which is run
1606         # even if previous commands failed).
1607         prev_base = ''
1608         cmds = []
1609         for c in self.cmdlist:
1610             ctxt = c.shell_make_quote()
1611             if prev_base and not c.always_run:
1612                 prev_log = os.path.join(logsdir, prev_base)
1613             else:
1614                 prev_log = ''
1615             this_log = os.path.join(logsdir, c.logbase)
1616             if not c.always_run:
1617                 prev_base = c.logbase
1618             if c.dir is None:
1619                 dir = ''
1620             else:
1621                 dir = c.dir
1622             if c.path is None:
1623                 path = ''
1624             else:
1625                 path = c.path
1626             prelims = [wrapper, prev_log, this_log, c.desc, dir, path]
1627             prelim_txt = Command.shell_make_quote_list(prelims, False)
1628             cmds.append('\t@%s %s' % (prelim_txt, ctxt))
1629         return '\n'.join(cmds)
1630
1631     def status_logs(self, logsdir):
1632         """Return the list of log files with command status."""
1633         return [os.path.join(logsdir, '%s-status.txt' % c.logbase)
1634                 for c in self.cmdlist]
1635
1636
1637 def get_parser():
1638     """Return an argument parser for this module."""
1639     parser = argparse.ArgumentParser(description=__doc__)
1640     parser.add_argument('-j', dest='parallelism',
1641                         help='Run this number of jobs in parallel',
1642                         type=int, default=os.cpu_count())
1643     parser.add_argument('--keep', dest='keep',
1644                         help='Whether to keep all build directories, '
1645                         'none or only those from failed builds',
1646                         default='none', choices=('none', 'all', 'failed'))
1647     parser.add_argument('--replace-sources', action='store_true',
1648                         help='Remove and replace source directories '
1649                         'with the wrong version of a component')
1650     parser.add_argument('--strip', action='store_true',
1651                         help='Strip installed glibc libraries')
1652     parser.add_argument('--full-gcc', action='store_true',
1653                         help='Build GCC with all languages and libsanitizer')
1654     parser.add_argument('topdir',
1655                         help='Toplevel working directory')
1656     parser.add_argument('action',
1657                         help='What to do',
1658                         choices=('checkout', 'bot-cycle', 'bot',
1659                                  'host-libraries', 'compilers', 'glibcs'))
1660     parser.add_argument('configs',
1661                         help='Versions to check out or configurations to build',
1662                         nargs='*')
1663     return parser
1664
1665
1666 def main(argv):
1667     """The main entry point."""
1668     parser = get_parser()
1669     opts = parser.parse_args(argv)
1670     topdir = os.path.abspath(opts.topdir)
1671     ctx = Context(topdir, opts.parallelism, opts.keep, opts.replace_sources,
1672                   opts.strip, opts.full_gcc, opts.action)
1673     ctx.run_builds(opts.action, opts.configs)
1674
1675
1676 if __name__ == '__main__':
1677     main(sys.argv[1:])