blob: c363d99e7c521df0117551af0feb74aecb3108c9 [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.
33CHANGELOG_URL = ('http://build.chromium.org'
34 '/f/chromium/perf/dashboard/ui/changelog.html'
35 '?url=/trunk/src&range=%d%%3A%d')
[email protected]f6a71a72009-10-08 19:55:3836
[email protected]4df583c2014-07-31 17:11:5537# URL template for viewing changelogs between official versions.
38OFFICIAL_CHANGELOG_URL = ('http://omahaproxy.appspot.com/changelog'
39 '?old_version=%s&new_version=%s')
[email protected]d0149c5c2012-05-29 21:12:1140
[email protected]b2fe7f22011-10-25 22:58:3141# DEPS file URL.
[email protected]fc3702e2013-11-09 04:23:0042DEPS_FILE = 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
[email protected]b2fe7f22011-10-25 22:58:3143
[email protected]4df583c2014-07-31 17:11:5544# Blink changelogs URL.
45BLINK_CHANGELOG_URL = ('http://build.chromium.org'
46 '/f/chromium/perf/dashboard/ui/changelog_blink.html'
47 '?url=/trunk&range=%d%%3A%d')
48
49DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s ('
50 'known good), but no later than %s (first known bad).')
51DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s ('
52 'known bad), but no later than %s (first known good).')
[email protected]05ff3fd2012-04-17 23:24:0653
[email protected]3e7c85322014-06-27 20:27:3654CHROMIUM_GITHASH_TO_SVN_URL = (
55 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5556
[email protected]3e7c85322014-06-27 20:27:3657BLINK_GITHASH_TO_SVN_URL = (
58 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5559
60GITHASH_TO_SVN_URL = {
61 'chromium': CHROMIUM_GITHASH_TO_SVN_URL,
62 'blink': BLINK_GITHASH_TO_SVN_URL,
63}
64
65# Search pattern to be matched in the JSON output from
[email protected]3e7c85322014-06-27 20:27:3666# CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision).
67CHROMIUM_SEARCH_PATTERN = (
68 r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ')
[email protected]4df583c2014-07-31 17:11:5569
[email protected]3e7c85322014-06-27 20:27:3670# Search pattern to be matched in the json output from
71# BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision).
72BLINK_SEARCH_PATTERN = (
73 r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ')
[email protected]4df583c2014-07-31 17:11:5574
75SEARCH_PATTERN = {
76 'chromium': CHROMIUM_SEARCH_PATTERN,
77 'blink': BLINK_SEARCH_PATTERN,
78}
[email protected]3e7c85322014-06-27 20:27:3679
[email protected]480369782014-08-22 20:15:5880CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with '
81 'no configured credentials')
82
[email protected]67e0bc62009-09-03 22:06:0983###############################################################################
84
[email protected]83048502014-08-21 16:48:4485import httplib
[email protected]4c6fec6b2013-09-17 17:44:0886import json
[email protected]7ad66a72009-09-04 17:52:3387import optparse
[email protected]67e0bc62009-09-03 22:06:0988import os
89import re
[email protected]61ea90a2013-09-26 10:17:3490import shlex
[email protected]67e0bc62009-09-03 22:06:0991import shutil
[email protected]afe30662011-07-30 01:05:5292import subprocess
[email protected]67e0bc62009-09-03 22:06:0993import sys
[email protected]7ad66a72009-09-04 17:52:3394import tempfile
[email protected]afe30662011-07-30 01:05:5295import threading
[email protected]67e0bc62009-09-03 22:06:0996import urllib
[email protected]d0149c5c2012-05-29 21:12:1197from distutils.version import LooseVersion
[email protected]183706d92011-06-10 13:06:2298from xml.etree import ElementTree
[email protected]bd8dcb92010-03-31 01:05:2499import zipfile
100
[email protected]cb155a82011-11-29 17:25:34101
[email protected]183706d92011-06-10 13:06:22102class PathContext(object):
103 """A PathContext is used to carry the information used to construct URLs and
104 paths when dealing with the storage server and archives."""
[email protected]4c6fec6b2013-09-17 17:44:08105 def __init__(self, base_url, platform, good_revision, bad_revision,
[email protected]965e18fc2014-08-14 20:10:18106 is_official, is_asan, use_local_repo, flash_path = None,
[email protected]cdb77062014-07-21 18:07:15107 pdf_path = None):
[email protected]183706d92011-06-10 13:06:22108 super(PathContext, self).__init__()
109 # Store off the input parameters.
[email protected]4c6fec6b2013-09-17 17:44:08110 self.base_url = base_url
[email protected]183706d92011-06-10 13:06:22111 self.platform = platform # What's passed in to the '-a/--archive' option.
112 self.good_revision = good_revision
113 self.bad_revision = bad_revision
[email protected]d0149c5c2012-05-29 21:12:11114 self.is_official = is_official
[email protected]011886692014-08-01 21:00:21115 self.is_asan = is_asan
116 self.build_type = 'release'
[email protected]fc3702e2013-11-09 04:23:00117 self.flash_path = flash_path
[email protected]3e7c85322014-06-27 20:27:36118 # Dictionary which stores svn revision number as key and it's
119 # corresponding git hash as value. This data is populated in
120 # _FetchAndParse and used later in GetDownloadURL while downloading
121 # the build.
122 self.githash_svn_dict = {}
[email protected]cdb77062014-07-21 18:07:15123 self.pdf_path = pdf_path
[email protected]183706d92011-06-10 13:06:22124
125 # The name of the ZIP file in a revision directory on the server.
126 self.archive_name = None
127
[email protected]6a7a5d62014-07-09 04:45:50128 # If the script is run from a local Chromium checkout,
129 # "--use-local-repo" option can be used to make the script run faster.
130 # It uses "git svn find-rev <SHA1>" command to convert git hash to svn
131 # revision number.
132 self.use_local_repo = use_local_repo
133
[email protected]183706d92011-06-10 13:06:22134 # Set some internal members:
135 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
136 # _archive_extract_dir = Uncompressed directory in the archive_name file.
137 # _binary_name = The name of the executable to run.
[email protected]7aec9e82013-05-09 05:09:23138 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]183706d92011-06-10 13:06:22139 self._binary_name = 'chrome'
[email protected]480369782014-08-22 20:15:58140 elif self.platform in ('mac', 'mac64'):
[email protected]183706d92011-06-10 13:06:22141 self.archive_name = 'chrome-mac.zip'
142 self._archive_extract_dir = 'chrome-mac'
[email protected]480369782014-08-22 20:15:58143 elif self.platform in ('win', 'win64'):
[email protected]183706d92011-06-10 13:06:22144 self.archive_name = 'chrome-win32.zip'
145 self._archive_extract_dir = 'chrome-win32'
146 self._binary_name = 'chrome.exe'
147 else:
[email protected]afe30662011-07-30 01:05:52148 raise Exception('Invalid platform: %s' % self.platform)
[email protected]183706d92011-06-10 13:06:22149
[email protected]d0149c5c2012-05-29 21:12:11150 if is_official:
151 if self.platform == 'linux':
[email protected]83048502014-08-21 16:48:44152 self._listing_platform_dir = 'precise32/'
153 self.archive_name = 'chrome-precise32.zip'
154 self._archive_extract_dir = 'chrome-precise32'
[email protected]d0149c5c2012-05-29 21:12:11155 elif self.platform == 'linux64':
[email protected]83048502014-08-21 16:48:44156 self._listing_platform_dir = 'precise64/'
157 self.archive_name = 'chrome-precise64.zip'
158 self._archive_extract_dir = 'chrome-precise64'
[email protected]d0149c5c2012-05-29 21:12:11159 elif self.platform == 'mac':
160 self._listing_platform_dir = 'mac/'
161 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
[email protected]480369782014-08-22 20:15:58162 elif self.platform == 'mac64':
163 self._listing_platform_dir = 'mac64/'
164 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
[email protected]d0149c5c2012-05-29 21:12:11165 elif self.platform == 'win':
[email protected]965e18fc2014-08-14 20:10:18166 self._listing_platform_dir = 'win/'
[email protected]00ae3c02014-08-21 18:41:23167 self.archive_name = 'chrome-win.zip'
168 self._archive_extract_dir = 'chrome-win'
[email protected]480369782014-08-22 20:15:58169 elif self.platform == 'win64':
170 self._listing_platform_dir = 'win64/'
171 self.archive_name = 'chrome-win64.zip'
172 self._archive_extract_dir = 'chrome-win64'
[email protected]d0149c5c2012-05-29 21:12:11173 else:
[email protected]7aec9e82013-05-09 05:09:23174 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]d0149c5c2012-05-29 21:12:11175 self.archive_name = 'chrome-linux.zip'
176 self._archive_extract_dir = 'chrome-linux'
177 if self.platform == 'linux':
178 self._listing_platform_dir = 'Linux/'
179 elif self.platform == 'linux64':
180 self._listing_platform_dir = 'Linux_x64/'
[email protected]7aec9e82013-05-09 05:09:23181 elif self.platform == 'linux-arm':
182 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
[email protected]d0149c5c2012-05-29 21:12:11183 elif self.platform == 'mac':
184 self._listing_platform_dir = 'Mac/'
185 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
186 elif self.platform == 'win':
187 self._listing_platform_dir = 'Win/'
188
[email protected]011886692014-08-01 21:00:21189 def GetASANPlatformDir(self):
190 """ASAN builds are in directories like "linux-release", or have filenames
191 like "asan-win32-release-277079.zip". This aligns to our platform names
192 except in the case of Windows where they use "win32" instead of "win"."""
193 if self.platform == 'win':
194 return 'win32'
195 else:
196 return self.platform
197
[email protected]183706d92011-06-10 13:06:22198 def GetListingURL(self, marker=None):
199 """Returns the URL for a directory listing, with an optional marker."""
200 marker_param = ''
201 if marker:
202 marker_param = '&marker=' + str(marker)
[email protected]011886692014-08-01 21:00:21203 if self.is_asan:
204 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type)
205 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param
206 else:
207 return (self.base_url + '/?delimiter=/&prefix=' +
208 self._listing_platform_dir + marker_param)
[email protected]183706d92011-06-10 13:06:22209
210 def GetDownloadURL(self, revision):
211 """Gets the download URL for a build archive of a specific revision."""
[email protected]011886692014-08-01 21:00:21212 if self.is_asan:
213 return '%s/%s-%s/%s-%d.zip' % (
214 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
215 self.GetASANBaseName(), revision)
[email protected]d0149c5c2012-05-29 21:12:11216 if self.is_official:
[email protected]4df583c2014-07-31 17:11:55217 return '%s/%s/%s%s' % (
[email protected]d0149c5c2012-05-29 21:12:11218 OFFICIAL_BASE_URL, revision, self._listing_platform_dir,
219 self.archive_name)
220 else:
[email protected]3e7c85322014-06-27 20:27:36221 if str(revision) in self.githash_svn_dict:
222 revision = self.githash_svn_dict[str(revision)]
[email protected]4df583c2014-07-31 17:11:55223 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
[email protected]4c6fec6b2013-09-17 17:44:08224 revision, self.archive_name)
[email protected]183706d92011-06-10 13:06:22225
226 def GetLastChangeURL(self):
227 """Returns a URL to the LAST_CHANGE file."""
[email protected]4c6fec6b2013-09-17 17:44:08228 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
[email protected]183706d92011-06-10 13:06:22229
[email protected]011886692014-08-01 21:00:21230 def GetASANBaseName(self):
231 """Returns the base name of the ASAN zip file."""
232 if 'linux' in self.platform:
233 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(),
234 self.build_type)
235 else:
236 return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type)
237
238 def GetLaunchPath(self, revision):
[email protected]183706d92011-06-10 13:06:22239 """Returns a relative path (presumably from the archive extraction location)
240 that is used to run the executable."""
[email protected]011886692014-08-01 21:00:21241 if self.is_asan:
242 extract_dir = '%s-%d' % (self.GetASANBaseName(), revision)
243 else:
244 extract_dir = self._archive_extract_dir
245 return os.path.join(extract_dir, self._binary_name)
[email protected]183706d92011-06-10 13:06:22246
[email protected]afe30662011-07-30 01:05:52247 def ParseDirectoryIndex(self):
248 """Parses the Google Storage directory listing into a list of revision
[email protected]eadd95d2012-11-02 22:42:09249 numbers."""
[email protected]afe30662011-07-30 01:05:52250
251 def _FetchAndParse(url):
252 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
253 next-marker is not None, then the listing is a partial listing and another
254 fetch should be performed with next-marker being the marker= GET
255 parameter."""
256 handle = urllib.urlopen(url)
257 document = ElementTree.parse(handle)
258
259 # All nodes in the tree are namespaced. Get the root's tag name to extract
260 # the namespace. Etree does namespaces as |{namespace}tag|.
261 root_tag = document.getroot().tag
262 end_ns_pos = root_tag.find('}')
263 if end_ns_pos == -1:
[email protected]4df583c2014-07-31 17:11:55264 raise Exception('Could not locate end namespace for directory index')
[email protected]afe30662011-07-30 01:05:52265 namespace = root_tag[:end_ns_pos + 1]
266
267 # Find the prefix (_listing_platform_dir) and whether or not the list is
268 # truncated.
269 prefix_len = len(document.find(namespace + 'Prefix').text)
270 next_marker = None
271 is_truncated = document.find(namespace + 'IsTruncated')
272 if is_truncated is not None and is_truncated.text.lower() == 'true':
273 next_marker = document.find(namespace + 'NextMarker').text
[email protected]afe30662011-07-30 01:05:52274 # Get a list of all the revisions.
[email protected]afe30662011-07-30 01:05:52275 revisions = []
[email protected]3e7c85322014-06-27 20:27:36276 githash_svn_dict = {}
[email protected]011886692014-08-01 21:00:21277 if self.is_asan:
278 asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName()))
279 # Non ASAN builds are in a <revision> directory. The ASAN builds are
280 # flat
281 all_prefixes = document.findall(namespace + 'Contents/' +
282 namespace + 'Key')
283 for prefix in all_prefixes:
284 m = asan_regex.match(prefix.text)
285 if m:
286 try:
287 revisions.append(int(m.group(1)))
288 except ValueError:
289 pass
290 else:
291 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
292 namespace + 'Prefix')
293 # The <Prefix> nodes have content of the form of
294 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
295 # trailing slash to just have a number.
296 for prefix in all_prefixes:
297 revnum = prefix.text[prefix_len:-1]
298 try:
299 if not revnum.isdigit():
300 git_hash = revnum
301 revnum = self.GetSVNRevisionFromGitHash(git_hash)
302 githash_svn_dict[revnum] = git_hash
303 if revnum is not None:
304 revnum = int(revnum)
305 revisions.append(revnum)
306 except ValueError:
307 pass
[email protected]3e7c85322014-06-27 20:27:36308 return (revisions, next_marker, githash_svn_dict)
[email protected]9639b002013-08-30 14:45:52309
[email protected]afe30662011-07-30 01:05:52310 # Fetch the first list of revisions.
[email protected]4df583c2014-07-31 17:11:55311 (revisions, next_marker, self.githash_svn_dict) = _FetchAndParse(
312 self.GetListingURL())
[email protected]afe30662011-07-30 01:05:52313 # If the result list was truncated, refetch with the next marker. Do this
314 # until an entire directory listing is done.
315 while next_marker:
316 next_url = self.GetListingURL(next_marker)
[email protected]3e7c85322014-06-27 20:27:36317 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url)
[email protected]afe30662011-07-30 01:05:52318 revisions.extend(new_revisions)
[email protected]3e7c85322014-06-27 20:27:36319 self.githash_svn_dict.update(new_dict)
[email protected]afe30662011-07-30 01:05:52320 return revisions
321
[email protected]6a7a5d62014-07-09 04:45:50322 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot):
[email protected]3e7c85322014-06-27 20:27:36323 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1
[email protected]2e0f2672014-08-13 20:32:58324 response = urllib.urlopen(json_url)
325 if response.getcode() == 200:
326 try:
327 data = json.loads(response.read()[4:])
328 except ValueError:
329 print 'ValueError for JSON URL: %s' % json_url
330 raise ValueError
331 else:
332 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36333 if 'message' in data:
334 message = data['message'].split('\n')
335 message = [line for line in message if line.strip()]
336 search_pattern = re.compile(SEARCH_PATTERN[depot])
337 result = search_pattern.search(message[len(message)-1])
338 if result:
339 return result.group(1)
340 print 'Failed to get svn revision number for %s' % git_sha1
[email protected]1f99f4d2014-07-23 16:44:14341 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36342
[email protected]6a7a5d62014-07-09 04:45:50343 def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot):
344 def _RunGit(command, path):
345 command = ['git'] + command
346 if path:
347 original_path = os.getcwd()
348 os.chdir(path)
349 shell = sys.platform.startswith('win')
350 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
351 stderr=subprocess.PIPE)
352 (output, _) = proc.communicate()
353
354 if path:
355 os.chdir(original_path)
356 return (output, proc.returncode)
357
358 path = None
359 if depot == 'blink':
360 path = os.path.join(os.getcwd(), 'third_party', 'WebKit')
361 if os.path.basename(os.getcwd()) == 'src':
362 command = ['svn', 'find-rev', git_sha1]
363 (git_output, return_code) = _RunGit(command, path)
364 if not return_code:
365 return git_output.strip('\n')
[email protected]1f99f4d2014-07-23 16:44:14366 raise ValueError
[email protected]6a7a5d62014-07-09 04:45:50367 else:
368 print ('Script should be run from src folder. ' +
369 'Eg: python tools/bisect-builds.py -g 280588 -b 280590' +
370 '--archive linux64 --use-local-repo')
371 sys.exit(1)
372
373 def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'):
374 if not self.use_local_repo:
375 return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot)
376 else:
377 return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot)
378
[email protected]afe30662011-07-30 01:05:52379 def GetRevList(self):
380 """Gets the list of revision numbers between self.good_revision and
381 self.bad_revision."""
382 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09383 minrev = min(self.good_revision, self.bad_revision)
384 maxrev = max(self.good_revision, self.bad_revision)
[email protected]37ed3172013-09-24 23:49:30385 revlist_all = map(int, self.ParseDirectoryIndex())
386
387 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
[email protected]afe30662011-07-30 01:05:52388 revlist.sort()
[email protected]37ed3172013-09-24 23:49:30389
390 # Set good and bad revisions to be legit revisions.
391 if revlist:
392 if self.good_revision < self.bad_revision:
393 self.good_revision = revlist[0]
394 self.bad_revision = revlist[-1]
395 else:
396 self.bad_revision = revlist[0]
397 self.good_revision = revlist[-1]
398
399 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
400 if self.base_url == WEBKIT_BASE_URL:
401 revlist_all.sort()
402 self.good_revision = FixChromiumRevForBlink(revlist,
403 revlist_all,
404 self,
405 self.good_revision)
406 self.bad_revision = FixChromiumRevForBlink(revlist,
407 revlist_all,
408 self,
409 self.bad_revision)
[email protected]afe30662011-07-30 01:05:52410 return revlist
411
[email protected]d0149c5c2012-05-29 21:12:11412 def GetOfficialBuildsList(self):
413 """Gets the list of official build numbers between self.good_revision and
414 self.bad_revision."""
[email protected]83048502014-08-21 16:48:44415
416 def CheckDepotToolsInPath():
417 delimiter = ';' if sys.platform.startswith('win') else ':'
418 path_list = os.environ['PATH'].split(delimiter)
419 for path in path_list:
420 if path.find('depot_tools') != -1:
421 return path
422 return None
423
424 def RunGsutilCommand(args):
425 gsutil_path = CheckDepotToolsInPath()
426 if gsutil_path is None:
427 print ('Follow the instructions in this document '
428 'http://dev.chromium.org/developers/how-tos/install-depot-tools'
429 ' to install depot_tools and then try again.')
430 sys.exit(1)
431 gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil')
432 gsutil = subprocess.Popen([sys.executable, gsutil_path] + args,
433 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
434 env=None)
435 stdout, stderr = gsutil.communicate()
436 if gsutil.returncode:
[email protected]480369782014-08-22 20:15:58437 if (re.findall(r'status[ |=]40[1|3]', stderr) or
438 stderr.startswith(CREDENTIAL_ERROR_MESSAGE)):
[email protected]83048502014-08-21 16:48:44439 print ('Follow these steps to configure your credentials and try'
440 ' running the bisect-builds.py again.:\n'
441 ' 1. Run "python %s config" and follow its instructions.\n'
442 ' 2. If you have a @google.com account, use that account.\n'
443 ' 3. For the project-id, just enter 0.' % gsutil_path)
444 sys.exit(1)
445 else:
[email protected]480369782014-08-22 20:15:58446 raise Exception('Error running the gsutil command: %s' % stderr)
[email protected]83048502014-08-21 16:48:44447 return stdout
448
449 def GsutilList(bucket):
450 query = 'gs://%s/' % bucket
451 stdout = RunGsutilCommand(['ls', query])
452 return [url[len(query):].strip('/') for url in stdout.splitlines()]
453
[email protected]d0149c5c2012-05-29 21:12:11454 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09455 minrev = min(self.good_revision, self.bad_revision)
456 maxrev = max(self.good_revision, self.bad_revision)
[email protected]83048502014-08-21 16:48:44457 build_numbers = GsutilList(GS_BUCKET_NAME)
458 revision_re = re.compile(r'(\d\d\.\d\.\d{4}\.\d+)')
459 build_numbers = filter(lambda b: revision_re.search(b), build_numbers)
[email protected]d0149c5c2012-05-29 21:12:11460 final_list = []
[email protected]d0149c5c2012-05-29 21:12:11461 parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
[email protected]83048502014-08-21 16:48:44462 connection = httplib.HTTPConnection(GOOGLE_APIS_URL)
[email protected]d0149c5c2012-05-29 21:12:11463 for build_number in sorted(parsed_build_numbers):
[email protected]83048502014-08-21 16:48:44464 if build_number > maxrev:
465 break
466 if build_number < minrev:
467 continue
468 path = ('/' + GS_BUCKET_NAME + '/' + str(build_number) + '/' +
[email protected]4df583c2014-07-31 17:11:55469 self._listing_platform_dir + self.archive_name)
[email protected]83048502014-08-21 16:48:44470 connection.request('HEAD', path)
471 response = connection.getresponse()
472 if response.status == 200:
473 final_list.append(str(build_number))
474 response.read()
475 connection.close()
[email protected]801fb652012-07-20 20:13:50476 return final_list
[email protected]bd8dcb92010-03-31 01:05:24477
[email protected]fc3702e2013-11-09 04:23:00478def UnzipFilenameToDir(filename, directory):
479 """Unzip |filename| to |directory|."""
[email protected]afe30662011-07-30 01:05:52480 cwd = os.getcwd()
481 if not os.path.isabs(filename):
482 filename = os.path.join(cwd, filename)
[email protected]bd8dcb92010-03-31 01:05:24483 zf = zipfile.ZipFile(filename)
484 # Make base.
[email protected]fc3702e2013-11-09 04:23:00485 if not os.path.isdir(directory):
486 os.mkdir(directory)
487 os.chdir(directory)
[email protected]e29c08c2012-09-17 20:50:50488 # Extract files.
489 for info in zf.infolist():
490 name = info.filename
491 if name.endswith('/'): # dir
492 if not os.path.isdir(name):
493 os.makedirs(name)
494 else: # file
[email protected]fc3702e2013-11-09 04:23:00495 directory = os.path.dirname(name)
496 if not os.path.isdir(directory):
497 os.makedirs(directory)
[email protected]e29c08c2012-09-17 20:50:50498 out = open(name, 'wb')
499 out.write(zf.read(name))
500 out.close()
501 # Set permissions. Permission info in external_attr is shifted 16 bits.
502 os.chmod(name, info.external_attr >> 16L)
503 os.chdir(cwd)
[email protected]bd8dcb92010-03-31 01:05:24504
[email protected]67e0bc62009-09-03 22:06:09505
[email protected]468a9772011-08-09 18:42:00506def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
[email protected]afe30662011-07-30 01:05:52507 """Downloads and unzips revision |rev|.
508 @param context A PathContext instance.
509 @param rev The Chromium revision number/tag to download.
510 @param filename The destination for the downloaded file.
511 @param quit_event A threading.Event which will be set by the master thread to
512 indicate that the download should be aborted.
[email protected]468a9772011-08-09 18:42:00513 @param progress_event A threading.Event which will be set by the master thread
514 to indicate that the progress of the download should be
515 displayed.
[email protected]afe30662011-07-30 01:05:52516 """
517 def ReportHook(blocknum, blocksize, totalsize):
[email protected]946be752011-10-25 23:34:21518 if quit_event and quit_event.isSet():
[email protected]4df583c2014-07-31 17:11:55519 raise RuntimeError('Aborting download of revision %s' % str(rev))
[email protected]946be752011-10-25 23:34:21520 if progress_event and progress_event.isSet():
[email protected]468a9772011-08-09 18:42:00521 size = blocknum * blocksize
522 if totalsize == -1: # Total size not known.
[email protected]4df583c2014-07-31 17:11:55523 progress = 'Received %d bytes' % size
[email protected]468a9772011-08-09 18:42:00524 else:
525 size = min(totalsize, size)
[email protected]4df583c2014-07-31 17:11:55526 progress = 'Received %d of %d bytes, %.2f%%' % (
[email protected]468a9772011-08-09 18:42:00527 size, totalsize, 100.0 * size / totalsize)
528 # Send a \r to let all progress messages use just one line of output.
[email protected]4df583c2014-07-31 17:11:55529 sys.stdout.write('\r' + progress)
[email protected]468a9772011-08-09 18:42:00530 sys.stdout.flush()
[email protected]7ad66a72009-09-04 17:52:33531
[email protected]afe30662011-07-30 01:05:52532 download_url = context.GetDownloadURL(rev)
533 try:
534 urllib.urlretrieve(download_url, filename, ReportHook)
[email protected]946be752011-10-25 23:34:21535 if progress_event and progress_event.isSet():
[email protected]ecaba01e62011-10-26 05:33:28536 print
[email protected]4df583c2014-07-31 17:11:55537 except RuntimeError:
[email protected]afe30662011-07-30 01:05:52538 pass
[email protected]7ad66a72009-09-04 17:52:33539
[email protected]7ad66a72009-09-04 17:52:33540
[email protected]4df583c2014-07-31 17:11:55541def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
[email protected]afe30662011-07-30 01:05:52542 """Given a zipped revision, unzip it and run the test."""
[email protected]4df583c2014-07-31 17:11:55543 print 'Trying revision %s...' % str(revision)
[email protected]3ff00b72011-07-20 21:34:47544
[email protected]afe30662011-07-30 01:05:52545 # Create a temp directory and unzip the revision into it.
[email protected]7ad66a72009-09-04 17:52:33546 cwd = os.getcwd()
547 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
[email protected]4df583c2014-07-31 17:11:55548 UnzipFilenameToDir(zip_file, tempdir)
[email protected]7ad66a72009-09-04 17:52:33549 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09550
[email protected]5e93cf162012-01-28 02:16:56551 # Run the build as many times as specified.
[email protected]4646a752013-07-19 22:14:34552 testargs = ['--user-data-dir=%s' % profile] + args
[email protected]d0149c5c2012-05-29 21:12:11553 # The sandbox must be run as root on Official Chrome, so bypass it.
[email protected]cdb77062014-07-21 18:07:15554 if ((context.is_official or context.flash_path or context.pdf_path) and
[email protected]fc3702e2013-11-09 04:23:00555 context.platform.startswith('linux')):
[email protected]d0149c5c2012-05-29 21:12:11556 testargs.append('--no-sandbox')
[email protected]fc3702e2013-11-09 04:23:00557 if context.flash_path:
558 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
559 # We have to pass a large enough Flash version, which currently needs not
560 # be correct. Instead of requiring the user of the script to figure out and
561 # pass the correct version we just spoof it.
562 testargs.append('--ppapi-flash-version=99.9.999.999')
[email protected]d0149c5c2012-05-29 21:12:11563
[email protected]cdb77062014-07-21 18:07:15564 # TODO(vitalybuka): Remove in the future. See crbug.com/395687.
565 if context.pdf_path:
[email protected]011886692014-08-01 21:00:21566 shutil.copy(context.pdf_path,
567 os.path.dirname(context.GetLaunchPath(revision)))
[email protected]cdb77062014-07-21 18:07:15568 testargs.append('--enable-print-preview')
569
[email protected]4646a752013-07-19 22:14:34570 runcommand = []
[email protected]61ea90a2013-09-26 10:17:34571 for token in shlex.split(command):
[email protected]4df583c2014-07-31 17:11:55572 if token == '%a':
[email protected]4646a752013-07-19 22:14:34573 runcommand.extend(testargs)
574 else:
[email protected]4df583c2014-07-31 17:11:55575 runcommand.append(
[email protected]011886692014-08-01 21:00:21576 token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))).
577 replace('%s', ' '.join(testargs)))
[email protected]4646a752013-07-19 22:14:34578
[email protected]d59c8712014-02-11 21:04:57579 results = []
[email protected]4df583c2014-07-31 17:11:55580 for _ in range(num_runs):
[email protected]4646a752013-07-19 22:14:34581 subproc = subprocess.Popen(runcommand,
[email protected]5e93cf162012-01-28 02:16:56582 bufsize=-1,
583 stdout=subprocess.PIPE,
584 stderr=subprocess.PIPE)
585 (stdout, stderr) = subproc.communicate()
[email protected]d59c8712014-02-11 21:04:57586 results.append((subproc.returncode, stdout, stderr))
[email protected]7ad66a72009-09-04 17:52:33587
588 os.chdir(cwd)
[email protected]7ad66a72009-09-04 17:52:33589 try:
590 shutil.rmtree(tempdir, True)
[email protected]4df583c2014-07-31 17:11:55591 except Exception:
[email protected]7ad66a72009-09-04 17:52:33592 pass
[email protected]67e0bc62009-09-03 22:06:09593
[email protected]d59c8712014-02-11 21:04:57594 for (returncode, stdout, stderr) in results:
595 if returncode:
596 return (returncode, stdout, stderr)
597 return results[0]
[email protected]79f14742010-03-10 01:01:57598
[email protected]cb155a82011-11-29 17:25:34599
[email protected]4df583c2014-07-31 17:11:55600# The arguments official_builds, status, stdout and stderr are unused.
601# They are present here because this function is passed to Bisect which then
602# calls it with 5 arguments.
603# pylint: disable=W0613
[email protected]d0149c5c2012-05-29 21:12:11604def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
[email protected]4df583c2014-07-31 17:11:55605 """Asks the user whether build |rev| is good or bad."""
[email protected]79f14742010-03-10 01:01:57606 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09607 while True:
[email protected]4df583c2014-07-31 17:11:55608 response = raw_input('Revision %s is '
[email protected]1d4a06242013-08-20 22:53:12609 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
[email protected]53bb6342012-06-01 04:11:00610 str(rev))
[email protected]1d4a06242013-08-20 22:53:12611 if response and response in ('g', 'b', 'r', 'u'):
[email protected]53bb6342012-06-01 04:11:00612 return response
[email protected]afe30662011-07-30 01:05:52613 if response and response == 'q':
614 raise SystemExit()
[email protected]67e0bc62009-09-03 22:06:09615
[email protected]cb155a82011-11-29 17:25:34616
[email protected]011886692014-08-01 21:00:21617def IsGoodASANBuild(rev, official_builds, status, stdout, stderr):
618 """Determine if an ASAN build |rev| is good or bad
619
620 Will examine stderr looking for the error message emitted by ASAN. If not
621 found then will fallback to asking the user."""
622 if stderr:
623 bad_count = 0
624 for line in stderr.splitlines():
625 print line
626 if line.find('ERROR: AddressSanitizer:') != -1:
627 bad_count += 1
628 if bad_count > 0:
629 print 'Revision %d determined to be bad.' % rev
630 return 'b'
631 return AskIsGoodBuild(rev, official_builds, status, stdout, stderr)
632
[email protected]53bb6342012-06-01 04:11:00633class DownloadJob(object):
634 """DownloadJob represents a task to download a given Chromium revision."""
[email protected]4df583c2014-07-31 17:11:55635
636 def __init__(self, context, name, rev, zip_file):
[email protected]53bb6342012-06-01 04:11:00637 super(DownloadJob, self).__init__()
638 # Store off the input parameters.
639 self.context = context
640 self.name = name
641 self.rev = rev
[email protected]4df583c2014-07-31 17:11:55642 self.zip_file = zip_file
[email protected]53bb6342012-06-01 04:11:00643 self.quit_event = threading.Event()
644 self.progress_event = threading.Event()
[email protected]4df583c2014-07-31 17:11:55645 self.thread = None
[email protected]53bb6342012-06-01 04:11:00646
647 def Start(self):
648 """Starts the download."""
649 fetchargs = (self.context,
650 self.rev,
[email protected]4df583c2014-07-31 17:11:55651 self.zip_file,
[email protected]53bb6342012-06-01 04:11:00652 self.quit_event,
653 self.progress_event)
654 self.thread = threading.Thread(target=FetchRevision,
655 name=self.name,
656 args=fetchargs)
657 self.thread.start()
658
659 def Stop(self):
660 """Stops the download which must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55661 assert self.thread, 'DownloadJob must be started before Stop is called.'
[email protected]53bb6342012-06-01 04:11:00662 self.quit_event.set()
663 self.thread.join()
[email protected]4df583c2014-07-31 17:11:55664 os.unlink(self.zip_file)
[email protected]53bb6342012-06-01 04:11:00665
666 def WaitFor(self):
667 """Prints a message and waits for the download to complete. The download
668 must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55669 assert self.thread, 'DownloadJob must be started before WaitFor is called.'
670 print 'Downloading revision %s...' % str(self.rev)
[email protected]53bb6342012-06-01 04:11:00671 self.progress_event.set() # Display progress of download.
672 self.thread.join()
673
674
[email protected]2e0f2672014-08-13 20:32:58675def Bisect(context,
[email protected]5e93cf162012-01-28 02:16:56676 num_runs=1,
[email protected]4df583c2014-07-31 17:11:55677 command='%p %a',
[email protected]60ac66e32011-07-18 16:08:25678 try_args=(),
[email protected]afe30662011-07-30 01:05:52679 profile=None,
[email protected]d59c8712014-02-11 21:04:57680 interactive=True,
[email protected]53bb6342012-06-01 04:11:00681 evaluate=AskIsGoodBuild):
[email protected]afe30662011-07-30 01:05:52682 """Given known good and known bad revisions, run a binary search on all
683 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25684
[email protected]2e0f2672014-08-13 20:32:58685 @param context PathContext object initialized with user provided parameters.
[email protected]5e93cf162012-01-28 02:16:56686 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52687 @param try_args A tuple of arguments to pass to the test application.
688 @param profile The name of the user profile to run with.
[email protected]d59c8712014-02-11 21:04:57689 @param interactive If it is false, use command exit code for good or bad
690 judgment of the argument build.
[email protected]53bb6342012-06-01 04:11:00691 @param evaluate A function which returns 'g' if the argument build is good,
692 'b' if it's bad or 'u' if unknown.
[email protected]afe30662011-07-30 01:05:52693
694 Threading is used to fetch Chromium revisions in the background, speeding up
695 the user's experience. For example, suppose the bounds of the search are
696 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
697 whether revision 50 is good or bad, the next revision to check will be either
698 25 or 75. So, while revision 50 is being checked, the script will download
699 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
700 known:
701
702 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
703 is run on rev 75.
704
705 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
706 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25707 """
708
[email protected]afe30662011-07-30 01:05:52709 if not profile:
710 profile = 'profile'
711
[email protected]2e0f2672014-08-13 20:32:58712 good_rev = context.good_revision
713 bad_rev = context.bad_revision
[email protected]afe30662011-07-30 01:05:52714 cwd = os.getcwd()
715
[email protected]28a3c122014-08-09 11:04:51716 print 'Downloading list of known revisions...',
[email protected]83048502014-08-21 16:48:44717 if not context.use_local_repo and not context.is_official:
[email protected]28a3c122014-08-09 11:04:51718 print '(use --use-local-repo for speed if you have a local checkout)'
719 else:
720 print
[email protected]d0149c5c2012-05-29 21:12:11721 _GetDownloadPath = lambda rev: os.path.join(cwd,
722 '%s-%s' % (str(rev), context.archive_name))
[email protected]2e0f2672014-08-13 20:32:58723 if context.is_official:
[email protected]d0149c5c2012-05-29 21:12:11724 revlist = context.GetOfficialBuildsList()
725 else:
726 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52727
728 # Get a list of revisions to bisect across.
729 if len(revlist) < 2: # Don't have enough builds to bisect.
730 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
731 raise RuntimeError(msg)
732
733 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09734 minrev = 0
735 maxrev = len(revlist) - 1
736 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52737 rev = revlist[pivot]
[email protected]4df583c2014-07-31 17:11:55738 zip_file = _GetDownloadPath(rev)
739 fetch = DownloadJob(context, 'initial_fetch', rev, zip_file)
[email protected]eadd95d2012-11-02 22:42:09740 fetch.Start()
741 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25742
743 # Binary search time!
[email protected]4df583c2014-07-31 17:11:55744 while fetch and fetch.zip_file and maxrev - minrev > 1:
[email protected]eadd95d2012-11-02 22:42:09745 if bad_rev < good_rev:
[email protected]4df583c2014-07-31 17:11:55746 min_str, max_str = 'bad', 'good'
[email protected]eadd95d2012-11-02 22:42:09747 else:
[email protected]4df583c2014-07-31 17:11:55748 min_str, max_str = 'good', 'bad'
749 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str,
[email protected]eadd95d2012-11-02 22:42:09750 revlist[maxrev], max_str)
751
[email protected]afe30662011-07-30 01:05:52752 # Pre-fetch next two possible pivots
753 # - down_pivot is the next revision to check if the current revision turns
754 # out to be bad.
755 # - up_pivot is the next revision to check if the current revision turns
756 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09757 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00758 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09759 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52760 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00761 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
762 _GetDownloadPath(down_rev))
763 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25764
[email protected]eadd95d2012-11-02 22:42:09765 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00766 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09767 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52768 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00769 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
770 _GetDownloadPath(up_rev))
771 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25772
[email protected]afe30662011-07-30 01:05:52773 # Run test on the pivot revision.
[email protected]e29c08c2012-09-17 20:50:50774 status = None
775 stdout = None
776 stderr = None
777 try:
778 (status, stdout, stderr) = RunRevision(context,
779 rev,
[email protected]4df583c2014-07-31 17:11:55780 fetch.zip_file,
[email protected]e29c08c2012-09-17 20:50:50781 profile,
782 num_runs,
[email protected]4646a752013-07-19 22:14:34783 command,
[email protected]e29c08c2012-09-17 20:50:50784 try_args)
785 except Exception, e:
[email protected]fc3702e2013-11-09 04:23:00786 print >> sys.stderr, e
[email protected]60ac66e32011-07-18 16:08:25787
[email protected]53bb6342012-06-01 04:11:00788 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52789 # On that basis, kill one of the background downloads and complete the
790 # other, as described in the comments above.
791 try:
[email protected]d59c8712014-02-11 21:04:57792 if not interactive:
793 if status:
794 answer = 'b'
795 print 'Bad revision: %s' % rev
796 else:
797 answer = 'g'
798 print 'Good revision: %s' % rev
799 else:
[email protected]2e0f2672014-08-13 20:32:58800 answer = evaluate(rev, context.is_official, status, stdout, stderr)
[email protected]4df583c2014-07-31 17:11:55801 if ((answer == 'g' and good_rev < bad_rev)
802 or (answer == 'b' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12803 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09804 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00805 if down_fetch:
806 down_fetch.Stop() # Kill the download of the older revision.
[email protected]1d4a06242013-08-20 22:53:12807 fetch = None
[email protected]53bb6342012-06-01 04:11:00808 if up_fetch:
809 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52810 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09811 fetch = up_fetch
[email protected]4df583c2014-07-31 17:11:55812 elif ((answer == 'b' and good_rev < bad_rev)
813 or (answer == 'g' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12814 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09815 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00816 if up_fetch:
817 up_fetch.Stop() # Kill the download of the newer revision.
[email protected]1d4a06242013-08-20 22:53:12818 fetch = None
[email protected]53bb6342012-06-01 04:11:00819 if down_fetch:
820 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52821 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09822 fetch = down_fetch
[email protected]1d4a06242013-08-20 22:53:12823 elif answer == 'r':
824 pass # Retry requires no changes.
[email protected]53bb6342012-06-01 04:11:00825 elif answer == 'u':
826 # Nuke the revision from the revlist and choose a new pivot.
[email protected]1d4a06242013-08-20 22:53:12827 fetch.Stop()
[email protected]53bb6342012-06-01 04:11:00828 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09829 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00830
[email protected]eadd95d2012-11-02 22:42:09831 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00832 # Alternate between using down_pivot or up_pivot for the new pivot
833 # point, without affecting the range. Do this instead of setting the
834 # pivot to the midpoint of the new range because adjacent revisions
835 # are likely affected by the same issue that caused the (u)nknown
836 # response.
837 if up_fetch and down_fetch:
838 fetch = [up_fetch, down_fetch][len(revlist) % 2]
839 elif up_fetch:
840 fetch = up_fetch
841 else:
842 fetch = down_fetch
843 fetch.WaitFor()
844 if fetch == up_fetch:
845 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
846 else:
847 pivot = down_pivot
[email protected]4df583c2014-07-31 17:11:55848 zip_file = fetch.zip_file
[email protected]53bb6342012-06-01 04:11:00849
850 if down_fetch and fetch != down_fetch:
851 down_fetch.Stop()
852 if up_fetch and fetch != up_fetch:
853 up_fetch.Stop()
854 else:
[email protected]4df583c2014-07-31 17:11:55855 assert False, 'Unexpected return value from evaluate(): ' + answer
[email protected]afe30662011-07-30 01:05:52856 except SystemExit:
[email protected]4df583c2014-07-31 17:11:55857 print 'Cleaning up...'
[email protected]5e93cf162012-01-28 02:16:56858 for f in [_GetDownloadPath(revlist[down_pivot]),
859 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52860 try:
861 os.unlink(f)
862 except OSError:
863 pass
864 sys.exit(0)
865
866 rev = revlist[pivot]
867
[email protected]2e0f2672014-08-13 20:32:58868 return (revlist[minrev], revlist[maxrev], context)
[email protected]60ac66e32011-07-18 16:08:25869
870
[email protected]37ed3172013-09-24 23:49:30871def GetBlinkDEPSRevisionForChromiumRevision(rev):
[email protected]4c6fec6b2013-09-17 17:44:08872 """Returns the blink revision that was in REVISIONS file at
[email protected]b2fe7f22011-10-25 22:58:31873 chromium revision |rev|."""
874 # . doesn't match newlines without re.DOTALL, so this is safe.
[email protected]37ed3172013-09-24 23:49:30875 blink_re = re.compile(r'webkit_revision\D*(\d+)')
876 url = urllib.urlopen(DEPS_FILE % rev)
877 m = blink_re.search(url.read())
878 url.close()
879 if m:
880 return int(m.group(1))
881 else:
[email protected]4df583c2014-07-31 17:11:55882 raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
[email protected]37ed3172013-09-24 23:49:30883
884
[email protected]2e0f2672014-08-13 20:32:58885def GetBlinkRevisionForChromiumRevision(context, rev):
[email protected]37ed3172013-09-24 23:49:30886 """Returns the blink revision that was in REVISIONS file at
887 chromium revision |rev|."""
[email protected]3e7c85322014-06-27 20:27:36888 def _IsRevisionNumber(revision):
889 if isinstance(revision, int):
890 return True
891 else:
892 return revision.isdigit()
[email protected]2e0f2672014-08-13 20:32:58893 if str(rev) in context.githash_svn_dict:
894 rev = context.githash_svn_dict[str(rev)]
895 file_url = '%s/%s%s/REVISIONS' % (context.base_url,
896 context._listing_platform_dir, rev)
[email protected]4c6fec6b2013-09-17 17:44:08897 url = urllib.urlopen(file_url)
[email protected]2e0f2672014-08-13 20:32:58898 if url.getcode() == 200:
899 try:
900 data = json.loads(url.read())
901 except ValueError:
902 print 'ValueError for JSON URL: %s' % file_url
903 raise ValueError
904 else:
905 raise ValueError
[email protected]b2fe7f22011-10-25 22:58:31906 url.close()
[email protected]4c6fec6b2013-09-17 17:44:08907 if 'webkit_revision' in data:
[email protected]3e7c85322014-06-27 20:27:36908 blink_rev = data['webkit_revision']
909 if not _IsRevisionNumber(blink_rev):
[email protected]2e0f2672014-08-13 20:32:58910 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
[email protected]3e7c85322014-06-27 20:27:36911 return blink_rev
[email protected]b2fe7f22011-10-25 22:58:31912 else:
[email protected]ff50d1c2013-04-17 18:49:36913 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:31914
[email protected]4df583c2014-07-31 17:11:55915
[email protected]37ed3172013-09-24 23:49:30916def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
917 """Returns the chromium revision that has the correct blink revision
918 for blink bisect, DEPS and REVISIONS file might not match since
919 blink snapshots point to tip of tree blink.
920 Note: The revisions_final variable might get modified to include
921 additional revisions."""
[email protected]37ed3172013-09-24 23:49:30922 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(rev)
923
924 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
925 idx = revisions.index(rev)
926 if idx > 0:
927 rev = revisions[idx-1]
928 if rev not in revisions_final:
929 revisions_final.insert(0, rev)
930
931 revisions_final.sort()
932 return rev
[email protected]b2fe7f22011-10-25 22:58:31933
[email protected]4df583c2014-07-31 17:11:55934
[email protected]5980b752014-07-02 00:34:40935def GetChromiumRevision(context, url):
[email protected]801fb652012-07-20 20:13:50936 """Returns the chromium revision read from given URL."""
937 try:
938 # Location of the latest build revision number
[email protected]5980b752014-07-02 00:34:40939 latest_revision = urllib.urlopen(url).read()
940 if latest_revision.isdigit():
941 return int(latest_revision)
942 return context.GetSVNRevisionFromGitHash(latest_revision)
[email protected]4df583c2014-07-31 17:11:55943 except Exception:
944 print 'Could not determine latest revision. This could be bad...'
[email protected]801fb652012-07-20 20:13:50945 return 999999999
946
947
[email protected]67e0bc62009-09-03 22:06:09948def main():
[email protected]2c1d2732009-10-29 19:52:17949 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:31950 'Perform binary search on the snapshot builds to find a minimal\n'
951 'range of revisions where a behavior change happened. The\n'
952 'behaviors are described as "good" and "bad".\n'
953 'It is NOT assumed that the behavior of the later revision is\n'
[email protected]09c58da2013-01-07 21:30:17954 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:38955 '\n'
[email protected]887c9182013-02-12 20:30:31956 'Revision numbers should use\n'
957 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
958 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
959 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
960 ' for earlier revs.\n'
961 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
962 ' are incorrect, they are from branches.\n'
963 '\n'
[email protected]178aab72010-10-08 17:21:38964 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:33965 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:57966 # Strangely, the default help output doesn't include the choice list.
[email protected]480369782014-08-22 20:15:58967 choices = ['mac', 'mac64', 'win', 'win64', 'linux', 'linux64', 'linux-arm']
[email protected]4082b182011-05-02 20:30:17968 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
[email protected]7ad66a72009-09-04 17:52:33969 parser.add_option('-a', '--archive',
[email protected]4df583c2014-07-31 17:11:55970 choices=choices,
971 help='The buildbot archive to bisect [%s].' %
972 '|'.join(choices))
973 parser.add_option('-o',
974 action='store_true',
975 dest='official_builds',
976 help='Bisect across official Chrome builds (internal '
977 'only) instead of Chromium archives.')
978 parser.add_option('-b', '--bad',
979 type='str',
980 help='A bad revision to start bisection. '
981 'May be earlier or later than the good revision. '
982 'Default is HEAD.')
983 parser.add_option('-f', '--flash_path',
984 type='str',
985 help='Absolute path to a recent Adobe Pepper Flash '
986 'binary to be used in this bisection (e.g. '
987 'on Windows C:\...\pepflashplayer.dll and on Linux '
988 '/opt/google/chrome/PepperFlash/'
989 'libpepflashplayer.so).')
990 parser.add_option('-d', '--pdf_path',
991 type='str',
992 help='Absolute path to a recent PDF plugin '
993 'binary to be used in this bisection (e.g. '
994 'on Windows C:\...\pdf.dll and on Linux '
995 '/opt/google/chrome/libpdf.so). Option also enables '
996 'print preview.')
997 parser.add_option('-g', '--good',
998 type='str',
999 help='A good revision to start bisection. ' +
1000 'May be earlier or later than the bad revision. ' +
1001 'Default is 0.')
1002 parser.add_option('-p', '--profile', '--user-data-dir',
1003 type='str',
1004 default='profile',
1005 help='Profile to use; this will not reset every run. '
1006 'Defaults to a clean profile.')
1007 parser.add_option('-t', '--times',
1008 type='int',
1009 default=1,
1010 help='Number of times to run each build before asking '
1011 'if it\'s good or bad. Temporary profiles are reused.')
1012 parser.add_option('-c', '--command',
1013 type='str',
1014 default='%p %a',
1015 help='Command to execute. %p and %a refer to Chrome '
1016 'executable and specified extra arguments '
1017 'respectively. Use %s to specify all extra arguments '
1018 'as one string. Defaults to "%p %a". Note that any '
1019 'extra paths specified should be absolute.')
1020 parser.add_option('-l', '--blink',
1021 action='store_true',
1022 help='Use Blink bisect instead of Chromium. ')
1023 parser.add_option('', '--not-interactive',
1024 action='store_true',
1025 default=False,
1026 help='Use command exit code to tell good/bad revision.')
[email protected]011886692014-08-01 21:00:211027 parser.add_option('--asan',
1028 dest='asan',
1029 action='store_true',
1030 default=False,
1031 help='Allow the script to bisect ASAN builds')
[email protected]6a7a5d62014-07-09 04:45:501032 parser.add_option('--use-local-repo',
1033 dest='use_local_repo',
1034 action='store_true',
1035 default=False,
[email protected]4df583c2014-07-31 17:11:551036 help='Allow the script to convert git SHA1 to SVN '
1037 'revision using "git svn find-rev <SHA1>" '
1038 'command from a Chromium checkout.')
[email protected]b3b20512013-08-26 18:51:041039
[email protected]7ad66a72009-09-04 17:52:331040 (opts, args) = parser.parse_args()
1041
1042 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:381043 print 'Error: missing required parameter: --archive'
1044 print
[email protected]7ad66a72009-09-04 17:52:331045 parser.print_help()
1046 return 1
1047
[email protected]011886692014-08-01 21:00:211048 if opts.asan:
1049 supported_platforms = ['linux', 'mac', 'win']
1050 if opts.archive not in supported_platforms:
1051 print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
1052 '|'.join(supported_platforms))
1053 return 1
1054 if opts.official_builds:
1055 print 'Error: Do not yet support bisecting official ASAN builds.'
1056 return 1
1057
1058 if opts.asan:
1059 base_url = ASAN_BASE_URL
1060 elif opts.blink:
[email protected]4c6fec6b2013-09-17 17:44:081061 base_url = WEBKIT_BASE_URL
1062 else:
1063 base_url = CHROMIUM_BASE_URL
1064
[email protected]183706d92011-06-10 13:06:221065 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]2e0f2672014-08-13 20:32:581066 context = PathContext(base_url, opts.archive, opts.good, opts.bad,
[email protected]965e18fc2014-08-14 20:10:181067 opts.official_builds, opts.asan, opts.use_local_repo,
1068 opts.flash_path, opts.pdf_path)
[email protected]67e0bc62009-09-03 22:06:091069 # Pick a starting point, try to get HEAD for this.
[email protected]2e0f2672014-08-13 20:32:581070 if not opts.bad:
1071 context.bad_revision = '999.0.0.0'
1072 context.bad_revision = GetChromiumRevision(
1073 context, context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:091074
1075 # Find out when we were good.
[email protected]2e0f2672014-08-13 20:32:581076 if not opts.good:
1077 context.good_revision = '0.0.0.0' if opts.official_builds else 0
[email protected]801fb652012-07-20 20:13:501078
[email protected]fc3702e2013-11-09 04:23:001079 if opts.flash_path:
[email protected]2e0f2672014-08-13 20:32:581080 msg = 'Could not find Flash binary at %s' % opts.flash_path
1081 assert os.path.exists(opts.flash_path), msg
[email protected]fc3702e2013-11-09 04:23:001082
[email protected]cdb77062014-07-21 18:07:151083 if opts.pdf_path:
[email protected]2e0f2672014-08-13 20:32:581084 msg = 'Could not find PDF binary at %s' % opts.pdf_path
1085 assert os.path.exists(opts.pdf_path), msg
[email protected]cdb77062014-07-21 18:07:151086
[email protected]801fb652012-07-20 20:13:501087 if opts.official_builds:
[email protected]2e0f2672014-08-13 20:32:581088 context.good_revision = LooseVersion(context.good_revision)
1089 context.bad_revision = LooseVersion(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501090 else:
[email protected]2e0f2672014-08-13 20:32:581091 context.good_revision = int(context.good_revision)
1092 context.bad_revision = int(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501093
[email protected]5e93cf162012-01-28 02:16:561094 if opts.times < 1:
1095 print('Number of times to run (%d) must be greater than or equal to 1.' %
1096 opts.times)
1097 parser.print_help()
1098 return 1
1099
[email protected]011886692014-08-01 21:00:211100 if opts.asan:
1101 evaluator = IsGoodASANBuild
1102 else:
1103 evaluator = AskIsGoodBuild
1104
[email protected]2e0f2672014-08-13 20:32:581105 # Save these revision numbers to compare when showing the changelog URL
1106 # after the bisect.
1107 good_rev = context.good_revision
1108 bad_rev = context.bad_revision
1109
1110 (min_chromium_rev, max_chromium_rev, context) = Bisect(
1111 context, opts.times, opts.command, args, opts.profile,
[email protected]011886692014-08-01 21:00:211112 not opts.not_interactive, evaluator)
[email protected]67e0bc62009-09-03 22:06:091113
[email protected]ff50d1c2013-04-17 18:49:361114 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:311115 try:
[email protected]4c6fec6b2013-09-17 17:44:081116 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1117 min_chromium_rev)
1118 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1119 max_chromium_rev)
[email protected]4df583c2014-07-31 17:11:551120 except Exception:
[email protected]b2fe7f22011-10-25 22:58:311121 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:361122 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:311123
[email protected]3bdaa4752013-09-30 20:13:361124 if opts.blink:
1125 # We're done. Let the user know the results in an official manner.
1126 if good_rev > bad_rev:
1127 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
1128 else:
1129 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
[email protected]eadd95d2012-11-02 22:42:091130
[email protected]ff50d1c2013-04-17 18:49:361131 print 'BLINK CHANGELOG URL:'
1132 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]3bdaa4752013-09-30 20:13:361133
[email protected]d0149c5c2012-05-29 21:12:111134 else:
[email protected]3bdaa4752013-09-30 20:13:361135 # We're done. Let the user know the results in an official manner.
1136 if good_rev > bad_rev:
1137 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
1138 str(max_chromium_rev))
1139 else:
1140 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
1141 str(max_chromium_rev))
1142 if min_blink_rev != max_blink_rev:
[email protected]4df583c2014-07-31 17:11:551143 print ('NOTE: There is a Blink roll in the range, '
1144 'you might also want to do a Blink bisect.')
[email protected]3bdaa4752013-09-30 20:13:361145
1146 print 'CHANGELOG URL:'
1147 if opts.official_builds:
1148 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
1149 else:
1150 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:341151
[email protected]4df583c2014-07-31 17:11:551152
[email protected]67e0bc62009-09-03 22:06:091153if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:331154 sys.exit(main())