Repository navigation
Closed
Description
Goal
Replace fbuild's external managed-zccache binary (zccache wrap … + zccache start + zccache fp …) with an in-process ZccacheService running inside
fbuild-daemon, mirroring the embedded-backend model that soldr landed in
forge-ci/soldr#977.
Current state
crates/fbuild-build/src/managed_zccache.rspinsMANAGED_ZCCACHE_VERSION
= 1.12.9 and downloads three binaries (zccache,zccache-daemon,
zccache-fp) into~/.fbuild/<mode>/bin/zccache-<ver>/.crates/fbuild-build/src/zccache.rsresolves that binary (FBUILD_NO_ZCCACHE
→FBUILD_ZCCACHE_BIN→ managed → env PATH), then per compile:- prepends
[\"zccache\", \"wrap\", …]viawrap_args, - launches a separate
zccache startdaemon (ensure_running,
intentionally detached per Daemon teardown hardening: process containment + socket lingering + console events #32 so it outlives the fbuild daemon), - shells out to
zccache fp check/zccache fp mark-successfor
workspace fingerprints.
- prepends
- Roughly half of
zccache.rs(~300 lines) exists to keep wrapper-process
cache keys stable: workspace-relative cwd derivation
(compile_cwd_from_output),--sysroot/-Irewriting
(normalize_flags_for_compile_cwd), Windows UNC\?\stripping. All of
it is only necessary because the cache lives behind a child process.
Why migrate (mirroring soldr's reasoning)
- One tokio runtime. Embedded service shares
fbuild-daemon's runtime,
so a single tokio-console attach spans build orchestration and the cache
layer. Today they are two independent processes with separate runtimes. - No per-compile exec + IPC overhead.
compile_many.rscurrently spawns
one wrapper process per TU; embedded dispatch is an in-process function
call. - Shrinks
zccache.rs. Path/cwd/UNC normalization is a workaround for
wrapper-process semantics. Embedded API takes paths directly. - No managed-binary download to maintain. Removes the GitHub release
dependency, the lockstep version bumps inpyproject.toml/
MANAGED_ZCCACHE_VERSION/ the per-platform asset matrix, and the
~/.fbuild/<mode>/bin/zccache-<ver>/install dance. - Removes the intentionally-detached daemon lifecycle. No need for
zccache startto outlivefbuild-daemon— the cache is the daemon.
Soldr proved the model: CompileBackend::{Wrapped, Embedded} on daemon
state, runtime opt-in via SOLDR_ZCCACHE_EMBEDDED=1, wrapper path kept as
fallback. See soldr commit
41d4753 (forge-ci/soldr#977).
Proposed approach (phased)
Phase 1 — Embedded backend prep (opt-in, default off)
- Add optional
embeddedCargo feature onfbuild-buildpulling in the
zccachelibrary as agit rev = \"…\"dep (start with a pinned rev; only
graduate to a_vender/zccachesubmodule if iteration friction shows up —
see "vendoring" below). - Add
CompileBackend::{Wrapped, Embedded}enum, held on a daemon state
struct infbuild-daemon.fbuild-buildaccesses it through a handle in
the same way orchestrators access other daemon-owned state today. - Runtime opt-in via
FBUILD_ZCCACHE_EMBEDDED=1. Wrapper path stays
default — zero behavioral change without the flag. - New
crates/fbuild-daemon/src/zccache_embedded.rswrapsZccacheService
with fbuild's identity defaults +~/.fbuild/<mode>/cache/root.
Phase 2 — Route per-compile dispatch through embedded
compile_many.rs/compile_exec.rs(or equivalent) gain a path that
callsZccacheService::compile(...)directly when
CompileBackend::Embeddedis active. Wrapped path untouched.- Verify cache-key compatibility: identical artifacts and hit rate between
wrapped and embedded on the same workspace, or document and justify any
divergence.
Phase 3 — Embedded fingerprint API
- Migrate
check_fingerprint/mark_fingerprint_successoff the
zccache fpCLI to the embedded equivalent. If upstream doesn't expose
it as a library API yet, file a zccache-side issue.
Phase 4 — Retire wrapper
- Once embedded mode is the default for one release cycle without
regressions, deletemanaged_zccache.rs, the wrapper code path in
zccache.rs, and the path-normalization helpers that only existed to
serve wrapper cache keys. - Drop the related env vars (
FBUILD_ZCCACHE_BIN,
FBUILD_ZCCACHE_EMBEDDED) once the wrapper path is gone.
Open design questions
- Where in the dep graph does the embedded service live? Per the
monocrate policy, no new crate. Two viable spots: (a) a module under
fbuild-daemon(matches soldr — long-lived daemon owns the service),
or (b) a module underfbuild-buildbehind the feature flag. Soldr put
it undersoldr-cli/src/daemon/; in fbuild that maps to
fbuild-daemon/src/zccache_embedded.rs, withfbuild-buildgetting a
handle through the daemon state struct it already consumes. - Vendoring. Soldr learned the hard way (
f176c31→c2014cc→
2f14453) that flat-vendoring zccache into_vender/zccache/is wrong;
the working model is a git submodule for fast iteration or a plain
git-pinned Cargo dep when no upstream changes are needed. Recommend
starting at the plain git-pin and only adopting the submodule if a
cross-repo change forces it. - Cache-key compatibility. The wrapper-mode helpers
(compile_cwd_from_output,normalize_flags_for_compile_cwd, UNC
stripping) normalize keys to be workspace-relative. Embedded mode must
preserve that normalization (either insideZccacheServiceor as a
preprocessing step in the fbuild call site) or hot caches built with the
wrapper will miss after switching. - Concurrency.
compile_many.rsparallelizes compiles by spawning N
wrapper processes today. Embedded must support parallel in-process
compiles without forcing a single-mutex bottleneck — confirm
ZccacheServiceisSyncand the appropriate handle pattern. fbuild-cliboundary. The CLI is a thin HTTP client and must not
depend onfbuild-daemon. The embedded backend lives entirely on the
daemon side; the CLI sees no API change.
Test plan / acceptance
- Default builds (no flag) continue to use the wrapper; all existing
tests inzccache.rsstill pass. - New integration test: with
FBUILD_ZCCACHE_EMBEDDED=1, build
tests/platform/uno, verify a second build hits cache, and verify
via process snapshot that nozccache.exewas spawned. - Wrapped vs embedded produce identical object files on
tests/platform/uno+tests/platform/esp32dev(or document the
diff with rationale). - Cold + warm timing comparison published in the PR — quantify the
per-TU wrapper-spawn overhead this removes. -
soldr cargo clippy --workspace --all-targets -- -D warningsclean
with and without theembeddedfeature.
References
- soldr#977 — embedded backend prep, the
CompileBackend::{Wrapped, Embedded}enum,SOLDR_ZCCACHE_EMBEDDED=1runtime opt-in - soldr commits
f176c31/c2014cc/2f14453— vendoring strategy
evolution; flat-vendor is the anti-pattern, submodule + path-dep is the
destination crates/fbuild-build/src/zccache.rs— current wrapper-mode call sitecrates/fbuild-build/src/managed_zccache.rs— current managed-binary
download- Daemon teardown hardening: process containment + socket lingering + console events #32 — original rationale for the intentionally-detached
zccache start(becomes moot under embedded)
Metadata
Metadata
Assignees
Labels
No labels
Type
Projects
- StatusShow more project fieldsTriage
Activity
zackees commentedon Jun 28, 2026
Sub-issue tracking
Phases broken out for sequential delivery via
/clud-fix. Each phase shipsas its own PR. Phase N+1 starts once Phase N is merged to
main.FBUILD_ZCCACHE_EMBEDDEDopt-inZccacheServicezccache fpshelloutsThis issue auto-closes once all four sub-issues close.
zackees commentedon Jun 28, 2026
Adding prerequisite sub-issue:
crate_guard.pyrejects Cargo.toml writes inside.claude/worktrees/(blocks /clud-pr)Sequencing: ship #794 first (small) so subsequent Phase PRs don't need Bash-heredoc workarounds.
zackees commentedon Jun 28, 2026
[meta #789] sub #794 → CLOSED (validated by ci/hooks/test_crate_guard.py — 10 cases including worktree-path-strip; merged in #795 at 20:25Z). Resuming Phase 1 (#790).
feat(zccache): Phase 2-4 — per-compile dispatch + embedded fingerprin…
zackees commentedon Jun 28, 2026
Per maintainer direction, Phases 1–4 collapsed into a single omnibus PR (#798) rather than four stacked PRs:
All four sub-issues will auto-close when #798 merges. Wrapper-mode code retained as the safety-net fallback; code deletion + env-var removal tracked as a follow-up after one release cycle of embedded-default validation.
feat(zccache): embedded backend prep behind FBUILD_ZCCACHE_EMBEDDED (#…
feat(zccache): Phase 2-4 — per-compile dispatch + embedded fingerprin…
zackees commentedon Jun 28, 2026
Closed by #798. All four phases of the zccache embedded migration landed in a single omnibus PR per maintainer direction:
Net behavior: every compile and every fingerprint check dispatches through the in-process ZccacheService running on fbuild-daemon's tokio runtime; no per-TU zccache.exe child process is spawned in the default config. Wrapper-mode code remains in the source tree as the safety-net fallback (FBUILD_ZCCACHE_EMBEDDED=0 forces it for debugging).
Phase 4 stage 2 (delete managed_zccache.rs, wrap_args, the wrapper arms in compile_source, the zccache fp shellouts, and the FBUILD_NO_ZCCACHE / FBUILD_ZCCACHE_BIN env vars) is a follow-up after one release cycle of validation on the embedded default. Will track in a fresh issue when we're ready to ship the cleanup.
chore: bump version to 2.3.12