1#!/usr/bin/env python 2# Copyright (c) 2012 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Snapshot Build Bisect Tool 7 8This script bisects a snapshot archive using binary search. It starts at 9a bad revision (it will try to guess HEAD) and asks for a last known-good 10revision. It will then binary search across this revision range by downloading, 11unzipping, and opening Chromium for you. After testing the specific revision, 12it will ask you whether it is good or bad before continuing the search. 13""" 14 15from __future__ import print_function 16 17# The base URL for stored build archives. 18CHROMIUM_BASE_URL = ('http://commondatastorage.googleapis.com' 19 '/chromium-browser-snapshots') 20WEBKIT_BASE_URL = ('http://commondatastorage.googleapis.com' 21 '/chromium-webkit-snapshots') 22ASAN_BASE_URL = ('http://commondatastorage.googleapis.com' 23 '/chromium-browser-asan') 24 25# URL template for viewing changelogs between revisions. 26CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s') 27 28# URL to convert SVN revision to git hash. 29CRREV_URL = ('https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/') 30 31# DEPS file URL. 32DEPS_FILE = ('https://chromium.googlesource.com/chromium/src/+/%s/DEPS') 33 34# Blink changelogs URL. 35BLINK_CHANGELOG_URL = ('http://build.chromium.org' 36 '/f/chromium/perf/dashboard/ui/changelog_blink.html' 37 '?url=/trunk&range=%d%%3A%d') 38 39DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s (' 40 'known good), but no later than %s (first known bad).') 41DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s (' 42 'known bad), but no later than %s (first known good).') 43 44CHROMIUM_GITHASH_TO_SVN_URL = ( 45 'https://chromium.googlesource.com/chromium/src/+/%s?format=json') 46 47BLINK_GITHASH_TO_SVN_URL = ( 48 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json') 49 50GITHASH_TO_SVN_URL = { 51 'chromium': CHROMIUM_GITHASH_TO_SVN_URL, 52 'blink': BLINK_GITHASH_TO_SVN_URL, 53} 54 55VERSION_HISTORY_URL = ('https://versionhistory.googleapis.com/v1/chrome' 56 '/platforms/win/channels/stable/versions/all/releases') 57 58OMAHA_REVISIONS_URL = ('https://omahaproxy.appspot.com/deps.json?version=%s') 59 60# Search pattern to be matched in the JSON output from 61# CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision). 62CHROMIUM_SEARCH_PATTERN_OLD = ( 63 r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ') 64CHROMIUM_SEARCH_PATTERN = ( 65 r'Cr-Commit-Position: refs/heads/master@{#(\d+)}') 66 67# Search pattern to be matched in the json output from 68# BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision). 69BLINK_SEARCH_PATTERN = ( 70 r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ') 71 72SEARCH_PATTERN = { 73 'chromium': CHROMIUM_SEARCH_PATTERN, 74 'blink': BLINK_SEARCH_PATTERN, 75} 76 77CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with ' 78 'no configured credentials') 79 80############################################################################### 81 82import glob 83import json 84import optparse 85import os 86import re 87import shlex 88import shutil 89import subprocess 90import sys 91import tempfile 92import threading 93from distutils.version import LooseVersion 94from xml.etree import ElementTree 95import zipfile 96 97if sys.version_info[0] == 3: 98 import urllib.request as urllib 99else: 100 import urllib 101 102 103class PathContext(object): 104 """A PathContext is used to carry the information used to construct URLs and 105 paths when dealing with the storage server and archives.""" 106 def __init__(self, base_url, platform, good_revision, bad_revision, 107 is_asan, use_local_cache, flash_path = None): 108 super(PathContext, self).__init__() 109 # Store off the input parameters. 110 self.base_url = base_url 111 self.platform = platform # What's passed in to the '-a/--archive' option. 112 self.good_revision = good_revision 113 self.bad_revision = bad_revision 114 self.is_asan = is_asan 115 self.build_type = 'release' 116 self.flash_path = flash_path 117 # Dictionary which stores svn revision number as key and it's 118 # corresponding git hash as value. This data is populated in 119 # _FetchAndParse and used later in GetDownloadURL while downloading 120 # the build. 121 self.githash_svn_dict = {} 122 # The name of the ZIP file in a revision directory on the server. 123 self.archive_name = None 124 125 # Whether to cache and use the list of known revisions in a local file to 126 # speed up the initialization of the script at the next run. 127 self.use_local_cache = use_local_cache 128 129 # Locate the local checkout to speed up the script by using locally stored 130 # metadata. 131 abs_file_path = os.path.abspath(os.path.realpath(__file__)) 132 local_src_path = os.path.join(os.path.dirname(abs_file_path), '..') 133 if abs_file_path.endswith(os.path.join('tools', 'bisect-builds.py')) and\ 134 os.path.exists(os.path.join(local_src_path, '.git')): 135 self.local_src_path = os.path.normpath(local_src_path) 136 else: 137 self.local_src_path = None 138 139 # Set some internal members: 140 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. 141 # _archive_extract_dir = Uncompressed directory in the archive_name file. 142 # _binary_name = The name of the executable to run. 143 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'): 144 self._binary_name = 'chrome' 145 elif self.platform in ('mac', 'mac64'): 146 self.archive_name = 'chrome-mac.zip' 147 self._archive_extract_dir = 'chrome-mac' 148 elif self.platform in ('win', 'win64'): 149 # Note: changed at revision 591483; see GetDownloadURL and GetLaunchPath 150 # below where these are patched. 151 self.archive_name = 'chrome-win32.zip' 152 self._archive_extract_dir = 'chrome-win32' 153 self._binary_name = 'chrome.exe' 154 else: 155 raise Exception('Invalid platform: %s' % self.platform) 156 157 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'): 158 # Note: changed at revision 591483; see GetDownloadURL and GetLaunchPath 159 # below where these are patched. 160 self.archive_name = 'chrome-linux.zip' 161 self._archive_extract_dir = 'chrome-linux' 162 if self.platform == 'linux': 163 self._listing_platform_dir = 'Linux/' 164 elif self.platform == 'linux64': 165 self._listing_platform_dir = 'Linux_x64/' 166 elif self.platform == 'linux-arm': 167 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/' 168 elif self.platform == 'chromeos': 169 self._listing_platform_dir = 'Linux_ChromiumOS_Full/' 170 elif self.platform in ('mac', 'mac64'): 171 self._listing_platform_dir = 'Mac/' 172 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' 173 elif self.platform == 'win': 174 self._listing_platform_dir = 'Win/' 175 elif self.platform == 'win64': 176 self._listing_platform_dir = 'Win_x64/' 177 178 def GetASANPlatformDir(self): 179 """ASAN builds are in directories like "linux-release", or have filenames 180 like "asan-win32-release-277079.zip". This aligns to our platform names 181 except in the case of Windows where they use "win32" instead of "win".""" 182 if self.platform == 'win': 183 return 'win32' 184 else: 185 return self.platform 186 187 def GetListingURL(self, marker=None): 188 """Returns the URL for a directory listing, with an optional marker.""" 189 marker_param = '' 190 if marker: 191 marker_param = '&marker=' + str(marker) 192 if self.is_asan: 193 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type) 194 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param 195 else: 196 return (self.base_url + '/?delimiter=/&prefix=' + 197 self._listing_platform_dir + marker_param) 198 199 def GetDownloadURL(self, revision): 200 """Gets the download URL for a build archive of a specific revision.""" 201 if self.is_asan: 202 return '%s/%s-%s/%s-%d.zip' % ( 203 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type, 204 self.GetASANBaseName(), revision) 205 if str(revision) in self.githash_svn_dict: 206 revision = self.githash_svn_dict[str(revision)] 207 archive_name = self.archive_name 208 209 # At revision 591483, the names of two of the archives changed 210 # due to: https://chromium-review.googlesource.com/#/q/1226086 211 # See: http://crbug.com/789612 212 if revision >= 591483: 213 if self.platform == 'chromeos': 214 archive_name = 'chrome-chromeos.zip' 215 elif self.platform in ('win', 'win64'): 216 archive_name = 'chrome-win.zip' 217 218 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir, 219 revision, archive_name) 220 221 def GetLastChangeURL(self): 222 """Returns a URL to the LAST_CHANGE file.""" 223 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE' 224 225 def GetASANBaseName(self): 226 """Returns the base name of the ASAN zip file.""" 227 if 'linux' in self.platform: 228 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(), 229 self.build_type) 230 else: 231 return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type) 232 233 def GetLaunchPath(self, revision): 234 """Returns a relative path (presumably from the archive extraction location) 235 that is used to run the executable.""" 236 if self.is_asan: 237 extract_dir = '%s-%d' % (self.GetASANBaseName(), revision) 238 else: 239 extract_dir = self._archive_extract_dir 240 241 # At revision 591483, the names of two of the archives changed 242 # due to: https://chromium-review.googlesource.com/#/q/1226086 243 # See: http://crbug.com/789612 244 if revision >= 591483: 245 if self.platform == 'chromeos': 246 extract_dir = 'chrome-chromeos' 247 elif self.platform in ('win', 'win64'): 248 extract_dir = 'chrome-win' 249 250 return os.path.join(extract_dir, self._binary_name) 251 252 def ParseDirectoryIndex(self, last_known_rev): 253 """Parses the Google Storage directory listing into a list of revision 254 numbers.""" 255 256 def _GetMarkerForRev(revision): 257 if self.is_asan: 258 return '%s-%s/%s-%d.zip' % ( 259 self.GetASANPlatformDir(), self.build_type, 260 self.GetASANBaseName(), revision) 261 return '%s%d' % (self._listing_platform_dir, revision) 262 263 def _FetchAndParse(url): 264 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If 265 next-marker is not None, then the listing is a partial listing and another 266 fetch should be performed with next-marker being the marker= GET 267 parameter.""" 268 handle = urllib.urlopen(url) 269 document = ElementTree.parse(handle) 270 271 # All nodes in the tree are namespaced. Get the root's tag name to extract 272 # the namespace. Etree does namespaces as |{namespace}tag|. 273 root_tag = document.getroot().tag 274 end_ns_pos = root_tag.find('}') 275 if end_ns_pos == -1: 276 raise Exception('Could not locate end namespace for directory index') 277 namespace = root_tag[:end_ns_pos + 1] 278 279 # Find the prefix (_listing_platform_dir) and whether or not the list is 280 # truncated. 281 prefix_len = len(document.find(namespace + 'Prefix').text) 282 next_marker = None 283 is_truncated = document.find(namespace + 'IsTruncated') 284 if is_truncated is not None and is_truncated.text.lower() == 'true': 285 next_marker = document.find(namespace + 'NextMarker').text 286 # Get a list of all the revisions. 287 revisions = [] 288 githash_svn_dict = {} 289 if self.is_asan: 290 asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName())) 291 # Non ASAN builds are in a <revision> directory. The ASAN builds are 292 # flat 293 all_prefixes = document.findall(namespace + 'Contents/' + 294 namespace + 'Key') 295 for prefix in all_prefixes: 296 m = asan_regex.match(prefix.text) 297 if m: 298 try: 299 revisions.append(int(m.group(1))) 300 except ValueError: 301 pass 302 else: 303 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + 304 namespace + 'Prefix') 305 # The <Prefix> nodes have content of the form of 306 # |_listing_platform_dir/revision/|. Strip off the platform dir and the 307 # trailing slash to just have a number. 308 for prefix in all_prefixes: 309 revnum = prefix.text[prefix_len:-1] 310 try: 311 revnum = int(revnum) 312 revisions.append(revnum) 313 # Notes: 314 # Ignore hash in chromium-browser-snapshots as they are invalid 315 # Resulting in 404 error in fetching pages: 316 # https://chromium.googlesource.com/chromium/src/+/[rev_hash] 317 except ValueError: 318 pass 319 return (revisions, next_marker, githash_svn_dict) 320 321 # Fetch the first list of revisions. 322 if last_known_rev: 323 revisions = [] 324 # Optimization: Start paging at the last known revision (local cache). 325 next_marker = _GetMarkerForRev(last_known_rev) 326 # Optimization: Stop paging at the last known revision (remote). 327 last_change_rev = GetChromiumRevision(self, self.GetLastChangeURL()) 328 if last_known_rev == last_change_rev: 329 return [] 330 else: 331 (revisions, next_marker, new_dict) = _FetchAndParse(self.GetListingURL()) 332 self.githash_svn_dict.update(new_dict) 333 last_change_rev = None 334 335 # If the result list was truncated, refetch with the next marker. Do this 336 # until an entire directory listing is done. 337 while next_marker: 338 sys.stdout.write('\rFetching revisions at marker %s' % next_marker) 339 sys.stdout.flush() 340 341 next_url = self.GetListingURL(next_marker) 342 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url) 343 revisions.extend(new_revisions) 344 self.githash_svn_dict.update(new_dict) 345 if last_change_rev and last_change_rev in new_revisions: 346 break 347 sys.stdout.write('\r') 348 sys.stdout.flush() 349 return revisions 350 351 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot): 352 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1 353 response = urllib.urlopen(json_url) 354 if response.getcode() == 200: 355 try: 356 data = json.loads(response.read()[4:]) 357 except ValueError: 358 print('ValueError for JSON URL: %s' % json_url) 359 raise ValueError 360 else: 361 raise ValueError 362 if 'message' in data: 363 message = data['message'].split('\n') 364 message = [line for line in message if line.strip()] 365 search_pattern = re.compile(SEARCH_PATTERN[depot]) 366 result = search_pattern.search(message[len(message)-1]) 367 if result: 368 return result.group(1) 369 else: 370 if depot == 'chromium': 371 result = re.search(CHROMIUM_SEARCH_PATTERN_OLD, 372 message[len(message)-1]) 373 if result: 374 return result.group(1) 375 print('Failed to get svn revision number for %s' % git_sha1) 376 raise ValueError 377 378 def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot): 379 def _RunGit(command, path): 380 command = ['git'] + command 381 shell = sys.platform.startswith('win') 382 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE, 383 stderr=subprocess.PIPE, cwd=path) 384 (output, _) = proc.communicate() 385 return (output, proc.returncode) 386 387 path = self.local_src_path 388 if depot == 'blink': 389 path = os.path.join(self.local_src_path, 'third_party', 'WebKit') 390 revision = None 391 try: 392 command = ['svn', 'find-rev', git_sha1] 393 (git_output, return_code) = _RunGit(command, path) 394 if not return_code: 395 revision = git_output.strip('\n') 396 except ValueError: 397 pass 398 if not revision: 399 command = ['log', '-n1', '--format=%s', git_sha1] 400 (git_output, return_code) = _RunGit(command, path) 401 if not return_code: 402 revision = re.match('SVN changes up to revision ([0-9]+)', git_output) 403 revision = revision.group(1) if revision else None 404 if revision: 405 return revision 406 raise ValueError 407 408 def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'): 409 if not self.local_src_path: 410 return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot) 411 else: 412 return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot) 413 414 def GetRevList(self, archive): 415 """Gets the list of revision numbers between self.good_revision and 416 self.bad_revision.""" 417 418 cache = {} 419 # The cache is stored in the same directory as bisect-builds.py 420 cache_filename = os.path.join( 421 os.path.abspath(os.path.dirname(__file__)), 422 '.bisect-builds-cache.json') 423 cache_dict_key = self.GetListingURL() 424 425 def _LoadBucketFromCache(): 426 if self.use_local_cache: 427 try: 428 with open(cache_filename) as cache_file: 429 for (key, value) in json.load(cache_file).items(): 430 cache[key] = value 431 revisions = cache.get(cache_dict_key, []) 432 githash_svn_dict = cache.get('githash_svn_dict', {}) 433 if revisions: 434 print('Loaded revisions %d-%d from %s' % 435 (revisions[0], revisions[-1], cache_filename)) 436 return (revisions, githash_svn_dict) 437 except (EnvironmentError, ValueError): 438 pass 439 return ([], {}) 440 441 def _SaveBucketToCache(): 442 """Save the list of revisions and the git-svn mappings to a file. 443 The list of revisions is assumed to be sorted.""" 444 if self.use_local_cache: 445 cache[cache_dict_key] = revlist_all 446 cache['githash_svn_dict'] = self.githash_svn_dict 447 try: 448 with open(cache_filename, 'w') as cache_file: 449 json.dump(cache, cache_file) 450 print('Saved revisions %d-%d to %s' % 451 (revlist_all[0], revlist_all[-1], cache_filename)) 452 except EnvironmentError: 453 pass 454 455 # Download the revlist and filter for just the range between good and bad. 456 minrev = min(self.good_revision, self.bad_revision) 457 maxrev = max(self.good_revision, self.bad_revision) 458 459 (revlist_all, self.githash_svn_dict) = _LoadBucketFromCache() 460 last_known_rev = revlist_all[-1] if revlist_all else 0 461 if last_known_rev < maxrev: 462 revlist_all.extend(map(int, self.ParseDirectoryIndex(last_known_rev))) 463 revlist_all = list(set(revlist_all)) 464 revlist_all.sort() 465 _SaveBucketToCache() 466 467 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)] 468 if len(revlist) < 2: # Don't have enough builds to bisect. 469 last_known_rev = revlist_all[-1] if revlist_all else 0 470 first_known_rev = revlist_all[0] if revlist_all else 0 471 # Check for specifying a number before the available range. 472 if maxrev < first_known_rev: 473 msg = ( 474 'First available bisect revision for %s is %d. Be sure to specify revision ' 475 'numbers, not branch numbers.' % (archive, first_known_rev)) 476 raise (RuntimeError(msg)) 477 478 # Check for specifying a number beyond the available range. 479 if maxrev > last_known_rev: 480 # Check for the special case of linux where bisect builds stopped at 481 # revision 382086, around March 2016. 482 if archive == 'linux': 483 msg = 'Last available bisect revision for %s is %d. Try linux64 instead.' % ( 484 archive, last_known_rev) 485 else: 486 msg = 'Last available bisect revision for %s is %d. Try a different good/bad range.' % ( 487 archive, last_known_rev) 488 raise (RuntimeError(msg)) 489 490 # Otherwise give a generic message. 491 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist 492 raise RuntimeError(msg) 493 494 # Set good and bad revisions to be legit revisions. 495 if revlist: 496 if self.good_revision < self.bad_revision: 497 self.good_revision = revlist[0] 498 self.bad_revision = revlist[-1] 499 else: 500 self.bad_revision = revlist[0] 501 self.good_revision = revlist[-1] 502 503 # Fix chromium rev so that the deps blink revision matches REVISIONS file. 504 if self.base_url == WEBKIT_BASE_URL: 505 revlist_all.sort() 506 self.good_revision = FixChromiumRevForBlink(revlist, 507 revlist_all, 508 self, 509 self.good_revision) 510 self.bad_revision = FixChromiumRevForBlink(revlist, 511 revlist_all, 512 self, 513 self.bad_revision) 514 return revlist 515 516 517def IsMac(): 518 return sys.platform.startswith('darwin') 519 520 521def UnzipFilenameToDir(filename, directory): 522 """Unzip |filename| to |directory|.""" 523 cwd = os.getcwd() 524 if not os.path.isabs(filename): 525 filename = os.path.join(cwd, filename) 526 # Make base. 527 if not os.path.isdir(directory): 528 os.mkdir(directory) 529 os.chdir(directory) 530 531 # The Python ZipFile does not support symbolic links, which makes it 532 # unsuitable for Mac builds. so use ditto instead. 533 if IsMac(): 534 unzip_cmd = ['ditto', '-x', '-k', filename, '.'] 535 proc = subprocess.Popen(unzip_cmd, bufsize=0, stdout=subprocess.PIPE, 536 stderr=subprocess.PIPE) 537 proc.communicate() 538 os.chdir(cwd) 539 return 540 541 zf = zipfile.ZipFile(filename) 542 # Extract files. 543 for info in zf.infolist(): 544 name = info.filename 545 if name.endswith('/'): # dir 546 if not os.path.isdir(name): 547 os.makedirs(name) 548 else: # file 549 directory = os.path.dirname(name) 550 if not os.path.isdir(directory): 551 os.makedirs(directory) 552 out = open(name, 'wb') 553 out.write(zf.read(name)) 554 out.close() 555 # Set permissions. Permission info in external_attr is shifted 16 bits. 556 os.chmod(name, info.external_attr >> 16) 557 os.chdir(cwd) 558 559 560def FetchRevision(context, rev, filename, quit_event=None, progress_event=None): 561 """Downloads and unzips revision |rev|. 562 @param context A PathContext instance. 563 @param rev The Chromium revision number/tag to download. 564 @param filename The destination for the downloaded file. 565 @param quit_event A threading.Event which will be set by the master thread to 566 indicate that the download should be aborted. 567 @param progress_event A threading.Event which will be set by the master thread 568 to indicate that the progress of the download should be 569 displayed. 570 """ 571 def ReportHook(blocknum, blocksize, totalsize): 572 if quit_event and quit_event.isSet(): 573 raise RuntimeError('Aborting download of revision %s' % str(rev)) 574 if progress_event and progress_event.isSet(): 575 size = blocknum * blocksize 576 if totalsize == -1: # Total size not known. 577 progress = 'Received %d bytes' % size 578 else: 579 size = min(totalsize, size) 580 progress = 'Received %d of %d bytes, %.2f%%' % ( 581 size, totalsize, 100.0 * size / totalsize) 582 # Send a \r to let all progress messages use just one line of output. 583 sys.stdout.write('\r' + progress) 584 sys.stdout.flush() 585 download_url = context.GetDownloadURL(rev) 586 try: 587 urllib.urlretrieve(download_url, filename, ReportHook) 588 if progress_event and progress_event.isSet(): 589 print() 590 591 except RuntimeError: 592 pass 593 594 595def CopyMissingFileFromCurrentSource(src_glob, dst): 596 """Work around missing files in archives. 597 This happens when archives of Chrome don't contain all of the files 598 needed to build it. In many cases we can work around this using 599 files from the current checkout. The source is in the form of a glob 600 so that it can try to look for possible sources of the file in 601 multiple locations, but we just arbitrarily try the first match. 602 603 Silently fail if this doesn't work because we don't yet have clear 604 markers for builds that require certain files or a way to test 605 whether or not launching Chrome succeeded. 606 """ 607 if not os.path.exists(dst): 608 matches = glob.glob(src_glob) 609 if matches: 610 shutil.copy2(matches[0], dst) 611 612 613def RunRevision(context, revision, zip_file, profile, num_runs, command, args): 614 """Given a zipped revision, unzip it and run the test.""" 615 print('Trying revision %s...' % str(revision)) 616 617 # Create a temp directory and unzip the revision into it. 618 cwd = os.getcwd() 619 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') 620 UnzipFilenameToDir(zip_file, tempdir) 621 622 # Hack: Some Chrome OS archives are missing some files; try to copy them 623 # from the local directory. 624 if context.platform == 'chromeos' and revision < 591483: 625 CopyMissingFileFromCurrentSource('third_party/icu/common/icudtl.dat', 626 '%s/chrome-linux/icudtl.dat' % tempdir) 627 CopyMissingFileFromCurrentSource('*out*/*/libminigbm.so', 628 '%s/chrome-linux/libminigbm.so' % tempdir) 629 630 os.chdir(tempdir) 631 632 # Run the build as many times as specified. 633 testargs = ['--user-data-dir=%s' % profile] + args 634 # The sandbox must be run as root on Official Chrome, so bypass it. 635 if (context.flash_path and context.platform.startswith('linux')): 636 testargs.append('--no-sandbox') 637 if context.flash_path: 638 testargs.append('--ppapi-flash-path=%s' % context.flash_path) 639 # We have to pass a large enough Flash version, which currently needs not 640 # be correct. Instead of requiring the user of the script to figure out and 641 # pass the correct version we just spoof it. 642 testargs.append('--ppapi-flash-version=99.9.999.999') 643 644 runcommand = [] 645 for token in shlex.split(command): 646 if token == '%a': 647 runcommand.extend(testargs) 648 else: 649 runcommand.append( 650 token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))). 651 replace('%s', ' '.join(testargs))) 652 result = None 653 try: 654 for _ in range(num_runs): 655 subproc = subprocess.Popen( 656 runcommand, 657 bufsize=-1, 658 stdout=subprocess.PIPE, 659 stderr=subprocess.PIPE) 660 (stdout, stderr) = subproc.communicate() 661 result = (subproc.returncode, stdout, stderr) 662 if subproc.returncode: 663 break 664 return result 665 finally: 666 os.chdir(cwd) 667 try: 668 shutil.rmtree(tempdir, True) 669 except Exception: 670 pass 671 672 673# The arguments status, stdout and stderr are unused. 674# They are present here because this function is passed to Bisect which then 675# calls it with 5 arguments. 676# pylint: disable=W0613 677def AskIsGoodBuild(rev, exit_status, stdout, stderr): 678 """Asks the user whether build |rev| is good or bad.""" 679 if exit_status: 680 print('Chrome exit_status: %d. Use s to see output' % exit_status) 681 # Loop until we get a response that we can parse. 682 while True: 683 prompt = ('Revision %s is ' 684 '[(g)ood/(b)ad/(r)etry/(u)nknown/(s)tdout/(q)uit]: ' % str(rev)) 685 if sys.version_info[0] == 3: 686 response = input(prompt) 687 else: 688 response = raw_input(prompt) 689 if response in ('g', 'b', 'r', 'u'): 690 return response 691 if response == 'q': 692 raise SystemExit() 693 if response == 's': 694 print(stdout) 695 print(stderr) 696 697 698def IsGoodASANBuild(rev, exit_status, stdout, stderr): 699 """Determine if an ASAN build |rev| is good or bad 700 701 Will examine stderr looking for the error message emitted by ASAN. If not 702 found then will fallback to asking the user.""" 703 if stderr: 704 bad_count = 0 705 for line in stderr.splitlines(): 706 print(line) 707 if line.find('ERROR: AddressSanitizer:') != -1: 708 bad_count += 1 709 if bad_count > 0: 710 print('Revision %d determined to be bad.' % rev) 711 return 'b' 712 return AskIsGoodBuild(rev, exit_status, stdout, stderr) 713 714 715def DidCommandSucceed(rev, exit_status, stdout, stderr): 716 if exit_status: 717 print('Bad revision: %s' % rev) 718 return 'b' 719 else: 720 print('Good revision: %s' % rev) 721 return 'g' 722 723 724class DownloadJob(object): 725 """DownloadJob represents a task to download a given Chromium revision.""" 726 727 def __init__(self, context, name, rev, zip_file): 728 super(DownloadJob, self).__init__() 729 # Store off the input parameters. 730 self.context = context 731 self.name = name 732 self.rev = rev 733 self.zip_file = zip_file 734 self.quit_event = threading.Event() 735 self.progress_event = threading.Event() 736 self.thread = None 737 738 def Start(self): 739 """Starts the download.""" 740 fetchargs = (self.context, 741 self.rev, 742 self.zip_file, 743 self.quit_event, 744 self.progress_event) 745 self.thread = threading.Thread(target=FetchRevision, 746 name=self.name, 747 args=fetchargs) 748 self.thread.start() 749 750 def Stop(self): 751 """Stops the download which must have been started previously.""" 752 assert self.thread, 'DownloadJob must be started before Stop is called.' 753 self.quit_event.set() 754 self.thread.join() 755 os.unlink(self.zip_file) 756 757 def WaitFor(self): 758 """Prints a message and waits for the download to complete. The download 759 must have been started previously.""" 760 assert self.thread, 'DownloadJob must be started before WaitFor is called.' 761 print('Downloading revision %s...' % str(self.rev)) 762 self.progress_event.set() # Display progress of download. 763 try: 764 while self.thread.is_alive(): 765 # The parameter to join is needed to keep the main thread responsive to 766 # signals. Without it, the program will not respond to interruptions. 767 self.thread.join(1) 768 except (KeyboardInterrupt, SystemExit): 769 self.Stop() 770 raise 771 772 773def VerifyEndpoint(fetch, context, rev, profile, num_runs, command, try_args, 774 evaluate, expected_answer): 775 fetch.WaitFor() 776 try: 777 answer = 'r' 778 # This is intended to allow evaluate() to return 'r' to retry RunRevision. 779 while answer == 'r': 780 (exit_status, stdout, stderr) = RunRevision( 781 context, rev, fetch.zip_file, profile, num_runs, command, try_args) 782 answer = evaluate(rev, exit_status, stdout, stderr) 783 except Exception as e: 784 print(e, file=sys.stderr) 785 raise SystemExit 786 if (answer != expected_answer): 787 print('Unexpected result at a range boundary! Your range is not correct.') 788 raise SystemExit 789 790 791def Bisect(context, 792 num_runs=1, 793 command='%p %a', 794 try_args=(), 795 profile=None, 796 evaluate=AskIsGoodBuild, 797 verify_range=False, 798 archive=None): 799 """Given known good and known bad revisions, run a binary search on all 800 archived revisions to determine the last known good revision. 801 802 @param context PathContext object initialized with user provided parameters. 803 @param num_runs Number of times to run each build for asking good/bad. 804 @param try_args A tuple of arguments to pass to the test application. 805 @param profile The name of the user profile to run with. 806 @param evaluate A function which returns 'g' if the argument build is good, 807 'b' if it's bad or 'u' if unknown. 808 @param verify_range If true, tests the first and last revisions in the range 809 before proceeding with the bisect. 810 811 Threading is used to fetch Chromium revisions in the background, speeding up 812 the user's experience. For example, suppose the bounds of the search are 813 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on 814 whether revision 50 is good or bad, the next revision to check will be either 815 25 or 75. So, while revision 50 is being checked, the script will download 816 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is 817 known: 818 819 - If rev 50 is good, the download of rev 25 is cancelled, and the next test 820 is run on rev 75. 821 822 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test 823 is run on rev 25. 824 """ 825 826 if not profile: 827 profile = 'profile' 828 829 good_rev = context.good_revision 830 bad_rev = context.bad_revision 831 cwd = os.getcwd() 832 833 print('Downloading list of known revisions...', end=' ') 834 if not context.use_local_cache: 835 print('(use --use-local-cache to cache and re-use the list of revisions)') 836 else: 837 print() 838 _GetDownloadPath = lambda rev: os.path.join(cwd, 839 '%s-%s' % (str(rev), context.archive_name)) 840 841 # Get a list of revisions to bisect across. 842 revlist = context.GetRevList(archive) 843 844 # Figure out our bookends and first pivot point; fetch the pivot revision. 845 minrev = 0 846 maxrev = len(revlist) - 1 847 pivot = int(maxrev / 2) 848 rev = revlist[pivot] 849 fetch = DownloadJob(context, 'initial_fetch', rev, _GetDownloadPath(rev)) 850 fetch.Start() 851 852 if verify_range: 853 minrev_fetch = DownloadJob( 854 context, 'minrev_fetch', revlist[minrev], 855 _GetDownloadPath(revlist[minrev])) 856 maxrev_fetch = DownloadJob( 857 context, 'maxrev_fetch', revlist[maxrev], 858 _GetDownloadPath(revlist[maxrev])) 859 minrev_fetch.Start() 860 maxrev_fetch.Start() 861 try: 862 VerifyEndpoint(minrev_fetch, context, revlist[minrev], profile, num_runs, 863 command, try_args, evaluate, 'b' if bad_rev < good_rev else 'g') 864 VerifyEndpoint(maxrev_fetch, context, revlist[maxrev], profile, num_runs, 865 command, try_args, evaluate, 'g' if bad_rev < good_rev else 'b') 866 except (KeyboardInterrupt, SystemExit): 867 print('Cleaning up...') 868 fetch.Stop() 869 sys.exit(0) 870 finally: 871 minrev_fetch.Stop() 872 maxrev_fetch.Stop() 873 874 fetch.WaitFor() 875 876 # Binary search time! 877 prefetch_revisions = True 878 while fetch and fetch.zip_file and maxrev - minrev > 1: 879 if bad_rev < good_rev: 880 min_str, max_str = 'bad', 'good' 881 else: 882 min_str, max_str = 'good', 'bad' 883 print( 884 'Bisecting range [%s (%s), %s (%s)], ' 885 'roughly %d steps left.' % (revlist[minrev], min_str, revlist[maxrev], 886 max_str, int(maxrev - minrev).bit_length())) 887 888 # Pre-fetch next two possible pivots 889 # - down_pivot is the next revision to check if the current revision turns 890 # out to be bad. 891 # - up_pivot is the next revision to check if the current revision turns 892 # out to be good. 893 down_pivot = int((pivot - minrev) / 2) + minrev 894 if prefetch_revisions: 895 down_fetch = None 896 if down_pivot != pivot and down_pivot != minrev: 897 down_rev = revlist[down_pivot] 898 down_fetch = DownloadJob(context, 'down_fetch', down_rev, 899 _GetDownloadPath(down_rev)) 900 down_fetch.Start() 901 902 up_pivot = int((maxrev - pivot) / 2) + pivot 903 if prefetch_revisions: 904 up_fetch = None 905 if up_pivot != pivot and up_pivot != maxrev: 906 up_rev = revlist[up_pivot] 907 up_fetch = DownloadJob(context, 'up_fetch', up_rev, 908 _GetDownloadPath(up_rev)) 909 up_fetch.Start() 910 911 # Run test on the pivot revision. 912 exit_status = None 913 stdout = None 914 stderr = None 915 try: 916 (exit_status, stdout, stderr) = RunRevision( 917 context, rev, fetch.zip_file, profile, num_runs, command, try_args) 918 except Exception as e: 919 print(e, file=sys.stderr) 920 921 # Call the evaluate function to see if the current revision is good or bad. 922 # On that basis, kill one of the background downloads and complete the 923 # other, as described in the comments above. 924 try: 925 answer = evaluate(rev, exit_status, stdout, stderr) 926 prefetch_revisions = True 927 if ((answer == 'g' and good_rev < bad_rev) 928 or (answer == 'b' and bad_rev < good_rev)): 929 fetch.Stop() 930 minrev = pivot 931 if down_fetch: 932 down_fetch.Stop() # Kill the download of the older revision. 933 fetch = None 934 if up_fetch: 935 up_fetch.WaitFor() 936 pivot = up_pivot 937 fetch = up_fetch 938 elif ((answer == 'b' and good_rev < bad_rev) 939 or (answer == 'g' and bad_rev < good_rev)): 940 fetch.Stop() 941 maxrev = pivot 942 if up_fetch: 943 up_fetch.Stop() # Kill the download of the newer revision. 944 fetch = None 945 if down_fetch: 946 down_fetch.WaitFor() 947 pivot = down_pivot 948 fetch = down_fetch 949 elif answer == 'r': 950 # Don't redundantly prefetch. 951 prefetch_revisions = False 952 elif answer == 'u': 953 # Nuke the revision from the revlist and choose a new pivot. 954 fetch.Stop() 955 revlist.pop(pivot) 956 maxrev -= 1 # Assumes maxrev >= pivot. 957 958 if maxrev - minrev > 1: 959 # Alternate between using down_pivot or up_pivot for the new pivot 960 # point, without affecting the range. Do this instead of setting the 961 # pivot to the midpoint of the new range because adjacent revisions 962 # are likely affected by the same issue that caused the (u)nknown 963 # response. 964 if up_fetch and down_fetch: 965 fetch = [up_fetch, down_fetch][len(revlist) % 2] 966 elif up_fetch: 967 fetch = up_fetch 968 else: 969 fetch = down_fetch 970 fetch.WaitFor() 971 if fetch == up_fetch: 972 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized. 973 else: 974 pivot = down_pivot 975 976 if down_fetch and fetch != down_fetch: 977 down_fetch.Stop() 978 if up_fetch and fetch != up_fetch: 979 up_fetch.Stop() 980 else: 981 assert False, 'Unexpected return value from evaluate(): ' + answer 982 except (KeyboardInterrupt, SystemExit): 983 print('Cleaning up...') 984 for f in [_GetDownloadPath(rev), 985 _GetDownloadPath(revlist[down_pivot]), 986 _GetDownloadPath(revlist[up_pivot])]: 987 try: 988 os.unlink(f) 989 except OSError: 990 pass 991 sys.exit(0) 992 993 rev = revlist[pivot] 994 995 return (revlist[minrev], revlist[maxrev], context) 996 997 998def GetBlinkDEPSRevisionForChromiumRevision(self, rev): 999 """Returns the blink revision that was in REVISIONS file at 1000 chromium revision |rev|.""" 1001 1002 def _GetBlinkRev(url, blink_re): 1003 m = blink_re.search(url.read()) 1004 url.close() 1005 if m: 1006 return m.group(1) 1007 1008 url = urllib.urlopen(DEPS_FILE % GetGitHashFromSVNRevision(rev)) 1009 if url.getcode() == 200: 1010 blink_re = re.compile(r'webkit_revision\D*\d+;\D*\d+;(\w+)') 1011 blink_git_sha = _GetBlinkRev(url, blink_re) 1012 return self.GetSVNRevisionFromGitHash(blink_git_sha, 'blink') 1013 raise Exception('Could not get Blink revision for Chromium rev %d' % rev) 1014 1015 1016def GetBlinkRevisionForChromiumRevision(context, rev): 1017 """Returns the blink revision that was in REVISIONS file at 1018 chromium revision |rev|.""" 1019 def _IsRevisionNumber(revision): 1020 if isinstance(revision, int): 1021 return True 1022 else: 1023 return revision.isdigit() 1024 if str(rev) in context.githash_svn_dict: 1025 rev = context.githash_svn_dict[str(rev)] 1026 file_url = '%s/%s%s/REVISIONS' % (context.base_url, 1027 context._listing_platform_dir, rev) 1028 url = urllib.urlopen(file_url) 1029 if url.getcode() == 200: 1030 try: 1031 data = json.loads(url.read()) 1032 except ValueError: 1033 print('ValueError for JSON URL: %s' % file_url) 1034 raise ValueError 1035 else: 1036 raise ValueError 1037 url.close() 1038 if 'webkit_revision' in data: 1039 blink_rev = data['webkit_revision'] 1040 if not _IsRevisionNumber(blink_rev): 1041 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink')) 1042 return blink_rev 1043 else: 1044 raise Exception('Could not get blink revision for cr rev %d' % rev) 1045 1046 1047def FixChromiumRevForBlink(revisions_final, revisions, self, rev): 1048 """Returns the chromium revision that has the correct blink revision 1049 for blink bisect, DEPS and REVISIONS file might not match since 1050 blink snapshots point to tip of tree blink. 1051 Note: The revisions_final variable might get modified to include 1052 additional revisions.""" 1053 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(self, rev) 1054 1055 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev): 1056 idx = revisions.index(rev) 1057 if idx > 0: 1058 rev = revisions[idx-1] 1059 if rev not in revisions_final: 1060 revisions_final.insert(0, rev) 1061 1062 revisions_final.sort() 1063 return rev 1064 1065 1066def GetChromiumRevision(context, url): 1067 """Returns the chromium revision read from given URL.""" 1068 try: 1069 # Location of the latest build revision number 1070 latest_revision = urllib.urlopen(url).read() 1071 if latest_revision.isdigit(): 1072 return int(latest_revision) 1073 return context.GetSVNRevisionFromGitHash(latest_revision) 1074 except Exception: 1075 print('Could not determine latest revision. This could be bad...') 1076 return 999999999 1077 1078 1079def GetRevision(revision_text): 1080 """Translates from a text description of a revision to an integral revision 1081 number. Currently supported formats are a number (i.e.; '782793') or a 1082 milestone specifier (i.e.; 'M85') or a full version string 1083 (i.e. '85.0.4183.121').""" 1084 1085 # Check if we already have a revision number, such as when -g or -b is 1086 # omitted. 1087 if type(revision_text) == type(0): 1088 return revision_text 1089 1090 # Translate from stable milestone name to the latest version number released 1091 # for that milestone, i.e.; 'M85' to '85.0.4183.121'. 1092 if revision_text[:1].upper() == 'M': 1093 milestone = revision_text[1:] 1094 response = urllib.urlopen(VERSION_HISTORY_URL) 1095 version_history = json.loads(response.read()) 1096 version_matcher = re.compile( 1097 '.*versions/(\d*)\.(\d*)\.(\d*)\.(\d*)/releases.*') 1098 for version in version_history['releases']: 1099 match = version_matcher.match(version['name']) 1100 # There will be multiple versions of each milestone, but we just grab the 1101 # first one that we see which will be the most recent version. If you need 1102 # more granularity then specify a full version number or revision number. 1103 if match and match.groups()[0] == milestone: 1104 revision_text = '.'.join(match.groups()) 1105 break 1106 if revision_text[:1].upper() == 'M': 1107 raise Exception('No stable release matching %s found.' % revision_text) 1108 1109 # Translate from version number to commit position, also known as revision 1110 # number. 1111 if len(revision_text.split('.')) == 4: 1112 response = urllib.urlopen(OMAHA_REVISIONS_URL % revision_text) 1113 revision_details = json.loads(response.read()) 1114 revision_text = revision_details['chromium_base_position'] 1115 1116 # Translate from text commit position to integer commit position. 1117 return int(revision_text) 1118 1119 1120def GetGitHashFromSVNRevision(svn_revision): 1121 crrev_url = CRREV_URL + str(svn_revision) 1122 url = urllib.urlopen(crrev_url) 1123 if url.getcode() == 200: 1124 data = json.loads(url.read()) 1125 if 'git_sha' in data: 1126 return data['git_sha'] 1127 1128def PrintChangeLog(min_chromium_rev, max_chromium_rev): 1129 """Prints the changelog URL.""" 1130 1131 print(' ' + CHANGELOG_URL % (GetGitHashFromSVNRevision(min_chromium_rev), 1132 GetGitHashFromSVNRevision(max_chromium_rev))) 1133 1134 1135def error_internal_option(option, opt, value, parser): 1136 raise optparse.OptionValueError( 1137 'The -o and -r options are only\navailable in the internal version of ' 1138 'this script. Google\nemployees should visit http://go/bisect-builds ' 1139 'for\nconfiguration instructions.') 1140 1141def main(): 1142 usage = ('%prog [options] [-- chromium-options]\n' 1143 'Perform binary search on the snapshot builds to find a minimal\n' 1144 'range of revisions where a behavior change happened. The\n' 1145 'behaviors are described as "good" and "bad".\n' 1146 'It is NOT assumed that the behavior of the later revision is\n' 1147 'the bad one.\n' 1148 '\n' 1149 'Revision numbers should use\n' 1150 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n' 1151 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n' 1152 ' for earlier revs.\n' 1153 ' Chrome\'s about: build number and omahaproxy branch_revision\n' 1154 ' are incorrect, they are from branches.\n' 1155 '\n' 1156 'Use "-- <args-to-pass-to-chromium>" to pass arbitrary extra \n' 1157 'arguments to the test binaries.\n' 1158 'E.g., add "-- --no-first-run" to bypass the first run prompts.') 1159 parser = optparse.OptionParser(usage=usage) 1160 # Strangely, the default help output doesn't include the choice list. 1161 choices = ['mac', 'mac64', 'win', 'win64', 'linux', 'linux64', 'linux-arm', 1162 'chromeos'] 1163 parser.add_option('-a', '--archive', 1164 choices=choices, 1165 help='The buildbot archive to bisect [%s].' % 1166 '|'.join(choices)) 1167 parser.add_option('-b', 1168 '--bad', 1169 type='str', 1170 help='A bad revision to start bisection. ' 1171 'May be earlier or later than the good revision. ' 1172 'Default is HEAD. Can be a revision number, milestone ' 1173 'name (eg. M85, matches the most recent stable release of ' 1174 'that milestone) or version number (eg. 85.0.4183.121)') 1175 parser.add_option('-f', '--flash_path', 1176 type='str', 1177 help='Absolute path to a recent Adobe Pepper Flash ' 1178 'binary to be used in this bisection (e.g. ' 1179 'on Windows C:\...\pepflashplayer.dll and on Linux ' 1180 '/opt/google/chrome/PepperFlash/' 1181 'libpepflashplayer.so).') 1182 parser.add_option('-g', 1183 '--good', 1184 type='str', 1185 help='A good revision to start bisection. ' + 1186 'May be earlier or later than the bad revision. ' + 1187 'Default is 0. Can be a revision number, milestone ' 1188 'name (eg. M85, matches the most recent stable release of ' 1189 'that milestone) or version number (eg. 85.0.4183.121)') 1190 parser.add_option('-p', '--profile', '--user-data-dir', 1191 type='str', 1192 default='profile', 1193 help='Profile to use; this will not reset every run. ' 1194 'Defaults to a clean profile.') 1195 parser.add_option('-t', '--times', 1196 type='int', 1197 default=1, 1198 help='Number of times to run each build before asking ' 1199 'if it\'s good or bad. Temporary profiles are reused.') 1200 parser.add_option('-c', 1201 '--command', 1202 type='str', 1203 default='%p %a', 1204 help='Command to execute. %p and %a refer to Chrome ' 1205 'executable and specified extra arguments respectively. ' 1206 'Use %s to specify all extra arguments as one string. ' 1207 'Defaults to "%p %a". Note that any extra paths specified ' 1208 'should be absolute. If you just need to append an ' 1209 'argument to the Chrome command line use "-- ' 1210 '<args-to-pass-to-chromium>" instead.') 1211 parser.add_option('-l', '--blink', 1212 action='store_true', 1213 help='Use Blink bisect instead of Chromium. ') 1214 parser.add_option('', '--not-interactive', 1215 action='store_true', 1216 default=False, 1217 help='Use command exit code to tell good/bad revision.') 1218 parser.add_option('--asan', 1219 dest='asan', 1220 action='store_true', 1221 default=False, 1222 help='Allow the script to bisect ASAN builds') 1223 parser.add_option('--use-local-cache', 1224 dest='use_local_cache', 1225 action='store_true', 1226 default=False, 1227 help='Use a local file in the current directory to cache ' 1228 'a list of known revisions to speed up the ' 1229 'initialization of this script.') 1230 parser.add_option('--verify-range', 1231 dest='verify_range', 1232 action='store_true', 1233 default=False, 1234 help='Test the first and last revisions in the range ' + 1235 'before proceeding with the bisect.') 1236 parser.add_option("-r", action="callback", callback=error_internal_option) 1237 parser.add_option("-o", action="callback", callback=error_internal_option) 1238 1239 (opts, args) = parser.parse_args() 1240 1241 if opts.archive is None: 1242 print('Error: missing required parameter: --archive') 1243 print() 1244 parser.print_help() 1245 return 1 1246 1247 if opts.asan: 1248 supported_platforms = ['linux', 'mac', 'win'] 1249 if opts.archive not in supported_platforms: 1250 print('Error: ASAN bisecting only supported on these platforms: [%s].' % 1251 ('|'.join(supported_platforms))) 1252 return 1 1253 1254 if opts.asan: 1255 base_url = ASAN_BASE_URL 1256 elif opts.blink: 1257 base_url = WEBKIT_BASE_URL 1258 else: 1259 base_url = CHROMIUM_BASE_URL 1260 1261 # Create the context. Initialize 0 for the revisions as they are set below. 1262 context = PathContext(base_url, opts.archive, opts.good, opts.bad, 1263 opts.asan, opts.use_local_cache, 1264 opts.flash_path) 1265 1266 # Pick a starting point, try to get HEAD for this. 1267 if not opts.bad: 1268 context.bad_revision = '999.0.0.0' 1269 context.bad_revision = GetChromiumRevision( 1270 context, context.GetLastChangeURL()) 1271 1272 # Find out when we were good. 1273 if not opts.good: 1274 context.good_revision = 0 1275 1276 if opts.flash_path: 1277 msg = 'Could not find Flash binary at %s' % opts.flash_path 1278 assert os.path.exists(opts.flash_path), msg 1279 1280 context.good_revision = GetRevision(context.good_revision) 1281 context.bad_revision = GetRevision(context.bad_revision) 1282 1283 if opts.times < 1: 1284 print('Number of times to run (%d) must be greater than or equal to 1.' % 1285 opts.times) 1286 parser.print_help() 1287 return 1 1288 1289 if opts.not_interactive: 1290 evaluator = DidCommandSucceed 1291 elif opts.asan: 1292 evaluator = IsGoodASANBuild 1293 else: 1294 evaluator = AskIsGoodBuild 1295 1296 # Save these revision numbers to compare when showing the changelog URL 1297 # after the bisect. 1298 good_rev = context.good_revision 1299 bad_rev = context.bad_revision 1300 1301 print('Scanning from %d to %d (%d revisions).' % 1302 (good_rev, bad_rev, abs(good_rev - bad_rev))) 1303 1304 (min_chromium_rev, max_chromium_rev, 1305 context) = Bisect(context, opts.times, opts.command, args, opts.profile, 1306 evaluator, opts.verify_range, opts.archive) 1307 1308 # Get corresponding blink revisions. 1309 try: 1310 min_blink_rev = GetBlinkRevisionForChromiumRevision(context, 1311 min_chromium_rev) 1312 max_blink_rev = GetBlinkRevisionForChromiumRevision(context, 1313 max_chromium_rev) 1314 except Exception: 1315 # Silently ignore the failure. 1316 min_blink_rev, max_blink_rev = 0, 0 1317 1318 if opts.blink: 1319 # We're done. Let the user know the results in an official manner. 1320 if good_rev > bad_rev: 1321 print(DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))) 1322 else: 1323 print(DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))) 1324 1325 print('BLINK CHANGELOG URL:') 1326 print(' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)) 1327 1328 else: 1329 # We're done. Let the user know the results in an official manner. 1330 if good_rev > bad_rev: 1331 print(DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), 1332 str(max_chromium_rev))) 1333 else: 1334 print(DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), 1335 str(max_chromium_rev))) 1336 if min_blink_rev != max_blink_rev: 1337 print ('NOTE: There is a Blink roll in the range, ' 1338 'you might also want to do a Blink bisect.') 1339 1340 print('CHANGELOG URL:') 1341 PrintChangeLog(min_chromium_rev, max_chromium_rev) 1342 1343 1344if __name__ == '__main__': 1345 sys.exit(main()) 1346