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 "linker-plugin-lto",
120 "lto",
121 "metadata",
122 "no-prepopulate-passes",
123 "opt-level",
124 "overflow-checks",
125 "panic",
126 "prefer-dynamic",
127 "relocation-model",
128 "rpath",
129 "save-temps",
130 "soft-float",
131 "split-debuginfo",
132 "split-dwarf-kind",
133 "strip",
134 "symbol-mangling-version",
135 "target-cpu",
136 "target-feature",
137 "tls-model",
138];
139
140const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
141const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
142const MAX_DECODED_PREDICTION_BYTES: usize = 16 * 1024 * 1024;
146const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
147
148const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
151 "wasm32-unknown-unknown",
152 "wasm32-wasip1",
153 "wasm32-wasip1-threads",
154 "wasm32-wasip2",
155 "wasm32v1-none",
156 "wasm64-unknown-unknown",
157];
158
159#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
160#[strum(serialize_all = "kebab-case")]
161#[non_exhaustive]
171pub enum BypassReason {
172 #[error("rustc argument {index} is not valid UTF-8")]
174 NonUtf8Argument {
175 index: usize,
177 },
178 #[error("could not model rustc response file: {0}")]
180 ResponseFile(String),
181 #[error("rustc flag is not modeled by the cache adapter: {0}")]
183 UnknownFlag(String),
184 #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
186 UnknownCodegenOption(String),
187 #[error("rustc flag requires a value: {0}")]
189 MissingValue(String),
190 #[error("rustc invocation is a compiler query, not a compilation")]
192 CompilerQuery,
193 #[error("rustc invocation reads source from standard input")]
195 StandardInput,
196 #[error("rustc invocation has no source input")]
198 MissingInput,
199 #[error("rustc invocation has multiple source inputs")]
201 MultipleInputs,
202 #[error("incremental compilation cannot be combined with action caching")]
204 Incremental,
205 #[error("rustc crate type is not cacheable yet: {0}")]
207 UnsupportedCrateType(String),
208 #[error("rustc output type is not cacheable yet: {0}")]
210 UnsupportedEmit(String),
211 #[error("rustc invocation does not emit a cacheable artifact")]
213 NoCacheableOutput,
214 #[error("rustc invocation does not emit dependency information")]
216 NoDepInfo,
217 #[error("rustc output paths do not share one directory")]
219 SplitOutputDirectories,
220 #[error("rustc output path has no file name: {0}")]
222 InvalidOutputPath(PathBuf),
223 #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
225 ImplicitEmitWithOutputFile(PathBuf),
226 #[error("native library lookup is not cacheable yet")]
228 NativeLibrary,
229 #[error("rustc output name does not distinguish a program from a library: {0}")]
231 AmbiguousOutputName(PathBuf),
232 #[error("native link is not reproducible across checkouts: {0}")]
234 UnportableNativeLink(String),
235 #[error("rustc link argument is not modeled by the cache adapter: {0}")]
237 UnmodeledLinkArgument(String),
238 #[error("rustc search path kind is not cacheable yet: {0}")]
240 UnsupportedSearchPath(String),
241 #[error("rustc extern does not identify an input artifact: {0}")]
243 UnresolvedExtern(String),
244 #[error("absolute path has no stable cache mapping: {0}")]
246 UnmappedAbsolutePath(PathBuf),
247 #[error("cache key paths must be valid UTF-8: {0}")]
249 NonUtf8Path(PathBuf),
250 #[error("cache action working directory must be absolute: {0}")]
252 RelativeWorkingDirectory(PathBuf),
253 #[error("cache path mapping must use an absolute root: {0}")]
255 RelativePathMapping(PathBuf),
256 #[error("cache path mapping placeholder is invalid: {0}")]
258 InvalidPathPlaceholder(String),
259 #[error("required compiler input was not provided: {0}")]
261 MissingRequiredInput(String),
262 #[error("compiler input has an invalid digest: {0}")]
264 InvalidInputDigest(String),
265 #[error("compiler input appears more than once with different content: {0}")]
267 ConflictingInput(String),
268 #[error("rustc dep-info is malformed: {0}")]
270 MalformedDepInfo(String),
271 #[error("failed to read rustc dep-info {path}: {message}")]
273 DepInfoRead {
274 path: PathBuf,
276 message: String,
278 },
279 #[error("rustc dep-info output path must be absolute: {0}")]
281 RelativeDepInfoPath(PathBuf),
282 #[error("rustc dep-info output path cannot contain a comma: {0}")]
284 UnsafeDepInfoPath(PathBuf),
285 #[error("failed to read compiler input {path}: {message}")]
287 InputRead {
288 path: PathBuf,
290 message: String,
292 },
293 #[error("compiler input changed after discovery: {0}")]
295 InputChanged(PathBuf),
296 #[error("compiler input was modified during compilation: {0}")]
298 InputModifiedDuringCompilation(PathBuf),
299 #[error("discovered inputs were collected from a different working directory")]
301 DiscoveryWorkingDirectory,
302 #[error("compiler environment input has conflicting values: {0}")]
304 ConflictingEnvironment(String),
305 #[error("failed to serialize the rustc action: {0}")]
307 Serialization(String),
308 #[error("rustc action prediction is unsupported")]
310 UnsupportedPrediction,
311 #[error("rustc action prediction contains an invalid input path: {0}")]
313 InvalidPredictedInput(String),
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
317enum Argument {
318 Plain(String),
319 Path {
320 flag: String,
321 path: PathBuf,
322 },
323 SearchPath {
324 kind: String,
325 path: PathBuf,
326 },
327 Extern {
328 name: String,
329 path: Option<PathBuf>,
330 },
331 Emit(Vec<Emit>),
332 RemapPath {
333 from: PathBuf,
334 to: String,
335 },
336 OsoPrefix {
339 path: PathBuf,
340 trailing_slash: bool,
341 },
342 FuseLd(String),
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
349struct Emit {
350 kind: String,
351 path: Option<PathBuf>,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq)]
355pub struct RustcInvocation {
360 arguments: Vec<Argument>,
361 source: PathBuf,
362 required_inputs: Vec<PathBuf>,
363 crate_name: String,
364 extra_filename: String,
365 out_dir: Option<PathBuf>,
366 explicit_output: Option<PathBuf>,
367 emits: Vec<Emit>,
368 target: Option<String>,
369 link_output: LinkOutput,
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373enum LinkOutput {
374 Library,
375 WasmExecutable,
376 NativeExecutable,
377 NativeProcMacro,
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
386#[non_exhaustive]
387pub struct ParseOptions {
388 pub cache_native_links: bool,
391}
392
393impl ParseOptions {
394 pub fn caching_native_links(enabled: bool) -> Self {
396 Self {
397 cache_native_links: enabled,
398 }
399 }
400}
401
402#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct RustcOutputs {
405 pub directory: PathBuf,
407 pub files: Vec<PathBuf>,
409 pub dep_info: PathBuf,
411}
412
413impl RustcInvocation {
414 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
423 Self::parse_with(arguments, ParseOptions::default())
424 }
425
426 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
429 let expanded = expand_response_files(arguments)?;
430 Parser::new(&expanded.arguments, options).parse()
431 }
432
433 pub fn links_natively(&self) -> bool {
436 matches!(
437 self.link_output,
438 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
439 )
440 }
441
442 pub fn linker_override(&self) -> Option<&Path> {
445 self.arguments.iter().find_map(|argument| {
446 let Argument::Plain(value) = argument else {
447 return None;
448 };
449 value.strip_prefix("--codegen=linker=").map(Path::new)
450 })
451 }
452
453 pub fn fuse_ld(&self) -> Option<&str> {
456 self.arguments.iter().find_map(|argument| {
457 let Argument::FuseLd(selection) = argument else {
458 return None;
459 };
460 Some(selection.as_str())
461 })
462 }
463
464 fn emits_windows_pdb(&self) -> bool {
465 if !cfg!(windows) || !self.links_natively() {
466 return false;
467 }
468 let mut enabled = false;
469 for argument in &self.arguments {
470 let Argument::Plain(value) = argument else {
471 continue;
472 };
473 if value == "-g" {
474 enabled = true;
475 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
476 enabled = !matches!(value, "0" | "none");
477 }
478 }
479 enabled
480 }
481
482 fn native_search_is_inert(&self) -> bool {
495 self.link_output == LinkOutput::Library
496 }
497
498 pub fn source(&self) -> &Path {
500 &self.source
501 }
502
503 pub fn target(&self) -> Option<&str> {
505 self.target.as_deref()
506 }
507
508 pub fn crate_name(&self) -> &str {
510 &self.crate_name
511 }
512
513 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
526 let linked = self
527 .arguments
528 .iter()
529 .filter_map(|argument| match argument {
530 Argument::Extern {
531 path: Some(path), ..
532 } => Some(path.as_path()),
533 _ => None,
534 })
535 .collect::<BTreeSet<_>>();
536 let owned = discovered
537 .inputs
538 .iter()
539 .filter(|input| !linked.contains(input.path.as_path()))
540 .map(|input| (input.path.as_path(), &input.digest))
541 .collect::<BTreeMap<_, _>>();
542 let mut bytes = Vec::new();
543 for (path, digest) in owned {
544 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
545 bytes.push(0);
546 bytes.extend_from_slice(digest.key().as_bytes());
547 bytes.push(0);
548 }
549 CacheDigest::blake3(&bytes)
550 }
551
552 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
557 if !working_dir.is_absolute() {
558 return Err(BypassReason::RelativeWorkingDirectory(
559 working_dir.to_path_buf(),
560 ));
561 }
562 let explicit_output = self
563 .explicit_output
564 .as_deref()
565 .map(|path| absolute_path(path, working_dir));
566 let output_directory = explicit_output
567 .as_deref()
568 .and_then(Path::parent)
569 .map(Path::to_path_buf)
570 .or_else(|| {
571 self.out_dir
572 .as_deref()
573 .map(|path| absolute_path(path, working_dir))
574 })
575 .unwrap_or_else(|| normalize_components(working_dir));
576 if let Some(output) = &explicit_output
580 && self.emits.iter().any(|emit| {
581 emit.path.is_none()
582 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
583 })
584 {
585 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
586 }
587 let mut files = BTreeSet::new();
588 let mut dep_info = None;
589 for emit in &self.emits {
590 if emit.kind == "dep-info" {
591 let path = emit.path.as_ref().map_or_else(
592 || {
593 explicit_output.clone().map_or_else(
594 || {
595 output_directory
596 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
597 },
598 |path| path.with_extension("d"),
599 )
600 },
601 |path| absolute_path(path, working_dir),
602 );
603 if path.file_name().is_none() {
604 return Err(BypassReason::InvalidOutputPath(path));
605 }
606 dep_info = Some(path);
607 continue;
608 }
609 let (prefix, extension) = match emit.kind.as_str() {
610 "link" => match self.link_output {
611 LinkOutput::Library => ("lib", "rlib"),
612 LinkOutput::WasmExecutable => ("", "wasm"),
613 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
614 LinkOutput::NativeProcMacro => (
615 std::env::consts::DLL_PREFIX,
616 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
617 ),
618 },
619 "metadata" => ("lib", "rmeta"),
620 _ => continue,
621 };
622 let path = if let Some(path) = &emit.path {
623 absolute_path(path, working_dir)
624 } else {
625 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
626 output_directory.join(if extension.is_empty() {
627 name
628 } else {
629 format!("{name}.{extension}")
630 })
631 };
632 if path.file_name().is_none() {
633 return Err(BypassReason::InvalidOutputPath(path));
634 }
635 if path.parent() != Some(output_directory.as_path()) {
636 return Err(BypassReason::SplitOutputDirectories);
637 }
638 let has_library_extension = path
643 .extension()
644 .and_then(|extension| extension.to_str())
645 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
646 || (cfg!(windows)
647 && path
648 .file_stem()
649 .and_then(|stem| Path::new(stem).extension())
650 .and_then(|extension| extension.to_str())
651 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
652 if emit.kind == "link"
653 && !matches!(self.link_output, LinkOutput::Library)
654 && has_library_extension
655 {
656 return Err(BypassReason::AmbiguousOutputName(path));
657 }
658 if emit.kind == "link" && self.emits_windows_pdb() {
659 files.insert(path.with_extension("pdb"));
660 }
661 files.insert(path);
662 }
663 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
664 if dep_info.parent() != Some(output_directory.as_path()) {
665 return Err(BypassReason::SplitOutputDirectories);
666 }
667 Ok(RustcOutputs {
668 directory: output_directory,
669 files: files.into_iter().collect(),
670 dep_info,
671 })
672 }
673
674 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
680 self.action_linked_by(context, None)
681 }
682
683 pub fn action_linked_by(
692 &self,
693 context: ActionContext,
694 linker: Option<LinkerIdentity>,
695 ) -> Result<RustcAction, BypassReason> {
696 ActionBuilder::new(self, context).linked_by(linker).build()
697 }
698
699 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
701 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
702 let bytes = canonical_json(&descriptor)
703 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
704 Ok(CacheDigest::blake3(&bytes))
705 }
706
707 pub fn prediction(
710 &self,
711 context: &ActionContext,
712 discovered: &DiscoveredInputs,
713 ) -> Result<RustcInputPrediction, BypassReason> {
714 let builder = ActionBuilder::new(self, context.clone());
715 builder.validate_mappings()?;
716 let mut native_directories = BTreeSet::new();
717 for argument in &self.arguments {
718 if let Argument::SearchPath { kind, path } = argument
719 && kind == "native"
720 {
721 match builder.normalize_path(path) {
722 Ok(normalized) => {
723 native_directories.insert(normalized);
724 }
725 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
731 }
732 Err(error) => return Err(error),
733 }
734 }
735 }
736 let mut inputs = BTreeSet::new();
743 for input in &discovered.inputs {
744 let normalized = builder.normalize_path(&input.path)?;
745 if !under_any_directory(&normalized, &native_directories) {
746 inputs.insert(normalized);
747 }
748 }
749 inputs.extend(
750 native_directories
751 .into_iter()
752 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
753 );
754 Ok(RustcInputPrediction {
755 version: 4,
756 inputs: inputs.into_iter().collect(),
757 environment: discovered.environment.keys().cloned().collect(),
758 compiler_duration_ns: 0,
759 crate_name: String::new(),
760 })
761 }
762}
763
764impl RustcOutputs {
765 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
768 (crate_name == "build_script_build")
769 .then(|| self.files.iter().find(|path| self.is_executable(path)))
770 .flatten()
771 .map(PathBuf::as_path)
772 }
773
774 pub fn is_executable(&self, path: &Path) -> bool {
782 self.files.iter().any(|output| output == path)
783 && !matches!(
784 path.extension().and_then(|extension| extension.to_str()),
785 Some("rlib" | "rmeta")
786 )
787 }
788}
789
790#[derive(Debug, Clone, PartialEq, Eq)]
791pub struct PathMapping {
793 pub root: PathBuf,
795 pub placeholder: String,
797}
798
799impl PathMapping {
800 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
802 Self {
803 root: root.into(),
804 placeholder: placeholder.into(),
805 }
806 }
807
808 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
811 let mut ordered = mappings.to_vec();
812 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
813 ordered
814 }
815}
816
817fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
818 mappings
819 .iter()
820 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
821 .collect()
822}
823
824pub fn normalize_mapped_path(
831 path: &Path,
832 working_dir: &Path,
833 mappings: &[PathMapping],
834) -> Result<String, BypassReason> {
835 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
836}
837
838#[derive(Debug, Clone, PartialEq, Eq)]
839pub struct CompilerIdentity {
841 pub toolchain: String,
843 pub rustc_version: String,
845 pub host: String,
847 pub driver: Option<String>,
849}
850
851#[derive(Debug, Clone, PartialEq, Eq)]
853pub struct ActionInput {
854 pub path: PathBuf,
856 pub digest: CacheDigest,
858}
859
860#[derive(Debug, Clone, PartialEq, Eq)]
862pub struct ActionContext {
863 pub compiler: CompilerIdentity,
865 pub working_dir: PathBuf,
867 pub path_mappings: Vec<PathMapping>,
869 pub environment: BTreeMap<String, Option<String>>,
871 pub portable_environment: BTreeSet<String>,
878 pub inputs: Vec<ActionInput>,
880}
881
882#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
892#[serde(deny_unknown_fields)]
893pub struct LinkerIdentity {
894 pub driver: String,
896 pub driver_version: String,
898 pub linker_version: String,
900 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
902 pub crt_objects: BTreeMap<String, CacheDigest>,
903 #[serde(skip_serializing_if = "Option::is_none", default)]
905 pub sdk: Option<String>,
906 #[serde(skip_serializing_if = "Option::is_none", default)]
908 pub deployment_target: Option<String>,
909}
910
911#[derive(Debug, Clone, PartialEq, Eq)]
913pub struct RustcAction {
914 pub digest: CacheDigest,
916 pub bytes: Vec<u8>,
918}
919
920#[derive(Debug, Clone, PartialEq, Eq)]
923pub struct RustcInputPrediction {
924 pub version: u8,
926 pub inputs: Vec<String>,
928 pub environment: Vec<String>,
930 pub compiler_duration_ns: u64,
933 pub crate_name: String,
935}
936
937#[derive(Serialize, Deserialize)]
938#[serde(deny_unknown_fields)]
939struct RustcInputPredictionWire {
940 version: u8,
941 inputs: Vec<String>,
942 environment: Vec<String>,
943 #[serde(default, skip_serializing_if = "is_zero")]
944 compiler_duration_ns: u64,
945 #[serde(default, skip_serializing_if = "String::is_empty")]
946 crate_name: String,
947}
948
949impl Serialize for RustcInputPrediction {
950 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
951 where
952 S: Serializer,
953 {
954 RustcInputPredictionWire {
955 version: self.version,
956 inputs: if self.version == 4 {
957 compact_prediction_inputs(&self.inputs)
958 } else {
959 self.inputs.clone()
960 },
961 environment: self.environment.clone(),
962 compiler_duration_ns: self.compiler_duration_ns,
963 crate_name: self.crate_name.clone(),
964 }
965 .serialize(serializer)
966 }
967}
968
969impl<'de> Deserialize<'de> for RustcInputPrediction {
970 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
971 where
972 D: Deserializer<'de>,
973 {
974 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
975 let inputs = if wire.version == 4 {
976 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
977 } else {
978 wire.inputs
979 };
980 Ok(Self {
981 version: wire.version,
982 inputs,
983 environment: wire.environment,
984 compiler_duration_ns: wire.compiler_duration_ns,
985 crate_name: wire.crate_name,
986 })
987 }
988}
989
990fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
991 let mut previous = "";
992 inputs
993 .iter()
994 .map(|input| {
995 let mut shared = previous
996 .bytes()
997 .zip(input.bytes())
998 .take_while(|(left, right)| left == right)
999 .count();
1000 while !input.is_char_boundary(shared) {
1001 shared -= 1;
1002 }
1003 let compact = format!("{shared}:{}", &input[shared..]);
1004 previous = input;
1005 compact
1006 })
1007 .collect()
1008}
1009
1010fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1011 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1012 let mut decoded_bytes = 0_usize;
1013 for input in inputs {
1014 let (shared_text, suffix) = input
1015 .split_once(':')
1016 .ok_or("compact rustc prediction input has no prefix length")?;
1017 let shared: usize = shared_text
1018 .parse()
1019 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1020 if shared.to_string() != shared_text
1021 || shared > expanded.last().map_or(0, String::len)
1022 || expanded
1023 .last()
1024 .is_some_and(|previous| !previous.is_char_boundary(shared))
1025 {
1026 return Err("compact rustc prediction prefix is not canonical");
1027 }
1028 let mut path = expanded
1029 .last()
1030 .map_or_else(String::new, |previous| previous[..shared].to_string());
1031 path.push_str(suffix);
1032 decoded_bytes = decoded_bytes
1033 .checked_add(path.len())
1034 .ok_or("compact rustc prediction is too large")?;
1035 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1036 return Err("compact rustc prediction is too large");
1037 }
1038 expanded.push(path);
1039 }
1040 Ok(expanded)
1041}
1042
1043fn is_zero(value: &u64) -> bool {
1044 *value == 0
1045}
1046
1047impl RustcInputPrediction {
1048 pub fn discover(
1051 &self,
1052 working_dir: &Path,
1053 path_mappings: &[PathMapping],
1054 digests: &dyn FileDigestCache,
1055 ) -> Result<DiscoveredInputs, BypassReason> {
1056 if !matches!(self.version, 2..=4) {
1057 return Err(BypassReason::UnsupportedPrediction);
1058 }
1059 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1060 return Err(BypassReason::UnsupportedPrediction);
1061 }
1062 let mut paths = BTreeSet::new();
1063 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1064 let mut native_bytes = 0_u64;
1065 for path in &self.inputs {
1066 if self.version >= 3
1067 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1068 {
1069 let directory = denormalize_path(path, path_mappings)?;
1070 dep_info::collect_native_directory(
1071 &directory,
1072 &admitted_roots,
1073 &mut paths,
1074 &mut native_bytes,
1075 )?;
1076 } else {
1077 paths.insert(denormalize_path(path, path_mappings)?);
1078 }
1079 }
1080 let environment = self
1081 .environment
1082 .iter()
1083 .map(|name| {
1084 if name.is_empty() || name.contains(['=', '\0']) {
1085 return Err(BypassReason::UnsupportedPrediction);
1086 }
1087 let value = std::env::var_os(name)
1088 .map(|value| {
1089 value
1090 .into_string()
1091 .map_err(|_| BypassReason::UnsupportedPrediction)
1092 })
1093 .transpose()?;
1094 Ok((name.clone(), value))
1095 })
1096 .collect::<Result<BTreeMap<_, _>, _>>()?;
1097 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1098 }
1099}
1100
1101#[derive(Serialize)]
1102struct ActionDescriptor {
1103 version: u8,
1104 kind: &'static str,
1105 adapter_version: u8,
1106 compiler: CompilerDescriptor,
1107 arguments: Vec<String>,
1108 environment: BTreeMap<String, Option<String>>,
1109 inputs: Vec<InputDescriptor>,
1110 #[serde(skip_serializing_if = "Option::is_none")]
1113 linker: Option<LinkerIdentity>,
1114}
1115
1116#[derive(Serialize)]
1117struct InvocationDescriptor {
1118 version: u8,
1119 kind: &'static str,
1120 adapter_version: u8,
1121 compiler: CompilerDescriptor,
1122 arguments: Vec<String>,
1123 required_inputs: Vec<String>,
1124}
1125
1126#[derive(Serialize)]
1127struct CompilerDescriptor {
1128 toolchain: String,
1129 rustc_version: String,
1130 host: String,
1131 #[serde(skip_serializing_if = "Option::is_none")]
1132 driver: Option<String>,
1133}
1134
1135#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1136struct InputDescriptor {
1137 path: String,
1138 digest: CacheDigest,
1139}
1140
1141struct Parser<'a> {
1142 arguments: &'a [OsString],
1143 index: usize,
1144 parsed: Vec<Argument>,
1145 source: Option<PathBuf>,
1146 crate_types: Vec<String>,
1147 emits: Vec<Emit>,
1148 required_inputs: Vec<PathBuf>,
1149 test: bool,
1150 crate_name: Option<String>,
1151 extra_filename: String,
1152 out_dir: Option<PathBuf>,
1153 explicit_output: Option<PathBuf>,
1154 target: Option<String>,
1155 options: ParseOptions,
1156}
1157
1158struct ExpandedArguments {
1159 arguments: Vec<OsString>,
1160}
1161
1162#[derive(Default)]
1163struct ResponseExpander {
1164 shell_argfiles: bool,
1165 next_is_unstable_option: bool,
1166 arguments: Vec<OsString>,
1167}
1168
1169impl ResponseExpander {
1170 fn push(&mut self, argument: String) {
1171 if self.next_is_unstable_option {
1172 self.shell_argfiles |= argument == "shell-argfiles";
1173 self.next_is_unstable_option = false;
1174 } else if let Some(option) = argument.strip_prefix("-Z") {
1175 if option.is_empty() {
1176 self.next_is_unstable_option = true;
1177 } else {
1178 self.shell_argfiles |= option == "shell-argfiles";
1179 }
1180 }
1181 self.arguments.push(argument.into());
1182 }
1183}
1184
1185fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1188 let mut expanded = ResponseExpander::default();
1189 for (index, argument) in arguments.iter().enumerate() {
1190 let argument = argument
1191 .to_str()
1192 .ok_or(BypassReason::NonUtf8Argument { index })?;
1193 let Some(argfile) = argument.strip_prefix('@') else {
1194 expanded.push(argument.to_string());
1195 continue;
1196 };
1197 let (path, shell) = match argfile.split_once(':') {
1198 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1199 _ => (argfile, false),
1200 };
1201 let contents = std::fs::read_to_string(path).map_err(|error| {
1202 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1203 })?;
1204 if shell {
1205 let arguments = shlex::split(&contents).ok_or_else(|| {
1206 BypassReason::ResponseFile(format!(
1207 "invalid shell-style arguments in {}",
1208 Path::new(path).display()
1209 ))
1210 })?;
1211 for argument in arguments {
1212 expanded.push(argument);
1213 }
1214 } else {
1215 for argument in contents.lines() {
1216 expanded.push(argument.to_string());
1217 }
1218 }
1219 }
1220 Ok(ExpandedArguments {
1221 arguments: expanded.arguments,
1222 })
1223}
1224
1225impl<'a> Parser<'a> {
1226 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1227 Self {
1228 arguments,
1229 options,
1230 index: 0,
1231 parsed: Vec::new(),
1232 source: None,
1233 crate_types: Vec::new(),
1234 emits: Vec::new(),
1235 required_inputs: Vec::new(),
1236 test: false,
1237 crate_name: None,
1238 extra_filename: String::new(),
1239 out_dir: None,
1240 explicit_output: None,
1241 target: None,
1242 }
1243 }
1244
1245 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1246 while self.index < self.arguments.len() {
1247 let value = self.current()?.to_string();
1248 self.index += 1;
1249 if let Some(long) = value.strip_prefix("--") {
1250 self.parse_long(long)?;
1251 } else if value.starts_with('-') && value != "-" {
1252 self.parse_short(&value)?;
1253 } else {
1254 self.parse_input(&value)?;
1255 }
1256 }
1257
1258 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1259 let link_output = self.classify()?;
1260 let crate_name = self.crate_name.clone().map_or_else(
1261 || {
1262 source
1263 .file_stem()
1264 .and_then(|name| name.to_str())
1265 .map(|name| name.replace('-', "_"))
1266 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1267 },
1268 Ok,
1269 )?;
1270 self.required_inputs.push(source.clone());
1271 Ok(RustcInvocation {
1272 arguments: self.parsed,
1273 source,
1274 required_inputs: self.required_inputs,
1275 crate_name,
1276 extra_filename: self.extra_filename,
1277 out_dir: self.out_dir,
1278 explicit_output: self.explicit_output,
1279 emits: self.emits,
1280 target: self.target,
1281 link_output,
1282 })
1283 }
1284
1285 fn current(&self) -> Result<&str, BypassReason> {
1286 self.arguments[self.index]
1287 .to_str()
1288 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1289 }
1290
1291 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1292 if let Some(value) = inline {
1293 if value.is_empty() {
1294 return Err(BypassReason::MissingValue(flag.into()));
1295 }
1296 return Ok(value.into());
1297 }
1298 if self.index >= self.arguments.len() {
1299 return Err(BypassReason::MissingValue(flag.into()));
1300 }
1301 let value = self.current()?.to_string();
1302 self.index += 1;
1303 Ok(value)
1304 }
1305
1306 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1307 let (flag, inline) = value
1308 .split_once('=')
1309 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1310 let rendered_flag = format!("--{flag}");
1311 match flag {
1312 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1313 "test" => {
1314 self.test = true;
1315 self.parsed.push(Argument::Plain(rendered_flag));
1316 Ok(())
1317 }
1318 "verbose" => {
1319 self.parsed.push(Argument::Plain(rendered_flag));
1320 Ok(())
1321 }
1322 "crate-name" => {
1323 let value = self.take_value(&rendered_flag, inline)?;
1324 self.crate_name = Some(value.clone());
1325 self.parsed
1326 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1327 Ok(())
1328 }
1329 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1330 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1331 | "deny" | "forbid" | "cap-lints" => {
1332 let value = self.take_value(&rendered_flag, inline)?;
1333 self.parsed
1334 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1335 Ok(())
1336 }
1337 "target" => {
1338 let value = self.take_value(&rendered_flag, inline)?;
1339 self.target = Some(value.clone());
1340 if value.ends_with(".json") || value.contains(['/', '\\']) {
1341 let path = PathBuf::from(value);
1342 self.required_inputs.push(path.clone());
1343 self.parsed.push(Argument::Path {
1344 flag: rendered_flag,
1345 path,
1346 });
1347 } else {
1348 self.target = Some(value.clone());
1349 self.parsed
1350 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1351 }
1352 Ok(())
1353 }
1354 "crate-type" => {
1355 let value = self.take_value(&rendered_flag, inline)?;
1356 self.crate_types
1357 .extend(value.split(',').map(ToOwned::to_owned));
1358 self.parsed
1359 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1360 Ok(())
1361 }
1362 "emit" => {
1363 let value = self.take_value(&rendered_flag, inline)?;
1364 let emits = parse_emits(&value);
1365 self.emits.extend(emits.clone());
1366 self.parsed.push(Argument::Emit(emits));
1367 Ok(())
1368 }
1369 "out-dir" => {
1370 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1371 self.out_dir = Some(path.clone());
1372 self.parsed.push(Argument::Path {
1373 flag: rendered_flag,
1374 path,
1375 });
1376 Ok(())
1377 }
1378 "sysroot" => {
1379 let path = self.take_value(&rendered_flag, inline)?;
1380 self.parsed.push(Argument::Path {
1381 flag: rendered_flag,
1382 path: path.into(),
1383 });
1384 Ok(())
1385 }
1386 "extern" => {
1387 let value = self.take_value(&rendered_flag, inline)?;
1388 let (name, path) = value
1389 .split_once('=')
1390 .map_or((value.as_str(), None), |(name, path)| {
1391 (name, Some(PathBuf::from(path)))
1392 });
1393 if let Some(path) = &path {
1394 self.required_inputs.push(path.clone());
1395 }
1396 self.parsed.push(Argument::Extern {
1397 name: name.into(),
1398 path,
1399 });
1400 Ok(())
1401 }
1402 "remap-path-prefix" => {
1403 let value = self.take_value(&rendered_flag, inline)?;
1404 let Some((from, to)) = value.split_once('=') else {
1405 return Err(BypassReason::MissingValue(rendered_flag));
1406 };
1407 self.parsed.push(Argument::RemapPath {
1408 from: from.into(),
1409 to: to.into(),
1410 });
1411 Ok(())
1412 }
1413 "codegen" => {
1414 let value = self.take_value(&rendered_flag, inline)?;
1415 self.parse_codegen(&value)
1416 }
1417 "jobs-frontend" => {
1418 let value = self.take_value(&rendered_flag, inline)?;
1419 self.parsed
1420 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1421 Ok(())
1422 }
1423 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1424 }
1425 }
1426
1427 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1428 if let Some(attached) = value.strip_prefix("-Z") {
1429 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1430 match option.as_str() {
1431 "shell-argfiles" | "unstable-options" => {
1432 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1433 return Ok(());
1434 }
1435 "threads" | "threads=" => {
1436 return Err(BypassReason::MissingValue("-Zthreads".into()));
1437 }
1438 _ => {}
1439 }
1440 if option.starts_with("threads=") {
1441 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1442 return Ok(());
1443 }
1444 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1445 }
1446 match value {
1447 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1450 "-g" | "-O" | "-v" => {
1451 self.parsed.push(Argument::Plain(value.into()));
1452 return Ok(());
1453 }
1454 _ => {}
1455 }
1456 for (short, long) in [
1457 ("-A", "--allow"),
1458 ("-W", "--warn"),
1459 ("-D", "--deny"),
1460 ("-F", "--forbid"),
1461 ] {
1462 if let Some(attached) = value.strip_prefix(short) {
1463 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1464 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1465 return Ok(());
1466 }
1467 }
1468 if let Some(attached) = value.strip_prefix("-C") {
1469 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1470 return self.parse_codegen(&option);
1471 }
1472 if let Some(attached) = value.strip_prefix("-L") {
1473 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1474 let (kind, path) = search
1475 .split_once('=')
1476 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1477 if !matches!(kind, "dependency" | "native") {
1478 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1479 }
1480 self.parsed.push(Argument::SearchPath {
1481 kind: kind.into(),
1482 path: path.into(),
1483 });
1484 return Ok(());
1485 }
1486 if value == "-l" || value.starts_with("-l") {
1487 return Err(BypassReason::NativeLibrary);
1488 }
1489 if let Some(attached) = value.strip_prefix("-o") {
1490 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1491 self.explicit_output = Some(path.clone().into());
1492 self.parsed.push(Argument::Path {
1493 flag: "-o".into(),
1494 path: path.into(),
1495 });
1496 return Ok(());
1497 }
1498 Err(BypassReason::UnknownFlag(value.into()))
1499 }
1500
1501 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1502 let name = value.split_once('=').map_or(value, |(name, _)| name);
1503 if name == "incremental" {
1504 return Err(BypassReason::Incremental);
1505 }
1506 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err()
1507 && !(cfg!(windows) && name == "linker")
1508 {
1509 return Err(BypassReason::UnknownCodegenOption(name.into()));
1510 }
1511 if name == "linker-plugin-lto"
1517 && let Some((_, selection)) = value.split_once('=')
1518 && !matches!(
1519 selection,
1520 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1521 )
1522 {
1523 return Err(BypassReason::UnknownCodegenOption(format!(
1524 "linker-plugin-lto={selection}"
1525 )));
1526 }
1527 if name == "link-arg"
1534 && let Some((_, option)) = value.split_once('=')
1535 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1536 && !selection.is_empty()
1537 && !selection.contains(',')
1538 && !self.parsed.iter().any(
1539 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1540 )
1541 {
1542 if !self.parsed.iter().any(
1543 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1544 ) {
1545 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1546 }
1547 return Ok(());
1548 }
1549 if matches!(name, "link-arg" | "link-args")
1554 && let Some((_, option)) = value.split_once('=')
1555 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1556 && !prefix.trim_end_matches('/').is_empty()
1557 && !prefix.contains(',')
1558 {
1559 let trailing_slash = prefix.ends_with('/');
1560 self.parsed.push(Argument::OsoPrefix {
1561 path: PathBuf::from(prefix.trim_end_matches('/')),
1562 trailing_slash,
1563 });
1564 return Ok(());
1565 }
1566 self.parsed
1567 .push(Argument::Plain(format!("--codegen={value}")));
1568 if name == "extra-filename" {
1569 self.extra_filename = value
1570 .split_once('=')
1571 .map_or(String::new(), |(_, value)| value.to_string());
1572 }
1573 Ok(())
1574 }
1575
1576 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1577 if value == "-" {
1578 return Err(BypassReason::StandardInput);
1579 }
1580 if self.source.replace(value.into()).is_some() {
1581 return Err(BypassReason::MultipleInputs);
1582 }
1583 Ok(())
1584 }
1585
1586 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1587 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1596 let builds_a_library = !self.test
1597 && !self.crate_types.is_empty()
1598 && self
1599 .crate_types
1600 .iter()
1601 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1602 let link_output = if never_links || builds_a_library {
1603 LinkOutput::Library
1604 } else if self
1605 .target
1606 .as_deref()
1607 .is_some_and(compiler_bundled_wasm_target)
1608 && ((self.test && self.crate_types.is_empty())
1609 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1610 {
1611 if self.parsed.iter().any(|argument| match argument {
1612 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1613 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1614 !matches!(
1615 value.rsplit_once('=').map(|(_, value)| value),
1616 Some("y" | "yes" | "on" | "true")
1617 )
1618 }
1619 _ => false,
1620 }) {
1621 return Err(BypassReason::UnknownCodegenOption(
1622 "link-self-contained".into(),
1623 ));
1624 }
1625 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1626 && self.parsed.iter().any(|argument| {
1627 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1628 })
1629 {
1630 return Err(BypassReason::UnknownCodegenOption(
1631 "target-feature=-crt-static".into(),
1632 ));
1633 }
1634 LinkOutput::WasmExecutable
1638 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1639 self.check_native_link_is_portable()?;
1640 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1641 LinkOutput::NativeProcMacro
1642 } else {
1643 LinkOutput::NativeExecutable
1644 }
1645 } else if self.test {
1646 return Err(BypassReason::UnsupportedCrateType("test".into()));
1647 } else {
1648 return Err(BypassReason::UnsupportedCrateType(
1649 self.crate_types
1650 .iter()
1651 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1652 .cloned()
1653 .unwrap_or_else(|| "bin".into()),
1654 ));
1655 };
1656 if link_output != LinkOutput::Library
1665 && let Some(option) = self.first_link_argument()
1666 {
1667 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1668 }
1669 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1676 let Argument::FuseLd(selection) = argument else {
1677 return None;
1678 };
1679 Some(selection.as_str())
1680 }) && !matches!(
1681 link_output,
1682 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1683 ) {
1684 return Err(BypassReason::UnmodeledLinkArgument(format!(
1685 "link-arg=-fuse-ld={selection}"
1686 )));
1687 }
1688 if self.parsed.iter().any(|argument| {
1689 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1690 }) && !matches!(
1691 link_output,
1692 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1693 ) {
1694 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1695 }
1696 if !matches!(
1700 link_output,
1701 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1702 ) && self
1703 .parsed
1704 .iter()
1705 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1706 {
1707 return Err(BypassReason::UnmodeledLinkArgument(
1708 "link-arg=-Wl,-oso_prefix".into(),
1709 ));
1710 }
1711 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1712 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1713 _ => None,
1714 }) {
1715 return Err(BypassReason::UnresolvedExtern(name.clone()));
1716 }
1717 if let Some(emit) = self
1718 .emits
1719 .iter()
1720 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1721 {
1722 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1723 }
1724 if !self
1725 .emits
1726 .iter()
1727 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1728 {
1729 return Err(BypassReason::NoCacheableOutput);
1730 }
1731 Ok(link_output)
1732 }
1733}
1734
1735impl Parser<'_> {
1736 fn first_link_argument(&self) -> Option<&str> {
1739 self.parsed.iter().find_map(|argument| {
1740 let Argument::Plain(value) = argument else {
1741 return None;
1742 };
1743 let option = value.strip_prefix("--codegen=")?;
1744 let name = option.split_once('=').map_or(option, |(name, _)| name);
1745 matches!(name, "link-arg" | "link-args").then_some(option)
1746 })
1747 }
1748
1749 fn oso_prefix_covers_outputs(&self) -> bool {
1758 let Some(directory) = self
1759 .out_dir
1760 .as_deref()
1761 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
1762 else {
1763 return false;
1764 };
1765 if !directory.is_absolute() {
1766 return false;
1767 }
1768 let directory = normalize_components(directory);
1769 self.parsed.iter().any(|argument| {
1770 let Argument::OsoPrefix { path, .. } = argument else {
1771 return false;
1772 };
1773 path.is_absolute() && directory.starts_with(normalize_components(path))
1774 })
1775 }
1776
1777 fn links_a_native_artifact(&self) -> bool {
1784 self.target.is_none()
1785 && self.emits.iter().any(|emit| emit.kind == "link")
1790 && ((self.test && self.crate_types.is_empty())
1791 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro")))
1792 }
1793
1794 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
1801 for argument in &self.parsed {
1802 let Argument::Plain(value) = argument else {
1803 continue;
1804 };
1805 let (name, value) = if value == "-g" {
1808 ("debuginfo", Some("2"))
1809 } else if let Some(option) = value.strip_prefix("--codegen=") {
1810 match option.split_once('=') {
1811 Some((name, value)) => (name, Some(value)),
1812 None => (option, None),
1816 }
1817 } else {
1818 continue;
1819 };
1820 let unportable = match name {
1821 "split-debuginfo" => match value {
1827 Some("off") => false,
1828 Some("unpacked") if cfg!(target_os = "macos") => {
1829 !self.oso_prefix_covers_outputs()
1830 }
1831 _ => true,
1832 },
1833 "debuginfo" if cfg!(target_os = "macos") => {
1842 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
1843 }
1844 "rpath" => is_enabled(value),
1852 "prefer-dynamic" => {
1853 is_enabled(value)
1854 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
1855 }
1856 "link-self-contained" => true,
1859 _ => false,
1860 };
1861 if unportable {
1862 return Err(BypassReason::UnportableNativeLink(match value {
1863 Some(value) => format!("{name}={value}"),
1864 None => name.to_owned(),
1865 }));
1866 }
1867 }
1868 Ok(())
1869 }
1870}
1871
1872fn is_enabled(value: Option<&str>) -> bool {
1875 matches!(value, None | Some("y" | "yes" | "on" | "true"))
1876}
1877
1878fn compiler_bundled_wasm_target(target: &str) -> bool {
1879 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
1880}
1881
1882fn parse_emits(value: &str) -> Vec<Emit> {
1883 value
1884 .split(',')
1885 .map(|emit| {
1886 let (kind, path) = emit
1887 .split_once('=')
1888 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
1889 Emit {
1890 kind: kind.into(),
1891 path,
1892 }
1893 })
1894 .collect()
1895}
1896
1897struct ActionBuilder<'a> {
1898 invocation: &'a RustcInvocation,
1899 context: ActionContext,
1900 mappings: Vec<SharedPathMapping>,
1901 linker: Option<LinkerIdentity>,
1902}
1903
1904impl<'a> ActionBuilder<'a> {
1905 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
1906 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1907 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
1908 Self {
1909 linker: None,
1910 invocation,
1911 mappings,
1912 context,
1913 }
1914 }
1915
1916 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
1917 self.linker = linker;
1918 self
1919 }
1920
1921 fn build(self) -> Result<RustcAction, BypassReason> {
1922 self.validate_mappings()?;
1923 let invocation = self.invocation_descriptor()?;
1924 let environment = self.environment_descriptor()?;
1925
1926 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1927 for input in &self.context.inputs {
1928 input
1929 .digest
1930 .validate()
1931 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
1932 let path = self.normalize_path(&input.path)?;
1933 if inputs
1934 .insert(path.clone(), input.digest.clone())
1935 .is_some_and(|existing| existing != input.digest)
1936 {
1937 return Err(BypassReason::ConflictingInput(path));
1938 }
1939 }
1940 let required = self
1941 .invocation
1942 .required_inputs
1943 .iter()
1944 .map(|path| self.normalize_path(path))
1945 .collect::<Result<BTreeSet<_>, _>>()?;
1946 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1947 return Err(BypassReason::MissingRequiredInput(missing.clone()));
1948 }
1949 let inputs = inputs
1950 .into_iter()
1951 .map(|(path, digest)| InputDescriptor { path, digest })
1952 .collect();
1953 if self.invocation.links_natively() && self.linker.is_none() {
1956 return Err(BypassReason::UnportableNativeLink(
1957 "linker identity is unknown".into(),
1958 ));
1959 }
1960 let descriptor = ActionDescriptor {
1961 version: ACTION_SCHEMA_VERSION,
1962 kind: "rustc",
1963 adapter_version: ADAPTER_VERSION,
1964 compiler: invocation.compiler,
1965 arguments: invocation.arguments,
1966 environment,
1967 inputs,
1968 linker: self.linker.clone(),
1969 };
1970 let bytes = canonical_json(&descriptor)
1971 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
1972 let digest = CacheDigest::blake3(&bytes);
1973 Ok(RustcAction { digest, bytes })
1974 }
1975
1976 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
1977 self.validate_mappings()?;
1978 let arguments = self
1979 .invocation
1980 .arguments
1981 .iter()
1982 .map(|argument| self.normalize_argument(argument))
1983 .collect::<Result<Vec<_>, _>>()?;
1984 let required_inputs = self
1985 .invocation
1986 .required_inputs
1987 .iter()
1988 .map(|path| self.normalize_path(path))
1989 .collect::<Result<BTreeSet<_>, _>>()?
1990 .into_iter()
1991 .collect();
1992 Ok(InvocationDescriptor {
1993 version: ACTION_SCHEMA_VERSION,
1994 kind: "rustc",
1995 adapter_version: ADAPTER_VERSION,
1996 compiler: CompilerDescriptor {
1997 toolchain: self.context.compiler.toolchain.clone(),
1998 rustc_version: self.context.compiler.rustc_version.clone(),
1999 host: self.context.compiler.host.clone(),
2000 driver: self.context.compiler.driver.clone(),
2001 },
2002 arguments,
2003 required_inputs,
2004 })
2005 }
2006
2007 fn validate_mappings(&self) -> Result<(), BypassReason> {
2008 if !self.context.working_dir.is_absolute() {
2009 return Err(BypassReason::RelativeWorkingDirectory(
2010 self.context.working_dir.clone(),
2011 ));
2012 }
2013 let mut roots = BTreeSet::new();
2014 let mut placeholders = BTreeSet::new();
2015 for mapping in &self.mappings {
2016 if !mapping.root.is_absolute() {
2017 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2018 }
2019 if mapping.placeholder.is_empty()
2020 || !mapping
2021 .placeholder
2022 .bytes()
2023 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2024 || !roots.insert(normalize_components(&mapping.root))
2025 || !placeholders.insert(&mapping.placeholder)
2026 {
2027 return Err(BypassReason::InvalidPathPlaceholder(
2028 mapping.placeholder.clone(),
2029 ));
2030 }
2031 }
2032 Ok(())
2033 }
2034
2035 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2036 match argument {
2037 Argument::Plain(value) => Ok(value.clone()),
2038 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2039 Argument::SearchPath { kind, path } => {
2040 let text = match self.normalize_path(path) {
2041 Ok(text) => text,
2042 Err(BypassReason::UnmappedAbsolutePath(absolute))
2050 if kind == "native" && self.invocation.native_search_is_inert() =>
2051 {
2052 absolute
2053 .to_str()
2054 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
2055 .to_string()
2056 }
2057 Err(error) => return Err(error),
2058 };
2059 Ok(format!("-L{kind}={text}"))
2060 }
2061 Argument::Extern { name, path } => match path {
2062 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2063 None => Ok(format!("--extern={name}")),
2064 },
2065 Argument::Emit(emits) => Ok(format!(
2066 "--emit={}",
2067 emits
2068 .iter()
2069 .map(|emit| match &emit.path {
2070 Some(path) => self
2071 .normalize_path(path)
2072 .map(|path| format!("{}={path}", emit.kind)),
2073 None => Ok(emit.kind.clone()),
2074 })
2075 .collect::<Result<Vec<_>, _>>()?
2076 .join(",")
2077 )),
2078 Argument::RemapPath { from, to } => Ok(format!(
2079 "--remap-path-prefix={}={}",
2080 self.normalize_path(from)?,
2081 to
2082 )),
2083 Argument::OsoPrefix {
2084 path,
2085 trailing_slash,
2086 } => Ok(format!(
2087 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2088 self.normalize_path(path)?,
2089 if *trailing_slash { "/" } else { "" }
2090 )),
2091 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2092 }
2093 }
2094
2095 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2102 self.context
2103 .environment
2104 .iter()
2105 .map(|(name, value)| {
2106 let value = match value {
2107 Some(value) if self.context.portable_environment.contains(name) => {
2108 Some(self.normalize_path(Path::new(value))?)
2109 }
2110 value => value.clone(),
2111 };
2112 Ok((name.clone(), value))
2113 })
2114 .collect()
2115 }
2116
2117 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2118 normalize_resolved_shared_path(path, &self.context.working_dir, &self.mappings)
2119 .map_err(Into::into)
2120 }
2121}
2122
2123fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2127 directories.iter().any(|directory| {
2128 path.len() > directory.len()
2129 && path.as_bytes()[directory.len()] == b'/'
2130 && path.starts_with(directory)
2131 })
2132}
2133
2134fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2135 for mapping in mappings {
2136 let prefix = format!("${{{}}}", mapping.placeholder);
2137 let suffix = if value == prefix {
2138 ""
2139 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2140 suffix
2141 } else {
2142 continue;
2143 };
2144 if !mapping.root.is_absolute()
2145 || (!suffix.is_empty()
2146 && suffix.split('/').any(|component| {
2147 component.is_empty()
2148 || matches!(component, "." | "..")
2149 || component.contains('\\')
2150 }))
2151 {
2152 return Err(BypassReason::InvalidPredictedInput(value.into()));
2153 }
2154 let mut path = normalize_components(&mapping.root);
2155 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2156 return Ok(path);
2157 }
2158 Err(BypassReason::InvalidPredictedInput(value.into()))
2159}
2160
2161fn normalize_components(path: &Path) -> PathBuf {
2162 let mut normalized = PathBuf::new();
2163 for component in path.components() {
2164 match component {
2165 Component::CurDir => {}
2166 Component::ParentDir => {
2167 normalized.pop();
2168 }
2169 component => normalized.push(component.as_os_str()),
2170 }
2171 }
2172 normalized
2173}
2174
2175fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2176 if path.is_absolute() {
2177 normalize_components(path)
2178 } else {
2179 normalize_components(&working_dir.join(path))
2180 }
2181}
2182
2183#[cfg(test)]
2184#[path = "rustc_cache_tests.rs"]
2185mod tests;