Chromium Code Reviews| Index: tools/bisect_repackage/bisect_repackage_linux.py |
| diff --git a/tools/bisect_repackage/bisect_repackage_linux.py b/tools/bisect_repackage/bisect_repackage_linux.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..4ec67e91f1bfc3cccae07b91fb8a5bf87ee9b749 |
| --- /dev/null |
| +++ b/tools/bisect_repackage/bisect_repackage_linux.py |
| @@ -0,0 +1,393 @@ |
| +# Copyright (c) 2016 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +""" Bisect repackage tool for Linux |
| + |
| +This script repacakges chrome builds for manual bisect script. |
| +""" |
| + |
| +#Declares required files to run manual bisect script on chrome Linux |
| +#builds in perf. Binary files that should be stripped to reduce zip file |
| +#size are declared. The file list was gotten from the local chrome |
| +#executable path in Linux. (This can be retrieved by typing 'chrome://version' |
| +#in chrome and following the executable path. The list needs to be updated if |
| +#future chrome versions require additional files. |
| +FILES_TO_ARCHIVE = [ |
| + 'chrome', |
| + 'chrome_100_percent.pak', |
| + 'chrome_200_percent.pak', |
| + 'default_apps', |
| + 'icudtl.dat', |
| + 'libwidevinecdm.so', |
| + 'locales', |
| + 'nacl_helper', |
| + 'nacl_helper_bootstrap', |
| + 'nacl_irt_x86_64.nexe', |
| + 'natives_blob.bin', |
| + 'PepperFlash', |
| + 'product_logo_48.png', |
| + 'resources.pak', |
| + 'snapshot_blob.bin', |
| + 'xdg-mime', |
| + 'xdg-settings', |
| +] |
| + |
| +# Declares what files should be stripped of symbols to reduce the archive size. |
| +STRIP_LIST = [ |
| + 'chrome', |
| + 'nacl_helper' |
| +] |
| + |
| +# API to convert Githash to Commit position number. |
| +CHROMIUM_GITHASH_TO_SVN_URL = ( |
| + 'https://cr-rev.appspot.com/_ah/api/crrev/v1/commit/%s') |
| + |
| +BUILDER_NAME = 'Linux Builder' |
| +ARCHIVE_PREFIX = 'full-build-linux' |
| +REVISION_MAP_FILE = 'revision_map.json' |
| + |
| +################################################################################ |
| +import os |
| +import bisect_repackage_utils |
| +import zipfile |
| +import zlib |
| +import json |
| +import sys |
| +import optparse |
| +import re |
| +import urllib |
| +import threading |
| +from multiprocessing import Pool |
| +from functools import partial |
| + |
| +# Declares where files should be stored temporarily for staging. |
| +# TODO (miimnk): use tempfile.mkdtemp to make temporary folders |
| +STAGING_DIR= os.path.join('.', 'a', 'tmp_forward') |
|
dimu
2016/08/19 04:14:05
is there any cleanup for the staging dir?
miimnk
2016/08/19 20:29:43
Erased the two global variables and used 'tempfile
|
| +STAGING_DIR_BACKWARD = os.path.join('.', 'a', 'tmp_backward') |
| + |
| + |
| +class PathContext(object): |
| + """A PathContext is used to carry the information used to construct URLs and |
| + paths when dealing with the storage server and archives.""" |
| + def __init__(self, original_gs_url, repackage_gs_url, |
| + builder_name=BUILDER_NAME, file_prefix=ARCHIVE_PREFIX, |
| + revision_map=REVISION_MAP_FILE): |
| + super(PathContext, self).__init__() |
| + self.original_gs_url = original_gs_url |
| + self.repackage_gs_url = repackage_gs_url |
| + self.builder_name = builder_name |
| + self.file_prefix = file_prefix |
| + self.revision_map = revision_map |
| + |
| + |
| +def get_cp_from_hash(git_hash): |
| + """Converts a GitHash to Commit position number""" |
| + json_url = CHROMIUM_GITHASH_TO_SVN_URL % git_hash |
| + response = urllib.urlopen(json_url) |
| + if response.getcode() == 200: |
| + try: |
| + data = json.loads(response.read()) |
| + except ValueError: |
| + print 'ValueError for JSON URL: %s' % json_url |
|
dimu
2016/08/19 04:14:05
It would be better to use logging instead of print
miimnk
2016/08/19 20:29:43
Done.
|
| + return None |
|
dimu
2016/08/19 04:14:05
throw exception for failure, and use try/catch in
miimnk
2016/08/19 20:29:43
Done.
|
| + else: |
| + print 'ValueError for JSON URL: %s' % json_url |
| + return None |
| + if 'number' in data: |
| + return data['number'] |
| + print 'Failed to get svn revision number for %s' % git_hash |
| + return None |
| + |
| +def create_cp_from_hash_map(hash_list): |
| + """Creates a dictionary that maps from Commit positio number |
| + to corresponding GitHash""" |
| + hash_map = {} |
| + count = 0 |
| + for git_hash in hash_list: |
| + cp_num = get_cp_from_hash(git_hash) |
| + hash_map[cp_num] = git_hash |
| + print "Downloaded %s / %s git hash" %(count, len(hash_list)) |
| + count += 1 |
| + return hash_map |
| + |
| +def isProperHash(regex_match): |
|
dimu
2016/08/19 04:14:05
move it to utils
dimu
2016/08/19 04:14:05
isGitCommitHash
miimnk
2016/08/19 20:29:42
Done.
miimnk
2016/08/19 20:29:43
Done.
|
| + """Checks if match is correct SHA1 hash""" |
| + if len(regex_match) == 40: return True |
|
dimu
2016/08/19 04:14:05
use re.match(r"^[0-9,A-F]{40}$", regex_match.upper
miimnk
2016/08/19 20:29:43
Done.
|
| + return False |
| + |
| +def isProperRevision(regex_match): |
|
dimu
2016/08/19 04:14:05
move this to utils
miimnk
2016/08/19 20:29:43
Done.
|
| + """Checks if match is correct revision(Cp number) format""" |
| + if len(regex_match) == 6: return True |
|
dimu
2016/08/19 04:14:05
also use regex
miimnk
2016/08/19 20:29:43
Done.
|
| + return False |
| + |
| +def get_list_of_suffix(bucket_address, prefix, filter_function): |
| + """Gets the list of suffixes in files in a google storage bucket. |
| + For example, a google storage bucket containing one file |
| + 'full-build-linux_20983' will return ['20983'] if prefix is |
| + provided as 'full-build-linux'. Google Storage bucket |
| + containing multiple files will return multiple suffixes. """ |
| + file_list = bisect_repackage_utils.GSutilList(bucket_address) |
| + suffix_list = [] |
| + extract_suffix = '.*?%s_(.*?)\.zip' %(prefix) |
| + for file in file_list: |
| + match = re.match(extract_suffix, file) |
| + if match and filter_function(match.groups()[0]): |
| + suffix_list.append(match.groups()[0]) |
| + return suffix_list |
| + |
| +def download_build(cp_num, revision_map, zip_file_name, context): |
| + """ Download a single build corresponding to the cp_num and context.""" |
| + file_url = '%s/%s/%s_%s.zip' %(context.original_gs_url, context.builder_name, |
| + context.file_prefix, revision_map[cp_num]) |
| + bisect_repackage_utils.GSUtilDownloadFile(file_url, zip_file_name) |
| + |
| +def upload_build(zip_file, context): |
| + """Uploads a single build in zip_file to the repackage_gs_url in context.""" |
| + gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| + upload_url = gs_base_url + '/' |
| + bisect_repackage_utils.GSUtilCopy(zip_file, upload_url) |
| + |
| +def download_revision_map(context): |
| + """Downloads the revision map in original_gs_url in context.""" |
| + gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| + download_url = gs_base_url + '/' + context.revision_map |
| + try: |
| + bisect_repackage_utils.GSUtilDownloadFile(download_url, |
| + context.revision_map) |
| + except Exception, e: |
| + raise ValueError('Revision map does not exist. Re run the program with' |
| + '-c option to upload a revision map to google storage') |
| + |
| +def get_revision_map(context): |
| + """Downloads and returns the revision map in repackage_gs_url in context.""" |
| + bisect_repackage_utils.RemoveFile(context.revision_map) |
| + download_revision_map(context) |
| + with open(context.revision_map, 'r') as f: |
|
dimu
2016/08/19 04:14:05
nit: better naming: revision_file
miimnk
2016/08/19 20:29:43
Done.
|
| + revision_map = json.load(f) |
| + bisect_repackage_utils.RemoveFile(context.revision_map) |
| + return revision_map |
| + |
| +def upload_revision_map(revision_map, context): |
| + """Upload the given revision_map to the repackage_gs_url in context.""" |
| + with open(context.revision_map, 'w') as fp: |
| + json.dump(revision_map, fp) |
| + gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| + upload_url = gs_base_url + '/' |
| + bisect_repackage_utils.GSUtilCopy(context.revision_map, upload_url) |
| + bisect_repackage_utils.RemoveFile(context.revision_map) |
| + |
| +def create_upload_revision_map(context): |
| + """ Creates and uploads a dictionary that maps from GitHash to CP number.""" |
| + gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) |
| + hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, isProperHash) |
| + cp_num_to_hash_map = create_cp_from_hash_map(hash_list) |
| + upload_revision_map(cp_num_to_hash_map, context) |
| + |
| +def update_upload_revision_map(context): |
| + """ Updates and uploads a dictionary that maps from GitHash to CP number.""" |
| + gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) |
| + revision_map = get_revision_map(context) |
| + hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, isProperHash) |
| + hash_list = list(set(hash_list)-set(revision_map.values())) |
| + cp_num_to_hash_map = create_cp_from_hash_map(hash_list) |
| + upload_revision_map(cp_num_to_hash_map, context) |
| + |
| +def make_filtered_archive(file_archive, archive_name): |
|
dimu
2016/08/19 04:14:05
better naming
miimnk
2016/08/19 20:29:43
Done. Changed to 'make_lightweight_archive'.
|
| + """Repackages and strips the archive according to FILES_TO_ARCHIVE and |
| + STRIP_LIST defined on the top.""" |
| + return bisect_repackage_utils.MakeZip('.', |
| + archive_name, |
| + FILES_TO_ARCHIVE, |
| + file_archive, |
| + raise_error=False, |
| + strip_files=STRIP_LIST) |
| + |
| +def remove_created_files_and_path(files, paths): |
| + """ Removes all the files and paths passed in.""" |
| + for file in files: |
| + bisect_repackage_utils.RemoveFile(file) |
| + for path in paths: |
| + bisect_repackage_utils.RemovePath(path) |
| + |
| +def verify_chrome_run(zip_dir): |
| + """This function executes chrome executable in zip_dir. Raises error if the |
| + execution fails for any reason.""" |
| + try: |
| + command = [os.path.join(zip_dir, 'chrome')] |
| + code = bisect_repackage_utils.RunCommand(command) |
| + if code != 0: |
| + raise ValueError("Executing Chrome Failed: Need to check ") |
| + except Exception, e: |
| + raise ValueError("Executing Chrome Failed: Need to check ") |
| + |
| +def repackage_single_revision(revision_map, isForwardArchive, verify_run, |
| + staging_dir, context, cp_num): |
| + """Repackages a single Chrome build for manual bisect.""" |
| + archive_name = '%s_%s' %(context.file_prefix, cp_num) |
| + file_archive = os.path.join(staging_dir, archive_name) |
| + zip_file_name = '%s.zip' %file_archive |
| + |
| + bisect_repackage_utils.RemoveFile(zip_file_name) |
| + download_build(cp_num, revision_map, zip_file_name, context) |
| + extract_dir = os.path.join(staging_dir, archive_name) |
| + bisect_repackage_utils.ExtractZip(zip_file_name, extract_dir) |
| + extracted_folder = os.path.join(extract_dir, context.file_prefix) |
| + |
| + (zip_dir, zip_file) = make_filtered_archive(extracted_folder, archive_name) |
| + |
| + if verify_run: |
| + verify_chrome_run(zip_dir) |
| + |
| + upload_build(zip_file, context) |
| + # Removed temporary files created during repackaging process. |
| + remove_created_files_and_path([zip_file, zip_file_name], |
| + [zip_dir, extract_dir]) |
| + |
| +def repackage_revisions(revisions, revision_map, isForwardArchive, verify_run, |
| + staging_dir, context, quit_event=None, |
| + progress_event=None): |
| + """ Repackages all Chrome builds listed in revisions. This function calls |
| + 'repackage_single_revision' with multithreading pool.'""" |
| + p = Pool(3) |
| + func = partial(repackage_single_revision, revision_map, isForwardArchive, |
| + verify_run, staging_dir, context) |
| + p.imap(func, revisions) |
| + p.close() |
| + p.join() |
| + |
| +def get_uploaded_builds(context): |
| + """Returns already uploaded commit positions numbers in |
| + context.repackage_gs_url""" |
| + gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) |
| + return get_list_of_suffix(gs_base_url, context.file_prefix, isProperRevision) |
| + |
| +def get_revisions_to_package(revision_map, context): |
| + """ Returns revisions that need to be repackaged. The first half of |
| + the revisions will be sorted in ascending order and the second half of |
| + the revisions will be sorted in desending order. |
| + |
| + The first half will be used for repackaging revisions in forward direction, |
| + and the second half will be used for repackaging revisions in backward |
| + direction.""" |
| + already_packaged = get_uploaded_builds(context) |
| + not_already_packaged = list(set(revision_map.keys())-set(already_packaged)) |
| + revisions_to_package = sorted(not_already_packaged) |
| + revisions_to_package = filter(lambda a: a != 'null', revisions_to_package) |
| + |
| + forward_rev = revisions_to_package[:len(revisions_to_package)/2] |
| + backward_rev = sorted(revisions_to_package[len(revisions_to_package)/2:], |
| + reverse=True) |
| + return (forward_rev, backward_rev) |
| + |
| +class RepackageJob(object): |
| + def __init__(self, name, revisions_to_package, revision_map, isForwardArchive, |
| + verify_run, staging_dir, context): |
| + super(RepackageJob, self).__init__() |
| + self.name = name |
| + self.revisions_to_package = revisions_to_package |
| + self.revision_map = revision_map |
| + self.isForwardArchive = isForwardArchive |
| + self.verify_run = verify_run |
| + self.staging_dir = staging_dir |
| + self.context = context |
| + self.quit_event = threading.Event() |
| + self.progress_event = threading.Event() |
| + self.thread = None |
| + |
| + def Start(self): |
| + """Starts the download.""" |
| + fetchargs = (self.revisions_to_package, |
| + self.revision_map, |
| + self.isForwardArchive, |
| + self.verify_run, |
| + self.staging_dir, |
| + self.context, |
| + self.quit_event, |
| + self.progress_event) |
| + self.thread = threading.Thread(target=repackage_revisions, |
| + name=self.name, |
| + args=fetchargs) |
| + self.thread.start() |
| + |
| + def Stop(self): |
| + """Stops the download which must have been started previously.""" |
| + assert self.thread, 'DownloadJob must be started before Stop is called.' |
| + self.quit_event.set() |
| + self.thread.join() |
| + |
| + def WaitFor(self): |
| + """Prints a message and waits for the download to complete. The download |
| + must have been started previously.""" |
| + assert self.thread, 'DownloadJob must be started before WaitFor is called.' |
| + self.progress_event.set() # Display progress of download. def Stop(self): |
| + assert self.thread, 'DownloadJob must be started before Stop is called.' |
| + self.quit_event.set() |
| + self.thread.join() |
| + |
| +def main(argv): |
| + option_parser = optparse.OptionParser() |
| + |
| + # Verifies that the chrome executable runs |
| + option_parser.add_option('-v', '--verify', |
| + action='store_true', |
| + help='Verifies that the Chrome executes normally' |
| + 'without errors') |
| + |
| + # This option will update the revision map. |
| + option_parser.add_option('-u', '--update', |
| + action='store_true', |
| + help='Updates the list of revisions to repackage') |
| + |
| + # This option will creates the revision map. |
| + option_parser.add_option('-c', '--create', |
| + action='store_true', |
| + help='Creates the list of revisions to repackage') |
| + |
| + # Original bucket that contains perf builds |
| + option_parser.add_option('-o', '--original', |
| + type='str', |
| + help='Google storage url containing original Chrome builds') |
| + |
| + # Bucket that should archive lightweight perf builds |
| + option_parser.add_option('-r', '--repackage', |
| + type='str', |
| + help='Google storage url to re-archive Chrome builds') |
| + |
| + verify_run = False |
| + (opts, args) = option_parser.parse_args() |
| + if not opts.original or not opts.repackage: |
| + raise ValueError('Need to specify original gs bucket url and' |
| + 'repackage gs bucket url') |
| + context = PathContext(opts.original, opts.repackage) |
| + |
| + if opts.create: |
| + create_upload_revision_map(context) |
| + |
| + if opts.update: |
| + update_upload_revision_map(context) |
| + |
| + if opts.verify: |
| + verify_run = True |
| + |
| + revision_map = get_revision_map(context) |
| + (forward_rev, backward_rev) = get_revisions_to_package(revision_map, context) |
| + bisect_repackage_utils.MaybeMakeDirectory(STAGING_DIR) |
| + bisect_repackage_utils.MaybeMakeDirectory(STAGING_DIR_BACKWARD) |
| + |
| + # Spwan two threads: One repackaging from oldest builds. The other repackaging |
| + # from the newest builds. |
| + forward_fetch = RepackageJob('forward_fetch', forward_rev, revision_map, True, |
| + verify_run, os.path.abspath(STAGING_DIR), |
| + context) |
| + backward_fetch = RepackageJob('backward_fetch', backward_rev, revision_map, |
| + False, verify_run, |
| + os.path.abspath(STAGING_DIR_BACKWARD), |
| + context) |
| + forward_fetch.Start() |
| + backward_fetch.Start() |
| + forward_fetch.WaitFor() |
| + backward_fetch.WaitFor() |
| + |
| +if '__main__' == __name__: |
| + sys.exit(main(sys.argv)) |