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 root URL for storage.
16 CHROMIUM_BASE_URL
= 'http://commondatastorage.googleapis.com/chromium-browser-snapshots'
17 WEBKIT_BASE_URL
= 'http://commondatastorage.googleapis.com/chromium-webkit-snapshots'
19 # The root URL for official builds.
20 OFFICIAL_BASE_URL
= 'http://master.chrome.corp.google.com/official_builds'
23 CHANGELOG_URL
= 'http://build.chromium.org/f/chromium/' \
24 'perf/dashboard/ui/changelog.html?' \
25 'url=/trunk/src&range=%d%%3A%d'
27 # Official Changelogs URL.
28 OFFICIAL_CHANGELOG_URL
= 'http://omahaproxy.appspot.com/'\
29 'changelog?old_version=%s&new_version=%s'
32 DEPS_FILE
= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
33 # Blink Changelogs URL.
34 BLINK_CHANGELOG_URL
= 'http://build.chromium.org/f/chromium/' \
35 'perf/dashboard/ui/changelog_blink.html?' \
36 'url=/trunk&range=%d%%3A%d'
38 DONE_MESSAGE_GOOD_MIN
= 'You are probably looking for a change made after %s ' \
39 '(known good), but no later than %s (first known bad).'
40 DONE_MESSAGE_GOOD_MAX
= 'You are probably looking for a change made after %s ' \
41 '(known bad), but no later than %s (first known good).'
43 ###############################################################################
56 from distutils
.version
import LooseVersion
57 from xml
.etree
import ElementTree
61 class PathContext(object):
62 """A PathContext is used to carry the information used to construct URLs and
63 paths when dealing with the storage server and archives."""
64 def __init__(self
, base_url
, platform
, good_revision
, bad_revision
,
65 is_official
, is_aura
, flash_path
= None):
66 super(PathContext
, self
).__init
__()
67 # Store off the input parameters.
68 self
.base_url
= base_url
69 self
.platform
= platform
# What's passed in to the '-a/--archive' option.
70 self
.good_revision
= good_revision
71 self
.bad_revision
= bad_revision
72 self
.is_official
= is_official
73 self
.is_aura
= is_aura
74 self
.flash_path
= flash_path
76 # The name of the ZIP file in a revision directory on the server.
77 self
.archive_name
= None
79 # Set some internal members:
80 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
81 # _archive_extract_dir = Uncompressed directory in the archive_name file.
82 # _binary_name = The name of the executable to run.
83 if self
.platform
in ('linux', 'linux64', 'linux-arm'):
84 self
._binary
_name
= 'chrome'
85 elif self
.platform
== 'mac':
86 self
.archive_name
= 'chrome-mac.zip'
87 self
._archive
_extract
_dir
= 'chrome-mac'
88 elif self
.platform
== 'win':
89 self
.archive_name
= 'chrome-win32.zip'
90 self
._archive
_extract
_dir
= 'chrome-win32'
91 self
._binary
_name
= 'chrome.exe'
93 raise Exception('Invalid platform: %s' % self
.platform
)
96 if self
.platform
== 'linux':
97 self
._listing
_platform
_dir
= 'precise32bit/'
98 self
.archive_name
= 'chrome-precise32bit.zip'
99 self
._archive
_extract
_dir
= 'chrome-precise32bit'
100 elif self
.platform
== 'linux64':
101 self
._listing
_platform
_dir
= 'precise64bit/'
102 self
.archive_name
= 'chrome-precise64bit.zip'
103 self
._archive
_extract
_dir
= 'chrome-precise64bit'
104 elif self
.platform
== 'mac':
105 self
._listing
_platform
_dir
= 'mac/'
106 self
._binary
_name
= 'Google Chrome.app/Contents/MacOS/Google Chrome'
107 elif self
.platform
== 'win':
109 self
._listing
_platform
_dir
= 'win-aura/'
111 self
._listing
_platform
_dir
= 'win/'
113 if self
.platform
in ('linux', 'linux64', 'linux-arm'):
114 self
.archive_name
= 'chrome-linux.zip'
115 self
._archive
_extract
_dir
= 'chrome-linux'
116 if self
.platform
== 'linux':
117 self
._listing
_platform
_dir
= 'Linux/'
118 elif self
.platform
== 'linux64':
119 self
._listing
_platform
_dir
= 'Linux_x64/'
120 elif self
.platform
== 'linux-arm':
121 self
._listing
_platform
_dir
= 'Linux_ARM_Cross-Compile/'
122 elif self
.platform
== 'mac':
123 self
._listing
_platform
_dir
= 'Mac/'
124 self
._binary
_name
= 'Chromium.app/Contents/MacOS/Chromium'
125 elif self
.platform
== 'win':
126 self
._listing
_platform
_dir
= 'Win/'
128 def GetListingURL(self
, marker
=None):
129 """Returns the URL for a directory listing, with an optional marker."""
132 marker_param
= '&marker=' + str(marker
)
133 return self
.base_url
+ '/?delimiter=/&prefix=' + \
134 self
._listing
_platform
_dir
+ marker_param
136 def GetDownloadURL(self
, revision
):
137 """Gets the download URL for a build archive of a specific revision."""
139 return "%s/%s/%s%s" % (
140 OFFICIAL_BASE_URL
, revision
, self
._listing
_platform
_dir
,
143 return "%s/%s%s/%s" % (self
.base_url
, self
._listing
_platform
_dir
,
144 revision
, self
.archive_name
)
146 def GetLastChangeURL(self
):
147 """Returns a URL to the LAST_CHANGE file."""
148 return self
.base_url
+ '/' + self
._listing
_platform
_dir
+ 'LAST_CHANGE'
150 def GetLaunchPath(self
):
151 """Returns a relative path (presumably from the archive extraction location)
152 that is used to run the executable."""
153 return os
.path
.join(self
._archive
_extract
_dir
, self
._binary
_name
)
155 def IsAuraBuild(self
, build
):
156 """Check the given build is Aura."""
157 return build
.split('.')[3] == '1'
159 def IsASANBuild(self
, build
):
160 """Check the given build is ASAN build."""
161 return build
.split('.')[3] == '2'
163 def ParseDirectoryIndex(self
):
164 """Parses the Google Storage directory listing into a list of revision
167 def _FetchAndParse(url
):
168 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
169 next-marker is not None, then the listing is a partial listing and another
170 fetch should be performed with next-marker being the marker= GET
172 handle
= urllib
.urlopen(url
)
173 document
= ElementTree
.parse(handle
)
175 # All nodes in the tree are namespaced. Get the root's tag name to extract
176 # the namespace. Etree does namespaces as |{namespace}tag|.
177 root_tag
= document
.getroot().tag
178 end_ns_pos
= root_tag
.find('}')
180 raise Exception("Could not locate end namespace for directory index")
181 namespace
= root_tag
[:end_ns_pos
+ 1]
183 # Find the prefix (_listing_platform_dir) and whether or not the list is
185 prefix_len
= len(document
.find(namespace
+ 'Prefix').text
)
187 is_truncated
= document
.find(namespace
+ 'IsTruncated')
188 if is_truncated
is not None and is_truncated
.text
.lower() == 'true':
189 next_marker
= document
.find(namespace
+ 'NextMarker').text
191 # Get a list of all the revisions.
192 all_prefixes
= document
.findall(namespace
+ 'CommonPrefixes/' +
193 namespace
+ 'Prefix')
194 # The <Prefix> nodes have content of the form of
195 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
196 # trailing slash to just have a number.
198 for prefix
in all_prefixes
:
199 revnum
= prefix
.text
[prefix_len
:-1]
202 revisions
.append(revnum
)
205 return (revisions
, next_marker
)
207 # Fetch the first list of revisions.
208 (revisions
, next_marker
) = _FetchAndParse(self
.GetListingURL())
210 # If the result list was truncated, refetch with the next marker. Do this
211 # until an entire directory listing is done.
213 next_url
= self
.GetListingURL(next_marker
)
214 (new_revisions
, next_marker
) = _FetchAndParse(next_url
)
215 revisions
.extend(new_revisions
)
218 def GetRevList(self
):
219 """Gets the list of revision numbers between self.good_revision and
220 self.bad_revision."""
221 # Download the revlist and filter for just the range between good and bad.
222 minrev
= min(self
.good_revision
, self
.bad_revision
)
223 maxrev
= max(self
.good_revision
, self
.bad_revision
)
224 revlist_all
= map(int, self
.ParseDirectoryIndex())
226 revlist
= [x
for x
in revlist_all
if x
>= int(minrev
) and x
<= int(maxrev
)]
229 # Set good and bad revisions to be legit revisions.
231 if self
.good_revision
< self
.bad_revision
:
232 self
.good_revision
= revlist
[0]
233 self
.bad_revision
= revlist
[-1]
235 self
.bad_revision
= revlist
[0]
236 self
.good_revision
= revlist
[-1]
238 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
239 if self
.base_url
== WEBKIT_BASE_URL
:
241 self
.good_revision
= FixChromiumRevForBlink(revlist
,
245 self
.bad_revision
= FixChromiumRevForBlink(revlist
,
251 def GetOfficialBuildsList(self
):
252 """Gets the list of official build numbers between self.good_revision and
253 self.bad_revision."""
254 # Download the revlist and filter for just the range between good and bad.
255 minrev
= min(self
.good_revision
, self
.bad_revision
)
256 maxrev
= max(self
.good_revision
, self
.bad_revision
)
257 handle
= urllib
.urlopen(OFFICIAL_BASE_URL
)
258 dirindex
= handle
.read()
260 build_numbers
= re
.findall(r
'<a href="([0-9][0-9].*)/">', dirindex
)
263 parsed_build_numbers
= [LooseVersion(x
) for x
in build_numbers
]
264 for build_number
in sorted(parsed_build_numbers
):
265 path
= OFFICIAL_BASE_URL
+ '/' + str(build_number
) + '/' + \
266 self
._listing
_platform
_dir
+ self
.archive_name
269 connection
= urllib
.urlopen(path
)
271 if build_number
> maxrev
:
273 if build_number
>= minrev
:
274 # If we are bisecting Aura, we want to include only builds which
277 if self
.IsAuraBuild(str(build_number
)):
278 final_list
.append(str(build_number
))
279 # If we are bisecting only official builds (without --aura),
280 # we can not include builds which ends with '.1' or '.2' since
281 # they have different folder hierarchy inside.
282 elif (not self
.IsAuraBuild(str(build_number
)) and
283 not self
.IsASANBuild(str(build_number
))):
284 final_list
.append(str(build_number
))
285 except urllib
.HTTPError
, e
:
289 def UnzipFilenameToDir(filename
, directory
):
290 """Unzip |filename| to |directory|."""
292 if not os
.path
.isabs(filename
):
293 filename
= os
.path
.join(cwd
, filename
)
294 zf
= zipfile
.ZipFile(filename
)
296 if not os
.path
.isdir(directory
):
300 for info
in zf
.infolist():
302 if name
.endswith('/'): # dir
303 if not os
.path
.isdir(name
):
306 directory
= os
.path
.dirname(name
)
307 if not os
.path
.isdir(directory
):
308 os
.makedirs(directory
)
309 out
= open(name
, 'wb')
310 out
.write(zf
.read(name
))
312 # Set permissions. Permission info in external_attr is shifted 16 bits.
313 os
.chmod(name
, info
.external_attr
>> 16L)
317 def FetchRevision(context
, rev
, filename
, quit_event
=None, progress_event
=None):
318 """Downloads and unzips revision |rev|.
319 @param context A PathContext instance.
320 @param rev The Chromium revision number/tag to download.
321 @param filename The destination for the downloaded file.
322 @param quit_event A threading.Event which will be set by the master thread to
323 indicate that the download should be aborted.
324 @param progress_event A threading.Event which will be set by the master thread
325 to indicate that the progress of the download should be
328 def ReportHook(blocknum
, blocksize
, totalsize
):
329 if quit_event
and quit_event
.isSet():
330 raise RuntimeError("Aborting download of revision %s" % str(rev
))
331 if progress_event
and progress_event
.isSet():
332 size
= blocknum
* blocksize
333 if totalsize
== -1: # Total size not known.
334 progress
= "Received %d bytes" % size
336 size
= min(totalsize
, size
)
337 progress
= "Received %d of %d bytes, %.2f%%" % (
338 size
, totalsize
, 100.0 * size
/ totalsize
)
339 # Send a \r to let all progress messages use just one line of output.
340 sys
.stdout
.write("\r" + progress
)
343 download_url
= context
.GetDownloadURL(rev
)
345 urllib
.urlretrieve(download_url
, filename
, ReportHook
)
346 if progress_event
and progress_event
.isSet():
348 except RuntimeError, e
:
352 def RunRevision(context
, revision
, zipfile
, profile
, num_runs
, command
, args
):
353 """Given a zipped revision, unzip it and run the test."""
354 print "Trying revision %s..." % str(revision
)
356 # Create a temp directory and unzip the revision into it.
358 tempdir
= tempfile
.mkdtemp(prefix
='bisect_tmp')
359 UnzipFilenameToDir(zipfile
, tempdir
)
362 # Run the build as many times as specified.
363 testargs
= ['--user-data-dir=%s' % profile
] + args
364 # The sandbox must be run as root on Official Chrome, so bypass it.
365 if ((context
.is_official
or context
.flash_path
) and
366 context
.platform
.startswith('linux')):
367 testargs
.append('--no-sandbox')
368 if context
.flash_path
:
369 testargs
.append('--ppapi-flash-path=%s' % context
.flash_path
)
370 # We have to pass a large enough Flash version, which currently needs not
371 # be correct. Instead of requiring the user of the script to figure out and
372 # pass the correct version we just spoof it.
373 testargs
.append('--ppapi-flash-version=99.9.999.999')
376 for token
in shlex
.split(command
):
378 runcommand
.extend(testargs
)
381 token
.replace('%p', context
.GetLaunchPath()) \
382 .replace('%s', ' '.join(testargs
)))
384 for i
in range(0, num_runs
):
385 subproc
= subprocess
.Popen(runcommand
,
387 stdout
=subprocess
.PIPE
,
388 stderr
=subprocess
.PIPE
)
389 (stdout
, stderr
) = subproc
.communicate()
393 shutil
.rmtree(tempdir
, True)
397 return (subproc
.returncode
, stdout
, stderr
)
400 def AskIsGoodBuild(rev
, official_builds
, status
, stdout
, stderr
):
401 """Ask the user whether build |rev| is good or bad."""
402 # Loop until we get a response that we can parse.
404 response
= raw_input('Revision %s is ' \
405 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
407 if response
and response
in ('g', 'b', 'r', 'u'):
409 if response
and response
== 'q':
413 class DownloadJob(object):
414 """DownloadJob represents a task to download a given Chromium revision."""
415 def __init__(self
, context
, name
, rev
, zipfile
):
416 super(DownloadJob
, self
).__init
__()
417 # Store off the input parameters.
418 self
.context
= context
421 self
.zipfile
= zipfile
422 self
.quit_event
= threading
.Event()
423 self
.progress_event
= threading
.Event()
426 """Starts the download."""
427 fetchargs
= (self
.context
,
432 self
.thread
= threading
.Thread(target
=FetchRevision
,
438 """Stops the download which must have been started previously."""
439 self
.quit_event
.set()
441 os
.unlink(self
.zipfile
)
444 """Prints a message and waits for the download to complete. The download
445 must have been started previously."""
446 print "Downloading revision %s..." % str(self
.rev
)
447 self
.progress_event
.set() # Display progress of download.
462 evaluate
=AskIsGoodBuild
):
463 """Given known good and known bad revisions, run a binary search on all
464 archived revisions to determine the last known good revision.
466 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.).
467 @param official_builds Specify build type (Chromium or Official build).
468 @param good_rev Number/tag of the known good revision.
469 @param bad_rev Number/tag of the known bad revision.
470 @param num_runs Number of times to run each build for asking good/bad.
471 @param try_args A tuple of arguments to pass to the test application.
472 @param profile The name of the user profile to run with.
473 @param evaluate A function which returns 'g' if the argument build is good,
474 'b' if it's bad or 'u' if unknown.
476 Threading is used to fetch Chromium revisions in the background, speeding up
477 the user's experience. For example, suppose the bounds of the search are
478 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
479 whether revision 50 is good or bad, the next revision to check will be either
480 25 or 75. So, while revision 50 is being checked, the script will download
481 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
484 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
487 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
494 context
= PathContext(base_url
, platform
, good_rev
, bad_rev
,
495 official_builds
, is_aura
, flash_path
)
498 print "Downloading list of known revisions..."
499 _GetDownloadPath
= lambda rev
: os
.path
.join(cwd
,
500 '%s-%s' % (str(rev
), context
.archive_name
))
502 revlist
= context
.GetOfficialBuildsList()
504 revlist
= context
.GetRevList()
506 # Get a list of revisions to bisect across.
507 if len(revlist
) < 2: # Don't have enough builds to bisect.
508 msg
= 'We don\'t have enough builds to bisect. revlist: %s' % revlist
509 raise RuntimeError(msg
)
511 # Figure out our bookends and first pivot point; fetch the pivot revision.
513 maxrev
= len(revlist
) - 1
516 zipfile
= _GetDownloadPath(rev
)
517 fetch
= DownloadJob(context
, 'initial_fetch', rev
, zipfile
)
521 # Binary search time!
522 while fetch
and fetch
.zipfile
and maxrev
- minrev
> 1:
523 if bad_rev
< good_rev
:
524 min_str
, max_str
= "bad", "good"
526 min_str
, max_str
= "good", "bad"
527 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist
[minrev
], min_str
, \
528 revlist
[maxrev
], max_str
)
530 # Pre-fetch next two possible pivots
531 # - down_pivot is the next revision to check if the current revision turns
533 # - up_pivot is the next revision to check if the current revision turns
535 down_pivot
= int((pivot
- minrev
) / 2) + minrev
537 if down_pivot
!= pivot
and down_pivot
!= minrev
:
538 down_rev
= revlist
[down_pivot
]
539 down_fetch
= DownloadJob(context
, 'down_fetch', down_rev
,
540 _GetDownloadPath(down_rev
))
543 up_pivot
= int((maxrev
- pivot
) / 2) + pivot
545 if up_pivot
!= pivot
and up_pivot
!= maxrev
:
546 up_rev
= revlist
[up_pivot
]
547 up_fetch
= DownloadJob(context
, 'up_fetch', up_rev
,
548 _GetDownloadPath(up_rev
))
551 # Run test on the pivot revision.
556 (status
, stdout
, stderr
) = RunRevision(context
,
564 print >> sys
.stderr
, e
566 # Call the evaluate function to see if the current revision is good or bad.
567 # On that basis, kill one of the background downloads and complete the
568 # other, as described in the comments above.
570 answer
= evaluate(rev
, official_builds
, status
, stdout
, stderr
)
571 if answer
== 'g' and good_rev
< bad_rev
or \
572 answer
== 'b' and bad_rev
< good_rev
:
576 down_fetch
.Stop() # Kill the download of the older revision.
582 elif answer
== 'b' and good_rev
< bad_rev
or \
583 answer
== 'g' and bad_rev
< good_rev
:
587 up_fetch
.Stop() # Kill the download of the newer revision.
594 pass # Retry requires no changes.
596 # Nuke the revision from the revlist and choose a new pivot.
599 maxrev
-= 1 # Assumes maxrev >= pivot.
601 if maxrev
- minrev
> 1:
602 # Alternate between using down_pivot or up_pivot for the new pivot
603 # point, without affecting the range. Do this instead of setting the
604 # pivot to the midpoint of the new range because adjacent revisions
605 # are likely affected by the same issue that caused the (u)nknown
607 if up_fetch
and down_fetch
:
608 fetch
= [up_fetch
, down_fetch
][len(revlist
) % 2]
614 if fetch
== up_fetch
:
615 pivot
= up_pivot
- 1 # Subtracts 1 because revlist was resized.
618 zipfile
= fetch
.zipfile
620 if down_fetch
and fetch
!= down_fetch
:
622 if up_fetch
and fetch
!= up_fetch
:
625 assert False, "Unexpected return value from evaluate(): " + answer
627 print "Cleaning up..."
628 for f
in [_GetDownloadPath(revlist
[down_pivot
]),
629 _GetDownloadPath(revlist
[up_pivot
])]:
638 return (revlist
[minrev
], revlist
[maxrev
])
641 def GetBlinkDEPSRevisionForChromiumRevision(rev
):
642 """Returns the blink revision that was in REVISIONS file at
643 chromium revision |rev|."""
644 # . doesn't match newlines without re.DOTALL, so this is safe.
645 blink_re
= re
.compile(r
'webkit_revision\D*(\d+)')
646 url
= urllib
.urlopen(DEPS_FILE
% rev
)
647 m
= blink_re
.search(url
.read())
650 return int(m
.group(1))
652 raise Exception('Could not get Blink revision for Chromium rev %d'
656 def GetBlinkRevisionForChromiumRevision(self
, rev
):
657 """Returns the blink revision that was in REVISIONS file at
658 chromium revision |rev|."""
659 file_url
= "%s/%s%d/REVISIONS" % (self
.base_url
,
660 self
._listing
_platform
_dir
, rev
)
661 url
= urllib
.urlopen(file_url
)
662 data
= json
.loads(url
.read())
664 if 'webkit_revision' in data
:
665 return data
['webkit_revision']
667 raise Exception('Could not get blink revision for cr rev %d' % rev
)
669 def FixChromiumRevForBlink(revisions_final
, revisions
, self
, rev
):
670 """Returns the chromium revision that has the correct blink revision
671 for blink bisect, DEPS and REVISIONS file might not match since
672 blink snapshots point to tip of tree blink.
673 Note: The revisions_final variable might get modified to include
674 additional revisions."""
676 blink_deps_rev
= GetBlinkDEPSRevisionForChromiumRevision(rev
)
678 while (GetBlinkRevisionForChromiumRevision(self
, rev
) > blink_deps_rev
):
679 idx
= revisions
.index(rev
)
681 rev
= revisions
[idx
-1]
682 if rev
not in revisions_final
:
683 revisions_final
.insert(0, rev
)
685 revisions_final
.sort()
688 def GetChromiumRevision(url
):
689 """Returns the chromium revision read from given URL."""
691 # Location of the latest build revision number
692 return int(urllib
.urlopen(url
).read())
694 print('Could not determine latest revision. This could be bad...')
699 usage
= ('%prog [options] [-- chromium-options]\n'
700 'Perform binary search on the snapshot builds to find a minimal\n'
701 'range of revisions where a behavior change happened. The\n'
702 'behaviors are described as "good" and "bad".\n'
703 'It is NOT assumed that the behavior of the later revision is\n'
706 'Revision numbers should use\n'
707 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
708 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
709 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
710 ' for earlier revs.\n'
711 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
712 ' are incorrect, they are from branches.\n'
714 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
715 parser
= optparse
.OptionParser(usage
=usage
)
716 # Strangely, the default help output doesn't include the choice list.
717 choices
= ['mac', 'win', 'linux', 'linux64', 'linux-arm']
718 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
719 parser
.add_option('-a', '--archive',
721 help = 'The buildbot archive to bisect [%s].' %
723 parser
.add_option('-o', action
="store_true", dest
='official_builds',
724 help = 'Bisect across official ' +
725 'Chrome builds (internal only) instead of ' +
726 'Chromium archives.')
727 parser
.add_option('-b', '--bad', type = 'str',
728 help = 'A bad revision to start bisection. ' +
729 'May be earlier or later than the good revision. ' +
731 parser
.add_option('-f', '--flash_path', type = 'str',
732 help = 'Absolute path to a recent Adobe Pepper Flash ' +
733 'binary to be used in this bisection (e.g. ' +
734 'on Windows C:\...\pepflashplayer.dll and on Linux ' +
735 '/opt/google/chrome/PepperFlash/libpepflashplayer.so).')
736 parser
.add_option('-g', '--good', type = 'str',
737 help = 'A good revision to start bisection. ' +
738 'May be earlier or later than the bad revision. ' +
740 parser
.add_option('-p', '--profile', '--user-data-dir', type = 'str',
741 help = 'Profile to use; this will not reset every run. ' +
742 'Defaults to a clean profile.', default
= 'profile')
743 parser
.add_option('-t', '--times', type = 'int',
744 help = 'Number of times to run each build before asking ' +
745 'if it\'s good or bad. Temporary profiles are reused.',
747 parser
.add_option('-c', '--command', type = 'str',
748 help = 'Command to execute. %p and %a refer to Chrome ' +
749 'executable and specified extra arguments respectively. ' +
750 'Use %s to specify all extra arguments as one string. ' +
751 'Defaults to "%p %a". Note that any extra paths ' +
752 'specified should be absolute.',
754 parser
.add_option('-l', '--blink', action
='store_true',
755 help = 'Use Blink bisect instead of Chromium. ')
756 parser
.add_option('--aura',
760 help='Allow the script to bisect aura builds')
762 (opts
, args
) = parser
.parse_args()
764 if opts
.archive
is None:
765 print 'Error: missing required parameter: --archive'
771 if opts
.archive
!= 'win' or not opts
.official_builds
:
772 print 'Error: Aura is supported only on Windows platform '\
773 'and official builds.'
777 base_url
= WEBKIT_BASE_URL
779 base_url
= CHROMIUM_BASE_URL
781 # Create the context. Initialize 0 for the revisions as they are set below.
782 context
= PathContext(base_url
, opts
.archive
, 0, 0,
783 opts
.official_builds
, opts
.aura
, None)
784 # Pick a starting point, try to get HEAD for this.
788 bad_rev
= '999.0.0.0'
789 if not opts
.official_builds
:
790 bad_rev
= GetChromiumRevision(context
.GetLastChangeURL())
792 # Find out when we were good.
796 good_rev
= '0.0.0.0' if opts
.official_builds
else 0
799 flash_path
= opts
.flash_path
800 msg
= 'Could not find Flash binary at %s' % flash_path
801 assert os
.path
.exists(flash_path
), msg
803 if opts
.official_builds
:
804 good_rev
= LooseVersion(good_rev
)
805 bad_rev
= LooseVersion(bad_rev
)
807 good_rev
= int(good_rev
)
808 bad_rev
= int(bad_rev
)
811 print('Number of times to run (%d) must be greater than or equal to 1.' %
816 (min_chromium_rev
, max_chromium_rev
) = Bisect(
817 base_url
, opts
.archive
, opts
.official_builds
, opts
.aura
, good_rev
,
818 bad_rev
, opts
.times
, opts
.command
, args
, opts
.profile
, opts
.flash_path
)
820 # Get corresponding blink revisions.
822 min_blink_rev
= GetBlinkRevisionForChromiumRevision(context
,
824 max_blink_rev
= GetBlinkRevisionForChromiumRevision(context
,
827 # Silently ignore the failure.
828 min_blink_rev
, max_blink_rev
= 0, 0
831 # We're done. Let the user know the results in an official manner.
832 if good_rev
> bad_rev
:
833 print DONE_MESSAGE_GOOD_MAX
% (str(min_blink_rev
), str(max_blink_rev
))
835 print DONE_MESSAGE_GOOD_MIN
% (str(min_blink_rev
), str(max_blink_rev
))
837 print 'BLINK CHANGELOG URL:'
838 print ' ' + BLINK_CHANGELOG_URL
% (max_blink_rev
, min_blink_rev
)
841 # We're done. Let the user know the results in an official manner.
842 if good_rev
> bad_rev
:
843 print DONE_MESSAGE_GOOD_MAX
% (str(min_chromium_rev
),
844 str(max_chromium_rev
))
846 print DONE_MESSAGE_GOOD_MIN
% (str(min_chromium_rev
),
847 str(max_chromium_rev
))
848 if min_blink_rev
!= max_blink_rev
:
849 print ("NOTE: There is a Blink roll in the range, "
850 "you might also want to do a Blink bisect.")
852 print 'CHANGELOG URL:'
853 if opts
.official_builds
:
854 print OFFICIAL_CHANGELOG_URL
% (min_chromium_rev
, max_chromium_rev
)
856 print ' ' + CHANGELOG_URL
% (min_chromium_rev
, max_chromium_rev
)
858 if __name__
== '__main__':