ServiceWorker: Enqueue state change events until the worker thread gets ready
[chromium-blink-merge.git] / tools / bisect-builds.py
blob5e7db2db5018e272dcc0a1d02c23254cf92b2d5d
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.
6 """Snapshot Build Bisect Tool
8 This script bisects a snapshot archive using binary search. It starts at
9 a bad revision (it will try to guess HEAD) and asks for a last known-good
10 revision. It will then binary search across this revision range by downloading,
11 unzipping, and opening Chromium for you. After testing the specific revision,
12 it will ask you whether it is good or bad before continuing the search.
13 """
15 # The base URL for stored build archives.
16 CHROMIUM_BASE_URL = ('http://commondatastorage.googleapis.com'
17 '/chromium-browser-snapshots')
18 WEBKIT_BASE_URL = ('http://commondatastorage.googleapis.com'
19 '/chromium-webkit-snapshots')
20 ASAN_BASE_URL = ('http://commondatastorage.googleapis.com'
21 '/chromium-browser-asan')
23 # GS bucket name.
24 GS_BUCKET_NAME = 'chrome-unsigned/desktop-W15K3Y'
26 # Base URL for downloading official builds.
27 GOOGLE_APIS_URL = 'commondatastorage.googleapis.com'
29 # The base URL for official builds.
30 OFFICIAL_BASE_URL = 'http://%s/%s' % (GOOGLE_APIS_URL, GS_BUCKET_NAME)
32 # URL template for viewing changelogs between revisions.
33 CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s')
35 # GS bucket name for android unsigned official builds.
36 ANDROID_BUCKET_NAME = 'chrome-unsigned/android-C4MPAR1'
38 # The base URL for android official builds.
39 ANDROID_OFFICIAL_BASE_URL = 'http://%s/%s' % (GOOGLE_APIS_URL, ANDROID_BUCKET_NAME)
41 # URL to convert SVN revision to git hash.
42 CRREV_URL = ('https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/')
44 # URL template for viewing changelogs between official versions.
45 OFFICIAL_CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/'
46 'src/+log/%s..%s?pretty=full')
48 # DEPS file URL.
49 DEPS_FILE_OLD = ('http://src.chromium.org/viewvc/chrome/trunk/src/'
50 'DEPS?revision=%d')
51 DEPS_FILE_NEW = ('https://chromium.googlesource.com/chromium/src/+/%s/DEPS')
53 # Blink changelogs URL.
54 BLINK_CHANGELOG_URL = ('http://build.chromium.org'
55 '/f/chromium/perf/dashboard/ui/changelog_blink.html'
56 '?url=/trunk&range=%d%%3A%d')
58 DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s ('
59 'known good), but no later than %s (first known bad).')
60 DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s ('
61 'known bad), but no later than %s (first known good).')
63 CHROMIUM_GITHASH_TO_SVN_URL = (
64 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
66 BLINK_GITHASH_TO_SVN_URL = (
67 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json')
69 GITHASH_TO_SVN_URL = {
70 'chromium': CHROMIUM_GITHASH_TO_SVN_URL,
71 'blink': BLINK_GITHASH_TO_SVN_URL,
74 # Search pattern to be matched in the JSON output from
75 # CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision).
76 CHROMIUM_SEARCH_PATTERN_OLD = (
77 r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ')
78 CHROMIUM_SEARCH_PATTERN = (
79 r'Cr-Commit-Position: refs/heads/master@{#(\d+)}')
81 # Search pattern to be matched in the json output from
82 # BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision).
83 BLINK_SEARCH_PATTERN = (
84 r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ')
86 SEARCH_PATTERN = {
87 'chromium': CHROMIUM_SEARCH_PATTERN,
88 'blink': BLINK_SEARCH_PATTERN,
91 CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with '
92 'no configured credentials')
94 # Package name needed to uninstall chrome from Android devices.
95 ANDROID_CHROME_PACKAGE_NAME = {
96 'Chrome.apk': 'com.google.android.apps.chrome',
97 'ChromeBeta.apk': 'com.chrome.beta',
98 'ChromeCanary.apk': 'com.chrome.canary',
99 'ChromeDev.apk': 'com.google.android.apps.chrome_dev',
100 'ChromeStable.apk': 'com.android.chrome',
101 'ChromeWork.apk': 'com.chrome.work',
104 ###############################################################################
106 import httplib
107 import json
108 import optparse
109 import os
110 import re
111 import shlex
112 import shutil
113 import subprocess
114 import sys
115 import tempfile
116 import threading
117 import urllib
118 from distutils.version import LooseVersion
119 from xml.etree import ElementTree
120 import zipfile
123 class PathContext(object):
124 """A PathContext is used to carry the information used to construct URLs and
125 paths when dealing with the storage server and archives."""
126 def __init__(self, base_url, platform, good_revision, bad_revision,
127 is_official, is_asan, use_local_cache, flash_path = None,
128 pdf_path = None, android_apk = None):
129 super(PathContext, self).__init__()
130 # Store off the input parameters.
131 self.base_url = base_url
132 self.platform = platform # What's passed in to the '-a/--archive' option.
133 self.good_revision = good_revision
134 self.bad_revision = bad_revision
135 self.is_official = is_official
136 self.is_asan = is_asan
137 self.build_type = 'release'
138 self.flash_path = flash_path
139 # Dictionary which stores svn revision number as key and it's
140 # corresponding git hash as value. This data is populated in
141 # _FetchAndParse and used later in GetDownloadURL while downloading
142 # the build.
143 self.githash_svn_dict = {}
144 self.pdf_path = pdf_path
145 # android_apk defaults to Chrome.apk
146 self.android_apk = android_apk if android_apk else 'Chrome.apk'
147 # The name of the ZIP file in a revision directory on the server.
148 self.archive_name = None
150 # Whether to cache and use the list of known revisions in a local file to
151 # speed up the initialization of the script at the next run.
152 self.use_local_cache = use_local_cache
154 # Locate the local checkout to speed up the script by using locally stored
155 # metadata.
156 abs_file_path = os.path.abspath(os.path.realpath(__file__))
157 local_src_path = os.path.join(os.path.dirname(abs_file_path), '..')
158 if abs_file_path.endswith(os.path.join('tools', 'bisect-builds.py')) and\
159 os.path.exists(os.path.join(local_src_path, '.git')):
160 self.local_src_path = os.path.normpath(local_src_path)
161 else:
162 self.local_src_path = None
164 # If the script is being used for android builds.
165 self.android = self.platform.startswith('android')
167 # Set some internal members:
168 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
169 # _archive_extract_dir = Uncompressed directory in the archive_name file.
170 # _binary_name = The name of the executable to run.
171 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
172 self._binary_name = 'chrome'
173 elif self.platform in ('mac', 'mac64'):
174 self.archive_name = 'chrome-mac.zip'
175 self._archive_extract_dir = 'chrome-mac'
176 elif self.platform in ('win', 'win64'):
177 self.archive_name = 'chrome-win32.zip'
178 self._archive_extract_dir = 'chrome-win32'
179 self._binary_name = 'chrome.exe'
180 elif self.android:
181 pass
182 else:
183 raise Exception('Invalid platform: %s' % self.platform)
185 if is_official:
186 if self.platform == 'linux':
187 self._listing_platform_dir = 'precise32/'
188 self.archive_name = 'chrome-precise32.zip'
189 self._archive_extract_dir = 'chrome-precise32'
190 elif self.platform == 'linux64':
191 self._listing_platform_dir = 'precise64/'
192 self.archive_name = 'chrome-precise64.zip'
193 self._archive_extract_dir = 'chrome-precise64'
194 elif self.platform == 'mac':
195 self._listing_platform_dir = 'mac/'
196 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
197 elif self.platform == 'mac64':
198 self._listing_platform_dir = 'mac64/'
199 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
200 elif self.platform == 'win':
201 self._listing_platform_dir = 'win/'
202 self.archive_name = 'chrome-win.zip'
203 self._archive_extract_dir = 'chrome-win'
204 elif self.platform == 'win64':
205 self._listing_platform_dir = 'win64/'
206 self.archive_name = 'chrome-win64.zip'
207 self._archive_extract_dir = 'chrome-win64'
208 elif self.platform == 'android-arm':
209 self._listing_platform_dir = 'arm/'
210 self.archive_name = self.android_apk
211 elif self.platform == 'android-arm-64':
212 self._listing_platform_dir = 'arm_64/'
213 self.archive_name = self.android_apk
214 elif self.platform == 'android-x86':
215 self._listing_platform_dir = 'x86/'
216 self.archive_name = self.android_apk
217 elif self.platform == 'android-x64-64':
218 self._listing_platform_dir = 'x86_64/'
219 self.archive_name = self.android_apk
220 else:
221 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
222 self.archive_name = 'chrome-linux.zip'
223 self._archive_extract_dir = 'chrome-linux'
224 if self.platform == 'linux':
225 self._listing_platform_dir = 'Linux/'
226 elif self.platform == 'linux64':
227 self._listing_platform_dir = 'Linux_x64/'
228 elif self.platform == 'linux-arm':
229 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
230 elif self.platform == 'chromeos':
231 self._listing_platform_dir = 'Linux_ChromiumOS_Full/'
232 elif self.platform == 'mac':
233 self._listing_platform_dir = 'Mac/'
234 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
235 elif self.platform == 'win':
236 self._listing_platform_dir = 'Win/'
238 def GetASANPlatformDir(self):
239 """ASAN builds are in directories like "linux-release", or have filenames
240 like "asan-win32-release-277079.zip". This aligns to our platform names
241 except in the case of Windows where they use "win32" instead of "win"."""
242 if self.platform == 'win':
243 return 'win32'
244 else:
245 return self.platform
247 def GetListingURL(self, marker=None):
248 """Returns the URL for a directory listing, with an optional marker."""
249 marker_param = ''
250 if marker:
251 marker_param = '&marker=' + str(marker)
252 if self.is_asan:
253 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type)
254 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param
255 else:
256 return (self.base_url + '/?delimiter=/&prefix=' +
257 self._listing_platform_dir + marker_param)
259 def GetDownloadURL(self, revision):
260 """Gets the download URL for a build archive of a specific revision."""
261 if self.is_asan:
262 return '%s/%s-%s/%s-%d.zip' % (
263 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
264 self.GetASANBaseName(), revision)
265 if self.is_official:
266 if self.android:
267 official_base_url = ANDROID_OFFICIAL_BASE_URL
268 else:
269 official_base_url = OFFICIAL_BASE_URL
270 return '%s/%s/%s%s' % (
271 official_base_url, revision, self._listing_platform_dir,
272 self.archive_name)
273 else:
274 if str(revision) in self.githash_svn_dict:
275 revision = self.githash_svn_dict[str(revision)]
276 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
277 revision, self.archive_name)
279 def GetLastChangeURL(self):
280 """Returns a URL to the LAST_CHANGE file."""
281 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
283 def GetASANBaseName(self):
284 """Returns the base name of the ASAN zip file."""
285 if 'linux' in self.platform:
286 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(),
287 self.build_type)
288 else:
289 return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type)
291 def GetLaunchPath(self, revision):
292 """Returns a relative path (presumably from the archive extraction location)
293 that is used to run the executable."""
294 if self.is_asan:
295 extract_dir = '%s-%d' % (self.GetASANBaseName(), revision)
296 else:
297 extract_dir = self._archive_extract_dir
298 return os.path.join(extract_dir, self._binary_name)
300 def ParseDirectoryIndex(self, last_known_rev):
301 """Parses the Google Storage directory listing into a list of revision
302 numbers."""
304 def _GetMarkerForRev(revision):
305 if self.is_asan:
306 return '%s-%s/%s-%d.zip' % (
307 self.GetASANPlatformDir(), self.build_type,
308 self.GetASANBaseName(), revision)
309 return '%s%d' % (self._listing_platform_dir, revision)
311 def _FetchAndParse(url):
312 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
313 next-marker is not None, then the listing is a partial listing and another
314 fetch should be performed with next-marker being the marker= GET
315 parameter."""
316 handle = urllib.urlopen(url)
317 document = ElementTree.parse(handle)
319 # All nodes in the tree are namespaced. Get the root's tag name to extract
320 # the namespace. Etree does namespaces as |{namespace}tag|.
321 root_tag = document.getroot().tag
322 end_ns_pos = root_tag.find('}')
323 if end_ns_pos == -1:
324 raise Exception('Could not locate end namespace for directory index')
325 namespace = root_tag[:end_ns_pos + 1]
327 # Find the prefix (_listing_platform_dir) and whether or not the list is
328 # truncated.
329 prefix_len = len(document.find(namespace + 'Prefix').text)
330 next_marker = None
331 is_truncated = document.find(namespace + 'IsTruncated')
332 if is_truncated is not None and is_truncated.text.lower() == 'true':
333 next_marker = document.find(namespace + 'NextMarker').text
334 # Get a list of all the revisions.
335 revisions = []
336 githash_svn_dict = {}
337 if self.is_asan:
338 asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName()))
339 # Non ASAN builds are in a <revision> directory. The ASAN builds are
340 # flat
341 all_prefixes = document.findall(namespace + 'Contents/' +
342 namespace + 'Key')
343 for prefix in all_prefixes:
344 m = asan_regex.match(prefix.text)
345 if m:
346 try:
347 revisions.append(int(m.group(1)))
348 except ValueError:
349 pass
350 else:
351 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
352 namespace + 'Prefix')
353 # The <Prefix> nodes have content of the form of
354 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
355 # trailing slash to just have a number.
356 for prefix in all_prefixes:
357 revnum = prefix.text[prefix_len:-1]
358 try:
359 if not revnum.isdigit():
360 # During the svn-git migration, some items were stored by hash.
361 # These items may appear anywhere in the list of items.
362 # If |last_known_rev| is set, assume that the full list has been
363 # retrieved before (including the hashes), so we can safely skip
364 # all git hashes and focus on the numeric revision numbers.
365 if last_known_rev:
366 revnum = None
367 else:
368 git_hash = revnum
369 revnum = self.GetSVNRevisionFromGitHash(git_hash)
370 githash_svn_dict[revnum] = git_hash
371 if revnum is not None:
372 revnum = int(revnum)
373 revisions.append(revnum)
374 except ValueError:
375 pass
376 return (revisions, next_marker, githash_svn_dict)
378 # Fetch the first list of revisions.
379 if last_known_rev:
380 revisions = []
381 # Optimization: Start paging at the last known revision (local cache).
382 next_marker = _GetMarkerForRev(last_known_rev)
383 # Optimization: Stop paging at the last known revision (remote).
384 last_change_rev = GetChromiumRevision(self, self.GetLastChangeURL())
385 if last_known_rev == last_change_rev:
386 return []
387 else:
388 (revisions, next_marker, new_dict) = _FetchAndParse(self.GetListingURL())
389 self.githash_svn_dict.update(new_dict)
390 last_change_rev = None
392 # If the result list was truncated, refetch with the next marker. Do this
393 # until an entire directory listing is done.
394 while next_marker:
395 sys.stdout.write('\rFetching revisions at marker %s' % next_marker)
396 sys.stdout.flush()
398 next_url = self.GetListingURL(next_marker)
399 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url)
400 revisions.extend(new_revisions)
401 self.githash_svn_dict.update(new_dict)
402 if last_change_rev and last_change_rev in new_revisions:
403 break
404 sys.stdout.write('\r')
405 sys.stdout.flush()
406 return revisions
408 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot):
409 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1
410 response = urllib.urlopen(json_url)
411 if response.getcode() == 200:
412 try:
413 data = json.loads(response.read()[4:])
414 except ValueError:
415 print 'ValueError for JSON URL: %s' % json_url
416 raise ValueError
417 else:
418 raise ValueError
419 if 'message' in data:
420 message = data['message'].split('\n')
421 message = [line for line in message if line.strip()]
422 search_pattern = re.compile(SEARCH_PATTERN[depot])
423 result = search_pattern.search(message[len(message)-1])
424 if result:
425 return result.group(1)
426 else:
427 if depot == 'chromium':
428 result = re.search(CHROMIUM_SEARCH_PATTERN_OLD,
429 message[len(message)-1])
430 if result:
431 return result.group(1)
432 print 'Failed to get svn revision number for %s' % git_sha1
433 raise ValueError
435 def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot):
436 def _RunGit(command, path):
437 command = ['git'] + command
438 shell = sys.platform.startswith('win')
439 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
440 stderr=subprocess.PIPE, cwd=path)
441 (output, _) = proc.communicate()
442 return (output, proc.returncode)
444 path = self.local_src_path
445 if depot == 'blink':
446 path = os.path.join(self.local_src_path, 'third_party', 'WebKit')
447 revision = None
448 try:
449 command = ['svn', 'find-rev', git_sha1]
450 (git_output, return_code) = _RunGit(command, path)
451 if not return_code:
452 revision = git_output.strip('\n')
453 except ValueError:
454 pass
455 if not revision:
456 command = ['log', '-n1', '--format=%s', git_sha1]
457 (git_output, return_code) = _RunGit(command, path)
458 if not return_code:
459 revision = re.match('SVN changes up to revision ([0-9]+)', git_output)
460 revision = revision.group(1) if revision else None
461 if revision:
462 return revision
463 raise ValueError
465 def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'):
466 if not self.local_src_path:
467 return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot)
468 else:
469 return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot)
471 def GetRevList(self):
472 """Gets the list of revision numbers between self.good_revision and
473 self.bad_revision."""
475 cache = {}
476 # The cache is stored in the same directory as bisect-builds.py
477 cache_filename = os.path.join(
478 os.path.abspath(os.path.dirname(__file__)),
479 '.bisect-builds-cache.json')
480 cache_dict_key = self.GetListingURL()
482 def _LoadBucketFromCache():
483 if self.use_local_cache:
484 try:
485 with open(cache_filename) as cache_file:
486 cache = json.load(cache_file)
487 revisions = cache.get(cache_dict_key, [])
488 githash_svn_dict = cache.get('githash_svn_dict', {})
489 if revisions:
490 print 'Loaded revisions %d-%d from %s' % (revisions[0],
491 revisions[-1], cache_filename)
492 return (revisions, githash_svn_dict)
493 except (EnvironmentError, ValueError):
494 pass
495 return ([], {})
497 def _SaveBucketToCache():
498 """Save the list of revisions and the git-svn mappings to a file.
499 The list of revisions is assumed to be sorted."""
500 if self.use_local_cache:
501 cache[cache_dict_key] = revlist_all
502 cache['githash_svn_dict'] = self.githash_svn_dict
503 try:
504 with open(cache_filename, 'w') as cache_file:
505 json.dump(cache, cache_file)
506 print 'Saved revisions %d-%d to %s' % (
507 revlist_all[0], revlist_all[-1], cache_filename)
508 except EnvironmentError:
509 pass
511 # Download the revlist and filter for just the range between good and bad.
512 minrev = min(self.good_revision, self.bad_revision)
513 maxrev = max(self.good_revision, self.bad_revision)
515 (revlist_all, self.githash_svn_dict) = _LoadBucketFromCache()
516 last_known_rev = revlist_all[-1] if revlist_all else 0
517 if last_known_rev < maxrev:
518 revlist_all.extend(map(int, self.ParseDirectoryIndex(last_known_rev)))
519 revlist_all = list(set(revlist_all))
520 revlist_all.sort()
521 _SaveBucketToCache()
523 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
525 # Set good and bad revisions to be legit revisions.
526 if revlist:
527 if self.good_revision < self.bad_revision:
528 self.good_revision = revlist[0]
529 self.bad_revision = revlist[-1]
530 else:
531 self.bad_revision = revlist[0]
532 self.good_revision = revlist[-1]
534 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
535 if self.base_url == WEBKIT_BASE_URL:
536 revlist_all.sort()
537 self.good_revision = FixChromiumRevForBlink(revlist,
538 revlist_all,
539 self,
540 self.good_revision)
541 self.bad_revision = FixChromiumRevForBlink(revlist,
542 revlist_all,
543 self,
544 self.bad_revision)
545 return revlist
547 def GetOfficialBuildsList(self):
548 """Gets the list of official build numbers between self.good_revision and
549 self.bad_revision."""
551 def CheckDepotToolsInPath():
552 delimiter = ';' if sys.platform.startswith('win') else ':'
553 path_list = os.environ['PATH'].split(delimiter)
554 for path in path_list:
555 if path.rstrip(os.path.sep).endswith('depot_tools'):
556 return path
557 return None
559 def RunGsutilCommand(args):
560 gsutil_path = CheckDepotToolsInPath()
561 if gsutil_path is None:
562 print ('Follow the instructions in this document '
563 'http://dev.chromium.org/developers/how-tos/install-depot-tools'
564 ' to install depot_tools and then try again.')
565 sys.exit(1)
566 gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil')
567 gsutil = subprocess.Popen([sys.executable, gsutil_path] + args,
568 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
569 env=None)
570 stdout, stderr = gsutil.communicate()
571 if gsutil.returncode:
572 if (re.findall(r'status[ |=]40[1|3]', stderr) or
573 stderr.startswith(CREDENTIAL_ERROR_MESSAGE)):
574 print ('Follow these steps to configure your credentials and try'
575 ' running the bisect-builds.py again.:\n'
576 ' 1. Run "python %s config" and follow its instructions.\n'
577 ' 2. If you have a @google.com account, use that account.\n'
578 ' 3. For the project-id, just enter 0.' % gsutil_path)
579 sys.exit(1)
580 else:
581 raise Exception('Error running the gsutil command: %s' % stderr)
582 return stdout
584 def GsutilList(bucket):
585 query = 'gs://%s/' % bucket
586 stdout = RunGsutilCommand(['ls', query])
587 return [url[len(query):].strip('/') for url in stdout.splitlines()]
589 # Download the revlist and filter for just the range between good and bad.
590 minrev = min(self.good_revision, self.bad_revision)
591 maxrev = max(self.good_revision, self.bad_revision)
592 if self.android:
593 gs_bucket_name = ANDROID_BUCKET_NAME
594 else:
595 gs_bucket_name = GS_BUCKET_NAME
596 build_numbers = GsutilList(gs_bucket_name)
597 revision_re = re.compile(r'(\d\d\.\d\.\d{4}\.\d+)')
598 build_numbers = filter(lambda b: revision_re.search(b), build_numbers)
599 final_list = []
600 parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
601 connection = httplib.HTTPConnection(GOOGLE_APIS_URL)
602 for build_number in sorted(parsed_build_numbers):
603 if build_number > maxrev:
604 break
605 if build_number < minrev:
606 continue
607 path = ('/' + gs_bucket_name + '/' + str(build_number) + '/' +
608 self._listing_platform_dir + self.archive_name)
609 connection.request('HEAD', path)
610 response = connection.getresponse()
611 if response.status == 200:
612 final_list.append(str(build_number))
613 response.read()
614 connection.close()
615 return final_list
617 def UnzipFilenameToDir(filename, directory):
618 """Unzip |filename| to |directory|."""
619 cwd = os.getcwd()
620 if not os.path.isabs(filename):
621 filename = os.path.join(cwd, filename)
622 zf = zipfile.ZipFile(filename)
623 # Make base.
624 if not os.path.isdir(directory):
625 os.mkdir(directory)
626 os.chdir(directory)
627 # Extract files.
628 for info in zf.infolist():
629 name = info.filename
630 if name.endswith('/'): # dir
631 if not os.path.isdir(name):
632 os.makedirs(name)
633 else: # file
634 directory = os.path.dirname(name)
635 if not os.path.isdir(directory):
636 os.makedirs(directory)
637 out = open(name, 'wb')
638 out.write(zf.read(name))
639 out.close()
640 # Set permissions. Permission info in external_attr is shifted 16 bits.
641 os.chmod(name, info.external_attr >> 16L)
642 os.chdir(cwd)
645 def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
646 """Downloads and unzips revision |rev|.
647 @param context A PathContext instance.
648 @param rev The Chromium revision number/tag to download.
649 @param filename The destination for the downloaded file.
650 @param quit_event A threading.Event which will be set by the master thread to
651 indicate that the download should be aborted.
652 @param progress_event A threading.Event which will be set by the master thread
653 to indicate that the progress of the download should be
654 displayed.
656 def ReportHook(blocknum, blocksize, totalsize):
657 if quit_event and quit_event.isSet():
658 raise RuntimeError('Aborting download of revision %s' % str(rev))
659 if progress_event and progress_event.isSet():
660 size = blocknum * blocksize
661 if totalsize == -1: # Total size not known.
662 progress = 'Received %d bytes' % size
663 else:
664 size = min(totalsize, size)
665 progress = 'Received %d of %d bytes, %.2f%%' % (
666 size, totalsize, 100.0 * size / totalsize)
667 # Send a \r to let all progress messages use just one line of output.
668 sys.stdout.write('\r' + progress)
669 sys.stdout.flush()
671 download_url = context.GetDownloadURL(rev)
672 try:
673 urllib.urlretrieve(download_url, filename, ReportHook)
674 if progress_event and progress_event.isSet():
675 print
676 except RuntimeError:
677 pass
680 def IsADBInstalled():
681 """Checks if ADB is in the environment path."""
682 try:
683 adb_output = subprocess.check_output(['adb', 'version'])
684 return ('Android Debug Bridge' in adb_output)
685 except OSError:
686 return False
689 def GetAndroidDeviceList():
690 """Returns the list of Android devices attached to the host machine."""
691 lines = subprocess.check_output(['adb', 'devices']).split('\n')[1:]
692 devices = []
693 for line in lines:
694 m = re.match('^(.*?)\s+device$', line)
695 if not m:
696 continue
697 devices.append(m.group(1))
698 return devices
701 def RunAndroidRevision(context, revision, apk_file):
702 """Given a Chrome apk, install it on a local device, and launch Chrome."""
703 devices = GetAndroidDeviceList()
704 if len(devices) is not 1:
705 sys.exit('Please have 1 Android device plugged in. %d devices found'
706 % len(devices))
708 devnull = open(os.devnull, 'w')
709 package_name = ANDROID_CHROME_PACKAGE_NAME[context.android_apk]
711 print 'Installing new Chrome version...'
712 subprocess.call(['adb', 'install', '-r', '-d', apk_file],
713 stdout=devnull, stderr=devnull)
715 print 'Launching Chrome...\n'
716 subprocess.call(['adb', 'shell', 'am', 'start', '-a',
717 'android.intent.action.VIEW', '-n', package_name +
718 '/com.google.android.apps.chrome.Main'], stdout=devnull, stderr=devnull)
721 def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
722 """Given a zipped revision, unzip it and run the test."""
723 print 'Trying revision %s...' % str(revision)
725 if context.android:
726 RunAndroidRevision(context, revision, zip_file)
727 # TODO(mikecase): Support running command to auto-bisect Android.
728 return (None, None, None)
730 # Create a temp directory and unzip the revision into it.
731 cwd = os.getcwd()
732 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
733 UnzipFilenameToDir(zip_file, tempdir)
735 # Hack: Chrome OS archives are missing icudtl.dat; try to copy it from
736 # the local directory.
737 if context.platform == 'chromeos':
738 icudtl_path = 'third_party/icu/source/data/in/icudtl.dat'
739 if not os.access(icudtl_path, os.F_OK):
740 print 'Couldn\'t find: ' + icudtl_path
741 sys.exit()
742 os.system('cp %s %s/chrome-linux/' % (icudtl_path, tempdir))
744 os.chdir(tempdir)
746 # Run the build as many times as specified.
747 testargs = ['--user-data-dir=%s' % profile] + args
748 # The sandbox must be run as root on Official Chrome, so bypass it.
749 if ((context.is_official or context.flash_path or context.pdf_path) and
750 context.platform.startswith('linux')):
751 testargs.append('--no-sandbox')
752 if context.flash_path:
753 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
754 # We have to pass a large enough Flash version, which currently needs not
755 # be correct. Instead of requiring the user of the script to figure out and
756 # pass the correct version we just spoof it.
757 testargs.append('--ppapi-flash-version=99.9.999.999')
759 # TODO(vitalybuka): Remove in the future. See crbug.com/395687.
760 if context.pdf_path:
761 shutil.copy(context.pdf_path,
762 os.path.dirname(context.GetLaunchPath(revision)))
763 testargs.append('--enable-print-preview')
765 runcommand = []
766 for token in shlex.split(command):
767 if token == '%a':
768 runcommand.extend(testargs)
769 else:
770 runcommand.append(
771 token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))).
772 replace('%s', ' '.join(testargs)))
774 results = []
775 for _ in range(num_runs):
776 subproc = subprocess.Popen(runcommand,
777 bufsize=-1,
778 stdout=subprocess.PIPE,
779 stderr=subprocess.PIPE)
780 (stdout, stderr) = subproc.communicate()
781 results.append((subproc.returncode, stdout, stderr))
783 os.chdir(cwd)
784 try:
785 shutil.rmtree(tempdir, True)
786 except Exception:
787 pass
789 for (returncode, stdout, stderr) in results:
790 if returncode:
791 return (returncode, stdout, stderr)
792 return results[0]
795 # The arguments official_builds, status, stdout and stderr are unused.
796 # They are present here because this function is passed to Bisect which then
797 # calls it with 5 arguments.
798 # pylint: disable=W0613
799 def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
800 """Asks the user whether build |rev| is good or bad."""
801 # Loop until we get a response that we can parse.
802 while True:
803 response = raw_input('Revision %s is '
804 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
805 str(rev))
806 if response and response in ('g', 'b', 'r', 'u'):
807 return response
808 if response and response == 'q':
809 raise SystemExit()
812 def IsGoodASANBuild(rev, official_builds, status, stdout, stderr):
813 """Determine if an ASAN build |rev| is good or bad
815 Will examine stderr looking for the error message emitted by ASAN. If not
816 found then will fallback to asking the user."""
817 if stderr:
818 bad_count = 0
819 for line in stderr.splitlines():
820 print line
821 if line.find('ERROR: AddressSanitizer:') != -1:
822 bad_count += 1
823 if bad_count > 0:
824 print 'Revision %d determined to be bad.' % rev
825 return 'b'
826 return AskIsGoodBuild(rev, official_builds, status, stdout, stderr)
828 class DownloadJob(object):
829 """DownloadJob represents a task to download a given Chromium revision."""
831 def __init__(self, context, name, rev, zip_file):
832 super(DownloadJob, self).__init__()
833 # Store off the input parameters.
834 self.context = context
835 self.name = name
836 self.rev = rev
837 self.zip_file = zip_file
838 self.quit_event = threading.Event()
839 self.progress_event = threading.Event()
840 self.thread = None
842 def Start(self):
843 """Starts the download."""
844 fetchargs = (self.context,
845 self.rev,
846 self.zip_file,
847 self.quit_event,
848 self.progress_event)
849 self.thread = threading.Thread(target=FetchRevision,
850 name=self.name,
851 args=fetchargs)
852 self.thread.start()
854 def Stop(self):
855 """Stops the download which must have been started previously."""
856 assert self.thread, 'DownloadJob must be started before Stop is called.'
857 self.quit_event.set()
858 self.thread.join()
859 os.unlink(self.zip_file)
861 def WaitFor(self):
862 """Prints a message and waits for the download to complete. The download
863 must have been started previously."""
864 assert self.thread, 'DownloadJob must be started before WaitFor is called.'
865 print 'Downloading revision %s...' % str(self.rev)
866 self.progress_event.set() # Display progress of download.
867 self.thread.join()
870 def Bisect(context,
871 num_runs=1,
872 command='%p %a',
873 try_args=(),
874 profile=None,
875 interactive=True,
876 evaluate=AskIsGoodBuild):
877 """Given known good and known bad revisions, run a binary search on all
878 archived revisions to determine the last known good revision.
880 @param context PathContext object initialized with user provided parameters.
881 @param num_runs Number of times to run each build for asking good/bad.
882 @param try_args A tuple of arguments to pass to the test application.
883 @param profile The name of the user profile to run with.
884 @param interactive If it is false, use command exit code for good or bad
885 judgment of the argument build.
886 @param evaluate A function which returns 'g' if the argument build is good,
887 'b' if it's bad or 'u' if unknown.
889 Threading is used to fetch Chromium revisions in the background, speeding up
890 the user's experience. For example, suppose the bounds of the search are
891 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
892 whether revision 50 is good or bad, the next revision to check will be either
893 25 or 75. So, while revision 50 is being checked, the script will download
894 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
895 known:
897 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
898 is run on rev 75.
900 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
901 is run on rev 25.
904 if not profile:
905 profile = 'profile'
907 good_rev = context.good_revision
908 bad_rev = context.bad_revision
909 cwd = os.getcwd()
911 print 'Downloading list of known revisions...',
912 if not context.use_local_cache and not context.is_official:
913 print '(use --use-local-cache to cache and re-use the list of revisions)'
914 else:
915 print
916 _GetDownloadPath = lambda rev: os.path.join(cwd,
917 '%s-%s' % (str(rev), context.archive_name))
918 if context.is_official:
919 revlist = context.GetOfficialBuildsList()
920 else:
921 revlist = context.GetRevList()
923 # Get a list of revisions to bisect across.
924 if len(revlist) < 2: # Don't have enough builds to bisect.
925 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
926 raise RuntimeError(msg)
928 # Figure out our bookends and first pivot point; fetch the pivot revision.
929 minrev = 0
930 maxrev = len(revlist) - 1
931 pivot = maxrev / 2
932 rev = revlist[pivot]
933 zip_file = _GetDownloadPath(rev)
934 fetch = DownloadJob(context, 'initial_fetch', rev, zip_file)
935 fetch.Start()
936 fetch.WaitFor()
938 # Binary search time!
939 while fetch and fetch.zip_file and maxrev - minrev > 1:
940 if bad_rev < good_rev:
941 min_str, max_str = 'bad', 'good'
942 else:
943 min_str, max_str = 'good', 'bad'
944 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str,
945 revlist[maxrev], max_str)
947 # Pre-fetch next two possible pivots
948 # - down_pivot is the next revision to check if the current revision turns
949 # out to be bad.
950 # - up_pivot is the next revision to check if the current revision turns
951 # out to be good.
952 down_pivot = int((pivot - minrev) / 2) + minrev
953 down_fetch = None
954 if down_pivot != pivot and down_pivot != minrev:
955 down_rev = revlist[down_pivot]
956 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
957 _GetDownloadPath(down_rev))
958 down_fetch.Start()
960 up_pivot = int((maxrev - pivot) / 2) + pivot
961 up_fetch = None
962 if up_pivot != pivot and up_pivot != maxrev:
963 up_rev = revlist[up_pivot]
964 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
965 _GetDownloadPath(up_rev))
966 up_fetch.Start()
968 # Run test on the pivot revision.
969 status = None
970 stdout = None
971 stderr = None
972 try:
973 (status, stdout, stderr) = RunRevision(context,
974 rev,
975 fetch.zip_file,
976 profile,
977 num_runs,
978 command,
979 try_args)
980 except Exception, e:
981 print >> sys.stderr, e
983 # Call the evaluate function to see if the current revision is good or bad.
984 # On that basis, kill one of the background downloads and complete the
985 # other, as described in the comments above.
986 try:
987 if not interactive:
988 if status:
989 answer = 'b'
990 print 'Bad revision: %s' % rev
991 else:
992 answer = 'g'
993 print 'Good revision: %s' % rev
994 else:
995 answer = evaluate(rev, context.is_official, status, stdout, stderr)
996 if ((answer == 'g' and good_rev < bad_rev)
997 or (answer == 'b' and bad_rev < good_rev)):
998 fetch.Stop()
999 minrev = pivot
1000 if down_fetch:
1001 down_fetch.Stop() # Kill the download of the older revision.
1002 fetch = None
1003 if up_fetch:
1004 up_fetch.WaitFor()
1005 pivot = up_pivot
1006 fetch = up_fetch
1007 elif ((answer == 'b' and good_rev < bad_rev)
1008 or (answer == 'g' and bad_rev < good_rev)):
1009 fetch.Stop()
1010 maxrev = pivot
1011 if up_fetch:
1012 up_fetch.Stop() # Kill the download of the newer revision.
1013 fetch = None
1014 if down_fetch:
1015 down_fetch.WaitFor()
1016 pivot = down_pivot
1017 fetch = down_fetch
1018 elif answer == 'r':
1019 pass # Retry requires no changes.
1020 elif answer == 'u':
1021 # Nuke the revision from the revlist and choose a new pivot.
1022 fetch.Stop()
1023 revlist.pop(pivot)
1024 maxrev -= 1 # Assumes maxrev >= pivot.
1026 if maxrev - minrev > 1:
1027 # Alternate between using down_pivot or up_pivot for the new pivot
1028 # point, without affecting the range. Do this instead of setting the
1029 # pivot to the midpoint of the new range because adjacent revisions
1030 # are likely affected by the same issue that caused the (u)nknown
1031 # response.
1032 if up_fetch and down_fetch:
1033 fetch = [up_fetch, down_fetch][len(revlist) % 2]
1034 elif up_fetch:
1035 fetch = up_fetch
1036 else:
1037 fetch = down_fetch
1038 fetch.WaitFor()
1039 if fetch == up_fetch:
1040 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
1041 else:
1042 pivot = down_pivot
1043 zip_file = fetch.zip_file
1045 if down_fetch and fetch != down_fetch:
1046 down_fetch.Stop()
1047 if up_fetch and fetch != up_fetch:
1048 up_fetch.Stop()
1049 else:
1050 assert False, 'Unexpected return value from evaluate(): ' + answer
1051 except SystemExit:
1052 print 'Cleaning up...'
1053 for f in [_GetDownloadPath(revlist[down_pivot]),
1054 _GetDownloadPath(revlist[up_pivot])]:
1055 try:
1056 os.unlink(f)
1057 except OSError:
1058 pass
1059 sys.exit(0)
1061 rev = revlist[pivot]
1063 return (revlist[minrev], revlist[maxrev], context)
1066 def GetBlinkDEPSRevisionForChromiumRevision(self, rev):
1067 """Returns the blink revision that was in REVISIONS file at
1068 chromium revision |rev|."""
1070 def _GetBlinkRev(url, blink_re):
1071 m = blink_re.search(url.read())
1072 url.close()
1073 if m:
1074 return m.group(1)
1076 url = urllib.urlopen(DEPS_FILE_OLD % rev)
1077 if url.getcode() == 200:
1078 # . doesn't match newlines without re.DOTALL, so this is safe.
1079 blink_re = re.compile(r'webkit_revision\D*(\d+)')
1080 return int(_GetBlinkRev(url, blink_re))
1081 else:
1082 url = urllib.urlopen(DEPS_FILE_NEW % GetGitHashFromSVNRevision(rev))
1083 if url.getcode() == 200:
1084 blink_re = re.compile(r'webkit_revision\D*\d+;\D*\d+;(\w+)')
1085 blink_git_sha = _GetBlinkRev(url, blink_re)
1086 return self.GetSVNRevisionFromGitHash(blink_git_sha, 'blink')
1087 raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
1090 def GetBlinkRevisionForChromiumRevision(context, rev):
1091 """Returns the blink revision that was in REVISIONS file at
1092 chromium revision |rev|."""
1093 def _IsRevisionNumber(revision):
1094 if isinstance(revision, int):
1095 return True
1096 else:
1097 return revision.isdigit()
1098 if str(rev) in context.githash_svn_dict:
1099 rev = context.githash_svn_dict[str(rev)]
1100 file_url = '%s/%s%s/REVISIONS' % (context.base_url,
1101 context._listing_platform_dir, rev)
1102 url = urllib.urlopen(file_url)
1103 if url.getcode() == 200:
1104 try:
1105 data = json.loads(url.read())
1106 except ValueError:
1107 print 'ValueError for JSON URL: %s' % file_url
1108 raise ValueError
1109 else:
1110 raise ValueError
1111 url.close()
1112 if 'webkit_revision' in data:
1113 blink_rev = data['webkit_revision']
1114 if not _IsRevisionNumber(blink_rev):
1115 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
1116 return blink_rev
1117 else:
1118 raise Exception('Could not get blink revision for cr rev %d' % rev)
1121 def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
1122 """Returns the chromium revision that has the correct blink revision
1123 for blink bisect, DEPS and REVISIONS file might not match since
1124 blink snapshots point to tip of tree blink.
1125 Note: The revisions_final variable might get modified to include
1126 additional revisions."""
1127 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(self, rev)
1129 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
1130 idx = revisions.index(rev)
1131 if idx > 0:
1132 rev = revisions[idx-1]
1133 if rev not in revisions_final:
1134 revisions_final.insert(0, rev)
1136 revisions_final.sort()
1137 return rev
1140 def GetChromiumRevision(context, url):
1141 """Returns the chromium revision read from given URL."""
1142 try:
1143 # Location of the latest build revision number
1144 latest_revision = urllib.urlopen(url).read()
1145 if latest_revision.isdigit():
1146 return int(latest_revision)
1147 return context.GetSVNRevisionFromGitHash(latest_revision)
1148 except Exception:
1149 print 'Could not determine latest revision. This could be bad...'
1150 return 999999999
1152 def GetGitHashFromSVNRevision(svn_revision):
1153 crrev_url = CRREV_URL + str(svn_revision)
1154 url = urllib.urlopen(crrev_url)
1155 if url.getcode() == 200:
1156 data = json.loads(url.read())
1157 if 'git_sha' in data:
1158 return data['git_sha']
1160 def PrintChangeLog(min_chromium_rev, max_chromium_rev):
1161 """Prints the changelog URL."""
1163 print (' ' + CHANGELOG_URL % (GetGitHashFromSVNRevision(min_chromium_rev),
1164 GetGitHashFromSVNRevision(max_chromium_rev)))
1167 def main():
1168 usage = ('%prog [options] [-- chromium-options]\n'
1169 'Perform binary search on the snapshot builds to find a minimal\n'
1170 'range of revisions where a behavior change happened. The\n'
1171 'behaviors are described as "good" and "bad".\n'
1172 'It is NOT assumed that the behavior of the later revision is\n'
1173 'the bad one.\n'
1174 '\n'
1175 'Revision numbers should use\n'
1176 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
1177 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
1178 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
1179 ' for earlier revs.\n'
1180 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
1181 ' are incorrect, they are from branches.\n'
1182 '\n'
1183 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
1184 parser = optparse.OptionParser(usage=usage)
1185 # Strangely, the default help output doesn't include the choice list.
1186 choices = ['mac', 'mac64', 'win', 'win64', 'linux', 'linux64', 'linux-arm',
1187 'android-arm', 'android-arm-64', 'android-x86', 'android-x86-64',
1188 'chromeos']
1189 apk_choices = ['Chrome.apk', 'ChromeBeta.apk', 'ChromeCanary.apk',
1190 'ChromeDev.apk', 'ChromeStable.apk']
1191 parser.add_option('-a', '--archive',
1192 choices=choices,
1193 help='The buildbot archive to bisect [%s].' %
1194 '|'.join(choices))
1195 parser.add_option('-o',
1196 action='store_true',
1197 dest='official_builds',
1198 help='Bisect across official Chrome builds (internal '
1199 'only) instead of Chromium archives.')
1200 parser.add_option('-b', '--bad',
1201 type='str',
1202 help='A bad revision to start bisection. '
1203 'May be earlier or later than the good revision. '
1204 'Default is HEAD.')
1205 parser.add_option('-f', '--flash_path',
1206 type='str',
1207 help='Absolute path to a recent Adobe Pepper Flash '
1208 'binary to be used in this bisection (e.g. '
1209 'on Windows C:\...\pepflashplayer.dll and on Linux '
1210 '/opt/google/chrome/PepperFlash/'
1211 'libpepflashplayer.so).')
1212 parser.add_option('-d', '--pdf_path',
1213 type='str',
1214 help='Absolute path to a recent PDF plugin '
1215 'binary to be used in this bisection (e.g. '
1216 'on Windows C:\...\pdf.dll and on Linux '
1217 '/opt/google/chrome/libpdf.so). Option also enables '
1218 'print preview.')
1219 parser.add_option('-g', '--good',
1220 type='str',
1221 help='A good revision to start bisection. ' +
1222 'May be earlier or later than the bad revision. ' +
1223 'Default is 0.')
1224 parser.add_option('-p', '--profile', '--user-data-dir',
1225 type='str',
1226 default='profile',
1227 help='Profile to use; this will not reset every run. '
1228 'Defaults to a clean profile.')
1229 parser.add_option('-t', '--times',
1230 type='int',
1231 default=1,
1232 help='Number of times to run each build before asking '
1233 'if it\'s good or bad. Temporary profiles are reused.')
1234 parser.add_option('-c', '--command',
1235 type='str',
1236 default='%p %a',
1237 help='Command to execute. %p and %a refer to Chrome '
1238 'executable and specified extra arguments '
1239 'respectively. Use %s to specify all extra arguments '
1240 'as one string. Defaults to "%p %a". Note that any '
1241 'extra paths specified should be absolute.')
1242 parser.add_option('-l', '--blink',
1243 action='store_true',
1244 help='Use Blink bisect instead of Chromium. ')
1245 parser.add_option('', '--not-interactive',
1246 action='store_true',
1247 default=False,
1248 help='Use command exit code to tell good/bad revision.')
1249 parser.add_option('--asan',
1250 dest='asan',
1251 action='store_true',
1252 default=False,
1253 help='Allow the script to bisect ASAN builds')
1254 parser.add_option('--use-local-cache',
1255 dest='use_local_cache',
1256 action='store_true',
1257 default=False,
1258 help='Use a local file in the current directory to cache '
1259 'a list of known revisions to speed up the '
1260 'initialization of this script.')
1261 parser.add_option('--adb-path',
1262 dest='adb_path',
1263 help='Absolute path to adb. If you do not have adb in your '
1264 'enviroment PATH and want to bisect Android then '
1265 'you need to specify the path here.')
1266 parser.add_option('--apk',
1267 dest='apk',
1268 choices=apk_choices,
1269 help='Name of apk you want to bisect. [%s]' %
1270 '|'.join(apk_choices))
1272 (opts, args) = parser.parse_args()
1274 if opts.archive is None:
1275 print 'Error: missing required parameter: --archive'
1276 print
1277 parser.print_help()
1278 return 1
1280 if opts.asan:
1281 supported_platforms = ['linux', 'mac', 'win']
1282 if opts.archive not in supported_platforms:
1283 print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
1284 '|'.join(supported_platforms))
1285 return 1
1286 if opts.official_builds:
1287 print 'Error: Do not yet support bisecting official ASAN builds.'
1288 return 1
1290 if opts.asan:
1291 base_url = ASAN_BASE_URL
1292 elif opts.blink:
1293 base_url = WEBKIT_BASE_URL
1294 else:
1295 base_url = CHROMIUM_BASE_URL
1297 # Create the context. Initialize 0 for the revisions as they are set below.
1298 context = PathContext(base_url, opts.archive, opts.good, opts.bad,
1299 opts.official_builds, opts.asan, opts.use_local_cache,
1300 opts.flash_path, opts.pdf_path, opts.apk)
1302 # TODO(mikecase): Add support to bisect on nonofficial builds for Android.
1303 if context.android and not opts.official_builds:
1304 sys.exit('Can only bisect on official builds for Android.')
1306 # If bisecting Android, we make sure we have ADB setup.
1307 if context.android:
1308 if opts.adb_path:
1309 os.environ['PATH'] = '%s:%s' % (os.path.dirname(opts.adb_path),
1310 os.environ['PATH'])
1311 if not IsADBInstalled():
1312 sys.exit('Please have "adb" in PATH or use adb_path command line option'
1313 'to bisect Android builds.')
1315 # Pick a starting point, try to get HEAD for this.
1316 if not opts.bad:
1317 context.bad_revision = '999.0.0.0'
1318 context.bad_revision = GetChromiumRevision(
1319 context, context.GetLastChangeURL())
1321 # Find out when we were good.
1322 if not opts.good:
1323 context.good_revision = '0.0.0.0' if opts.official_builds else 0
1325 if opts.flash_path:
1326 msg = 'Could not find Flash binary at %s' % opts.flash_path
1327 assert os.path.exists(opts.flash_path), msg
1329 if opts.pdf_path:
1330 msg = 'Could not find PDF binary at %s' % opts.pdf_path
1331 assert os.path.exists(opts.pdf_path), msg
1333 if opts.official_builds:
1334 context.good_revision = LooseVersion(context.good_revision)
1335 context.bad_revision = LooseVersion(context.bad_revision)
1336 else:
1337 context.good_revision = int(context.good_revision)
1338 context.bad_revision = int(context.bad_revision)
1340 if opts.times < 1:
1341 print('Number of times to run (%d) must be greater than or equal to 1.' %
1342 opts.times)
1343 parser.print_help()
1344 return 1
1346 if opts.asan:
1347 evaluator = IsGoodASANBuild
1348 else:
1349 evaluator = AskIsGoodBuild
1351 # Save these revision numbers to compare when showing the changelog URL
1352 # after the bisect.
1353 good_rev = context.good_revision
1354 bad_rev = context.bad_revision
1356 (min_chromium_rev, max_chromium_rev, context) = Bisect(
1357 context, opts.times, opts.command, args, opts.profile,
1358 not opts.not_interactive, evaluator)
1360 # Get corresponding blink revisions.
1361 try:
1362 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1363 min_chromium_rev)
1364 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1365 max_chromium_rev)
1366 except Exception:
1367 # Silently ignore the failure.
1368 min_blink_rev, max_blink_rev = 0, 0
1370 if opts.blink:
1371 # We're done. Let the user know the results in an official manner.
1372 if good_rev > bad_rev:
1373 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
1374 else:
1375 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
1377 print 'BLINK CHANGELOG URL:'
1378 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
1380 else:
1381 # We're done. Let the user know the results in an official manner.
1382 if good_rev > bad_rev:
1383 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
1384 str(max_chromium_rev))
1385 else:
1386 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
1387 str(max_chromium_rev))
1388 if min_blink_rev != max_blink_rev:
1389 print ('NOTE: There is a Blink roll in the range, '
1390 'you might also want to do a Blink bisect.')
1392 print 'CHANGELOG URL:'
1393 if opts.official_builds:
1394 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
1395 else:
1396 PrintChangeLog(min_chromium_rev, max_chromium_rev)
1399 if __name__ == '__main__':
1400 sys.exit(main())