OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 """Bisect repackage tool for Linux. |
| 5 |
| 6 This script repacakges chrome builds for manual bisect script. |
| 7 """ |
| 8 |
| 9 from functools import partial |
| 10 import json |
| 11 import logging |
| 12 from multiprocessing import Pool |
| 13 import optparse |
| 14 import os |
| 15 import re |
| 16 import sys |
| 17 import tempfile |
| 18 import threading |
| 19 import urllib |
| 20 import bisect_repackage_utils |
| 21 |
| 22 # Declares required files to run manual bisect script on chrome Linux |
| 23 # builds in perf. Binary files that should be stripped to reduce zip file |
| 24 # size are declared. The file list was gotten from the local chrome |
| 25 # executable path. (This can be retrieved by typing 'chrome://version' |
| 26 # in chrome and following the executable path. The list needs to be updated if |
| 27 # future chrome versions require additional files. |
| 28 CHROME_REQUIRED_FILES = { |
| 29 'linux': [ |
| 30 'chrome', |
| 31 'chrome_100_percent.pak', |
| 32 'chrome_200_percent.pak', |
| 33 'default_apps', |
| 34 'icudtl.dat', |
| 35 'libwidevinecdm.so', |
| 36 'locales', |
| 37 'nacl_helper', |
| 38 'nacl_helper_bootstrap', |
| 39 'nacl_irt_x86_64.nexe', |
| 40 'natives_blob.bin', |
| 41 'PepperFlash', |
| 42 'product_logo_48.png', |
| 43 'resources.pak', |
| 44 'snapshot_blob.bin', |
| 45 'xdg-mime', |
| 46 'xdg-settings' |
| 47 ], |
| 48 'win64': [ |
| 49 'chrome.dll', |
| 50 'chrome.exe', |
| 51 'chrome_100_percent.pak', |
| 52 'chrome_200_percent.pak', |
| 53 'chrome_child.dll', |
| 54 'chrome_elf.dll', |
| 55 'chrome_watcher.dll', |
| 56 'default_apps', |
| 57 'd3dcompiler_47.dll', |
| 58 'icudtl.dat', |
| 59 'libEGL.dll', |
| 60 'libGLESv2.dll', |
| 61 'locales', |
| 62 'nacl_irt_x86_64.nexe', |
| 63 'natives_blob.bin', |
| 64 'PepperFlash', |
| 65 'resources.pak', |
| 66 'SecondaryTile.png', |
| 67 'snapshot_blob.bin' |
| 68 ], |
| 69 'mac': [ |
| 70 'Google Chrome.app' |
| 71 ] |
| 72 } |
| 73 |
| 74 CHROME_WHITELIST_FILES = { |
| 75 # ^$ means not to include any files from whitelist |
| 76 'linux': '^$', |
| 77 'win64': '^\d+\.\d+\.\d+\.\d+\.manifest$', |
| 78 'mac': '^$' |
| 79 } |
| 80 |
| 81 CHROME_STRIP_LIST = { |
| 82 'linux': [ |
| 83 'chrome', |
| 84 'nacl_helper' |
| 85 ], |
| 86 'win64': [ |
| 87 # No stripping symbols from win64 archives |
| 88 ], |
| 89 'mac': [ |
| 90 # No stripping symbols from mac archives |
| 91 ] |
| 92 } |
| 93 |
| 94 # API to convert Githash to Commit position number. |
| 95 CHROMIUM_GITHASH_TO_SVN_URL = ( |
| 96 'https://cr-rev.appspot.com/_ah/api/crrev/v1/commit/%s') |
| 97 |
| 98 REVISION_MAP_FILE = 'revision_map.json' |
| 99 |
| 100 BUILDER_NAME = { |
| 101 'linux': 'Linux Builder', |
| 102 'mac': 'Mac Builder', |
| 103 'win32': 'Win Builder', |
| 104 'win64': 'Win x64 Builder' |
| 105 } |
| 106 |
| 107 ARCHIVE_PREFIX = { |
| 108 'linux': 'full-build-linux', |
| 109 'mac': 'full-build-mac', |
| 110 'win32': 'full-build-win32', |
| 111 'win64': 'full-build-win32' |
| 112 } |
| 113 |
| 114 class ChromeExecutionError(Exception): |
| 115 """Raised when Chrome execution fails.""" |
| 116 pass |
| 117 |
| 118 class GitConversionError(Exception): |
| 119 """Raised when Chrome execution fails.""" |
| 120 pass |
| 121 |
| 122 class PathContext(object): |
| 123 """Stores information to repackage from a bucket to another. |
| 124 |
| 125 A PathContext is used to carry the information used to construct URLs and |
| 126 paths when dealing with the storage server and archives. |
| 127 """ |
| 128 |
| 129 def __init__(self, original_gs_url, repackage_gs_url, |
| 130 archive, revision_file=REVISION_MAP_FILE): |
| 131 super(PathContext, self).__init__() |
| 132 self.original_gs_url = original_gs_url |
| 133 self.repackage_gs_url = repackage_gs_url |
| 134 self.archive = archive |
| 135 self.builder_name = BUILDER_NAME[archive] |
| 136 self.file_prefix = ARCHIVE_PREFIX[archive] |
| 137 self.revision_file = revision_file |
| 138 |
| 139 |
| 140 def get_cp_from_hash(git_hash): |
| 141 """Converts a git hash to commit position number.""" |
| 142 json_url = CHROMIUM_GITHASH_TO_SVN_URL % git_hash |
| 143 response = urllib.urlopen(json_url) |
| 144 if response.getcode() == 200: |
| 145 try: |
| 146 data = json.loads(response.read()) |
| 147 except Exception,e: |
| 148 logging.warning('JSON URL: %s, Error Message: %s' % json_url, e) |
| 149 raise GitConversionError |
| 150 else: |
| 151 logging.warning('JSON URL: %s, Error Message: %s' % json_url, e) |
| 152 raise GitConversionError |
| 153 if 'number' in data: |
| 154 return data['number'] |
| 155 logging.warning('JSON URL: %s, Error Message: %s' % json_url, e) |
| 156 raise GitConversionError |
| 157 |
| 158 |
| 159 def create_cp_from_hash_map(hash_list): |
| 160 """Returns dict used for conversion of hash list. |
| 161 |
| 162 Creates a dictionary that maps from Commit position number |
| 163 to corresponding GitHash. |
| 164 """ |
| 165 hash_map = {} |
| 166 for git_hash in hash_list: |
| 167 try: |
| 168 cp_num = get_cp_from_hash(git_hash) |
| 169 hash_map[cp_num] = git_hash |
| 170 except GitConversionError: |
| 171 pass |
| 172 return hash_map |
| 173 |
| 174 |
| 175 def get_list_of_suffix(bucket_address, prefix, filter_function): |
| 176 """Gets the list of suffixes in files in a google storage bucket. |
| 177 |
| 178 Example: a google storage bucket containing one file |
| 179 'full-build-linux_20983' will return ['20983'] if prefix is |
| 180 provided as 'full-build-linux'. Google Storage bucket |
| 181 containing multiple files will return multiple suffixes. |
| 182 |
| 183 Args: |
| 184 bucket_address(String): Bucket URL to examine files from. |
| 185 prefix(String): The prefix used in creating build file names |
| 186 filter_function: A function that returns true if the extracted |
| 187 suffix is in correct format and false otherwise. It allows |
| 188 only proper suffix to be extracted and returned. |
| 189 |
| 190 Returns: |
| 191 (List) list of proper suffixes in the bucket. |
| 192 """ |
| 193 file_list = bisect_repackage_utils.GSutilList(bucket_address) |
| 194 suffix_list = [] |
| 195 extract_suffix = '.*?%s_(.*?)\.zip' %(prefix) |
| 196 for file in file_list: |
| 197 match = re.match(extract_suffix, file) |
| 198 if match and filter_function(match.groups()[0]): |
| 199 suffix_list.append(match.groups()[0]) |
| 200 return suffix_list |
| 201 |
| 202 |
| 203 def download_build(cp_num, revision_map, zip_file_name, context): |
| 204 """Download a single build corresponding to the cp_num and context.""" |
| 205 file_url = '%s/%s/%s_%s.zip' %(context.original_gs_url, context.builder_name, |
| 206 context.file_prefix, revision_map[cp_num]) |
| 207 bisect_repackage_utils.GSUtilDownloadFile(file_url, zip_file_name) |
| 208 |
| 209 |
| 210 def upload_build(zip_file, context): |
| 211 """Uploads a single build in zip_file to the repackage_gs_url in context.""" |
| 212 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| 213 upload_url = gs_base_url + '/' |
| 214 bisect_repackage_utils.GSUtilCopy(zip_file, upload_url) |
| 215 |
| 216 |
| 217 def download_revision_map(context): |
| 218 """Downloads the revision map in original_gs_url in context.""" |
| 219 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| 220 download_url = gs_base_url + '/' + context.revision_file |
| 221 bisect_repackage_utils.GSUtilDownloadFile(download_url, |
| 222 context.revision_file) |
| 223 |
| 224 |
| 225 def get_revision_map(context): |
| 226 """Downloads and returns the revision map in repackage_gs_url in context.""" |
| 227 bisect_repackage_utils.RemoveFile(context.revision_file) |
| 228 download_revision_map(context) |
| 229 with open(context.revision_file, 'r') as revision_file: |
| 230 revision_map = json.load(revision_file) |
| 231 bisect_repackage_utils.RemoveFile(context.revision_file) |
| 232 return revision_map |
| 233 |
| 234 |
| 235 def upload_revision_map(revision_map, context): |
| 236 """Upload the given revision_map to the repackage_gs_url in context.""" |
| 237 with open(context.revision_file, 'w') as revision_file: |
| 238 json.dump(revision_map, revision_file) |
| 239 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| 240 upload_url = gs_base_url + '/' |
| 241 bisect_repackage_utils.GSUtilCopy(context.revision_file, upload_url) |
| 242 bisect_repackage_utils.RemoveFile(context.revision_file) |
| 243 |
| 244 |
| 245 def create_upload_revision_map(context): |
| 246 """Creates and uploads a dictionary that maps from GitHash to CP number.""" |
| 247 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) |
| 248 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, |
| 249 bisect_repackage_utils.IsGitCommitHash) |
| 250 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) |
| 251 upload_revision_map(cp_num_to_hash_map, context) |
| 252 |
| 253 |
| 254 def update_upload_revision_map(context): |
| 255 """Updates and uploads a dictionary that maps from GitHash to CP number.""" |
| 256 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) |
| 257 revision_map = get_revision_map(context) |
| 258 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, |
| 259 bisect_repackage_utils.IsGitCommitHash) |
| 260 hash_list = list(set(hash_list)-set(revision_map.values())) |
| 261 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) |
| 262 merged_dict = dict(cp_num_to_hash_map.items() + revision_map.items()) |
| 263 upload_revision_map(merged_dict, context) |
| 264 |
| 265 |
| 266 def make_lightweight_archive(file_archive, archive_name, files_to_archive, |
| 267 context, staging_dir): |
| 268 """Repackages and strips the archive. |
| 269 |
| 270 Repacakges and strips according to CHROME_REQUIRED_FILES and |
| 271 CHROME_STRIP_LIST. |
| 272 """ |
| 273 strip_list = CHROME_STRIP_LIST[context.archive] |
| 274 tmp_archive = os.path.join(staging_dir, 'tmp_%s' % archive_name) |
| 275 (zip_file, zip_dir) = bisect_repackage_utils.MakeZip(tmp_archive, |
| 276 archive_name, |
| 277 files_to_archive, |
| 278 file_archive, |
| 279 raise_error=False, |
| 280 strip_files=strip_list) |
| 281 return (zip_file, zip_dir, tmp_archive) |
| 282 |
| 283 |
| 284 def remove_created_files_and_path(files, paths): |
| 285 """Removes all the files and paths passed in.""" |
| 286 for file in files: |
| 287 bisect_repackage_utils.RemoveFile(file) |
| 288 for path in paths: |
| 289 bisect_repackage_utils.RemovePath(path) |
| 290 |
| 291 |
| 292 def verify_chrome_run(zip_dir): |
| 293 """This function executes chrome executable in zip_dir. |
| 294 |
| 295 Currently, it is only supported for Linux Chrome builds. |
| 296 Raises error if the execution fails for any reason. |
| 297 """ |
| 298 try: |
| 299 command = [os.path.join(zip_dir, 'chrome')] |
| 300 code = bisect_repackage_utils.RunCommand(command) |
| 301 if code != 0: |
| 302 raise ChromeExecutionError('An error occurred when executing Chrome') |
| 303 except ChromeExecutionError,e: |
| 304 print str(e) |
| 305 |
| 306 |
| 307 def get_whitelist_files(extracted_folder, archive): |
| 308 """Gets all the files & directories matching whitelisted regex.""" |
| 309 whitelist_files = [] |
| 310 all_files = os.listdir(extracted_folder) |
| 311 for file in all_files: |
| 312 if re.match(CHROME_WHITELIST_FILES[archive], file): |
| 313 whitelist_files.append(file) |
| 314 return whitelist_files |
| 315 |
| 316 |
| 317 def repackage_single_revision(revision_map, verify_run, staging_dir, |
| 318 context, cp_num): |
| 319 """Repackages a single Chrome build for manual bisect.""" |
| 320 archive_name = '%s_%s' %(context.file_prefix, cp_num) |
| 321 file_archive = os.path.join(staging_dir, archive_name) |
| 322 zip_file_name = '%s.zip' % (file_archive) |
| 323 download_build(cp_num, revision_map, zip_file_name, context) |
| 324 extract_dir = os.path.join(staging_dir, archive_name) |
| 325 bisect_repackage_utils.ExtractZip(zip_file_name, extract_dir) |
| 326 extracted_folder = os.path.join(extract_dir, context.file_prefix) |
| 327 if CHROME_WHITELIST_FILES[context.archive]: |
| 328 whitelist_files = get_whitelist_files(extracted_folder, context.archive) |
| 329 files_to_include = whitelist_files + CHROME_REQUIRED_FILES[context.archive] |
| 330 else: |
| 331 files_to_include = CHROME_REQUIRED_FILES[context.archive] |
| 332 (zip_dir, zip_file, tmp_archive) = make_lightweight_archive(extracted_folder, |
| 333 archive_name, |
| 334 files_to_include, |
| 335 context, |
| 336 staging_dir) |
| 337 |
| 338 if verify_run: |
| 339 verify_chrome_run(zip_dir) |
| 340 |
| 341 upload_build(zip_file, context) |
| 342 # Removed temporary files created during repackaging process. |
| 343 remove_created_files_and_path([zip_file_name], |
| 344 [zip_dir, extract_dir, tmp_archive]) |
| 345 |
| 346 |
| 347 def repackage_revisions(revisions, revision_map, verify_run, staging_dir, |
| 348 context, quit_event=None, progress_event=None): |
| 349 """Repackages all Chrome builds listed in revisions. |
| 350 |
| 351 This function calls 'repackage_single_revision' with multithreading pool. |
| 352 """ |
| 353 p = Pool(3) |
| 354 func = partial(repackage_single_revision, revision_map, verify_run, |
| 355 staging_dir, context) |
| 356 p.imap(func, revisions) |
| 357 p.close() |
| 358 p.join() |
| 359 |
| 360 |
| 361 def get_uploaded_builds(context): |
| 362 """Returns already uploaded revisions in original bucket.""" |
| 363 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| 364 return get_list_of_suffix(gs_base_url, context.file_prefix, |
| 365 bisect_repackage_utils.IsCommitPosition) |
| 366 |
| 367 |
| 368 def get_revisions_to_package(revision_map, context): |
| 369 """Returns revisions that need to be repackaged. |
| 370 |
| 371 It subtracts revisions that are already packaged from all revisions that |
| 372 need to be packaged. The revisions will be sorted in descending order. |
| 373 """ |
| 374 already_packaged = get_uploaded_builds(context) |
| 375 not_already_packaged = list(set(revision_map.keys())-set(already_packaged)) |
| 376 revisions_to_package = sorted(not_already_packaged, reverse=True) |
| 377 return revisions_to_package |
| 378 |
| 379 |
| 380 class RepackageJob(object): |
| 381 |
| 382 def __init__(self, name, revisions_to_package, revision_map, verify_run, |
| 383 staging_dir, context): |
| 384 super(RepackageJob, self).__init__() |
| 385 self.name = name |
| 386 self.revisions_to_package = revisions_to_package |
| 387 self.revision_map = revision_map |
| 388 self.verify_run = verify_run |
| 389 self.staging_dir = staging_dir |
| 390 self.context = context |
| 391 self.quit_event = threading.Event() |
| 392 self.progress_event = threading.Event() |
| 393 self.thread = None |
| 394 |
| 395 def Start(self): |
| 396 """Starts the download.""" |
| 397 fetchargs = (self.revisions_to_package, |
| 398 self.revision_map, |
| 399 self.verify_run, |
| 400 self.staging_dir, |
| 401 self.context, |
| 402 self.quit_event, |
| 403 self.progress_event) |
| 404 self.thread = threading.Thread(target=repackage_revisions, |
| 405 name=self.name, |
| 406 args=fetchargs) |
| 407 self.thread.start() |
| 408 |
| 409 def Stop(self): |
| 410 """Stops the download which must have been started previously.""" |
| 411 assert self.thread, 'DownloadJob must be started before Stop is called.' |
| 412 self.quit_event.set() |
| 413 self.thread.join() |
| 414 |
| 415 def WaitFor(self): |
| 416 """Prints a message and waits for the download to complete.""" |
| 417 assert self.thread, 'DownloadJob must be started before WaitFor is called.' |
| 418 self.progress_event.set() # Display progress of download. def Stop(self): |
| 419 assert self.thread, 'DownloadJob must be started before Stop is called.' |
| 420 self.quit_event.set() |
| 421 self.thread.join() |
| 422 |
| 423 |
| 424 def main(argv): |
| 425 option_parser = optparse.OptionParser() |
| 426 |
| 427 choices = ['mac', 'win32', 'win64', 'linux'] |
| 428 |
| 429 option_parser.add_option('-a', '--archive', |
| 430 choices=choices, |
| 431 help='Builders to repacakge from [%s].' % |
| 432 '|'.join(choices)) |
| 433 |
| 434 # Verifies that the chrome executable runs |
| 435 option_parser.add_option('-v', '--verify', |
| 436 action='store_true', |
| 437 help='Verifies that the Chrome executes normally' |
| 438 'without errors') |
| 439 |
| 440 # This option will update the revision map. |
| 441 option_parser.add_option('-u', '--update', |
| 442 action='store_true', |
| 443 help='Updates the list of revisions to repackage') |
| 444 |
| 445 # This option will creates the revision map. |
| 446 option_parser.add_option('-c', '--create', |
| 447 action='store_true', |
| 448 help='Creates the list of revisions to repackage') |
| 449 |
| 450 # Original bucket that contains perf builds |
| 451 option_parser.add_option('-o', '--original', |
| 452 type='str', |
| 453 help='Google storage url containing original' |
| 454 'Chrome builds') |
| 455 |
| 456 # Bucket that should archive lightweight perf builds |
| 457 option_parser.add_option('-r', '--repackage', |
| 458 type='str', |
| 459 help='Google storage url to re-archive Chrome' |
| 460 'builds') |
| 461 |
| 462 verify_run = False |
| 463 (opts, args) = option_parser.parse_args() |
| 464 if opts.archive is None: |
| 465 print 'Error: missing required parameter: --archive' |
| 466 option_parser.print_help() |
| 467 return 1 |
| 468 if not opts.original or not opts.repackage: |
| 469 raise ValueError('Need to specify original gs bucket url and' |
| 470 'repackage gs bucket url') |
| 471 context = PathContext(opts.original, opts.repackage, opts.archive) |
| 472 |
| 473 if opts.create: |
| 474 create_upload_revision_map(context) |
| 475 |
| 476 if opts.update: |
| 477 update_upload_revision_map(context) |
| 478 |
| 479 if opts.verify: |
| 480 verify_run = True |
| 481 |
| 482 revision_map = get_revision_map(context) |
| 483 backward_rev = get_revisions_to_package(revision_map, context) |
| 484 base_dir = os.path.join('.', context.archive) |
| 485 # Clears any uncleared staging directories and create one |
| 486 bisect_repackage_utils.RemovePath(base_dir) |
| 487 bisect_repackage_utils.MaybeMakeDirectory(base_dir) |
| 488 staging_dir = os.path.abspath(tempfile.mkdtemp(prefix='staging', |
| 489 dir=base_dir)) |
| 490 repackage = RepackageJob('backward_fetch', backward_rev, revision_map, |
| 491 verify_run, staging_dir, context) |
| 492 # Multi-threading is not currently being used. But it can be used in |
| 493 # cases when the repackaging needs to be quicker. |
| 494 try: |
| 495 repackage.Start() |
| 496 repackage.WaitFor() |
| 497 except (KeyboardInterrupt, SystemExit): |
| 498 print 'Cleaning up...' |
| 499 bisect_repackage_utils.RemovePath(staging_dir) |
| 500 print 'Cleaning up...' |
| 501 bisect_repackage_utils.RemovePath(staging_dir) |
| 502 |
| 503 |
| 504 if '__main__' == __name__: |
| 505 sys.exit(main(sys.argv)) |
OLD | NEW |