2 # Copyright (c) 2010 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.
6 """Snapshot Build Bisect Tool
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
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,
12 it will ask you whether it is good or bad before continuing the search.
15 # Base URL to download snapshots from.
16 BUILD_BASE_URL
= 'http://build.chromium.org/f/chromium/snapshots/'
18 # The type (platform) of the build archive. This is what's passed in to the
19 # '-a/--archive' option.
20 BUILD_ARCHIVE_TYPE
= ''
22 # The selected archive to bisect.
23 BUILD_ARCHIVE_DIR
= ''
25 # The location of the builds.
26 BUILD_ARCHIVE_URL
= '/%d/'
28 # Name of the build archive.
31 # Directory name inside the archive.
34 # Name of the executable.
37 # URL to the ViewVC commit page.
38 BUILD_VIEWVC_URL
= 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d'
41 CHANGELOG_URL
= 'http://build.chromium.org/f/chromium/' \
42 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d'
44 ###############################################################################
58 def UnzipFilenameToDir(filename
, dir):
59 """Unzip |filename| to directory |dir|."""
60 zf
= zipfile
.ZipFile(filename
)
64 if not os
.path
.isdir(dir):
68 for info
in zf
.infolist():
70 if name
.endswith('/'): # dir
71 if not os
.path
.isdir(name
):
74 dir = os
.path
.dirname(name
)
75 if not os
.path
.isdir(dir):
77 out
= open(name
, 'wb')
78 out
.write(zf
.read(name
))
80 # Set permissions. Permission info in external_attr is shifted 16 bits.
81 os
.chmod(name
, info
.external_attr
>> 16L)
88 def SetArchiveVars(archive
):
89 """Set a bunch of global variables appropriate for the specified archive."""
90 global BUILD_ARCHIVE_TYPE
91 global BUILD_ARCHIVE_DIR
97 BUILD_ARCHIVE_TYPE
= archive
98 BUILD_ARCHIVE_DIR
= 'chromium-rel-' + BUILD_ARCHIVE_TYPE
100 if BUILD_ARCHIVE_TYPE
in ('linux', 'linux-64', 'linux-chromiumos'):
101 BUILD_ZIP_NAME
= 'chrome-linux.zip'
102 BUILD_DIR_NAME
= 'chrome-linux'
103 BUILD_EXE_NAME
= 'chrome'
104 elif BUILD_ARCHIVE_TYPE
in ('mac'):
105 BUILD_ZIP_NAME
= 'chrome-mac.zip'
106 BUILD_DIR_NAME
= 'chrome-mac'
107 BUILD_EXE_NAME
= 'Chromium.app/Contents/MacOS/Chromium'
108 elif BUILD_ARCHIVE_TYPE
in ('xp'):
109 BUILD_ZIP_NAME
= 'chrome-win32.zip'
110 BUILD_DIR_NAME
= 'chrome-win32'
111 BUILD_EXE_NAME
= 'chrome.exe'
113 BUILD_BASE_URL
+= BUILD_ARCHIVE_DIR
115 def ParseDirectoryIndex(url
):
116 """Parses the HTML directory listing into a list of revision numbers."""
117 handle
= urllib
.urlopen(url
)
118 dirindex
= handle
.read()
120 return re
.findall(r
'<a href="([0-9]*)/">\1/</a>', dirindex
)
122 def 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
)
131 def TryRevision(rev
, profile
, args
):
132 """Downloads revision |rev|, unzips it, and opens it for the user to test.
133 |profile| is the profile to use."""
134 # Do this in a temp dir so we don't collide with user files.
136 tempdir
= tempfile
.mkdtemp(prefix
='bisect_tmp')
140 download_url
= BUILD_BASE_URL
+ (BUILD_ARCHIVE_URL
% rev
) + BUILD_ZIP_NAME
141 def _Reporthook(blocknum
, blocksize
, totalsize
):
142 size
= blocknum
* blocksize
143 if totalsize
== -1: # Total size not known.
144 progress
= "Received %d bytes" % size
146 size
= min(totalsize
, size
)
147 progress
= "Received %d of %d bytes, %.2f%%" % (
148 size
, totalsize
, 100.0 * size
/ totalsize
)
149 # Send a \r to let all progress messages use just one line of output.
150 sys
.stdout
.write("\r" + progress
)
153 print 'Fetching ' + download_url
154 urllib
.urlretrieve(download_url
, BUILD_ZIP_NAME
, _Reporthook
)
157 print('Could not retrieve the download. Sorry.')
161 print 'Unzipping ...'
162 UnzipFilenameToDir(BUILD_ZIP_NAME
, os
.curdir
)
164 # Tell the system to open the app.
165 args
= ['--user-data-dir=%s' % profile
] + args
166 flags
= ' '.join(map(pipes
.quote
, args
))
167 exe
= os
.path
.join(os
.getcwd(), BUILD_DIR_NAME
, BUILD_EXE_NAME
)
168 cmd
= '%s %s' % (exe
, flags
)
169 print 'Running %s' % cmd
173 print 'Cleaning temp dir ...'
175 shutil
.rmtree(tempdir
, True)
180 def AskIsGoodBuild(rev
):
181 """Ask the user whether build |rev| is good or bad."""
182 # Loop until we get a response that we can parse.
184 response
= raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev
))
185 if response
and response
in ('g', 'b'):
186 return response
== 'g'
189 usage
= ('%prog [options] [-- chromium-options]\n'
190 'Perform binary search on the snapshot builds.\n'
192 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
193 parser
= optparse
.OptionParser(usage
=usage
)
194 # Strangely, the default help output doesn't include the choice list.
195 choices
= ['mac', 'xp', 'linux', 'linux-64', 'linux-chromiumos']
196 parser
.add_option('-a', '--archive',
198 help = 'The buildbot archive to bisect [%s].' %
200 parser
.add_option('-b', '--bad', type = 'int',
201 help = 'The bad revision to bisect to.')
202 parser
.add_option('-g', '--good', type = 'int',
203 help = 'The last known good revision to bisect from.')
204 parser
.add_option('-p', '--profile', '--user-data-dir', type = 'str',
205 help = 'Profile to use; this will not reset every run. ' +
206 'Defaults to a clean profile.')
207 (opts
, args
) = parser
.parse_args()
209 if opts
.archive
is None:
210 print 'Error: missing required parameter: --archive'
215 if opts
.bad
and opts
.good
and (opts
.good
> opts
.bad
):
216 print ('The good revision (%d) must precede the bad revision (%d).\n' %
217 (opts
.good
, opts
.bad
))
221 SetArchiveVars(opts
.archive
)
223 # Pick a starting point, try to get HEAD for this.
229 # Location of the latest build revision number
230 BUILD_LATEST_URL
= '%s/LATEST' % (BUILD_BASE_URL
)
231 nh
= urllib
.urlopen(BUILD_LATEST_URL
)
232 latest
= int(nh
.read())
234 bad_rev
= raw_input('Bad revision [HEAD:%d]: ' % latest
)
237 bad_rev
= int(bad_rev
)
239 print('Could not determine latest revision. This could be bad...')
240 bad_rev
= int(raw_input('Bad revision: '))
242 # Find out when we were good.
248 good_rev
= int(raw_input('Last known good [0]: '))
252 # Get a list of revisions to bisect across.
253 revlist
= GetRevList(good_rev
, bad_rev
)
254 if len(revlist
) < 2: # Don't have enough builds to bisect
255 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist
258 # If we don't have a |good_rev|, set it to be the first revision possible.
260 good_rev
= revlist
[0]
262 # These are indexes of |revlist|.
264 bad
= len(revlist
) - 1
265 last_known_good_rev
= revlist
[good
]
267 # Binary search time!
269 candidates
= revlist
[good
:bad
]
270 num_poss
= len(candidates
)
272 print('%d candidates. %d tries left.' %
273 (num_poss
, round(math
.log(num_poss
, 2))))
275 print('Candidates: %s' % revlist
[good
:bad
])
277 # Cut the problem in half...
278 test
= int((bad
- good
) / 2) + good
279 test_rev
= revlist
[test
]
281 # Let the user give this rev a spin (in her own profile, if she wants).
282 profile
= opts
.profile
284 profile
= 'profile' # In a temp dir.
285 TryRevision(test_rev
, profile
, args
)
286 if AskIsGoodBuild(test_rev
):
287 last_known_good_rev
= revlist
[good
]
292 # We're done. Let the user know the results in an official manner.
293 print('You are probably looking for build %d.' % revlist
[bad
])
294 print('CHANGELOG URL:')
295 print(CHANGELOG_URL
% (last_known_good_rev
, revlist
[bad
]))
296 print('Built at revision:')
297 print(BUILD_VIEWVC_URL
% revlist
[bad
])
299 if __name__
== '__main__':