#!/usr/bin/python3 # Build many configurations of glibc. # Copyright (C) 2016-2020 Free Software Foundation, Inc. # This file is part of the GNU C Library. # # The GNU C Library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # The GNU C Library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with the GNU C Library; if not, see # . """Build many configurations of glibc. This script takes as arguments a directory name (containing a src subdirectory with sources of the relevant toolchain components) and a description of what to do: 'checkout', to check out sources into that directory, 'bot-cycle', to run a series of checkout and build steps, 'bot', to run 'bot-cycle' repeatedly, 'host-libraries', to build libraries required by the toolchain, 'compilers', to build cross-compilers for various configurations, or 'glibcs', to build glibc for various configurations and run the compilation parts of the testsuite. Subsequent arguments name the versions of components to check out (- "$this_log"\n' 'echo >> "$this_log"\n' 'echo "Description: $desc" >> "$this_log"\n' 'printf "%s" "Command:" >> "$this_log"\n' 'for word in "$@"; do\n' ' if expr "$word" : "[]+,./0-9@A-Z_a-z-]\\\\{1,\\\\}\\$" > /dev/null; then\n' ' printf " %s" "$word"\n' ' else\n' ' printf " \'"\n' ' printf "%s" "$word" | sed -e "s/\'/\'\\\\\\\\\'\'/"\n' ' printf "\'"\n' ' fi\n' 'done >> "$this_log"\n' 'echo >> "$this_log"\n' 'echo "Directory: $dir" >> "$this_log"\n' 'echo "Path addition: $path" >> "$this_log"\n' 'echo >> "$this_log"\n' 'record_status ()\n' '{\n' ' echo >> "$this_log"\n' ' echo "$1: $desc" > "$this_status"\n' ' echo "$1: $desc" >> "$this_log"\n' ' echo >> "$this_log"\n' ' date >> "$this_log"\n' ' echo "$1: $desc"\n' ' exit 0\n' '}\n' 'check_error ()\n' '{\n' ' if [ "$1" != "0" ]; then\n' ' record_status FAIL\n' ' fi\n' '}\n' 'if [ "$prev_base" ] && ! grep -q "^PASS" "$prev_status"; then\n' ' record_status UNRESOLVED\n' 'fi\n' 'if [ "$dir" ]; then\n' ' cd "$dir"\n' ' check_error "$?"\n' 'fi\n' 'if [ "$path" ]; then\n' ' PATH=$path:$PATH\n' 'fi\n' '"$@" < /dev/null >> "$this_log" 2>&1\n' 'check_error "$?"\n' 'record_status PASS\n') with open(self.wrapper, 'w') as f: f.write(wrapper_text) # Mode 0o755. mode_exec = (stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP| stat.S_IROTH|stat.S_IXOTH) os.chmod(self.wrapper, mode_exec) save_logs_text = ( '#!/bin/sh\n' 'if ! [ -f tests.sum ]; then\n' ' echo "No test summary available."\n' ' exit 0\n' 'fi\n' 'save_file ()\n' '{\n' ' echo "Contents of $1:"\n' ' echo\n' ' cat "$1"\n' ' echo\n' ' echo "End of contents of $1."\n' ' echo\n' '}\n' 'save_file tests.sum\n' 'non_pass_tests=$(grep -v "^PASS: " tests.sum | sed -e "s/^PASS: //")\n' 'for t in $non_pass_tests; do\n' ' if [ -f "$t.out" ]; then\n' ' save_file "$t.out"\n' ' fi\n' 'done\n') with open(self.save_logs, 'w') as f: f.write(save_logs_text) os.chmod(self.save_logs, mode_exec) def do_build(self): """Do the actual build.""" cmd = ['make', '-j%d' % self.parallelism] subprocess.run(cmd, cwd=self.builddir, check=True) def build_host_libraries(self): """Build the host libraries.""" installdir = self.host_libraries_installdir builddir = os.path.join(self.builddir, 'host-libraries') logsdir = os.path.join(self.logsdir, 'host-libraries') self.remove_recreate_dirs(installdir, builddir, logsdir) cmdlist = CommandList('host-libraries', self.keep) self.build_host_library(cmdlist, 'gmp') self.build_host_library(cmdlist, 'mpfr', ['--with-gmp=%s' % installdir]) self.build_host_library(cmdlist, 'mpc', ['--with-gmp=%s' % installdir, '--with-mpfr=%s' % installdir]) cmdlist.add_command('done', ['touch', os.path.join(installdir, 'ok')]) self.add_makefile_cmdlist('host-libraries', cmdlist, logsdir) def build_host_library(self, cmdlist, lib, extra_opts=None): """Build one host library.""" srcdir = self.component_srcdir(lib) builddir = self.component_builddir('host-libraries', None, lib) installdir = self.host_libraries_installdir cmdlist.push_subdesc(lib) cmdlist.create_use_dir(builddir) cfg_cmd = [os.path.join(srcdir, 'configure'), '--prefix=%s' % installdir, '--disable-shared'] if extra_opts: cfg_cmd.extend (extra_opts) cmdlist.add_command('configure', cfg_cmd) cmdlist.add_command('build', ['make']) cmdlist.add_command('check', ['make', 'check']) cmdlist.add_command('install', ['make', 'install']) cmdlist.cleanup_dir() cmdlist.pop_subdesc() def build_compilers(self, configs): """Build the compilers.""" if not configs: self.remove_dirs(os.path.join(self.builddir, 'compilers')) self.remove_dirs(os.path.join(self.installdir, 'compilers')) self.remove_dirs(os.path.join(self.logsdir, 'compilers')) configs = sorted(self.configs.keys()) for c in configs: self.configs[c].build() def build_glibcs(self, configs): """Build the glibcs.""" if not configs: self.remove_dirs(os.path.join(self.builddir, 'glibcs')) self.remove_dirs(os.path.join(self.installdir, 'glibcs')) self.remove_dirs(os.path.join(self.logsdir, 'glibcs')) configs = sorted(self.glibc_configs.keys()) for c in configs: self.glibc_configs[c].build() def update_syscalls(self, configs): """Update the glibc syscall lists.""" if not configs: self.remove_dirs(os.path.join(self.builddir, 'update-syscalls')) self.remove_dirs(os.path.join(self.logsdir, 'update-syscalls')) configs = sorted(self.glibc_configs.keys()) for c in configs: self.glibc_configs[c].update_syscalls() def load_versions_json(self): """Load information about source directory versions.""" if not os.access(self.versions_json, os.F_OK): self.versions = {} return with open(self.versions_json, 'r') as f: self.versions = json.load(f) def store_json(self, data, filename): """Store information in a JSON file.""" filename_tmp = filename + '.tmp' with open(filename_tmp, 'w') as f: json.dump(data, f, indent=2, sort_keys=True) os.rename(filename_tmp, filename) def store_versions_json(self): """Store information about source directory versions.""" self.store_json(self.versions, self.versions_json) def set_component_version(self, component, version, explicit, revision): """Set the version information for a component.""" self.versions[component] = {'version': version, 'explicit': explicit, 'revision': revision} self.store_versions_json() def checkout(self, versions): """Check out the desired component versions.""" default_versions = {'binutils': 'vcs-2.35', 'gcc': 'vcs-10', 'glibc': 'vcs-mainline', 'gmp': '6.2.0', 'linux': '5.8', 'mpc': '1.2.0', 'mpfr': '4.1.0', 'mig': 'vcs-mainline', 'gnumach': 'vcs-mainline', 'hurd': 'vcs-mainline'} use_versions = {} explicit_versions = {} for v in versions: found_v = False for k in default_versions.keys(): kx = k + '-' if v.startswith(kx): vx = v[len(kx):] if k in use_versions: print('error: multiple versions for %s' % k) exit(1) use_versions[k] = vx explicit_versions[k] = True found_v = True break if not found_v: print('error: unknown component in %s' % v) exit(1) for k in default_versions.keys(): if k not in use_versions: if k in self.versions and self.versions[k]['explicit']: use_versions[k] = self.versions[k]['version'] explicit_versions[k] = True else: use_versions[k] = default_versions[k] explicit_versions[k] = False os.makedirs(self.srcdir, exist_ok=True) for k in sorted(default_versions.keys()): update = os.access(self.component_srcdir(k), os.F_OK) v = use_versions[k] if (update and k in self.versions and v != self.versions[k]['version']): if not self.replace_sources: print('error: version of %s has changed from %s to %s, ' 'use --replace-sources to check out again' % (k, self.versions[k]['version'], v)) exit(1) shutil.rmtree(self.component_srcdir(k)) update = False if v.startswith('vcs-'): revision = self.checkout_vcs(k, v[4:], update) else: self.checkout_tar(k, v, update) revision = v self.set_component_version(k, v, explicit_versions[k], revision) if self.get_script_text() != self.script_text: # Rerun the checkout process in case the updated script # uses different default versions or new components. self.exec_self() def checkout_vcs(self, component, version, update): """Check out the given version of the given component from version control. Return a revision identifier.""" if component == 'binutils': git_url = 'git://sourceware.org/git/binutils-gdb.git' if version == 'mainline': git_branch = 'master' else: trans = str.maketrans({'.': '_'}) git_branch = 'binutils-%s-branch' % version.translate(trans) return self.git_checkout(component, git_url, git_branch, update) elif component == 'gcc': if version == 'mainline': branch = 'master' else: branch = 'releases/gcc-%s' % version return self.gcc_checkout(branch, update) elif component == 'glibc': git_url = 'git://sourceware.org/git/glibc.git' if version == 'mainline': git_branch = 'master' else: git_branch = 'release/%s/master' % version r = self.git_checkout(component, git_url, git_branch, update) self.fix_glibc_timestamps() return r elif component == 'gnumach': git_url = 'git://git.savannah.gnu.org/hurd/gnumach.git' git_branch = 'master' r = self.git_checkout(component, git_url, git_branch, update) subprocess.run(['autoreconf', '-i'], cwd=self.component_srcdir(component), check=True) return r elif component == 'mig': git_url = 'git://git.savannah.gnu.org/hurd/mig.git' git_branch = 'master' r = self.git_checkout(component, git_url, git_branch, update) subprocess.run(['autoreconf', '-i'], cwd=self.component_srcdir(component), check=True) return r elif component == 'hurd': git_url = 'git://git.savannah.gnu.org/hurd/hurd.git' git_branch = 'master' r = self.git_checkout(component, git_url, git_branch, update) subprocess.run(['autoconf'], cwd=self.component_srcdir(component), check=True) return r else: print('error: component %s coming from VCS' % component) exit(1) def git_checkout(self, component, git_url, git_branch, update): """Check out a component from git. Return a commit identifier.""" if update: subprocess.run(['git', 'remote', 'prune', 'origin'], cwd=self.component_srcdir(component), check=True) if self.replace_sources: subprocess.run(['git', 'clean', '-dxfq'], cwd=self.component_srcdir(component), check=True) subprocess.run(['git', 'pull', '-q'], cwd=self.component_srcdir(component), check=True) else: if self.shallow: depth_arg = ('--depth', '1') else: depth_arg = () subprocess.run(['git', 'clone', '-q', '-b', git_branch, *depth_arg, git_url, self.component_srcdir(component)], check=True) r = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=self.component_srcdir(component), stdout=subprocess.PIPE, check=True, universal_newlines=True).stdout return r.rstrip() def fix_glibc_timestamps(self): """Fix timestamps in a glibc checkout.""" # Ensure that builds do not try to regenerate generated files # in the source tree. srcdir = self.component_srcdir('glibc') # These files have Makefile dependencies to regenerate them in # the source tree that may be active during a normal build. # Some other files have such dependencies but do not need to # be touched because nothing in a build depends on the files # in question. for f in ('sysdeps/mach/hurd/bits/errno.h',): to_touch = os.path.join(srcdir, f) subprocess.run(['touch', '-c', to_touch], check=True) for dirpath, dirnames, filenames in os.walk(srcdir): for f in filenames: if (f == 'configure' or f == 'preconfigure' or f.endswith('-kw.h')): to_touch = os.path.join(dirpath, f) subprocess.run(['touch', to_touch], check=True) def gcc_checkout(self, branch, update): """Check out GCC from git. Return the commit identifier.""" if os.access(os.path.join(self.component_srcdir('gcc'), '.svn'), os.F_OK): if not self.replace_sources: print('error: GCC has moved from SVN to git, use ' '--replace-sources to check out again') exit(1) shutil.rmtree(self.component_srcdir('gcc')) update = False if not update: self.git_checkout('gcc', 'git://gcc.gnu.org/git/gcc.git', branch, update) subprocess.run(['contrib/gcc_update', '--silent'], cwd=self.component_srcdir('gcc'), check=True) r = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=self.component_srcdir('gcc'), stdout=subprocess.PIPE, check=True, universal_newlines=True).stdout return r.rstrip() def checkout_tar(self, component, version, update): """Check out the given version of the given component from a tarball.""" if update: return url_map = {'binutils': 'https://ftp.gnu.org/gnu/binutils/binutils-%(version)s.tar.bz2', 'gcc': 'https://ftp.gnu.org/gnu/gcc/gcc-%(version)s/gcc-%(version)s.tar.gz', 'gmp': 'https://ftp.gnu.org/gnu/gmp/gmp-%(version)s.tar.xz', 'linux': 'https://www.kernel.org/pub/linux/kernel/v%(major)s.x/linux-%(version)s.tar.xz', 'mpc': 'https://ftp.gnu.org/gnu/mpc/mpc-%(version)s.tar.gz', 'mpfr': 'https://ftp.gnu.org/gnu/mpfr/mpfr-%(version)s.tar.xz', 'mig': 'https://ftp.gnu.org/gnu/mig/mig-%(version)s.tar.bz2', 'gnumach': 'https://ftp.gnu.org/gnu/gnumach/gnumach-%(version)s.tar.bz2', 'hurd': 'https://ftp.gnu.org/gnu/hurd/hurd-%(version)s.tar.bz2'} if component not in url_map: print('error: component %s coming from tarball' % component) exit(1) version_major = version.split('.')[0] url = url_map[component] % {'version': version, 'major': version_major} filename = os.path.join(self.srcdir, url.split('/')[-1]) response = urllib.request.urlopen(url) data = response.read() with open(filename, 'wb') as f: f.write(data) subprocess.run(['tar', '-C', self.srcdir, '-x', '-f', filename], check=True) os.rename(os.path.join(self.srcdir, '%s-%s' % (component, version)), self.component_srcdir(component)) os.remove(filename) def load_build_state_json(self): """Load information about the state of previous builds.""" if os.access(self.build_state_json, os.F_OK): with open(self.build_state_json, 'r') as f: self.build_state = json.load(f) else: self.build_state = {} for k in ('host-libraries', 'compilers', 'glibcs', 'update-syscalls'): if k not in self.build_state: self.build_state[k] = {} if 'build-time' not in self.build_state[k]: self.build_state[k]['build-time'] = '' if 'build-versions' not in self.build_state[k]: self.build_state[k]['build-versions'] = {} if 'build-results' not in self.build_state[k]: self.build_state[k]['build-results'] = {} if 'result-changes' not in self.build_state[k]: self.build_state[k]['result-changes'] = {} if 'ever-passed' not in self.build_state[k]: self.build_state[k]['ever-passed'] = [] def store_build_state_json(self): """Store information about the state of previous builds.""" self.store_json(self.build_state, self.build_state_json) def clear_last_build_state(self, action): """Clear information about the state of part of the build.""" # We clear the last build time and versions when starting a # new build. The results of the last build are kept around, # as comparison is still meaningful if this build is aborted # and a new one started. self.build_state[action]['build-time'] = '' self.build_state[action]['build-versions'] = {} self.store_build_state_json() def update_build_state(self, action, build_time, build_versions): """Update the build state after a build.""" build_time = build_time.replace(microsecond=0) self.build_state[action]['build-time'] = str(build_time) self.build_state[action]['build-versions'] = build_versions build_results = {} for log in self.status_log_list: with open(log, 'r') as f: log_text = f.read() log_text = log_text.rstrip() m = re.fullmatch('([A-Z]+): (.*)', log_text) result = m.group(1) test_name = m.group(2) assert test_name not in build_results build_results[test_name] = result old_build_results = self.build_state[action]['build-results'] self.build_state[action]['build-results'] = build_results result_changes = {} all_tests = set(old_build_results.keys()) | set(build_results.keys()) for t in all_tests: if t in old_build_results: old_res = old_build_results[t] else: old_res = '(New test)' if t in build_results: new_res = build_results[t] else: new_res = '(Test removed)' if old_res != new_res: result_changes[t] = '%s -> %s' % (old_res, new_res) self.build_state[action]['result-changes'] = result_changes old_ever_passed = {t for t in self.build_state[action]['ever-passed'] if t in build_results} new_passes = {t for t in build_results if build_results[t] == 'PASS'} self.build_state[action]['ever-passed'] = sorted(old_ever_passed | new_passes) self.store_build_state_json() def load_bot_config_json(self): """Load bot configuration.""" with open(self.bot_config_json, 'r') as f: self.bot_config = json.load(f) def part_build_old(self, action, delay): """Return whether the last build for a given action was at least a given number of seconds ago, or does not have a time recorded.""" old_time_str = self.build_state[action]['build-time'] if not old_time_str: return True old_time = datetime.datetime.strptime(old_time_str, '%Y-%m-%d %H:%M:%S') new_time = datetime.datetime.utcnow() delta = new_time - old_time return delta.total_seconds() >= delay def bot_cycle(self): """Run a single round of checkout and builds.""" print('Bot cycle starting %s.' % str(datetime.datetime.utcnow())) self.load_bot_config_json() actions = ('host-libraries', 'compilers', 'glibcs') self.bot_run_self(['--replace-sources'], 'checkout') self.load_versions_json() if self.get_script_text() != self.script_text: print('Script changed, re-execing.') # On script change, all parts of the build should be rerun. for a in actions: self.clear_last_build_state(a) self.exec_self() check_components = {'host-libraries': ('gmp', 'mpfr', 'mpc'), 'compilers': ('binutils', 'gcc', 'glibc', 'linux', 'mig', 'gnumach', 'hurd'), 'glibcs': ('glibc',)} must_build = {} for a in actions: build_vers = self.build_state[a]['build-versions'] must_build[a] = False if not self.build_state[a]['build-time']: must_build[a] = True old_vers = {} new_vers = {} for c in check_components[a]: if c in build_vers: old_vers[c] = build_vers[c] new_vers[c] = {'version': self.versions[c]['version'], 'revision': self.versions[c]['revision']} if new_vers == old_vers: print('Versions for %s unchanged.' % a) else: print('Versions changed or rebuild forced for %s.' % a) if a == 'compilers' and not self.part_build_old( a, self.bot_config['compilers-rebuild-delay']): print('Not requiring rebuild of compilers this soon.') else: must_build[a] = True if must_build['host-libraries']: must_build['compilers'] = True if must_build['compilers']: must_build['glibcs'] = True for a in actions: if must_build[a]: print('Must rebuild %s.' % a) self.clear_last_build_state(a) else: print('No need to rebuild %s.' % a) if os.access(self.logsdir, os.F_OK): shutil.rmtree(self.logsdir_old, ignore_errors=True) shutil.copytree(self.logsdir, self.logsdir_old) for a in actions: if must_build[a]: build_time = datetime.datetime.utcnow() print('Rebuilding %s at %s.' % (a, str(build_time))) self.bot_run_self([], a) self.load_build_state_json() self.bot_build_mail(a, build_time) print('Bot cycle done at %s.' % str(datetime.datetime.utcnow())) def bot_build_mail(self, action, build_time): """Send email with the results of a build.""" if not ('email-from' in self.bot_config and 'email-server' in self.bot_config and 'email-subject' in self.bot_config and 'email-to' in self.bot_config): if not self.email_warning: print("Email not configured, not sending.") self.email_warning = True return build_time = build_time.replace(microsecond=0) subject = (self.bot_config['email-subject'] % {'action': action, 'build-time': str(build_time)}) results = self.build_state[action]['build-results'] changes = self.build_state[action]['result-changes'] ever_passed = set(self.build_state[action]['ever-passed']) versions = self.build_state[action]['build-versions'] new_regressions = {k for k in changes if changes[k] == 'PASS -> FAIL'} all_regressions = {k for k in ever_passed if results[k] == 'FAIL'} all_fails = {k for k in results if results[k] == 'FAIL'} if new_regressions: new_reg_list = sorted(['FAIL: %s' % k for k in new_regressions]) new_reg_text = ('New regressions:\n\n%s\n\n' % '\n'.join(new_reg_list)) else: new_reg_text = '' if all_regressions: all_reg_list = sorted(['FAIL: %s' % k for k in all_regressions]) all_reg_text = ('All regressions:\n\n%s\n\n' % '\n'.join(all_reg_list)) else: all_reg_text = '' if all_fails: all_fail_list = sorted(['FAIL: %s' % k for k in all_fails]) all_fail_text = ('All failures:\n\n%s\n\n' % '\n'.join(all_fail_list)) else: all_fail_text = '' if changes: changes_list = sorted(changes.keys()) changes_list = ['%s: %s' % (changes[k], k) for k in changes_list] changes_text = ('All changed results:\n\n%s\n\n' % '\n'.join(changes_list)) else: changes_text = '' results_text = (new_reg_text + all_reg_text + all_fail_text + changes_text) if not results_text: results_text = 'Clean build with unchanged results.\n\n' versions_list = sorted(versions.keys()) versions_list = ['%s: %s (%s)' % (k, versions[k]['version'], versions[k]['revision']) for k in versions_list] versions_text = ('Component versions for this build:\n\n%s\n' % '\n'.join(versions_list)) body_text = results_text + versions_text msg = email.mime.text.MIMEText(body_text) msg['Subject'] = subject msg['From'] = self.bot_config['email-from'] msg['To'] = self.bot_config['email-to'] msg['Message-ID'] = email.utils.make_msgid() msg['Date'] = email.utils.format_datetime(datetime.datetime.utcnow()) with smtplib.SMTP(self.bot_config['email-server']) as s: s.send_message(msg) def bot_run_self(self, opts, action, check=True): """Run a copy of this script with given options.""" cmd = [sys.executable, sys.argv[0], '--keep=none', '-j%d' % self.parallelism] if self.full_gcc: cmd.append('--full-gcc') cmd.extend(opts) cmd.extend([self.topdir, action]) sys.stdout.flush() subprocess.run(cmd, check=check) def bot(self): """Run repeated rounds of checkout and builds.""" while True: self.load_bot_config_json() if not self.bot_config['run']: print('Bot exiting by request.') exit(0) self.bot_run_self([], 'bot-cycle', check=False) self.load_bot_config_json() if not self.bot_config['run']: print('Bot exiting by request.') exit(0) time.sleep(self.bot_config['delay']) if self.get_script_text() != self.script_text: print('Script changed, bot re-execing.') self.exec_self() class LinuxHeadersPolicyForBuild(object): """Names and directories for installing Linux headers. Build variant.""" def __init__(self, config): self.arch = config.arch self.srcdir = config.ctx.component_srcdir('linux') self.builddir = config.component_builddir('linux') self.headers_dir = os.path.join(config.sysroot, 'usr') class LinuxHeadersPolicyForUpdateSyscalls(object): """Names and directories for Linux headers. update-syscalls variant.""" def __init__(self, glibc, headers_dir): self.arch = glibc.compiler.arch self.srcdir = glibc.compiler.ctx.component_srcdir('linux') self.builddir = glibc.ctx.component_builddir( 'update-syscalls', glibc.name, 'build-linux') self.headers_dir = headers_dir def install_linux_headers(policy, cmdlist): """Install Linux kernel headers.""" arch_map = {'aarch64': 'arm64', 'alpha': 'alpha', 'arc': 'arc', 'arm': 'arm', 'csky': 'csky', 'hppa': 'parisc', 'i486': 'x86', 'i586': 'x86', 'i686': 'x86', 'i786': 'x86', 'ia64': 'ia64', 'm68k': 'm68k', 'microblaze': 'microblaze', 'mips': 'mips', 'nios2': 'nios2', 'powerpc': 'powerpc', 's390': 's390', 'riscv32': 'riscv', 'riscv64': 'riscv', 'sh': 'sh', 'sparc': 'sparc', 'x86_64': 'x86'} linux_arch = None for k in arch_map: if policy.arch.startswith(k): linux_arch = arch_map[k] break assert linux_arch is not None cmdlist.push_subdesc('linux') cmdlist.create_use_dir(policy.builddir) cmdlist.add_command('install-headers', ['make', '-C', policy.srcdir, 'O=%s' % policy.builddir, 'ARCH=%s' % linux_arch, 'INSTALL_HDR_PATH=%s' % policy.headers_dir, 'headers_install']) cmdlist.cleanup_dir() cmdlist.pop_subdesc() class Config(object): """A configuration for building a compiler and associated libraries.""" def __init__(self, ctx, arch, os_name, variant=None, gcc_cfg=None, first_gcc_cfg=None, binutils_cfg=None, glibcs=None, extra_glibcs=None): """Initialize a Config object.""" self.ctx = ctx self.arch = arch self.os = os_name self.variant = variant if variant is None: self.name = '%s-%s' % (arch, os_name) else: self.name = '%s-%s-%s' % (arch, os_name, variant) self.triplet = '%s-glibc-%s' % (arch, os_name) if gcc_cfg is None: self.gcc_cfg = [] else: self.gcc_cfg = gcc_cfg if first_gcc_cfg is None: self.first_gcc_cfg = [] else: self.first_gcc_cfg = first_gcc_cfg if binutils_cfg is None: self.binutils_cfg = [] else: self.binutils_cfg = binutils_cfg if glibcs is None: glibcs = [{'variant': variant}] if extra_glibcs is None: extra_glibcs = [] glibcs = [Glibc(self, **g) for g in glibcs] extra_glibcs = [Glibc(self, **g) for g in extra_glibcs] self.all_glibcs = glibcs + extra_glibcs self.compiler_glibcs = glibcs self.installdir = ctx.compiler_installdir(self.name) self.bindir = ctx.compiler_bindir(self.name) self.sysroot = ctx.compiler_sysroot(self.name) self.builddir = os.path.join(ctx.builddir, 'compilers', self.name) self.logsdir = os.path.join(ctx.logsdir, 'compilers', self.name) def component_builddir(self, component): """Return the directory to use for a (non-glibc) build.""" return self.ctx.component_builddir('compilers', self.name, component) def build(self): """Generate commands to build this compiler.""" self.ctx.remove_recreate_dirs(self.installdir, self.builddir, self.logsdir) cmdlist = CommandList('compilers-%s' % self.name, self.ctx.keep) cmdlist.add_command('check-host-libraries', ['test', '-f', os.path.join(self.ctx.host_libraries_installdir, 'ok')]) cmdlist.use_path(self.bindir) self.build_cross_tool(cmdlist, 'binutils', 'binutils', ['--disable-gdb', '--disable-gdbserver', '--disable-libdecnumber', '--disable-readline', '--disable-sim'] + self.binutils_cfg) if self.os.startswith('linux'): install_linux_headers(LinuxHeadersPolicyForBuild(self), cmdlist) self.build_gcc(cmdlist, True) if self.os == 'gnu': self.install_gnumach_headers(cmdlist) self.build_cross_tool(cmdlist, 'mig', 'mig') self.install_hurd_headers(cmdlist) for g in self.compiler_glibcs: cmdlist.push_subdesc('glibc') cmdlist.push_subdesc(g.name) g.build_glibc(cmdlist, GlibcPolicyForCompiler(g)) cmdlist.pop_subdesc() cmdlist.pop_subdesc() self.build_gcc(cmdlist, False) cmdlist.add_command('done', ['touch', os.path.join(self.installdir, 'ok')]) self.ctx.add_makefile_cmdlist('compilers-%s' % self.name, cmdlist, self.logsdir) def build_cross_tool(self, cmdlist, tool_src, tool_build, extra_opts=None): """Build one cross tool.""" srcdir = self.ctx.component_srcdir(tool_src) builddir = self.component_builddir(tool_build) cmdlist.push_subdesc(tool_build) cmdlist.create_use_dir(builddir) cfg_cmd = [os.path.join(srcdir, 'configure'), '--prefix=%s' % self.installdir, '--build=%s' % self.ctx.build_triplet, '--host=%s' % self.ctx.build_triplet, '--target=%s' % self.triplet, '--with-sysroot=%s' % self.sysroot] if extra_opts: cfg_cmd.extend(extra_opts) cmdlist.add_command('configure', cfg_cmd) cmdlist.add_command('build', ['make']) # Parallel "make install" for GCC has race conditions that can # cause it to fail; see # . Such # problems are not known for binutils, but doing the # installation in parallel within a particular toolchain build # (as opposed to installation of one toolchain from # build-many-glibcs.py running in parallel to the installation # of other toolchains being built) is not known to be # significantly beneficial, so it is simplest just to disable # parallel install for cross tools here. cmdlist.add_command('install', ['make', '-j1', 'install']) cmdlist.cleanup_dir() cmdlist.pop_subdesc() def install_gnumach_headers(self, cmdlist): """Install GNU Mach headers.""" srcdir = self.ctx.component_srcdir('gnumach') builddir = self.component_builddir('gnumach') cmdlist.push_subdesc('gnumach') cmdlist.create_use_dir(builddir) cmdlist.add_command('configure', [os.path.join(srcdir, 'configure'), '--build=%s' % self.ctx.build_triplet, '--host=%s' % self.triplet, '--prefix=', 'CC=%s-gcc -nostdlib' % self.triplet]) cmdlist.add_command('install', ['make', 'DESTDIR=%s' % self.sysroot, 'install-data']) cmdlist.cleanup_dir() cmdlist.pop_subdesc() def install_hurd_headers(self, cmdlist): """Install Hurd headers.""" srcdir = self.ctx.component_srcdir('hurd') builddir = self.component_builddir('hurd') cmdlist.push_subdesc('hurd') cmdlist.create_use_dir(builddir) cmdlist.add_command('configure', [os.path.join(srcdir, 'configure'), '--build=%s' % self.ctx.build_triplet, '--host=%s' % self.triplet, '--prefix=', '--disable-profile', '--without-parted', 'CC=%s-gcc -nostdlib' % self.triplet]) cmdlist.add_command('install', ['make', 'prefix=%s' % self.sysroot, 'no_deps=t', 'install-headers']) cmdlist.cleanup_dir() cmdlist.pop_subdesc() def build_gcc(self, cmdlist, bootstrap): """Build GCC.""" # libssp is of little relevance with glibc's own stack # checking support. libcilkrts does not support GNU/Hurd (and # has been removed in GCC 8, so --disable-libcilkrts can be # removed once glibc no longer supports building with older # GCC versions). cfg_opts = list(self.gcc_cfg) cfg_opts += ['--disable-libssp', '--disable-libcilkrts'] host_libs = self.ctx.host_libraries_installdir cfg_opts += ['--with-gmp=%s' % host_libs, '--with-mpfr=%s' % host_libs, '--with-mpc=%s' % host_libs] if bootstrap: tool_build = 'gcc-first' # Building a static-only, C-only compiler that is # sufficient to build glibc. Various libraries and # features that may require libc headers must be disabled. # When configuring with a sysroot, --with-newlib is # required to define inhibit_libc (to stop some parts of # libgcc including libc headers); --without-headers is not # sufficient. cfg_opts += ['--enable-languages=c', '--disable-shared', '--disable-threads', '--disable-libatomic', '--disable-decimal-float', '--disable-libffi', '--disable-libgomp', '--disable-libitm', '--disable-libmpx', '--disable-libquadmath', '--disable-libsanitizer', '--without-headers', '--with-newlib', '--with-glibc-version=%s' % self.ctx.glibc_version ] cfg_opts += self.first_gcc_cfg else: tool_build = 'gcc' # libsanitizer commonly breaks because of glibc header # changes, or on unusual targets. C++ pre-compiled # headers are not used during the glibc build and are # expensive to create. if not self.ctx.full_gcc: cfg_opts += ['--disable-libsanitizer', '--disable-libstdcxx-pch'] langs = 'all' if self.ctx.full_gcc else 'c,c++' cfg_opts += ['--enable-languages=%s' % langs, '--enable-shared', '--enable-threads'] self.build_cross_tool(cmdlist, 'gcc', tool_build, cfg_opts) class GlibcPolicyDefault(object): """Build policy for glibc: common defaults.""" def __init__(self, glibc): self.srcdir = glibc.ctx.component_srcdir('glibc') self.use_usr = glibc.os != 'gnu' self.prefix = '/usr' if self.use_usr else '' self.configure_args = [ '--prefix=%s' % self.prefix, '--enable-profile', '--build=%s' % glibc.ctx.build_triplet, '--host=%s' % glibc.triplet, 'CC=%s' % glibc.tool_name('gcc'), 'CXX=%s' % glibc.tool_name('g++'), 'AR=%s' % glibc.tool_name('ar'), 'AS=%s' % glibc.tool_name('as'), 'LD=%s' % glibc.tool_name('ld'), 'NM=%s' % glibc.tool_name('nm'), 'OBJCOPY=%s' % glibc.tool_name('objcopy'), 'OBJDUMP=%s' % glibc.tool_name('objdump'), 'RANLIB=%s' % glibc.tool_name('ranlib'), 'READELF=%s' % glibc.tool_name('readelf'), 'STRIP=%s' % glibc.tool_name('strip'), ] if glibc.os == 'gnu': self.configure_args.append('MIG=%s' % glibc.tool_name('mig')) self.configure_args += glibc.cfg def configure(self, cmdlist): """Invoked to add the configure command to the command list.""" cmdlist.add_command('configure', [os.path.join(self.srcdir, 'configure'), *self.configure_args]) def extra_commands(self, cmdlist): """Invoked to inject additional commands (make check) after build.""" pass class GlibcPolicyForCompiler(GlibcPolicyDefault): """Build policy for glibc during the compilers stage.""" def __init__(self, glibc): super().__init__(glibc) self.builddir = glibc.ctx.component_builddir( 'compilers', glibc.compiler.name, 'glibc', glibc.name) self.installdir = glibc.compiler.sysroot class GlibcPolicyForBuild(GlibcPolicyDefault): """Build policy for glibc during the glibcs stage.""" def __init__(self, glibc): super().__init__(glibc) self.builddir = glibc.ctx.component_builddir( 'glibcs', glibc.name, 'glibc') self.installdir = glibc.ctx.glibc_installdir(glibc.name) if glibc.ctx.strip: self.strip = glibc.tool_name('strip') else: self.strip = None self.save_logs = glibc.ctx.save_logs def extra_commands(self, cmdlist): if self.strip: # Avoid picking up libc.so and libpthread.so, which are # linker scripts stored in /lib on Hurd. libc and # libpthread are still stripped via their libc-X.YY.so # implementation files. find_command = (('find %s/lib* -name "*.so"' + r' \! -name libc.so \! -name libpthread.so') % self.installdir) cmdlist.add_command('strip', ['sh', '-c', ('%s $(%s)' % (self.strip, find_command))]) cmdlist.add_command('check', ['make', 'check']) cmdlist.add_command('save-logs', [self.save_logs], always_run=True) class GlibcPolicyForUpdateSyscalls(GlibcPolicyDefault): """Build policy for glibc during update-syscalls.""" def __init__(self, glibc): super().__init__(glibc) self.builddir = glibc.ctx.component_builddir( 'update-syscalls', glibc.name, 'glibc') self.linuxdir = glibc.ctx.component_builddir( 'update-syscalls', glibc.name, 'linux') self.linux_policy = LinuxHeadersPolicyForUpdateSyscalls( glibc, self.linuxdir) self.configure_args.insert( 0, '--with-headers=%s' % os.path.join(self.linuxdir, 'include')) # self.installdir not set because installation is not supported class Glibc(object): """A configuration for building glibc.""" def __init__(self, compiler, arch=None, os_name=None, variant=None, cfg=None, ccopts=None): """Initialize a Glibc object.""" self.ctx = compiler.ctx self.compiler = compiler if arch is None: self.arch = compiler.arch else: self.arch = arch if os_name is None: self.os = compiler.os else: self.os = os_name self.variant = variant if variant is None: self.name = '%s-%s' % (self.arch, self.os) else: self.name = '%s-%s-%s' % (self.arch, self.os, variant) self.triplet = '%s-glibc-%s' % (self.arch, self.os) if cfg is None: self.cfg = [] else: self.cfg = cfg self.ccopts = ccopts def tool_name(self, tool): """Return the name of a cross-compilation tool.""" ctool = '%s-%s' % (self.compiler.triplet, tool) if self.ccopts and (tool == 'gcc' or tool == 'g++'): ctool = '%s %s' % (ctool, self.ccopts) return ctool def build(self): """Generate commands to build this glibc.""" builddir = self.ctx.component_builddir('glibcs', self.name, 'glibc') installdir = self.ctx.glibc_installdir(self.name) logsdir = os.path.join(self.ctx.logsdir, 'glibcs', self.name) self.ctx.remove_recreate_dirs(installdir, builddir, logsdir) cmdlist = CommandList('glibcs-%s' % self.name, self.ctx.keep) cmdlist.add_command('check-compilers', ['test', '-f', os.path.join(self.compiler.installdir, 'ok')]) cmdlist.use_path(self.compiler.bindir) self.build_glibc(cmdlist, GlibcPolicyForBuild(self)) self.ctx.add_makefile_cmdlist('glibcs-%s' % self.name, cmdlist, logsdir) def build_glibc(self, cmdlist, policy): """Generate commands to build this glibc, either as part of a compiler build or with the bootstrapped compiler (and in the latter case, run tests as well).""" cmdlist.create_use_dir(policy.builddir) policy.configure(cmdlist) cmdlist.add_command('build', ['make']) cmdlist.add_command('install', ['make', 'install', 'install_root=%s' % policy.installdir]) # GCC uses paths such as lib/../lib64, so make sure lib # directories always exist. mkdir_cmd = ['mkdir', '-p', os.path.join(policy.installdir, 'lib')] if policy.use_usr: mkdir_cmd += [os.path.join(policy.installdir, 'usr', 'lib')] cmdlist.add_command('mkdir-lib', mkdir_cmd) policy.extra_commands(cmdlist) cmdlist.cleanup_dir() def update_syscalls(self): if self.os == 'gnu': # Hurd does not have system call tables that need updating. return policy = GlibcPolicyForUpdateSyscalls(self) logsdir = os.path.join(self.ctx.logsdir, 'update-syscalls', self.name) self.ctx.remove_recreate_dirs(policy.builddir, logsdir) cmdlist = CommandList('update-syscalls-%s' % self.name, self.ctx.keep) cmdlist.add_command('check-compilers', ['test', '-f', os.path.join(self.compiler.installdir, 'ok')]) cmdlist.use_path(self.compiler.bindir) install_linux_headers(policy.linux_policy, cmdlist) cmdlist.create_use_dir(policy.builddir) policy.configure(cmdlist) cmdlist.add_command('build', ['make', 'update-syscall-lists']) cmdlist.cleanup_dir() self.ctx.add_makefile_cmdlist('update-syscalls-%s' % self.name, cmdlist, logsdir) class Command(object): """A command run in the build process.""" def __init__(self, desc, num, dir, path, command, always_run=False): """Initialize a Command object.""" self.dir = dir self.path = path self.desc = desc trans = str.maketrans({' ': '-'}) self.logbase = '%03d-%s' % (num, desc.translate(trans)) self.command = command self.always_run = always_run @staticmethod def shell_make_quote_string(s): """Given a string not containing a newline, quote it for use by the shell and make.""" assert '\n' not in s if re.fullmatch('[]+,./0-9@A-Z_a-z-]+', s): return s strans = str.maketrans({"'": "'\\''"}) s = "'%s'" % s.translate(strans) mtrans = str.maketrans({'$': '$$'}) return s.translate(mtrans) @staticmethod def shell_make_quote_list(l, translate_make): """Given a list of strings not containing newlines, quote them for use by the shell and make, returning a single string. If translate_make is true and the first string is 'make', change it to $(MAKE).""" l = [Command.shell_make_quote_string(s) for s in l] if translate_make and l[0] == 'make': l[0] = '$(MAKE)' return ' '.join(l) def shell_make_quote(self): """Return this command quoted for the shell and make.""" return self.shell_make_quote_list(self.command, True) class CommandList(object): """A list of commands run in the build process.""" def __init__(self, desc, keep): """Initialize a CommandList object.""" self.cmdlist = [] self.dir = None self.path = None self.desc = [desc] self.keep = keep def desc_txt(self, desc): """Return the description to use for a command.""" return '%s %s' % (' '.join(self.desc), desc) def use_dir(self, dir): """Set the default directory for subsequent commands.""" self.dir = dir def use_path(self, path): """Set a directory to be prepended to the PATH for subsequent commands.""" self.path = path def push_subdesc(self, subdesc): """Set the default subdescription for subsequent commands (e.g., the name of a component being built, within the series of commands building it).""" self.desc.append(subdesc) def pop_subdesc(self): """Pop a subdescription from the list of descriptions.""" self.desc.pop() def create_use_dir(self, dir): """Remove and recreate a directory and use it for subsequent commands.""" self.add_command_dir('rm', None, ['rm', '-rf', dir]) self.add_command_dir('mkdir', None, ['mkdir', '-p', dir]) self.use_dir(dir) def add_command_dir(self, desc, dir, command, always_run=False): """Add a command to run in a given directory.""" cmd = Command(self.desc_txt(desc), len(self.cmdlist), dir, self.path, command, always_run) self.cmdlist.append(cmd) def add_command(self, desc, command, always_run=False): """Add a command to run in the default directory.""" cmd = Command(self.desc_txt(desc), len(self.cmdlist), self.dir, self.path, command, always_run) self.cmdlist.append(cmd) def cleanup_dir(self, desc='cleanup', dir=None): """Clean up a build directory. If no directory is specified, the default directory is cleaned up and ceases to be the default directory.""" if dir is None: dir = self.dir self.use_dir(None) if self.keep != 'all': self.add_command_dir(desc, None, ['rm', '-rf', dir], always_run=(self.keep == 'none')) def makefile_commands(self, wrapper, logsdir): """Return the sequence of commands in the form of text for a Makefile. The given wrapper script takes arguments: base of logs for previous command, or empty; base of logs for this command; description; directory; PATH addition; the command itself.""" # prev_base is the base of the name for logs of the previous # command that is not always-run (that is, a build command, # whose failure should stop subsequent build commands from # being run, as opposed to a cleanup command, which is run # even if previous commands failed). prev_base = '' cmds = [] for c in self.cmdlist: ctxt = c.shell_make_quote() if prev_base and not c.always_run: prev_log = os.path.join(logsdir, prev_base) else: prev_log = '' this_log = os.path.join(logsdir, c.logbase) if not c.always_run: prev_base = c.logbase if c.dir is None: dir = '' else: dir = c.dir if c.path is None: path = '' else: path = c.path prelims = [wrapper, prev_log, this_log, c.desc, dir, path] prelim_txt = Command.shell_make_quote_list(prelims, False) cmds.append('\t@%s %s' % (prelim_txt, ctxt)) return '\n'.join(cmds) def status_logs(self, logsdir): """Return the list of log files with command status.""" return [os.path.join(logsdir, '%s-status.txt' % c.logbase) for c in self.cmdlist] def get_parser(): """Return an argument parser for this module.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-j', dest='parallelism', help='Run this number of jobs in parallel', type=int, default=os.cpu_count()) parser.add_argument('--keep', dest='keep', help='Whether to keep all build directories, ' 'none or only those from failed builds', default='none', choices=('none', 'all', 'failed')) parser.add_argument('--replace-sources', action='store_true', help='Remove and replace source directories ' 'with the wrong version of a component') parser.add_argument('--strip', action='store_true', help='Strip installed glibc libraries') parser.add_argument('--full-gcc', action='store_true', help='Build GCC with all languages and libsanitizer') parser.add_argument('--shallow', action='store_true', help='Do not download Git history during checkout') parser.add_argument('topdir', help='Toplevel working directory') parser.add_argument('action', help='What to do', choices=('checkout', 'bot-cycle', 'bot', 'host-libraries', 'compilers', 'glibcs', 'update-syscalls', 'list-compilers', 'list-glibcs')) parser.add_argument('configs', help='Versions to check out or configurations to build', nargs='*') return parser def main(argv): """The main entry point.""" parser = get_parser() opts = parser.parse_args(argv) topdir = os.path.abspath(opts.topdir) ctx = Context(topdir, opts.parallelism, opts.keep, opts.replace_sources, opts.strip, opts.full_gcc, opts.action, shallow=opts.shallow) ctx.run_builds(opts.action, opts.configs) if __name__ == '__main__': main(sys.argv[1:])