xref: /aosp_15_r20/external/toolchain-utils/rust_tools/copy_rust_bootstrap.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2022 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Copies rust-bootstrap artifacts from an SDK build to localmirror.
7
8We use localmirror to host these artifacts, but they've changed a bit over
9time, so simply `gsutil cp $FROM $TO` doesn't work. This script allows the
10convenience of the old `cp` command.
11"""
12
13import argparse
14import logging
15import os
16from pathlib import Path
17import shutil
18import subprocess
19import sys
20import tempfile
21from typing import List
22
23
24_LOCALMIRROR_ROOT = "gs://chromeos-localmirror/distfiles/"
25
26
27def _is_in_chroot() -> bool:
28    return Path("/etc/cros_chroot_version").exists()
29
30
31def _ensure_lbzip2_is_installed():
32    if shutil.which("lbzip2"):
33        return
34
35    logging.info("Auto-installing lbzip2...")
36    subprocess.run(["sudo", "emerge", "-g", "lbzip2"], check=True)
37
38
39def determine_target_path(sdk_path: str) -> str:
40    """Determine where `sdk_path` should sit in localmirror."""
41    gs_prefix = "gs://"
42    if not sdk_path.startswith(gs_prefix):
43        raise ValueError(f"Invalid GS path: {sdk_path!r}")
44
45    file_name = Path(sdk_path[len(gs_prefix) :]).name
46    return _LOCALMIRROR_ROOT + file_name
47
48
49def _download(remote_path: str, local_file: Path):
50    """Downloads the given gs:// path to the given local file."""
51    logging.info("Downloading %s -> %s", remote_path, local_file)
52    subprocess.run(
53        ["gsutil", "cp", remote_path, str(local_file)],
54        check=True,
55        stdin=subprocess.DEVNULL,
56    )
57
58
59def _debinpkgify(binpkg_file: Path) -> Path:
60    """Converts a binpkg into the files it installs.
61
62    Note that this function makes temporary files in the same directory as
63    `binpkg_file`. It makes no attempt to clean them up.
64    """
65    logging.info("Converting %s from a binpkg...", binpkg_file)
66
67    # The SDK builder produces binary packages:
68    # https://wiki.gentoo.org/wiki/Binary_package_guide
69    #
70    # Which means that `binpkg_file` is in the XPAK format. We want to split
71    # that out, and recompress it from zstd (which is the compression format
72    # that CrOS uses) to bzip2 (which is what we've historically used, and
73    # which is what our ebuild expects).
74    tmpdir = binpkg_file.parent
75
76    def _mkstemp(suffix=None) -> Path:
77        fd, file_path = tempfile.mkstemp(dir=tmpdir, suffix=suffix)
78        os.close(fd)
79        return Path(file_path)
80
81    # First, split the actual artifacts that land in the chroot out to
82    # `temp_file`.
83    artifacts_file = _mkstemp()
84    logging.info(
85        "Extracting artifacts from %s into %s...", binpkg_file, artifacts_file
86    )
87    with artifacts_file.open("wb") as f:
88        subprocess.run(
89            [
90                "qtbz2",
91                "-s",
92                "-t",
93                "-O",
94                str(binpkg_file),
95            ],
96            check=True,
97            stdout=f,
98        )
99
100    decompressed_artifacts_file = _mkstemp()
101    decompressed_artifacts_file.unlink()
102    logging.info(
103        "Decompressing artifacts from %s to %s...",
104        artifacts_file,
105        decompressed_artifacts_file,
106    )
107    subprocess.run(
108        [
109            "zstd",
110            "-d",
111            str(artifacts_file),
112            "-o",
113            str(decompressed_artifacts_file),
114        ],
115        check=True,
116    )
117
118    # Finally, recompress it as a tbz2.
119    tbz2_file = _mkstemp(".tbz2")
120    logging.info(
121        "Recompressing artifacts from %s to %s (this may take a while)...",
122        decompressed_artifacts_file,
123        tbz2_file,
124    )
125    with tbz2_file.open("wb") as f:
126        subprocess.run(
127            [
128                "lbzip2",
129                "-9",
130                "-c",
131                str(decompressed_artifacts_file),
132            ],
133            check=True,
134            stdout=f,
135        )
136    return tbz2_file
137
138
139def _upload(local_file: Path, remote_path: str, force: bool):
140    """Uploads the local file to the given gs:// path."""
141    logging.info("Uploading %s -> %s", local_file, remote_path)
142    cmd_base = ["gsutil", "cp", "-a", "public-read"]
143    if not force:
144        cmd_base.append("-n")
145    subprocess.run(
146        cmd_base + [str(local_file), remote_path],
147        check=True,
148        stdin=subprocess.DEVNULL,
149    )
150
151
152def main(argv: List[str]):
153    logging.basicConfig(
154        format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: "
155        "%(message)s",
156        level=logging.INFO,
157    )
158
159    parser = argparse.ArgumentParser(
160        description=__doc__,
161        formatter_class=argparse.RawDescriptionHelpFormatter,
162    )
163
164    parser.add_argument(
165        "sdk_artifact",
166        help="Path to the SDK rust-bootstrap artifact to copy. e.g., "
167        "gs://chromeos-prebuilt/host/amd64/amd64-host/"
168        "chroot-2022.07.12.134334/packages/dev-lang/"
169        "rust-bootstrap-1.59.0.tbz2.",
170    )
171    parser.add_argument(
172        "-n",
173        "--dry-run",
174        action="store_true",
175        help="Do everything except actually uploading the artifact.",
176    )
177    parser.add_argument(
178        "--force",
179        action="store_true",
180        help="Upload the artifact even if one exists in localmirror already.",
181    )
182    opts = parser.parse_args(argv)
183
184    if not _is_in_chroot():
185        parser.error("Run me from within the chroot.")
186    _ensure_lbzip2_is_installed()
187
188    target_path = determine_target_path(opts.sdk_artifact)
189    with tempfile.TemporaryDirectory() as tempdir:
190        download_path = Path(tempdir) / "sdk_artifact"
191        _download(opts.sdk_artifact, download_path)
192        file_to_upload = _debinpkgify(download_path)
193        if opts.dry_run:
194            logging.info(
195                "--dry-run specified; skipping upload of %s to %s",
196                file_to_upload,
197                target_path,
198            )
199        else:
200            _upload(file_to_upload, target_path, opts.force)
201
202
203if __name__ == "__main__":
204    main(sys.argv[1:])
205