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