Files
rerun-io--rerun/scripts/check_forbidden_dependencies.py
2026-07-13 13:05:14 +08:00

154 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Check that certain workspace crates don't depend on forbidden external crates.
Uses `cargo tree` to resolve the full dependency tree for each entry,
then verifies that none of the forbidden crates appear.
Each entry in FORBIDDEN_DEPENDENCIES is a tuple of:
- A crate spec: the workspace crate name, optionally followed by
`--all-features` or `--no-default-features`
- A set of crate names that must NOT appear in the dependency tree
"""
from __future__ import annotations
import subprocess
import sys
# (crate_spec, forbidden_deps)
#
# crate_spec is the workspace crate name, optionally followed by
# `--all-features` or `--no-default-features`.
FORBIDDEN_DEPENDENCIES: list[tuple[str, set[str]]] = [
("re_sdk", {"datafusion", "egui", "lance"}),
("re_sdk --all-features", {"datafusion", "egui", "lance"}),
("rerun-cli --no-default-features", {"datafusion", "egui", "lance"}),
]
def get_dependencies(crate_spec: str) -> set[str]:
"""Get all dependencies of a crate using `cargo tree`."""
parts = crate_spec.split()
crate_name = parts[0]
extra_args = parts[1:]
cmd = [
"cargo",
"tree",
"--package",
crate_name,
"--prefix",
"none",
] + extra_args
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"ERROR: `{' '.join(cmd)}` failed:\n{result.stderr}", file=sys.stderr)
sys.exit(1)
deps: set[str] = set()
for line in result.stdout.splitlines():
line = line.strip()
if not line:
continue
# Each line looks like: `crate_name vX.Y.Z [optional extra]`
dep_name = line.split()[0]
deps.add(dep_name)
return deps
def get_dependency_path(crate_spec: str, forbidden_dep: str) -> str:
"""Find a shortest dependency path from a crate to a forbidden dependency.
Runs `cargo tree` with the default tree-prefix output and finds the first
line matching the forbidden dep. Because `cargo tree` prints depth-first,
the first match is one of the shortest paths. We then walk backwards
through the output to reconstruct the chain from root to the forbidden dep.
"""
parts = crate_spec.split()
crate_name = parts[0]
extra_args = parts[1:]
cmd = [
"cargo",
"tree",
"--package",
crate_name,
"--edges",
"normal",
] + extra_args
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
return f" (failed to get dependency path: {result.stderr.strip()})"
lines = result.stdout.splitlines()
# Find the first line that references the forbidden dep.
target_idx = None
for i, line in enumerate(lines):
# Extract the crate name from the tree line (strip tree-drawing characters)
stripped = line.lstrip("│├└─ ─\t")
if stripped.split()[0] == forbidden_dep:
target_idx = i
break
if target_idx is None:
return f" (could not find {forbidden_dep} in cargo tree output)"
def indent_level(line: str) -> int:
"""Count the indentation level from the tree-drawing prefix."""
# Each level of depth adds 4 characters of prefix (e.g. "│ " or "├── ")
content_start = 0
for j, ch in enumerate(line):
if ch not in "│├└─ ─\t":
content_start = j
break
return content_start
# Walk backwards to reconstruct the path from root to forbidden dep
path_lines: list[str] = [lines[target_idx]]
current_indent = indent_level(lines[target_idx])
for i in range(target_idx - 1, -1, -1):
line_indent = indent_level(lines[i])
if line_indent < current_indent:
path_lines.append(lines[i])
current_indent = line_indent
if line_indent == 0:
break
path_lines.reverse()
return "\n".join(path_lines)
def main() -> int:
errors: list[str] = []
for crate_spec, forbidden in FORBIDDEN_DEPENDENCIES:
print(f"Checking {crate_spec}…")
deps = get_dependencies(crate_spec)
for forbidden_dep in sorted(forbidden):
if forbidden_dep in deps:
path = get_dependency_path(crate_spec, forbidden_dep)
errors.append(f"{crate_spec} depends on `{forbidden_dep}`, which is forbidden:\n{path}")
if errors:
print("\nForbidden dependencies found:\n")
for error in errors:
print(error)
print()
return 1
print("\n✓ No forbidden dependencies found")
return 0
if __name__ == "__main__":
sys.exit(main())