This workstation configures ONE Cargo target outside the checkouts for every worktree below the project directory. A build in any worktree therefore replaced the `qbz` another worktree had just built: a parallel session's release build in its own worktree replaced the binary under test in the main one. scripts/qt-target.py now decides where builds go. When the target Cargo resolves lies outside the worktree and nothing pins it (CARGO_TARGET_DIR or CARGO_BUILD_TARGET_DIR), the worktree builds in its own sibling directory, `<target>-worktrees/<worktree>`; rebuilding the same worktree still replaces its own binary. Cargo's default target inside the checkout (macOS, CI, Windows) and explicit overrides are untouched, and no machine path is committed. Two checkouts that share a directory name get a stable suffix instead of one target. qt-run.sh, the runtime gate and direct qt-cargo.py builds and tests all use it. qt-run.sh prints the target and the binary path, and re-points crates/target after each successful build, never over a real directory. The aarch64 scripts build into and install from the same target, and the qbzd acceptance script names the build command that lands its binary there. The first build of a profile in a new worktree target is seeded from the shared target with copy-on-write clones, skipping incremental caches: Cargo rewrites moved build-script paths through root-output (custom_build.rs, "rewrite paths if the target directory has been moved"), so unchanged crates are not compiled again and no data is duplicated. Without clone support nothing is copied and Cargo builds from scratch. The resolution and shortcut handling start from the uncommitted draft in the build-storage-layout worktree. Verified on the main worktree: seeded from the shared release target, Cargo found everything fresh in two seconds, and crates/target now points at the worktree's own target. 33 build-gate tests cover derivation, overrides, name collisions, linking, seeding and the runner end to end. cc
214 lines
8.6 KiB
Python
214 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Each worktree's own physical Cargo target, and its crates/target shortcut.
|
|
|
|
A host may put Cargo's target OUTSIDE the checkout, on a build disk shared by
|
|
every worktree below one project directory. Then every worktree compiled into
|
|
the SAME directory, and a build in any of them replaced the `qbz` binary another
|
|
worktree had just built and was running or smoke-testing (2026-09-14: a parallel
|
|
session's release build replaced the binary under test in the main worktree).
|
|
|
|
So when the target Cargo resolves lies outside the worktree, and nobody pinned it
|
|
explicitly with CARGO_TARGET_DIR or CARGO_BUILD_TARGET_DIR, the worktree builds
|
|
in its own sibling directory instead:
|
|
|
|
<configured target>-worktrees/<worktree directory name>
|
|
|
|
Building the same worktree again still replaces its own binary; other worktrees
|
|
keep theirs. A target inside the checkout (Cargo's default: macOS, CI, Windows)
|
|
and an explicit override are returned unchanged. No machine-specific path lives
|
|
here: Cargo configuration and the environment choose the storage.
|
|
|
|
The first build of a profile in a new worktree target is seeded from the shared
|
|
target with copy-on-write clones (`cp --reflink=always`), so the registry crates
|
|
that did not change are not compiled again and no data is duplicated; where the
|
|
filesystem cannot clone, nothing is copied and Cargo simply builds from scratch.
|
|
Never remove a real target directory to make room for a symlink.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
|
|
EXPLICIT_ENV = ("CARGO_TARGET_DIR", "CARGO_BUILD_TARGET_DIR")
|
|
MARKER = ".qbz-worktree"
|
|
SUFFIX = "-worktrees"
|
|
# Workspace crates recompile per worktree anyway (their metadata hash includes
|
|
# the checkout path), and incremental caches are only kept for those.
|
|
SEED_SKIP = {"incremental"}
|
|
|
|
|
|
def cargo_metadata(manifest=None, env=None):
|
|
command = ["cargo", "metadata", "--no-deps", "--format-version", "1"]
|
|
if manifest is not None:
|
|
command += ["--manifest-path", str(manifest)]
|
|
return json.loads(subprocess.check_output(command, text=True, env=env))
|
|
|
|
|
|
def worktree_root(workspace_root):
|
|
workspace_root = Path(workspace_root)
|
|
try:
|
|
top = subprocess.check_output(
|
|
["git", "-C", str(workspace_root), "rev-parse", "--show-toplevel"],
|
|
text=True, stderr=subprocess.DEVNULL).strip()
|
|
if top:
|
|
return Path(top).resolve()
|
|
except (OSError, subprocess.SubprocessError):
|
|
pass
|
|
# This repository keeps its Cargo workspace in crates/.
|
|
return workspace_root.resolve().parent
|
|
|
|
|
|
def is_inside(path, root):
|
|
try:
|
|
Path(path).relative_to(root)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def explicit(env):
|
|
return any(env.get(name) for name in EXPLICIT_ENV)
|
|
|
|
|
|
def worktree_target(configured, root, env):
|
|
"""The target this worktree builds in (see the module docstring)."""
|
|
configured = Path(configured).resolve()
|
|
root = Path(root).resolve()
|
|
if explicit(env) or is_inside(configured, root):
|
|
return configured
|
|
name = re.sub(r"[^A-Za-z0-9._-]+", "-", root.name).strip("-") or "worktree"
|
|
candidate = configured.parent / f"{configured.name}{SUFFIX}" / name
|
|
try:
|
|
claimed = (candidate / MARKER).read_text().strip()
|
|
except OSError:
|
|
claimed = ""
|
|
if claimed and claimed != str(root):
|
|
# Two checkouts with the same directory name: the second one gets a
|
|
# stable suffix instead of sharing (and overwriting) the first.
|
|
digest = hashlib.sha1(str(root).encode()).hexdigest()[:8]
|
|
candidate = candidate.with_name(f"{name}-{digest}")
|
|
return candidate
|
|
|
|
|
|
def shared_target(manifest=None, env=None):
|
|
"""Cargo's configured target with any explicit override removed."""
|
|
env = dict(os.environ if env is None else env)
|
|
for name in EXPLICIT_ENV:
|
|
env.pop(name, None)
|
|
return Path(cargo_metadata(manifest, env)["target_directory"]).resolve()
|
|
|
|
|
|
def resolve_target(manifest=None, env=None):
|
|
env = os.environ if env is None else env
|
|
metadata = cargo_metadata(manifest, dict(env))
|
|
root = worktree_root(metadata["workspace_root"])
|
|
return worktree_target(metadata["target_directory"], root, env)
|
|
|
|
|
|
def is_worktree_target(target, shared):
|
|
target = Path(target).resolve()
|
|
shared = Path(shared).resolve()
|
|
return target.parent == shared.parent / f"{shared.name}{SUFFIX}"
|
|
|
|
|
|
def link_target(manifest, target, root=None):
|
|
target = Path(target).resolve()
|
|
alias = Path(manifest).resolve().parent / "target"
|
|
if not target.is_dir():
|
|
raise RuntimeError(f"build target does not exist: {target}")
|
|
if root is not None and target.parent.name.endswith(SUFFIX):
|
|
# Claim this worktree target for its checkout (see worktree_target).
|
|
(target / MARKER).write_text(f"{Path(root).resolve()}\n")
|
|
if alias.resolve() == target:
|
|
return
|
|
if alias.exists() and not alias.is_symlink():
|
|
raise RuntimeError(f"preserving existing target directory: {alias}; migrate it before linking")
|
|
if os.name == "nt":
|
|
# A normal Windows Cargo target already uses alias. External targets
|
|
# must remain usable without requiring symlink privileges in CI.
|
|
print(f"[qt-target] external target: {target} (shortcut not created on Windows)", file=sys.stderr)
|
|
return
|
|
temporary = alias.with_name(f".target-link-{uuid.uuid4().hex}")
|
|
try:
|
|
temporary.symlink_to(target, target_is_directory=True)
|
|
temporary.replace(alias)
|
|
finally:
|
|
temporary.unlink(missing_ok=True)
|
|
|
|
|
|
def seed_target(target, shared, profiles, clone=None):
|
|
"""Clone each missing profile directory of `shared` into `target`.
|
|
|
|
Returns the profiles seeded. A profile already present in `target`, absent
|
|
from `shared`, or that the filesystem cannot clone is left alone.
|
|
"""
|
|
target = Path(target)
|
|
shared = Path(shared)
|
|
clone = clone or reflink_clone
|
|
seeded = []
|
|
if target.resolve() == shared.resolve() or not is_worktree_target(target, shared):
|
|
return seeded
|
|
for profile in profiles:
|
|
source = shared / profile
|
|
destination = target / profile
|
|
if destination.exists() or not source.is_dir():
|
|
continue
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
staging = target / f".seed-{profile}-{uuid.uuid4().hex}"
|
|
staging.mkdir()
|
|
try:
|
|
entries = sorted(entry for entry in source.iterdir() if entry.name not in SEED_SKIP)
|
|
if entries:
|
|
clone(entries, staging)
|
|
# Appears whole or not at all: an interrupted clone never looks
|
|
# like a finished target to Cargo.
|
|
staging.rename(destination)
|
|
seeded.append(profile)
|
|
except (OSError, subprocess.SubprocessError) as error:
|
|
print(f"[qt-target] not seeding {profile} ({error}); Cargo builds it from scratch", file=sys.stderr)
|
|
finally:
|
|
if staging.exists():
|
|
shutil.rmtree(staging, ignore_errors=True)
|
|
return seeded
|
|
|
|
|
|
def reflink_clone(entries, destination):
|
|
if not sys.platform.startswith("linux"):
|
|
raise OSError("copy-on-write clones are only used with GNU cp")
|
|
subprocess.run(["cp", "-a", "--reflink=always", *map(str, entries), str(destination)],
|
|
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("operation", choices=["resolve", "link", "seed"])
|
|
parser.add_argument("profiles", nargs="*", help="seed: profile directories, e.g. release debug")
|
|
parser.add_argument("--manifest-path", default="crates/Cargo.toml", type=Path)
|
|
parser.add_argument("--target-dir", type=Path)
|
|
args = parser.parse_args()
|
|
if args.operation == "resolve":
|
|
print(args.target_dir.resolve() if args.target_dir else resolve_target(args.manifest_path))
|
|
return
|
|
target = args.target_dir.resolve() if args.target_dir else resolve_target(args.manifest_path)
|
|
if args.operation == "link":
|
|
root = worktree_root(Path(args.manifest_path).resolve().parent)
|
|
link_target(args.manifest_path, target, root)
|
|
return
|
|
shared = shared_target(args.manifest_path)
|
|
for profile in seed_target(target, shared, args.profiles or ["release"]):
|
|
print(f"[qt-target] seeded {profile} from {shared / profile}", file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except (OSError, RuntimeError, subprocess.SubprocessError, KeyError, ValueError) as error:
|
|
print(f"[qt-target] {error}", file=sys.stderr)
|
|
sys.exit(1)
|