1#![deny(missing_docs)]
30
31use mbx_cache_core::{
32 CacheDigest, FileDigestCache, PathMapping as SharedPathMapping, PathNormalizationError,
33 canonical_json, normalize_mapped_path as normalize_shared_path,
34 normalize_resolved_mapped_path as normalize_resolved_shared_path, resolve_path_mappings,
35};
36use serde::{Deserialize, Deserializer, Serialize, Serializer};
37use std::collections::{BTreeMap, BTreeSet};
38use std::ffi::OsString;
39use std::path::{Component, Path, PathBuf};
40use thiserror::Error;
41
42mod dep_info;
43
44pub use dep_info::{DepInfoCommand, DiscoveredInputs, RustcDepInfo};
45
46pub const ACTION_SCHEMA_VERSION: u8 = 1;
48pub const ADAPTER_VERSION: u8 = 2;
55
56impl BypassReason {
57 pub fn kind(&self) -> &'static str {
62 self.into()
63 }
64
65 pub fn remediation(&self) -> Option<&'static str> {
71 match self {
72 Self::Incremental => Some(
73 "Set `MBX_INCREMENTAL=0`; mbx will then disable Cargo incremental state and cache the compilation.",
74 ),
75 Self::UnportableNativeLink(detail) if detail.contains("linker") => Some(
76 "Make the native linker resolvable on `PATH`, or configure a linker mbx can identify for this target.",
77 ),
78 Self::UnportableNativeLink(_) => Some(
79 "Remove the reported `-C` option from the active Cargo profile or `RUSTFLAGS` to make these links cacheable.",
80 ),
81 Self::UnknownFlag(_) | Self::UnknownCodegenOption(_) => Some(
82 "Remove the reported compiler option, or upgrade mbx if the option should be modeled.",
83 ),
84 Self::UnmodeledLinkArgument(_) => Some(
85 "Remove the reported linker argument, or keep these links uncached if the argument is required.",
86 ),
87 Self::UnmappedAbsolutePath(_) => Some(
88 "Move the input under the workspace, target, Cargo, toolchain, or home roots so mbx can give it a portable cache name.",
89 ),
90 _ => None,
91 }
92 }
93}
94
95impl From<PathNormalizationError> for BypassReason {
96 fn from(reason: PathNormalizationError) -> Self {
97 match reason {
98 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
99 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
100 }
101 }
102}
103
104const SUPPORTED_CODEGEN_OPTIONS: &[&str] = &[
105 "codegen-units",
106 "control-flow-guard",
107 "debug-assertions",
108 "debuginfo",
109 "default-linker-libraries",
110 "embed-bitcode",
111 "extra-filename",
112 "force-frame-pointers",
113 "force-unwind-tables",
114 "instrument-coverage",
115 "link-arg",
116 "link-args",
117 "link-dead-code",
118 "link-self-contained",
119 "lto",
120 "metadata",
121 "no-prepopulate-passes",
122 "opt-level",
123 "overflow-checks",
124 "panic",
125 "prefer-dynamic",
126 "relocation-model",
127 "rpath",
128 "save-temps",
129 "soft-float",
130 "split-debuginfo",
131 "split-dwarf-kind",
132 "strip",
133 "symbol-mangling-version",
134 "target-cpu",
135 "target-feature",
136 "tls-model",
137];
138
139const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
140const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
141const MAX_DECODED_PREDICTION_BYTES: usize = 16 * 1024 * 1024;
145const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
146
147const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
150 "wasm32-unknown-unknown",
151 "wasm32-wasip1",
152 "wasm32-wasip1-threads",
153 "wasm32-wasip2",
154 "wasm32v1-none",
155 "wasm64-unknown-unknown",
156];
157
158#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
159#[strum(serialize_all = "kebab-case")]
160#[non_exhaustive]
170pub enum BypassReason {
171 #[error("rustc argument {index} is not valid UTF-8")]
173 NonUtf8Argument {
174 index: usize,
176 },
177 #[error("could not model rustc response file: {0}")]
179 ResponseFile(String),
180 #[error("rustc flag is not modeled by the cache adapter: {0}")]
182 UnknownFlag(String),
183 #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
185 UnknownCodegenOption(String),
186 #[error("rustc flag requires a value: {0}")]
188 MissingValue(String),
189 #[error("rustc invocation is a compiler query, not a compilation")]
191 CompilerQuery,
192 #[error("rustc invocation reads source from standard input")]
194 StandardInput,
195 #[error("rustc invocation has no source input")]
197 MissingInput,
198 #[error("rustc invocation has multiple source inputs")]
200 MultipleInputs,
201 #[error("incremental compilation cannot be combined with action caching")]
203 Incremental,
204 #[error("rustc crate type is not cacheable yet: {0}")]
206 UnsupportedCrateType(String),
207 #[error("rustc output type is not cacheable yet: {0}")]
209 UnsupportedEmit(String),
210 #[error("rustc invocation does not emit a cacheable artifact")]
212 NoCacheableOutput,
213 #[error("rustc invocation does not emit dependency information")]
215 NoDepInfo,
216 #[error("rustc output paths do not share one directory")]
218 SplitOutputDirectories,
219 #[error("rustc output path has no file name: {0}")]
221 InvalidOutputPath(PathBuf),
222 #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
224 ImplicitEmitWithOutputFile(PathBuf),
225 #[error("native library lookup is not cacheable yet")]
227 NativeLibrary,
228 #[error("rustc output name does not distinguish a program from a library: {0}")]
230 AmbiguousOutputName(PathBuf),
231 #[error("native link is not reproducible across checkouts: {0}")]
233 UnportableNativeLink(String),
234 #[error("rustc link argument is not modeled by the cache adapter: {0}")]
236 UnmodeledLinkArgument(String),
237 #[error("rustc search path kind is not cacheable yet: {0}")]
239 UnsupportedSearchPath(String),
240 #[error("rustc extern does not identify an input artifact: {0}")]
242 UnresolvedExtern(String),
243 #[error("absolute path has no stable cache mapping: {0}")]
245 UnmappedAbsolutePath(PathBuf),
246 #[error("cache key paths must be valid UTF-8: {0}")]
248 NonUtf8Path(PathBuf),
249 #[error("cache action working directory must be absolute: {0}")]
251 RelativeWorkingDirectory(PathBuf),
252 #[error("cache path mapping must use an absolute root: {0}")]
254 RelativePathMapping(PathBuf),
255 #[error("cache path mapping placeholder is invalid: {0}")]
257 InvalidPathPlaceholder(String),
258 #[error("required compiler input was not provided: {0}")]
260 MissingRequiredInput(String),
261 #[error("compiler input has an invalid digest: {0}")]
263 InvalidInputDigest(String),
264 #[error("compiler input appears more than once with different content: {0}")]
266 ConflictingInput(String),
267 #[error("rustc dep-info is malformed: {0}")]
269 MalformedDepInfo(String),
270 #[error("failed to read rustc dep-info {path}: {message}")]
272 DepInfoRead {
273 path: PathBuf,
275 message: String,
277 },
278 #[error("rustc dep-info output path must be absolute: {0}")]
280 RelativeDepInfoPath(PathBuf),
281 #[error("rustc dep-info output path cannot contain a comma: {0}")]
283 UnsafeDepInfoPath(PathBuf),
284 #[error("failed to read compiler input {path}: {message}")]
286 InputRead {
287 path: PathBuf,
289 message: String,
291 },
292 #[error("compiler input changed after discovery: {0}")]
294 InputChanged(PathBuf),
295 #[error("compiler input was modified during compilation: {0}")]
297 InputModifiedDuringCompilation(PathBuf),
298 #[error("discovered inputs were collected from a different working directory")]
300 DiscoveryWorkingDirectory,
301 #[error("compiler environment input has conflicting values: {0}")]
303 ConflictingEnvironment(String),
304 #[error("failed to serialize the rustc action: {0}")]
306 Serialization(String),
307 #[error("rustc action prediction is unsupported")]
309 UnsupportedPrediction,
310 #[error("rustc action prediction contains an invalid input path: {0}")]
312 InvalidPredictedInput(String),
313}
314
315#[derive(Debug, Clone, PartialEq, Eq)]
316enum Argument {
317 Plain(String),
318 Path {
319 flag: String,
320 path: PathBuf,
321 },
322 SearchPath {
323 kind: String,
324 path: PathBuf,
325 },
326 Extern {
327 name: String,
328 path: Option<PathBuf>,
329 },
330 Emit(Vec<Emit>),
331 RemapPath {
332 from: PathBuf,
333 to: String,
334 },
335 OsoPrefix {
338 path: PathBuf,
339 trailing_slash: bool,
340 },
341 FuseLd(String),
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348struct Emit {
349 kind: String,
350 path: Option<PathBuf>,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub struct RustcInvocation {
359 arguments: Vec<Argument>,
360 source: PathBuf,
361 required_inputs: Vec<PathBuf>,
362 crate_name: String,
363 extra_filename: String,
364 out_dir: Option<PathBuf>,
365 explicit_output: Option<PathBuf>,
366 emits: Vec<Emit>,
367 target: Option<String>,
368 link_output: LinkOutput,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372enum LinkOutput {
373 Library,
374 WasmExecutable,
375 NativeExecutable,
376 NativeProcMacro,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
385#[non_exhaustive]
386pub struct ParseOptions {
387 pub cache_native_links: bool,
390}
391
392impl ParseOptions {
393 pub fn caching_native_links(enabled: bool) -> Self {
395 Self {
396 cache_native_links: enabled,
397 }
398 }
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct RustcOutputs {
404 pub directory: PathBuf,
406 pub files: Vec<PathBuf>,
408 pub dep_info: PathBuf,
410}
411
412impl RustcInvocation {
413 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
422 Self::parse_with(arguments, ParseOptions::default())
423 }
424
425 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
428 let expanded = expand_response_files(arguments)?;
429 Parser::new(&expanded.arguments, options).parse()
430 }
431
432 pub fn links_natively(&self) -> bool {
435 matches!(
436 self.link_output,
437 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
438 )
439 }
440
441 pub fn linker_override(&self) -> Option<&Path> {
444 self.arguments.iter().find_map(|argument| {
445 let Argument::Plain(value) = argument else {
446 return None;
447 };
448 value.strip_prefix("--codegen=linker=").map(Path::new)
449 })
450 }
451
452 pub fn fuse_ld(&self) -> Option<&str> {
455 self.arguments.iter().find_map(|argument| {
456 let Argument::FuseLd(selection) = argument else {
457 return None;
458 };
459 Some(selection.as_str())
460 })
461 }
462
463 fn emits_windows_pdb(&self) -> bool {
464 if !cfg!(windows) || !self.links_natively() {
465 return false;
466 }
467 let mut enabled = false;
468 for argument in &self.arguments {
469 let Argument::Plain(value) = argument else {
470 continue;
471 };
472 if value == "-g" {
473 enabled = true;
474 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
475 enabled = !matches!(value, "0" | "none");
476 }
477 }
478 enabled
479 }
480
481 fn native_search_is_inert(&self) -> bool {
494 self.link_output == LinkOutput::Library
495 }
496
497 pub fn source(&self) -> &Path {
499 &self.source
500 }
501
502 pub fn target(&self) -> Option<&str> {
504 self.target.as_deref()
505 }
506
507 pub fn crate_name(&self) -> &str {
509 &self.crate_name
510 }
511
512 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
525 let linked = self
526 .arguments
527 .iter()
528 .filter_map(|argument| match argument {
529 Argument::Extern {
530 path: Some(path), ..
531 } => Some(path.as_path()),
532 _ => None,
533 })
534 .collect::<BTreeSet<_>>();
535 let owned = discovered
536 .inputs
537 .iter()
538 .filter(|input| !linked.contains(input.path.as_path()))
539 .map(|input| (input.path.as_path(), &input.digest))
540 .collect::<BTreeMap<_, _>>();
541 let mut bytes = Vec::new();
542 for (path, digest) in owned {
543 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
544 bytes.push(0);
545 bytes.extend_from_slice(digest.key().as_bytes());
546 bytes.push(0);
547 }
548 CacheDigest::blake3(&bytes)
549 }
550
551 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
556 if !working_dir.is_absolute() {
557 return Err(BypassReason::RelativeWorkingDirectory(
558 working_dir.to_path_buf(),
559 ));
560 }
561 let explicit_output = self
562 .explicit_output
563 .as_deref()
564 .map(|path| absolute_path(path, working_dir));
565 let output_directory = explicit_output
566 .as_deref()
567 .and_then(Path::parent)
568 .map(Path::to_path_buf)
569 .or_else(|| {
570 self.out_dir
571 .as_deref()
572 .map(|path| absolute_path(path, working_dir))
573 })
574 .unwrap_or_else(|| normalize_components(working_dir));
575 if let Some(output) = &explicit_output
579 && self.emits.iter().any(|emit| {
580 emit.path.is_none()
581 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
582 })
583 {
584 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
585 }
586 let mut files = BTreeSet::new();
587 let mut dep_info = None;
588 for emit in &self.emits {
589 if emit.kind == "dep-info" {
590 let path = emit.path.as_ref().map_or_else(
591 || {
592 explicit_output.clone().map_or_else(
593 || {
594 output_directory
595 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
596 },
597 |path| path.with_extension("d"),
598 )
599 },
600 |path| absolute_path(path, working_dir),
601 );
602 if path.file_name().is_none() {
603 return Err(BypassReason::InvalidOutputPath(path));
604 }
605 dep_info = Some(path);
606 continue;
607 }
608 let (prefix, extension) = match emit.kind.as_str() {
609 "link" => match self.link_output {
610 LinkOutput::Library => ("lib", "rlib"),
611 LinkOutput::WasmExecutable => ("", "wasm"),
612 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
613 LinkOutput::NativeProcMacro => (
614 std::env::consts::DLL_PREFIX,
615 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
616 ),
617 },
618 "metadata" => ("lib", "rmeta"),
619 _ => continue,
620 };
621 let path = if let Some(path) = &emit.path {
622 absolute_path(path, working_dir)
623 } else {
624 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
625 output_directory.join(if extension.is_empty() {
626 name
627 } else {
628 format!("{name}.{extension}")
629 })
630 };
631 if path.file_name().is_none() {
632 return Err(BypassReason::InvalidOutputPath(path));
633 }
634 if path.parent() != Some(output_directory.as_path()) {
635 return Err(BypassReason::SplitOutputDirectories);
636 }
637 let has_library_extension = path
642 .extension()
643 .and_then(|extension| extension.to_str())
644 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
645 || (cfg!(windows)
646 && path
647 .file_stem()
648 .and_then(|stem| Path::new(stem).extension())
649 .and_then(|extension| extension.to_str())
650 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
651 if emit.kind == "link"
652 && !matches!(self.link_output, LinkOutput::Library)
653 && has_library_extension
654 {
655 return Err(BypassReason::AmbiguousOutputName(path));
656 }
657 if emit.kind == "link" && self.emits_windows_pdb() {
658 files.insert(path.with_extension("pdb"));
659 }
660 files.insert(path);
661 }
662 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
663 if dep_info.parent() != Some(output_directory.as_path()) {
664 return Err(BypassReason::SplitOutputDirectories);
665 }
666 Ok(RustcOutputs {
667 directory: output_directory,
668 files: files.into_iter().collect(),
669 dep_info,
670 })
671 }
672
673 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
679 self.action_linked_by(context, None)
680 }
681
682 pub fn action_linked_by(
691 &self,
692 context: ActionContext,
693 linker: Option<LinkerIdentity>,
694 ) -> Result<RustcAction, BypassReason> {
695 ActionBuilder::new(self, context).linked_by(linker).build()
696 }
697
698 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
700 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
701 let bytes = canonical_json(&descriptor)
702 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
703 Ok(CacheDigest::blake3(&bytes))
704 }
705
706 pub fn prediction(
709 &self,
710 context: &ActionContext,
711 discovered: &DiscoveredInputs,
712 ) -> Result<RustcInputPrediction, BypassReason> {
713 let builder = ActionBuilder::new(self, context.clone());
714 builder.validate_mappings()?;
715 let mut native_directories = BTreeSet::new();
716 for argument in &self.arguments {
717 if let Argument::SearchPath { kind, path } = argument
718 && kind == "native"
719 {
720 match builder.normalize_path(path) {
721 Ok(normalized) => {
722 native_directories.insert(normalized);
723 }
724 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
730 }
731 Err(error) => return Err(error),
732 }
733 }
734 }
735 let mut inputs = BTreeSet::new();
742 for input in &discovered.inputs {
743 let normalized = builder.normalize_path(&input.path)?;
744 if !under_any_directory(&normalized, &native_directories) {
745 inputs.insert(normalized);
746 }
747 }
748 inputs.extend(
749 native_directories
750 .into_iter()
751 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
752 );
753 Ok(RustcInputPrediction {
754 version: 4,
755 inputs: inputs.into_iter().collect(),
756 environment: discovered.environment.keys().cloned().collect(),
757 compiler_duration_ns: 0,
758 crate_name: String::new(),
759 })
760 }
761}
762
763impl RustcOutputs {
764 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
767 (crate_name == "build_script_build")
768 .then(|| self.files.iter().find(|path| self.is_executable(path)))
769 .flatten()
770 .map(PathBuf::as_path)
771 }
772
773 pub fn is_executable(&self, path: &Path) -> bool {
781 self.files.iter().any(|output| output == path)
782 && !matches!(
783 path.extension().and_then(|extension| extension.to_str()),
784 Some("rlib" | "rmeta")
785 )
786 }
787}
788
789#[derive(Debug, Clone, PartialEq, Eq)]
790pub struct PathMapping {
792 pub root: PathBuf,
794 pub placeholder: String,
796}
797
798impl PathMapping {
799 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
801 Self {
802 root: root.into(),
803 placeholder: placeholder.into(),
804 }
805 }
806
807 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
810 let mut ordered = mappings.to_vec();
811 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
812 ordered
813 }
814}
815
816fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
817 mappings
818 .iter()
819 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
820 .collect()
821}
822
823pub fn normalize_mapped_path(
830 path: &Path,
831 working_dir: &Path,
832 mappings: &[PathMapping],
833) -> Result<String, BypassReason> {
834 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
835}
836
837#[derive(Debug, Clone, PartialEq, Eq)]
838pub struct CompilerIdentity {
840 pub toolchain: String,
842 pub rustc_version: String,
844 pub host: String,
846 pub driver: Option<String>,
848}
849
850#[derive(Debug, Clone, PartialEq, Eq)]
852pub struct ActionInput {
853 pub path: PathBuf,
855 pub digest: CacheDigest,
857}
858
859#[derive(Debug, Clone, PartialEq, Eq)]
861pub struct ActionContext {
862 pub compiler: CompilerIdentity,
864 pub working_dir: PathBuf,
866 pub path_mappings: Vec<PathMapping>,
868 pub environment: BTreeMap<String, Option<String>>,
870 pub portable_environment: BTreeSet<String>,
877 pub inputs: Vec<ActionInput>,
879}
880
881#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
891#[serde(deny_unknown_fields)]
892pub struct LinkerIdentity {
893 pub driver: String,
895 pub driver_version: String,
897 pub linker_version: String,
899 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
901 pub crt_objects: BTreeMap<String, CacheDigest>,
902 #[serde(skip_serializing_if = "Option::is_none", default)]
904 pub sdk: Option<String>,
905 #[serde(skip_serializing_if = "Option::is_none", default)]
907 pub deployment_target: Option<String>,
908}
909
910#[derive(Debug, Clone, PartialEq, Eq)]
912pub struct RustcAction {
913 pub digest: CacheDigest,
915 pub bytes: Vec<u8>,
917}
918
919#[derive(Debug, Clone, PartialEq, Eq)]
922pub struct RustcInputPrediction {
923 pub version: u8,
925 pub inputs: Vec<String>,
927 pub environment: Vec<String>,
929 pub compiler_duration_ns: u64,
932 pub crate_name: String,
934}
935
936#[derive(Serialize, Deserialize)]
937#[serde(deny_unknown_fields)]
938struct RustcInputPredictionWire {
939 version: u8,
940 inputs: Vec<String>,
941 environment: Vec<String>,
942 #[serde(default, skip_serializing_if = "is_zero")]
943 compiler_duration_ns: u64,
944 #[serde(default, skip_serializing_if = "String::is_empty")]
945 crate_name: String,
946}
947
948impl Serialize for RustcInputPrediction {
949 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
950 where
951 S: Serializer,
952 {
953 RustcInputPredictionWire {
954 version: self.version,
955 inputs: if self.version == 4 {
956 compact_prediction_inputs(&self.inputs)
957 } else {
958 self.inputs.clone()
959 },
960 environment: self.environment.clone(),
961 compiler_duration_ns: self.compiler_duration_ns,
962 crate_name: self.crate_name.clone(),
963 }
964 .serialize(serializer)
965 }
966}
967
968impl<'de> Deserialize<'de> for RustcInputPrediction {
969 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
970 where
971 D: Deserializer<'de>,
972 {
973 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
974 let inputs = if wire.version == 4 {
975 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
976 } else {
977 wire.inputs
978 };
979 Ok(Self {
980 version: wire.version,
981 inputs,
982 environment: wire.environment,
983 compiler_duration_ns: wire.compiler_duration_ns,
984 crate_name: wire.crate_name,
985 })
986 }
987}
988
989fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
990 let mut previous = "";
991 inputs
992 .iter()
993 .map(|input| {
994 let mut shared = previous
995 .bytes()
996 .zip(input.bytes())
997 .take_while(|(left, right)| left == right)
998 .count();
999 while !input.is_char_boundary(shared) {
1000 shared -= 1;
1001 }
1002 let compact = format!("{shared}:{}", &input[shared..]);
1003 previous = input;
1004 compact
1005 })
1006 .collect()
1007}
1008
1009fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1010 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1011 let mut decoded_bytes = 0_usize;
1012 for input in inputs {
1013 let (shared_text, suffix) = input
1014 .split_once(':')
1015 .ok_or("compact rustc prediction input has no prefix length")?;
1016 let shared: usize = shared_text
1017 .parse()
1018 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1019 if shared.to_string() != shared_text
1020 || shared > expanded.last().map_or(0, String::len)
1021 || expanded
1022 .last()
1023 .is_some_and(|previous| !previous.is_char_boundary(shared))
1024 {
1025 return Err("compact rustc prediction prefix is not canonical");
1026 }
1027 let mut path = expanded
1028 .last()
1029 .map_or_else(String::new, |previous| previous[..shared].to_string());
1030 path.push_str(suffix);
1031 decoded_bytes = decoded_bytes
1032 .checked_add(path.len())
1033 .ok_or("compact rustc prediction is too large")?;
1034 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1035 return Err("compact rustc prediction is too large");
1036 }
1037 expanded.push(path);
1038 }
1039 Ok(expanded)
1040}
1041
1042fn is_zero(value: &u64) -> bool {
1043 *value == 0
1044}
1045
1046impl RustcInputPrediction {
1047 pub fn discover(
1050 &self,
1051 working_dir: &Path,
1052 path_mappings: &[PathMapping],
1053 digests: &dyn FileDigestCache,
1054 ) -> Result<DiscoveredInputs, BypassReason> {
1055 if !matches!(self.version, 2..=4) {
1056 return Err(BypassReason::UnsupportedPrediction);
1057 }
1058 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1059 return Err(BypassReason::UnsupportedPrediction);
1060 }
1061 let mut paths = BTreeSet::new();
1062 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1063 let mut native_bytes = 0_u64;
1064 for path in &self.inputs {
1065 if self.version >= 3
1066 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1067 {
1068 let directory = denormalize_path(path, path_mappings)?;
1069 dep_info::collect_native_directory(
1070 &directory,
1071 &admitted_roots,
1072 &mut paths,
1073 &mut native_bytes,
1074 )?;
1075 } else {
1076 paths.insert(denormalize_path(path, path_mappings)?);
1077 }
1078 }
1079 let environment = self
1080 .environment
1081 .iter()
1082 .map(|name| {
1083 if name.is_empty() || name.contains(['=', '\0']) {
1084 return Err(BypassReason::UnsupportedPrediction);
1085 }
1086 let value = std::env::var_os(name)
1087 .map(|value| {
1088 value
1089 .into_string()
1090 .map_err(|_| BypassReason::UnsupportedPrediction)
1091 })
1092 .transpose()?;
1093 Ok((name.clone(), value))
1094 })
1095 .collect::<Result<BTreeMap<_, _>, _>>()?;
1096 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1097 }
1098}
1099
1100#[derive(Serialize)]
1101struct ActionDescriptor {
1102 version: u8,
1103 kind: &'static str,
1104 adapter_version: u8,
1105 compiler: CompilerDescriptor,
1106 arguments: Vec<String>,
1107 environment: BTreeMap<String, Option<String>>,
1108 inputs: Vec<InputDescriptor>,
1109 #[serde(skip_serializing_if = "Option::is_none")]
1112 linker: Option<LinkerIdentity>,
1113}
1114
1115#[derive(Serialize)]
1116struct InvocationDescriptor {
1117 version: u8,
1118 kind: &'static str,
1119 adapter_version: u8,
1120 compiler: CompilerDescriptor,
1121 arguments: Vec<String>,
1122 required_inputs: Vec<String>,
1123}
1124
1125#[derive(Serialize)]
1126struct CompilerDescriptor {
1127 toolchain: String,
1128 rustc_version: String,
1129 host: String,
1130 #[serde(skip_serializing_if = "Option::is_none")]
1131 driver: Option<String>,
1132}
1133
1134#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1135struct InputDescriptor {
1136 path: String,
1137 digest: CacheDigest,
1138}
1139
1140struct Parser<'a> {
1141 arguments: &'a [OsString],
1142 index: usize,
1143 parsed: Vec<Argument>,
1144 source: Option<PathBuf>,
1145 crate_types: Vec<String>,
1146 emits: Vec<Emit>,
1147 required_inputs: Vec<PathBuf>,
1148 test: bool,
1149 crate_name: Option<String>,
1150 extra_filename: String,
1151 out_dir: Option<PathBuf>,
1152 explicit_output: Option<PathBuf>,
1153 target: Option<String>,
1154 options: ParseOptions,
1155}
1156
1157struct ExpandedArguments {
1158 arguments: Vec<OsString>,
1159}
1160
1161#[derive(Default)]
1162struct ResponseExpander {
1163 shell_argfiles: bool,
1164 next_is_unstable_option: bool,
1165 arguments: Vec<OsString>,
1166}
1167
1168impl ResponseExpander {
1169 fn push(&mut self, argument: String) {
1170 if self.next_is_unstable_option {
1171 self.shell_argfiles |= argument == "shell-argfiles";
1172 self.next_is_unstable_option = false;
1173 } else if let Some(option) = argument.strip_prefix("-Z") {
1174 if option.is_empty() {
1175 self.next_is_unstable_option = true;
1176 } else {
1177 self.shell_argfiles |= option == "shell-argfiles";
1178 }
1179 }
1180 self.arguments.push(argument.into());
1181 }
1182}
1183
1184fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1187 let mut expanded = ResponseExpander::default();
1188 for (index, argument) in arguments.iter().enumerate() {
1189 let argument = argument
1190 .to_str()
1191 .ok_or(BypassReason::NonUtf8Argument { index })?;
1192 let Some(argfile) = argument.strip_prefix('@') else {
1193 expanded.push(argument.to_string());
1194 continue;
1195 };
1196 let (path, shell) = match argfile.split_once(':') {
1197 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1198 _ => (argfile, false),
1199 };
1200 let contents = std::fs::read_to_string(path).map_err(|error| {
1201 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1202 })?;
1203 if shell {
1204 let arguments = shlex::split(&contents).ok_or_else(|| {
1205 BypassReason::ResponseFile(format!(
1206 "invalid shell-style arguments in {}",
1207 Path::new(path).display()
1208 ))
1209 })?;
1210 for argument in arguments {
1211 expanded.push(argument);
1212 }
1213 } else {
1214 for argument in contents.lines() {
1215 expanded.push(argument.to_string());
1216 }
1217 }
1218 }
1219 Ok(ExpandedArguments {
1220 arguments: expanded.arguments,
1221 })
1222}
1223
1224impl<'a> Parser<'a> {
1225 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1226 Self {
1227 arguments,
1228 options,
1229 index: 0,
1230 parsed: Vec::new(),
1231 source: None,
1232 crate_types: Vec::new(),
1233 emits: Vec::new(),
1234 required_inputs: Vec::new(),
1235 test: false,
1236 crate_name: None,
1237 extra_filename: String::new(),
1238 out_dir: None,
1239 explicit_output: None,
1240 target: None,
1241 }
1242 }
1243
1244 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1245 while self.index < self.arguments.len() {
1246 let value = self.current()?.to_string();
1247 self.index += 1;
1248 if let Some(long) = value.strip_prefix("--") {
1249 self.parse_long(long)?;
1250 } else if value.starts_with('-') && value != "-" {
1251 self.parse_short(&value)?;
1252 } else {
1253 self.parse_input(&value)?;
1254 }
1255 }
1256
1257 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1258 let link_output = self.classify()?;
1259 let crate_name = self.crate_name.clone().map_or_else(
1260 || {
1261 source
1262 .file_stem()
1263 .and_then(|name| name.to_str())
1264 .map(|name| name.replace('-', "_"))
1265 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1266 },
1267 Ok,
1268 )?;
1269 self.required_inputs.push(source.clone());
1270 Ok(RustcInvocation {
1271 arguments: self.parsed,
1272 source,
1273 required_inputs: self.required_inputs,
1274 crate_name,
1275 extra_filename: self.extra_filename,
1276 out_dir: self.out_dir,
1277 explicit_output: self.explicit_output,
1278 emits: self.emits,
1279 target: self.target,
1280 link_output,
1281 })
1282 }
1283
1284 fn current(&self) -> Result<&str, BypassReason> {
1285 self.arguments[self.index]
1286 .to_str()
1287 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1288 }
1289
1290 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1291 if let Some(value) = inline {
1292 if value.is_empty() {
1293 return Err(BypassReason::MissingValue(flag.into()));
1294 }
1295 return Ok(value.into());
1296 }
1297 if self.index >= self.arguments.len() {
1298 return Err(BypassReason::MissingValue(flag.into()));
1299 }
1300 let value = self.current()?.to_string();
1301 self.index += 1;
1302 Ok(value)
1303 }
1304
1305 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1306 let (flag, inline) = value
1307 .split_once('=')
1308 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1309 let rendered_flag = format!("--{flag}");
1310 match flag {
1311 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1312 "test" => {
1313 self.test = true;
1314 self.parsed.push(Argument::Plain(rendered_flag));
1315 Ok(())
1316 }
1317 "verbose" => {
1318 self.parsed.push(Argument::Plain(rendered_flag));
1319 Ok(())
1320 }
1321 "crate-name" => {
1322 let value = self.take_value(&rendered_flag, inline)?;
1323 self.crate_name = Some(value.clone());
1324 self.parsed
1325 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1326 Ok(())
1327 }
1328 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1329 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1330 | "deny" | "forbid" | "cap-lints" => {
1331 let value = self.take_value(&rendered_flag, inline)?;
1332 self.parsed
1333 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1334 Ok(())
1335 }
1336 "target" => {
1337 let value = self.take_value(&rendered_flag, inline)?;
1338 self.target = Some(value.clone());
1339 if value.ends_with(".json") || value.contains(['/', '\\']) {
1340 let path = PathBuf::from(value);
1341 self.required_inputs.push(path.clone());
1342 self.parsed.push(Argument::Path {
1343 flag: rendered_flag,
1344 path,
1345 });
1346 } else {
1347 self.target = Some(value.clone());
1348 self.parsed
1349 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1350 }
1351 Ok(())
1352 }
1353 "crate-type" => {
1354 let value = self.take_value(&rendered_flag, inline)?;
1355 self.crate_types
1356 .extend(value.split(',').map(ToOwned::to_owned));
1357 self.parsed
1358 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1359 Ok(())
1360 }
1361 "emit" => {
1362 let value = self.take_value(&rendered_flag, inline)?;
1363 let emits = parse_emits(&value);
1364 self.emits.extend(emits.clone());
1365 self.parsed.push(Argument::Emit(emits));
1366 Ok(())
1367 }
1368 "out-dir" => {
1369 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1370 self.out_dir = Some(path.clone());
1371 self.parsed.push(Argument::Path {
1372 flag: rendered_flag,
1373 path,
1374 });
1375 Ok(())
1376 }
1377 "sysroot" => {
1378 let path = self.take_value(&rendered_flag, inline)?;
1379 self.parsed.push(Argument::Path {
1380 flag: rendered_flag,
1381 path: path.into(),
1382 });
1383 Ok(())
1384 }
1385 "extern" => {
1386 let value = self.take_value(&rendered_flag, inline)?;
1387 let (name, path) = value
1388 .split_once('=')
1389 .map_or((value.as_str(), None), |(name, path)| {
1390 (name, Some(PathBuf::from(path)))
1391 });
1392 if let Some(path) = &path {
1393 self.required_inputs.push(path.clone());
1394 }
1395 self.parsed.push(Argument::Extern {
1396 name: name.into(),
1397 path,
1398 });
1399 Ok(())
1400 }
1401 "remap-path-prefix" => {
1402 let value = self.take_value(&rendered_flag, inline)?;
1403 let Some((from, to)) = value.split_once('=') else {
1404 return Err(BypassReason::MissingValue(rendered_flag));
1405 };
1406 self.parsed.push(Argument::RemapPath {
1407 from: from.into(),
1408 to: to.into(),
1409 });
1410 Ok(())
1411 }
1412 "codegen" => {
1413 let value = self.take_value(&rendered_flag, inline)?;
1414 self.parse_codegen(&value)
1415 }
1416 "jobs-frontend" => {
1417 let value = self.take_value(&rendered_flag, inline)?;
1418 self.parsed
1419 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1420 Ok(())
1421 }
1422 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1423 }
1424 }
1425
1426 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1427 if let Some(attached) = value.strip_prefix("-Z") {
1428 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1429 match option.as_str() {
1430 "shell-argfiles" | "unstable-options" => {
1431 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1432 return Ok(());
1433 }
1434 "threads" | "threads=" => {
1435 return Err(BypassReason::MissingValue("-Zthreads".into()));
1436 }
1437 _ => {}
1438 }
1439 if option.starts_with("threads=") {
1440 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1441 return Ok(());
1442 }
1443 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1444 }
1445 match value {
1446 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1449 "-g" | "-O" | "-v" => {
1450 self.parsed.push(Argument::Plain(value.into()));
1451 return Ok(());
1452 }
1453 _ => {}
1454 }
1455 for (short, long) in [
1456 ("-A", "--allow"),
1457 ("-W", "--warn"),
1458 ("-D", "--deny"),
1459 ("-F", "--forbid"),
1460 ] {
1461 if let Some(attached) = value.strip_prefix(short) {
1462 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1463 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1464 return Ok(());
1465 }
1466 }
1467 if let Some(attached) = value.strip_prefix("-C") {
1468 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1469 return self.parse_codegen(&option);
1470 }
1471 if let Some(attached) = value.strip_prefix("-L") {
1472 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1473 let (kind, path) = search
1474 .split_once('=')
1475 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1476 if !matches!(kind, "dependency" | "native") {
1477 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1478 }
1479 self.parsed.push(Argument::SearchPath {
1480 kind: kind.into(),
1481 path: path.into(),
1482 });
1483 return Ok(());
1484 }
1485 if value == "-l" || value.starts_with("-l") {
1486 return Err(BypassReason::NativeLibrary);
1487 }
1488 if let Some(attached) = value.strip_prefix("-o") {
1489 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1490 self.explicit_output = Some(path.clone().into());
1491 self.parsed.push(Argument::Path {
1492 flag: "-o".into(),
1493 path: path.into(),
1494 });
1495 return Ok(());
1496 }
1497 Err(BypassReason::UnknownFlag(value.into()))
1498 }
1499
1500 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1501 let name = value.split_once('=').map_or(value, |(name, _)| name);
1502 if name == "incremental" {
1503 return Err(BypassReason::Incremental);
1504 }
1505 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err()
1506 && !(cfg!(windows) && name == "linker")
1507 {
1508 return Err(BypassReason::UnknownCodegenOption(name.into()));
1509 }
1510 if name == "link-arg"
1517 && let Some((_, option)) = value.split_once('=')
1518 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1519 && !selection.is_empty()
1520 && !selection.contains(',')
1521 && !self.parsed.iter().any(
1522 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1523 )
1524 {
1525 if !self.parsed.iter().any(
1526 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1527 ) {
1528 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1529 }
1530 return Ok(());
1531 }
1532 if matches!(name, "link-arg" | "link-args")
1537 && let Some((_, option)) = value.split_once('=')
1538 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1539 && !prefix.trim_end_matches('/').is_empty()
1540 && !prefix.contains(',')
1541 {
1542 let trailing_slash = prefix.ends_with('/');
1543 self.parsed.push(Argument::OsoPrefix {
1544 path: PathBuf::from(prefix.trim_end_matches('/')),
1545 trailing_slash,
1546 });
1547 return Ok(());
1548 }
1549 self.parsed
1550 .push(Argument::Plain(format!("--codegen={value}")));
1551 if name == "extra-filename" {
1552 self.extra_filename = value
1553 .split_once('=')
1554 .map_or(String::new(), |(_, value)| value.to_string());
1555 }
1556 Ok(())
1557 }
1558
1559 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1560 if value == "-" {
1561 return Err(BypassReason::StandardInput);
1562 }
1563 if self.source.replace(value.into()).is_some() {
1564 return Err(BypassReason::MultipleInputs);
1565 }
1566 Ok(())
1567 }
1568
1569 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1570 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1579 let builds_a_library = !self.test
1580 && !self.crate_types.is_empty()
1581 && self
1582 .crate_types
1583 .iter()
1584 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1585 let link_output = if never_links || builds_a_library {
1586 LinkOutput::Library
1587 } else if self
1588 .target
1589 .as_deref()
1590 .is_some_and(compiler_bundled_wasm_target)
1591 && ((self.test && self.crate_types.is_empty())
1592 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1593 {
1594 if self.parsed.iter().any(|argument| match argument {
1595 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1596 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1597 !matches!(
1598 value.rsplit_once('=').map(|(_, value)| value),
1599 Some("y" | "yes" | "on" | "true")
1600 )
1601 }
1602 _ => false,
1603 }) {
1604 return Err(BypassReason::UnknownCodegenOption(
1605 "link-self-contained".into(),
1606 ));
1607 }
1608 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1609 && self.parsed.iter().any(|argument| {
1610 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1611 })
1612 {
1613 return Err(BypassReason::UnknownCodegenOption(
1614 "target-feature=-crt-static".into(),
1615 ));
1616 }
1617 LinkOutput::WasmExecutable
1621 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1622 self.check_native_link_is_portable()?;
1623 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1624 LinkOutput::NativeProcMacro
1625 } else {
1626 LinkOutput::NativeExecutable
1627 }
1628 } else if self.test {
1629 return Err(BypassReason::UnsupportedCrateType("test".into()));
1630 } else {
1631 return Err(BypassReason::UnsupportedCrateType(
1632 self.crate_types
1633 .iter()
1634 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1635 .cloned()
1636 .unwrap_or_else(|| "bin".into()),
1637 ));
1638 };
1639 if link_output != LinkOutput::Library
1648 && let Some(option) = self.first_link_argument()
1649 {
1650 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1651 }
1652 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1659 let Argument::FuseLd(selection) = argument else {
1660 return None;
1661 };
1662 Some(selection.as_str())
1663 }) && !matches!(
1664 link_output,
1665 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1666 ) {
1667 return Err(BypassReason::UnmodeledLinkArgument(format!(
1668 "link-arg=-fuse-ld={selection}"
1669 )));
1670 }
1671 if self.parsed.iter().any(|argument| {
1672 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1673 }) && !matches!(
1674 link_output,
1675 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1676 ) {
1677 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1678 }
1679 if !matches!(
1683 link_output,
1684 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1685 ) && self
1686 .parsed
1687 .iter()
1688 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1689 {
1690 return Err(BypassReason::UnmodeledLinkArgument(
1691 "link-arg=-Wl,-oso_prefix".into(),
1692 ));
1693 }
1694 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1695 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1696 _ => None,
1697 }) {
1698 return Err(BypassReason::UnresolvedExtern(name.clone()));
1699 }
1700 if let Some(emit) = self
1701 .emits
1702 .iter()
1703 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1704 {
1705 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1706 }
1707 if !self
1708 .emits
1709 .iter()
1710 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1711 {
1712 return Err(BypassReason::NoCacheableOutput);
1713 }
1714 Ok(link_output)
1715 }
1716}
1717
1718impl Parser<'_> {
1719 fn first_link_argument(&self) -> Option<&str> {
1722 self.parsed.iter().find_map(|argument| {
1723 let Argument::Plain(value) = argument else {
1724 return None;
1725 };
1726 let option = value.strip_prefix("--codegen=")?;
1727 let name = option.split_once('=').map_or(option, |(name, _)| name);
1728 matches!(name, "link-arg" | "link-args").then_some(option)
1729 })
1730 }
1731
1732 fn oso_prefix_covers_outputs(&self) -> bool {
1741 let Some(directory) = self
1742 .out_dir
1743 .as_deref()
1744 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
1745 else {
1746 return false;
1747 };
1748 if !directory.is_absolute() {
1749 return false;
1750 }
1751 let directory = normalize_components(directory);
1752 self.parsed.iter().any(|argument| {
1753 let Argument::OsoPrefix { path, .. } = argument else {
1754 return false;
1755 };
1756 path.is_absolute() && directory.starts_with(normalize_components(path))
1757 })
1758 }
1759
1760 fn links_a_native_artifact(&self) -> bool {
1767 self.target.is_none()
1768 && self.emits.iter().any(|emit| emit.kind == "link")
1773 && ((self.test && self.crate_types.is_empty())
1774 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro")))
1775 }
1776
1777 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
1784 for argument in &self.parsed {
1785 let Argument::Plain(value) = argument else {
1786 continue;
1787 };
1788 let (name, value) = if value == "-g" {
1791 ("debuginfo", Some("2"))
1792 } else if let Some(option) = value.strip_prefix("--codegen=") {
1793 match option.split_once('=') {
1794 Some((name, value)) => (name, Some(value)),
1795 None => (option, None),
1799 }
1800 } else {
1801 continue;
1802 };
1803 let unportable = match name {
1804 "split-debuginfo" => match value {
1810 Some("off") => false,
1811 Some("unpacked") if cfg!(target_os = "macos") => {
1812 !self.oso_prefix_covers_outputs()
1813 }
1814 _ => true,
1815 },
1816 "debuginfo" if cfg!(target_os = "macos") => {
1825 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
1826 }
1827 "rpath" => is_enabled(value),
1835 "prefer-dynamic" => {
1836 is_enabled(value)
1837 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
1838 }
1839 "link-self-contained" => true,
1842 _ => false,
1843 };
1844 if unportable {
1845 return Err(BypassReason::UnportableNativeLink(match value {
1846 Some(value) => format!("{name}={value}"),
1847 None => name.to_owned(),
1848 }));
1849 }
1850 }
1851 Ok(())
1852 }
1853}
1854
1855fn is_enabled(value: Option<&str>) -> bool {
1858 matches!(value, None | Some("y" | "yes" | "on" | "true"))
1859}
1860
1861fn compiler_bundled_wasm_target(target: &str) -> bool {
1862 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
1863}
1864
1865fn parse_emits(value: &str) -> Vec<Emit> {
1866 value
1867 .split(',')
1868 .map(|emit| {
1869 let (kind, path) = emit
1870 .split_once('=')
1871 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
1872 Emit {
1873 kind: kind.into(),
1874 path,
1875 }
1876 })
1877 .collect()
1878}
1879
1880struct ActionBuilder<'a> {
1881 invocation: &'a RustcInvocation,
1882 context: ActionContext,
1883 mappings: Vec<SharedPathMapping>,
1884 linker: Option<LinkerIdentity>,
1885}
1886
1887impl<'a> ActionBuilder<'a> {
1888 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
1889 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1890 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
1891 Self {
1892 linker: None,
1893 invocation,
1894 mappings,
1895 context,
1896 }
1897 }
1898
1899 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
1900 self.linker = linker;
1901 self
1902 }
1903
1904 fn build(self) -> Result<RustcAction, BypassReason> {
1905 self.validate_mappings()?;
1906 let invocation = self.invocation_descriptor()?;
1907 let environment = self.environment_descriptor()?;
1908
1909 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1910 for input in &self.context.inputs {
1911 input
1912 .digest
1913 .validate()
1914 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
1915 let path = self.normalize_path(&input.path)?;
1916 if inputs
1917 .insert(path.clone(), input.digest.clone())
1918 .is_some_and(|existing| existing != input.digest)
1919 {
1920 return Err(BypassReason::ConflictingInput(path));
1921 }
1922 }
1923 let required = self
1924 .invocation
1925 .required_inputs
1926 .iter()
1927 .map(|path| self.normalize_path(path))
1928 .collect::<Result<BTreeSet<_>, _>>()?;
1929 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1930 return Err(BypassReason::MissingRequiredInput(missing.clone()));
1931 }
1932 let inputs = inputs
1933 .into_iter()
1934 .map(|(path, digest)| InputDescriptor { path, digest })
1935 .collect();
1936 if self.invocation.links_natively() && self.linker.is_none() {
1939 return Err(BypassReason::UnportableNativeLink(
1940 "linker identity is unknown".into(),
1941 ));
1942 }
1943 let descriptor = ActionDescriptor {
1944 version: ACTION_SCHEMA_VERSION,
1945 kind: "rustc",
1946 adapter_version: ADAPTER_VERSION,
1947 compiler: invocation.compiler,
1948 arguments: invocation.arguments,
1949 environment,
1950 inputs,
1951 linker: self.linker.clone(),
1952 };
1953 let bytes = canonical_json(&descriptor)
1954 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
1955 let digest = CacheDigest::blake3(&bytes);
1956 Ok(RustcAction { digest, bytes })
1957 }
1958
1959 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
1960 self.validate_mappings()?;
1961 let arguments = self
1962 .invocation
1963 .arguments
1964 .iter()
1965 .map(|argument| self.normalize_argument(argument))
1966 .collect::<Result<Vec<_>, _>>()?;
1967 let required_inputs = self
1968 .invocation
1969 .required_inputs
1970 .iter()
1971 .map(|path| self.normalize_path(path))
1972 .collect::<Result<BTreeSet<_>, _>>()?
1973 .into_iter()
1974 .collect();
1975 Ok(InvocationDescriptor {
1976 version: ACTION_SCHEMA_VERSION,
1977 kind: "rustc",
1978 adapter_version: ADAPTER_VERSION,
1979 compiler: CompilerDescriptor {
1980 toolchain: self.context.compiler.toolchain.clone(),
1981 rustc_version: self.context.compiler.rustc_version.clone(),
1982 host: self.context.compiler.host.clone(),
1983 driver: self.context.compiler.driver.clone(),
1984 },
1985 arguments,
1986 required_inputs,
1987 })
1988 }
1989
1990 fn validate_mappings(&self) -> Result<(), BypassReason> {
1991 if !self.context.working_dir.is_absolute() {
1992 return Err(BypassReason::RelativeWorkingDirectory(
1993 self.context.working_dir.clone(),
1994 ));
1995 }
1996 let mut roots = BTreeSet::new();
1997 let mut placeholders = BTreeSet::new();
1998 for mapping in &self.mappings {
1999 if !mapping.root.is_absolute() {
2000 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2001 }
2002 if mapping.placeholder.is_empty()
2003 || !mapping
2004 .placeholder
2005 .bytes()
2006 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2007 || !roots.insert(normalize_components(&mapping.root))
2008 || !placeholders.insert(&mapping.placeholder)
2009 {
2010 return Err(BypassReason::InvalidPathPlaceholder(
2011 mapping.placeholder.clone(),
2012 ));
2013 }
2014 }
2015 Ok(())
2016 }
2017
2018 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2019 match argument {
2020 Argument::Plain(value) => Ok(value.clone()),
2021 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2022 Argument::SearchPath { kind, path } => {
2023 let text = match self.normalize_path(path) {
2024 Ok(text) => text,
2025 Err(BypassReason::UnmappedAbsolutePath(absolute))
2033 if kind == "native" && self.invocation.native_search_is_inert() =>
2034 {
2035 absolute
2036 .to_str()
2037 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
2038 .to_string()
2039 }
2040 Err(error) => return Err(error),
2041 };
2042 Ok(format!("-L{kind}={text}"))
2043 }
2044 Argument::Extern { name, path } => match path {
2045 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2046 None => Ok(format!("--extern={name}")),
2047 },
2048 Argument::Emit(emits) => Ok(format!(
2049 "--emit={}",
2050 emits
2051 .iter()
2052 .map(|emit| match &emit.path {
2053 Some(path) => self
2054 .normalize_path(path)
2055 .map(|path| format!("{}={path}", emit.kind)),
2056 None => Ok(emit.kind.clone()),
2057 })
2058 .collect::<Result<Vec<_>, _>>()?
2059 .join(",")
2060 )),
2061 Argument::RemapPath { from, to } => Ok(format!(
2062 "--remap-path-prefix={}={}",
2063 self.normalize_path(from)?,
2064 to
2065 )),
2066 Argument::OsoPrefix {
2067 path,
2068 trailing_slash,
2069 } => Ok(format!(
2070 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2071 self.normalize_path(path)?,
2072 if *trailing_slash { "/" } else { "" }
2073 )),
2074 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2075 }
2076 }
2077
2078 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2085 self.context
2086 .environment
2087 .iter()
2088 .map(|(name, value)| {
2089 let value = match value {
2090 Some(value) if self.context.portable_environment.contains(name) => {
2091 Some(self.normalize_path(Path::new(value))?)
2092 }
2093 value => value.clone(),
2094 };
2095 Ok((name.clone(), value))
2096 })
2097 .collect()
2098 }
2099
2100 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2101 normalize_resolved_shared_path(path, &self.context.working_dir, &self.mappings)
2102 .map_err(Into::into)
2103 }
2104}
2105
2106fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2110 directories.iter().any(|directory| {
2111 path.len() > directory.len()
2112 && path.as_bytes()[directory.len()] == b'/'
2113 && path.starts_with(directory)
2114 })
2115}
2116
2117fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2118 for mapping in mappings {
2119 let prefix = format!("${{{}}}", mapping.placeholder);
2120 let suffix = if value == prefix {
2121 ""
2122 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2123 suffix
2124 } else {
2125 continue;
2126 };
2127 if !mapping.root.is_absolute()
2128 || (!suffix.is_empty()
2129 && suffix.split('/').any(|component| {
2130 component.is_empty()
2131 || matches!(component, "." | "..")
2132 || component.contains('\\')
2133 }))
2134 {
2135 return Err(BypassReason::InvalidPredictedInput(value.into()));
2136 }
2137 let mut path = normalize_components(&mapping.root);
2138 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2139 return Ok(path);
2140 }
2141 Err(BypassReason::InvalidPredictedInput(value.into()))
2142}
2143
2144fn normalize_components(path: &Path) -> PathBuf {
2145 let mut normalized = PathBuf::new();
2146 for component in path.components() {
2147 match component {
2148 Component::CurDir => {}
2149 Component::ParentDir => {
2150 normalized.pop();
2151 }
2152 component => normalized.push(component.as_os_str()),
2153 }
2154 }
2155 normalized
2156}
2157
2158fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2159 if path.is_absolute() {
2160 normalize_components(path)
2161 } else {
2162 normalize_components(&working_dir.join(path))
2163 }
2164}
2165
2166#[cfg(test)]
2167#[path = "rustc_cache_tests.rs"]
2168mod tests;