|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Install simple cargo tools listed in cargo-tools.txt. |
| 3 | +
|
| 4 | +Reads crate names and versions from cargo-tools.txt, installs each via |
| 5 | +``cargo install --locked``, moves the resulting binaries to /usr/local/bin, |
| 6 | +and cleans up cargo registry/build artifacts. |
| 7 | +
|
| 8 | +This script is shared between c10s, debian, and ubuntu container builds. |
| 9 | +Prerequisites: rustup and a C linker (gcc) must already be installed. |
| 10 | +
|
| 11 | +For tools with special requirements (e.g. kani-verifier which needs |
| 12 | +a setup step and its own KANI_HOME), use a dedicated install script instead. |
| 13 | +""" |
| 14 | + |
| 15 | +import os |
| 16 | +import re |
| 17 | +import subprocess |
| 18 | +import sys |
| 19 | +from pathlib import Path |
| 20 | + |
| 21 | +CARGO_HOME = Path("/usr/local/cargo") |
| 22 | +INSTALL_DIR = Path("/usr/local/bin") |
| 23 | + |
| 24 | +# Version strings must be alphanumeric with dots, hyphens, and an optional |
| 25 | +# leading 'v'. This rejects path traversal sequences and other surprises. |
| 26 | +_VERSION_RE = re.compile(r"^v?[A-Za-z0-9]+(?:[.\-][A-Za-z0-9]+)*$") |
| 27 | + |
| 28 | + |
| 29 | +def parse_cargo_tools(path: Path) -> list[tuple[str, str]]: |
| 30 | + """Parse cargo-tools.txt, returning [(crate, version)] in order.""" |
| 31 | + tools = [] |
| 32 | + for lineno, line in enumerate(path.read_text().splitlines(), 1): |
| 33 | + line = line.strip() |
| 34 | + if not line or line.startswith("#"): |
| 35 | + continue |
| 36 | + if "@" not in line: |
| 37 | + print(f"warning: skipping malformed line: {line}", file=sys.stderr) |
| 38 | + continue |
| 39 | + crate, version = line.split("@", 1) |
| 40 | + if not _VERSION_RE.match(version): |
| 41 | + print( |
| 42 | + f"error: {path}:{lineno}: invalid version string: {version!r}", |
| 43 | + file=sys.stderr, |
| 44 | + ) |
| 45 | + sys.exit(1) |
| 46 | + tools.append((crate, version)) |
| 47 | + return tools |
| 48 | + |
| 49 | + |
| 50 | +def install_crate(crate: str, version: str) -> None: |
| 51 | + """Install a single crate via cargo install.""" |
| 52 | + print(f"installing {crate}@{version}") |
| 53 | + subprocess.run( |
| 54 | + [ |
| 55 | + "/bin/time", "-f", "%E %C", |
| 56 | + "cargo", "install", "--locked", crate, "--version", version, |
| 57 | + ], |
| 58 | + check=True, |
| 59 | + ) |
| 60 | + |
| 61 | + |
| 62 | +def collect_binaries() -> None: |
| 63 | + """Move cargo-installed binaries to INSTALL_DIR. |
| 64 | +
|
| 65 | + Skips rustup-managed symlinks (cargo, rustc, rustup, etc.) which |
| 66 | + are symlinks in CARGO_HOME/bin. |
| 67 | + """ |
| 68 | + cargo_bin = CARGO_HOME / "bin" |
| 69 | + for entry in sorted(cargo_bin.iterdir()): |
| 70 | + if entry.is_symlink(): |
| 71 | + continue |
| 72 | + if not entry.is_file(): |
| 73 | + continue |
| 74 | + dst = INSTALL_DIR / entry.name |
| 75 | + entry.rename(dst) |
| 76 | + print(f"installed {dst}") |
| 77 | + |
| 78 | + |
| 79 | +def cleanup() -> None: |
| 80 | + """Remove cargo registry and build artifacts.""" |
| 81 | + import shutil |
| 82 | + |
| 83 | + for subdir in ("registry", "git"): |
| 84 | + p = CARGO_HOME / subdir |
| 85 | + if p.exists(): |
| 86 | + shutil.rmtree(p) |
| 87 | + print(f"cleaned {p}") |
| 88 | + |
| 89 | + |
| 90 | +def main() -> None: |
| 91 | + os.environ["RUSTUP_HOME"] = "/usr/local/rustup" |
| 92 | + os.environ["CARGO_HOME"] = str(CARGO_HOME) |
| 93 | + # Ensure cargo and rustc are on PATH |
| 94 | + path = os.environ.get("PATH", "") |
| 95 | + os.environ["PATH"] = f"/usr/local/bin:{path}" |
| 96 | + |
| 97 | + script_dir = Path(__file__).parent |
| 98 | + tools_file = script_dir / "cargo-tools.txt" |
| 99 | + tools = parse_cargo_tools(tools_file) |
| 100 | + |
| 101 | + if not tools: |
| 102 | + print("error: no tools found in cargo-tools.txt", file=sys.stderr) |
| 103 | + sys.exit(1) |
| 104 | + |
| 105 | + for crate, version in tools: |
| 106 | + install_crate(crate, version) |
| 107 | + |
| 108 | + collect_binaries() |
| 109 | + cleanup() |
| 110 | + |
| 111 | + |
| 112 | +if __name__ == "__main__": |
| 113 | + main() |
0 commit comments