1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# Copyright 2022 The ChromiumOS Authors 3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 4*760c253cSXin Li# found in the LICENSE file. 5*760c253cSXin Li 6*760c253cSXin Li"""Help creating a Rust ebuild with CRATES. 7*760c253cSXin Li 8*760c253cSXin LiThis script is meant to help someone creating a Rust ebuild of the type 9*760c253cSXin Licurrently used by sys-apps/ripgrep and sys-apps/rust-analyzer. 10*760c253cSXin Li 11*760c253cSXin LiIn these ebuilds, the CRATES variable is used to list all dependencies, rather 12*760c253cSXin Lithan creating an ebuild for each dependency. This style of ebuild can be used 13*760c253cSXin Lifor a crate which is only intended for use in the chromiumos SDK, and which has 14*760c253cSXin Limany dependencies which otherwise won't be used. 15*760c253cSXin Li 16*760c253cSXin LiTo create such an ebuild, there are essentially two tasks that must be done: 17*760c253cSXin Li 18*760c253cSXin Li1. Determine all transitive dependent crates and version and list them in the 19*760c253cSXin LiCRATES variable. Ignore crates that are already included in the main crate's 20*760c253cSXin Lirepository. 21*760c253cSXin Li 22*760c253cSXin Li2. Find which dependent crates are not already on a chromeos mirror, retrieve 23*760c253cSXin Lithem from crates.io, and upload them to `gs://chromeos-localmirror/distfiles`. 24*760c253cSXin Li 25*760c253cSXin LiThis script parses the crate's lockfile to list transitive dependent crates, 26*760c253cSXin Liand either lists crates to be uploaded or actually uploads them. 27*760c253cSXin Li 28*760c253cSXin LiOf course these can be done manually instead. If you choose to do these steps 29*760c253cSXin Limanually, I recommend *not* using the `cargo download` tool, and instead obtain 30*760c253cSXin Lidependent crates at 31*760c253cSXin Li`https://crates.io/api/v1/crates/{crate_name}/{crate_version}/download`. 32*760c253cSXin Li 33*760c253cSXin LiExample usage: 34*760c253cSXin Li 35*760c253cSXin Li # Here we instruct the script to ignore crateA and crateB, presumably 36*760c253cSXin Li # because they are already included in the same repository as some-crate. 37*760c253cSXin Li # This will not actually upload any crates to `gs`. 38*760c253cSXin Li python3 crate_ebuild_help.py --lockfile some-crate/Cargo.lock \ 39*760c253cSXin Li --ignore crateA --ignore crateB --dry-run 40*760c253cSXin Li 41*760c253cSXin Li # Similar to the above, but here we'll actually carry out the uploads. 42*760c253cSXin Li python3 crate_ebuild_help.py --lockfile some-crate/Cargo.lock \ 43*760c253cSXin Li --ignore crateA --ignore crateB 44*760c253cSXin Li 45*760c253cSXin LiSee the ebuild files for ripgrep or rust-analyzer for other details. 46*760c253cSXin Li""" 47*760c253cSXin Li 48*760c253cSXin Liimport argparse 49*760c253cSXin Liimport concurrent.futures 50*760c253cSXin Lifrom pathlib import Path 51*760c253cSXin Liimport subprocess 52*760c253cSXin Liimport tempfile 53*760c253cSXin Lifrom typing import List, Tuple 54*760c253cSXin Liimport urllib.request 55*760c253cSXin Li 56*760c253cSXin Li# Python 3.11 has `tomllib`, so maybe eventually we can switch to that. 57*760c253cSXin Liimport toml 58*760c253cSXin Li 59*760c253cSXin Li 60*760c253cSXin Lidef run(args: List[str]) -> bool: 61*760c253cSXin Li result = subprocess.run( 62*760c253cSXin Li args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False 63*760c253cSXin Li ) 64*760c253cSXin Li return result.returncode == 0 65*760c253cSXin Li 66*760c253cSXin Li 67*760c253cSXin Lidef run_check(args: List[str]): 68*760c253cSXin Li subprocess.run( 69*760c253cSXin Li args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True 70*760c253cSXin Li ) 71*760c253cSXin Li 72*760c253cSXin Li 73*760c253cSXin Lidef gs_address_exists(address: str) -> bool: 74*760c253cSXin Li # returns False if the file isn't there 75*760c253cSXin Li return run(["gsutil.py", "ls", address]) 76*760c253cSXin Li 77*760c253cSXin Li 78*760c253cSXin Lidef crate_already_uploaded(crate_name: str, crate_version: str) -> bool: 79*760c253cSXin Li filename = f"{crate_name}-{crate_version}.crate" 80*760c253cSXin Li return gs_address_exists( 81*760c253cSXin Li f"gs://chromeos-localmirror/distfiles/{filename}" 82*760c253cSXin Li ) or gs_address_exists(f"gs://chromeos-mirror/gentoo/distfiles/{filename}") 83*760c253cSXin Li 84*760c253cSXin Li 85*760c253cSXin Lidef download_crate(crate_name: str, crate_version: str, localpath: Path): 86*760c253cSXin Li urllib.request.urlretrieve( 87*760c253cSXin Li f"https://crates.io/api/v1/crates/{crate_name}/{crate_version}/download", 88*760c253cSXin Li localpath, 89*760c253cSXin Li ) 90*760c253cSXin Li 91*760c253cSXin Li 92*760c253cSXin Lidef upload_crate(crate_name: str, crate_version: str, localpath: Path): 93*760c253cSXin Li run_check( 94*760c253cSXin Li [ 95*760c253cSXin Li "gsutil.py", 96*760c253cSXin Li "cp", 97*760c253cSXin Li "-n", 98*760c253cSXin Li "-a", 99*760c253cSXin Li "public-read", 100*760c253cSXin Li str(localpath), 101*760c253cSXin Li f"gs://chromeos-localmirror/distfiles/{crate_name}-{crate_version}.crate", 102*760c253cSXin Li ] 103*760c253cSXin Li ) 104*760c253cSXin Li 105*760c253cSXin Li 106*760c253cSXin Lidef main(): 107*760c253cSXin Li parser = argparse.ArgumentParser( 108*760c253cSXin Li description="Help prepare a Rust crate for an ebuild." 109*760c253cSXin Li ) 110*760c253cSXin Li parser.add_argument( 111*760c253cSXin Li "--lockfile", 112*760c253cSXin Li type=str, 113*760c253cSXin Li required=True, 114*760c253cSXin Li help="Path to the lockfile of the crate in question.", 115*760c253cSXin Li ) 116*760c253cSXin Li parser.add_argument( 117*760c253cSXin Li "--ignore", 118*760c253cSXin Li type=str, 119*760c253cSXin Li action="append", 120*760c253cSXin Li required=False, 121*760c253cSXin Li default=[], 122*760c253cSXin Li help="Ignore the crate by this name (may be used multiple times).", 123*760c253cSXin Li ) 124*760c253cSXin Li parser.add_argument( 125*760c253cSXin Li "--dry-run", 126*760c253cSXin Li action="store_true", 127*760c253cSXin Li help="Don't actually download/upload crates, just print their names.", 128*760c253cSXin Li ) 129*760c253cSXin Li ns = parser.parse_args() 130*760c253cSXin Li 131*760c253cSXin Li to_ignore = set(ns.ignore) 132*760c253cSXin Li 133*760c253cSXin Li toml_contents = toml.load(ns.lockfile) 134*760c253cSXin Li packages = toml_contents["package"] 135*760c253cSXin Li 136*760c253cSXin Li crates = [ 137*760c253cSXin Li (pkg["name"], pkg["version"]) 138*760c253cSXin Li for pkg in packages 139*760c253cSXin Li if pkg["name"] not in to_ignore 140*760c253cSXin Li ] 141*760c253cSXin Li crates.sort() 142*760c253cSXin Li 143*760c253cSXin Li print("Dependent crates:") 144*760c253cSXin Li for name, version in crates: 145*760c253cSXin Li print(f"{name}-{version}") 146*760c253cSXin Li print() 147*760c253cSXin Li 148*760c253cSXin Li if ns.dry_run: 149*760c253cSXin Li print("Crates that would be uploaded (skipping ones already uploaded):") 150*760c253cSXin Li else: 151*760c253cSXin Li print("Uploading crates (skipping ones already uploaded):") 152*760c253cSXin Li 153*760c253cSXin Li def maybe_upload(crate: Tuple[str, str]) -> str: 154*760c253cSXin Li name, version = crate 155*760c253cSXin Li if crate_already_uploaded(name, version): 156*760c253cSXin Li return "" 157*760c253cSXin Li if not ns.dry_run: 158*760c253cSXin Li with tempfile.TemporaryDirectory() as temp_dir: 159*760c253cSXin Li path = Path(temp_dir.name, f"{name}-{version}.crate") 160*760c253cSXin Li download_crate(name, version, path) 161*760c253cSXin Li upload_crate(name, version, path) 162*760c253cSXin Li return f"{name}-{version}" 163*760c253cSXin Li 164*760c253cSXin Li # Simple benchmarking on my machine with rust-analyzer's Cargo.lock, using 165*760c253cSXin Li # the --dry-run option, gives a wall time of 277 seconds with max_workers=1 166*760c253cSXin Li # and 70 seconds with max_workers=4. 167*760c253cSXin Li with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: 168*760c253cSXin Li crates_len = len(crates) 169*760c253cSXin Li for i, s in enumerate(executor.map(maybe_upload, crates)): 170*760c253cSXin Li if s: 171*760c253cSXin Li j = i + 1 172*760c253cSXin Li print(f"[{j}/{crates_len}] {s}") 173*760c253cSXin Li print() 174*760c253cSXin Li 175*760c253cSXin Li 176*760c253cSXin Liif __name__ == "__main__": 177*760c253cSXin Li main() 178