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 15# The root URL for storage. 16CHROMIUM_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' 17WEBKIT_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-webkit-snapshots' 18 19# The root URL for official builds. 20OFFICIAL_BASE_URL = 'http://master.chrome.corp.google.com/official_builds' 21 22# Changelogs URL. 23CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ 24 'perf/dashboard/ui/changelog.html?' \ 25 'url=/trunk/src&range=%d%%3A%d' 26 27# Official Changelogs URL. 28OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\ 29 'changelog?old_version=%s&new_version=%s' 30 31# DEPS file URL. 32DEPS_FILE = 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' 33# Blink Changelogs URL. 34BLINK_CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ 35 'perf/dashboard/ui/changelog_blink.html?' \ 36 'url=/trunk&range=%d%%3A%d' 37 38DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \ 39 '(known good), but no later than %s (first known bad).' 40DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \ 41 '(known bad), but no later than %s (first known good).' 42 43############################################################################### 44 45import json 46import optparse 47import os 48import re 49import shlex 50import shutil 51import subprocess 52import sys 53import tempfile 54import threading 55import urllib 56from distutils.version import LooseVersion 57from xml.etree import ElementTree 58import zipfile 59 60 61class PathContext(object): 62 """A PathContext is used to carry the information used to construct URLs and 63 paths when dealing with the storage server and archives.""" 64 def __init__(self, base_url, platform, good_revision, bad_revision, 65 is_official, is_aura, flash_path = None): 66 super(PathContext, self).__init__() 67 # Store off the input parameters. 68 self.base_url = base_url 69 self.platform = platform # What's passed in to the '-a/--archive' option. 70 self.good_revision = good_revision 71 self.bad_revision = bad_revision 72 self.is_official = is_official 73 self.is_aura = is_aura 74 self.flash_path = flash_path 75 76 # The name of the ZIP file in a revision directory on the server. 77 self.archive_name = None 78 79 # Set some internal members: 80 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. 81 # _archive_extract_dir = Uncompressed directory in the archive_name file. 82 # _binary_name = The name of the executable to run. 83 if self.platform in ('linux', 'linux64', 'linux-arm'): 84 self._binary_name = 'chrome' 85 elif self.platform == 'mac': 86 self.archive_name = 'chrome-mac.zip' 87 self._archive_extract_dir = 'chrome-mac' 88 elif self.platform == 'win': 89 self.archive_name = 'chrome-win32.zip' 90 self._archive_extract_dir = 'chrome-win32' 91 self._binary_name = 'chrome.exe' 92 else: 93 raise Exception('Invalid platform: %s' % self.platform) 94 95 if is_official: 96 if self.platform == 'linux': 97 self._listing_platform_dir = 'precise32bit/' 98 self.archive_name = 'chrome-precise32bit.zip' 99 self._archive_extract_dir = 'chrome-precise32bit' 100 elif self.platform == 'linux64': 101 self._listing_platform_dir = 'precise64bit/' 102 self.archive_name = 'chrome-precise64bit.zip' 103 self._archive_extract_dir = 'chrome-precise64bit' 104 elif self.platform == 'mac': 105 self._listing_platform_dir = 'mac/' 106 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome' 107 elif self.platform == 'win': 108 if self.is_aura: 109 self._listing_platform_dir = 'win-aura/' 110 else: 111 self._listing_platform_dir = 'win/' 112 else: 113 if self.platform in ('linux', 'linux64', 'linux-arm'): 114 self.archive_name = 'chrome-linux.zip' 115 self._archive_extract_dir = 'chrome-linux' 116 if self.platform == 'linux': 117 self._listing_platform_dir = 'Linux/' 118 elif self.platform == 'linux64': 119 self._listing_platform_dir = 'Linux_x64/' 120 elif self.platform == 'linux-arm': 121 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/' 122 elif self.platform == 'mac': 123 self._listing_platform_dir = 'Mac/' 124 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' 125 elif self.platform == 'win': 126 self._listing_platform_dir = 'Win/' 127 128 def GetListingURL(self, marker=None): 129 """Returns the URL for a directory listing, with an optional marker.""" 130 marker_param = '' 131 if marker: 132 marker_param = '&marker=' + str(marker) 133 return self.base_url + '/?delimiter=/&prefix=' + \ 134 self._listing_platform_dir + marker_param 135 136 def GetDownloadURL(self, revision): 137 """Gets the download URL for a build archive of a specific revision.""" 138 if self.is_official: 139 return "%s/%s/%s%s" % ( 140 OFFICIAL_BASE_URL, revision, self._listing_platform_dir, 141 self.archive_name) 142 else: 143 return "%s/%s%s/%s" % (self.base_url, self._listing_platform_dir, 144 revision, self.archive_name) 145 146 def GetLastChangeURL(self): 147 """Returns a URL to the LAST_CHANGE file.""" 148 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE' 149 150 def GetLaunchPath(self): 151 """Returns a relative path (presumably from the archive extraction location) 152 that is used to run the executable.""" 153 return os.path.join(self._archive_extract_dir, self._binary_name) 154 155 def IsAuraBuild(self, build): 156 """Check the given build is Aura.""" 157 return build.split('.')[3] == '1' 158 159 def IsASANBuild(self, build): 160 """Check the given build is ASAN build.""" 161 return build.split('.')[3] == '2' 162 163 def ParseDirectoryIndex(self): 164 """Parses the Google Storage directory listing into a list of revision 165 numbers.""" 166 167 def _FetchAndParse(url): 168 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If 169 next-marker is not None, then the listing is a partial listing and another 170 fetch should be performed with next-marker being the marker= GET 171 parameter.""" 172 handle = urllib.urlopen(url) 173 document = ElementTree.parse(handle) 174 175 # All nodes in the tree are namespaced. Get the root's tag name to extract 176 # the namespace. Etree does namespaces as |{namespace}tag|. 177 root_tag = document.getroot().tag 178 end_ns_pos = root_tag.find('}') 179 if end_ns_pos == -1: 180 raise Exception("Could not locate end namespace for directory index") 181 namespace = root_tag[:end_ns_pos + 1] 182 183 # Find the prefix (_listing_platform_dir) and whether or not the list is 184 # truncated. 185 prefix_len = len(document.find(namespace + 'Prefix').text) 186 next_marker = None 187 is_truncated = document.find(namespace + 'IsTruncated') 188 if is_truncated is not None and is_truncated.text.lower() == 'true': 189 next_marker = document.find(namespace + 'NextMarker').text 190 191 # Get a list of all the revisions. 192 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + 193 namespace + 'Prefix') 194 # The <Prefix> nodes have content of the form of 195 # |_listing_platform_dir/revision/|. Strip off the platform dir and the 196 # trailing slash to just have a number. 197 revisions = [] 198 for prefix in all_prefixes: 199 revnum = prefix.text[prefix_len:-1] 200 try: 201 revnum = int(revnum) 202 revisions.append(revnum) 203 except ValueError: 204 pass 205 return (revisions, next_marker) 206 207 # Fetch the first list of revisions. 208 (revisions, next_marker) = _FetchAndParse(self.GetListingURL()) 209 210 # If the result list was truncated, refetch with the next marker. Do this 211 # until an entire directory listing is done. 212 while next_marker: 213 next_url = self.GetListingURL(next_marker) 214 (new_revisions, next_marker) = _FetchAndParse(next_url) 215 revisions.extend(new_revisions) 216 return revisions 217 218 def GetRevList(self): 219 """Gets the list of revision numbers between self.good_revision and 220 self.bad_revision.""" 221 # Download the revlist and filter for just the range between good and bad. 222 minrev = min(self.good_revision, self.bad_revision) 223 maxrev = max(self.good_revision, self.bad_revision) 224 revlist_all = map(int, self.ParseDirectoryIndex()) 225 226 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)] 227 revlist.sort() 228 229 # Set good and bad revisions to be legit revisions. 230 if revlist: 231 if self.good_revision < self.bad_revision: 232 self.good_revision = revlist[0] 233 self.bad_revision = revlist[-1] 234 else: 235 self.bad_revision = revlist[0] 236 self.good_revision = revlist[-1] 237 238 # Fix chromium rev so that the deps blink revision matches REVISIONS file. 239 if self.base_url == WEBKIT_BASE_URL: 240 revlist_all.sort() 241 self.good_revision = FixChromiumRevForBlink(revlist, 242 revlist_all, 243 self, 244 self.good_revision) 245 self.bad_revision = FixChromiumRevForBlink(revlist, 246 revlist_all, 247 self, 248 self.bad_revision) 249 return revlist 250 251 def GetOfficialBuildsList(self): 252 """Gets the list of official build numbers between self.good_revision and 253 self.bad_revision.""" 254 # Download the revlist and filter for just the range between good and bad. 255 minrev = min(self.good_revision, self.bad_revision) 256 maxrev = max(self.good_revision, self.bad_revision) 257 handle = urllib.urlopen(OFFICIAL_BASE_URL) 258 dirindex = handle.read() 259 handle.close() 260 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex) 261 final_list = [] 262 i = 0 263 parsed_build_numbers = [LooseVersion(x) for x in build_numbers] 264 for build_number in sorted(parsed_build_numbers): 265 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \ 266 self._listing_platform_dir + self.archive_name 267 i = i + 1 268 try: 269 connection = urllib.urlopen(path) 270 connection.close() 271 if build_number > maxrev: 272 break 273 if build_number >= minrev: 274 # If we are bisecting Aura, we want to include only builds which 275 # ends with ".1". 276 if self.is_aura: 277 if self.IsAuraBuild(str(build_number)): 278 final_list.append(str(build_number)) 279 # If we are bisecting only official builds (without --aura), 280 # we can not include builds which ends with '.1' or '.2' since 281 # they have different folder hierarchy inside. 282 elif (not self.IsAuraBuild(str(build_number)) and 283 not self.IsASANBuild(str(build_number))): 284 final_list.append(str(build_number)) 285 except urllib.HTTPError, e: 286 pass 287 return final_list 288 289def UnzipFilenameToDir(filename, directory): 290 """Unzip |filename| to |directory|.""" 291 cwd = os.getcwd() 292 if not os.path.isabs(filename): 293 filename = os.path.join(cwd, filename) 294 zf = zipfile.ZipFile(filename) 295 # Make base. 296 if not os.path.isdir(directory): 297 os.mkdir(directory) 298 os.chdir(directory) 299 # Extract files. 300 for info in zf.infolist(): 301 name = info.filename 302 if name.endswith('/'): # dir 303 if not os.path.isdir(name): 304 os.makedirs(name) 305 else: # file 306 directory = os.path.dirname(name) 307 if not os.path.isdir(directory): 308 os.makedirs(directory) 309 out = open(name, 'wb') 310 out.write(zf.read(name)) 311 out.close() 312 # Set permissions. Permission info in external_attr is shifted 16 bits. 313 os.chmod(name, info.external_attr >> 16L) 314 os.chdir(cwd) 315 316 317def FetchRevision(context, rev, filename, quit_event=None, progress_event=None): 318 """Downloads and unzips revision |rev|. 319 @param context A PathContext instance. 320 @param rev The Chromium revision number/tag to download. 321 @param filename The destination for the downloaded file. 322 @param quit_event A threading.Event which will be set by the master thread to 323 indicate that the download should be aborted. 324 @param progress_event A threading.Event which will be set by the master thread 325 to indicate that the progress of the download should be 326 displayed. 327 """ 328 def ReportHook(blocknum, blocksize, totalsize): 329 if quit_event and quit_event.isSet(): 330 raise RuntimeError("Aborting download of revision %s" % str(rev)) 331 if progress_event and progress_event.isSet(): 332 size = blocknum * blocksize 333 if totalsize == -1: # Total size not known. 334 progress = "Received %d bytes" % size 335 else: 336 size = min(totalsize, size) 337 progress = "Received %d of %d bytes, %.2f%%" % ( 338 size, totalsize, 100.0 * size / totalsize) 339 # Send a \r to let all progress messages use just one line of output. 340 sys.stdout.write("\r" + progress) 341 sys.stdout.flush() 342 343 download_url = context.GetDownloadURL(rev) 344 try: 345 urllib.urlretrieve(download_url, filename, ReportHook) 346 if progress_event and progress_event.isSet(): 347 print 348 except RuntimeError, e: 349 pass 350 351 352def RunRevision(context, revision, zipfile, profile, num_runs, command, args): 353 """Given a zipped revision, unzip it and run the test.""" 354 print "Trying revision %s..." % str(revision) 355 356 # Create a temp directory and unzip the revision into it. 357 cwd = os.getcwd() 358 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') 359 UnzipFilenameToDir(zipfile, tempdir) 360 os.chdir(tempdir) 361 362 # Run the build as many times as specified. 363 testargs = ['--user-data-dir=%s' % profile] + args 364 # The sandbox must be run as root on Official Chrome, so bypass it. 365 if ((context.is_official or context.flash_path) and 366 context.platform.startswith('linux')): 367 testargs.append('--no-sandbox') 368 if context.flash_path: 369 testargs.append('--ppapi-flash-path=%s' % context.flash_path) 370 # We have to pass a large enough Flash version, which currently needs not 371 # be correct. Instead of requiring the user of the script to figure out and 372 # pass the correct version we just spoof it. 373 testargs.append('--ppapi-flash-version=99.9.999.999') 374 375 runcommand = [] 376 for token in shlex.split(command): 377 if token == "%a": 378 runcommand.extend(testargs) 379 else: 380 runcommand.append( \ 381 token.replace('%p', context.GetLaunchPath()) \ 382 .replace('%s', ' '.join(testargs))) 383 384 for i in range(0, num_runs): 385 subproc = subprocess.Popen(runcommand, 386 bufsize=-1, 387 stdout=subprocess.PIPE, 388 stderr=subprocess.PIPE) 389 (stdout, stderr) = subproc.communicate() 390 391 os.chdir(cwd) 392 try: 393 shutil.rmtree(tempdir, True) 394 except Exception, e: 395 pass 396 397 return (subproc.returncode, stdout, stderr) 398 399 400def AskIsGoodBuild(rev, official_builds, status, stdout, stderr): 401 """Ask the user whether build |rev| is good or bad.""" 402 # Loop until we get a response that we can parse. 403 while True: 404 response = raw_input('Revision %s is ' \ 405 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' % 406 str(rev)) 407 if response and response in ('g', 'b', 'r', 'u'): 408 return response 409 if response and response == 'q': 410 raise SystemExit() 411 412 413class DownloadJob(object): 414 """DownloadJob represents a task to download a given Chromium revision.""" 415 def __init__(self, context, name, rev, zipfile): 416 super(DownloadJob, self).__init__() 417 # Store off the input parameters. 418 self.context = context 419 self.name = name 420 self.rev = rev 421 self.zipfile = zipfile 422 self.quit_event = threading.Event() 423 self.progress_event = threading.Event() 424 425 def Start(self): 426 """Starts the download.""" 427 fetchargs = (self.context, 428 self.rev, 429 self.zipfile, 430 self.quit_event, 431 self.progress_event) 432 self.thread = threading.Thread(target=FetchRevision, 433 name=self.name, 434 args=fetchargs) 435 self.thread.start() 436 437 def Stop(self): 438 """Stops the download which must have been started previously.""" 439 self.quit_event.set() 440 self.thread.join() 441 os.unlink(self.zipfile) 442 443 def WaitFor(self): 444 """Prints a message and waits for the download to complete. The download 445 must have been started previously.""" 446 print "Downloading revision %s..." % str(self.rev) 447 self.progress_event.set() # Display progress of download. 448 self.thread.join() 449 450 451def Bisect(base_url, 452 platform, 453 official_builds, 454 is_aura, 455 good_rev=0, 456 bad_rev=0, 457 num_runs=1, 458 command="%p %a", 459 try_args=(), 460 profile=None, 461 flash_path=None, 462 evaluate=AskIsGoodBuild): 463 """Given known good and known bad revisions, run a binary search on all 464 archived revisions to determine the last known good revision. 465 466 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). 467 @param official_builds Specify build type (Chromium or Official build). 468 @param good_rev Number/tag of the known good revision. 469 @param bad_rev Number/tag of the known bad revision. 470 @param num_runs Number of times to run each build for asking good/bad. 471 @param try_args A tuple of arguments to pass to the test application. 472 @param profile The name of the user profile to run with. 473 @param evaluate A function which returns 'g' if the argument build is good, 474 'b' if it's bad or 'u' if unknown. 475 476 Threading is used to fetch Chromium revisions in the background, speeding up 477 the user's experience. For example, suppose the bounds of the search are 478 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on 479 whether revision 50 is good or bad, the next revision to check will be either 480 25 or 75. So, while revision 50 is being checked, the script will download 481 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is 482 known: 483 484 - If rev 50 is good, the download of rev 25 is cancelled, and the next test 485 is run on rev 75. 486 487 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test 488 is run on rev 25. 489 """ 490 491 if not profile: 492 profile = 'profile' 493 494 context = PathContext(base_url, platform, good_rev, bad_rev, 495 official_builds, is_aura, flash_path) 496 cwd = os.getcwd() 497 498 print "Downloading list of known revisions..." 499 _GetDownloadPath = lambda rev: os.path.join(cwd, 500 '%s-%s' % (str(rev), context.archive_name)) 501 if official_builds: 502 revlist = context.GetOfficialBuildsList() 503 else: 504 revlist = context.GetRevList() 505 506 # Get a list of revisions to bisect across. 507 if len(revlist) < 2: # Don't have enough builds to bisect. 508 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist 509 raise RuntimeError(msg) 510 511 # Figure out our bookends and first pivot point; fetch the pivot revision. 512 minrev = 0 513 maxrev = len(revlist) - 1 514 pivot = maxrev / 2 515 rev = revlist[pivot] 516 zipfile = _GetDownloadPath(rev) 517 fetch = DownloadJob(context, 'initial_fetch', rev, zipfile) 518 fetch.Start() 519 fetch.WaitFor() 520 521 # Binary search time! 522 while fetch and fetch.zipfile and maxrev - minrev > 1: 523 if bad_rev < good_rev: 524 min_str, max_str = "bad", "good" 525 else: 526 min_str, max_str = "good", "bad" 527 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str, \ 528 revlist[maxrev], max_str) 529 530 # Pre-fetch next two possible pivots 531 # - down_pivot is the next revision to check if the current revision turns 532 # out to be bad. 533 # - up_pivot is the next revision to check if the current revision turns 534 # out to be good. 535 down_pivot = int((pivot - minrev) / 2) + minrev 536 down_fetch = None 537 if down_pivot != pivot and down_pivot != minrev: 538 down_rev = revlist[down_pivot] 539 down_fetch = DownloadJob(context, 'down_fetch', down_rev, 540 _GetDownloadPath(down_rev)) 541 down_fetch.Start() 542 543 up_pivot = int((maxrev - pivot) / 2) + pivot 544 up_fetch = None 545 if up_pivot != pivot and up_pivot != maxrev: 546 up_rev = revlist[up_pivot] 547 up_fetch = DownloadJob(context, 'up_fetch', up_rev, 548 _GetDownloadPath(up_rev)) 549 up_fetch.Start() 550 551 # Run test on the pivot revision. 552 status = None 553 stdout = None 554 stderr = None 555 try: 556 (status, stdout, stderr) = RunRevision(context, 557 rev, 558 fetch.zipfile, 559 profile, 560 num_runs, 561 command, 562 try_args) 563 except Exception, e: 564 print >> sys.stderr, e 565 566 # Call the evaluate function to see if the current revision is good or bad. 567 # On that basis, kill one of the background downloads and complete the 568 # other, as described in the comments above. 569 try: 570 answer = evaluate(rev, official_builds, status, stdout, stderr) 571 if answer == 'g' and good_rev < bad_rev or \ 572 answer == 'b' and bad_rev < good_rev: 573 fetch.Stop() 574 minrev = pivot 575 if down_fetch: 576 down_fetch.Stop() # Kill the download of the older revision. 577 fetch = None 578 if up_fetch: 579 up_fetch.WaitFor() 580 pivot = up_pivot 581 fetch = up_fetch 582 elif answer == 'b' and good_rev < bad_rev or \ 583 answer == 'g' and bad_rev < good_rev: 584 fetch.Stop() 585 maxrev = pivot 586 if up_fetch: 587 up_fetch.Stop() # Kill the download of the newer revision. 588 fetch = None 589 if down_fetch: 590 down_fetch.WaitFor() 591 pivot = down_pivot 592 fetch = down_fetch 593 elif answer == 'r': 594 pass # Retry requires no changes. 595 elif answer == 'u': 596 # Nuke the revision from the revlist and choose a new pivot. 597 fetch.Stop() 598 revlist.pop(pivot) 599 maxrev -= 1 # Assumes maxrev >= pivot. 600 601 if maxrev - minrev > 1: 602 # Alternate between using down_pivot or up_pivot for the new pivot 603 # point, without affecting the range. Do this instead of setting the 604 # pivot to the midpoint of the new range because adjacent revisions 605 # are likely affected by the same issue that caused the (u)nknown 606 # response. 607 if up_fetch and down_fetch: 608 fetch = [up_fetch, down_fetch][len(revlist) % 2] 609 elif up_fetch: 610 fetch = up_fetch 611 else: 612 fetch = down_fetch 613 fetch.WaitFor() 614 if fetch == up_fetch: 615 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized. 616 else: 617 pivot = down_pivot 618 zipfile = fetch.zipfile 619 620 if down_fetch and fetch != down_fetch: 621 down_fetch.Stop() 622 if up_fetch and fetch != up_fetch: 623 up_fetch.Stop() 624 else: 625 assert False, "Unexpected return value from evaluate(): " + answer 626 except SystemExit: 627 print "Cleaning up..." 628 for f in [_GetDownloadPath(revlist[down_pivot]), 629 _GetDownloadPath(revlist[up_pivot])]: 630 try: 631 os.unlink(f) 632 except OSError: 633 pass 634 sys.exit(0) 635 636 rev = revlist[pivot] 637 638 return (revlist[minrev], revlist[maxrev]) 639 640 641def GetBlinkDEPSRevisionForChromiumRevision(rev): 642 """Returns the blink revision that was in REVISIONS file at 643 chromium revision |rev|.""" 644 # . doesn't match newlines without re.DOTALL, so this is safe. 645 blink_re = re.compile(r'webkit_revision\D*(\d+)') 646 url = urllib.urlopen(DEPS_FILE % rev) 647 m = blink_re.search(url.read()) 648 url.close() 649 if m: 650 return int(m.group(1)) 651 else: 652 raise Exception('Could not get Blink revision for Chromium rev %d' 653 % rev) 654 655 656def GetBlinkRevisionForChromiumRevision(self, rev): 657 """Returns the blink revision that was in REVISIONS file at 658 chromium revision |rev|.""" 659 file_url = "%s/%s%d/REVISIONS" % (self.base_url, 660 self._listing_platform_dir, rev) 661 url = urllib.urlopen(file_url) 662 data = json.loads(url.read()) 663 url.close() 664 if 'webkit_revision' in data: 665 return data['webkit_revision'] 666 else: 667 raise Exception('Could not get blink revision for cr rev %d' % rev) 668 669def FixChromiumRevForBlink(revisions_final, revisions, self, rev): 670 """Returns the chromium revision that has the correct blink revision 671 for blink bisect, DEPS and REVISIONS file might not match since 672 blink snapshots point to tip of tree blink. 673 Note: The revisions_final variable might get modified to include 674 additional revisions.""" 675 676 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(rev) 677 678 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev): 679 idx = revisions.index(rev) 680 if idx > 0: 681 rev = revisions[idx-1] 682 if rev not in revisions_final: 683 revisions_final.insert(0, rev) 684 685 revisions_final.sort() 686 return rev 687 688def GetChromiumRevision(url): 689 """Returns the chromium revision read from given URL.""" 690 try: 691 # Location of the latest build revision number 692 return int(urllib.urlopen(url).read()) 693 except Exception, e: 694 print('Could not determine latest revision. This could be bad...') 695 return 999999999 696 697 698def main(): 699 usage = ('%prog [options] [-- chromium-options]\n' 700 'Perform binary search on the snapshot builds to find a minimal\n' 701 'range of revisions where a behavior change happened. The\n' 702 'behaviors are described as "good" and "bad".\n' 703 'It is NOT assumed that the behavior of the later revision is\n' 704 'the bad one.\n' 705 '\n' 706 'Revision numbers should use\n' 707 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n' 708 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n' 709 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n' 710 ' for earlier revs.\n' 711 ' Chrome\'s about: build number and omahaproxy branch_revision\n' 712 ' are incorrect, they are from branches.\n' 713 '\n' 714 'Tip: add "-- --no-first-run" to bypass the first run prompts.') 715 parser = optparse.OptionParser(usage=usage) 716 # Strangely, the default help output doesn't include the choice list. 717 choices = ['mac', 'win', 'linux', 'linux64', 'linux-arm'] 718 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 719 parser.add_option('-a', '--archive', 720 choices = choices, 721 help = 'The buildbot archive to bisect [%s].' % 722 '|'.join(choices)) 723 parser.add_option('-o', action="store_true", dest='official_builds', 724 help = 'Bisect across official ' + 725 'Chrome builds (internal only) instead of ' + 726 'Chromium archives.') 727 parser.add_option('-b', '--bad', type = 'str', 728 help = 'A bad revision to start bisection. ' + 729 'May be earlier or later than the good revision. ' + 730 'Default is HEAD.') 731 parser.add_option('-f', '--flash_path', type = 'str', 732 help = 'Absolute path to a recent Adobe Pepper Flash ' + 733 'binary to be used in this bisection (e.g. ' + 734 'on Windows C:\...\pepflashplayer.dll and on Linux ' + 735 '/opt/google/chrome/PepperFlash/libpepflashplayer.so).') 736 parser.add_option('-g', '--good', type = 'str', 737 help = 'A good revision to start bisection. ' + 738 'May be earlier or later than the bad revision. ' + 739 'Default is 0.') 740 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', 741 help = 'Profile to use; this will not reset every run. ' + 742 'Defaults to a clean profile.', default = 'profile') 743 parser.add_option('-t', '--times', type = 'int', 744 help = 'Number of times to run each build before asking ' + 745 'if it\'s good or bad. Temporary profiles are reused.', 746 default = 1) 747 parser.add_option('-c', '--command', type = 'str', 748 help = 'Command to execute. %p and %a refer to Chrome ' + 749 'executable and specified extra arguments respectively. ' + 750 'Use %s to specify all extra arguments as one string. ' + 751 'Defaults to "%p %a". Note that any extra paths ' + 752 'specified should be absolute.', 753 default = '%p %a') 754 parser.add_option('-l', '--blink', action='store_true', 755 help = 'Use Blink bisect instead of Chromium. ') 756 parser.add_option('--aura', 757 dest='aura', 758 action='store_true', 759 default=False, 760 help='Allow the script to bisect aura builds') 761 762 (opts, args) = parser.parse_args() 763 764 if opts.archive is None: 765 print 'Error: missing required parameter: --archive' 766 print 767 parser.print_help() 768 return 1 769 770 if opts.aura: 771 if opts.archive != 'win' or not opts.official_builds: 772 print 'Error: Aura is supported only on Windows platform '\ 773 'and official builds.' 774 return 1 775 776 if opts.blink: 777 base_url = WEBKIT_BASE_URL 778 else: 779 base_url = CHROMIUM_BASE_URL 780 781 # Create the context. Initialize 0 for the revisions as they are set below. 782 context = PathContext(base_url, opts.archive, 0, 0, 783 opts.official_builds, opts.aura, None) 784 # Pick a starting point, try to get HEAD for this. 785 if opts.bad: 786 bad_rev = opts.bad 787 else: 788 bad_rev = '999.0.0.0' 789 if not opts.official_builds: 790 bad_rev = GetChromiumRevision(context.GetLastChangeURL()) 791 792 # Find out when we were good. 793 if opts.good: 794 good_rev = opts.good 795 else: 796 good_rev = '0.0.0.0' if opts.official_builds else 0 797 798 if opts.flash_path: 799 flash_path = opts.flash_path 800 msg = 'Could not find Flash binary at %s' % flash_path 801 assert os.path.exists(flash_path), msg 802 803 if opts.official_builds: 804 good_rev = LooseVersion(good_rev) 805 bad_rev = LooseVersion(bad_rev) 806 else: 807 good_rev = int(good_rev) 808 bad_rev = int(bad_rev) 809 810 if opts.times < 1: 811 print('Number of times to run (%d) must be greater than or equal to 1.' % 812 opts.times) 813 parser.print_help() 814 return 1 815 816 (min_chromium_rev, max_chromium_rev) = Bisect( 817 base_url, opts.archive, opts.official_builds, opts.aura, good_rev, 818 bad_rev, opts.times, opts.command, args, opts.profile, opts.flash_path) 819 820 # Get corresponding blink revisions. 821 try: 822 min_blink_rev = GetBlinkRevisionForChromiumRevision(context, 823 min_chromium_rev) 824 max_blink_rev = GetBlinkRevisionForChromiumRevision(context, 825 max_chromium_rev) 826 except Exception, e: 827 # Silently ignore the failure. 828 min_blink_rev, max_blink_rev = 0, 0 829 830 if opts.blink: 831 # We're done. Let the user know the results in an official manner. 832 if good_rev > bad_rev: 833 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev)) 834 else: 835 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev)) 836 837 print 'BLINK CHANGELOG URL:' 838 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev) 839 840 else: 841 # We're done. Let the user know the results in an official manner. 842 if good_rev > bad_rev: 843 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), 844 str(max_chromium_rev)) 845 else: 846 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), 847 str(max_chromium_rev)) 848 if min_blink_rev != max_blink_rev: 849 print ("NOTE: There is a Blink roll in the range, " 850 "you might also want to do a Blink bisect.") 851 852 print 'CHANGELOG URL:' 853 if opts.official_builds: 854 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) 855 else: 856 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) 857 858if __name__ == '__main__': 859 sys.exit(main()) 860