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