Skip to content

Commit 4e87713

Browse files
authored
devenv: Add data-driven cargo tool installation (#166)
I want to use our devcontainer in GHA to e.g. automate releases, and we need `cargo-vendor-filterer` for that. Add cargo-tools.txt and install-cargo-tools.sh, mirroring the existing tool-versions.txt / fetch-tools.py pattern for pre-built binaries. Adding a new cargo-installable tool is now a one-line addition to cargo-tools.txt with a Renovate annotation — no per-tool install scripts or Containerfile ARGs needed. Initial tools: cargo-vendor-filterer 0.5.18, cargo-edit 0.13.9. Kani stays as a dedicated install script since it has special requirements (gcc, cargo-kani setup, KANI_HOME directory). Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent c297f04 commit 4e87713

6 files changed

Lines changed: 132 additions & 0 deletions

File tree

devenv/.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@
1919
!install-rust.sh
2020
!install-uv.sh
2121
!install-kani.sh
22+
!install-cargo-tools.py
23+
!cargo-tools.txt
2224
!devenv-selftest.sh
2325
!userns-setup

devenv/Containerfile.c10s

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ COPY --from=kani /usr/local/rustup /usr/local/rustup
7979
COPY --from=kani /usr/local/kani /usr/local/kani
8080
# Point rustup at the system-wide installation, but let CARGO_HOME default to ~/.cargo
8181
ENV RUSTUP_HOME=/usr/local/rustup
82+
# Simple cargo-installable tools (cargo-vendor-filterer, cargo-edit, etc.)
83+
COPY cargo-tools.txt install-cargo-tools.py /run/src/
84+
RUN /run/src/install-cargo-tools.py
8285
# Point Kani at the system-wide installation
8386
ENV KANI_HOME=/usr/local/kani
8487
# Configure uv for system-wide tool installation

devenv/Containerfile.debian

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ COPY --from=kani /usr/local/rustup /usr/local/rustup
7878
COPY --from=kani /usr/local/kani /usr/local/kani
7979
# Point rustup at the system-wide installation, but let CARGO_HOME default to ~/.cargo
8080
ENV RUSTUP_HOME=/usr/local/rustup
81+
# Simple cargo-installable tools (cargo-vendor-filterer, cargo-edit, etc.)
82+
COPY cargo-tools.txt install-cargo-tools.py /run/src/
83+
RUN /run/src/install-cargo-tools.py
8184
# Point Kani at the system-wide installation
8285
ENV KANI_HOME=/usr/local/kani
8386
# Setup for codespaces

devenv/Containerfile.ubuntu

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ COPY --from=kani /usr/local/rustup /usr/local/rustup
9393
COPY --from=kani /usr/local/kani /usr/local/kani
9494
# Point rustup at the system-wide installation, but let CARGO_HOME default to ~/.cargo
9595
ENV RUSTUP_HOME=/usr/local/rustup
96+
# Simple cargo-installable tools (cargo-vendor-filterer, cargo-edit, etc.)
97+
COPY cargo-tools.txt install-cargo-tools.py /run/src/
98+
RUN /run/src/install-cargo-tools.py
9699
# Point Kani at the system-wide installation
97100
ENV KANI_HOME=/usr/local/kani
98101
# Setup for codespaces

devenv/cargo-tools.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Cargo tools installed via install-cargo-tools.sh
2+
# Format: crate@version (one per line)
3+
# Renovate annotations allow automated version updates.
4+
5+
# renovate: datasource=crate depName=cargo-vendor-filterer
6+
cargo-vendor-filterer@0.5.18
7+
# renovate: datasource=crate depName=cargo-edit
8+
cargo-edit@0.13.9

devenv/install-cargo-tools.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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

Comments
 (0)