blob: 5da8007f23beb999bc58ea031759d1c2c8174e86 [file] [log] [blame]
#!/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:])