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