OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Snapshot Build Bisect Tool | 6 """Snapshot Build Bisect Tool |
7 | 7 |
8 This script bisects a snapshot archive using binary search. It starts at | 8 This script bisects a snapshot archive using binary search. It starts at |
9 a bad revision (it will try to guess HEAD) and asks for a last known-good | 9 a bad revision (it will try to guess HEAD) and asks for a last known-good |
10 revision. It will then binary search across this revision range by downloading, | 10 revision. It will then binary search across this revision range by downloading, |
11 unzipping, and opening Chromium for you. After testing the specific revision, | 11 unzipping, and opening Chromium for you. After testing the specific revision, |
12 it will ask you whether it is good or bad before continuing the search. | 12 it will ask you whether it is good or bad before continuing the search. |
13 """ | 13 """ |
14 | 14 |
15 # Base URL to download snapshots from. | 15 # Base URL to download snapshots from. |
16 BUILD_BASE_URL = 'http://build.chromium.org/f/chromium/snapshots/' | 16 BUILD_BASE_URL = 'http://build.chromium.org/f/chromium/continuous/' |
17 | |
18 # The index file that lists all the builds. This lives in BUILD_BASE_URL. | |
19 BUILD_INDEX_FILE = 'all_builds.txt' | |
17 | 20 |
18 # The type (platform) of the build archive. This is what's passed in to the | 21 # The type (platform) of the build archive. This is what's passed in to the |
19 # '-a/--archive' option. | 22 # '-a/--archive' option. |
20 BUILD_ARCHIVE_TYPE = '' | 23 BUILD_ARCHIVE_TYPE = '' |
21 | 24 |
22 # The selected archive to bisect. | 25 # The location of the builds. Format this with a (date, revision) tuple, which |
23 BUILD_ARCHIVE_DIR = '' | 26 # can be obtained through ParseIndexLine(). |
24 | 27 BUILD_ARCHIVE_URL = '/%s/%d/' |
25 # The location of the builds. | |
26 BUILD_ARCHIVE_URL = '/%d/' | |
27 | 28 |
28 # Name of the build archive. | 29 # Name of the build archive. |
29 BUILD_ZIP_NAME = '' | 30 BUILD_ZIP_NAME = '' |
30 | 31 |
31 # Directory name inside the archive. | 32 # Directory name inside the archive. |
32 BUILD_DIR_NAME = '' | 33 BUILD_DIR_NAME = '' |
33 | 34 |
34 # Name of the executable. | 35 # Name of the executable. |
35 BUILD_EXE_NAME = '' | 36 BUILD_EXE_NAME = '' |
36 | 37 |
(...skipping 10 matching lines...) Expand all Loading... | |
47 import optparse | 48 import optparse |
48 import os | 49 import os |
49 import pipes | 50 import pipes |
50 import re | 51 import re |
51 import shutil | 52 import shutil |
52 import sys | 53 import sys |
53 import tempfile | 54 import tempfile |
54 import urllib | 55 import urllib |
55 import zipfile | 56 import zipfile |
56 | 57 |
57 | |
58 def UnzipFilenameToDir(filename, dir): | 58 def UnzipFilenameToDir(filename, dir): |
59 """Unzip |filename| to directory |dir|.""" | 59 """Unzip |filename| to directory |dir|.""" |
60 zf = zipfile.ZipFile(filename) | 60 zf = zipfile.ZipFile(filename) |
61 # Make base. | 61 # Make base. |
62 pushd = os.getcwd() | 62 pushd = os.getcwd() |
63 try: | 63 try: |
64 if not os.path.isdir(dir): | 64 if not os.path.isdir(dir): |
65 os.mkdir(dir) | 65 os.mkdir(dir) |
66 os.chdir(dir) | 66 os.chdir(dir) |
67 # Extract files. | 67 # Extract files. |
68 for info in zf.infolist(): | 68 for info in zf.infolist(): |
69 name = info.filename | 69 name = info.filename |
70 if name.endswith('/'): # dir | 70 if name.endswith('/'): # dir |
71 if not os.path.isdir(name): | 71 if not os.path.isdir(name): |
72 os.makedirs(name) | 72 os.makedirs(name) |
73 else: # file | 73 else: # file |
74 dir = os.path.dirname(name) | 74 dir = os.path.dirname(name) |
75 if not os.path.isdir(dir): | 75 if not os.path.isdir(dir): |
76 os.makedirs(dir) | 76 os.makedirs(dir) |
77 out = open(name, 'wb') | 77 out = open(name, 'wb') |
78 out.write(zf.read(name)) | 78 out.write(zf.read(name)) |
79 out.close() | 79 out.close() |
80 # Set permissions. Permission info in external_attr is shifted 16 bits. | 80 # Set permissions. Permission info in external_attr is shifted 16 bits. |
81 os.chmod(name, info.external_attr >> 16L) | 81 os.chmod(name, info.external_attr >> 16L) |
82 os.chdir(pushd) | 82 os.chdir(pushd) |
83 except Exception, e: | 83 except Exception, e: |
84 print >>sys.stderr, e | 84 print >>sys.stderr, e |
85 sys.exit(1) | 85 sys.exit(1) |
86 | 86 |
Evan Martin
2011/04/14 19:03:56
fwiw, double-newline after function bodies is reco
Robert Sesek
2011/04/29 20:25:44
Done.
| |
87 | |
88 def SetArchiveVars(archive): | 87 def SetArchiveVars(archive): |
89 """Set a bunch of global variables appropriate for the specified archive.""" | 88 """Set a bunch of global variables appropriate for the specified archive.""" |
90 global BUILD_ARCHIVE_TYPE | 89 global BUILD_ARCHIVE_TYPE |
91 global BUILD_ARCHIVE_DIR | |
92 global BUILD_ZIP_NAME | 90 global BUILD_ZIP_NAME |
93 global BUILD_DIR_NAME | 91 global BUILD_DIR_NAME |
94 global BUILD_EXE_NAME | 92 global BUILD_EXE_NAME |
95 global BUILD_BASE_URL | 93 global BUILD_BASE_URL |
96 | 94 |
97 BUILD_ARCHIVE_TYPE = archive | 95 BUILD_ARCHIVE_TYPE = archive |
98 BUILD_ARCHIVE_DIR = 'chromium-rel-' + BUILD_ARCHIVE_TYPE | |
99 | 96 |
100 if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64', 'linux-chromiumos'): | 97 if BUILD_ARCHIVE_TYPE in ('linux', 'linux64', 'linux-chromiumos'): |
101 BUILD_ZIP_NAME = 'chrome-linux.zip' | 98 BUILD_ZIP_NAME = 'chrome-linux.zip' |
102 BUILD_DIR_NAME = 'chrome-linux' | 99 BUILD_DIR_NAME = 'chrome-linux' |
103 BUILD_EXE_NAME = 'chrome' | 100 BUILD_EXE_NAME = 'chrome' |
104 elif BUILD_ARCHIVE_TYPE in ('mac'): | 101 elif BUILD_ARCHIVE_TYPE in ('mac'): |
105 BUILD_ZIP_NAME = 'chrome-mac.zip' | 102 BUILD_ZIP_NAME = 'chrome-mac.zip' |
106 BUILD_DIR_NAME = 'chrome-mac' | 103 BUILD_DIR_NAME = 'chrome-mac' |
107 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' | 104 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' |
108 elif BUILD_ARCHIVE_TYPE in ('xp'): | 105 elif BUILD_ARCHIVE_TYPE in ('xp'): |
109 BUILD_ZIP_NAME = 'chrome-win32.zip' | 106 BUILD_ZIP_NAME = 'chrome-win32.zip' |
110 BUILD_DIR_NAME = 'chrome-win32' | 107 BUILD_DIR_NAME = 'chrome-win32' |
111 BUILD_EXE_NAME = 'chrome.exe' | 108 BUILD_EXE_NAME = 'chrome.exe' |
112 | 109 |
113 BUILD_BASE_URL += BUILD_ARCHIVE_DIR | 110 def ParseDirectoryIndex(url): |
111 """Parses the all_builds.txt index file. The format of this file is: | |
112 mac/2011-02-16/75130 | |
113 mac/2011-02-16/75218 | |
114 mac/2011-02-16/75226 | |
115 mac/2011-02-16/75234 | |
116 mac/2011-02-16/75184 | |
117 This function will return a list of DATE/REVISION strings for the platform | |
118 specified by BUILD_ARCHIVE_TYPE. | |
119 """ | |
120 handle = urllib.urlopen(url) | |
121 dirindex = handle.readlines() | |
122 handle.close() | |
114 | 123 |
115 def ParseDirectoryIndex(url): | 124 # Only return values for the specified platform. |
116 """Parses the HTML directory listing into a list of revision numbers.""" | 125 archtype = BUILD_ARCHIVE_TYPE |
117 handle = urllib.urlopen(url) | 126 dirindex = filter(lambda l: l.startswith(archtype), dirindex) |
118 dirindex = handle.read() | 127 |
119 handle.close() | 128 # Remove the newline separator and the platform token. |
120 return re.findall(r'<a href="([0-9]*)/">\1/</a>', dirindex) | 129 dirindex = map(lambda l: l[len(archtype) + 1:].strip(), dirindex) |
130 dirindex.sort() | |
131 return dirindex | |
132 | |
133 def ParseIndexLine(iline): | |
134 """Takes an index line returned by ParseDirectoryIndex() and returns a | |
135 2-tuple of (date, revision). |date| is a string and |revision| is an int.""" | |
136 split = iline.split('/') | |
137 assert(len(split) == 2) | |
138 return (split[0], int(split[1])) | |
139 | |
140 def GetRevision(iline): | |
141 """Takes an index line, parses it, and returns the revision.""" | |
142 return ParseIndexLine(iline)[1] | |
121 | 143 |
122 def GetRevList(good, bad): | 144 def GetRevList(good, bad): |
123 """Gets the list of revision numbers between |good| and |bad|.""" | 145 """Gets the list of revision numbers between |good| and |bad|.""" |
124 # Download the main revlist. | 146 # Download the main revlist. |
125 revlist = ParseDirectoryIndex(BUILD_BASE_URL) | 147 revlist = ParseDirectoryIndex(BUILD_BASE_URL + BUILD_INDEX_FILE) |
126 revlist = map(int, revlist) | 148 |
127 revlist = filter(lambda r: range(good, bad).__contains__(int(r)), revlist) | 149 revrange = range(good, bad) |
150 revlist = filter(lambda r: revrange.__contains__(GetRevision(r)), | |
Evan Martin
2011/04/14 19:03:56
GetRevision(r) in revrange
?
Robert Sesek
2011/04/29 20:25:44
Done.
| |
151 revlist) | |
128 revlist.sort() | 152 revlist.sort() |
129 return revlist | 153 return revlist |
130 | 154 |
131 def TryRevision(rev, profile, args): | 155 def TryRevision(iline, profile, args): |
132 """Downloads revision |rev|, unzips it, and opens it for the user to test. | 156 """Downloads revision from |iline|, unzips it, and opens it for the user to |
133 |profile| is the profile to use.""" | 157 test. |profile| is the profile to use.""" |
134 # Do this in a temp dir so we don't collide with user files. | 158 # Do this in a temp dir so we don't collide with user files. |
135 cwd = os.getcwd() | 159 cwd = os.getcwd() |
136 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 160 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
137 os.chdir(tempdir) | 161 os.chdir(tempdir) |
138 | 162 |
139 # Download the file. | 163 # Download the file. |
140 download_url = BUILD_BASE_URL + (BUILD_ARCHIVE_URL % rev) + BUILD_ZIP_NAME | 164 download_url = BUILD_BASE_URL + BUILD_ARCHIVE_TYPE + \ |
141 def _Reporthook(blocknum, blocksize, totalsize): | 165 (BUILD_ARCHIVE_URL % ParseIndexLine(iline)) + BUILD_ZIP_NAME |
166 def _ReportHook(blocknum, blocksize, totalsize): | |
142 size = blocknum * blocksize | 167 size = blocknum * blocksize |
143 if totalsize == -1: # Total size not known. | 168 if totalsize == -1: # Total size not known. |
144 progress = "Received %d bytes" % size | 169 progress = "Received %d bytes" % size |
145 else: | 170 else: |
146 size = min(totalsize, size) | 171 size = min(totalsize, size) |
147 progress = "Received %d of %d bytes, %.2f%%" % ( | 172 progress = "Received %d of %d bytes, %.2f%%" % ( |
148 size, totalsize, 100.0 * size / totalsize) | 173 size, totalsize, 100.0 * size / totalsize) |
149 # Send a \r to let all progress messages use just one line of output. | 174 # Send a \r to let all progress messages use just one line of output. |
150 sys.stdout.write("\r" + progress) | 175 sys.stdout.write("\r" + progress) |
151 sys.stdout.flush() | 176 sys.stdout.flush() |
152 try: | 177 try: |
153 print 'Fetching ' + download_url | 178 print 'Fetching ' + download_url |
154 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _Reporthook) | 179 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _ReportHook) |
155 print | 180 print |
156 except Exception, e: | 181 except Exception, e: |
157 print('Could not retrieve the download. Sorry.') | 182 print('Could not retrieve the download. Sorry.') |
158 sys.exit(-1) | 183 sys.exit(-1) |
159 | 184 |
160 # Unzip the file. | 185 # Unzip the file. |
161 print 'Unzipping ...' | 186 print 'Unzipping ...' |
162 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) | 187 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) |
163 | 188 |
164 # Tell the system to open the app. | 189 # Tell the system to open the app. |
165 args = ['--user-data-dir=%s' % profile] + args | 190 args = ['--user-data-dir=%s' % profile] + args |
166 flags = ' '.join(map(pipes.quote, args)) | 191 flags = ' '.join(map(pipes.quote, args)) |
167 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) | 192 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) |
168 cmd = '%s %s' % (exe, flags) | 193 cmd = '%s %s' % (exe, flags) |
169 print 'Running %s' % cmd | 194 print 'Running %s' % cmd |
170 os.system(cmd) | 195 os.system(cmd) |
171 | 196 |
172 os.chdir(cwd) | 197 os.chdir(cwd) |
173 print 'Cleaning temp dir ...' | 198 print 'Cleaning temp dir ...' |
174 try: | 199 try: |
175 shutil.rmtree(tempdir, True) | 200 shutil.rmtree(tempdir, True) |
176 except Exception, e: | 201 except Exception, e: |
177 pass | 202 pass |
178 | 203 |
179 | 204 def AskIsGoodBuild(iline): |
180 def AskIsGoodBuild(rev): | 205 """Ask the user whether build from index line |iline| is good or bad.""" |
181 """Ask the user whether build |rev| is good or bad.""" | |
182 # Loop until we get a response that we can parse. | 206 # Loop until we get a response that we can parse. |
183 while True: | 207 while True: |
184 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) | 208 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % GetRevision(iline)) |
185 if response and response in ('g', 'b'): | 209 if response and response in ('g', 'b'): |
186 return response == 'g' | 210 return response == 'g' |
187 | 211 |
188 def main(): | 212 def main(): |
189 usage = ('%prog [options] [-- chromium-options]\n' | 213 usage = ('%prog [options] [-- chromium-options]\n' |
190 'Perform binary search on the snapshot builds.\n' | 214 'Perform binary search on the snapshot builds.\n' |
191 '\n' | 215 '\n' |
192 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 216 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
193 parser = optparse.OptionParser(usage=usage) | 217 parser = optparse.OptionParser(usage=usage) |
194 # Strangely, the default help output doesn't include the choice list. | 218 # Strangely, the default help output doesn't include the choice list. |
195 choices = ['mac', 'xp', 'linux', 'linux-64', 'linux-chromiumos'] | 219 choices = ['mac', 'xp', 'linux', 'linux64']#, 'linux-chromiumos'] http://crbug .com/78158 |
Evan Martin
2011/04/14 19:03:56
typo here, extra pasted url
Robert Sesek
2011/04/29 20:25:44
No, it's actually a bug that chromiumos doesn't ha
| |
196 parser.add_option('-a', '--archive', | 220 parser.add_option('-a', '--archive', |
197 choices = choices, | 221 choices = choices, |
198 help = 'The buildbot archive to bisect [%s].' % | 222 help = 'The buildbot archive to bisect [%s].' % |
199 '|'.join(choices)) | 223 '|'.join(choices)) |
200 parser.add_option('-b', '--bad', type = 'int', | 224 parser.add_option('-b', '--bad', type = 'int', |
201 help = 'The bad revision to bisect to.') | 225 help = 'The bad revision to bisect to.') |
202 parser.add_option('-g', '--good', type = 'int', | 226 parser.add_option('-g', '--good', type = 'int', |
203 help = 'The last known good revision to bisect from.') | 227 help = 'The last known good revision to bisect from.') |
204 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 228 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
205 help = 'Profile to use; this will not reset every run. ' + | 229 help = 'Profile to use; this will not reset every run. ' + |
(...skipping 14 matching lines...) Expand all Loading... | |
220 | 244 |
221 SetArchiveVars(opts.archive) | 245 SetArchiveVars(opts.archive) |
222 | 246 |
223 # Pick a starting point, try to get HEAD for this. | 247 # Pick a starting point, try to get HEAD for this. |
224 if opts.bad: | 248 if opts.bad: |
225 bad_rev = opts.bad | 249 bad_rev = opts.bad |
226 else: | 250 else: |
227 bad_rev = 0 | 251 bad_rev = 0 |
228 try: | 252 try: |
229 # Location of the latest build revision number | 253 # Location of the latest build revision number |
230 BUILD_LATEST_URL = '%s/LATEST' % (BUILD_BASE_URL) | 254 BUILD_LATEST_URL = '%s/LATEST/REVISION' % (BUILD_BASE_URL) |
231 nh = urllib.urlopen(BUILD_LATEST_URL) | 255 nh = urllib.urlopen(BUILD_LATEST_URL) |
232 latest = int(nh.read()) | 256 latest = int(nh.read()) |
233 nh.close() | 257 nh.close() |
234 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) | 258 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) |
235 if (bad_rev == ''): | 259 if (bad_rev == ''): |
236 bad_rev = latest | 260 bad_rev = latest |
237 bad_rev = int(bad_rev) | 261 bad_rev = int(bad_rev) |
238 except Exception, e: | 262 except Exception, e: |
239 print('Could not determine latest revision. This could be bad...') | 263 print('Could not determine latest revision. This could be bad...') |
240 bad_rev = int(raw_input('Bad revision: ')) | 264 bad_rev = int(raw_input('Bad revision: ')) |
(...skipping 24 matching lines...) Expand all Loading... | |
265 last_known_good_rev = revlist[good] | 289 last_known_good_rev = revlist[good] |
266 | 290 |
267 # Binary search time! | 291 # Binary search time! |
268 while good < bad: | 292 while good < bad: |
269 candidates = revlist[good:bad] | 293 candidates = revlist[good:bad] |
270 num_poss = len(candidates) | 294 num_poss = len(candidates) |
271 if num_poss > 10: | 295 if num_poss > 10: |
272 print('%d candidates. %d tries left.' % | 296 print('%d candidates. %d tries left.' % |
273 (num_poss, round(math.log(num_poss, 2)))) | 297 (num_poss, round(math.log(num_poss, 2)))) |
274 else: | 298 else: |
275 print('Candidates: %s' % revlist[good:bad]) | 299 print('Candidates: %s' % map(GetRevision, revlist[good:bad])) |
276 | 300 |
277 # Cut the problem in half... | 301 # Cut the problem in half... |
278 test = int((bad - good) / 2) + good | 302 test = int((bad - good) / 2) + good |
279 test_rev = revlist[test] | 303 test_rev = revlist[test] |
280 | 304 |
281 # Let the user give this rev a spin (in her own profile, if she wants). | 305 # Let the user give this rev a spin (in her own profile, if she wants). |
282 profile = opts.profile | 306 profile = opts.profile |
283 if not profile: | 307 if not profile: |
284 profile = 'profile' # In a temp dir. | 308 profile = 'profile' # In a temp dir. |
285 TryRevision(test_rev, profile, args) | 309 TryRevision(test_rev, profile, args) |
286 if AskIsGoodBuild(test_rev): | 310 if AskIsGoodBuild(test_rev): |
287 last_known_good_rev = revlist[good] | 311 last_known_good_rev = revlist[good] |
288 good = test + 1 | 312 good = test + 1 |
289 else: | 313 else: |
290 bad = test | 314 bad = test |
291 | 315 |
292 # We're done. Let the user know the results in an official manner. | 316 # We're done. Let the user know the results in an official manner. |
293 print('You are probably looking for build %d.' % revlist[bad]) | 317 bad_revision = GetRevision(revlist[bad]) |
318 print('You are probably looking for build %d.' % bad_revision) | |
294 print('CHANGELOG URL:') | 319 print('CHANGELOG URL:') |
295 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad])) | 320 print(CHANGELOG_URL % (GetRevision(last_known_good_rev), bad_revision)) |
296 print('Built at revision:') | 321 print('Built at revision:') |
297 print(BUILD_VIEWVC_URL % revlist[bad]) | 322 print(BUILD_VIEWVC_URL % bad_revision) |
298 | 323 |
299 if __name__ == '__main__': | 324 if __name__ == '__main__': |
300 sys.exit(main()) | 325 sys.exit(main()) |
OLD | NEW |