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 required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
509 self.required_inputs
510 .iter()
511 .map(|path| absolute_path(path, working_dir))
512 .collect()
513 }
514
515 pub fn target(&self) -> Option<&str> {
517 self.target.as_deref()
518 }
519
520 pub fn crate_name(&self) -> &str {
522 &self.crate_name
523 }
524
525 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
538 let linked = self
539 .arguments
540 .iter()
541 .filter_map(|argument| match argument {
542 Argument::Extern {
543 path: Some(path), ..
544 } => Some(path.as_path()),
545 _ => None,
546 })
547 .collect::<BTreeSet<_>>();
548 let owned = discovered
549 .inputs
550 .iter()
551 .filter(|input| !linked.contains(input.path.as_path()))
552 .map(|input| (input.path.as_path(), &input.digest))
553 .collect::<BTreeMap<_, _>>();
554 let mut bytes = Vec::new();
555 for (path, digest) in owned {
556 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
557 bytes.push(0);
558 bytes.extend_from_slice(digest.key().as_bytes());
559 bytes.push(0);
560 }
561 CacheDigest::blake3(&bytes)
562 }
563
564 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
569 if !working_dir.is_absolute() {
570 return Err(BypassReason::RelativeWorkingDirectory(
571 working_dir.to_path_buf(),
572 ));
573 }
574 let explicit_output = self
575 .explicit_output
576 .as_deref()
577 .map(|path| absolute_path(path, working_dir));
578 let output_directory = explicit_output
579 .as_deref()
580 .and_then(Path::parent)
581 .map(Path::to_path_buf)
582 .or_else(|| {
583 self.out_dir
584 .as_deref()
585 .map(|path| absolute_path(path, working_dir))
586 })
587 .unwrap_or_else(|| normalize_components(working_dir));
588 if let Some(output) = &explicit_output
592 && self.emits.iter().any(|emit| {
593 emit.path.is_none()
594 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
595 })
596 {
597 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
598 }
599 let mut files = BTreeSet::new();
600 let mut dep_info = None;
601 for emit in &self.emits {
602 if emit.kind == "dep-info" {
603 let path = emit.path.as_ref().map_or_else(
604 || {
605 explicit_output.clone().map_or_else(
606 || {
607 output_directory
608 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
609 },
610 |path| path.with_extension("d"),
611 )
612 },
613 |path| absolute_path(path, working_dir),
614 );
615 if path.file_name().is_none() {
616 return Err(BypassReason::InvalidOutputPath(path));
617 }
618 dep_info = Some(path);
619 continue;
620 }
621 let (prefix, extension) = match emit.kind.as_str() {
622 "link" => match self.link_output {
623 LinkOutput::Library => ("lib", "rlib"),
624 LinkOutput::WasmExecutable => ("", "wasm"),
625 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
626 LinkOutput::NativeProcMacro => (
627 std::env::consts::DLL_PREFIX,
628 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
629 ),
630 },
631 "metadata" => ("lib", "rmeta"),
632 _ => continue,
633 };
634 let path = if let Some(path) = &emit.path {
635 absolute_path(path, working_dir)
636 } else {
637 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
638 output_directory.join(if extension.is_empty() {
639 name
640 } else {
641 format!("{name}.{extension}")
642 })
643 };
644 if path.file_name().is_none() {
645 return Err(BypassReason::InvalidOutputPath(path));
646 }
647 if path.parent() != Some(output_directory.as_path()) {
648 return Err(BypassReason::SplitOutputDirectories);
649 }
650 let has_library_extension = path
655 .extension()
656 .and_then(|extension| extension.to_str())
657 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
658 || (cfg!(windows)
659 && path
660 .file_stem()
661 .and_then(|stem| Path::new(stem).extension())
662 .and_then(|extension| extension.to_str())
663 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
664 if emit.kind == "link"
665 && !matches!(self.link_output, LinkOutput::Library)
666 && has_library_extension
667 {
668 return Err(BypassReason::AmbiguousOutputName(path));
669 }
670 if emit.kind == "link" && self.emits_windows_pdb() {
671 files.insert(path.with_extension("pdb"));
672 }
673 files.insert(path);
674 }
675 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
676 if dep_info.parent() != Some(output_directory.as_path()) {
677 return Err(BypassReason::SplitOutputDirectories);
678 }
679 Ok(RustcOutputs {
680 directory: output_directory,
681 files: files.into_iter().collect(),
682 dep_info,
683 })
684 }
685
686 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
692 self.action_linked_by(context, None)
693 }
694
695 pub fn action_linked_by(
704 &self,
705 context: ActionContext,
706 linker: Option<LinkerIdentity>,
707 ) -> Result<RustcAction, BypassReason> {
708 ActionBuilder::new(self, context).linked_by(linker).build()
709 }
710
711 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
713 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
714 let bytes = canonical_json(&descriptor)
715 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
716 Ok(CacheDigest::blake3(&bytes))
717 }
718
719 pub fn prediction(
722 &self,
723 context: &ActionContext,
724 discovered: &DiscoveredInputs,
725 ) -> Result<RustcInputPrediction, BypassReason> {
726 let builder = ActionBuilder::new(self, context.clone());
727 builder.validate_mappings()?;
728 let mut native_directories = BTreeSet::new();
729 for argument in &self.arguments {
730 if let Argument::SearchPath { kind, path } = argument
731 && kind == "native"
732 {
733 match builder.normalize_path(path) {
734 Ok(normalized) => {
735 native_directories.insert(normalized);
736 }
737 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
743 }
744 Err(error) => return Err(error),
745 }
746 }
747 }
748 let mut inputs = BTreeSet::new();
755 for input in &discovered.inputs {
756 let normalized = builder.normalize_path(&input.path)?;
757 if !under_any_directory(&normalized, &native_directories) {
758 inputs.insert(normalized);
759 }
760 }
761 inputs.extend(
762 native_directories
763 .into_iter()
764 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
765 );
766 Ok(RustcInputPrediction {
767 version: 4,
768 inputs: inputs.into_iter().collect(),
769 environment: discovered.environment.keys().cloned().collect(),
770 compiler_duration_ns: 0,
771 crate_name: String::new(),
772 })
773 }
774}
775
776impl RustcOutputs {
777 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
780 (crate_name == "build_script_build")
781 .then(|| self.files.iter().find(|path| self.is_executable(path)))
782 .flatten()
783 .map(PathBuf::as_path)
784 }
785
786 pub fn is_executable(&self, path: &Path) -> bool {
794 self.files.iter().any(|output| output == path)
795 && !matches!(
796 path.extension().and_then(|extension| extension.to_str()),
797 Some("rlib" | "rmeta")
798 )
799 }
800}
801
802#[derive(Debug, Clone, PartialEq, Eq)]
803pub struct PathMapping {
805 pub root: PathBuf,
807 pub placeholder: String,
809}
810
811impl PathMapping {
812 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
814 Self {
815 root: root.into(),
816 placeholder: placeholder.into(),
817 }
818 }
819
820 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
823 let mut ordered = mappings.to_vec();
824 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
825 ordered
826 }
827}
828
829fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
830 mappings
831 .iter()
832 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
833 .collect()
834}
835
836pub fn normalize_mapped_path(
843 path: &Path,
844 working_dir: &Path,
845 mappings: &[PathMapping],
846) -> Result<String, BypassReason> {
847 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
848}
849
850#[derive(Debug, Clone, PartialEq, Eq)]
851pub struct CompilerIdentity {
853 pub toolchain: String,
855 pub rustc_version: String,
857 pub host: String,
859 pub driver: Option<String>,
861}
862
863#[derive(Debug, Clone, PartialEq, Eq)]
865pub struct ActionInput {
866 pub path: PathBuf,
868 pub digest: CacheDigest,
870}
871
872#[derive(Debug, Clone, PartialEq, Eq)]
874pub struct ActionContext {
875 pub compiler: CompilerIdentity,
877 pub working_dir: PathBuf,
879 pub path_mappings: Vec<PathMapping>,
881 pub environment: BTreeMap<String, Option<String>>,
883 pub portable_environment: BTreeSet<String>,
890 pub inputs: Vec<ActionInput>,
892}
893
894#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
904#[serde(deny_unknown_fields)]
905pub struct LinkerIdentity {
906 pub driver: String,
908 pub driver_version: String,
910 pub linker_version: String,
912 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
914 pub crt_objects: BTreeMap<String, CacheDigest>,
915 #[serde(skip_serializing_if = "Option::is_none", default)]
917 pub sdk: Option<String>,
918 #[serde(skip_serializing_if = "Option::is_none", default)]
920 pub deployment_target: Option<String>,
921}
922
923#[derive(Debug, Clone, PartialEq, Eq)]
925pub struct RustcAction {
926 pub digest: CacheDigest,
928 pub bytes: Vec<u8>,
930}
931
932#[derive(Debug, Clone, PartialEq, Eq)]
935pub struct RustcInputPrediction {
936 pub version: u8,
938 pub inputs: Vec<String>,
940 pub environment: Vec<String>,
942 pub compiler_duration_ns: u64,
945 pub crate_name: String,
947}
948
949#[derive(Serialize, Deserialize)]
950#[serde(deny_unknown_fields)]
951struct RustcInputPredictionWire {
952 version: u8,
953 inputs: Vec<String>,
954 environment: Vec<String>,
955 #[serde(default, skip_serializing_if = "is_zero")]
956 compiler_duration_ns: u64,
957 #[serde(default, skip_serializing_if = "String::is_empty")]
958 crate_name: String,
959}
960
961impl Serialize for RustcInputPrediction {
962 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
963 where
964 S: Serializer,
965 {
966 RustcInputPredictionWire {
967 version: self.version,
968 inputs: if self.version == 4 {
969 compact_prediction_inputs(&self.inputs)
970 } else {
971 self.inputs.clone()
972 },
973 environment: self.environment.clone(),
974 compiler_duration_ns: self.compiler_duration_ns,
975 crate_name: self.crate_name.clone(),
976 }
977 .serialize(serializer)
978 }
979}
980
981impl<'de> Deserialize<'de> for RustcInputPrediction {
982 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
983 where
984 D: Deserializer<'de>,
985 {
986 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
987 let inputs = if wire.version == 4 {
988 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
989 } else {
990 wire.inputs
991 };
992 Ok(Self {
993 version: wire.version,
994 inputs,
995 environment: wire.environment,
996 compiler_duration_ns: wire.compiler_duration_ns,
997 crate_name: wire.crate_name,
998 })
999 }
1000}
1001
1002fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1003 let mut previous = "";
1004 inputs
1005 .iter()
1006 .map(|input| {
1007 let mut shared = previous
1008 .bytes()
1009 .zip(input.bytes())
1010 .take_while(|(left, right)| left == right)
1011 .count();
1012 while !input.is_char_boundary(shared) {
1013 shared -= 1;
1014 }
1015 let compact = format!("{shared}:{}", &input[shared..]);
1016 previous = input;
1017 compact
1018 })
1019 .collect()
1020}
1021
1022fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1023 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1024 let mut decoded_bytes = 0_usize;
1025 for input in inputs {
1026 let (shared_text, suffix) = input
1027 .split_once(':')
1028 .ok_or("compact rustc prediction input has no prefix length")?;
1029 let shared: usize = shared_text
1030 .parse()
1031 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1032 if shared.to_string() != shared_text
1033 || shared > expanded.last().map_or(0, String::len)
1034 || expanded
1035 .last()
1036 .is_some_and(|previous| !previous.is_char_boundary(shared))
1037 {
1038 return Err("compact rustc prediction prefix is not canonical");
1039 }
1040 let mut path = expanded
1041 .last()
1042 .map_or_else(String::new, |previous| previous[..shared].to_string());
1043 path.push_str(suffix);
1044 decoded_bytes = decoded_bytes
1045 .checked_add(path.len())
1046 .ok_or("compact rustc prediction is too large")?;
1047 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1048 return Err("compact rustc prediction is too large");
1049 }
1050 expanded.push(path);
1051 }
1052 Ok(expanded)
1053}
1054
1055fn is_zero(value: &u64) -> bool {
1056 *value == 0
1057}
1058
1059impl RustcInputPrediction {
1060 pub fn discover(
1063 &self,
1064 working_dir: &Path,
1065 path_mappings: &[PathMapping],
1066 digests: &dyn FileDigestCache,
1067 ) -> Result<DiscoveredInputs, BypassReason> {
1068 if !matches!(self.version, 2..=4) {
1069 return Err(BypassReason::UnsupportedPrediction);
1070 }
1071 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1072 return Err(BypassReason::UnsupportedPrediction);
1073 }
1074 let mut paths = BTreeSet::new();
1075 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1076 let mut native_bytes = 0_u64;
1077 for path in &self.inputs {
1078 if self.version >= 3
1079 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1080 {
1081 let directory = denormalize_path(path, path_mappings)?;
1082 dep_info::collect_native_directory(
1083 &directory,
1084 &admitted_roots,
1085 &mut paths,
1086 &mut native_bytes,
1087 )?;
1088 } else {
1089 paths.insert(denormalize_path(path, path_mappings)?);
1090 }
1091 }
1092 let environment = self
1093 .environment
1094 .iter()
1095 .map(|name| {
1096 if name.is_empty() || name.contains(['=', '\0']) {
1097 return Err(BypassReason::UnsupportedPrediction);
1098 }
1099 let value = std::env::var_os(name)
1100 .map(|value| {
1101 value
1102 .into_string()
1103 .map_err(|_| BypassReason::UnsupportedPrediction)
1104 })
1105 .transpose()?;
1106 Ok((name.clone(), value))
1107 })
1108 .collect::<Result<BTreeMap<_, _>, _>>()?;
1109 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1110 }
1111}
1112
1113#[derive(Serialize)]
1114struct ActionDescriptor {
1115 version: u8,
1116 kind: &'static str,
1117 adapter_version: u8,
1118 compiler: CompilerDescriptor,
1119 arguments: Vec<String>,
1120 environment: BTreeMap<String, Option<String>>,
1121 inputs: Vec<InputDescriptor>,
1122 #[serde(skip_serializing_if = "Option::is_none")]
1125 linker: Option<LinkerIdentity>,
1126}
1127
1128#[derive(Serialize)]
1129struct InvocationDescriptor {
1130 version: u8,
1131 kind: &'static str,
1132 adapter_version: u8,
1133 compiler: CompilerDescriptor,
1134 arguments: Vec<String>,
1135 required_inputs: Vec<String>,
1136}
1137
1138#[derive(Serialize)]
1139struct CompilerDescriptor {
1140 toolchain: String,
1141 rustc_version: String,
1142 host: String,
1143 #[serde(skip_serializing_if = "Option::is_none")]
1144 driver: Option<String>,
1145}
1146
1147#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1148struct InputDescriptor {
1149 path: String,
1150 digest: CacheDigest,
1151}
1152
1153struct Parser<'a> {
1154 arguments: &'a [OsString],
1155 index: usize,
1156 parsed: Vec<Argument>,
1157 source: Option<PathBuf>,
1158 crate_types: Vec<String>,
1159 emits: Vec<Emit>,
1160 required_inputs: Vec<PathBuf>,
1161 test: bool,
1162 crate_name: Option<String>,
1163 extra_filename: String,
1164 out_dir: Option<PathBuf>,
1165 explicit_output: Option<PathBuf>,
1166 target: Option<String>,
1167 options: ParseOptions,
1168}
1169
1170struct ExpandedArguments {
1171 arguments: Vec<OsString>,
1172}
1173
1174#[derive(Default)]
1175struct ResponseExpander {
1176 shell_argfiles: bool,
1177 next_is_unstable_option: bool,
1178 arguments: Vec<OsString>,
1179}
1180
1181impl ResponseExpander {
1182 fn push(&mut self, argument: String) {
1183 if self.next_is_unstable_option {
1184 self.shell_argfiles |= argument == "shell-argfiles";
1185 self.next_is_unstable_option = false;
1186 } else if let Some(option) = argument.strip_prefix("-Z") {
1187 if option.is_empty() {
1188 self.next_is_unstable_option = true;
1189 } else {
1190 self.shell_argfiles |= option == "shell-argfiles";
1191 }
1192 }
1193 self.arguments.push(argument.into());
1194 }
1195}
1196
1197fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1200 let mut expanded = ResponseExpander::default();
1201 for (index, argument) in arguments.iter().enumerate() {
1202 let argument = argument
1203 .to_str()
1204 .ok_or(BypassReason::NonUtf8Argument { index })?;
1205 let Some(argfile) = argument.strip_prefix('@') else {
1206 expanded.push(argument.to_string());
1207 continue;
1208 };
1209 let (path, shell) = match argfile.split_once(':') {
1210 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1211 _ => (argfile, false),
1212 };
1213 let contents = std::fs::read_to_string(path).map_err(|error| {
1214 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1215 })?;
1216 if shell {
1217 let arguments = shlex::split(&contents).ok_or_else(|| {
1218 BypassReason::ResponseFile(format!(
1219 "invalid shell-style arguments in {}",
1220 Path::new(path).display()
1221 ))
1222 })?;
1223 for argument in arguments {
1224 expanded.push(argument);
1225 }
1226 } else {
1227 for argument in contents.lines() {
1228 expanded.push(argument.to_string());
1229 }
1230 }
1231 }
1232 Ok(ExpandedArguments {
1233 arguments: expanded.arguments,
1234 })
1235}
1236
1237impl<'a> Parser<'a> {
1238 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1239 Self {
1240 arguments,
1241 options,
1242 index: 0,
1243 parsed: Vec::new(),
1244 source: None,
1245 crate_types: Vec::new(),
1246 emits: Vec::new(),
1247 required_inputs: Vec::new(),
1248 test: false,
1249 crate_name: None,
1250 extra_filename: String::new(),
1251 out_dir: None,
1252 explicit_output: None,
1253 target: None,
1254 }
1255 }
1256
1257 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1258 while self.index < self.arguments.len() {
1259 let value = self.current()?.to_string();
1260 self.index += 1;
1261 if let Some(long) = value.strip_prefix("--") {
1262 self.parse_long(long)?;
1263 } else if value.starts_with('-') && value != "-" {
1264 self.parse_short(&value)?;
1265 } else {
1266 self.parse_input(&value)?;
1267 }
1268 }
1269
1270 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1271 let link_output = self.classify()?;
1272 let crate_name = self.crate_name.clone().map_or_else(
1273 || {
1274 source
1275 .file_stem()
1276 .and_then(|name| name.to_str())
1277 .map(|name| name.replace('-', "_"))
1278 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1279 },
1280 Ok,
1281 )?;
1282 self.required_inputs.push(source.clone());
1283 Ok(RustcInvocation {
1284 arguments: self.parsed,
1285 source,
1286 required_inputs: self.required_inputs,
1287 crate_name,
1288 extra_filename: self.extra_filename,
1289 out_dir: self.out_dir,
1290 explicit_output: self.explicit_output,
1291 emits: self.emits,
1292 target: self.target,
1293 link_output,
1294 })
1295 }
1296
1297 fn current(&self) -> Result<&str, BypassReason> {
1298 self.arguments[self.index]
1299 .to_str()
1300 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1301 }
1302
1303 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1304 if let Some(value) = inline {
1305 if value.is_empty() {
1306 return Err(BypassReason::MissingValue(flag.into()));
1307 }
1308 return Ok(value.into());
1309 }
1310 if self.index >= self.arguments.len() {
1311 return Err(BypassReason::MissingValue(flag.into()));
1312 }
1313 let value = self.current()?.to_string();
1314 self.index += 1;
1315 Ok(value)
1316 }
1317
1318 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1319 let (flag, inline) = value
1320 .split_once('=')
1321 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1322 let rendered_flag = format!("--{flag}");
1323 match flag {
1324 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1325 "test" => {
1326 self.test = true;
1327 self.parsed.push(Argument::Plain(rendered_flag));
1328 Ok(())
1329 }
1330 "verbose" => {
1331 self.parsed.push(Argument::Plain(rendered_flag));
1332 Ok(())
1333 }
1334 "crate-name" => {
1335 let value = self.take_value(&rendered_flag, inline)?;
1336 self.crate_name = Some(value.clone());
1337 self.parsed
1338 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1339 Ok(())
1340 }
1341 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1342 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1343 | "deny" | "forbid" | "cap-lints" => {
1344 let value = self.take_value(&rendered_flag, inline)?;
1345 self.parsed
1346 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1347 Ok(())
1348 }
1349 "target" => {
1350 let value = self.take_value(&rendered_flag, inline)?;
1351 self.target = Some(value.clone());
1352 if value.ends_with(".json") || value.contains(['/', '\\']) {
1353 let path = PathBuf::from(value);
1354 self.required_inputs.push(path.clone());
1355 self.parsed.push(Argument::Path {
1356 flag: rendered_flag,
1357 path,
1358 });
1359 } else {
1360 self.target = Some(value.clone());
1361 self.parsed
1362 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1363 }
1364 Ok(())
1365 }
1366 "crate-type" => {
1367 let value = self.take_value(&rendered_flag, inline)?;
1368 self.crate_types
1369 .extend(value.split(',').map(ToOwned::to_owned));
1370 self.parsed
1371 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1372 Ok(())
1373 }
1374 "emit" => {
1375 let value = self.take_value(&rendered_flag, inline)?;
1376 let emits = parse_emits(&value);
1377 self.emits.extend(emits.clone());
1378 self.parsed.push(Argument::Emit(emits));
1379 Ok(())
1380 }
1381 "out-dir" => {
1382 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1383 self.out_dir = Some(path.clone());
1384 self.parsed.push(Argument::Path {
1385 flag: rendered_flag,
1386 path,
1387 });
1388 Ok(())
1389 }
1390 "sysroot" => {
1391 let path = self.take_value(&rendered_flag, inline)?;
1392 self.parsed.push(Argument::Path {
1393 flag: rendered_flag,
1394 path: path.into(),
1395 });
1396 Ok(())
1397 }
1398 "extern" => {
1399 let value = self.take_value(&rendered_flag, inline)?;
1400 let (name, path) = value
1401 .split_once('=')
1402 .map_or((value.as_str(), None), |(name, path)| {
1403 (name, Some(PathBuf::from(path)))
1404 });
1405 if let Some(path) = &path {
1406 self.required_inputs.push(path.clone());
1407 }
1408 self.parsed.push(Argument::Extern {
1409 name: name.into(),
1410 path,
1411 });
1412 Ok(())
1413 }
1414 "remap-path-prefix" => {
1415 let value = self.take_value(&rendered_flag, inline)?;
1416 let Some((from, to)) = value.split_once('=') else {
1417 return Err(BypassReason::MissingValue(rendered_flag));
1418 };
1419 self.parsed.push(Argument::RemapPath {
1420 from: from.into(),
1421 to: to.into(),
1422 });
1423 Ok(())
1424 }
1425 "codegen" => {
1426 let value = self.take_value(&rendered_flag, inline)?;
1427 self.parse_codegen(&value)
1428 }
1429 "jobs-frontend" => {
1430 let value = self.take_value(&rendered_flag, inline)?;
1431 self.parsed
1432 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1433 Ok(())
1434 }
1435 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1436 }
1437 }
1438
1439 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1440 if let Some(attached) = value.strip_prefix("-Z") {
1441 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1442 match option.as_str() {
1443 "shell-argfiles" | "unstable-options" => {
1444 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1445 return Ok(());
1446 }
1447 "threads" | "threads=" => {
1448 return Err(BypassReason::MissingValue("-Zthreads".into()));
1449 }
1450 _ => {}
1451 }
1452 if option.starts_with("threads=") {
1453 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1454 return Ok(());
1455 }
1456 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1457 }
1458 match value {
1459 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1462 "-g" | "-O" | "-v" => {
1463 self.parsed.push(Argument::Plain(value.into()));
1464 return Ok(());
1465 }
1466 _ => {}
1467 }
1468 for (short, long) in [
1469 ("-A", "--allow"),
1470 ("-W", "--warn"),
1471 ("-D", "--deny"),
1472 ("-F", "--forbid"),
1473 ] {
1474 if let Some(attached) = value.strip_prefix(short) {
1475 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1476 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1477 return Ok(());
1478 }
1479 }
1480 if let Some(attached) = value.strip_prefix("-C") {
1481 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1482 return self.parse_codegen(&option);
1483 }
1484 if let Some(attached) = value.strip_prefix("-L") {
1485 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1486 let (kind, path) = search
1487 .split_once('=')
1488 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1489 if !matches!(kind, "dependency" | "native") {
1490 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1491 }
1492 self.parsed.push(Argument::SearchPath {
1493 kind: kind.into(),
1494 path: path.into(),
1495 });
1496 return Ok(());
1497 }
1498 if value == "-l" || value.starts_with("-l") {
1499 return Err(BypassReason::NativeLibrary);
1500 }
1501 if let Some(attached) = value.strip_prefix("-o") {
1502 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1503 self.explicit_output = Some(path.clone().into());
1504 self.parsed.push(Argument::Path {
1505 flag: "-o".into(),
1506 path: path.into(),
1507 });
1508 return Ok(());
1509 }
1510 Err(BypassReason::UnknownFlag(value.into()))
1511 }
1512
1513 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1514 let name = value.split_once('=').map_or(value, |(name, _)| name);
1515 if name == "incremental" {
1516 return Err(BypassReason::Incremental);
1517 }
1518 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1519 return Err(BypassReason::UnknownCodegenOption(name.into()));
1520 }
1521 if name == "linker-plugin-lto"
1527 && let Some((_, selection)) = value.split_once('=')
1528 && !matches!(
1529 selection,
1530 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1531 )
1532 {
1533 return Err(BypassReason::UnknownCodegenOption(format!(
1534 "linker-plugin-lto={selection}"
1535 )));
1536 }
1537 if name == "link-arg"
1544 && let Some((_, option)) = value.split_once('=')
1545 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1546 && !selection.is_empty()
1547 && !selection.contains(',')
1548 && !self.parsed.iter().any(
1549 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1550 )
1551 {
1552 if !self.parsed.iter().any(
1553 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1554 ) {
1555 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1556 }
1557 return Ok(());
1558 }
1559 if matches!(name, "link-arg" | "link-args")
1564 && let Some((_, option)) = value.split_once('=')
1565 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1566 && !prefix.trim_end_matches('/').is_empty()
1567 && !prefix.contains(',')
1568 {
1569 let trailing_slash = prefix.ends_with('/');
1570 self.parsed.push(Argument::OsoPrefix {
1571 path: PathBuf::from(prefix.trim_end_matches('/')),
1572 trailing_slash,
1573 });
1574 return Ok(());
1575 }
1576 self.parsed
1577 .push(Argument::Plain(format!("--codegen={value}")));
1578 if name == "extra-filename" {
1579 self.extra_filename = value
1580 .split_once('=')
1581 .map_or(String::new(), |(_, value)| value.to_string());
1582 }
1583 Ok(())
1584 }
1585
1586 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1587 if value == "-" {
1588 return Err(BypassReason::StandardInput);
1589 }
1590 if self.source.replace(value.into()).is_some() {
1591 return Err(BypassReason::MultipleInputs);
1592 }
1593 Ok(())
1594 }
1595
1596 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1597 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1606 let builds_a_library = !self.test
1607 && !self.crate_types.is_empty()
1608 && self
1609 .crate_types
1610 .iter()
1611 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1612 let link_output = if never_links || builds_a_library {
1613 LinkOutput::Library
1614 } else if self
1615 .target
1616 .as_deref()
1617 .is_some_and(compiler_bundled_wasm_target)
1618 && ((self.test && self.crate_types.is_empty())
1619 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1620 {
1621 if self.parsed.iter().any(|argument| match argument {
1622 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1623 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1624 !matches!(
1625 value.rsplit_once('=').map(|(_, value)| value),
1626 Some("y" | "yes" | "on" | "true")
1627 )
1628 }
1629 _ => false,
1630 }) {
1631 return Err(BypassReason::UnknownCodegenOption(
1632 "link-self-contained".into(),
1633 ));
1634 }
1635 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1636 && self.parsed.iter().any(|argument| {
1637 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1638 })
1639 {
1640 return Err(BypassReason::UnknownCodegenOption(
1641 "target-feature=-crt-static".into(),
1642 ));
1643 }
1644 LinkOutput::WasmExecutable
1648 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1649 self.check_native_link_is_portable()?;
1650 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1651 LinkOutput::NativeProcMacro
1652 } else {
1653 LinkOutput::NativeExecutable
1654 }
1655 } else if self.test {
1656 return Err(BypassReason::UnsupportedCrateType("test".into()));
1657 } else {
1658 return Err(BypassReason::UnsupportedCrateType(
1659 self.crate_types
1660 .iter()
1661 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1662 .cloned()
1663 .unwrap_or_else(|| "bin".into()),
1664 ));
1665 };
1666 if link_output != LinkOutput::Library
1675 && let Some(option) = self.first_link_argument()
1676 {
1677 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1678 }
1679 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1686 let Argument::FuseLd(selection) = argument else {
1687 return None;
1688 };
1689 Some(selection.as_str())
1690 }) && !matches!(
1691 link_output,
1692 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1693 ) {
1694 return Err(BypassReason::UnmodeledLinkArgument(format!(
1695 "link-arg=-fuse-ld={selection}"
1696 )));
1697 }
1698 if self.parsed.iter().any(|argument| {
1699 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1700 }) && !matches!(
1701 link_output,
1702 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1703 ) {
1704 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1705 }
1706 if !matches!(
1710 link_output,
1711 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1712 ) && self
1713 .parsed
1714 .iter()
1715 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1716 {
1717 return Err(BypassReason::UnmodeledLinkArgument(
1718 "link-arg=-Wl,-oso_prefix".into(),
1719 ));
1720 }
1721 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1722 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1723 _ => None,
1724 }) {
1725 return Err(BypassReason::UnresolvedExtern(name.clone()));
1726 }
1727 if let Some(emit) = self
1728 .emits
1729 .iter()
1730 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1731 {
1732 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1733 }
1734 if !self
1735 .emits
1736 .iter()
1737 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1738 {
1739 return Err(BypassReason::NoCacheableOutput);
1740 }
1741 Ok(link_output)
1742 }
1743}
1744
1745impl Parser<'_> {
1746 fn first_link_argument(&self) -> Option<&str> {
1749 self.parsed.iter().find_map(|argument| {
1750 let Argument::Plain(value) = argument else {
1751 return None;
1752 };
1753 let option = value.strip_prefix("--codegen=")?;
1754 let name = option.split_once('=').map_or(option, |(name, _)| name);
1755 matches!(name, "link-arg" | "link-args").then_some(option)
1756 })
1757 }
1758
1759 fn oso_prefix_covers_outputs(&self) -> bool {
1768 let Some(directory) = self
1769 .out_dir
1770 .as_deref()
1771 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
1772 else {
1773 return false;
1774 };
1775 if !directory.is_absolute() {
1776 return false;
1777 }
1778 let directory = normalize_components(directory);
1779 self.parsed.iter().any(|argument| {
1780 let Argument::OsoPrefix { path, .. } = argument else {
1781 return false;
1782 };
1783 path.is_absolute() && directory.starts_with(normalize_components(path))
1784 })
1785 }
1786
1787 fn links_a_native_artifact(&self) -> bool {
1794 self.target.is_none()
1795 && self.emits.iter().any(|emit| emit.kind == "link")
1800 && ((self.test && self.crate_types.is_empty())
1801 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro")))
1802 }
1803
1804 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
1811 for argument in &self.parsed {
1812 let Argument::Plain(value) = argument else {
1813 continue;
1814 };
1815 let (name, value) = if value == "-g" {
1818 ("debuginfo", Some("2"))
1819 } else if let Some(option) = value.strip_prefix("--codegen=") {
1820 match option.split_once('=') {
1821 Some((name, value)) => (name, Some(value)),
1822 None => (option, None),
1826 }
1827 } else {
1828 continue;
1829 };
1830 let unportable = match name {
1831 "split-debuginfo" => match value {
1837 Some("off") => false,
1838 Some("unpacked") if cfg!(target_os = "macos") => {
1839 !self.oso_prefix_covers_outputs()
1840 }
1841 _ => true,
1842 },
1843 "debuginfo" if cfg!(target_os = "macos") => {
1852 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
1853 }
1854 "rpath" => is_enabled(value),
1862 "prefer-dynamic" => {
1863 is_enabled(value)
1864 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
1865 }
1866 "link-self-contained" => true,
1869 _ => false,
1870 };
1871 if unportable {
1872 return Err(BypassReason::UnportableNativeLink(match value {
1873 Some(value) => format!("{name}={value}"),
1874 None => name.to_owned(),
1875 }));
1876 }
1877 }
1878 Ok(())
1879 }
1880}
1881
1882fn is_enabled(value: Option<&str>) -> bool {
1885 matches!(value, None | Some("y" | "yes" | "on" | "true"))
1886}
1887
1888fn compiler_bundled_wasm_target(target: &str) -> bool {
1889 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
1890}
1891
1892fn parse_emits(value: &str) -> Vec<Emit> {
1893 value
1894 .split(',')
1895 .map(|emit| {
1896 let (kind, path) = emit
1897 .split_once('=')
1898 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
1899 Emit {
1900 kind: kind.into(),
1901 path,
1902 }
1903 })
1904 .collect()
1905}
1906
1907struct ActionBuilder<'a> {
1908 invocation: &'a RustcInvocation,
1909 context: ActionContext,
1910 mappings: Vec<SharedPathMapping>,
1911 linker: Option<LinkerIdentity>,
1912}
1913
1914impl<'a> ActionBuilder<'a> {
1915 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
1916 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1917 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
1918 Self {
1919 linker: None,
1920 invocation,
1921 mappings,
1922 context,
1923 }
1924 }
1925
1926 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
1927 self.linker = linker;
1928 self
1929 }
1930
1931 fn build(self) -> Result<RustcAction, BypassReason> {
1932 self.validate_mappings()?;
1933 let invocation = self.invocation_descriptor()?;
1934 let environment = self.environment_descriptor()?;
1935
1936 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1937 for input in &self.context.inputs {
1938 input
1939 .digest
1940 .validate()
1941 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
1942 let path = self.normalize_path(&input.path)?;
1943 if inputs
1944 .insert(path.clone(), input.digest.clone())
1945 .is_some_and(|existing| existing != input.digest)
1946 {
1947 return Err(BypassReason::ConflictingInput(path));
1948 }
1949 }
1950 let required = self
1951 .invocation
1952 .required_inputs
1953 .iter()
1954 .map(|path| self.normalize_path(path))
1955 .collect::<Result<BTreeSet<_>, _>>()?;
1956 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1957 return Err(BypassReason::MissingRequiredInput(missing.clone()));
1958 }
1959 let inputs = inputs
1960 .into_iter()
1961 .map(|(path, digest)| InputDescriptor { path, digest })
1962 .collect();
1963 if self.invocation.links_natively() && self.linker.is_none() {
1966 return Err(BypassReason::UnportableNativeLink(
1967 "linker identity is unknown".into(),
1968 ));
1969 }
1970 let descriptor = ActionDescriptor {
1971 version: ACTION_SCHEMA_VERSION,
1972 kind: "rustc",
1973 adapter_version: ADAPTER_VERSION,
1974 compiler: invocation.compiler,
1975 arguments: invocation.arguments,
1976 environment,
1977 inputs,
1978 linker: self.linker.clone(),
1979 };
1980 let bytes = canonical_json(&descriptor)
1981 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
1982 let digest = CacheDigest::blake3(&bytes);
1983 Ok(RustcAction { digest, bytes })
1984 }
1985
1986 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
1987 self.validate_mappings()?;
1988 let arguments = self
1989 .invocation
1990 .arguments
1991 .iter()
1992 .map(|argument| self.normalize_argument(argument))
1993 .collect::<Result<Vec<_>, _>>()?;
1994 let required_inputs = self
1995 .invocation
1996 .required_inputs
1997 .iter()
1998 .map(|path| self.normalize_path(path))
1999 .collect::<Result<BTreeSet<_>, _>>()?
2000 .into_iter()
2001 .collect();
2002 Ok(InvocationDescriptor {
2003 version: ACTION_SCHEMA_VERSION,
2004 kind: "rustc",
2005 adapter_version: ADAPTER_VERSION,
2006 compiler: CompilerDescriptor {
2007 toolchain: self.context.compiler.toolchain.clone(),
2008 rustc_version: self.context.compiler.rustc_version.clone(),
2009 host: self.context.compiler.host.clone(),
2010 driver: self.context.compiler.driver.clone(),
2011 },
2012 arguments,
2013 required_inputs,
2014 })
2015 }
2016
2017 fn validate_mappings(&self) -> Result<(), BypassReason> {
2018 if !self.context.working_dir.is_absolute() {
2019 return Err(BypassReason::RelativeWorkingDirectory(
2020 self.context.working_dir.clone(),
2021 ));
2022 }
2023 let mut roots = BTreeSet::new();
2024 let mut placeholders = BTreeSet::new();
2025 for mapping in &self.mappings {
2026 if !mapping.root.is_absolute() {
2027 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2028 }
2029 if mapping.placeholder.is_empty()
2030 || !mapping
2031 .placeholder
2032 .bytes()
2033 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2034 || !roots.insert(normalize_components(&mapping.root))
2035 || !placeholders.insert(&mapping.placeholder)
2036 {
2037 return Err(BypassReason::InvalidPathPlaceholder(
2038 mapping.placeholder.clone(),
2039 ));
2040 }
2041 }
2042 Ok(())
2043 }
2044
2045 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2046 match argument {
2047 Argument::Plain(value) => Ok(value.clone()),
2048 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2049 Argument::SearchPath { kind, path } => {
2050 let text = match self.normalize_path(path) {
2051 Ok(text) => text,
2052 Err(BypassReason::UnmappedAbsolutePath(absolute))
2060 if kind == "native" && self.invocation.native_search_is_inert() =>
2061 {
2062 absolute
2063 .to_str()
2064 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
2065 .to_string()
2066 }
2067 Err(error) => return Err(error),
2068 };
2069 Ok(format!("-L{kind}={text}"))
2070 }
2071 Argument::Extern { name, path } => match path {
2072 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2073 None => Ok(format!("--extern={name}")),
2074 },
2075 Argument::Emit(emits) => Ok(format!(
2076 "--emit={}",
2077 emits
2078 .iter()
2079 .map(|emit| match &emit.path {
2080 Some(path) => self
2081 .normalize_path(path)
2082 .map(|path| format!("{}={path}", emit.kind)),
2083 None => Ok(emit.kind.clone()),
2084 })
2085 .collect::<Result<Vec<_>, _>>()?
2086 .join(",")
2087 )),
2088 Argument::RemapPath { from, to } => Ok(format!(
2089 "--remap-path-prefix={}={}",
2090 self.normalize_path(from)?,
2091 to
2092 )),
2093 Argument::OsoPrefix {
2094 path,
2095 trailing_slash,
2096 } => Ok(format!(
2097 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2098 self.normalize_path(path)?,
2099 if *trailing_slash { "/" } else { "" }
2100 )),
2101 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2102 }
2103 }
2104
2105 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2112 self.context
2113 .environment
2114 .iter()
2115 .map(|(name, value)| {
2116 let value = match value {
2117 Some(value) if self.context.portable_environment.contains(name) => {
2118 Some(self.normalize_path(Path::new(value))?)
2119 }
2120 value => value.clone(),
2121 };
2122 Ok((name.clone(), value))
2123 })
2124 .collect()
2125 }
2126
2127 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2128 normalize_resolved_shared_path(path, &self.context.working_dir, &self.mappings)
2129 .map_err(Into::into)
2130 }
2131}
2132
2133fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2137 directories.iter().any(|directory| {
2138 path.len() > directory.len()
2139 && path.as_bytes()[directory.len()] == b'/'
2140 && path.starts_with(directory)
2141 })
2142}
2143
2144fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2145 for mapping in mappings {
2146 let prefix = format!("${{{}}}", mapping.placeholder);
2147 let suffix = if value == prefix {
2148 ""
2149 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2150 suffix
2151 } else {
2152 continue;
2153 };
2154 if !mapping.root.is_absolute()
2155 || (!suffix.is_empty()
2156 && suffix.split('/').any(|component| {
2157 component.is_empty()
2158 || matches!(component, "." | "..")
2159 || component.contains('\\')
2160 }))
2161 {
2162 return Err(BypassReason::InvalidPredictedInput(value.into()));
2163 }
2164 let mut path = normalize_components(&mapping.root);
2165 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2166 return Ok(path);
2167 }
2168 Err(BypassReason::InvalidPredictedInput(value.into()))
2169}
2170
2171fn normalize_components(path: &Path) -> PathBuf {
2172 let mut normalized = PathBuf::new();
2173 for component in path.components() {
2174 match component {
2175 Component::CurDir => {}
2176 Component::ParentDir => {
2177 normalized.pop();
2178 }
2179 component => normalized.push(component.as_os_str()),
2180 }
2181 }
2182 normalized
2183}
2184
2185fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2186 if path.is_absolute() {
2187 normalize_components(path)
2188 } else {
2189 normalize_components(&working_dir.join(path))
2190 }
2191}
2192
2193#[cfg(test)]
2194#[path = "rustc_cache_tests.rs"]
2195mod tests;