#!/usr/bin/env python3 """Point Navop at a local `gpui-pre` snapshot instead of the crates.io one. `gpui-pre-*` is a crates.io republication of Zed's gpui crates. gpui-component cuts a fresh snapshot every week with its own `script/bump-gpui.ts`, which copies the crates out of a Zed checkout, renames them (`gpui` -> `gpui-pre`, `util` -> `gpui-pre-util`, ...) and rewrites them into a standalone workspace. Navop needs the dynamic-texture API (`DynamicTexture`, `Window::update_dynamic_texture`, ...) which only exists on Zed's `dynamic-texture` branch, so this script runs that same pipeline against a local Zed checkout and then rewrites the generated `[patch.crates-io]` block in Navop's root `Cargo.toml` to point at the staged crates. Why a path patch rather than a git patch: the staged snapshots carry the `gpui-pre-*` package names and a version that satisfies Navop's `^0.3.1` requirement, so `[patch.crates-io]` accepts them. A Zed checkout on its own declares `gpui`/`util`/... at Zed's own versions and would never match. The staged workspace has to live outside every Cargo workspace. Cargo resolves `workspace = true` against the nearest ancestor manifest that owns the package as a member, and a snapshot left under `gpui-component/target/` gets attributed to gpui-component's workspace, which has no `accesskit`/`gpui_pre_*` entries to inherit from. The pipeline's own verification hits the same problem and works around it the same way: copy the staging somewhere neutral before pointing a manifest at it. `/../.gpui-pre/workspace` is that place — it sits next to the two checkouts, inside no workspace and no repository. Usage: script/patch-local-gpui-pre.py [options] Options: --zed PATH Zed checkout to snapshot (default: the `zed` checkout next to the Navop and gpui-component checkouts) --component PATH gpui-component checkout that owns the staging pipeline (default: the `gpui-component` checkout next to Navop) --patch-dir PATH Where to put the copy the patch points at (default: `/../.gpui-pre`) --version VERSION Version to stamp on the staged crates; must satisfy the `gpui-pre` requirement in Cargo.toml (default: 0.3.99) --no-stage Reuse the workspace already staged, only refresh the copy and the patch block --update-lock Run `cargo update` for the patched crates so Cargo.lock moves onto the snapshot (without it, Cargo keeps the published version and ignores the patch) --remove Delete the patch block and stop -h, --help Show this help Run it again whenever the local Zed branch changes: the patch paths stay the same, only the staged sources are rebuilt. """ from __future__ import annotations import argparse import json import os import re import shutil import subprocess import sys from pathlib import Path BEGIN_MARKER = "# >>> local gpui-pre patch (generated by script/patch-local-gpui-pre.py) >>>" END_MARKER = "# <<< local gpui-pre patch <<<" DEFAULT_VERSION = "0.3.99" PUBLISH_PREFIX = "gpui-pre" class Error(Exception): """A user-facing failure, printed without a traceback.""" def run(cmd: list[str], cwd: Path | None = None) -> None: print(f"$ {' '.join(cmd)}") result = subprocess.run(cmd, cwd=cwd) if result.returncode != 0: raise Error(f"command failed with exit code {result.returncode}: {' '.join(cmd)}") def run_capture(cmd: list[str], cwd: Path | None = None) -> str: result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) if result.returncode != 0: raise Error(f"command failed with exit code {result.returncode}: {' '.join(cmd)}") return result.stdout def sibling(root: Path, name: str, marker: str, flag: str) -> Path: """Find a checkout next to the Navop root, at any of the nesting depths. Navop is normally checked out as `/navop`, but the gpui-kit migration lives in `/navop/.worktrees/gpui-kit`, one level deeper, so the sibling is two, three or four levels up. """ for depth in range(1, 5): candidate = root for _ in range(depth): candidate = candidate.parent candidate = candidate / name if (candidate / marker).exists(): return candidate.resolve() raise Error( f"could not find a {name} checkout next to {root}; pass {flag} PATH" ) def find_bun() -> str: """Locate the Bun runtime that runs the staging pipeline.""" found = shutil.which("bun") if found: return found # A `npm install bun` under $HOME is a common fallback; it installs a shim # at ~/node_modules/.bin/bun. fallback = Path.home() / "node_modules" / ".bin" / "bun" if fallback.is_file(): return str(fallback) raise Error( "bun is not installed; install it with `npm install -g bun` (or " "`brew install oven-sh/bun/bun`) and re-run" ) def stage(component: Path, zed: Path, version: str) -> Path: """Run gpui-component's snapshot pipeline into its `target/gpui-pre`.""" script = component / "script" / "bump-gpui.ts" if not script.is_file(): raise Error(f"{script} not found; is {component} a gpui-component checkout?") if not (zed / "Cargo.toml").is_file(): raise Error(f"{zed} does not look like a Zed checkout (no Cargo.toml)") run( [ find_bun(), str(script), version, "--zed", str(zed), "--stage-only", ], cwd=component, ) workspace = component / "target" / "gpui-pre" / "workspace" if not (workspace / "Cargo.toml").is_file(): raise Error(f"the pipeline did not write {workspace}") return workspace def mirror(staged: Path, patch_dir: Path) -> Path: """Copy the staged workspace somewhere that belongs to no workspace. The staging under `gpui-component/target/` cannot be patched directly: Cargo would resolve its `workspace = true` keys against gpui-component's workspace, which knows nothing about `accesskit` or the `gpui-pre-*` packages themselves. A copy outside every checkout resolves against the staged root, as intended. `target/` is left behind — it is build output, not part of the snapshot. """ destination = patch_dir / "workspace" if destination.exists(): shutil.rmtree(destination) shutil.copytree(staged, destination, ignore=shutil.ignore_patterns("target")) return destination def staged_crates(workspace: Path) -> list[tuple[str, Path]]: """Read the staged workspace's dependency table for published name -> dir. The pipeline lists every republished crate as ` = { path = "...", package = "gpui-pre-...", version = "=..." }`, which is exactly the mapping the patch needs. """ manifest = (workspace / "Cargo.toml").read_text() section = re.search( r"^\[workspace\.dependencies\]\s*$(.*?)(?=^\[|\Z)", manifest, re.MULTILINE | re.DOTALL, ) if section is None: raise Error(f"{workspace / 'Cargo.toml'} has no [workspace.dependencies]") crates: list[tuple[str, Path]] = [] for line in section.group(1).splitlines(): if re.match(r"^\s*[\w-]+\s*=\s*\{", line) is None: continue package = re.search(r'\bpackage\s*=\s*"([^"]+)"', line) path = re.search(r'\bpath\s*=\s*"([^"]+)"', line) if package is None or path is None: continue name = package.group(1) if not name.startswith(PUBLISH_PREFIX): continue crates.append((name, (workspace / path.group(1)).resolve())) if not crates: raise Error(f"no {PUBLISH_PREFIX}-* crates found in {workspace / 'Cargo.toml'}") return sorted(set(crates)) def patch_block(manifest_dir: Path, crates: list[tuple[str, Path]]) -> str: width = max(len(name) for name, _ in crates) lines = [ BEGIN_MARKER, "#", "# LOCAL ONLY: the published gpui-pre-* snapshot predates Zed's", "# dynamic-texture API. This redirects every gpui-pre-* crate at the local", "# Zed checkout that has it. Re-run script/patch-local-gpui-pre.py after", "# changing that branch; `--remove` deletes the block.", "[patch.crates-io]", ] lines += [ f'{name.ljust(width)} = {{ path = "{os.path.relpath(path, manifest_dir)}" }}' for name, path in crates ] lines.append(END_MARKER) return "\n".join(lines) + "\n" def rewrite_manifest(root: Path, block: str | None) -> None: manifest = root / "Cargo.toml" text = manifest.read_text() pattern = re.compile( rf"^{re.escape(BEGIN_MARKER)}$.*?^{re.escape(END_MARKER)}\n?", re.MULTILINE | re.DOTALL, ) if block is None: if pattern.search(text) is None: print(f"{manifest} has no local gpui-pre patch; nothing to remove") return manifest.write_text(pattern.sub("", text).rstrip() + "\n") print(f"removed the local gpui-pre patch from {manifest}") return if pattern.search(text) is not None: manifest.write_text(pattern.sub(block, text)) else: manifest.write_text(text.rstrip() + "\n\n" + block) print(f"wrote {len(block.splitlines()) - 6} patch entries to {manifest}") def graph_crates( root: Path, crates: list[tuple[str, Path]] ) -> tuple[list[str], list[str]]: """Split the patched crates into those Navop resolves and those it does not. `cargo update -p` fails on a package that is not in the graph, and Cargo reports a patch that resolved to nothing as unused, so both lists are worth knowing. `cargo metadata` is the only authoritative answer: Cargo.lock keeps stale entries for packages the graph has dropped. """ metadata = json.loads( run_capture(["cargo", "metadata", "--format-version", "1"], cwd=root) ) resolved = {package["name"] for package in metadata["packages"]} used = [name for name, _ in crates if name in resolved] unused = [name for name, _ in crates if name not in resolved] return used, unused def main(argv: list[str]) -> int: parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("--zed") parser.add_argument("--component") parser.add_argument("--patch-dir") parser.add_argument("--version", default=DEFAULT_VERSION) parser.add_argument("--no-stage", action="store_true") parser.add_argument("--update-lock", action="store_true") parser.add_argument("--remove", action="store_true") args = parser.parse_args(argv) root = Path(__file__).resolve().parent.parent if args.remove: rewrite_manifest(root, None) return 0 component = ( Path(args.component).expanduser().resolve() if args.component else sibling(root, "gpui-component", "script/bump-gpui.ts", "--component") ) zed = ( Path(args.zed).expanduser().resolve() if args.zed else sibling(root, "zed", "crates/gpui/Cargo.toml", "--zed") ) patch_dir = ( Path(args.patch_dir).expanduser().resolve() if args.patch_dir else component.parent / ".gpui-pre" ) staged = component / "target" / "gpui-pre" / "workspace" if not args.no_stage: staged = stage(component, zed, args.version) elif not (staged / "Cargo.toml").is_file(): raise Error(f"--no-stage was given but {staged} is empty") workspace = mirror(staged, patch_dir) crates = staged_crates(workspace) rewrite_manifest(root, patch_block(root, crates)) print(f"snapshot from {zed}") print(f"staged workspace: {staged}") print(f"patched workspace: {workspace}") print(f"{len(crates)} crates patched: {', '.join(name for name, _ in crates)}") # Cargo keeps a locked version when it still satisfies the requirement, so # a patch alone does not move Cargo.lock onto the snapshot. used, unused = graph_crates(root, crates) if unused: print( "not reached by Navop's graph, so Cargo will report them as unused " f"patches: {', '.join(unused)}" ) if args.update_lock: packages = [arg for name in used for arg in ("-p", name)] run(["cargo", "update", *packages], cwd=root) else: print("run this to move Cargo.lock onto the snapshot, then rebuild:") print(" cargo update " + " ".join(f"-p {name}" for name in used)) return 0 if __name__ == "__main__": try: sys.exit(main(sys.argv[1:])) except Error as error: print(f"error: {error}", file=sys.stderr) sys.exit(1)