1#![deny(missing_docs)]
30
31use mbx_cache_core::{
32 CacheDigest, FileDigestCache, PathAliases, PathMapping as SharedPathMapping,
33 PathNormalizationError, canonical_json, normalize_mapped_path as normalize_shared_path,
34 normalize_resolved_mapped_path_with as normalize_resolved_shared_path_with,
35 resolve_path_mappings,
36};
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38use std::collections::{BTreeMap, BTreeSet};
39use std::ffi::OsString;
40use std::path::{Component, Path, PathBuf};
41use thiserror::Error;
42
43mod dep_info;
44
45pub use dep_info::{DepInfoCommand, DiscoveredInputs, RustcDepInfo};
46
47pub const ACTION_SCHEMA_VERSION: u8 = 1;
49pub const ADAPTER_VERSION: u8 = 2;
56
57impl BypassReason {
58 pub fn kind(&self) -> &'static str {
63 self.into()
64 }
65
66 pub fn remediation(&self) -> Option<&'static str> {
72 match self {
73 Self::Incremental => Some(
74 "Set `MBX_INCREMENTAL=0`; mbx will then disable Cargo incremental state and cache the compilation.",
75 ),
76 Self::UnportableNativeLink(detail) if detail.contains("linker") => Some(
77 "Make the native linker resolvable on `PATH`, or configure a linker mbx can identify for this target.",
78 ),
79 Self::UnportableNativeLink(_) => Some(
80 "Remove the reported `-C` option from the active Cargo profile or `RUSTFLAGS` to make these links cacheable.",
81 ),
82 Self::UnknownFlag(_) | Self::UnknownCodegenOption(_) => Some(
83 "Remove the reported compiler option, or upgrade mbx if the option should be modeled.",
84 ),
85 Self::UnmodeledLinkArgument(_) => Some(
86 "Remove the reported linker argument, or keep these links uncached if the argument is required.",
87 ),
88 Self::UnmappedAbsolutePath(_) => Some(
89 "Move the input under the workspace, target, Cargo, toolchain, or home roots so mbx can give it a portable cache name.",
90 ),
91 _ => None,
92 }
93 }
94}
95
96impl From<PathNormalizationError> for BypassReason {
97 fn from(reason: PathNormalizationError) -> Self {
98 match reason {
99 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
100 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
101 }
102 }
103}
104
105const SUPPORTED_CODEGEN_OPTIONS: &[&str] = &[
106 "codegen-units",
107 "control-flow-guard",
108 "debug-assertions",
109 "debuginfo",
110 "default-linker-libraries",
111 "embed-bitcode",
112 "extra-filename",
113 "force-frame-pointers",
114 "force-unwind-tables",
115 "instrument-coverage",
116 "link-arg",
117 "link-args",
118 "link-dead-code",
119 "link-self-contained",
120 "linker-plugin-lto",
121 "lto",
122 "metadata",
123 "no-prepopulate-passes",
124 "opt-level",
125 "overflow-checks",
126 "panic",
127 "prefer-dynamic",
128 "relocation-model",
129 "rpath",
130 "save-temps",
131 "soft-float",
132 "split-debuginfo",
133 "split-dwarf-kind",
134 "strip",
135 "symbol-mangling-version",
136 "target-cpu",
137 "target-feature",
138 "tls-model",
139];
140
141const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
142const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
143const MAX_DECODED_PREDICTION_BYTES: usize = 16 * 1024 * 1024;
147const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
148
149const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
152 "wasm32-unknown-unknown",
153 "wasm32-wasip1",
154 "wasm32-wasip1-threads",
155 "wasm32-wasip2",
156 "wasm32v1-none",
157 "wasm64-unknown-unknown",
158];
159
160#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
161#[strum(serialize_all = "kebab-case")]
162#[non_exhaustive]
172pub enum BypassReason {
173 #[error("rustc argument {index} is not valid UTF-8")]
175 NonUtf8Argument {
176 index: usize,
178 },
179 #[error("could not model rustc response file: {0}")]
181 ResponseFile(String),
182 #[error("rustc flag is not modeled by the cache adapter: {0}")]
184 UnknownFlag(String),
185 #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
187 UnknownCodegenOption(String),
188 #[error("rustc flag requires a value: {0}")]
190 MissingValue(String),
191 #[error("rustc invocation is a compiler query, not a compilation")]
193 CompilerQuery,
194 #[error("rustc invocation reads source from standard input")]
196 StandardInput,
197 #[error("rustc invocation has no source input")]
199 MissingInput,
200 #[error("rustc invocation has multiple source inputs")]
202 MultipleInputs,
203 #[error("incremental compilation cannot be combined with action caching")]
205 Incremental,
206 #[error("rustc crate type is not cacheable yet: {0}")]
208 UnsupportedCrateType(String),
209 #[error("rustc output type is not cacheable yet: {0}")]
211 UnsupportedEmit(String),
212 #[error("rustc invocation does not emit a cacheable artifact")]
214 NoCacheableOutput,
215 #[error("rustc invocation does not emit dependency information")]
217 NoDepInfo,
218 #[error("rustc output paths do not share one directory")]
220 SplitOutputDirectories,
221 #[error("rustc output path has no file name: {0}")]
223 InvalidOutputPath(PathBuf),
224 #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
226 ImplicitEmitWithOutputFile(PathBuf),
227 #[error("native library on a linked output is not cacheable yet: {0}")]
230 NativeLibrary(String),
231 #[error("native static library was not found on any search path: {0}")]
234 MissingNativeLibrary(String),
235 #[error("static library file names are not known for a custom target: {0}")]
238 CustomTargetNativeLibrary(String),
239 #[error("rustc output name does not distinguish a program from a library: {0}")]
241 AmbiguousOutputName(PathBuf),
242 #[error("native link is not reproducible across checkouts: {0}")]
244 UnportableNativeLink(String),
245 #[error("rustc link argument is not modeled by the cache adapter: {0}")]
247 UnmodeledLinkArgument(String),
248 #[error("rustc search path kind is not cacheable yet: {0}")]
250 UnsupportedSearchPath(String),
251 #[error("rustc extern does not identify an input artifact: {0}")]
253 UnresolvedExtern(String),
254 #[error("absolute path has no stable cache mapping: {0}")]
256 UnmappedAbsolutePath(PathBuf),
257 #[error("cache key paths must be valid UTF-8: {0}")]
259 NonUtf8Path(PathBuf),
260 #[error("cache action working directory must be absolute: {0}")]
262 RelativeWorkingDirectory(PathBuf),
263 #[error("cache path mapping must use an absolute root: {0}")]
265 RelativePathMapping(PathBuf),
266 #[error("cache path mapping placeholder is invalid: {0}")]
268 InvalidPathPlaceholder(String),
269 #[error("required compiler input was not provided: {0}")]
271 MissingRequiredInput(String),
272 #[error("compiler input has an invalid digest: {0}")]
274 InvalidInputDigest(String),
275 #[error("compiler input appears more than once with different content: {0}")]
277 ConflictingInput(String),
278 #[error("rustc dep-info is malformed: {0}")]
280 MalformedDepInfo(String),
281 #[error("failed to read rustc dep-info {path}: {message}")]
283 DepInfoRead {
284 path: PathBuf,
286 message: String,
288 },
289 #[error("rustc dep-info output path must be absolute: {0}")]
291 RelativeDepInfoPath(PathBuf),
292 #[error("rustc dep-info output path cannot contain a comma: {0}")]
294 UnsafeDepInfoPath(PathBuf),
295 #[error("failed to read compiler input {path}: {message}")]
297 InputRead {
298 path: PathBuf,
300 message: String,
302 },
303 #[error("compiler input changed after discovery: {0}")]
305 InputChanged(PathBuf),
306 #[error("compiler input was modified during compilation: {0}")]
308 InputModifiedDuringCompilation(PathBuf),
309 #[error("discovered inputs were collected from a different working directory")]
311 DiscoveryWorkingDirectory,
312 #[error("compiler environment input has conflicting values: {0}")]
314 ConflictingEnvironment(String),
315 #[error("failed to serialize the rustc action: {0}")]
317 Serialization(String),
318 #[error("rustc action prediction is unsupported")]
320 UnsupportedPrediction,
321 #[error("rustc action prediction contains an invalid input path: {0}")]
323 InvalidPredictedInput(String),
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
327enum Argument {
328 Plain(String),
329 Path {
330 flag: String,
331 path: PathBuf,
332 },
333 SearchPath {
334 kind: String,
335 path: PathBuf,
336 },
337 Extern {
338 name: String,
339 path: Option<PathBuf>,
340 },
341 Emit(Vec<Emit>),
342 RemapPath {
343 from: PathBuf,
344 to: String,
345 },
346 InstallName(String),
348 OsoPrefix {
351 path: PathBuf,
352 trailing_slash: bool,
353 },
354 FuseLd(String),
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361struct Emit {
362 kind: String,
363 path: Option<PathBuf>,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct RustcInvocation {
372 arguments: Vec<Argument>,
373 source: PathBuf,
374 required_inputs: Vec<PathBuf>,
375 crate_name: String,
376 extra_filename: String,
377 out_dir: Option<PathBuf>,
378 explicit_output: Option<PathBuf>,
379 emits: Vec<Emit>,
380 target: Option<String>,
381 link_output: LinkOutput,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385enum LinkOutput {
386 Library,
387 WasmExecutable,
388 NativeExecutable,
389 NativeProcMacro,
390 NativeSharedLibrary,
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
403#[non_exhaustive]
404pub struct ParseOptions {
405 pub cache_native_links: bool,
408 pub custom_target_search: bool,
414}
415
416impl ParseOptions {
417 pub fn caching_native_links(enabled: bool) -> Self {
419 Self {
420 cache_native_links: enabled,
421 ..Self::default()
422 }
423 }
424
425 pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
428 self.custom_target_search = enabled;
429 self
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct RustcOutputs {
436 pub directory: PathBuf,
438 pub files: Vec<PathBuf>,
440 pub dep_info: PathBuf,
442}
443
444impl RustcInvocation {
445 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
454 Self::parse_with(arguments, ParseOptions::default())
455 }
456
457 pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
462 Ok(expand_response_files(arguments)?.arguments)
463 }
464
465 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
468 let expanded = expand_response_files(arguments)?;
469 Parser::new(&expanded.arguments, options).parse()
470 }
471
472 pub fn links_natively(&self) -> bool {
475 matches!(
476 self.link_output,
477 LinkOutput::NativeExecutable
478 | LinkOutput::NativeProcMacro
479 | LinkOutput::NativeSharedLibrary
480 )
481 }
482
483 pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
487 if !cfg!(target_os = "macos")
488 || self.link_output != LinkOutput::NativeProcMacro
489 || self
490 .arguments
491 .iter()
492 .any(|arg| matches!(arg, Argument::InstallName(_)))
493 {
494 return None;
495 }
496 let file = outputs
497 .files
498 .iter()
499 .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
500 let name = file.file_name()?.to_str()?;
501 if !safe_install_name(name) {
502 return None;
503 }
504 Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
505 }
506
507 pub fn linker_override(&self) -> Option<&Path> {
510 self.arguments.iter().find_map(|argument| {
511 let Argument::Plain(value) = argument else {
512 return None;
513 };
514 value.strip_prefix("--codegen=linker=").map(Path::new)
515 })
516 }
517
518 pub fn fuse_ld(&self) -> Option<&str> {
521 self.arguments.iter().find_map(|argument| {
522 let Argument::FuseLd(selection) = argument else {
523 return None;
524 };
525 Some(selection.as_str())
526 })
527 }
528
529 fn emits_windows_pdb(&self) -> bool {
530 if !cfg!(windows) || !self.links_natively() {
531 return false;
532 }
533 let mut enabled = false;
534 for argument in &self.arguments {
535 let Argument::Plain(value) = argument else {
536 continue;
537 };
538 if value == "-g" {
539 enabled = true;
540 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
541 enabled = !matches!(value, "0" | "none");
542 }
543 }
544 enabled
545 }
546
547 fn native_search_is_inert(&self) -> bool {
562 self.link_output == LinkOutput::Library
563 }
564
565 pub fn source(&self) -> &Path {
567 &self.source
568 }
569
570 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
576 self.required_inputs
577 .iter()
578 .map(|path| absolute_path(path, working_dir))
579 .collect()
580 }
581
582 pub fn target(&self) -> Option<&str> {
584 self.target.as_deref()
585 }
586
587 pub fn crate_name(&self) -> &str {
589 &self.crate_name
590 }
591
592 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
605 let linked = self
606 .arguments
607 .iter()
608 .filter_map(|argument| match argument {
609 Argument::Extern {
610 path: Some(path), ..
611 } => Some(path.as_path()),
612 _ => None,
613 })
614 .collect::<BTreeSet<_>>();
615 let owned = discovered
616 .inputs
617 .iter()
618 .filter(|input| !linked.contains(input.path.as_path()))
619 .map(|input| (input.path.as_path(), &input.digest))
620 .collect::<BTreeMap<_, _>>();
621 let mut bytes = Vec::new();
622 for (path, digest) in owned {
623 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
624 bytes.push(0);
625 bytes.extend_from_slice(digest.key().as_bytes());
626 bytes.push(0);
627 }
628 CacheDigest::blake3(&bytes)
629 }
630
631 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
636 if !working_dir.is_absolute() {
637 return Err(BypassReason::RelativeWorkingDirectory(
638 working_dir.to_path_buf(),
639 ));
640 }
641 let explicit_output = self
642 .explicit_output
643 .as_deref()
644 .map(|path| absolute_path(path, working_dir));
645 let output_directory = explicit_output
646 .as_deref()
647 .and_then(Path::parent)
648 .map(Path::to_path_buf)
649 .or_else(|| {
650 self.out_dir
651 .as_deref()
652 .map(|path| absolute_path(path, working_dir))
653 })
654 .unwrap_or_else(|| normalize_components(working_dir));
655 if let Some(output) = &explicit_output
659 && self.emits.iter().any(|emit| {
660 emit.path.is_none()
661 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
662 })
663 {
664 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
665 }
666 let mut files = BTreeSet::new();
667 let mut dep_info = None;
668 for emit in &self.emits {
669 if emit.kind == "dep-info" {
670 let path = emit.path.as_ref().map_or_else(
671 || {
672 explicit_output.clone().map_or_else(
673 || {
674 output_directory
675 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
676 },
677 |path| path.with_extension("d"),
678 )
679 },
680 |path| absolute_path(path, working_dir),
681 );
682 if path.file_name().is_none() {
683 return Err(BypassReason::InvalidOutputPath(path));
684 }
685 dep_info = Some(path);
686 continue;
687 }
688 let (prefix, extension) = match emit.kind.as_str() {
689 "link" => match self.link_output {
690 LinkOutput::Library => ("lib", "rlib"),
691 LinkOutput::WasmExecutable => ("", "wasm"),
692 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
693 LinkOutput::NativeProcMacro | LinkOutput::NativeSharedLibrary => (
694 std::env::consts::DLL_PREFIX,
695 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
696 ),
697 },
698 "metadata" => ("lib", "rmeta"),
699 _ => continue,
700 };
701 let path = if let Some(path) = &emit.path {
702 absolute_path(path, working_dir)
703 } else {
704 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
705 output_directory.join(if extension.is_empty() {
706 name
707 } else {
708 format!("{name}.{extension}")
709 })
710 };
711 if path.file_name().is_none() {
712 return Err(BypassReason::InvalidOutputPath(path));
713 }
714 if path.parent() != Some(output_directory.as_path()) {
715 return Err(BypassReason::SplitOutputDirectories);
716 }
717 let has_library_extension = path
722 .extension()
723 .and_then(|extension| extension.to_str())
724 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
725 || (cfg!(windows)
726 && path
727 .file_stem()
728 .and_then(|stem| Path::new(stem).extension())
729 .and_then(|extension| extension.to_str())
730 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
731 if emit.kind == "link"
732 && !matches!(self.link_output, LinkOutput::Library)
733 && has_library_extension
734 {
735 return Err(BypassReason::AmbiguousOutputName(path));
736 }
737 if emit.kind == "link" && self.emits_windows_pdb() {
738 files.insert(path.with_extension("pdb"));
739 }
740 files.insert(path);
741 }
742 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
743 if dep_info.parent() != Some(output_directory.as_path()) {
744 return Err(BypassReason::SplitOutputDirectories);
745 }
746 Ok(RustcOutputs {
747 directory: output_directory,
748 files: files.into_iter().collect(),
749 dep_info,
750 })
751 }
752
753 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
759 self.action_linked_by(context, None)
760 }
761
762 pub fn action_linked_by(
771 &self,
772 context: ActionContext,
773 linker: Option<LinkerIdentity>,
774 ) -> Result<RustcAction, BypassReason> {
775 ActionBuilder::new(self, context).linked_by(linker).build()
776 }
777
778 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
780 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
781 let bytes = canonical_json(&descriptor)
782 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
783 Ok(CacheDigest::blake3(&bytes))
784 }
785
786 pub fn prediction(
789 &self,
790 context: &ActionContext,
791 discovered: &DiscoveredInputs,
792 ) -> Result<RustcInputPrediction, BypassReason> {
793 let builder = ActionBuilder::new(self, context.clone());
794 builder.validate_mappings()?;
795 let mut native_directories = BTreeSet::new();
796 for argument in &self.arguments {
797 if let Argument::SearchPath { kind, path } = argument
798 && kind == "native"
799 {
800 match builder.normalize_path(path) {
801 Ok(normalized) => {
802 native_directories.insert(normalized);
803 }
804 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
810 }
811 Err(error) => return Err(error),
812 }
813 }
814 }
815 let mut inputs = BTreeSet::new();
822 for input in &discovered.inputs {
823 let normalized = builder.normalize_path(&input.path)?;
824 if !under_any_directory(&normalized, &native_directories) {
825 inputs.insert(normalized);
826 }
827 }
828 inputs.extend(
829 native_directories
830 .into_iter()
831 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
832 );
833 Ok(RustcInputPrediction {
834 version: 4,
835 inputs: inputs.into_iter().collect(),
836 environment: discovered.environment.keys().cloned().collect(),
837 compiler_duration_ns: 0,
838 crate_name: String::new(),
839 })
840 }
841}
842
843pub fn is_build_script_crate_name(crate_name: &str) -> bool {
847 crate_name
848 .strip_prefix("build_script_")
849 .is_some_and(|stem| !stem.is_empty())
850}
851
852impl RustcOutputs {
853 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
863 is_build_script_crate_name(crate_name)
864 .then(|| self.files.iter().find(|path| self.is_executable(path)))
865 .flatten()
866 .map(PathBuf::as_path)
867 }
868
869 pub fn is_executable(&self, path: &Path) -> bool {
877 self.files.iter().any(|output| output == path)
878 && !matches!(
879 path.extension().and_then(|extension| extension.to_str()),
880 Some("rlib" | "rmeta")
881 )
882 }
883}
884
885#[derive(Debug, Clone, PartialEq, Eq)]
886pub struct PathMapping {
888 pub root: PathBuf,
890 pub placeholder: String,
892}
893
894impl PathMapping {
895 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
897 Self {
898 root: root.into(),
899 placeholder: placeholder.into(),
900 }
901 }
902
903 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
906 let mut ordered = mappings.to_vec();
907 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
908 ordered
909 }
910}
911
912fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
913 mappings
914 .iter()
915 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
916 .collect()
917}
918
919pub fn normalize_mapped_path(
926 path: &Path,
927 working_dir: &Path,
928 mappings: &[PathMapping],
929) -> Result<String, BypassReason> {
930 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
931}
932
933#[derive(Debug, Clone, PartialEq, Eq)]
934pub struct CompilerIdentity {
936 pub toolchain: String,
938 pub rustc_version: String,
940 pub host: String,
942 pub driver: Option<String>,
944}
945
946#[derive(Debug, Clone, PartialEq, Eq)]
948pub struct ActionInput {
949 pub path: PathBuf,
951 pub digest: CacheDigest,
953}
954
955#[derive(Debug, Clone, PartialEq, Eq)]
957pub struct ActionContext {
958 pub compiler: CompilerIdentity,
960 pub working_dir: PathBuf,
962 pub path_mappings: Vec<PathMapping>,
964 pub environment: BTreeMap<String, Option<String>>,
966 pub portable_environment: BTreeSet<String>,
973 pub inputs: Vec<ActionInput>,
975}
976
977#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
987#[serde(deny_unknown_fields)]
988pub struct LinkerIdentity {
989 pub driver: String,
991 pub driver_version: String,
993 pub linker_version: String,
995 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
997 pub crt_objects: BTreeMap<String, CacheDigest>,
998 #[serde(skip_serializing_if = "Option::is_none", default)]
1000 pub sdk: Option<String>,
1001 #[serde(skip_serializing_if = "Option::is_none", default)]
1003 pub deployment_target: Option<String>,
1004}
1005
1006#[derive(Debug, Clone, PartialEq, Eq)]
1008pub struct RustcAction {
1009 pub digest: CacheDigest,
1011 pub bytes: Vec<u8>,
1013}
1014
1015#[derive(Debug, Clone, PartialEq, Eq)]
1018pub struct RustcInputPrediction {
1019 pub version: u8,
1021 pub inputs: Vec<String>,
1023 pub environment: Vec<String>,
1025 pub compiler_duration_ns: u64,
1028 pub crate_name: String,
1030}
1031
1032#[derive(Serialize, Deserialize)]
1033#[serde(deny_unknown_fields)]
1034struct RustcInputPredictionWire {
1035 version: u8,
1036 inputs: Vec<String>,
1037 environment: Vec<String>,
1038 #[serde(default, skip_serializing_if = "is_zero")]
1039 compiler_duration_ns: u64,
1040 #[serde(default, skip_serializing_if = "String::is_empty")]
1041 crate_name: String,
1042}
1043
1044impl Serialize for RustcInputPrediction {
1045 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1046 where
1047 S: Serializer,
1048 {
1049 RustcInputPredictionWire {
1050 version: self.version,
1051 inputs: if self.version == 4 {
1052 compact_prediction_inputs(&self.inputs)
1053 } else {
1054 self.inputs.clone()
1055 },
1056 environment: self.environment.clone(),
1057 compiler_duration_ns: self.compiler_duration_ns,
1058 crate_name: self.crate_name.clone(),
1059 }
1060 .serialize(serializer)
1061 }
1062}
1063
1064impl<'de> Deserialize<'de> for RustcInputPrediction {
1065 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1066 where
1067 D: Deserializer<'de>,
1068 {
1069 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1070 let inputs = if wire.version == 4 {
1071 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1072 } else {
1073 wire.inputs
1074 };
1075 Ok(Self {
1076 version: wire.version,
1077 inputs,
1078 environment: wire.environment,
1079 compiler_duration_ns: wire.compiler_duration_ns,
1080 crate_name: wire.crate_name,
1081 })
1082 }
1083}
1084
1085fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1086 let mut previous = "";
1087 inputs
1088 .iter()
1089 .map(|input| {
1090 let mut shared = previous
1091 .bytes()
1092 .zip(input.bytes())
1093 .take_while(|(left, right)| left == right)
1094 .count();
1095 while !input.is_char_boundary(shared) {
1096 shared -= 1;
1097 }
1098 let compact = format!("{shared}:{}", &input[shared..]);
1099 previous = input;
1100 compact
1101 })
1102 .collect()
1103}
1104
1105fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1106 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1107 let mut decoded_bytes = 0_usize;
1108 for input in inputs {
1109 let (shared_text, suffix) = input
1110 .split_once(':')
1111 .ok_or("compact rustc prediction input has no prefix length")?;
1112 let shared: usize = shared_text
1113 .parse()
1114 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1115 if shared.to_string() != shared_text
1116 || shared > expanded.last().map_or(0, String::len)
1117 || expanded
1118 .last()
1119 .is_some_and(|previous| !previous.is_char_boundary(shared))
1120 {
1121 return Err("compact rustc prediction prefix is not canonical");
1122 }
1123 let mut path = expanded
1124 .last()
1125 .map_or_else(String::new, |previous| previous[..shared].to_string());
1126 path.push_str(suffix);
1127 decoded_bytes = decoded_bytes
1128 .checked_add(path.len())
1129 .ok_or("compact rustc prediction is too large")?;
1130 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1131 return Err("compact rustc prediction is too large");
1132 }
1133 expanded.push(path);
1134 }
1135 Ok(expanded)
1136}
1137
1138fn is_zero(value: &u64) -> bool {
1139 *value == 0
1140}
1141
1142impl RustcInputPrediction {
1143 pub fn discover(
1146 &self,
1147 working_dir: &Path,
1148 path_mappings: &[PathMapping],
1149 digests: &dyn FileDigestCache,
1150 ) -> Result<DiscoveredInputs, BypassReason> {
1151 if !matches!(self.version, 2..=4) {
1152 return Err(BypassReason::UnsupportedPrediction);
1153 }
1154 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1155 return Err(BypassReason::UnsupportedPrediction);
1156 }
1157 let mut paths = BTreeSet::new();
1158 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1159 let mut native_bytes = 0_u64;
1160 for path in &self.inputs {
1161 if self.version >= 3
1162 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1163 {
1164 let directory = denormalize_path(path, path_mappings)?;
1165 dep_info::collect_native_directory(
1166 &directory,
1167 &admitted_roots,
1168 &mut paths,
1169 &mut native_bytes,
1170 )?;
1171 } else {
1172 paths.insert(denormalize_path(path, path_mappings)?);
1173 }
1174 }
1175 let environment = self
1176 .environment
1177 .iter()
1178 .map(|name| {
1179 if name.is_empty() || name.contains(['=', '\0']) {
1180 return Err(BypassReason::UnsupportedPrediction);
1181 }
1182 let value = std::env::var_os(name)
1183 .map(|value| {
1184 value
1185 .into_string()
1186 .map_err(|_| BypassReason::UnsupportedPrediction)
1187 })
1188 .transpose()?;
1189 Ok((name.clone(), value))
1190 })
1191 .collect::<Result<BTreeMap<_, _>, _>>()?;
1192 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1193 }
1194}
1195
1196#[derive(Serialize)]
1197struct ActionDescriptor {
1198 version: u8,
1199 kind: &'static str,
1200 adapter_version: u8,
1201 compiler: CompilerDescriptor,
1202 arguments: Vec<String>,
1203 environment: BTreeMap<String, Option<String>>,
1204 inputs: Vec<InputDescriptor>,
1205 #[serde(skip_serializing_if = "Option::is_none")]
1208 linker: Option<LinkerIdentity>,
1209}
1210
1211#[derive(Serialize)]
1212struct InvocationDescriptor {
1213 version: u8,
1214 kind: &'static str,
1215 adapter_version: u8,
1216 compiler: CompilerDescriptor,
1217 arguments: Vec<String>,
1218 required_inputs: Vec<String>,
1219}
1220
1221#[derive(Serialize)]
1222struct CompilerDescriptor {
1223 toolchain: String,
1224 rustc_version: String,
1225 host: String,
1226 #[serde(skip_serializing_if = "Option::is_none")]
1227 driver: Option<String>,
1228}
1229
1230#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1231struct InputDescriptor {
1232 path: String,
1233 digest: CacheDigest,
1234}
1235
1236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1238enum StaticLibraryNaming {
1239 Msvc,
1241 Unix,
1243}
1244
1245struct Parser<'a> {
1246 arguments: &'a [OsString],
1247 index: usize,
1248 parsed: Vec<Argument>,
1249 source: Option<PathBuf>,
1250 crate_types: Vec<String>,
1251 emits: Vec<Emit>,
1252 required_inputs: Vec<PathBuf>,
1253 native_libraries: Vec<String>,
1254 test: bool,
1255 crate_name: Option<String>,
1256 extra_filename: String,
1257 out_dir: Option<PathBuf>,
1258 explicit_output: Option<PathBuf>,
1259 target: Option<String>,
1260 options: ParseOptions,
1261}
1262
1263struct ExpandedArguments {
1264 arguments: Vec<OsString>,
1265}
1266
1267#[derive(Default)]
1268struct ResponseExpander {
1269 shell_argfiles: bool,
1270 next_is_unstable_option: bool,
1271 arguments: Vec<OsString>,
1272}
1273
1274impl ResponseExpander {
1275 fn push(&mut self, argument: String) {
1276 if self.next_is_unstable_option {
1277 self.shell_argfiles |= argument == "shell-argfiles";
1278 self.next_is_unstable_option = false;
1279 } else if let Some(option) = argument.strip_prefix("-Z") {
1280 if option.is_empty() {
1281 self.next_is_unstable_option = true;
1282 } else {
1283 self.shell_argfiles |= option == "shell-argfiles";
1284 }
1285 }
1286 self.arguments.push(argument.into());
1287 }
1288}
1289
1290fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1293 let mut expanded = ResponseExpander::default();
1294 for (index, argument) in arguments.iter().enumerate() {
1295 let argument = argument
1296 .to_str()
1297 .ok_or(BypassReason::NonUtf8Argument { index })?;
1298 let Some(argfile) = argument.strip_prefix('@') else {
1299 expanded.push(argument.to_string());
1300 continue;
1301 };
1302 let (path, shell) = match argfile.split_once(':') {
1303 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1304 _ => (argfile, false),
1305 };
1306 let contents = std::fs::read_to_string(path).map_err(|error| {
1307 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1308 })?;
1309 if shell {
1310 let arguments = shlex::split(&contents).ok_or_else(|| {
1311 BypassReason::ResponseFile(format!(
1312 "invalid shell-style arguments in {}",
1313 Path::new(path).display()
1314 ))
1315 })?;
1316 for argument in arguments {
1317 expanded.push(argument);
1318 }
1319 } else {
1320 for argument in contents.lines() {
1321 expanded.push(argument.to_string());
1322 }
1323 }
1324 }
1325 Ok(ExpandedArguments {
1326 arguments: expanded.arguments,
1327 })
1328}
1329
1330impl<'a> Parser<'a> {
1331 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1332 Self {
1333 arguments,
1334 options,
1335 index: 0,
1336 parsed: Vec::new(),
1337 source: None,
1338 crate_types: Vec::new(),
1339 emits: Vec::new(),
1340 required_inputs: Vec::new(),
1341 native_libraries: Vec::new(),
1342 test: false,
1343 crate_name: None,
1344 extra_filename: String::new(),
1345 out_dir: None,
1346 explicit_output: None,
1347 target: None,
1348 }
1349 }
1350
1351 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1352 while self.index < self.arguments.len() {
1353 let value = self.current()?.to_string();
1354 self.index += 1;
1355 if let Some(long) = value.strip_prefix("--") {
1356 self.parse_long(long)?;
1357 } else if value.starts_with('-') && value != "-" {
1358 self.parse_short(&value)?;
1359 } else {
1360 self.parse_input(&value)?;
1361 }
1362 }
1363
1364 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1365 let link_output = self.classify()?;
1366 self.require_native_libraries(link_output)?;
1367 let crate_name = self.crate_name.clone().map_or_else(
1368 || {
1369 source
1370 .file_stem()
1371 .and_then(|name| name.to_str())
1372 .map(|name| name.replace('-', "_"))
1373 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1374 },
1375 Ok,
1376 )?;
1377 self.required_inputs.push(source.clone());
1378 Ok(RustcInvocation {
1379 arguments: self.parsed,
1380 source,
1381 required_inputs: self.required_inputs,
1382 crate_name,
1383 extra_filename: self.extra_filename,
1384 out_dir: self.out_dir,
1385 explicit_output: self.explicit_output,
1386 emits: self.emits,
1387 target: self.target,
1388 link_output,
1389 })
1390 }
1391
1392 fn current(&self) -> Result<&str, BypassReason> {
1393 self.arguments[self.index]
1394 .to_str()
1395 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1396 }
1397
1398 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1399 if let Some(value) = inline {
1400 if value.is_empty() {
1401 return Err(BypassReason::MissingValue(flag.into()));
1402 }
1403 return Ok(value.into());
1404 }
1405 if self.index >= self.arguments.len() {
1406 return Err(BypassReason::MissingValue(flag.into()));
1407 }
1408 let value = self.current()?.to_string();
1409 self.index += 1;
1410 Ok(value)
1411 }
1412
1413 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1414 let (flag, inline) = value
1415 .split_once('=')
1416 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1417 let rendered_flag = format!("--{flag}");
1418 match flag {
1419 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1420 "test" => {
1421 self.test = true;
1422 self.parsed.push(Argument::Plain(rendered_flag));
1423 Ok(())
1424 }
1425 "verbose" => {
1426 self.parsed.push(Argument::Plain(rendered_flag));
1427 Ok(())
1428 }
1429 "crate-name" => {
1430 let value = self.take_value(&rendered_flag, inline)?;
1431 self.crate_name = Some(value.clone());
1432 self.parsed
1433 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1434 Ok(())
1435 }
1436 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1437 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1438 | "deny" | "forbid" | "cap-lints" => {
1439 let value = self.take_value(&rendered_flag, inline)?;
1440 self.parsed
1441 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1442 Ok(())
1443 }
1444 "target" => {
1445 let value = self.take_value(&rendered_flag, inline)?;
1446 let effective = self.target.is_none();
1450 if effective {
1451 self.target = Some(value.clone());
1452 }
1453 if value.ends_with(".json") || value.contains(['/', '\\']) {
1454 let path = PathBuf::from(value);
1455 if effective {
1456 self.required_inputs.push(path.clone());
1457 }
1458 self.parsed.push(Argument::Path {
1459 flag: rendered_flag,
1460 path,
1461 });
1462 } else {
1463 self.parsed
1464 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1465 }
1466 Ok(())
1467 }
1468 "crate-type" => {
1469 let value = self.take_value(&rendered_flag, inline)?;
1470 self.crate_types
1471 .extend(value.split(',').map(ToOwned::to_owned));
1472 self.parsed
1473 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1474 Ok(())
1475 }
1476 "emit" => {
1477 let value = self.take_value(&rendered_flag, inline)?;
1478 let emits = parse_emits(&value);
1479 self.emits.extend(emits.clone());
1480 self.parsed.push(Argument::Emit(emits));
1481 Ok(())
1482 }
1483 "out-dir" => {
1484 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1485 self.out_dir = Some(path.clone());
1486 self.parsed.push(Argument::Path {
1487 flag: rendered_flag,
1488 path,
1489 });
1490 Ok(())
1491 }
1492 "sysroot" => {
1493 let path = self.take_value(&rendered_flag, inline)?;
1494 self.parsed.push(Argument::Path {
1495 flag: rendered_flag,
1496 path: path.into(),
1497 });
1498 Ok(())
1499 }
1500 "extern" => {
1501 let value = self.take_value(&rendered_flag, inline)?;
1502 let (name, path) = value
1503 .split_once('=')
1504 .map_or((value.as_str(), None), |(name, path)| {
1505 (name, Some(PathBuf::from(path)))
1506 });
1507 if let Some(path) = &path {
1508 self.required_inputs.push(path.clone());
1509 }
1510 self.parsed.push(Argument::Extern {
1511 name: name.into(),
1512 path,
1513 });
1514 Ok(())
1515 }
1516 "remap-path-prefix" => {
1517 let value = self.take_value(&rendered_flag, inline)?;
1518 let Some((from, to)) = value.split_once('=') else {
1519 return Err(BypassReason::MissingValue(rendered_flag));
1520 };
1521 self.parsed.push(Argument::RemapPath {
1522 from: from.into(),
1523 to: to.into(),
1524 });
1525 Ok(())
1526 }
1527 "codegen" => {
1528 let value = self.take_value(&rendered_flag, inline)?;
1529 self.parse_codegen(&value)
1530 }
1531 "jobs-frontend" => {
1532 let value = self.take_value(&rendered_flag, inline)?;
1533 self.parsed
1534 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1535 Ok(())
1536 }
1537 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1538 }
1539 }
1540
1541 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1542 if let Some(attached) = value.strip_prefix("-Z") {
1543 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1544 match option.as_str() {
1545 "embed-metadata=no"
1549 | "embed-metadata=yes"
1550 | "force-unstable-if-unmarked"
1551 | "shell-argfiles"
1552 | "unstable-options" => {
1553 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1554 return Ok(());
1555 }
1556 "threads" | "threads=" => {
1557 return Err(BypassReason::MissingValue("-Zthreads".into()));
1558 }
1559 _ => {}
1560 }
1561 if option.starts_with("threads=") {
1562 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1563 return Ok(());
1564 }
1565 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1566 }
1567 match value {
1568 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1571 "-g" | "-O" | "-v" => {
1572 self.parsed.push(Argument::Plain(value.into()));
1573 return Ok(());
1574 }
1575 _ => {}
1576 }
1577 for (short, long) in [
1578 ("-A", "--allow"),
1579 ("-W", "--warn"),
1580 ("-D", "--deny"),
1581 ("-F", "--forbid"),
1582 ] {
1583 if let Some(attached) = value.strip_prefix(short) {
1584 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1585 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1586 return Ok(());
1587 }
1588 }
1589 if let Some(attached) = value.strip_prefix("-C") {
1590 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1591 return self.parse_codegen(&option);
1592 }
1593 if let Some(attached) = value.strip_prefix("-L") {
1594 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1595 let (kind, path) = search
1596 .split_once('=')
1597 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1598 if !matches!(kind, "dependency" | "native") {
1599 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1600 }
1601 self.parsed.push(Argument::SearchPath {
1602 kind: kind.into(),
1603 path: path.into(),
1604 });
1605 return Ok(());
1606 }
1607 if let Some(attached) = value.strip_prefix("-l") {
1608 let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1609 self.parsed.push(Argument::Plain(format!("-l{library}")));
1610 self.native_libraries.push(library);
1611 return Ok(());
1612 }
1613 if let Some(attached) = value.strip_prefix("-o") {
1614 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1615 self.explicit_output = Some(path.clone().into());
1616 self.parsed.push(Argument::Path {
1617 flag: "-o".into(),
1618 path: path.into(),
1619 });
1620 return Ok(());
1621 }
1622 Err(BypassReason::UnknownFlag(value.into()))
1623 }
1624
1625 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1626 let name = value.split_once('=').map_or(value, |(name, _)| name);
1627 if name == "incremental" {
1628 return Err(BypassReason::Incremental);
1629 }
1630 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1631 return Err(BypassReason::UnknownCodegenOption(name.into()));
1632 }
1633 if name == "linker-plugin-lto"
1639 && let Some((_, selection)) = value.split_once('=')
1640 && !matches!(
1641 selection,
1642 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1643 )
1644 {
1645 return Err(BypassReason::UnknownCodegenOption(format!(
1646 "linker-plugin-lto={selection}"
1647 )));
1648 }
1649 if name == "link-arg"
1656 && let Some((_, option)) = value.split_once('=')
1657 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1658 && !selection.is_empty()
1659 && !selection.contains(',')
1660 && !self.parsed.iter().any(
1661 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1662 )
1663 {
1664 if !self.parsed.iter().any(
1665 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1666 ) {
1667 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1668 }
1669 return Ok(());
1670 }
1671 if name == "link-arg"
1674 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1675 && safe_install_name(option)
1676 {
1677 self.parsed.push(Argument::InstallName(option.to_owned()));
1678 return Ok(());
1679 }
1680 if matches!(name, "link-arg" | "link-args")
1685 && let Some((_, option)) = value.split_once('=')
1686 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1687 && !prefix.trim_end_matches('/').is_empty()
1688 && !prefix.contains(',')
1689 {
1690 let trailing_slash = prefix.ends_with('/');
1691 self.parsed.push(Argument::OsoPrefix {
1692 path: PathBuf::from(prefix.trim_end_matches('/')),
1693 trailing_slash,
1694 });
1695 return Ok(());
1696 }
1697 self.parsed
1698 .push(Argument::Plain(format!("--codegen={value}")));
1699 if name == "extra-filename" {
1700 self.extra_filename = value
1701 .split_once('=')
1702 .map_or(String::new(), |(_, value)| value.to_string());
1703 }
1704 Ok(())
1705 }
1706
1707 fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1722 for library in std::mem::take(&mut self.native_libraries) {
1723 if link_output != LinkOutput::Library {
1724 return Err(BypassReason::NativeLibrary(library));
1725 }
1726 let Some((kind, name)) = library.split_once('=') else {
1734 continue;
1735 };
1736 let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1737 match kind {
1738 "static" => {}
1739 "dylib" | "framework" | "link-arg" => continue,
1740 _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1741 }
1742 let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1745 let (mut verbatim, mut bundle) = (false, true);
1746 for modifier in modifiers.split(',') {
1747 match modifier {
1748 "+verbatim" => verbatim = true,
1749 "-verbatim" => verbatim = false,
1750 "+bundle" => bundle = true,
1751 "-bundle" => bundle = false,
1752 _ => {}
1753 }
1754 }
1755 if !bundle {
1756 continue;
1757 }
1758 let archive = self
1759 .find_native_static_library(name, verbatim)
1760 .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1761 .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1762 self.required_inputs.push(archive);
1763 }
1764 Ok(())
1765 }
1766
1767 fn find_native_static_library(
1779 &self,
1780 name: &str,
1781 verbatim: bool,
1782 ) -> Result<Option<PathBuf>, ()> {
1783 let file_names = if verbatim {
1784 vec![name.to_owned()]
1785 } else {
1786 match self.static_library_naming() {
1787 Some(StaticLibraryNaming::Msvc) => {
1788 vec![format!("{name}.lib"), format!("lib{name}.a")]
1789 }
1790 Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1791 None => return Err(()),
1792 }
1793 };
1794 Ok(self
1795 .parsed
1796 .iter()
1797 .filter_map(|argument| match argument {
1798 Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1799 _ => None,
1800 })
1801 .flat_map(|directory| {
1802 file_names
1803 .iter()
1804 .map(move |file_name| directory.join(file_name))
1805 })
1806 .find(|candidate| candidate.is_file()))
1807 }
1808
1809 fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1817 let Some(target) = self.target.as_deref() else {
1818 return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1819 StaticLibraryNaming::Msvc
1820 } else {
1821 StaticLibraryNaming::Unix
1822 });
1823 };
1824 if target.ends_with(".json")
1825 || target.contains(['/', '\\'])
1826 || self.options.custom_target_search
1827 {
1828 return None;
1829 }
1830 Some(if target.contains("msvc") || target.ends_with("-uefi") {
1833 StaticLibraryNaming::Msvc
1834 } else {
1835 StaticLibraryNaming::Unix
1836 })
1837 }
1838
1839 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1840 if value == "-" {
1841 return Err(BypassReason::StandardInput);
1842 }
1843 if self.source.replace(value.into()).is_some() {
1844 return Err(BypassReason::MultipleInputs);
1845 }
1846 Ok(())
1847 }
1848
1849 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1850 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1859 let builds_a_library = !self.test
1860 && !self.crate_types.is_empty()
1861 && self
1862 .crate_types
1863 .iter()
1864 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1865 let link_output = if never_links || builds_a_library {
1866 LinkOutput::Library
1867 } else if self
1868 .target
1869 .as_deref()
1870 .is_some_and(compiler_bundled_wasm_target)
1871 && ((self.test && self.crate_types.is_empty())
1872 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1873 {
1874 if self.parsed.iter().any(|argument| match argument {
1875 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1876 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1877 !matches!(
1878 value.rsplit_once('=').map(|(_, value)| value),
1879 Some("y" | "yes" | "on" | "true")
1880 )
1881 }
1882 _ => false,
1883 }) {
1884 return Err(BypassReason::UnknownCodegenOption(
1885 "link-self-contained".into(),
1886 ));
1887 }
1888 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1889 && self.parsed.iter().any(|argument| {
1890 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1891 })
1892 {
1893 return Err(BypassReason::UnknownCodegenOption(
1894 "target-feature=-crt-static".into(),
1895 ));
1896 }
1897 LinkOutput::WasmExecutable
1901 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1902 self.check_native_link_is_portable()?;
1903 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1904 LinkOutput::NativeProcMacro
1905 } else if matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib") {
1906 LinkOutput::NativeSharedLibrary
1907 } else {
1908 LinkOutput::NativeExecutable
1909 }
1910 } else if self.test {
1911 return Err(BypassReason::UnsupportedCrateType("test".into()));
1912 } else {
1913 return Err(BypassReason::UnsupportedCrateType(
1914 self.crate_types
1915 .iter()
1916 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1917 .cloned()
1918 .unwrap_or_else(|| "bin".into()),
1919 ));
1920 };
1921 if link_output != LinkOutput::Library
1930 && let Some(option) = self.first_link_argument(link_output)
1931 {
1932 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1933 }
1934 if self
1935 .parsed
1936 .iter()
1937 .any(|arg| matches!(arg, Argument::InstallName(_)))
1938 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
1939 {
1940 return Err(BypassReason::UnmodeledLinkArgument(
1941 "link-arg=-Wl,-install_name".into(),
1942 ));
1943 }
1944 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1951 let Argument::FuseLd(selection) = argument else {
1952 return None;
1953 };
1954 Some(selection.as_str())
1955 }) && !matches!(
1956 link_output,
1957 LinkOutput::Library
1958 | LinkOutput::NativeExecutable
1959 | LinkOutput::NativeProcMacro
1960 | LinkOutput::NativeSharedLibrary
1961 ) {
1962 return Err(BypassReason::UnmodeledLinkArgument(format!(
1963 "link-arg=-fuse-ld={selection}"
1964 )));
1965 }
1966 if self.parsed.iter().any(|argument| {
1967 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1968 }) && !matches!(
1969 link_output,
1970 LinkOutput::NativeExecutable
1971 | LinkOutput::NativeProcMacro
1972 | LinkOutput::NativeSharedLibrary
1973 ) {
1974 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1975 }
1976 if !matches!(
1980 link_output,
1981 LinkOutput::Library
1982 | LinkOutput::NativeExecutable
1983 | LinkOutput::NativeProcMacro
1984 | LinkOutput::NativeSharedLibrary
1985 ) && self
1986 .parsed
1987 .iter()
1988 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1989 {
1990 return Err(BypassReason::UnmodeledLinkArgument(
1991 "link-arg=-Wl,-oso_prefix".into(),
1992 ));
1993 }
1994 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1995 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1996 _ => None,
1997 }) {
1998 return Err(BypassReason::UnresolvedExtern(name.clone()));
1999 }
2000 if let Some(emit) = self
2001 .emits
2002 .iter()
2003 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
2004 {
2005 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
2006 }
2007 if !self
2008 .emits
2009 .iter()
2010 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
2011 {
2012 return Err(BypassReason::NoCacheableOutput);
2013 }
2014 Ok(link_output)
2015 }
2016}
2017
2018impl Parser<'_> {
2019 fn first_link_argument(&self, link_output: LinkOutput) -> Option<&str> {
2022 self.parsed.iter().find_map(|argument| {
2023 let Argument::Plain(value) = argument else {
2024 return None;
2025 };
2026 let option = value.strip_prefix("--codegen=")?;
2027 let (name, value) = option.split_once('=').unwrap_or((option, ""));
2028 (matches!(name, "link-arg" | "link-args")
2029 && !(name == "link-arg" && elf_keyword_is_modeled(link_output, value)))
2030 .then_some(option)
2031 })
2032 }
2033
2034 fn oso_prefix_covers_outputs(&self) -> bool {
2043 let Some(directory) = self
2044 .out_dir
2045 .as_deref()
2046 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2047 else {
2048 return false;
2049 };
2050 if !directory.is_absolute() {
2051 return false;
2052 }
2053 let directory = normalize_components(directory);
2054 self.parsed.iter().any(|argument| {
2055 let Argument::OsoPrefix { path, .. } = argument else {
2056 return false;
2057 };
2058 path.is_absolute()
2059 && (directory.starts_with(normalize_components(path))
2060 || std::fs::canonicalize(&directory)
2061 .ok()
2062 .zip(std::fs::canonicalize(path).ok())
2063 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2064 })
2065 }
2066
2067 fn links_a_native_artifact(&self) -> bool {
2074 self.target.is_none()
2075 && self.emits.iter().any(|emit| emit.kind == "link")
2080 && ((self.test && self.crate_types.is_empty())
2081 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro"))
2082 || (cfg!(target_os = "linux")
2083 && matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib")))
2084 }
2085
2086 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2093 for argument in &self.parsed {
2094 let Argument::Plain(value) = argument else {
2095 continue;
2096 };
2097 let (name, value) = if value == "-g" {
2100 ("debuginfo", Some("2"))
2101 } else if let Some(option) = value.strip_prefix("--codegen=") {
2102 match option.split_once('=') {
2103 Some((name, value)) => (name, Some(value)),
2104 None => (option, None),
2108 }
2109 } else {
2110 continue;
2111 };
2112 let unportable = match name {
2113 "split-debuginfo" => match value {
2119 Some("off") => false,
2120 Some("unpacked") if cfg!(target_os = "macos") => {
2121 !self.oso_prefix_covers_outputs()
2122 }
2123 _ => true,
2124 },
2125 "debuginfo" if cfg!(target_os = "macos") => {
2134 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2135 }
2136 "rpath" => is_enabled(value),
2144 "prefer-dynamic" => {
2145 is_enabled(value)
2146 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2147 }
2148 "link-self-contained" => true,
2151 _ => false,
2152 };
2153 if unportable {
2154 return Err(BypassReason::UnportableNativeLink(match value {
2155 Some(value) => format!("{name}={value}"),
2156 None => name.to_owned(),
2157 }));
2158 }
2159 }
2160 Ok(())
2161 }
2162}
2163
2164fn safe_install_name(name: &str) -> bool {
2165 !name.is_empty()
2166 && name
2167 .bytes()
2168 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2169}
2170
2171fn is_enabled(value: Option<&str>) -> bool {
2174 matches!(value, None | Some("y" | "yes" | "on" | "true"))
2175}
2176
2177fn elf_keyword_is_modeled(link_output: LinkOutput, value: &str) -> bool {
2187 cfg!(target_os = "linux")
2188 && matches!(
2189 link_output,
2190 LinkOutput::NativeExecutable
2191 | LinkOutput::NativeProcMacro
2192 | LinkOutput::NativeSharedLibrary
2193 )
2194 && value.strip_prefix("-Wl,-z,").is_some_and(|keyword| {
2195 matches!(
2196 keyword,
2197 "defs"
2198 | "lazy"
2199 | "nodelete"
2200 | "nodlopen"
2201 | "noexecstack"
2202 | "norelro"
2203 | "now"
2204 | "origin"
2205 | "relro"
2206 )
2207 })
2208}
2209
2210fn compiler_bundled_wasm_target(target: &str) -> bool {
2211 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2212}
2213
2214fn parse_emits(value: &str) -> Vec<Emit> {
2215 value
2216 .split(',')
2217 .map(|emit| {
2218 let (kind, path) = emit
2219 .split_once('=')
2220 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2221 Emit {
2222 kind: kind.into(),
2223 path,
2224 }
2225 })
2226 .collect()
2227}
2228
2229struct ActionBuilder<'a> {
2230 invocation: &'a RustcInvocation,
2231 context: ActionContext,
2232 mappings: Vec<SharedPathMapping>,
2233 aliases: PathAliases,
2237 linker: Option<LinkerIdentity>,
2238}
2239
2240impl<'a> ActionBuilder<'a> {
2241 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2242 context.path_mappings = PathMapping::ordered(&context.path_mappings);
2243 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2244 Self {
2245 linker: None,
2246 invocation,
2247 mappings,
2248 aliases: PathAliases::default(),
2249 context,
2250 }
2251 }
2252
2253 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2254 self.linker = linker;
2255 self
2256 }
2257
2258 fn build(self) -> Result<RustcAction, BypassReason> {
2259 self.validate_mappings()?;
2260 let invocation = self.invocation_descriptor()?;
2261 let environment = self.environment_descriptor()?;
2262
2263 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2264 for input in &self.context.inputs {
2265 input
2266 .digest
2267 .validate()
2268 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2269 let path = self.normalize_path(&input.path)?;
2270 if inputs
2271 .insert(path.clone(), input.digest.clone())
2272 .is_some_and(|existing| existing != input.digest)
2273 {
2274 return Err(BypassReason::ConflictingInput(path));
2275 }
2276 }
2277 let required = self
2278 .invocation
2279 .required_inputs
2280 .iter()
2281 .map(|path| self.normalize_path(path))
2282 .collect::<Result<BTreeSet<_>, _>>()?;
2283 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2284 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2285 }
2286 let inputs = inputs
2287 .into_iter()
2288 .map(|(path, digest)| InputDescriptor { path, digest })
2289 .collect();
2290 if self.invocation.links_natively() && self.linker.is_none() {
2293 return Err(BypassReason::UnportableNativeLink(
2294 "linker identity is unknown".into(),
2295 ));
2296 }
2297 let descriptor = ActionDescriptor {
2298 version: ACTION_SCHEMA_VERSION,
2299 kind: "rustc",
2300 adapter_version: ADAPTER_VERSION,
2301 compiler: invocation.compiler,
2302 arguments: invocation.arguments,
2303 environment,
2304 inputs,
2305 linker: self.linker.clone(),
2306 };
2307 let bytes = canonical_json(&descriptor)
2308 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2309 let digest = CacheDigest::blake3(&bytes);
2310 Ok(RustcAction { digest, bytes })
2311 }
2312
2313 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2314 self.validate_mappings()?;
2315 let arguments = self
2316 .invocation
2317 .arguments
2318 .iter()
2319 .map(|argument| self.normalize_argument(argument))
2320 .collect::<Result<Vec<_>, _>>()?;
2321 let required_inputs = self
2322 .invocation
2323 .required_inputs
2324 .iter()
2325 .map(|path| self.normalize_path(path))
2326 .collect::<Result<BTreeSet<_>, _>>()?
2327 .into_iter()
2328 .collect();
2329 Ok(InvocationDescriptor {
2330 version: ACTION_SCHEMA_VERSION,
2331 kind: "rustc",
2332 adapter_version: ADAPTER_VERSION,
2333 compiler: CompilerDescriptor {
2334 toolchain: self.context.compiler.toolchain.clone(),
2335 rustc_version: self.context.compiler.rustc_version.clone(),
2336 host: self.context.compiler.host.clone(),
2337 driver: self.context.compiler.driver.clone(),
2338 },
2339 arguments,
2340 required_inputs,
2341 })
2342 }
2343
2344 fn validate_mappings(&self) -> Result<(), BypassReason> {
2345 if !self.context.working_dir.is_absolute() {
2346 return Err(BypassReason::RelativeWorkingDirectory(
2347 self.context.working_dir.clone(),
2348 ));
2349 }
2350 let mut roots = BTreeSet::new();
2351 let mut placeholders = BTreeSet::new();
2352 for mapping in &self.mappings {
2353 if !mapping.root.is_absolute() {
2354 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2355 }
2356 if mapping.placeholder.is_empty()
2357 || !mapping
2358 .placeholder
2359 .bytes()
2360 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2361 || !roots.insert(normalize_components(&mapping.root))
2362 || !placeholders.insert(&mapping.placeholder)
2363 {
2364 return Err(BypassReason::InvalidPathPlaceholder(
2365 mapping.placeholder.clone(),
2366 ));
2367 }
2368 }
2369 Ok(())
2370 }
2371
2372 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2373 match argument {
2374 Argument::Plain(value) => Ok(value.clone()),
2375 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2376 Argument::SearchPath { kind, path } => {
2377 let text = match self.normalize_path(path) {
2378 Ok(text) => text,
2379 Err(BypassReason::UnmappedAbsolutePath(absolute))
2387 if kind == "native" && self.invocation.native_search_is_inert() =>
2388 {
2389 absolute
2390 .to_str()
2391 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
2392 .to_string()
2393 }
2394 Err(error) => return Err(error),
2395 };
2396 Ok(format!("-L{kind}={text}"))
2397 }
2398 Argument::Extern { name, path } => match path {
2399 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2400 None => Ok(format!("--extern={name}")),
2401 },
2402 Argument::Emit(emits) => Ok(format!(
2403 "--emit={}",
2404 emits
2405 .iter()
2406 .map(|emit| match &emit.path {
2407 Some(path) => self
2408 .normalize_path(path)
2409 .map(|path| format!("{}={path}", emit.kind)),
2410 None => Ok(emit.kind.clone()),
2411 })
2412 .collect::<Result<Vec<_>, _>>()?
2413 .join(",")
2414 )),
2415 Argument::RemapPath { from, to } => Ok(format!(
2416 "--remap-path-prefix={}={}",
2417 self.normalize_path(from)?,
2418 to
2419 )),
2420 Argument::InstallName(name) => Ok(format!(
2421 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2422 )),
2423 Argument::OsoPrefix {
2424 path,
2425 trailing_slash,
2426 } => Ok(format!(
2427 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2428 self.normalize_path(path)?,
2429 if *trailing_slash { "/" } else { "" }
2430 )),
2431 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2432 }
2433 }
2434
2435 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2442 self.context
2443 .environment
2444 .iter()
2445 .map(|(name, value)| {
2446 let value = match value {
2447 Some(value) if self.context.portable_environment.contains(name) => {
2448 Some(self.normalize_path(Path::new(value))?)
2449 }
2450 value => value.clone(),
2451 };
2452 Ok((name.clone(), value))
2453 })
2454 .collect()
2455 }
2456
2457 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2458 normalize_resolved_shared_path_with(
2459 &self.aliases,
2460 path,
2461 &self.context.working_dir,
2462 &self.mappings,
2463 )
2464 .map_err(Into::into)
2465 }
2466}
2467
2468fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2472 directories.iter().any(|directory| {
2473 path.len() > directory.len()
2474 && path.as_bytes()[directory.len()] == b'/'
2475 && path.starts_with(directory)
2476 })
2477}
2478
2479fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2480 for mapping in mappings {
2481 let prefix = format!("${{{}}}", mapping.placeholder);
2482 let suffix = if value == prefix {
2483 ""
2484 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2485 suffix
2486 } else {
2487 continue;
2488 };
2489 if !mapping.root.is_absolute()
2490 || (!suffix.is_empty()
2491 && suffix.split('/').any(|component| {
2492 component.is_empty()
2493 || matches!(component, "." | "..")
2494 || component.contains('\\')
2495 }))
2496 {
2497 return Err(BypassReason::InvalidPredictedInput(value.into()));
2498 }
2499 let mut path = normalize_components(&mapping.root);
2500 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2501 return Ok(path);
2502 }
2503 Err(BypassReason::InvalidPredictedInput(value.into()))
2504}
2505
2506fn normalize_components(path: &Path) -> PathBuf {
2507 let mut normalized = PathBuf::new();
2508 for component in path.components() {
2509 match component {
2510 Component::CurDir => {}
2511 Component::ParentDir => {
2512 normalized.pop();
2513 }
2514 component => normalized.push(component.as_os_str()),
2515 }
2516 }
2517 normalized
2518}
2519
2520fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2521 if path.is_absolute() {
2522 normalize_components(path)
2523 } else {
2524 normalize_components(&working_dir.join(path))
2525 }
2526}
2527
2528#[cfg(test)]
2529#[path = "rustc_cache_tests.rs"]
2530mod tests;