From ab01123569225cc3a734e7cbdf2434b7acc451d3 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:49:05 -0400 Subject: [PATCH 1/8] [SVLS-9976] chore(dev): patch-deps.sh to build against local dependency checkouts --- .gitignore | 2 + scripts/dev/README.md | 33 +++++ scripts/dev/patch-deps.sh | 247 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100755 scripts/dev/patch-deps.sh diff --git a/.gitignore b/.gitignore index cf89e2fc3..5f8817dcf 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ local_tests/java/out/ local_tests/java/.gradle/ bottlecap/target bottlecap/proptest-regressions +# scripts/dev/patch-deps.sh state and Cargo.lock backup +bottlecap/.patch-deps # scripts/dev/worktree.sh default location .worktrees diff --git a/scripts/dev/README.md b/scripts/dev/README.md index 26295c223..9f5e6ba8e 100644 --- a/scripts/dev/README.md +++ b/scripts/dev/README.md @@ -7,6 +7,7 @@ None of them edits shell rc files, `~/.cargo/config.toml` or Docker settings. |--------|---------| | `setup-build-cache.sh` | Install or detect `sccache`, print the env vars for the shared compile cache | | `worktree.sh` | Create a git worktree that shares the compile cache | +| `patch-deps.sh` | Point bottlecap at local checkouts of serverless-components, libdatadog, saluki or dd-trace-rs, and undo it | ## setup-build-cache.sh @@ -122,6 +123,38 @@ commands for the new worktree. All worktrees build into the shared `CARGO_TARGET_DIR`; see the measurements above for why there is no per-worktree option, and `setup-build-cache.sh --tests` for the one case that needs one. +## patch-deps.sh + +```sh +scripts/dev/patch-deps.sh add ~/src/serverless-components # or libdatadog, saluki, dd-trace-rs, libddwaf-rust +scripts/dev/patch-deps.sh status +scripts/dev/patch-deps.sh remove serverless-components +scripts/dev/patch-deps.sh restore # remove all, restore Cargo.lock +``` + +`add` runs `cargo metadata` on the checkout and on bottlecap, takes every crate +that is in both (transitive crates too, so types stay consistent), groups them +by source and writes one managed region at the end of `bottlecap/Cargo.toml`: + +```toml +# >>> patch-deps: managed by scripts/dev/patch-deps.sh. Do not commit. >>> +[patch."https://github.com/DataDog/serverless-components"] +datadog-agent-config = { path = "/Users/me/src/serverless-components/crates/datadog-agent-config" } # serverless-components +... +# <<< patch-deps <<< +``` + +Cargo rewrites `Cargo.lock` for patched crates, so the first `add` (no patch +active) backs the lock up in `bottlecap/.patch-deps/` (in `.gitignore`), after +the manifest was written. `restore` puts it back. After each change the script +resolves the graph, prints one line per crate with `active` or `NOT USED`, and +records the sha256 of the patched lock. If the lock changed since then (you +added or bumped a dependency), `restore` refuses (exit 4) so the backup does not +erase that change; check `git diff bottlecap/Cargo.lock`, then `restore --force`. +A crates.io patch is only used when the local version is semver compatible +with the locked one; a libdatadog checkout that is behind the lock shows +`NOT USED` for every crate. Exit code 3 means at least one patch is not used. + ## CI release binaries per commit GitLab compiles each flavor in the `bottlecap ()` job diff --git a/scripts/dev/patch-deps.sh b/scripts/dev/patch-deps.sh new file mode 100755 index 000000000..3bcccbcf2 --- /dev/null +++ b/scripts/dev/patch-deps.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# Point bottlecap at a local checkout of a dependency repo, and undo it again. +# +# scripts/dev/patch-deps.sh add # e.g. ../serverless-components or ~/src/libdatadog +# scripts/dev/patch-deps.sh remove # drop one checkout +# scripts/dev/patch-deps.sh restore [--force] # drop all, restore Cargo.lock +# scripts/dev/patch-deps.sh status +# +# `add` reads `cargo metadata` of the checkout and of bottlecap, takes every +# crate that appears in both, groups them by source (crates.io or git URL) and +# writes one managed `[patch]` region at the end of bottlecap/Cargo.toml. +# It backs up Cargo.lock once, because cargo rewrites it for patched crates. +# Works for serverless-components, libdatadog, saluki, dd-trace-rs and libddwaf-rust. +# +# A crates.io patch is only used when the local version is semver compatible +# with the locked one. The script prints "NOT USED" when cargo ignores a patch. +# `restore` refuses to overwrite a Cargo.lock that changed since the last +# patch-deps run (for example after you added a dependency); pass --force to +# restore the backup anyway. Never commit the managed region or the patched lock. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec python3 -I - "$here/../../bottlecap" "$@" <<'PY' +import hashlib, json, os, re, shutil, subprocess, sys + +BEGIN = "# >>> patch-deps: managed by scripts/dev/patch-deps.sh. Do not commit. >>>" +END = "# <<< patch-deps <<<" +REGION = re.compile(re.escape(BEGIN) + r".*?" + re.escape(END) + r"\n?", re.S) + +bottlecap = os.path.abspath(sys.argv[1]) +manifest = os.path.join(bottlecap, "Cargo.toml") +lock = os.path.join(bottlecap, "Cargo.lock") +state_dir = os.path.join(bottlecap, ".patch-deps") +state_file = os.path.join(state_dir, "state.json") +lock_backup = os.path.join(state_dir, "Cargo.lock.orig") +lock_hash_file = os.path.join(state_dir, "Cargo.lock.sha256") +args = sys.argv[2:] +force = "--force" in args +args = [a for a in args if a != "--force"] + + +def die(msg, code=1): + print(f"patch-deps: error: {msg}", file=sys.stderr) + sys.exit(code) + + +def metadata(path, no_deps): + cmd = ["cargo", "metadata", "--format-version", "1", "--manifest-path", path] + if no_deps: + cmd.append("--no-deps") + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode != 0: + die(f"cargo metadata failed for {path}:\n{r.stderr}") + return json.loads(r.stdout), r.stderr + + +def patch_key(source): + # registry+https://github.com/rust-lang/crates.io-index -> crates-io + # sparse+https://index.crates.io/ -> crates-io + # git+https://github.com/DataDog/saluki/?rev=abc#abc -> https://github.com/DataDog/saluki/ + if source.startswith(("registry+", "sparse+")): + return "crates-io" + if source.startswith("git+"): + return source[4:].split("#", 1)[0].split("?", 1)[0] + return None + + +def sha256_of(path): + with open(path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + + +def load_state(): + if os.path.exists(state_file): + with open(state_file) as f: + return json.load(f) + return {} + + +def save_state(state): + os.makedirs(state_dir, exist_ok=True) + with open(state_file, "w") as f: + json.dump(state, f, indent=2, sort_keys=True) + + +def read_manifest(): + with open(manifest) as f: + return f.read() + + +def write_region(state): + """Rewrite the managed region. Dies before touching any file when the manifest has a foreign [patch].""" + text = REGION.sub("", read_manifest()).rstrip("\n") + "\n" + stray = [l for l in text.splitlines() if l.startswith("[patch")] + if stray: + die(f"Cargo.toml already has a [patch] table outside the managed region: {stray}") + if state: + tables = {} + for name, entries in sorted(state.items()): + for crate, info in sorted(entries["crates"].items()): + tables.setdefault(info["key"], []).append((crate, info["path"], name)) + lines = ["", BEGIN] + for key, crates in sorted(tables.items()): + lines.append(f"[patch.{key}]" if key == "crates-io" else f'[patch."{key}"]') + for crate, path, name in crates: + lines.append(f'{crate} = {{ path = "{path}" }} # {name}') + lines.append(END) + text += "\n".join(lines) + "\n" + with open(manifest, "w") as f: + f.write(text) + + +def backup_lock(): + """Take a fresh backup. Called only when no patch was active, so the lock is the pristine one.""" + if os.path.exists(lock): + os.makedirs(state_dir, exist_ok=True) + shutil.copy2(lock, lock_backup) + + +def record_lock_hash(): + if os.path.exists(lock): + os.makedirs(state_dir, exist_ok=True) + with open(lock_hash_file, "w") as f: + f.write(sha256_of(lock)) + + +def check_lock_unchanged(): + if not (os.path.exists(lock_hash_file) and os.path.exists(lock)): + return + with open(lock_hash_file) as f: + recorded = f.read().strip() + if recorded != sha256_of(lock) and not force: + die("Cargo.lock changed since the last patch-deps run (did you add or bump a dependency?). " + "Inspect it with `git diff bottlecap/Cargo.lock`, then re-run with --force to restore the backup anyway.", code=4) + + +def restore_lock(): + if os.path.exists(lock_backup): + shutil.move(lock_backup, lock) + print("patch-deps: restored Cargo.lock", file=sys.stderr) + + +def resolve_and_report(state): + """Re-run cargo metadata on bottlecap so Cargo.lock updates, then show what is in effect.""" + meta, stderr = metadata(manifest, no_deps=False) + unused = [l.strip() for l in stderr.splitlines() if "was not used in the crate graph" in l] + by_name = {p["name"]: p for p in meta["packages"]} + for name, entries in sorted(state.items()): + print(f"{name} ({entries['checkout']})") + for crate, info in sorted(entries["crates"].items()): + p = by_name.get(crate) + active = p is not None and p["source"] is None and os.path.dirname(p["manifest_path"]) == info["path"] + status = "active" if active else "NOT USED (version not compatible with the locked one?)" + print(f" {crate:28s} locked {info['locked']:>8s} local {info['local']:>8s} {status}") + for l in unused: + print(f" cargo: {l}", file=sys.stderr) + record_lock_hash() + return not unused + + +def cmd_add(checkout): + checkout = os.path.abspath(os.path.expanduser(checkout)) + cm = os.path.join(checkout, "Cargo.toml") + if not os.path.exists(cm): + die(f"{cm} not found") + local, _ = metadata(cm, no_deps=True) + local_pkgs = {p["name"]: p for p in local["packages"]} + graph, _ = metadata(manifest, no_deps=False) + crates = {} + for p in graph["packages"]: + if p["name"] in local_pkgs and p["source"]: + key = patch_key(p["source"]) + if key is None: + continue + lp = local_pkgs[p["name"]] + crates[p["name"]] = { + "key": key, + "path": os.path.dirname(lp["manifest_path"]), + "locked": p["version"], + "local": lp["version"], + } + if not crates: + die(f"no crate of {checkout} is in bottlecap's dependency graph") + name = os.path.basename(checkout.rstrip("/")) + state = load_state() + fresh = not state + new_state = dict(state) + new_state[name] = {"checkout": checkout, "crates": crates} + write_region(new_state) + if fresh: + backup_lock() + save_state(new_state) + for c, info in sorted(crates.items()): + if info["locked"].split(".")[0] != info["local"].split(".")[0]: + print(f"patch-deps: warning: {c} local {info['local']} vs locked {info['locked']}: major version differs, cargo will not use the patch", + file=sys.stderr) + ok = resolve_and_report(new_state) + print(f"patch-deps: {len(crates)} crate(s) from {name} written to {manifest}", file=sys.stderr) + sys.exit(0 if ok else 3) + + +def cmd_remove(which): + state = load_state() + name = which if which in state else os.path.basename(os.path.abspath(os.path.expanduser(which)).rstrip("/")) + if name not in state: + die(f"{which} is not patched (known: {', '.join(state) or 'none'})") + if len(state) == 1: + cmd_restore() + return + del state[name] + write_region(state) + save_state(state) + resolve_and_report(state) + print(f"patch-deps: removed {name}", file=sys.stderr) + + +def cmd_restore(): + check_lock_unchanged() + write_region({}) + restore_lock() + shutil.rmtree(state_dir, ignore_errors=True) + print("patch-deps: Cargo.toml has no managed region now", file=sys.stderr) + + +def cmd_status(): + state = load_state() + if not state: + print("patch-deps: nothing patched") + return + resolve_and_report(state) + + +if not args or args[0] in ("-h", "--help"): + print("usage: patch-deps.sh add | remove | restore [--force] | status", file=sys.stderr) + sys.exit(2) +cmd, rest = args[0], args[1:] +if cmd == "add" and len(rest) == 1: + cmd_add(rest[0]) +elif cmd == "remove" and len(rest) == 1: + cmd_remove(rest[0]) +elif cmd == "restore" and not rest: + cmd_restore() +elif cmd == "status" and not rest: + cmd_status() +else: + die("bad arguments. usage: add | remove | restore [--force] | status", code=2) +PY From 229b80652b4483e8168366b679029102cf552c7f Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:27:51 -0400 Subject: [PATCH 2/8] chore(dev): address review feedback on patch-deps.sh --- scripts/dev/patch-deps.sh | 44 ++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/scripts/dev/patch-deps.sh b/scripts/dev/patch-deps.sh index 3bcccbcf2..87cf70baa 100755 --- a/scripts/dev/patch-deps.sh +++ b/scripts/dev/patch-deps.sh @@ -131,7 +131,7 @@ def check_lock_unchanged(): recorded = f.read().strip() if recorded != sha256_of(lock) and not force: die("Cargo.lock changed since the last patch-deps run (did you add or bump a dependency?). " - "Inspect it with `git diff bottlecap/Cargo.lock`, then re-run with --force to restore the backup anyway.", code=4) + "Inspect it with `git diff bottlecap/Cargo.lock`, then re-run with --force to go ahead anyway.", code=4) def restore_lock(): @@ -140,7 +140,7 @@ def restore_lock(): print("patch-deps: restored Cargo.lock", file=sys.stderr) -def resolve_and_report(state): +def resolve_and_report(state, record=True): """Re-run cargo metadata on bottlecap so Cargo.lock updates, then show what is in effect.""" meta, stderr = metadata(manifest, no_deps=False) unused = [l.strip() for l in stderr.splitlines() if "was not used in the crate graph" in l] @@ -154,7 +154,8 @@ def resolve_and_report(state): print(f" {crate:28s} locked {info['locked']:>8s} local {info['local']:>8s} {status}") for l in unused: print(f" cargo: {l}", file=sys.stderr) - record_lock_hash() + if record: + record_lock_hash() return not unused @@ -165,24 +166,32 @@ def cmd_add(checkout): die(f"{cm} not found") local, _ = metadata(cm, no_deps=True) local_pkgs = {p["name"]: p for p in local["packages"]} + name = os.path.basename(checkout.rstrip("/")) + state = load_state() + patched = state.get(name, {}).get("crates", {}) graph, _ = metadata(manifest, no_deps=False) crates = {} for p in graph["packages"]: - if p["name"] in local_pkgs and p["source"]: - key = patch_key(p["source"]) - if key is None: - continue - lp = local_pkgs[p["name"]] - crates[p["name"]] = { - "key": key, - "path": os.path.dirname(lp["manifest_path"]), - "locked": p["version"], - "local": lp["version"], - } + if p["name"] not in local_pkgs: + continue + if not p["source"]: + # Already patched by an earlier `add` of this checkout: cargo shows it as a path package. + prev = patched.get(p["name"]) + if prev and os.path.dirname(p["manifest_path"]) == prev["path"]: + crates[p["name"]] = dict(prev, local=local_pkgs[p["name"]]["version"]) + continue + key = patch_key(p["source"]) + if key is None: + continue + lp = local_pkgs[p["name"]] + crates[p["name"]] = { + "key": key, + "path": os.path.dirname(lp["manifest_path"]), + "locked": p["version"], + "local": lp["version"], + } if not crates: die(f"no crate of {checkout} is in bottlecap's dependency graph") - name = os.path.basename(checkout.rstrip("/")) - state = load_state() fresh = not state new_state = dict(state) new_state[name] = {"checkout": checkout, "crates": crates} @@ -204,6 +213,7 @@ def cmd_remove(which): name = which if which in state else os.path.basename(os.path.abspath(os.path.expanduser(which)).rstrip("/")) if name not in state: die(f"{which} is not patched (known: {', '.join(state) or 'none'})") + check_lock_unchanged() if len(state) == 1: cmd_restore() return @@ -227,7 +237,7 @@ def cmd_status(): if not state: print("patch-deps: nothing patched") return - resolve_and_report(state) + sys.exit(0 if resolve_and_report(state, record=False) else 3) if not args or args[0] in ("-h", "--help"): From 9c083f63fe39488ef8309388da8b0c7faa5ce026 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:38:11 -0400 Subject: [PATCH 3/8] chore(dev): check the lock on add and keep exit 3 on partial remove in patch-deps.sh --- scripts/dev/patch-deps.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/dev/patch-deps.sh b/scripts/dev/patch-deps.sh index 87cf70baa..75488f79a 100755 --- a/scripts/dev/patch-deps.sh +++ b/scripts/dev/patch-deps.sh @@ -168,22 +168,23 @@ def cmd_add(checkout): local_pkgs = {p["name"]: p for p in local["packages"]} name = os.path.basename(checkout.rstrip("/")) state = load_state() + check_lock_unchanged() patched = state.get(name, {}).get("crates", {}) graph, _ = metadata(manifest, no_deps=False) crates = {} for p in graph["packages"]: if p["name"] not in local_pkgs: continue + lp = local_pkgs[p["name"]] if not p["source"]: # Already patched by an earlier `add` of this checkout: cargo shows it as a path package. prev = patched.get(p["name"]) if prev and os.path.dirname(p["manifest_path"]) == prev["path"]: - crates[p["name"]] = dict(prev, local=local_pkgs[p["name"]]["version"]) + crates[p["name"]] = dict(prev, path=os.path.dirname(lp["manifest_path"]), local=lp["version"]) continue key = patch_key(p["source"]) if key is None: continue - lp = local_pkgs[p["name"]] crates[p["name"]] = { "key": key, "path": os.path.dirname(lp["manifest_path"]), @@ -220,8 +221,9 @@ def cmd_remove(which): del state[name] write_region(state) save_state(state) - resolve_and_report(state) + ok = resolve_and_report(state) print(f"patch-deps: removed {name}", file=sys.stderr) + sys.exit(0 if ok else 3) def cmd_restore(): From fd767459184fe3ec475a52e66d2c7b156a024619 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:22:17 -0500 Subject: [PATCH 4/8] chore(dev): harden patch-deps for forced lock edits, moved checkouts, quoted paths and duplicate crates - Accept --force only with restore, so add and remove can not record an edited Cargo.lock as the new baseline. - Point an earlier add of the same checkout at the new path before resolving, so a moved checkout can be re-added. - Write checkout paths as escaped TOML strings. - Patch a crate in every source it comes from, not only the last one. --- scripts/dev/README.md | 5 ++-- scripts/dev/patch-deps.sh | 57 ++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/scripts/dev/README.md b/scripts/dev/README.md index 17e8dcb83..cfac10a36 100644 --- a/scripts/dev/README.md +++ b/scripts/dev/README.md @@ -154,8 +154,9 @@ active) backs the lock up in `bottlecap/.patch-deps/` (in `.gitignore`), after the manifest was written. `restore` puts it back. After each change the script resolves the graph, prints one line per crate with `active` or `NOT USED`, and records the sha256 of the patched lock. If the lock changed since then (you -added or bumped a dependency), `restore` refuses (exit 4) so the backup does not -erase that change; check `git diff bottlecap/Cargo.lock`, then `restore --force`. +added or bumped a dependency), `add`, `remove` and `restore` refuse (exit 4) so +the backup does not erase that change; check `git diff bottlecap/Cargo.lock`, +then `restore --force`. Only `restore` takes `--force`. A crates.io patch is only used when the local version is semver compatible with the locked one; a libdatadog checkout that is behind the lock shows `NOT USED` for every crate. Exit code 3 means at least one patch is not used. diff --git a/scripts/dev/patch-deps.sh b/scripts/dev/patch-deps.sh index 75488f79a..5302ed49b 100755 --- a/scripts/dev/patch-deps.sh +++ b/scripts/dev/patch-deps.sh @@ -14,9 +14,9 @@ # # A crates.io patch is only used when the local version is semver compatible # with the locked one. The script prints "NOT USED" when cargo ignores a patch. -# `restore` refuses to overwrite a Cargo.lock that changed since the last -# patch-deps run (for example after you added a dependency); pass --force to -# restore the backup anyway. Never commit the managed region or the patched lock. +# `add`, `remove` and `restore` refuse to run when Cargo.lock changed since the +# last patch-deps run (for example after you added a dependency); `restore --force` +# restores the backup anyway. Never commit the managed region or the patched lock. set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -98,12 +98,13 @@ def write_region(state): tables = {} for name, entries in sorted(state.items()): for crate, info in sorted(entries["crates"].items()): - tables.setdefault(info["key"], []).append((crate, info["path"], name)) + for key in info["keys"]: + tables.setdefault(key, []).append((crate, info["path"], name)) lines = ["", BEGIN] for key, crates in sorted(tables.items()): lines.append(f"[patch.{key}]" if key == "crates-io" else f'[patch."{key}"]') for crate, path, name in crates: - lines.append(f'{crate} = {{ path = "{path}" }} # {name}') + lines.append(f"{crate} = {{ path = {json.dumps(path, ensure_ascii=False)} }} # {name}") lines.append(END) text += "\n".join(lines) + "\n" with open(manifest, "w") as f: @@ -131,7 +132,7 @@ def check_lock_unchanged(): recorded = f.read().strip() if recorded != sha256_of(lock) and not force: die("Cargo.lock changed since the last patch-deps run (did you add or bump a dependency?). " - "Inspect it with `git diff bottlecap/Cargo.lock`, then re-run with --force to go ahead anyway.", code=4) + "Inspect it with `git diff bottlecap/Cargo.lock`, then run `restore --force` to restore the backup anyway.", code=4) def restore_lock(): @@ -144,14 +145,13 @@ def resolve_and_report(state, record=True): """Re-run cargo metadata on bottlecap so Cargo.lock updates, then show what is in effect.""" meta, stderr = metadata(manifest, no_deps=False) unused = [l.strip() for l in stderr.splitlines() if "was not used in the crate graph" in l] - by_name = {p["name"]: p for p in meta["packages"]} for name, entries in sorted(state.items()): print(f"{name} ({entries['checkout']})") for crate, info in sorted(entries["crates"].items()): - p = by_name.get(crate) - active = p is not None and p["source"] is None and os.path.dirname(p["manifest_path"]) == info["path"] + active = any(p["name"] == crate and p["source"] is None and os.path.dirname(p["manifest_path"]) == info["path"] + for p in meta["packages"]) status = "active" if active else "NOT USED (version not compatible with the locked one?)" - print(f" {crate:28s} locked {info['locked']:>8s} local {info['local']:>8s} {status}") + print(f" {crate:28s} locked {', '.join(info['locked']):>8s} local {info['local']:>8s} {status}") for l in unused: print(f" cargo: {l}", file=sys.stderr) if record: @@ -169,7 +169,11 @@ def cmd_add(checkout): name = os.path.basename(checkout.rstrip("/")) state = load_state() check_lock_unchanged() - patched = state.get(name, {}).get("crates", {}) + # Point an earlier `add` of this checkout at the new path first, so a moved checkout still resolves. + patched = {c: dict(info, path=os.path.dirname(local_pkgs[c]["manifest_path"])) + for c, info in state.get(name, {}).get("crates", {}).items() if c in local_pkgs} + if name in state: + write_region({**state, name: {"checkout": checkout, "crates": patched}}) graph, _ = metadata(manifest, no_deps=False) crates = {} for p in graph["packages"]: @@ -179,18 +183,19 @@ def cmd_add(checkout): if not p["source"]: # Already patched by an earlier `add` of this checkout: cargo shows it as a path package. prev = patched.get(p["name"]) - if prev and os.path.dirname(p["manifest_path"]) == prev["path"]: - crates[p["name"]] = dict(prev, path=os.path.dirname(lp["manifest_path"]), local=lp["version"]) - continue - key = patch_key(p["source"]) - if key is None: - continue - crates[p["name"]] = { - "key": key, - "path": os.path.dirname(lp["manifest_path"]), - "locked": p["version"], - "local": lp["version"], - } + if not (prev and os.path.dirname(p["manifest_path"]) == prev["path"]): + continue + keys, locked = prev["keys"], prev["locked"] + else: + key = patch_key(p["source"]) + if key is None: + continue + keys, locked = [key], [p["version"]] + # The same crate can come from several sources or versions: patch each source. + info = crates.setdefault(p["name"], {"keys": [], "locked": [], "path": os.path.dirname(lp["manifest_path"]), + "local": lp["version"]}) + info["keys"] = sorted(set(info["keys"] + keys)) + info["locked"] = sorted(set(info["locked"] + locked)) if not crates: die(f"no crate of {checkout} is in bottlecap's dependency graph") fresh = not state @@ -201,8 +206,8 @@ def cmd_add(checkout): backup_lock() save_state(new_state) for c, info in sorted(crates.items()): - if info["locked"].split(".")[0] != info["local"].split(".")[0]: - print(f"patch-deps: warning: {c} local {info['local']} vs locked {info['locked']}: major version differs, cargo will not use the patch", + if all(v.split(".")[0] != info["local"].split(".")[0] for v in info["locked"]): + print(f"patch-deps: warning: {c} local {info['local']} vs locked {', '.join(info['locked'])}: major version differs, cargo will not use the patch", file=sys.stderr) ok = resolve_and_report(new_state) print(f"patch-deps: {len(crates)} crate(s) from {name} written to {manifest}", file=sys.stderr) @@ -246,6 +251,8 @@ if not args or args[0] in ("-h", "--help"): print("usage: patch-deps.sh add | remove | restore [--force] | status", file=sys.stderr) sys.exit(2) cmd, rest = args[0], args[1:] +if force and cmd != "restore": + die("--force is only valid with restore", code=2) if cmd == "add" and len(rest) == 1: cmd_add(rest[0]) elif cmd == "remove" and len(rest) == 1: From ba019bb194a6d03c50495964a312df2a2fc7e530 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:59:58 -0500 Subject: [PATCH 5/8] chore(dev): select differing local versions in patch-deps and tighten its lock and name checks - add runs `cargo update` on a crate whose locked version differs from the local one (`--precise` for crates.io), so cargo selects the patch - the exit code comes from the resolved packages too, not only from the cargo warning that quiet mode hides - a deleted Cargo.lock counts as changed when a hash is recorded - status records the lock hash when the lock was unchanged before it ran - add refuses a second checkout with the same name while the first exists --- scripts/dev/README.md | 13 +++++++++---- scripts/dev/patch-deps.sh | 37 +++++++++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/scripts/dev/README.md b/scripts/dev/README.md index cfac10a36..9208ec529 100644 --- a/scripts/dev/README.md +++ b/scripts/dev/README.md @@ -153,13 +153,18 @@ Cargo rewrites `Cargo.lock` for patched crates, so the first `add` (no patch active) backs the lock up in `bottlecap/.patch-deps/` (in `.gitignore`), after the manifest was written. `restore` puts it back. After each change the script resolves the graph, prints one line per crate with `active` or `NOT USED`, and -records the sha256 of the patched lock. If the lock changed since then (you +records the sha256 of the patched lock (`status` records it only when the lock +was unchanged before it ran). If the lock changed or was deleted since then (you added or bumped a dependency), `add`, `remove` and `restore` refuse (exit 4) so the backup does not erase that change; check `git diff bottlecap/Cargo.lock`, then `restore --force`. Only `restore` takes `--force`. -A crates.io patch is only used when the local version is semver compatible -with the locked one; a libdatadog checkout that is behind the lock shows -`NOT USED` for every crate. Exit code 3 means at least one patch is not used. +When a local version differs from the locked one, `add` runs `cargo update` on +that crate (`--precise ` for crates.io) so cargo selects the +patch. Cargo ignores a patch whose version does not match the requirements on +that crate, for example a libdatadog checkout with another major version; the +script shows `NOT USED` and exits 3. +A checkout is known by its directory name. `add` refuses a second checkout with +the same name while the first one exists; a moved checkout replaces the old entry. ## CI release binaries per commit diff --git a/scripts/dev/patch-deps.sh b/scripts/dev/patch-deps.sh index 5302ed49b..a16eeb059 100755 --- a/scripts/dev/patch-deps.sh +++ b/scripts/dev/patch-deps.sh @@ -12,8 +12,9 @@ # It backs up Cargo.lock once, because cargo rewrites it for patched crates. # Works for serverless-components, libdatadog, saluki, dd-trace-rs and libddwaf-rust. # -# A crates.io patch is only used when the local version is semver compatible -# with the locked one. The script prints "NOT USED" when cargo ignores a patch. +# When a local version differs from the locked one, `add` runs `cargo update` on +# that crate so cargo selects the patch. Cargo ignores a patch whose version does +# not match bottlecap's requirements; the script then prints "NOT USED". # `add`, `remove` and `restore` refuse to run when Cargo.lock changed since the # last patch-deps run (for example after you added a dependency); `restore --force` # restores the backup anyway. Never commit the managed region or the patched lock. @@ -125,13 +126,17 @@ def record_lock_hash(): f.write(sha256_of(lock)) -def check_lock_unchanged(): - if not (os.path.exists(lock_hash_file) and os.path.exists(lock)): - return +def lock_unchanged(): + if not os.path.exists(lock_hash_file): + return True with open(lock_hash_file) as f: recorded = f.read().strip() - if recorded != sha256_of(lock) and not force: - die("Cargo.lock changed since the last patch-deps run (did you add or bump a dependency?). " + return os.path.exists(lock) and recorded == sha256_of(lock) + + +def check_lock_unchanged(): + if not (force or lock_unchanged()): + die("Cargo.lock changed or was deleted since the last patch-deps run (did you add or bump a dependency?). " "Inspect it with `git diff bottlecap/Cargo.lock`, then run `restore --force` to restore the backup anyway.", code=4) @@ -145,18 +150,20 @@ def resolve_and_report(state, record=True): """Re-run cargo metadata on bottlecap so Cargo.lock updates, then show what is in effect.""" meta, stderr = metadata(manifest, no_deps=False) unused = [l.strip() for l in stderr.splitlines() if "was not used in the crate graph" in l] + ok = not unused for name, entries in sorted(state.items()): print(f"{name} ({entries['checkout']})") for crate, info in sorted(entries["crates"].items()): active = any(p["name"] == crate and p["source"] is None and os.path.dirname(p["manifest_path"]) == info["path"] for p in meta["packages"]) + ok = ok and active status = "active" if active else "NOT USED (version not compatible with the locked one?)" print(f" {crate:28s} locked {', '.join(info['locked']):>8s} local {info['local']:>8s} {status}") for l in unused: print(f" cargo: {l}", file=sys.stderr) if record: record_lock_hash() - return not unused + return ok def cmd_add(checkout): @@ -168,6 +175,9 @@ def cmd_add(checkout): local_pkgs = {p["name"]: p for p in local["packages"]} name = os.path.basename(checkout.rstrip("/")) state = load_state() + old = state.get(name, {}).get("checkout") + if old and os.path.exists(old) and not os.path.samefile(old, checkout): + die(f"{name} is already patched from {old}. Run `remove {name}` first.") check_lock_unchanged() # Point an earlier `add` of this checkout at the new path first, so a moved checkout still resolves. patched = {c: dict(info, path=os.path.dirname(local_pkgs[c]["manifest_path"])) @@ -175,7 +185,7 @@ def cmd_add(checkout): if name in state: write_region({**state, name: {"checkout": checkout, "crates": patched}}) graph, _ = metadata(manifest, no_deps=False) - crates = {} + crates, updates = {}, [] for p in graph["packages"]: if p["name"] not in local_pkgs: continue @@ -191,6 +201,10 @@ def cmd_add(checkout): if key is None: continue keys, locked = [key], [p["version"]] + if p["version"] != lp["version"]: + # Cargo keeps a locked version that differs from the patch, so unlock it. For crates.io, + # --precise selects the local version, or fails and changes nothing if no requirement allows it. + updates.append(["-p", p["id"]] + (["--precise", lp["version"]] if key == "crates-io" else [])) # The same crate can come from several sources or versions: patch each source. info = crates.setdefault(p["name"], {"keys": [], "locked": [], "path": os.path.dirname(lp["manifest_path"]), "local": lp["version"]}) @@ -209,6 +223,8 @@ def cmd_add(checkout): if all(v.split(".")[0] != info["local"].split(".")[0] for v in info["locked"]): print(f"patch-deps: warning: {c} local {info['local']} vs locked {', '.join(info['locked'])}: major version differs, cargo will not use the patch", file=sys.stderr) + for u in updates: + subprocess.run(["cargo", "update", "--manifest-path", manifest] + u, capture_output=True) ok = resolve_and_report(new_state) print(f"patch-deps: {len(crates)} crate(s) from {name} written to {manifest}", file=sys.stderr) sys.exit(0 if ok else 3) @@ -244,7 +260,8 @@ def cmd_status(): if not state: print("patch-deps: nothing patched") return - sys.exit(0 if resolve_and_report(state, record=False) else 3) + # Record the lock update that this resolve makes, but never accept an earlier foreign edit. + sys.exit(0 if resolve_and_report(state, record=lock_unchanged()) else 3) if not args or args[0] in ("-h", "--help"): From 842a081bc0aac124d3ef15395be51fefd36b08f1 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:28:37 -0500 Subject: [PATCH 6/8] chore(dev): make patch-deps status read-only and check every package of a patched crate - status resolves with `cargo metadata --locked`. It never writes Cargo.lock or the recorded lock hash, and fails when the lock needs an update. - a crate is active only when no package of that name still comes from crates.io or git. The report lists each remote package with its version and source, and the command exits 3. - drop the stderr scan for unused patches and the conditional hash record in status, which the two rules above replace. --- scripts/dev/README.md | 21 ++++++++----- scripts/dev/patch-deps.sh | 66 ++++++++++++++++++--------------------- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/scripts/dev/README.md b/scripts/dev/README.md index 9208ec529..4410dc0f1 100644 --- a/scripts/dev/README.md +++ b/scripts/dev/README.md @@ -151,13 +151,20 @@ datadog-agent-config = { path = "/Users/me/src/serverless-components/crates/data Cargo rewrites `Cargo.lock` for patched crates, so the first `add` (no patch active) backs the lock up in `bottlecap/.patch-deps/` (in `.gitignore`), after -the manifest was written. `restore` puts it back. After each change the script -resolves the graph, prints one line per crate with `active` or `NOT USED`, and -records the sha256 of the patched lock (`status` records it only when the lock -was unchanged before it ran). If the lock changed or was deleted since then (you -added or bumped a dependency), `add`, `remove` and `restore` refuse (exit 4) so -the backup does not erase that change; check `git diff bottlecap/Cargo.lock`, -then `restore --force`. Only `restore` takes `--force`. +the manifest was written. `restore` puts it back. After `add` and `remove` the +script resolves the graph, prints one line per crate and records the sha256 of +the patched lock. A crate is `active` only when every package of that name from +crates.io or git resolves to the local path. Otherwise the script shows +`PARTLY USED` or `NOT USED`, lists each package that is still remote with its +version and source, and exits 3. +`status` is read-only. It runs `cargo metadata --locked`, prints the same report +and never writes `Cargo.lock` or the recorded sha256. If the lock needs an +update (`Cargo.toml` or a checkout changed), `status` fails (exit 1); after a +checkout changed its dependencies, run `add ` again. +If the lock changed or was deleted since the last `add` or `remove` (you added +or bumped a dependency), `add`, `remove` and `restore` refuse (exit 4) so the +backup does not erase that change; check `git diff bottlecap/Cargo.lock`, then +`restore --force`. Only `restore` takes `--force`. When a local version differs from the locked one, `add` runs `cargo update` on that crate (`--precise ` for crates.io) so cargo selects the patch. Cargo ignores a patch whose version does not match the requirements on diff --git a/scripts/dev/patch-deps.sh b/scripts/dev/patch-deps.sh index a16eeb059..068e376a0 100755 --- a/scripts/dev/patch-deps.sh +++ b/scripts/dev/patch-deps.sh @@ -15,8 +15,10 @@ # When a local version differs from the locked one, `add` runs `cargo update` on # that crate so cargo selects the patch. Cargo ignores a patch whose version does # not match bottlecap's requirements; the script then prints "NOT USED". +# A crate is active only when no package of that name still comes from crates.io or git. +# `status` is read-only: it never writes Cargo.lock or the recorded lock hash. # `add`, `remove` and `restore` refuse to run when Cargo.lock changed since the -# last patch-deps run (for example after you added a dependency); `restore --force` +# last `add` or `remove` (for example after you added a dependency); `restore --force` # restores the backup anyway. Never commit the managed region or the patched lock. set -euo pipefail @@ -45,14 +47,12 @@ def die(msg, code=1): sys.exit(code) -def metadata(path, no_deps): - cmd = ["cargo", "metadata", "--format-version", "1", "--manifest-path", path] - if no_deps: - cmd.append("--no-deps") +def metadata(path, *flags, hint=""): + cmd = ["cargo", "metadata", "--format-version", "1", "--manifest-path", path, *flags] r = subprocess.run(cmd, capture_output=True, text=True) if r.returncode != 0: - die(f"cargo metadata failed for {path}:\n{r.stderr}") - return json.loads(r.stdout), r.stderr + die(f"cargo metadata failed for {path}:\n{r.stderr}{hint}") + return json.loads(r.stdout) def patch_key(source): @@ -126,17 +126,13 @@ def record_lock_hash(): f.write(sha256_of(lock)) -def lock_unchanged(): - if not os.path.exists(lock_hash_file): - return True +def check_lock_unchanged(): + if force or not os.path.exists(lock_hash_file): + return with open(lock_hash_file) as f: recorded = f.read().strip() - return os.path.exists(lock) and recorded == sha256_of(lock) - - -def check_lock_unchanged(): - if not (force or lock_unchanged()): - die("Cargo.lock changed or was deleted since the last patch-deps run (did you add or bump a dependency?). " + if not (os.path.exists(lock) and recorded == sha256_of(lock)): + die("Cargo.lock changed or was deleted since the last `add` or `remove` (did you add or bump a dependency?). " "Inspect it with `git diff bottlecap/Cargo.lock`, then run `restore --force` to restore the backup anyway.", code=4) @@ -146,23 +142,20 @@ def restore_lock(): print("patch-deps: restored Cargo.lock", file=sys.stderr) -def resolve_and_report(state, record=True): - """Re-run cargo metadata on bottlecap so Cargo.lock updates, then show what is in effect.""" - meta, stderr = metadata(manifest, no_deps=False) - unused = [l.strip() for l in stderr.splitlines() if "was not used in the crate graph" in l] - ok = not unused +def report(state, meta): + """Show what is in effect. A crate is active only if no package of that name still comes from crates.io or git.""" + ok = True for name, entries in sorted(state.items()): print(f"{name} ({entries['checkout']})") for crate, info in sorted(entries["crates"].items()): - active = any(p["name"] == crate and p["source"] is None and os.path.dirname(p["manifest_path"]) == info["path"] - for p in meta["packages"]) - ok = ok and active - status = "active" if active else "NOT USED (version not compatible with the locked one?)" + pkgs = [p for p in meta["packages"] if p["name"] == crate] + used = any(p["source"] is None and os.path.dirname(p["manifest_path"]) == info["path"] for p in pkgs) + remote = [p for p in pkgs if p["source"] and patch_key(p["source"])] + ok = ok and used and not remote + status = ("active" if not remote else "PARTLY USED") if used else "NOT USED (version not compatible with the locked one?)" print(f" {crate:28s} locked {', '.join(info['locked']):>8s} local {info['local']:>8s} {status}") - for l in unused: - print(f" cargo: {l}", file=sys.stderr) - if record: - record_lock_hash() + for p in remote: + print(f" still remote: {p['version']} {p['source']}") return ok @@ -171,7 +164,7 @@ def cmd_add(checkout): cm = os.path.join(checkout, "Cargo.toml") if not os.path.exists(cm): die(f"{cm} not found") - local, _ = metadata(cm, no_deps=True) + local = metadata(cm, "--no-deps") local_pkgs = {p["name"]: p for p in local["packages"]} name = os.path.basename(checkout.rstrip("/")) state = load_state() @@ -184,7 +177,7 @@ def cmd_add(checkout): for c, info in state.get(name, {}).get("crates", {}).items() if c in local_pkgs} if name in state: write_region({**state, name: {"checkout": checkout, "crates": patched}}) - graph, _ = metadata(manifest, no_deps=False) + graph = metadata(manifest) crates, updates = {}, [] for p in graph["packages"]: if p["name"] not in local_pkgs: @@ -225,7 +218,8 @@ def cmd_add(checkout): file=sys.stderr) for u in updates: subprocess.run(["cargo", "update", "--manifest-path", manifest] + u, capture_output=True) - ok = resolve_and_report(new_state) + ok = report(new_state, metadata(manifest)) # cargo metadata updates Cargo.lock for the new patches + record_lock_hash() print(f"patch-deps: {len(crates)} crate(s) from {name} written to {manifest}", file=sys.stderr) sys.exit(0 if ok else 3) @@ -242,7 +236,8 @@ def cmd_remove(which): del state[name] write_region(state) save_state(state) - ok = resolve_and_report(state) + ok = report(state, metadata(manifest)) + record_lock_hash() print(f"patch-deps: removed {name}", file=sys.stderr) sys.exit(0 if ok else 3) @@ -260,8 +255,9 @@ def cmd_status(): if not state: print("patch-deps: nothing patched") return - # Record the lock update that this resolve makes, but never accept an earlier foreign edit. - sys.exit(0 if resolve_and_report(state, record=lock_unchanged()) else 3) + meta = metadata(manifest, "--locked", hint="patch-deps: status never writes Cargo.lock. " + "After a checkout changed its dependencies, run `add ` again.") + sys.exit(0 if report(state, meta) else 3) if not args or args[0] in ("-h", "--help"): From 281cf50bfabe6b569cb1adfbbcae0866b3f2c091 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:47:58 -0500 Subject: [PATCH 7/8] chore(dev): refuse unlocked Cargo.toml edits and find indented or quoted patch tables in patch-deps --- scripts/dev/README.md | 7 +++++++ scripts/dev/patch-deps.sh | 28 ++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/scripts/dev/README.md b/scripts/dev/README.md index 4410dc0f1..d3e1d22a0 100644 --- a/scripts/dev/README.md +++ b/scripts/dev/README.md @@ -165,6 +165,13 @@ If the lock changed or was deleted since the last `add` or `remove` (you added or bumped a dependency), `add`, `remove` and `restore` refuse (exit 4) so the backup does not erase that change; check `git diff bottlecap/Cargo.lock`, then `restore --force`. Only `restore` takes `--force`. +If `Cargo.toml` changed since the last `add` or `remove` and `Cargo.lock` does +not match it, `add` and `remove` refuse (exit 1), so the recorded lock holds +only the script's own changes. Run `restore`, then `cargo check`, then `add` +again. Before the first `add`, the lock must also match `Cargo.toml`. +The script refuses a `[patch]` table or a root-level `patch.` key outside +the managed region, also with leading spaces or quoted keys. The check reads lines, +not TOML, so it does not see a header inside a multi-line string or array. When a local version differs from the locked one, `add` runs `cargo update` on that crate (`--precise ` for crates.io) so cargo selects the patch. Cargo ignores a patch whose version does not match the requirements on diff --git a/scripts/dev/patch-deps.sh b/scripts/dev/patch-deps.sh index 068e376a0..1355ec227 100755 --- a/scripts/dev/patch-deps.sh +++ b/scripts/dev/patch-deps.sh @@ -19,16 +19,19 @@ # `status` is read-only: it never writes Cargo.lock or the recorded lock hash. # `add`, `remove` and `restore` refuse to run when Cargo.lock changed since the # last `add` or `remove` (for example after you added a dependency); `restore --force` -# restores the backup anyway. Never commit the managed region or the patched lock. +# restores the backup anyway. `add` and `remove` also refuse when Cargo.toml changed +# and Cargo.lock does not match it. Never commit the managed region or the patched lock. set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" exec python3 -I - "$here/../../bottlecap" "$@" <<'PY' -import hashlib, json, os, re, shutil, subprocess, sys +import hashlib, itertools, json, os, re, shutil, subprocess, sys BEGIN = "# >>> patch-deps: managed by scripts/dev/patch-deps.sh. Do not commit. >>>" END = "# <<< patch-deps <<<" REGION = re.compile(re.escape(BEGIN) + r".*?" + re.escape(END) + r"\n?", re.S) +# TOML allows whitespace before and inside a table header, and quoted keys: ` [ "patch" . crates-io ]`. +HEADER, PATCH = r"\s*\[", r"""\s*(?:patch|"patch"|'patch')\s*""" bottlecap = os.path.abspath(sys.argv[1]) manifest = os.path.join(bottlecap, "Cargo.toml") @@ -37,6 +40,7 @@ state_dir = os.path.join(bottlecap, ".patch-deps") state_file = os.path.join(state_dir, "state.json") lock_backup = os.path.join(state_dir, "Cargo.lock.orig") lock_hash_file = os.path.join(state_dir, "Cargo.lock.sha256") +manifest_hash_file = os.path.join(state_dir, "Cargo.toml.sha256") args = sys.argv[2:] force = "--force" in args args = [a for a in args if a != "--force"] @@ -92,9 +96,11 @@ def read_manifest(): def write_region(state): """Rewrite the managed region. Dies before touching any file when the manifest has a foreign [patch].""" text = REGION.sub("", read_manifest()).rstrip("\n") + "\n" - stray = [l for l in text.splitlines() if l.startswith("[patch")] + lines = text.splitlines() + root = itertools.takewhile(lambda l: not re.match(HEADER, l), lines) # keys before the first table header + stray = [l for l in lines if re.match(HEADER + PATCH + r"[.\]]", l)] + [l for l in root if re.match(PATCH + "[.=]", l)] if stray: - die(f"Cargo.toml already has a [patch] table outside the managed region: {stray}") + die(f"Cargo.toml already has a [patch] table or key outside the managed region: {stray}") if state: tables = {} for name, entries in sorted(state.items()): @@ -124,6 +130,8 @@ def record_lock_hash(): os.makedirs(state_dir, exist_ok=True) with open(lock_hash_file, "w") as f: f.write(sha256_of(lock)) + with open(manifest_hash_file, "w") as f: + f.write(sha256_of(manifest)) def check_lock_unchanged(): @@ -136,6 +144,16 @@ def check_lock_unchanged(): "Inspect it with `git diff bottlecap/Cargo.lock`, then run `restore --force` to restore the backup anyway.", code=4) +def check_lock_matches_manifest(): + """Refuse when Cargo.toml changed since the last `add` or `remove` and Cargo.lock does not match it.""" + if os.path.exists(manifest_hash_file): + with open(manifest_hash_file) as f: + if f.read() == sha256_of(manifest): + return + metadata(manifest, "--locked", hint="patch-deps: update Cargo.lock first: " + + ("run `restore`, `cargo check`, then `add` again." if os.path.exists(state_file) else "run `cargo check`.")) + + def restore_lock(): if os.path.exists(lock_backup): shutil.move(lock_backup, lock) @@ -172,6 +190,7 @@ def cmd_add(checkout): if old and os.path.exists(old) and not os.path.samefile(old, checkout): die(f"{name} is already patched from {old}. Run `remove {name}` first.") check_lock_unchanged() + check_lock_matches_manifest() # Point an earlier `add` of this checkout at the new path first, so a moved checkout still resolves. patched = {c: dict(info, path=os.path.dirname(local_pkgs[c]["manifest_path"])) for c, info in state.get(name, {}).get("crates", {}).items() if c in local_pkgs} @@ -233,6 +252,7 @@ def cmd_remove(which): if len(state) == 1: cmd_restore() return + check_lock_matches_manifest() del state[name] write_region(state) save_state(state) From ea8ce4e92d177541682e04d4079b8668dc00fef0 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Fri, 6 Nov 2026 11:54:55 -0500 Subject: [PATCH 8/8] chore(dev): check Cargo.lock against Cargo.toml before patch-deps removes the last checkout --- scripts/dev/patch-deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dev/patch-deps.sh b/scripts/dev/patch-deps.sh index 1355ec227..b675c0e66 100755 --- a/scripts/dev/patch-deps.sh +++ b/scripts/dev/patch-deps.sh @@ -249,10 +249,10 @@ def cmd_remove(which): if name not in state: die(f"{which} is not patched (known: {', '.join(state) or 'none'})") check_lock_unchanged() + check_lock_matches_manifest() if len(state) == 1: cmd_restore() return - check_lock_matches_manifest() del state[name] write_region(state) save_state(state)