Something went wrong. Try again.
Rewild Your Web
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298#!/usr/bin/env python3"""Report where Beaver's workspace dependencies drifted from upstream Servo's.
Beaver's root Cargo.toml is the real workspace root for the vendored Servocrates under source/components, so every `foo = { workspace = true }` in a Servocomponent resolves against our root, not source/Cargo.toml. Any version orfeature that upstream bumped has to be mirrored here by hand or the build breaks.
scripts/cargo_sync_check.py [--check] [--all] [repo-root]
--check exit 1 when there is anything actionable (for use in a build script) --all also list Beaver-only dependencies and identical entries"""
import sysfrom pathlib import Path
try: import tomllibexcept ImportError: # pragma: no cover sys.exit("cargo_sync_check.py needs Python 3.11+ (for tomllib)")
# Where a dependency is consumed inside source/. Only `components` matters for# Beaver: the ports/ffi/tests trees are servoshell's and we do not build them.AREA_ORDER = ["components", "ports", "ffi", "tests", "support", "?"]
def color(enabled): if not enabled: return lambda _code, text: text return lambda code, text: f"\033[{code}m{text}\033[0m"
def load_workspace_deps(path): data = tomllib.loads(path.read_text()) try: return data["workspace"]["dependencies"] except KeyError: sys.exit(f"{path} has no [workspace.dependencies] table")
def normalize(spec): """A dependency spec as a dict, with the noise that differs between the two roots (path prefixes, `=` version pinning) removed.""" d = {"version": spec} if isinstance(spec, str) else dict(spec) d.pop("path", None) if "version" in d: d["version"] = d["version"].lstrip("=") if "features" in d: d["features"] = sorted(d["features"]) return d
def version_key(version): """A sortable semver-ish key. Returns None for anything we can't order like multi-part ranges and wildcards.""" v = version.strip().lstrip("=^~ ") if not v or "," in v or "*" in v or v[0] in "<>": return None core, _, pre = v.partition("-") nums = [] for part in core.split("."): digits = "".join(ch for ch in part if ch.isdigit()) if not digits: return None nums.append(int(digits)) nums = (nums + [0, 0, 0])[:3] if not pre: # No prerelease sorts above any prerelease of the same core version. return (tuple(nums), (1,)) ids = tuple( (0, int(p), "") if p.isdigit() else (1, 0, p) for p in pre.replace(".", " ").split() ) return (tuple(nums), (0, ids))
def describe(spec): """One-line rendering of a spec, for the report.""" d = normalize(spec) bits = [] if "version" in d: bits.append(d["version"]) if "git" in d: if "rev" in d: bits.append(f"git {d['rev'][:12]}") elif "branch" in d: bits.append(f"git @{d['branch']}") else: bits.append("git") if d.get("default-features") is False: bits.append("no-default") if d.get("features"): bits.append("+" + ",".join(d["features"])) return " ".join(bits) or "-"
def scan_workspace_true_usage(source_dir): """Map dependency name -> set of source/ directories that inherit it from the workspace. Tells us whether a missing entry can actually break the build.""" usage = {} sections = ("dependencies", "dev-dependencies", "build-dependencies") for manifest in source_dir.rglob("Cargo.toml"): if manifest == source_dir / "Cargo.toml": continue try: data = tomllib.loads(manifest.read_text()) except (tomllib.TOMLDecodeError, OSError): continue where = manifest.parent.relative_to(source_dir).as_posix() tables = [data] tables.extend((data.get("target") or {}).values()) for table in tables: for section in sections: for name, spec in (table.get(section) or {}).items(): if isinstance(spec, dict) and spec.get("workspace"): usage.setdefault(name, set()).add(where) return usage
def areas_of(dirs): areas = set() for d in dirs: top = d.split("/")[0] areas.add(top if top in AREA_ORDER else "?") return areas
def areas_label(dirs): if not dirs: return "unused in source/" return ", ".join(a for a in AREA_ORDER if a in areas_of(dirs))
def main(argv): check = "--check" in argv show_all = "--all" in argv positional = [a for a in argv if not a.startswith("-")] root = Path(positional[0]) if positional else Path(__file__).resolve().parent.parent beaver_manifest = root / "Cargo.toml" servo_manifest = root / "source" / "Cargo.toml" for p in (beaver_manifest, servo_manifest): if not p.is_file(): sys.exit(f"not found: {p}")
c = color(sys.stdout.isatty()) beaver = load_workspace_deps(beaver_manifest) servo = load_workspace_deps(servo_manifest) usage = scan_workspace_true_usage(root / "source")
missing, behind, ahead, git_rev, feature_only, unorderable, same = ( [], [], [], [], [], [], [], )
for name in sorted(servo): if name not in beaver: dirs = usage.get(name, set()) missing.append((name, describe(servo[name]), dirs)) continue
b, s = normalize(beaver[name]), normalize(servo[name]) if b == s: same.append(name) continue
# Local Servo components: same crate either way, only the path prefix # and `=` pinning differ, and normalize() already dropped those. is_local = "path" in ( beaver[name] if isinstance(beaver[name], dict) else {} ) or "path" in (servo[name] if isinstance(servo[name], dict) else {})
if "git" in b or "git" in s: # Handled below, URL driven, so we also catch leftover entries that # upstream dropped but that still pin the old rev. continue
bv, sv = b.get("version"), s.get("version") if bv is not None and sv is not None and bv != sv: bk, sk = version_key(bv), version_key(sv) if bk is None or sk is None: unorderable.append((name, bv, sv)) elif bk < sk: behind.append((name, bv, sv)) elif bk > sk: ahead.append((name, bv, sv)) continue
if is_local: continue
# Same version, different features / default-features. feature_only.append((name, describe(beaver[name]), describe(servo[name])))
# Git dependencies, compared per repository URL rather than per name: a # crate we still pin that upstream dropped (e.g. an old stylo sub-crate) # would otherwise pull in a second checkout at a different rev. upstream_revs = {} for name, spec in servo.items(): d = normalize(spec) if "git" in d and "rev" in d: upstream_revs.setdefault(d["git"], set()).add(d["rev"]) for name, spec in sorted(beaver.items()): d = normalize(spec) if "git" not in d or "rev" not in d: continue revs = upstream_revs.get(d["git"]) if revs and d["rev"] not in revs: note = "" if name in servo else " (upstream no longer has this crate)" git_rev.append((name, d["rev"], "/".join(sorted(revs)), note))
# Beaver entries pointing at a source/ directory that no longer exists, # e.g. a component upstream renamed or removed. stale_paths = [] for name, spec in sorted(beaver.items()): if isinstance(spec, dict) and str(spec.get("path", "")).startswith("source/"): if not (root / spec["path"]).is_dir(): stale_paths.append((name, spec["path"]))
beaver_only = sorted(set(beaver) - set(servo))
def header(title, items, note=""): tag = c("1", title) count = c("2", f"({len(items)})") print(f"\n{tag} {count}{' ' + c('2', note) if note else ''}") if not items: print(" none")
width = 30 print(c("1", f"beaver {beaver_manifest} vs servo {servo_manifest}"))
header("Missing from Beaver's root", missing) breaking_missing = 0 for name, spec, dirs in sorted( missing, key=lambda m: ("components" not in areas_of(m[2]), m[0]) ): breaks = "components" in areas_of(dirs) breaking_missing += breaks mark = c("31", "!") if breaks else " " print(f" {mark} {name:<{width}} {spec:<28} used by: {areas_label(dirs)}") if breaks: # The coarse area is not enough to judge: components/media/examples # is an upstream-only member, components/media/audio is not. print(f" {'':<{width}} {c('2', ' ' + ', '.join(sorted(dirs)))}")
header("Beaver behind upstream", behind) for name, bv, sv in behind: print(f" {name:<{width}} {c('33', bv):<20} -> {sv}")
header("Git revision mismatch", git_rev) for name, bv, sv, note in git_rev: print(f" {name:<{width}} {c('33', bv)} -> {sv}{c('2', note)}")
header("Same version, different features", feature_only) for name, bs, ss in feature_only: print(f" {name:<{width}} {c('33', bs):<28} -> {ss}")
if unorderable: header("Version differs (not comparable)", unorderable) for name, bv, sv in unorderable: print(f" {name:<{width}} {bv:<20} -> {sv}")
if stale_paths: header("Beaver path points at a missing directory", stale_paths) for name, path in stale_paths: print(f" {c('31', '!')} {name:<{width}} {path}")
if ahead: header("Beaver ahead of upstream", ahead) for name, bv, sv in ahead: print(f" {name:<{width}} {bv:<20} <- upstream {sv}")
if show_all: header("Beaver-only dependencies", beaver_only) for name in beaver_only: print(f" {name:<{width}} {describe(beaver[name])}") header("Identical", same) print(" " + ", ".join(same))
actionable = len(behind) + len(git_rev) + len(feature_only) + breaking_missing print( f"\n{c('1', 'summary')}: {breaking_missing} missing-and-used-by-components, " f"{len(behind)} behind, {len(git_rev)} git-rev, {len(feature_only)} feature-only, " f"{len(missing) - breaking_missing} missing-but-unused, {len(ahead)} ahead" ) if check and actionable: return 1 return 0
if __name__ == "__main__": sys.exit(main(sys.argv[1:]))