blob: 3eeee608e133c63341d8fe6d64cac429b465435c [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]4df583c2014-07-31 17:11:5515# The base URL for stored build archives.
16CHROMIUM_BASE_URL = ('http://commondatastorage.googleapis.com'
17 '/chromium-browser-snapshots')
18WEBKIT_BASE_URL = ('http://commondatastorage.googleapis.com'
19 '/chromium-webkit-snapshots')
[email protected]011886692014-08-01 21:00:2120ASAN_BASE_URL = ('http://commondatastorage.googleapis.com'
21 '/chromium-browser-asan')
[email protected]67e0bc62009-09-03 22:06:0922
[email protected]83048502014-08-21 16:48:4423# GS bucket name.
24GS_BUCKET_NAME = 'chrome-unsigned/desktop-W15K3Y'
25
26# Base URL for downloading official builds.
27GOOGLE_APIS_URL = 'commondatastorage.googleapis.com'
28
[email protected]4df583c2014-07-31 17:11:5529# The base URL for official builds.
[email protected]83048502014-08-21 16:48:4430OFFICIAL_BASE_URL = 'http://%s/%s' % (GOOGLE_APIS_URL, GS_BUCKET_NAME)
[email protected]d0149c5c2012-05-29 21:12:1131
[email protected]4df583c2014-07-31 17:11:5532# URL template for viewing changelogs between revisions.
pshenoy9ce271f2014-09-02 22:14:0533CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s')
34
35# URL to convert SVN revision to git hash.
36CRREV_URL = ('http://crrev.com/')
37
38# Search pattern to match git hash.
39GITHASH_SEARCH_PATTERN = (r'<title>(\w+)\s')
[email protected]f6a71a72009-10-08 19:55:3840
[email protected]4df583c2014-07-31 17:11:5541# URL template for viewing changelogs between official versions.
pshenoyea8fe7d42014-08-26 22:25:2642OFFICIAL_CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/'
43 'src/+log/%s..%s?pretty=full')
[email protected]d0149c5c2012-05-29 21:12:1144
[email protected]b2fe7f22011-10-25 22:58:3145# DEPS file URL.
[email protected]fc3702e2013-11-09 04:23:0046DEPS_FILE = 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
[email protected]b2fe7f22011-10-25 22:58:3147
[email protected]4df583c2014-07-31 17:11:5548# Blink changelogs URL.
49BLINK_CHANGELOG_URL = ('http://build.chromium.org'
50 '/f/chromium/perf/dashboard/ui/changelog_blink.html'
51 '?url=/trunk&range=%d%%3A%d')
52
53DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s ('
54 'known good), but no later than %s (first known bad).')
55DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s ('
56 'known bad), but no later than %s (first known good).')
[email protected]05ff3fd2012-04-17 23:24:0657
[email protected]3e7c85322014-06-27 20:27:3658CHROMIUM_GITHASH_TO_SVN_URL = (
59 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5560
[email protected]3e7c85322014-06-27 20:27:3661BLINK_GITHASH_TO_SVN_URL = (
62 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5563
64GITHASH_TO_SVN_URL = {
65 'chromium': CHROMIUM_GITHASH_TO_SVN_URL,
66 'blink': BLINK_GITHASH_TO_SVN_URL,
67}
68
69# Search pattern to be matched in the JSON output from
[email protected]3e7c85322014-06-27 20:27:3670# CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision).
71CHROMIUM_SEARCH_PATTERN = (
72 r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ')
[email protected]4df583c2014-07-31 17:11:5573
[email protected]3e7c85322014-06-27 20:27:3674# Search pattern to be matched in the json output from
75# BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision).
76BLINK_SEARCH_PATTERN = (
77 r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ')
[email protected]4df583c2014-07-31 17:11:5578
79SEARCH_PATTERN = {
80 'chromium': CHROMIUM_SEARCH_PATTERN,
81 'blink': BLINK_SEARCH_PATTERN,
82}
[email protected]3e7c85322014-06-27 20:27:3683
[email protected]480369782014-08-22 20:15:5884CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with '
85 'no configured credentials')
86
[email protected]67e0bc62009-09-03 22:06:0987###############################################################################
88
[email protected]83048502014-08-21 16:48:4489import httplib
[email protected]4c6fec6b2013-09-17 17:44:0890import json
[email protected]7ad66a72009-09-04 17:52:3391import optparse
[email protected]67e0bc62009-09-03 22:06:0992import os
93import re
[email protected]61ea90a2013-09-26 10:17:3494import shlex
[email protected]67e0bc62009-09-03 22:06:0995import shutil
[email protected]afe30662011-07-30 01:05:5296import subprocess
[email protected]67e0bc62009-09-03 22:06:0997import sys
[email protected]7ad66a72009-09-04 17:52:3398import tempfile
[email protected]afe30662011-07-30 01:05:5299import threading
[email protected]67e0bc62009-09-03 22:06:09100import urllib
[email protected]d0149c5c2012-05-29 21:12:11101from distutils.version import LooseVersion
[email protected]183706d92011-06-10 13:06:22102from xml.etree import ElementTree
[email protected]bd8dcb92010-03-31 01:05:24103import zipfile
104
[email protected]cb155a82011-11-29 17:25:34105
[email protected]183706d92011-06-10 13:06:22106class PathContext(object):
107 """A PathContext is used to carry the information used to construct URLs and
108 paths when dealing with the storage server and archives."""
[email protected]4c6fec6b2013-09-17 17:44:08109 def __init__(self, base_url, platform, good_revision, bad_revision,
[email protected]965e18fc2014-08-14 20:10:18110 is_official, is_asan, use_local_repo, flash_path = None,
[email protected]cdb77062014-07-21 18:07:15111 pdf_path = None):
[email protected]183706d92011-06-10 13:06:22112 super(PathContext, self).__init__()
113 # Store off the input parameters.
[email protected]4c6fec6b2013-09-17 17:44:08114 self.base_url = base_url
[email protected]183706d92011-06-10 13:06:22115 self.platform = platform # What's passed in to the '-a/--archive' option.
116 self.good_revision = good_revision
117 self.bad_revision = bad_revision
[email protected]d0149c5c2012-05-29 21:12:11118 self.is_official = is_official
[email protected]011886692014-08-01 21:00:21119 self.is_asan = is_asan
120 self.build_type = 'release'
[email protected]fc3702e2013-11-09 04:23:00121 self.flash_path = flash_path
[email protected]3e7c85322014-06-27 20:27:36122 # Dictionary which stores svn revision number as key and it's
123 # corresponding git hash as value. This data is populated in
124 # _FetchAndParse and used later in GetDownloadURL while downloading
125 # the build.
126 self.githash_svn_dict = {}
[email protected]cdb77062014-07-21 18:07:15127 self.pdf_path = pdf_path
[email protected]183706d92011-06-10 13:06:22128
129 # The name of the ZIP file in a revision directory on the server.
130 self.archive_name = None
131
[email protected]6a7a5d62014-07-09 04:45:50132 # If the script is run from a local Chromium checkout,
133 # "--use-local-repo" option can be used to make the script run faster.
134 # It uses "git svn find-rev <SHA1>" command to convert git hash to svn
135 # revision number.
136 self.use_local_repo = use_local_repo
137
[email protected]183706d92011-06-10 13:06:22138 # Set some internal members:
139 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
140 # _archive_extract_dir = Uncompressed directory in the archive_name file.
141 # _binary_name = The name of the executable to run.
[email protected]7aec9e82013-05-09 05:09:23142 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]183706d92011-06-10 13:06:22143 self._binary_name = 'chrome'
[email protected]480369782014-08-22 20:15:58144 elif self.platform in ('mac', 'mac64'):
[email protected]183706d92011-06-10 13:06:22145 self.archive_name = 'chrome-mac.zip'
146 self._archive_extract_dir = 'chrome-mac'
[email protected]480369782014-08-22 20:15:58147 elif self.platform in ('win', 'win64'):
[email protected]183706d92011-06-10 13:06:22148 self.archive_name = 'chrome-win32.zip'
149 self._archive_extract_dir = 'chrome-win32'
150 self._binary_name = 'chrome.exe'
151 else:
[email protected]afe30662011-07-30 01:05:52152 raise Exception('Invalid platform: %s' % self.platform)
[email protected]183706d92011-06-10 13:06:22153
[email protected]d0149c5c2012-05-29 21:12:11154 if is_official:
155 if self.platform == 'linux':
[email protected]83048502014-08-21 16:48:44156 self._listing_platform_dir = 'precise32/'
157 self.archive_name = 'chrome-precise32.zip'
158 self._archive_extract_dir = 'chrome-precise32'
[email protected]d0149c5c2012-05-29 21:12:11159 elif self.platform == 'linux64':
[email protected]83048502014-08-21 16:48:44160 self._listing_platform_dir = 'precise64/'
161 self.archive_name = 'chrome-precise64.zip'
162 self._archive_extract_dir = 'chrome-precise64'
[email protected]d0149c5c2012-05-29 21:12:11163 elif self.platform == 'mac':
164 self._listing_platform_dir = 'mac/'
165 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
[email protected]480369782014-08-22 20:15:58166 elif self.platform == 'mac64':
167 self._listing_platform_dir = 'mac64/'
168 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
[email protected]d0149c5c2012-05-29 21:12:11169 elif self.platform == 'win':
[email protected]965e18fc2014-08-14 20:10:18170 self._listing_platform_dir = 'win/'
[email protected]00ae3c02014-08-21 18:41:23171 self.archive_name = 'chrome-win.zip'
172 self._archive_extract_dir = 'chrome-win'
[email protected]480369782014-08-22 20:15:58173 elif self.platform == 'win64':
174 self._listing_platform_dir = 'win64/'
175 self.archive_name = 'chrome-win64.zip'
176 self._archive_extract_dir = 'chrome-win64'
[email protected]d0149c5c2012-05-29 21:12:11177 else:
[email protected]7aec9e82013-05-09 05:09:23178 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]d0149c5c2012-05-29 21:12:11179 self.archive_name = 'chrome-linux.zip'
180 self._archive_extract_dir = 'chrome-linux'
181 if self.platform == 'linux':
182 self._listing_platform_dir = 'Linux/'
183 elif self.platform == 'linux64':
184 self._listing_platform_dir = 'Linux_x64/'
[email protected]7aec9e82013-05-09 05:09:23185 elif self.platform == 'linux-arm':
186 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
[email protected]d0149c5c2012-05-29 21:12:11187 elif self.platform == 'mac':
188 self._listing_platform_dir = 'Mac/'
189 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
190 elif self.platform == 'win':
191 self._listing_platform_dir = 'Win/'
192
[email protected]011886692014-08-01 21:00:21193 def GetASANPlatformDir(self):
194 """ASAN builds are in directories like "linux-release", or have filenames
195 like "asan-win32-release-277079.zip". This aligns to our platform names
196 except in the case of Windows where they use "win32" instead of "win"."""
197 if self.platform == 'win':
198 return 'win32'
199 else:
200 return self.platform
201
[email protected]183706d92011-06-10 13:06:22202 def GetListingURL(self, marker=None):
203 """Returns the URL for a directory listing, with an optional marker."""
204 marker_param = ''
205 if marker:
206 marker_param = '&marker=' + str(marker)
[email protected]011886692014-08-01 21:00:21207 if self.is_asan:
208 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type)
209 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param
210 else:
211 return (self.base_url + '/?delimiter=/&prefix=' +
212 self._listing_platform_dir + marker_param)
[email protected]183706d92011-06-10 13:06:22213
214 def GetDownloadURL(self, revision):
215 """Gets the download URL for a build archive of a specific revision."""
[email protected]011886692014-08-01 21:00:21216 if self.is_asan:
217 return '%s/%s-%s/%s-%d.zip' % (
218 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
219 self.GetASANBaseName(), revision)
[email protected]d0149c5c2012-05-29 21:12:11220 if self.is_official:
[email protected]4df583c2014-07-31 17:11:55221 return '%s/%s/%s%s' % (
[email protected]d0149c5c2012-05-29 21:12:11222 OFFICIAL_BASE_URL, revision, self._listing_platform_dir,
223 self.archive_name)
224 else:
[email protected]3e7c85322014-06-27 20:27:36225 if str(revision) in self.githash_svn_dict:
226 revision = self.githash_svn_dict[str(revision)]
[email protected]4df583c2014-07-31 17:11:55227 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
[email protected]4c6fec6b2013-09-17 17:44:08228 revision, self.archive_name)
[email protected]183706d92011-06-10 13:06:22229
230 def GetLastChangeURL(self):
231 """Returns a URL to the LAST_CHANGE file."""
[email protected]4c6fec6b2013-09-17 17:44:08232 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
[email protected]183706d92011-06-10 13:06:22233
[email protected]011886692014-08-01 21:00:21234 def GetASANBaseName(self):
235 """Returns the base name of the ASAN zip file."""
236 if 'linux' in self.platform:
237 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(),
238 self.build_type)
239 else:
240 return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type)
241
242 def GetLaunchPath(self, revision):
[email protected]183706d92011-06-10 13:06:22243 """Returns a relative path (presumably from the archive extraction location)
244 that is used to run the executable."""
[email protected]011886692014-08-01 21:00:21245 if self.is_asan:
246 extract_dir = '%s-%d' % (self.GetASANBaseName(), revision)
247 else:
248 extract_dir = self._archive_extract_dir
249 return os.path.join(extract_dir, self._binary_name)
[email protected]183706d92011-06-10 13:06:22250
[email protected]afe30662011-07-30 01:05:52251 def ParseDirectoryIndex(self):
252 """Parses the Google Storage directory listing into a list of revision
[email protected]eadd95d2012-11-02 22:42:09253 numbers."""
[email protected]afe30662011-07-30 01:05:52254
255 def _FetchAndParse(url):
256 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
257 next-marker is not None, then the listing is a partial listing and another
258 fetch should be performed with next-marker being the marker= GET
259 parameter."""
260 handle = urllib.urlopen(url)
261 document = ElementTree.parse(handle)
262
263 # All nodes in the tree are namespaced. Get the root's tag name to extract
264 # the namespace. Etree does namespaces as |{namespace}tag|.
265 root_tag = document.getroot().tag
266 end_ns_pos = root_tag.find('}')
267 if end_ns_pos == -1:
[email protected]4df583c2014-07-31 17:11:55268 raise Exception('Could not locate end namespace for directory index')
[email protected]afe30662011-07-30 01:05:52269 namespace = root_tag[:end_ns_pos + 1]
270
271 # Find the prefix (_listing_platform_dir) and whether or not the list is
272 # truncated.
273 prefix_len = len(document.find(namespace + 'Prefix').text)
274 next_marker = None
275 is_truncated = document.find(namespace + 'IsTruncated')
276 if is_truncated is not None and is_truncated.text.lower() == 'true':
277 next_marker = document.find(namespace + 'NextMarker').text
[email protected]afe30662011-07-30 01:05:52278 # Get a list of all the revisions.
[email protected]afe30662011-07-30 01:05:52279 revisions = []
[email protected]3e7c85322014-06-27 20:27:36280 githash_svn_dict = {}
[email protected]011886692014-08-01 21:00:21281 if self.is_asan:
282 asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName()))
283 # Non ASAN builds are in a <revision> directory. The ASAN builds are
284 # flat
285 all_prefixes = document.findall(namespace + 'Contents/' +
286 namespace + 'Key')
287 for prefix in all_prefixes:
288 m = asan_regex.match(prefix.text)
289 if m:
290 try:
291 revisions.append(int(m.group(1)))
292 except ValueError:
293 pass
294 else:
295 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
296 namespace + 'Prefix')
297 # The <Prefix> nodes have content of the form of
298 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
299 # trailing slash to just have a number.
300 for prefix in all_prefixes:
301 revnum = prefix.text[prefix_len:-1]
302 try:
303 if not revnum.isdigit():
304 git_hash = revnum
305 revnum = self.GetSVNRevisionFromGitHash(git_hash)
306 githash_svn_dict[revnum] = git_hash
307 if revnum is not None:
308 revnum = int(revnum)
309 revisions.append(revnum)
310 except ValueError:
311 pass
[email protected]3e7c85322014-06-27 20:27:36312 return (revisions, next_marker, githash_svn_dict)
[email protected]9639b002013-08-30 14:45:52313
[email protected]afe30662011-07-30 01:05:52314 # Fetch the first list of revisions.
[email protected]4df583c2014-07-31 17:11:55315 (revisions, next_marker, self.githash_svn_dict) = _FetchAndParse(
316 self.GetListingURL())
[email protected]afe30662011-07-30 01:05:52317 # If the result list was truncated, refetch with the next marker. Do this
318 # until an entire directory listing is done.
319 while next_marker:
320 next_url = self.GetListingURL(next_marker)
[email protected]3e7c85322014-06-27 20:27:36321 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url)
[email protected]afe30662011-07-30 01:05:52322 revisions.extend(new_revisions)
[email protected]3e7c85322014-06-27 20:27:36323 self.githash_svn_dict.update(new_dict)
[email protected]afe30662011-07-30 01:05:52324 return revisions
325
[email protected]6a7a5d62014-07-09 04:45:50326 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot):
[email protected]3e7c85322014-06-27 20:27:36327 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1
[email protected]2e0f2672014-08-13 20:32:58328 response = urllib.urlopen(json_url)
329 if response.getcode() == 200:
330 try:
331 data = json.loads(response.read()[4:])
332 except ValueError:
333 print 'ValueError for JSON URL: %s' % json_url
334 raise ValueError
335 else:
336 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36337 if 'message' in data:
338 message = data['message'].split('\n')
339 message = [line for line in message if line.strip()]
340 search_pattern = re.compile(SEARCH_PATTERN[depot])
341 result = search_pattern.search(message[len(message)-1])
342 if result:
343 return result.group(1)
344 print 'Failed to get svn revision number for %s' % git_sha1
[email protected]1f99f4d2014-07-23 16:44:14345 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36346
[email protected]6a7a5d62014-07-09 04:45:50347 def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot):
348 def _RunGit(command, path):
349 command = ['git'] + command
350 if path:
351 original_path = os.getcwd()
352 os.chdir(path)
353 shell = sys.platform.startswith('win')
354 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
355 stderr=subprocess.PIPE)
356 (output, _) = proc.communicate()
357
358 if path:
359 os.chdir(original_path)
360 return (output, proc.returncode)
361
362 path = None
363 if depot == 'blink':
364 path = os.path.join(os.getcwd(), 'third_party', 'WebKit')
365 if os.path.basename(os.getcwd()) == 'src':
366 command = ['svn', 'find-rev', git_sha1]
367 (git_output, return_code) = _RunGit(command, path)
368 if not return_code:
369 return git_output.strip('\n')
[email protected]1f99f4d2014-07-23 16:44:14370 raise ValueError
[email protected]6a7a5d62014-07-09 04:45:50371 else:
372 print ('Script should be run from src folder. ' +
373 'Eg: python tools/bisect-builds.py -g 280588 -b 280590' +
374 '--archive linux64 --use-local-repo')
375 sys.exit(1)
376
377 def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'):
378 if not self.use_local_repo:
379 return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot)
380 else:
381 return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot)
382
[email protected]afe30662011-07-30 01:05:52383 def GetRevList(self):
384 """Gets the list of revision numbers between self.good_revision and
385 self.bad_revision."""
386 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09387 minrev = min(self.good_revision, self.bad_revision)
388 maxrev = max(self.good_revision, self.bad_revision)
[email protected]37ed3172013-09-24 23:49:30389 revlist_all = map(int, self.ParseDirectoryIndex())
390
391 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
[email protected]afe30662011-07-30 01:05:52392 revlist.sort()
[email protected]37ed3172013-09-24 23:49:30393
394 # Set good and bad revisions to be legit revisions.
395 if revlist:
396 if self.good_revision < self.bad_revision:
397 self.good_revision = revlist[0]
398 self.bad_revision = revlist[-1]
399 else:
400 self.bad_revision = revlist[0]
401 self.good_revision = revlist[-1]
402
403 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
404 if self.base_url == WEBKIT_BASE_URL:
405 revlist_all.sort()
406 self.good_revision = FixChromiumRevForBlink(revlist,
407 revlist_all,
408 self,
409 self.good_revision)
410 self.bad_revision = FixChromiumRevForBlink(revlist,
411 revlist_all,
412 self,
413 self.bad_revision)
[email protected]afe30662011-07-30 01:05:52414 return revlist
415
[email protected]d0149c5c2012-05-29 21:12:11416 def GetOfficialBuildsList(self):
417 """Gets the list of official build numbers between self.good_revision and
418 self.bad_revision."""
[email protected]83048502014-08-21 16:48:44419
420 def CheckDepotToolsInPath():
421 delimiter = ';' if sys.platform.startswith('win') else ':'
422 path_list = os.environ['PATH'].split(delimiter)
423 for path in path_list:
424 if path.find('depot_tools') != -1:
425 return path
426 return None
427
428 def RunGsutilCommand(args):
429 gsutil_path = CheckDepotToolsInPath()
430 if gsutil_path is None:
431 print ('Follow the instructions in this document '
432 'http://dev.chromium.org/developers/how-tos/install-depot-tools'
433 ' to install depot_tools and then try again.')
434 sys.exit(1)
435 gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil')
436 gsutil = subprocess.Popen([sys.executable, gsutil_path] + args,
437 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
438 env=None)
439 stdout, stderr = gsutil.communicate()
440 if gsutil.returncode:
[email protected]480369782014-08-22 20:15:58441 if (re.findall(r'status[ |=]40[1|3]', stderr) or
442 stderr.startswith(CREDENTIAL_ERROR_MESSAGE)):
[email protected]83048502014-08-21 16:48:44443 print ('Follow these steps to configure your credentials and try'
444 ' running the bisect-builds.py again.:\n'
445 ' 1. Run "python %s config" and follow its instructions.\n'
446 ' 2. If you have a @google.com account, use that account.\n'
447 ' 3. For the project-id, just enter 0.' % gsutil_path)
448 sys.exit(1)
449 else:
[email protected]480369782014-08-22 20:15:58450 raise Exception('Error running the gsutil command: %s' % stderr)
[email protected]83048502014-08-21 16:48:44451 return stdout
452
453 def GsutilList(bucket):
454 query = 'gs://%s/' % bucket
455 stdout = RunGsutilCommand(['ls', query])
456 return [url[len(query):].strip('/') for url in stdout.splitlines()]
457
[email protected]d0149c5c2012-05-29 21:12:11458 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09459 minrev = min(self.good_revision, self.bad_revision)
460 maxrev = max(self.good_revision, self.bad_revision)
[email protected]83048502014-08-21 16:48:44461 build_numbers = GsutilList(GS_BUCKET_NAME)
462 revision_re = re.compile(r'(\d\d\.\d\.\d{4}\.\d+)')
463 build_numbers = filter(lambda b: revision_re.search(b), build_numbers)
[email protected]d0149c5c2012-05-29 21:12:11464 final_list = []
[email protected]d0149c5c2012-05-29 21:12:11465 parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
[email protected]83048502014-08-21 16:48:44466 connection = httplib.HTTPConnection(GOOGLE_APIS_URL)
[email protected]d0149c5c2012-05-29 21:12:11467 for build_number in sorted(parsed_build_numbers):
[email protected]83048502014-08-21 16:48:44468 if build_number > maxrev:
469 break
470 if build_number < minrev:
471 continue
472 path = ('/' + GS_BUCKET_NAME + '/' + str(build_number) + '/' +
[email protected]4df583c2014-07-31 17:11:55473 self._listing_platform_dir + self.archive_name)
[email protected]83048502014-08-21 16:48:44474 connection.request('HEAD', path)
475 response = connection.getresponse()
476 if response.status == 200:
477 final_list.append(str(build_number))
478 response.read()
479 connection.close()
[email protected]801fb652012-07-20 20:13:50480 return final_list
[email protected]bd8dcb92010-03-31 01:05:24481
[email protected]fc3702e2013-11-09 04:23:00482def UnzipFilenameToDir(filename, directory):
483 """Unzip |filename| to |directory|."""
[email protected]afe30662011-07-30 01:05:52484 cwd = os.getcwd()
485 if not os.path.isabs(filename):
486 filename = os.path.join(cwd, filename)
[email protected]bd8dcb92010-03-31 01:05:24487 zf = zipfile.ZipFile(filename)
488 # Make base.
[email protected]fc3702e2013-11-09 04:23:00489 if not os.path.isdir(directory):
490 os.mkdir(directory)
491 os.chdir(directory)
[email protected]e29c08c2012-09-17 20:50:50492 # Extract files.
493 for info in zf.infolist():
494 name = info.filename
495 if name.endswith('/'): # dir
496 if not os.path.isdir(name):
497 os.makedirs(name)
498 else: # file
[email protected]fc3702e2013-11-09 04:23:00499 directory = os.path.dirname(name)
500 if not os.path.isdir(directory):
501 os.makedirs(directory)
[email protected]e29c08c2012-09-17 20:50:50502 out = open(name, 'wb')
503 out.write(zf.read(name))
504 out.close()
505 # Set permissions. Permission info in external_attr is shifted 16 bits.
506 os.chmod(name, info.external_attr >> 16L)
507 os.chdir(cwd)
[email protected]bd8dcb92010-03-31 01:05:24508
[email protected]67e0bc62009-09-03 22:06:09509
[email protected]468a9772011-08-09 18:42:00510def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
[email protected]afe30662011-07-30 01:05:52511 """Downloads and unzips revision |rev|.
512 @param context A PathContext instance.
513 @param rev The Chromium revision number/tag to download.
514 @param filename The destination for the downloaded file.
515 @param quit_event A threading.Event which will be set by the master thread to
516 indicate that the download should be aborted.
[email protected]468a9772011-08-09 18:42:00517 @param progress_event A threading.Event which will be set by the master thread
518 to indicate that the progress of the download should be
519 displayed.
[email protected]afe30662011-07-30 01:05:52520 """
521 def ReportHook(blocknum, blocksize, totalsize):
[email protected]946be752011-10-25 23:34:21522 if quit_event and quit_event.isSet():
[email protected]4df583c2014-07-31 17:11:55523 raise RuntimeError('Aborting download of revision %s' % str(rev))
[email protected]946be752011-10-25 23:34:21524 if progress_event and progress_event.isSet():
[email protected]468a9772011-08-09 18:42:00525 size = blocknum * blocksize
526 if totalsize == -1: # Total size not known.
[email protected]4df583c2014-07-31 17:11:55527 progress = 'Received %d bytes' % size
[email protected]468a9772011-08-09 18:42:00528 else:
529 size = min(totalsize, size)
[email protected]4df583c2014-07-31 17:11:55530 progress = 'Received %d of %d bytes, %.2f%%' % (
[email protected]468a9772011-08-09 18:42:00531 size, totalsize, 100.0 * size / totalsize)
532 # Send a \r to let all progress messages use just one line of output.
[email protected]4df583c2014-07-31 17:11:55533 sys.stdout.write('\r' + progress)
[email protected]468a9772011-08-09 18:42:00534 sys.stdout.flush()
[email protected]7ad66a72009-09-04 17:52:33535
[email protected]afe30662011-07-30 01:05:52536 download_url = context.GetDownloadURL(rev)
537 try:
538 urllib.urlretrieve(download_url, filename, ReportHook)
[email protected]946be752011-10-25 23:34:21539 if progress_event and progress_event.isSet():
[email protected]ecaba01e62011-10-26 05:33:28540 print
[email protected]4df583c2014-07-31 17:11:55541 except RuntimeError:
[email protected]afe30662011-07-30 01:05:52542 pass
[email protected]7ad66a72009-09-04 17:52:33543
[email protected]7ad66a72009-09-04 17:52:33544
[email protected]4df583c2014-07-31 17:11:55545def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
[email protected]afe30662011-07-30 01:05:52546 """Given a zipped revision, unzip it and run the test."""
[email protected]4df583c2014-07-31 17:11:55547 print 'Trying revision %s...' % str(revision)
[email protected]3ff00b72011-07-20 21:34:47548
[email protected]afe30662011-07-30 01:05:52549 # Create a temp directory and unzip the revision into it.
[email protected]7ad66a72009-09-04 17:52:33550 cwd = os.getcwd()
551 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
[email protected]4df583c2014-07-31 17:11:55552 UnzipFilenameToDir(zip_file, tempdir)
[email protected]7ad66a72009-09-04 17:52:33553 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09554
[email protected]5e93cf162012-01-28 02:16:56555 # Run the build as many times as specified.
[email protected]4646a752013-07-19 22:14:34556 testargs = ['--user-data-dir=%s' % profile] + args
[email protected]d0149c5c2012-05-29 21:12:11557 # The sandbox must be run as root on Official Chrome, so bypass it.
[email protected]cdb77062014-07-21 18:07:15558 if ((context.is_official or context.flash_path or context.pdf_path) and
[email protected]fc3702e2013-11-09 04:23:00559 context.platform.startswith('linux')):
[email protected]d0149c5c2012-05-29 21:12:11560 testargs.append('--no-sandbox')
[email protected]fc3702e2013-11-09 04:23:00561 if context.flash_path:
562 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
563 # We have to pass a large enough Flash version, which currently needs not
564 # be correct. Instead of requiring the user of the script to figure out and
565 # pass the correct version we just spoof it.
566 testargs.append('--ppapi-flash-version=99.9.999.999')
[email protected]d0149c5c2012-05-29 21:12:11567
[email protected]cdb77062014-07-21 18:07:15568 # TODO(vitalybuka): Remove in the future. See crbug.com/395687.
569 if context.pdf_path:
[email protected]011886692014-08-01 21:00:21570 shutil.copy(context.pdf_path,
571 os.path.dirname(context.GetLaunchPath(revision)))
[email protected]cdb77062014-07-21 18:07:15572 testargs.append('--enable-print-preview')
573
[email protected]4646a752013-07-19 22:14:34574 runcommand = []
[email protected]61ea90a2013-09-26 10:17:34575 for token in shlex.split(command):
[email protected]4df583c2014-07-31 17:11:55576 if token == '%a':
[email protected]4646a752013-07-19 22:14:34577 runcommand.extend(testargs)
578 else:
[email protected]4df583c2014-07-31 17:11:55579 runcommand.append(
[email protected]011886692014-08-01 21:00:21580 token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))).
581 replace('%s', ' '.join(testargs)))
[email protected]4646a752013-07-19 22:14:34582
[email protected]d59c8712014-02-11 21:04:57583 results = []
[email protected]4df583c2014-07-31 17:11:55584 for _ in range(num_runs):
[email protected]4646a752013-07-19 22:14:34585 subproc = subprocess.Popen(runcommand,
[email protected]5e93cf162012-01-28 02:16:56586 bufsize=-1,
587 stdout=subprocess.PIPE,
588 stderr=subprocess.PIPE)
589 (stdout, stderr) = subproc.communicate()
[email protected]d59c8712014-02-11 21:04:57590 results.append((subproc.returncode, stdout, stderr))
[email protected]7ad66a72009-09-04 17:52:33591
592 os.chdir(cwd)
[email protected]7ad66a72009-09-04 17:52:33593 try:
594 shutil.rmtree(tempdir, True)
[email protected]4df583c2014-07-31 17:11:55595 except Exception:
[email protected]7ad66a72009-09-04 17:52:33596 pass
[email protected]67e0bc62009-09-03 22:06:09597
[email protected]d59c8712014-02-11 21:04:57598 for (returncode, stdout, stderr) in results:
599 if returncode:
600 return (returncode, stdout, stderr)
601 return results[0]
[email protected]79f14742010-03-10 01:01:57602
[email protected]cb155a82011-11-29 17:25:34603
[email protected]4df583c2014-07-31 17:11:55604# The arguments official_builds, status, stdout and stderr are unused.
605# They are present here because this function is passed to Bisect which then
606# calls it with 5 arguments.
607# pylint: disable=W0613
[email protected]d0149c5c2012-05-29 21:12:11608def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
[email protected]4df583c2014-07-31 17:11:55609 """Asks the user whether build |rev| is good or bad."""
[email protected]79f14742010-03-10 01:01:57610 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09611 while True:
[email protected]4df583c2014-07-31 17:11:55612 response = raw_input('Revision %s is '
[email protected]1d4a06242013-08-20 22:53:12613 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
[email protected]53bb6342012-06-01 04:11:00614 str(rev))
[email protected]1d4a06242013-08-20 22:53:12615 if response and response in ('g', 'b', 'r', 'u'):
[email protected]53bb6342012-06-01 04:11:00616 return response
[email protected]afe30662011-07-30 01:05:52617 if response and response == 'q':
618 raise SystemExit()
[email protected]67e0bc62009-09-03 22:06:09619
[email protected]cb155a82011-11-29 17:25:34620
[email protected]011886692014-08-01 21:00:21621def IsGoodASANBuild(rev, official_builds, status, stdout, stderr):
622 """Determine if an ASAN build |rev| is good or bad
623
624 Will examine stderr looking for the error message emitted by ASAN. If not
625 found then will fallback to asking the user."""
626 if stderr:
627 bad_count = 0
628 for line in stderr.splitlines():
629 print line
630 if line.find('ERROR: AddressSanitizer:') != -1:
631 bad_count += 1
632 if bad_count > 0:
633 print 'Revision %d determined to be bad.' % rev
634 return 'b'
635 return AskIsGoodBuild(rev, official_builds, status, stdout, stderr)
636
[email protected]53bb6342012-06-01 04:11:00637class DownloadJob(object):
638 """DownloadJob represents a task to download a given Chromium revision."""
[email protected]4df583c2014-07-31 17:11:55639
640 def __init__(self, context, name, rev, zip_file):
[email protected]53bb6342012-06-01 04:11:00641 super(DownloadJob, self).__init__()
642 # Store off the input parameters.
643 self.context = context
644 self.name = name
645 self.rev = rev
[email protected]4df583c2014-07-31 17:11:55646 self.zip_file = zip_file
[email protected]53bb6342012-06-01 04:11:00647 self.quit_event = threading.Event()
648 self.progress_event = threading.Event()
[email protected]4df583c2014-07-31 17:11:55649 self.thread = None
[email protected]53bb6342012-06-01 04:11:00650
651 def Start(self):
652 """Starts the download."""
653 fetchargs = (self.context,
654 self.rev,
[email protected]4df583c2014-07-31 17:11:55655 self.zip_file,
[email protected]53bb6342012-06-01 04:11:00656 self.quit_event,
657 self.progress_event)
658 self.thread = threading.Thread(target=FetchRevision,
659 name=self.name,
660 args=fetchargs)
661 self.thread.start()
662
663 def Stop(self):
664 """Stops the download which must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55665 assert self.thread, 'DownloadJob must be started before Stop is called.'
[email protected]53bb6342012-06-01 04:11:00666 self.quit_event.set()
667 self.thread.join()
[email protected]4df583c2014-07-31 17:11:55668 os.unlink(self.zip_file)
[email protected]53bb6342012-06-01 04:11:00669
670 def WaitFor(self):
671 """Prints a message and waits for the download to complete. The download
672 must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55673 assert self.thread, 'DownloadJob must be started before WaitFor is called.'
674 print 'Downloading revision %s...' % str(self.rev)
[email protected]53bb6342012-06-01 04:11:00675 self.progress_event.set() # Display progress of download.
676 self.thread.join()
677
678
[email protected]2e0f2672014-08-13 20:32:58679def Bisect(context,
[email protected]5e93cf162012-01-28 02:16:56680 num_runs=1,
[email protected]4df583c2014-07-31 17:11:55681 command='%p %a',
[email protected]60ac66e32011-07-18 16:08:25682 try_args=(),
[email protected]afe30662011-07-30 01:05:52683 profile=None,
[email protected]d59c8712014-02-11 21:04:57684 interactive=True,
[email protected]53bb6342012-06-01 04:11:00685 evaluate=AskIsGoodBuild):
[email protected]afe30662011-07-30 01:05:52686 """Given known good and known bad revisions, run a binary search on all
687 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25688
[email protected]2e0f2672014-08-13 20:32:58689 @param context PathContext object initialized with user provided parameters.
[email protected]5e93cf162012-01-28 02:16:56690 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52691 @param try_args A tuple of arguments to pass to the test application.
692 @param profile The name of the user profile to run with.
[email protected]d59c8712014-02-11 21:04:57693 @param interactive If it is false, use command exit code for good or bad
694 judgment of the argument build.
[email protected]53bb6342012-06-01 04:11:00695 @param evaluate A function which returns 'g' if the argument build is good,
696 'b' if it's bad or 'u' if unknown.
[email protected]afe30662011-07-30 01:05:52697
698 Threading is used to fetch Chromium revisions in the background, speeding up
699 the user's experience. For example, suppose the bounds of the search are
700 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
701 whether revision 50 is good or bad, the next revision to check will be either
702 25 or 75. So, while revision 50 is being checked, the script will download
703 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
704 known:
705
706 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
707 is run on rev 75.
708
709 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
710 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25711 """
712
[email protected]afe30662011-07-30 01:05:52713 if not profile:
714 profile = 'profile'
715
[email protected]2e0f2672014-08-13 20:32:58716 good_rev = context.good_revision
717 bad_rev = context.bad_revision
[email protected]afe30662011-07-30 01:05:52718 cwd = os.getcwd()
719
[email protected]28a3c122014-08-09 11:04:51720 print 'Downloading list of known revisions...',
[email protected]83048502014-08-21 16:48:44721 if not context.use_local_repo and not context.is_official:
[email protected]28a3c122014-08-09 11:04:51722 print '(use --use-local-repo for speed if you have a local checkout)'
723 else:
724 print
[email protected]d0149c5c2012-05-29 21:12:11725 _GetDownloadPath = lambda rev: os.path.join(cwd,
726 '%s-%s' % (str(rev), context.archive_name))
[email protected]2e0f2672014-08-13 20:32:58727 if context.is_official:
[email protected]d0149c5c2012-05-29 21:12:11728 revlist = context.GetOfficialBuildsList()
729 else:
730 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52731
732 # Get a list of revisions to bisect across.
733 if len(revlist) < 2: # Don't have enough builds to bisect.
734 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
735 raise RuntimeError(msg)
736
737 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09738 minrev = 0
739 maxrev = len(revlist) - 1
740 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52741 rev = revlist[pivot]
[email protected]4df583c2014-07-31 17:11:55742 zip_file = _GetDownloadPath(rev)
743 fetch = DownloadJob(context, 'initial_fetch', rev, zip_file)
[email protected]eadd95d2012-11-02 22:42:09744 fetch.Start()
745 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25746
747 # Binary search time!
[email protected]4df583c2014-07-31 17:11:55748 while fetch and fetch.zip_file and maxrev - minrev > 1:
[email protected]eadd95d2012-11-02 22:42:09749 if bad_rev < good_rev:
[email protected]4df583c2014-07-31 17:11:55750 min_str, max_str = 'bad', 'good'
[email protected]eadd95d2012-11-02 22:42:09751 else:
[email protected]4df583c2014-07-31 17:11:55752 min_str, max_str = 'good', 'bad'
753 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str,
[email protected]eadd95d2012-11-02 22:42:09754 revlist[maxrev], max_str)
755
[email protected]afe30662011-07-30 01:05:52756 # Pre-fetch next two possible pivots
757 # - down_pivot is the next revision to check if the current revision turns
758 # out to be bad.
759 # - up_pivot is the next revision to check if the current revision turns
760 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09761 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00762 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09763 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52764 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00765 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
766 _GetDownloadPath(down_rev))
767 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25768
[email protected]eadd95d2012-11-02 22:42:09769 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00770 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09771 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52772 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00773 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
774 _GetDownloadPath(up_rev))
775 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25776
[email protected]afe30662011-07-30 01:05:52777 # Run test on the pivot revision.
[email protected]e29c08c2012-09-17 20:50:50778 status = None
779 stdout = None
780 stderr = None
781 try:
782 (status, stdout, stderr) = RunRevision(context,
783 rev,
[email protected]4df583c2014-07-31 17:11:55784 fetch.zip_file,
[email protected]e29c08c2012-09-17 20:50:50785 profile,
786 num_runs,
[email protected]4646a752013-07-19 22:14:34787 command,
[email protected]e29c08c2012-09-17 20:50:50788 try_args)
789 except Exception, e:
[email protected]fc3702e2013-11-09 04:23:00790 print >> sys.stderr, e
[email protected]60ac66e32011-07-18 16:08:25791
[email protected]53bb6342012-06-01 04:11:00792 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52793 # On that basis, kill one of the background downloads and complete the
794 # other, as described in the comments above.
795 try:
[email protected]d59c8712014-02-11 21:04:57796 if not interactive:
797 if status:
798 answer = 'b'
799 print 'Bad revision: %s' % rev
800 else:
801 answer = 'g'
802 print 'Good revision: %s' % rev
803 else:
[email protected]2e0f2672014-08-13 20:32:58804 answer = evaluate(rev, context.is_official, status, stdout, stderr)
[email protected]4df583c2014-07-31 17:11:55805 if ((answer == 'g' and good_rev < bad_rev)
806 or (answer == 'b' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12807 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09808 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00809 if down_fetch:
810 down_fetch.Stop() # Kill the download of the older revision.
[email protected]1d4a06242013-08-20 22:53:12811 fetch = None
[email protected]53bb6342012-06-01 04:11:00812 if up_fetch:
813 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52814 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09815 fetch = up_fetch
[email protected]4df583c2014-07-31 17:11:55816 elif ((answer == 'b' and good_rev < bad_rev)
817 or (answer == 'g' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12818 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09819 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00820 if up_fetch:
821 up_fetch.Stop() # Kill the download of the newer revision.
[email protected]1d4a06242013-08-20 22:53:12822 fetch = None
[email protected]53bb6342012-06-01 04:11:00823 if down_fetch:
824 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52825 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09826 fetch = down_fetch
[email protected]1d4a06242013-08-20 22:53:12827 elif answer == 'r':
828 pass # Retry requires no changes.
[email protected]53bb6342012-06-01 04:11:00829 elif answer == 'u':
830 # Nuke the revision from the revlist and choose a new pivot.
[email protected]1d4a06242013-08-20 22:53:12831 fetch.Stop()
[email protected]53bb6342012-06-01 04:11:00832 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09833 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00834
[email protected]eadd95d2012-11-02 22:42:09835 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00836 # Alternate between using down_pivot or up_pivot for the new pivot
837 # point, without affecting the range. Do this instead of setting the
838 # pivot to the midpoint of the new range because adjacent revisions
839 # are likely affected by the same issue that caused the (u)nknown
840 # response.
841 if up_fetch and down_fetch:
842 fetch = [up_fetch, down_fetch][len(revlist) % 2]
843 elif up_fetch:
844 fetch = up_fetch
845 else:
846 fetch = down_fetch
847 fetch.WaitFor()
848 if fetch == up_fetch:
849 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
850 else:
851 pivot = down_pivot
[email protected]4df583c2014-07-31 17:11:55852 zip_file = fetch.zip_file
[email protected]53bb6342012-06-01 04:11:00853
854 if down_fetch and fetch != down_fetch:
855 down_fetch.Stop()
856 if up_fetch and fetch != up_fetch:
857 up_fetch.Stop()
858 else:
[email protected]4df583c2014-07-31 17:11:55859 assert False, 'Unexpected return value from evaluate(): ' + answer
[email protected]afe30662011-07-30 01:05:52860 except SystemExit:
[email protected]4df583c2014-07-31 17:11:55861 print 'Cleaning up...'
[email protected]5e93cf162012-01-28 02:16:56862 for f in [_GetDownloadPath(revlist[down_pivot]),
863 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52864 try:
865 os.unlink(f)
866 except OSError:
867 pass
868 sys.exit(0)
869
870 rev = revlist[pivot]
871
[email protected]2e0f2672014-08-13 20:32:58872 return (revlist[minrev], revlist[maxrev], context)
[email protected]60ac66e32011-07-18 16:08:25873
874
[email protected]37ed3172013-09-24 23:49:30875def GetBlinkDEPSRevisionForChromiumRevision(rev):
[email protected]4c6fec6b2013-09-17 17:44:08876 """Returns the blink revision that was in REVISIONS file at
[email protected]b2fe7f22011-10-25 22:58:31877 chromium revision |rev|."""
878 # . doesn't match newlines without re.DOTALL, so this is safe.
[email protected]37ed3172013-09-24 23:49:30879 blink_re = re.compile(r'webkit_revision\D*(\d+)')
880 url = urllib.urlopen(DEPS_FILE % rev)
881 m = blink_re.search(url.read())
882 url.close()
883 if m:
884 return int(m.group(1))
885 else:
[email protected]4df583c2014-07-31 17:11:55886 raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
[email protected]37ed3172013-09-24 23:49:30887
888
[email protected]2e0f2672014-08-13 20:32:58889def GetBlinkRevisionForChromiumRevision(context, rev):
[email protected]37ed3172013-09-24 23:49:30890 """Returns the blink revision that was in REVISIONS file at
891 chromium revision |rev|."""
[email protected]3e7c85322014-06-27 20:27:36892 def _IsRevisionNumber(revision):
893 if isinstance(revision, int):
894 return True
895 else:
896 return revision.isdigit()
[email protected]2e0f2672014-08-13 20:32:58897 if str(rev) in context.githash_svn_dict:
898 rev = context.githash_svn_dict[str(rev)]
899 file_url = '%s/%s%s/REVISIONS' % (context.base_url,
900 context._listing_platform_dir, rev)
[email protected]4c6fec6b2013-09-17 17:44:08901 url = urllib.urlopen(file_url)
[email protected]2e0f2672014-08-13 20:32:58902 if url.getcode() == 200:
903 try:
904 data = json.loads(url.read())
905 except ValueError:
906 print 'ValueError for JSON URL: %s' % file_url
907 raise ValueError
908 else:
909 raise ValueError
[email protected]b2fe7f22011-10-25 22:58:31910 url.close()
[email protected]4c6fec6b2013-09-17 17:44:08911 if 'webkit_revision' in data:
[email protected]3e7c85322014-06-27 20:27:36912 blink_rev = data['webkit_revision']
913 if not _IsRevisionNumber(blink_rev):
[email protected]2e0f2672014-08-13 20:32:58914 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
[email protected]3e7c85322014-06-27 20:27:36915 return blink_rev
[email protected]b2fe7f22011-10-25 22:58:31916 else:
[email protected]ff50d1c2013-04-17 18:49:36917 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:31918
[email protected]4df583c2014-07-31 17:11:55919
[email protected]37ed3172013-09-24 23:49:30920def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
921 """Returns the chromium revision that has the correct blink revision
922 for blink bisect, DEPS and REVISIONS file might not match since
923 blink snapshots point to tip of tree blink.
924 Note: The revisions_final variable might get modified to include
925 additional revisions."""
[email protected]37ed3172013-09-24 23:49:30926 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(rev)
927
928 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
929 idx = revisions.index(rev)
930 if idx > 0:
931 rev = revisions[idx-1]
932 if rev not in revisions_final:
933 revisions_final.insert(0, rev)
934
935 revisions_final.sort()
936 return rev
[email protected]b2fe7f22011-10-25 22:58:31937
[email protected]4df583c2014-07-31 17:11:55938
[email protected]5980b752014-07-02 00:34:40939def GetChromiumRevision(context, url):
[email protected]801fb652012-07-20 20:13:50940 """Returns the chromium revision read from given URL."""
941 try:
942 # Location of the latest build revision number
[email protected]5980b752014-07-02 00:34:40943 latest_revision = urllib.urlopen(url).read()
944 if latest_revision.isdigit():
945 return int(latest_revision)
946 return context.GetSVNRevisionFromGitHash(latest_revision)
[email protected]4df583c2014-07-31 17:11:55947 except Exception:
948 print 'Could not determine latest revision. This could be bad...'
[email protected]801fb652012-07-20 20:13:50949 return 999999999
950
pshenoy9ce271f2014-09-02 22:14:05951def PrintChangeLog(min_chromium_rev, max_chromium_rev):
952 """Prints the changelog URL."""
953
954 def _GetGitHashFromSVNRevision(svn_revision):
955 crrev_url = CRREV_URL + str(svn_revision)
956 url = urllib.urlopen(crrev_url)
957 if url.getcode() == 200:
958 result = re.search(GITHASH_SEARCH_PATTERN, url.read())
959 return result.group(1)
960
961 print (' ' + CHANGELOG_URL % (_GetGitHashFromSVNRevision(min_chromium_rev),
962 _GetGitHashFromSVNRevision(max_chromium_rev)))
963
[email protected]801fb652012-07-20 20:13:50964
[email protected]67e0bc62009-09-03 22:06:09965def main():
[email protected]2c1d2732009-10-29 19:52:17966 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:31967 'Perform binary search on the snapshot builds to find a minimal\n'
968 'range of revisions where a behavior change happened. The\n'
969 'behaviors are described as "good" and "bad".\n'
970 'It is NOT assumed that the behavior of the later revision is\n'
[email protected]09c58da2013-01-07 21:30:17971 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:38972 '\n'
[email protected]887c9182013-02-12 20:30:31973 'Revision numbers should use\n'
974 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
975 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
976 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
977 ' for earlier revs.\n'
978 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
979 ' are incorrect, they are from branches.\n'
980 '\n'
[email protected]178aab72010-10-08 17:21:38981 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:33982 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:57983 # Strangely, the default help output doesn't include the choice list.
[email protected]480369782014-08-22 20:15:58984 choices = ['mac', 'mac64', 'win', 'win64', 'linux', 'linux64', 'linux-arm']
[email protected]4082b182011-05-02 20:30:17985 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
[email protected]7ad66a72009-09-04 17:52:33986 parser.add_option('-a', '--archive',
[email protected]4df583c2014-07-31 17:11:55987 choices=choices,
988 help='The buildbot archive to bisect [%s].' %
989 '|'.join(choices))
990 parser.add_option('-o',
991 action='store_true',
992 dest='official_builds',
993 help='Bisect across official Chrome builds (internal '
994 'only) instead of Chromium archives.')
995 parser.add_option('-b', '--bad',
996 type='str',
997 help='A bad revision to start bisection. '
998 'May be earlier or later than the good revision. '
999 'Default is HEAD.')
1000 parser.add_option('-f', '--flash_path',
1001 type='str',
1002 help='Absolute path to a recent Adobe Pepper Flash '
1003 'binary to be used in this bisection (e.g. '
1004 'on Windows C:\...\pepflashplayer.dll and on Linux '
1005 '/opt/google/chrome/PepperFlash/'
1006 'libpepflashplayer.so).')
1007 parser.add_option('-d', '--pdf_path',
1008 type='str',
1009 help='Absolute path to a recent PDF plugin '
1010 'binary to be used in this bisection (e.g. '
1011 'on Windows C:\...\pdf.dll and on Linux '
1012 '/opt/google/chrome/libpdf.so). Option also enables '
1013 'print preview.')
1014 parser.add_option('-g', '--good',
1015 type='str',
1016 help='A good revision to start bisection. ' +
1017 'May be earlier or later than the bad revision. ' +
1018 'Default is 0.')
1019 parser.add_option('-p', '--profile', '--user-data-dir',
1020 type='str',
1021 default='profile',
1022 help='Profile to use; this will not reset every run. '
1023 'Defaults to a clean profile.')
1024 parser.add_option('-t', '--times',
1025 type='int',
1026 default=1,
1027 help='Number of times to run each build before asking '
1028 'if it\'s good or bad. Temporary profiles are reused.')
1029 parser.add_option('-c', '--command',
1030 type='str',
1031 default='%p %a',
1032 help='Command to execute. %p and %a refer to Chrome '
1033 'executable and specified extra arguments '
1034 'respectively. Use %s to specify all extra arguments '
1035 'as one string. Defaults to "%p %a". Note that any '
1036 'extra paths specified should be absolute.')
1037 parser.add_option('-l', '--blink',
1038 action='store_true',
1039 help='Use Blink bisect instead of Chromium. ')
1040 parser.add_option('', '--not-interactive',
1041 action='store_true',
1042 default=False,
1043 help='Use command exit code to tell good/bad revision.')
[email protected]011886692014-08-01 21:00:211044 parser.add_option('--asan',
1045 dest='asan',
1046 action='store_true',
1047 default=False,
1048 help='Allow the script to bisect ASAN builds')
[email protected]6a7a5d62014-07-09 04:45:501049 parser.add_option('--use-local-repo',
1050 dest='use_local_repo',
1051 action='store_true',
1052 default=False,
[email protected]4df583c2014-07-31 17:11:551053 help='Allow the script to convert git SHA1 to SVN '
1054 'revision using "git svn find-rev <SHA1>" '
1055 'command from a Chromium checkout.')
[email protected]b3b20512013-08-26 18:51:041056
[email protected]7ad66a72009-09-04 17:52:331057 (opts, args) = parser.parse_args()
1058
1059 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:381060 print 'Error: missing required parameter: --archive'
1061 print
[email protected]7ad66a72009-09-04 17:52:331062 parser.print_help()
1063 return 1
1064
[email protected]011886692014-08-01 21:00:211065 if opts.asan:
1066 supported_platforms = ['linux', 'mac', 'win']
1067 if opts.archive not in supported_platforms:
1068 print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
1069 '|'.join(supported_platforms))
1070 return 1
1071 if opts.official_builds:
1072 print 'Error: Do not yet support bisecting official ASAN builds.'
1073 return 1
1074
1075 if opts.asan:
1076 base_url = ASAN_BASE_URL
1077 elif opts.blink:
[email protected]4c6fec6b2013-09-17 17:44:081078 base_url = WEBKIT_BASE_URL
1079 else:
1080 base_url = CHROMIUM_BASE_URL
1081
[email protected]183706d92011-06-10 13:06:221082 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]2e0f2672014-08-13 20:32:581083 context = PathContext(base_url, opts.archive, opts.good, opts.bad,
[email protected]965e18fc2014-08-14 20:10:181084 opts.official_builds, opts.asan, opts.use_local_repo,
1085 opts.flash_path, opts.pdf_path)
[email protected]67e0bc62009-09-03 22:06:091086 # Pick a starting point, try to get HEAD for this.
[email protected]2e0f2672014-08-13 20:32:581087 if not opts.bad:
1088 context.bad_revision = '999.0.0.0'
1089 context.bad_revision = GetChromiumRevision(
1090 context, context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:091091
1092 # Find out when we were good.
[email protected]2e0f2672014-08-13 20:32:581093 if not opts.good:
1094 context.good_revision = '0.0.0.0' if opts.official_builds else 0
[email protected]801fb652012-07-20 20:13:501095
[email protected]fc3702e2013-11-09 04:23:001096 if opts.flash_path:
[email protected]2e0f2672014-08-13 20:32:581097 msg = 'Could not find Flash binary at %s' % opts.flash_path
1098 assert os.path.exists(opts.flash_path), msg
[email protected]fc3702e2013-11-09 04:23:001099
[email protected]cdb77062014-07-21 18:07:151100 if opts.pdf_path:
[email protected]2e0f2672014-08-13 20:32:581101 msg = 'Could not find PDF binary at %s' % opts.pdf_path
1102 assert os.path.exists(opts.pdf_path), msg
[email protected]cdb77062014-07-21 18:07:151103
[email protected]801fb652012-07-20 20:13:501104 if opts.official_builds:
[email protected]2e0f2672014-08-13 20:32:581105 context.good_revision = LooseVersion(context.good_revision)
1106 context.bad_revision = LooseVersion(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501107 else:
[email protected]2e0f2672014-08-13 20:32:581108 context.good_revision = int(context.good_revision)
1109 context.bad_revision = int(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501110
[email protected]5e93cf162012-01-28 02:16:561111 if opts.times < 1:
1112 print('Number of times to run (%d) must be greater than or equal to 1.' %
1113 opts.times)
1114 parser.print_help()
1115 return 1
1116
[email protected]011886692014-08-01 21:00:211117 if opts.asan:
1118 evaluator = IsGoodASANBuild
1119 else:
1120 evaluator = AskIsGoodBuild
1121
[email protected]2e0f2672014-08-13 20:32:581122 # Save these revision numbers to compare when showing the changelog URL
1123 # after the bisect.
1124 good_rev = context.good_revision
1125 bad_rev = context.bad_revision
1126
1127 (min_chromium_rev, max_chromium_rev, context) = Bisect(
1128 context, opts.times, opts.command, args, opts.profile,
[email protected]011886692014-08-01 21:00:211129 not opts.not_interactive, evaluator)
[email protected]67e0bc62009-09-03 22:06:091130
[email protected]ff50d1c2013-04-17 18:49:361131 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:311132 try:
[email protected]4c6fec6b2013-09-17 17:44:081133 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1134 min_chromium_rev)
1135 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1136 max_chromium_rev)
[email protected]4df583c2014-07-31 17:11:551137 except Exception:
[email protected]b2fe7f22011-10-25 22:58:311138 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:361139 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:311140
[email protected]3bdaa4752013-09-30 20:13:361141 if opts.blink:
1142 # We're done. Let the user know the results in an official manner.
1143 if good_rev > bad_rev:
1144 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
1145 else:
1146 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
[email protected]eadd95d2012-11-02 22:42:091147
[email protected]ff50d1c2013-04-17 18:49:361148 print 'BLINK CHANGELOG URL:'
1149 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]3bdaa4752013-09-30 20:13:361150
[email protected]d0149c5c2012-05-29 21:12:111151 else:
[email protected]3bdaa4752013-09-30 20:13:361152 # We're done. Let the user know the results in an official manner.
1153 if good_rev > bad_rev:
1154 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
1155 str(max_chromium_rev))
1156 else:
1157 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
1158 str(max_chromium_rev))
1159 if min_blink_rev != max_blink_rev:
[email protected]4df583c2014-07-31 17:11:551160 print ('NOTE: There is a Blink roll in the range, '
1161 'you might also want to do a Blink bisect.')
[email protected]3bdaa4752013-09-30 20:13:361162
1163 print 'CHANGELOG URL:'
1164 if opts.official_builds:
1165 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
1166 else:
pshenoy9ce271f2014-09-02 22:14:051167 PrintChangeLog(min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:341168
[email protected]4df583c2014-07-31 17:11:551169
[email protected]67e0bc62009-09-03 22:06:091170if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:331171 sys.exit(main())