about summary refs log tree commit diff
path: root/scripts/build-many-glibcs.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/build-many-glibcs.py')
-rwxr-xr-xscripts/build-many-glibcs.py170
1 files changed, 161 insertions, 9 deletions
diff --git a/scripts/build-many-glibcs.py b/scripts/build-many-glibcs.py
index d5355d851e..658a22e65f 100755
--- a/scripts/build-many-glibcs.py
+++ b/scripts/build-many-glibcs.py
@@ -22,21 +22,26 @@
 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, '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 (<component>-<version),
-for 'checkout', or, for actions other than 'checkout', name
-configurations for which compilers or glibc are to be built.
+directory, 'bot-cycle', to run a series of checkout and build steps,
+'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 (<component>-<version), for
+'checkout', or, for actions other than 'checkout' and 'bot-cycle',
+name configurations for which compilers or glibc are to be built.
+
 """
 
 import argparse
 import datetime
+import email.mime.text
+import email.utils
 import json
 import os
 import re
 import shutil
+import smtplib
 import stat
 import subprocess
 import sys
@@ -55,6 +60,7 @@ class Context(object):
         self.srcdir = os.path.join(topdir, 'src')
         self.versions_json = os.path.join(self.srcdir, 'versions.json')
         self.build_state_json = os.path.join(topdir, 'build-state.json')
+        self.bot_config_json = os.path.join(topdir, 'bot-config.json')
         self.installdir = os.path.join(topdir, 'install')
         self.host_libraries_installdir = os.path.join(self.installdir,
                                                       'host-libraries')
@@ -392,6 +398,12 @@ class Context(object):
         if action == 'checkout':
             self.checkout(configs)
             return
+        if action == 'bot-cycle':
+            if configs:
+                print('error: configurations specified for bot-cycle')
+                exit(1)
+            self.bot_cycle()
+            return
         if action == 'host-libraries' and configs:
             print('error: configurations specified for host-libraries')
             exit(1)
@@ -860,6 +872,146 @@ class Context(object):
                                                          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'),
+                            '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)
+        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."""
+        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):
+        """Run a copy of this script with given options."""
+        cmd = [sys.executable, sys.argv[0], '--keep=none',
+               '-j%d' % self.parallelism]
+        cmd.extend(opts)
+        cmd.extend([self.topdir, action])
+        subprocess.run(cmd, check=True)
+
 
 class Config(object):
     """A configuration for building a compiler and associated libraries."""
@@ -1312,8 +1464,8 @@ def get_parser():
                         help='Toplevel working directory')
     parser.add_argument('action',
                         help='What to do',
-                        choices=('checkout', 'host-libraries', 'compilers',
-                                 'glibcs'))
+                        choices=('checkout', 'bot-cycle', 'host-libraries',
+                                 'compilers', 'glibcs'))
     parser.add_argument('configs',
                         help='Versions to check out or configurations to build',
                         nargs='*')