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 |
| 5 """ Bisect repackage tool for Linux |
| 6 |
| 7 This script repacakges chrome builds for manual bisect script. |
| 8 """ |
| 9 |
| 10 #Declares required files to run manual bisect script on chrome Linux |
| 11 #builds in perf. Binary files that should be stripped to reduce zip file |
| 12 #size are declared. The file list was gotten from the local chrome |
| 13 #executable path in Linux. (This can be retrieved by typing 'chrome://version' |
| 14 #in chrome and following the executable path. The list needs to be updated if |
| 15 #future chrome versions require additional files. |
| 16 FILES_TO_ARCHIVE = [ |
| 17 'chrome', |
| 18 'chrome_100_percent.pak', |
| 19 'chrome_200_percent.pak', |
| 20 'default_apps', |
| 21 'icudtl.dat', |
| 22 'libwidevinecdm.so', |
| 23 'locales', |
| 24 'nacl_helper', |
| 25 'nacl_helper_bootstrap', |
| 26 'nacl_irt_x86_64.nexe', |
| 27 'natives_blob.bin', |
| 28 'PepperFlash', |
| 29 'product_logo_48.png', |
| 30 'resources.pak', |
| 31 'snapshot_blob.bin', |
| 32 'xdg-mime', |
| 33 'xdg-settings', |
| 34 ] |
| 35 |
| 36 # Declares what files should be stripped of symbols to reduce the archive size. |
| 37 STRIP_LIST = [ |
| 38 'chrome', |
| 39 'nacl_helper' |
| 40 ] |
| 41 |
| 42 # API to convert Githash to Commit position number. |
| 43 CHROMIUM_GITHASH_TO_SVN_URL = ( |
| 44 'https://cr-rev.appspot.com/_ah/api/crrev/v1/commit/%s') |
| 45 |
| 46 BUILDER_NAME = 'Linux Builder' |
| 47 ARCHIVE_PREFIX = 'full-build-linux' |
| 48 REVISION_MAP_FILE = 'revision_map.json' |
| 49 |
| 50 ################################################################################ |
| 51 import os |
| 52 import bisect_repackage_utils |
| 53 import zipfile |
| 54 import zlib |
| 55 import json |
| 56 import sys |
| 57 import optparse |
| 58 import re |
| 59 import urllib |
| 60 import threading |
| 61 from multiprocessing import Pool |
| 62 from functools import partial |
| 63 import tempfile |
| 64 import logging |
| 65 |
| 66 class PathContext(object): |
| 67 """A PathContext is used to carry the information used to construct URLs and |
| 68 paths when dealing with the storage server and archives.""" |
| 69 def __init__(self, original_gs_url, repackage_gs_url, |
| 70 builder_name=BUILDER_NAME, file_prefix=ARCHIVE_PREFIX, |
| 71 revision_file=REVISION_MAP_FILE): |
| 72 super(PathContext, self).__init__() |
| 73 self.original_gs_url = original_gs_url |
| 74 self.repackage_gs_url = repackage_gs_url |
| 75 self.builder_name = builder_name |
| 76 self.file_prefix = file_prefix |
| 77 self.revision_file = revision_file |
| 78 |
| 79 |
| 80 def get_cp_from_hash(git_hash): |
| 81 """Converts a GitHash to Commit position number""" |
| 82 json_url = CHROMIUM_GITHASH_TO_SVN_URL % git_hash |
| 83 response = urllib.urlopen(json_url) |
| 84 if response.getcode() == 200: |
| 85 try: |
| 86 data = json.loads(response.read()) |
| 87 except ValueError: |
| 88 logging.warning('ValueError for JSON URL: %s' % json_url) |
| 89 raise ValueError('Failed to extract commit position') |
| 90 else: |
| 91 logging.warning('ValueError for JSON URL: %s' % json_url) |
| 92 raise ValueError('Failed to extract commit position') |
| 93 if 'number' in data: |
| 94 return data['number'] |
| 95 logging.warning('Failed to get svn revision number for %s' % git_hash) |
| 96 raise ValueError('Failed to extract commit position') |
| 97 |
| 98 def create_cp_from_hash_map(hash_list): |
| 99 """Creates a dictionary that maps from Commit positio number |
| 100 to corresponding GitHash""" |
| 101 hash_map = {} |
| 102 count = 0 |
| 103 for git_hash in hash_list: |
| 104 try: |
| 105 cp_num = get_cp_from_hash(git_hash) |
| 106 hash_map[cp_num] = git_hash |
| 107 print "Downloaded %s / %s git hash" %(count, len(hash_list)) |
| 108 count += 1 |
| 109 except ValueError: |
| 110 pass |
| 111 return hash_map |
| 112 |
| 113 def get_list_of_suffix(bucket_address, prefix, filter_function): |
| 114 """Gets the list of suffixes in files in a google storage bucket. |
| 115 For example, a google storage bucket containing one file |
| 116 'full-build-linux_20983' will return ['20983'] if prefix is |
| 117 provided as 'full-build-linux'. Google Storage bucket |
| 118 containing multiple files will return multiple suffixes. """ |
| 119 file_list = bisect_repackage_utils.GSutilList(bucket_address) |
| 120 suffix_list = [] |
| 121 extract_suffix = '.*?%s_(.*?)\.zip' %(prefix) |
| 122 for file in file_list: |
| 123 match = re.match(extract_suffix, file) |
| 124 if match and filter_function(match.groups()[0]): |
| 125 suffix_list.append(match.groups()[0]) |
| 126 return suffix_list |
| 127 |
| 128 def download_build(cp_num, revision_map, zip_file_name, context): |
| 129 """ Download a single build corresponding to the cp_num and context.""" |
| 130 file_url = '%s/%s/%s_%s.zip' %(context.original_gs_url, context.builder_name, |
| 131 context.file_prefix, revision_map[cp_num]) |
| 132 bisect_repackage_utils.GSUtilDownloadFile(file_url, zip_file_name) |
| 133 |
| 134 def upload_build(zip_file, context): |
| 135 """Uploads a single build in zip_file to the repackage_gs_url in context.""" |
| 136 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| 137 upload_url = gs_base_url + '/' |
| 138 bisect_repackage_utils.GSUtilCopy(zip_file, upload_url) |
| 139 |
| 140 def download_revision_map(context): |
| 141 """Downloads the revision map in original_gs_url in context.""" |
| 142 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| 143 download_url = gs_base_url + '/' + context.revision_file |
| 144 try: |
| 145 bisect_repackage_utils.GSUtilDownloadFile(download_url, |
| 146 context.revision_file) |
| 147 except Exception, e: |
| 148 raise ValueError('Revision map does not exist. Re run the program with' |
| 149 '-c option to upload a revision map to google storage') |
| 150 |
| 151 def get_revision_map(context): |
| 152 """Downloads and returns the revision map in repackage_gs_url in context.""" |
| 153 bisect_repackage_utils.RemoveFile(context.revision_file) |
| 154 download_revision_map(context) |
| 155 with open(context.revision_file, 'r') as f: |
| 156 revision_map = json.load(f) |
| 157 bisect_repackage_utils.RemoveFile(context.revision_file) |
| 158 return revision_map |
| 159 |
| 160 def upload_revision_map(revision_map, context): |
| 161 """Upload the given revision_map to the repackage_gs_url in context.""" |
| 162 with open(context.revision_file, 'w') as fp: |
| 163 json.dump(revision_map, fp) |
| 164 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| 165 upload_url = gs_base_url + '/' |
| 166 bisect_repackage_utils.GSUtilCopy(context.revision_file, upload_url) |
| 167 bisect_repackage_utils.RemoveFile(context.revision_file) |
| 168 |
| 169 def create_upload_revision_map(context): |
| 170 """ Creates and uploads a dictionary that maps from GitHash to CP number.""" |
| 171 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) |
| 172 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, |
| 173 bisect_repackage_utils.isGitCommitHash) |
| 174 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) |
| 175 upload_revision_map(cp_num_to_hash_map, context) |
| 176 |
| 177 def update_upload_revision_map(context): |
| 178 """ Updates and uploads a dictionary that maps from GitHash to CP number.""" |
| 179 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) |
| 180 revision_map = get_revision_map(context) |
| 181 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, |
| 182 bisect_repackage_utils.isGitCommitHash) |
| 183 hash_list = list(set(hash_list)-set(revision_map.values())) |
| 184 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) |
| 185 merged_dict = dict(cp.num_to_hash_map.items() + revision_map.items()) |
| 186 upload_revision_map(cp_num_to_hash_map, context) |
| 187 |
| 188 def make_lightweight_archive(file_archive, archive_name): |
| 189 """Repackages and strips the archive according to FILES_TO_ARCHIVE and |
| 190 STRIP_LIST defined on the top.""" |
| 191 return bisect_repackage_utils.MakeZip('.', |
| 192 archive_name, |
| 193 FILES_TO_ARCHIVE, |
| 194 file_archive, |
| 195 raise_error=False, |
| 196 strip_files=STRIP_LIST) |
| 197 |
| 198 def remove_created_files_and_path(files, paths): |
| 199 """ Removes all the files and paths passed in.""" |
| 200 for file in files: |
| 201 bisect_repackage_utils.RemoveFile(file) |
| 202 for path in paths: |
| 203 bisect_repackage_utils.RemovePath(path) |
| 204 |
| 205 def verify_chrome_run(zip_dir): |
| 206 """This function executes chrome executable in zip_dir. Raises error if the |
| 207 execution fails for any reason.""" |
| 208 try: |
| 209 command = [os.path.join(zip_dir, 'chrome')] |
| 210 code = bisect_repackage_utils.RunCommand(command) |
| 211 if code != 0: |
| 212 raise ValueError("Executing Chrome Failed: Need to check ") |
| 213 except Exception, e: |
| 214 raise ValueError("Executing Chrome Failed: Need to check ") |
| 215 |
| 216 def repackage_single_revision(revision_map, isForwardArchive, verify_run, |
| 217 staging_dir, context, cp_num): |
| 218 """Repackages a single Chrome build for manual bisect.""" |
| 219 archive_name = '%s_%s' %(context.file_prefix, cp_num) |
| 220 file_archive = os.path.join(staging_dir, archive_name) |
| 221 zip_file_name = '%s.zip' %file_archive |
| 222 |
| 223 bisect_repackage_utils.RemoveFile(zip_file_name) |
| 224 download_build(cp_num, revision_map, zip_file_name, context) |
| 225 extract_dir = os.path.join(staging_dir, archive_name) |
| 226 bisect_repackage_utils.ExtractZip(zip_file_name, extract_dir) |
| 227 extracted_folder = os.path.join(extract_dir, context.file_prefix) |
| 228 |
| 229 (zip_dir, zip_file) = make_lightweight_archive(extracted_folder, archive_name) |
| 230 |
| 231 if verify_run: |
| 232 verify_chrome_run(zip_dir) |
| 233 |
| 234 upload_build(zip_file, context) |
| 235 # Removed temporary files created during repackaging process. |
| 236 remove_created_files_and_path([zip_file, zip_file_name], |
| 237 [zip_dir, extract_dir]) |
| 238 |
| 239 def repackage_revisions(revisions, revision_map, isForwardArchive, verify_run, |
| 240 staging_dir, context, quit_event=None, |
| 241 progress_event=None): |
| 242 """ Repackages all Chrome builds listed in revisions. This function calls |
| 243 'repackage_single_revision' with multithreading pool.'""" |
| 244 p = Pool(3) |
| 245 func = partial(repackage_single_revision, revision_map, isForwardArchive, |
| 246 verify_run, staging_dir, context) |
| 247 p.imap(func, revisions) |
| 248 p.close() |
| 249 p.join() |
| 250 |
| 251 def get_uploaded_builds(context): |
| 252 """Returns already uploaded commit positions numbers in |
| 253 context.repackage_gs_url""" |
| 254 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| 255 return get_list_of_suffix(gs_base_url, context.file_prefix, |
| 256 bisect_repackage_utils.isCommitPosition) |
| 257 |
| 258 def get_revisions_to_package(revision_map, context): |
| 259 """ Returns revisions that need to be repackaged. The first half of |
| 260 the revisions will be sorted in ascending order and the second half of |
| 261 the revisions will be sorted in desending order. |
| 262 |
| 263 The first half will be used for repackaging revisions in forward direction, |
| 264 and the second half will be used for repackaging revisions in backward |
| 265 direction.""" |
| 266 already_packaged = get_uploaded_builds(context) |
| 267 not_already_packaged = list(set(revision_map.keys())-set(already_packaged)) |
| 268 revisions_to_package = sorted(not_already_packaged) |
| 269 revisions_to_package = filter(lambda a: a != 'null', revisions_to_package) |
| 270 |
| 271 forward_rev = revisions_to_package[:len(revisions_to_package)/2] |
| 272 backward_rev = sorted(revisions_to_package[len(revisions_to_package)/2:], |
| 273 reverse=True) |
| 274 return (forward_rev, backward_rev) |
| 275 |
| 276 class RepackageJob(object): |
| 277 def __init__(self, name, revisions_to_package, revision_map, isForwardArchive, |
| 278 verify_run, staging_dir, context): |
| 279 super(RepackageJob, self).__init__() |
| 280 self.name = name |
| 281 self.revisions_to_package = revisions_to_package |
| 282 self.revision_map = revision_map |
| 283 self.isForwardArchive = isForwardArchive |
| 284 self.verify_run = verify_run |
| 285 self.staging_dir = staging_dir |
| 286 self.context = context |
| 287 self.quit_event = threading.Event() |
| 288 self.progress_event = threading.Event() |
| 289 self.thread = None |
| 290 |
| 291 def Start(self): |
| 292 """Starts the download.""" |
| 293 fetchargs = (self.revisions_to_package, |
| 294 self.revision_map, |
| 295 self.isForwardArchive, |
| 296 self.verify_run, |
| 297 self.staging_dir, |
| 298 self.context, |
| 299 self.quit_event, |
| 300 self.progress_event) |
| 301 self.thread = threading.Thread(target=repackage_revisions, |
| 302 name=self.name, |
| 303 args=fetchargs) |
| 304 self.thread.start() |
| 305 |
| 306 def Stop(self): |
| 307 """Stops the download which must have been started previously.""" |
| 308 assert self.thread, 'DownloadJob must be started before Stop is called.' |
| 309 self.quit_event.set() |
| 310 self.thread.join() |
| 311 |
| 312 def WaitFor(self): |
| 313 """Prints a message and waits for the download to complete. The download |
| 314 must have been started previously.""" |
| 315 assert self.thread, 'DownloadJob must be started before WaitFor is called.' |
| 316 self.progress_event.set() # Display progress of download. def Stop(self): |
| 317 assert self.thread, 'DownloadJob must be started before Stop is called.' |
| 318 self.quit_event.set() |
| 319 self.thread.join() |
| 320 |
| 321 def main(argv): |
| 322 option_parser = optparse.OptionParser() |
| 323 |
| 324 # Verifies that the chrome executable runs |
| 325 option_parser.add_option('-v', '--verify', |
| 326 action='store_true', |
| 327 help='Verifies that the Chrome executes normally' |
| 328 'without errors') |
| 329 |
| 330 # This option will update the revision map. |
| 331 option_parser.add_option('-u', '--update', |
| 332 action='store_true', |
| 333 help='Updates the list of revisions to repackage') |
| 334 |
| 335 # This option will creates the revision map. |
| 336 option_parser.add_option('-c', '--create', |
| 337 action='store_true', |
| 338 help='Creates the list of revisions to repackage') |
| 339 |
| 340 # Original bucket that contains perf builds |
| 341 option_parser.add_option('-o', '--original', |
| 342 type='str', |
| 343 help='Google storage url containing original Chrome builds') |
| 344 |
| 345 # Bucket that should archive lightweight perf builds |
| 346 option_parser.add_option('-r', '--repackage', |
| 347 type='str', |
| 348 help='Google storage url to re-archive Chrome builds') |
| 349 |
| 350 verify_run = False |
| 351 (opts, args) = option_parser.parse_args() |
| 352 if not opts.original or not opts.repackage: |
| 353 raise ValueError('Need to specify original gs bucket url and' |
| 354 'repackage gs bucket url') |
| 355 context = PathContext(opts.original, opts.repackage) |
| 356 |
| 357 if opts.create: |
| 358 create_upload_revision_map(context) |
| 359 |
| 360 if opts.update: |
| 361 update_upload_revision_map(context) |
| 362 |
| 363 if opts.verify: |
| 364 verify_run = True |
| 365 |
| 366 revision_map = get_revision_map(context) |
| 367 (forward_rev, backward_rev) = get_revisions_to_package(revision_map, context) |
| 368 bisect_repackage_utils.MaybeMakeDirectory(STAGING_DIR) |
| 369 bisect_repackage_utils.MaybeMakeDirectory(STAGING_DIR_BACKWARD) |
| 370 staging_dir = tempfile.mkdtemp(prefix='staging_fwd') |
| 371 staging_dir_bwd = tempfile.mkdtemp(prefix='staging_bwd') |
| 372 |
| 373 # Spwan two threads: One repackaging from oldest builds. The other repackaging |
| 374 # from the newest builds. |
| 375 forward_fetch = RepackageJob('forward_fetch', forward_rev, revision_map, True, |
| 376 verify_run, staging_dir, context) |
| 377 backward_fetch = RepackageJob('backward_fetch', backward_rev, revision_map, |
| 378 False, verify_run, staging_dir_bwd, |
| 379 context) |
| 380 forward_fetch.Start() |
| 381 backward_fetch.Start() |
| 382 forward_fetch.WaitFor() |
| 383 backward_fetch.WaitFor() |
| 384 |
| 385 if '__main__' == __name__: |
| 386 sys.exit(main(sys.argv)) |
OLD | NEW |