| #!/usr/bin/python |
| # Copyright 2022 The Cobalt Authors. All Rights Reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Tools for creating a compressed tar archive. |
| |
| Usage: |
| create_archive.py -d <dest_path> -s <source_paths> |
| |
| Args: |
| dest_path: Path (can be absolute or relative) to the destination folder |
| where the compressed archive is created. |
| source_paths: Paths (can be absolute or relative) to the source folders |
| which should archived. |
| |
| Returns: |
| 0 on success. |
| |
| Raises: |
| ValueError: An error occurred when adding a source to the archive, when |
| compressing the archive, or when the archive has uppercase symlink names. |
| """ |
| |
| import argparse |
| import glob |
| import logging |
| import os |
| import subprocess |
| import sys |
| import time |
| |
| import worker_tools |
| |
| # Use strict include filter to pass artifacts to Mobile Harness |
| TEST_PATTERNS = [ |
| 'content/*', |
| 'install/*', |
| # TODO: All of those should be built under deploy/ |
| '*.apk', |
| 'ds_archive.zip', |
| 'app_launcher.zip', |
| 'crashpad_handler', |
| 'elf_loader_sandbox' |
| ] |
| |
| # Reference: https://sevenzip.osdn.jp/chm/cmdline/switches/ |
| _7Z_PATH = r'%ProgramFiles%\7-Zip\7z.exe' # pylint:disable=invalid-name |
| # 1 is fastest and least compression, max is 9, see -mx parameter in docs |
| _COMPRESSION_LEVEL = 1 |
| |
| _LINUX_PARALLEL_ZIP_EXTENSION = 'tar.xz' |
| _LINUX_PARALLEL_ZIP_PROGRAM = 'xz -T0' |
| _LINUX_SERIAL_ZIP_EXTENSION = 'tar.gz' |
| _LINUX_SERIAL_ZIP_PROGRAM = 'gzip' |
| |
| |
| # TODO(b/143570416): Refactor this method and similar ones in a common file. |
| def _IsWindows(): |
| return sys.platform in ['win32'] |
| |
| |
| class InvalidArchiveExtensionException(RuntimeError): |
| pass |
| |
| |
| def _CheckArchiveExtension(archive_path, is_parallel): |
| if is_parallel and (_LINUX_PARALLEL_ZIP_EXTENSION not in archive_path): |
| raise InvalidArchiveExtensionException( |
| f'Parallelized zip requested, but path is {archive_path}') |
| if not is_parallel and (_LINUX_PARALLEL_ZIP_EXTENSION in archive_path): |
| raise InvalidArchiveExtensionException( |
| f'Serialized zip requested, but path is: {archive_path}') |
| |
| |
| def _CreateCompressedArchive(source_paths, |
| dest_path, |
| patterns, |
| intermediate, |
| is_parallel=False): |
| """Create a compressed tar archive. |
| |
| This function creates a compressed tar archive from all of the source |
| directories in |source_paths|. The basename of each source directory is |
| maintained within the archive to prevent file overwriting on extraction. |
| |
| Args: |
| source_paths: Paths (that can be absolute or relative) to the source folders |
| which should be archived. |
| dest_path: Path (can be absolute or relative) that the compressed archive |
| should be created at. |
| patterns: Patterns of allowable files to archive. |
| intermediate: Flag to archive intermediate build artifacts needed by |
| Buildbot. |
| |
| Raises: |
| ValueError: An error occurred when adding a source to the archive, when |
| compressing the archive, or when the archive has uppercase symlink names. |
| """ |
| # Ensure the directory where we will create our archive exists, and that the |
| # directory does not contain a previous archive. |
| dest_parent = os.path.dirname(os.path.abspath(dest_path)) |
| if os.path.exists(dest_path): |
| os.remove(dest_path) |
| elif not os.path.exists(dest_parent): |
| os.makedirs(dest_parent) |
| |
| # Get the current working directory to return to once we are done archiving. |
| cwd = os.getcwd() |
| |
| _CheckArchiveExtension(dest_path, is_parallel) |
| |
| try: |
| intermediate_tar_path = os.path.join( |
| dest_parent, 'create_archive_intermediate_artifacts.tar') |
| |
| # Generate the commands to archive each source directory and execute them |
| # from the parent directory of the source we are adding to the archive. |
| for source_path in source_paths: |
| # Determine if only certain contents of the source_path are to be |
| # included. |
| if ',' in source_path: |
| source_path_parts = source_path.split(',') |
| source_path = source_path_parts[0] |
| included_paths = source_path_parts[1:] |
| else: |
| included_paths = [''] |
| |
| logging.info('source path: %s', source_path) |
| logging.info('included subpaths: %s', included_paths) |
| |
| if not os.path.exists(source_path): |
| raise ValueError(f'Failed to find source path "{source_path}".') |
| |
| for target_path in included_paths: |
| if not os.path.exists(os.path.join(source_path, target_path)): |
| raise ValueError(f'Failed to find target path "{target_path}".') |
| |
| source_parent_dir, source_dir_name = os.path.split( |
| os.path.abspath(source_path)) |
| |
| os.chdir(source_parent_dir) |
| t_start = time.time() |
| for included_path in included_paths: |
| # Check every path to make sure there is no symlink being created with |
| # uppercase in it. This causes issues in Windows Archives. |
| for dirpath, dirnames, _ in os.walk(included_path): |
| for dirname in dirnames: |
| iterpath = os.path.join(dirpath, dirname) |
| if os.path.islink(iterpath) and iterpath.lower() != iterpath: |
| raise ValueError('Archive contains uppercase symlink names.\n' |
| 'Please change these names to lowercase.') |
| # Iteratively add filtered source files to the final tar |
| _AddSourceToTar( |
| os.path.join(source_dir_name, included_path), intermediate_tar_path, |
| patterns, intermediate) |
| t_archive = time.time() - t_start |
| logging.info('Archiving took %5.2f seconds', t_archive) |
| os.chdir(cwd) |
| |
| archive_size = os.path.getsize(intermediate_tar_path) / 1e9 |
| logging.info('Size of %s: %5.2f Gi', intermediate_tar_path, archive_size) |
| # Compress the archive. |
| cmd = _CreateZipCommand(intermediate_tar_path, dest_path, is_parallel) |
| logging.info('Compressing "%s" into %s with "%s".', intermediate_tar_path, |
| dest_path, cmd) |
| t_start = time.time() |
| subprocess.check_call(cmd, shell=True) |
| t_compress = time.time() - t_start |
| logging.info('Compressing took %5.2f seconds', t_compress) |
| archive_size = os.path.getsize(dest_path) / 1e9 |
| logging.info('Size of %s: %5.2f Gi', dest_path, archive_size) |
| finally: |
| if os.path.exists(intermediate_tar_path): |
| os.remove(intermediate_tar_path) |
| |
| |
| def UncompressArchive(source_path, dest_path, is_parallel=False): |
| """Uncompress tar archive. |
| |
| Args: |
| source_path: Path (that can be absolute or relative) to the archive. |
| dest_path: Path (can be absolute or relative) where the archive should be |
| uncompressed to. |
| """ |
| _CheckArchiveExtension(source_path, is_parallel) |
| |
| # Creates and cleans up destination path |
| dest_path = os.path.abspath(dest_path) |
| if not os.path.exists(dest_path): |
| os.makedirs(dest_path) |
| |
| try: |
| intermediate_tar_path = os.path.join( |
| dest_path, 'create_archive_intermediate_artifacts.tar') |
| |
| # Unzips archive |
| cmd = _CreateUnzipCommand(source_path, intermediate_tar_path, is_parallel) |
| logging.info('Unzipping "%s" into %s with "%s".', source_path, |
| intermediate_tar_path, cmd) |
| subprocess.check_call(cmd, shell=True) |
| |
| # Untars archive |
| cmd = _CreateUntarCommand(intermediate_tar_path) |
| logging.info('Untarring "%s" into "%s" with "%s".', intermediate_tar_path, |
| dest_path, cmd) |
| subprocess.check_call(cmd, shell=True, cwd=dest_path) |
| finally: |
| os.remove(intermediate_tar_path) |
| |
| |
| def _AddSourceToTar(source_path, intermediate_tar_path, patterns, intermediate): |
| """Add a directory to an archive. |
| |
| The tarfile Python 2.7 module does not work with symlinks, so the operation |
| is done in platform-specific ways. |
| |
| Args: |
| source_path: Source directory to add to archive. |
| intermediate_tar_path: Path to tar file to add to (or create). |
| patterns: Patterns of allowable files to archive. |
| intermediate: Flag to archive intermediate build artifacts needed by |
| Buildbot. |
| """ |
| cmd = _CreateTarCommand(source_path, intermediate_tar_path, patterns, |
| intermediate) |
| logging.info('Adding "%s" to "%s" with "%s".', source_path, |
| intermediate_tar_path, cmd) |
| subprocess.check_call(cmd, shell=True) |
| |
| |
| def _CreateTarCommand(source_path, intermediate_tar_path, patterns, |
| intermediate): |
| if _IsWindows(): |
| return _CreateWindowsTarCmd(source_path, intermediate_tar_path, patterns, |
| intermediate) |
| return _CreateLinuxTarCmd(source_path, intermediate_tar_path, patterns, |
| intermediate) |
| |
| |
| def _CreateWindowsTarCmd(source_path, intermediate_tar_path, patterns, |
| intermediate): |
| exclude_patterns = worker_tools.INTERMEDIATE_FILE_NAMES_WILDCARDS |
| if intermediate: |
| exclude_patterns = worker_tools.BUILDBOT_INTERMEDIATE_FILE_NAMES_WILDCARDS |
| excludes = ' '.join([ |
| f'-xr!{x.replace(worker_tools.SOURCE_DIR, source_path)}' |
| for x in exclude_patterns |
| ]) |
| contents = [ |
| os.path.join(source_path, pattern) |
| for pattern in patterns |
| if glob.glob(os.path.join(source_path, pattern)) |
| ] |
| files_to_tar = ' '.join(contents) |
| return (f'"{_7Z_PATH}" a {excludes} -bsp1 -snl -ttar ' |
| f'{intermediate_tar_path} {files_to_tar}') |
| |
| |
| def _CreateLinuxTarCmd(source_path, intermediate_tar_path, patterns, |
| intermediate): |
| exclude_patterns = worker_tools.INTERMEDIATE_FILE_NAMES_WILDCARDS |
| if intermediate: |
| exclude_patterns = worker_tools.BUILDBOT_INTERMEDIATE_FILE_NAMES_WILDCARDS |
| excludes = ' '.join([ |
| f'--exclude="{x.replace(worker_tools.SOURCE_DIR, source_path)}"' |
| for x in exclude_patterns |
| ]) |
| mode = 'r' if os.path.exists(intermediate_tar_path) else 'c' |
| contents = [ |
| os.path.join(source_path, pattern) |
| for pattern in patterns |
| if glob.glob(os.path.join(source_path, pattern)) |
| ] |
| files_to_tar = ' '.join(contents) |
| return (f'tar -{mode}f {intermediate_tar_path} --format=posix ' |
| f'{excludes} {files_to_tar}') |
| |
| |
| def _CreateUntarCommand(intermediate_tar_path): |
| if _IsWindows(): |
| return f'"{_7Z_PATH}" x -bsp1 {intermediate_tar_path}' |
| else: |
| return f'tar -xf {intermediate_tar_path}' |
| |
| |
| def _CreateZipCommand(intermediate_tar_path, dest_path, is_parallel=False): |
| if _IsWindows(): |
| return (f'"{_7Z_PATH}" a -bsp1 -mx={_COMPRESSION_LEVEL} ' |
| f'-mmt=on {dest_path} {intermediate_tar_path}') |
| else: |
| zip_program = ( |
| _LINUX_PARALLEL_ZIP_PROGRAM |
| if is_parallel else _LINUX_SERIAL_ZIP_PROGRAM) |
| return f'{zip_program} -vc -1 {intermediate_tar_path} > {dest_path}' |
| |
| |
| def _CreateUnzipCommand(source_path, intermediate_tar_path, is_parallel=False): |
| if _IsWindows(): |
| tar_parent = os.path.dirname(intermediate_tar_path) |
| return f'"{_7Z_PATH}" x -bsp1 {source_path} -o{tar_parent}' |
| else: |
| zip_program = ( |
| _LINUX_PARALLEL_ZIP_PROGRAM |
| if is_parallel else _LINUX_SERIAL_ZIP_PROGRAM) |
| return f'{zip_program} -dvc {source_path} > {intermediate_tar_path}' |
| |
| |
| def _CleanUp(intermediate_tar_path): |
| if os.path.exists(intermediate_tar_path): |
| os.remove(intermediate_tar_path) |
| |
| |
| def main(): |
| logging_format = '[%(levelname)s:%(filename)s:%(lineno)s] %(message)s' |
| logging.basicConfig( |
| level=logging.INFO, format=logging_format, datefmt='%H:%M:%S') |
| |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '-d', |
| '--dest_path', |
| type=str, |
| required=True, |
| help='The path to create the archive at.') |
| parser.add_argument( |
| '-s', |
| '--source_paths', |
| type=str, |
| nargs='+', |
| required=True, |
| help='The paths to the source folders which should be archived. ' |
| 'To include only specific subdirectories, but compress relative ' |
| 'to some root directory, pass: "<root>,<target_subdir>,...".') |
| parser.add_argument( |
| '-x', |
| '--extract', |
| default=False, |
| action='store_true', |
| help='Uncompresses tar archive.') |
| parser.add_argument( |
| '-p', |
| '--patterns', |
| type=str, |
| nargs='+', |
| required=False, |
| default=['*'], |
| help='The patterns of allowable files to archive. Defaults to *.') |
| parser.add_argument( |
| '--test_infra', |
| default=False, |
| action='store_true', |
| help='Utilizes predefined test infrastructure patterns.') |
| parser.add_argument( |
| '--intermediate', |
| default=False, |
| action='store_true', |
| help='Archives intermediate build artifacts needed by Buildbot.') |
| parser.add_argument( |
| '--parallel', |
| default=False, |
| action='store_true', |
| help='Archives using parallelized methods.') |
| args = parser.parse_args() |
| |
| worker_tools.MoveToWindowsShortSymlink() |
| if args.extract: |
| UncompressArchive(args.source_paths[0], args.dest_path, args.parallel) |
| else: |
| _CreateCompressedArchive( |
| args.source_paths, args.dest_path, |
| TEST_PATTERNS if args.test_infra else args.patterns, args.intermediate, |
| args.parallel) |
| |
| |
| if __name__ == '__main__': |
| main() |