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}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
398#[non_exhaustive]
399pub struct ParseOptions {
400 pub cache_native_links: bool,
403 pub custom_target_search: bool,
409}
410
411impl ParseOptions {
412 pub fn caching_native_links(enabled: bool) -> Self {
414 Self {
415 cache_native_links: enabled,
416 ..Self::default()
417 }
418 }
419
420 pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
423 self.custom_target_search = enabled;
424 self
425 }
426}
427
428#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct RustcOutputs {
431 pub directory: PathBuf,
433 pub files: Vec<PathBuf>,
435 pub dep_info: PathBuf,
437}
438
439impl RustcInvocation {
440 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
449 Self::parse_with(arguments, ParseOptions::default())
450 }
451
452 pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
457 Ok(expand_response_files(arguments)?.arguments)
458 }
459
460 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
463 let expanded = expand_response_files(arguments)?;
464 Parser::new(&expanded.arguments, options).parse()
465 }
466
467 pub fn links_natively(&self) -> bool {
470 matches!(
471 self.link_output,
472 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
473 )
474 }
475
476 pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
480 if !cfg!(target_os = "macos")
481 || self.link_output != LinkOutput::NativeProcMacro
482 || self
483 .arguments
484 .iter()
485 .any(|arg| matches!(arg, Argument::InstallName(_)))
486 {
487 return None;
488 }
489 let file = outputs
490 .files
491 .iter()
492 .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
493 let name = file.file_name()?.to_str()?;
494 if !safe_install_name(name) {
495 return None;
496 }
497 Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
498 }
499
500 pub fn linker_override(&self) -> Option<&Path> {
503 self.arguments.iter().find_map(|argument| {
504 let Argument::Plain(value) = argument else {
505 return None;
506 };
507 value.strip_prefix("--codegen=linker=").map(Path::new)
508 })
509 }
510
511 pub fn fuse_ld(&self) -> Option<&str> {
514 self.arguments.iter().find_map(|argument| {
515 let Argument::FuseLd(selection) = argument else {
516 return None;
517 };
518 Some(selection.as_str())
519 })
520 }
521
522 fn emits_windows_pdb(&self) -> bool {
523 if !cfg!(windows) || !self.links_natively() {
524 return false;
525 }
526 let mut enabled = false;
527 for argument in &self.arguments {
528 let Argument::Plain(value) = argument else {
529 continue;
530 };
531 if value == "-g" {
532 enabled = true;
533 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
534 enabled = !matches!(value, "0" | "none");
535 }
536 }
537 enabled
538 }
539
540 fn native_search_is_inert(&self) -> bool {
555 self.link_output == LinkOutput::Library
556 }
557
558 pub fn source(&self) -> &Path {
560 &self.source
561 }
562
563 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
569 self.required_inputs
570 .iter()
571 .map(|path| absolute_path(path, working_dir))
572 .collect()
573 }
574
575 pub fn target(&self) -> Option<&str> {
577 self.target.as_deref()
578 }
579
580 pub fn crate_name(&self) -> &str {
582 &self.crate_name
583 }
584
585 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
598 let linked = self
599 .arguments
600 .iter()
601 .filter_map(|argument| match argument {
602 Argument::Extern {
603 path: Some(path), ..
604 } => Some(path.as_path()),
605 _ => None,
606 })
607 .collect::<BTreeSet<_>>();
608 let owned = discovered
609 .inputs
610 .iter()
611 .filter(|input| !linked.contains(input.path.as_path()))
612 .map(|input| (input.path.as_path(), &input.digest))
613 .collect::<BTreeMap<_, _>>();
614 let mut bytes = Vec::new();
615 for (path, digest) in owned {
616 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
617 bytes.push(0);
618 bytes.extend_from_slice(digest.key().as_bytes());
619 bytes.push(0);
620 }
621 CacheDigest::blake3(&bytes)
622 }
623
624 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
629 if !working_dir.is_absolute() {
630 return Err(BypassReason::RelativeWorkingDirectory(
631 working_dir.to_path_buf(),
632 ));
633 }
634 let explicit_output = self
635 .explicit_output
636 .as_deref()
637 .map(|path| absolute_path(path, working_dir));
638 let output_directory = explicit_output
639 .as_deref()
640 .and_then(Path::parent)
641 .map(Path::to_path_buf)
642 .or_else(|| {
643 self.out_dir
644 .as_deref()
645 .map(|path| absolute_path(path, working_dir))
646 })
647 .unwrap_or_else(|| normalize_components(working_dir));
648 if let Some(output) = &explicit_output
652 && self.emits.iter().any(|emit| {
653 emit.path.is_none()
654 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
655 })
656 {
657 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
658 }
659 let mut files = BTreeSet::new();
660 let mut dep_info = None;
661 for emit in &self.emits {
662 if emit.kind == "dep-info" {
663 let path = emit.path.as_ref().map_or_else(
664 || {
665 explicit_output.clone().map_or_else(
666 || {
667 output_directory
668 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
669 },
670 |path| path.with_extension("d"),
671 )
672 },
673 |path| absolute_path(path, working_dir),
674 );
675 if path.file_name().is_none() {
676 return Err(BypassReason::InvalidOutputPath(path));
677 }
678 dep_info = Some(path);
679 continue;
680 }
681 let (prefix, extension) = match emit.kind.as_str() {
682 "link" => match self.link_output {
683 LinkOutput::Library => ("lib", "rlib"),
684 LinkOutput::WasmExecutable => ("", "wasm"),
685 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
686 LinkOutput::NativeProcMacro => (
687 std::env::consts::DLL_PREFIX,
688 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
689 ),
690 },
691 "metadata" => ("lib", "rmeta"),
692 _ => continue,
693 };
694 let path = if let Some(path) = &emit.path {
695 absolute_path(path, working_dir)
696 } else {
697 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
698 output_directory.join(if extension.is_empty() {
699 name
700 } else {
701 format!("{name}.{extension}")
702 })
703 };
704 if path.file_name().is_none() {
705 return Err(BypassReason::InvalidOutputPath(path));
706 }
707 if path.parent() != Some(output_directory.as_path()) {
708 return Err(BypassReason::SplitOutputDirectories);
709 }
710 let has_library_extension = path
715 .extension()
716 .and_then(|extension| extension.to_str())
717 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
718 || (cfg!(windows)
719 && path
720 .file_stem()
721 .and_then(|stem| Path::new(stem).extension())
722 .and_then(|extension| extension.to_str())
723 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
724 if emit.kind == "link"
725 && !matches!(self.link_output, LinkOutput::Library)
726 && has_library_extension
727 {
728 return Err(BypassReason::AmbiguousOutputName(path));
729 }
730 if emit.kind == "link" && self.emits_windows_pdb() {
731 files.insert(path.with_extension("pdb"));
732 }
733 files.insert(path);
734 }
735 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
736 if dep_info.parent() != Some(output_directory.as_path()) {
737 return Err(BypassReason::SplitOutputDirectories);
738 }
739 Ok(RustcOutputs {
740 directory: output_directory,
741 files: files.into_iter().collect(),
742 dep_info,
743 })
744 }
745
746 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
752 self.action_linked_by(context, None)
753 }
754
755 pub fn action_linked_by(
764 &self,
765 context: ActionContext,
766 linker: Option<LinkerIdentity>,
767 ) -> Result<RustcAction, BypassReason> {
768 ActionBuilder::new(self, context).linked_by(linker).build()
769 }
770
771 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
773 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
774 let bytes = canonical_json(&descriptor)
775 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
776 Ok(CacheDigest::blake3(&bytes))
777 }
778
779 pub fn prediction(
782 &self,
783 context: &ActionContext,
784 discovered: &DiscoveredInputs,
785 ) -> Result<RustcInputPrediction, BypassReason> {
786 let builder = ActionBuilder::new(self, context.clone());
787 builder.validate_mappings()?;
788 let mut native_directories = BTreeSet::new();
789 for argument in &self.arguments {
790 if let Argument::SearchPath { kind, path } = argument
791 && kind == "native"
792 {
793 match builder.normalize_path(path) {
794 Ok(normalized) => {
795 native_directories.insert(normalized);
796 }
797 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
803 }
804 Err(error) => return Err(error),
805 }
806 }
807 }
808 let mut inputs = BTreeSet::new();
815 for input in &discovered.inputs {
816 let normalized = builder.normalize_path(&input.path)?;
817 if !under_any_directory(&normalized, &native_directories) {
818 inputs.insert(normalized);
819 }
820 }
821 inputs.extend(
822 native_directories
823 .into_iter()
824 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
825 );
826 Ok(RustcInputPrediction {
827 version: 4,
828 inputs: inputs.into_iter().collect(),
829 environment: discovered.environment.keys().cloned().collect(),
830 compiler_duration_ns: 0,
831 crate_name: String::new(),
832 })
833 }
834}
835
836pub fn is_build_script_crate_name(crate_name: &str) -> bool {
840 crate_name
841 .strip_prefix("build_script_")
842 .is_some_and(|stem| !stem.is_empty())
843}
844
845impl RustcOutputs {
846 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
856 is_build_script_crate_name(crate_name)
857 .then(|| self.files.iter().find(|path| self.is_executable(path)))
858 .flatten()
859 .map(PathBuf::as_path)
860 }
861
862 pub fn is_executable(&self, path: &Path) -> bool {
870 self.files.iter().any(|output| output == path)
871 && !matches!(
872 path.extension().and_then(|extension| extension.to_str()),
873 Some("rlib" | "rmeta")
874 )
875 }
876}
877
878#[derive(Debug, Clone, PartialEq, Eq)]
879pub struct PathMapping {
881 pub root: PathBuf,
883 pub placeholder: String,
885}
886
887impl PathMapping {
888 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
890 Self {
891 root: root.into(),
892 placeholder: placeholder.into(),
893 }
894 }
895
896 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
899 let mut ordered = mappings.to_vec();
900 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
901 ordered
902 }
903}
904
905fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
906 mappings
907 .iter()
908 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
909 .collect()
910}
911
912pub fn normalize_mapped_path(
919 path: &Path,
920 working_dir: &Path,
921 mappings: &[PathMapping],
922) -> Result<String, BypassReason> {
923 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
924}
925
926#[derive(Debug, Clone, PartialEq, Eq)]
927pub struct CompilerIdentity {
929 pub toolchain: String,
931 pub rustc_version: String,
933 pub host: String,
935 pub driver: Option<String>,
937}
938
939#[derive(Debug, Clone, PartialEq, Eq)]
941pub struct ActionInput {
942 pub path: PathBuf,
944 pub digest: CacheDigest,
946}
947
948#[derive(Debug, Clone, PartialEq, Eq)]
950pub struct ActionContext {
951 pub compiler: CompilerIdentity,
953 pub working_dir: PathBuf,
955 pub path_mappings: Vec<PathMapping>,
957 pub environment: BTreeMap<String, Option<String>>,
959 pub portable_environment: BTreeSet<String>,
966 pub inputs: Vec<ActionInput>,
968}
969
970#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
980#[serde(deny_unknown_fields)]
981pub struct LinkerIdentity {
982 pub driver: String,
984 pub driver_version: String,
986 pub linker_version: String,
988 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
990 pub crt_objects: BTreeMap<String, CacheDigest>,
991 #[serde(skip_serializing_if = "Option::is_none", default)]
993 pub sdk: Option<String>,
994 #[serde(skip_serializing_if = "Option::is_none", default)]
996 pub deployment_target: Option<String>,
997}
998
999#[derive(Debug, Clone, PartialEq, Eq)]
1001pub struct RustcAction {
1002 pub digest: CacheDigest,
1004 pub bytes: Vec<u8>,
1006}
1007
1008#[derive(Debug, Clone, PartialEq, Eq)]
1011pub struct RustcInputPrediction {
1012 pub version: u8,
1014 pub inputs: Vec<String>,
1016 pub environment: Vec<String>,
1018 pub compiler_duration_ns: u64,
1021 pub crate_name: String,
1023}
1024
1025#[derive(Serialize, Deserialize)]
1026#[serde(deny_unknown_fields)]
1027struct RustcInputPredictionWire {
1028 version: u8,
1029 inputs: Vec<String>,
1030 environment: Vec<String>,
1031 #[serde(default, skip_serializing_if = "is_zero")]
1032 compiler_duration_ns: u64,
1033 #[serde(default, skip_serializing_if = "String::is_empty")]
1034 crate_name: String,
1035}
1036
1037impl Serialize for RustcInputPrediction {
1038 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1039 where
1040 S: Serializer,
1041 {
1042 RustcInputPredictionWire {
1043 version: self.version,
1044 inputs: if self.version == 4 {
1045 compact_prediction_inputs(&self.inputs)
1046 } else {
1047 self.inputs.clone()
1048 },
1049 environment: self.environment.clone(),
1050 compiler_duration_ns: self.compiler_duration_ns,
1051 crate_name: self.crate_name.clone(),
1052 }
1053 .serialize(serializer)
1054 }
1055}
1056
1057impl<'de> Deserialize<'de> for RustcInputPrediction {
1058 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1059 where
1060 D: Deserializer<'de>,
1061 {
1062 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1063 let inputs = if wire.version == 4 {
1064 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1065 } else {
1066 wire.inputs
1067 };
1068 Ok(Self {
1069 version: wire.version,
1070 inputs,
1071 environment: wire.environment,
1072 compiler_duration_ns: wire.compiler_duration_ns,
1073 crate_name: wire.crate_name,
1074 })
1075 }
1076}
1077
1078fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1079 let mut previous = "";
1080 inputs
1081 .iter()
1082 .map(|input| {
1083 let mut shared = previous
1084 .bytes()
1085 .zip(input.bytes())
1086 .take_while(|(left, right)| left == right)
1087 .count();
1088 while !input.is_char_boundary(shared) {
1089 shared -= 1;
1090 }
1091 let compact = format!("{shared}:{}", &input[shared..]);
1092 previous = input;
1093 compact
1094 })
1095 .collect()
1096}
1097
1098fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1099 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1100 let mut decoded_bytes = 0_usize;
1101 for input in inputs {
1102 let (shared_text, suffix) = input
1103 .split_once(':')
1104 .ok_or("compact rustc prediction input has no prefix length")?;
1105 let shared: usize = shared_text
1106 .parse()
1107 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1108 if shared.to_string() != shared_text
1109 || shared > expanded.last().map_or(0, String::len)
1110 || expanded
1111 .last()
1112 .is_some_and(|previous| !previous.is_char_boundary(shared))
1113 {
1114 return Err("compact rustc prediction prefix is not canonical");
1115 }
1116 let mut path = expanded
1117 .last()
1118 .map_or_else(String::new, |previous| previous[..shared].to_string());
1119 path.push_str(suffix);
1120 decoded_bytes = decoded_bytes
1121 .checked_add(path.len())
1122 .ok_or("compact rustc prediction is too large")?;
1123 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1124 return Err("compact rustc prediction is too large");
1125 }
1126 expanded.push(path);
1127 }
1128 Ok(expanded)
1129}
1130
1131fn is_zero(value: &u64) -> bool {
1132 *value == 0
1133}
1134
1135impl RustcInputPrediction {
1136 pub fn discover(
1139 &self,
1140 working_dir: &Path,
1141 path_mappings: &[PathMapping],
1142 digests: &dyn FileDigestCache,
1143 ) -> Result<DiscoveredInputs, BypassReason> {
1144 if !matches!(self.version, 2..=4) {
1145 return Err(BypassReason::UnsupportedPrediction);
1146 }
1147 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1148 return Err(BypassReason::UnsupportedPrediction);
1149 }
1150 let mut paths = BTreeSet::new();
1151 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1152 let mut native_bytes = 0_u64;
1153 for path in &self.inputs {
1154 if self.version >= 3
1155 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1156 {
1157 let directory = denormalize_path(path, path_mappings)?;
1158 dep_info::collect_native_directory(
1159 &directory,
1160 &admitted_roots,
1161 &mut paths,
1162 &mut native_bytes,
1163 )?;
1164 } else {
1165 paths.insert(denormalize_path(path, path_mappings)?);
1166 }
1167 }
1168 let environment = self
1169 .environment
1170 .iter()
1171 .map(|name| {
1172 if name.is_empty() || name.contains(['=', '\0']) {
1173 return Err(BypassReason::UnsupportedPrediction);
1174 }
1175 let value = std::env::var_os(name)
1176 .map(|value| {
1177 value
1178 .into_string()
1179 .map_err(|_| BypassReason::UnsupportedPrediction)
1180 })
1181 .transpose()?;
1182 Ok((name.clone(), value))
1183 })
1184 .collect::<Result<BTreeMap<_, _>, _>>()?;
1185 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1186 }
1187}
1188
1189#[derive(Serialize)]
1190struct ActionDescriptor {
1191 version: u8,
1192 kind: &'static str,
1193 adapter_version: u8,
1194 compiler: CompilerDescriptor,
1195 arguments: Vec<String>,
1196 environment: BTreeMap<String, Option<String>>,
1197 inputs: Vec<InputDescriptor>,
1198 #[serde(skip_serializing_if = "Option::is_none")]
1201 linker: Option<LinkerIdentity>,
1202}
1203
1204#[derive(Serialize)]
1205struct InvocationDescriptor {
1206 version: u8,
1207 kind: &'static str,
1208 adapter_version: u8,
1209 compiler: CompilerDescriptor,
1210 arguments: Vec<String>,
1211 required_inputs: Vec<String>,
1212}
1213
1214#[derive(Serialize)]
1215struct CompilerDescriptor {
1216 toolchain: String,
1217 rustc_version: String,
1218 host: String,
1219 #[serde(skip_serializing_if = "Option::is_none")]
1220 driver: Option<String>,
1221}
1222
1223#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1224struct InputDescriptor {
1225 path: String,
1226 digest: CacheDigest,
1227}
1228
1229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1231enum StaticLibraryNaming {
1232 Msvc,
1234 Unix,
1236}
1237
1238struct Parser<'a> {
1239 arguments: &'a [OsString],
1240 index: usize,
1241 parsed: Vec<Argument>,
1242 source: Option<PathBuf>,
1243 crate_types: Vec<String>,
1244 emits: Vec<Emit>,
1245 required_inputs: Vec<PathBuf>,
1246 native_libraries: Vec<String>,
1247 test: bool,
1248 crate_name: Option<String>,
1249 extra_filename: String,
1250 out_dir: Option<PathBuf>,
1251 explicit_output: Option<PathBuf>,
1252 target: Option<String>,
1253 options: ParseOptions,
1254}
1255
1256struct ExpandedArguments {
1257 arguments: Vec<OsString>,
1258}
1259
1260#[derive(Default)]
1261struct ResponseExpander {
1262 shell_argfiles: bool,
1263 next_is_unstable_option: bool,
1264 arguments: Vec<OsString>,
1265}
1266
1267impl ResponseExpander {
1268 fn push(&mut self, argument: String) {
1269 if self.next_is_unstable_option {
1270 self.shell_argfiles |= argument == "shell-argfiles";
1271 self.next_is_unstable_option = false;
1272 } else if let Some(option) = argument.strip_prefix("-Z") {
1273 if option.is_empty() {
1274 self.next_is_unstable_option = true;
1275 } else {
1276 self.shell_argfiles |= option == "shell-argfiles";
1277 }
1278 }
1279 self.arguments.push(argument.into());
1280 }
1281}
1282
1283fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1286 let mut expanded = ResponseExpander::default();
1287 for (index, argument) in arguments.iter().enumerate() {
1288 let argument = argument
1289 .to_str()
1290 .ok_or(BypassReason::NonUtf8Argument { index })?;
1291 let Some(argfile) = argument.strip_prefix('@') else {
1292 expanded.push(argument.to_string());
1293 continue;
1294 };
1295 let (path, shell) = match argfile.split_once(':') {
1296 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1297 _ => (argfile, false),
1298 };
1299 let contents = std::fs::read_to_string(path).map_err(|error| {
1300 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1301 })?;
1302 if shell {
1303 let arguments = shlex::split(&contents).ok_or_else(|| {
1304 BypassReason::ResponseFile(format!(
1305 "invalid shell-style arguments in {}",
1306 Path::new(path).display()
1307 ))
1308 })?;
1309 for argument in arguments {
1310 expanded.push(argument);
1311 }
1312 } else {
1313 for argument in contents.lines() {
1314 expanded.push(argument.to_string());
1315 }
1316 }
1317 }
1318 Ok(ExpandedArguments {
1319 arguments: expanded.arguments,
1320 })
1321}
1322
1323impl<'a> Parser<'a> {
1324 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1325 Self {
1326 arguments,
1327 options,
1328 index: 0,
1329 parsed: Vec::new(),
1330 source: None,
1331 crate_types: Vec::new(),
1332 emits: Vec::new(),
1333 required_inputs: Vec::new(),
1334 native_libraries: Vec::new(),
1335 test: false,
1336 crate_name: None,
1337 extra_filename: String::new(),
1338 out_dir: None,
1339 explicit_output: None,
1340 target: None,
1341 }
1342 }
1343
1344 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1345 while self.index < self.arguments.len() {
1346 let value = self.current()?.to_string();
1347 self.index += 1;
1348 if let Some(long) = value.strip_prefix("--") {
1349 self.parse_long(long)?;
1350 } else if value.starts_with('-') && value != "-" {
1351 self.parse_short(&value)?;
1352 } else {
1353 self.parse_input(&value)?;
1354 }
1355 }
1356
1357 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1358 let link_output = self.classify()?;
1359 self.require_native_libraries(link_output)?;
1360 let crate_name = self.crate_name.clone().map_or_else(
1361 || {
1362 source
1363 .file_stem()
1364 .and_then(|name| name.to_str())
1365 .map(|name| name.replace('-', "_"))
1366 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1367 },
1368 Ok,
1369 )?;
1370 self.required_inputs.push(source.clone());
1371 Ok(RustcInvocation {
1372 arguments: self.parsed,
1373 source,
1374 required_inputs: self.required_inputs,
1375 crate_name,
1376 extra_filename: self.extra_filename,
1377 out_dir: self.out_dir,
1378 explicit_output: self.explicit_output,
1379 emits: self.emits,
1380 target: self.target,
1381 link_output,
1382 })
1383 }
1384
1385 fn current(&self) -> Result<&str, BypassReason> {
1386 self.arguments[self.index]
1387 .to_str()
1388 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1389 }
1390
1391 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1392 if let Some(value) = inline {
1393 if value.is_empty() {
1394 return Err(BypassReason::MissingValue(flag.into()));
1395 }
1396 return Ok(value.into());
1397 }
1398 if self.index >= self.arguments.len() {
1399 return Err(BypassReason::MissingValue(flag.into()));
1400 }
1401 let value = self.current()?.to_string();
1402 self.index += 1;
1403 Ok(value)
1404 }
1405
1406 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1407 let (flag, inline) = value
1408 .split_once('=')
1409 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1410 let rendered_flag = format!("--{flag}");
1411 match flag {
1412 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1413 "test" => {
1414 self.test = true;
1415 self.parsed.push(Argument::Plain(rendered_flag));
1416 Ok(())
1417 }
1418 "verbose" => {
1419 self.parsed.push(Argument::Plain(rendered_flag));
1420 Ok(())
1421 }
1422 "crate-name" => {
1423 let value = self.take_value(&rendered_flag, inline)?;
1424 self.crate_name = Some(value.clone());
1425 self.parsed
1426 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1427 Ok(())
1428 }
1429 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1430 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1431 | "deny" | "forbid" | "cap-lints" => {
1432 let value = self.take_value(&rendered_flag, inline)?;
1433 self.parsed
1434 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1435 Ok(())
1436 }
1437 "target" => {
1438 let value = self.take_value(&rendered_flag, inline)?;
1439 let effective = self.target.is_none();
1443 if effective {
1444 self.target = Some(value.clone());
1445 }
1446 if value.ends_with(".json") || value.contains(['/', '\\']) {
1447 let path = PathBuf::from(value);
1448 if effective {
1449 self.required_inputs.push(path.clone());
1450 }
1451 self.parsed.push(Argument::Path {
1452 flag: rendered_flag,
1453 path,
1454 });
1455 } else {
1456 self.parsed
1457 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1458 }
1459 Ok(())
1460 }
1461 "crate-type" => {
1462 let value = self.take_value(&rendered_flag, inline)?;
1463 self.crate_types
1464 .extend(value.split(',').map(ToOwned::to_owned));
1465 self.parsed
1466 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1467 Ok(())
1468 }
1469 "emit" => {
1470 let value = self.take_value(&rendered_flag, inline)?;
1471 let emits = parse_emits(&value);
1472 self.emits.extend(emits.clone());
1473 self.parsed.push(Argument::Emit(emits));
1474 Ok(())
1475 }
1476 "out-dir" => {
1477 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1478 self.out_dir = Some(path.clone());
1479 self.parsed.push(Argument::Path {
1480 flag: rendered_flag,
1481 path,
1482 });
1483 Ok(())
1484 }
1485 "sysroot" => {
1486 let path = self.take_value(&rendered_flag, inline)?;
1487 self.parsed.push(Argument::Path {
1488 flag: rendered_flag,
1489 path: path.into(),
1490 });
1491 Ok(())
1492 }
1493 "extern" => {
1494 let value = self.take_value(&rendered_flag, inline)?;
1495 let (name, path) = value
1496 .split_once('=')
1497 .map_or((value.as_str(), None), |(name, path)| {
1498 (name, Some(PathBuf::from(path)))
1499 });
1500 if let Some(path) = &path {
1501 self.required_inputs.push(path.clone());
1502 }
1503 self.parsed.push(Argument::Extern {
1504 name: name.into(),
1505 path,
1506 });
1507 Ok(())
1508 }
1509 "remap-path-prefix" => {
1510 let value = self.take_value(&rendered_flag, inline)?;
1511 let Some((from, to)) = value.split_once('=') else {
1512 return Err(BypassReason::MissingValue(rendered_flag));
1513 };
1514 self.parsed.push(Argument::RemapPath {
1515 from: from.into(),
1516 to: to.into(),
1517 });
1518 Ok(())
1519 }
1520 "codegen" => {
1521 let value = self.take_value(&rendered_flag, inline)?;
1522 self.parse_codegen(&value)
1523 }
1524 "jobs-frontend" => {
1525 let value = self.take_value(&rendered_flag, inline)?;
1526 self.parsed
1527 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1528 Ok(())
1529 }
1530 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1531 }
1532 }
1533
1534 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1535 if let Some(attached) = value.strip_prefix("-Z") {
1536 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1537 match option.as_str() {
1538 "embed-metadata=no" | "embed-metadata=yes" | "shell-argfiles"
1539 | "unstable-options" => {
1540 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1541 return Ok(());
1542 }
1543 "threads" | "threads=" => {
1544 return Err(BypassReason::MissingValue("-Zthreads".into()));
1545 }
1546 _ => {}
1547 }
1548 if option.starts_with("threads=") {
1549 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1550 return Ok(());
1551 }
1552 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1553 }
1554 match value {
1555 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1558 "-g" | "-O" | "-v" => {
1559 self.parsed.push(Argument::Plain(value.into()));
1560 return Ok(());
1561 }
1562 _ => {}
1563 }
1564 for (short, long) in [
1565 ("-A", "--allow"),
1566 ("-W", "--warn"),
1567 ("-D", "--deny"),
1568 ("-F", "--forbid"),
1569 ] {
1570 if let Some(attached) = value.strip_prefix(short) {
1571 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1572 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1573 return Ok(());
1574 }
1575 }
1576 if let Some(attached) = value.strip_prefix("-C") {
1577 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1578 return self.parse_codegen(&option);
1579 }
1580 if let Some(attached) = value.strip_prefix("-L") {
1581 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1582 let (kind, path) = search
1583 .split_once('=')
1584 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1585 if !matches!(kind, "dependency" | "native") {
1586 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1587 }
1588 self.parsed.push(Argument::SearchPath {
1589 kind: kind.into(),
1590 path: path.into(),
1591 });
1592 return Ok(());
1593 }
1594 if let Some(attached) = value.strip_prefix("-l") {
1595 let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1596 self.parsed.push(Argument::Plain(format!("-l{library}")));
1597 self.native_libraries.push(library);
1598 return Ok(());
1599 }
1600 if let Some(attached) = value.strip_prefix("-o") {
1601 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1602 self.explicit_output = Some(path.clone().into());
1603 self.parsed.push(Argument::Path {
1604 flag: "-o".into(),
1605 path: path.into(),
1606 });
1607 return Ok(());
1608 }
1609 Err(BypassReason::UnknownFlag(value.into()))
1610 }
1611
1612 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1613 let name = value.split_once('=').map_or(value, |(name, _)| name);
1614 if name == "incremental" {
1615 return Err(BypassReason::Incremental);
1616 }
1617 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1618 return Err(BypassReason::UnknownCodegenOption(name.into()));
1619 }
1620 if name == "linker-plugin-lto"
1626 && let Some((_, selection)) = value.split_once('=')
1627 && !matches!(
1628 selection,
1629 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1630 )
1631 {
1632 return Err(BypassReason::UnknownCodegenOption(format!(
1633 "linker-plugin-lto={selection}"
1634 )));
1635 }
1636 if name == "link-arg"
1643 && let Some((_, option)) = value.split_once('=')
1644 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1645 && !selection.is_empty()
1646 && !selection.contains(',')
1647 && !self.parsed.iter().any(
1648 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1649 )
1650 {
1651 if !self.parsed.iter().any(
1652 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1653 ) {
1654 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1655 }
1656 return Ok(());
1657 }
1658 if name == "link-arg"
1661 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1662 && safe_install_name(option)
1663 {
1664 self.parsed.push(Argument::InstallName(option.to_owned()));
1665 return Ok(());
1666 }
1667 if matches!(name, "link-arg" | "link-args")
1672 && let Some((_, option)) = value.split_once('=')
1673 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1674 && !prefix.trim_end_matches('/').is_empty()
1675 && !prefix.contains(',')
1676 {
1677 let trailing_slash = prefix.ends_with('/');
1678 self.parsed.push(Argument::OsoPrefix {
1679 path: PathBuf::from(prefix.trim_end_matches('/')),
1680 trailing_slash,
1681 });
1682 return Ok(());
1683 }
1684 self.parsed
1685 .push(Argument::Plain(format!("--codegen={value}")));
1686 if name == "extra-filename" {
1687 self.extra_filename = value
1688 .split_once('=')
1689 .map_or(String::new(), |(_, value)| value.to_string());
1690 }
1691 Ok(())
1692 }
1693
1694 fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1709 for library in std::mem::take(&mut self.native_libraries) {
1710 if link_output != LinkOutput::Library {
1711 return Err(BypassReason::NativeLibrary(library));
1712 }
1713 let Some((kind, name)) = library.split_once('=') else {
1721 continue;
1722 };
1723 let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1724 match kind {
1725 "static" => {}
1726 "dylib" | "framework" | "link-arg" => continue,
1727 _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1728 }
1729 let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1732 let (mut verbatim, mut bundle) = (false, true);
1733 for modifier in modifiers.split(',') {
1734 match modifier {
1735 "+verbatim" => verbatim = true,
1736 "-verbatim" => verbatim = false,
1737 "+bundle" => bundle = true,
1738 "-bundle" => bundle = false,
1739 _ => {}
1740 }
1741 }
1742 if !bundle {
1743 continue;
1744 }
1745 let archive = self
1746 .find_native_static_library(name, verbatim)
1747 .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1748 .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1749 self.required_inputs.push(archive);
1750 }
1751 Ok(())
1752 }
1753
1754 fn find_native_static_library(
1766 &self,
1767 name: &str,
1768 verbatim: bool,
1769 ) -> Result<Option<PathBuf>, ()> {
1770 let file_names = if verbatim {
1771 vec![name.to_owned()]
1772 } else {
1773 match self.static_library_naming() {
1774 Some(StaticLibraryNaming::Msvc) => {
1775 vec![format!("{name}.lib"), format!("lib{name}.a")]
1776 }
1777 Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1778 None => return Err(()),
1779 }
1780 };
1781 Ok(self
1782 .parsed
1783 .iter()
1784 .filter_map(|argument| match argument {
1785 Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1786 _ => None,
1787 })
1788 .flat_map(|directory| {
1789 file_names
1790 .iter()
1791 .map(move |file_name| directory.join(file_name))
1792 })
1793 .find(|candidate| candidate.is_file()))
1794 }
1795
1796 fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1804 let Some(target) = self.target.as_deref() else {
1805 return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1806 StaticLibraryNaming::Msvc
1807 } else {
1808 StaticLibraryNaming::Unix
1809 });
1810 };
1811 if target.ends_with(".json")
1812 || target.contains(['/', '\\'])
1813 || self.options.custom_target_search
1814 {
1815 return None;
1816 }
1817 Some(if target.contains("msvc") || target.ends_with("-uefi") {
1820 StaticLibraryNaming::Msvc
1821 } else {
1822 StaticLibraryNaming::Unix
1823 })
1824 }
1825
1826 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1827 if value == "-" {
1828 return Err(BypassReason::StandardInput);
1829 }
1830 if self.source.replace(value.into()).is_some() {
1831 return Err(BypassReason::MultipleInputs);
1832 }
1833 Ok(())
1834 }
1835
1836 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1837 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1846 let builds_a_library = !self.test
1847 && !self.crate_types.is_empty()
1848 && self
1849 .crate_types
1850 .iter()
1851 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1852 let link_output = if never_links || builds_a_library {
1853 LinkOutput::Library
1854 } else if self
1855 .target
1856 .as_deref()
1857 .is_some_and(compiler_bundled_wasm_target)
1858 && ((self.test && self.crate_types.is_empty())
1859 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1860 {
1861 if self.parsed.iter().any(|argument| match argument {
1862 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1863 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1864 !matches!(
1865 value.rsplit_once('=').map(|(_, value)| value),
1866 Some("y" | "yes" | "on" | "true")
1867 )
1868 }
1869 _ => false,
1870 }) {
1871 return Err(BypassReason::UnknownCodegenOption(
1872 "link-self-contained".into(),
1873 ));
1874 }
1875 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1876 && self.parsed.iter().any(|argument| {
1877 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1878 })
1879 {
1880 return Err(BypassReason::UnknownCodegenOption(
1881 "target-feature=-crt-static".into(),
1882 ));
1883 }
1884 LinkOutput::WasmExecutable
1888 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1889 self.check_native_link_is_portable()?;
1890 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1891 LinkOutput::NativeProcMacro
1892 } else {
1893 LinkOutput::NativeExecutable
1894 }
1895 } else if self.test {
1896 return Err(BypassReason::UnsupportedCrateType("test".into()));
1897 } else {
1898 return Err(BypassReason::UnsupportedCrateType(
1899 self.crate_types
1900 .iter()
1901 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1902 .cloned()
1903 .unwrap_or_else(|| "bin".into()),
1904 ));
1905 };
1906 if link_output != LinkOutput::Library
1915 && let Some(option) = self.first_link_argument()
1916 {
1917 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1918 }
1919 if self
1920 .parsed
1921 .iter()
1922 .any(|arg| matches!(arg, Argument::InstallName(_)))
1923 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
1924 {
1925 return Err(BypassReason::UnmodeledLinkArgument(
1926 "link-arg=-Wl,-install_name".into(),
1927 ));
1928 }
1929 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1936 let Argument::FuseLd(selection) = argument else {
1937 return None;
1938 };
1939 Some(selection.as_str())
1940 }) && !matches!(
1941 link_output,
1942 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1943 ) {
1944 return Err(BypassReason::UnmodeledLinkArgument(format!(
1945 "link-arg=-fuse-ld={selection}"
1946 )));
1947 }
1948 if self.parsed.iter().any(|argument| {
1949 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1950 }) && !matches!(
1951 link_output,
1952 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1953 ) {
1954 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1955 }
1956 if !matches!(
1960 link_output,
1961 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1962 ) && self
1963 .parsed
1964 .iter()
1965 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1966 {
1967 return Err(BypassReason::UnmodeledLinkArgument(
1968 "link-arg=-Wl,-oso_prefix".into(),
1969 ));
1970 }
1971 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1972 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1973 _ => None,
1974 }) {
1975 return Err(BypassReason::UnresolvedExtern(name.clone()));
1976 }
1977 if let Some(emit) = self
1978 .emits
1979 .iter()
1980 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1981 {
1982 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1983 }
1984 if !self
1985 .emits
1986 .iter()
1987 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1988 {
1989 return Err(BypassReason::NoCacheableOutput);
1990 }
1991 Ok(link_output)
1992 }
1993}
1994
1995impl Parser<'_> {
1996 fn first_link_argument(&self) -> Option<&str> {
1999 self.parsed.iter().find_map(|argument| {
2000 let Argument::Plain(value) = argument else {
2001 return None;
2002 };
2003 let option = value.strip_prefix("--codegen=")?;
2004 let name = option.split_once('=').map_or(option, |(name, _)| name);
2005 matches!(name, "link-arg" | "link-args").then_some(option)
2006 })
2007 }
2008
2009 fn oso_prefix_covers_outputs(&self) -> bool {
2018 let Some(directory) = self
2019 .out_dir
2020 .as_deref()
2021 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2022 else {
2023 return false;
2024 };
2025 if !directory.is_absolute() {
2026 return false;
2027 }
2028 let directory = normalize_components(directory);
2029 self.parsed.iter().any(|argument| {
2030 let Argument::OsoPrefix { path, .. } = argument else {
2031 return false;
2032 };
2033 path.is_absolute()
2034 && (directory.starts_with(normalize_components(path))
2035 || std::fs::canonicalize(&directory)
2036 .ok()
2037 .zip(std::fs::canonicalize(path).ok())
2038 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2039 })
2040 }
2041
2042 fn links_a_native_artifact(&self) -> bool {
2049 self.target.is_none()
2050 && self.emits.iter().any(|emit| emit.kind == "link")
2055 && ((self.test && self.crate_types.is_empty())
2056 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro")))
2057 }
2058
2059 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2066 for argument in &self.parsed {
2067 let Argument::Plain(value) = argument else {
2068 continue;
2069 };
2070 let (name, value) = if value == "-g" {
2073 ("debuginfo", Some("2"))
2074 } else if let Some(option) = value.strip_prefix("--codegen=") {
2075 match option.split_once('=') {
2076 Some((name, value)) => (name, Some(value)),
2077 None => (option, None),
2081 }
2082 } else {
2083 continue;
2084 };
2085 let unportable = match name {
2086 "split-debuginfo" => match value {
2092 Some("off") => false,
2093 Some("unpacked") if cfg!(target_os = "macos") => {
2094 !self.oso_prefix_covers_outputs()
2095 }
2096 _ => true,
2097 },
2098 "debuginfo" if cfg!(target_os = "macos") => {
2107 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2108 }
2109 "rpath" => is_enabled(value),
2117 "prefer-dynamic" => {
2118 is_enabled(value)
2119 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2120 }
2121 "link-self-contained" => true,
2124 _ => false,
2125 };
2126 if unportable {
2127 return Err(BypassReason::UnportableNativeLink(match value {
2128 Some(value) => format!("{name}={value}"),
2129 None => name.to_owned(),
2130 }));
2131 }
2132 }
2133 Ok(())
2134 }
2135}
2136
2137fn safe_install_name(name: &str) -> bool {
2138 !name.is_empty()
2139 && name
2140 .bytes()
2141 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2142}
2143
2144fn is_enabled(value: Option<&str>) -> bool {
2147 matches!(value, None | Some("y" | "yes" | "on" | "true"))
2148}
2149
2150fn compiler_bundled_wasm_target(target: &str) -> bool {
2151 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2152}
2153
2154fn parse_emits(value: &str) -> Vec<Emit> {
2155 value
2156 .split(',')
2157 .map(|emit| {
2158 let (kind, path) = emit
2159 .split_once('=')
2160 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2161 Emit {
2162 kind: kind.into(),
2163 path,
2164 }
2165 })
2166 .collect()
2167}
2168
2169struct ActionBuilder<'a> {
2170 invocation: &'a RustcInvocation,
2171 context: ActionContext,
2172 mappings: Vec<SharedPathMapping>,
2173 aliases: PathAliases,
2177 linker: Option<LinkerIdentity>,
2178}
2179
2180impl<'a> ActionBuilder<'a> {
2181 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2182 context.path_mappings = PathMapping::ordered(&context.path_mappings);
2183 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2184 Self {
2185 linker: None,
2186 invocation,
2187 mappings,
2188 aliases: PathAliases::default(),
2189 context,
2190 }
2191 }
2192
2193 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2194 self.linker = linker;
2195 self
2196 }
2197
2198 fn build(self) -> Result<RustcAction, BypassReason> {
2199 self.validate_mappings()?;
2200 let invocation = self.invocation_descriptor()?;
2201 let environment = self.environment_descriptor()?;
2202
2203 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2204 for input in &self.context.inputs {
2205 input
2206 .digest
2207 .validate()
2208 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2209 let path = self.normalize_path(&input.path)?;
2210 if inputs
2211 .insert(path.clone(), input.digest.clone())
2212 .is_some_and(|existing| existing != input.digest)
2213 {
2214 return Err(BypassReason::ConflictingInput(path));
2215 }
2216 }
2217 let required = self
2218 .invocation
2219 .required_inputs
2220 .iter()
2221 .map(|path| self.normalize_path(path))
2222 .collect::<Result<BTreeSet<_>, _>>()?;
2223 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2224 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2225 }
2226 let inputs = inputs
2227 .into_iter()
2228 .map(|(path, digest)| InputDescriptor { path, digest })
2229 .collect();
2230 if self.invocation.links_natively() && self.linker.is_none() {
2233 return Err(BypassReason::UnportableNativeLink(
2234 "linker identity is unknown".into(),
2235 ));
2236 }
2237 let descriptor = ActionDescriptor {
2238 version: ACTION_SCHEMA_VERSION,
2239 kind: "rustc",
2240 adapter_version: ADAPTER_VERSION,
2241 compiler: invocation.compiler,
2242 arguments: invocation.arguments,
2243 environment,
2244 inputs,
2245 linker: self.linker.clone(),
2246 };
2247 let bytes = canonical_json(&descriptor)
2248 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2249 let digest = CacheDigest::blake3(&bytes);
2250 Ok(RustcAction { digest, bytes })
2251 }
2252
2253 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2254 self.validate_mappings()?;
2255 let arguments = self
2256 .invocation
2257 .arguments
2258 .iter()
2259 .map(|argument| self.normalize_argument(argument))
2260 .collect::<Result<Vec<_>, _>>()?;
2261 let required_inputs = self
2262 .invocation
2263 .required_inputs
2264 .iter()
2265 .map(|path| self.normalize_path(path))
2266 .collect::<Result<BTreeSet<_>, _>>()?
2267 .into_iter()
2268 .collect();
2269 Ok(InvocationDescriptor {
2270 version: ACTION_SCHEMA_VERSION,
2271 kind: "rustc",
2272 adapter_version: ADAPTER_VERSION,
2273 compiler: CompilerDescriptor {
2274 toolchain: self.context.compiler.toolchain.clone(),
2275 rustc_version: self.context.compiler.rustc_version.clone(),
2276 host: self.context.compiler.host.clone(),
2277 driver: self.context.compiler.driver.clone(),
2278 },
2279 arguments,
2280 required_inputs,
2281 })
2282 }
2283
2284 fn validate_mappings(&self) -> Result<(), BypassReason> {
2285 if !self.context.working_dir.is_absolute() {
2286 return Err(BypassReason::RelativeWorkingDirectory(
2287 self.context.working_dir.clone(),
2288 ));
2289 }
2290 let mut roots = BTreeSet::new();
2291 let mut placeholders = BTreeSet::new();
2292 for mapping in &self.mappings {
2293 if !mapping.root.is_absolute() {
2294 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2295 }
2296 if mapping.placeholder.is_empty()
2297 || !mapping
2298 .placeholder
2299 .bytes()
2300 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2301 || !roots.insert(normalize_components(&mapping.root))
2302 || !placeholders.insert(&mapping.placeholder)
2303 {
2304 return Err(BypassReason::InvalidPathPlaceholder(
2305 mapping.placeholder.clone(),
2306 ));
2307 }
2308 }
2309 Ok(())
2310 }
2311
2312 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2313 match argument {
2314 Argument::Plain(value) => Ok(value.clone()),
2315 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2316 Argument::SearchPath { kind, path } => {
2317 let text = match self.normalize_path(path) {
2318 Ok(text) => text,
2319 Err(BypassReason::UnmappedAbsolutePath(absolute))
2327 if kind == "native" && self.invocation.native_search_is_inert() =>
2328 {
2329 absolute
2330 .to_str()
2331 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
2332 .to_string()
2333 }
2334 Err(error) => return Err(error),
2335 };
2336 Ok(format!("-L{kind}={text}"))
2337 }
2338 Argument::Extern { name, path } => match path {
2339 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2340 None => Ok(format!("--extern={name}")),
2341 },
2342 Argument::Emit(emits) => Ok(format!(
2343 "--emit={}",
2344 emits
2345 .iter()
2346 .map(|emit| match &emit.path {
2347 Some(path) => self
2348 .normalize_path(path)
2349 .map(|path| format!("{}={path}", emit.kind)),
2350 None => Ok(emit.kind.clone()),
2351 })
2352 .collect::<Result<Vec<_>, _>>()?
2353 .join(",")
2354 )),
2355 Argument::RemapPath { from, to } => Ok(format!(
2356 "--remap-path-prefix={}={}",
2357 self.normalize_path(from)?,
2358 to
2359 )),
2360 Argument::InstallName(name) => Ok(format!(
2361 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2362 )),
2363 Argument::OsoPrefix {
2364 path,
2365 trailing_slash,
2366 } => Ok(format!(
2367 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2368 self.normalize_path(path)?,
2369 if *trailing_slash { "/" } else { "" }
2370 )),
2371 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2372 }
2373 }
2374
2375 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2382 self.context
2383 .environment
2384 .iter()
2385 .map(|(name, value)| {
2386 let value = match value {
2387 Some(value) if self.context.portable_environment.contains(name) => {
2388 Some(self.normalize_path(Path::new(value))?)
2389 }
2390 value => value.clone(),
2391 };
2392 Ok((name.clone(), value))
2393 })
2394 .collect()
2395 }
2396
2397 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2398 normalize_resolved_shared_path_with(
2399 &self.aliases,
2400 path,
2401 &self.context.working_dir,
2402 &self.mappings,
2403 )
2404 .map_err(Into::into)
2405 }
2406}
2407
2408fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2412 directories.iter().any(|directory| {
2413 path.len() > directory.len()
2414 && path.as_bytes()[directory.len()] == b'/'
2415 && path.starts_with(directory)
2416 })
2417}
2418
2419fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2420 for mapping in mappings {
2421 let prefix = format!("${{{}}}", mapping.placeholder);
2422 let suffix = if value == prefix {
2423 ""
2424 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2425 suffix
2426 } else {
2427 continue;
2428 };
2429 if !mapping.root.is_absolute()
2430 || (!suffix.is_empty()
2431 && suffix.split('/').any(|component| {
2432 component.is_empty()
2433 || matches!(component, "." | "..")
2434 || component.contains('\\')
2435 }))
2436 {
2437 return Err(BypassReason::InvalidPredictedInput(value.into()));
2438 }
2439 let mut path = normalize_components(&mapping.root);
2440 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2441 return Ok(path);
2442 }
2443 Err(BypassReason::InvalidPredictedInput(value.into()))
2444}
2445
2446fn normalize_components(path: &Path) -> PathBuf {
2447 let mut normalized = PathBuf::new();
2448 for component in path.components() {
2449 match component {
2450 Component::CurDir => {}
2451 Component::ParentDir => {
2452 normalized.pop();
2453 }
2454 component => normalized.push(component.as_os_str()),
2455 }
2456 }
2457 normalized
2458}
2459
2460fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2461 if path.is_absolute() {
2462 normalize_components(path)
2463 } else {
2464 normalize_components(&working_dir.join(path))
2465 }
2466}
2467
2468#[cfg(test)]
2469#[path = "rustc_cache_tests.rs"]
2470mod tests;