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