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