1#![deny(missing_docs)]
30
31use mbx_cache_core::{
32 CacheDigest, FileDigestCache, PathAliases, PathMapping as SharedPathMapping,
33 PathNormalizationError, canonical_json, normalize_mapped_path as normalize_shared_path,
34 normalize_resolved_mapped_path_with as normalize_resolved_shared_path_with,
35 resolve_path_mappings,
36};
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38use std::collections::{BTreeMap, BTreeSet};
39use std::ffi::OsString;
40use std::path::{Component, Path, PathBuf};
41use thiserror::Error;
42
43mod dep_info;
44
45pub use dep_info::{DepInfoCommand, DiscoveredInputs, RustcDepInfo};
46
47pub const ACTION_SCHEMA_VERSION: u8 = 1;
49pub const ADAPTER_VERSION: u8 = 2;
56
57impl BypassReason {
58 pub fn kind(&self) -> &'static str {
63 self.into()
64 }
65
66 pub fn remediation(&self) -> Option<&'static str> {
72 match self {
73 Self::Incremental => Some(
74 "Set `MBX_INCREMENTAL=0`; mbx will then disable Cargo incremental state and cache the compilation.",
75 ),
76 Self::UnportableNativeLink(detail) if detail.contains("linker") => Some(
77 "Make the native linker resolvable on `PATH`, or configure a linker mbx can identify for this target.",
78 ),
79 Self::UnportableNativeLink(_) => Some(
80 "Remove the reported `-C` option from the active Cargo profile or `RUSTFLAGS` to make these links cacheable.",
81 ),
82 Self::UnknownFlag(_) | Self::UnknownCodegenOption(_) => Some(
83 "Remove the reported compiler option, or upgrade mbx if the option should be modeled.",
84 ),
85 Self::UnmodeledLinkArgument(_) => Some(
86 "Remove the reported linker argument, or keep these links uncached if the argument is required.",
87 ),
88 Self::UnmappedAbsolutePath(_) => Some(
89 "Move the input under the workspace, target, Cargo, toolchain, or home roots so mbx can give it a portable cache name.",
90 ),
91 _ => None,
92 }
93 }
94}
95
96impl From<PathNormalizationError> for BypassReason {
97 fn from(reason: PathNormalizationError) -> Self {
98 match reason {
99 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
100 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
101 }
102 }
103}
104
105const SUPPORTED_CODEGEN_OPTIONS: &[&str] = &[
106 "codegen-units",
107 "control-flow-guard",
108 "debug-assertions",
109 "debuginfo",
110 "default-linker-libraries",
111 "embed-bitcode",
112 "extra-filename",
113 "force-frame-pointers",
114 "force-unwind-tables",
115 "instrument-coverage",
116 "link-arg",
117 "link-args",
118 "link-dead-code",
119 "link-self-contained",
120 "linker-plugin-lto",
121 "lto",
122 "metadata",
123 "no-prepopulate-passes",
124 "opt-level",
125 "overflow-checks",
126 "panic",
127 "prefer-dynamic",
128 "relocation-model",
129 "rpath",
130 "save-temps",
131 "soft-float",
132 "split-debuginfo",
133 "split-dwarf-kind",
134 "strip",
135 "symbol-mangling-version",
136 "target-cpu",
137 "target-feature",
138 "tls-model",
139];
140
141const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
142const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
143const MAX_DECODED_PREDICTION_BYTES: usize = 16 * 1024 * 1024;
147const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
148
149const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
152 "wasm32-unknown-unknown",
153 "wasm32-wasip1",
154 "wasm32-wasip1-threads",
155 "wasm32-wasip2",
156 "wasm32v1-none",
157 "wasm64-unknown-unknown",
158];
159
160#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
161#[strum(serialize_all = "kebab-case")]
162#[non_exhaustive]
172pub enum BypassReason {
173 #[error("rustc argument {index} is not valid UTF-8")]
175 NonUtf8Argument {
176 index: usize,
178 },
179 #[error("could not model rustc response file: {0}")]
181 ResponseFile(String),
182 #[error("rustc flag is not modeled by the cache adapter: {0}")]
184 UnknownFlag(String),
185 #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
187 UnknownCodegenOption(String),
188 #[error("rustc flag requires a value: {0}")]
190 MissingValue(String),
191 #[error("rustc invocation is a compiler query, not a compilation")]
193 CompilerQuery,
194 #[error("rustc invocation reads source from standard input")]
196 StandardInput,
197 #[error("rustc invocation has no source input")]
199 MissingInput,
200 #[error("rustc invocation has multiple source inputs")]
202 MultipleInputs,
203 #[error("incremental compilation cannot be combined with action caching")]
205 Incremental,
206 #[error("rustc crate type is not cacheable yet: {0}")]
208 UnsupportedCrateType(String),
209 #[error("rustc output type is not cacheable yet: {0}")]
211 UnsupportedEmit(String),
212 #[error("rustc invocation does not emit a cacheable artifact")]
214 NoCacheableOutput,
215 #[error("rustc invocation does not emit dependency information")]
217 NoDepInfo,
218 #[error("rustc output paths do not share one directory")]
220 SplitOutputDirectories,
221 #[error("rustc output path has no file name: {0}")]
223 InvalidOutputPath(PathBuf),
224 #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
226 ImplicitEmitWithOutputFile(PathBuf),
227 #[error("native library lookup is not cacheable yet")]
229 NativeLibrary,
230 #[error("rustc output name does not distinguish a program from a library: {0}")]
232 AmbiguousOutputName(PathBuf),
233 #[error("native link is not reproducible across checkouts: {0}")]
235 UnportableNativeLink(String),
236 #[error("rustc link argument is not modeled by the cache adapter: {0}")]
238 UnmodeledLinkArgument(String),
239 #[error("rustc search path kind is not cacheable yet: {0}")]
241 UnsupportedSearchPath(String),
242 #[error("rustc extern does not identify an input artifact: {0}")]
244 UnresolvedExtern(String),
245 #[error("absolute path has no stable cache mapping: {0}")]
247 UnmappedAbsolutePath(PathBuf),
248 #[error("cache key paths must be valid UTF-8: {0}")]
250 NonUtf8Path(PathBuf),
251 #[error("cache action working directory must be absolute: {0}")]
253 RelativeWorkingDirectory(PathBuf),
254 #[error("cache path mapping must use an absolute root: {0}")]
256 RelativePathMapping(PathBuf),
257 #[error("cache path mapping placeholder is invalid: {0}")]
259 InvalidPathPlaceholder(String),
260 #[error("required compiler input was not provided: {0}")]
262 MissingRequiredInput(String),
263 #[error("compiler input has an invalid digest: {0}")]
265 InvalidInputDigest(String),
266 #[error("compiler input appears more than once with different content: {0}")]
268 ConflictingInput(String),
269 #[error("rustc dep-info is malformed: {0}")]
271 MalformedDepInfo(String),
272 #[error("failed to read rustc dep-info {path}: {message}")]
274 DepInfoRead {
275 path: PathBuf,
277 message: String,
279 },
280 #[error("rustc dep-info output path must be absolute: {0}")]
282 RelativeDepInfoPath(PathBuf),
283 #[error("rustc dep-info output path cannot contain a comma: {0}")]
285 UnsafeDepInfoPath(PathBuf),
286 #[error("failed to read compiler input {path}: {message}")]
288 InputRead {
289 path: PathBuf,
291 message: String,
293 },
294 #[error("compiler input changed after discovery: {0}")]
296 InputChanged(PathBuf),
297 #[error("compiler input was modified during compilation: {0}")]
299 InputModifiedDuringCompilation(PathBuf),
300 #[error("discovered inputs were collected from a different working directory")]
302 DiscoveryWorkingDirectory,
303 #[error("compiler environment input has conflicting values: {0}")]
305 ConflictingEnvironment(String),
306 #[error("failed to serialize the rustc action: {0}")]
308 Serialization(String),
309 #[error("rustc action prediction is unsupported")]
311 UnsupportedPrediction,
312 #[error("rustc action prediction contains an invalid input path: {0}")]
314 InvalidPredictedInput(String),
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
318enum Argument {
319 Plain(String),
320 Path {
321 flag: String,
322 path: PathBuf,
323 },
324 SearchPath {
325 kind: String,
326 path: PathBuf,
327 },
328 Extern {
329 name: String,
330 path: Option<PathBuf>,
331 },
332 Emit(Vec<Emit>),
333 RemapPath {
334 from: PathBuf,
335 to: String,
336 },
337 OsoPrefix {
340 path: PathBuf,
341 trailing_slash: bool,
342 },
343 FuseLd(String),
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350struct Emit {
351 kind: String,
352 path: Option<PathBuf>,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq)]
356pub struct RustcInvocation {
361 arguments: Vec<Argument>,
362 source: PathBuf,
363 required_inputs: Vec<PathBuf>,
364 crate_name: String,
365 extra_filename: String,
366 out_dir: Option<PathBuf>,
367 explicit_output: Option<PathBuf>,
368 emits: Vec<Emit>,
369 target: Option<String>,
370 link_output: LinkOutput,
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374enum LinkOutput {
375 Library,
376 WasmExecutable,
377 NativeExecutable,
378 NativeProcMacro,
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
387#[non_exhaustive]
388pub struct ParseOptions {
389 pub cache_native_links: bool,
392}
393
394impl ParseOptions {
395 pub fn caching_native_links(enabled: bool) -> Self {
397 Self {
398 cache_native_links: enabled,
399 }
400 }
401}
402
403#[derive(Debug, Clone, PartialEq, Eq)]
405pub struct RustcOutputs {
406 pub directory: PathBuf,
408 pub files: Vec<PathBuf>,
410 pub dep_info: PathBuf,
412}
413
414impl RustcInvocation {
415 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
424 Self::parse_with(arguments, ParseOptions::default())
425 }
426
427 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
430 let expanded = expand_response_files(arguments)?;
431 Parser::new(&expanded.arguments, options).parse()
432 }
433
434 pub fn links_natively(&self) -> bool {
437 matches!(
438 self.link_output,
439 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
440 )
441 }
442
443 pub fn linker_override(&self) -> Option<&Path> {
446 self.arguments.iter().find_map(|argument| {
447 let Argument::Plain(value) = argument else {
448 return None;
449 };
450 value.strip_prefix("--codegen=linker=").map(Path::new)
451 })
452 }
453
454 pub fn fuse_ld(&self) -> Option<&str> {
457 self.arguments.iter().find_map(|argument| {
458 let Argument::FuseLd(selection) = argument else {
459 return None;
460 };
461 Some(selection.as_str())
462 })
463 }
464
465 fn emits_windows_pdb(&self) -> bool {
466 if !cfg!(windows) || !self.links_natively() {
467 return false;
468 }
469 let mut enabled = false;
470 for argument in &self.arguments {
471 let Argument::Plain(value) = argument else {
472 continue;
473 };
474 if value == "-g" {
475 enabled = true;
476 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
477 enabled = !matches!(value, "0" | "none");
478 }
479 }
480 enabled
481 }
482
483 fn native_search_is_inert(&self) -> bool {
496 self.link_output == LinkOutput::Library
497 }
498
499 pub fn source(&self) -> &Path {
501 &self.source
502 }
503
504 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
510 self.required_inputs
511 .iter()
512 .map(|path| absolute_path(path, working_dir))
513 .collect()
514 }
515
516 pub fn target(&self) -> Option<&str> {
518 self.target.as_deref()
519 }
520
521 pub fn crate_name(&self) -> &str {
523 &self.crate_name
524 }
525
526 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
539 let linked = self
540 .arguments
541 .iter()
542 .filter_map(|argument| match argument {
543 Argument::Extern {
544 path: Some(path), ..
545 } => Some(path.as_path()),
546 _ => None,
547 })
548 .collect::<BTreeSet<_>>();
549 let owned = discovered
550 .inputs
551 .iter()
552 .filter(|input| !linked.contains(input.path.as_path()))
553 .map(|input| (input.path.as_path(), &input.digest))
554 .collect::<BTreeMap<_, _>>();
555 let mut bytes = Vec::new();
556 for (path, digest) in owned {
557 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
558 bytes.push(0);
559 bytes.extend_from_slice(digest.key().as_bytes());
560 bytes.push(0);
561 }
562 CacheDigest::blake3(&bytes)
563 }
564
565 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
570 if !working_dir.is_absolute() {
571 return Err(BypassReason::RelativeWorkingDirectory(
572 working_dir.to_path_buf(),
573 ));
574 }
575 let explicit_output = self
576 .explicit_output
577 .as_deref()
578 .map(|path| absolute_path(path, working_dir));
579 let output_directory = explicit_output
580 .as_deref()
581 .and_then(Path::parent)
582 .map(Path::to_path_buf)
583 .or_else(|| {
584 self.out_dir
585 .as_deref()
586 .map(|path| absolute_path(path, working_dir))
587 })
588 .unwrap_or_else(|| normalize_components(working_dir));
589 if let Some(output) = &explicit_output
593 && self.emits.iter().any(|emit| {
594 emit.path.is_none()
595 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
596 })
597 {
598 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
599 }
600 let mut files = BTreeSet::new();
601 let mut dep_info = None;
602 for emit in &self.emits {
603 if emit.kind == "dep-info" {
604 let path = emit.path.as_ref().map_or_else(
605 || {
606 explicit_output.clone().map_or_else(
607 || {
608 output_directory
609 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
610 },
611 |path| path.with_extension("d"),
612 )
613 },
614 |path| absolute_path(path, working_dir),
615 );
616 if path.file_name().is_none() {
617 return Err(BypassReason::InvalidOutputPath(path));
618 }
619 dep_info = Some(path);
620 continue;
621 }
622 let (prefix, extension) = match emit.kind.as_str() {
623 "link" => match self.link_output {
624 LinkOutput::Library => ("lib", "rlib"),
625 LinkOutput::WasmExecutable => ("", "wasm"),
626 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
627 LinkOutput::NativeProcMacro => (
628 std::env::consts::DLL_PREFIX,
629 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
630 ),
631 },
632 "metadata" => ("lib", "rmeta"),
633 _ => continue,
634 };
635 let path = if let Some(path) = &emit.path {
636 absolute_path(path, working_dir)
637 } else {
638 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
639 output_directory.join(if extension.is_empty() {
640 name
641 } else {
642 format!("{name}.{extension}")
643 })
644 };
645 if path.file_name().is_none() {
646 return Err(BypassReason::InvalidOutputPath(path));
647 }
648 if path.parent() != Some(output_directory.as_path()) {
649 return Err(BypassReason::SplitOutputDirectories);
650 }
651 let has_library_extension = path
656 .extension()
657 .and_then(|extension| extension.to_str())
658 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
659 || (cfg!(windows)
660 && path
661 .file_stem()
662 .and_then(|stem| Path::new(stem).extension())
663 .and_then(|extension| extension.to_str())
664 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
665 if emit.kind == "link"
666 && !matches!(self.link_output, LinkOutput::Library)
667 && has_library_extension
668 {
669 return Err(BypassReason::AmbiguousOutputName(path));
670 }
671 if emit.kind == "link" && self.emits_windows_pdb() {
672 files.insert(path.with_extension("pdb"));
673 }
674 files.insert(path);
675 }
676 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
677 if dep_info.parent() != Some(output_directory.as_path()) {
678 return Err(BypassReason::SplitOutputDirectories);
679 }
680 Ok(RustcOutputs {
681 directory: output_directory,
682 files: files.into_iter().collect(),
683 dep_info,
684 })
685 }
686
687 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
693 self.action_linked_by(context, None)
694 }
695
696 pub fn action_linked_by(
705 &self,
706 context: ActionContext,
707 linker: Option<LinkerIdentity>,
708 ) -> Result<RustcAction, BypassReason> {
709 ActionBuilder::new(self, context).linked_by(linker).build()
710 }
711
712 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
714 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
715 let bytes = canonical_json(&descriptor)
716 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
717 Ok(CacheDigest::blake3(&bytes))
718 }
719
720 pub fn prediction(
723 &self,
724 context: &ActionContext,
725 discovered: &DiscoveredInputs,
726 ) -> Result<RustcInputPrediction, BypassReason> {
727 let builder = ActionBuilder::new(self, context.clone());
728 builder.validate_mappings()?;
729 let mut native_directories = BTreeSet::new();
730 for argument in &self.arguments {
731 if let Argument::SearchPath { kind, path } = argument
732 && kind == "native"
733 {
734 match builder.normalize_path(path) {
735 Ok(normalized) => {
736 native_directories.insert(normalized);
737 }
738 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
744 }
745 Err(error) => return Err(error),
746 }
747 }
748 }
749 let mut inputs = BTreeSet::new();
756 for input in &discovered.inputs {
757 let normalized = builder.normalize_path(&input.path)?;
758 if !under_any_directory(&normalized, &native_directories) {
759 inputs.insert(normalized);
760 }
761 }
762 inputs.extend(
763 native_directories
764 .into_iter()
765 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
766 );
767 Ok(RustcInputPrediction {
768 version: 4,
769 inputs: inputs.into_iter().collect(),
770 environment: discovered.environment.keys().cloned().collect(),
771 compiler_duration_ns: 0,
772 crate_name: String::new(),
773 })
774 }
775}
776
777impl RustcOutputs {
778 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
781 (crate_name == "build_script_build")
782 .then(|| self.files.iter().find(|path| self.is_executable(path)))
783 .flatten()
784 .map(PathBuf::as_path)
785 }
786
787 pub fn is_executable(&self, path: &Path) -> bool {
795 self.files.iter().any(|output| output == path)
796 && !matches!(
797 path.extension().and_then(|extension| extension.to_str()),
798 Some("rlib" | "rmeta")
799 )
800 }
801}
802
803#[derive(Debug, Clone, PartialEq, Eq)]
804pub struct PathMapping {
806 pub root: PathBuf,
808 pub placeholder: String,
810}
811
812impl PathMapping {
813 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
815 Self {
816 root: root.into(),
817 placeholder: placeholder.into(),
818 }
819 }
820
821 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
824 let mut ordered = mappings.to_vec();
825 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
826 ordered
827 }
828}
829
830fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
831 mappings
832 .iter()
833 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
834 .collect()
835}
836
837pub fn normalize_mapped_path(
844 path: &Path,
845 working_dir: &Path,
846 mappings: &[PathMapping],
847) -> Result<String, BypassReason> {
848 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
849}
850
851#[derive(Debug, Clone, PartialEq, Eq)]
852pub struct CompilerIdentity {
854 pub toolchain: String,
856 pub rustc_version: String,
858 pub host: String,
860 pub driver: Option<String>,
862}
863
864#[derive(Debug, Clone, PartialEq, Eq)]
866pub struct ActionInput {
867 pub path: PathBuf,
869 pub digest: CacheDigest,
871}
872
873#[derive(Debug, Clone, PartialEq, Eq)]
875pub struct ActionContext {
876 pub compiler: CompilerIdentity,
878 pub working_dir: PathBuf,
880 pub path_mappings: Vec<PathMapping>,
882 pub environment: BTreeMap<String, Option<String>>,
884 pub portable_environment: BTreeSet<String>,
891 pub inputs: Vec<ActionInput>,
893}
894
895#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
905#[serde(deny_unknown_fields)]
906pub struct LinkerIdentity {
907 pub driver: String,
909 pub driver_version: String,
911 pub linker_version: String,
913 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
915 pub crt_objects: BTreeMap<String, CacheDigest>,
916 #[serde(skip_serializing_if = "Option::is_none", default)]
918 pub sdk: Option<String>,
919 #[serde(skip_serializing_if = "Option::is_none", default)]
921 pub deployment_target: Option<String>,
922}
923
924#[derive(Debug, Clone, PartialEq, Eq)]
926pub struct RustcAction {
927 pub digest: CacheDigest,
929 pub bytes: Vec<u8>,
931}
932
933#[derive(Debug, Clone, PartialEq, Eq)]
936pub struct RustcInputPrediction {
937 pub version: u8,
939 pub inputs: Vec<String>,
941 pub environment: Vec<String>,
943 pub compiler_duration_ns: u64,
946 pub crate_name: String,
948}
949
950#[derive(Serialize, Deserialize)]
951#[serde(deny_unknown_fields)]
952struct RustcInputPredictionWire {
953 version: u8,
954 inputs: Vec<String>,
955 environment: Vec<String>,
956 #[serde(default, skip_serializing_if = "is_zero")]
957 compiler_duration_ns: u64,
958 #[serde(default, skip_serializing_if = "String::is_empty")]
959 crate_name: String,
960}
961
962impl Serialize for RustcInputPrediction {
963 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
964 where
965 S: Serializer,
966 {
967 RustcInputPredictionWire {
968 version: self.version,
969 inputs: if self.version == 4 {
970 compact_prediction_inputs(&self.inputs)
971 } else {
972 self.inputs.clone()
973 },
974 environment: self.environment.clone(),
975 compiler_duration_ns: self.compiler_duration_ns,
976 crate_name: self.crate_name.clone(),
977 }
978 .serialize(serializer)
979 }
980}
981
982impl<'de> Deserialize<'de> for RustcInputPrediction {
983 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
984 where
985 D: Deserializer<'de>,
986 {
987 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
988 let inputs = if wire.version == 4 {
989 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
990 } else {
991 wire.inputs
992 };
993 Ok(Self {
994 version: wire.version,
995 inputs,
996 environment: wire.environment,
997 compiler_duration_ns: wire.compiler_duration_ns,
998 crate_name: wire.crate_name,
999 })
1000 }
1001}
1002
1003fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1004 let mut previous = "";
1005 inputs
1006 .iter()
1007 .map(|input| {
1008 let mut shared = previous
1009 .bytes()
1010 .zip(input.bytes())
1011 .take_while(|(left, right)| left == right)
1012 .count();
1013 while !input.is_char_boundary(shared) {
1014 shared -= 1;
1015 }
1016 let compact = format!("{shared}:{}", &input[shared..]);
1017 previous = input;
1018 compact
1019 })
1020 .collect()
1021}
1022
1023fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1024 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1025 let mut decoded_bytes = 0_usize;
1026 for input in inputs {
1027 let (shared_text, suffix) = input
1028 .split_once(':')
1029 .ok_or("compact rustc prediction input has no prefix length")?;
1030 let shared: usize = shared_text
1031 .parse()
1032 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1033 if shared.to_string() != shared_text
1034 || shared > expanded.last().map_or(0, String::len)
1035 || expanded
1036 .last()
1037 .is_some_and(|previous| !previous.is_char_boundary(shared))
1038 {
1039 return Err("compact rustc prediction prefix is not canonical");
1040 }
1041 let mut path = expanded
1042 .last()
1043 .map_or_else(String::new, |previous| previous[..shared].to_string());
1044 path.push_str(suffix);
1045 decoded_bytes = decoded_bytes
1046 .checked_add(path.len())
1047 .ok_or("compact rustc prediction is too large")?;
1048 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1049 return Err("compact rustc prediction is too large");
1050 }
1051 expanded.push(path);
1052 }
1053 Ok(expanded)
1054}
1055
1056fn is_zero(value: &u64) -> bool {
1057 *value == 0
1058}
1059
1060impl RustcInputPrediction {
1061 pub fn discover(
1064 &self,
1065 working_dir: &Path,
1066 path_mappings: &[PathMapping],
1067 digests: &dyn FileDigestCache,
1068 ) -> Result<DiscoveredInputs, BypassReason> {
1069 if !matches!(self.version, 2..=4) {
1070 return Err(BypassReason::UnsupportedPrediction);
1071 }
1072 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1073 return Err(BypassReason::UnsupportedPrediction);
1074 }
1075 let mut paths = BTreeSet::new();
1076 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1077 let mut native_bytes = 0_u64;
1078 for path in &self.inputs {
1079 if self.version >= 3
1080 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1081 {
1082 let directory = denormalize_path(path, path_mappings)?;
1083 dep_info::collect_native_directory(
1084 &directory,
1085 &admitted_roots,
1086 &mut paths,
1087 &mut native_bytes,
1088 )?;
1089 } else {
1090 paths.insert(denormalize_path(path, path_mappings)?);
1091 }
1092 }
1093 let environment = self
1094 .environment
1095 .iter()
1096 .map(|name| {
1097 if name.is_empty() || name.contains(['=', '\0']) {
1098 return Err(BypassReason::UnsupportedPrediction);
1099 }
1100 let value = std::env::var_os(name)
1101 .map(|value| {
1102 value
1103 .into_string()
1104 .map_err(|_| BypassReason::UnsupportedPrediction)
1105 })
1106 .transpose()?;
1107 Ok((name.clone(), value))
1108 })
1109 .collect::<Result<BTreeMap<_, _>, _>>()?;
1110 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1111 }
1112}
1113
1114#[derive(Serialize)]
1115struct ActionDescriptor {
1116 version: u8,
1117 kind: &'static str,
1118 adapter_version: u8,
1119 compiler: CompilerDescriptor,
1120 arguments: Vec<String>,
1121 environment: BTreeMap<String, Option<String>>,
1122 inputs: Vec<InputDescriptor>,
1123 #[serde(skip_serializing_if = "Option::is_none")]
1126 linker: Option<LinkerIdentity>,
1127}
1128
1129#[derive(Serialize)]
1130struct InvocationDescriptor {
1131 version: u8,
1132 kind: &'static str,
1133 adapter_version: u8,
1134 compiler: CompilerDescriptor,
1135 arguments: Vec<String>,
1136 required_inputs: Vec<String>,
1137}
1138
1139#[derive(Serialize)]
1140struct CompilerDescriptor {
1141 toolchain: String,
1142 rustc_version: String,
1143 host: String,
1144 #[serde(skip_serializing_if = "Option::is_none")]
1145 driver: Option<String>,
1146}
1147
1148#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1149struct InputDescriptor {
1150 path: String,
1151 digest: CacheDigest,
1152}
1153
1154struct Parser<'a> {
1155 arguments: &'a [OsString],
1156 index: usize,
1157 parsed: Vec<Argument>,
1158 source: Option<PathBuf>,
1159 crate_types: Vec<String>,
1160 emits: Vec<Emit>,
1161 required_inputs: Vec<PathBuf>,
1162 test: bool,
1163 crate_name: Option<String>,
1164 extra_filename: String,
1165 out_dir: Option<PathBuf>,
1166 explicit_output: Option<PathBuf>,
1167 target: Option<String>,
1168 options: ParseOptions,
1169}
1170
1171struct ExpandedArguments {
1172 arguments: Vec<OsString>,
1173}
1174
1175#[derive(Default)]
1176struct ResponseExpander {
1177 shell_argfiles: bool,
1178 next_is_unstable_option: bool,
1179 arguments: Vec<OsString>,
1180}
1181
1182impl ResponseExpander {
1183 fn push(&mut self, argument: String) {
1184 if self.next_is_unstable_option {
1185 self.shell_argfiles |= argument == "shell-argfiles";
1186 self.next_is_unstable_option = false;
1187 } else if let Some(option) = argument.strip_prefix("-Z") {
1188 if option.is_empty() {
1189 self.next_is_unstable_option = true;
1190 } else {
1191 self.shell_argfiles |= option == "shell-argfiles";
1192 }
1193 }
1194 self.arguments.push(argument.into());
1195 }
1196}
1197
1198fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1201 let mut expanded = ResponseExpander::default();
1202 for (index, argument) in arguments.iter().enumerate() {
1203 let argument = argument
1204 .to_str()
1205 .ok_or(BypassReason::NonUtf8Argument { index })?;
1206 let Some(argfile) = argument.strip_prefix('@') else {
1207 expanded.push(argument.to_string());
1208 continue;
1209 };
1210 let (path, shell) = match argfile.split_once(':') {
1211 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1212 _ => (argfile, false),
1213 };
1214 let contents = std::fs::read_to_string(path).map_err(|error| {
1215 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1216 })?;
1217 if shell {
1218 let arguments = shlex::split(&contents).ok_or_else(|| {
1219 BypassReason::ResponseFile(format!(
1220 "invalid shell-style arguments in {}",
1221 Path::new(path).display()
1222 ))
1223 })?;
1224 for argument in arguments {
1225 expanded.push(argument);
1226 }
1227 } else {
1228 for argument in contents.lines() {
1229 expanded.push(argument.to_string());
1230 }
1231 }
1232 }
1233 Ok(ExpandedArguments {
1234 arguments: expanded.arguments,
1235 })
1236}
1237
1238impl<'a> Parser<'a> {
1239 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1240 Self {
1241 arguments,
1242 options,
1243 index: 0,
1244 parsed: Vec::new(),
1245 source: None,
1246 crate_types: Vec::new(),
1247 emits: Vec::new(),
1248 required_inputs: Vec::new(),
1249 test: false,
1250 crate_name: None,
1251 extra_filename: String::new(),
1252 out_dir: None,
1253 explicit_output: None,
1254 target: None,
1255 }
1256 }
1257
1258 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1259 while self.index < self.arguments.len() {
1260 let value = self.current()?.to_string();
1261 self.index += 1;
1262 if let Some(long) = value.strip_prefix("--") {
1263 self.parse_long(long)?;
1264 } else if value.starts_with('-') && value != "-" {
1265 self.parse_short(&value)?;
1266 } else {
1267 self.parse_input(&value)?;
1268 }
1269 }
1270
1271 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1272 let link_output = self.classify()?;
1273 let crate_name = self.crate_name.clone().map_or_else(
1274 || {
1275 source
1276 .file_stem()
1277 .and_then(|name| name.to_str())
1278 .map(|name| name.replace('-', "_"))
1279 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1280 },
1281 Ok,
1282 )?;
1283 self.required_inputs.push(source.clone());
1284 Ok(RustcInvocation {
1285 arguments: self.parsed,
1286 source,
1287 required_inputs: self.required_inputs,
1288 crate_name,
1289 extra_filename: self.extra_filename,
1290 out_dir: self.out_dir,
1291 explicit_output: self.explicit_output,
1292 emits: self.emits,
1293 target: self.target,
1294 link_output,
1295 })
1296 }
1297
1298 fn current(&self) -> Result<&str, BypassReason> {
1299 self.arguments[self.index]
1300 .to_str()
1301 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1302 }
1303
1304 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1305 if let Some(value) = inline {
1306 if value.is_empty() {
1307 return Err(BypassReason::MissingValue(flag.into()));
1308 }
1309 return Ok(value.into());
1310 }
1311 if self.index >= self.arguments.len() {
1312 return Err(BypassReason::MissingValue(flag.into()));
1313 }
1314 let value = self.current()?.to_string();
1315 self.index += 1;
1316 Ok(value)
1317 }
1318
1319 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1320 let (flag, inline) = value
1321 .split_once('=')
1322 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1323 let rendered_flag = format!("--{flag}");
1324 match flag {
1325 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1326 "test" => {
1327 self.test = true;
1328 self.parsed.push(Argument::Plain(rendered_flag));
1329 Ok(())
1330 }
1331 "verbose" => {
1332 self.parsed.push(Argument::Plain(rendered_flag));
1333 Ok(())
1334 }
1335 "crate-name" => {
1336 let value = self.take_value(&rendered_flag, inline)?;
1337 self.crate_name = Some(value.clone());
1338 self.parsed
1339 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1340 Ok(())
1341 }
1342 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1343 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1344 | "deny" | "forbid" | "cap-lints" => {
1345 let value = self.take_value(&rendered_flag, inline)?;
1346 self.parsed
1347 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1348 Ok(())
1349 }
1350 "target" => {
1351 let value = self.take_value(&rendered_flag, inline)?;
1352 self.target = Some(value.clone());
1353 if value.ends_with(".json") || value.contains(['/', '\\']) {
1354 let path = PathBuf::from(value);
1355 self.required_inputs.push(path.clone());
1356 self.parsed.push(Argument::Path {
1357 flag: rendered_flag,
1358 path,
1359 });
1360 } else {
1361 self.target = Some(value.clone());
1362 self.parsed
1363 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1364 }
1365 Ok(())
1366 }
1367 "crate-type" => {
1368 let value = self.take_value(&rendered_flag, inline)?;
1369 self.crate_types
1370 .extend(value.split(',').map(ToOwned::to_owned));
1371 self.parsed
1372 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1373 Ok(())
1374 }
1375 "emit" => {
1376 let value = self.take_value(&rendered_flag, inline)?;
1377 let emits = parse_emits(&value);
1378 self.emits.extend(emits.clone());
1379 self.parsed.push(Argument::Emit(emits));
1380 Ok(())
1381 }
1382 "out-dir" => {
1383 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1384 self.out_dir = Some(path.clone());
1385 self.parsed.push(Argument::Path {
1386 flag: rendered_flag,
1387 path,
1388 });
1389 Ok(())
1390 }
1391 "sysroot" => {
1392 let path = self.take_value(&rendered_flag, inline)?;
1393 self.parsed.push(Argument::Path {
1394 flag: rendered_flag,
1395 path: path.into(),
1396 });
1397 Ok(())
1398 }
1399 "extern" => {
1400 let value = self.take_value(&rendered_flag, inline)?;
1401 let (name, path) = value
1402 .split_once('=')
1403 .map_or((value.as_str(), None), |(name, path)| {
1404 (name, Some(PathBuf::from(path)))
1405 });
1406 if let Some(path) = &path {
1407 self.required_inputs.push(path.clone());
1408 }
1409 self.parsed.push(Argument::Extern {
1410 name: name.into(),
1411 path,
1412 });
1413 Ok(())
1414 }
1415 "remap-path-prefix" => {
1416 let value = self.take_value(&rendered_flag, inline)?;
1417 let Some((from, to)) = value.split_once('=') else {
1418 return Err(BypassReason::MissingValue(rendered_flag));
1419 };
1420 self.parsed.push(Argument::RemapPath {
1421 from: from.into(),
1422 to: to.into(),
1423 });
1424 Ok(())
1425 }
1426 "codegen" => {
1427 let value = self.take_value(&rendered_flag, inline)?;
1428 self.parse_codegen(&value)
1429 }
1430 "jobs-frontend" => {
1431 let value = self.take_value(&rendered_flag, inline)?;
1432 self.parsed
1433 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1434 Ok(())
1435 }
1436 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1437 }
1438 }
1439
1440 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1441 if let Some(attached) = value.strip_prefix("-Z") {
1442 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1443 match option.as_str() {
1444 "embed-metadata=no" | "embed-metadata=yes" | "shell-argfiles"
1445 | "unstable-options" => {
1446 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1447 return Ok(());
1448 }
1449 "threads" | "threads=" => {
1450 return Err(BypassReason::MissingValue("-Zthreads".into()));
1451 }
1452 _ => {}
1453 }
1454 if option.starts_with("threads=") {
1455 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1456 return Ok(());
1457 }
1458 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1459 }
1460 match value {
1461 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1464 "-g" | "-O" | "-v" => {
1465 self.parsed.push(Argument::Plain(value.into()));
1466 return Ok(());
1467 }
1468 _ => {}
1469 }
1470 for (short, long) in [
1471 ("-A", "--allow"),
1472 ("-W", "--warn"),
1473 ("-D", "--deny"),
1474 ("-F", "--forbid"),
1475 ] {
1476 if let Some(attached) = value.strip_prefix(short) {
1477 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1478 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1479 return Ok(());
1480 }
1481 }
1482 if let Some(attached) = value.strip_prefix("-C") {
1483 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1484 return self.parse_codegen(&option);
1485 }
1486 if let Some(attached) = value.strip_prefix("-L") {
1487 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1488 let (kind, path) = search
1489 .split_once('=')
1490 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1491 if !matches!(kind, "dependency" | "native") {
1492 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1493 }
1494 self.parsed.push(Argument::SearchPath {
1495 kind: kind.into(),
1496 path: path.into(),
1497 });
1498 return Ok(());
1499 }
1500 if value == "-l" || value.starts_with("-l") {
1501 return Err(BypassReason::NativeLibrary);
1502 }
1503 if let Some(attached) = value.strip_prefix("-o") {
1504 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1505 self.explicit_output = Some(path.clone().into());
1506 self.parsed.push(Argument::Path {
1507 flag: "-o".into(),
1508 path: path.into(),
1509 });
1510 return Ok(());
1511 }
1512 Err(BypassReason::UnknownFlag(value.into()))
1513 }
1514
1515 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1516 let name = value.split_once('=').map_or(value, |(name, _)| name);
1517 if name == "incremental" {
1518 return Err(BypassReason::Incremental);
1519 }
1520 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1521 return Err(BypassReason::UnknownCodegenOption(name.into()));
1522 }
1523 if name == "linker-plugin-lto"
1529 && let Some((_, selection)) = value.split_once('=')
1530 && !matches!(
1531 selection,
1532 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1533 )
1534 {
1535 return Err(BypassReason::UnknownCodegenOption(format!(
1536 "linker-plugin-lto={selection}"
1537 )));
1538 }
1539 if name == "link-arg"
1546 && let Some((_, option)) = value.split_once('=')
1547 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1548 && !selection.is_empty()
1549 && !selection.contains(',')
1550 && !self.parsed.iter().any(
1551 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1552 )
1553 {
1554 if !self.parsed.iter().any(
1555 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1556 ) {
1557 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1558 }
1559 return Ok(());
1560 }
1561 if matches!(name, "link-arg" | "link-args")
1566 && let Some((_, option)) = value.split_once('=')
1567 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1568 && !prefix.trim_end_matches('/').is_empty()
1569 && !prefix.contains(',')
1570 {
1571 let trailing_slash = prefix.ends_with('/');
1572 self.parsed.push(Argument::OsoPrefix {
1573 path: PathBuf::from(prefix.trim_end_matches('/')),
1574 trailing_slash,
1575 });
1576 return Ok(());
1577 }
1578 self.parsed
1579 .push(Argument::Plain(format!("--codegen={value}")));
1580 if name == "extra-filename" {
1581 self.extra_filename = value
1582 .split_once('=')
1583 .map_or(String::new(), |(_, value)| value.to_string());
1584 }
1585 Ok(())
1586 }
1587
1588 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1589 if value == "-" {
1590 return Err(BypassReason::StandardInput);
1591 }
1592 if self.source.replace(value.into()).is_some() {
1593 return Err(BypassReason::MultipleInputs);
1594 }
1595 Ok(())
1596 }
1597
1598 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1599 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1608 let builds_a_library = !self.test
1609 && !self.crate_types.is_empty()
1610 && self
1611 .crate_types
1612 .iter()
1613 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1614 let link_output = if never_links || builds_a_library {
1615 LinkOutput::Library
1616 } else if self
1617 .target
1618 .as_deref()
1619 .is_some_and(compiler_bundled_wasm_target)
1620 && ((self.test && self.crate_types.is_empty())
1621 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1622 {
1623 if self.parsed.iter().any(|argument| match argument {
1624 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1625 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1626 !matches!(
1627 value.rsplit_once('=').map(|(_, value)| value),
1628 Some("y" | "yes" | "on" | "true")
1629 )
1630 }
1631 _ => false,
1632 }) {
1633 return Err(BypassReason::UnknownCodegenOption(
1634 "link-self-contained".into(),
1635 ));
1636 }
1637 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1638 && self.parsed.iter().any(|argument| {
1639 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1640 })
1641 {
1642 return Err(BypassReason::UnknownCodegenOption(
1643 "target-feature=-crt-static".into(),
1644 ));
1645 }
1646 LinkOutput::WasmExecutable
1650 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1651 self.check_native_link_is_portable()?;
1652 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1653 LinkOutput::NativeProcMacro
1654 } else {
1655 LinkOutput::NativeExecutable
1656 }
1657 } else if self.test {
1658 return Err(BypassReason::UnsupportedCrateType("test".into()));
1659 } else {
1660 return Err(BypassReason::UnsupportedCrateType(
1661 self.crate_types
1662 .iter()
1663 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1664 .cloned()
1665 .unwrap_or_else(|| "bin".into()),
1666 ));
1667 };
1668 if link_output != LinkOutput::Library
1677 && let Some(option) = self.first_link_argument()
1678 {
1679 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1680 }
1681 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1688 let Argument::FuseLd(selection) = argument else {
1689 return None;
1690 };
1691 Some(selection.as_str())
1692 }) && !matches!(
1693 link_output,
1694 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1695 ) {
1696 return Err(BypassReason::UnmodeledLinkArgument(format!(
1697 "link-arg=-fuse-ld={selection}"
1698 )));
1699 }
1700 if self.parsed.iter().any(|argument| {
1701 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1702 }) && !matches!(
1703 link_output,
1704 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1705 ) {
1706 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1707 }
1708 if !matches!(
1712 link_output,
1713 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1714 ) && self
1715 .parsed
1716 .iter()
1717 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1718 {
1719 return Err(BypassReason::UnmodeledLinkArgument(
1720 "link-arg=-Wl,-oso_prefix".into(),
1721 ));
1722 }
1723 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1724 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1725 _ => None,
1726 }) {
1727 return Err(BypassReason::UnresolvedExtern(name.clone()));
1728 }
1729 if let Some(emit) = self
1730 .emits
1731 .iter()
1732 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1733 {
1734 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1735 }
1736 if !self
1737 .emits
1738 .iter()
1739 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1740 {
1741 return Err(BypassReason::NoCacheableOutput);
1742 }
1743 Ok(link_output)
1744 }
1745}
1746
1747impl Parser<'_> {
1748 fn first_link_argument(&self) -> Option<&str> {
1751 self.parsed.iter().find_map(|argument| {
1752 let Argument::Plain(value) = argument else {
1753 return None;
1754 };
1755 let option = value.strip_prefix("--codegen=")?;
1756 let name = option.split_once('=').map_or(option, |(name, _)| name);
1757 matches!(name, "link-arg" | "link-args").then_some(option)
1758 })
1759 }
1760
1761 fn oso_prefix_covers_outputs(&self) -> bool {
1770 let Some(directory) = self
1771 .out_dir
1772 .as_deref()
1773 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
1774 else {
1775 return false;
1776 };
1777 if !directory.is_absolute() {
1778 return false;
1779 }
1780 let directory = normalize_components(directory);
1781 self.parsed.iter().any(|argument| {
1782 let Argument::OsoPrefix { path, .. } = argument else {
1783 return false;
1784 };
1785 path.is_absolute() && directory.starts_with(normalize_components(path))
1786 })
1787 }
1788
1789 fn links_a_native_artifact(&self) -> bool {
1796 self.target.is_none()
1797 && self.emits.iter().any(|emit| emit.kind == "link")
1802 && ((self.test && self.crate_types.is_empty())
1803 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro")))
1804 }
1805
1806 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
1813 for argument in &self.parsed {
1814 let Argument::Plain(value) = argument else {
1815 continue;
1816 };
1817 let (name, value) = if value == "-g" {
1820 ("debuginfo", Some("2"))
1821 } else if let Some(option) = value.strip_prefix("--codegen=") {
1822 match option.split_once('=') {
1823 Some((name, value)) => (name, Some(value)),
1824 None => (option, None),
1828 }
1829 } else {
1830 continue;
1831 };
1832 let unportable = match name {
1833 "split-debuginfo" => match value {
1839 Some("off") => false,
1840 Some("unpacked") if cfg!(target_os = "macos") => {
1841 !self.oso_prefix_covers_outputs()
1842 }
1843 _ => true,
1844 },
1845 "debuginfo" if cfg!(target_os = "macos") => {
1854 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
1855 }
1856 "rpath" => is_enabled(value),
1864 "prefer-dynamic" => {
1865 is_enabled(value)
1866 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
1867 }
1868 "link-self-contained" => true,
1871 _ => false,
1872 };
1873 if unportable {
1874 return Err(BypassReason::UnportableNativeLink(match value {
1875 Some(value) => format!("{name}={value}"),
1876 None => name.to_owned(),
1877 }));
1878 }
1879 }
1880 Ok(())
1881 }
1882}
1883
1884fn is_enabled(value: Option<&str>) -> bool {
1887 matches!(value, None | Some("y" | "yes" | "on" | "true"))
1888}
1889
1890fn compiler_bundled_wasm_target(target: &str) -> bool {
1891 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
1892}
1893
1894fn parse_emits(value: &str) -> Vec<Emit> {
1895 value
1896 .split(',')
1897 .map(|emit| {
1898 let (kind, path) = emit
1899 .split_once('=')
1900 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
1901 Emit {
1902 kind: kind.into(),
1903 path,
1904 }
1905 })
1906 .collect()
1907}
1908
1909struct ActionBuilder<'a> {
1910 invocation: &'a RustcInvocation,
1911 context: ActionContext,
1912 mappings: Vec<SharedPathMapping>,
1913 aliases: PathAliases,
1917 linker: Option<LinkerIdentity>,
1918}
1919
1920impl<'a> ActionBuilder<'a> {
1921 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
1922 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1923 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
1924 Self {
1925 linker: None,
1926 invocation,
1927 mappings,
1928 aliases: PathAliases::default(),
1929 context,
1930 }
1931 }
1932
1933 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
1934 self.linker = linker;
1935 self
1936 }
1937
1938 fn build(self) -> Result<RustcAction, BypassReason> {
1939 self.validate_mappings()?;
1940 let invocation = self.invocation_descriptor()?;
1941 let environment = self.environment_descriptor()?;
1942
1943 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1944 for input in &self.context.inputs {
1945 input
1946 .digest
1947 .validate()
1948 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
1949 let path = self.normalize_path(&input.path)?;
1950 if inputs
1951 .insert(path.clone(), input.digest.clone())
1952 .is_some_and(|existing| existing != input.digest)
1953 {
1954 return Err(BypassReason::ConflictingInput(path));
1955 }
1956 }
1957 let required = self
1958 .invocation
1959 .required_inputs
1960 .iter()
1961 .map(|path| self.normalize_path(path))
1962 .collect::<Result<BTreeSet<_>, _>>()?;
1963 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1964 return Err(BypassReason::MissingRequiredInput(missing.clone()));
1965 }
1966 let inputs = inputs
1967 .into_iter()
1968 .map(|(path, digest)| InputDescriptor { path, digest })
1969 .collect();
1970 if self.invocation.links_natively() && self.linker.is_none() {
1973 return Err(BypassReason::UnportableNativeLink(
1974 "linker identity is unknown".into(),
1975 ));
1976 }
1977 let descriptor = ActionDescriptor {
1978 version: ACTION_SCHEMA_VERSION,
1979 kind: "rustc",
1980 adapter_version: ADAPTER_VERSION,
1981 compiler: invocation.compiler,
1982 arguments: invocation.arguments,
1983 environment,
1984 inputs,
1985 linker: self.linker.clone(),
1986 };
1987 let bytes = canonical_json(&descriptor)
1988 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
1989 let digest = CacheDigest::blake3(&bytes);
1990 Ok(RustcAction { digest, bytes })
1991 }
1992
1993 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
1994 self.validate_mappings()?;
1995 let arguments = self
1996 .invocation
1997 .arguments
1998 .iter()
1999 .map(|argument| self.normalize_argument(argument))
2000 .collect::<Result<Vec<_>, _>>()?;
2001 let required_inputs = self
2002 .invocation
2003 .required_inputs
2004 .iter()
2005 .map(|path| self.normalize_path(path))
2006 .collect::<Result<BTreeSet<_>, _>>()?
2007 .into_iter()
2008 .collect();
2009 Ok(InvocationDescriptor {
2010 version: ACTION_SCHEMA_VERSION,
2011 kind: "rustc",
2012 adapter_version: ADAPTER_VERSION,
2013 compiler: CompilerDescriptor {
2014 toolchain: self.context.compiler.toolchain.clone(),
2015 rustc_version: self.context.compiler.rustc_version.clone(),
2016 host: self.context.compiler.host.clone(),
2017 driver: self.context.compiler.driver.clone(),
2018 },
2019 arguments,
2020 required_inputs,
2021 })
2022 }
2023
2024 fn validate_mappings(&self) -> Result<(), BypassReason> {
2025 if !self.context.working_dir.is_absolute() {
2026 return Err(BypassReason::RelativeWorkingDirectory(
2027 self.context.working_dir.clone(),
2028 ));
2029 }
2030 let mut roots = BTreeSet::new();
2031 let mut placeholders = BTreeSet::new();
2032 for mapping in &self.mappings {
2033 if !mapping.root.is_absolute() {
2034 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2035 }
2036 if mapping.placeholder.is_empty()
2037 || !mapping
2038 .placeholder
2039 .bytes()
2040 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2041 || !roots.insert(normalize_components(&mapping.root))
2042 || !placeholders.insert(&mapping.placeholder)
2043 {
2044 return Err(BypassReason::InvalidPathPlaceholder(
2045 mapping.placeholder.clone(),
2046 ));
2047 }
2048 }
2049 Ok(())
2050 }
2051
2052 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2053 match argument {
2054 Argument::Plain(value) => Ok(value.clone()),
2055 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2056 Argument::SearchPath { kind, path } => {
2057 let text = match self.normalize_path(path) {
2058 Ok(text) => text,
2059 Err(BypassReason::UnmappedAbsolutePath(absolute))
2067 if kind == "native" && self.invocation.native_search_is_inert() =>
2068 {
2069 absolute
2070 .to_str()
2071 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
2072 .to_string()
2073 }
2074 Err(error) => return Err(error),
2075 };
2076 Ok(format!("-L{kind}={text}"))
2077 }
2078 Argument::Extern { name, path } => match path {
2079 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2080 None => Ok(format!("--extern={name}")),
2081 },
2082 Argument::Emit(emits) => Ok(format!(
2083 "--emit={}",
2084 emits
2085 .iter()
2086 .map(|emit| match &emit.path {
2087 Some(path) => self
2088 .normalize_path(path)
2089 .map(|path| format!("{}={path}", emit.kind)),
2090 None => Ok(emit.kind.clone()),
2091 })
2092 .collect::<Result<Vec<_>, _>>()?
2093 .join(",")
2094 )),
2095 Argument::RemapPath { from, to } => Ok(format!(
2096 "--remap-path-prefix={}={}",
2097 self.normalize_path(from)?,
2098 to
2099 )),
2100 Argument::OsoPrefix {
2101 path,
2102 trailing_slash,
2103 } => Ok(format!(
2104 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2105 self.normalize_path(path)?,
2106 if *trailing_slash { "/" } else { "" }
2107 )),
2108 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2109 }
2110 }
2111
2112 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2119 self.context
2120 .environment
2121 .iter()
2122 .map(|(name, value)| {
2123 let value = match value {
2124 Some(value) if self.context.portable_environment.contains(name) => {
2125 Some(self.normalize_path(Path::new(value))?)
2126 }
2127 value => value.clone(),
2128 };
2129 Ok((name.clone(), value))
2130 })
2131 .collect()
2132 }
2133
2134 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2135 normalize_resolved_shared_path_with(
2136 &self.aliases,
2137 path,
2138 &self.context.working_dir,
2139 &self.mappings,
2140 )
2141 .map_err(Into::into)
2142 }
2143}
2144
2145fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2149 directories.iter().any(|directory| {
2150 path.len() > directory.len()
2151 && path.as_bytes()[directory.len()] == b'/'
2152 && path.starts_with(directory)
2153 })
2154}
2155
2156fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2157 for mapping in mappings {
2158 let prefix = format!("${{{}}}", mapping.placeholder);
2159 let suffix = if value == prefix {
2160 ""
2161 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2162 suffix
2163 } else {
2164 continue;
2165 };
2166 if !mapping.root.is_absolute()
2167 || (!suffix.is_empty()
2168 && suffix.split('/').any(|component| {
2169 component.is_empty()
2170 || matches!(component, "." | "..")
2171 || component.contains('\\')
2172 }))
2173 {
2174 return Err(BypassReason::InvalidPredictedInput(value.into()));
2175 }
2176 let mut path = normalize_components(&mapping.root);
2177 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2178 return Ok(path);
2179 }
2180 Err(BypassReason::InvalidPredictedInput(value.into()))
2181}
2182
2183fn normalize_components(path: &Path) -> PathBuf {
2184 let mut normalized = PathBuf::new();
2185 for component in path.components() {
2186 match component {
2187 Component::CurDir => {}
2188 Component::ParentDir => {
2189 normalized.pop();
2190 }
2191 component => normalized.push(component.as_os_str()),
2192 }
2193 }
2194 normalized
2195}
2196
2197fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2198 if path.is_absolute() {
2199 normalize_components(path)
2200 } else {
2201 normalize_components(&working_dir.join(path))
2202 }
2203}
2204
2205#[cfg(test)]
2206#[path = "rustc_cache_tests.rs"]
2207mod tests;