summaryrefslogtreecommitdiff
path: root/chromium/tools/bisect-builds.py
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@theqtcompany.com>2016-05-09 14:22:11 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2016-05-09 15:11:45 +0000
commit2ddb2d3e14eef3de7dbd0cef553d669b9ac2361c (patch)
treee75f511546c5fd1a173e87c1f9fb11d7ac8d1af3 /chromium/tools/bisect-builds.py
parenta4f3d46271c57e8155ba912df46a05559d14726e (diff)
downloadqtwebengine-chromium-2ddb2d3e14eef3de7dbd0cef553d669b9ac2361c.tar.gz
BASELINE: Update Chromium to 51.0.2704.41
Also adds in all smaller components by reversing logic for exclusion. Change-Id: Ibf90b506e7da088ea2f65dcf23f2b0992c504422 Reviewed-by: Joerg Bornemann <joerg.bornemann@theqtcompany.com>
Diffstat (limited to 'chromium/tools/bisect-builds.py')
-rwxr-xr-xchromium/tools/bisect-builds.py1309
1 files changed, 1309 insertions, 0 deletions
diff --git a/chromium/tools/bisect-builds.py b/chromium/tools/bisect-builds.py
new file mode 100755
index 00000000000..e2c577d3184
--- /dev/null
+++ b/chromium/tools/bisect-builds.py
@@ -0,0 +1,1309 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Snapshot Build Bisect Tool
+
+This script bisects a snapshot archive using binary search. It starts at
+a bad revision (it will try to guess HEAD) and asks for a last known-good
+revision. It will then binary search across this revision range by downloading,
+unzipping, and opening Chromium for you. After testing the specific revision,
+it will ask you whether it is good or bad before continuing the search.
+"""
+
+# The base URL for stored build archives.
+CHROMIUM_BASE_URL = ('http://commondatastorage.googleapis.com'
+ '/chromium-browser-snapshots')
+WEBKIT_BASE_URL = ('http://commondatastorage.googleapis.com'
+ '/chromium-webkit-snapshots')
+ASAN_BASE_URL = ('http://commondatastorage.googleapis.com'
+ '/chromium-browser-asan')
+
+# GS bucket name.
+GS_BUCKET_NAME = 'chrome-unsigned/desktop-W15K3Y'
+
+# Base URL for downloading official builds.
+GOOGLE_APIS_URL = 'commondatastorage.googleapis.com'
+
+# The base URL for official builds.
+OFFICIAL_BASE_URL = 'http://%s/%s' % (GOOGLE_APIS_URL, GS_BUCKET_NAME)
+
+# URL template for viewing changelogs between revisions.
+CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s')
+
+# URL to convert SVN revision to git hash.
+CRREV_URL = ('https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/')
+
+# URL template for viewing changelogs between official versions.
+OFFICIAL_CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/'
+ 'src/+log/%s..%s?pretty=full')
+
+# DEPS file URL.
+DEPS_FILE_OLD = ('http://src.chromium.org/viewvc/chrome/trunk/src/'
+ 'DEPS?revision=%d')
+DEPS_FILE_NEW = ('https://chromium.googlesource.com/chromium/src/+/%s/DEPS')
+
+# Blink changelogs URL.
+BLINK_CHANGELOG_URL = ('http://build.chromium.org'
+ '/f/chromium/perf/dashboard/ui/changelog_blink.html'
+ '?url=/trunk&range=%d%%3A%d')
+
+DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s ('
+ 'known good), but no later than %s (first known bad).')
+DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s ('
+ 'known bad), but no later than %s (first known good).')
+
+CHROMIUM_GITHASH_TO_SVN_URL = (
+ 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
+
+BLINK_GITHASH_TO_SVN_URL = (
+ 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json')
+
+GITHASH_TO_SVN_URL = {
+ 'chromium': CHROMIUM_GITHASH_TO_SVN_URL,
+ 'blink': BLINK_GITHASH_TO_SVN_URL,
+}
+
+# Search pattern to be matched in the JSON output from
+# CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision).
+CHROMIUM_SEARCH_PATTERN_OLD = (
+ r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ')
+CHROMIUM_SEARCH_PATTERN = (
+ r'Cr-Commit-Position: refs/heads/master@{#(\d+)}')
+
+# Search pattern to be matched in the json output from
+# BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision).
+BLINK_SEARCH_PATTERN = (
+ r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ')
+
+SEARCH_PATTERN = {
+ 'chromium': CHROMIUM_SEARCH_PATTERN,
+ 'blink': BLINK_SEARCH_PATTERN,
+}
+
+CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with '
+ 'no configured credentials')
+
+###############################################################################
+
+import httplib
+import json
+import optparse
+import os
+import re
+import shlex
+import shutil
+import subprocess
+import sys
+import tempfile
+import threading
+import urllib
+from distutils.version import LooseVersion
+from xml.etree import ElementTree
+import zipfile
+
+
+class PathContext(object):
+ """A PathContext is used to carry the information used to construct URLs and
+ paths when dealing with the storage server and archives."""
+ def __init__(self, base_url, platform, good_revision, bad_revision,
+ is_official, is_asan, use_local_cache, flash_path = None):
+ super(PathContext, self).__init__()
+ # Store off the input parameters.
+ self.base_url = base_url
+ self.platform = platform # What's passed in to the '-a/--archive' option.
+ self.good_revision = good_revision
+ self.bad_revision = bad_revision
+ self.is_official = is_official
+ self.is_asan = is_asan
+ self.build_type = 'release'
+ self.flash_path = flash_path
+ # Dictionary which stores svn revision number as key and it's
+ # corresponding git hash as value. This data is populated in
+ # _FetchAndParse and used later in GetDownloadURL while downloading
+ # the build.
+ self.githash_svn_dict = {}
+ # The name of the ZIP file in a revision directory on the server.
+ self.archive_name = None
+
+ # Whether to cache and use the list of known revisions in a local file to
+ # speed up the initialization of the script at the next run.
+ self.use_local_cache = use_local_cache
+
+ # Locate the local checkout to speed up the script by using locally stored
+ # metadata.
+ abs_file_path = os.path.abspath(os.path.realpath(__file__))
+ local_src_path = os.path.join(os.path.dirname(abs_file_path), '..')
+ if abs_file_path.endswith(os.path.join('tools', 'bisect-builds.py')) and\
+ os.path.exists(os.path.join(local_src_path, '.git')):
+ self.local_src_path = os.path.normpath(local_src_path)
+ else:
+ self.local_src_path = None
+
+ # Set some internal members:
+ # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
+ # _archive_extract_dir = Uncompressed directory in the archive_name file.
+ # _binary_name = The name of the executable to run.
+ if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
+ self._binary_name = 'chrome'
+ elif self.platform in ('mac', 'mac64'):
+ self.archive_name = 'chrome-mac.zip'
+ self._archive_extract_dir = 'chrome-mac'
+ elif self.platform in ('win', 'win64'):
+ self.archive_name = 'chrome-win32.zip'
+ self._archive_extract_dir = 'chrome-win32'
+ self._binary_name = 'chrome.exe'
+ else:
+ raise Exception('Invalid platform: %s' % self.platform)
+
+ if is_official:
+ if self.platform == 'linux':
+ self._listing_platform_dir = 'precise32/'
+ self.archive_name = 'chrome-precise32.zip'
+ self._archive_extract_dir = 'chrome-precise32'
+ elif self.platform == 'linux64':
+ self._listing_platform_dir = 'precise64/'
+ self.archive_name = 'chrome-precise64.zip'
+ self._archive_extract_dir = 'chrome-precise64'
+ elif self.platform == 'mac':
+ self._listing_platform_dir = 'mac/'
+ self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
+ elif self.platform == 'mac64':
+ self._listing_platform_dir = 'mac64/'
+ self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
+ elif self.platform == 'win':
+ self._listing_platform_dir = 'win/'
+ self.archive_name = 'chrome-win.zip'
+ self._archive_extract_dir = 'chrome-win'
+ elif self.platform == 'win64':
+ self._listing_platform_dir = 'win64/'
+ self.archive_name = 'chrome-win64.zip'
+ self._archive_extract_dir = 'chrome-win64'
+ else:
+ if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
+ self.archive_name = 'chrome-linux.zip'
+ self._archive_extract_dir = 'chrome-linux'
+ if self.platform == 'linux':
+ self._listing_platform_dir = 'Linux/'
+ elif self.platform == 'linux64':
+ self._listing_platform_dir = 'Linux_x64/'
+ elif self.platform == 'linux-arm':
+ self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
+ elif self.platform == 'chromeos':
+ self._listing_platform_dir = 'Linux_ChromiumOS_Full/'
+ # There is no 64-bit distinction for non-official mac builds.
+ elif self.platform in ('mac', 'mac64'):
+ self._listing_platform_dir = 'Mac/'
+ self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
+ elif self.platform == 'win':
+ self._listing_platform_dir = 'Win/'
+
+ def GetASANPlatformDir(self):
+ """ASAN builds are in directories like "linux-release", or have filenames
+ like "asan-win32-release-277079.zip". This aligns to our platform names
+ except in the case of Windows where they use "win32" instead of "win"."""
+ if self.platform == 'win':
+ return 'win32'
+ else:
+ return self.platform
+
+ def GetListingURL(self, marker=None):
+ """Returns the URL for a directory listing, with an optional marker."""
+ marker_param = ''
+ if marker:
+ marker_param = '&marker=' + str(marker)
+ if self.is_asan:
+ prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type)
+ return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param
+ else:
+ return (self.base_url + '/?delimiter=/&prefix=' +
+ self._listing_platform_dir + marker_param)
+
+ def GetDownloadURL(self, revision):
+ """Gets the download URL for a build archive of a specific revision."""
+ if self.is_asan:
+ return '%s/%s-%s/%s-%d.zip' % (
+ ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
+ self.GetASANBaseName(), revision)
+ if self.is_official:
+ return '%s/%s/%s%s' % (
+ OFFICIAL_BASE_URL, revision, self._listing_platform_dir,
+ self.archive_name)
+ else:
+ if str(revision) in self.githash_svn_dict:
+ revision = self.githash_svn_dict[str(revision)]
+ return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
+ revision, self.archive_name)
+
+ def GetLastChangeURL(self):
+ """Returns a URL to the LAST_CHANGE file."""
+ return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
+
+ def GetASANBaseName(self):
+ """Returns the base name of the ASAN zip file."""
+ if 'linux' in self.platform:
+ return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(),
+ self.build_type)
+ else:
+ return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type)
+
+ def GetLaunchPath(self, revision):
+ """Returns a relative path (presumably from the archive extraction location)
+ that is used to run the executable."""
+ if self.is_asan:
+ extract_dir = '%s-%d' % (self.GetASANBaseName(), revision)
+ else:
+ extract_dir = self._archive_extract_dir
+ return os.path.join(extract_dir, self._binary_name)
+
+ def ParseDirectoryIndex(self, last_known_rev):
+ """Parses the Google Storage directory listing into a list of revision
+ numbers."""
+
+ def _GetMarkerForRev(revision):
+ if self.is_asan:
+ return '%s-%s/%s-%d.zip' % (
+ self.GetASANPlatformDir(), self.build_type,
+ self.GetASANBaseName(), revision)
+ return '%s%d' % (self._listing_platform_dir, revision)
+
+ def _FetchAndParse(url):
+ """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
+ next-marker is not None, then the listing is a partial listing and another
+ fetch should be performed with next-marker being the marker= GET
+ parameter."""
+ handle = urllib.urlopen(url)
+ document = ElementTree.parse(handle)
+
+ # All nodes in the tree are namespaced. Get the root's tag name to extract
+ # the namespace. Etree does namespaces as |{namespace}tag|.
+ root_tag = document.getroot().tag
+ end_ns_pos = root_tag.find('}')
+ if end_ns_pos == -1:
+ raise Exception('Could not locate end namespace for directory index')
+ namespace = root_tag[:end_ns_pos + 1]
+
+ # Find the prefix (_listing_platform_dir) and whether or not the list is
+ # truncated.
+ prefix_len = len(document.find(namespace + 'Prefix').text)
+ next_marker = None
+ is_truncated = document.find(namespace + 'IsTruncated')
+ if is_truncated is not None and is_truncated.text.lower() == 'true':
+ next_marker = document.find(namespace + 'NextMarker').text
+ # Get a list of all the revisions.
+ revisions = []
+ githash_svn_dict = {}
+ if self.is_asan:
+ asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName()))
+ # Non ASAN builds are in a <revision> directory. The ASAN builds are
+ # flat
+ all_prefixes = document.findall(namespace + 'Contents/' +
+ namespace + 'Key')
+ for prefix in all_prefixes:
+ m = asan_regex.match(prefix.text)
+ if m:
+ try:
+ revisions.append(int(m.group(1)))
+ except ValueError:
+ pass
+ else:
+ all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
+ namespace + 'Prefix')
+ # The <Prefix> nodes have content of the form of
+ # |_listing_platform_dir/revision/|. Strip off the platform dir and the
+ # trailing slash to just have a number.
+ for prefix in all_prefixes:
+ revnum = prefix.text[prefix_len:-1]
+ try:
+ revnum = int(revnum)
+ revisions.append(revnum)
+ # Notes:
+ # Ignore hash in chromium-browser-snapshots as they are invalid
+ # Resulting in 404 error in fetching pages:
+ # https://chromium.googlesource.com/chromium/src/+/[rev_hash]
+ except ValueError:
+ pass
+ return (revisions, next_marker, githash_svn_dict)
+
+ # Fetch the first list of revisions.
+ if last_known_rev:
+ revisions = []
+ # Optimization: Start paging at the last known revision (local cache).
+ next_marker = _GetMarkerForRev(last_known_rev)
+ # Optimization: Stop paging at the last known revision (remote).
+ last_change_rev = GetChromiumRevision(self, self.GetLastChangeURL())
+ if last_known_rev == last_change_rev:
+ return []
+ else:
+ (revisions, next_marker, new_dict) = _FetchAndParse(self.GetListingURL())
+ self.githash_svn_dict.update(new_dict)
+ last_change_rev = None
+
+ # If the result list was truncated, refetch with the next marker. Do this
+ # until an entire directory listing is done.
+ while next_marker:
+ sys.stdout.write('\rFetching revisions at marker %s' % next_marker)
+ sys.stdout.flush()
+
+ next_url = self.GetListingURL(next_marker)
+ (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url)
+ revisions.extend(new_revisions)
+ self.githash_svn_dict.update(new_dict)
+ if last_change_rev and last_change_rev in new_revisions:
+ break
+ sys.stdout.write('\r')
+ sys.stdout.flush()
+ return revisions
+
+ def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot):
+ json_url = GITHASH_TO_SVN_URL[depot] % git_sha1
+ response = urllib.urlopen(json_url)
+ if response.getcode() == 200:
+ try:
+ data = json.loads(response.read()[4:])
+ except ValueError:
+ print 'ValueError for JSON URL: %s' % json_url
+ raise ValueError
+ else:
+ raise ValueError
+ if 'message' in data:
+ message = data['message'].split('\n')
+ message = [line for line in message if line.strip()]
+ search_pattern = re.compile(SEARCH_PATTERN[depot])
+ result = search_pattern.search(message[len(message)-1])
+ if result:
+ return result.group(1)
+ else:
+ if depot == 'chromium':
+ result = re.search(CHROMIUM_SEARCH_PATTERN_OLD,
+ message[len(message)-1])
+ if result:
+ return result.group(1)
+ print 'Failed to get svn revision number for %s' % git_sha1
+ raise ValueError
+
+ def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot):
+ def _RunGit(command, path):
+ command = ['git'] + command
+ shell = sys.platform.startswith('win')
+ proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, cwd=path)
+ (output, _) = proc.communicate()
+ return (output, proc.returncode)
+
+ path = self.local_src_path
+ if depot == 'blink':
+ path = os.path.join(self.local_src_path, 'third_party', 'WebKit')
+ revision = None
+ try:
+ command = ['svn', 'find-rev', git_sha1]
+ (git_output, return_code) = _RunGit(command, path)
+ if not return_code:
+ revision = git_output.strip('\n')
+ except ValueError:
+ pass
+ if not revision:
+ command = ['log', '-n1', '--format=%s', git_sha1]
+ (git_output, return_code) = _RunGit(command, path)
+ if not return_code:
+ revision = re.match('SVN changes up to revision ([0-9]+)', git_output)
+ revision = revision.group(1) if revision else None
+ if revision:
+ return revision
+ raise ValueError
+
+ def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'):
+ if not self.local_src_path:
+ return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot)
+ else:
+ return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot)
+
+ def GetRevList(self):
+ """Gets the list of revision numbers between self.good_revision and
+ self.bad_revision."""
+
+ cache = {}
+ # The cache is stored in the same directory as bisect-builds.py
+ cache_filename = os.path.join(
+ os.path.abspath(os.path.dirname(__file__)),
+ '.bisect-builds-cache.json')
+ cache_dict_key = self.GetListingURL()
+
+ def _LoadBucketFromCache():
+ if self.use_local_cache:
+ try:
+ with open(cache_filename) as cache_file:
+ for (key, value) in json.load(cache_file).items():
+ cache[key] = value
+ revisions = cache.get(cache_dict_key, [])
+ githash_svn_dict = cache.get('githash_svn_dict', {})
+ if revisions:
+ print 'Loaded revisions %d-%d from %s' % (revisions[0],
+ revisions[-1], cache_filename)
+ return (revisions, githash_svn_dict)
+ except (EnvironmentError, ValueError):
+ pass
+ return ([], {})
+
+ def _SaveBucketToCache():
+ """Save the list of revisions and the git-svn mappings to a file.
+ The list of revisions is assumed to be sorted."""
+ if self.use_local_cache:
+ cache[cache_dict_key] = revlist_all
+ cache['githash_svn_dict'] = self.githash_svn_dict
+ try:
+ with open(cache_filename, 'w') as cache_file:
+ json.dump(cache, cache_file)
+ print 'Saved revisions %d-%d to %s' % (
+ revlist_all[0], revlist_all[-1], cache_filename)
+ except EnvironmentError:
+ pass
+
+ # Download the revlist and filter for just the range between good and bad.
+ minrev = min(self.good_revision, self.bad_revision)
+ maxrev = max(self.good_revision, self.bad_revision)
+
+ (revlist_all, self.githash_svn_dict) = _LoadBucketFromCache()
+ last_known_rev = revlist_all[-1] if revlist_all else 0
+ if last_known_rev < maxrev:
+ revlist_all.extend(map(int, self.ParseDirectoryIndex(last_known_rev)))
+ revlist_all = list(set(revlist_all))
+ revlist_all.sort()
+ _SaveBucketToCache()
+
+ revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
+
+ # Set good and bad revisions to be legit revisions.
+ if revlist:
+ if self.good_revision < self.bad_revision:
+ self.good_revision = revlist[0]
+ self.bad_revision = revlist[-1]
+ else:
+ self.bad_revision = revlist[0]
+ self.good_revision = revlist[-1]
+
+ # Fix chromium rev so that the deps blink revision matches REVISIONS file.
+ if self.base_url == WEBKIT_BASE_URL:
+ revlist_all.sort()
+ self.good_revision = FixChromiumRevForBlink(revlist,
+ revlist_all,
+ self,
+ self.good_revision)
+ self.bad_revision = FixChromiumRevForBlink(revlist,
+ revlist_all,
+ self,
+ self.bad_revision)
+ return revlist
+
+ def GetOfficialBuildsList(self):
+ """Gets the list of official build numbers between self.good_revision and
+ self.bad_revision."""
+
+ def CheckDepotToolsInPath():
+ delimiter = ';' if sys.platform.startswith('win') else ':'
+ path_list = os.environ['PATH'].split(delimiter)
+ for path in path_list:
+ if path.rstrip(os.path.sep).endswith('depot_tools'):
+ return path
+ return None
+
+ def RunGsutilCommand(args):
+ gsutil_path = CheckDepotToolsInPath()
+ if gsutil_path is None:
+ print ('Follow the instructions in this document '
+ 'http://dev.chromium.org/developers/how-tos/install-depot-tools'
+ ' to install depot_tools and then try again.')
+ sys.exit(1)
+ gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil')
+ gsutil = subprocess.Popen([sys.executable, gsutil_path] + args,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ env=None)
+ stdout, stderr = gsutil.communicate()
+ if gsutil.returncode:
+ if (re.findall(r'status[ |=]40[1|3]', stderr) or
+ stderr.startswith(CREDENTIAL_ERROR_MESSAGE)):
+ print ('Follow these steps to configure your credentials and try'
+ ' running the bisect-builds.py again.:\n'
+ ' 1. Run "python %s config" and follow its instructions.\n'
+ ' 2. If you have a @google.com account, use that account.\n'
+ ' 3. For the project-id, just enter 0.' % gsutil_path)
+ sys.exit(1)
+ else:
+ raise Exception('Error running the gsutil command: %s' % stderr)
+ return stdout
+
+ def GsutilList(bucket):
+ query = 'gs://%s/' % bucket
+ stdout = RunGsutilCommand(['ls', query])
+ return [url[len(query):].strip('/') for url in stdout.splitlines()]
+
+ # Download the revlist and filter for just the range between good and bad.
+ minrev = min(self.good_revision, self.bad_revision)
+ maxrev = max(self.good_revision, self.bad_revision)
+ build_numbers = GsutilList(GS_BUCKET_NAME)
+ revision_re = re.compile(r'(\d\d\.\d\.\d{4}\.\d+)')
+ build_numbers = filter(lambda b: revision_re.search(b), build_numbers)
+ final_list = []
+ parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
+ connection = httplib.HTTPConnection(GOOGLE_APIS_URL)
+ for build_number in sorted(parsed_build_numbers):
+ if build_number > maxrev:
+ break
+ if build_number < minrev:
+ continue
+ path = ('/' + GS_BUCKET_NAME + '/' + str(build_number) + '/' +
+ self._listing_platform_dir + self.archive_name)
+ connection.request('HEAD', path)
+ response = connection.getresponse()
+ if response.status == 200:
+ final_list.append(str(build_number))
+ response.read()
+ connection.close()
+ return final_list
+
+def UnzipFilenameToDir(filename, directory):
+ """Unzip |filename| to |directory|."""
+ cwd = os.getcwd()
+ if not os.path.isabs(filename):
+ filename = os.path.join(cwd, filename)
+ zf = zipfile.ZipFile(filename)
+ # Make base.
+ if not os.path.isdir(directory):
+ os.mkdir(directory)
+ os.chdir(directory)
+ # Extract files.
+ for info in zf.infolist():
+ name = info.filename
+ if name.endswith('/'): # dir
+ if not os.path.isdir(name):
+ os.makedirs(name)
+ else: # file
+ directory = os.path.dirname(name)
+ if not os.path.isdir(directory):
+ os.makedirs(directory)
+ out = open(name, 'wb')
+ out.write(zf.read(name))
+ out.close()
+ # Set permissions. Permission info in external_attr is shifted 16 bits.
+ os.chmod(name, info.external_attr >> 16L)
+ os.chdir(cwd)
+
+
+def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
+ """Downloads and unzips revision |rev|.
+ @param context A PathContext instance.
+ @param rev The Chromium revision number/tag to download.
+ @param filename The destination for the downloaded file.
+ @param quit_event A threading.Event which will be set by the master thread to
+ indicate that the download should be aborted.
+ @param progress_event A threading.Event which will be set by the master thread
+ to indicate that the progress of the download should be
+ displayed.
+ """
+ def ReportHook(blocknum, blocksize, totalsize):
+ if quit_event and quit_event.isSet():
+ raise RuntimeError('Aborting download of revision %s' % str(rev))
+ if progress_event and progress_event.isSet():
+ size = blocknum * blocksize
+ if totalsize == -1: # Total size not known.
+ progress = 'Received %d bytes' % size
+ else:
+ size = min(totalsize, size)
+ progress = 'Received %d of %d bytes, %.2f%%' % (
+ size, totalsize, 100.0 * size / totalsize)
+ # Send a \r to let all progress messages use just one line of output.
+ sys.stdout.write('\r' + progress)
+ sys.stdout.flush()
+ download_url = context.GetDownloadURL(rev)
+ try:
+ urllib.urlretrieve(download_url, filename, ReportHook)
+ if progress_event and progress_event.isSet():
+ print
+
+ except RuntimeError:
+ pass
+
+
+def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
+ """Given a zipped revision, unzip it and run the test."""
+ print 'Trying revision %s...' % str(revision)
+
+ # Create a temp directory and unzip the revision into it.
+ cwd = os.getcwd()
+ tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
+ UnzipFilenameToDir(zip_file, tempdir)
+
+ # Hack: Chrome OS archives are missing icudtl.dat; try to copy it from
+ # the local directory.
+ if context.platform == 'chromeos':
+ icudtl_path = 'third_party/icu/source/data/in/icudtl.dat'
+ if not os.access(icudtl_path, os.F_OK):
+ print 'Couldn\'t find: ' + icudtl_path
+ sys.exit()
+ os.system('cp %s %s/chrome-linux/' % (icudtl_path, tempdir))
+
+ os.chdir(tempdir)
+
+ # Run the build as many times as specified.
+ testargs = ['--user-data-dir=%s' % profile] + args
+ # The sandbox must be run as root on Official Chrome, so bypass it.
+ if ((context.is_official or context.flash_path) and
+ context.platform.startswith('linux')):
+ testargs.append('--no-sandbox')
+ if context.flash_path:
+ testargs.append('--ppapi-flash-path=%s' % context.flash_path)
+ # We have to pass a large enough Flash version, which currently needs not
+ # be correct. Instead of requiring the user of the script to figure out and
+ # pass the correct version we just spoof it.
+ testargs.append('--ppapi-flash-version=99.9.999.999')
+
+ runcommand = []
+ for token in shlex.split(command):
+ if token == '%a':
+ runcommand.extend(testargs)
+ else:
+ runcommand.append(
+ token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))).
+ replace('%s', ' '.join(testargs)))
+
+ results = []
+ for _ in range(num_runs):
+ subproc = subprocess.Popen(runcommand,
+ bufsize=-1,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ (stdout, stderr) = subproc.communicate()
+ results.append((subproc.returncode, stdout, stderr))
+ os.chdir(cwd)
+ try:
+ shutil.rmtree(tempdir, True)
+ except Exception:
+ pass
+
+ for (returncode, stdout, stderr) in results:
+ if returncode:
+ return (returncode, stdout, stderr)
+ return results[0]
+
+
+# The arguments official_builds, status, stdout and stderr are unused.
+# They are present here because this function is passed to Bisect which then
+# calls it with 5 arguments.
+# pylint: disable=W0613
+def AskIsGoodBuild(rev, official_builds, exit_status, stdout, stderr):
+ """Asks the user whether build |rev| is good or bad."""
+ # Loop until we get a response that we can parse.
+ while True:
+ response = raw_input('Revision %s is '
+ '[(g)ood/(b)ad/(r)etry/(u)nknown/(s)tdout/(q)uit]: ' %
+ str(rev))
+ if response in ('g', 'b', 'r', 'u'):
+ return response
+ if response == 'q':
+ raise SystemExit()
+ if response == 's':
+ print stdout
+ print stderr
+
+
+def IsGoodASANBuild(rev, official_builds, exit_status, stdout, stderr):
+ """Determine if an ASAN build |rev| is good or bad
+
+ Will examine stderr looking for the error message emitted by ASAN. If not
+ found then will fallback to asking the user."""
+ if stderr:
+ bad_count = 0
+ for line in stderr.splitlines():
+ print line
+ if line.find('ERROR: AddressSanitizer:') != -1:
+ bad_count += 1
+ if bad_count > 0:
+ print 'Revision %d determined to be bad.' % rev
+ return 'b'
+ return AskIsGoodBuild(rev, official_builds, exit_status, stdout, stderr)
+
+
+def DidCommandSucceed(rev, official_builds, exit_status, stdout, stderr):
+ if exit_status:
+ print 'Bad revision: %s' % rev
+ return 'b'
+ else:
+ print 'Good revision: %s' % rev
+ return 'g'
+
+
+class DownloadJob(object):
+ """DownloadJob represents a task to download a given Chromium revision."""
+
+ def __init__(self, context, name, rev, zip_file):
+ super(DownloadJob, self).__init__()
+ # Store off the input parameters.
+ self.context = context
+ self.name = name
+ self.rev = rev
+ self.zip_file = zip_file
+ self.quit_event = threading.Event()
+ self.progress_event = threading.Event()
+ self.thread = None
+
+ def Start(self):
+ """Starts the download."""
+ fetchargs = (self.context,
+ self.rev,
+ self.zip_file,
+ self.quit_event,
+ self.progress_event)
+ self.thread = threading.Thread(target=FetchRevision,
+ name=self.name,
+ args=fetchargs)
+ self.thread.start()
+
+ def Stop(self):
+ """Stops the download which must have been started previously."""
+ assert self.thread, 'DownloadJob must be started before Stop is called.'
+ self.quit_event.set()
+ self.thread.join()
+ os.unlink(self.zip_file)
+
+ def WaitFor(self):
+ """Prints a message and waits for the download to complete. The download
+ must have been started previously."""
+ assert self.thread, 'DownloadJob must be started before WaitFor is called.'
+ print 'Downloading revision %s...' % str(self.rev)
+ self.progress_event.set() # Display progress of download.
+ try:
+ while self.thread.isAlive():
+ # The parameter to join is needed to keep the main thread responsive to
+ # signals. Without it, the program will not respond to interruptions.
+ self.thread.join(1)
+ except (KeyboardInterrupt, SystemExit):
+ self.Stop()
+ raise
+
+
+def VerifyEndpoint(fetch, context, rev, profile, num_runs, command, try_args,
+ evaluate, expected_answer):
+ fetch.WaitFor()
+ try:
+ (exit_status, stdout, stderr) = RunRevision(
+ context, rev, fetch.zip_file, profile, num_runs, command, try_args)
+ except Exception, e:
+ print >> sys.stderr, e
+ if (evaluate(rev, context.is_official, exit_status, stdout, stderr) !=
+ expected_answer):
+ print 'Unexpected result at a range boundary! Your range is not correct.'
+ raise SystemExit
+
+
+def Bisect(context,
+ num_runs=1,
+ command='%p %a',
+ try_args=(),
+ profile=None,
+ evaluate=AskIsGoodBuild,
+ verify_range=False):
+ """Given known good and known bad revisions, run a binary search on all
+ archived revisions to determine the last known good revision.
+
+ @param context PathContext object initialized with user provided parameters.
+ @param num_runs Number of times to run each build for asking good/bad.
+ @param try_args A tuple of arguments to pass to the test application.
+ @param profile The name of the user profile to run with.
+ @param evaluate A function which returns 'g' if the argument build is good,
+ 'b' if it's bad or 'u' if unknown.
+ @param verify_range If true, tests the first and last revisions in the range
+ before proceeding with the bisect.
+
+ Threading is used to fetch Chromium revisions in the background, speeding up
+ the user's experience. For example, suppose the bounds of the search are
+ good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
+ whether revision 50 is good or bad, the next revision to check will be either
+ 25 or 75. So, while revision 50 is being checked, the script will download
+ revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
+ known:
+
+ - If rev 50 is good, the download of rev 25 is cancelled, and the next test
+ is run on rev 75.
+
+ - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
+ is run on rev 25.
+ """
+
+ if not profile:
+ profile = 'profile'
+
+ good_rev = context.good_revision
+ bad_rev = context.bad_revision
+ cwd = os.getcwd()
+
+ print 'Downloading list of known revisions...',
+ if not context.use_local_cache and not context.is_official:
+ print '(use --use-local-cache to cache and re-use the list of revisions)'
+ else:
+ print
+ _GetDownloadPath = lambda rev: os.path.join(cwd,
+ '%s-%s' % (str(rev), context.archive_name))
+ if context.is_official:
+ revlist = context.GetOfficialBuildsList()
+ else:
+ revlist = context.GetRevList()
+
+ # Get a list of revisions to bisect across.
+ if len(revlist) < 2: # Don't have enough builds to bisect.
+ msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
+ raise RuntimeError(msg)
+
+ # Figure out our bookends and first pivot point; fetch the pivot revision.
+ minrev = 0
+ maxrev = len(revlist) - 1
+ pivot = maxrev / 2
+ rev = revlist[pivot]
+ fetch = DownloadJob(context, 'initial_fetch', rev, _GetDownloadPath(rev))
+ fetch.Start()
+
+ if verify_range:
+ minrev_fetch = DownloadJob(
+ context, 'minrev_fetch', revlist[minrev],
+ _GetDownloadPath(revlist[minrev]))
+ maxrev_fetch = DownloadJob(
+ context, 'maxrev_fetch', revlist[maxrev],
+ _GetDownloadPath(revlist[maxrev]))
+ minrev_fetch.Start()
+ maxrev_fetch.Start()
+ try:
+ VerifyEndpoint(minrev_fetch, context, revlist[minrev], profile, num_runs,
+ command, try_args, evaluate, 'b' if bad_rev < good_rev else 'g')
+ VerifyEndpoint(maxrev_fetch, context, revlist[maxrev], profile, num_runs,
+ command, try_args, evaluate, 'g' if bad_rev < good_rev else 'b')
+ except (KeyboardInterrupt, SystemExit):
+ print 'Cleaning up...'
+ fetch.Stop()
+ sys.exit(0)
+ finally:
+ minrev_fetch.Stop()
+ maxrev_fetch.Stop()
+
+ fetch.WaitFor()
+
+ # Binary search time!
+ while fetch and fetch.zip_file and maxrev - minrev > 1:
+ if bad_rev < good_rev:
+ min_str, max_str = 'bad', 'good'
+ else:
+ min_str, max_str = 'good', 'bad'
+ print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str,
+ revlist[maxrev], max_str)
+
+ # Pre-fetch next two possible pivots
+ # - down_pivot is the next revision to check if the current revision turns
+ # out to be bad.
+ # - up_pivot is the next revision to check if the current revision turns
+ # out to be good.
+ down_pivot = int((pivot - minrev) / 2) + minrev
+ down_fetch = None
+ if down_pivot != pivot and down_pivot != minrev:
+ down_rev = revlist[down_pivot]
+ down_fetch = DownloadJob(context, 'down_fetch', down_rev,
+ _GetDownloadPath(down_rev))
+ down_fetch.Start()
+
+ up_pivot = int((maxrev - pivot) / 2) + pivot
+ up_fetch = None
+ if up_pivot != pivot and up_pivot != maxrev:
+ up_rev = revlist[up_pivot]
+ up_fetch = DownloadJob(context, 'up_fetch', up_rev,
+ _GetDownloadPath(up_rev))
+ up_fetch.Start()
+
+ # Run test on the pivot revision.
+ exit_status = None
+ stdout = None
+ stderr = None
+ try:
+ (exit_status, stdout, stderr) = RunRevision(
+ context, rev, fetch.zip_file, profile, num_runs, command, try_args)
+ except Exception, e:
+ print >> sys.stderr, e
+
+ # Call the evaluate function to see if the current revision is good or bad.
+ # On that basis, kill one of the background downloads and complete the
+ # other, as described in the comments above.
+ try:
+ answer = evaluate(rev, context.is_official, exit_status, stdout, stderr)
+ if ((answer == 'g' and good_rev < bad_rev)
+ or (answer == 'b' and bad_rev < good_rev)):
+ fetch.Stop()
+ minrev = pivot
+ if down_fetch:
+ down_fetch.Stop() # Kill the download of the older revision.
+ fetch = None
+ if up_fetch:
+ up_fetch.WaitFor()
+ pivot = up_pivot
+ fetch = up_fetch
+ elif ((answer == 'b' and good_rev < bad_rev)
+ or (answer == 'g' and bad_rev < good_rev)):
+ fetch.Stop()
+ maxrev = pivot
+ if up_fetch:
+ up_fetch.Stop() # Kill the download of the newer revision.
+ fetch = None
+ if down_fetch:
+ down_fetch.WaitFor()
+ pivot = down_pivot
+ fetch = down_fetch
+ elif answer == 'r':
+ pass # Retry requires no changes.
+ elif answer == 'u':
+ # Nuke the revision from the revlist and choose a new pivot.
+ fetch.Stop()
+ revlist.pop(pivot)
+ maxrev -= 1 # Assumes maxrev >= pivot.
+
+ if maxrev - minrev > 1:
+ # Alternate between using down_pivot or up_pivot for the new pivot
+ # point, without affecting the range. Do this instead of setting the
+ # pivot to the midpoint of the new range because adjacent revisions
+ # are likely affected by the same issue that caused the (u)nknown
+ # response.
+ if up_fetch and down_fetch:
+ fetch = [up_fetch, down_fetch][len(revlist) % 2]
+ elif up_fetch:
+ fetch = up_fetch
+ else:
+ fetch = down_fetch
+ fetch.WaitFor()
+ if fetch == up_fetch:
+ pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
+ else:
+ pivot = down_pivot
+
+ if down_fetch and fetch != down_fetch:
+ down_fetch.Stop()
+ if up_fetch and fetch != up_fetch:
+ up_fetch.Stop()
+ else:
+ assert False, 'Unexpected return value from evaluate(): ' + answer
+ except (KeyboardInterrupt, SystemExit):
+ print 'Cleaning up...'
+ for f in [_GetDownloadPath(rev),
+ _GetDownloadPath(revlist[down_pivot]),
+ _GetDownloadPath(revlist[up_pivot])]:
+ try:
+ os.unlink(f)
+ except OSError:
+ pass
+ sys.exit(0)
+
+ rev = revlist[pivot]
+
+ return (revlist[minrev], revlist[maxrev], context)
+
+
+def GetBlinkDEPSRevisionForChromiumRevision(self, rev):
+ """Returns the blink revision that was in REVISIONS file at
+ chromium revision |rev|."""
+
+ def _GetBlinkRev(url, blink_re):
+ m = blink_re.search(url.read())
+ url.close()
+ if m:
+ return m.group(1)
+
+ url = urllib.urlopen(DEPS_FILE_OLD % rev)
+ if url.getcode() == 200:
+ # . doesn't match newlines without re.DOTALL, so this is safe.
+ blink_re = re.compile(r'webkit_revision\D*(\d+)')
+ return int(_GetBlinkRev(url, blink_re))
+ else:
+ url = urllib.urlopen(DEPS_FILE_NEW % GetGitHashFromSVNRevision(rev))
+ if url.getcode() == 200:
+ blink_re = re.compile(r'webkit_revision\D*\d+;\D*\d+;(\w+)')
+ blink_git_sha = _GetBlinkRev(url, blink_re)
+ return self.GetSVNRevisionFromGitHash(blink_git_sha, 'blink')
+ raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
+
+
+def GetBlinkRevisionForChromiumRevision(context, rev):
+ """Returns the blink revision that was in REVISIONS file at
+ chromium revision |rev|."""
+ def _IsRevisionNumber(revision):
+ if isinstance(revision, int):
+ return True
+ else:
+ return revision.isdigit()
+ if str(rev) in context.githash_svn_dict:
+ rev = context.githash_svn_dict[str(rev)]
+ file_url = '%s/%s%s/REVISIONS' % (context.base_url,
+ context._listing_platform_dir, rev)
+ url = urllib.urlopen(file_url)
+ if url.getcode() == 200:
+ try:
+ data = json.loads(url.read())
+ except ValueError:
+ print 'ValueError for JSON URL: %s' % file_url
+ raise ValueError
+ else:
+ raise ValueError
+ url.close()
+ if 'webkit_revision' in data:
+ blink_rev = data['webkit_revision']
+ if not _IsRevisionNumber(blink_rev):
+ blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
+ return blink_rev
+ else:
+ raise Exception('Could not get blink revision for cr rev %d' % rev)
+
+
+def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
+ """Returns the chromium revision that has the correct blink revision
+ for blink bisect, DEPS and REVISIONS file might not match since
+ blink snapshots point to tip of tree blink.
+ Note: The revisions_final variable might get modified to include
+ additional revisions."""
+ blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(self, rev)
+
+ while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
+ idx = revisions.index(rev)
+ if idx > 0:
+ rev = revisions[idx-1]
+ if rev not in revisions_final:
+ revisions_final.insert(0, rev)
+
+ revisions_final.sort()
+ return rev
+
+
+def GetChromiumRevision(context, url):
+ """Returns the chromium revision read from given URL."""
+ try:
+ # Location of the latest build revision number
+ latest_revision = urllib.urlopen(url).read()
+ if latest_revision.isdigit():
+ return int(latest_revision)
+ return context.GetSVNRevisionFromGitHash(latest_revision)
+ except Exception:
+ print 'Could not determine latest revision. This could be bad...'
+ return 999999999
+
+def GetGitHashFromSVNRevision(svn_revision):
+ crrev_url = CRREV_URL + str(svn_revision)
+ url = urllib.urlopen(crrev_url)
+ if url.getcode() == 200:
+ data = json.loads(url.read())
+ if 'git_sha' in data:
+ return data['git_sha']
+
+def PrintChangeLog(min_chromium_rev, max_chromium_rev):
+ """Prints the changelog URL."""
+
+ print (' ' + CHANGELOG_URL % (GetGitHashFromSVNRevision(min_chromium_rev),
+ GetGitHashFromSVNRevision(max_chromium_rev)))
+
+
+def main():
+ usage = ('%prog [options] [-- chromium-options]\n'
+ 'Perform binary search on the snapshot builds to find a minimal\n'
+ 'range of revisions where a behavior change happened. The\n'
+ 'behaviors are described as "good" and "bad".\n'
+ 'It is NOT assumed that the behavior of the later revision is\n'
+ 'the bad one.\n'
+ '\n'
+ 'Revision numbers should use\n'
+ ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
+ ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
+ ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
+ ' for earlier revs.\n'
+ ' Chrome\'s about: build number and omahaproxy branch_revision\n'
+ ' are incorrect, they are from branches.\n'
+ '\n'
+ 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
+ parser = optparse.OptionParser(usage=usage)
+ # Strangely, the default help output doesn't include the choice list.
+ choices = ['mac', 'mac64', 'win', 'win64', 'linux', 'linux64', 'linux-arm',
+ 'chromeos']
+ parser.add_option('-a', '--archive',
+ choices=choices,
+ help='The buildbot archive to bisect [%s].' %
+ '|'.join(choices))
+ parser.add_option('-o',
+ action='store_true',
+ dest='official_builds',
+ help='Bisect across official Chrome builds (internal '
+ 'only) instead of Chromium archives.')
+ parser.add_option('-b', '--bad',
+ type='str',
+ help='A bad revision to start bisection. '
+ 'May be earlier or later than the good revision. '
+ 'Default is HEAD.')
+ parser.add_option('-f', '--flash_path',
+ type='str',
+ help='Absolute path to a recent Adobe Pepper Flash '
+ 'binary to be used in this bisection (e.g. '
+ 'on Windows C:\...\pepflashplayer.dll and on Linux '
+ '/opt/google/chrome/PepperFlash/'
+ 'libpepflashplayer.so).')
+ parser.add_option('-g', '--good',
+ type='str',
+ help='A good revision to start bisection. ' +
+ 'May be earlier or later than the bad revision. ' +
+ 'Default is 0.')
+ parser.add_option('-p', '--profile', '--user-data-dir',
+ type='str',
+ default='profile',
+ help='Profile to use; this will not reset every run. '
+ 'Defaults to a clean profile.')
+ parser.add_option('-t', '--times',
+ type='int',
+ default=1,
+ help='Number of times to run each build before asking '
+ 'if it\'s good or bad. Temporary profiles are reused.')
+ parser.add_option('-c', '--command',
+ type='str',
+ default='%p %a',
+ help='Command to execute. %p and %a refer to Chrome '
+ 'executable and specified extra arguments '
+ 'respectively. Use %s to specify all extra arguments '
+ 'as one string. Defaults to "%p %a". Note that any '
+ 'extra paths specified should be absolute.')
+ parser.add_option('-l', '--blink',
+ action='store_true',
+ help='Use Blink bisect instead of Chromium. ')
+ parser.add_option('', '--not-interactive',
+ action='store_true',
+ default=False,
+ help='Use command exit code to tell good/bad revision.')
+ parser.add_option('--asan',
+ dest='asan',
+ action='store_true',
+ default=False,
+ help='Allow the script to bisect ASAN builds')
+ parser.add_option('--use-local-cache',
+ dest='use_local_cache',
+ action='store_true',
+ default=False,
+ help='Use a local file in the current directory to cache '
+ 'a list of known revisions to speed up the '
+ 'initialization of this script.')
+ parser.add_option('--verify-range',
+ dest='verify_range',
+ action='store_true',
+ default=False,
+ help='Test the first and last revisions in the range ' +
+ 'before proceeding with the bisect.')
+
+ (opts, args) = parser.parse_args()
+
+ if opts.archive is None:
+ print 'Error: missing required parameter: --archive'
+ print
+ parser.print_help()
+ return 1
+
+ if opts.asan:
+ supported_platforms = ['linux', 'mac', 'win']
+ if opts.archive not in supported_platforms:
+ print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
+ '|'.join(supported_platforms))
+ return 1
+ if opts.official_builds:
+ print 'Error: Do not yet support bisecting official ASAN builds.'
+ return 1
+
+ if opts.asan:
+ base_url = ASAN_BASE_URL
+ elif opts.blink:
+ base_url = WEBKIT_BASE_URL
+ else:
+ base_url = CHROMIUM_BASE_URL
+
+ # Create the context. Initialize 0 for the revisions as they are set below.
+ context = PathContext(base_url, opts.archive, opts.good, opts.bad,
+ opts.official_builds, opts.asan, opts.use_local_cache,
+ opts.flash_path)
+
+ # Pick a starting point, try to get HEAD for this.
+ if not opts.bad:
+ context.bad_revision = '999.0.0.0'
+ context.bad_revision = GetChromiumRevision(
+ context, context.GetLastChangeURL())
+
+ # Find out when we were good.
+ if not opts.good:
+ context.good_revision = '0.0.0.0' if opts.official_builds else 0
+
+ if opts.flash_path:
+ msg = 'Could not find Flash binary at %s' % opts.flash_path
+ assert os.path.exists(opts.flash_path), msg
+
+ if opts.official_builds:
+ context.good_revision = LooseVersion(context.good_revision)
+ context.bad_revision = LooseVersion(context.bad_revision)
+ else:
+ context.good_revision = int(context.good_revision)
+ context.bad_revision = int(context.bad_revision)
+
+ if opts.times < 1:
+ print('Number of times to run (%d) must be greater than or equal to 1.' %
+ opts.times)
+ parser.print_help()
+ return 1
+
+ if opts.not_interactive:
+ evaluator = DidCommandSucceed
+ elif opts.asan:
+ evaluator = IsGoodASANBuild
+ else:
+ evaluator = AskIsGoodBuild
+
+ # Save these revision numbers to compare when showing the changelog URL
+ # after the bisect.
+ good_rev = context.good_revision
+ bad_rev = context.bad_revision
+
+ (min_chromium_rev, max_chromium_rev, context) = Bisect(
+ context, opts.times, opts.command, args, opts.profile,
+ evaluator, opts.verify_range)
+
+ # Get corresponding blink revisions.
+ try:
+ min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
+ min_chromium_rev)
+ max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
+ max_chromium_rev)
+ except Exception:
+ # Silently ignore the failure.
+ min_blink_rev, max_blink_rev = 0, 0
+
+ if opts.blink:
+ # We're done. Let the user know the results in an official manner.
+ if good_rev > bad_rev:
+ print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
+ else:
+ print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
+
+ print 'BLINK CHANGELOG URL:'
+ print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
+
+ else:
+ # We're done. Let the user know the results in an official manner.
+ if good_rev > bad_rev:
+ print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
+ str(max_chromium_rev))
+ else:
+ print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
+ str(max_chromium_rev))
+ if min_blink_rev != max_blink_rev:
+ print ('NOTE: There is a Blink roll in the range, '
+ 'you might also want to do a Blink bisect.')
+
+ print 'CHANGELOG URL:'
+ if opts.official_builds:
+ print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
+ else:
+ PrintChangeLog(min_chromium_rev, max_chromium_rev)
+
+
+if __name__ == '__main__':
+ sys.exit(main())