Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Snapshot Build Bisect Tool | 6 """Snapshot Build Bisect Tool |
| 7 | 7 |
| 8 This script bisects a snapshot archive using binary search. It starts at | 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 | 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, | 10 revision. It will then binary search across this revision range by downloading, |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 25 | 25 |
| 26 # Base URL for downloading official builds. | 26 # Base URL for downloading official builds. |
| 27 GOOGLE_APIS_URL = 'commondatastorage.googleapis.com' | 27 GOOGLE_APIS_URL = 'commondatastorage.googleapis.com' |
| 28 | 28 |
| 29 # The base URL for official builds. | 29 # The base URL for official builds. |
| 30 OFFICIAL_BASE_URL = 'http://%s/%s' % (GOOGLE_APIS_URL, GS_BUCKET_NAME) | 30 OFFICIAL_BASE_URL = 'http://%s/%s' % (GOOGLE_APIS_URL, GS_BUCKET_NAME) |
| 31 | 31 |
| 32 # URL template for viewing changelogs between revisions. | 32 # URL template for viewing changelogs between revisions. |
| 33 CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s') | 33 CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s') |
| 34 | 34 |
| 35 # GS bucket name for tip of tree android builds. | |
| 36 ANDROID_TOT_BUCKET_NAME = ('chrome-android-tot/bisect') | |
| 37 | |
| 35 # GS bucket name for android unsigned official builds. | 38 # GS bucket name for android unsigned official builds. |
| 36 ANDROID_BUCKET_NAME = 'chrome-unsigned/android-C4MPAR1' | 39 ANDROID_BUCKET_NAME = 'chrome-unsigned/android-C4MPAR1' |
| 37 | 40 |
| 38 # The base URL for android official builds. | 41 # The base URL for android official builds. |
| 39 ANDROID_OFFICIAL_BASE_URL = 'http://%s/%s' % (GOOGLE_APIS_URL, ANDROID_BUCKET_NA ME) | 42 ANDROID_OFFICIAL_BASE_URL = 'http://%s/%s' % (GOOGLE_APIS_URL, ANDROID_BUCKET_NA ME) |
| 40 | 43 |
| 41 # URL to convert SVN revision to git hash. | 44 # URL to convert SVN revision to git hash. |
| 42 CRREV_URL = ('https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/') | 45 CRREV_URL = ('https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/') |
| 43 | 46 |
| 44 # URL template for viewing changelogs between official versions. | 47 # URL template for viewing changelogs between official versions. |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 134 self.is_official = is_official | 137 self.is_official = is_official |
| 135 self.is_asan = is_asan | 138 self.is_asan = is_asan |
| 136 self.build_type = 'release' | 139 self.build_type = 'release' |
| 137 self.flash_path = flash_path | 140 self.flash_path = flash_path |
| 138 # Dictionary which stores svn revision number as key and it's | 141 # Dictionary which stores svn revision number as key and it's |
| 139 # corresponding git hash as value. This data is populated in | 142 # corresponding git hash as value. This data is populated in |
| 140 # _FetchAndParse and used later in GetDownloadURL while downloading | 143 # _FetchAndParse and used later in GetDownloadURL while downloading |
| 141 # the build. | 144 # the build. |
| 142 self.githash_svn_dict = {} | 145 self.githash_svn_dict = {} |
| 143 self.pdf_path = pdf_path | 146 self.pdf_path = pdf_path |
| 144 # android_apk defaults to Chrome.apk | |
| 145 self.android_apk = android_apk if android_apk else 'Chrome.apk' | |
| 146 # The name of the ZIP file in a revision directory on the server. | 147 # The name of the ZIP file in a revision directory on the server. |
| 147 self.archive_name = None | 148 self.archive_name = None |
| 148 | 149 # Whether the build should be downloaded using gsutil. |
| 150 self.download_with_gsutil = False | |
| 149 # If the script is run from a local Chromium checkout, | 151 # If the script is run from a local Chromium checkout, |
| 150 # "--use-local-repo" option can be used to make the script run faster. | 152 # "--use-local-repo" option can be used to make the script run faster. |
| 151 # It uses "git svn find-rev <SHA1>" command to convert git hash to svn | 153 # It uses "git svn find-rev <SHA1>" command to convert git hash to svn |
| 152 # revision number. | 154 # revision number. |
| 153 self.use_local_repo = use_local_repo | 155 self.use_local_repo = use_local_repo |
| 154 | 156 |
| 155 # If the script is being used for android builds. | 157 # If the script is being used for android builds. |
| 156 self.android = self.platform.startswith('android') | 158 self.is_android = self.platform.startswith('android') |
| 159 # android_apk defaults to Chrome.apk | |
| 160 if self.is_android: | |
| 161 self.android_apk = android_apk if android_apk else 'Chrome.apk' | |
| 157 | 162 |
| 158 # Set some internal members: | 163 # Set some internal members: |
| 159 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. | 164 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. |
| 160 # _archive_extract_dir = Uncompressed directory in the archive_name file. | 165 # _archive_extract_dir = Uncompressed directory in the archive_name file. |
| 161 # _binary_name = The name of the executable to run. | 166 # _binary_name = The name of the executable to run. |
| 162 if self.platform in ('linux', 'linux64', 'linux-arm'): | 167 if self.platform in ('linux', 'linux64', 'linux-arm'): |
| 163 self._binary_name = 'chrome' | 168 self._binary_name = 'chrome' |
| 164 elif self.platform in ('mac', 'mac64'): | 169 elif self.platform in ('mac', 'mac64'): |
| 165 self.archive_name = 'chrome-mac.zip' | 170 self.archive_name = 'chrome-mac.zip' |
| 166 self._archive_extract_dir = 'chrome-mac' | 171 self._archive_extract_dir = 'chrome-mac' |
| 167 elif self.platform in ('win', 'win64'): | 172 elif self.platform in ('win', 'win64'): |
| 168 self.archive_name = 'chrome-win32.zip' | 173 self.archive_name = 'chrome-win32.zip' |
| 169 self._archive_extract_dir = 'chrome-win32' | 174 self._archive_extract_dir = 'chrome-win32' |
| 170 self._binary_name = 'chrome.exe' | 175 self._binary_name = 'chrome.exe' |
| 171 elif self.android: | 176 elif self.is_android: |
| 172 pass | 177 pass |
| 173 else: | 178 else: |
| 174 raise Exception('Invalid platform: %s' % self.platform) | 179 raise Exception('Invalid platform: %s' % self.platform) |
| 175 | 180 |
| 176 if is_official: | 181 if is_official: |
| 177 if self.platform == 'linux': | 182 if self.platform == 'linux': |
| 178 self._listing_platform_dir = 'precise32/' | 183 self._listing_platform_dir = 'precise32/' |
| 179 self.archive_name = 'chrome-precise32.zip' | 184 self.archive_name = 'chrome-precise32.zip' |
| 180 self._archive_extract_dir = 'chrome-precise32' | 185 self._archive_extract_dir = 'chrome-precise32' |
| 181 elif self.platform == 'linux64': | 186 elif self.platform == 'linux64': |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 216 self._listing_platform_dir = 'Linux/' | 221 self._listing_platform_dir = 'Linux/' |
| 217 elif self.platform == 'linux64': | 222 elif self.platform == 'linux64': |
| 218 self._listing_platform_dir = 'Linux_x64/' | 223 self._listing_platform_dir = 'Linux_x64/' |
| 219 elif self.platform == 'linux-arm': | 224 elif self.platform == 'linux-arm': |
| 220 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/' | 225 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/' |
| 221 elif self.platform == 'mac': | 226 elif self.platform == 'mac': |
| 222 self._listing_platform_dir = 'Mac/' | 227 self._listing_platform_dir = 'Mac/' |
| 223 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | 228 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' |
| 224 elif self.platform == 'win': | 229 elif self.platform == 'win': |
| 225 self._listing_platform_dir = 'Win/' | 230 self._listing_platform_dir = 'Win/' |
| 231 elif self.platform == 'android-arm': | |
| 232 self.archive_name = 'bisect_android.zip' | |
| 233 # Need to download builds using gsutil instead of visiting url for | |
| 234 # authentication reasons. | |
| 235 self.download_with_gsutil = True | |
| 226 | 236 |
| 227 def GetASANPlatformDir(self): | 237 def GetASANPlatformDir(self): |
| 228 """ASAN builds are in directories like "linux-release", or have filenames | 238 """ASAN builds are in directories like "linux-release", or have filenames |
| 229 like "asan-win32-release-277079.zip". This aligns to our platform names | 239 like "asan-win32-release-277079.zip". This aligns to our platform names |
| 230 except in the case of Windows where they use "win32" instead of "win".""" | 240 except in the case of Windows where they use "win32" instead of "win".""" |
| 231 if self.platform == 'win': | 241 if self.platform == 'win': |
| 232 return 'win32' | 242 return 'win32' |
| 233 else: | 243 else: |
| 234 return self.platform | 244 return self.platform |
| 235 | 245 |
| 236 def GetListingURL(self, marker=None): | 246 def GetListingURL(self, marker=None): |
| 237 """Returns the URL for a directory listing, with an optional marker.""" | 247 """Returns the URL for a directory listing, with an optional marker.""" |
| 238 marker_param = '' | 248 marker_param = '' |
| 239 if marker: | 249 if marker: |
| 240 marker_param = '&marker=' + str(marker) | 250 marker_param = '&marker=' + str(marker) |
| 241 if self.is_asan: | 251 if self.is_asan: |
| 242 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type) | 252 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type) |
| 243 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param | 253 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param |
| 244 else: | 254 else: |
| 245 return (self.base_url + '/?delimiter=/&prefix=' + | 255 return (self.base_url + '/?delimiter=/&prefix=' + |
| 246 self._listing_platform_dir + marker_param) | 256 self._listing_platform_dir + marker_param) |
| 247 | 257 |
| 248 def GetDownloadURL(self, revision): | 258 def GetDownloadURL(self, revision): |
| 249 """Gets the download URL for a build archive of a specific revision.""" | 259 """Gets the download URL for a build archive of a specific revision.""" |
| 250 if self.is_asan: | 260 if self.is_asan: |
| 251 return '%s/%s-%s/%s-%d.zip' % ( | 261 return '%s/%s-%s/%s-%d.zip' % ( |
| 252 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type, | 262 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type, |
| 253 self.GetASANBaseName(), revision) | 263 self.GetASANBaseName(), revision) |
| 254 if self.is_official: | 264 if self.is_official: |
| 255 if self.android: | 265 if self.is_android: |
| 256 official_base_url = ANDROID_OFFICIAL_BASE_URL | 266 official_base_url = ANDROID_OFFICIAL_BASE_URL |
| 257 else: | 267 else: |
| 258 official_base_url = OFFICIAL_BASE_URL | 268 official_base_url = OFFICIAL_BASE_URL |
| 259 return '%s/%s/%s%s' % ( | 269 return '%s/%s/%s%s' % ( |
| 260 official_base_url, revision, self._listing_platform_dir, | 270 official_base_url, revision, self._listing_platform_dir, |
| 261 self.archive_name) | 271 self.archive_name) |
| 262 else: | 272 else: |
| 263 if str(revision) in self.githash_svn_dict: | 273 if self.is_android: |
| 264 revision = self.githash_svn_dict[str(revision)] | 274 # These files need to be downloaded through gsutil. |
| 265 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir, | 275 return ('gs://%s/%s/%s' % (ANDROID_TOT_BUCKET_NAME, revision, |
| 266 revision, self.archive_name) | 276 self.archive_name)) |
| 277 else: | |
| 278 if str(revision) in self.githash_svn_dict: | |
| 279 revision = self.githash_svn_dict[str(revision)] | |
| 280 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir, | |
| 281 revision, self.archive_name) | |
| 267 | 282 |
| 268 def GetLastChangeURL(self): | 283 def GetLastChangeURL(self): |
| 269 """Returns a URL to the LAST_CHANGE file.""" | 284 """Returns a URL to the LAST_CHANGE file.""" |
| 270 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 285 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
| 271 | 286 |
| 272 def GetASANBaseName(self): | 287 def GetASANBaseName(self): |
| 273 """Returns the base name of the ASAN zip file.""" | 288 """Returns the base name of the ASAN zip file.""" |
| 274 if 'linux' in self.platform: | 289 if 'linux' in self.platform: |
| 275 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(), | 290 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(), |
| 276 self.build_type) | 291 self.build_type) |
| (...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 450 self.good_revision = FixChromiumRevForBlink(revlist, | 465 self.good_revision = FixChromiumRevForBlink(revlist, |
| 451 revlist_all, | 466 revlist_all, |
| 452 self, | 467 self, |
| 453 self.good_revision) | 468 self.good_revision) |
| 454 self.bad_revision = FixChromiumRevForBlink(revlist, | 469 self.bad_revision = FixChromiumRevForBlink(revlist, |
| 455 revlist_all, | 470 revlist_all, |
| 456 self, | 471 self, |
| 457 self.bad_revision) | 472 self.bad_revision) |
| 458 return revlist | 473 return revlist |
| 459 | 474 |
| 475 def _GetHashToNumberDict(self): | |
| 476 """Gets the mapping of git hashes to git numbers from Google Storage.""" | |
| 477 gs_file = 'gs://%s/gitnumbers_dict.json' % ANDROID_TOT_BUCKET_NAME | |
| 478 local_file = 'gitnumbers_dict.json' | |
| 479 GsutilDownload(gs_file, local_file) | |
| 480 json_data = open(local_file).read() | |
| 481 os.remove(local_file) | |
| 482 return json.loads(json_data) | |
| 483 | |
| 484 def GetAndroidToTRevisions(self): | |
| 485 """Gets the ordered list of revisions between self.good_revision and | |
| 486 self.bad_revision from the Android tip of tree GS bucket. | |
| 487 """ | |
| 488 # Dictionary that maps git hashes to git numbers. The git numbers | |
| 489 # let us order the revisions. | |
| 490 hash_to_num = self._GetHashToNumberDict() | |
| 491 try: | |
| 492 good_rev_num = hash_to_num[self.good_revision] | |
| 493 bad_rev_num = hash_to_num[self.bad_revision] | |
| 494 except KeyError: | |
| 495 exit('Error. Make sure the good and bad revisions are valid git hashes.') | |
| 496 | |
| 497 # List of all builds by their git hashes in the storage bucket. | |
| 498 hash_list = GsutilList(ANDROID_TOT_BUCKET_NAME) | |
| 499 | |
| 500 # Get list of builds that we want to bisect over. | |
| 501 final_list = [] | |
| 502 minnum = min(good_rev_num, bad_rev_num) | |
| 503 maxnum = max(good_rev_num, bad_rev_num) | |
| 504 for githash in hash_list: | |
| 505 if len(githash) != 40: | |
| 506 continue | |
| 507 gitnumber = hash_to_num[githash] | |
| 508 if minnum < gitnumber < maxnum: | |
| 509 final_list.append(githash) | |
| 510 return sorted(final_list, key=lambda h: hash_to_num[h]) | |
| 511 | |
| 460 def GetOfficialBuildsList(self): | 512 def GetOfficialBuildsList(self): |
| 461 """Gets the list of official build numbers between self.good_revision and | 513 """Gets the list of official build numbers between self.good_revision and |
| 462 self.bad_revision.""" | 514 self.bad_revision.""" |
| 463 | 515 |
| 464 def CheckDepotToolsInPath(): | |
| 465 delimiter = ';' if sys.platform.startswith('win') else ':' | |
| 466 path_list = os.environ['PATH'].split(delimiter) | |
| 467 for path in path_list: | |
| 468 if path.rstrip(os.path.sep).endswith('depot_tools'): | |
| 469 return path | |
| 470 return None | |
| 471 | |
| 472 def RunGsutilCommand(args): | |
| 473 gsutil_path = CheckDepotToolsInPath() | |
| 474 if gsutil_path is None: | |
| 475 print ('Follow the instructions in this document ' | |
| 476 'http://dev.chromium.org/developers/how-tos/install-depot-tools' | |
| 477 ' to install depot_tools and then try again.') | |
| 478 sys.exit(1) | |
| 479 gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil') | |
| 480 gsutil = subprocess.Popen([sys.executable, gsutil_path] + args, | |
| 481 stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
| 482 env=None) | |
| 483 stdout, stderr = gsutil.communicate() | |
| 484 if gsutil.returncode: | |
| 485 if (re.findall(r'status[ |=]40[1|3]', stderr) or | |
| 486 stderr.startswith(CREDENTIAL_ERROR_MESSAGE)): | |
| 487 print ('Follow these steps to configure your credentials and try' | |
| 488 ' running the bisect-builds.py again.:\n' | |
| 489 ' 1. Run "python %s config" and follow its instructions.\n' | |
| 490 ' 2. If you have a @google.com account, use that account.\n' | |
| 491 ' 3. For the project-id, just enter 0.' % gsutil_path) | |
| 492 sys.exit(1) | |
| 493 else: | |
| 494 raise Exception('Error running the gsutil command: %s' % stderr) | |
| 495 return stdout | |
| 496 | |
| 497 def GsutilList(bucket): | |
| 498 query = 'gs://%s/' % bucket | |
| 499 stdout = RunGsutilCommand(['ls', query]) | |
| 500 return [url[len(query):].strip('/') for url in stdout.splitlines()] | |
| 501 | |
| 502 # Download the revlist and filter for just the range between good and bad. | 516 # Download the revlist and filter for just the range between good and bad. |
| 503 minrev = min(self.good_revision, self.bad_revision) | 517 minrev = min(self.good_revision, self.bad_revision) |
| 504 maxrev = max(self.good_revision, self.bad_revision) | 518 maxrev = max(self.good_revision, self.bad_revision) |
| 505 if self.android: | 519 if self.is_android: |
| 506 gs_bucket_name = ANDROID_BUCKET_NAME | 520 gs_bucket_name = ANDROID_BUCKET_NAME |
| 507 else: | 521 else: |
| 508 gs_bucket_name = GS_BUCKET_NAME | 522 gs_bucket_name = GS_BUCKET_NAME |
| 509 build_numbers = GsutilList(gs_bucket_name) | 523 build_numbers = GsutilList(gs_bucket_name) |
| 510 revision_re = re.compile(r'(\d\d\.\d\.\d{4}\.\d+)') | 524 revision_re = re.compile(r'(\d\d\.\d\.\d{4}\.\d+)') |
| 511 build_numbers = filter(lambda b: revision_re.search(b), build_numbers) | 525 build_numbers = filter(lambda b: revision_re.search(b), build_numbers) |
| 512 final_list = [] | 526 final_list = [] |
| 513 parsed_build_numbers = [LooseVersion(x) for x in build_numbers] | 527 parsed_build_numbers = [LooseVersion(x) for x in build_numbers] |
| 514 connection = httplib.HTTPConnection(GOOGLE_APIS_URL) | 528 connection = httplib.HTTPConnection(GOOGLE_APIS_URL) |
| 515 for build_number in sorted(parsed_build_numbers): | 529 for build_number in sorted(parsed_build_numbers): |
| 516 if build_number > maxrev: | 530 if build_number > maxrev: |
| 517 break | 531 break |
| 518 if build_number < minrev: | 532 if build_number < minrev: |
| 519 continue | 533 continue |
| 520 path = ('/' + gs_bucket_name + '/' + str(build_number) + '/' + | 534 path = ('/' + gs_bucket_name + '/' + str(build_number) + '/' + |
| 521 self._listing_platform_dir + self.archive_name) | 535 self._listing_platform_dir + self.archive_name) |
| 522 connection.request('HEAD', path) | 536 connection.request('HEAD', path) |
| 523 response = connection.getresponse() | 537 response = connection.getresponse() |
| 524 if response.status == 200: | 538 if response.status == 200: |
| 525 final_list.append(str(build_number)) | 539 final_list.append(str(build_number)) |
| 526 response.read() | 540 response.read() |
| 527 connection.close() | 541 connection.close() |
| 528 return final_list | 542 return final_list |
| 529 | 543 |
|
Robert Sesek
2015/02/04 17:27:54
nit: add another blank line
mikecase (-- gone --)
2015/02/04 22:50:44
Done.
| |
| 544 def CheckDepotToolsInPath(): | |
| 545 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.
| |
| 546 path_list = os.environ['PATH'].split(delimiter) | |
| 547 for path in path_list: | |
| 548 if path.rstrip(os.path.sep).endswith('depot_tools'): | |
| 549 return path | |
| 550 return None | |
| 551 | |
| 552 | |
| 553 def RunGsutilCommand(args): | |
| 554 gsutil_path = CheckDepotToolsInPath() | |
| 555 if gsutil_path is None: | |
| 556 print ('Follow the instructions in this document ' | |
| 557 'http://dev.chromium.org/developers/how-tos/install-depot-tools' | |
| 558 ' to install depot_tools and then try again.') | |
| 559 sys.exit(1) | |
| 560 gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil') | |
| 561 gsutil = subprocess.Popen([sys.executable, gsutil_path] + args, | |
| 562 stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
| 563 env=None) | |
| 564 stdout, stderr = gsutil.communicate() | |
| 565 if gsutil.returncode: | |
| 566 if (re.findall(r'status[ |=]40[1|3]', stderr) or | |
| 567 stderr.startswith(CREDENTIAL_ERROR_MESSAGE)): | |
| 568 print ('Follow these steps to configure your credentials and try' | |
| 569 ' running the bisect-builds.py again.:\n' | |
| 570 ' 1. Run "python %s config" and follow its instructions.\n' | |
| 571 ' 2. If you have a @google.com account, use that account.\n' | |
| 572 ' 3. For the project-id, just enter 0.' % gsutil_path) | |
| 573 sys.exit(1) | |
| 574 else: | |
| 575 raise Exception('Error running the gsutil command: %s' % stderr) | |
| 576 return stdout | |
| 577 | |
| 578 | |
| 579 def GsutilList(bucket): | |
| 580 query = 'gs://%s/' % bucket | |
| 581 stdout = RunGsutilCommand(['ls', query]) | |
| 582 return [url[len(query):].strip('/') for url in stdout.splitlines()] | |
| 583 | |
| 584 | |
| 585 def GsutilDownload(gs_download_url, filename): | |
| 586 RunGsutilCommand(['cp', gs_download_url, filename]) | |
| 587 | |
| 588 | |
| 530 def UnzipFilenameToDir(filename, directory): | 589 def UnzipFilenameToDir(filename, directory): |
| 531 """Unzip |filename| to |directory|.""" | 590 """Unzip |filename| to |directory|.""" |
| 532 cwd = os.getcwd() | 591 cwd = os.getcwd() |
| 533 if not os.path.isabs(filename): | 592 if not os.path.isabs(filename): |
| 534 filename = os.path.join(cwd, filename) | 593 filename = os.path.join(cwd, filename) |
| 535 zf = zipfile.ZipFile(filename) | 594 zf = zipfile.ZipFile(filename) |
| 536 # Make base. | 595 # Make base. |
| 537 if not os.path.isdir(directory): | 596 if not os.path.isdir(directory): |
| 538 os.mkdir(directory) | 597 os.mkdir(directory) |
| 539 os.chdir(directory) | 598 os.chdir(directory) |
| 540 # Extract files. | 599 # Extract files. |
| 541 for info in zf.infolist(): | 600 for info in zf.infolist(): |
| 542 name = info.filename | 601 name = info.filename |
| 543 if name.endswith('/'): # dir | 602 if name.endswith('/'): # dir |
| 544 if not os.path.isdir(name): | 603 if not os.path.isdir(name): |
| 545 os.makedirs(name) | 604 os.makedirs(name) |
| 546 else: # file | 605 else: # file |
| 547 directory = os.path.dirname(name) | 606 directory = os.path.dirname(name) |
| 548 if not os.path.isdir(directory): | 607 if directory and not os.path.isdir(directory): |
| 549 os.makedirs(directory) | 608 os.makedirs(directory) |
| 550 out = open(name, 'wb') | 609 out = open(name, 'wb') |
| 551 out.write(zf.read(name)) | 610 out.write(zf.read(name)) |
| 552 out.close() | 611 out.close() |
| 553 # Set permissions. Permission info in external_attr is shifted 16 bits. | 612 # Set permissions. Permission info in external_attr is shifted 16 bits. |
| 554 os.chmod(name, info.external_attr >> 16L) | 613 os.chmod(name, info.external_attr >> 16L) |
| 555 os.chdir(cwd) | 614 os.chdir(cwd) |
| 556 | 615 |
| 557 | 616 |
| 558 def FetchRevision(context, rev, filename, quit_event=None, progress_event=None): | 617 def FetchRevision(context, rev, filename, quit_event=None, progress_event=None): |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 573 size = blocknum * blocksize | 632 size = blocknum * blocksize |
| 574 if totalsize == -1: # Total size not known. | 633 if totalsize == -1: # Total size not known. |
| 575 progress = 'Received %d bytes' % size | 634 progress = 'Received %d bytes' % size |
| 576 else: | 635 else: |
| 577 size = min(totalsize, size) | 636 size = min(totalsize, size) |
| 578 progress = 'Received %d of %d bytes, %.2f%%' % ( | 637 progress = 'Received %d of %d bytes, %.2f%%' % ( |
| 579 size, totalsize, 100.0 * size / totalsize) | 638 size, totalsize, 100.0 * size / totalsize) |
| 580 # Send a \r to let all progress messages use just one line of output. | 639 # Send a \r to let all progress messages use just one line of output. |
| 581 sys.stdout.write('\r' + progress) | 640 sys.stdout.write('\r' + progress) |
| 582 sys.stdout.flush() | 641 sys.stdout.flush() |
| 583 | |
| 584 download_url = context.GetDownloadURL(rev) | 642 download_url = context.GetDownloadURL(rev) |
| 585 try: | 643 try: |
| 586 urllib.urlretrieve(download_url, filename, ReportHook) | 644 if context.download_with_gsutil: |
| 645 GsutilDownload(download_url, filename) | |
| 646 else: | |
| 647 urllib.urlretrieve(download_url, filename, ReportHook) | |
| 587 if progress_event and progress_event.isSet(): | 648 if progress_event and progress_event.isSet(): |
| 588 print | 649 print |
| 650 | |
| 589 except RuntimeError: | 651 except RuntimeError: |
| 590 pass | 652 pass |
| 591 | 653 |
| 592 | 654 |
| 655 def RunADBCommand(args): | |
| 656 cmd = ['adb'] + args | |
| 657 adb = subprocess.Popen(['adb'] + args, | |
| 658 stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
| 659 env=None) | |
| 660 stdout, stderr = adb.communicate() | |
| 661 return stdout | |
| 662 | |
| 663 | |
| 593 def IsADBInstalled(): | 664 def IsADBInstalled(): |
| 594 """Checks if ADB is in the environment path.""" | 665 """Checks if ADB is in the environment path.""" |
| 595 try: | 666 try: |
| 596 adb_output = subprocess.check_output(['adb', 'version']) | 667 adb_output = RunADBCommand(['version']) |
| 597 return ('Android Debug Bridge' in adb_output) | 668 return ('Android Debug Bridge' in adb_output) |
| 598 except OSError: | 669 except OSError: |
| 599 return False | 670 return False |
| 600 | 671 |
| 601 | 672 |
| 602 def GetAndroidDeviceList(): | 673 def GetAndroidDeviceList(): |
| 603 """Returns the list of Android devices attached to the host machine.""" | 674 """Returns the list of Android devices attached to the host machine.""" |
| 604 lines = subprocess.check_output(['adb', 'devices']).split('\n')[1:] | 675 lines = RunADBCommand(['devices']).split('\n')[1:] |
| 605 devices = [] | 676 devices = [] |
| 606 for line in lines: | 677 for line in lines: |
| 607 m = re.match('^(.*?)\s+device$', line) | 678 m = re.match('^(.*?)\s+device$', line) |
| 608 if not m: | 679 if not m: |
| 609 continue | 680 continue |
| 610 devices.append(m.group(1)) | 681 devices.append(m.group(1)) |
| 611 return devices | 682 return devices |
| 612 | 683 |
| 613 | 684 |
| 614 def RunAndroidRevision(context, revision, apk_file): | 685 def RunAndroidRevision(context, revision, zip_file): |
| 615 """Given a Chrome apk, install it on a local device, and launch Chrome.""" | 686 """Given a Chrome apk, install it on a local device, and launch Chrome.""" |
| 616 devices = GetAndroidDeviceList() | 687 devices = GetAndroidDeviceList() |
| 617 if len(devices) is not 1: | 688 if len(devices) is not 1: |
| 618 sys.exit('Please have 1 Android device plugged in. %d devices found' | 689 sys.exit('Please have 1 Android device plugged in. %d devices found' |
| 619 % len(devices)) | 690 % len(devices)) |
| 620 | 691 |
| 621 devnull = open(os.devnull, 'w') | 692 if context.is_official: |
| 693 # Downloaded file is just the .apk in this case. | |
| 694 apk_file = zip_file | |
| 695 else: | |
| 696 cwd = os.getcwd() | |
| 697 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | |
| 698 UnzipFilenameToDir(zip_file, tempdir) | |
| 699 os.chdir(tempdir) | |
| 700 apk_file = context.android_apk | |
| 701 | |
| 622 package_name = ANDROID_CHROME_PACKAGE_NAME[context.android_apk] | 702 package_name = ANDROID_CHROME_PACKAGE_NAME[context.android_apk] |
| 623 | 703 print 'Installing...' |
| 624 print 'Installing new Chrome version...' | 704 RunADBCommand(['install', '-r', '-d', apk_file]) |
| 625 subprocess.call(['adb', 'install', '-r', '-d', apk_file], | |
| 626 stdout=devnull, stderr=devnull) | |
| 627 | 705 |
| 628 print 'Launching Chrome...\n' | 706 print 'Launching Chrome...\n' |
| 629 subprocess.call(['adb', 'shell', 'am', 'start', '-a', | 707 RunADBCommand(['shell', 'am', 'start', '-a', |
| 630 'android.intent.action.VIEW', '-n', package_name + | 708 'android.intent.action.VIEW', '-n', package_name + |
| 631 '/com.google.android.apps.chrome.Main'], stdout=devnull, stderr=devnull) | 709 '/com.google.android.apps.chrome.Main']) |
| 632 | 710 |
| 633 | 711 |
| 634 def RunRevision(context, revision, zip_file, profile, num_runs, command, args): | 712 def RunRevision(context, revision, zip_file, profile, num_runs, command, args): |
| 635 """Given a zipped revision, unzip it and run the test.""" | 713 """Given a zipped revision, unzip it and run the test.""" |
| 636 print 'Trying revision %s...' % str(revision) | 714 print 'Trying revision %s...' % str(revision) |
| 637 | 715 |
| 638 if context.android: | 716 if context.is_android: |
| 639 RunAndroidRevision(context, revision, zip_file) | 717 RunAndroidRevision(context, revision, zip_file) |
| 640 # TODO(mikecase): Support running command to auto-bisect Android. | 718 # TODO(mikecase): Support running command to auto-bisect Android. |
| 641 return (None, None, None) | 719 return (None, None, None) |
| 642 | 720 |
| 643 # Create a temp directory and unzip the revision into it. | 721 # Create a temp directory and unzip the revision into it. |
| 644 cwd = os.getcwd() | 722 cwd = os.getcwd() |
| 645 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 723 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
| 646 UnzipFilenameToDir(zip_file, tempdir) | 724 UnzipFilenameToDir(zip_file, tempdir) |
| 647 os.chdir(tempdir) | 725 os.chdir(tempdir) |
| 648 | 726 |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 675 replace('%s', ' '.join(testargs))) | 753 replace('%s', ' '.join(testargs))) |
| 676 | 754 |
| 677 results = [] | 755 results = [] |
| 678 for _ in range(num_runs): | 756 for _ in range(num_runs): |
| 679 subproc = subprocess.Popen(runcommand, | 757 subproc = subprocess.Popen(runcommand, |
| 680 bufsize=-1, | 758 bufsize=-1, |
| 681 stdout=subprocess.PIPE, | 759 stdout=subprocess.PIPE, |
| 682 stderr=subprocess.PIPE) | 760 stderr=subprocess.PIPE) |
| 683 (stdout, stderr) = subproc.communicate() | 761 (stdout, stderr) = subproc.communicate() |
| 684 results.append((subproc.returncode, stdout, stderr)) | 762 results.append((subproc.returncode, stdout, stderr)) |
| 685 | |
| 686 os.chdir(cwd) | 763 os.chdir(cwd) |
| 687 try: | 764 try: |
| 688 shutil.rmtree(tempdir, True) | 765 shutil.rmtree(tempdir, True) |
| 689 except Exception: | 766 except Exception: |
| 690 pass | 767 pass |
| 691 | 768 |
| 692 for (returncode, stdout, stderr) in results: | 769 for (returncode, stdout, stderr) in results: |
| 693 if returncode: | 770 if returncode: |
| 694 return (returncode, stdout, stderr) | 771 return (returncode, stdout, stderr) |
| 695 return results[0] | 772 return results[0] |
| (...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 813 | 890 |
| 814 print 'Downloading list of known revisions...', | 891 print 'Downloading list of known revisions...', |
| 815 if not context.use_local_repo and not context.is_official: | 892 if not context.use_local_repo and not context.is_official: |
| 816 print '(use --use-local-repo for speed if you have a local checkout)' | 893 print '(use --use-local-repo for speed if you have a local checkout)' |
| 817 else: | 894 else: |
| 818 print | 895 print |
| 819 _GetDownloadPath = lambda rev: os.path.join(cwd, | 896 _GetDownloadPath = lambda rev: os.path.join(cwd, |
| 820 '%s-%s' % (str(rev), context.archive_name)) | 897 '%s-%s' % (str(rev), context.archive_name)) |
| 821 if context.is_official: | 898 if context.is_official: |
| 822 revlist = context.GetOfficialBuildsList() | 899 revlist = context.GetOfficialBuildsList() |
| 900 elif context.is_android: # Android non-official | |
| 901 revlist = context.GetAndroidToTRevisions() | |
| 823 else: | 902 else: |
| 824 revlist = context.GetRevList() | 903 revlist = context.GetRevList() |
| 825 | 904 |
| 826 # Get a list of revisions to bisect across. | 905 # Get a list of revisions to bisect across. |
| 827 if len(revlist) < 2: # Don't have enough builds to bisect. | 906 if len(revlist) < 2: # Don't have enough builds to bisect. |
| 828 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 907 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
| 829 raise RuntimeError(msg) | 908 raise RuntimeError(msg) |
| 830 | 909 |
| 831 # Figure out our bookends and first pivot point; fetch the pivot revision. | 910 # Figure out our bookends and first pivot point; fetch the pivot revision. |
| 832 minrev = 0 | 911 minrev = 0 |
| (...skipping 362 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1195 elif opts.blink: | 1274 elif opts.blink: |
| 1196 base_url = WEBKIT_BASE_URL | 1275 base_url = WEBKIT_BASE_URL |
| 1197 else: | 1276 else: |
| 1198 base_url = CHROMIUM_BASE_URL | 1277 base_url = CHROMIUM_BASE_URL |
| 1199 | 1278 |
| 1200 # Create the context. Initialize 0 for the revisions as they are set below. | 1279 # Create the context. Initialize 0 for the revisions as they are set below. |
| 1201 context = PathContext(base_url, opts.archive, opts.good, opts.bad, | 1280 context = PathContext(base_url, opts.archive, opts.good, opts.bad, |
| 1202 opts.official_builds, opts.asan, opts.use_local_repo, | 1281 opts.official_builds, opts.asan, opts.use_local_repo, |
| 1203 opts.flash_path, opts.pdf_path, opts.apk) | 1282 opts.flash_path, opts.pdf_path, opts.apk) |
| 1204 | 1283 |
| 1205 # TODO(mikecase): Add support to bisect on nonofficial builds for Android. | 1284 if context.is_android and not opts.official_builds: |
| 1206 if context.android and not opts.official_builds: | 1285 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.
| |
| 1207 sys.exit('Can only bisect on official builds for Android.') | 1286 context.android_apk != 'Chrome.apk'): |
| 1287 sys.exit('For non-official builds, can only bisect' | |
| 1288 ' Chrome.apk arm builds.') | |
| 1208 | 1289 |
| 1209 # If bisecting Android, we make sure we have ADB setup. | 1290 # If bisecting Android, we make sure we have ADB setup. |
| 1210 if context.android: | 1291 if context.is_android: |
| 1211 if opts.adb_path: | 1292 if opts.adb_path: |
| 1212 os.environ['PATH'] = '%s:%s' % (os.path.dirname(opts.adb_path), | 1293 os.environ['PATH'] = '%s:%s' % (os.path.dirname(opts.adb_path), |
| 1213 os.environ['PATH']) | 1294 os.environ['PATH']) |
| 1214 if not IsADBInstalled(): | 1295 if not IsADBInstalled(): |
| 1215 sys.exit('Please have "adb" in PATH or use adb_path command line option' | 1296 sys.exit('Please have "adb" in PATH or use adb_path command line option' |
| 1216 'to bisect Android builds.') | 1297 'to bisect Android builds.') |
| 1217 | 1298 |
| 1218 # Pick a starting point, try to get HEAD for this. | 1299 # Pick a starting point, try to get HEAD for this. |
| 1219 if not opts.bad: | 1300 if not opts.bad: |
| 1220 context.bad_revision = '999.0.0.0' | 1301 context.bad_revision = '999.0.0.0' |
| 1221 context.bad_revision = GetChromiumRevision( | 1302 context.bad_revision = GetChromiumRevision( |
| 1222 context, context.GetLastChangeURL()) | 1303 context, context.GetLastChangeURL()) |
| 1223 | 1304 |
| 1224 # Find out when we were good. | 1305 # Find out when we were good. |
| 1225 if not opts.good: | 1306 if not opts.good: |
| 1226 context.good_revision = '0.0.0.0' if opts.official_builds else 0 | 1307 context.good_revision = '0.0.0.0' if opts.official_builds else 0 |
| 1227 | 1308 |
| 1228 if opts.flash_path: | 1309 if opts.flash_path: |
| 1229 msg = 'Could not find Flash binary at %s' % opts.flash_path | 1310 msg = 'Could not find Flash binary at %s' % opts.flash_path |
| 1230 assert os.path.exists(opts.flash_path), msg | 1311 assert os.path.exists(opts.flash_path), msg |
| 1231 | 1312 |
| 1232 if opts.pdf_path: | 1313 if opts.pdf_path: |
| 1233 msg = 'Could not find PDF binary at %s' % opts.pdf_path | 1314 msg = 'Could not find PDF binary at %s' % opts.pdf_path |
| 1234 assert os.path.exists(opts.pdf_path), msg | 1315 assert os.path.exists(opts.pdf_path), msg |
| 1235 | 1316 |
| 1236 if opts.official_builds: | 1317 if opts.official_builds: |
| 1237 context.good_revision = LooseVersion(context.good_revision) | 1318 context.good_revision = LooseVersion(context.good_revision) |
| 1238 context.bad_revision = LooseVersion(context.bad_revision) | 1319 context.bad_revision = LooseVersion(context.bad_revision) |
| 1320 elif context.is_android: | |
| 1321 # Revisions are git hashes and should be left as strings. | |
| 1322 pass | |
| 1239 else: | 1323 else: |
| 1240 context.good_revision = int(context.good_revision) | 1324 context.good_revision = int(context.good_revision) |
| 1241 context.bad_revision = int(context.bad_revision) | 1325 context.bad_revision = int(context.bad_revision) |
| 1242 | 1326 |
| 1243 if opts.times < 1: | 1327 if opts.times < 1: |
| 1244 print('Number of times to run (%d) must be greater than or equal to 1.' % | 1328 print('Number of times to run (%d) must be greater than or equal to 1.' % |
| 1245 opts.times) | 1329 opts.times) |
| 1246 parser.print_help() | 1330 parser.print_help() |
| 1247 return 1 | 1331 return 1 |
| 1248 | 1332 |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1294 | 1378 |
| 1295 print 'CHANGELOG URL:' | 1379 print 'CHANGELOG URL:' |
| 1296 if opts.official_builds: | 1380 if opts.official_builds: |
| 1297 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) | 1381 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) |
| 1298 else: | 1382 else: |
| 1299 PrintChangeLog(min_chromium_rev, max_chromium_rev) | 1383 PrintChangeLog(min_chromium_rev, max_chromium_rev) |
| 1300 | 1384 |
| 1301 | 1385 |
| 1302 if __name__ == '__main__': | 1386 if __name__ == '__main__': |
| 1303 sys.exit(main()) | 1387 sys.exit(main()) |
| OLD | NEW |