blob: c66b9897196b30fa45ded65fe44ee7f630bd0185 [file] [log] [blame]
#!/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()