blob: 71875792ebf9b8c7ff63cab7e86d1b76055a4c47 [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."""
[email protected]79f14742010-03-10 01:01:57644 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09645 while True:
[email protected]4df583c2014-07-31 17:11:55646 response = raw_input('Revision %s is '
wangxianzhud8c4c562015-12-15 23:39:51647 '[(g)ood/(b)ad/(r)etry/(u)nknown/(s)tdout/(q)uit]: ' %
[email protected]53bb6342012-06-01 04:11:00648 str(rev))
wangxianzhud8c4c562015-12-15 23:39:51649 if response in ('g', 'b', 'r', 'u'):
[email protected]53bb6342012-06-01 04:11:00650 return response
wangxianzhud8c4c562015-12-15 23:39:51651 if response == 'q':
[email protected]afe30662011-07-30 01:05:52652 raise SystemExit()
wangxianzhud8c4c562015-12-15 23:39:51653 if response == 's':
654 print stdout
655 print stderr
[email protected]67e0bc62009-09-03 22:06:09656
[email protected]cb155a82011-11-29 17:25:34657
Jason Kersey97bb027a2016-05-11 20:10:43658def IsGoodASANBuild(rev, exit_status, stdout, stderr):
[email protected]011886692014-08-01 21:00:21659 """Determine if an ASAN build |rev| is good or bad
660
661 Will examine stderr looking for the error message emitted by ASAN. If not
662 found then will fallback to asking the user."""
663 if stderr:
664 bad_count = 0
665 for line in stderr.splitlines():
666 print line
667 if line.find('ERROR: AddressSanitizer:') != -1:
668 bad_count += 1
669 if bad_count > 0:
670 print 'Revision %d determined to be bad.' % rev
671 return 'b'
Jason Kersey97bb027a2016-05-11 20:10:43672 return AskIsGoodBuild(rev, exit_status, stdout, stderr)
skobes21b5cdfb2016-03-21 23:13:02673
674
Jason Kersey97bb027a2016-05-11 20:10:43675def DidCommandSucceed(rev, exit_status, stdout, stderr):
skobes21b5cdfb2016-03-21 23:13:02676 if exit_status:
677 print 'Bad revision: %s' % rev
678 return 'b'
679 else:
680 print 'Good revision: %s' % rev
681 return 'g'
682
[email protected]011886692014-08-01 21:00:21683
[email protected]53bb6342012-06-01 04:11:00684class DownloadJob(object):
685 """DownloadJob represents a task to download a given Chromium revision."""
[email protected]4df583c2014-07-31 17:11:55686
687 def __init__(self, context, name, rev, zip_file):
[email protected]53bb6342012-06-01 04:11:00688 super(DownloadJob, self).__init__()
689 # Store off the input parameters.
690 self.context = context
691 self.name = name
692 self.rev = rev
[email protected]4df583c2014-07-31 17:11:55693 self.zip_file = zip_file
[email protected]53bb6342012-06-01 04:11:00694 self.quit_event = threading.Event()
695 self.progress_event = threading.Event()
[email protected]4df583c2014-07-31 17:11:55696 self.thread = None
[email protected]53bb6342012-06-01 04:11:00697
698 def Start(self):
699 """Starts the download."""
700 fetchargs = (self.context,
701 self.rev,
[email protected]4df583c2014-07-31 17:11:55702 self.zip_file,
[email protected]53bb6342012-06-01 04:11:00703 self.quit_event,
704 self.progress_event)
705 self.thread = threading.Thread(target=FetchRevision,
706 name=self.name,
707 args=fetchargs)
708 self.thread.start()
709
710 def Stop(self):
711 """Stops the download which must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55712 assert self.thread, 'DownloadJob must be started before Stop is called.'
[email protected]53bb6342012-06-01 04:11:00713 self.quit_event.set()
714 self.thread.join()
[email protected]4df583c2014-07-31 17:11:55715 os.unlink(self.zip_file)
[email protected]53bb6342012-06-01 04:11:00716
717 def WaitFor(self):
718 """Prints a message and waits for the download to complete. The download
719 must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55720 assert self.thread, 'DownloadJob must be started before WaitFor is called.'
721 print 'Downloading revision %s...' % str(self.rev)
[email protected]53bb6342012-06-01 04:11:00722 self.progress_event.set() # Display progress of download.
rob8a4543f2016-01-20 00:43:59723 try:
724 while self.thread.isAlive():
725 # The parameter to join is needed to keep the main thread responsive to
726 # signals. Without it, the program will not respond to interruptions.
727 self.thread.join(1)
728 except (KeyboardInterrupt, SystemExit):
729 self.Stop()
730 raise
[email protected]53bb6342012-06-01 04:11:00731
732
skobes21b5cdfb2016-03-21 23:13:02733def VerifyEndpoint(fetch, context, rev, profile, num_runs, command, try_args,
734 evaluate, expected_answer):
735 fetch.WaitFor()
736 try:
737 (exit_status, stdout, stderr) = RunRevision(
738 context, rev, fetch.zip_file, profile, num_runs, command, try_args)
739 except Exception, e:
740 print >> sys.stderr, e
Lei Zhang2fa76302018-11-09 20:16:31741 raise SystemExit
Jason Kersey97bb027a2016-05-11 20:10:43742 if (evaluate(rev, exit_status, stdout, stderr) != expected_answer):
skobes21b5cdfb2016-03-21 23:13:02743 print 'Unexpected result at a range boundary! Your range is not correct.'
744 raise SystemExit
745
746
[email protected]2e0f2672014-08-13 20:32:58747def Bisect(context,
[email protected]5e93cf162012-01-28 02:16:56748 num_runs=1,
[email protected]4df583c2014-07-31 17:11:55749 command='%p %a',
[email protected]60ac66e32011-07-18 16:08:25750 try_args=(),
[email protected]afe30662011-07-30 01:05:52751 profile=None,
skobes21b5cdfb2016-03-21 23:13:02752 evaluate=AskIsGoodBuild,
753 verify_range=False):
[email protected]afe30662011-07-30 01:05:52754 """Given known good and known bad revisions, run a binary search on all
755 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25756
[email protected]2e0f2672014-08-13 20:32:58757 @param context PathContext object initialized with user provided parameters.
[email protected]5e93cf162012-01-28 02:16:56758 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52759 @param try_args A tuple of arguments to pass to the test application.
760 @param profile The name of the user profile to run with.
[email protected]53bb6342012-06-01 04:11:00761 @param evaluate A function which returns 'g' if the argument build is good,
762 'b' if it's bad or 'u' if unknown.
skobes21b5cdfb2016-03-21 23:13:02763 @param verify_range If true, tests the first and last revisions in the range
764 before proceeding with the bisect.
[email protected]afe30662011-07-30 01:05:52765
766 Threading is used to fetch Chromium revisions in the background, speeding up
767 the user's experience. For example, suppose the bounds of the search are
768 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
769 whether revision 50 is good or bad, the next revision to check will be either
770 25 or 75. So, while revision 50 is being checked, the script will download
771 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
772 known:
773
774 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
775 is run on rev 75.
776
777 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
778 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25779 """
780
[email protected]afe30662011-07-30 01:05:52781 if not profile:
782 profile = 'profile'
783
[email protected]2e0f2672014-08-13 20:32:58784 good_rev = context.good_revision
785 bad_rev = context.bad_revision
[email protected]afe30662011-07-30 01:05:52786 cwd = os.getcwd()
787
[email protected]28a3c122014-08-09 11:04:51788 print 'Downloading list of known revisions...',
Jason Kersey97bb027a2016-05-11 20:10:43789 if not context.use_local_cache:
rob724c9062015-01-22 00:26:42790 print '(use --use-local-cache to cache and re-use the list of revisions)'
[email protected]28a3c122014-08-09 11:04:51791 else:
792 print
[email protected]d0149c5c2012-05-29 21:12:11793 _GetDownloadPath = lambda rev: os.path.join(cwd,
794 '%s-%s' % (str(rev), context.archive_name))
Jason Kersey97bb027a2016-05-11 20:10:43795 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52796
797 # Get a list of revisions to bisect across.
798 if len(revlist) < 2: # Don't have enough builds to bisect.
799 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
800 raise RuntimeError(msg)
801
802 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09803 minrev = 0
804 maxrev = len(revlist) - 1
805 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52806 rev = revlist[pivot]
skobes21b5cdfb2016-03-21 23:13:02807 fetch = DownloadJob(context, 'initial_fetch', rev, _GetDownloadPath(rev))
[email protected]eadd95d2012-11-02 22:42:09808 fetch.Start()
skobes21b5cdfb2016-03-21 23:13:02809
810 if verify_range:
811 minrev_fetch = DownloadJob(
812 context, 'minrev_fetch', revlist[minrev],
813 _GetDownloadPath(revlist[minrev]))
814 maxrev_fetch = DownloadJob(
815 context, 'maxrev_fetch', revlist[maxrev],
816 _GetDownloadPath(revlist[maxrev]))
817 minrev_fetch.Start()
818 maxrev_fetch.Start()
819 try:
820 VerifyEndpoint(minrev_fetch, context, revlist[minrev], profile, num_runs,
821 command, try_args, evaluate, 'b' if bad_rev < good_rev else 'g')
822 VerifyEndpoint(maxrev_fetch, context, revlist[maxrev], profile, num_runs,
823 command, try_args, evaluate, 'g' if bad_rev < good_rev else 'b')
824 except (KeyboardInterrupt, SystemExit):
825 print 'Cleaning up...'
826 fetch.Stop()
827 sys.exit(0)
828 finally:
829 minrev_fetch.Stop()
830 maxrev_fetch.Stop()
831
[email protected]eadd95d2012-11-02 22:42:09832 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25833
834 # Binary search time!
[email protected]4df583c2014-07-31 17:11:55835 while fetch and fetch.zip_file and maxrev - minrev > 1:
[email protected]eadd95d2012-11-02 22:42:09836 if bad_rev < good_rev:
[email protected]4df583c2014-07-31 17:11:55837 min_str, max_str = 'bad', 'good'
[email protected]eadd95d2012-11-02 22:42:09838 else:
[email protected]4df583c2014-07-31 17:11:55839 min_str, max_str = 'good', 'bad'
zinovy.nis0951bca2017-04-12 19:23:54840 print ('Bisecting range [%s (%s), %s (%s)], '
841 'roughly %d steps left.') % (revlist[minrev], min_str,
842 revlist[maxrev], max_str,
843 int(maxrev - minrev)
844 .bit_length())
[email protected]eadd95d2012-11-02 22:42:09845
[email protected]afe30662011-07-30 01:05:52846 # Pre-fetch next two possible pivots
847 # - down_pivot is the next revision to check if the current revision turns
848 # out to be bad.
849 # - up_pivot is the next revision to check if the current revision turns
850 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09851 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00852 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09853 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52854 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00855 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
856 _GetDownloadPath(down_rev))
857 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25858
[email protected]eadd95d2012-11-02 22:42:09859 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00860 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09861 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52862 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00863 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
864 _GetDownloadPath(up_rev))
865 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25866
[email protected]afe30662011-07-30 01:05:52867 # Run test on the pivot revision.
skobes21b5cdfb2016-03-21 23:13:02868 exit_status = None
[email protected]e29c08c2012-09-17 20:50:50869 stdout = None
870 stderr = None
871 try:
skobes21b5cdfb2016-03-21 23:13:02872 (exit_status, stdout, stderr) = RunRevision(
873 context, rev, fetch.zip_file, profile, num_runs, command, try_args)
[email protected]e29c08c2012-09-17 20:50:50874 except Exception, e:
[email protected]fc3702e2013-11-09 04:23:00875 print >> sys.stderr, e
[email protected]60ac66e32011-07-18 16:08:25876
[email protected]53bb6342012-06-01 04:11:00877 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52878 # On that basis, kill one of the background downloads and complete the
879 # other, as described in the comments above.
880 try:
Jason Kersey97bb027a2016-05-11 20:10:43881 answer = evaluate(rev, exit_status, stdout, stderr)
[email protected]4df583c2014-07-31 17:11:55882 if ((answer == 'g' and good_rev < bad_rev)
883 or (answer == 'b' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12884 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09885 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00886 if down_fetch:
887 down_fetch.Stop() # Kill the download of the older revision.
[email protected]1d4a06242013-08-20 22:53:12888 fetch = None
[email protected]53bb6342012-06-01 04:11:00889 if up_fetch:
890 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52891 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09892 fetch = up_fetch
[email protected]4df583c2014-07-31 17:11:55893 elif ((answer == 'b' and good_rev < bad_rev)
894 or (answer == 'g' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12895 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09896 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00897 if up_fetch:
898 up_fetch.Stop() # Kill the download of the newer revision.
[email protected]1d4a06242013-08-20 22:53:12899 fetch = None
[email protected]53bb6342012-06-01 04:11:00900 if down_fetch:
901 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52902 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09903 fetch = down_fetch
[email protected]1d4a06242013-08-20 22:53:12904 elif answer == 'r':
905 pass # Retry requires no changes.
[email protected]53bb6342012-06-01 04:11:00906 elif answer == 'u':
907 # Nuke the revision from the revlist and choose a new pivot.
[email protected]1d4a06242013-08-20 22:53:12908 fetch.Stop()
[email protected]53bb6342012-06-01 04:11:00909 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09910 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00911
[email protected]eadd95d2012-11-02 22:42:09912 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00913 # Alternate between using down_pivot or up_pivot for the new pivot
914 # point, without affecting the range. Do this instead of setting the
915 # pivot to the midpoint of the new range because adjacent revisions
916 # are likely affected by the same issue that caused the (u)nknown
917 # response.
918 if up_fetch and down_fetch:
919 fetch = [up_fetch, down_fetch][len(revlist) % 2]
920 elif up_fetch:
921 fetch = up_fetch
922 else:
923 fetch = down_fetch
924 fetch.WaitFor()
925 if fetch == up_fetch:
926 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
927 else:
928 pivot = down_pivot
[email protected]53bb6342012-06-01 04:11:00929
930 if down_fetch and fetch != down_fetch:
931 down_fetch.Stop()
932 if up_fetch and fetch != up_fetch:
933 up_fetch.Stop()
934 else:
[email protected]4df583c2014-07-31 17:11:55935 assert False, 'Unexpected return value from evaluate(): ' + answer
skobes21b5cdfb2016-03-21 23:13:02936 except (KeyboardInterrupt, SystemExit):
[email protected]4df583c2014-07-31 17:11:55937 print 'Cleaning up...'
skobes21b5cdfb2016-03-21 23:13:02938 for f in [_GetDownloadPath(rev),
939 _GetDownloadPath(revlist[down_pivot]),
[email protected]5e93cf162012-01-28 02:16:56940 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52941 try:
942 os.unlink(f)
943 except OSError:
944 pass
945 sys.exit(0)
946
947 rev = revlist[pivot]
948
[email protected]2e0f2672014-08-13 20:32:58949 return (revlist[minrev], revlist[maxrev], context)
[email protected]60ac66e32011-07-18 16:08:25950
951
pshenoycd6bd682014-09-10 20:50:22952def GetBlinkDEPSRevisionForChromiumRevision(self, rev):
[email protected]4c6fec6b2013-09-17 17:44:08953 """Returns the blink revision that was in REVISIONS file at
[email protected]b2fe7f22011-10-25 22:58:31954 chromium revision |rev|."""
pshenoycd6bd682014-09-10 20:50:22955
956 def _GetBlinkRev(url, blink_re):
957 m = blink_re.search(url.read())
958 url.close()
959 if m:
fmalitaa898d222016-07-12 22:29:03960 return m.group(1)
pshenoycd6bd682014-09-10 20:50:22961
Di Mu08c59682016-07-11 23:05:07962 url = urllib.urlopen(DEPS_FILE % GetGitHashFromSVNRevision(rev))
pshenoycd6bd682014-09-10 20:50:22963 if url.getcode() == 200:
Di Mu08c59682016-07-11 23:05:07964 blink_re = re.compile(r'webkit_revision\D*\d+;\D*\d+;(\w+)')
965 blink_git_sha = _GetBlinkRev(url, blink_re)
966 return self.GetSVNRevisionFromGitHash(blink_git_sha, 'blink')
pshenoycd6bd682014-09-10 20:50:22967 raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
[email protected]37ed3172013-09-24 23:49:30968
969
[email protected]2e0f2672014-08-13 20:32:58970def GetBlinkRevisionForChromiumRevision(context, rev):
[email protected]37ed3172013-09-24 23:49:30971 """Returns the blink revision that was in REVISIONS file at
972 chromium revision |rev|."""
[email protected]3e7c85322014-06-27 20:27:36973 def _IsRevisionNumber(revision):
974 if isinstance(revision, int):
975 return True
976 else:
977 return revision.isdigit()
[email protected]2e0f2672014-08-13 20:32:58978 if str(rev) in context.githash_svn_dict:
979 rev = context.githash_svn_dict[str(rev)]
980 file_url = '%s/%s%s/REVISIONS' % (context.base_url,
981 context._listing_platform_dir, rev)
[email protected]4c6fec6b2013-09-17 17:44:08982 url = urllib.urlopen(file_url)
[email protected]2e0f2672014-08-13 20:32:58983 if url.getcode() == 200:
984 try:
985 data = json.loads(url.read())
986 except ValueError:
987 print 'ValueError for JSON URL: %s' % file_url
988 raise ValueError
989 else:
990 raise ValueError
[email protected]b2fe7f22011-10-25 22:58:31991 url.close()
[email protected]4c6fec6b2013-09-17 17:44:08992 if 'webkit_revision' in data:
[email protected]3e7c85322014-06-27 20:27:36993 blink_rev = data['webkit_revision']
994 if not _IsRevisionNumber(blink_rev):
[email protected]2e0f2672014-08-13 20:32:58995 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
[email protected]3e7c85322014-06-27 20:27:36996 return blink_rev
[email protected]b2fe7f22011-10-25 22:58:31997 else:
[email protected]ff50d1c2013-04-17 18:49:36998 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:31999
[email protected]4df583c2014-07-31 17:11:551000
[email protected]37ed3172013-09-24 23:49:301001def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
1002 """Returns the chromium revision that has the correct blink revision
1003 for blink bisect, DEPS and REVISIONS file might not match since
1004 blink snapshots point to tip of tree blink.
1005 Note: The revisions_final variable might get modified to include
1006 additional revisions."""
pshenoycd6bd682014-09-10 20:50:221007 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(self, rev)
[email protected]37ed3172013-09-24 23:49:301008
1009 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
1010 idx = revisions.index(rev)
1011 if idx > 0:
1012 rev = revisions[idx-1]
1013 if rev not in revisions_final:
1014 revisions_final.insert(0, rev)
1015
1016 revisions_final.sort()
1017 return rev
[email protected]b2fe7f22011-10-25 22:58:311018
[email protected]4df583c2014-07-31 17:11:551019
[email protected]5980b752014-07-02 00:34:401020def GetChromiumRevision(context, url):
[email protected]801fb652012-07-20 20:13:501021 """Returns the chromium revision read from given URL."""
1022 try:
1023 # Location of the latest build revision number
[email protected]5980b752014-07-02 00:34:401024 latest_revision = urllib.urlopen(url).read()
1025 if latest_revision.isdigit():
1026 return int(latest_revision)
1027 return context.GetSVNRevisionFromGitHash(latest_revision)
[email protected]4df583c2014-07-31 17:11:551028 except Exception:
1029 print 'Could not determine latest revision. This could be bad...'
[email protected]801fb652012-07-20 20:13:501030 return 999999999
1031
pshenoycd6bd682014-09-10 20:50:221032def GetGitHashFromSVNRevision(svn_revision):
1033 crrev_url = CRREV_URL + str(svn_revision)
1034 url = urllib.urlopen(crrev_url)
1035 if url.getcode() == 200:
1036 data = json.loads(url.read())
1037 if 'git_sha' in data:
1038 return data['git_sha']
1039
pshenoy9ce271f2014-09-02 22:14:051040def PrintChangeLog(min_chromium_rev, max_chromium_rev):
1041 """Prints the changelog URL."""
1042
pshenoycd6bd682014-09-10 20:50:221043 print (' ' + CHANGELOG_URL % (GetGitHashFromSVNRevision(min_chromium_rev),
1044 GetGitHashFromSVNRevision(max_chromium_rev)))
pshenoy9ce271f2014-09-02 22:14:051045
elawrence446bcc32017-04-14 17:18:511046def error_internal_option(option, opt, value, parser):
1047 raise optparse.OptionValueError(
1048 'The -o and -r options are only\navailable in the internal version of '
1049 'this script. Google\nemployees should visit http://go/bisect-builds '
1050 'for\nconfiguration instructions.')
[email protected]801fb652012-07-20 20:13:501051
[email protected]67e0bc62009-09-03 22:06:091052def main():
[email protected]2c1d2732009-10-29 19:52:171053 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:311054 'Perform binary search on the snapshot builds to find a minimal\n'
1055 'range of revisions where a behavior change happened. The\n'
1056 'behaviors are described as "good" and "bad".\n'
1057 'It is NOT assumed that the behavior of the later revision is\n'
[email protected]09c58da2013-01-07 21:30:171058 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:381059 '\n'
[email protected]887c9182013-02-12 20:30:311060 'Revision numbers should use\n'
[email protected]887c9182013-02-12 20:30:311061 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
1062 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
1063 ' for earlier revs.\n'
1064 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
1065 ' are incorrect, they are from branches.\n'
1066 '\n'
[email protected]178aab72010-10-08 17:21:381067 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:331068 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:571069 # Strangely, the default help output doesn't include the choice list.
mikecasea8cd284c2014-12-02 21:30:581070 choices = ['mac', 'mac64', 'win', 'win64', 'linux', 'linux64', 'linux-arm',
dmazzoni76e907d2015-01-22 08:14:491071 'chromeos']
[email protected]7ad66a72009-09-04 17:52:331072 parser.add_option('-a', '--archive',
[email protected]4df583c2014-07-31 17:11:551073 choices=choices,
1074 help='The buildbot archive to bisect [%s].' %
1075 '|'.join(choices))
[email protected]4df583c2014-07-31 17:11:551076 parser.add_option('-b', '--bad',
1077 type='str',
1078 help='A bad revision to start bisection. '
1079 'May be earlier or later than the good revision. '
1080 'Default is HEAD.')
1081 parser.add_option('-f', '--flash_path',
1082 type='str',
1083 help='Absolute path to a recent Adobe Pepper Flash '
1084 'binary to be used in this bisection (e.g. '
1085 'on Windows C:\...\pepflashplayer.dll and on Linux '
1086 '/opt/google/chrome/PepperFlash/'
1087 'libpepflashplayer.so).')
[email protected]4df583c2014-07-31 17:11:551088 parser.add_option('-g', '--good',
1089 type='str',
1090 help='A good revision to start bisection. ' +
1091 'May be earlier or later than the bad revision. ' +
1092 'Default is 0.')
1093 parser.add_option('-p', '--profile', '--user-data-dir',
1094 type='str',
1095 default='profile',
1096 help='Profile to use; this will not reset every run. '
1097 'Defaults to a clean profile.')
1098 parser.add_option('-t', '--times',
1099 type='int',
1100 default=1,
1101 help='Number of times to run each build before asking '
1102 'if it\'s good or bad. Temporary profiles are reused.')
1103 parser.add_option('-c', '--command',
1104 type='str',
1105 default='%p %a',
1106 help='Command to execute. %p and %a refer to Chrome '
1107 'executable and specified extra arguments '
1108 'respectively. Use %s to specify all extra arguments '
1109 'as one string. Defaults to "%p %a". Note that any '
1110 'extra paths specified should be absolute.')
1111 parser.add_option('-l', '--blink',
1112 action='store_true',
1113 help='Use Blink bisect instead of Chromium. ')
1114 parser.add_option('', '--not-interactive',
1115 action='store_true',
1116 default=False,
1117 help='Use command exit code to tell good/bad revision.')
[email protected]011886692014-08-01 21:00:211118 parser.add_option('--asan',
1119 dest='asan',
1120 action='store_true',
1121 default=False,
1122 help='Allow the script to bisect ASAN builds')
rob724c9062015-01-22 00:26:421123 parser.add_option('--use-local-cache',
1124 dest='use_local_cache',
[email protected]6a7a5d62014-07-09 04:45:501125 action='store_true',
1126 default=False,
rob724c9062015-01-22 00:26:421127 help='Use a local file in the current directory to cache '
1128 'a list of known revisions to speed up the '
1129 'initialization of this script.')
skobes21b5cdfb2016-03-21 23:13:021130 parser.add_option('--verify-range',
1131 dest='verify_range',
1132 action='store_true',
1133 default=False,
1134 help='Test the first and last revisions in the range ' +
1135 'before proceeding with the bisect.')
elawrence446bcc32017-04-14 17:18:511136 parser.add_option("-r", action="callback", callback=error_internal_option)
1137 parser.add_option("-o", action="callback", callback=error_internal_option)
[email protected]b3b20512013-08-26 18:51:041138
[email protected]7ad66a72009-09-04 17:52:331139 (opts, args) = parser.parse_args()
1140
1141 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:381142 print 'Error: missing required parameter: --archive'
1143 print
[email protected]7ad66a72009-09-04 17:52:331144 parser.print_help()
1145 return 1
1146
[email protected]011886692014-08-01 21:00:211147 if opts.asan:
1148 supported_platforms = ['linux', 'mac', 'win']
1149 if opts.archive not in supported_platforms:
1150 print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
1151 '|'.join(supported_platforms))
1152 return 1
[email protected]011886692014-08-01 21:00:211153
1154 if opts.asan:
1155 base_url = ASAN_BASE_URL
1156 elif opts.blink:
[email protected]4c6fec6b2013-09-17 17:44:081157 base_url = WEBKIT_BASE_URL
1158 else:
1159 base_url = CHROMIUM_BASE_URL
1160
[email protected]183706d92011-06-10 13:06:221161 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]2e0f2672014-08-13 20:32:581162 context = PathContext(base_url, opts.archive, opts.good, opts.bad,
Jason Kersey97bb027a2016-05-11 20:10:431163 opts.asan, opts.use_local_cache,
vitalybuka4d1e1e412015-07-06 17:21:061164 opts.flash_path)
mikecasea8cd284c2014-12-02 21:30:581165
[email protected]67e0bc62009-09-03 22:06:091166 # Pick a starting point, try to get HEAD for this.
[email protected]2e0f2672014-08-13 20:32:581167 if not opts.bad:
1168 context.bad_revision = '999.0.0.0'
1169 context.bad_revision = GetChromiumRevision(
1170 context, context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:091171
1172 # Find out when we were good.
[email protected]2e0f2672014-08-13 20:32:581173 if not opts.good:
Jason Kersey97bb027a2016-05-11 20:10:431174 context.good_revision = 0
[email protected]801fb652012-07-20 20:13:501175
[email protected]fc3702e2013-11-09 04:23:001176 if opts.flash_path:
[email protected]2e0f2672014-08-13 20:32:581177 msg = 'Could not find Flash binary at %s' % opts.flash_path
1178 assert os.path.exists(opts.flash_path), msg
[email protected]fc3702e2013-11-09 04:23:001179
Jason Kersey97bb027a2016-05-11 20:10:431180 context.good_revision = int(context.good_revision)
1181 context.bad_revision = int(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501182
[email protected]5e93cf162012-01-28 02:16:561183 if opts.times < 1:
1184 print('Number of times to run (%d) must be greater than or equal to 1.' %
1185 opts.times)
1186 parser.print_help()
1187 return 1
1188
skobes21b5cdfb2016-03-21 23:13:021189 if opts.not_interactive:
1190 evaluator = DidCommandSucceed
1191 elif opts.asan:
[email protected]011886692014-08-01 21:00:211192 evaluator = IsGoodASANBuild
1193 else:
1194 evaluator = AskIsGoodBuild
1195
[email protected]2e0f2672014-08-13 20:32:581196 # Save these revision numbers to compare when showing the changelog URL
1197 # after the bisect.
1198 good_rev = context.good_revision
1199 bad_rev = context.bad_revision
1200
1201 (min_chromium_rev, max_chromium_rev, context) = Bisect(
1202 context, opts.times, opts.command, args, opts.profile,
skobes21b5cdfb2016-03-21 23:13:021203 evaluator, opts.verify_range)
[email protected]67e0bc62009-09-03 22:06:091204
[email protected]ff50d1c2013-04-17 18:49:361205 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:311206 try:
[email protected]4c6fec6b2013-09-17 17:44:081207 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1208 min_chromium_rev)
1209 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1210 max_chromium_rev)
[email protected]4df583c2014-07-31 17:11:551211 except Exception:
[email protected]b2fe7f22011-10-25 22:58:311212 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:361213 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:311214
[email protected]3bdaa4752013-09-30 20:13:361215 if opts.blink:
1216 # We're done. Let the user know the results in an official manner.
1217 if good_rev > bad_rev:
1218 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
1219 else:
1220 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
[email protected]eadd95d2012-11-02 22:42:091221
[email protected]ff50d1c2013-04-17 18:49:361222 print 'BLINK CHANGELOG URL:'
1223 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]3bdaa4752013-09-30 20:13:361224
[email protected]d0149c5c2012-05-29 21:12:111225 else:
[email protected]3bdaa4752013-09-30 20:13:361226 # We're done. Let the user know the results in an official manner.
1227 if good_rev > bad_rev:
1228 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
1229 str(max_chromium_rev))
1230 else:
1231 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
1232 str(max_chromium_rev))
1233 if min_blink_rev != max_blink_rev:
[email protected]4df583c2014-07-31 17:11:551234 print ('NOTE: There is a Blink roll in the range, '
1235 'you might also want to do a Blink bisect.')
[email protected]3bdaa4752013-09-30 20:13:361236
1237 print 'CHANGELOG URL:'
Jason Kersey97bb027a2016-05-11 20:10:431238 PrintChangeLog(min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:341239
[email protected]4df583c2014-07-31 17:11:551240
[email protected]67e0bc62009-09-03 22:06:091241if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:331242 sys.exit(main())