blob: 64504bc8d9227db57e5e92b5adf443d35ec2b032 [file] [log] [blame]
[email protected]67e0bc62009-09-03 22:06:091#!/usr/bin/python2.5
[email protected]5957d452010-05-26 11:31:262# Copyright (c) 2010 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
15# Base URL to download snapshots from.
[email protected]bd8dcb92010-03-31 01:05:2416BUILD_BASE_URL = 'http://build.chromium.org/buildbot/snapshots/'
[email protected]67e0bc62009-09-03 22:06:0917
[email protected]7ad66a72009-09-04 17:52:3318# 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 = ''
[email protected]67e0bc62009-09-03 22:06:0924
25# The location of the builds.
[email protected]bd8dcb92010-03-31 01:05:2426BUILD_ARCHIVE_URL = '/%d/'
[email protected]67e0bc62009-09-03 22:06:0927
28# Name of the build archive.
[email protected]7ad66a72009-09-04 17:52:3329BUILD_ZIP_NAME = ''
[email protected]67e0bc62009-09-03 22:06:0930
31# Directory name inside the archive.
[email protected]7ad66a72009-09-04 17:52:3332BUILD_DIR_NAME = ''
[email protected]67e0bc62009-09-03 22:06:0933
34# Name of the executable.
[email protected]7ad66a72009-09-04 17:52:3335BUILD_EXE_NAME = ''
[email protected]67e0bc62009-09-03 22:06:0936
37# URL to the ViewVC commit page.
[email protected]bd8dcb92010-03-31 01:05:2438BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d'
[email protected]67e0bc62009-09-03 22:06:0939
[email protected]f6a71a72009-10-08 19:55:3840# Changelogs URL
[email protected]bd8dcb92010-03-31 01:05:2441CHANGELOG_URL = 'http://build.chromium.org/buildbot/' \
42 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d'
[email protected]f6a71a72009-10-08 19:55:3843
[email protected]67e0bc62009-09-03 22:06:0944###############################################################################
45
46import math
[email protected]7ad66a72009-09-04 17:52:3347import optparse
[email protected]67e0bc62009-09-03 22:06:0948import os
[email protected]d4bf3582009-09-20 00:56:3849import pipes
[email protected]67e0bc62009-09-03 22:06:0950import re
51import shutil
52import sys
[email protected]7ad66a72009-09-04 17:52:3353import tempfile
[email protected]67e0bc62009-09-03 22:06:0954import urllib
[email protected]bd8dcb92010-03-31 01:05:2455import 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
[email protected]67e0bc62009-09-03 22:06:0987
[email protected]7ad66a72009-09-04 17:52:3388def 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
100 if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64'):
101 BUILD_ZIP_NAME = 'chrome-linux.zip'
102 BUILD_DIR_NAME = 'chrome-linux'
[email protected]bd8dcb92010-03-31 01:05:24103 BUILD_EXE_NAME = 'chrome'
[email protected]7ad66a72009-09-04 17:52:33104 elif BUILD_ARCHIVE_TYPE in ('mac'):
105 BUILD_ZIP_NAME = 'chrome-mac.zip'
106 BUILD_DIR_NAME = 'chrome-mac'
[email protected]bd8dcb92010-03-31 01:05:24107 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium'
[email protected]7ad66a72009-09-04 17:52:33108 elif BUILD_ARCHIVE_TYPE in ('xp'):
109 BUILD_ZIP_NAME = 'chrome-win32.zip'
110 BUILD_DIR_NAME = 'chrome-win32'
[email protected]bd8dcb92010-03-31 01:05:24111 BUILD_EXE_NAME = 'chrome.exe'
[email protected]7ad66a72009-09-04 17:52:33112
113 BUILD_BASE_URL += BUILD_ARCHIVE_DIR
114
[email protected]67e0bc62009-09-03 22:06:09115def 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
[email protected]f6a71a72009-10-08 19:55:38131def TryRevision(rev, profile, args):
[email protected]d4bf3582009-09-20 00:56:38132 """Downloads revision |rev|, unzips it, and opens it for the user to test.
133 |profile| is the profile to use."""
[email protected]7ad66a72009-09-04 17:52:33134 # 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)
[email protected]67e0bc62009-09-03 22:06:09138
139 # Download the file.
140 download_url = BUILD_BASE_URL + (BUILD_ARCHIVE_URL % rev) + BUILD_ZIP_NAME
141 try:
[email protected]7ad66a72009-09-04 17:52:33142 print 'Fetching ' + download_url
[email protected]67e0bc62009-09-03 22:06:09143 urllib.urlretrieve(download_url, BUILD_ZIP_NAME)
144 except Exception, e:
[email protected]bd8dcb92010-03-31 01:05:24145 print('Could not retrieve the download. Sorry.')
[email protected]67e0bc62009-09-03 22:06:09146 sys.exit(-1)
147
148 # Unzip the file.
[email protected]bd8dcb92010-03-31 01:05:24149 print 'Unziping ...'
150 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir)
[email protected]67e0bc62009-09-03 22:06:09151
[email protected]7ad66a72009-09-04 17:52:33152 # Tell the system to open the app.
[email protected]f6a71a72009-10-08 19:55:38153 args = ['--user-data-dir=%s' % profile] + args
154 flags = ' '.join(map(pipes.quote, args))
[email protected]bd8dcb92010-03-31 01:05:24155 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)
[email protected]7ad66a72009-09-04 17:52:33159
160 os.chdir(cwd)
161 print 'Cleaning temp dir ...'
162 try:
163 shutil.rmtree(tempdir, True)
164 except Exception, e:
165 pass
[email protected]67e0bc62009-09-03 22:06:09166
[email protected]79f14742010-03-10 01:01:57167
[email protected]67e0bc62009-09-03 22:06:09168def AskIsGoodBuild(rev):
[email protected]79f14742010-03-10 01:01:57169 """Ask the user whether build |rev| is good or bad."""
170 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09171 while True:
[email protected]bd8dcb92010-03-31 01:05:24172 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev))
173 if response and response in ('g', 'b'):
174 return response == 'g'
[email protected]67e0bc62009-09-03 22:06:09175
176def main():
[email protected]2c1d2732009-10-29 19:52:17177 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]7ad66a72009-09-04 17:52:33178 'Perform binary search on the snapshot builds.')
179 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:57180 # Strangely, the default help output doesn't include the choice list.
181 choices = ['mac', 'xp', 'linux', 'linux-64']
[email protected]7ad66a72009-09-04 17:52:33182 parser.add_option('-a', '--archive',
[email protected]1a45d222009-09-19 01:58:57183 choices = choices,
184 help = 'The buildbot archive to bisect [%s].' %
185 '|'.join(choices))
[email protected]7ad66a72009-09-04 17:52:33186 parser.add_option('-b', '--bad', type = 'int',
187 help = 'The bad revision to bisect to.')
188 parser.add_option('-g', '--good', type = 'int',
189 help = 'The last known good revision to bisect from.')
[email protected]d4bf3582009-09-20 00:56:38190 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
191 help = 'Profile to use; this will not reset every run. ' +
192 'Defaults to a clean profile.')
[email protected]7ad66a72009-09-04 17:52:33193 (opts, args) = parser.parse_args()
194
195 if opts.archive is None:
196 parser.print_help()
197 return 1
198
199 if opts.bad and opts.good and (opts.good > opts.bad):
200 print ('The good revision (%d) must precede the bad revision (%d).\n' %
201 (opts.good, opts.bad))
202 parser.print_help()
203 return 1
204
205 SetArchiveVars(opts.archive)
[email protected]67e0bc62009-09-03 22:06:09206
207 # Pick a starting point, try to get HEAD for this.
[email protected]7ad66a72009-09-04 17:52:33208 if opts.bad:
209 bad_rev = opts.bad
210 else:
211 bad_rev = 0
212 try:
213 # Location of the latest build revision number
[email protected]bd8dcb92010-03-31 01:05:24214 BUILD_LATEST_URL = '%s/LATEST' % (BUILD_BASE_URL)
[email protected]7ad66a72009-09-04 17:52:33215 nh = urllib.urlopen(BUILD_LATEST_URL)
216 latest = int(nh.read())
217 nh.close()
[email protected]bd8dcb92010-03-31 01:05:24218 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest)
219 if (bad_rev == ''):
[email protected]7ad66a72009-09-04 17:52:33220 bad_rev = latest
221 bad_rev = int(bad_rev)
222 except Exception, e:
[email protected]bd8dcb92010-03-31 01:05:24223 print('Could not determine latest revision. This could be bad...')
224 bad_rev = int(raw_input('Bad revision: '))
[email protected]67e0bc62009-09-03 22:06:09225
226 # Find out when we were good.
[email protected]7ad66a72009-09-04 17:52:33227 if opts.good:
228 good_rev = opts.good
229 else:
230 good_rev = 0
231 try:
[email protected]bd8dcb92010-03-31 01:05:24232 good_rev = int(raw_input('Last known good [0]: '))
[email protected]7ad66a72009-09-04 17:52:33233 except Exception, e:
234 pass
[email protected]67e0bc62009-09-03 22:06:09235
236 # Get a list of revisions to bisect across.
237 revlist = GetRevList(good_rev, bad_rev)
[email protected]2c1d2732009-10-29 19:52:17238 if len(revlist) < 2: # Don't have enough builds to bisect
[email protected]bd8dcb92010-03-31 01:05:24239 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist
[email protected]2c1d2732009-10-29 19:52:17240 sys.exit(1)
[email protected]67e0bc62009-09-03 22:06:09241
242 # If we don't have a |good_rev|, set it to be the first revision possible.
243 if good_rev == 0:
244 good_rev = revlist[0]
245
246 # These are indexes of |revlist|.
247 good = 0
248 bad = len(revlist) - 1
[email protected]f6a71a72009-10-08 19:55:38249 last_known_good_rev = revlist[good]
[email protected]67e0bc62009-09-03 22:06:09250
251 # Binary search time!
252 while good < bad:
253 candidates = revlist[good:bad]
254 num_poss = len(candidates)
255 if num_poss > 10:
[email protected]bd8dcb92010-03-31 01:05:24256 print('%d candidates. %d tries left.' %
[email protected]67e0bc62009-09-03 22:06:09257 (num_poss, round(math.log(num_poss, 2))))
258 else:
[email protected]bd8dcb92010-03-31 01:05:24259 print('Candidates: %s' % revlist[good:bad])
[email protected]67e0bc62009-09-03 22:06:09260
261 # Cut the problem in half...
262 test = int((bad - good) / 2) + good
263 test_rev = revlist[test]
264
[email protected]d4bf3582009-09-20 00:56:38265 # Let the user give this rev a spin (in her own profile, if she wants).
266 profile = opts.profile
267 if not profile:
268 profile = 'profile' # In a temp dir.
[email protected]f6a71a72009-10-08 19:55:38269 TryRevision(test_rev, profile, args)
[email protected]67e0bc62009-09-03 22:06:09270 if AskIsGoodBuild(test_rev):
[email protected]f6a71a72009-10-08 19:55:38271 last_known_good_rev = revlist[good]
[email protected]67e0bc62009-09-03 22:06:09272 good = test + 1
273 else:
274 bad = test
275
276 # We're done. Let the user know the results in an official manner.
[email protected]bd8dcb92010-03-31 01:05:24277 print('You are probably looking for build %d.' % revlist[bad])
278 print('CHANGELOG URL:')
[email protected]f6a71a72009-10-08 19:55:38279 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad]))
[email protected]bd8dcb92010-03-31 01:05:24280 print('Built at revision:')
[email protected]67e0bc62009-09-03 22:06:09281 print(BUILD_VIEWVC_URL % revlist[bad])
282
283if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:33284 sys.exit(main())