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}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
344struct Emit {
345 kind: String,
346 path: Option<PathBuf>,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct RustcInvocation {
355 arguments: Vec<Argument>,
356 source: PathBuf,
357 required_inputs: Vec<PathBuf>,
358 crate_name: String,
359 extra_filename: String,
360 out_dir: Option<PathBuf>,
361 explicit_output: Option<PathBuf>,
362 emits: Vec<Emit>,
363 target: Option<String>,
364 link_output: LinkOutput,
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368enum LinkOutput {
369 Library,
370 WasmExecutable,
371 NativeExecutable,
372 NativeProcMacro,
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
381#[non_exhaustive]
382pub struct ParseOptions {
383 pub cache_native_links: bool,
386}
387
388impl ParseOptions {
389 pub fn caching_native_links(enabled: bool) -> Self {
391 Self {
392 cache_native_links: enabled,
393 }
394 }
395}
396
397#[derive(Debug, Clone, PartialEq, Eq)]
399pub struct RustcOutputs {
400 pub directory: PathBuf,
402 pub files: Vec<PathBuf>,
404 pub dep_info: PathBuf,
406}
407
408impl RustcInvocation {
409 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
418 Self::parse_with(arguments, ParseOptions::default())
419 }
420
421 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
424 let expanded = expand_response_files(arguments)?;
425 Parser::new(&expanded.arguments, options).parse()
426 }
427
428 pub fn links_natively(&self) -> bool {
431 matches!(
432 self.link_output,
433 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
434 )
435 }
436
437 pub fn linker_override(&self) -> Option<&Path> {
440 self.arguments.iter().find_map(|argument| {
441 let Argument::Plain(value) = argument else {
442 return None;
443 };
444 value.strip_prefix("--codegen=linker=").map(Path::new)
445 })
446 }
447
448 fn emits_windows_pdb(&self) -> bool {
449 if !cfg!(windows) || !self.links_natively() {
450 return false;
451 }
452 let mut enabled = false;
453 for argument in &self.arguments {
454 let Argument::Plain(value) = argument else {
455 continue;
456 };
457 if value == "-g" {
458 enabled = true;
459 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
460 enabled = !matches!(value, "0" | "none");
461 }
462 }
463 enabled
464 }
465
466 fn native_search_is_inert(&self) -> bool {
479 self.link_output == LinkOutput::Library
480 }
481
482 pub fn source(&self) -> &Path {
484 &self.source
485 }
486
487 pub fn target(&self) -> Option<&str> {
489 self.target.as_deref()
490 }
491
492 pub fn crate_name(&self) -> &str {
494 &self.crate_name
495 }
496
497 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
510 let linked = self
511 .arguments
512 .iter()
513 .filter_map(|argument| match argument {
514 Argument::Extern {
515 path: Some(path), ..
516 } => Some(path.as_path()),
517 _ => None,
518 })
519 .collect::<BTreeSet<_>>();
520 let owned = discovered
521 .inputs
522 .iter()
523 .filter(|input| !linked.contains(input.path.as_path()))
524 .map(|input| (input.path.as_path(), &input.digest))
525 .collect::<BTreeMap<_, _>>();
526 let mut bytes = Vec::new();
527 for (path, digest) in owned {
528 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
529 bytes.push(0);
530 bytes.extend_from_slice(digest.key().as_bytes());
531 bytes.push(0);
532 }
533 CacheDigest::blake3(&bytes)
534 }
535
536 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
541 if !working_dir.is_absolute() {
542 return Err(BypassReason::RelativeWorkingDirectory(
543 working_dir.to_path_buf(),
544 ));
545 }
546 let explicit_output = self
547 .explicit_output
548 .as_deref()
549 .map(|path| absolute_path(path, working_dir));
550 let output_directory = explicit_output
551 .as_deref()
552 .and_then(Path::parent)
553 .map(Path::to_path_buf)
554 .or_else(|| {
555 self.out_dir
556 .as_deref()
557 .map(|path| absolute_path(path, working_dir))
558 })
559 .unwrap_or_else(|| normalize_components(working_dir));
560 if let Some(output) = &explicit_output
564 && self.emits.iter().any(|emit| {
565 emit.path.is_none()
566 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
567 })
568 {
569 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
570 }
571 let mut files = BTreeSet::new();
572 let mut dep_info = None;
573 for emit in &self.emits {
574 if emit.kind == "dep-info" {
575 let path = emit.path.as_ref().map_or_else(
576 || {
577 explicit_output.clone().map_or_else(
578 || {
579 output_directory
580 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
581 },
582 |path| path.with_extension("d"),
583 )
584 },
585 |path| absolute_path(path, working_dir),
586 );
587 if path.file_name().is_none() {
588 return Err(BypassReason::InvalidOutputPath(path));
589 }
590 dep_info = Some(path);
591 continue;
592 }
593 let (prefix, extension) = match emit.kind.as_str() {
594 "link" => match self.link_output {
595 LinkOutput::Library => ("lib", "rlib"),
596 LinkOutput::WasmExecutable => ("", "wasm"),
597 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
598 LinkOutput::NativeProcMacro => (
599 std::env::consts::DLL_PREFIX,
600 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
601 ),
602 },
603 "metadata" => ("lib", "rmeta"),
604 _ => continue,
605 };
606 let path = if let Some(path) = &emit.path {
607 absolute_path(path, working_dir)
608 } else {
609 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
610 output_directory.join(if extension.is_empty() {
611 name
612 } else {
613 format!("{name}.{extension}")
614 })
615 };
616 if path.file_name().is_none() {
617 return Err(BypassReason::InvalidOutputPath(path));
618 }
619 if path.parent() != Some(output_directory.as_path()) {
620 return Err(BypassReason::SplitOutputDirectories);
621 }
622 let has_library_extension = path
627 .extension()
628 .and_then(|extension| extension.to_str())
629 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
630 || (cfg!(windows)
631 && path
632 .file_stem()
633 .and_then(|stem| Path::new(stem).extension())
634 .and_then(|extension| extension.to_str())
635 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
636 if emit.kind == "link"
637 && !matches!(self.link_output, LinkOutput::Library)
638 && has_library_extension
639 {
640 return Err(BypassReason::AmbiguousOutputName(path));
641 }
642 if emit.kind == "link" && self.emits_windows_pdb() {
643 files.insert(path.with_extension("pdb"));
644 }
645 files.insert(path);
646 }
647 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
648 if dep_info.parent() != Some(output_directory.as_path()) {
649 return Err(BypassReason::SplitOutputDirectories);
650 }
651 Ok(RustcOutputs {
652 directory: output_directory,
653 files: files.into_iter().collect(),
654 dep_info,
655 })
656 }
657
658 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
664 self.action_linked_by(context, None)
665 }
666
667 pub fn action_linked_by(
676 &self,
677 context: ActionContext,
678 linker: Option<LinkerIdentity>,
679 ) -> Result<RustcAction, BypassReason> {
680 ActionBuilder::new(self, context).linked_by(linker).build()
681 }
682
683 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
685 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
686 let bytes = canonical_json(&descriptor)
687 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
688 Ok(CacheDigest::blake3(&bytes))
689 }
690
691 pub fn prediction(
694 &self,
695 context: &ActionContext,
696 discovered: &DiscoveredInputs,
697 ) -> Result<RustcInputPrediction, BypassReason> {
698 let builder = ActionBuilder::new(self, context.clone());
699 builder.validate_mappings()?;
700 let mut native_directories = BTreeSet::new();
701 for argument in &self.arguments {
702 if let Argument::SearchPath { kind, path } = argument
703 && kind == "native"
704 {
705 match builder.normalize_path(path) {
706 Ok(normalized) => {
707 native_directories.insert(normalized);
708 }
709 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
715 }
716 Err(error) => return Err(error),
717 }
718 }
719 }
720 let mut inputs = BTreeSet::new();
727 for input in &discovered.inputs {
728 let normalized = builder.normalize_path(&input.path)?;
729 if !under_any_directory(&normalized, &native_directories) {
730 inputs.insert(normalized);
731 }
732 }
733 inputs.extend(
734 native_directories
735 .into_iter()
736 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
737 );
738 Ok(RustcInputPrediction {
739 version: 4,
740 inputs: inputs.into_iter().collect(),
741 environment: discovered.environment.keys().cloned().collect(),
742 compiler_duration_ns: 0,
743 crate_name: String::new(),
744 })
745 }
746}
747
748impl RustcOutputs {
749 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
752 (crate_name == "build_script_build")
753 .then(|| self.files.iter().find(|path| self.is_executable(path)))
754 .flatten()
755 .map(PathBuf::as_path)
756 }
757
758 pub fn is_executable(&self, path: &Path) -> bool {
766 self.files.iter().any(|output| output == path)
767 && !matches!(
768 path.extension().and_then(|extension| extension.to_str()),
769 Some("rlib" | "rmeta")
770 )
771 }
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
775pub struct PathMapping {
777 pub root: PathBuf,
779 pub placeholder: String,
781}
782
783impl PathMapping {
784 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
786 Self {
787 root: root.into(),
788 placeholder: placeholder.into(),
789 }
790 }
791
792 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
795 let mut ordered = mappings.to_vec();
796 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
797 ordered
798 }
799}
800
801fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
802 mappings
803 .iter()
804 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
805 .collect()
806}
807
808pub fn normalize_mapped_path(
815 path: &Path,
816 working_dir: &Path,
817 mappings: &[PathMapping],
818) -> Result<String, BypassReason> {
819 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
820}
821
822#[derive(Debug, Clone, PartialEq, Eq)]
823pub struct CompilerIdentity {
825 pub toolchain: String,
827 pub rustc_version: String,
829 pub host: String,
831}
832
833#[derive(Debug, Clone, PartialEq, Eq)]
835pub struct ActionInput {
836 pub path: PathBuf,
838 pub digest: CacheDigest,
840}
841
842#[derive(Debug, Clone, PartialEq, Eq)]
844pub struct ActionContext {
845 pub compiler: CompilerIdentity,
847 pub working_dir: PathBuf,
849 pub path_mappings: Vec<PathMapping>,
851 pub environment: BTreeMap<String, Option<String>>,
853 pub portable_environment: BTreeSet<String>,
860 pub inputs: Vec<ActionInput>,
862}
863
864#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
874#[serde(deny_unknown_fields)]
875pub struct LinkerIdentity {
876 pub driver: String,
878 pub driver_version: String,
880 pub linker_version: String,
882 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
884 pub crt_objects: BTreeMap<String, CacheDigest>,
885 #[serde(skip_serializing_if = "Option::is_none", default)]
887 pub sdk: Option<String>,
888 #[serde(skip_serializing_if = "Option::is_none", default)]
890 pub deployment_target: Option<String>,
891}
892
893#[derive(Debug, Clone, PartialEq, Eq)]
895pub struct RustcAction {
896 pub digest: CacheDigest,
898 pub bytes: Vec<u8>,
900}
901
902#[derive(Debug, Clone, PartialEq, Eq)]
905pub struct RustcInputPrediction {
906 pub version: u8,
908 pub inputs: Vec<String>,
910 pub environment: Vec<String>,
912 pub compiler_duration_ns: u64,
915 pub crate_name: String,
917}
918
919#[derive(Serialize, Deserialize)]
920#[serde(deny_unknown_fields)]
921struct RustcInputPredictionWire {
922 version: u8,
923 inputs: Vec<String>,
924 environment: Vec<String>,
925 #[serde(default, skip_serializing_if = "is_zero")]
926 compiler_duration_ns: u64,
927 #[serde(default, skip_serializing_if = "String::is_empty")]
928 crate_name: String,
929}
930
931impl Serialize for RustcInputPrediction {
932 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
933 where
934 S: Serializer,
935 {
936 RustcInputPredictionWire {
937 version: self.version,
938 inputs: if self.version == 4 {
939 compact_prediction_inputs(&self.inputs)
940 } else {
941 self.inputs.clone()
942 },
943 environment: self.environment.clone(),
944 compiler_duration_ns: self.compiler_duration_ns,
945 crate_name: self.crate_name.clone(),
946 }
947 .serialize(serializer)
948 }
949}
950
951impl<'de> Deserialize<'de> for RustcInputPrediction {
952 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
953 where
954 D: Deserializer<'de>,
955 {
956 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
957 let inputs = if wire.version == 4 {
958 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
959 } else {
960 wire.inputs
961 };
962 Ok(Self {
963 version: wire.version,
964 inputs,
965 environment: wire.environment,
966 compiler_duration_ns: wire.compiler_duration_ns,
967 crate_name: wire.crate_name,
968 })
969 }
970}
971
972fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
973 let mut previous = "";
974 inputs
975 .iter()
976 .map(|input| {
977 let mut shared = previous
978 .bytes()
979 .zip(input.bytes())
980 .take_while(|(left, right)| left == right)
981 .count();
982 while !input.is_char_boundary(shared) {
983 shared -= 1;
984 }
985 let compact = format!("{shared}:{}", &input[shared..]);
986 previous = input;
987 compact
988 })
989 .collect()
990}
991
992fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
993 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
994 let mut decoded_bytes = 0_usize;
995 for input in inputs {
996 let (shared_text, suffix) = input
997 .split_once(':')
998 .ok_or("compact rustc prediction input has no prefix length")?;
999 let shared: usize = shared_text
1000 .parse()
1001 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1002 if shared.to_string() != shared_text
1003 || shared > expanded.last().map_or(0, String::len)
1004 || expanded
1005 .last()
1006 .is_some_and(|previous| !previous.is_char_boundary(shared))
1007 {
1008 return Err("compact rustc prediction prefix is not canonical");
1009 }
1010 let mut path = expanded
1011 .last()
1012 .map_or_else(String::new, |previous| previous[..shared].to_string());
1013 path.push_str(suffix);
1014 decoded_bytes = decoded_bytes
1015 .checked_add(path.len())
1016 .ok_or("compact rustc prediction is too large")?;
1017 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1018 return Err("compact rustc prediction is too large");
1019 }
1020 expanded.push(path);
1021 }
1022 Ok(expanded)
1023}
1024
1025fn is_zero(value: &u64) -> bool {
1026 *value == 0
1027}
1028
1029impl RustcInputPrediction {
1030 pub fn discover(
1033 &self,
1034 working_dir: &Path,
1035 path_mappings: &[PathMapping],
1036 digests: &dyn FileDigestCache,
1037 ) -> Result<DiscoveredInputs, BypassReason> {
1038 if !matches!(self.version, 2..=4) {
1039 return Err(BypassReason::UnsupportedPrediction);
1040 }
1041 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1042 return Err(BypassReason::UnsupportedPrediction);
1043 }
1044 let mut paths = BTreeSet::new();
1045 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1046 let mut native_bytes = 0_u64;
1047 for path in &self.inputs {
1048 if self.version >= 3
1049 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1050 {
1051 let directory = denormalize_path(path, path_mappings)?;
1052 dep_info::collect_native_directory(
1053 &directory,
1054 &admitted_roots,
1055 &mut paths,
1056 &mut native_bytes,
1057 )?;
1058 } else {
1059 paths.insert(denormalize_path(path, path_mappings)?);
1060 }
1061 }
1062 let environment = self
1063 .environment
1064 .iter()
1065 .map(|name| {
1066 if name.is_empty() || name.contains(['=', '\0']) {
1067 return Err(BypassReason::UnsupportedPrediction);
1068 }
1069 let value = std::env::var_os(name)
1070 .map(|value| {
1071 value
1072 .into_string()
1073 .map_err(|_| BypassReason::UnsupportedPrediction)
1074 })
1075 .transpose()?;
1076 Ok((name.clone(), value))
1077 })
1078 .collect::<Result<BTreeMap<_, _>, _>>()?;
1079 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1080 }
1081}
1082
1083#[derive(Serialize)]
1084struct ActionDescriptor {
1085 version: u8,
1086 kind: &'static str,
1087 adapter_version: u8,
1088 compiler: CompilerDescriptor,
1089 arguments: Vec<String>,
1090 environment: BTreeMap<String, Option<String>>,
1091 inputs: Vec<InputDescriptor>,
1092 #[serde(skip_serializing_if = "Option::is_none")]
1095 linker: Option<LinkerIdentity>,
1096}
1097
1098#[derive(Serialize)]
1099struct InvocationDescriptor {
1100 version: u8,
1101 kind: &'static str,
1102 adapter_version: u8,
1103 compiler: CompilerDescriptor,
1104 arguments: Vec<String>,
1105 required_inputs: Vec<String>,
1106}
1107
1108#[derive(Serialize)]
1109struct CompilerDescriptor {
1110 toolchain: String,
1111 rustc_version: String,
1112 host: String,
1113}
1114
1115#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1116struct InputDescriptor {
1117 path: String,
1118 digest: CacheDigest,
1119}
1120
1121struct Parser<'a> {
1122 arguments: &'a [OsString],
1123 index: usize,
1124 parsed: Vec<Argument>,
1125 source: Option<PathBuf>,
1126 crate_types: Vec<String>,
1127 emits: Vec<Emit>,
1128 required_inputs: Vec<PathBuf>,
1129 test: bool,
1130 crate_name: Option<String>,
1131 extra_filename: String,
1132 out_dir: Option<PathBuf>,
1133 explicit_output: Option<PathBuf>,
1134 target: Option<String>,
1135 options: ParseOptions,
1136}
1137
1138struct ExpandedArguments {
1139 arguments: Vec<OsString>,
1140}
1141
1142#[derive(Default)]
1143struct ResponseExpander {
1144 shell_argfiles: bool,
1145 next_is_unstable_option: bool,
1146 arguments: Vec<OsString>,
1147}
1148
1149impl ResponseExpander {
1150 fn push(&mut self, argument: String) {
1151 if self.next_is_unstable_option {
1152 self.shell_argfiles |= argument == "shell-argfiles";
1153 self.next_is_unstable_option = false;
1154 } else if let Some(option) = argument.strip_prefix("-Z") {
1155 if option.is_empty() {
1156 self.next_is_unstable_option = true;
1157 } else {
1158 self.shell_argfiles |= option == "shell-argfiles";
1159 }
1160 }
1161 self.arguments.push(argument.into());
1162 }
1163}
1164
1165fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1168 let mut expanded = ResponseExpander::default();
1169 for (index, argument) in arguments.iter().enumerate() {
1170 let argument = argument
1171 .to_str()
1172 .ok_or(BypassReason::NonUtf8Argument { index })?;
1173 let Some(argfile) = argument.strip_prefix('@') else {
1174 expanded.push(argument.to_string());
1175 continue;
1176 };
1177 let (path, shell) = match argfile.split_once(':') {
1178 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1179 _ => (argfile, false),
1180 };
1181 let contents = std::fs::read_to_string(path).map_err(|error| {
1182 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1183 })?;
1184 if shell {
1185 let arguments = shlex::split(&contents).ok_or_else(|| {
1186 BypassReason::ResponseFile(format!(
1187 "invalid shell-style arguments in {}",
1188 Path::new(path).display()
1189 ))
1190 })?;
1191 for argument in arguments {
1192 expanded.push(argument);
1193 }
1194 } else {
1195 for argument in contents.lines() {
1196 expanded.push(argument.to_string());
1197 }
1198 }
1199 }
1200 Ok(ExpandedArguments {
1201 arguments: expanded.arguments,
1202 })
1203}
1204
1205impl<'a> Parser<'a> {
1206 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1207 Self {
1208 arguments,
1209 options,
1210 index: 0,
1211 parsed: Vec::new(),
1212 source: None,
1213 crate_types: Vec::new(),
1214 emits: Vec::new(),
1215 required_inputs: Vec::new(),
1216 test: false,
1217 crate_name: None,
1218 extra_filename: String::new(),
1219 out_dir: None,
1220 explicit_output: None,
1221 target: None,
1222 }
1223 }
1224
1225 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1226 while self.index < self.arguments.len() {
1227 let value = self.current()?.to_string();
1228 self.index += 1;
1229 if let Some(long) = value.strip_prefix("--") {
1230 self.parse_long(long)?;
1231 } else if value.starts_with('-') && value != "-" {
1232 self.parse_short(&value)?;
1233 } else {
1234 self.parse_input(&value)?;
1235 }
1236 }
1237
1238 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1239 let link_output = self.classify()?;
1240 let crate_name = self.crate_name.clone().map_or_else(
1241 || {
1242 source
1243 .file_stem()
1244 .and_then(|name| name.to_str())
1245 .map(|name| name.replace('-', "_"))
1246 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1247 },
1248 Ok,
1249 )?;
1250 self.required_inputs.push(source.clone());
1251 Ok(RustcInvocation {
1252 arguments: self.parsed,
1253 source,
1254 required_inputs: self.required_inputs,
1255 crate_name,
1256 extra_filename: self.extra_filename,
1257 out_dir: self.out_dir,
1258 explicit_output: self.explicit_output,
1259 emits: self.emits,
1260 target: self.target,
1261 link_output,
1262 })
1263 }
1264
1265 fn current(&self) -> Result<&str, BypassReason> {
1266 self.arguments[self.index]
1267 .to_str()
1268 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1269 }
1270
1271 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1272 if let Some(value) = inline {
1273 if value.is_empty() {
1274 return Err(BypassReason::MissingValue(flag.into()));
1275 }
1276 return Ok(value.into());
1277 }
1278 if self.index >= self.arguments.len() {
1279 return Err(BypassReason::MissingValue(flag.into()));
1280 }
1281 let value = self.current()?.to_string();
1282 self.index += 1;
1283 Ok(value)
1284 }
1285
1286 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1287 let (flag, inline) = value
1288 .split_once('=')
1289 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1290 let rendered_flag = format!("--{flag}");
1291 match flag {
1292 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1293 "test" => {
1294 self.test = true;
1295 self.parsed.push(Argument::Plain(rendered_flag));
1296 Ok(())
1297 }
1298 "verbose" => {
1299 self.parsed.push(Argument::Plain(rendered_flag));
1300 Ok(())
1301 }
1302 "crate-name" => {
1303 let value = self.take_value(&rendered_flag, inline)?;
1304 self.crate_name = Some(value.clone());
1305 self.parsed
1306 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1307 Ok(())
1308 }
1309 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1310 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1311 | "deny" | "forbid" | "cap-lints" => {
1312 let value = self.take_value(&rendered_flag, inline)?;
1313 self.parsed
1314 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1315 Ok(())
1316 }
1317 "target" => {
1318 let value = self.take_value(&rendered_flag, inline)?;
1319 self.target = Some(value.clone());
1320 if value.ends_with(".json") || value.contains(['/', '\\']) {
1321 let path = PathBuf::from(value);
1322 self.required_inputs.push(path.clone());
1323 self.parsed.push(Argument::Path {
1324 flag: rendered_flag,
1325 path,
1326 });
1327 } else {
1328 self.target = Some(value.clone());
1329 self.parsed
1330 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1331 }
1332 Ok(())
1333 }
1334 "crate-type" => {
1335 let value = self.take_value(&rendered_flag, inline)?;
1336 self.crate_types
1337 .extend(value.split(',').map(ToOwned::to_owned));
1338 self.parsed
1339 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1340 Ok(())
1341 }
1342 "emit" => {
1343 let value = self.take_value(&rendered_flag, inline)?;
1344 let emits = parse_emits(&value);
1345 self.emits.extend(emits.clone());
1346 self.parsed.push(Argument::Emit(emits));
1347 Ok(())
1348 }
1349 "out-dir" => {
1350 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1351 self.out_dir = Some(path.clone());
1352 self.parsed.push(Argument::Path {
1353 flag: rendered_flag,
1354 path,
1355 });
1356 Ok(())
1357 }
1358 "sysroot" => {
1359 let path = self.take_value(&rendered_flag, inline)?;
1360 self.parsed.push(Argument::Path {
1361 flag: rendered_flag,
1362 path: path.into(),
1363 });
1364 Ok(())
1365 }
1366 "extern" => {
1367 let value = self.take_value(&rendered_flag, inline)?;
1368 let (name, path) = value
1369 .split_once('=')
1370 .map_or((value.as_str(), None), |(name, path)| {
1371 (name, Some(PathBuf::from(path)))
1372 });
1373 if let Some(path) = &path {
1374 self.required_inputs.push(path.clone());
1375 }
1376 self.parsed.push(Argument::Extern {
1377 name: name.into(),
1378 path,
1379 });
1380 Ok(())
1381 }
1382 "remap-path-prefix" => {
1383 let value = self.take_value(&rendered_flag, inline)?;
1384 let Some((from, to)) = value.split_once('=') else {
1385 return Err(BypassReason::MissingValue(rendered_flag));
1386 };
1387 self.parsed.push(Argument::RemapPath {
1388 from: from.into(),
1389 to: to.into(),
1390 });
1391 Ok(())
1392 }
1393 "codegen" => {
1394 let value = self.take_value(&rendered_flag, inline)?;
1395 self.parse_codegen(&value)
1396 }
1397 "jobs-frontend" => {
1398 let value = self.take_value(&rendered_flag, inline)?;
1399 self.parsed
1400 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1401 Ok(())
1402 }
1403 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1404 }
1405 }
1406
1407 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1408 if let Some(attached) = value.strip_prefix("-Z") {
1409 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1410 match option.as_str() {
1411 "shell-argfiles" | "unstable-options" => {
1412 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1413 return Ok(());
1414 }
1415 "threads" | "threads=" => {
1416 return Err(BypassReason::MissingValue("-Zthreads".into()));
1417 }
1418 _ => {}
1419 }
1420 if option.starts_with("threads=") {
1421 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1422 return Ok(());
1423 }
1424 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1425 }
1426 match value {
1427 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1430 "-g" | "-O" | "-v" => {
1431 self.parsed.push(Argument::Plain(value.into()));
1432 return Ok(());
1433 }
1434 _ => {}
1435 }
1436 for (short, long) in [
1437 ("-A", "--allow"),
1438 ("-W", "--warn"),
1439 ("-D", "--deny"),
1440 ("-F", "--forbid"),
1441 ] {
1442 if let Some(attached) = value.strip_prefix(short) {
1443 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1444 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1445 return Ok(());
1446 }
1447 }
1448 if let Some(attached) = value.strip_prefix("-C") {
1449 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1450 return self.parse_codegen(&option);
1451 }
1452 if let Some(attached) = value.strip_prefix("-L") {
1453 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1454 let (kind, path) = search
1455 .split_once('=')
1456 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1457 if !matches!(kind, "dependency" | "native") {
1458 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1459 }
1460 self.parsed.push(Argument::SearchPath {
1461 kind: kind.into(),
1462 path: path.into(),
1463 });
1464 return Ok(());
1465 }
1466 if value == "-l" || value.starts_with("-l") {
1467 return Err(BypassReason::NativeLibrary);
1468 }
1469 if let Some(attached) = value.strip_prefix("-o") {
1470 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1471 self.explicit_output = Some(path.clone().into());
1472 self.parsed.push(Argument::Path {
1473 flag: "-o".into(),
1474 path: path.into(),
1475 });
1476 return Ok(());
1477 }
1478 Err(BypassReason::UnknownFlag(value.into()))
1479 }
1480
1481 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1482 let name = value.split_once('=').map_or(value, |(name, _)| name);
1483 if name == "incremental" {
1484 return Err(BypassReason::Incremental);
1485 }
1486 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err()
1487 && !(cfg!(windows) && name == "linker")
1488 {
1489 return Err(BypassReason::UnknownCodegenOption(name.into()));
1490 }
1491 if matches!(name, "link-arg" | "link-args")
1496 && let Some((_, option)) = value.split_once('=')
1497 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1498 && !prefix.trim_end_matches('/').is_empty()
1499 && !prefix.contains(',')
1500 {
1501 let trailing_slash = prefix.ends_with('/');
1502 self.parsed.push(Argument::OsoPrefix {
1503 path: PathBuf::from(prefix.trim_end_matches('/')),
1504 trailing_slash,
1505 });
1506 return Ok(());
1507 }
1508 self.parsed
1509 .push(Argument::Plain(format!("--codegen={value}")));
1510 if name == "extra-filename" {
1511 self.extra_filename = value
1512 .split_once('=')
1513 .map_or(String::new(), |(_, value)| value.to_string());
1514 }
1515 Ok(())
1516 }
1517
1518 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1519 if value == "-" {
1520 return Err(BypassReason::StandardInput);
1521 }
1522 if self.source.replace(value.into()).is_some() {
1523 return Err(BypassReason::MultipleInputs);
1524 }
1525 Ok(())
1526 }
1527
1528 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1529 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1538 let builds_a_library = !self.test
1539 && !self.crate_types.is_empty()
1540 && self
1541 .crate_types
1542 .iter()
1543 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1544 let link_output = if never_links || builds_a_library {
1545 LinkOutput::Library
1546 } else if self
1547 .target
1548 .as_deref()
1549 .is_some_and(compiler_bundled_wasm_target)
1550 && ((self.test && self.crate_types.is_empty())
1551 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1552 {
1553 if self.parsed.iter().any(|argument| match argument {
1554 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1555 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1556 !matches!(
1557 value.rsplit_once('=').map(|(_, value)| value),
1558 Some("y" | "yes" | "on" | "true")
1559 )
1560 }
1561 _ => false,
1562 }) {
1563 return Err(BypassReason::UnknownCodegenOption(
1564 "link-self-contained".into(),
1565 ));
1566 }
1567 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1568 && self.parsed.iter().any(|argument| {
1569 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1570 })
1571 {
1572 return Err(BypassReason::UnknownCodegenOption(
1573 "target-feature=-crt-static".into(),
1574 ));
1575 }
1576 LinkOutput::WasmExecutable
1580 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1581 self.check_native_link_is_portable()?;
1582 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1583 LinkOutput::NativeProcMacro
1584 } else {
1585 LinkOutput::NativeExecutable
1586 }
1587 } else if self.test {
1588 return Err(BypassReason::UnsupportedCrateType("test".into()));
1589 } else {
1590 return Err(BypassReason::UnsupportedCrateType(
1591 self.crate_types
1592 .iter()
1593 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1594 .cloned()
1595 .unwrap_or_else(|| "bin".into()),
1596 ));
1597 };
1598 if link_output != LinkOutput::Library
1607 && let Some(option) = self.first_link_argument()
1608 {
1609 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1610 }
1611 if self.parsed.iter().any(|argument| {
1612 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1613 }) && !matches!(
1614 link_output,
1615 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1616 ) {
1617 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1618 }
1619 if !matches!(
1623 link_output,
1624 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1625 ) && self
1626 .parsed
1627 .iter()
1628 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1629 {
1630 return Err(BypassReason::UnmodeledLinkArgument(
1631 "link-arg=-Wl,-oso_prefix".into(),
1632 ));
1633 }
1634 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1635 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1636 _ => None,
1637 }) {
1638 return Err(BypassReason::UnresolvedExtern(name.clone()));
1639 }
1640 if let Some(emit) = self
1641 .emits
1642 .iter()
1643 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1644 {
1645 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1646 }
1647 if !self
1648 .emits
1649 .iter()
1650 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1651 {
1652 return Err(BypassReason::NoCacheableOutput);
1653 }
1654 Ok(link_output)
1655 }
1656}
1657
1658impl Parser<'_> {
1659 fn first_link_argument(&self) -> Option<&str> {
1662 self.parsed.iter().find_map(|argument| {
1663 let Argument::Plain(value) = argument else {
1664 return None;
1665 };
1666 let option = value.strip_prefix("--codegen=")?;
1667 let name = option.split_once('=').map_or(option, |(name, _)| name);
1668 matches!(name, "link-arg" | "link-args").then_some(option)
1669 })
1670 }
1671
1672 fn oso_prefix_covers_outputs(&self) -> bool {
1681 let Some(directory) = self
1682 .out_dir
1683 .as_deref()
1684 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
1685 else {
1686 return false;
1687 };
1688 if !directory.is_absolute() {
1689 return false;
1690 }
1691 let directory = normalize_components(directory);
1692 self.parsed.iter().any(|argument| {
1693 let Argument::OsoPrefix { path, .. } = argument else {
1694 return false;
1695 };
1696 path.is_absolute() && directory.starts_with(normalize_components(path))
1697 })
1698 }
1699
1700 fn links_a_native_artifact(&self) -> bool {
1707 self.target.is_none()
1708 && self.emits.iter().any(|emit| emit.kind == "link")
1713 && ((self.test && self.crate_types.is_empty())
1714 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro")))
1715 }
1716
1717 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
1724 for argument in &self.parsed {
1725 let Argument::Plain(value) = argument else {
1726 continue;
1727 };
1728 let (name, value) = if value == "-g" {
1731 ("debuginfo", Some("2"))
1732 } else if let Some(option) = value.strip_prefix("--codegen=") {
1733 match option.split_once('=') {
1734 Some((name, value)) => (name, Some(value)),
1735 None => (option, None),
1739 }
1740 } else {
1741 continue;
1742 };
1743 let unportable = match name {
1744 "split-debuginfo" => match value {
1750 Some("off") => false,
1751 Some("unpacked") if cfg!(target_os = "macos") => {
1752 !self.oso_prefix_covers_outputs()
1753 }
1754 _ => true,
1755 },
1756 "debuginfo" if cfg!(target_os = "macos") => {
1765 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
1766 }
1767 "rpath" => is_enabled(value),
1775 "prefer-dynamic" => {
1776 is_enabled(value)
1777 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
1778 }
1779 "link-self-contained" => true,
1782 _ => false,
1783 };
1784 if unportable {
1785 return Err(BypassReason::UnportableNativeLink(match value {
1786 Some(value) => format!("{name}={value}"),
1787 None => name.to_owned(),
1788 }));
1789 }
1790 }
1791 Ok(())
1792 }
1793}
1794
1795fn is_enabled(value: Option<&str>) -> bool {
1798 matches!(value, None | Some("y" | "yes" | "on" | "true"))
1799}
1800
1801fn compiler_bundled_wasm_target(target: &str) -> bool {
1802 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
1803}
1804
1805fn parse_emits(value: &str) -> Vec<Emit> {
1806 value
1807 .split(',')
1808 .map(|emit| {
1809 let (kind, path) = emit
1810 .split_once('=')
1811 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
1812 Emit {
1813 kind: kind.into(),
1814 path,
1815 }
1816 })
1817 .collect()
1818}
1819
1820struct ActionBuilder<'a> {
1821 invocation: &'a RustcInvocation,
1822 context: ActionContext,
1823 mappings: Vec<SharedPathMapping>,
1824 linker: Option<LinkerIdentity>,
1825}
1826
1827impl<'a> ActionBuilder<'a> {
1828 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
1829 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1830 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
1831 Self {
1832 linker: None,
1833 invocation,
1834 mappings,
1835 context,
1836 }
1837 }
1838
1839 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
1840 self.linker = linker;
1841 self
1842 }
1843
1844 fn build(self) -> Result<RustcAction, BypassReason> {
1845 self.validate_mappings()?;
1846 let invocation = self.invocation_descriptor()?;
1847 let environment = self.environment_descriptor()?;
1848
1849 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1850 for input in &self.context.inputs {
1851 input
1852 .digest
1853 .validate()
1854 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
1855 let path = self.normalize_path(&input.path)?;
1856 if inputs
1857 .insert(path.clone(), input.digest.clone())
1858 .is_some_and(|existing| existing != input.digest)
1859 {
1860 return Err(BypassReason::ConflictingInput(path));
1861 }
1862 }
1863 let required = self
1864 .invocation
1865 .required_inputs
1866 .iter()
1867 .map(|path| self.normalize_path(path))
1868 .collect::<Result<BTreeSet<_>, _>>()?;
1869 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1870 return Err(BypassReason::MissingRequiredInput(missing.clone()));
1871 }
1872 let inputs = inputs
1873 .into_iter()
1874 .map(|(path, digest)| InputDescriptor { path, digest })
1875 .collect();
1876 if self.invocation.links_natively() && self.linker.is_none() {
1879 return Err(BypassReason::UnportableNativeLink(
1880 "linker identity is unknown".into(),
1881 ));
1882 }
1883 let descriptor = ActionDescriptor {
1884 version: ACTION_SCHEMA_VERSION,
1885 kind: "rustc",
1886 adapter_version: ADAPTER_VERSION,
1887 compiler: invocation.compiler,
1888 arguments: invocation.arguments,
1889 environment,
1890 inputs,
1891 linker: self.linker.clone(),
1892 };
1893 let bytes = canonical_json(&descriptor)
1894 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
1895 let digest = CacheDigest::blake3(&bytes);
1896 Ok(RustcAction { digest, bytes })
1897 }
1898
1899 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
1900 self.validate_mappings()?;
1901 let arguments = self
1902 .invocation
1903 .arguments
1904 .iter()
1905 .map(|argument| self.normalize_argument(argument))
1906 .collect::<Result<Vec<_>, _>>()?;
1907 let required_inputs = self
1908 .invocation
1909 .required_inputs
1910 .iter()
1911 .map(|path| self.normalize_path(path))
1912 .collect::<Result<BTreeSet<_>, _>>()?
1913 .into_iter()
1914 .collect();
1915 Ok(InvocationDescriptor {
1916 version: ACTION_SCHEMA_VERSION,
1917 kind: "rustc",
1918 adapter_version: ADAPTER_VERSION,
1919 compiler: CompilerDescriptor {
1920 toolchain: self.context.compiler.toolchain.clone(),
1921 rustc_version: self.context.compiler.rustc_version.clone(),
1922 host: self.context.compiler.host.clone(),
1923 },
1924 arguments,
1925 required_inputs,
1926 })
1927 }
1928
1929 fn validate_mappings(&self) -> Result<(), BypassReason> {
1930 if !self.context.working_dir.is_absolute() {
1931 return Err(BypassReason::RelativeWorkingDirectory(
1932 self.context.working_dir.clone(),
1933 ));
1934 }
1935 let mut roots = BTreeSet::new();
1936 let mut placeholders = BTreeSet::new();
1937 for mapping in &self.mappings {
1938 if !mapping.root.is_absolute() {
1939 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
1940 }
1941 if mapping.placeholder.is_empty()
1942 || !mapping
1943 .placeholder
1944 .bytes()
1945 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1946 || !roots.insert(normalize_components(&mapping.root))
1947 || !placeholders.insert(&mapping.placeholder)
1948 {
1949 return Err(BypassReason::InvalidPathPlaceholder(
1950 mapping.placeholder.clone(),
1951 ));
1952 }
1953 }
1954 Ok(())
1955 }
1956
1957 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
1958 match argument {
1959 Argument::Plain(value) => Ok(value.clone()),
1960 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1961 Argument::SearchPath { kind, path } => {
1962 let text = match self.normalize_path(path) {
1963 Ok(text) => text,
1964 Err(BypassReason::UnmappedAbsolutePath(absolute))
1972 if kind == "native" && self.invocation.native_search_is_inert() =>
1973 {
1974 absolute
1975 .to_str()
1976 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
1977 .to_string()
1978 }
1979 Err(error) => return Err(error),
1980 };
1981 Ok(format!("-L{kind}={text}"))
1982 }
1983 Argument::Extern { name, path } => match path {
1984 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
1985 None => Ok(format!("--extern={name}")),
1986 },
1987 Argument::Emit(emits) => Ok(format!(
1988 "--emit={}",
1989 emits
1990 .iter()
1991 .map(|emit| match &emit.path {
1992 Some(path) => self
1993 .normalize_path(path)
1994 .map(|path| format!("{}={path}", emit.kind)),
1995 None => Ok(emit.kind.clone()),
1996 })
1997 .collect::<Result<Vec<_>, _>>()?
1998 .join(",")
1999 )),
2000 Argument::RemapPath { from, to } => Ok(format!(
2001 "--remap-path-prefix={}={}",
2002 self.normalize_path(from)?,
2003 to
2004 )),
2005 Argument::OsoPrefix {
2006 path,
2007 trailing_slash,
2008 } => Ok(format!(
2009 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2010 self.normalize_path(path)?,
2011 if *trailing_slash { "/" } else { "" }
2012 )),
2013 }
2014 }
2015
2016 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2023 self.context
2024 .environment
2025 .iter()
2026 .map(|(name, value)| {
2027 let value = match value {
2028 Some(value) if self.context.portable_environment.contains(name) => {
2029 Some(self.normalize_path(Path::new(value))?)
2030 }
2031 value => value.clone(),
2032 };
2033 Ok((name.clone(), value))
2034 })
2035 .collect()
2036 }
2037
2038 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2039 normalize_resolved_shared_path(path, &self.context.working_dir, &self.mappings)
2040 .map_err(Into::into)
2041 }
2042}
2043
2044fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2048 directories.iter().any(|directory| {
2049 path.len() > directory.len()
2050 && path.as_bytes()[directory.len()] == b'/'
2051 && path.starts_with(directory)
2052 })
2053}
2054
2055fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2056 for mapping in mappings {
2057 let prefix = format!("${{{}}}", mapping.placeholder);
2058 let suffix = if value == prefix {
2059 ""
2060 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2061 suffix
2062 } else {
2063 continue;
2064 };
2065 if !mapping.root.is_absolute()
2066 || (!suffix.is_empty()
2067 && suffix.split('/').any(|component| {
2068 component.is_empty()
2069 || matches!(component, "." | "..")
2070 || component.contains('\\')
2071 }))
2072 {
2073 return Err(BypassReason::InvalidPredictedInput(value.into()));
2074 }
2075 let mut path = normalize_components(&mapping.root);
2076 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2077 return Ok(path);
2078 }
2079 Err(BypassReason::InvalidPredictedInput(value.into()))
2080}
2081
2082fn normalize_components(path: &Path) -> PathBuf {
2083 let mut normalized = PathBuf::new();
2084 for component in path.components() {
2085 match component {
2086 Component::CurDir => {}
2087 Component::ParentDir => {
2088 normalized.pop();
2089 }
2090 component => normalized.push(component.as_os_str()),
2091 }
2092 }
2093 normalized
2094}
2095
2096fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2097 if path.is_absolute() {
2098 normalize_components(path)
2099 } else {
2100 normalize_components(&working_dir.join(path))
2101 }
2102}
2103
2104#[cfg(test)]
2105#[path = "rustc_cache_tests.rs"]
2106mod tests;