Chromium Code Reviews| 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 # ^$ meanse not to include any files from whitelist | |
|
prasadv
2016/09/09 21:34:01
nit: s/meanse/means
miimnk
2016/09/09 23:13:10
Done.
| |
| 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 | |
| 115 class PathContext(object): | |
| 116 """Stores information to repackage from a bucket to another. | |
| 117 | |
| 118 A PathContext is used to carry the information used to construct URLs and | |
| 119 paths when dealing with the storage server and archives. | |
| 120 """ | |
| 121 | |
| 122 def __init__(self, original_gs_url, repackage_gs_url, | |
| 123 archive, revision_file=REVISION_MAP_FILE): | |
| 124 super(PathContext, self).__init__() | |
| 125 self.original_gs_url = original_gs_url | |
| 126 self.repackage_gs_url = repackage_gs_url | |
| 127 self.archive = archive | |
| 128 self.builder_name = BUILDER_NAME[archive] | |
| 129 self.file_prefix = ARCHIVE_PREFIX[archive] | |
| 130 self.revision_file = revision_file | |
| 131 | |
| 132 | |
| 133 def get_cp_from_hash(git_hash): | |
| 134 """Converts a GitHash to Commit position number.""" | |
|
prasadv
2016/09/09 21:34:02
s/GitHash/git hash
s/Commit/commit
miimnk
2016/09/09 23:13:11
Done.
| |
| 135 json_url = CHROMIUM_GITHASH_TO_SVN_URL % git_hash | |
| 136 response = urllib.urlopen(json_url) | |
| 137 if response.getcode() == 200: | |
| 138 try: | |
| 139 data = json.loads(response.read()) | |
| 140 except ValueError: | |
|
prasadv
2016/09/09 21:34:01
May be you should display the actual error in the
miimnk
2016/09/09 23:13:10
Changed to:
except Exception,e:
logging
| |
| 141 logging.warning('ValueError for JSON URL: %s' % json_url) | |
| 142 raise ValueError('Failed to extract commit position') | |
| 143 else: | |
| 144 logging.warning('ValueError for JSON URL: %s' % json_url) | |
| 145 raise ValueError('Failed to extract commit position') | |
|
prasadv
2016/09/09 21:34:02
Can we display the the actual response code and th
miimnk
2016/09/09 23:13:10
Changed to:
except Exception,e:
logging
| |
| 146 if 'number' in data: | |
| 147 return data['number'] | |
| 148 logging.warning('Failed to get svn revision number for %s' % git_hash) | |
| 149 raise ValueError('Failed to extract commit position') | |
| 150 | |
| 151 | |
| 152 def create_cp_from_hash_map(hash_list): | |
| 153 """Returns dict used for conversion of hash list. | |
| 154 | |
| 155 Creates a dictionary that maps from Commit position number | |
| 156 to corresponding GitHash. | |
| 157 """ | |
| 158 hash_map = {} | |
| 159 count = 0 | |
| 160 for git_hash in hash_list: | |
| 161 try: | |
| 162 cp_num = get_cp_from_hash(git_hash) | |
| 163 hash_map[cp_num] = git_hash | |
| 164 print 'Downloaded %s / %s git hash' %(count, len(hash_list)) | |
|
prasadv
2016/09/09 21:34:01
Is there any specific reason for printing this?
miimnk
2016/09/09 23:13:10
Removed
| |
| 165 count += 1 | |
| 166 except ValueError: | |
| 167 pass | |
| 168 return hash_map | |
| 169 | |
| 170 | |
| 171 def get_list_of_suffix(bucket_address, prefix, filter_function): | |
| 172 """Gets the list of suffixes in files in a google storage bucket. | |
| 173 | |
| 174 Example: a google storage bucket containing one file | |
| 175 'full-build-linux_20983' will return ['20983'] if prefix is | |
| 176 provided as 'full-build-linux'. Google Storage bucket | |
| 177 containing multiple files will return multiple suffixes. | |
| 178 | |
| 179 Args: | |
| 180 bucket_address(String): Bucket URL to examine files from. | |
| 181 prefix(String): The prefix used in creating build file names | |
| 182 filter_function: A function that returns true if the extracted | |
| 183 suffix is in correct format and false otherwise. It allows | |
| 184 only proper suffix to be extracted and returned. | |
| 185 | |
| 186 Returns: | |
| 187 (List) list of proper suffixes in the bucket. | |
| 188 """ | |
| 189 file_list = bisect_repackage_utils.GSutilList(bucket_address) | |
| 190 suffix_list = [] | |
| 191 extract_suffix = '.*?%s_(.*?)\.zip' %(prefix) | |
| 192 for file in file_list: | |
| 193 match = re.match(extract_suffix, file) | |
| 194 if match and filter_function(match.groups()[0]): | |
| 195 suffix_list.append(match.groups()[0]) | |
| 196 return suffix_list | |
| 197 | |
| 198 | |
| 199 def download_build(cp_num, revision_map, zip_file_name, context): | |
| 200 """Download a single build corresponding to the cp_num and context.""" | |
| 201 file_url = '%s/%s/%s_%s.zip' %(context.original_gs_url, context.builder_name, | |
| 202 context.file_prefix, revision_map[cp_num]) | |
| 203 bisect_repackage_utils.GSUtilDownloadFile(file_url, zip_file_name) | |
| 204 | |
| 205 | |
| 206 def upload_build(zip_file, context): | |
| 207 """Uploads a single build in zip_file to the repackage_gs_url in context.""" | |
| 208 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
| 209 upload_url = gs_base_url + '/' | |
| 210 bisect_repackage_utils.GSUtilCopy(zip_file, upload_url) | |
| 211 | |
| 212 | |
| 213 def download_revision_map(context): | |
| 214 """Downloads the revision map in original_gs_url in context.""" | |
| 215 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
| 216 download_url = gs_base_url + '/' + context.revision_file | |
| 217 bisect_repackage_utils.GSUtilDownloadFile(download_url, | |
| 218 context.revision_file) | |
| 219 | |
| 220 | |
| 221 def get_revision_map(context): | |
| 222 """Downloads and returns the revision map in repackage_gs_url in context.""" | |
| 223 bisect_repackage_utils.RemoveFile(context.revision_file) | |
| 224 download_revision_map(context) | |
| 225 with open(context.revision_file, 'r') as revision_file: | |
| 226 revision_map = json.load(revision_file) | |
| 227 bisect_repackage_utils.RemoveFile(context.revision_file) | |
| 228 return revision_map | |
| 229 | |
| 230 | |
| 231 def upload_revision_map(revision_map, context): | |
| 232 """Upload the given revision_map to the repackage_gs_url in context.""" | |
| 233 with open(context.revision_file, 'w') as revision_file: | |
| 234 json.dump(revision_map, revision_file) | |
| 235 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
| 236 upload_url = gs_base_url + '/' | |
| 237 bisect_repackage_utils.GSUtilCopy(context.revision_file, upload_url) | |
| 238 bisect_repackage_utils.RemoveFile(context.revision_file) | |
| 239 | |
| 240 | |
| 241 def create_upload_revision_map(context): | |
| 242 """Creates and uploads a dictionary that maps from GitHash to CP number.""" | |
| 243 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) | |
| 244 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, | |
| 245 bisect_repackage_utils.IsGitCommitHash) | |
| 246 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) | |
| 247 upload_revision_map(cp_num_to_hash_map, context) | |
| 248 | |
| 249 | |
| 250 def update_upload_revision_map(context): | |
| 251 """Updates and uploads a dictionary that maps from GitHash to CP number.""" | |
| 252 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) | |
| 253 revision_map = get_revision_map(context) | |
| 254 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, | |
| 255 bisect_repackage_utils.IsGitCommitHash) | |
| 256 hash_list = list(set(hash_list)-set(revision_map.values())) | |
| 257 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) | |
| 258 merged_dict = dict(cp_num_to_hash_map.items() + revision_map.items()) | |
| 259 upload_revision_map(merged_dict, context) | |
| 260 | |
| 261 | |
| 262 def make_lightweight_archive(file_archive, archive_name, files_to_archive, | |
| 263 context, staging_dir): | |
| 264 """Repackages and strips the archive. | |
| 265 | |
| 266 Repacakges and strips according to CHROME_REQUIRED_FILES and | |
| 267 CHROME_STRIP_LIST. | |
| 268 """ | |
| 269 strip_list = CHROME_STRIP_LIST[context.archive] | |
| 270 tmp_archive = os.path.join(staging_dir, 'tmp_%s' % archive_name) | |
| 271 (zip_file, zip_dir) = bisect_repackage_utils.MakeZip(tmp_archive, | |
| 272 archive_name, | |
| 273 files_to_archive, | |
| 274 file_archive, | |
| 275 raise_error=False, | |
| 276 strip_files=strip_list) | |
| 277 return (zip_file, zip_dir, tmp_archive) | |
| 278 | |
| 279 | |
| 280 def remove_created_files_and_path(files, paths): | |
| 281 """Removes all the files and paths passed in.""" | |
| 282 for file in files: | |
| 283 bisect_repackage_utils.RemoveFile(file) | |
| 284 for path in paths: | |
| 285 bisect_repackage_utils.RemovePath(path) | |
| 286 | |
| 287 | |
| 288 def verify_chrome_run(zip_dir): | |
| 289 """This function executes chrome executable in zip_dir. | |
| 290 | |
| 291 Currently, it is only supported for Linux Chrome builds. | |
| 292 Raises error if the execution fails for any reason. | |
| 293 """ | |
| 294 try: | |
| 295 command = [os.path.join(zip_dir, 'chrome')] | |
| 296 code = bisect_repackage_utils.RunCommand(command) | |
| 297 if code != 0: | |
| 298 raise ValueError('Executing Chrome Failed: Need to check ') | |
|
prasadv
2016/09/09 21:34:02
I think we should define a userdefine exception an
miimnk
2016/09/09 23:13:10
Changed to
try:
command = [os.path.join(zi
| |
| 299 except Exception, e: | |
| 300 raise ValueError('Executing Chrome Failed: Need to check ') | |
| 301 | |
| 302 | |
| 303 def get_whitelist_files(extracted_folder, archive): | |
| 304 """Gets all the files & directories matching whitelisted regex.""" | |
| 305 whitelist_files = [] | |
| 306 all_files = os.listdir(extracted_folder) | |
| 307 for file in all_files: | |
| 308 if re.match(CHROME_WHITELIST_FILES[archive], file): | |
| 309 whitelist_files.append(file) | |
| 310 return whitelist_files | |
| 311 | |
| 312 | |
| 313 def repackage_single_revision(revision_map, verify_run, staging_dir, | |
| 314 context, cp_num): | |
| 315 """Repackages a single Chrome build for manual bisect.""" | |
| 316 archive_name = '%s_%s' %(context.file_prefix, cp_num) | |
| 317 file_archive = os.path.join(staging_dir, archive_name) | |
| 318 zip_file_name = '%s.zip' % (file_archive) | |
| 319 download_build(cp_num, revision_map, zip_file_name, context) | |
| 320 extract_dir = os.path.join(staging_dir, archive_name) | |
| 321 bisect_repackage_utils.ExtractZip(zip_file_name, extract_dir) | |
| 322 extracted_folder = os.path.join(extract_dir, context.file_prefix) | |
| 323 if CHROME_WHITELIST_FILES[context.archive]: | |
| 324 whitelist_files = get_whitelist_files(extracted_folder, context.archive) | |
| 325 files_to_include = whitelist_files + CHROME_REQUIRED_FILES[context.archive] | |
| 326 else: | |
| 327 files_to_include = CHROME_REQUIRED_FILES[context.archive] | |
| 328 (zip_dir, zip_file, tmp_archive) = make_lightweight_archive(extracted_folder, | |
| 329 archive_name, | |
| 330 files_to_include, | |
| 331 context, | |
| 332 staging_dir) | |
| 333 | |
| 334 if verify_run: | |
| 335 verify_chrome_run(zip_dir) | |
| 336 | |
| 337 upload_build(zip_file, context) | |
| 338 # Removed temporary files created during repackaging process. | |
| 339 remove_created_files_and_path([zip_file_name], | |
| 340 [zip_dir, extract_dir, tmp_archive]) | |
| 341 | |
| 342 | |
| 343 def repackage_revisions(revisions, revision_map, verify_run, staging_dir, | |
| 344 context, quit_event=None, progress_event=None): | |
| 345 """Repackages all Chrome builds listed in revisions. | |
| 346 | |
| 347 This function calls 'repackage_single_revision' with multithreading pool. | |
| 348 """ | |
| 349 p = Pool(3) | |
| 350 func = partial(repackage_single_revision, revision_map, verify_run, | |
| 351 staging_dir, context) | |
| 352 p.imap(func, revisions) | |
| 353 p.close() | |
| 354 p.join() | |
| 355 | |
| 356 | |
| 357 def get_uploaded_builds(context): | |
| 358 """Returns already uploaded revisions in original bucket.""" | |
| 359 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
| 360 return get_list_of_suffix(gs_base_url, context.file_prefix, | |
| 361 bisect_repackage_utils.IsCommitPosition) | |
| 362 | |
| 363 | |
| 364 def get_revisions_to_package(revision_map, context): | |
| 365 """Returns revisions that need to be repackaged. | |
| 366 | |
| 367 It subtracts revisions that are already packaged from all revisions that | |
| 368 need to be packaged. The revisions will be sorted in descending order. | |
| 369 """ | |
| 370 already_packaged = get_uploaded_builds(context) | |
| 371 not_already_packaged = list(set(revision_map.keys())-set(already_packaged)) | |
| 372 revisions_to_package = sorted(not_already_packaged, reverse=True) | |
| 373 return revisions_to_package | |
| 374 | |
| 375 | |
| 376 class RepackageJob(object): | |
| 377 | |
| 378 def __init__(self, name, revisions_to_package, revision_map, verify_run, | |
| 379 staging_dir, context): | |
| 380 super(RepackageJob, self).__init__() | |
| 381 self.name = name | |
| 382 self.revisions_to_package = revisions_to_package | |
| 383 self.revision_map = revision_map | |
| 384 self.verify_run = verify_run | |
| 385 self.staging_dir = staging_dir | |
| 386 self.context = context | |
| 387 self.quit_event = threading.Event() | |
| 388 self.progress_event = threading.Event() | |
| 389 self.thread = None | |
| 390 | |
| 391 def Start(self): | |
| 392 """Starts the download.""" | |
| 393 fetchargs = (self.revisions_to_package, | |
| 394 self.revision_map, | |
| 395 self.verify_run, | |
| 396 self.staging_dir, | |
| 397 self.context, | |
| 398 self.quit_event, | |
| 399 self.progress_event) | |
| 400 self.thread = threading.Thread(target=repackage_revisions, | |
| 401 name=self.name, | |
| 402 args=fetchargs) | |
| 403 self.thread.start() | |
| 404 | |
| 405 def Stop(self): | |
| 406 """Stops the download which must have been started previously.""" | |
| 407 assert self.thread, 'DownloadJob must be started before Stop is called.' | |
| 408 self.quit_event.set() | |
| 409 self.thread.join() | |
| 410 | |
| 411 def WaitFor(self): | |
| 412 """Prints a message and waits for the download to complete.""" | |
| 413 assert self.thread, 'DownloadJob must be started before WaitFor is called.' | |
| 414 self.progress_event.set() # Display progress of download. def Stop(self): | |
| 415 assert self.thread, 'DownloadJob must be started before Stop is called.' | |
| 416 self.quit_event.set() | |
| 417 self.thread.join() | |
| 418 | |
| 419 | |
| 420 def main(argv): | |
| 421 option_parser = optparse.OptionParser() | |
| 422 | |
| 423 choices = ['mac', 'win32', 'win64', 'linux'] | |
| 424 | |
| 425 option_parser.add_option('-a', '--archive', | |
| 426 choices=choices, | |
| 427 help='Builders to repacakge from [%s].' % | |
| 428 '|'.join(choices)) | |
| 429 | |
| 430 # Verifies that the chrome executable runs | |
| 431 option_parser.add_option('-v', '--verify', | |
| 432 action='store_true', | |
| 433 help='Verifies that the Chrome executes normally' | |
| 434 'without errors') | |
| 435 | |
| 436 # This option will update the revision map. | |
| 437 option_parser.add_option('-u', '--update', | |
| 438 action='store_true', | |
| 439 help='Updates the list of revisions to repackage') | |
| 440 | |
| 441 # This option will creates the revision map. | |
| 442 option_parser.add_option('-c', '--create', | |
| 443 action='store_true', | |
| 444 help='Creates the list of revisions to repackage') | |
| 445 | |
| 446 # Original bucket that contains perf builds | |
| 447 option_parser.add_option('-o', '--original', | |
| 448 type='str', | |
| 449 help='Google storage url containing original' | |
| 450 'Chrome builds') | |
| 451 | |
| 452 # Bucket that should archive lightweight perf builds | |
| 453 option_parser.add_option('-r', '--repackage', | |
| 454 type='str', | |
| 455 help='Google storage url to re-archive Chrome' | |
| 456 'builds') | |
| 457 | |
| 458 verify_run = False | |
| 459 (opts, args) = option_parser.parse_args() | |
| 460 if opts.archive is None: | |
| 461 print 'Error: missing required parameter: --archive' | |
| 462 option_parser.print_help() | |
| 463 return 1 | |
| 464 if not opts.original or not opts.repackage: | |
| 465 raise ValueError('Need to specify original gs bucket url and' | |
| 466 'repackage gs bucket url') | |
| 467 context = PathContext(opts.original, opts.repackage, opts.archive) | |
| 468 | |
| 469 if opts.create: | |
| 470 create_upload_revision_map(context) | |
| 471 | |
| 472 if opts.update: | |
| 473 update_upload_revision_map(context) | |
| 474 | |
| 475 if opts.verify: | |
| 476 verify_run = True | |
| 477 | |
| 478 revision_map = get_revision_map(context) | |
| 479 backward_rev = get_revisions_to_package(revision_map, context) | |
| 480 base_dir = os.path.join('.', context.archive) | |
| 481 # Clears any uncleared staging directories and create one | |
| 482 bisect_repackage_utils.RemovePath(base_dir) | |
| 483 bisect_repackage_utils.MaybeMakeDirectory(base_dir) | |
| 484 staging_dir = os.path.abspath(tempfile.mkdtemp(prefix='staging', | |
| 485 dir=base_dir)) | |
| 486 repackage = RepackageJob('backward_fetch', backward_rev, revision_map, | |
| 487 verify_run, staging_dir, context) | |
| 488 # Multi-threading is not currently being used. But it can be used in | |
| 489 # cases when the repackaging needs to be quicker. | |
| 490 try: | |
| 491 # Only run backward fetch | |
| 492 repackage.Start() | |
| 493 repackage.WaitFor() | |
| 494 except (KeyboardInterrupt, SystemExit): | |
| 495 print 'Cleaning up...' | |
| 496 bisect_repackage_utils.RemovePath(staging_dir) | |
| 497 print 'Cleaning up...' | |
| 498 bisect_repackage_utils.RemovePath(staging_dir) | |
| 499 | |
| 500 | |
| 501 if '__main__' == __name__: | |
| 502 sys.exit(main(sys.argv)) | |
| OLD | NEW |