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