Chromium Code Reviews| 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) |