Skip to content

cmdline: warn instead of recursing when invoked as a non-sccache name that is sccache itself#2754

Open
sylvestre wants to merge 1 commit into
mozilla:mainfrom
sylvestre:self
Open

cmdline: warn instead of recursing when invoked as a non-sccache name that is sccache itself#2754
sylvestre wants to merge 1 commit into
mozilla:mainfrom
sylvestre:self

Conversation

@sylvestre

Copy link
Copy Markdown
Collaborator

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.

… 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-commenter

codecov-commenter commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.20%. Comparing base (68cd330) to head (39d01f8).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sylvestre sylvestre requested a review from glandium July 1, 2026 19:25
anthonyshew added a commit to vercel/turborepo that referenced this pull request Jul 7, 2026
…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).
Comment thread src/cmdline.rs
== 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)",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use CARGO_PKG_NAME for the warning message, arguably.

Comment thread src/cmdline.rs
Comment on lines +238 to +239
if PathBuf::from(&args[0]).canonicalize().ok()
== env::current_exe().ok().and_then(|p| p.canonicalize().ok())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/cmdline.rs
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()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should be able to use exe_filename.display()

Comment thread src/cmdline.rs

// 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants