Index: tools/bisect-builds.py
|
diff --git a/tools/bisect-builds.py b/tools/bisect-builds.py
|
index 5e7db2db5018e272dcc0a1d02c23254cf92b2d5d..99fcb45727904c5338fd122613b97ac4b68db49e 100755
|
--- a/tools/bisect-builds.py
|
+++ b/tools/bisect-builds.py
|
@@ -32,6 +32,9 @@ OFFICIAL_BASE_URL = 'http://%s/%s' % (GOOGLE_APIS_URL, GS_BUCKET_NAME)
|
# URL template for viewing changelogs between revisions.
|
CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s')
|
|
+# GS bucket name for tip of tree android builds.
|
+ANDROID_TOT_BUCKET_NAME = ('chrome-android-tot/bisect')
|
+
|
# GS bucket name for android unsigned official builds.
|
ANDROID_BUCKET_NAME = 'chrome-unsigned/android-C4MPAR1'
|
|
@@ -142,8 +145,6 @@ class PathContext(object):
|
# the build.
|
self.githash_svn_dict = {}
|
self.pdf_path = pdf_path
|
- # android_apk defaults to Chrome.apk
|
- self.android_apk = android_apk if android_apk else 'Chrome.apk'
|
# The name of the ZIP file in a revision directory on the server.
|
self.archive_name = None
|
|
@@ -161,8 +162,14 @@ class PathContext(object):
|
else:
|
self.local_src_path = None
|
|
+ # Whether the build should be downloaded using gsutil.
|
+ self.download_with_gsutil = False
|
+
|
# If the script is being used for android builds.
|
- self.android = self.platform.startswith('android')
|
+ self.is_android = self.platform.startswith('android')
|
+ # android_apk defaults to Chrome.apk
|
+ if self.is_android:
|
+ self.android_apk = android_apk if android_apk else 'Chrome.apk'
|
|
# Set some internal members:
|
# _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
|
@@ -177,7 +184,7 @@ class PathContext(object):
|
self.archive_name = 'chrome-win32.zip'
|
self._archive_extract_dir = 'chrome-win32'
|
self._binary_name = 'chrome.exe'
|
- elif self.android:
|
+ elif self.is_android:
|
pass
|
else:
|
raise Exception('Invalid platform: %s' % self.platform)
|
@@ -234,6 +241,11 @@ class PathContext(object):
|
self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
|
elif self.platform == 'win':
|
self._listing_platform_dir = 'Win/'
|
+ elif self.platform == 'android-arm':
|
+ self.archive_name = 'bisect_android.zip'
|
+ # Need to download builds using gsutil instead of visiting url for
|
+ # authentication reasons.
|
+ self.download_with_gsutil = True
|
|
def GetASANPlatformDir(self):
|
"""ASAN builds are in directories like "linux-release", or have filenames
|
@@ -263,7 +275,7 @@ class PathContext(object):
|
ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
|
self.GetASANBaseName(), revision)
|
if self.is_official:
|
- if self.android:
|
+ if self.is_android:
|
official_base_url = ANDROID_OFFICIAL_BASE_URL
|
else:
|
official_base_url = OFFICIAL_BASE_URL
|
@@ -271,10 +283,15 @@ class PathContext(object):
|
official_base_url, revision, self._listing_platform_dir,
|
self.archive_name)
|
else:
|
- if str(revision) in self.githash_svn_dict:
|
- revision = self.githash_svn_dict[str(revision)]
|
- return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
|
- revision, self.archive_name)
|
+ if self.is_android:
|
+ # These files need to be downloaded through gsutil.
|
+ return ('gs://%s/%s/%s' % (ANDROID_TOT_BUCKET_NAME, revision,
|
+ self.archive_name))
|
+ else:
|
+ if str(revision) in self.githash_svn_dict:
|
+ revision = self.githash_svn_dict[str(revision)]
|
+ return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
|
+ revision, self.archive_name)
|
|
def GetLastChangeURL(self):
|
"""Returns a URL to the LAST_CHANGE file."""
|
@@ -544,52 +561,51 @@ class PathContext(object):
|
self.bad_revision)
|
return revlist
|
|
+ def _GetHashToNumberDict(self):
|
+ """Gets the mapping of git hashes to git numbers from Google Storage."""
|
+ gs_file = 'gs://%s/gitnumbers_dict.json' % ANDROID_TOT_BUCKET_NAME
|
+ local_file = 'gitnumbers_dict.json'
|
+ GsutilDownload(gs_file, local_file)
|
+ json_data = open(local_file).read()
|
+ os.remove(local_file)
|
+ return json.loads(json_data)
|
+
|
+ def GetAndroidToTRevisions(self):
|
+ """Gets the ordered list of revisions between self.good_revision and
|
+ self.bad_revision from the Android tip of tree GS bucket.
|
+ """
|
+ # Dictionary that maps git hashes to git numbers. The git numbers
|
+ # let us order the revisions.
|
+ hash_to_num = self._GetHashToNumberDict()
|
+ try:
|
+ good_rev_num = hash_to_num[self.good_revision]
|
+ bad_rev_num = hash_to_num[self.bad_revision]
|
+ except KeyError:
|
+ exit('Error. Make sure the good and bad revisions are valid git hashes.')
|
+
|
+ # List of all builds by their git hashes in the storage bucket.
|
+ hash_list = GsutilList(ANDROID_TOT_BUCKET_NAME)
|
+
|
+ # Get list of builds that we want to bisect over.
|
+ final_list = []
|
+ minnum = min(good_rev_num, bad_rev_num)
|
+ maxnum = max(good_rev_num, bad_rev_num)
|
+ for githash in hash_list:
|
+ if len(githash) != 40:
|
+ continue
|
+ gitnumber = hash_to_num[githash]
|
+ if minnum < gitnumber < maxnum:
|
+ final_list.append(githash)
|
+ return sorted(final_list, key=lambda h: hash_to_num[h])
|
+
|
def GetOfficialBuildsList(self):
|
"""Gets the list of official build numbers between self.good_revision and
|
self.bad_revision."""
|
|
- def CheckDepotToolsInPath():
|
- delimiter = ';' if sys.platform.startswith('win') else ':'
|
- path_list = os.environ['PATH'].split(delimiter)
|
- for path in path_list:
|
- if path.rstrip(os.path.sep).endswith('depot_tools'):
|
- return path
|
- return None
|
-
|
- def RunGsutilCommand(args):
|
- gsutil_path = CheckDepotToolsInPath()
|
- if gsutil_path is None:
|
- print ('Follow the instructions in this document '
|
- 'http://dev.chromium.org/developers/how-tos/install-depot-tools'
|
- ' to install depot_tools and then try again.')
|
- sys.exit(1)
|
- gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil')
|
- gsutil = subprocess.Popen([sys.executable, gsutil_path] + args,
|
- stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
- env=None)
|
- stdout, stderr = gsutil.communicate()
|
- if gsutil.returncode:
|
- if (re.findall(r'status[ |=]40[1|3]', stderr) or
|
- stderr.startswith(CREDENTIAL_ERROR_MESSAGE)):
|
- print ('Follow these steps to configure your credentials and try'
|
- ' running the bisect-builds.py again.:\n'
|
- ' 1. Run "python %s config" and follow its instructions.\n'
|
- ' 2. If you have a @google.com account, use that account.\n'
|
- ' 3. For the project-id, just enter 0.' % gsutil_path)
|
- sys.exit(1)
|
- else:
|
- raise Exception('Error running the gsutil command: %s' % stderr)
|
- return stdout
|
-
|
- def GsutilList(bucket):
|
- query = 'gs://%s/' % bucket
|
- stdout = RunGsutilCommand(['ls', query])
|
- return [url[len(query):].strip('/') for url in stdout.splitlines()]
|
-
|
# Download the revlist and filter for just the range between good and bad.
|
minrev = min(self.good_revision, self.bad_revision)
|
maxrev = max(self.good_revision, self.bad_revision)
|
- if self.android:
|
+ if self.is_android:
|
gs_bucket_name = ANDROID_BUCKET_NAME
|
else:
|
gs_bucket_name = GS_BUCKET_NAME
|
@@ -614,6 +630,52 @@ class PathContext(object):
|
connection.close()
|
return final_list
|
|
+
|
+def CheckDepotToolsInPath():
|
+ delimiter = ';' if sys.platform.startswith('win') else ':'
|
+ path_list = os.environ['PATH'].split(delimiter)
|
+ for path in path_list:
|
+ if path.rstrip(os.path.sep).endswith('depot_tools'):
|
+ return path
|
+ return None
|
+
|
+
|
+def RunGsutilCommand(args):
|
+ gsutil_path = CheckDepotToolsInPath()
|
+ if gsutil_path is None:
|
+ print ('Follow the instructions in this document '
|
+ 'http://dev.chromium.org/developers/how-tos/install-depot-tools'
|
+ ' to install depot_tools and then try again.')
|
+ sys.exit(1)
|
+ gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil')
|
+ gsutil = subprocess.Popen([sys.executable, gsutil_path] + args,
|
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
+ env=None)
|
+ stdout, stderr = gsutil.communicate()
|
+ if gsutil.returncode:
|
+ if (re.findall(r'status[ |=]40[1|3]', stderr) or
|
+ stderr.startswith(CREDENTIAL_ERROR_MESSAGE)):
|
+ print ('Follow these steps to configure your credentials and try'
|
+ ' running the bisect-builds.py again.:\n'
|
+ ' 1. Run "python %s config" and follow its instructions.\n'
|
+ ' 2. If you have a @google.com account, use that account.\n'
|
+ ' 3. For the project-id, just enter 0.' % gsutil_path)
|
+ sys.exit(1)
|
+ else:
|
+ raise Exception('Error running the gsutil command: %s' % stderr)
|
+ return stdout
|
+
|
+
|
+def GsutilList(bucket):
|
+ query = 'gs://%s/' % bucket
|
+ stdout = RunGsutilCommand(['ls', query])
|
+ return [url[len(query):].strip('/') for url in stdout.splitlines()]
|
+
|
+
|
+def GsutilDownload(gs_download_url, filename):
|
+ RunGsutilCommand(['cp', gs_download_url, filename])
|
+
|
+
|
def UnzipFilenameToDir(filename, directory):
|
"""Unzip |filename| to |directory|."""
|
cwd = os.getcwd()
|
@@ -632,7 +694,7 @@ def UnzipFilenameToDir(filename, directory):
|
os.makedirs(name)
|
else: # file
|
directory = os.path.dirname(name)
|
- if not os.path.isdir(directory):
|
+ if directory and not os.path.isdir(directory):
|
os.makedirs(directory)
|
out = open(name, 'wb')
|
out.write(zf.read(name))
|
@@ -667,20 +729,32 @@ def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
|
# Send a \r to let all progress messages use just one line of output.
|
sys.stdout.write('\r' + progress)
|
sys.stdout.flush()
|
-
|
download_url = context.GetDownloadURL(rev)
|
try:
|
- urllib.urlretrieve(download_url, filename, ReportHook)
|
+ if context.download_with_gsutil:
|
+ GsutilDownload(download_url, filename)
|
+ else:
|
+ urllib.urlretrieve(download_url, filename, ReportHook)
|
if progress_event and progress_event.isSet():
|
print
|
+
|
except RuntimeError:
|
pass
|
|
|
+def RunADBCommand(args):
|
+ cmd = ['adb'] + args
|
+ adb = subprocess.Popen(['adb'] + args,
|
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
+ env=None)
|
+ stdout, stderr = adb.communicate()
|
+ return stdout
|
+
|
+
|
def IsADBInstalled():
|
"""Checks if ADB is in the environment path."""
|
try:
|
- adb_output = subprocess.check_output(['adb', 'version'])
|
+ adb_output = RunADBCommand(['version'])
|
return ('Android Debug Bridge' in adb_output)
|
except OSError:
|
return False
|
@@ -688,7 +762,7 @@ def IsADBInstalled():
|
|
def GetAndroidDeviceList():
|
"""Returns the list of Android devices attached to the host machine."""
|
- lines = subprocess.check_output(['adb', 'devices']).split('\n')[1:]
|
+ lines = RunADBCommand(['devices']).split('\n')[1:]
|
devices = []
|
for line in lines:
|
m = re.match('^(.*?)\s+device$', line)
|
@@ -698,31 +772,38 @@ def GetAndroidDeviceList():
|
return devices
|
|
|
-def RunAndroidRevision(context, revision, apk_file):
|
+def RunAndroidRevision(context, revision, zip_file):
|
"""Given a Chrome apk, install it on a local device, and launch Chrome."""
|
devices = GetAndroidDeviceList()
|
if len(devices) is not 1:
|
sys.exit('Please have 1 Android device plugged in. %d devices found'
|
% len(devices))
|
|
- devnull = open(os.devnull, 'w')
|
- package_name = ANDROID_CHROME_PACKAGE_NAME[context.android_apk]
|
+ if context.is_official:
|
+ # Downloaded file is just the .apk in this case.
|
+ apk_file = zip_file
|
+ else:
|
+ cwd = os.getcwd()
|
+ tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
|
+ UnzipFilenameToDir(zip_file, tempdir)
|
+ os.chdir(tempdir)
|
+ apk_file = context.android_apk
|
|
- print 'Installing new Chrome version...'
|
- subprocess.call(['adb', 'install', '-r', '-d', apk_file],
|
- stdout=devnull, stderr=devnull)
|
+ package_name = ANDROID_CHROME_PACKAGE_NAME[context.android_apk]
|
+ print 'Installing...'
|
+ RunADBCommand(['install', '-r', '-d', apk_file])
|
|
print 'Launching Chrome...\n'
|
- subprocess.call(['adb', 'shell', 'am', 'start', '-a',
|
+ RunADBCommand(['shell', 'am', 'start', '-a',
|
'android.intent.action.VIEW', '-n', package_name +
|
- '/com.google.android.apps.chrome.Main'], stdout=devnull, stderr=devnull)
|
+ '/com.google.android.apps.chrome.Main'])
|
|
|
def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
|
"""Given a zipped revision, unzip it and run the test."""
|
print 'Trying revision %s...' % str(revision)
|
|
- if context.android:
|
+ if context.is_android:
|
RunAndroidRevision(context, revision, zip_file)
|
# TODO(mikecase): Support running command to auto-bisect Android.
|
return (None, None, None)
|
@@ -779,7 +860,6 @@ def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
|
stderr=subprocess.PIPE)
|
(stdout, stderr) = subproc.communicate()
|
results.append((subproc.returncode, stdout, stderr))
|
-
|
os.chdir(cwd)
|
try:
|
shutil.rmtree(tempdir, True)
|
@@ -917,6 +997,8 @@ def Bisect(context,
|
'%s-%s' % (str(rev), context.archive_name))
|
if context.is_official:
|
revlist = context.GetOfficialBuildsList()
|
+ elif context.is_android: # Android non-official
|
+ revlist = context.GetAndroidToTRevisions()
|
else:
|
revlist = context.GetRevList()
|
|
@@ -1299,12 +1381,14 @@ def main():
|
opts.official_builds, opts.asan, opts.use_local_cache,
|
opts.flash_path, opts.pdf_path, opts.apk)
|
|
- # TODO(mikecase): Add support to bisect on nonofficial builds for Android.
|
- if context.android and not opts.official_builds:
|
- sys.exit('Can only bisect on official builds for Android.')
|
+ if context.is_android and not opts.official_builds:
|
+ if (context.platform != 'android-arm' or
|
+ context.android_apk != 'Chrome.apk'):
|
+ sys.exit('For non-official builds, can only bisect'
|
+ ' Chrome.apk arm builds.')
|
|
# If bisecting Android, we make sure we have ADB setup.
|
- if context.android:
|
+ if context.is_android:
|
if opts.adb_path:
|
os.environ['PATH'] = '%s:%s' % (os.path.dirname(opts.adb_path),
|
os.environ['PATH'])
|
@@ -1333,6 +1417,9 @@ def main():
|
if opts.official_builds:
|
context.good_revision = LooseVersion(context.good_revision)
|
context.bad_revision = LooseVersion(context.bad_revision)
|
+ elif context.is_android:
|
+ # Revisions are git hashes and should be left as strings.
|
+ pass
|
else:
|
context.good_revision = int(context.good_revision)
|
context.bad_revision = int(context.bad_revision)
|
|