cmdline: warn instead of recursing when invoked as a non-sccache name that is sccache itself#2754
cmdline: warn instead of recursing when invoked as a non-sccache name that is sccache itself#2754sylvestre wants to merge 1 commit into
Conversation
… that is sccache itself When sccache is run under a name other than `sccache`, it acts as a compiler masquerade and prepends `sccache` to argv. If that name resolves back to the sccache binary itself (no real compiler of that name on PATH), the prepend made sccache wrap itself, and the `<compiler> -vV` rustc-detection probe re-entered the same path under the same name -- recursing forever and spawning a server-side process per core (a fork bomb that just hangs). Seen when an sccache binary was installed at a path whose filename was not `sccache`. Detect that case and warn, running as a normal sccache invocation instead of prepending the name. Add an integration test that runs the binary under a non-sccache, non-PATH name and asserts it warns and behaves as sccache rather than hanging.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2754 +/- ##
==========================================
+ Coverage 74.18% 74.20% +0.02%
==========================================
Files 71 71
Lines 40491 40515 +24
==========================================
+ Hits 30037 30063 +26
+ Misses 10454 10452 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…13293) ## Why The compile cache (#13288) required `sccache` on `PATH` — an install step in every adopting CI job, undercutting its "zero extra infrastructure" premise. sccache is a Rust lib+bin crate, so turbo can link it and act as the compiler wrapper itself: enabling the flag becomes the entire setup. The blocker was sccache's only public entrypoint, which reads `env::args_os()` and dispatches on argv[0]'s file name — any other name triggers compiler-masquerade mode (and, when the name resolves back to itself, the fork-loop also reported in mozilla/sccache#2754). We carry a small patch on the existing `vercel/sccache` fork (branch `vercel/main-from-args`, based on the `v0.16.0` tag) adding `main_from_args`/`try_parse_from`: explicit args, no argv[0] heuristics, `SCCACHE_START_SERVER` respawn intact. The fork is public, so anonymous `cargo build` works for OSS contributors, and the rev is pinned in `Cargo.lock`. ## What - `sccache` git dependency (`default-features = false, features = ["webdav"]` — no cloud backends, just the protocol our proxy speaks). - Multicall dispatch in `crates/turborepo/src/main.rs` alongside the LSP/ctrl-c shims: invocations carrying `TURBO_SCCACHE_WRAPPER=1` with a compiler-shaped first argument (`rustc`/`clippy-driver`), or sccache's internal `SCCACHE_START_SERVER=1` respawn, route to `sccache::main_from_args`. - Injection now sets `RUSTC_WRAPPER=<current turbo binary>` + the marker env var instead of `"sccache"`; the `which sccache` PATH check is deleted. - Docs: flag docs and ARCHITECTURE.md no longer mention a PATH requirement. ## How - **Misroute safety**: dispatch requires the marker env (only present in Cargo task environments turbo itself decorated) *and* a compiler-shaped argv[1] — a stray `turbo run build` inside a marked environment stays a normal turbo run, and sccache can never be handed an arbitrary first argument to execute. - **Server respawn**: sccache's client spawns `current_exe()` with `SCCACHE_START_SERVER=1`; that's the turbo binary, whose dispatch routes it back into the embedded server. Verified end-to-end with the built binary: wrapper passthrough, background-server self-spawn, and a real `cargo build` miss→hit cycle through `RUSTC_WRAPPER=turbo`. - **Lockfile**: `byteorder` 1.4.3→1.5.0, `serde_json` 1.0.149→1.0.146 (sccache upstream caps `<1.0.147`), plus sccache's tree. `cargo-deny check licenses` passes (Apache-2.0 allowlisted). ## Why two local servers? A fair review question: the goal was sccache's invocation-hashing logic, yet a run spawns both sccache's background server and turbo's proxy. Each is load-bearing: - **sccache's server** is the price of the hashing brain being fast. Cargo invokes the wrapper hundreds of times per build, so each invocation must cost ~nothing; sccache's architecture amortizes config parsing, compiler detection, and storage connections into one resident process with disposable thin clients. It ships with the brain — removing it means reimplementing sccache's core, which is exactly what this PR avoids. - **The proxy is a credential boundary, not plumbing.** The alternative (patching the fork so the embedded server talks to the Remote Cache directly via `turborepo-api-client`) requires the server to construct credentials from its environment — putting the real team cache token in the env of every rustc invocation and every third-party `build.rs`, held by a daemon that outlives turbo. With the proxy, task processes only ever see a loopback URL plus a locally-scoped bearer token that's worthless once the proxy dies with the run; the real token never leaves turbo's own process. Both listeners are loopback-only. The configuration where this genuinely collapses to one process is a future native wrapper (compilation-unit caching inside turbo's daemon, no sccache at all); the trait surface and injection here don't care what the wrapper is, so that door stays open. ## Testing - Dispatch unit tests (respawn, wrapper shape, marker/compiler guards; Windows path case gated `cfg(windows)`). - Updated `compile_cache_env` test; `cargo lint`, fmt, and touched-crate suites green. - Manual: debug turbo as `RUSTC_WRAPPER` on a scratch crate — compile requests served, cache miss on cold build, hit after `cargo clean`. ## Follow-ups - Drop the now-unneeded `mozilla-actions/sccache-action` step from #13292 once this merges. - Measure release binary size delta from CI artifacts (spike suggests low single-digit MB after dep sharing).
| == env::current_exe().ok().and_then(|p| p.canonicalize().ok()) | ||
| { | ||
| warn!( | ||
| "sccache invoked as `{}`, which resolves to sccache itself; running as sccache (put a real `{}` on PATH to wrap it)", |
There was a problem hiding this comment.
This should use CARGO_PKG_NAME for the warning message, arguably.
| if PathBuf::from(&args[0]).canonicalize().ok() | ||
| == env::current_exe().ok().and_then(|p| p.canonicalize().ok()) |
There was a problem hiding this comment.
Nit: when both sides fail, we get None == None, which makes it true and emit the warning and do the masquerading, which may not represent reality because we really don't know what's up. It might be better to emit a different warning in that case.
| warn!( | ||
| "sccache invoked as `{}`, which resolves to sccache itself; running as sccache (put a real `{}` on PATH to wrap it)", | ||
| exe.display(), | ||
| exe_filename.to_string_lossy() |
There was a problem hiding this comment.
you should be able to use exe_filename.display()
|
|
||
| // If $name resolves back to sccache itself, masquerading would | ||
| // make it wrap itself and recurse forever (`<compiler> -vV`). Warn | ||
| // and run as plain sccache instead of prepending the name. |
There was a problem hiding this comment.
I'm not sure this is the appropriate behavior. Like the typical case would be calling cc -o foo.o -c foo.c where cc is sccache, and there is no other cc in the PATH. This would make that command equivalent to sccache -o foo.o -c foo.c and fail because -o and -c are not sccache flags, which is a weird error mode. It would be better to say cc couldn't be found, probably doing so in the Err case above rather than add a somewhat redundant check.
When sccache is run under a name other than
sccache, it acts as a compiler masquerade and prependssccacheto argv. If that name resolves back to the sccache binary itself (no real compiler of that name on PATH), the prepend made sccache wrap itself, and the<compiler> -vVrustc-detection probe re-entered the same path under the same name -- recursing forever and spawning a server-side process per core (a fork bomb that just hangs). Seen when an sccache binary was installed at a path whose filename was notsccache.Detect that case and warn, running as a normal sccache invocation instead of prepending the name. Add an integration test that runs the binary under a non-sccache, non-PATH name and asserts it warns and behaves as sccache rather than hanging.