diff options
Diffstat (limited to 'crate_ebuild_help.py')
-rwxr-xr-x | crate_ebuild_help.py | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/crate_ebuild_help.py b/crate_ebuild_help.py new file mode 100755 index 00000000..c66b9897 --- /dev/null +++ b/crate_ebuild_help.py @@ -0,0 +1,177 @@ +#!/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. + +"""Help creating a Rust ebuild with CRATES. + +This script is meant to help someone creating a Rust ebuild of the type +currently used by sys-apps/ripgrep and sys-apps/rust-analyzer. + +In these ebuilds, the CRATES variable is used to list all dependencies, rather +than creating an ebuild for each dependency. This style of ebuild can be used +for a crate which is only intended for use in the chromiumos SDK, and which has +many dependencies which otherwise won't be used. + +To create such an ebuild, there are essentially two tasks that must be done: + +1. Determine all transitive dependent crates and version and list them in the +CRATES variable. Ignore crates that are already included in the main crate's +repository. + +2. Find which dependent crates are not already on a chromeos mirror, retrieve +them from crates.io, and upload them to `gs://chromeos-localmirror/distfiles`. + +This script parses the crate's lockfile to list transitive dependent crates, +and either lists crates to be uploaded or actually uploads them. + +Of course these can be done manually instead. If you choose to do these steps +manually, I recommend *not* using the `cargo download` tool, and instead obtain +dependent crates at +`https://crates.io/api/v1/crates/{crate_name}/{crate_version}/download`. + +Example usage: + + # Here we instruct the script to ignore crateA and crateB, presumably + # because they are already included in the same repository as some-crate. + # This will not actually upload any crates to `gs`. + python3 crate_ebuild_help.py --lockfile some-crate/Cargo.lock \ + --ignore crateA --ignore crateB --dry-run + + # Similar to the above, but here we'll actually carry out the uploads. + python3 crate_ebuild_help.py --lockfile some-crate/Cargo.lock \ + --ignore crateA --ignore crateB + +See the ebuild files for ripgrep or rust-analyzer for other details. +""" + +import argparse +import concurrent.futures +from pathlib import Path +import subprocess +import tempfile +from typing import List, Tuple +import urllib.request + +# Python 3.11 has `tomllib`, so maybe eventually we can switch to that. +import toml + + +def run(args: List[str]) -> bool: + result = subprocess.run( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False + ) + return result.returncode == 0 + + +def run_check(args: List[str]): + subprocess.run( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True + ) + + +def gs_address_exists(address: str) -> bool: + # returns False if the file isn't there + return run(["gsutil.py", "ls", address]) + + +def crate_already_uploaded(crate_name: str, crate_version: str) -> bool: + filename = f"{crate_name}-{crate_version}.crate" + return gs_address_exists( + f"gs://chromeos-localmirror/distfiles/{filename}" + ) or gs_address_exists(f"gs://chromeos-mirror/gentoo/distfiles/{filename}") + + +def download_crate(crate_name: str, crate_version: str, localpath: Path): + urllib.request.urlretrieve( + f"https://crates.io/api/v1/crates/{crate_name}/{crate_version}/download", + localpath, + ) + + +def upload_crate(crate_name: str, crate_version: str, localpath: Path): + run_check( + [ + "gsutil.py", + "cp", + "-n", + "-a", + "public-read", + str(localpath), + f"gs://chromeos-localmirror/distfiles/{crate_name}-{crate_version}.crate", + ] + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Help prepare a Rust crate for an ebuild." + ) + parser.add_argument( + "--lockfile", + type=str, + required=True, + help="Path to the lockfile of the crate in question.", + ) + parser.add_argument( + "--ignore", + type=str, + action="append", + required=False, + default=[], + help="Ignore the crate by this name (may be used multiple times).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Don't actually download/upload crates, just print their names.", + ) + ns = parser.parse_args() + + to_ignore = set(ns.ignore) + + toml_contents = toml.load(ns.lockfile) + packages = toml_contents["package"] + + crates = [ + (pkg["name"], pkg["version"]) + for pkg in packages + if pkg["name"] not in to_ignore + ] + crates.sort() + + print("Dependent crates:") + for name, version in crates: + print(f"{name}-{version}") + print() + + if ns.dry_run: + print("Crates that would be uploaded (skipping ones already uploaded):") + else: + print("Uploading crates (skipping ones already uploaded):") + + def maybe_upload(crate: Tuple[str, str]) -> str: + name, version = crate + if crate_already_uploaded(name, version): + return "" + if not ns.dry_run: + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir.name, f"{name}-{version}.crate") + download_crate(name, version, path) + upload_crate(name, version, path) + return f"{name}-{version}" + + # Simple benchmarking on my machine with rust-analyzer's Cargo.lock, using + # the --dry-run option, gives a wall time of 277 seconds with max_workers=1 + # and 70 seconds with max_workers=4. + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: + crates_len = len(crates) + for i, s in enumerate(executor.map(maybe_upload, crates)): + if s: + j = i + 1 + print(f"[{j}/{crates_len}] {s}") + print() + + +if __name__ == "__main__": + main() |