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