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