blob: 84807bccbfaea3180e59cf5fb01a332e64810838 [file] [log] [blame]
evan@chromium.org5684aaf2010-10-09 02:21:38 +09001#!/usr/bin/python
maruel@chromium.org07e92d52010-05-26 20:31:26 +09002# Copyright (c) 2010 The Chromium Authors. All rights reserved.
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +09003# 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
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +09008This script bisects a snapshot archive using binary search. It starts at
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +09009a 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.
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090013"""
14
15# Base URL to download snapshots from.
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +090016BUILD_BASE_URL = 'http://build.chromium.org/buildbot/snapshots/'
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090017
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +090018# The type (platform) of the build archive. This is what's passed in to the
19# '-a/--archive' option.
20BUILD_ARCHIVE_TYPE = ''
21
22# The selected archive to bisect.
23BUILD_ARCHIVE_DIR = ''
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090024
25# The location of the builds.
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +090026BUILD_ARCHIVE_URL = '/%d/'
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090027
28# Name of the build archive.
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +090029BUILD_ZIP_NAME = ''
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090030
31# Directory name inside the archive.
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +090032BUILD_DIR_NAME = ''
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090033
34# Name of the executable.
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +090035BUILD_EXE_NAME = ''
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090036
37# URL to the ViewVC commit page.
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +090038BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d'
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090039
nirnimesh@chromium.orgc2fedbd2009-10-09 04:55:38 +090040# Changelogs URL
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +090041CHANGELOG_URL = 'http://build.chromium.org/buildbot/' \
42 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d'
nirnimesh@chromium.orgc2fedbd2009-10-09 04:55:38 +090043
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090044###############################################################################
45
46import math
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +090047import optparse
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090048import os
rsesek@chromium.orge92f0f22009-09-20 09:56:38 +090049import pipes
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090050import re
51import shutil
52import sys
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +090053import tempfile
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090054import urllib
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +090055import zipfile
56
57
58def UnzipFilenameToDir(filename, dir):
59 """Unzip |filename| to directory |dir|."""
60 zf = zipfile.ZipFile(filename)
61 # Make base.
62 pushd = os.getcwd()
63 try:
64 if not os.path.isdir(dir):
65 os.mkdir(dir)
66 os.chdir(dir)
67 # Extract files.
68 for info in zf.infolist():
69 name = info.filename
70 if name.endswith('/'): # dir
71 if not os.path.isdir(name):
72 os.makedirs(name)
73 else: # file
74 dir = os.path.dirname(name)
75 if not os.path.isdir(dir):
76 os.makedirs(dir)
77 out = open(name, 'wb')
78 out.write(zf.read(name))
79 out.close()
80 # Set permissions. Permission info in external_attr is shifted 16 bits.
81 os.chmod(name, info.external_attr >> 16L)
82 os.chdir(pushd)
83 except Exception, e:
84 print >>sys.stderr, e
85 sys.exit(1)
86
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +090087
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +090088def SetArchiveVars(archive):
89 """Set a bunch of global variables appropriate for the specified archive."""
90 global BUILD_ARCHIVE_TYPE
91 global BUILD_ARCHIVE_DIR
92 global BUILD_ZIP_NAME
93 global BUILD_DIR_NAME
94 global BUILD_EXE_NAME
95 global BUILD_BASE_URL
96
97 BUILD_ARCHIVE_TYPE = archive
98 BUILD_ARCHIVE_DIR = 'chromium-rel-' + BUILD_ARCHIVE_TYPE
99
sky@chromium.org8b1461b2010-09-09 23:14:20 +0900100 if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64', 'linux-chromiumos'):
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900101 BUILD_ZIP_NAME = 'chrome-linux.zip'
102 BUILD_DIR_NAME = 'chrome-linux'
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900103 BUILD_EXE_NAME = 'chrome'
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900104 elif BUILD_ARCHIVE_TYPE in ('mac'):
105 BUILD_ZIP_NAME = 'chrome-mac.zip'
106 BUILD_DIR_NAME = 'chrome-mac'
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900107 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium'
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900108 elif BUILD_ARCHIVE_TYPE in ('xp'):
109 BUILD_ZIP_NAME = 'chrome-win32.zip'
110 BUILD_DIR_NAME = 'chrome-win32'
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900111 BUILD_EXE_NAME = 'chrome.exe'
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900112
113 BUILD_BASE_URL += BUILD_ARCHIVE_DIR
114
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900115def ParseDirectoryIndex(url):
116 """Parses the HTML directory listing into a list of revision numbers."""
117 handle = urllib.urlopen(url)
118 dirindex = handle.read()
119 handle.close()
120 return re.findall(r'<a href="([0-9]*)/">\1/</a>', dirindex)
121
122def GetRevList(good, bad):
123 """Gets the list of revision numbers between |good| and |bad|."""
124 # Download the main revlist.
125 revlist = ParseDirectoryIndex(BUILD_BASE_URL)
126 revlist = map(int, revlist)
127 revlist = filter(lambda r: range(good, bad).__contains__(int(r)), revlist)
128 revlist.sort()
129 return revlist
130
nirnimesh@chromium.orgc2fedbd2009-10-09 04:55:38 +0900131def TryRevision(rev, profile, args):
rsesek@chromium.orge92f0f22009-09-20 09:56:38 +0900132 """Downloads revision |rev|, unzips it, and opens it for the user to test.
133 |profile| is the profile to use."""
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900134 # Do this in a temp dir so we don't collide with user files.
135 cwd = os.getcwd()
136 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
137 os.chdir(tempdir)
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900138
139 # Download the file.
140 download_url = BUILD_BASE_URL + (BUILD_ARCHIVE_URL % rev) + BUILD_ZIP_NAME
141 try:
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900142 print 'Fetching ' + download_url
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900143 urllib.urlretrieve(download_url, BUILD_ZIP_NAME)
144 except Exception, e:
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900145 print('Could not retrieve the download. Sorry.')
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900146 sys.exit(-1)
147
148 # Unzip the file.
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900149 print 'Unziping ...'
150 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir)
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900151
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900152 # Tell the system to open the app.
nirnimesh@chromium.orgc2fedbd2009-10-09 04:55:38 +0900153 args = ['--user-data-dir=%s' % profile] + args
154 flags = ' '.join(map(pipes.quote, args))
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900155 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME)
156 cmd = '%s %s' % (exe, flags)
157 print 'Running %s' % cmd
158 os.system(cmd)
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900159
160 os.chdir(cwd)
161 print 'Cleaning temp dir ...'
162 try:
163 shutil.rmtree(tempdir, True)
164 except Exception, e:
165 pass
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900166
evan@chromium.org6d583472010-03-10 10:01:57 +0900167
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900168def AskIsGoodBuild(rev):
evan@chromium.org6d583472010-03-10 10:01:57 +0900169 """Ask the user whether build |rev| is good or bad."""
170 # Loop until we get a response that we can parse.
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900171 while True:
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900172 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev))
173 if response and response in ('g', 'b'):
174 return response == 'g'
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900175
176def main():
nirnimesh@chromium.org670699b2009-10-30 04:52:17 +0900177 usage = ('%prog [options] [-- chromium-options]\n'
evan@chromium.org5684aaf2010-10-09 02:21:38 +0900178 'Perform binary search on the snapshot builds.\n'
179 '\n'
180 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900181 parser = optparse.OptionParser(usage=usage)
evan@chromium.orgcfce4c92009-09-19 10:58:57 +0900182 # Strangely, the default help output doesn't include the choice list.
sky@chromium.org8b1461b2010-09-09 23:14:20 +0900183 choices = ['mac', 'xp', 'linux', 'linux-64', 'linux-chromiumos']
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900184 parser.add_option('-a', '--archive',
evan@chromium.orgcfce4c92009-09-19 10:58:57 +0900185 choices = choices,
186 help = 'The buildbot archive to bisect [%s].' %
187 '|'.join(choices))
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900188 parser.add_option('-b', '--bad', type = 'int',
189 help = 'The bad revision to bisect to.')
190 parser.add_option('-g', '--good', type = 'int',
191 help = 'The last known good revision to bisect from.')
rsesek@chromium.orge92f0f22009-09-20 09:56:38 +0900192 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
193 help = 'Profile to use; this will not reset every run. ' +
194 'Defaults to a clean profile.')
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900195 (opts, args) = parser.parse_args()
196
197 if opts.archive is None:
evan@chromium.org5684aaf2010-10-09 02:21:38 +0900198 print 'Error: missing required parameter: --archive'
199 print
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900200 parser.print_help()
201 return 1
202
203 if opts.bad and opts.good and (opts.good > opts.bad):
204 print ('The good revision (%d) must precede the bad revision (%d).\n' %
205 (opts.good, opts.bad))
206 parser.print_help()
207 return 1
208
209 SetArchiveVars(opts.archive)
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900210
211 # Pick a starting point, try to get HEAD for this.
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900212 if opts.bad:
213 bad_rev = opts.bad
214 else:
215 bad_rev = 0
216 try:
217 # Location of the latest build revision number
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900218 BUILD_LATEST_URL = '%s/LATEST' % (BUILD_BASE_URL)
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900219 nh = urllib.urlopen(BUILD_LATEST_URL)
220 latest = int(nh.read())
221 nh.close()
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900222 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest)
223 if (bad_rev == ''):
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900224 bad_rev = latest
225 bad_rev = int(bad_rev)
226 except Exception, e:
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900227 print('Could not determine latest revision. This could be bad...')
228 bad_rev = int(raw_input('Bad revision: '))
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900229
230 # Find out when we were good.
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900231 if opts.good:
232 good_rev = opts.good
233 else:
234 good_rev = 0
235 try:
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900236 good_rev = int(raw_input('Last known good [0]: '))
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900237 except Exception, e:
238 pass
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900239
240 # Get a list of revisions to bisect across.
241 revlist = GetRevList(good_rev, bad_rev)
nirnimesh@chromium.org670699b2009-10-30 04:52:17 +0900242 if len(revlist) < 2: # Don't have enough builds to bisect
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900243 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist
nirnimesh@chromium.org670699b2009-10-30 04:52:17 +0900244 sys.exit(1)
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900245
246 # If we don't have a |good_rev|, set it to be the first revision possible.
247 if good_rev == 0:
248 good_rev = revlist[0]
249
250 # These are indexes of |revlist|.
251 good = 0
252 bad = len(revlist) - 1
nirnimesh@chromium.orgc2fedbd2009-10-09 04:55:38 +0900253 last_known_good_rev = revlist[good]
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900254
255 # Binary search time!
256 while good < bad:
257 candidates = revlist[good:bad]
258 num_poss = len(candidates)
259 if num_poss > 10:
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900260 print('%d candidates. %d tries left.' %
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900261 (num_poss, round(math.log(num_poss, 2))))
262 else:
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900263 print('Candidates: %s' % revlist[good:bad])
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900264
265 # Cut the problem in half...
266 test = int((bad - good) / 2) + good
267 test_rev = revlist[test]
268
rsesek@chromium.orge92f0f22009-09-20 09:56:38 +0900269 # Let the user give this rev a spin (in her own profile, if she wants).
270 profile = opts.profile
271 if not profile:
272 profile = 'profile' # In a temp dir.
nirnimesh@chromium.orgc2fedbd2009-10-09 04:55:38 +0900273 TryRevision(test_rev, profile, args)
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900274 if AskIsGoodBuild(test_rev):
nirnimesh@chromium.orgc2fedbd2009-10-09 04:55:38 +0900275 last_known_good_rev = revlist[good]
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900276 good = test + 1
277 else:
278 bad = test
279
280 # We're done. Let the user know the results in an official manner.
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900281 print('You are probably looking for build %d.' % revlist[bad])
282 print('CHANGELOG URL:')
nirnimesh@chromium.orgc2fedbd2009-10-09 04:55:38 +0900283 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad]))
nirnimesh@chromium.orgd881c102010-03-31 10:05:24 +0900284 print('Built at revision:')
rsesek@chromium.orge0c13e82009-09-04 07:06:09 +0900285 print(BUILD_VIEWVC_URL % revlist[bad])
286
287if __name__ == '__main__':
mmoss@chromium.orgaf003b82009-09-05 02:52:33 +0900288 sys.exit(main())