Skip to main content

stow_cli/
lib.rs

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