summaryrefslogtreecommitdiffstats
path: root/build/build-bisect.py
diff options
context:
space:
mode:
Diffstat (limited to 'build/build-bisect.py')
-rwxr-xr-xbuild/build-bisect.py284
1 files changed, 284 insertions, 0 deletions
diff --git a/build/build-bisect.py b/build/build-bisect.py
new file mode 100755
index 0000000..64504bc
--- /dev/null
+++ b/build/build-bisect.py
@@ -0,0 +1,284 @@
+#!/usr/bin/python2.5
+# Copyright (c) 2010 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Snapshot Build Bisect Tool
+
+This script bisects a snapshot archive using binary search. It starts at
+a bad revision (it will try to guess HEAD) and asks for a last known-good
+revision. It will then binary search across this revision range by downloading,
+unzipping, and opening Chromium for you. After testing the specific revision,
+it will ask you whether it is good or bad before continuing the search.
+"""
+
+# Base URL to download snapshots from.
+BUILD_BASE_URL = 'http://build.chromium.org/buildbot/snapshots/'
+
+# The type (platform) of the build archive. This is what's passed in to the
+# '-a/--archive' option.
+BUILD_ARCHIVE_TYPE = ''
+
+# The selected archive to bisect.
+BUILD_ARCHIVE_DIR = ''
+
+# The location of the builds.
+BUILD_ARCHIVE_URL = '/%d/'
+
+# Name of the build archive.
+BUILD_ZIP_NAME = ''
+
+# Directory name inside the archive.
+BUILD_DIR_NAME = ''
+
+# Name of the executable.
+BUILD_EXE_NAME = ''
+
+# URL to the ViewVC commit page.
+BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d'
+
+# Changelogs URL
+CHANGELOG_URL = 'http://build.chromium.org/buildbot/' \
+ 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d'
+
+###############################################################################
+
+import math
+import optparse
+import os
+import pipes
+import re
+import shutil
+import sys
+import tempfile
+import urllib
+import zipfile
+
+
+def UnzipFilenameToDir(filename, dir):
+ """Unzip |filename| to directory |dir|."""
+ zf = zipfile.ZipFile(filename)
+ # Make base.
+ pushd = os.getcwd()
+ try:
+ if not os.path.isdir(dir):
+ os.mkdir(dir)
+ os.chdir(dir)
+ # Extract files.
+ for info in zf.infolist():
+ name = info.filename
+ if name.endswith('/'): # dir
+ if not os.path.isdir(name):
+ os.makedirs(name)
+ else: # file
+ dir = os.path.dirname(name)
+ if not os.path.isdir(dir):
+ os.makedirs(dir)
+ out = open(name, 'wb')
+ out.write(zf.read(name))
+ out.close()
+ # Set permissions. Permission info in external_attr is shifted 16 bits.
+ os.chmod(name, info.external_attr >> 16L)
+ os.chdir(pushd)
+ except Exception, e:
+ print >>sys.stderr, e
+ sys.exit(1)
+
+
+def SetArchiveVars(archive):
+ """Set a bunch of global variables appropriate for the specified archive."""
+ global BUILD_ARCHIVE_TYPE
+ global BUILD_ARCHIVE_DIR
+ global BUILD_ZIP_NAME
+ global BUILD_DIR_NAME
+ global BUILD_EXE_NAME
+ global BUILD_BASE_URL
+
+ BUILD_ARCHIVE_TYPE = archive
+ BUILD_ARCHIVE_DIR = 'chromium-rel-' + BUILD_ARCHIVE_TYPE
+
+ if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64'):
+ BUILD_ZIP_NAME = 'chrome-linux.zip'
+ BUILD_DIR_NAME = 'chrome-linux'
+ BUILD_EXE_NAME = 'chrome'
+ elif BUILD_ARCHIVE_TYPE in ('mac'):
+ BUILD_ZIP_NAME = 'chrome-mac.zip'
+ BUILD_DIR_NAME = 'chrome-mac'
+ BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium'
+ elif BUILD_ARCHIVE_TYPE in ('xp'):
+ BUILD_ZIP_NAME = 'chrome-win32.zip'
+ BUILD_DIR_NAME = 'chrome-win32'
+ BUILD_EXE_NAME = 'chrome.exe'
+
+ BUILD_BASE_URL += BUILD_ARCHIVE_DIR
+
+def ParseDirectoryIndex(url):
+ """Parses the HTML directory listing into a list of revision numbers."""
+ handle = urllib.urlopen(url)
+ dirindex = handle.read()
+ handle.close()
+ return re.findall(r'<a href="([0-9]*)/">\1/</a>', dirindex)
+
+def GetRevList(good, bad):
+ """Gets the list of revision numbers between |good| and |bad|."""
+ # Download the main revlist.
+ revlist = ParseDirectoryIndex(BUILD_BASE_URL)
+ revlist = map(int, revlist)
+ revlist = filter(lambda r: range(good, bad).__contains__(int(r)), revlist)
+ revlist.sort()
+ return revlist
+
+def TryRevision(rev, profile, args):
+ """Downloads revision |rev|, unzips it, and opens it for the user to test.
+ |profile| is the profile to use."""
+ # Do this in a temp dir so we don't collide with user files.
+ cwd = os.getcwd()
+ tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
+ os.chdir(tempdir)
+
+ # Download the file.
+ download_url = BUILD_BASE_URL + (BUILD_ARCHIVE_URL % rev) + BUILD_ZIP_NAME
+ try:
+ print 'Fetching ' + download_url
+ urllib.urlretrieve(download_url, BUILD_ZIP_NAME)
+ except Exception, e:
+ print('Could not retrieve the download. Sorry.')
+ sys.exit(-1)
+
+ # Unzip the file.
+ print 'Unziping ...'
+ UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir)
+
+ # Tell the system to open the app.
+ args = ['--user-data-dir=%s' % profile] + args
+ flags = ' '.join(map(pipes.quote, args))
+ exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME)
+ cmd = '%s %s' % (exe, flags)
+ print 'Running %s' % cmd
+ os.system(cmd)
+
+ os.chdir(cwd)
+ print 'Cleaning temp dir ...'
+ try:
+ shutil.rmtree(tempdir, True)
+ except Exception, e:
+ pass
+
+
+def AskIsGoodBuild(rev):
+ """Ask the user whether build |rev| is good or bad."""
+ # Loop until we get a response that we can parse.
+ while True:
+ response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev))
+ if response and response in ('g', 'b'):
+ return response == 'g'
+
+def main():
+ usage = ('%prog [options] [-- chromium-options]\n'
+ 'Perform binary search on the snapshot builds.')
+ parser = optparse.OptionParser(usage=usage)
+ # Strangely, the default help output doesn't include the choice list.
+ choices = ['mac', 'xp', 'linux', 'linux-64']
+ parser.add_option('-a', '--archive',
+ choices = choices,
+ help = 'The buildbot archive to bisect [%s].' %
+ '|'.join(choices))
+ parser.add_option('-b', '--bad', type = 'int',
+ help = 'The bad revision to bisect to.')
+ parser.add_option('-g', '--good', type = 'int',
+ help = 'The last known good revision to bisect from.')
+ parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
+ help = 'Profile to use; this will not reset every run. ' +
+ 'Defaults to a clean profile.')
+ (opts, args) = parser.parse_args()
+
+ if opts.archive is None:
+ parser.print_help()
+ return 1
+
+ if opts.bad and opts.good and (opts.good > opts.bad):
+ print ('The good revision (%d) must precede the bad revision (%d).\n' %
+ (opts.good, opts.bad))
+ parser.print_help()
+ return 1
+
+ SetArchiveVars(opts.archive)
+
+ # Pick a starting point, try to get HEAD for this.
+ if opts.bad:
+ bad_rev = opts.bad
+ else:
+ bad_rev = 0
+ try:
+ # Location of the latest build revision number
+ BUILD_LATEST_URL = '%s/LATEST' % (BUILD_BASE_URL)
+ nh = urllib.urlopen(BUILD_LATEST_URL)
+ latest = int(nh.read())
+ nh.close()
+ bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest)
+ if (bad_rev == ''):
+ bad_rev = latest
+ bad_rev = int(bad_rev)
+ except Exception, e:
+ print('Could not determine latest revision. This could be bad...')
+ bad_rev = int(raw_input('Bad revision: '))
+
+ # Find out when we were good.
+ if opts.good:
+ good_rev = opts.good
+ else:
+ good_rev = 0
+ try:
+ good_rev = int(raw_input('Last known good [0]: '))
+ except Exception, e:
+ pass
+
+ # Get a list of revisions to bisect across.
+ revlist = GetRevList(good_rev, bad_rev)
+ if len(revlist) < 2: # Don't have enough builds to bisect
+ print 'We don\'t have enough builds to bisect. revlist: %s' % revlist
+ sys.exit(1)
+
+ # If we don't have a |good_rev|, set it to be the first revision possible.
+ if good_rev == 0:
+ good_rev = revlist[0]
+
+ # These are indexes of |revlist|.
+ good = 0
+ bad = len(revlist) - 1
+ last_known_good_rev = revlist[good]
+
+ # Binary search time!
+ while good < bad:
+ candidates = revlist[good:bad]
+ num_poss = len(candidates)
+ if num_poss > 10:
+ print('%d candidates. %d tries left.' %
+ (num_poss, round(math.log(num_poss, 2))))
+ else:
+ print('Candidates: %s' % revlist[good:bad])
+
+ # Cut the problem in half...
+ test = int((bad - good) / 2) + good
+ test_rev = revlist[test]
+
+ # Let the user give this rev a spin (in her own profile, if she wants).
+ profile = opts.profile
+ if not profile:
+ profile = 'profile' # In a temp dir.
+ TryRevision(test_rev, profile, args)
+ if AskIsGoodBuild(test_rev):
+ last_known_good_rev = revlist[good]
+ good = test + 1
+ else:
+ bad = test
+
+ # We're done. Let the user know the results in an official manner.
+ print('You are probably looking for build %d.' % revlist[bad])
+ print('CHANGELOG URL:')
+ print(CHANGELOG_URL % (last_known_good_rev, revlist[bad]))
+ print('Built at revision:')
+ print(BUILD_VIEWVC_URL % revlist[bad])
+
+if __name__ == '__main__':
+ sys.exit(main())