#!/usr/bin/env python3 # Copyright 2022 The ChromiumOS Authors. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Copies rust-bootstrap artifacts from an SDK build to localmirror. We use localmirror to host these artifacts, but they've changed a bit over time, so simply `gsutil.py cp $FROM $TO` doesn't work. This script allows the convenience of the old `cp` command. """ import argparse import logging import os from pathlib import Path import shutil import subprocess import sys import tempfile from typing import List _LOCALMIRROR_ROOT = 'gs://chromeos-localmirror/distfiles/' def _is_in_chroot() -> bool: return Path('/etc/cros_chroot_version').exists() def _ensure_pbzip2_is_installed(): if shutil.which('pbzip2'): return logging.info('Auto-installing pbzip2...') subprocess.run(['sudo', 'emerge', '-G', 'pbzip2'], check=True) def _determine_target_path(sdk_path: str) -> str: """Determine where `sdk_path` should sit in localmirror.""" gs_prefix = 'gs://' if not sdk_path.startswith(gs_prefix): raise ValueError(f'Invalid GS path: {sdk_path!r}') file_name = Path(sdk_path[len(gs_prefix):]).name return _LOCALMIRROR_ROOT + file_name def _download(remote_path: str, local_file: Path): """Downloads the given gs:// path to the given local file.""" logging.info('Downloading %s -> %s', remote_path, local_file) subprocess.run( ['gsutil.py', 'cp', remote_path, str(local_file)], check=True, ) def _debinpkgify(binpkg_file: Path) -> Path: """Converts a binpkg into the files it installs. Note that this function makes temporary files in the same directory as `binpkg_file`. It makes no attempt to clean them up. """ logging.info('Converting %s from a binpkg...', binpkg_file) # The SDK builder produces binary packages: # https://wiki.gentoo.org/wiki/Binary_package_guide # # Which means that `binpkg_file` is in the XPAK format. We want to split # that out, and recompress it from zstd (which is the compression format # that CrOS uses) to bzip2 (which is what we've historically used, and # which is what our ebuild expects). tmpdir = binpkg_file.parent def _mkstemp(suffix=None) -> str: fd, file_path = tempfile.mkstemp(dir=tmpdir, suffix=suffix) os.close(fd) return Path(file_path) # First, split the actual artifacts that land in the chroot out to # `temp_file`. artifacts_file = _mkstemp() logging.info('Extracting artifacts from %s into %s...', binpkg_file, artifacts_file) with artifacts_file.open('wb') as f: subprocess.run( [ 'qtbz2', '-s', '-t', '-O', str(binpkg_file), ], check=True, stdout=f, ) decompressed_artifacts_file = _mkstemp() decompressed_artifacts_file.unlink() logging.info('Decompressing artifacts from %s to %s...', artifacts_file, decompressed_artifacts_file) subprocess.run( [ 'zstd', '-d', str(artifacts_file), '-o', str(decompressed_artifacts_file), ], check=True, ) # Finally, recompress it as a tbz2. tbz2_file = _mkstemp('.tbz2') logging.info( 'Recompressing artifacts from %s to %s (this may take a while)...', decompressed_artifacts_file, tbz2_file) with tbz2_file.open('wb') as f: subprocess.run( [ 'pbzip2', '-9', '-c', str(decompressed_artifacts_file), ], check=True, stdout=f, ) return tbz2_file def _upload(local_file: Path, remote_path: str, force: bool): """Uploads the local file to the given gs:// path.""" logging.info('Uploading %s -> %s', local_file, remote_path) cmd_base = ['gsutil.py', 'cp', '-a', 'public-read'] if not force: cmd_base.append('-n') subprocess.run( cmd_base + [str(local_file), remote_path], check=True, stdin=subprocess.DEVNULL, ) def main(argv: List[str]): logging.basicConfig( format='>> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: ' '%(message)s', level=logging.INFO, ) parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( 'sdk_artifact', help='Path to the SDK rust-bootstrap artifact to copy. e.g., ' 'gs://chromeos-prebuilt/host/amd64/amd64-host/' 'chroot-2022.07.12.134334/packages/dev-lang/' 'rust-bootstrap-1.59.0.tbz2.') parser.add_argument( '-n', '--dry-run', action='store_true', help='Do everything except actually uploading the artifact.') parser.add_argument( '--force', action='store_true', help='Upload the artifact even if one exists in localmirror already.') opts = parser.parse_args(argv) if not _is_in_chroot(): parser.error('Run me from within the chroot.') _ensure_pbzip2_is_installed() target_path = _determine_target_path(opts.sdk_artifact) with tempfile.TemporaryDirectory() as tempdir: download_path = Path(tempdir) / 'sdk_artifact' _download(opts.sdk_artifact, download_path) file_to_upload = _debinpkgify(download_path) if opts.dry_run: logging.info('--dry-run specified; skipping upload of %s to %s', file_to_upload, target_path) else: _upload(file_to_upload, target_path, opts.force) if __name__ == '__main__': main(sys.argv[1:])