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