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