Skip to main content

stow_cli/
lib.rs

1//! Stow CLI: rustc-wrapper that intercepts every compilation unit, looks up a
2//! prebuilt artifact via the edge worker, and either injects the cached output
3//! into Cargo's target directory or falls through to a normal `rustc` build.
4//!
5//! Public binaries:
6//!
7//! * `stow-cli` — the canonical entrypoint installed on user machines.
8//! * `cargo-stow` — same binary exposed as a `cargo` subcommand.
9//! * `stow` — short alias.
10//!
11//! All three resolve to [`run`].
12
13// The wrapper's nested async serve chain overflows the default auto-trait
14// evaluation depth when rustc proves `Send` for `async_main`'s future.
15#![recursion_limit = "256"]
16
17mod admission;
18mod artifact_cache;
19mod budget;
20mod cache_policy;
21mod cargo_cmd;
22mod cc;
23mod circuit;
24mod cli_args;
25mod commands;
26mod config;
27mod fetch;
28mod graph_cache;
29mod inject;
30mod lockfile_graph_cache;
31mod prefetch;
32mod profile_guard;
33mod rustc_args;
34mod state_db;
35mod stats;
36mod verify;
37mod workspace_deps;
38use stow_shim as wrapper_shim;
39
40use std::collections::{BTreeMap, BTreeSet};
41use std::ffi::{OsStr, OsString};
42use std::io::{self, Write};
43use std::path::{Path, PathBuf};
44
45use async_process::Command;
46use clap::Parser;
47use stow_types::error::Context;
48use stow_types::identity::DependencyCompileKeyIdentity;
49use stow_types::public_cache::{
50    detect_registry_crate_version as shared_detect_registry_crate_version,
51    normalized_cache_profile, stable_c_metadata_for_compile_key, stable_registry_artifact_identity,
52};
53use tokio::io::AsyncWriteExt;
54use tracing_subscriber::EnvFilter;
55use tracing_subscriber::layer::SubscriberExt;
56use tracing_subscriber::util::SubscriberInitExt;
57
58use crate::artifact_cache::{
59    load_cached_bundle, load_cached_bundle_by_compile_key, load_semantic_cached_bundle,
60    prepare_local_cache, record_materialized_bundle_outputs,
61    record_materialized_local_build_outputs, remove_cached_bundle,
62    resolve_dependency_c_metadata_json,
63};
64use crate::cli_args::{Cli, Command as CliCommand, WrapperCommandArgs};
65use crate::config::StowConfig;
66use crate::fetch::FetchRequest;
67use stow_types::api::{BatchArtifactRequestEntry, DependencyGraphEntry};
68
69const STOW_EXPANDED_GRAPH_ENV: &str = "STOW_EXPANDED_GRAPH_JSON";
70pub(crate) const STOW_PREFETCH_ARTIFACTS_ENV: &str = "STOW_PREFETCH_ARTIFACTS_JSON";
71pub(crate) const STOW_ENABLE_SEMANTIC_FALLBACK_ENV: &str = "STOW_ENABLE_SEMANTIC_FALLBACK";
72const STOW_TRACE_WRAPPED_COMPILERS_ENV: &str = "STOW_TRACE_WRAPPED_COMPILERS";
73/// When set to a path, stow writes a Chrome-trace JSON to that file describing
74/// every instrumented span (`stow.startup`, `stow.project.context`,
75/// `stow.edge.graph.query`, `stow.wrapper.invoke`, ...). Open the file with
76/// <chrome://tracing> or perfetto.dev for a flame waterfall. Used to drive P1
77/// performance work — see plan P0.1.
78const STOW_TRACE_FILE_ENV: &str = "STOW_TRACE_FILE";
79
80/// Holds the tracing-chrome flush guard, if a Chrome trace was requested.
81///
82/// The guard must outlive `block_on` so the trace file is fully flushed.
83struct TracingGuard {
84    _chrome: Option<tracing_chrome::FlushGuard>,
85}
86
87/// Entry point for all three stow binaries: installs tracing when the
88/// invocation allows it, builds a tokio runtime sized to the invocation
89/// kind, and runs [`async_main`].
90///
91/// Wrapper invocations (`stow rustc`, `stow cc`) get a `current_thread`
92/// runtime — they run hundreds of times per build with at most one
93/// concurrent network task, so worker-pool spin-up is wasted overhead.
94/// User-facing commands that can fan out get the multi-threaded runtime.
95///
96/// # Errors
97///
98/// Returns an error when the tokio runtime cannot be built or when the
99/// selected subcommand fails.
100pub fn run() -> stow_types::error::Result<()> {
101    // `sigstore`'s `sigstore-trust-root` feature pulls `tough`, which depends
102    // on `rustls` with default features — that compiles in `aws_lc_rs`
103    // alongside the `ring` provider selected by `zenwave`, `sqlx`, and
104    // reqwest 0.12. `tough`'s rustls dep cannot be reconfigured, so rustls
105    // cannot auto-select a provider; install `ring` explicitly before any
106    // TLS client is built.
107    rustls::crypto::ring::default_provider()
108        .install_default()
109        .map_err(|_| stow_types::error::Error::msg("install ring CryptoProvider"))?;
110    let _tracing_guard = should_install_tracing().then(install_tracing);
111    let runtime = if is_wrapper_invocation() {
112        tokio::runtime::Builder::new_current_thread()
113            .enable_all()
114            .build()
115            .wrap_err("create tokio runtime for stow rustc wrapper")?
116    } else {
117        tokio::runtime::Builder::new_multi_thread()
118            .enable_all()
119            .build()
120            .wrap_err("create tokio runtime for stow cli")?
121    };
122    runtime.block_on(async_main())
123}
124
125/// The process arguments as the CLI parser sees them.
126///
127/// Cargo runs an external subcommand as `cargo-stow stow <args>`, repeating
128/// the subcommand name as `argv[1]`; that word is dropped so `cargo stow
129/// check` and `stow check` parse identically.
130fn process_args() -> Vec<OsString> {
131    strip_cargo_subcommand_word(std::env::args_os().collect())
132}
133
134fn strip_cargo_subcommand_word(mut args: Vec<OsString>) -> Vec<OsString> {
135    let invoked_as_cargo_subcommand = args
136        .first()
137        .and_then(|program| Path::new(program).file_stem())
138        .is_some_and(|stem| stem == "cargo-stow")
139        && args.get(1).is_some_and(|word| word == "stow");
140    if invoked_as_cargo_subcommand {
141        args.remove(1);
142    }
143    args
144}
145
146fn is_wrapper_invocation() -> bool {
147    matches!(
148        process_args().get(1).map(OsString::as_os_str),
149        Some(arg) if arg == "rustc" || arg == "cc"
150    )
151}
152
153fn should_install_tracing() -> bool {
154    let args = process_args();
155    should_install_tracing_for_args(
156        &args,
157        std::env::var_os("RUST_LOG").as_deref(),
158        std::env::var_os(STOW_TRACE_WRAPPED_COMPILERS_ENV),
159    )
160}
161
162fn should_install_tracing_for_args(
163    args: &[OsString],
164    rust_log: Option<&OsStr>,
165    trace_wrapped_compilers: Option<OsString>,
166) -> bool {
167    let is_wrapper_subcommand = matches!(
168        args.get(1).map(OsString::as_os_str),
169        Some(command) if command == "rustc" || command == "cc"
170    );
171    if is_wrapper_subcommand {
172        return trace_wrapped_compilers.is_some_and(|value| value != "0");
173    }
174    rust_log.is_some() || !is_wrapper_subcommand
175}
176
177#[tracing::instrument(name = "stow.startup", skip_all, fields(subcommand))]
178async fn async_main() -> stow_types::error::Result<()> {
179    let args = process_args();
180    let cli = parse_cli_or_exit(&args)?;
181    let span = tracing::Span::current();
182    span.record("subcommand", subcommand_name(&cli.command));
183    match cli.command {
184        CliCommand::Check(command) => cargo_cmd::run("check", command).await,
185        CliCommand::Build(command) => cargo_cmd::run("build", command).await,
186        CliCommand::Test(command) => cargo_cmd::run("test", command).await,
187        CliCommand::Predict(command) => cargo_cmd::predict(command).await,
188        CliCommand::Setup(args) => commands::setup_project(args).await,
189        CliCommand::Status => commands::status_project().await,
190        CliCommand::Clean => commands::clean_project().await,
191        CliCommand::CheckArtifact(command) => commands::check_artifact(command).await,
192        CliCommand::FetchArtifact(command) => commands::fetch_artifact(command).await,
193        CliCommand::Rustc(command) => run_rustc_wrapper(command).await,
194        CliCommand::Cc(command) => run_cc_wrapper(command).await,
195        CliCommand::PurgeCacheDir(command) => commands::purge_cache_dirs(command).await,
196    }
197}
198
199const fn subcommand_name(command: &CliCommand) -> &'static str {
200    match command {
201        CliCommand::Check(_) => "check",
202        CliCommand::Build(_) => "build",
203        CliCommand::Test(_) => "test",
204        CliCommand::Predict(_) => "predict",
205        CliCommand::Setup(_) => "setup",
206        CliCommand::Status => "status",
207        CliCommand::Clean => "clean",
208        CliCommand::CheckArtifact(_) => "check-artifact",
209        CliCommand::FetchArtifact(_) => "fetch-artifact",
210        CliCommand::Rustc(_) => "rustc",
211        CliCommand::Cc(_) => "cc",
212        CliCommand::PurgeCacheDir(_) => "purge-cache-dir",
213    }
214}
215
216async fn run_passthrough(
217    executable: &OsString,
218    wrapped_args: &[std::ffi::OsString],
219) -> stow_types::error::Result<()> {
220    let status = run_passthrough_status(executable, wrapped_args).await?;
221
222    std::process::exit(status.code().unwrap_or(1));
223}
224
225async fn run_passthrough_status(
226    executable: &OsString,
227    wrapped_args: &[std::ffi::OsString],
228) -> stow_types::error::Result<async_process::ExitStatus> {
229    Command::new(executable)
230        .args(wrapped_args)
231        .status()
232        .await
233        .wrap_err("failed to spawn wrapped compiler")
234}
235
236async fn run_rustc_passthrough(
237    executable: &OsString,
238    wrapped_args: &[std::ffi::OsString],
239    parsed: &rustc_args::ParsedRustcArgs,
240) -> stow_types::error::Result<()> {
241    let status = run_passthrough_status(executable, wrapped_args).await?;
242    if status.success() {
243        if let Ok(config) = StowConfig::load_local() {
244            match resolve_local_build_artifact(&config, executable, parsed).await {
245                Ok(Some(build)) => {
246                    log_nonfatal_result(
247                        "failed to materialize stable local build aliases after successful rustc build",
248                        inject::materialize_local_build_stable_aliases(parsed, &build.identity)
249                            .await,
250                    );
251                    log_nonfatal_result(
252                        "failed to record materialized stow output metadata after local rustc build",
253                        record_materialized_local_build_outputs(&config, parsed, &build.identity)
254                            .await,
255                    );
256                    if parsed.is_locally_cacheable() {
257                        log_nonfatal_result(
258                            "failed to store locally built artifact in the stow cache",
259                            artifact_cache::store_local_build_outputs(&config, parsed, &build)
260                                .await
261                                .map(|_| ()),
262                        );
263                    }
264                }
265                Ok(None) => {}
266                Err(error) => {
267                    tracing::warn!(
268                        error = %error,
269                        crate_name = %parsed.crate_name,
270                        "failed to resolve local artifact identity after successful rustc build"
271                    );
272                }
273            }
274        }
275        materialize_build_script_alias(parsed).await?;
276    }
277    std::process::exit(status.code().unwrap_or(1));
278}
279
280async fn materialize_build_script_alias(
281    parsed: &rustc_args::ParsedRustcArgs,
282) -> stow_types::error::Result<()> {
283    let Some(source_path) = parsed.output_binary_path() else {
284        return Ok(());
285    };
286    let Some(alias_path) = parsed.build_script_alias_path() else {
287        return Ok(());
288    };
289    if alias_path.exists() {
290        return Ok(());
291    }
292    if !source_path.exists() {
293        return Err(stow_types::stow_error!(
294            "build script output {} does not exist after successful rustc passthrough",
295            source_path.display()
296        ));
297    }
298
299    let source_for_copy = source_path.clone();
300    let alias_for_copy = alias_path.clone();
301    smol::unblock(move || {
302        reflink::reflink_or_copy(&source_for_copy, &alias_for_copy).wrap_err_with(|| {
303            format!(
304                "materialize cargo build script alias {} from {}",
305                alias_for_copy.display(),
306                source_for_copy.display()
307            )
308        })
309    })
310    .await?;
311
312    tracing::debug!(
313        source = %source_path.display(),
314        alias = %alias_path.display(),
315        "materialized cargo build script alias after rustc passthrough"
316    );
317    Ok(())
318}
319
320#[tracing::instrument(name = "stow.wrapper.invoke", skip_all, fields(crate_name, cache_hit))]
321async fn run_rustc_wrapper(command: WrapperCommandArgs) -> stow_types::error::Result<()> {
322    let rustc = &command.executable;
323    let parsed = match rustc_args::ParsedRustcArgs::parse(&command.wrapped_args) {
324        Ok(parsed) => parsed,
325        Err(error) if error.contains("missing --crate-name") => {
326            tracing::debug!(error = %error, "rustc probe invocation detected, bypassing cache");
327            return run_passthrough(rustc, &command.wrapped_args).await;
328        }
329        Err(error) => {
330            return Err(stow_types::stow_error!(
331                "parse rustc wrapper arguments: {error}"
332            ));
333        }
334    };
335
336    tracing::Span::current().record("crate_name", parsed.crate_name.as_str());
337    tracing::debug!(
338        crate_name = %parsed.crate_name,
339        crate_types = ?parsed.crate_types,
340        target = ?parsed.target,
341        c_metadata = ?parsed.c_metadata,
342        out_dir = ?parsed.out_dir,
343        proc_macro = parsed.is_proc_macro(),
344        output_rlib = ?parsed.output_rlib_path(),
345        output_rmeta = ?parsed.output_rmeta_path(),
346        cacheable = parsed.is_cacheable(),
347        "observed rustc wrapper invocation"
348    );
349
350    if !parsed.is_cacheable() {
351        return run_rustc_wrapper_local_only(rustc, &command.wrapped_args, &parsed).await;
352    }
353
354    if std::env::var_os("STOW_DISABLE_PUBLIC_CACHE").is_some() {
355        // The kill switch disables the *public* cache; a self-produced local
356        // entry is not public, so lookups still run against it.
357        tracing::debug!(
358            "public rust cache disabled for this cargo invocation, serving local lookups only"
359        );
360        return run_rustc_wrapper_local_only(rustc, &command.wrapped_args, &parsed).await;
361    }
362    let exact_public_cache_allowed = match cache_policy::public_cache_allowed(&parsed) {
363        Some(false) => {
364            tracing::debug!(
365                crate_name = %parsed.crate_name,
366                "public exact rust cache disabled by stow cache policy for this invocation"
367            );
368            false
369        }
370        Some(true) | None => true,
371    };
372
373    let Some(env) = prepare_wrapper_environment(rustc, &parsed).await else {
374        return run_rustc_passthrough(rustc, &command.wrapped_args, &parsed).await;
375    };
376    let request = FetchRequest {
377        target: &env.target,
378        rustc_version: &env.rustc_version,
379        c_metadata: env.request_c_metadata.as_str(),
380        crate_name: &parsed.crate_name,
381    };
382    if try_serve_local_cached_bundle(&env.config, &parsed, &request).await {
383        std::process::exit(0);
384    }
385    if try_serve_local_prefetched_graph_bundle(
386        &env.config,
387        &parsed,
388        &env.target,
389        &env.rustc_version,
390    )
391    .await
392    {
393        std::process::exit(0);
394    }
395    if let Some(semantic_request) = env.semantic_request.as_ref()
396        && try_serve_local_semantic_cached_bundle(&env.config, &parsed, semantic_request).await
397    {
398        std::process::exit(0);
399    }
400
401    match try_remote_serves(&env, &parsed, &request, exact_public_cache_allowed).await {
402        RemoteServe::Served => std::process::exit(0),
403        RemoteServe::Bypass => {
404            return run_rustc_passthrough(rustc, &command.wrapped_args, &parsed).await;
405        }
406        RemoteServe::Miss => {}
407    }
408
409    record_miss_and_passthrough(
410        &env.config,
411        &parsed,
412        &env.target,
413        &env.rustc_version,
414        rustc,
415        &command.wrapped_args,
416    )
417    .await
418}
419
420/// Everything the cache path needs once every bypass-capable preparation
421/// step has succeeded: the loaded config, the circuit state, the resolved
422/// invocation identity, and the lease that keeps this rustc version's local
423/// cache dir alive for the rest of the invocation.
424struct WrapperEnvironment {
425    config: StowConfig,
426    /// The breaker guards the *network*: remote fetches stop while tripped,
427    /// but a local entry still serves — a self-produced hit never touches
428    /// the edge, so it keeps paying off through the very outage that tripped
429    /// the breaker.
430    circuit_tripped: bool,
431    target: String,
432    rustc_version: String,
433    /// `target/rustc_version/c_metadata`, the negative-cache key.
434    cache_key: String,
435    /// The `c_metadata` to query with: the stable identity's when the
436    /// invocation's own was rewritten, else the raw cargo one.
437    request_c_metadata: String,
438    /// Semantic fallback request, only built when
439    /// `STOW_ENABLE_SEMANTIC_FALLBACK` opts in.
440    semantic_request: Option<fetch::SemanticFetchRequest>,
441    _version_cache_lease: artifact_cache::RustcVersionLease,
442}
443
444/// What the remote-fetch phase decided for an invocation.
445enum RemoteServe {
446    /// An artifact was written into the target dir; the wrapper exits 0.
447    Served,
448    /// No remote artifact applies; fall through to the miss path.
449    Miss,
450    /// The cache path failed or the artifact could not be used; run the
451    /// real rustc immediately.
452    Bypass,
453}
454
455/// Load the config and resolve everything the cache path needs for this
456/// invocation. Every step can legitimately be unavailable — the wrapper
457/// exists to accelerate builds, never to break them — so `None` means "run
458/// the real rustc".
459async fn prepare_wrapper_environment(
460    rustc: &OsString,
461    parsed: &rustc_args::ParsedRustcArgs,
462) -> Option<WrapperEnvironment> {
463    let config = match StowConfig::load() {
464        Ok(config) => config,
465        Err(error) => {
466            tracing::warn!(error = %error, "stow edge config unavailable, bypassing rust cache");
467            return None;
468        }
469    };
470    if let Err(error) = config.ensure_dirs().await {
471        tracing::warn!(error = %error, "failed to prepare stow cache directories, bypassing rust cache");
472        return None;
473    }
474    let circuit_tripped = match circuit::is_tripped(&config).await {
475        Ok(tripped) => tripped,
476        Err(error) => {
477            tracing::warn!(error = %error, "failed to read stow circuit state, bypassing rust cache");
478            return None;
479        }
480    };
481    if circuit_tripped {
482        tracing::debug!("circuit breaker tripped, serving local lookups only");
483    }
484
485    let target = match parsed.target.as_deref() {
486        Some(target) => target.to_owned(),
487        None => match rustc_args::detect_rustc_host_target(rustc).await {
488            Ok(target) => target,
489            Err(error) => {
490                tracing::warn!(error = %error, "failed to detect rustc host target, bypassing rust cache");
491                return None;
492            }
493        },
494    };
495    let Some(c_metadata) = parsed.c_metadata.as_deref() else {
496        tracing::warn!("cacheable rustc invocation is missing -C metadata, bypassing rust cache");
497        return None;
498    };
499    let rustc_version = match rustc_args::detect_rustc_version(rustc).await {
500        Ok(version) => version,
501        Err(error) => {
502            tracing::warn!(error = %error, "failed to detect rustc version, bypassing rust cache");
503            return None;
504        }
505    };
506    let cache_key = format!("{target}/{rustc_version}/{c_metadata}");
507    let version_cache_lease = match prepare_local_cache(&config, &rustc_version).await {
508        Ok(lease) => lease,
509        Err(error) => {
510            tracing::warn!(error = %error, "failed to prepare local stow artifact cache, bypassing rust cache");
511            return None;
512        }
513    };
514
515    let stable_exact_identity = match build_stable_exact_identity(
516        &config,
517        parsed,
518        &target,
519        &rustc_version,
520    )
521    .await
522    {
523        Ok(identity) => identity,
524        Err(error) => {
525            tracing::warn!(error = %error, "failed to resolve local artifact identity, bypassing rust cache");
526            return None;
527        }
528    };
529    let request_c_metadata = stable_exact_identity
530        .as_ref()
531        .map_or(c_metadata, |identity| identity.c_metadata.as_str())
532        .to_owned();
533    let semantic_fallback_enabled =
534        std::env::var_os(STOW_ENABLE_SEMANTIC_FALLBACK_ENV).is_some_and(|value| value != "0");
535    let semantic_request = if semantic_fallback_enabled {
536        match build_semantic_fetch_request(&config, parsed, &target, &rustc_version).await {
537            Ok(request) => request,
538            Err(error) => {
539                tracing::warn!(error = %error, "failed to build semantic fetch request, continuing without semantic fallback");
540                None
541            }
542        }
543    } else {
544        None
545    };
546    Some(WrapperEnvironment {
547        config,
548        circuit_tripped,
549        target,
550        rustc_version,
551        cache_key,
552        request_c_metadata,
553        semantic_request,
554        _version_cache_lease: version_cache_lease,
555    })
556}
557
558/// Try the edge for this invocation: the exact-artifact fetch first, then the
559/// semantic fallback when it is enabled and the exact path did not serve or
560/// disqualify the cache outright.
561async fn try_remote_serves(
562    env: &WrapperEnvironment,
563    parsed: &rustc_args::ParsedRustcArgs,
564    request: &FetchRequest<'_>,
565    exact_public_cache_allowed: bool,
566) -> RemoteServe {
567    if exact_public_cache_allowed && !env.circuit_tripped {
568        match try_remote_exact_serve(env, parsed, request).await {
569            RemoteServe::Miss => {}
570            outcome => return outcome,
571        }
572    }
573    if let Some(semantic_request) = env.semantic_request.as_ref()
574        && !env.circuit_tripped
575    {
576        return try_remote_semantic_serve(env, parsed, semantic_request).await;
577    }
578    RemoteServe::Miss
579}
580
581/// Fetch the exact-artifact bundle from the edge and serve it. `Miss` means
582/// the edge has no such artifact (or the negative cache already says so);
583/// `Bypass` means the artifact arrived but could not be used or the
584/// transport failed.
585async fn try_remote_exact_serve(
586    env: &WrapperEnvironment,
587    parsed: &rustc_args::ParsedRustcArgs,
588    request: &FetchRequest<'_>,
589) -> RemoteServe {
590    let negative_cache_hit = match circuit::negative_cache_contains(&env.config, &env.cache_key)
591        .await
592    {
593        Ok(hit) => hit,
594        Err(error) => {
595            tracing::warn!(error = %error, cache_key = %env.cache_key, "failed to read stow negative cache");
596            false
597        }
598    };
599    if negative_cache_hit {
600        tracing::debug!(cache_key = %env.cache_key, "negative cache hit, bypassing exact edge fetch");
601        return RemoteServe::Miss;
602    }
603    match fetch::download_bundle(&env.config, request).await {
604        Ok(bundle) => {
605            if try_serve_downloaded_bundle(&env.config, parsed, request, &bundle).await {
606                RemoteServe::Served
607            } else {
608                RemoteServe::Bypass
609            }
610        }
611        Err(fetch::FetchError::NotFound) => {
612            log_nonfatal_result(
613                "failed to record stow negative cache entry",
614                circuit::record_negative_cache(&env.config, &env.cache_key).await,
615            );
616            RemoteServe::Miss
617        }
618        Err(error) => {
619            record_circuit_failure(&env.config).await;
620            record_lookup_error(&env.config, parsed).await;
621            tracing::warn!(
622                crate_name = %parsed.crate_name,
623                target = %env.target,
624                rustc_version = %env.rustc_version,
625                error = %error,
626                "stow exact fetch failed, falling back to semantic or rustc"
627            );
628            RemoteServe::Miss
629        }
630    }
631}
632
633/// Fetch the semantic fallback bundle from the edge and serve it.
634async fn try_remote_semantic_serve(
635    env: &WrapperEnvironment,
636    parsed: &rustc_args::ParsedRustcArgs,
637    semantic_request: &fetch::SemanticFetchRequest,
638) -> RemoteServe {
639    match fetch::download_semantic_bundle(&env.config, semantic_request).await {
640        Ok(bundle) => {
641            if try_serve_semantic_downloaded_bundle(&env.config, parsed, semantic_request, &bundle)
642                .await
643            {
644                RemoteServe::Served
645            } else {
646                RemoteServe::Bypass
647            }
648        }
649        Err(fetch::FetchError::NotFound) => RemoteServe::Miss,
650        Err(error) => {
651            record_circuit_failure(&env.config).await;
652            record_lookup_error(&env.config, parsed).await;
653            tracing::warn!(
654                crate_name = %parsed.crate_name,
655                semantic_crate_name = %semantic_request.crate_name,
656                semantic_version = %semantic_request.version,
657                target = %env.target,
658                rustc_version = %env.rustc_version,
659                error = %error,
660                "stow semantic fetch failed, falling back to rustc"
661            );
662            RemoteServe::Bypass
663        }
664    }
665}
666
667/// Record a public-cache miss for a registry crate, then run the real rustc.
668/// Only a registry package can be a miss: a workspace member is first-party
669/// code the public cache never carries, so counting it would report the
670/// project's own crates as failures and make a healthy build look broken in
671/// the post-build summary.
672async fn record_miss_and_passthrough(
673    config: &StowConfig,
674    parsed: &rustc_args::ParsedRustcArgs,
675    target: &str,
676    rustc_version: &str,
677    rustc: &OsString,
678    wrapped_args: &[std::ffi::OsString],
679) -> stow_types::error::Result<()> {
680    if detect_registry_crate_version(parsed)?.is_some() {
681        log_nonfatal_result(
682            "failed to record rust cache miss stats",
683            stats::record_miss(config, &parsed.crate_name).await,
684        );
685        tracing::debug!(
686            crate_name = %parsed.crate_name,
687            target,
688            rustc_version,
689            "stow cache miss, falling back to rustc"
690        );
691    }
692    run_rustc_passthrough(rustc, wrapped_args, parsed).await
693}
694
695/// Count a remote-cache failure against the circuit breaker; the record
696/// itself must never fail the invocation.
697async fn record_circuit_failure(config: &StowConfig) {
698    log_nonfatal_result(
699        "failed to record stow circuit failure",
700        circuit::record_failure(config).await,
701    );
702}
703
704/// Count a remote-cache success toward resetting the circuit breaker.
705async fn record_circuit_success(config: &StowConfig) {
706    log_nonfatal_result(
707        "failed to record stow circuit success",
708        circuit::record_success(config).await,
709    );
710}
711
712/// Count a failed rust cache lookup as an error stat without failing the
713/// invocation — a stats write is not worth a missed compile.
714async fn record_lookup_error(config: &StowConfig, parsed: &rustc_args::ParsedRustcArgs) {
715    log_nonfatal_result(
716        "failed to record rust cache error stats",
717        stats::record_error(config, &parsed.crate_name).await,
718    );
719}
720
721/// Count a served rust cache lookup as a hit stat.
722async fn record_lookup_hit(config: &StowConfig, parsed: &rustc_args::ParsedRustcArgs) {
723    log_nonfatal_result(
724        "failed to record rust cache hit stats",
725        stats::record_hit(config, &parsed.crate_name).await,
726    );
727}
728
729/// Evict a local cache entry that can no longer be trusted to serve this
730/// invocation, warning with the invocation identity when eviction itself
731/// fails. `context` is the warning text for that failure.
732async fn evict_cached_bundle(
733    config: &StowConfig,
734    parsed: &rustc_args::ParsedRustcArgs,
735    request: &FetchRequest<'_>,
736    context: &'static str,
737) {
738    if let Err(error) = remove_cached_bundle(config, request).await {
739        tracing::warn!(
740            error = %error,
741            crate_name = %parsed.crate_name,
742            target = %request.target,
743            rustc_version = %request.rustc_version,
744            "{context}"
745        );
746    }
747}
748
749/// Wrapper path for invocations the remote cache does not cover — chiefly
750/// release-profile crates, which `is_cacheable` restricts to the canonical
751/// dev profile. A self-produced local entry covers dev *and* release builds,
752/// so look the identity up locally before compiling; on a miss the
753/// passthrough stores this build's outputs for the next worktree.
754async fn run_rustc_wrapper_local_only(
755    rustc: &OsString,
756    wrapped_args: &[std::ffi::OsString],
757    parsed: &rustc_args::ParsedRustcArgs,
758) -> stow_types::error::Result<()> {
759    if !parsed.is_locally_cacheable() {
760        return run_rustc_passthrough(rustc, wrapped_args, parsed).await;
761    }
762    let config = match StowConfig::load_local() {
763        Ok(config) => config,
764        Err(error) => {
765            tracing::warn!(error = %error, "stow local config unavailable, bypassing local artifact cache");
766            return run_rustc_passthrough(rustc, wrapped_args, parsed).await;
767        }
768    };
769    if let Err(error) = config.ensure_dirs().await {
770        tracing::warn!(error = %error, "failed to prepare stow cache directories, bypassing local artifact cache");
771        return run_rustc_passthrough(rustc, wrapped_args, parsed).await;
772    }
773    let target = match parsed.target.as_deref() {
774        Some(target) => target.to_owned(),
775        None => match rustc_args::detect_rustc_host_target(rustc).await {
776            Ok(target) => target,
777            Err(error) => {
778                tracing::warn!(error = %error, "failed to detect rustc host target, bypassing local artifact cache");
779                return run_rustc_passthrough(rustc, wrapped_args, parsed).await;
780            }
781        },
782    };
783    let rustc_version = match rustc_args::detect_rustc_version(rustc).await {
784        Ok(version) => version,
785        Err(error) => {
786            tracing::warn!(error = %error, "failed to detect rustc version, bypassing local artifact cache");
787            return run_rustc_passthrough(rustc, wrapped_args, parsed).await;
788        }
789    };
790    let _version_cache_lease = match prepare_local_cache(&config, &rustc_version).await {
791        Ok(lease) => lease,
792        Err(error) => {
793            tracing::warn!(error = %error, "failed to prepare local stow artifact cache, bypassing local artifact cache");
794            return run_rustc_passthrough(rustc, wrapped_args, parsed).await;
795        }
796    };
797    let identity = match build_stable_exact_identity(&config, parsed, &target, &rustc_version).await
798    {
799        Ok(identity) => identity,
800        Err(error) => {
801            tracing::warn!(error = %error, "failed to resolve local artifact identity, bypassing local artifact cache");
802            return run_rustc_passthrough(rustc, wrapped_args, parsed).await;
803        }
804    };
805    if let Some(identity) = identity {
806        let request = FetchRequest {
807            target: &target,
808            rustc_version: &rustc_version,
809            c_metadata: identity.c_metadata.as_str(),
810            crate_name: &parsed.crate_name,
811        };
812        if try_serve_local_cached_bundle(&config, parsed, &request).await {
813            std::process::exit(0);
814        }
815    }
816    run_rustc_passthrough(rustc, wrapped_args, parsed).await
817}
818
819#[tracing::instrument(name = "stow.wrapper.cc_invoke", skip_all)]
820async fn run_cc_wrapper(command: WrapperCommandArgs) -> stow_types::error::Result<()> {
821    let compiler = &command.executable;
822    let compiler_args = &command.wrapped_args;
823    let config = match StowConfig::load_local() {
824        Ok(config) => config,
825        Err(error) => {
826            tracing::warn!(error = %error, "stow local config unavailable, bypassing C/C++ cache");
827            return run_passthrough(compiler, compiler_args).await;
828        }
829    };
830    if let Err(error) = config.ensure_dirs().await {
831        tracing::warn!(error = %error, "failed to prepare stow cache directories, bypassing C/C++ cache");
832        return run_passthrough(compiler, compiler_args).await;
833    }
834
835    let outcome = match cc::try_compile(&config, compiler, compiler_args).await {
836        Ok(outcome) => outcome,
837        Err(error) => {
838            tracing::warn!(error = %error, "stow C/C++ cache failed, bypassing cache");
839            return run_passthrough(compiler, compiler_args).await;
840        }
841    };
842
843    match outcome {
844        cc::CcOutcome::Passthrough => run_passthrough(compiler, compiler_args).await,
845        cc::CcOutcome::Hit {
846            cache_key,
847            output_path,
848        } => {
849            log_nonfatal_result(
850                "failed to record C/C++ cache hit stats",
851                stats::record_hit(&config, &format!("cc:{cache_key}")).await,
852            );
853            tracing::info!(
854                cache_key = %cache_key,
855                output_path = %output_path.display(),
856                "served C/C++ compilation from local stow cache"
857            );
858            std::process::exit(0);
859        }
860        cc::CcOutcome::Miss {
861            cache_key,
862            cache_path,
863            output_path,
864        } => {
865            let compiler_status = Command::new(compiler)
866                .args(compiler_args)
867                .status()
868                .await
869                .wrap_err("failed to spawn wrapped C/C++ compiler")?;
870            if !compiler_status.success() {
871                log_nonfatal_result(
872                    "failed to record C/C++ cache error stats",
873                    stats::record_error(&config, &format!("cc:{cache_key}")).await,
874                );
875                std::process::exit(compiler_status.code().unwrap_or(1));
876            }
877
878            if let Err(error) = cc::store_compiled_object(&cache_path, &output_path).await {
879                tracing::warn!(
880                    error = %error,
881                    cache_key = %cache_key,
882                    output_path = %output_path.display(),
883                    "failed to store C/C++ compilation in local stow cache"
884                );
885                log_nonfatal_result(
886                    "failed to record C/C++ cache error stats",
887                    stats::record_error(&config, &format!("cc:{cache_key}")).await,
888                );
889                std::process::exit(0);
890            }
891            log_nonfatal_result(
892                "failed to record C/C++ cache miss stats",
893                stats::record_miss(&config, &format!("cc:{cache_key}")).await,
894            );
895            tracing::info!(
896                cache_key = %cache_key,
897                output_path = %output_path.display(),
898                "stored C/C++ compilation in local stow cache"
899            );
900            std::process::exit(0);
901        }
902    }
903}
904
905async fn try_serve_local_cached_bundle(
906    config: &StowConfig,
907    parsed: &rustc_args::ParsedRustcArgs,
908    request: &FetchRequest<'_>,
909) -> bool {
910    let cached_bundle = match load_cached_bundle(config, request).await {
911        Ok(bundle) => bundle,
912        Err(error) => {
913            tracing::warn!(
914                error = %error,
915                crate_name = %parsed.crate_name,
916                target = %request.target,
917                rustc_version = %request.rustc_version,
918                "failed to read local stow artifact cache entry"
919            );
920            log_nonfatal_result(
921                "failed to record rust cache error stats",
922                stats::record_error(config, &parsed.crate_name).await,
923            );
924            return false;
925        }
926    };
927    let Some(cached_bundle) = cached_bundle else {
928        return false;
929    };
930    try_serve_loaded_local_cached_bundle(config, parsed, request, cached_bundle).await
931}
932
933async fn try_serve_local_prefetched_graph_bundle(
934    config: &StowConfig,
935    parsed: &rustc_args::ParsedRustcArgs,
936    target: &str,
937    rustc_version: &str,
938) -> bool {
939    let Some((crate_name, version)) = detect_registry_crate_version(parsed).ok().flatten() else {
940        return false;
941    };
942    let expected_features_json = match resolve_semantic_features_json(&crate_name, &version, parsed)
943    {
944        Ok(features_json) => features_json,
945        Err(error) => {
946            tracing::warn!(
947                error = %error,
948                crate_name = %parsed.crate_name,
949                target,
950                rustc_version,
951                "failed to resolve semantic features for prefetched graph bundle lookup"
952            );
953            return false;
954        }
955    };
956    let expected_dependency_c_metadata_json =
957        match resolve_dependency_c_metadata_json(config, parsed).await {
958            Ok(Some(value)) => value,
959            Ok(None) if parsed.extern_crates.is_empty() => "[]".to_owned(),
960            Ok(None) => return false,
961            Err(error) => {
962                tracing::warn!(
963                    error = %error,
964                    crate_name = %parsed.crate_name,
965                    target,
966                    rustc_version,
967                    "failed to resolve prefetched graph dependency identities"
968                );
969                return false;
970            }
971        };
972    let candidate_c_metadatas =
973        match load_prefetched_graph_candidate_c_metadatas(&parsed.crate_name) {
974            Ok(candidates) => candidates,
975            Err(error) => {
976                tracing::warn!(
977                    error = %error,
978                    crate_name = %parsed.crate_name,
979                    target,
980                    rustc_version,
981                    "failed to parse prefetched graph artifact candidates"
982                );
983                return false;
984            }
985        };
986
987    for c_metadata in candidate_c_metadatas {
988        let request = FetchRequest {
989            target,
990            rustc_version,
991            c_metadata: c_metadata.as_str(),
992            crate_name: &parsed.crate_name,
993        };
994        let cached_bundle = match load_cached_bundle(config, &request).await {
995            Ok(Some(bundle)) => bundle,
996            Ok(None) => continue,
997            Err(error) => {
998                tracing::warn!(
999                    error = %error,
1000                    crate_name = %parsed.crate_name,
1001                    target,
1002                    rustc_version,
1003                    candidate_c_metadata = %c_metadata,
1004                    "failed to read prefetched graph bundle from local cache"
1005                );
1006                return false;
1007            }
1008        };
1009        if let Err(error) = validate_prefetched_graph_bundle(
1010            parsed,
1011            &version,
1012            &expected_features_json,
1013            &expected_dependency_c_metadata_json,
1014            &cached_bundle,
1015        ) {
1016            tracing::debug!(
1017                error = %error,
1018                crate_name = %parsed.crate_name,
1019                target,
1020                rustc_version,
1021                candidate_c_metadata = %c_metadata,
1022                "skipping prefetched graph bundle that does not match current invocation"
1023            );
1024            continue;
1025        }
1026        if try_serve_loaded_local_cached_bundle(config, parsed, &request, cached_bundle).await {
1027            return true;
1028        }
1029    }
1030    false
1031}
1032
1033async fn try_serve_loaded_local_cached_bundle(
1034    config: &StowConfig,
1035    parsed: &rustc_args::ParsedRustcArgs,
1036    request: &FetchRequest<'_>,
1037    cached_bundle: artifact_cache::CachedArtifactBundle,
1038) -> bool {
1039    if let Err(error) = validate_exact_bundle_semantics(
1040        parsed,
1041        &cached_bundle.profile,
1042        &cached_bundle.emit,
1043        &cached_bundle.kind,
1044        &cached_bundle.crate_types,
1045        &cached_bundle.crate_version,
1046    ) {
1047        tracing::warn!(
1048            error = %error,
1049            crate_name = %parsed.crate_name,
1050            target = %request.target,
1051            rustc_version = %request.rustc_version,
1052            "local stow artifact cache entry semantic mismatch, evicting and falling back to rustc"
1053        );
1054        drop(cached_bundle);
1055        evict_cached_bundle(
1056            config,
1057            parsed,
1058            request,
1059            "failed to evict local stow artifact cache entry with semantic mismatch",
1060        )
1061        .await;
1062        record_lookup_error(config, parsed).await;
1063        return false;
1064    }
1065
1066    if let Err(error) = verify::verify_cached_bundle_signature(config, &cached_bundle).await {
1067        tracing::warn!(
1068            error = %error,
1069            crate_name = %parsed.crate_name,
1070            target = %request.target,
1071            rustc_version = %request.rustc_version,
1072            "local stow artifact cache entry failed verification, evicting and falling back to rustc"
1073        );
1074        drop(cached_bundle);
1075        evict_cached_bundle(
1076            config,
1077            parsed,
1078            request,
1079            "failed to evict untrusted local stow artifact cache entry",
1080        )
1081        .await;
1082        record_lookup_error(config, parsed).await;
1083        return false;
1084    }
1085
1086    if let Err(error) =
1087        prune_materialized_aliases_for_cached_closure(config, parsed, request, &cached_bundle).await
1088    {
1089        tracing::warn!(
1090            error = %error,
1091            crate_name = %parsed.crate_name,
1092            target = %request.target,
1093            rustc_version = %request.rustc_version,
1094            "failed to materialize dependency closure aliases for local stow artifact cache entry"
1095        );
1096        record_lookup_error(config, parsed).await;
1097        return false;
1098    }
1099
1100    materialize_local_cached_bundle(config, parsed, request, cached_bundle).await
1101}
1102
1103/// Write a verified local cache entry's artifacts into the target dir.
1104/// `false` means the entry could not be materialized — it is evicted so the
1105/// next lookup does not trip over it again.
1106async fn materialize_local_cached_bundle(
1107    config: &StowConfig,
1108    parsed: &rustc_args::ParsedRustcArgs,
1109    request: &FetchRequest<'_>,
1110    cached_bundle: artifact_cache::CachedArtifactBundle,
1111) -> bool {
1112    match inject::write_artifacts(parsed, &cached_bundle).await {
1113        Ok(()) => finish_local_serve(config, parsed, request, cached_bundle).await,
1114        Err(error) => {
1115            tracing::warn!(
1116                error = %error,
1117                crate_name = %parsed.crate_name,
1118                target = %request.target,
1119                rustc_version = %request.rustc_version,
1120                "failed to materialize local stow artifact cache entry, evicting and falling back to rustc"
1121            );
1122            drop(cached_bundle);
1123            evict_cached_bundle(
1124                config,
1125                parsed,
1126                request,
1127                "failed to evict broken local stow artifact cache entry",
1128            )
1129            .await;
1130            record_lookup_error(config, parsed).await;
1131            false
1132        }
1133    }
1134}
1135
1136/// The bookkeeping that turns written artifacts into a served hit: record
1137/// what was materialized, replay rustc's artifact notifications so cargo
1138/// sees a normal compile, then count the hit.
1139async fn finish_local_serve(
1140    config: &StowConfig,
1141    parsed: &rustc_args::ParsedRustcArgs,
1142    request: &FetchRequest<'_>,
1143    cached_bundle: artifact_cache::CachedArtifactBundle,
1144) -> bool {
1145    if let Err(error) = record_materialized_bundle_outputs(config, parsed, &cached_bundle).await {
1146        tracing::warn!(
1147            error = %error,
1148            crate_name = %parsed.crate_name,
1149            target = %request.target,
1150            rustc_version = %request.rustc_version,
1151            "failed to record materialized local stow artifact outputs"
1152        );
1153        record_lookup_error(config, parsed).await;
1154        return false;
1155    }
1156    if let Err(error) = emit_cached_rustc_artifact_notifications(parsed).await {
1157        tracing::warn!(
1158            error = %error,
1159            crate_name = %parsed.crate_name,
1160            target = %request.target,
1161            rustc_version = %request.rustc_version,
1162            "failed to replay rustc artifact notifications for local stow artifact cache entry"
1163        );
1164        drop(cached_bundle);
1165        evict_cached_bundle(
1166            config,
1167            parsed,
1168            request,
1169            "failed to evict local stow artifact cache entry missing rustc artifact notifications",
1170        )
1171        .await;
1172        record_lookup_error(config, parsed).await;
1173        return false;
1174    }
1175    record_lookup_hit(config, parsed).await;
1176    tracing::info!(
1177        crate_name = %parsed.crate_name,
1178        target = %request.target,
1179        rustc_version = %request.rustc_version,
1180        "served rustc invocation from local stow artifact cache"
1181    );
1182    true
1183}
1184
1185fn load_prefetched_graph_candidate_c_metadatas(
1186    crate_name: &str,
1187) -> stow_types::error::Result<Vec<String>> {
1188    Ok(load_prefetched_graph_artifacts()?
1189        .into_iter()
1190        .filter(|entry| {
1191            canonical_crate_name(entry.crate_name.as_str()) == canonical_crate_name(crate_name)
1192        })
1193        .map(|entry| entry.c_metadata.into_inner())
1194        .collect())
1195}
1196
1197fn load_prefetched_graph_artifacts() -> stow_types::error::Result<Vec<BatchArtifactRequestEntry>> {
1198    let Some(raw) = std::env::var_os(STOW_PREFETCH_ARTIFACTS_ENV) else {
1199        return Ok(Vec::new());
1200    };
1201    let raw = raw.into_string().map_err(|_| {
1202        stow_types::stow_error!("{STOW_PREFETCH_ARTIFACTS_ENV} must be valid UTF-8")
1203    })?;
1204    serde_json::from_str::<Vec<BatchArtifactRequestEntry>>(&raw)
1205        .wrap_err_with(|| format!("parse {STOW_PREFETCH_ARTIFACTS_ENV}"))
1206}
1207
1208async fn prune_materialized_aliases_for_cached_closure(
1209    config: &StowConfig,
1210    parsed: &rustc_args::ParsedRustcArgs,
1211    request: &FetchRequest<'_>,
1212    cached_bundle: &artifact_cache::CachedArtifactBundle,
1213) -> stow_types::error::Result<()> {
1214    let Some(out_dir) = parsed.out_dir.as_ref() else {
1215        return Ok(());
1216    };
1217
1218    let mut bundles_by_compile_key = BTreeMap::new();
1219    let mut pending = serde_json::from_str::<Vec<DependencyCompileKeyIdentity>>(
1220        &cached_bundle.dependency_compile_keys_json,
1221    )?;
1222    let mut visited = BTreeSet::new();
1223    while let Some(dependency) = pending.pop() {
1224        if !visited.insert(dependency.compile_key.clone()) {
1225            continue;
1226        }
1227        let dependency_bundle = match load_cached_bundle_by_compile_key(
1228            config,
1229            request.rustc_version,
1230            &dependency.compile_key,
1231        )
1232        .await?
1233        {
1234            Some(bundle) => bundle,
1235            None => {
1236                download_closure_dependency_bundle(
1237                    config,
1238                    request.target,
1239                    request.rustc_version,
1240                    &dependency,
1241                )
1242                .await?
1243            }
1244        };
1245        let nested = serde_json::from_str::<Vec<DependencyCompileKeyIdentity>>(
1246            &dependency_bundle.dependency_compile_keys_json,
1247        )?;
1248        pending.extend(nested);
1249        bundles_by_compile_key.insert(dependency.compile_key, dependency_bundle);
1250    }
1251
1252    let mut keep_original_file_names = BTreeSet::new();
1253    let mut closure_compile_keys = BTreeSet::new();
1254    let mut closure_crates = BTreeSet::from([canonical_crate_name(&cached_bundle.crate_name)]);
1255    let mut closure_visited = BTreeSet::new();
1256    collect_dependency_closure_file_names(
1257        cached_bundle,
1258        &bundles_by_compile_key,
1259        &mut closure_visited,
1260        &mut keep_original_file_names,
1261        &mut closure_crates,
1262        &mut closure_compile_keys,
1263    )?;
1264
1265    for compile_key in &closure_compile_keys {
1266        let dependency_bundle = bundles_by_compile_key.get(compile_key).ok_or_else(|| {
1267            stow_types::stow_error!(
1268                "missing prefetched cached bundle for compile key {compile_key}"
1269            )
1270        })?;
1271        inject::materialize_original_outputs(out_dir, dependency_bundle).await?;
1272    }
1273
1274    Ok(())
1275}
1276
1277async fn download_closure_dependency_bundle(
1278    config: &StowConfig,
1279    target: &str,
1280    rustc_version: &str,
1281    dependency: &DependencyCompileKeyIdentity,
1282) -> stow_types::error::Result<artifact_cache::CachedArtifactBundle> {
1283    let c_metadata = stable_c_metadata_for_compile_key(&dependency.compile_key)?;
1284    let request = fetch::FetchRequest {
1285        target,
1286        rustc_version,
1287        c_metadata: &c_metadata,
1288        crate_name: dependency.crate_name.as_str(),
1289    };
1290    let bundle = fetch::download_bundle(config, &request)
1291        .await
1292        .map_err(|error| {
1293            stow_types::stow_error!(
1294                "download closure dependency bundle {} ({}) failed: {error}",
1295                dependency.compile_key,
1296                dependency.crate_name
1297            )
1298        })?;
1299    let cached_bundle = cache_verified_downloaded_bundle(config, &request, &bundle).await?;
1300    if cached_bundle.compile_key != dependency.compile_key {
1301        return Err(stow_types::stow_error!(
1302            "downloaded closure dependency compile key mismatch for {}: expected {}, got {}",
1303            dependency.crate_name,
1304            dependency.compile_key,
1305            cached_bundle.compile_key
1306        ));
1307    }
1308    Ok(cached_bundle)
1309}
1310
1311async fn cache_verified_downloaded_bundle(
1312    config: &StowConfig,
1313    request: &FetchRequest<'_>,
1314    bundle: &fetch::ArtifactBundle,
1315) -> stow_types::error::Result<artifact_cache::CachedArtifactBundle> {
1316    verify::verify_bundle_signature(config, bundle).await?;
1317    verify::store_downloaded_bundle_with_trust_marker(config, request, bundle).await
1318}
1319
1320fn collect_dependency_closure_file_names(
1321    bundle: &artifact_cache::CachedArtifactBundle,
1322    bundles_by_compile_key: &BTreeMap<String, artifact_cache::CachedArtifactBundle>,
1323    visited: &mut BTreeSet<String>,
1324    keep_original_file_names: &mut BTreeSet<String>,
1325    closure_crates: &mut BTreeSet<String>,
1326    closure_compile_keys: &mut BTreeSet<String>,
1327) -> stow_types::error::Result<()> {
1328    let dependencies = serde_json::from_str::<Vec<DependencyCompileKeyIdentity>>(
1329        &bundle.dependency_compile_keys_json,
1330    )?;
1331    for dependency in dependencies {
1332        if !visited.insert(dependency.compile_key.clone()) {
1333            continue;
1334        }
1335        closure_compile_keys.insert(dependency.compile_key.clone());
1336        let dependency_bundle = bundles_by_compile_key
1337            .get(&dependency.compile_key)
1338            .ok_or_else(|| {
1339                stow_types::stow_error!(
1340                    "missing prefetched cached bundle for compile key {} ({})",
1341                    dependency.compile_key,
1342                    dependency.crate_name
1343                )
1344            })?;
1345        closure_crates.insert(canonical_crate_name(&dependency_bundle.crate_name));
1346        for output in &dependency_bundle.outputs {
1347            keep_original_file_names.insert(output.file_name.clone());
1348        }
1349        collect_dependency_closure_file_names(
1350            dependency_bundle,
1351            bundles_by_compile_key,
1352            visited,
1353            keep_original_file_names,
1354            closure_crates,
1355            closure_compile_keys,
1356        )?;
1357    }
1358    Ok(())
1359}
1360
1361fn validate_prefetched_graph_bundle(
1362    parsed: &rustc_args::ParsedRustcArgs,
1363    expected_version: &str,
1364    expected_features_json: &str,
1365    expected_dependency_c_metadata_json: &str,
1366    cached_bundle: &artifact_cache::CachedArtifactBundle,
1367) -> stow_types::error::Result<()> {
1368    if canonical_crate_name(&cached_bundle.crate_name) != canonical_crate_name(&parsed.crate_name) {
1369        return Err(stow_types::stow_error!(
1370            "prefetched graph bundle crate name mismatch"
1371        ));
1372    }
1373    if cached_bundle.crate_version != expected_version {
1374        return Err(stow_types::stow_error!(
1375            "prefetched graph bundle crate version mismatch"
1376        ));
1377    }
1378    if cached_bundle.features_json != expected_features_json {
1379        return Err(stow_types::stow_error!(
1380            "prefetched graph bundle features mismatch"
1381        ));
1382    }
1383    if cached_bundle.dependency_c_metadata_json != expected_dependency_c_metadata_json {
1384        return Err(stow_types::stow_error!(
1385            "prefetched graph bundle dependency identities mismatch"
1386        ));
1387    }
1388    Ok(())
1389}
1390
1391async fn try_serve_downloaded_bundle(
1392    config: &StowConfig,
1393    parsed: &rustc_args::ParsedRustcArgs,
1394    request: &FetchRequest<'_>,
1395    bundle: &fetch::ArtifactBundle,
1396) -> bool {
1397    if let Err(error) = fetch::validate_bundle_identity(
1398        bundle,
1399        &parsed.crate_name,
1400        request.c_metadata,
1401        request.target,
1402        request.rustc_version,
1403    ) {
1404        tracing::warn!(
1405            error = %error,
1406            crate_name = %parsed.crate_name,
1407            target = %request.target,
1408            rustc_version = %request.rustc_version,
1409            "downloaded stow bundle identity mismatch"
1410        );
1411        // A miss, not an outage: the artifact arrived intact, it just does
1412        // not describe this invocation. Counting identity divergence toward
1413        // the circuit breaker meant a handful of legitimately-unmatched units
1414        // (the proc-macro host graph, typically) tripped it five invocations
1415        // in, and every remaining crate in the build then bypassed the cache
1416        // for the full reset window. Only transport and materialization
1417        // failures say the cache path itself is unhealthy.
1418        log_nonfatal_result(
1419            "failed to record rust cache miss stats",
1420            stats::record_miss(config, &parsed.crate_name).await,
1421        );
1422        return false;
1423    }
1424    if let Err(error) = validate_exact_bundle_semantics(
1425        parsed,
1426        &bundle.manifest.config.profile,
1427        &bundle.manifest.config.emit,
1428        &bundle.manifest.config.kind,
1429        &bundle.manifest.config.crate_types,
1430        &bundle.manifest.config.crate_version.to_string(),
1431    ) {
1432        tracing::warn!(
1433            error = %error,
1434            crate_name = %parsed.crate_name,
1435            target = %request.target,
1436            rustc_version = %request.rustc_version,
1437            "downloaded stow bundle semantic mismatch"
1438        );
1439        // A miss, not an outage: the artifact arrived intact, it just does
1440        // not describe this invocation. Counting identity divergence toward
1441        // the circuit breaker meant a handful of legitimately-unmatched units
1442        // (the proc-macro host graph, typically) tripped it five invocations
1443        // in, and every remaining crate in the build then bypassed the cache
1444        // for the full reset window. Only transport and materialization
1445        // failures say the cache path itself is unhealthy.
1446        log_nonfatal_result(
1447            "failed to record rust cache miss stats",
1448            stats::record_miss(config, &parsed.crate_name).await,
1449        );
1450        return false;
1451    }
1452    try_serve_verified_downloaded_bundle(config, parsed, request, bundle).await
1453}
1454
1455async fn try_serve_local_semantic_cached_bundle(
1456    config: &StowConfig,
1457    parsed: &rustc_args::ParsedRustcArgs,
1458    semantic_request: &fetch::SemanticFetchRequest,
1459) -> bool {
1460    let cached_bundle = match load_semantic_cached_bundle(config, semantic_request).await {
1461        Ok(bundle) => bundle,
1462        Err(error) => {
1463            tracing::warn!(
1464                error = %error,
1465                crate_name = %parsed.crate_name,
1466                semantic_crate_name = %semantic_request.crate_name,
1467                semantic_version = %semantic_request.version,
1468                target = %semantic_request.target,
1469                rustc_version = %semantic_request.rustc_version,
1470                "failed to read local semantic stow artifact cache entry"
1471            );
1472            record_lookup_error(config, parsed).await;
1473            return false;
1474        }
1475    };
1476    let Some(cached_bundle) = cached_bundle else {
1477        return false;
1478    };
1479    if let Err(error) = validate_exact_bundle_semantics(
1480        parsed,
1481        &cached_bundle.profile,
1482        &cached_bundle.emit,
1483        &cached_bundle.kind,
1484        &cached_bundle.crate_types,
1485        &cached_bundle.crate_version,
1486    ) {
1487        tracing::warn!(
1488            error = %error,
1489            crate_name = %parsed.crate_name,
1490            semantic_crate_name = %semantic_request.crate_name,
1491            semantic_version = %semantic_request.version,
1492            target = %semantic_request.target,
1493            rustc_version = %semantic_request.rustc_version,
1494            cached_c_metadata = %cached_bundle.c_metadata,
1495            "local semantic stow artifact cache entry semantic mismatch"
1496        );
1497        record_lookup_error(config, parsed).await;
1498        return false;
1499    }
1500    if let Err(error) = verify::verify_cached_bundle_signature(config, &cached_bundle).await {
1501        tracing::warn!(
1502            error = %error,
1503            crate_name = %parsed.crate_name,
1504            semantic_crate_name = %semantic_request.crate_name,
1505            semantic_version = %semantic_request.version,
1506            target = %semantic_request.target,
1507            rustc_version = %semantic_request.rustc_version,
1508            cached_c_metadata = %cached_bundle.c_metadata,
1509            "local semantic stow artifact cache entry failed verification"
1510        );
1511        record_lookup_error(config, parsed).await;
1512        return false;
1513    }
1514    let request = FetchRequest {
1515        target: &semantic_request.target,
1516        rustc_version: &semantic_request.rustc_version,
1517        c_metadata: &cached_bundle.c_metadata,
1518        crate_name: &cached_bundle.crate_name,
1519    };
1520    if let Err(error) =
1521        prune_materialized_aliases_for_cached_closure(config, parsed, &request, &cached_bundle)
1522            .await
1523    {
1524        tracing::warn!(
1525            error = %error,
1526            crate_name = %parsed.crate_name,
1527            semantic_crate_name = %semantic_request.crate_name,
1528            semantic_version = %semantic_request.version,
1529            target = %semantic_request.target,
1530            rustc_version = %semantic_request.rustc_version,
1531            cached_c_metadata = %cached_bundle.c_metadata,
1532            "failed to materialize dependency closure aliases for local semantic stow artifact cache entry"
1533        );
1534        record_lookup_error(config, parsed).await;
1535        return false;
1536    }
1537
1538    materialize_semantic_cached_bundle(config, parsed, semantic_request, cached_bundle).await
1539}
1540
1541/// Write a verified semantic cache entry's artifacts into the target dir and
1542/// finish the bookkeeping that makes it a served hit. Unlike the exact-local
1543/// path the entry is not evicted on failure: the semantic lookup is
1544/// best-effort, so a broken entry just misses again next time.
1545async fn materialize_semantic_cached_bundle(
1546    config: &StowConfig,
1547    parsed: &rustc_args::ParsedRustcArgs,
1548    semantic_request: &fetch::SemanticFetchRequest,
1549    cached_bundle: artifact_cache::CachedArtifactBundle,
1550) -> bool {
1551    if let Err(error) = inject::write_artifacts(parsed, &cached_bundle).await {
1552        tracing::warn!(
1553            error = %error,
1554            crate_name = %parsed.crate_name,
1555            semantic_crate_name = %semantic_request.crate_name,
1556            semantic_version = %semantic_request.version,
1557            target = %semantic_request.target,
1558            rustc_version = %semantic_request.rustc_version,
1559            cached_c_metadata = %cached_bundle.c_metadata,
1560            "failed to materialize local semantic stow artifact cache entry"
1561        );
1562        record_lookup_error(config, parsed).await;
1563        return false;
1564    }
1565    if let Err(error) = record_materialized_bundle_outputs(config, parsed, &cached_bundle).await {
1566        tracing::warn!(
1567            error = %error,
1568            crate_name = %parsed.crate_name,
1569            semantic_crate_name = %semantic_request.crate_name,
1570            semantic_version = %semantic_request.version,
1571            target = %semantic_request.target,
1572            rustc_version = %semantic_request.rustc_version,
1573            cached_c_metadata = %cached_bundle.c_metadata,
1574            "failed to record materialized local semantic stow artifact outputs"
1575        );
1576        record_lookup_error(config, parsed).await;
1577        return false;
1578    }
1579    if let Err(error) = emit_cached_rustc_artifact_notifications(parsed).await {
1580        tracing::warn!(
1581            error = %error,
1582            crate_name = %parsed.crate_name,
1583            semantic_crate_name = %semantic_request.crate_name,
1584            semantic_version = %semantic_request.version,
1585            target = %semantic_request.target,
1586            rustc_version = %semantic_request.rustc_version,
1587            cached_c_metadata = %cached_bundle.c_metadata,
1588            "failed to replay rustc artifact notifications for local semantic stow artifact cache entry"
1589        );
1590        record_lookup_error(config, parsed).await;
1591        return false;
1592    }
1593    record_lookup_hit(config, parsed).await;
1594    tracing::info!(
1595        crate_name = %parsed.crate_name,
1596        semantic_crate_name = %semantic_request.crate_name,
1597        semantic_version = %semantic_request.version,
1598        target = %semantic_request.target,
1599        rustc_version = %semantic_request.rustc_version,
1600        cached_c_metadata = %cached_bundle.c_metadata,
1601        "served rustc invocation from local semantic stow artifact cache"
1602    );
1603    true
1604}
1605
1606async fn try_serve_semantic_downloaded_bundle(
1607    config: &StowConfig,
1608    parsed: &rustc_args::ParsedRustcArgs,
1609    semantic_request: &fetch::SemanticFetchRequest,
1610    bundle: &fetch::ArtifactBundle,
1611) -> bool {
1612    if let Err(error) = fetch::validate_semantic_bundle_identity(bundle, semantic_request) {
1613        tracing::warn!(
1614            error = %error,
1615            crate_name = %parsed.crate_name,
1616            semantic_crate_name = %semantic_request.crate_name,
1617            semantic_version = %semantic_request.version,
1618            target = %semantic_request.target,
1619            rustc_version = %semantic_request.rustc_version,
1620            "downloaded stow semantic bundle identity mismatch"
1621        );
1622        // A miss, not an outage: the artifact arrived intact, it just does
1623        // not describe this invocation. Counting identity divergence toward
1624        // the circuit breaker meant a handful of legitimately-unmatched units
1625        // (the proc-macro host graph, typically) tripped it five invocations
1626        // in, and every remaining crate in the build then bypassed the cache
1627        // for the full reset window. Only transport and materialization
1628        // failures say the cache path itself is unhealthy.
1629        log_nonfatal_result(
1630            "failed to record rust cache miss stats",
1631            stats::record_miss(config, &parsed.crate_name).await,
1632        );
1633        return false;
1634    }
1635    match semantic_request_allowed_by_expanded_graph(semantic_request) {
1636        Ok(true) => {}
1637        Ok(false) => {
1638            tracing::warn!(
1639                crate_name = %parsed.crate_name,
1640                semantic_crate_name = %semantic_request.crate_name,
1641                semantic_version = %semantic_request.version,
1642                target = %semantic_request.target,
1643                rustc_version = %semantic_request.rustc_version,
1644                semantic_c_metadata = %bundle.manifest.config.c_metadata,
1645                "rejecting semantic bundle outside expanded dependency graph"
1646            );
1647            return false;
1648        }
1649        Err(error) => {
1650            tracing::warn!(
1651                error = %error,
1652                crate_name = %parsed.crate_name,
1653                semantic_crate_name = %semantic_request.crate_name,
1654                semantic_version = %semantic_request.version,
1655                target = %semantic_request.target,
1656                rustc_version = %semantic_request.rustc_version,
1657                "failed to validate semantic bundle against expanded dependency graph"
1658            );
1659            return false;
1660        }
1661    }
1662
1663    let request = FetchRequest {
1664        target: bundle.manifest.config.target.as_str(),
1665        rustc_version: bundle.manifest.config.rustc_version.as_str(),
1666        c_metadata: bundle.manifest.config.c_metadata.as_str(),
1667        crate_name: bundle.manifest.config.crate_name.as_str(),
1668    };
1669    try_serve_verified_downloaded_bundle(config, parsed, &request, bundle).await
1670}
1671
1672async fn try_serve_verified_downloaded_bundle(
1673    config: &StowConfig,
1674    parsed: &rustc_args::ParsedRustcArgs,
1675    request: &FetchRequest<'_>,
1676    bundle: &fetch::ArtifactBundle,
1677) -> bool {
1678    let cached_bundle = match cache_verified_downloaded_bundle(config, request, bundle).await {
1679        Ok(cached_bundle) => cached_bundle,
1680        Err(error) => {
1681            tracing::warn!(
1682                error = %error,
1683                crate_name = %parsed.crate_name,
1684                target = %request.target,
1685                rustc_version = %request.rustc_version,
1686                "failed to cache verified stow bundle"
1687            );
1688            record_circuit_failure(config).await;
1689            record_lookup_error(config, parsed).await;
1690            return false;
1691        }
1692    };
1693
1694    if let Err(error) =
1695        prune_materialized_aliases_for_cached_closure(config, parsed, request, &cached_bundle).await
1696    {
1697        tracing::warn!(
1698            error = %error,
1699            crate_name = %parsed.crate_name,
1700            target = %request.target,
1701            rustc_version = %request.rustc_version,
1702            "failed to materialize dependency closure aliases for downloaded stow bundle"
1703        );
1704        drop(cached_bundle);
1705        evict_cached_bundle(
1706            config,
1707            parsed,
1708            request,
1709            "failed to evict downloaded stow bundle with incomplete dependency closure aliases",
1710        )
1711        .await;
1712        record_circuit_failure(config).await;
1713        record_lookup_error(config, parsed).await;
1714        return false;
1715    }
1716
1717    materialize_downloaded_bundle(config, parsed, request, cached_bundle).await
1718}
1719
1720/// Write a verified downloaded bundle's artifacts into the target dir.
1721/// `false` means the freshly cached entry could not be materialized — it is
1722/// evicted and counted against the circuit breaker.
1723async fn materialize_downloaded_bundle(
1724    config: &StowConfig,
1725    parsed: &rustc_args::ParsedRustcArgs,
1726    request: &FetchRequest<'_>,
1727    cached_bundle: artifact_cache::CachedArtifactBundle,
1728) -> bool {
1729    match inject::write_artifacts(parsed, &cached_bundle).await {
1730        Ok(()) => finish_downloaded_serve(config, parsed, request, cached_bundle).await,
1731        Err(error) => {
1732            tracing::warn!(
1733                error = %error,
1734                crate_name = %parsed.crate_name,
1735                target = %request.target,
1736                rustc_version = %request.rustc_version,
1737                "failed to materialize verified stow bundle, evicting local cache entry"
1738            );
1739            drop(cached_bundle);
1740            evict_cached_bundle(
1741                config,
1742                parsed,
1743                request,
1744                "failed to evict verified-but-unusable stow cache entry",
1745            )
1746            .await;
1747            record_circuit_failure(config).await;
1748            record_lookup_error(config, parsed).await;
1749            false
1750        }
1751    }
1752}
1753
1754/// The bookkeeping that turns a materialized downloaded bundle into a served
1755/// hit: record outputs, replay rustc's artifact notifications, then count
1756/// the circuit success and the lookup hit.
1757async fn finish_downloaded_serve(
1758    config: &StowConfig,
1759    parsed: &rustc_args::ParsedRustcArgs,
1760    request: &FetchRequest<'_>,
1761    cached_bundle: artifact_cache::CachedArtifactBundle,
1762) -> bool {
1763    if let Err(error) = record_materialized_bundle_outputs(config, parsed, &cached_bundle).await {
1764        tracing::warn!(
1765            error = %error,
1766            crate_name = %parsed.crate_name,
1767            target = %request.target,
1768            rustc_version = %request.rustc_version,
1769            "failed to record materialized downloaded stow artifact outputs"
1770        );
1771        drop(cached_bundle);
1772        evict_cached_bundle(
1773            config,
1774            parsed,
1775            request,
1776            "failed to evict downloaded stow bundle missing materialized output metadata",
1777        )
1778        .await;
1779        record_circuit_failure(config).await;
1780        record_lookup_error(config, parsed).await;
1781        return false;
1782    }
1783    if let Err(error) = emit_cached_rustc_artifact_notifications(parsed).await {
1784        tracing::warn!(
1785            error = %error,
1786            crate_name = %parsed.crate_name,
1787            target = %request.target,
1788            rustc_version = %request.rustc_version,
1789            "failed to replay rustc artifact notifications for downloaded stow bundle"
1790        );
1791        drop(cached_bundle);
1792        evict_cached_bundle(
1793            config,
1794            parsed,
1795            request,
1796            "failed to evict downloaded stow bundle missing rustc artifact notifications",
1797        )
1798        .await;
1799        record_circuit_failure(config).await;
1800        record_lookup_error(config, parsed).await;
1801        return false;
1802    }
1803    record_circuit_success(config).await;
1804    record_lookup_hit(config, parsed).await;
1805    tracing::info!(
1806        crate_name = %parsed.crate_name,
1807        target = %request.target,
1808        rustc_version = %request.rustc_version,
1809        "served rustc invocation from downloaded stow artifact cache"
1810    );
1811    true
1812}
1813
1814async fn build_semantic_fetch_request(
1815    config: &StowConfig,
1816    parsed: &rustc_args::ParsedRustcArgs,
1817    target: &str,
1818    rustc_version: &str,
1819) -> stow_types::error::Result<Option<fetch::SemanticFetchRequest>> {
1820    let Some((crate_name, version)) = detect_registry_crate_version(parsed)? else {
1821        return Ok(None);
1822    };
1823    let dependency_c_metadata_json =
1824        match resolve_dependency_c_metadata_json(config, parsed).await? {
1825            Some(value) => value,
1826            None if parsed.extern_crates.is_empty() => "[]".to_owned(),
1827            None => return Ok(None),
1828        };
1829    let emit = parsed.emit.iter().cloned().collect::<Vec<_>>();
1830    let profile = semantic_request_profile(parsed)?;
1831    let kind = parsed_artifact_kind(parsed)?;
1832    let crate_types = parsed_crate_types(parsed)?;
1833    let features_json = resolve_semantic_features_json(&crate_name, &version, parsed)?;
1834    tracing::debug!(
1835        crate_name = %crate_name,
1836        version = %version,
1837        features_json = %features_json,
1838        dependency_c_metadata_json = %dependency_c_metadata_json,
1839        target = %target,
1840        rustc_version = %rustc_version,
1841        profile = ?profile,
1842        emit = ?emit,
1843        kind = %kind.as_str(),
1844        crate_types = ?crate_types,
1845        "constructed semantic fetch request"
1846    );
1847    Ok(Some(fetch::SemanticFetchRequest {
1848        crate_name,
1849        version,
1850        features_json,
1851        dependency_c_metadata_json,
1852        target: target.to_owned(),
1853        rustc_version: rustc_version.to_owned(),
1854        profile,
1855        emit,
1856        kind,
1857        crate_types,
1858    }))
1859}
1860
1861async fn build_stable_exact_identity(
1862    config: &StowConfig,
1863    parsed: &rustc_args::ParsedRustcArgs,
1864    target: &str,
1865    rustc_version: &str,
1866) -> stow_types::error::Result<Option<stow_types::public_cache::StableRegistryArtifactIdentity>> {
1867    let Some((crate_name, version)) = detect_registry_crate_version(parsed)? else {
1868        trace_identity_inputs(
1869            parsed,
1870            target,
1871            rustc_version,
1872            None,
1873            None,
1874            "not-a-registry-crate",
1875        )
1876        .await;
1877        return Ok(None);
1878    };
1879    let dependency_c_metadata_json =
1880        match resolve_dependency_c_metadata_json(config, parsed).await? {
1881            Some(value) => value,
1882            None if parsed.extern_crates.is_empty() => "[]".to_owned(),
1883            None => {
1884                trace_identity_inputs(
1885                    parsed,
1886                    target,
1887                    rustc_version,
1888                    None,
1889                    None,
1890                    "dependency-identities-unresolved",
1891                )
1892                .await;
1893                return Ok(None);
1894            }
1895        };
1896    let features_json = resolve_semantic_features_json(&crate_name, &version, parsed)?;
1897    let identity = stable_registry_artifact_identity(
1898        parsed,
1899        target,
1900        rustc_version,
1901        &features_json,
1902        &dependency_c_metadata_json,
1903    )?;
1904    trace_identity_inputs(
1905        parsed,
1906        target,
1907        rustc_version,
1908        Some(IdentityTraceInputs {
1909            crate_name: &crate_name,
1910            version: &version,
1911            features_json: &features_json,
1912            dependency_c_metadata_json: &dependency_c_metadata_json,
1913        }),
1914        identity.as_ref(),
1915        "computed",
1916    )
1917    .await;
1918    Ok(identity)
1919}
1920
1921/// Identity inputs captured by [`trace_identity_inputs`].
1922#[derive(serde::Serialize)]
1923struct IdentityTraceInputs<'a> {
1924    crate_name: &'a str,
1925    version: &'a str,
1926    features_json: &'a str,
1927    dependency_c_metadata_json: &'a str,
1928}
1929
1930#[derive(serde::Serialize)]
1931struct IdentityTraceRecord<'a> {
1932    outcome: &'a str,
1933    parsed_crate_name: &'a str,
1934    cargo_c_metadata: Option<&'a str>,
1935    target: &'a str,
1936    rustc_version: &'a str,
1937    emit: Vec<&'a str>,
1938    crate_types: &'a [String],
1939    profile: Option<stow_types::platform::Profile>,
1940    inputs: Option<IdentityTraceInputs<'a>>,
1941    computed_compile_key: Option<&'a str>,
1942    computed_c_metadata: Option<&'a str>,
1943}
1944
1945/// Debugging probe: when `STOW_IDENTITY_TRACE` names a directory, write one
1946/// JSON file per rustc invocation capturing every input that feeds the
1947/// stable compile key, so client-side keys can be diffed against D1 rows
1948/// field by field. Inert when the env var is unset.
1949async fn trace_identity_inputs(
1950    parsed: &rustc_args::ParsedRustcArgs,
1951    target: &str,
1952    rustc_version: &str,
1953    inputs: Option<IdentityTraceInputs<'_>>,
1954    identity: Option<&stow_types::public_cache::StableRegistryArtifactIdentity>,
1955    outcome: &str,
1956) {
1957    let Some(trace_dir) = std::env::var_os("STOW_IDENTITY_TRACE") else {
1958        return;
1959    };
1960    let record = IdentityTraceRecord {
1961        outcome,
1962        parsed_crate_name: &parsed.crate_name,
1963        cargo_c_metadata: parsed.c_metadata.as_deref(),
1964        target,
1965        rustc_version,
1966        emit: parsed.emit.iter().map(String::as_str).collect(),
1967        crate_types: &parsed.crate_types,
1968        profile: normalized_cache_profile(parsed).ok(),
1969        inputs,
1970        computed_compile_key: identity.map(|identity| identity.compile_key.as_str()),
1971        computed_c_metadata: identity.map(|identity| identity.c_metadata.as_str()),
1972    };
1973    let trace_dir = PathBuf::from(trace_dir);
1974    let file_name = format!(
1975        "{}-{}-{}.json",
1976        parsed.crate_name,
1977        parsed.c_metadata.as_deref().unwrap_or("none"),
1978        std::process::id()
1979    );
1980    let Ok(payload) = serde_json::to_vec(&record) else {
1981        return;
1982    };
1983    let _ = async_fs::create_dir_all(&trace_dir).await;
1984    let _ = async_fs::write(trace_dir.join(file_name), payload).await;
1985}
1986
1987/// Resolve the stable identity a finished local build would carry as a cache
1988/// entry, plus the identity inputs `store_local_build_outputs` persists with
1989/// it. `None` means the invocation is not a registry crate or its dependency
1990/// identities have not been recorded yet.
1991async fn resolve_local_build_artifact(
1992    config: &StowConfig,
1993    executable: &OsString,
1994    parsed: &rustc_args::ParsedRustcArgs,
1995) -> stow_types::error::Result<Option<artifact_cache::LocalBuildArtifact>> {
1996    let target = match parsed.target.as_deref() {
1997        Some(target) => target.to_owned(),
1998        None => rustc_args::detect_rustc_host_target(executable)
1999            .await
2000            .map_err(stow_types::error::Error::msg)?,
2001    };
2002    let rustc_version = rustc_args::detect_rustc_version(executable)
2003        .await
2004        .map_err(stow_types::error::Error::msg)?;
2005    let dependency_c_metadata_json =
2006        match resolve_dependency_c_metadata_json(config, parsed).await? {
2007            Some(value) => value,
2008            None if parsed.extern_crates.is_empty() => "[]".to_owned(),
2009            None => return Ok(None),
2010        };
2011    let Some((crate_name, version)) = detect_registry_crate_version(parsed)? else {
2012        return Ok(None);
2013    };
2014    let features_json = resolve_semantic_features_json(&crate_name, &version, parsed)?;
2015    let Some(identity) = stable_registry_artifact_identity(
2016        parsed,
2017        &target,
2018        &rustc_version,
2019        &features_json,
2020        &dependency_c_metadata_json,
2021    )?
2022    else {
2023        return Ok(None);
2024    };
2025    Ok(Some(artifact_cache::LocalBuildArtifact {
2026        target,
2027        rustc_version,
2028        identity,
2029        features_json,
2030        dependency_c_metadata_json,
2031        build_script_out_dir: std::env::var_os("OUT_DIR").map(PathBuf::from),
2032    }))
2033}
2034
2035fn semantic_request_profile(
2036    parsed: &rustc_args::ParsedRustcArgs,
2037) -> stow_types::error::Result<stow_types::platform::Profile> {
2038    normalized_requested_profile(parsed)
2039}
2040
2041fn resolve_semantic_features_json(
2042    crate_name: &str,
2043    version: &str,
2044    parsed: &rustc_args::ParsedRustcArgs,
2045) -> stow_types::error::Result<String> {
2046    if let Some(features_json) =
2047        lookup_expanded_graph_features_json(crate_name, version, &parsed.features)?
2048    {
2049        return Ok(features_json);
2050    }
2051    serde_json::to_string(&parsed.features.iter().cloned().collect::<Vec<_>>())
2052        .wrap_err("serialize semantic rustc features")
2053}
2054
2055fn lookup_expanded_graph_features_json(
2056    crate_name: &str,
2057    version: &str,
2058    parsed_features: &std::collections::BTreeSet<String>,
2059) -> stow_types::error::Result<Option<String>> {
2060    let Some(raw) = std::env::var_os(STOW_EXPANDED_GRAPH_ENV) else {
2061        return Ok(None);
2062    };
2063    let raw = raw
2064        .into_string()
2065        .map_err(|_| stow_types::stow_error!("{STOW_EXPANDED_GRAPH_ENV} must be valid UTF-8"))?;
2066    let entries = serde_json::from_str::<Vec<DependencyGraphEntry>>(&raw)
2067        .wrap_err_with(|| format!("parse {STOW_EXPANDED_GRAPH_ENV}"))?;
2068    let requested_version = semver::Version::parse(version)
2069        .wrap_err_with(|| format!("parse semantic request version `{version}`"))?;
2070    let canonical_name = canonical_crate_name(crate_name);
2071    let mut matches = entries
2072        .into_iter()
2073        .filter(|entry| {
2074            if canonical_crate_name(entry.crate_name.as_str()) != canonical_name
2075                || entry.version != requested_version
2076            {
2077                return false;
2078            }
2079            let expanded_features = entry
2080                .features
2081                .iter()
2082                .cloned()
2083                .collect::<std::collections::BTreeSet<_>>();
2084            parsed_features
2085                .iter()
2086                .all(|feature| expanded_features.contains(feature))
2087        })
2088        .collect::<Vec<_>>();
2089    matches.sort_by(|left, right| {
2090        left.features
2091            .len()
2092            .cmp(&right.features.len())
2093            .then(left.features.cmp(&right.features))
2094    });
2095    if let Some(best) = matches.first() {
2096        let best_len = best.features.len();
2097        matches.retain(|entry| entry.features.len() == best_len);
2098    }
2099    matches.dedup_by(|left, right| left.features == right.features);
2100    match matches.as_slice() {
2101        [] => Ok(None),
2102        [entry] => serde_json::to_string(&entry.features)
2103            .wrap_err("serialize expanded graph semantic features")
2104            .map(Some),
2105        _ => Err(stow_types::stow_error!(
2106            "{STOW_EXPANDED_GRAPH_ENV} contains duplicate exact feature sets for {} {}",
2107            crate_name,
2108            version
2109        )),
2110    }
2111}
2112
2113fn detect_registry_crate_version(
2114    parsed: &rustc_args::ParsedRustcArgs,
2115) -> stow_types::error::Result<Option<(String, String)>> {
2116    shared_detect_registry_crate_version(parsed)
2117}
2118
2119fn canonical_crate_name(name: &str) -> String {
2120    stow_types::public_cache::canonical_crate_name(name)
2121}
2122
2123fn validate_exact_bundle_semantics(
2124    parsed: &rustc_args::ParsedRustcArgs,
2125    profile: &stow_types::platform::Profile,
2126    emit: &[String],
2127    kind: &stow_types::artifact::ArtifactKind,
2128    crate_types: &[stow_types::artifact::RustCrateType],
2129    crate_version: &str,
2130) -> stow_types::error::Result<()> {
2131    // Version first, and unconditionally. The exact lookup is keyed on
2132    // `c_metadata`, which is supposed to encode the crate version — but
2133    // "supposed to" is not a check, and a collision serves one version's
2134    // compiled code for another's. That is how bitflags 2.5.0 came to be
2135    // injected into a bitflags 1.3.2 unit on dust, breaking the build with 126
2136    // conflicting-impl errors inside `nix`. Nothing downstream can detect it,
2137    // so it has to fail closed here.
2138    if let Some((_, requested_version)) = detect_registry_crate_version(parsed)?
2139        && requested_version != crate_version
2140    {
2141        return Err(stow_types::stow_error!(
2142            "exact bundle version mismatch: cached {crate_version}, invocation wants {requested_version}"
2143        ));
2144    }
2145    let expected_profile = normalized_requested_profile(parsed)?;
2146    if profile != &expected_profile {
2147        // Name the diverging field: a profile mismatch evicts the entry and
2148        // counts toward the circuit breaker, so a systematic one silently
2149        // disables the cache for the rest of the build. "Which knob" is the
2150        // whole diagnosis.
2151        return Err(stow_types::stow_error!(
2152            "exact bundle profile mismatch: cached {:?}, invocation wants {:?}",
2153            profile,
2154            expected_profile
2155        ));
2156    }
2157    let expected_emit = parsed
2158        .emit
2159        .iter()
2160        .cloned()
2161        .collect::<std::collections::BTreeSet<_>>();
2162    let actual_emit = emit
2163        .iter()
2164        .cloned()
2165        .collect::<std::collections::BTreeSet<_>>();
2166    if !expected_emit.iter().all(|emit| actual_emit.contains(emit)) {
2167        return Err(stow_types::stow_error!("exact bundle emit mismatch"));
2168    }
2169    let expected_kind = parsed_artifact_kind(parsed)?;
2170    if kind != &expected_kind {
2171        return Err(stow_types::stow_error!(
2172            "exact bundle artifact kind mismatch: expected {}, got {}",
2173            expected_kind.as_str(),
2174            kind.as_str()
2175        ));
2176    }
2177    let expected_crate_types = parsed_crate_types(parsed)?;
2178    if crate_types != expected_crate_types.as_slice() {
2179        return Err(stow_types::stow_error!("exact bundle crate types mismatch"));
2180    }
2181    Ok(())
2182}
2183
2184fn normalized_requested_profile(
2185    parsed: &rustc_args::ParsedRustcArgs,
2186) -> stow_types::error::Result<stow_types::platform::Profile> {
2187    normalized_cache_profile(parsed)
2188}
2189
2190fn semantic_request_allowed_by_expanded_graph(
2191    semantic_request: &fetch::SemanticFetchRequest,
2192) -> stow_types::error::Result<bool> {
2193    let Some(raw) = std::env::var_os(STOW_EXPANDED_GRAPH_ENV) else {
2194        return Ok(true);
2195    };
2196    let raw = raw
2197        .into_string()
2198        .map_err(|_| stow_types::stow_error!("{STOW_EXPANDED_GRAPH_ENV} must be valid UTF-8"))?;
2199    let entries = serde_json::from_str::<Vec<DependencyGraphEntry>>(&raw)
2200        .wrap_err_with(|| format!("parse {STOW_EXPANDED_GRAPH_ENV}"))?;
2201    Ok(entries.iter().any(|entry| {
2202        canonical_crate_name(entry.crate_name.as_str())
2203            == canonical_crate_name(&semantic_request.crate_name)
2204            && entry.version.to_string() == semantic_request.version
2205            && serde_json::to_string(&entry.features)
2206                .is_ok_and(|features_json| features_json == semantic_request.features_json)
2207    }))
2208}
2209
2210pub(crate) fn parsed_artifact_kind(
2211    parsed: &rustc_args::ParsedRustcArgs,
2212) -> stow_types::error::Result<stow_types::artifact::ArtifactKind> {
2213    let crate_types = parsed_crate_types(parsed)?;
2214    if crate_types
2215        .iter()
2216        .any(|crate_type| matches!(crate_type, stow_types::artifact::RustCrateType::ProcMacro))
2217    {
2218        return Ok(stow_types::artifact::ArtifactKind::ProcMacro);
2219    }
2220    if crate_types
2221        .iter()
2222        .any(|crate_type| matches!(crate_type, stow_types::artifact::RustCrateType::Dylib))
2223    {
2224        return Ok(stow_types::artifact::ArtifactKind::Dylib);
2225    }
2226    if crate_types.iter().any(|crate_type| {
2227        matches!(
2228            crate_type,
2229            stow_types::artifact::RustCrateType::Lib | stow_types::artifact::RustCrateType::Rlib
2230        )
2231    }) {
2232        return Ok(stow_types::artifact::ArtifactKind::Rlib);
2233    }
2234    Err(stow_types::stow_error!(
2235        "unsupported semantic artifact kind for crate types {:?}",
2236        parsed.crate_types
2237    ))
2238}
2239
2240pub(crate) fn parsed_crate_types(
2241    parsed: &rustc_args::ParsedRustcArgs,
2242) -> stow_types::error::Result<Vec<stow_types::artifact::RustCrateType>> {
2243    let mut crate_types = parsed
2244        .crate_types
2245        .iter()
2246        .map(|crate_type| match crate_type.as_str() {
2247            "lib" => Ok(stow_types::artifact::RustCrateType::Lib),
2248            "rlib" => Ok(stow_types::artifact::RustCrateType::Rlib),
2249            "dylib" => Ok(stow_types::artifact::RustCrateType::Dylib),
2250            "cdylib" => Ok(stow_types::artifact::RustCrateType::Cdylib),
2251            "staticlib" => Ok(stow_types::artifact::RustCrateType::Staticlib),
2252            "proc-macro" => Ok(stow_types::artifact::RustCrateType::ProcMacro),
2253            other => Err(stow_types::stow_error!(
2254                "unsupported rust crate type `{other}`"
2255            )),
2256        })
2257        .collect::<stow_types::error::Result<std::collections::BTreeSet<_>>>()?
2258        .into_iter()
2259        .collect::<Vec<_>>();
2260    crate_types.sort();
2261    Ok(crate_types)
2262}
2263
2264pub(crate) fn log_nonfatal_result(context: &'static str, result: stow_types::error::Result<()>) {
2265    if let Err(error) = result {
2266        tracing::warn!(error = %error, "{context}");
2267    }
2268}
2269
2270fn parse_cli_or_exit(args: &[std::ffi::OsString]) -> stow_types::error::Result<Cli> {
2271    match Cli::try_parse_from(args.iter().cloned()) {
2272        Ok(cli) => Ok(cli),
2273        Err(error) => {
2274            let kind = error.kind();
2275            error.print()?;
2276            if matches!(
2277                kind,
2278                clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
2279            ) {
2280                std::process::exit(0);
2281            }
2282            std::process::exit(2);
2283        }
2284    }
2285}
2286
2287pub(crate) use commands::detect_wrapper_commands;
2288
2289#[derive(Debug, PartialEq, Eq)]
2290struct RustcArtifactNotification {
2291    artifact: PathBuf,
2292    emit: &'static str,
2293}
2294
2295fn cached_rustc_artifact_notifications(
2296    parsed: &rustc_args::ParsedRustcArgs,
2297) -> stow_types::error::Result<Vec<RustcArtifactNotification>> {
2298    let mut notifications = Vec::new();
2299    if parsed.emit.contains("dep-info") {
2300        notifications.push(RustcArtifactNotification {
2301            artifact: parsed.output_dep_info_path().ok_or_else(|| {
2302                stow_types::stow_error!("cached rustc invocation is missing dep-info path")
2303            })?,
2304            emit: "dep-info",
2305        });
2306    }
2307    if parsed.emit.contains("metadata") {
2308        notifications.push(RustcArtifactNotification {
2309            artifact: parsed.output_rmeta_path().ok_or_else(|| {
2310                stow_types::stow_error!(
2311                    "cached rustc invocation cannot emit metadata for crate types {:?}",
2312                    parsed.crate_types
2313                )
2314            })?,
2315            emit: "metadata",
2316        });
2317    }
2318    if parsed.emit.contains("link") {
2319        let artifact = parsed
2320            .output_link_path()
2321            .map_err(stow_types::error::Error::msg)?
2322            .ok_or_else(|| {
2323                stow_types::stow_error!(
2324                    "cached rustc invocation cannot emit link artifact for crate types {:?}",
2325                    parsed.crate_types
2326                )
2327            })?;
2328        notifications.push(RustcArtifactNotification {
2329            artifact,
2330            emit: "link",
2331        });
2332    }
2333    Ok(notifications)
2334}
2335
2336async fn emit_cached_rustc_artifact_notifications(
2337    parsed: &rustc_args::ParsedRustcArgs,
2338) -> stow_types::error::Result<()> {
2339    if !parsed.requests_json_artifact_notifications() {
2340        return Ok(());
2341    }
2342
2343    let notifications = cached_rustc_artifact_notifications(parsed)?;
2344    let mut stderr = tokio::io::stderr();
2345    for notification in notifications {
2346        let message = serde_json::json!({
2347            "$message_type": "artifact",
2348            "artifact": notification.artifact,
2349            "emit": notification.emit,
2350        });
2351        let line = message.to_string();
2352        stderr
2353            .write_all(line.as_bytes())
2354            .await
2355            .wrap_err("write rustc artifact notification")?;
2356        stderr
2357            .write_all(b"\n")
2358            .await
2359            .wrap_err("terminate rustc artifact notification")?;
2360    }
2361    stderr
2362        .flush()
2363        .await
2364        .wrap_err("flush rustc artifact notifications")
2365}
2366
2367pub(crate) fn write_stdout(message: &str) -> stow_types::error::Result<()> {
2368    let mut stdout = io::stdout().lock();
2369    stdout.write_all(message.as_bytes())?;
2370    stdout.flush()?;
2371    Ok(())
2372}
2373
2374fn install_tracing() -> TracingGuard {
2375    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
2376    // stderr, never stdout: `run()` also serves the `rustc` / `cc` wrapper
2377    // subcommands, whose stdout must stay byte-identical to the wrapped
2378    // compiler's. Cargo hashes `rustc -vV` stdout into every unit's
2379    // `-C metadata`, so a single log line there changes the cache key of
2380    // every crate in the build on every invocation.
2381    let fmt_layer = tracing_subscriber::fmt::layer()
2382        .with_target(false)
2383        .with_writer(std::io::stderr);
2384
2385    let chrome = std::env::var_os(STOW_TRACE_FILE_ENV).map(|path| {
2386        tracing_chrome::ChromeLayerBuilder::new()
2387            .file(PathBuf::from(path))
2388            .include_args(true)
2389            .build()
2390    });
2391
2392    if let Some((chrome_layer, chrome_guard)) = chrome {
2393        let _ = tracing_subscriber::registry()
2394            .with(filter)
2395            .with(fmt_layer)
2396            .with(chrome_layer)
2397            .try_init();
2398        TracingGuard {
2399            _chrome: Some(chrome_guard),
2400        }
2401    } else {
2402        let _ = tracing_subscriber::registry()
2403            .with(filter)
2404            .with(fmt_layer)
2405            .try_init();
2406        TracingGuard { _chrome: None }
2407    }
2408}
2409
2410#[cfg(test)]
2411mod tests {
2412    use std::ffi::OsString;
2413    use std::path::PathBuf;
2414
2415    use super::{
2416        cached_rustc_artifact_notifications, should_install_tracing_for_args,
2417        strip_cargo_subcommand_word,
2418    };
2419    use crate::rustc_args::ParsedRustcArgs;
2420
2421    fn args(parts: &[&str]) -> Vec<std::ffi::OsString> {
2422        parts.iter().map(std::ffi::OsString::from).collect()
2423    }
2424
2425    fn env_value(value: Option<&str>) -> Option<OsString> {
2426        value.map(OsString::from)
2427    }
2428
2429    #[test]
2430    fn cargo_subcommand_invocation_drops_the_repeated_subcommand_word() {
2431        assert_eq!(
2432            strip_cargo_subcommand_word(args(&["/usr/bin/cargo-stow", "stow", "check"])),
2433            args(&["/usr/bin/cargo-stow", "check"])
2434        );
2435        assert_eq!(
2436            strip_cargo_subcommand_word(args(&["cargo-stow.exe", "stow", "check"])),
2437            args(&["cargo-stow.exe", "check"])
2438        );
2439        // Only cargo repeats the word; a direct `stow stow` is a user error
2440        // clap reports, and `cargo-stow check` stays as typed.
2441        assert_eq!(
2442            strip_cargo_subcommand_word(args(&["stow", "stow", "check"])),
2443            args(&["stow", "stow", "check"])
2444        );
2445        assert_eq!(
2446            strip_cargo_subcommand_word(args(&["cargo-stow", "check"])),
2447            args(&["cargo-stow", "check"])
2448        );
2449    }
2450
2451    #[test]
2452    fn wrapper_tracing_stays_disabled_under_rust_log_by_default() {
2453        assert!(!should_install_tracing_for_args(
2454            &args(&["stow", "rustc"]),
2455            env_value(Some("debug")).as_deref(),
2456            env_value(None),
2457        ));
2458        assert!(!should_install_tracing_for_args(
2459            &args(&["stow", "cc"]),
2460            env_value(Some("debug")).as_deref(),
2461            env_value(None),
2462        ));
2463    }
2464
2465    #[test]
2466    fn wrapper_tracing_requires_explicit_opt_in() {
2467        assert!(should_install_tracing_for_args(
2468            &args(&["stow", "rustc"]),
2469            env_value(None).as_deref(),
2470            env_value(Some("1")),
2471        ));
2472        assert!(!should_install_tracing_for_args(
2473            &args(&["stow", "rustc"]),
2474            env_value(None).as_deref(),
2475            env_value(Some("0")),
2476        ));
2477    }
2478
2479    #[test]
2480    fn top_level_commands_keep_tracing_behavior() {
2481        assert!(should_install_tracing_for_args(
2482            &args(&["stow", "check"]),
2483            env_value(None).as_deref(),
2484            env_value(None),
2485        ));
2486        assert!(should_install_tracing_for_args(
2487            &args(&["stow", "check"]),
2488            env_value(Some("debug")).as_deref(),
2489            env_value(None),
2490        ));
2491    }
2492
2493    #[test]
2494    fn cached_rlib_notifications_match_rustc_protocol() {
2495        let parsed = ParsedRustcArgs::parse(&args(&[
2496            "--crate-name",
2497            "autocfg",
2498            "--crate-type",
2499            "lib",
2500            "--out-dir",
2501            "/tmp/out",
2502            "--emit",
2503            "dep-info,metadata,link",
2504            "--json",
2505            "diagnostic-rendered-ansi,artifacts,future-incompat",
2506            "-C",
2507            "metadata=abc123",
2508            "-C",
2509            "extra-filename=-xyz789",
2510        ]))
2511        .expect("parse rustc args");
2512
2513        let notifications =
2514            cached_rustc_artifact_notifications(&parsed).expect("build artifact notifications");
2515
2516        assert_eq!(
2517            notifications,
2518            vec![
2519                super::RustcArtifactNotification {
2520                    artifact: PathBuf::from("/tmp/out/autocfg-xyz789.d"),
2521                    emit: "dep-info",
2522                },
2523                super::RustcArtifactNotification {
2524                    artifact: PathBuf::from("/tmp/out/libautocfg-xyz789.rmeta"),
2525                    emit: "metadata",
2526                },
2527                super::RustcArtifactNotification {
2528                    artifact: PathBuf::from("/tmp/out/libautocfg-xyz789.rlib"),
2529                    emit: "link",
2530                },
2531            ]
2532        );
2533        assert!(parsed.requests_json_artifact_notifications());
2534    }
2535
2536    #[test]
2537    fn cached_proc_macro_notifications_use_dylib_output() {
2538        let parsed = ParsedRustcArgs::parse(&args(&[
2539            "--crate-name",
2540            "serde_derive",
2541            "--crate-type",
2542            "proc-macro",
2543            "--target",
2544            "aarch64-apple-darwin",
2545            "--out-dir",
2546            "/tmp/out",
2547            "--emit",
2548            "dep-info,link",
2549            "--json",
2550            "artifacts",
2551            "-C",
2552            "metadata=pm123",
2553            "-C",
2554            "extra-filename=-xyz789",
2555        ]))
2556        .expect("parse rustc args");
2557
2558        let notifications =
2559            cached_rustc_artifact_notifications(&parsed).expect("build artifact notifications");
2560
2561        assert_eq!(
2562            notifications,
2563            vec![
2564                super::RustcArtifactNotification {
2565                    artifact: PathBuf::from("/tmp/out/serde_derive-xyz789.d"),
2566                    emit: "dep-info",
2567                },
2568                super::RustcArtifactNotification {
2569                    artifact: PathBuf::from("/tmp/out/libserde_derive-xyz789.dylib"),
2570                    emit: "link",
2571                },
2572            ]
2573        );
2574    }
2575}