Index: tools/bisect-builds.py |
diff --git a/tools/bisect-builds.py b/tools/bisect-builds.py |
index 52737259c8f1527894e64a0a93ed344ce260a3e0..bcaddfa0b36f0c79e63d8c3dfaf15b2d40182c7e 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' |
@@ -141,11 +144,10 @@ 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 |
- |
+ # Whether the build should be downloaded using gsutil. |
+ self.download_with_gsutil = False |
# If the script is run from a local Chromium checkout, |
# "--use-local-repo" option can be used to make the script run faster. |
# It uses "git svn find-rev <SHA1>" command to convert git hash to svn |
@@ -153,7 +155,10 @@ class PathContext(object): |
self.use_local_repo = use_local_repo |
# 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 '/'. |
@@ -168,7 +173,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) |
@@ -223,6 +228,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 |
@@ -252,7 +262,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 |
@@ -260,10 +270,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.""" |
@@ -457,52 +472,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 |
@@ -527,6 +541,51 @@ class PathContext(object): |
connection.close() |
return final_list |
Robert Sesek
2015/02/04 17:27:54
nit: add another blank line
mikecase (-- gone --)
2015/02/04 22:50:44
Done.
|
+def CheckDepotToolsInPath(): |
+ delimiter = ';' if sys.platform.startswith('win') else ':' |
Robert Sesek
2015/02/04 17:27:54
nit: indent is wrong
mikecase (-- gone --)
2015/02/04 22:50:44
Good eye.
|
+ 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() |
@@ -545,7 +604,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)) |
@@ -580,20 +639,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(): |
+ |
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 |
@@ -601,7 +672,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) |
@@ -611,31 +682,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) |
@@ -682,7 +760,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) |
@@ -820,6 +897,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() |
@@ -1202,12 +1281,14 @@ def main(): |
opts.official_builds, opts.asan, opts.use_local_repo, |
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 |
Robert Sesek
2015/02/04 17:27:54
nit: space before (
mikecase (-- gone --)
2015/02/04 22:50:44
Done.
|
+ 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']) |
@@ -1236,6 +1317,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) |