aboutsummaryrefslogtreecommitdiff
path: root/llvm_tools/nightly_revert_checker.py
diff options
context:
space:
mode:
Diffstat (limited to 'llvm_tools/nightly_revert_checker.py')
-rwxr-xr-xllvm_tools/nightly_revert_checker.py310
1 files changed, 310 insertions, 0 deletions
diff --git a/llvm_tools/nightly_revert_checker.py b/llvm_tools/nightly_revert_checker.py
new file mode 100755
index 00000000..3a23890a
--- /dev/null
+++ b/llvm_tools/nightly_revert_checker.py
@@ -0,0 +1,310 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Checks for new reverts in LLVM on a nightly basis.
+
+If any reverts are found that were previously unknown, this fires off an email.
+All LLVM SHAs to monitor are autodetected.
+"""
+
+# pylint: disable=cros-logging-import
+
+from __future__ import print_function
+
+import argparse
+import io
+import json
+import logging
+import os
+import pprint
+import subprocess
+import sys
+import typing as t
+
+import cros_utils.email_sender as email_sender
+import cros_utils.tiny_render as tiny_render
+import get_llvm_hash
+import git_llvm_rev
+import revert_checker
+
+State = t.Any
+
+
+def _find_interesting_android_shas(
+ android_llvm_toolchain_dir: str) -> t.List[t.Tuple[str]]:
+ llvm_project = os.path.join(android_llvm_toolchain_dir,
+ 'toolchain/llvm-project')
+
+ def get_llvm_merge_base(branch: str) -> str:
+ return subprocess.check_output(
+ ['git', 'merge-base', branch, 'aosp/upstream-master'],
+ cwd=llvm_project,
+ encoding='utf-8',
+ ).strip()
+
+ main_legacy = get_llvm_merge_base('aosp/master-legacy')
+ testing_upstream = get_llvm_merge_base('aosp/testing-upstream')
+ result = [('main-legacy', main_legacy)]
+
+ # If these are the same SHA, there's no point in tracking both.
+ if main_legacy != testing_upstream:
+ result.append(('testing-upstream', testing_upstream))
+ return result
+
+
+def _parse_llvm_ebuild_for_shas(
+ ebuild_file: io.TextIOWrapper) -> t.List[t.Tuple[str]]:
+
+ def parse_ebuild_assignment(line: str) -> str:
+ no_comments = line.split('#')[0]
+ no_assign = no_comments.split('=', 1)[1].strip()
+ assert no_assign.startswith('"') and no_assign.endswith('"'), no_assign
+ return no_assign[1:-1]
+
+ llvm_hash, llvm_next_hash = None, None
+ for line in ebuild_file:
+ if line.startswith('LLVM_HASH='):
+ llvm_hash = parse_ebuild_assignment(line)
+ if llvm_next_hash:
+ break
+ if line.startswith('LLVM_NEXT_HASH'):
+ llvm_next_hash = parse_ebuild_assignment(line)
+ if llvm_hash:
+ break
+ if not llvm_next_hash or not llvm_hash:
+ raise ValueError('Failed to detect SHAs for llvm/llvm_next. Got: '
+ 'llvm=%s; llvm_next=%s' % (llvm_hash, llvm_next_hash))
+
+ results = [('llvm', llvm_hash)]
+ if llvm_next_hash != llvm_hash:
+ results.append(('llvm-next', llvm_next_hash))
+ return results
+
+
+def _find_interesting_chromeos_shas(chromeos_base: str) -> t.List[t.Tuple[str]]:
+ llvm_dir = os.path.join(chromeos_base,
+ 'src/third_party/chromiumos-overlay/sys-devel/llvm')
+ candidate_ebuilds = [
+ os.path.join(llvm_dir, x)
+ for x in os.listdir(llvm_dir)
+ if '_pre' in x and not os.path.islink(os.path.join(llvm_dir, x))
+ ]
+
+ if len(candidate_ebuilds) != 1:
+ raise ValueError('Expected exactly one llvm ebuild candidate; got %s' %
+ pprint.pformat(candidate_ebuilds))
+
+ with open(candidate_ebuilds[0], encoding='utf-8') as f:
+ return _parse_llvm_ebuild_for_shas(f)
+
+
+_Email = t.NamedTuple('_Email', [
+ ('subject', str),
+ ('body', tiny_render.Piece),
+])
+
+
+def _generate_revert_email(
+ repository_name: str, friendly_name: str, sha: str,
+ prettify_sha: t.Callable[[str], tiny_render.Piece],
+ get_sha_description: t.Callable[[str], tiny_render.Piece],
+ new_reverts: t.List[revert_checker.Revert]) -> _Email:
+ email_pieces = [
+ 'It looks like there may be %s across %s (' % (
+ 'a new revert' if len(new_reverts) == 1 else 'new reverts',
+ friendly_name,
+ ),
+ prettify_sha(sha),
+ ').',
+ tiny_render.line_break,
+ tiny_render.line_break,
+ 'That is:' if len(new_reverts) == 1 else 'These are:',
+ ]
+
+ revert_listing = []
+ for revert in sorted(new_reverts, key=lambda r: r.sha):
+ revert_listing.append([
+ prettify_sha(revert.sha),
+ ' (appears to revert ',
+ prettify_sha(revert.reverted_sha),
+ '): ',
+ get_sha_description(revert.sha),
+ ])
+
+ email_pieces.append(tiny_render.UnorderedList(items=revert_listing))
+ email_pieces += [
+ tiny_render.line_break,
+ 'PTAL and consider reverting them locally.',
+ ]
+ return _Email(
+ subject='[revert-checker/%s] new %s discovered across %s' % (
+ repository_name,
+ 'revert' if len(new_reverts) == 1 else 'reverts',
+ friendly_name,
+ ),
+ body=email_pieces,
+ )
+
+
+_EmailRecipients = t.NamedTuple(
+ '_EmailRecipients',
+ [
+ ('well_known', t.List[str]),
+ ('direct', t.List[str]),
+ ],
+)
+
+
+def _send_revert_email(recipients: _EmailRecipients, email: _Email) -> None:
+ email_sender.EmailSender().SendX20Email(
+ subject=email.subject,
+ identifier='revert-checker',
+ well_known_recipients=recipients.well_known,
+ direct_recipients=['gbiv@google.com'] + recipients.direct,
+ text_body=tiny_render.render_text_pieces(email.body),
+ html_body=tiny_render.render_html_pieces(email.body),
+ )
+
+
+def _write_state(state_file: str, new_state: State) -> None:
+ try:
+ tmp_file = state_file + '.new'
+ with open(tmp_file, 'w', encoding='utf-8') as f:
+ json.dump(new_state, f, sort_keys=True, indent=2, separators=(',', ': '))
+ os.rename(tmp_file, state_file)
+ except:
+ try:
+ os.remove(tmp_file)
+ except FileNotFoundError:
+ pass
+ raise
+
+
+def _read_state(state_file: str) -> State:
+ try:
+ with open(state_file) as f:
+ return json.load(f)
+ except FileNotFoundError:
+ logging.info('No state file found at %r; starting with an empty slate',
+ state_file)
+ return {}
+
+
+def main(argv: t.List[str]) -> None:
+ parser = argparse.ArgumentParser(
+ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument(
+ '--state_file', required=True, help='File to store persistent state in.')
+ parser.add_argument(
+ '--llvm_dir', required=True, help='Up-to-date LLVM directory to use.')
+ parser.add_argument(
+ '--dry_run',
+ action='store_true',
+ help='Print email contents, rather than sending them.')
+ parser.add_argument('--debug', action='store_true')
+
+ subparsers = parser.add_subparsers(dest='repository')
+ subparsers.required = True
+
+ chromeos_subparser = subparsers.add_parser('chromeos')
+ chromeos_subparser.add_argument(
+ '--chromeos_dir', required=True, help='Up-to-date CrOS directory to use.')
+
+ android_subparser = subparsers.add_parser('android')
+ android_subparser.add_argument(
+ '--android_llvm_toolchain_dir',
+ required=True,
+ help='Up-to-date android-llvm-toolchain directory to use.')
+
+ opts = parser.parse_args(argv)
+
+ logging.basicConfig(
+ format='%(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: %(message)s',
+ level=logging.DEBUG if opts.debug else logging.INFO,
+ )
+
+ dry_run = opts.dry_run
+ llvm_dir = opts.llvm_dir
+ repository = opts.repository
+ state_file = opts.state_file
+
+ if repository == 'chromeos':
+ interesting_shas = _find_interesting_chromeos_shas(opts.chromeos_dir)
+ recipients = _EmailRecipients(well_known=['mage'], direct=[])
+ elif repository == 'android':
+ interesting_shas = _find_interesting_android_shas(
+ opts.android_llvm_toolchain_dir)
+ recipients = _EmailRecipients(
+ well_known=[], direct=['android-llvm-dev@google.com'])
+ else:
+ raise ValueError('Unknown repository %s' % opts.repository)
+
+ logging.info('Interesting SHAs were %r', interesting_shas)
+
+ state = _read_state(state_file)
+ logging.info('Loaded state\n%s', pprint.pformat(state))
+
+ def prettify_sha(sha: str) -> tiny_render.Piece:
+ rev = get_llvm_hash.GetVersionFrom(llvm_dir, sha)
+
+ # 12 is arbitrary, but should be unambiguous enough.
+ short_sha = sha[:12]
+ return tiny_render.Switch(
+ text='r%s (%s)' % (rev, short_sha),
+ html=tiny_render.Link(
+ href='https://reviews.llvm.org/rG' + sha, inner='r' + str(rev)),
+ )
+
+ def get_sha_description(sha: str) -> tiny_render.Piece:
+ return subprocess.check_output(
+ ['git', 'log', '-n1', '--format=%s', sha],
+ cwd=llvm_dir,
+ encoding='utf-8',
+ ).strip()
+
+ new_state: State = {}
+ revert_emails_to_send: t.List[t.Tuple[str, t.List[revert_checker
+ .Revert]]] = []
+ for friendly_name, sha in interesting_shas:
+ logging.info('Finding reverts across %s (%s)', friendly_name, sha)
+ all_reverts = revert_checker.find_reverts(
+ llvm_dir, sha, root='origin/' + git_llvm_rev.MAIN_BRANCH)
+ logging.info('Detected the following revert(s) across %s:\n%s',
+ friendly_name, pprint.pformat(all_reverts))
+
+ new_state[sha] = [r.sha for r in all_reverts]
+
+ if sha not in state:
+ logging.info('SHA %s is new to me', sha)
+ existing_reverts = set()
+ else:
+ existing_reverts = set(state[sha])
+
+ new_reverts = [r for r in all_reverts if r.sha not in existing_reverts]
+ if not new_reverts:
+ logging.info('...All of which have been reported.')
+ continue
+
+ revert_emails_to_send.append(
+ _generate_revert_email(repository, friendly_name, sha, prettify_sha,
+ get_sha_description, new_reverts))
+
+ # We want to be as free of obvious side-effects as possible in case something
+ # above breaks. Hence, send the email as late as possible.
+ for email in revert_emails_to_send:
+ if dry_run:
+ logging.info('Would send email:\nSubject: %s\nBody:\n%s\n', email.subject,
+ tiny_render.render_text_pieces(email.body))
+ else:
+ logging.info('Sending email with subject %r...', email.subject)
+ _send_revert_email(recipients, email)
+ logging.info('Email sent.')
+
+ _write_state(state_file, new_state)
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))