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.
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')
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')
49 DEPS_FILE_OLD
= ('http://src.chromium.org/viewvc/chrome/trunk/src/'
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+) ')
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 ###############################################################################
118 from distutils
.version
import LooseVersion
119 from xml
.etree
import ElementTree
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
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
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
)
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'
183 raise Exception('Invalid platform: %s' % self
.platform
)
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
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':
247 def GetListingURL(self
, marker
=None):
248 """Returns the URL for a directory listing, with an optional marker."""
251 marker_param
= '&marker=' + str(marker
)
253 prefix
= '%s-%s' % (self
.GetASANPlatformDir(), self
.build_type
)
254 return self
.base_url
+ '/?delimiter=&prefix=' + prefix
+ marker_param
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."""
262 return '%s/%s-%s/%s-%d.zip' % (
263 ASAN_BASE_URL
, self
.GetASANPlatformDir(), self
.build_type
,
264 self
.GetASANBaseName(), revision
)
267 official_base_url
= ANDROID_OFFICIAL_BASE_URL
269 official_base_url
= OFFICIAL_BASE_URL
270 return '%s/%s/%s%s' % (
271 official_base_url
, revision
, self
._listing
_platform
_dir
,
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(),
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."""
295 extract_dir
= '%s-%d' % (self
.GetASANBaseName(), revision
)
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
304 def _GetMarkerForRev(revision
):
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
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('}')
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
329 prefix_len
= len(document
.find(namespace
+ 'Prefix').text
)
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.
336 githash_svn_dict
= {}
338 asan_regex
= re
.compile(r
'.*%s-(\d+)\.zip$' % (self
.GetASANBaseName()))
339 # Non ASAN builds are in a <revision> directory. The ASAN builds are
341 all_prefixes
= document
.findall(namespace
+ 'Contents/' +
343 for prefix
in all_prefixes
:
344 m
= asan_regex
.match(prefix
.text
)
347 revisions
.append(int(m
.group(1)))
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]
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.
369 revnum
= self
.GetSVNRevisionFromGitHash(git_hash
)
370 githash_svn_dict
[revnum
] = git_hash
371 if revnum
is not None:
373 revisions
.append(revnum
)
376 return (revisions
, next_marker
, githash_svn_dict
)
378 # Fetch the first list of 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
:
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.
395 sys
.stdout
.write('\rFetching revisions at marker %s' % next_marker
)
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
:
404 sys
.stdout
.write('\r')
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:
413 data
= json
.loads(response
.read()[4:])
415 print 'ValueError for JSON URL: %s' % json_url
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])
425 return result
.group(1)
427 if depot
== 'chromium':
428 result
= re
.search(CHROMIUM_SEARCH_PATTERN_OLD
,
429 message
[len(message
)-1])
431 return result
.group(1)
432 print 'Failed to get svn revision number for %s' % git_sha1
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
446 path
= os
.path
.join(self
.local_src_path
, 'third_party', 'WebKit')
449 command
= ['svn', 'find-rev', git_sha1
]
450 (git_output
, return_code
) = _RunGit(command
, path
)
452 revision
= git_output
.strip('\n')
456 command
= ['log', '-n1', '--format=%s', git_sha1
]
457 (git_output
, return_code
) = _RunGit(command
, path
)
459 revision
= re
.match('SVN changes up to revision ([0-9]+)', git_output
)
460 revision
= revision
.group(1) if revision
else None
465 def GetSVNRevisionFromGitHash(self
, git_sha1
, depot
='chromium'):
466 if not self
.local_src_path
:
467 return self
._GetSVNRevisionFromGitHashWithoutGitCheckout
(git_sha1
, depot
)
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."""
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
:
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', {})
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):
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
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:
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
))
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.
527 if self
.good_revision
< self
.bad_revision
:
528 self
.good_revision
= revlist
[0]
529 self
.bad_revision
= revlist
[-1]
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
:
537 self
.good_revision
= FixChromiumRevForBlink(revlist
,
541 self
.bad_revision
= FixChromiumRevForBlink(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'):
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.')
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
,
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
)
581 raise Exception('Error running the gsutil command: %s' % stderr
)
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
)
593 gs_bucket_name
= ANDROID_BUCKET_NAME
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
)
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
:
605 if build_number
< minrev
:
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
))
617 def UnzipFilenameToDir(filename
, directory
):
618 """Unzip |filename| to |directory|."""
620 if not os
.path
.isabs(filename
):
621 filename
= os
.path
.join(cwd
, filename
)
622 zf
= zipfile
.ZipFile(filename
)
624 if not os
.path
.isdir(directory
):
628 for info
in zf
.infolist():
630 if name
.endswith('/'): # dir
631 if not os
.path
.isdir(name
):
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
))
640 # Set permissions. Permission info in external_attr is shifted 16 bits.
641 os
.chmod(name
, info
.external_attr
>> 16L)
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
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
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
)
671 download_url
= context
.GetDownloadURL(rev
)
673 urllib
.urlretrieve(download_url
, filename
, ReportHook
)
674 if progress_event
and progress_event
.isSet():
680 def IsADBInstalled():
681 """Checks if ADB is in the environment path."""
683 adb_output
= subprocess
.check_output(['adb', 'version'])
684 return ('Android Debug Bridge' in adb_output
)
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:]
694 m
= re
.match('^(.*?)\s+device$', line
)
697 devices
.append(m
.group(1))
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'
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
)
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.
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
742 os
.system('cp %s %s/chrome-linux/' % (icudtl_path
, 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.
761 shutil
.copy(context
.pdf_path
,
762 os
.path
.dirname(context
.GetLaunchPath(revision
)))
763 testargs
.append('--enable-print-preview')
766 for token
in shlex
.split(command
):
768 runcommand
.extend(testargs
)
771 token
.replace('%p', os
.path
.abspath(context
.GetLaunchPath(revision
))).
772 replace('%s', ' '.join(testargs
)))
775 for _
in range(num_runs
):
776 subproc
= subprocess
.Popen(runcommand
,
778 stdout
=subprocess
.PIPE
,
779 stderr
=subprocess
.PIPE
)
780 (stdout
, stderr
) = subproc
.communicate()
781 results
.append((subproc
.returncode
, stdout
, stderr
))
785 shutil
.rmtree(tempdir
, True)
789 for (returncode
, stdout
, stderr
) in results
:
791 return (returncode
, stdout
, stderr
)
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.
803 response
= raw_input('Revision %s is '
804 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
806 if response
and response
in ('g', 'b', 'r', 'u'):
808 if response
and response
== 'q':
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."""
819 for line
in stderr
.splitlines():
821 if line
.find('ERROR: AddressSanitizer:') != -1:
824 print 'Revision %d determined to be bad.' % rev
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
837 self
.zip_file
= zip_file
838 self
.quit_event
= threading
.Event()
839 self
.progress_event
= threading
.Event()
843 """Starts the download."""
844 fetchargs
= (self
.context
,
849 self
.thread
= threading
.Thread(target
=FetchRevision
,
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()
859 os
.unlink(self
.zip_file
)
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.
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
897 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
900 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
907 good_rev
= context
.good_revision
908 bad_rev
= context
.bad_revision
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)'
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()
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.
930 maxrev
= len(revlist
) - 1
933 zip_file
= _GetDownloadPath(rev
)
934 fetch
= DownloadJob(context
, 'initial_fetch', rev
, zip_file
)
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'
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
950 # - up_pivot is the next revision to check if the current revision turns
952 down_pivot
= int((pivot
- minrev
) / 2) + minrev
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
))
960 up_pivot
= int((maxrev
- pivot
) / 2) + pivot
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
))
968 # Run test on the pivot revision.
973 (status
, stdout
, stderr
) = RunRevision(context
,
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.
990 print 'Bad revision: %s' % rev
993 print 'Good revision: %s' % rev
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
)):
1001 down_fetch
.Stop() # Kill the download of the older revision.
1007 elif ((answer
== 'b' and good_rev
< bad_rev
)
1008 or (answer
== 'g' and bad_rev
< good_rev
)):
1012 up_fetch
.Stop() # Kill the download of the newer revision.
1015 down_fetch
.WaitFor()
1019 pass # Retry requires no changes.
1021 # Nuke the revision from the revlist and choose a new 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
1032 if up_fetch
and down_fetch
:
1033 fetch
= [up_fetch
, down_fetch
][len(revlist
) % 2]
1039 if fetch
== up_fetch
:
1040 pivot
= up_pivot
- 1 # Subtracts 1 because revlist was resized.
1043 zip_file
= fetch
.zip_file
1045 if down_fetch
and fetch
!= down_fetch
:
1047 if up_fetch
and fetch
!= up_fetch
:
1050 assert False, 'Unexpected return value from evaluate(): ' + answer
1052 print 'Cleaning up...'
1053 for f
in [_GetDownloadPath(revlist
[down_pivot
]),
1054 _GetDownloadPath(revlist
[up_pivot
])]:
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())
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
))
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):
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:
1105 data
= json
.loads(url
.read())
1107 print 'ValueError for JSON URL: %s' % file_url
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'))
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
)
1132 rev
= revisions
[idx
-1]
1133 if rev
not in revisions_final
:
1134 revisions_final
.insert(0, rev
)
1136 revisions_final
.sort()
1140 def GetChromiumRevision(context
, url
):
1141 """Returns the chromium revision read from given URL."""
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
)
1149 print 'Could not determine latest revision. This could be bad...'
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
)))
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'
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'
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',
1189 apk_choices
= ['Chrome.apk', 'ChromeBeta.apk', 'ChromeCanary.apk',
1190 'ChromeDev.apk', 'ChromeStable.apk']
1191 parser
.add_option('-a', '--archive',
1193 help='The buildbot archive to bisect [%s].' %
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',
1202 help='A bad revision to start bisection. '
1203 'May be earlier or later than the good revision. '
1205 parser
.add_option('-f', '--flash_path',
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',
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 '
1219 parser
.add_option('-g', '--good',
1221 help='A good revision to start bisection. ' +
1222 'May be earlier or later than the bad revision. ' +
1224 parser
.add_option('-p', '--profile', '--user-data-dir',
1227 help='Profile to use; this will not reset every run. '
1228 'Defaults to a clean profile.')
1229 parser
.add_option('-t', '--times',
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',
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',
1248 help='Use command exit code to tell good/bad revision.')
1249 parser
.add_option('--asan',
1251 action
='store_true',
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',
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',
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',
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'
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
))
1286 if opts
.official_builds
:
1287 print 'Error: Do not yet support bisecting official ASAN builds.'
1291 base_url
= ASAN_BASE_URL
1293 base_url
= WEBKIT_BASE_URL
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.
1309 os
.environ
['PATH'] = '%s:%s' % (os
.path
.dirname(opts
.adb_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.
1317 context
.bad_revision
= '999.0.0.0'
1318 context
.bad_revision
= GetChromiumRevision(
1319 context
, context
.GetLastChangeURL())
1321 # Find out when we were good.
1323 context
.good_revision
= '0.0.0.0' if opts
.official_builds
else 0
1326 msg
= 'Could not find Flash binary at %s' % opts
.flash_path
1327 assert os
.path
.exists(opts
.flash_path
), msg
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
)
1337 context
.good_revision
= int(context
.good_revision
)
1338 context
.bad_revision
= int(context
.bad_revision
)
1341 print('Number of times to run (%d) must be greater than or equal to 1.' %
1347 evaluator
= IsGoodASANBuild
1349 evaluator
= AskIsGoodBuild
1351 # Save these revision numbers to compare when showing the changelog URL
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.
1362 min_blink_rev
= GetBlinkRevisionForChromiumRevision(context
,
1364 max_blink_rev
= GetBlinkRevisionForChromiumRevision(context
,
1367 # Silently ignore the failure.
1368 min_blink_rev
, max_blink_rev
= 0, 0
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
))
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
)
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
))
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
)
1396 PrintChangeLog(min_chromium_rev
, max_chromium_rev
)
1399 if __name__
== '__main__':