blob: db16209d2bccdf81e699f4f5e4eab8fa66bd7a4f [file] [edit]
# 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 cp $FROM $TO` doesn't work. This script allows the
convenience of the old `cp` command.
Run this from outside of the chroot; it will enter the chroot as needed.
"""
import argparse
import logging
from pathlib import Path
import shlex
import subprocess
import tempfile
from typing import Sequence
from cros_utils import cros_paths
from llvm_tools import chroot
_LOCALMIRROR_ROOT = "gs://chromeos-localmirror/distfiles/"
def _chroot_run(command: Sequence[str | Path], chromiumos_root: Path) -> None:
run_command: list[str | Path] = ["cros_sdk", "--"]
run_command += command
subprocess.run(
run_command,
check=True,
cwd=chromiumos_root,
stdin=subprocess.DEVNULL,
)
def _ensure_lbzip2_is_installed(chromiumos_checkout: Path) -> None:
logging.info("Ensuring lbzip2 is installed...")
# `--noreplace` could be used, but checking `which lbzip2 ||` is
# significantly faster than invoking emerge, so prefer that.
install_cmd = shlex.join(("sudo", "emerge", "-g", "-j", "app-arch/lbzip2"))
update_script = f"which lbzip2 || {install_cmd}"
_chroot_run(
("bash", "-cux", update_script),
chromiumos_root=chromiumos_checkout,
)
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) -> None:
"""Downloads the given gs:// path to the given local file."""
logging.info("Downloading %s -> %s", remote_path, local_file)
subprocess.run(
["gsutil", "cp", remote_path, str(local_file)],
check=True,
stdin=subprocess.DEVNULL,
)
def _debinpkgify_in_chroot(
*,
chromiumos_checkout: Path,
chroot_binpkg_file: Path,
chroot_workdir_path: 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...", chroot_binpkg_file)
# The SDK builder produces binary packages:
# https://wiki.gentoo.org/wiki/Binary_package_guide
#
# Which means that `chroot_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).
# SUBTLE: Entering the chroot can lead to `cros_sdk` printing out
# housekeeping messages around things like chroot upgrades; the sequence of
# commands here is carefully written to keep all redirection _within the
# bash executed by `cros_sdk`.
tbz2_file = chroot_workdir_path / "recompressed.tbz2"
pipeline = (
f"qtbz2 --split --tarbz2 -O {shlex.quote(str(chroot_binpkg_file))}"
f" | zstd -d - --stdout"
f" | lbzip2 -z -9 --stdout"
f" > {shlex.quote(str(tbz2_file))}"
)
_chroot_run(
(
"bash",
"-o",
"pipefail",
"-ceux",
pipeline,
),
chromiumos_checkout,
)
return tbz2_file
def _upload(local_file: Path, remote_path: str, force: bool) -> None:
"""Uploads the local file to the given gs:// path."""
logging.info("Uploading %s -> %s", local_file, remote_path)
cmd_base = ["gsutil", "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]) -> None:
chromiumos_checkout = cros_paths.script_chromiumos_checkout_or_exit()
chroot.VerifyOutsideChroot()
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)
_ensure_lbzip2_is_installed(chromiumos_checkout=chromiumos_checkout)
host_tmpdir_base = (
chromiumos_checkout
/ cros_paths.DEFAULT_CHROOT_OUT_DIR
/ cros_paths.DEFAULT_CHROOT_TMPDIR_IN_OUT
)
chroot_tmpdir_base = Path("/tmp")
target_path = determine_target_path(opts.sdk_artifact)
with tempfile.TemporaryDirectory(dir=host_tmpdir_base) as raw_tempdir:
host_tmpdir = Path(raw_tempdir)
chroot_tmpdir = chroot_tmpdir_base / host_tmpdir.name
def host_tmp_path_to_chroot(host_path: Path) -> Path:
return chroot_tmpdir / host_path.relative_to(host_tmpdir)
def chroot_tmp_path_to_host(chroot_path: Path) -> Path:
return host_tmpdir / chroot_path.relative_to(chroot_tmpdir)
download_path = host_tmpdir / "sdk_artifact"
workdir_path = host_tmpdir / "workdir"
workdir_path.mkdir(parents=True)
_download(opts.sdk_artifact, download_path)
chroot_download_path = host_tmp_path_to_chroot(download_path)
chroot_workdir_path = host_tmp_path_to_chroot(workdir_path)
chroot_file_to_upload = _debinpkgify_in_chroot(
chromiumos_checkout=chromiumos_checkout,
chroot_binpkg_file=chroot_download_path,
chroot_workdir_path=chroot_workdir_path,
)
file_to_upload = chroot_tmp_path_to_host(chroot_file_to_upload)
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)