blob: 639c90b36f8fdd28e80e129230d72434b441716e [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]4df583c2014-07-31 17:11:5523# URL template for viewing changelogs between revisions.
pshenoy9ce271f2014-09-02 22:14:0524CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s')
25
26# URL to convert SVN revision to git hash.
pshenoy13cb79e02014-09-05 01:42:5327CRREV_URL = ('https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/')
[email protected]f6a71a72009-10-08 19:55:3828
[email protected]b2fe7f22011-10-25 22:58:3129# DEPS file URL.
Di Mu08c59682016-07-11 23:05:0730DEPS_FILE = ('https://chromium.googlesource.com/chromium/src/+/%s/DEPS')
[email protected]b2fe7f22011-10-25 22:58:3131
[email protected]4df583c2014-07-31 17:11:5532# Blink changelogs URL.
33BLINK_CHANGELOG_URL = ('http://build.chromium.org'
34 '/f/chromium/perf/dashboard/ui/changelog_blink.html'
35 '?url=/trunk&range=%d%%3A%d')
36
37DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s ('
38 'known good), but no later than %s (first known bad).')
39DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s ('
40 'known bad), but no later than %s (first known good).')
[email protected]05ff3fd2012-04-17 23:24:0641
[email protected]3e7c85322014-06-27 20:27:3642CHROMIUM_GITHASH_TO_SVN_URL = (
43 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5544
[email protected]3e7c85322014-06-27 20:27:3645BLINK_GITHASH_TO_SVN_URL = (
46 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5547
48GITHASH_TO_SVN_URL = {
49 'chromium': CHROMIUM_GITHASH_TO_SVN_URL,
50 'blink': BLINK_GITHASH_TO_SVN_URL,
51}
52
53# Search pattern to be matched in the JSON output from
[email protected]3e7c85322014-06-27 20:27:3654# CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision).
pshenoyb23a1452014-09-05 22:52:0555CHROMIUM_SEARCH_PATTERN_OLD = (
[email protected]3e7c85322014-06-27 20:27:3656 r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ')
pshenoyb23a1452014-09-05 22:52:0557CHROMIUM_SEARCH_PATTERN = (
58 r'Cr-Commit-Position: refs/heads/master@{#(\d+)}')
[email protected]4df583c2014-07-31 17:11:5559
[email protected]3e7c85322014-06-27 20:27:3660# Search pattern to be matched in the json output from
61# BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision).
62BLINK_SEARCH_PATTERN = (
63 r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ')
[email protected]4df583c2014-07-31 17:11:5564
65SEARCH_PATTERN = {
66 'chromium': CHROMIUM_SEARCH_PATTERN,
67 'blink': BLINK_SEARCH_PATTERN,
68}
[email protected]3e7c85322014-06-27 20:27:3669
[email protected]480369782014-08-22 20:15:5870CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with '
71 'no configured credentials')
72
[email protected]67e0bc62009-09-03 22:06:0973###############################################################################
74
Dominic Mazzoni215e80b2017-11-29 20:05:2775import glob
[email protected]83048502014-08-21 16:48:4476import httplib
[email protected]4c6fec6b2013-09-17 17:44:0877import json
[email protected]7ad66a72009-09-04 17:52:3378import optparse
[email protected]67e0bc62009-09-03 22:06:0979import os
80import re
[email protected]61ea90a2013-09-26 10:17:3481import shlex
[email protected]67e0bc62009-09-03 22:06:0982import shutil
[email protected]afe30662011-07-30 01:05:5283import subprocess
[email protected]67e0bc62009-09-03 22:06:0984import sys
[email protected]7ad66a72009-09-04 17:52:3385import tempfile
[email protected]afe30662011-07-30 01:05:5286import threading
[email protected]67e0bc62009-09-03 22:06:0987import urllib
[email protected]d0149c5c2012-05-29 21:12:1188from distutils.version import LooseVersion
[email protected]183706d92011-06-10 13:06:2289from xml.etree import ElementTree
[email protected]bd8dcb92010-03-31 01:05:2490import zipfile
91
[email protected]cb155a82011-11-29 17:25:3492
[email protected]183706d92011-06-10 13:06:2293class PathContext(object):
94 """A PathContext is used to carry the information used to construct URLs and
95 paths when dealing with the storage server and archives."""
[email protected]4c6fec6b2013-09-17 17:44:0896 def __init__(self, base_url, platform, good_revision, bad_revision,
Jason Kersey97bb027a2016-05-11 20:10:4397 is_asan, use_local_cache, flash_path = None):
[email protected]183706d92011-06-10 13:06:2298 super(PathContext, self).__init__()
99 # Store off the input parameters.
[email protected]4c6fec6b2013-09-17 17:44:08100 self.base_url = base_url
[email protected]183706d92011-06-10 13:06:22101 self.platform = platform # What's passed in to the '-a/--archive' option.
102 self.good_revision = good_revision
103 self.bad_revision = bad_revision
[email protected]011886692014-08-01 21:00:21104 self.is_asan = is_asan
105 self.build_type = 'release'
[email protected]fc3702e2013-11-09 04:23:00106 self.flash_path = flash_path
[email protected]3e7c85322014-06-27 20:27:36107 # Dictionary which stores svn revision number as key and it's
108 # corresponding git hash as value. This data is populated in
109 # _FetchAndParse and used later in GetDownloadURL while downloading
110 # the build.
111 self.githash_svn_dict = {}
[email protected]183706d92011-06-10 13:06:22112 # The name of the ZIP file in a revision directory on the server.
113 self.archive_name = None
114
rob724c9062015-01-22 00:26:42115 # Whether to cache and use the list of known revisions in a local file to
116 # speed up the initialization of the script at the next run.
117 self.use_local_cache = use_local_cache
118
119 # Locate the local checkout to speed up the script by using locally stored
120 # metadata.
121 abs_file_path = os.path.abspath(os.path.realpath(__file__))
122 local_src_path = os.path.join(os.path.dirname(abs_file_path), '..')
123 if abs_file_path.endswith(os.path.join('tools', 'bisect-builds.py')) and\
124 os.path.exists(os.path.join(local_src_path, '.git')):
125 self.local_src_path = os.path.normpath(local_src_path)
126 else:
127 self.local_src_path = None
[email protected]6a7a5d62014-07-09 04:45:50128
[email protected]183706d92011-06-10 13:06:22129 # Set some internal members:
130 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
131 # _archive_extract_dir = Uncompressed directory in the archive_name file.
132 # _binary_name = The name of the executable to run.
dmazzoni76e907d2015-01-22 08:14:49133 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
[email protected]183706d92011-06-10 13:06:22134 self._binary_name = 'chrome'
[email protected]480369782014-08-22 20:15:58135 elif self.platform in ('mac', 'mac64'):
[email protected]183706d92011-06-10 13:06:22136 self.archive_name = 'chrome-mac.zip'
137 self._archive_extract_dir = 'chrome-mac'
[email protected]480369782014-08-22 20:15:58138 elif self.platform in ('win', 'win64'):
Dominic Mazzonie84e40b2018-10-08 06:44:45139 # Note: changed at revision 591483; see GetDownloadURL and GetLaunchPath
140 # below where these are patched.
[email protected]183706d92011-06-10 13:06:22141 self.archive_name = 'chrome-win32.zip'
142 self._archive_extract_dir = 'chrome-win32'
143 self._binary_name = 'chrome.exe'
144 else:
[email protected]afe30662011-07-30 01:05:52145 raise Exception('Invalid platform: %s' % self.platform)
[email protected]183706d92011-06-10 13:06:22146
Jason Kersey97bb027a2016-05-11 20:10:43147 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
Dominic Mazzonie84e40b2018-10-08 06:44:45148 # Note: changed at revision 591483; see GetDownloadURL and GetLaunchPath
149 # below where these are patched.
Jason Kersey97bb027a2016-05-11 20:10:43150 self.archive_name = 'chrome-linux.zip'
151 self._archive_extract_dir = 'chrome-linux'
[email protected]d0149c5c2012-05-29 21:12:11152 if self.platform == 'linux':
Jason Kersey97bb027a2016-05-11 20:10:43153 self._listing_platform_dir = 'Linux/'
[email protected]d0149c5c2012-05-29 21:12:11154 elif self.platform == 'linux64':
Jason Kersey97bb027a2016-05-11 20:10:43155 self._listing_platform_dir = 'Linux_x64/'
156 elif self.platform == 'linux-arm':
157 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
158 elif self.platform == 'chromeos':
159 self._listing_platform_dir = 'Linux_ChromiumOS_Full/'
160 elif self.platform in ('mac', 'mac64'):
161 self._listing_platform_dir = 'Mac/'
162 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
163 elif self.platform == 'win':
164 self._listing_platform_dir = 'Win/'
jiawei.shao734efbc92016-09-23 02:11:45165 elif self.platform == 'win64':
166 self._listing_platform_dir = 'Win_x64/'
[email protected]d0149c5c2012-05-29 21:12:11167
[email protected]011886692014-08-01 21:00:21168 def GetASANPlatformDir(self):
169 """ASAN builds are in directories like "linux-release", or have filenames
170 like "asan-win32-release-277079.zip". This aligns to our platform names
171 except in the case of Windows where they use "win32" instead of "win"."""
172 if self.platform == 'win':
173 return 'win32'
174 else:
175 return self.platform
176
[email protected]183706d92011-06-10 13:06:22177 def GetListingURL(self, marker=None):
178 """Returns the URL for a directory listing, with an optional marker."""
179 marker_param = ''
180 if marker:
181 marker_param = '&marker=' + str(marker)
[email protected]011886692014-08-01 21:00:21182 if self.is_asan:
183 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type)
184 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param
185 else:
186 return (self.base_url + '/?delimiter=/&prefix=' +
187 self._listing_platform_dir + marker_param)
[email protected]183706d92011-06-10 13:06:22188
189 def GetDownloadURL(self, revision):
190 """Gets the download URL for a build archive of a specific revision."""
[email protected]011886692014-08-01 21:00:21191 if self.is_asan:
192 return '%s/%s-%s/%s-%d.zip' % (
193 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
194 self.GetASANBaseName(), revision)
Jason Kersey97bb027a2016-05-11 20:10:43195 if str(revision) in self.githash_svn_dict:
196 revision = self.githash_svn_dict[str(revision)]
Dominic Mazzonie84e40b2018-10-08 06:44:45197 archive_name = self.archive_name
198
199 # At revision 591483, the names of two of the archives changed
200 # due to: https://chromium-review.googlesource.com/#/q/1226086
201 # See: http://crbug.com/789612
202 if revision >= 591483:
203 if self.platform == 'chromeos':
204 archive_name = 'chrome-chromeos.zip'
205 elif self.platform in ('win', 'win64'):
206 archive_name = 'chrome-win.zip'
207
Jason Kersey97bb027a2016-05-11 20:10:43208 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
Dominic Mazzonie84e40b2018-10-08 06:44:45209 revision, archive_name)
[email protected]183706d92011-06-10 13:06:22210
211 def GetLastChangeURL(self):
212 """Returns a URL to the LAST_CHANGE file."""
[email protected]4c6fec6b2013-09-17 17:44:08213 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
[email protected]183706d92011-06-10 13:06:22214
[email protected]011886692014-08-01 21:00:21215 def GetASANBaseName(self):
216 """Returns the base name of the ASAN zip file."""
217 if 'linux' in self.platform:
218 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(),
219 self.build_type)
220 else:
221 return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type)
222
223 def GetLaunchPath(self, revision):
[email protected]183706d92011-06-10 13:06:22224 """Returns a relative path (presumably from the archive extraction location)
225 that is used to run the executable."""
[email protected]011886692014-08-01 21:00:21226 if self.is_asan:
227 extract_dir = '%s-%d' % (self.GetASANBaseName(), revision)
228 else:
229 extract_dir = self._archive_extract_dir
Dominic Mazzonie84e40b2018-10-08 06:44:45230
231 # At revision 591483, the names of two of the archives changed
232 # due to: https://chromium-review.googlesource.com/#/q/1226086
233 # See: http://crbug.com/789612
234 if revision >= 591483:
235 if self.platform == 'chromeos':
236 extract_dir = 'chrome-chromeos'
237 elif self.platform in ('win', 'win64'):
Lei Zhang1c8c6f7e2018-11-09 16:46:30238 extract_dir = 'chrome-win'
Dominic Mazzonie84e40b2018-10-08 06:44:45239
[email protected]011886692014-08-01 21:00:21240 return os.path.join(extract_dir, self._binary_name)
[email protected]183706d92011-06-10 13:06:22241
rob724c9062015-01-22 00:26:42242 def ParseDirectoryIndex(self, last_known_rev):
[email protected]afe30662011-07-30 01:05:52243 """Parses the Google Storage directory listing into a list of revision
[email protected]eadd95d2012-11-02 22:42:09244 numbers."""
[email protected]afe30662011-07-30 01:05:52245
rob724c9062015-01-22 00:26:42246 def _GetMarkerForRev(revision):
247 if self.is_asan:
248 return '%s-%s/%s-%d.zip' % (
249 self.GetASANPlatformDir(), self.build_type,
250 self.GetASANBaseName(), revision)
251 return '%s%d' % (self._listing_platform_dir, revision)
252
[email protected]afe30662011-07-30 01:05:52253 def _FetchAndParse(url):
254 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
255 next-marker is not None, then the listing is a partial listing and another
256 fetch should be performed with next-marker being the marker= GET
257 parameter."""
258 handle = urllib.urlopen(url)
259 document = ElementTree.parse(handle)
260
261 # All nodes in the tree are namespaced. Get the root's tag name to extract
262 # the namespace. Etree does namespaces as |{namespace}tag|.
263 root_tag = document.getroot().tag
264 end_ns_pos = root_tag.find('}')
265 if end_ns_pos == -1:
[email protected]4df583c2014-07-31 17:11:55266 raise Exception('Could not locate end namespace for directory index')
[email protected]afe30662011-07-30 01:05:52267 namespace = root_tag[:end_ns_pos + 1]
268
269 # Find the prefix (_listing_platform_dir) and whether or not the list is
270 # truncated.
271 prefix_len = len(document.find(namespace + 'Prefix').text)
272 next_marker = None
273 is_truncated = document.find(namespace + 'IsTruncated')
274 if is_truncated is not None and is_truncated.text.lower() == 'true':
275 next_marker = document.find(namespace + 'NextMarker').text
[email protected]afe30662011-07-30 01:05:52276 # Get a list of all the revisions.
[email protected]afe30662011-07-30 01:05:52277 revisions = []
[email protected]3e7c85322014-06-27 20:27:36278 githash_svn_dict = {}
[email protected]011886692014-08-01 21:00:21279 if self.is_asan:
280 asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName()))
281 # Non ASAN builds are in a <revision> directory. The ASAN builds are
282 # flat
283 all_prefixes = document.findall(namespace + 'Contents/' +
284 namespace + 'Key')
285 for prefix in all_prefixes:
286 m = asan_regex.match(prefix.text)
287 if m:
288 try:
289 revisions.append(int(m.group(1)))
290 except ValueError:
291 pass
292 else:
293 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
294 namespace + 'Prefix')
295 # The <Prefix> nodes have content of the form of
296 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
297 # trailing slash to just have a number.
298 for prefix in all_prefixes:
299 revnum = prefix.text[prefix_len:-1]
300 try:
dimua1dfa0ce2016-03-31 01:08:45301 revnum = int(revnum)
302 revisions.append(revnum)
303 # Notes:
304 # Ignore hash in chromium-browser-snapshots as they are invalid
305 # Resulting in 404 error in fetching pages:
306 # https://chromium.googlesource.com/chromium/src/+/[rev_hash]
[email protected]011886692014-08-01 21:00:21307 except ValueError:
308 pass
[email protected]3e7c85322014-06-27 20:27:36309 return (revisions, next_marker, githash_svn_dict)
[email protected]9639b002013-08-30 14:45:52310
[email protected]afe30662011-07-30 01:05:52311 # Fetch the first list of revisions.
rob724c9062015-01-22 00:26:42312 if last_known_rev:
313 revisions = []
314 # Optimization: Start paging at the last known revision (local cache).
315 next_marker = _GetMarkerForRev(last_known_rev)
316 # Optimization: Stop paging at the last known revision (remote).
317 last_change_rev = GetChromiumRevision(self, self.GetLastChangeURL())
318 if last_known_rev == last_change_rev:
319 return []
320 else:
321 (revisions, next_marker, new_dict) = _FetchAndParse(self.GetListingURL())
322 self.githash_svn_dict.update(new_dict)
323 last_change_rev = None
324
[email protected]afe30662011-07-30 01:05:52325 # If the result list was truncated, refetch with the next marker. Do this
326 # until an entire directory listing is done.
327 while next_marker:
rob724c9062015-01-22 00:26:42328 sys.stdout.write('\rFetching revisions at marker %s' % next_marker)
329 sys.stdout.flush()
330
[email protected]afe30662011-07-30 01:05:52331 next_url = self.GetListingURL(next_marker)
[email protected]3e7c85322014-06-27 20:27:36332 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url)
[email protected]afe30662011-07-30 01:05:52333 revisions.extend(new_revisions)
[email protected]3e7c85322014-06-27 20:27:36334 self.githash_svn_dict.update(new_dict)
rob724c9062015-01-22 00:26:42335 if last_change_rev and last_change_rev in new_revisions:
336 break
337 sys.stdout.write('\r')
338 sys.stdout.flush()
[email protected]afe30662011-07-30 01:05:52339 return revisions
340
[email protected]6a7a5d62014-07-09 04:45:50341 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot):
[email protected]3e7c85322014-06-27 20:27:36342 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1
[email protected]2e0f2672014-08-13 20:32:58343 response = urllib.urlopen(json_url)
344 if response.getcode() == 200:
345 try:
346 data = json.loads(response.read()[4:])
347 except ValueError:
348 print 'ValueError for JSON URL: %s' % json_url
349 raise ValueError
350 else:
351 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36352 if 'message' in data:
353 message = data['message'].split('\n')
354 message = [line for line in message if line.strip()]
355 search_pattern = re.compile(SEARCH_PATTERN[depot])
356 result = search_pattern.search(message[len(message)-1])
357 if result:
358 return result.group(1)
pshenoyb23a1452014-09-05 22:52:05359 else:
360 if depot == 'chromium':
361 result = re.search(CHROMIUM_SEARCH_PATTERN_OLD,
362 message[len(message)-1])
363 if result:
364 return result.group(1)
[email protected]3e7c85322014-06-27 20:27:36365 print 'Failed to get svn revision number for %s' % git_sha1
[email protected]1f99f4d2014-07-23 16:44:14366 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36367
[email protected]6a7a5d62014-07-09 04:45:50368 def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot):
369 def _RunGit(command, path):
370 command = ['git'] + command
[email protected]6a7a5d62014-07-09 04:45:50371 shell = sys.platform.startswith('win')
372 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
rob724c9062015-01-22 00:26:42373 stderr=subprocess.PIPE, cwd=path)
[email protected]6a7a5d62014-07-09 04:45:50374 (output, _) = proc.communicate()
[email protected]6a7a5d62014-07-09 04:45:50375 return (output, proc.returncode)
376
rob724c9062015-01-22 00:26:42377 path = self.local_src_path
[email protected]6a7a5d62014-07-09 04:45:50378 if depot == 'blink':
rob724c9062015-01-22 00:26:42379 path = os.path.join(self.local_src_path, 'third_party', 'WebKit')
380 revision = None
381 try:
[email protected]6a7a5d62014-07-09 04:45:50382 command = ['svn', 'find-rev', git_sha1]
383 (git_output, return_code) = _RunGit(command, path)
384 if not return_code:
rob724c9062015-01-22 00:26:42385 revision = git_output.strip('\n')
386 except ValueError:
387 pass
388 if not revision:
389 command = ['log', '-n1', '--format=%s', git_sha1]
390 (git_output, return_code) = _RunGit(command, path)
391 if not return_code:
392 revision = re.match('SVN changes up to revision ([0-9]+)', git_output)
393 revision = revision.group(1) if revision else None
394 if revision:
395 return revision
396 raise ValueError
[email protected]6a7a5d62014-07-09 04:45:50397
398 def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'):
rob724c9062015-01-22 00:26:42399 if not self.local_src_path:
[email protected]6a7a5d62014-07-09 04:45:50400 return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot)
401 else:
402 return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot)
403
[email protected]afe30662011-07-30 01:05:52404 def GetRevList(self):
405 """Gets the list of revision numbers between self.good_revision and
406 self.bad_revision."""
rob724c9062015-01-22 00:26:42407
408 cache = {}
409 # The cache is stored in the same directory as bisect-builds.py
410 cache_filename = os.path.join(
411 os.path.abspath(os.path.dirname(__file__)),
412 '.bisect-builds-cache.json')
413 cache_dict_key = self.GetListingURL()
414
415 def _LoadBucketFromCache():
416 if self.use_local_cache:
417 try:
418 with open(cache_filename) as cache_file:
rob1c836052015-05-18 16:34:02419 for (key, value) in json.load(cache_file).items():
420 cache[key] = value
rob724c9062015-01-22 00:26:42421 revisions = cache.get(cache_dict_key, [])
422 githash_svn_dict = cache.get('githash_svn_dict', {})
423 if revisions:
424 print 'Loaded revisions %d-%d from %s' % (revisions[0],
425 revisions[-1], cache_filename)
426 return (revisions, githash_svn_dict)
427 except (EnvironmentError, ValueError):
428 pass
429 return ([], {})
430
431 def _SaveBucketToCache():
432 """Save the list of revisions and the git-svn mappings to a file.
433 The list of revisions is assumed to be sorted."""
434 if self.use_local_cache:
435 cache[cache_dict_key] = revlist_all
436 cache['githash_svn_dict'] = self.githash_svn_dict
437 try:
438 with open(cache_filename, 'w') as cache_file:
439 json.dump(cache, cache_file)
440 print 'Saved revisions %d-%d to %s' % (
441 revlist_all[0], revlist_all[-1], cache_filename)
442 except EnvironmentError:
443 pass
444
[email protected]afe30662011-07-30 01:05:52445 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09446 minrev = min(self.good_revision, self.bad_revision)
447 maxrev = max(self.good_revision, self.bad_revision)
rob724c9062015-01-22 00:26:42448
449 (revlist_all, self.githash_svn_dict) = _LoadBucketFromCache()
450 last_known_rev = revlist_all[-1] if revlist_all else 0
451 if last_known_rev < maxrev:
452 revlist_all.extend(map(int, self.ParseDirectoryIndex(last_known_rev)))
453 revlist_all = list(set(revlist_all))
454 revlist_all.sort()
455 _SaveBucketToCache()
[email protected]37ed3172013-09-24 23:49:30456
457 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
[email protected]37ed3172013-09-24 23:49:30458
459 # Set good and bad revisions to be legit revisions.
460 if revlist:
461 if self.good_revision < self.bad_revision:
462 self.good_revision = revlist[0]
463 self.bad_revision = revlist[-1]
464 else:
465 self.bad_revision = revlist[0]
466 self.good_revision = revlist[-1]
467
468 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
469 if self.base_url == WEBKIT_BASE_URL:
470 revlist_all.sort()
471 self.good_revision = FixChromiumRevForBlink(revlist,
472 revlist_all,
473 self,
474 self.good_revision)
475 self.bad_revision = FixChromiumRevForBlink(revlist,
476 revlist_all,
477 self,
478 self.bad_revision)
[email protected]afe30662011-07-30 01:05:52479 return revlist
480
prasadv2375e6d2017-03-20 19:23:23481
482def IsMac():
483 return sys.platform.startswith('darwin')
484
485
[email protected]fc3702e2013-11-09 04:23:00486def UnzipFilenameToDir(filename, directory):
487 """Unzip |filename| to |directory|."""
[email protected]afe30662011-07-30 01:05:52488 cwd = os.getcwd()
489 if not os.path.isabs(filename):
490 filename = os.path.join(cwd, filename)
[email protected]bd8dcb92010-03-31 01:05:24491 # Make base.
[email protected]fc3702e2013-11-09 04:23:00492 if not os.path.isdir(directory):
493 os.mkdir(directory)
494 os.chdir(directory)
prasadv2375e6d2017-03-20 19:23:23495
496 # The Python ZipFile does not support symbolic links, which makes it
497 # unsuitable for Mac builds. so use ditto instead.
498 if IsMac():
499 unzip_cmd = ['ditto', '-x', '-k', filename, '.']
500 proc = subprocess.Popen(unzip_cmd, bufsize=0, stdout=subprocess.PIPE,
501 stderr=subprocess.PIPE)
502 proc.communicate()
503 os.chdir(cwd)
504 return
505
506 zf = zipfile.ZipFile(filename)
[email protected]e29c08c2012-09-17 20:50:50507 # Extract files.
508 for info in zf.infolist():
509 name = info.filename
510 if name.endswith('/'): # dir
511 if not os.path.isdir(name):
512 os.makedirs(name)
513 else: # file
[email protected]fc3702e2013-11-09 04:23:00514 directory = os.path.dirname(name)
John Budorick06e5df12015-02-27 17:44:27515 if not os.path.isdir(directory):
[email protected]fc3702e2013-11-09 04:23:00516 os.makedirs(directory)
[email protected]e29c08c2012-09-17 20:50:50517 out = open(name, 'wb')
518 out.write(zf.read(name))
519 out.close()
520 # Set permissions. Permission info in external_attr is shifted 16 bits.
521 os.chmod(name, info.external_attr >> 16L)
522 os.chdir(cwd)
[email protected]bd8dcb92010-03-31 01:05:24523
[email protected]67e0bc62009-09-03 22:06:09524
[email protected]468a9772011-08-09 18:42:00525def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
[email protected]afe30662011-07-30 01:05:52526 """Downloads and unzips revision |rev|.
527 @param context A PathContext instance.
528 @param rev The Chromium revision number/tag to download.
529 @param filename The destination for the downloaded file.
530 @param quit_event A threading.Event which will be set by the master thread to
531 indicate that the download should be aborted.
[email protected]468a9772011-08-09 18:42:00532 @param progress_event A threading.Event which will be set by the master thread
533 to indicate that the progress of the download should be
534 displayed.
[email protected]afe30662011-07-30 01:05:52535 """
536 def ReportHook(blocknum, blocksize, totalsize):
[email protected]946be752011-10-25 23:34:21537 if quit_event and quit_event.isSet():
[email protected]4df583c2014-07-31 17:11:55538 raise RuntimeError('Aborting download of revision %s' % str(rev))
[email protected]946be752011-10-25 23:34:21539 if progress_event and progress_event.isSet():
[email protected]468a9772011-08-09 18:42:00540 size = blocknum * blocksize
541 if totalsize == -1: # Total size not known.
[email protected]4df583c2014-07-31 17:11:55542 progress = 'Received %d bytes' % size
[email protected]468a9772011-08-09 18:42:00543 else:
544 size = min(totalsize, size)
[email protected]4df583c2014-07-31 17:11:55545 progress = 'Received %d of %d bytes, %.2f%%' % (
[email protected]468a9772011-08-09 18:42:00546 size, totalsize, 100.0 * size / totalsize)
547 # Send a \r to let all progress messages use just one line of output.
[email protected]4df583c2014-07-31 17:11:55548 sys.stdout.write('\r' + progress)
[email protected]468a9772011-08-09 18:42:00549 sys.stdout.flush()
[email protected]afe30662011-07-30 01:05:52550 download_url = context.GetDownloadURL(rev)
551 try:
John Budorick06e5df12015-02-27 17:44:27552 urllib.urlretrieve(download_url, filename, ReportHook)
[email protected]946be752011-10-25 23:34:21553 if progress_event and progress_event.isSet():
[email protected]ecaba01e62011-10-26 05:33:28554 print
mikecasee2b6ce82015-02-06 18:22:39555
[email protected]4df583c2014-07-31 17:11:55556 except RuntimeError:
[email protected]afe30662011-07-30 01:05:52557 pass
[email protected]7ad66a72009-09-04 17:52:33558
[email protected]7ad66a72009-09-04 17:52:33559
Dominic Mazzoni215e80b2017-11-29 20:05:27560def CopyMissingFileFromCurrentSource(src_glob, dst):
561 """Work around missing files in archives.
562 This happens when archives of Chrome don't contain all of the files
563 needed to build it. In many cases we can work around this using
564 files from the current checkout. The source is in the form of a glob
565 so that it can try to look for possible sources of the file in
566 multiple locations, but we just arbitrarily try the first match.
567
568 Silently fail if this doesn't work because we don't yet have clear
569 markers for builds that require certain files or a way to test
570 whether or not launching Chrome succeeded.
571 """
572 if not os.path.exists(dst):
573 matches = glob.glob(src_glob)
574 if matches:
575 shutil.copy2(matches[0], dst)
576
577
[email protected]4df583c2014-07-31 17:11:55578def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
[email protected]afe30662011-07-30 01:05:52579 """Given a zipped revision, unzip it and run the test."""
[email protected]4df583c2014-07-31 17:11:55580 print 'Trying revision %s...' % str(revision)
[email protected]3ff00b72011-07-20 21:34:47581
[email protected]afe30662011-07-30 01:05:52582 # Create a temp directory and unzip the revision into it.
[email protected]7ad66a72009-09-04 17:52:33583 cwd = os.getcwd()
584 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
[email protected]4df583c2014-07-31 17:11:55585 UnzipFilenameToDir(zip_file, tempdir)
dmazzoni76e907d2015-01-22 08:14:49586
Dominic Mazzoni215e80b2017-11-29 20:05:27587 # Hack: Some Chrome OS archives are missing some files; try to copy them
588 # from the local directory.
Dominic Mazzonie84e40b2018-10-08 06:44:45589 if context.platform == 'chromeos' and revision < 591483:
Dominic Mazzoni215e80b2017-11-29 20:05:27590 CopyMissingFileFromCurrentSource('third_party/icu/common/icudtl.dat',
591 '%s/chrome-linux/icudtl.dat' % tempdir)
592 CopyMissingFileFromCurrentSource('*out*/*/libminigbm.so',
593 '%s/chrome-linux/libminigbm.so' % tempdir)
dmazzoni76e907d2015-01-22 08:14:49594
[email protected]7ad66a72009-09-04 17:52:33595 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09596
[email protected]5e93cf162012-01-28 02:16:56597 # Run the build as many times as specified.
[email protected]4646a752013-07-19 22:14:34598 testargs = ['--user-data-dir=%s' % profile] + args
[email protected]d0149c5c2012-05-29 21:12:11599 # The sandbox must be run as root on Official Chrome, so bypass it.
Jason Kersey97bb027a2016-05-11 20:10:43600 if (context.flash_path and context.platform.startswith('linux')):
[email protected]d0149c5c2012-05-29 21:12:11601 testargs.append('--no-sandbox')
[email protected]fc3702e2013-11-09 04:23:00602 if context.flash_path:
603 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
604 # We have to pass a large enough Flash version, which currently needs not
605 # be correct. Instead of requiring the user of the script to figure out and
606 # pass the correct version we just spoof it.
607 testargs.append('--ppapi-flash-version=99.9.999.999')
[email protected]d0149c5c2012-05-29 21:12:11608
[email protected]4646a752013-07-19 22:14:34609 runcommand = []
[email protected]61ea90a2013-09-26 10:17:34610 for token in shlex.split(command):
[email protected]4df583c2014-07-31 17:11:55611 if token == '%a':
[email protected]4646a752013-07-19 22:14:34612 runcommand.extend(testargs)
613 else:
[email protected]4df583c2014-07-31 17:11:55614 runcommand.append(
[email protected]011886692014-08-01 21:00:21615 token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))).
616 replace('%s', ' '.join(testargs)))
[email protected]4646a752013-07-19 22:14:34617
[email protected]d59c8712014-02-11 21:04:57618 results = []
[email protected]4df583c2014-07-31 17:11:55619 for _ in range(num_runs):
[email protected]4646a752013-07-19 22:14:34620 subproc = subprocess.Popen(runcommand,
[email protected]5e93cf162012-01-28 02:16:56621 bufsize=-1,
622 stdout=subprocess.PIPE,
623 stderr=subprocess.PIPE)
624 (stdout, stderr) = subproc.communicate()
[email protected]d59c8712014-02-11 21:04:57625 results.append((subproc.returncode, stdout, stderr))
[email protected]7ad66a72009-09-04 17:52:33626 os.chdir(cwd)
[email protected]7ad66a72009-09-04 17:52:33627 try:
628 shutil.rmtree(tempdir, True)
[email protected]4df583c2014-07-31 17:11:55629 except Exception:
[email protected]7ad66a72009-09-04 17:52:33630 pass
[email protected]67e0bc62009-09-03 22:06:09631
[email protected]d59c8712014-02-11 21:04:57632 for (returncode, stdout, stderr) in results:
633 if returncode:
634 return (returncode, stdout, stderr)
635 return results[0]
[email protected]79f14742010-03-10 01:01:57636
[email protected]cb155a82011-11-29 17:25:34637
Jason Kersey97bb027a2016-05-11 20:10:43638# The arguments status, stdout and stderr are unused.
[email protected]4df583c2014-07-31 17:11:55639# They are present here because this function is passed to Bisect which then
640# calls it with 5 arguments.
641# pylint: disable=W0613
Jason Kersey97bb027a2016-05-11 20:10:43642def AskIsGoodBuild(rev, exit_status, stdout, stderr):
[email protected]4df583c2014-07-31 17:11:55643 """Asks the user whether build |rev| is good or bad."""
Fergal Daly0dd19532019-04-04 07:45:33644 if exit_status:
645 print 'Chrome exit_status: %d. Use s to see output' % exit_status
[email protected]79f14742010-03-10 01:01:57646 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09647 while True:
[email protected]4df583c2014-07-31 17:11:55648 response = raw_input('Revision %s is '
wangxianzhud8c4c562015-12-15 23:39:51649 '[(g)ood/(b)ad/(r)etry/(u)nknown/(s)tdout/(q)uit]: ' %
[email protected]53bb6342012-06-01 04:11:00650 str(rev))
wangxianzhud8c4c562015-12-15 23:39:51651 if response in ('g', 'b', 'r', 'u'):
[email protected]53bb6342012-06-01 04:11:00652 return response
wangxianzhud8c4c562015-12-15 23:39:51653 if response == 'q':
[email protected]afe30662011-07-30 01:05:52654 raise SystemExit()
wangxianzhud8c4c562015-12-15 23:39:51655 if response == 's':
656 print stdout
657 print stderr
[email protected]67e0bc62009-09-03 22:06:09658
[email protected]cb155a82011-11-29 17:25:34659
Jason Kersey97bb027a2016-05-11 20:10:43660def IsGoodASANBuild(rev, exit_status, stdout, stderr):
[email protected]011886692014-08-01 21:00:21661 """Determine if an ASAN build |rev| is good or bad
662
663 Will examine stderr looking for the error message emitted by ASAN. If not
664 found then will fallback to asking the user."""
665 if stderr:
666 bad_count = 0
667 for line in stderr.splitlines():
668 print line
669 if line.find('ERROR: AddressSanitizer:') != -1:
670 bad_count += 1
671 if bad_count > 0:
672 print 'Revision %d determined to be bad.' % rev
673 return 'b'
Jason Kersey97bb027a2016-05-11 20:10:43674 return AskIsGoodBuild(rev, exit_status, stdout, stderr)
skobes21b5cdfb2016-03-21 23:13:02675
676
Jason Kersey97bb027a2016-05-11 20:10:43677def DidCommandSucceed(rev, exit_status, stdout, stderr):
skobes21b5cdfb2016-03-21 23:13:02678 if exit_status:
679 print 'Bad revision: %s' % rev
680 return 'b'
681 else:
682 print 'Good revision: %s' % rev
683 return 'g'
684
[email protected]011886692014-08-01 21:00:21685
[email protected]53bb6342012-06-01 04:11:00686class DownloadJob(object):
687 """DownloadJob represents a task to download a given Chromium revision."""
[email protected]4df583c2014-07-31 17:11:55688
689 def __init__(self, context, name, rev, zip_file):
[email protected]53bb6342012-06-01 04:11:00690 super(DownloadJob, self).__init__()
691 # Store off the input parameters.
692 self.context = context
693 self.name = name
694 self.rev = rev
[email protected]4df583c2014-07-31 17:11:55695 self.zip_file = zip_file
[email protected]53bb6342012-06-01 04:11:00696 self.quit_event = threading.Event()
697 self.progress_event = threading.Event()
[email protected]4df583c2014-07-31 17:11:55698 self.thread = None
[email protected]53bb6342012-06-01 04:11:00699
700 def Start(self):
701 """Starts the download."""
702 fetchargs = (self.context,
703 self.rev,
[email protected]4df583c2014-07-31 17:11:55704 self.zip_file,
[email protected]53bb6342012-06-01 04:11:00705 self.quit_event,
706 self.progress_event)
707 self.thread = threading.Thread(target=FetchRevision,
708 name=self.name,
709 args=fetchargs)
710 self.thread.start()
711
712 def Stop(self):
713 """Stops the download which must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55714 assert self.thread, 'DownloadJob must be started before Stop is called.'
[email protected]53bb6342012-06-01 04:11:00715 self.quit_event.set()
716 self.thread.join()
[email protected]4df583c2014-07-31 17:11:55717 os.unlink(self.zip_file)
[email protected]53bb6342012-06-01 04:11:00718
719 def WaitFor(self):
720 """Prints a message and waits for the download to complete. The download
721 must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55722 assert self.thread, 'DownloadJob must be started before WaitFor is called.'
723 print 'Downloading revision %s...' % str(self.rev)
[email protected]53bb6342012-06-01 04:11:00724 self.progress_event.set() # Display progress of download.
rob8a4543f2016-01-20 00:43:59725 try:
726 while self.thread.isAlive():
727 # The parameter to join is needed to keep the main thread responsive to
728 # signals. Without it, the program will not respond to interruptions.
729 self.thread.join(1)
730 except (KeyboardInterrupt, SystemExit):
731 self.Stop()
732 raise
[email protected]53bb6342012-06-01 04:11:00733
734
skobes21b5cdfb2016-03-21 23:13:02735def VerifyEndpoint(fetch, context, rev, profile, num_runs, command, try_args,
736 evaluate, expected_answer):
737 fetch.WaitFor()
738 try:
739 (exit_status, stdout, stderr) = RunRevision(
740 context, rev, fetch.zip_file, profile, num_runs, command, try_args)
741 except Exception, e:
742 print >> sys.stderr, e
Lei Zhang2fa76302018-11-09 20:16:31743 raise SystemExit
Jason Kersey97bb027a2016-05-11 20:10:43744 if (evaluate(rev, exit_status, stdout, stderr) != expected_answer):
skobes21b5cdfb2016-03-21 23:13:02745 print 'Unexpected result at a range boundary! Your range is not correct.'
746 raise SystemExit
747
748
[email protected]2e0f2672014-08-13 20:32:58749def Bisect(context,
[email protected]5e93cf162012-01-28 02:16:56750 num_runs=1,
[email protected]4df583c2014-07-31 17:11:55751 command='%p %a',
[email protected]60ac66e32011-07-18 16:08:25752 try_args=(),
[email protected]afe30662011-07-30 01:05:52753 profile=None,
skobes21b5cdfb2016-03-21 23:13:02754 evaluate=AskIsGoodBuild,
755 verify_range=False):
[email protected]afe30662011-07-30 01:05:52756 """Given known good and known bad revisions, run a binary search on all
757 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25758
[email protected]2e0f2672014-08-13 20:32:58759 @param context PathContext object initialized with user provided parameters.
[email protected]5e93cf162012-01-28 02:16:56760 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52761 @param try_args A tuple of arguments to pass to the test application.
762 @param profile The name of the user profile to run with.
[email protected]53bb6342012-06-01 04:11:00763 @param evaluate A function which returns 'g' if the argument build is good,
764 'b' if it's bad or 'u' if unknown.
skobes21b5cdfb2016-03-21 23:13:02765 @param verify_range If true, tests the first and last revisions in the range
766 before proceeding with the bisect.
[email protected]afe30662011-07-30 01:05:52767
768 Threading is used to fetch Chromium revisions in the background, speeding up
769 the user's experience. For example, suppose the bounds of the search are
770 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
771 whether revision 50 is good or bad, the next revision to check will be either
772 25 or 75. So, while revision 50 is being checked, the script will download
773 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
774 known:
775
776 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
777 is run on rev 75.
778
779 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
780 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25781 """
782
[email protected]afe30662011-07-30 01:05:52783 if not profile:
784 profile = 'profile'
785
[email protected]2e0f2672014-08-13 20:32:58786 good_rev = context.good_revision
787 bad_rev = context.bad_revision
[email protected]afe30662011-07-30 01:05:52788 cwd = os.getcwd()
789
[email protected]28a3c122014-08-09 11:04:51790 print 'Downloading list of known revisions...',
Jason Kersey97bb027a2016-05-11 20:10:43791 if not context.use_local_cache:
rob724c9062015-01-22 00:26:42792 print '(use --use-local-cache to cache and re-use the list of revisions)'
[email protected]28a3c122014-08-09 11:04:51793 else:
794 print
[email protected]d0149c5c2012-05-29 21:12:11795 _GetDownloadPath = lambda rev: os.path.join(cwd,
796 '%s-%s' % (str(rev), context.archive_name))
Jason Kersey97bb027a2016-05-11 20:10:43797 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52798
799 # Get a list of revisions to bisect across.
800 if len(revlist) < 2: # Don't have enough builds to bisect.
801 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
802 raise RuntimeError(msg)
803
804 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09805 minrev = 0
806 maxrev = len(revlist) - 1
807 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52808 rev = revlist[pivot]
skobes21b5cdfb2016-03-21 23:13:02809 fetch = DownloadJob(context, 'initial_fetch', rev, _GetDownloadPath(rev))
[email protected]eadd95d2012-11-02 22:42:09810 fetch.Start()
skobes21b5cdfb2016-03-21 23:13:02811
812 if verify_range:
813 minrev_fetch = DownloadJob(
814 context, 'minrev_fetch', revlist[minrev],
815 _GetDownloadPath(revlist[minrev]))
816 maxrev_fetch = DownloadJob(
817 context, 'maxrev_fetch', revlist[maxrev],
818 _GetDownloadPath(revlist[maxrev]))
819 minrev_fetch.Start()
820 maxrev_fetch.Start()
821 try:
822 VerifyEndpoint(minrev_fetch, context, revlist[minrev], profile, num_runs,
823 command, try_args, evaluate, 'b' if bad_rev < good_rev else 'g')
824 VerifyEndpoint(maxrev_fetch, context, revlist[maxrev], profile, num_runs,
825 command, try_args, evaluate, 'g' if bad_rev < good_rev else 'b')
826 except (KeyboardInterrupt, SystemExit):
827 print 'Cleaning up...'
828 fetch.Stop()
829 sys.exit(0)
830 finally:
831 minrev_fetch.Stop()
832 maxrev_fetch.Stop()
833
[email protected]eadd95d2012-11-02 22:42:09834 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25835
836 # Binary search time!
[email protected]4df583c2014-07-31 17:11:55837 while fetch and fetch.zip_file and maxrev - minrev > 1:
[email protected]eadd95d2012-11-02 22:42:09838 if bad_rev < good_rev:
[email protected]4df583c2014-07-31 17:11:55839 min_str, max_str = 'bad', 'good'
[email protected]eadd95d2012-11-02 22:42:09840 else:
[email protected]4df583c2014-07-31 17:11:55841 min_str, max_str = 'good', 'bad'
zinovy.nis0951bca2017-04-12 19:23:54842 print ('Bisecting range [%s (%s), %s (%s)], '
843 'roughly %d steps left.') % (revlist[minrev], min_str,
844 revlist[maxrev], max_str,
845 int(maxrev - minrev)
846 .bit_length())
[email protected]eadd95d2012-11-02 22:42:09847
[email protected]afe30662011-07-30 01:05:52848 # Pre-fetch next two possible pivots
849 # - down_pivot is the next revision to check if the current revision turns
850 # out to be bad.
851 # - up_pivot is the next revision to check if the current revision turns
852 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09853 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00854 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09855 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52856 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00857 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
858 _GetDownloadPath(down_rev))
859 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25860
[email protected]eadd95d2012-11-02 22:42:09861 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00862 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09863 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52864 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00865 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
866 _GetDownloadPath(up_rev))
867 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25868
[email protected]afe30662011-07-30 01:05:52869 # Run test on the pivot revision.
skobes21b5cdfb2016-03-21 23:13:02870 exit_status = None
[email protected]e29c08c2012-09-17 20:50:50871 stdout = None
872 stderr = None
873 try:
skobes21b5cdfb2016-03-21 23:13:02874 (exit_status, stdout, stderr) = RunRevision(
875 context, rev, fetch.zip_file, profile, num_runs, command, try_args)
[email protected]e29c08c2012-09-17 20:50:50876 except Exception, e:
[email protected]fc3702e2013-11-09 04:23:00877 print >> sys.stderr, e
[email protected]60ac66e32011-07-18 16:08:25878
[email protected]53bb6342012-06-01 04:11:00879 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52880 # On that basis, kill one of the background downloads and complete the
881 # other, as described in the comments above.
882 try:
Jason Kersey97bb027a2016-05-11 20:10:43883 answer = evaluate(rev, exit_status, stdout, stderr)
[email protected]4df583c2014-07-31 17:11:55884 if ((answer == 'g' and good_rev < bad_rev)
885 or (answer == 'b' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12886 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09887 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00888 if down_fetch:
889 down_fetch.Stop() # Kill the download of the older revision.
[email protected]1d4a06242013-08-20 22:53:12890 fetch = None
[email protected]53bb6342012-06-01 04:11:00891 if up_fetch:
892 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52893 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09894 fetch = up_fetch
[email protected]4df583c2014-07-31 17:11:55895 elif ((answer == 'b' and good_rev < bad_rev)
896 or (answer == 'g' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12897 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09898 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00899 if up_fetch:
900 up_fetch.Stop() # Kill the download of the newer revision.
[email protected]1d4a06242013-08-20 22:53:12901 fetch = None
[email protected]53bb6342012-06-01 04:11:00902 if down_fetch:
903 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52904 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09905 fetch = down_fetch
[email protected]1d4a06242013-08-20 22:53:12906 elif answer == 'r':
907 pass # Retry requires no changes.
[email protected]53bb6342012-06-01 04:11:00908 elif answer == 'u':
909 # Nuke the revision from the revlist and choose a new pivot.
[email protected]1d4a06242013-08-20 22:53:12910 fetch.Stop()
[email protected]53bb6342012-06-01 04:11:00911 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09912 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00913
[email protected]eadd95d2012-11-02 22:42:09914 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00915 # Alternate between using down_pivot or up_pivot for the new pivot
916 # point, without affecting the range. Do this instead of setting the
917 # pivot to the midpoint of the new range because adjacent revisions
918 # are likely affected by the same issue that caused the (u)nknown
919 # response.
920 if up_fetch and down_fetch:
921 fetch = [up_fetch, down_fetch][len(revlist) % 2]
922 elif up_fetch:
923 fetch = up_fetch
924 else:
925 fetch = down_fetch
926 fetch.WaitFor()
927 if fetch == up_fetch:
928 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
929 else:
930 pivot = down_pivot
[email protected]53bb6342012-06-01 04:11:00931
932 if down_fetch and fetch != down_fetch:
933 down_fetch.Stop()
934 if up_fetch and fetch != up_fetch:
935 up_fetch.Stop()
936 else:
[email protected]4df583c2014-07-31 17:11:55937 assert False, 'Unexpected return value from evaluate(): ' + answer
skobes21b5cdfb2016-03-21 23:13:02938 except (KeyboardInterrupt, SystemExit):
[email protected]4df583c2014-07-31 17:11:55939 print 'Cleaning up...'
skobes21b5cdfb2016-03-21 23:13:02940 for f in [_GetDownloadPath(rev),
941 _GetDownloadPath(revlist[down_pivot]),
[email protected]5e93cf162012-01-28 02:16:56942 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52943 try:
944 os.unlink(f)
945 except OSError:
946 pass
947 sys.exit(0)
948
949 rev = revlist[pivot]
950
[email protected]2e0f2672014-08-13 20:32:58951 return (revlist[minrev], revlist[maxrev], context)
[email protected]60ac66e32011-07-18 16:08:25952
953
pshenoycd6bd682014-09-10 20:50:22954def GetBlinkDEPSRevisionForChromiumRevision(self, rev):
[email protected]4c6fec6b2013-09-17 17:44:08955 """Returns the blink revision that was in REVISIONS file at
[email protected]b2fe7f22011-10-25 22:58:31956 chromium revision |rev|."""
pshenoycd6bd682014-09-10 20:50:22957
958 def _GetBlinkRev(url, blink_re):
959 m = blink_re.search(url.read())
960 url.close()
961 if m:
fmalitaa898d222016-07-12 22:29:03962 return m.group(1)
pshenoycd6bd682014-09-10 20:50:22963
Di Mu08c59682016-07-11 23:05:07964 url = urllib.urlopen(DEPS_FILE % GetGitHashFromSVNRevision(rev))
pshenoycd6bd682014-09-10 20:50:22965 if url.getcode() == 200:
Di Mu08c59682016-07-11 23:05:07966 blink_re = re.compile(r'webkit_revision\D*\d+;\D*\d+;(\w+)')
967 blink_git_sha = _GetBlinkRev(url, blink_re)
968 return self.GetSVNRevisionFromGitHash(blink_git_sha, 'blink')
pshenoycd6bd682014-09-10 20:50:22969 raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
[email protected]37ed3172013-09-24 23:49:30970
971
[email protected]2e0f2672014-08-13 20:32:58972def GetBlinkRevisionForChromiumRevision(context, rev):
[email protected]37ed3172013-09-24 23:49:30973 """Returns the blink revision that was in REVISIONS file at
974 chromium revision |rev|."""
[email protected]3e7c85322014-06-27 20:27:36975 def _IsRevisionNumber(revision):
976 if isinstance(revision, int):
977 return True
978 else:
979 return revision.isdigit()
[email protected]2e0f2672014-08-13 20:32:58980 if str(rev) in context.githash_svn_dict:
981 rev = context.githash_svn_dict[str(rev)]
982 file_url = '%s/%s%s/REVISIONS' % (context.base_url,
983 context._listing_platform_dir, rev)
[email protected]4c6fec6b2013-09-17 17:44:08984 url = urllib.urlopen(file_url)
[email protected]2e0f2672014-08-13 20:32:58985 if url.getcode() == 200:
986 try:
987 data = json.loads(url.read())
988 except ValueError:
989 print 'ValueError for JSON URL: %s' % file_url
990 raise ValueError
991 else:
992 raise ValueError
[email protected]b2fe7f22011-10-25 22:58:31993 url.close()
[email protected]4c6fec6b2013-09-17 17:44:08994 if 'webkit_revision' in data:
[email protected]3e7c85322014-06-27 20:27:36995 blink_rev = data['webkit_revision']
996 if not _IsRevisionNumber(blink_rev):
[email protected]2e0f2672014-08-13 20:32:58997 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
[email protected]3e7c85322014-06-27 20:27:36998 return blink_rev
[email protected]b2fe7f22011-10-25 22:58:31999 else:
[email protected]ff50d1c2013-04-17 18:49:361000 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:311001
[email protected]4df583c2014-07-31 17:11:551002
[email protected]37ed3172013-09-24 23:49:301003def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
1004 """Returns the chromium revision that has the correct blink revision
1005 for blink bisect, DEPS and REVISIONS file might not match since
1006 blink snapshots point to tip of tree blink.
1007 Note: The revisions_final variable might get modified to include
1008 additional revisions."""
pshenoycd6bd682014-09-10 20:50:221009 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(self, rev)
[email protected]37ed3172013-09-24 23:49:301010
1011 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
1012 idx = revisions.index(rev)
1013 if idx > 0:
1014 rev = revisions[idx-1]
1015 if rev not in revisions_final:
1016 revisions_final.insert(0, rev)
1017
1018 revisions_final.sort()
1019 return rev
[email protected]b2fe7f22011-10-25 22:58:311020
[email protected]4df583c2014-07-31 17:11:551021
[email protected]5980b752014-07-02 00:34:401022def GetChromiumRevision(context, url):
[email protected]801fb652012-07-20 20:13:501023 """Returns the chromium revision read from given URL."""
1024 try:
1025 # Location of the latest build revision number
[email protected]5980b752014-07-02 00:34:401026 latest_revision = urllib.urlopen(url).read()
1027 if latest_revision.isdigit():
1028 return int(latest_revision)
1029 return context.GetSVNRevisionFromGitHash(latest_revision)
[email protected]4df583c2014-07-31 17:11:551030 except Exception:
1031 print 'Could not determine latest revision. This could be bad...'
[email protected]801fb652012-07-20 20:13:501032 return 999999999
1033
pshenoycd6bd682014-09-10 20:50:221034def GetGitHashFromSVNRevision(svn_revision):
1035 crrev_url = CRREV_URL + str(svn_revision)
1036 url = urllib.urlopen(crrev_url)
1037 if url.getcode() == 200:
1038 data = json.loads(url.read())
1039 if 'git_sha' in data:
1040 return data['git_sha']
1041
pshenoy9ce271f2014-09-02 22:14:051042def PrintChangeLog(min_chromium_rev, max_chromium_rev):
1043 """Prints the changelog URL."""
1044
pshenoycd6bd682014-09-10 20:50:221045 print (' ' + CHANGELOG_URL % (GetGitHashFromSVNRevision(min_chromium_rev),
1046 GetGitHashFromSVNRevision(max_chromium_rev)))
pshenoy9ce271f2014-09-02 22:14:051047
elawrence446bcc32017-04-14 17:18:511048def error_internal_option(option, opt, value, parser):
1049 raise optparse.OptionValueError(
1050 'The -o and -r options are only\navailable in the internal version of '
1051 'this script. Google\nemployees should visit http://go/bisect-builds '
1052 'for\nconfiguration instructions.')
[email protected]801fb652012-07-20 20:13:501053
[email protected]67e0bc62009-09-03 22:06:091054def main():
[email protected]2c1d2732009-10-29 19:52:171055 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:311056 'Perform binary search on the snapshot builds to find a minimal\n'
1057 'range of revisions where a behavior change happened. The\n'
1058 'behaviors are described as "good" and "bad".\n'
1059 'It is NOT assumed that the behavior of the later revision is\n'
[email protected]09c58da2013-01-07 21:30:171060 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:381061 '\n'
[email protected]887c9182013-02-12 20:30:311062 'Revision numbers should use\n'
[email protected]887c9182013-02-12 20:30:311063 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
1064 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
1065 ' for earlier revs.\n'
1066 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
1067 ' are incorrect, they are from branches.\n'
1068 '\n'
[email protected]178aab72010-10-08 17:21:381069 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:331070 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:571071 # Strangely, the default help output doesn't include the choice list.
mikecasea8cd284c2014-12-02 21:30:581072 choices = ['mac', 'mac64', 'win', 'win64', 'linux', 'linux64', 'linux-arm',
dmazzoni76e907d2015-01-22 08:14:491073 'chromeos']
[email protected]7ad66a72009-09-04 17:52:331074 parser.add_option('-a', '--archive',
[email protected]4df583c2014-07-31 17:11:551075 choices=choices,
1076 help='The buildbot archive to bisect [%s].' %
1077 '|'.join(choices))
[email protected]4df583c2014-07-31 17:11:551078 parser.add_option('-b', '--bad',
1079 type='str',
1080 help='A bad revision to start bisection. '
1081 'May be earlier or later than the good revision. '
1082 'Default is HEAD.')
1083 parser.add_option('-f', '--flash_path',
1084 type='str',
1085 help='Absolute path to a recent Adobe Pepper Flash '
1086 'binary to be used in this bisection (e.g. '
1087 'on Windows C:\...\pepflashplayer.dll and on Linux '
1088 '/opt/google/chrome/PepperFlash/'
1089 'libpepflashplayer.so).')
[email protected]4df583c2014-07-31 17:11:551090 parser.add_option('-g', '--good',
1091 type='str',
1092 help='A good revision to start bisection. ' +
1093 'May be earlier or later than the bad revision. ' +
1094 'Default is 0.')
1095 parser.add_option('-p', '--profile', '--user-data-dir',
1096 type='str',
1097 default='profile',
1098 help='Profile to use; this will not reset every run. '
1099 'Defaults to a clean profile.')
1100 parser.add_option('-t', '--times',
1101 type='int',
1102 default=1,
1103 help='Number of times to run each build before asking '
1104 'if it\'s good or bad. Temporary profiles are reused.')
1105 parser.add_option('-c', '--command',
1106 type='str',
1107 default='%p %a',
1108 help='Command to execute. %p and %a refer to Chrome '
1109 'executable and specified extra arguments '
1110 'respectively. Use %s to specify all extra arguments '
1111 'as one string. Defaults to "%p %a". Note that any '
1112 'extra paths specified should be absolute.')
1113 parser.add_option('-l', '--blink',
1114 action='store_true',
1115 help='Use Blink bisect instead of Chromium. ')
1116 parser.add_option('', '--not-interactive',
1117 action='store_true',
1118 default=False,
1119 help='Use command exit code to tell good/bad revision.')
[email protected]011886692014-08-01 21:00:211120 parser.add_option('--asan',
1121 dest='asan',
1122 action='store_true',
1123 default=False,
1124 help='Allow the script to bisect ASAN builds')
rob724c9062015-01-22 00:26:421125 parser.add_option('--use-local-cache',
1126 dest='use_local_cache',
[email protected]6a7a5d62014-07-09 04:45:501127 action='store_true',
1128 default=False,
rob724c9062015-01-22 00:26:421129 help='Use a local file in the current directory to cache '
1130 'a list of known revisions to speed up the '
1131 'initialization of this script.')
skobes21b5cdfb2016-03-21 23:13:021132 parser.add_option('--verify-range',
1133 dest='verify_range',
1134 action='store_true',
1135 default=False,
1136 help='Test the first and last revisions in the range ' +
1137 'before proceeding with the bisect.')
elawrence446bcc32017-04-14 17:18:511138 parser.add_option("-r", action="callback", callback=error_internal_option)
1139 parser.add_option("-o", action="callback", callback=error_internal_option)
[email protected]b3b20512013-08-26 18:51:041140
[email protected]7ad66a72009-09-04 17:52:331141 (opts, args) = parser.parse_args()
1142
1143 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:381144 print 'Error: missing required parameter: --archive'
1145 print
[email protected]7ad66a72009-09-04 17:52:331146 parser.print_help()
1147 return 1
1148
[email protected]011886692014-08-01 21:00:211149 if opts.asan:
1150 supported_platforms = ['linux', 'mac', 'win']
1151 if opts.archive not in supported_platforms:
1152 print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
1153 '|'.join(supported_platforms))
1154 return 1
[email protected]011886692014-08-01 21:00:211155
1156 if opts.asan:
1157 base_url = ASAN_BASE_URL
1158 elif opts.blink:
[email protected]4c6fec6b2013-09-17 17:44:081159 base_url = WEBKIT_BASE_URL
1160 else:
1161 base_url = CHROMIUM_BASE_URL
1162
[email protected]183706d92011-06-10 13:06:221163 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]2e0f2672014-08-13 20:32:581164 context = PathContext(base_url, opts.archive, opts.good, opts.bad,
Jason Kersey97bb027a2016-05-11 20:10:431165 opts.asan, opts.use_local_cache,
vitalybuka4d1e1e412015-07-06 17:21:061166 opts.flash_path)
mikecasea8cd284c2014-12-02 21:30:581167
[email protected]67e0bc62009-09-03 22:06:091168 # Pick a starting point, try to get HEAD for this.
[email protected]2e0f2672014-08-13 20:32:581169 if not opts.bad:
1170 context.bad_revision = '999.0.0.0'
1171 context.bad_revision = GetChromiumRevision(
1172 context, context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:091173
1174 # Find out when we were good.
[email protected]2e0f2672014-08-13 20:32:581175 if not opts.good:
Jason Kersey97bb027a2016-05-11 20:10:431176 context.good_revision = 0
[email protected]801fb652012-07-20 20:13:501177
[email protected]fc3702e2013-11-09 04:23:001178 if opts.flash_path:
[email protected]2e0f2672014-08-13 20:32:581179 msg = 'Could not find Flash binary at %s' % opts.flash_path
1180 assert os.path.exists(opts.flash_path), msg
[email protected]fc3702e2013-11-09 04:23:001181
Jason Kersey97bb027a2016-05-11 20:10:431182 context.good_revision = int(context.good_revision)
1183 context.bad_revision = int(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501184
[email protected]5e93cf162012-01-28 02:16:561185 if opts.times < 1:
1186 print('Number of times to run (%d) must be greater than or equal to 1.' %
1187 opts.times)
1188 parser.print_help()
1189 return 1
1190
skobes21b5cdfb2016-03-21 23:13:021191 if opts.not_interactive:
1192 evaluator = DidCommandSucceed
1193 elif opts.asan:
[email protected]011886692014-08-01 21:00:211194 evaluator = IsGoodASANBuild
1195 else:
1196 evaluator = AskIsGoodBuild
1197
[email protected]2e0f2672014-08-13 20:32:581198 # Save these revision numbers to compare when showing the changelog URL
1199 # after the bisect.
1200 good_rev = context.good_revision
1201 bad_rev = context.bad_revision
1202
1203 (min_chromium_rev, max_chromium_rev, context) = Bisect(
1204 context, opts.times, opts.command, args, opts.profile,
skobes21b5cdfb2016-03-21 23:13:021205 evaluator, opts.verify_range)
[email protected]67e0bc62009-09-03 22:06:091206
[email protected]ff50d1c2013-04-17 18:49:361207 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:311208 try:
[email protected]4c6fec6b2013-09-17 17:44:081209 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1210 min_chromium_rev)
1211 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1212 max_chromium_rev)
[email protected]4df583c2014-07-31 17:11:551213 except Exception:
[email protected]b2fe7f22011-10-25 22:58:311214 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:361215 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:311216
[email protected]3bdaa4752013-09-30 20:13:361217 if opts.blink:
1218 # We're done. Let the user know the results in an official manner.
1219 if good_rev > bad_rev:
1220 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
1221 else:
1222 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
[email protected]eadd95d2012-11-02 22:42:091223
[email protected]ff50d1c2013-04-17 18:49:361224 print 'BLINK CHANGELOG URL:'
1225 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]3bdaa4752013-09-30 20:13:361226
[email protected]d0149c5c2012-05-29 21:12:111227 else:
[email protected]3bdaa4752013-09-30 20:13:361228 # We're done. Let the user know the results in an official manner.
1229 if good_rev > bad_rev:
1230 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
1231 str(max_chromium_rev))
1232 else:
1233 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
1234 str(max_chromium_rev))
1235 if min_blink_rev != max_blink_rev:
[email protected]4df583c2014-07-31 17:11:551236 print ('NOTE: There is a Blink roll in the range, '
1237 'you might also want to do a Blink bisect.')
[email protected]3bdaa4752013-09-30 20:13:361238
1239 print 'CHANGELOG URL:'
Jason Kersey97bb027a2016-05-11 20:10:431240 PrintChangeLog(min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:341241
[email protected]4df583c2014-07-31 17:11:551242
[email protected]67e0bc62009-09-03 22:06:091243if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:331244 sys.exit(main())