xref: /aosp_15_r20/external/toolchain-utils/crate_ebuild_help.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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