blob: 00128fbd1614c56ee50d5cb819fcc4490783a6b7 [file] [log] [blame]
[email protected]cb155a82011-11-29 17:25:341#!/usr/bin/env python
[email protected]5e93cf162012-01-28 02:16:562# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]67e0bc62009-09-03 22:06:093# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Snapshot Build Bisect Tool
7
[email protected]7ad66a72009-09-04 17:52:338This script bisects a snapshot archive using binary search. It starts at
[email protected]67e0bc62009-09-03 22:06:099a bad revision (it will try to guess HEAD) and asks for a last known-good
10revision. It will then binary search across this revision range by downloading,
11unzipping, and opening Chromium for you. After testing the specific revision,
12it will ask you whether it is good or bad before continuing the search.
[email protected]67e0bc62009-09-03 22:06:0913"""
14
[email protected]183706d92011-06-10 13:06:2215# The root URL for storage.
[email protected]4c6fec6b2013-09-17 17:44:0816CHROMIUM_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots'
17WEBKIT_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-webkit-snapshots'
[email protected]67e0bc62009-09-03 22:06:0918
[email protected]d0149c5c2012-05-29 21:12:1119# The root URL for official builds.
[email protected]b2905832012-07-19 21:28:4320OFFICIAL_BASE_URL = 'http://master.chrome.corp.google.com/official_builds'
[email protected]d0149c5c2012-05-29 21:12:1121
[email protected]183706d92011-06-10 13:06:2222# Changelogs URL.
[email protected]07247462010-12-24 07:45:5623CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
[email protected]ff50d1c2013-04-17 18:49:3624 'perf/dashboard/ui/changelog.html?' \
25 'url=/trunk/src&range=%d%%3A%d'
[email protected]f6a71a72009-10-08 19:55:3826
[email protected]d0149c5c2012-05-29 21:12:1127# Official Changelogs URL.
28OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\
29 'changelog?old_version=%s&new_version=%s'
30
[email protected]b2fe7f22011-10-25 22:58:3131# DEPS file URL.
[email protected]fc3702e2013-11-09 04:23:0032DEPS_FILE = 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
[email protected]ff50d1c2013-04-17 18:49:3633# Blink Changelogs URL.
34BLINK_CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
35 'perf/dashboard/ui/changelog_blink.html?' \
36 'url=/trunk&range=%d%%3A%d'
[email protected]b2fe7f22011-10-25 22:58:3137
[email protected]eadd95d2012-11-02 22:42:0938DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \
39 '(known good), but no later than %s (first known bad).'
40DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \
41 '(known bad), but no later than %s (first known good).'
[email protected]05ff3fd2012-04-17 23:24:0642
[email protected]67e0bc62009-09-03 22:06:0943###############################################################################
44
[email protected]4c6fec6b2013-09-17 17:44:0845import json
[email protected]7ad66a72009-09-04 17:52:3346import optparse
[email protected]67e0bc62009-09-03 22:06:0947import os
48import re
[email protected]61ea90a2013-09-26 10:17:3449import shlex
[email protected]67e0bc62009-09-03 22:06:0950import shutil
[email protected]afe30662011-07-30 01:05:5251import subprocess
[email protected]67e0bc62009-09-03 22:06:0952import sys
[email protected]7ad66a72009-09-04 17:52:3353import tempfile
[email protected]afe30662011-07-30 01:05:5254import threading
[email protected]67e0bc62009-09-03 22:06:0955import urllib
[email protected]d0149c5c2012-05-29 21:12:1156from distutils.version import LooseVersion
[email protected]183706d92011-06-10 13:06:2257from xml.etree import ElementTree
[email protected]bd8dcb92010-03-31 01:05:2458import zipfile
59
[email protected]cb155a82011-11-29 17:25:3460
[email protected]183706d92011-06-10 13:06:2261class 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."""
[email protected]4c6fec6b2013-09-17 17:44:0864 def __init__(self, base_url, platform, good_revision, bad_revision,
[email protected]fc3702e2013-11-09 04:23:0065 is_official, is_aura, flash_path = None):
[email protected]183706d92011-06-10 13:06:2266 super(PathContext, self).__init__()
67 # Store off the input parameters.
[email protected]4c6fec6b2013-09-17 17:44:0868 self.base_url = base_url
[email protected]183706d92011-06-10 13:06:2269 self.platform = platform # What's passed in to the '-a/--archive' option.
70 self.good_revision = good_revision
71 self.bad_revision = bad_revision
[email protected]d0149c5c2012-05-29 21:12:1172 self.is_official = is_official
[email protected]b3b20512013-08-26 18:51:0473 self.is_aura = is_aura
[email protected]fc3702e2013-11-09 04:23:0074 self.flash_path = flash_path
[email protected]183706d92011-06-10 13:06:2275
76 # The name of the ZIP file in a revision directory on the server.
77 self.archive_name = None
78
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.
[email protected]7aec9e82013-05-09 05:09:2383 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]183706d92011-06-10 13:06:2284 self._binary_name = 'chrome'
[email protected]183706d92011-06-10 13:06:2285 elif self.platform == 'mac':
[email protected]183706d92011-06-10 13:06:2286 self.archive_name = 'chrome-mac.zip'
87 self._archive_extract_dir = 'chrome-mac'
[email protected]183706d92011-06-10 13:06:2288 elif self.platform == 'win':
[email protected]183706d92011-06-10 13:06:2289 self.archive_name = 'chrome-win32.zip'
90 self._archive_extract_dir = 'chrome-win32'
91 self._binary_name = 'chrome.exe'
92 else:
[email protected]afe30662011-07-30 01:05:5293 raise Exception('Invalid platform: %s' % self.platform)
[email protected]183706d92011-06-10 13:06:2294
[email protected]d0149c5c2012-05-29 21:12:1195 if is_official:
96 if self.platform == 'linux':
[email protected]9639b002013-08-30 14:45:5297 self._listing_platform_dir = 'precise32bit/'
98 self.archive_name = 'chrome-precise32bit.zip'
99 self._archive_extract_dir = 'chrome-precise32bit'
[email protected]d0149c5c2012-05-29 21:12:11100 elif self.platform == 'linux64':
[email protected]9639b002013-08-30 14:45:52101 self._listing_platform_dir = 'precise64bit/'
102 self.archive_name = 'chrome-precise64bit.zip'
103 self._archive_extract_dir = 'chrome-precise64bit'
[email protected]d0149c5c2012-05-29 21:12:11104 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':
[email protected]b3b20512013-08-26 18:51:04108 if self.is_aura:
109 self._listing_platform_dir = 'win-aura/'
110 else:
111 self._listing_platform_dir = 'win/'
[email protected]d0149c5c2012-05-29 21:12:11112 else:
[email protected]7aec9e82013-05-09 05:09:23113 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]d0149c5c2012-05-29 21:12:11114 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/'
[email protected]7aec9e82013-05-09 05:09:23120 elif self.platform == 'linux-arm':
121 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
[email protected]d0149c5c2012-05-29 21:12:11122 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/'
127
[email protected]183706d92011-06-10 13:06:22128 def GetListingURL(self, marker=None):
129 """Returns the URL for a directory listing, with an optional marker."""
130 marker_param = ''
131 if marker:
132 marker_param = '&marker=' + str(marker)
[email protected]4c6fec6b2013-09-17 17:44:08133 return self.base_url + '/?delimiter=/&prefix=' + \
134 self._listing_platform_dir + marker_param
[email protected]183706d92011-06-10 13:06:22135
136 def GetDownloadURL(self, revision):
137 """Gets the download URL for a build archive of a specific revision."""
[email protected]d0149c5c2012-05-29 21:12:11138 if self.is_official:
139 return "%s/%s/%s%s" % (
140 OFFICIAL_BASE_URL, revision, self._listing_platform_dir,
141 self.archive_name)
142 else:
[email protected]4c6fec6b2013-09-17 17:44:08143 return "%s/%s%s/%s" % (self.base_url, self._listing_platform_dir,
144 revision, self.archive_name)
[email protected]183706d92011-06-10 13:06:22145
146 def GetLastChangeURL(self):
147 """Returns a URL to the LAST_CHANGE file."""
[email protected]4c6fec6b2013-09-17 17:44:08148 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
[email protected]183706d92011-06-10 13:06:22149
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)
154
[email protected]b3b20512013-08-26 18:51:04155 def IsAuraBuild(self, build):
156 """Check the given build is Aura."""
157 return build.split('.')[3] == '1'
158
159 def IsASANBuild(self, build):
160 """Check the given build is ASAN build."""
161 return build.split('.')[3] == '2'
162
[email protected]afe30662011-07-30 01:05:52163 def ParseDirectoryIndex(self):
164 """Parses the Google Storage directory listing into a list of revision
[email protected]eadd95d2012-11-02 22:42:09165 numbers."""
[email protected]afe30662011-07-30 01:05:52166
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
171 parameter."""
172 handle = urllib.urlopen(url)
173 document = ElementTree.parse(handle)
174
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('}')
179 if end_ns_pos == -1:
180 raise Exception("Could not locate end namespace for directory index")
181 namespace = root_tag[:end_ns_pos + 1]
182
183 # Find the prefix (_listing_platform_dir) and whether or not the list is
184 # truncated.
185 prefix_len = len(document.find(namespace + 'Prefix').text)
186 next_marker = None
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
190
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.
197 revisions = []
198 for prefix in all_prefixes:
199 revnum = prefix.text[prefix_len:-1]
200 try:
201 revnum = int(revnum)
202 revisions.append(revnum)
203 except ValueError:
204 pass
205 return (revisions, next_marker)
[email protected]9639b002013-08-30 14:45:52206
[email protected]afe30662011-07-30 01:05:52207 # Fetch the first list of revisions.
208 (revisions, next_marker) = _FetchAndParse(self.GetListingURL())
209
210 # If the result list was truncated, refetch with the next marker. Do this
211 # until an entire directory listing is done.
212 while next_marker:
213 next_url = self.GetListingURL(next_marker)
214 (new_revisions, next_marker) = _FetchAndParse(next_url)
215 revisions.extend(new_revisions)
[email protected]afe30662011-07-30 01:05:52216 return revisions
217
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.
[email protected]eadd95d2012-11-02 22:42:09222 minrev = min(self.good_revision, self.bad_revision)
223 maxrev = max(self.good_revision, self.bad_revision)
[email protected]37ed3172013-09-24 23:49:30224 revlist_all = map(int, self.ParseDirectoryIndex())
225
226 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
[email protected]afe30662011-07-30 01:05:52227 revlist.sort()
[email protected]37ed3172013-09-24 23:49:30228
229 # Set good and bad revisions to be legit revisions.
230 if revlist:
231 if self.good_revision < self.bad_revision:
232 self.good_revision = revlist[0]
233 self.bad_revision = revlist[-1]
234 else:
235 self.bad_revision = revlist[0]
236 self.good_revision = revlist[-1]
237
238 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
239 if self.base_url == WEBKIT_BASE_URL:
240 revlist_all.sort()
241 self.good_revision = FixChromiumRevForBlink(revlist,
242 revlist_all,
243 self,
244 self.good_revision)
245 self.bad_revision = FixChromiumRevForBlink(revlist,
246 revlist_all,
247 self,
248 self.bad_revision)
[email protected]afe30662011-07-30 01:05:52249 return revlist
250
[email protected]d0149c5c2012-05-29 21:12:11251 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.
[email protected]eadd95d2012-11-02 22:42:09255 minrev = min(self.good_revision, self.bad_revision)
256 maxrev = max(self.good_revision, self.bad_revision)
[email protected]d0149c5c2012-05-29 21:12:11257 handle = urllib.urlopen(OFFICIAL_BASE_URL)
258 dirindex = handle.read()
259 handle.close()
260 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex)
261 final_list = []
[email protected]d0149c5c2012-05-29 21:12:11262 i = 0
[email protected]d0149c5c2012-05-29 21:12:11263 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
267 i = i + 1
268 try:
269 connection = urllib.urlopen(path)
270 connection.close()
[email protected]801fb652012-07-20 20:13:50271 if build_number > maxrev:
272 break
273 if build_number >= minrev:
[email protected]b3b20512013-08-26 18:51:04274 # If we are bisecting Aura, we want to include only builds which
275 # ends with ".1".
276 if self.is_aura:
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))
[email protected]d0149c5c2012-05-29 21:12:11285 except urllib.HTTPError, e:
286 pass
[email protected]801fb652012-07-20 20:13:50287 return final_list
[email protected]bd8dcb92010-03-31 01:05:24288
[email protected]fc3702e2013-11-09 04:23:00289def UnzipFilenameToDir(filename, directory):
290 """Unzip |filename| to |directory|."""
[email protected]afe30662011-07-30 01:05:52291 cwd = os.getcwd()
292 if not os.path.isabs(filename):
293 filename = os.path.join(cwd, filename)
[email protected]bd8dcb92010-03-31 01:05:24294 zf = zipfile.ZipFile(filename)
295 # Make base.
[email protected]fc3702e2013-11-09 04:23:00296 if not os.path.isdir(directory):
297 os.mkdir(directory)
298 os.chdir(directory)
[email protected]e29c08c2012-09-17 20:50:50299 # Extract files.
300 for info in zf.infolist():
301 name = info.filename
302 if name.endswith('/'): # dir
303 if not os.path.isdir(name):
304 os.makedirs(name)
305 else: # file
[email protected]fc3702e2013-11-09 04:23:00306 directory = os.path.dirname(name)
307 if not os.path.isdir(directory):
308 os.makedirs(directory)
[email protected]e29c08c2012-09-17 20:50:50309 out = open(name, 'wb')
310 out.write(zf.read(name))
311 out.close()
312 # Set permissions. Permission info in external_attr is shifted 16 bits.
313 os.chmod(name, info.external_attr >> 16L)
314 os.chdir(cwd)
[email protected]bd8dcb92010-03-31 01:05:24315
[email protected]67e0bc62009-09-03 22:06:09316
[email protected]468a9772011-08-09 18:42:00317def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
[email protected]afe30662011-07-30 01:05:52318 """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.
[email protected]468a9772011-08-09 18:42:00324 @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
326 displayed.
[email protected]afe30662011-07-30 01:05:52327 """
328 def ReportHook(blocknum, blocksize, totalsize):
[email protected]946be752011-10-25 23:34:21329 if quit_event and quit_event.isSet():
[email protected]d0149c5c2012-05-29 21:12:11330 raise RuntimeError("Aborting download of revision %s" % str(rev))
[email protected]946be752011-10-25 23:34:21331 if progress_event and progress_event.isSet():
[email protected]468a9772011-08-09 18:42:00332 size = blocknum * blocksize
333 if totalsize == -1: # Total size not known.
334 progress = "Received %d bytes" % size
335 else:
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)
341 sys.stdout.flush()
[email protected]7ad66a72009-09-04 17:52:33342
[email protected]afe30662011-07-30 01:05:52343 download_url = context.GetDownloadURL(rev)
344 try:
345 urllib.urlretrieve(download_url, filename, ReportHook)
[email protected]946be752011-10-25 23:34:21346 if progress_event and progress_event.isSet():
[email protected]ecaba01e62011-10-26 05:33:28347 print
[email protected]afe30662011-07-30 01:05:52348 except RuntimeError, e:
349 pass
[email protected]7ad66a72009-09-04 17:52:33350
[email protected]7ad66a72009-09-04 17:52:33351
[email protected]4646a752013-07-19 22:14:34352def RunRevision(context, revision, zipfile, profile, num_runs, command, args):
[email protected]afe30662011-07-30 01:05:52353 """Given a zipped revision, unzip it and run the test."""
[email protected]d0149c5c2012-05-29 21:12:11354 print "Trying revision %s..." % str(revision)
[email protected]3ff00b72011-07-20 21:34:47355
[email protected]afe30662011-07-30 01:05:52356 # Create a temp directory and unzip the revision into it.
[email protected]7ad66a72009-09-04 17:52:33357 cwd = os.getcwd()
358 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
[email protected]afe30662011-07-30 01:05:52359 UnzipFilenameToDir(zipfile, tempdir)
[email protected]7ad66a72009-09-04 17:52:33360 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09361
[email protected]5e93cf162012-01-28 02:16:56362 # Run the build as many times as specified.
[email protected]4646a752013-07-19 22:14:34363 testargs = ['--user-data-dir=%s' % profile] + args
[email protected]d0149c5c2012-05-29 21:12:11364 # The sandbox must be run as root on Official Chrome, so bypass it.
[email protected]fc3702e2013-11-09 04:23:00365 if ((context.is_official or context.flash_path) and
366 context.platform.startswith('linux')):
[email protected]d0149c5c2012-05-29 21:12:11367 testargs.append('--no-sandbox')
[email protected]fc3702e2013-11-09 04:23:00368 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')
[email protected]d0149c5c2012-05-29 21:12:11374
[email protected]4646a752013-07-19 22:14:34375 runcommand = []
[email protected]61ea90a2013-09-26 10:17:34376 for token in shlex.split(command):
[email protected]4646a752013-07-19 22:14:34377 if token == "%a":
378 runcommand.extend(testargs)
379 else:
380 runcommand.append( \
381 token.replace('%p', context.GetLaunchPath()) \
382 .replace('%s', ' '.join(testargs)))
383
[email protected]5e93cf162012-01-28 02:16:56384 for i in range(0, num_runs):
[email protected]4646a752013-07-19 22:14:34385 subproc = subprocess.Popen(runcommand,
[email protected]5e93cf162012-01-28 02:16:56386 bufsize=-1,
387 stdout=subprocess.PIPE,
388 stderr=subprocess.PIPE)
389 (stdout, stderr) = subproc.communicate()
[email protected]7ad66a72009-09-04 17:52:33390
391 os.chdir(cwd)
[email protected]7ad66a72009-09-04 17:52:33392 try:
393 shutil.rmtree(tempdir, True)
394 except Exception, e:
395 pass
[email protected]67e0bc62009-09-03 22:06:09396
[email protected]afe30662011-07-30 01:05:52397 return (subproc.returncode, stdout, stderr)
[email protected]79f14742010-03-10 01:01:57398
[email protected]cb155a82011-11-29 17:25:34399
[email protected]d0149c5c2012-05-29 21:12:11400def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
[email protected]183706d92011-06-10 13:06:22401 """Ask the user whether build |rev| is good or bad."""
[email protected]79f14742010-03-10 01:01:57402 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09403 while True:
[email protected]1d4a06242013-08-20 22:53:12404 response = raw_input('Revision %s is ' \
405 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
[email protected]53bb6342012-06-01 04:11:00406 str(rev))
[email protected]1d4a06242013-08-20 22:53:12407 if response and response in ('g', 'b', 'r', 'u'):
[email protected]53bb6342012-06-01 04:11:00408 return response
[email protected]afe30662011-07-30 01:05:52409 if response and response == 'q':
410 raise SystemExit()
[email protected]67e0bc62009-09-03 22:06:09411
[email protected]cb155a82011-11-29 17:25:34412
[email protected]53bb6342012-06-01 04:11:00413class 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
419 self.name = name
420 self.rev = rev
421 self.zipfile = zipfile
422 self.quit_event = threading.Event()
423 self.progress_event = threading.Event()
424
425 def Start(self):
426 """Starts the download."""
427 fetchargs = (self.context,
428 self.rev,
429 self.zipfile,
430 self.quit_event,
431 self.progress_event)
432 self.thread = threading.Thread(target=FetchRevision,
433 name=self.name,
434 args=fetchargs)
435 self.thread.start()
436
437 def Stop(self):
438 """Stops the download which must have been started previously."""
439 self.quit_event.set()
440 self.thread.join()
441 os.unlink(self.zipfile)
442
443 def WaitFor(self):
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.
448 self.thread.join()
449
450
[email protected]4c6fec6b2013-09-17 17:44:08451def Bisect(base_url,
452 platform,
[email protected]d0149c5c2012-05-29 21:12:11453 official_builds,
[email protected]b3b20512013-08-26 18:51:04454 is_aura,
[email protected]afe30662011-07-30 01:05:52455 good_rev=0,
456 bad_rev=0,
[email protected]5e93cf162012-01-28 02:16:56457 num_runs=1,
[email protected]4646a752013-07-19 22:14:34458 command="%p %a",
[email protected]60ac66e32011-07-18 16:08:25459 try_args=(),
[email protected]afe30662011-07-30 01:05:52460 profile=None,
[email protected]fc3702e2013-11-09 04:23:00461 flash_path=None,
[email protected]53bb6342012-06-01 04:11:00462 evaluate=AskIsGoodBuild):
[email protected]afe30662011-07-30 01:05:52463 """Given known good and known bad revisions, run a binary search on all
464 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25465
[email protected]afe30662011-07-30 01:05:52466 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.).
[email protected]d0149c5c2012-05-29 21:12:11467 @param official_builds Specify build type (Chromium or Official build).
[email protected]eadd95d2012-11-02 22:42:09468 @param good_rev Number/tag of the known good revision.
469 @param bad_rev Number/tag of the known bad revision.
[email protected]5e93cf162012-01-28 02:16:56470 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52471 @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.
[email protected]53bb6342012-06-01 04:11:00473 @param evaluate A function which returns 'g' if the argument build is good,
474 'b' if it's bad or 'u' if unknown.
[email protected]afe30662011-07-30 01:05:52475
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
482 known:
483
484 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
485 is run on rev 75.
486
487 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
488 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25489 """
490
[email protected]afe30662011-07-30 01:05:52491 if not profile:
492 profile = 'profile'
493
[email protected]4c6fec6b2013-09-17 17:44:08494 context = PathContext(base_url, platform, good_rev, bad_rev,
[email protected]fc3702e2013-11-09 04:23:00495 official_builds, is_aura, flash_path)
[email protected]afe30662011-07-30 01:05:52496 cwd = os.getcwd()
497
[email protected]468a9772011-08-09 18:42:00498 print "Downloading list of known revisions..."
[email protected]d0149c5c2012-05-29 21:12:11499 _GetDownloadPath = lambda rev: os.path.join(cwd,
500 '%s-%s' % (str(rev), context.archive_name))
501 if official_builds:
502 revlist = context.GetOfficialBuildsList()
503 else:
504 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52505
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)
510
511 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09512 minrev = 0
513 maxrev = len(revlist) - 1
514 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52515 rev = revlist[pivot]
516 zipfile = _GetDownloadPath(rev)
[email protected]eadd95d2012-11-02 22:42:09517 fetch = DownloadJob(context, 'initial_fetch', rev, zipfile)
518 fetch.Start()
519 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25520
521 # Binary search time!
[email protected]eadd95d2012-11-02 22:42:09522 while fetch and fetch.zipfile and maxrev - minrev > 1:
523 if bad_rev < good_rev:
524 min_str, max_str = "bad", "good"
525 else:
526 min_str, max_str = "good", "bad"
527 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str, \
528 revlist[maxrev], max_str)
529
[email protected]afe30662011-07-30 01:05:52530 # Pre-fetch next two possible pivots
531 # - down_pivot is the next revision to check if the current revision turns
532 # out to be bad.
533 # - up_pivot is the next revision to check if the current revision turns
534 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09535 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00536 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09537 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52538 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00539 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
540 _GetDownloadPath(down_rev))
541 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25542
[email protected]eadd95d2012-11-02 22:42:09543 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00544 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09545 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52546 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00547 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
548 _GetDownloadPath(up_rev))
549 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25550
[email protected]afe30662011-07-30 01:05:52551 # Run test on the pivot revision.
[email protected]e29c08c2012-09-17 20:50:50552 status = None
553 stdout = None
554 stderr = None
555 try:
556 (status, stdout, stderr) = RunRevision(context,
557 rev,
[email protected]eadd95d2012-11-02 22:42:09558 fetch.zipfile,
[email protected]e29c08c2012-09-17 20:50:50559 profile,
560 num_runs,
[email protected]4646a752013-07-19 22:14:34561 command,
[email protected]e29c08c2012-09-17 20:50:50562 try_args)
563 except Exception, e:
[email protected]fc3702e2013-11-09 04:23:00564 print >> sys.stderr, e
[email protected]60ac66e32011-07-18 16:08:25565
[email protected]53bb6342012-06-01 04:11:00566 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52567 # On that basis, kill one of the background downloads and complete the
568 # other, as described in the comments above.
569 try:
[email protected]53bb6342012-06-01 04:11:00570 answer = evaluate(rev, official_builds, status, stdout, stderr)
[email protected]eadd95d2012-11-02 22:42:09571 if answer == 'g' and good_rev < bad_rev or \
572 answer == 'b' and bad_rev < good_rev:
[email protected]1d4a06242013-08-20 22:53:12573 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09574 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00575 if down_fetch:
576 down_fetch.Stop() # Kill the download of the older revision.
[email protected]1d4a06242013-08-20 22:53:12577 fetch = None
[email protected]53bb6342012-06-01 04:11:00578 if up_fetch:
579 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52580 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09581 fetch = up_fetch
582 elif answer == 'b' and good_rev < bad_rev or \
583 answer == 'g' and bad_rev < good_rev:
[email protected]1d4a06242013-08-20 22:53:12584 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09585 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00586 if up_fetch:
587 up_fetch.Stop() # Kill the download of the newer revision.
[email protected]1d4a06242013-08-20 22:53:12588 fetch = None
[email protected]53bb6342012-06-01 04:11:00589 if down_fetch:
590 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52591 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09592 fetch = down_fetch
[email protected]1d4a06242013-08-20 22:53:12593 elif answer == 'r':
594 pass # Retry requires no changes.
[email protected]53bb6342012-06-01 04:11:00595 elif answer == 'u':
596 # Nuke the revision from the revlist and choose a new pivot.
[email protected]1d4a06242013-08-20 22:53:12597 fetch.Stop()
[email protected]53bb6342012-06-01 04:11:00598 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09599 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00600
[email protected]eadd95d2012-11-02 22:42:09601 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00602 # 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
606 # response.
607 if up_fetch and down_fetch:
608 fetch = [up_fetch, down_fetch][len(revlist) % 2]
609 elif up_fetch:
610 fetch = up_fetch
611 else:
612 fetch = down_fetch
613 fetch.WaitFor()
614 if fetch == up_fetch:
615 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
616 else:
617 pivot = down_pivot
618 zipfile = fetch.zipfile
619
620 if down_fetch and fetch != down_fetch:
621 down_fetch.Stop()
622 if up_fetch and fetch != up_fetch:
623 up_fetch.Stop()
624 else:
625 assert False, "Unexpected return value from evaluate(): " + answer
[email protected]afe30662011-07-30 01:05:52626 except SystemExit:
[email protected]468a9772011-08-09 18:42:00627 print "Cleaning up..."
[email protected]5e93cf162012-01-28 02:16:56628 for f in [_GetDownloadPath(revlist[down_pivot]),
629 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52630 try:
631 os.unlink(f)
632 except OSError:
633 pass
634 sys.exit(0)
635
636 rev = revlist[pivot]
637
[email protected]eadd95d2012-11-02 22:42:09638 return (revlist[minrev], revlist[maxrev])
[email protected]60ac66e32011-07-18 16:08:25639
640
[email protected]37ed3172013-09-24 23:49:30641def GetBlinkDEPSRevisionForChromiumRevision(rev):
[email protected]4c6fec6b2013-09-17 17:44:08642 """Returns the blink revision that was in REVISIONS file at
[email protected]b2fe7f22011-10-25 22:58:31643 chromium revision |rev|."""
644 # . doesn't match newlines without re.DOTALL, so this is safe.
[email protected]37ed3172013-09-24 23:49:30645 blink_re = re.compile(r'webkit_revision\D*(\d+)')
646 url = urllib.urlopen(DEPS_FILE % rev)
647 m = blink_re.search(url.read())
648 url.close()
649 if m:
650 return int(m.group(1))
651 else:
652 raise Exception('Could not get Blink revision for Chromium rev %d'
653 % rev)
654
655
656def GetBlinkRevisionForChromiumRevision(self, rev):
657 """Returns the blink revision that was in REVISIONS file at
658 chromium revision |rev|."""
[email protected]4c6fec6b2013-09-17 17:44:08659 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())
[email protected]b2fe7f22011-10-25 22:58:31663 url.close()
[email protected]4c6fec6b2013-09-17 17:44:08664 if 'webkit_revision' in data:
665 return data['webkit_revision']
[email protected]b2fe7f22011-10-25 22:58:31666 else:
[email protected]ff50d1c2013-04-17 18:49:36667 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:31668
[email protected]37ed3172013-09-24 23:49:30669def 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."""
675
676 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(rev)
677
678 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
679 idx = revisions.index(rev)
680 if idx > 0:
681 rev = revisions[idx-1]
682 if rev not in revisions_final:
683 revisions_final.insert(0, rev)
684
685 revisions_final.sort()
686 return rev
[email protected]b2fe7f22011-10-25 22:58:31687
[email protected]801fb652012-07-20 20:13:50688def GetChromiumRevision(url):
689 """Returns the chromium revision read from given URL."""
690 try:
691 # Location of the latest build revision number
692 return int(urllib.urlopen(url).read())
693 except Exception, e:
694 print('Could not determine latest revision. This could be bad...')
695 return 999999999
696
697
[email protected]67e0bc62009-09-03 22:06:09698def main():
[email protected]2c1d2732009-10-29 19:52:17699 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:31700 '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'
[email protected]09c58da2013-01-07 21:30:17704 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:38705 '\n'
[email protected]887c9182013-02-12 20:30:31706 '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'
713 '\n'
[email protected]178aab72010-10-08 17:21:38714 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:33715 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:57716 # Strangely, the default help output doesn't include the choice list.
[email protected]7aec9e82013-05-09 05:09:23717 choices = ['mac', 'win', 'linux', 'linux64', 'linux-arm']
[email protected]4082b182011-05-02 20:30:17718 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
[email protected]7ad66a72009-09-04 17:52:33719 parser.add_option('-a', '--archive',
[email protected]1a45d222009-09-19 01:58:57720 choices = choices,
721 help = 'The buildbot archive to bisect [%s].' %
722 '|'.join(choices))
[email protected]d0149c5c2012-05-29 21:12:11723 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',
[email protected]09c58da2013-01-07 21:30:17728 help = 'A bad revision to start bisection. ' +
729 'May be earlier or later than the good revision. ' +
730 'Default is HEAD.')
[email protected]fc3702e2013-11-09 04:23:00731 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).')
[email protected]d0149c5c2012-05-29 21:12:11736 parser.add_option('-g', '--good', type = 'str',
[email protected]09c58da2013-01-07 21:30:17737 help = 'A good revision to start bisection. ' +
738 'May be earlier or later than the bad revision. ' +
[email protected]801fb652012-07-20 20:13:50739 'Default is 0.')
[email protected]d4bf3582009-09-20 00:56:38740 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
741 help = 'Profile to use; this will not reset every run. ' +
[email protected]60ac66e32011-07-18 16:08:25742 'Defaults to a clean profile.', default = 'profile')
[email protected]5e93cf162012-01-28 02:16:56743 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.',
746 default = 1)
[email protected]4646a752013-07-19 22:14:34747 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.',
[email protected]fc3702e2013-11-09 04:23:00753 default = '%p %a')
[email protected]4c6fec6b2013-09-17 17:44:08754 parser.add_option('-l', '--blink', action='store_true',
755 help = 'Use Blink bisect instead of Chromium. ')
[email protected]b3b20512013-08-26 18:51:04756 parser.add_option('--aura',
757 dest='aura',
758 action='store_true',
759 default=False,
760 help='Allow the script to bisect aura builds')
761
[email protected]7ad66a72009-09-04 17:52:33762 (opts, args) = parser.parse_args()
763
764 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:38765 print 'Error: missing required parameter: --archive'
766 print
[email protected]7ad66a72009-09-04 17:52:33767 parser.print_help()
768 return 1
769
[email protected]b3b20512013-08-26 18:51:04770 if opts.aura:
771 if opts.archive != 'win' or not opts.official_builds:
772 print 'Error: Aura is supported only on Windows platform '\
773 'and official builds.'
774 return 1
775
[email protected]4c6fec6b2013-09-17 17:44:08776 if opts.blink:
777 base_url = WEBKIT_BASE_URL
778 else:
779 base_url = CHROMIUM_BASE_URL
780
[email protected]183706d92011-06-10 13:06:22781 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]4c6fec6b2013-09-17 17:44:08782 context = PathContext(base_url, opts.archive, 0, 0,
[email protected]fc3702e2013-11-09 04:23:00783 opts.official_builds, opts.aura, None)
[email protected]67e0bc62009-09-03 22:06:09784 # Pick a starting point, try to get HEAD for this.
[email protected]7ad66a72009-09-04 17:52:33785 if opts.bad:
786 bad_rev = opts.bad
787 else:
[email protected]801fb652012-07-20 20:13:50788 bad_rev = '999.0.0.0'
789 if not opts.official_builds:
790 bad_rev = GetChromiumRevision(context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:09791
792 # Find out when we were good.
[email protected]7ad66a72009-09-04 17:52:33793 if opts.good:
794 good_rev = opts.good
795 else:
[email protected]801fb652012-07-20 20:13:50796 good_rev = '0.0.0.0' if opts.official_builds else 0
797
[email protected]fc3702e2013-11-09 04:23:00798 if opts.flash_path:
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
802
[email protected]801fb652012-07-20 20:13:50803 if opts.official_builds:
804 good_rev = LooseVersion(good_rev)
805 bad_rev = LooseVersion(bad_rev)
806 else:
807 good_rev = int(good_rev)
808 bad_rev = int(bad_rev)
809
[email protected]5e93cf162012-01-28 02:16:56810 if opts.times < 1:
811 print('Number of times to run (%d) must be greater than or equal to 1.' %
812 opts.times)
813 parser.print_help()
814 return 1
815
[email protected]eadd95d2012-11-02 22:42:09816 (min_chromium_rev, max_chromium_rev) = Bisect(
[email protected]4c6fec6b2013-09-17 17:44:08817 base_url, opts.archive, opts.official_builds, opts.aura, good_rev,
[email protected]fc3702e2013-11-09 04:23:00818 bad_rev, opts.times, opts.command, args, opts.profile, opts.flash_path)
[email protected]67e0bc62009-09-03 22:06:09819
[email protected]ff50d1c2013-04-17 18:49:36820 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:31821 try:
[email protected]4c6fec6b2013-09-17 17:44:08822 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
823 min_chromium_rev)
824 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
825 max_chromium_rev)
[email protected]b2fe7f22011-10-25 22:58:31826 except Exception, e:
827 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:36828 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:31829
[email protected]3bdaa4752013-09-30 20:13:36830 if opts.blink:
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))
834 else:
835 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
[email protected]eadd95d2012-11-02 22:42:09836
[email protected]ff50d1c2013-04-17 18:49:36837 print 'BLINK CHANGELOG URL:'
838 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]3bdaa4752013-09-30 20:13:36839
[email protected]d0149c5c2012-05-29 21:12:11840 else:
[email protected]3bdaa4752013-09-30 20:13:36841 # 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))
845 else:
846 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
847 str(max_chromium_rev))
848 if min_blink_rev != max_blink_rev:
[email protected]20fea8cd2013-10-01 00:10:21849 print ("NOTE: There is a Blink roll in the range, "
850 "you might also want to do a Blink bisect.")
[email protected]3bdaa4752013-09-30 20:13:36851
852 print 'CHANGELOG URL:'
853 if opts.official_builds:
854 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
855 else:
856 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:34857
[email protected]67e0bc62009-09-03 22:06:09858if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:33859 sys.exit(main())