import os
import re
import sys
import tomllib
OURS = re.compile(r"(makenot\.work|git\.sr\.ht/~maxmj)", re.I)
SKIP_DIRS = {
"target", ".git", "node_modules", "dist", "vendor",
"_archive", "_scratch", "trash", "_meta", "vtebench",
}
MAX_DEPTH = 4
DEP_SECTIONS = ("dependencies", "dev-dependencies", "build-dependencies")
def manifests(root):
out = []
stack = [(root, 0)]
while stack:
d, depth = stack.pop()
try:
entries = list(os.scandir(d))
except OSError:
continue
for e in entries:
if e.is_file() and e.name == "Cargo.toml":
out.append(e.path)
elif e.is_dir() and e.name not in SKIP_DIRS and depth < MAX_DEPTH:
stack.append((e.path, depth + 1))
return out
def load(path):
try:
with open(path, "rb") as fh:
return tomllib.load(fh)
except (OSError, tomllib.TOMLDecodeError):
return None
def dep_tables(doc):
for section in DEP_SECTIONS:
table = doc.get(section)
if isinstance(table, dict):
yield table
for cfg in (doc.get("target") or {}).values():
if not isinstance(cfg, dict):
continue
for section in DEP_SECTIONS:
table = cfg.get(section)
if isinstance(table, dict):
yield table
ws = doc.get("workspace") or {}
table = ws.get("dependencies")
if isinstance(table, dict):
yield table
def parse_version(v):
core = str(v).split("+")[0].split("-")[0]
parts = []
for piece in core.split(".")[:3]:
try:
parts.append(int(piece))
except ValueError:
parts.append(0)
while len(parts) < 3:
parts.append(0)
return tuple(parts)
def satisfies(req, version):
req = req.strip()
if not req or any(c in req for c in "<>*,~"):
return None
if "-" in str(version).split("+")[0] and "-" not in req:
return False
exact = req.startswith("=")
req = req.lstrip("^=").strip()
if not req:
return None
given = req.split(".")
try:
r = [int(p) for p in given[:3]]
except ValueError:
return None
v = parse_version(version)
if exact:
return tuple(v[: len(r)]) == tuple(r)
if r[0] > 0:
return v[0] == r[0] and v[1:] >= tuple(r[1:] + [0] * (2 - len(r[1:])))
if len(r) == 1:
return v[0] == 0
if r[1] > 0:
return v[0] == 0 and v[1] == r[1] and v[2] >= (r[2] if len(r) > 2 else 0)
if len(r) > 2:
return v[:3] == (0, 0, r[2])
return v[0] == 0 and v[1] == 0
def main():
if len(sys.argv) < 2:
print(__doc__.strip(), file=sys.stderr)
return 2
tree = os.path.realpath(sys.argv[1])
repo = os.path.realpath(sys.argv[2]) if len(sys.argv) > 2 else None
paths = manifests(tree)
docs = {p: load(p) for p in paths}
ws_version = {}
for p, doc in docs.items():
if not doc:
continue
v = ((doc.get("workspace") or {}).get("package") or {}).get("version")
if isinstance(v, str):
ws_version[os.path.dirname(p)] = v
def resolve_version(manifest_path, pkg):
v = pkg.get("version")
if isinstance(v, str):
return v
d = os.path.dirname(manifest_path)
while d.startswith(tree):
if d in ws_version:
return ws_version[d]
d = os.path.dirname(d)
return None
versions = {}
for p, doc in docs.items():
if not doc:
continue
pkg = doc.get("package")
if not isinstance(pkg, dict) or not isinstance(pkg.get("name"), str):
continue
v = resolve_version(p, pkg)
if v:
versions[pkg["name"]] = (v, p)
broken, unchecked, absent, graded = [], 0, set(), 0
for p, doc in docs.items():
if not doc:
continue
for table in dep_tables(doc):
for key, spec in table.items():
if not isinstance(spec, dict):
continue
git = spec.get("git")
req = spec.get("version")
if not isinstance(git, str) or not isinstance(req, str):
continue
if not OURS.search(git):
continue
name = spec.get("package") if isinstance(spec.get("package"), str) else key
known = versions.get(name)
if known is None:
absent.add(name)
continue
verdict = satisfies(req, known[0])
if verdict is None:
unchecked += 1
continue
graded += 1
if not verdict:
broken.append((p, name, req, known[0], known[1]))
if not broken:
print(
f"pre-push: internal deps coherent ({graded} requirements"
+ (f", {unchecked} unchecked" if unchecked else "")
+ (f", {len(absent)} crates not in this tree" if absent else "")
+ ")."
)
return 0
def rel(path):
return os.path.relpath(path, tree)
ours, theirs = [], []
for item in broken:
consumer_manifest, name, req, have, provider_manifest = item
mine = repo is not None and (
consumer_manifest.startswith(repo + os.sep)
or provider_manifest.startswith(repo + os.sep)
)
(ours if mine else theirs).append(item)
for consumer_manifest, name, req, have, provider_manifest in ours + theirs:
print(
f" {rel(consumer_manifest)}: requires {name} \"{req}\", "
f"the tree has {have} ({rel(provider_manifest)})",
file=sys.stderr,
)
if repo is None:
print(f"internal deps: {len(broken)} unresolvable requirements.", file=sys.stderr)
return 1
if not ours:
print(
f"pre-push: {len(theirs)} unresolvable requirements elsewhere in the "
"tree (listed above, not this push's).",
)
return 0
print("", file=sys.stderr)
print(
"pre-push: this push leaves a dependency that cannot resolve.\n"
" A version requirement states which major a consumer was written against,\n"
" so bumping a library and fixing its consumers is one pass (CLAUDE.md,\n"
" \"a breaking bump of an in-house crate is forward-fixed, in the same pass\").\n"
" Fix: bump the requirement in the manifests above, make the consumers\n"
" compile, and push them with this one.",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
sys.exit(main())