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