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 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')
52 DEPS_FILE_OLD
= ('http://src.chromium.org/viewvc/chrome/trunk/src/'
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+) ')
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 ###############################################################################
121 from distutils
.version
import LooseVersion
122 from xml
.etree
import ElementTree
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
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
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
)
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
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
:
190 raise Exception('Invalid platform: %s' % self
.platform
)
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
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':
259 def GetListingURL(self
, marker
=None):
260 """Returns the URL for a directory listing, with an optional marker."""
263 marker_param
= '&marker=' + str(marker
)
265 prefix
= '%s-%s' % (self
.GetASANPlatformDir(), self
.build_type
)
266 return self
.base_url
+ '/?delimiter=&prefix=' + prefix
+ marker_param
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."""
274 return '%s/%s-%s/%s-%d.zip' % (
275 ASAN_BASE_URL
, self
.GetASANPlatformDir(), self
.build_type
,
276 self
.GetASANBaseName(), revision
)
279 official_base_url
= ANDROID_OFFICIAL_BASE_URL
281 official_base_url
= OFFICIAL_BASE_URL
282 return '%s/%s/%s%s' % (
283 official_base_url
, revision
, self
._listing
_platform
_dir
,
287 # These files need to be downloaded through gsutil.
288 return ('gs://%s/%s/%s' % (ANDROID_TOT_BUCKET_NAME
, revision
,
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(),
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."""
312 extract_dir
= '%s-%d' % (self
.GetASANBaseName(), revision
)
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
321 def _GetMarkerForRev(revision
):
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
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('}')
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
346 prefix_len
= len(document
.find(namespace
+ 'Prefix').text
)
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.
353 githash_svn_dict
= {}
355 asan_regex
= re
.compile(r
'.*%s-(\d+)\.zip$' % (self
.GetASANBaseName()))
356 # Non ASAN builds are in a <revision> directory. The ASAN builds are
358 all_prefixes
= document
.findall(namespace
+ 'Contents/' +
360 for prefix
in all_prefixes
:
361 m
= asan_regex
.match(prefix
.text
)
364 revisions
.append(int(m
.group(1)))
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]
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.
386 revnum
= self
.GetSVNRevisionFromGitHash(git_hash
)
387 githash_svn_dict
[revnum
] = git_hash
388 if revnum
is not None:
390 revisions
.append(revnum
)
393 return (revisions
, next_marker
, githash_svn_dict
)
395 # Fetch the first list of 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
:
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.
412 sys
.stdout
.write('\rFetching revisions at marker %s' % next_marker
)
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
:
421 sys
.stdout
.write('\r')
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:
430 data
= json
.loads(response
.read()[4:])
432 print 'ValueError for JSON URL: %s' % json_url
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])
442 return result
.group(1)
444 if depot
== 'chromium':
445 result
= re
.search(CHROMIUM_SEARCH_PATTERN_OLD
,
446 message
[len(message
)-1])
448 return result
.group(1)
449 print 'Failed to get svn revision number for %s' % git_sha1
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
463 path
= os
.path
.join(self
.local_src_path
, 'third_party', 'WebKit')
466 command
= ['svn', 'find-rev', git_sha1
]
467 (git_output
, return_code
) = _RunGit(command
, path
)
469 revision
= git_output
.strip('\n')
473 command
= ['log', '-n1', '--format=%s', git_sha1
]
474 (git_output
, return_code
) = _RunGit(command
, path
)
476 revision
= re
.match('SVN changes up to revision ([0-9]+)', git_output
)
477 revision
= revision
.group(1) if revision
else None
482 def GetSVNRevisionFromGitHash(self
, git_sha1
, depot
='chromium'):
483 if not self
.local_src_path
:
484 return self
._GetSVNRevisionFromGitHashWithoutGitCheckout
(git_sha1
, depot
)
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."""
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
:
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', {})
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):
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
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:
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
))
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.
544 if self
.good_revision
< self
.bad_revision
:
545 self
.good_revision
= revlist
[0]
546 self
.bad_revision
= revlist
[-1]
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
:
554 self
.good_revision
= FixChromiumRevForBlink(revlist
,
558 self
.bad_revision
= FixChromiumRevForBlink(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
()
581 good_rev_num
= hash_to_num
[self
.good_revision
]
582 bad_rev_num
= hash_to_num
[self
.bad_revision
]
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.
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:
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
)
609 gs_bucket_name
= ANDROID_BUCKET_NAME
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
)
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
:
621 if build_number
< minrev
:
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
))
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'):
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.')
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
,
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
)
665 raise Exception('Error running the gsutil command: %s' % stderr
)
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|."""
682 if not os
.path
.isabs(filename
):
683 filename
= os
.path
.join(cwd
, filename
)
684 zf
= zipfile
.ZipFile(filename
)
686 if not os
.path
.isdir(directory
):
690 for info
in zf
.infolist():
692 if name
.endswith('/'): # dir
693 if not os
.path
.isdir(name
):
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
))
702 # Set permissions. Permission info in external_attr is shifted 16 bits.
703 os
.chmod(name
, info
.external_attr
>> 16L)
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
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
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
)
732 download_url
= context
.GetDownloadURL(rev
)
734 if context
.download_with_gsutil
:
735 GsutilDownload(download_url
, filename
)
737 urllib
.urlretrieve(download_url
, filename
, ReportHook
)
738 if progress_event
and progress_event
.isSet():
745 def RunADBCommand(args
):
747 adb
= subprocess
.Popen(['adb'] + args
,
748 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
750 stdout
, stderr
= adb
.communicate()
754 def IsADBInstalled():
755 """Checks if ADB is in the environment path."""
757 adb_output
= RunADBCommand(['version'])
758 return ('Android Debug Bridge' in adb_output
)
763 def GetAndroidDeviceList():
764 """Returns the list of Android devices attached to the host machine."""
765 lines
= RunADBCommand(['devices']).split('\n')[1:]
768 m
= re
.match('^(.*?)\s+device$', line
)
771 devices
.append(m
.group(1))
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'
782 if context
.is_official
:
783 # Downloaded file is just the .apk in this case.
787 tempdir
= tempfile
.mkdtemp(prefix
='bisect_tmp')
788 UnzipFilenameToDir(zip_file
, 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.
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
823 os
.system('cp %s %s/chrome-linux/' % (icudtl_path
, 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.
842 shutil
.copy(context
.pdf_path
,
843 os
.path
.dirname(context
.GetLaunchPath(revision
)))
844 testargs
.append('--enable-print-preview')
847 for token
in shlex
.split(command
):
849 runcommand
.extend(testargs
)
852 token
.replace('%p', os
.path
.abspath(context
.GetLaunchPath(revision
))).
853 replace('%s', ' '.join(testargs
)))
856 for _
in range(num_runs
):
857 subproc
= subprocess
.Popen(runcommand
,
859 stdout
=subprocess
.PIPE
,
860 stderr
=subprocess
.PIPE
)
861 (stdout
, stderr
) = subproc
.communicate()
862 results
.append((subproc
.returncode
, stdout
, stderr
))
865 shutil
.rmtree(tempdir
, True)
869 for (returncode
, stdout
, stderr
) in results
:
871 return (returncode
, stdout
, stderr
)
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.
883 response
= raw_input('Revision %s is '
884 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
886 if response
and response
in ('g', 'b', 'r', 'u'):
888 if response
and response
== 'q':
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."""
899 for line
in stderr
.splitlines():
901 if line
.find('ERROR: AddressSanitizer:') != -1:
904 print 'Revision %d determined to be bad.' % rev
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
917 self
.zip_file
= zip_file
918 self
.quit_event
= threading
.Event()
919 self
.progress_event
= threading
.Event()
923 """Starts the download."""
924 fetchargs
= (self
.context
,
929 self
.thread
= threading
.Thread(target
=FetchRevision
,
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()
939 os
.unlink(self
.zip_file
)
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.
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
977 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
980 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
987 good_rev
= context
.good_revision
988 bad_rev
= context
.bad_revision
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)'
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()
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.
1012 maxrev
= len(revlist
) - 1
1014 rev
= revlist
[pivot
]
1015 zip_file
= _GetDownloadPath(rev
)
1016 fetch
= DownloadJob(context
, 'initial_fetch', rev
, zip_file
)
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'
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
1032 # - up_pivot is the next revision to check if the current revision turns
1034 down_pivot
= int((pivot
- minrev
) / 2) + minrev
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
))
1042 up_pivot
= int((maxrev
- pivot
) / 2) + pivot
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
))
1050 # Run test on the pivot revision.
1055 (status
, stdout
, stderr
) = RunRevision(context
,
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.
1072 print 'Bad revision: %s' % rev
1075 print 'Good revision: %s' % rev
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
)):
1083 down_fetch
.Stop() # Kill the download of the older revision.
1089 elif ((answer
== 'b' and good_rev
< bad_rev
)
1090 or (answer
== 'g' and bad_rev
< good_rev
)):
1094 up_fetch
.Stop() # Kill the download of the newer revision.
1097 down_fetch
.WaitFor()
1101 pass # Retry requires no changes.
1103 # Nuke the revision from the revlist and choose a new 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
1114 if up_fetch
and down_fetch
:
1115 fetch
= [up_fetch
, down_fetch
][len(revlist
) % 2]
1121 if fetch
== up_fetch
:
1122 pivot
= up_pivot
- 1 # Subtracts 1 because revlist was resized.
1125 zip_file
= fetch
.zip_file
1127 if down_fetch
and fetch
!= down_fetch
:
1129 if up_fetch
and fetch
!= up_fetch
:
1132 assert False, 'Unexpected return value from evaluate(): ' + answer
1134 print 'Cleaning up...'
1135 for f
in [_GetDownloadPath(revlist
[down_pivot
]),
1136 _GetDownloadPath(revlist
[up_pivot
])]:
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())
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
))
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):
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:
1187 data
= json
.loads(url
.read())
1189 print 'ValueError for JSON URL: %s' % file_url
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'))
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
)
1214 rev
= revisions
[idx
-1]
1215 if rev
not in revisions_final
:
1216 revisions_final
.insert(0, rev
)
1218 revisions_final
.sort()
1222 def GetChromiumRevision(context
, url
):
1223 """Returns the chromium revision read from given URL."""
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
)
1231 print 'Could not determine latest revision. This could be bad...'
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
)))
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'
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'
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',
1271 apk_choices
= ['Chrome.apk', 'ChromeBeta.apk', 'ChromeCanary.apk',
1272 'ChromeDev.apk', 'ChromeStable.apk']
1273 parser
.add_option('-a', '--archive',
1275 help='The buildbot archive to bisect [%s].' %
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',
1284 help='A bad revision to start bisection. '
1285 'May be earlier or later than the good revision. '
1287 parser
.add_option('-f', '--flash_path',
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',
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 '
1301 parser
.add_option('-g', '--good',
1303 help='A good revision to start bisection. ' +
1304 'May be earlier or later than the bad revision. ' +
1306 parser
.add_option('-p', '--profile', '--user-data-dir',
1309 help='Profile to use; this will not reset every run. '
1310 'Defaults to a clean profile.')
1311 parser
.add_option('-t', '--times',
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',
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',
1330 help='Use command exit code to tell good/bad revision.')
1331 parser
.add_option('--asan',
1333 action
='store_true',
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',
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',
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',
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'
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
))
1368 if opts
.official_builds
:
1369 print 'Error: Do not yet support bisecting official ASAN builds.'
1373 base_url
= ASAN_BASE_URL
1375 base_url
= WEBKIT_BASE_URL
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
:
1393 os
.environ
['PATH'] = '%s:%s' % (os
.path
.dirname(opts
.adb_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.
1401 context
.bad_revision
= '999.0.0.0'
1402 context
.bad_revision
= GetChromiumRevision(
1403 context
, context
.GetLastChangeURL())
1405 # Find out when we were good.
1407 context
.good_revision
= '0.0.0.0' if opts
.official_builds
else 0
1410 msg
= 'Could not find Flash binary at %s' % opts
.flash_path
1411 assert os
.path
.exists(opts
.flash_path
), msg
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.
1424 context
.good_revision
= int(context
.good_revision
)
1425 context
.bad_revision
= int(context
.bad_revision
)
1428 print('Number of times to run (%d) must be greater than or equal to 1.' %
1434 evaluator
= IsGoodASANBuild
1436 evaluator
= AskIsGoodBuild
1438 # Save these revision numbers to compare when showing the changelog URL
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.
1449 min_blink_rev
= GetBlinkRevisionForChromiumRevision(context
,
1451 max_blink_rev
= GetBlinkRevisionForChromiumRevision(context
,
1454 # Silently ignore the failure.
1455 min_blink_rev
, max_blink_rev
= 0, 0
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
))
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
)
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
))
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
)
1483 PrintChangeLog(min_chromium_rev
, max_chromium_rev
)
1486 if __name__
== '__main__':