From cea38d7ce1f24c1adafd827911a28109b367569b Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Fri, 17 Jul 2026 01:35:18 +0530 Subject: [PATCH 1/2] feat(worktree): share sccache across worktrees when available (#133) Wires rustc-wrapper=sccache + SCCACHE_BASEDIRS into each worktree's isolated .cargo/config.toml (soft skip when sccache isn't on PATH), so registry-dep compiles still hit cache across sibling worktrees even though local-crate target dirs stay isolated. Documents the build-isolation setup and the remaining ambient-env gap in AGENTS.md. Agentflare-Agent: claude-code_2-1-216_agent Agentflare-Branch: HEAD --- AGENTS.md | 15 +++++++ crates/flare-git-core/src/worktree.rs | 57 +++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c2de4830..169cfa5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,3 +93,18 @@ ambient `CARGO_TARGET_DIR`. Never add "Generated with Claude Code" or "Co-Authored-By: Claude" signatures. Commit messages are the message only. + +## Build isolation (claim worktrees) + +Each claim worktree (`.worktrees/task//`) gets its own `.cargo/config.toml` +with a relative `target-dir`, so `cargo build`/`test` in one worktree can't +reuse another worktree's stale local-crate artifacts (item #133; cargo +#12516/#14053/#7740). If `sccache` is on `PATH`, the same config also sets it +as `rustc-wrapper` with `SCCACHE_BASEDIRS` pointed at the worktree's own path, +so registry-dependency compiles still share a cache across worktrees. + +**Known gap:** an ambient `CARGO_TARGET_DIR` environment variable always +overrides that config file (Cargo's CLI-flag > env-var > config-file +precedence) — no per-worktree config can outrank it. If you have +`CARGO_TARGET_DIR` set globally, either unset it or trust CI over local test +runs in a claim worktree (tracked in item #139). diff --git a/crates/flare-git-core/src/worktree.rs b/crates/flare-git-core/src/worktree.rs index 1f0077dd..d8d5bc59 100644 --- a/crates/flare-git-core/src/worktree.rs +++ b/crates/flare-git-core/src/worktree.rs @@ -143,8 +143,14 @@ fn warn_if_ambient_target_dir() { /// Local workspace crates must NOT be shared across worktrees (silent /// contamination); registry deps are safe but are better served by a shared /// sccache. A relative `target-dir = "target"` resolves per-checkout, giving -/// each worktree its own isolated cache. Soft-fails (eprintln) — never blocks -/// a claim. +/// each worktree its own isolated cache. When `sccache` is on `PATH`, also +/// wires it up as the `rustc-wrapper` with `SCCACHE_BASEDIRS` set to this +/// worktree's own absolute path — sccache hashes absolute source paths into +/// its cache key by default, so without stripping that prefix, identical +/// dependency source in a sibling worktree would never hit +/// (mozilla/sccache#196; a `--remap-path-prefix` rustflag looks tempting but +/// itself varies per worktree and defeats the cache key instead). Soft-fails +/// (eprintln) — never blocks a claim. fn isolate_worktree_target_dir(worktree_path: &Path) { let cargo_dir = worktree_path.join(".cargo"); let _ = std::fs::create_dir_all(&cargo_dir); @@ -152,10 +158,16 @@ fn isolate_worktree_target_dir(worktree_path: &Path) { if config_path.exists() { return; // don't clobber an intentional worktree-local override } - let content = "[build]\n# Isolated per worktree (see item #133). Registry deps are\n\ + let mut content = "[build]\n# Isolated per worktree (see item #133). Registry deps are\n\ # better shared via sccache (RUSTC_WRAPPER + SCCACHE_BASEDIRS),\n\ # not a shared CARGO_TARGET_DIR, which leaks artifacts across worktrees.\n\ - target-dir = \"target\"\n"; + target-dir = \"target\"\n" + .to_string(); + if sccache_available() { + content.push_str("rustc-wrapper = \"sccache\"\n\n[env]\nSCCACHE_BASEDIRS = '"); + content.push_str(&worktree_path.to_string_lossy()); + content.push_str("'\n"); + } if let Err(e) = std::fs::write(&config_path, content) { eprintln!( "worktree: could not write isolated .cargo/config.toml for {}: {e}", @@ -164,6 +176,17 @@ fn isolate_worktree_target_dir(worktree_path: &Path) { } } +/// True when the `sccache` binary is reachable on `PATH`. +fn sccache_available() -> bool { + Command::new("sccache") + .arg("--version") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + /// Creates an isolated git worktree for `item` against `target_branch`. /// /// Deliberately takes an already-resolved `target_branch` instead of a @@ -538,6 +561,32 @@ mod tests { ); } + #[test] + fn isolate_worktree_target_dir_wires_sccache_when_available() { + let tmp = TempDir::new().unwrap(); + let wt = tmp.path().join(".worktrees").join("task").join("1"); + std::fs::create_dir_all(&wt).unwrap(); + isolate_worktree_target_dir(&wt); + let config = wt.join(".cargo").join("config.toml"); + let content = std::fs::read_to_string(&config).unwrap(); + if sccache_available() { + assert!( + content.contains("rustc-wrapper = \"sccache\""), + "expected sccache wired up as rustc-wrapper, got: {content}" + ); + let basedir_line = format!("SCCACHE_BASEDIRS = '{}'", wt.to_string_lossy()); + assert!( + content.contains(&basedir_line), + "expected SCCACHE_BASEDIRS to strip this worktree's own path, got: {content}" + ); + } else { + assert!( + !content.contains("rustc-wrapper") && !content.contains("SCCACHE_BASEDIRS"), + "must not reference sccache when it isn't on PATH, got: {content}" + ); + } + } + #[test] fn warn_if_ambient_target_dir_warns_when_set() { // Just asserts the function runs without panicking whether or not the From 55b36c22f8fa2e935325ba15bbed713e8876ac2c Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 21 Jul 2026 23:21:17 +0530 Subject: [PATCH 2/2] fix(worktree): escape SCCACHE_BASEDIRS as a TOML basic string; dedupe AGENTS.md section TOML literal strings ('...') can't escape a single quote, so a worktree path containing one (e.g. a Windows username like John's) produced invalid .cargo/config.toml. Switch to a basic string with backslashes and double quotes escaped. Also merges the new sccache doc sentence into the existing 'Cargo target-dir isolation' AGENTS.md section instead of duplicating it. Agentflare-Agent: claude-code_2-1-216_agent Agentflare-Branch: task/133 Agentflare-Item: 133 --- AGENTS.md | 17 +---------------- crates/flare-git-core/src/worktree.rs | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 169cfa5d..df9bb84f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,24 +87,9 @@ shadow the isolation, and CI's `target-dir-guard` job (.github/workflows/ci.yml) fails the build outright if the var is set project-wide. The residual gap is a bare shell opened inside a worktree without going through `agentflare run` — `cargo` there will still honor an -ambient `CARGO_TARGET_DIR`. +ambient `CARGO_TARGET_DIR`. If `sccache` is on `PATH`, the isolated config also sets it as `rustc-wrapper` with `SCCACHE_BASEDIRS` pointed at the worktree's own path, so registry-dependency compiles still share a cache across worktrees. ## Git Never add "Generated with Claude Code" or "Co-Authored-By: Claude" signatures. Commit messages are the message only. - -## Build isolation (claim worktrees) - -Each claim worktree (`.worktrees/task//`) gets its own `.cargo/config.toml` -with a relative `target-dir`, so `cargo build`/`test` in one worktree can't -reuse another worktree's stale local-crate artifacts (item #133; cargo -#12516/#14053/#7740). If `sccache` is on `PATH`, the same config also sets it -as `rustc-wrapper` with `SCCACHE_BASEDIRS` pointed at the worktree's own path, -so registry-dependency compiles still share a cache across worktrees. - -**Known gap:** an ambient `CARGO_TARGET_DIR` environment variable always -overrides that config file (Cargo's CLI-flag > env-var > config-file -precedence) — no per-worktree config can outrank it. If you have -`CARGO_TARGET_DIR` set globally, either unset it or trust CI over local test -runs in a claim worktree (tracked in item #139). diff --git a/crates/flare-git-core/src/worktree.rs b/crates/flare-git-core/src/worktree.rs index d8d5bc59..4751433f 100644 --- a/crates/flare-git-core/src/worktree.rs +++ b/crates/flare-git-core/src/worktree.rs @@ -164,9 +164,17 @@ fn isolate_worktree_target_dir(worktree_path: &Path) { target-dir = \"target\"\n" .to_string(); if sccache_available() { - content.push_str("rustc-wrapper = \"sccache\"\n\n[env]\nSCCACHE_BASEDIRS = '"); - content.push_str(&worktree_path.to_string_lossy()); - content.push_str("'\n"); + // TOML literal strings ('...') can't escape a single quote, so a + // worktree path containing one (e.g. "C:\Users\John's PC\repo") + // would produce invalid TOML. Use a basic string instead, with + // backslashes and double quotes escaped. + let escaped_path = worktree_path + .to_string_lossy() + .replace('\\', "\\\\") + .replace('"', "\\\""); + content.push_str(&format!( + "rustc-wrapper = \"sccache\"\n\n[env]\nSCCACHE_BASEDIRS = \"{escaped_path}\"\n" + )); } if let Err(e) = std::fs::write(&config_path, content) { eprintln!( @@ -574,7 +582,11 @@ mod tests { content.contains("rustc-wrapper = \"sccache\""), "expected sccache wired up as rustc-wrapper, got: {content}" ); - let basedir_line = format!("SCCACHE_BASEDIRS = '{}'", wt.to_string_lossy()); + let escaped = wt + .to_string_lossy() + .replace('\\', "\\\\") + .replace('"', "\\\""); + let basedir_line = format!("SCCACHE_BASEDIRS = \"{escaped}\""); assert!( content.contains(&basedir_line), "expected SCCACHE_BASEDIRS to strip this worktree's own path, got: {content}"