1#![recursion_limit = "256"]
16
17mod admission;
18mod artifact_cache;
19mod budget;
20pub 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";
86const STOW_TRACE_FILE_ENV: &str = "STOW_TRACE_FILE";
92
93struct TracingGuard {
97 _chrome: Option<tracing_chrome::FlushGuard>,
98}
99
100pub fn run() -> stow_types::error::Result<()> {
114 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 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
154const MAIN_STACK_BYTES: usize = 16 * 1024 * 1024;
160
161fn 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 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
197fn 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
337fn 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
359enum Outcome {
366 Served,
368 Compile(Box<PostCompile>),
371}
372
373struct PostCompile {
377 executable: OsString,
378 parsed: Option<rustc_args::ParsedRustcArgs>,
381}
382
383impl PostCompile {
384 fn raw(executable: &OsString) -> Self {
386 Self {
387 executable: executable.clone(),
388 parsed: None,
389 }
390 }
391}
392
393async 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
413async 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
520enum UnparseableInvocation {
527 Probe(String),
529 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
547fn 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#[derive(Debug, Default)]
581pub(crate) struct BuildSupervisor {
582 env_cache: WrapperEnvCache,
585 observations: std::sync::Mutex<Vec<artifact_cache::ObservedUnit>>,
586}
587
588impl BuildSupervisor {
589 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 self.observations
623 .lock()
624 .expect("observations mutex")
625 .push(unit);
626 }
627 }
628}
629
630#[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 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 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
660async 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
689async fn run_rustc_standalone(command: &WrapperCommandArgs) -> stow_types::error::Result<()> {
692 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
724async 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 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
841struct WrapperEnvironment {
846 env_base: std::sync::Arc<WrapperEnvBase>,
849 circuit_tripped: bool,
854 target: String,
855 rustc_version: String,
856 cache_key: String,
858 request_c_metadata: String,
861 semantic_request: Option<fetch::SemanticFetchRequest>,
864 host_glibc: Option<stow_types::glibc::GlibcVersion>,
868}
869
870#[derive(Debug)]
876pub(crate) struct WrapperEnvBase {
877 rustc: OsString,
880 pub(crate) config: StowConfig,
881 pub(crate) host_target: String,
885 pub(crate) rustc_version: String,
886 _version_lease: artifact_cache::RustcVersionLease,
887}
888
889#[derive(Debug, Default)]
894pub(crate) struct WrapperEnvCache {
895 env_base: tokio::sync::OnceCell<Option<std::sync::Arc<WrapperEnvBase>>>,
897 local_base: tokio::sync::OnceCell<Option<std::sync::Arc<WrapperEnvBase>>>,
900}
901
902impl WrapperEnvCache {
903 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 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
938async 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
986enum RemoteServe {
988 Served,
990 Miss,
992 Bypass,
995 Refused,
1000}
1001
1002async 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
1078async 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 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 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
1142async 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 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
1220async 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 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
1299async 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
1310async 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
1335async 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
1344async 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
1352async 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
1361async 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
1374async 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
1382async 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
1402async 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
1452enum CcResolution {
1456 Explicit(OsString),
1457 Resolve(cc::CcKind),
1458}
1459
1460fn 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#[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#[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#[derive(Clone, Copy)]
1747enum EntryOrigin {
1748 ExactKey,
1752 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
1845async 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
1879async 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 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 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
2316async 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 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
2506async 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
2541async 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#[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
2742async 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
2784async 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 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#[derive(Debug)]
2921enum BundleMismatch {
2922 Profile {
2927 cached: String,
2929 wanted: String,
2931 },
2932 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
2949fn 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 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 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 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 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 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}