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