blob: 1c6b41614e9514db5977d76cd34d12b67d7dfe6a [file] [log] [blame]
[email protected]67e0bc62009-09-03 22:06:091#!/usr/bin/python2.5
2# Copyright (c) 2009 The Chromium Authors. All rights reserved.
3# 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]7ad66a72009-09-04 17:52:3316BUILD_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.
26BUILD_ARCHIVE_URL = "/%d/"
27
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.
38BUILD_VIEWVC_URL = "http://src.chromium.org/viewvc/chrome?view=rev&revision=%d"
39
[email protected]f6a71a72009-10-08 19:55:3840# Changelogs URL
41CHANGELOG_URL = "http://build.chromium.org/buildbot/" \
42 "perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d"
43
[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
55
[email protected]7ad66a72009-09-04 17:52:3356def SetArchiveVars(archive):
57 """Set a bunch of global variables appropriate for the specified archive."""
58 global BUILD_ARCHIVE_TYPE
59 global BUILD_ARCHIVE_DIR
60 global BUILD_ZIP_NAME
61 global BUILD_DIR_NAME
62 global BUILD_EXE_NAME
63 global BUILD_BASE_URL
64
65 BUILD_ARCHIVE_TYPE = archive
66 BUILD_ARCHIVE_DIR = 'chromium-rel-' + BUILD_ARCHIVE_TYPE
67
68 if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64'):
69 BUILD_ZIP_NAME = 'chrome-linux.zip'
70 BUILD_DIR_NAME = 'chrome-linux'
71 BUILD_EXE_NAME = "chrome"
72 elif BUILD_ARCHIVE_TYPE in ('mac'):
73 BUILD_ZIP_NAME = 'chrome-mac.zip'
74 BUILD_DIR_NAME = 'chrome-mac'
[email protected]d4bf3582009-09-20 00:56:3875 BUILD_EXE_NAME = "Chromium.app/Contents/MacOS/Chromium"
[email protected]7ad66a72009-09-04 17:52:3376 elif BUILD_ARCHIVE_TYPE in ('xp'):
77 BUILD_ZIP_NAME = 'chrome-win32.zip'
78 BUILD_DIR_NAME = 'chrome-win32'
79 BUILD_EXE_NAME = "chrome.exe"
80
81 BUILD_BASE_URL += BUILD_ARCHIVE_DIR
82
[email protected]67e0bc62009-09-03 22:06:0983def ParseDirectoryIndex(url):
84 """Parses the HTML directory listing into a list of revision numbers."""
85 handle = urllib.urlopen(url)
86 dirindex = handle.read()
87 handle.close()
88 return re.findall(r'<a href="([0-9]*)/">\1/</a>', dirindex)
89
90def GetRevList(good, bad):
91 """Gets the list of revision numbers between |good| and |bad|."""
92 # Download the main revlist.
93 revlist = ParseDirectoryIndex(BUILD_BASE_URL)
94 revlist = map(int, revlist)
95 revlist = filter(lambda r: range(good, bad).__contains__(int(r)), revlist)
96 revlist.sort()
97 return revlist
98
[email protected]f6a71a72009-10-08 19:55:3899def TryRevision(rev, profile, args):
[email protected]d4bf3582009-09-20 00:56:38100 """Downloads revision |rev|, unzips it, and opens it for the user to test.
101 |profile| is the profile to use."""
[email protected]7ad66a72009-09-04 17:52:33102 # Do this in a temp dir so we don't collide with user files.
103 cwd = os.getcwd()
104 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
105 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09106
107 # Download the file.
108 download_url = BUILD_BASE_URL + (BUILD_ARCHIVE_URL % rev) + BUILD_ZIP_NAME
109 try:
[email protected]7ad66a72009-09-04 17:52:33110 print 'Fetching ' + download_url
[email protected]67e0bc62009-09-03 22:06:09111 urllib.urlretrieve(download_url, BUILD_ZIP_NAME)
112 except Exception, e:
113 print("Could not retrieve the download. Sorry.")
[email protected]67e0bc62009-09-03 22:06:09114 sys.exit(-1)
115
116 # Unzip the file.
[email protected]7ad66a72009-09-04 17:52:33117 print 'Unzipping ...'
[email protected]67e0bc62009-09-03 22:06:09118 os.system("unzip -q %s" % BUILD_ZIP_NAME)
119
[email protected]7ad66a72009-09-04 17:52:33120 # Tell the system to open the app.
[email protected]f6a71a72009-10-08 19:55:38121 args = ['--user-data-dir=%s' % profile] + args
122 flags = ' '.join(map(pipes.quote, args))
[email protected]1a45d222009-09-19 01:58:57123 print 'Running %s/%s/%s %s' % (os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME,
124 flags)
[email protected]d4bf3582009-09-20 00:56:38125 if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64', 'mac'):
[email protected]f6a71a72009-10-08 19:55:38126 os.system("%s/%s %s" % (BUILD_DIR_NAME, BUILD_EXE_NAME, flags))
[email protected]7ad66a72009-09-04 17:52:33127 elif BUILD_ARCHIVE_TYPE in ('xp'):
128 # TODO(mmoss) Does Windows need 'start' or something?
[email protected]1a45d222009-09-19 01:58:57129 os.system("%s/%s %s" % (BUILD_DIR_NAME, BUILD_EXE_NAME, flags))
[email protected]7ad66a72009-09-04 17:52:33130
131 os.chdir(cwd)
132 print 'Cleaning temp dir ...'
133 try:
134 shutil.rmtree(tempdir, True)
135 except Exception, e:
136 pass
[email protected]67e0bc62009-09-03 22:06:09137
[email protected]79f14742010-03-10 01:01:57138
[email protected]67e0bc62009-09-03 22:06:09139def AskIsGoodBuild(rev):
[email protected]79f14742010-03-10 01:01:57140 """Ask the user whether build |rev| is good or bad."""
141 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09142 while True:
[email protected]79f14742010-03-10 01:01:57143 response = raw_input("\nBuild %d is [(g)ood/(b)ad]: " % int(rev))
144 if response and response in ("g", "b"):
145 return response == "g"
[email protected]67e0bc62009-09-03 22:06:09146
147def main():
[email protected]2c1d2732009-10-29 19:52:17148 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]7ad66a72009-09-04 17:52:33149 'Perform binary search on the snapshot builds.')
150 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:57151 # Strangely, the default help output doesn't include the choice list.
152 choices = ['mac', 'xp', 'linux', 'linux-64']
[email protected]7ad66a72009-09-04 17:52:33153 parser.add_option('-a', '--archive',
[email protected]1a45d222009-09-19 01:58:57154 choices = choices,
155 help = 'The buildbot archive to bisect [%s].' %
156 '|'.join(choices))
[email protected]7ad66a72009-09-04 17:52:33157 parser.add_option('-b', '--bad', type = 'int',
158 help = 'The bad revision to bisect to.')
159 parser.add_option('-g', '--good', type = 'int',
160 help = 'The last known good revision to bisect from.')
[email protected]d4bf3582009-09-20 00:56:38161 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
162 help = 'Profile to use; this will not reset every run. ' +
163 'Defaults to a clean profile.')
[email protected]7ad66a72009-09-04 17:52:33164 (opts, args) = parser.parse_args()
165
166 if opts.archive is None:
167 parser.print_help()
168 return 1
169
170 if opts.bad and opts.good and (opts.good > opts.bad):
171 print ('The good revision (%d) must precede the bad revision (%d).\n' %
172 (opts.good, opts.bad))
173 parser.print_help()
174 return 1
175
176 SetArchiveVars(opts.archive)
[email protected]67e0bc62009-09-03 22:06:09177
178 # Pick a starting point, try to get HEAD for this.
[email protected]7ad66a72009-09-04 17:52:33179 if opts.bad:
180 bad_rev = opts.bad
181 else:
182 bad_rev = 0
183 try:
184 # Location of the latest build revision number
185 BUILD_LATEST_URL = "%s/LATEST" % (BUILD_BASE_URL)
186 nh = urllib.urlopen(BUILD_LATEST_URL)
187 latest = int(nh.read())
188 nh.close()
189 bad_rev = raw_input("Bad revision [HEAD:%d]: " % latest)
190 if (bad_rev == ""):
191 bad_rev = latest
192 bad_rev = int(bad_rev)
193 except Exception, e:
194 print("Could not determine latest revision. This could be bad...")
195 bad_rev = int(raw_input("Bad revision: "))
[email protected]67e0bc62009-09-03 22:06:09196
197 # Find out when we were good.
[email protected]7ad66a72009-09-04 17:52:33198 if opts.good:
199 good_rev = opts.good
200 else:
201 good_rev = 0
202 try:
203 good_rev = int(raw_input("Last known good [0]: "))
204 except Exception, e:
205 pass
[email protected]67e0bc62009-09-03 22:06:09206
207 # Get a list of revisions to bisect across.
208 revlist = GetRevList(good_rev, bad_rev)
[email protected]2c1d2732009-10-29 19:52:17209 if len(revlist) < 2: # Don't have enough builds to bisect
210 print "We don't have enough builds to bisect. revlist: %s" % revlist
211 sys.exit(1)
[email protected]67e0bc62009-09-03 22:06:09212
213 # If we don't have a |good_rev|, set it to be the first revision possible.
214 if good_rev == 0:
215 good_rev = revlist[0]
216
217 # These are indexes of |revlist|.
218 good = 0
219 bad = len(revlist) - 1
[email protected]f6a71a72009-10-08 19:55:38220 last_known_good_rev = revlist[good]
[email protected]67e0bc62009-09-03 22:06:09221
222 # Binary search time!
223 while good < bad:
224 candidates = revlist[good:bad]
225 num_poss = len(candidates)
226 if num_poss > 10:
227 print("%d candidates. %d tries left." %
228 (num_poss, round(math.log(num_poss, 2))))
229 else:
230 print("Candidates: %s" % revlist[good:bad])
231
232 # Cut the problem in half...
233 test = int((bad - good) / 2) + good
234 test_rev = revlist[test]
235
[email protected]d4bf3582009-09-20 00:56:38236 # Let the user give this rev a spin (in her own profile, if she wants).
237 profile = opts.profile
238 if not profile:
239 profile = 'profile' # In a temp dir.
[email protected]f6a71a72009-10-08 19:55:38240 TryRevision(test_rev, profile, args)
[email protected]67e0bc62009-09-03 22:06:09241 if AskIsGoodBuild(test_rev):
[email protected]f6a71a72009-10-08 19:55:38242 last_known_good_rev = revlist[good]
[email protected]67e0bc62009-09-03 22:06:09243 good = test + 1
244 else:
245 bad = test
246
247 # We're done. Let the user know the results in an official manner.
248 print("You are probably looking for build %d." % revlist[bad])
[email protected]f6a71a72009-10-08 19:55:38249 print("CHANGELOG URL:")
250 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad]))
251 print("Built at revision:")
[email protected]67e0bc62009-09-03 22:06:09252 print(BUILD_VIEWVC_URL % revlist[bad])
253
254if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:33255 sys.exit(main())