Files
navop/script/patch-local-gpui-pre.py
user.email 6a1e7f6dd0 feat(remote_desktop_view): restore dynamic texture via a self-maintained gpui-pre fork
The dynamic-texture API only exists on our zed dynamic-texture branch, so
the published gpui-pre-* snapshot predates it. Instead of a path patch
that ties the build to one host, point the [patch.crates-io] block at a
git fork we own:

    feigeCode/gpui-pre, tag fork-0.3.99

The fork is cut from zed@cae01216cb with gpui-component's
script/bump-gpui.ts pipeline. Re-publish with
script/publish-gpui-pre-fork.py and move the patch with
script/migrate-to-git-fork.py; both live in script/.

With the API available again, revert the eda5371ae RenderImage fallback
in remote_desktop_view:

- surface.rs uses DynamicTexture directly; the backing framebuffer
  becomes the upload source for paint_dynamic_texture.
- render.rs paints dirty rects via update_dynamic_texture /
  paint_dynamic_texture instead of a whole-frame paint_image, and
  releases retired handles with drop_dynamic_texture.
- view.rs drops the placeholder handles; the contract tests assert the
  dynamic-texture path.

cargo check --workspace --locked: clean. cargo test -p
remote_desktop_view --lib: 241 passed.
2026-09-10 20:17:50 +08:00

328 lines
13 KiB
Python
Executable File

#!/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-component>/../.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-component>/../.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 `<workspace>/navop`, but the gpui-kit
migration lives in `<workspace>/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
`<alias> = { 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)