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