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 InstallName(String),
339 OsoPrefix {
342 path: PathBuf,
343 trailing_slash: bool,
344 },
345 FuseLd(String),
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
352struct Emit {
353 kind: String,
354 path: Option<PathBuf>,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq)]
358pub struct RustcInvocation {
363 arguments: Vec<Argument>,
364 source: PathBuf,
365 required_inputs: Vec<PathBuf>,
366 crate_name: String,
367 extra_filename: String,
368 out_dir: Option<PathBuf>,
369 explicit_output: Option<PathBuf>,
370 emits: Vec<Emit>,
371 target: Option<String>,
372 link_output: LinkOutput,
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376enum LinkOutput {
377 Library,
378 WasmExecutable,
379 NativeExecutable,
380 NativeProcMacro,
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
389#[non_exhaustive]
390pub struct ParseOptions {
391 pub cache_native_links: bool,
394}
395
396impl ParseOptions {
397 pub fn caching_native_links(enabled: bool) -> Self {
399 Self {
400 cache_native_links: enabled,
401 }
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Eq)]
407pub struct RustcOutputs {
408 pub directory: PathBuf,
410 pub files: Vec<PathBuf>,
412 pub dep_info: PathBuf,
414}
415
416impl RustcInvocation {
417 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
426 Self::parse_with(arguments, ParseOptions::default())
427 }
428
429 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
432 let expanded = expand_response_files(arguments)?;
433 Parser::new(&expanded.arguments, options).parse()
434 }
435
436 pub fn links_natively(&self) -> bool {
439 matches!(
440 self.link_output,
441 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
442 )
443 }
444
445 pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
449 if !cfg!(target_os = "macos")
450 || self.link_output != LinkOutput::NativeProcMacro
451 || self
452 .arguments
453 .iter()
454 .any(|arg| matches!(arg, Argument::InstallName(_)))
455 {
456 return None;
457 }
458 let file = outputs
459 .files
460 .iter()
461 .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
462 let name = file.file_name()?.to_str()?;
463 if !safe_install_name(name) {
464 return None;
465 }
466 Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
467 }
468
469 pub fn linker_override(&self) -> Option<&Path> {
472 self.arguments.iter().find_map(|argument| {
473 let Argument::Plain(value) = argument else {
474 return None;
475 };
476 value.strip_prefix("--codegen=linker=").map(Path::new)
477 })
478 }
479
480 pub fn fuse_ld(&self) -> Option<&str> {
483 self.arguments.iter().find_map(|argument| {
484 let Argument::FuseLd(selection) = argument else {
485 return None;
486 };
487 Some(selection.as_str())
488 })
489 }
490
491 fn emits_windows_pdb(&self) -> bool {
492 if !cfg!(windows) || !self.links_natively() {
493 return false;
494 }
495 let mut enabled = false;
496 for argument in &self.arguments {
497 let Argument::Plain(value) = argument else {
498 continue;
499 };
500 if value == "-g" {
501 enabled = true;
502 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
503 enabled = !matches!(value, "0" | "none");
504 }
505 }
506 enabled
507 }
508
509 fn native_search_is_inert(&self) -> bool {
522 self.link_output == LinkOutput::Library
523 }
524
525 pub fn source(&self) -> &Path {
527 &self.source
528 }
529
530 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
536 self.required_inputs
537 .iter()
538 .map(|path| absolute_path(path, working_dir))
539 .collect()
540 }
541
542 pub fn target(&self) -> Option<&str> {
544 self.target.as_deref()
545 }
546
547 pub fn crate_name(&self) -> &str {
549 &self.crate_name
550 }
551
552 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
565 let linked = self
566 .arguments
567 .iter()
568 .filter_map(|argument| match argument {
569 Argument::Extern {
570 path: Some(path), ..
571 } => Some(path.as_path()),
572 _ => None,
573 })
574 .collect::<BTreeSet<_>>();
575 let owned = discovered
576 .inputs
577 .iter()
578 .filter(|input| !linked.contains(input.path.as_path()))
579 .map(|input| (input.path.as_path(), &input.digest))
580 .collect::<BTreeMap<_, _>>();
581 let mut bytes = Vec::new();
582 for (path, digest) in owned {
583 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
584 bytes.push(0);
585 bytes.extend_from_slice(digest.key().as_bytes());
586 bytes.push(0);
587 }
588 CacheDigest::blake3(&bytes)
589 }
590
591 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
596 if !working_dir.is_absolute() {
597 return Err(BypassReason::RelativeWorkingDirectory(
598 working_dir.to_path_buf(),
599 ));
600 }
601 let explicit_output = self
602 .explicit_output
603 .as_deref()
604 .map(|path| absolute_path(path, working_dir));
605 let output_directory = explicit_output
606 .as_deref()
607 .and_then(Path::parent)
608 .map(Path::to_path_buf)
609 .or_else(|| {
610 self.out_dir
611 .as_deref()
612 .map(|path| absolute_path(path, working_dir))
613 })
614 .unwrap_or_else(|| normalize_components(working_dir));
615 if let Some(output) = &explicit_output
619 && self.emits.iter().any(|emit| {
620 emit.path.is_none()
621 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
622 })
623 {
624 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
625 }
626 let mut files = BTreeSet::new();
627 let mut dep_info = None;
628 for emit in &self.emits {
629 if emit.kind == "dep-info" {
630 let path = emit.path.as_ref().map_or_else(
631 || {
632 explicit_output.clone().map_or_else(
633 || {
634 output_directory
635 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
636 },
637 |path| path.with_extension("d"),
638 )
639 },
640 |path| absolute_path(path, working_dir),
641 );
642 if path.file_name().is_none() {
643 return Err(BypassReason::InvalidOutputPath(path));
644 }
645 dep_info = Some(path);
646 continue;
647 }
648 let (prefix, extension) = match emit.kind.as_str() {
649 "link" => match self.link_output {
650 LinkOutput::Library => ("lib", "rlib"),
651 LinkOutput::WasmExecutable => ("", "wasm"),
652 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
653 LinkOutput::NativeProcMacro => (
654 std::env::consts::DLL_PREFIX,
655 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
656 ),
657 },
658 "metadata" => ("lib", "rmeta"),
659 _ => continue,
660 };
661 let path = if let Some(path) = &emit.path {
662 absolute_path(path, working_dir)
663 } else {
664 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
665 output_directory.join(if extension.is_empty() {
666 name
667 } else {
668 format!("{name}.{extension}")
669 })
670 };
671 if path.file_name().is_none() {
672 return Err(BypassReason::InvalidOutputPath(path));
673 }
674 if path.parent() != Some(output_directory.as_path()) {
675 return Err(BypassReason::SplitOutputDirectories);
676 }
677 let has_library_extension = path
682 .extension()
683 .and_then(|extension| extension.to_str())
684 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
685 || (cfg!(windows)
686 && path
687 .file_stem()
688 .and_then(|stem| Path::new(stem).extension())
689 .and_then(|extension| extension.to_str())
690 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
691 if emit.kind == "link"
692 && !matches!(self.link_output, LinkOutput::Library)
693 && has_library_extension
694 {
695 return Err(BypassReason::AmbiguousOutputName(path));
696 }
697 if emit.kind == "link" && self.emits_windows_pdb() {
698 files.insert(path.with_extension("pdb"));
699 }
700 files.insert(path);
701 }
702 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
703 if dep_info.parent() != Some(output_directory.as_path()) {
704 return Err(BypassReason::SplitOutputDirectories);
705 }
706 Ok(RustcOutputs {
707 directory: output_directory,
708 files: files.into_iter().collect(),
709 dep_info,
710 })
711 }
712
713 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
719 self.action_linked_by(context, None)
720 }
721
722 pub fn action_linked_by(
731 &self,
732 context: ActionContext,
733 linker: Option<LinkerIdentity>,
734 ) -> Result<RustcAction, BypassReason> {
735 ActionBuilder::new(self, context).linked_by(linker).build()
736 }
737
738 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
740 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
741 let bytes = canonical_json(&descriptor)
742 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
743 Ok(CacheDigest::blake3(&bytes))
744 }
745
746 pub fn prediction(
749 &self,
750 context: &ActionContext,
751 discovered: &DiscoveredInputs,
752 ) -> Result<RustcInputPrediction, BypassReason> {
753 let builder = ActionBuilder::new(self, context.clone());
754 builder.validate_mappings()?;
755 let mut native_directories = BTreeSet::new();
756 for argument in &self.arguments {
757 if let Argument::SearchPath { kind, path } = argument
758 && kind == "native"
759 {
760 match builder.normalize_path(path) {
761 Ok(normalized) => {
762 native_directories.insert(normalized);
763 }
764 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
770 }
771 Err(error) => return Err(error),
772 }
773 }
774 }
775 let mut inputs = BTreeSet::new();
782 for input in &discovered.inputs {
783 let normalized = builder.normalize_path(&input.path)?;
784 if !under_any_directory(&normalized, &native_directories) {
785 inputs.insert(normalized);
786 }
787 }
788 inputs.extend(
789 native_directories
790 .into_iter()
791 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
792 );
793 Ok(RustcInputPrediction {
794 version: 4,
795 inputs: inputs.into_iter().collect(),
796 environment: discovered.environment.keys().cloned().collect(),
797 compiler_duration_ns: 0,
798 crate_name: String::new(),
799 })
800 }
801}
802
803impl RustcOutputs {
804 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
807 (crate_name == "build_script_build")
808 .then(|| self.files.iter().find(|path| self.is_executable(path)))
809 .flatten()
810 .map(PathBuf::as_path)
811 }
812
813 pub fn is_executable(&self, path: &Path) -> bool {
821 self.files.iter().any(|output| output == path)
822 && !matches!(
823 path.extension().and_then(|extension| extension.to_str()),
824 Some("rlib" | "rmeta")
825 )
826 }
827}
828
829#[derive(Debug, Clone, PartialEq, Eq)]
830pub struct PathMapping {
832 pub root: PathBuf,
834 pub placeholder: String,
836}
837
838impl PathMapping {
839 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
841 Self {
842 root: root.into(),
843 placeholder: placeholder.into(),
844 }
845 }
846
847 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
850 let mut ordered = mappings.to_vec();
851 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
852 ordered
853 }
854}
855
856fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
857 mappings
858 .iter()
859 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
860 .collect()
861}
862
863pub fn normalize_mapped_path(
870 path: &Path,
871 working_dir: &Path,
872 mappings: &[PathMapping],
873) -> Result<String, BypassReason> {
874 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
875}
876
877#[derive(Debug, Clone, PartialEq, Eq)]
878pub struct CompilerIdentity {
880 pub toolchain: String,
882 pub rustc_version: String,
884 pub host: String,
886 pub driver: Option<String>,
888}
889
890#[derive(Debug, Clone, PartialEq, Eq)]
892pub struct ActionInput {
893 pub path: PathBuf,
895 pub digest: CacheDigest,
897}
898
899#[derive(Debug, Clone, PartialEq, Eq)]
901pub struct ActionContext {
902 pub compiler: CompilerIdentity,
904 pub working_dir: PathBuf,
906 pub path_mappings: Vec<PathMapping>,
908 pub environment: BTreeMap<String, Option<String>>,
910 pub portable_environment: BTreeSet<String>,
917 pub inputs: Vec<ActionInput>,
919}
920
921#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
931#[serde(deny_unknown_fields)]
932pub struct LinkerIdentity {
933 pub driver: String,
935 pub driver_version: String,
937 pub linker_version: String,
939 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
941 pub crt_objects: BTreeMap<String, CacheDigest>,
942 #[serde(skip_serializing_if = "Option::is_none", default)]
944 pub sdk: Option<String>,
945 #[serde(skip_serializing_if = "Option::is_none", default)]
947 pub deployment_target: Option<String>,
948}
949
950#[derive(Debug, Clone, PartialEq, Eq)]
952pub struct RustcAction {
953 pub digest: CacheDigest,
955 pub bytes: Vec<u8>,
957}
958
959#[derive(Debug, Clone, PartialEq, Eq)]
962pub struct RustcInputPrediction {
963 pub version: u8,
965 pub inputs: Vec<String>,
967 pub environment: Vec<String>,
969 pub compiler_duration_ns: u64,
972 pub crate_name: String,
974}
975
976#[derive(Serialize, Deserialize)]
977#[serde(deny_unknown_fields)]
978struct RustcInputPredictionWire {
979 version: u8,
980 inputs: Vec<String>,
981 environment: Vec<String>,
982 #[serde(default, skip_serializing_if = "is_zero")]
983 compiler_duration_ns: u64,
984 #[serde(default, skip_serializing_if = "String::is_empty")]
985 crate_name: String,
986}
987
988impl Serialize for RustcInputPrediction {
989 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
990 where
991 S: Serializer,
992 {
993 RustcInputPredictionWire {
994 version: self.version,
995 inputs: if self.version == 4 {
996 compact_prediction_inputs(&self.inputs)
997 } else {
998 self.inputs.clone()
999 },
1000 environment: self.environment.clone(),
1001 compiler_duration_ns: self.compiler_duration_ns,
1002 crate_name: self.crate_name.clone(),
1003 }
1004 .serialize(serializer)
1005 }
1006}
1007
1008impl<'de> Deserialize<'de> for RustcInputPrediction {
1009 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1010 where
1011 D: Deserializer<'de>,
1012 {
1013 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1014 let inputs = if wire.version == 4 {
1015 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1016 } else {
1017 wire.inputs
1018 };
1019 Ok(Self {
1020 version: wire.version,
1021 inputs,
1022 environment: wire.environment,
1023 compiler_duration_ns: wire.compiler_duration_ns,
1024 crate_name: wire.crate_name,
1025 })
1026 }
1027}
1028
1029fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1030 let mut previous = "";
1031 inputs
1032 .iter()
1033 .map(|input| {
1034 let mut shared = previous
1035 .bytes()
1036 .zip(input.bytes())
1037 .take_while(|(left, right)| left == right)
1038 .count();
1039 while !input.is_char_boundary(shared) {
1040 shared -= 1;
1041 }
1042 let compact = format!("{shared}:{}", &input[shared..]);
1043 previous = input;
1044 compact
1045 })
1046 .collect()
1047}
1048
1049fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1050 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1051 let mut decoded_bytes = 0_usize;
1052 for input in inputs {
1053 let (shared_text, suffix) = input
1054 .split_once(':')
1055 .ok_or("compact rustc prediction input has no prefix length")?;
1056 let shared: usize = shared_text
1057 .parse()
1058 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1059 if shared.to_string() != shared_text
1060 || shared > expanded.last().map_or(0, String::len)
1061 || expanded
1062 .last()
1063 .is_some_and(|previous| !previous.is_char_boundary(shared))
1064 {
1065 return Err("compact rustc prediction prefix is not canonical");
1066 }
1067 let mut path = expanded
1068 .last()
1069 .map_or_else(String::new, |previous| previous[..shared].to_string());
1070 path.push_str(suffix);
1071 decoded_bytes = decoded_bytes
1072 .checked_add(path.len())
1073 .ok_or("compact rustc prediction is too large")?;
1074 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1075 return Err("compact rustc prediction is too large");
1076 }
1077 expanded.push(path);
1078 }
1079 Ok(expanded)
1080}
1081
1082fn is_zero(value: &u64) -> bool {
1083 *value == 0
1084}
1085
1086impl RustcInputPrediction {
1087 pub fn discover(
1090 &self,
1091 working_dir: &Path,
1092 path_mappings: &[PathMapping],
1093 digests: &dyn FileDigestCache,
1094 ) -> Result<DiscoveredInputs, BypassReason> {
1095 if !matches!(self.version, 2..=4) {
1096 return Err(BypassReason::UnsupportedPrediction);
1097 }
1098 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1099 return Err(BypassReason::UnsupportedPrediction);
1100 }
1101 let mut paths = BTreeSet::new();
1102 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1103 let mut native_bytes = 0_u64;
1104 for path in &self.inputs {
1105 if self.version >= 3
1106 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1107 {
1108 let directory = denormalize_path(path, path_mappings)?;
1109 dep_info::collect_native_directory(
1110 &directory,
1111 &admitted_roots,
1112 &mut paths,
1113 &mut native_bytes,
1114 )?;
1115 } else {
1116 paths.insert(denormalize_path(path, path_mappings)?);
1117 }
1118 }
1119 let environment = self
1120 .environment
1121 .iter()
1122 .map(|name| {
1123 if name.is_empty() || name.contains(['=', '\0']) {
1124 return Err(BypassReason::UnsupportedPrediction);
1125 }
1126 let value = std::env::var_os(name)
1127 .map(|value| {
1128 value
1129 .into_string()
1130 .map_err(|_| BypassReason::UnsupportedPrediction)
1131 })
1132 .transpose()?;
1133 Ok((name.clone(), value))
1134 })
1135 .collect::<Result<BTreeMap<_, _>, _>>()?;
1136 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1137 }
1138}
1139
1140#[derive(Serialize)]
1141struct ActionDescriptor {
1142 version: u8,
1143 kind: &'static str,
1144 adapter_version: u8,
1145 compiler: CompilerDescriptor,
1146 arguments: Vec<String>,
1147 environment: BTreeMap<String, Option<String>>,
1148 inputs: Vec<InputDescriptor>,
1149 #[serde(skip_serializing_if = "Option::is_none")]
1152 linker: Option<LinkerIdentity>,
1153}
1154
1155#[derive(Serialize)]
1156struct InvocationDescriptor {
1157 version: u8,
1158 kind: &'static str,
1159 adapter_version: u8,
1160 compiler: CompilerDescriptor,
1161 arguments: Vec<String>,
1162 required_inputs: Vec<String>,
1163}
1164
1165#[derive(Serialize)]
1166struct CompilerDescriptor {
1167 toolchain: String,
1168 rustc_version: String,
1169 host: String,
1170 #[serde(skip_serializing_if = "Option::is_none")]
1171 driver: Option<String>,
1172}
1173
1174#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1175struct InputDescriptor {
1176 path: String,
1177 digest: CacheDigest,
1178}
1179
1180struct Parser<'a> {
1181 arguments: &'a [OsString],
1182 index: usize,
1183 parsed: Vec<Argument>,
1184 source: Option<PathBuf>,
1185 crate_types: Vec<String>,
1186 emits: Vec<Emit>,
1187 required_inputs: Vec<PathBuf>,
1188 test: bool,
1189 crate_name: Option<String>,
1190 extra_filename: String,
1191 out_dir: Option<PathBuf>,
1192 explicit_output: Option<PathBuf>,
1193 target: Option<String>,
1194 options: ParseOptions,
1195}
1196
1197struct ExpandedArguments {
1198 arguments: Vec<OsString>,
1199}
1200
1201#[derive(Default)]
1202struct ResponseExpander {
1203 shell_argfiles: bool,
1204 next_is_unstable_option: bool,
1205 arguments: Vec<OsString>,
1206}
1207
1208impl ResponseExpander {
1209 fn push(&mut self, argument: String) {
1210 if self.next_is_unstable_option {
1211 self.shell_argfiles |= argument == "shell-argfiles";
1212 self.next_is_unstable_option = false;
1213 } else if let Some(option) = argument.strip_prefix("-Z") {
1214 if option.is_empty() {
1215 self.next_is_unstable_option = true;
1216 } else {
1217 self.shell_argfiles |= option == "shell-argfiles";
1218 }
1219 }
1220 self.arguments.push(argument.into());
1221 }
1222}
1223
1224fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1227 let mut expanded = ResponseExpander::default();
1228 for (index, argument) in arguments.iter().enumerate() {
1229 let argument = argument
1230 .to_str()
1231 .ok_or(BypassReason::NonUtf8Argument { index })?;
1232 let Some(argfile) = argument.strip_prefix('@') else {
1233 expanded.push(argument.to_string());
1234 continue;
1235 };
1236 let (path, shell) = match argfile.split_once(':') {
1237 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1238 _ => (argfile, false),
1239 };
1240 let contents = std::fs::read_to_string(path).map_err(|error| {
1241 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1242 })?;
1243 if shell {
1244 let arguments = shlex::split(&contents).ok_or_else(|| {
1245 BypassReason::ResponseFile(format!(
1246 "invalid shell-style arguments in {}",
1247 Path::new(path).display()
1248 ))
1249 })?;
1250 for argument in arguments {
1251 expanded.push(argument);
1252 }
1253 } else {
1254 for argument in contents.lines() {
1255 expanded.push(argument.to_string());
1256 }
1257 }
1258 }
1259 Ok(ExpandedArguments {
1260 arguments: expanded.arguments,
1261 })
1262}
1263
1264impl<'a> Parser<'a> {
1265 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1266 Self {
1267 arguments,
1268 options,
1269 index: 0,
1270 parsed: Vec::new(),
1271 source: None,
1272 crate_types: Vec::new(),
1273 emits: Vec::new(),
1274 required_inputs: Vec::new(),
1275 test: false,
1276 crate_name: None,
1277 extra_filename: String::new(),
1278 out_dir: None,
1279 explicit_output: None,
1280 target: None,
1281 }
1282 }
1283
1284 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1285 while self.index < self.arguments.len() {
1286 let value = self.current()?.to_string();
1287 self.index += 1;
1288 if let Some(long) = value.strip_prefix("--") {
1289 self.parse_long(long)?;
1290 } else if value.starts_with('-') && value != "-" {
1291 self.parse_short(&value)?;
1292 } else {
1293 self.parse_input(&value)?;
1294 }
1295 }
1296
1297 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1298 let link_output = self.classify()?;
1299 let crate_name = self.crate_name.clone().map_or_else(
1300 || {
1301 source
1302 .file_stem()
1303 .and_then(|name| name.to_str())
1304 .map(|name| name.replace('-', "_"))
1305 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1306 },
1307 Ok,
1308 )?;
1309 self.required_inputs.push(source.clone());
1310 Ok(RustcInvocation {
1311 arguments: self.parsed,
1312 source,
1313 required_inputs: self.required_inputs,
1314 crate_name,
1315 extra_filename: self.extra_filename,
1316 out_dir: self.out_dir,
1317 explicit_output: self.explicit_output,
1318 emits: self.emits,
1319 target: self.target,
1320 link_output,
1321 })
1322 }
1323
1324 fn current(&self) -> Result<&str, BypassReason> {
1325 self.arguments[self.index]
1326 .to_str()
1327 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1328 }
1329
1330 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1331 if let Some(value) = inline {
1332 if value.is_empty() {
1333 return Err(BypassReason::MissingValue(flag.into()));
1334 }
1335 return Ok(value.into());
1336 }
1337 if self.index >= self.arguments.len() {
1338 return Err(BypassReason::MissingValue(flag.into()));
1339 }
1340 let value = self.current()?.to_string();
1341 self.index += 1;
1342 Ok(value)
1343 }
1344
1345 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1346 let (flag, inline) = value
1347 .split_once('=')
1348 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1349 let rendered_flag = format!("--{flag}");
1350 match flag {
1351 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1352 "test" => {
1353 self.test = true;
1354 self.parsed.push(Argument::Plain(rendered_flag));
1355 Ok(())
1356 }
1357 "verbose" => {
1358 self.parsed.push(Argument::Plain(rendered_flag));
1359 Ok(())
1360 }
1361 "crate-name" => {
1362 let value = self.take_value(&rendered_flag, inline)?;
1363 self.crate_name = Some(value.clone());
1364 self.parsed
1365 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1366 Ok(())
1367 }
1368 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1369 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1370 | "deny" | "forbid" | "cap-lints" => {
1371 let value = self.take_value(&rendered_flag, inline)?;
1372 self.parsed
1373 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1374 Ok(())
1375 }
1376 "target" => {
1377 let value = self.take_value(&rendered_flag, inline)?;
1378 self.target = Some(value.clone());
1379 if value.ends_with(".json") || value.contains(['/', '\\']) {
1380 let path = PathBuf::from(value);
1381 self.required_inputs.push(path.clone());
1382 self.parsed.push(Argument::Path {
1383 flag: rendered_flag,
1384 path,
1385 });
1386 } else {
1387 self.target = Some(value.clone());
1388 self.parsed
1389 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1390 }
1391 Ok(())
1392 }
1393 "crate-type" => {
1394 let value = self.take_value(&rendered_flag, inline)?;
1395 self.crate_types
1396 .extend(value.split(',').map(ToOwned::to_owned));
1397 self.parsed
1398 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1399 Ok(())
1400 }
1401 "emit" => {
1402 let value = self.take_value(&rendered_flag, inline)?;
1403 let emits = parse_emits(&value);
1404 self.emits.extend(emits.clone());
1405 self.parsed.push(Argument::Emit(emits));
1406 Ok(())
1407 }
1408 "out-dir" => {
1409 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1410 self.out_dir = Some(path.clone());
1411 self.parsed.push(Argument::Path {
1412 flag: rendered_flag,
1413 path,
1414 });
1415 Ok(())
1416 }
1417 "sysroot" => {
1418 let path = self.take_value(&rendered_flag, inline)?;
1419 self.parsed.push(Argument::Path {
1420 flag: rendered_flag,
1421 path: path.into(),
1422 });
1423 Ok(())
1424 }
1425 "extern" => {
1426 let value = self.take_value(&rendered_flag, inline)?;
1427 let (name, path) = value
1428 .split_once('=')
1429 .map_or((value.as_str(), None), |(name, path)| {
1430 (name, Some(PathBuf::from(path)))
1431 });
1432 if let Some(path) = &path {
1433 self.required_inputs.push(path.clone());
1434 }
1435 self.parsed.push(Argument::Extern {
1436 name: name.into(),
1437 path,
1438 });
1439 Ok(())
1440 }
1441 "remap-path-prefix" => {
1442 let value = self.take_value(&rendered_flag, inline)?;
1443 let Some((from, to)) = value.split_once('=') else {
1444 return Err(BypassReason::MissingValue(rendered_flag));
1445 };
1446 self.parsed.push(Argument::RemapPath {
1447 from: from.into(),
1448 to: to.into(),
1449 });
1450 Ok(())
1451 }
1452 "codegen" => {
1453 let value = self.take_value(&rendered_flag, inline)?;
1454 self.parse_codegen(&value)
1455 }
1456 "jobs-frontend" => {
1457 let value = self.take_value(&rendered_flag, inline)?;
1458 self.parsed
1459 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1460 Ok(())
1461 }
1462 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1463 }
1464 }
1465
1466 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1467 if let Some(attached) = value.strip_prefix("-Z") {
1468 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1469 match option.as_str() {
1470 "embed-metadata=no" | "embed-metadata=yes" | "shell-argfiles"
1471 | "unstable-options" => {
1472 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1473 return Ok(());
1474 }
1475 "threads" | "threads=" => {
1476 return Err(BypassReason::MissingValue("-Zthreads".into()));
1477 }
1478 _ => {}
1479 }
1480 if option.starts_with("threads=") {
1481 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1482 return Ok(());
1483 }
1484 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1485 }
1486 match value {
1487 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1490 "-g" | "-O" | "-v" => {
1491 self.parsed.push(Argument::Plain(value.into()));
1492 return Ok(());
1493 }
1494 _ => {}
1495 }
1496 for (short, long) in [
1497 ("-A", "--allow"),
1498 ("-W", "--warn"),
1499 ("-D", "--deny"),
1500 ("-F", "--forbid"),
1501 ] {
1502 if let Some(attached) = value.strip_prefix(short) {
1503 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1504 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1505 return Ok(());
1506 }
1507 }
1508 if let Some(attached) = value.strip_prefix("-C") {
1509 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1510 return self.parse_codegen(&option);
1511 }
1512 if let Some(attached) = value.strip_prefix("-L") {
1513 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1514 let (kind, path) = search
1515 .split_once('=')
1516 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1517 if !matches!(kind, "dependency" | "native") {
1518 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1519 }
1520 self.parsed.push(Argument::SearchPath {
1521 kind: kind.into(),
1522 path: path.into(),
1523 });
1524 return Ok(());
1525 }
1526 if value == "-l" || value.starts_with("-l") {
1527 return Err(BypassReason::NativeLibrary);
1528 }
1529 if let Some(attached) = value.strip_prefix("-o") {
1530 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1531 self.explicit_output = Some(path.clone().into());
1532 self.parsed.push(Argument::Path {
1533 flag: "-o".into(),
1534 path: path.into(),
1535 });
1536 return Ok(());
1537 }
1538 Err(BypassReason::UnknownFlag(value.into()))
1539 }
1540
1541 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1542 let name = value.split_once('=').map_or(value, |(name, _)| name);
1543 if name == "incremental" {
1544 return Err(BypassReason::Incremental);
1545 }
1546 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1547 return Err(BypassReason::UnknownCodegenOption(name.into()));
1548 }
1549 if name == "linker-plugin-lto"
1555 && let Some((_, selection)) = value.split_once('=')
1556 && !matches!(
1557 selection,
1558 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1559 )
1560 {
1561 return Err(BypassReason::UnknownCodegenOption(format!(
1562 "linker-plugin-lto={selection}"
1563 )));
1564 }
1565 if name == "link-arg"
1572 && let Some((_, option)) = value.split_once('=')
1573 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1574 && !selection.is_empty()
1575 && !selection.contains(',')
1576 && !self.parsed.iter().any(
1577 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1578 )
1579 {
1580 if !self.parsed.iter().any(
1581 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1582 ) {
1583 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1584 }
1585 return Ok(());
1586 }
1587 if name == "link-arg"
1590 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1591 && safe_install_name(option)
1592 {
1593 self.parsed.push(Argument::InstallName(option.to_owned()));
1594 return Ok(());
1595 }
1596 if matches!(name, "link-arg" | "link-args")
1601 && let Some((_, option)) = value.split_once('=')
1602 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1603 && !prefix.trim_end_matches('/').is_empty()
1604 && !prefix.contains(',')
1605 {
1606 let trailing_slash = prefix.ends_with('/');
1607 self.parsed.push(Argument::OsoPrefix {
1608 path: PathBuf::from(prefix.trim_end_matches('/')),
1609 trailing_slash,
1610 });
1611 return Ok(());
1612 }
1613 self.parsed
1614 .push(Argument::Plain(format!("--codegen={value}")));
1615 if name == "extra-filename" {
1616 self.extra_filename = value
1617 .split_once('=')
1618 .map_or(String::new(), |(_, value)| value.to_string());
1619 }
1620 Ok(())
1621 }
1622
1623 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1624 if value == "-" {
1625 return Err(BypassReason::StandardInput);
1626 }
1627 if self.source.replace(value.into()).is_some() {
1628 return Err(BypassReason::MultipleInputs);
1629 }
1630 Ok(())
1631 }
1632
1633 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1634 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1643 let builds_a_library = !self.test
1644 && !self.crate_types.is_empty()
1645 && self
1646 .crate_types
1647 .iter()
1648 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1649 let link_output = if never_links || builds_a_library {
1650 LinkOutput::Library
1651 } else if self
1652 .target
1653 .as_deref()
1654 .is_some_and(compiler_bundled_wasm_target)
1655 && ((self.test && self.crate_types.is_empty())
1656 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1657 {
1658 if self.parsed.iter().any(|argument| match argument {
1659 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1660 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1661 !matches!(
1662 value.rsplit_once('=').map(|(_, value)| value),
1663 Some("y" | "yes" | "on" | "true")
1664 )
1665 }
1666 _ => false,
1667 }) {
1668 return Err(BypassReason::UnknownCodegenOption(
1669 "link-self-contained".into(),
1670 ));
1671 }
1672 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1673 && self.parsed.iter().any(|argument| {
1674 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1675 })
1676 {
1677 return Err(BypassReason::UnknownCodegenOption(
1678 "target-feature=-crt-static".into(),
1679 ));
1680 }
1681 LinkOutput::WasmExecutable
1685 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1686 self.check_native_link_is_portable()?;
1687 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1688 LinkOutput::NativeProcMacro
1689 } else {
1690 LinkOutput::NativeExecutable
1691 }
1692 } else if self.test {
1693 return Err(BypassReason::UnsupportedCrateType("test".into()));
1694 } else {
1695 return Err(BypassReason::UnsupportedCrateType(
1696 self.crate_types
1697 .iter()
1698 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1699 .cloned()
1700 .unwrap_or_else(|| "bin".into()),
1701 ));
1702 };
1703 if link_output != LinkOutput::Library
1712 && let Some(option) = self.first_link_argument()
1713 {
1714 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1715 }
1716 if self
1717 .parsed
1718 .iter()
1719 .any(|arg| matches!(arg, Argument::InstallName(_)))
1720 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
1721 {
1722 return Err(BypassReason::UnmodeledLinkArgument(
1723 "link-arg=-Wl,-install_name".into(),
1724 ));
1725 }
1726 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1733 let Argument::FuseLd(selection) = argument else {
1734 return None;
1735 };
1736 Some(selection.as_str())
1737 }) && !matches!(
1738 link_output,
1739 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1740 ) {
1741 return Err(BypassReason::UnmodeledLinkArgument(format!(
1742 "link-arg=-fuse-ld={selection}"
1743 )));
1744 }
1745 if self.parsed.iter().any(|argument| {
1746 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1747 }) && !matches!(
1748 link_output,
1749 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1750 ) {
1751 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1752 }
1753 if !matches!(
1757 link_output,
1758 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1759 ) && self
1760 .parsed
1761 .iter()
1762 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1763 {
1764 return Err(BypassReason::UnmodeledLinkArgument(
1765 "link-arg=-Wl,-oso_prefix".into(),
1766 ));
1767 }
1768 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1769 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1770 _ => None,
1771 }) {
1772 return Err(BypassReason::UnresolvedExtern(name.clone()));
1773 }
1774 if let Some(emit) = self
1775 .emits
1776 .iter()
1777 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1778 {
1779 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1780 }
1781 if !self
1782 .emits
1783 .iter()
1784 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1785 {
1786 return Err(BypassReason::NoCacheableOutput);
1787 }
1788 Ok(link_output)
1789 }
1790}
1791
1792impl Parser<'_> {
1793 fn first_link_argument(&self) -> Option<&str> {
1796 self.parsed.iter().find_map(|argument| {
1797 let Argument::Plain(value) = argument else {
1798 return None;
1799 };
1800 let option = value.strip_prefix("--codegen=")?;
1801 let name = option.split_once('=').map_or(option, |(name, _)| name);
1802 matches!(name, "link-arg" | "link-args").then_some(option)
1803 })
1804 }
1805
1806 fn oso_prefix_covers_outputs(&self) -> bool {
1815 let Some(directory) = self
1816 .out_dir
1817 .as_deref()
1818 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
1819 else {
1820 return false;
1821 };
1822 if !directory.is_absolute() {
1823 return false;
1824 }
1825 let directory = normalize_components(directory);
1826 self.parsed.iter().any(|argument| {
1827 let Argument::OsoPrefix { path, .. } = argument else {
1828 return false;
1829 };
1830 path.is_absolute()
1831 && (directory.starts_with(normalize_components(path))
1832 || std::fs::canonicalize(&directory)
1833 .ok()
1834 .zip(std::fs::canonicalize(path).ok())
1835 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
1836 })
1837 }
1838
1839 fn links_a_native_artifact(&self) -> bool {
1846 self.target.is_none()
1847 && self.emits.iter().any(|emit| emit.kind == "link")
1852 && ((self.test && self.crate_types.is_empty())
1853 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro")))
1854 }
1855
1856 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
1863 for argument in &self.parsed {
1864 let Argument::Plain(value) = argument else {
1865 continue;
1866 };
1867 let (name, value) = if value == "-g" {
1870 ("debuginfo", Some("2"))
1871 } else if let Some(option) = value.strip_prefix("--codegen=") {
1872 match option.split_once('=') {
1873 Some((name, value)) => (name, Some(value)),
1874 None => (option, None),
1878 }
1879 } else {
1880 continue;
1881 };
1882 let unportable = match name {
1883 "split-debuginfo" => match value {
1889 Some("off") => false,
1890 Some("unpacked") if cfg!(target_os = "macos") => {
1891 !self.oso_prefix_covers_outputs()
1892 }
1893 _ => true,
1894 },
1895 "debuginfo" if cfg!(target_os = "macos") => {
1904 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
1905 }
1906 "rpath" => is_enabled(value),
1914 "prefer-dynamic" => {
1915 is_enabled(value)
1916 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
1917 }
1918 "link-self-contained" => true,
1921 _ => false,
1922 };
1923 if unportable {
1924 return Err(BypassReason::UnportableNativeLink(match value {
1925 Some(value) => format!("{name}={value}"),
1926 None => name.to_owned(),
1927 }));
1928 }
1929 }
1930 Ok(())
1931 }
1932}
1933
1934fn safe_install_name(name: &str) -> bool {
1935 !name.is_empty()
1936 && name
1937 .bytes()
1938 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
1939}
1940
1941fn is_enabled(value: Option<&str>) -> bool {
1944 matches!(value, None | Some("y" | "yes" | "on" | "true"))
1945}
1946
1947fn compiler_bundled_wasm_target(target: &str) -> bool {
1948 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
1949}
1950
1951fn parse_emits(value: &str) -> Vec<Emit> {
1952 value
1953 .split(',')
1954 .map(|emit| {
1955 let (kind, path) = emit
1956 .split_once('=')
1957 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
1958 Emit {
1959 kind: kind.into(),
1960 path,
1961 }
1962 })
1963 .collect()
1964}
1965
1966struct ActionBuilder<'a> {
1967 invocation: &'a RustcInvocation,
1968 context: ActionContext,
1969 mappings: Vec<SharedPathMapping>,
1970 aliases: PathAliases,
1974 linker: Option<LinkerIdentity>,
1975}
1976
1977impl<'a> ActionBuilder<'a> {
1978 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
1979 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1980 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
1981 Self {
1982 linker: None,
1983 invocation,
1984 mappings,
1985 aliases: PathAliases::default(),
1986 context,
1987 }
1988 }
1989
1990 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
1991 self.linker = linker;
1992 self
1993 }
1994
1995 fn build(self) -> Result<RustcAction, BypassReason> {
1996 self.validate_mappings()?;
1997 let invocation = self.invocation_descriptor()?;
1998 let environment = self.environment_descriptor()?;
1999
2000 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2001 for input in &self.context.inputs {
2002 input
2003 .digest
2004 .validate()
2005 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2006 let path = self.normalize_path(&input.path)?;
2007 if inputs
2008 .insert(path.clone(), input.digest.clone())
2009 .is_some_and(|existing| existing != input.digest)
2010 {
2011 return Err(BypassReason::ConflictingInput(path));
2012 }
2013 }
2014 let required = self
2015 .invocation
2016 .required_inputs
2017 .iter()
2018 .map(|path| self.normalize_path(path))
2019 .collect::<Result<BTreeSet<_>, _>>()?;
2020 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2021 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2022 }
2023 let inputs = inputs
2024 .into_iter()
2025 .map(|(path, digest)| InputDescriptor { path, digest })
2026 .collect();
2027 if self.invocation.links_natively() && self.linker.is_none() {
2030 return Err(BypassReason::UnportableNativeLink(
2031 "linker identity is unknown".into(),
2032 ));
2033 }
2034 let descriptor = ActionDescriptor {
2035 version: ACTION_SCHEMA_VERSION,
2036 kind: "rustc",
2037 adapter_version: ADAPTER_VERSION,
2038 compiler: invocation.compiler,
2039 arguments: invocation.arguments,
2040 environment,
2041 inputs,
2042 linker: self.linker.clone(),
2043 };
2044 let bytes = canonical_json(&descriptor)
2045 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2046 let digest = CacheDigest::blake3(&bytes);
2047 Ok(RustcAction { digest, bytes })
2048 }
2049
2050 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2051 self.validate_mappings()?;
2052 let arguments = self
2053 .invocation
2054 .arguments
2055 .iter()
2056 .map(|argument| self.normalize_argument(argument))
2057 .collect::<Result<Vec<_>, _>>()?;
2058 let required_inputs = self
2059 .invocation
2060 .required_inputs
2061 .iter()
2062 .map(|path| self.normalize_path(path))
2063 .collect::<Result<BTreeSet<_>, _>>()?
2064 .into_iter()
2065 .collect();
2066 Ok(InvocationDescriptor {
2067 version: ACTION_SCHEMA_VERSION,
2068 kind: "rustc",
2069 adapter_version: ADAPTER_VERSION,
2070 compiler: CompilerDescriptor {
2071 toolchain: self.context.compiler.toolchain.clone(),
2072 rustc_version: self.context.compiler.rustc_version.clone(),
2073 host: self.context.compiler.host.clone(),
2074 driver: self.context.compiler.driver.clone(),
2075 },
2076 arguments,
2077 required_inputs,
2078 })
2079 }
2080
2081 fn validate_mappings(&self) -> Result<(), BypassReason> {
2082 if !self.context.working_dir.is_absolute() {
2083 return Err(BypassReason::RelativeWorkingDirectory(
2084 self.context.working_dir.clone(),
2085 ));
2086 }
2087 let mut roots = BTreeSet::new();
2088 let mut placeholders = BTreeSet::new();
2089 for mapping in &self.mappings {
2090 if !mapping.root.is_absolute() {
2091 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2092 }
2093 if mapping.placeholder.is_empty()
2094 || !mapping
2095 .placeholder
2096 .bytes()
2097 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2098 || !roots.insert(normalize_components(&mapping.root))
2099 || !placeholders.insert(&mapping.placeholder)
2100 {
2101 return Err(BypassReason::InvalidPathPlaceholder(
2102 mapping.placeholder.clone(),
2103 ));
2104 }
2105 }
2106 Ok(())
2107 }
2108
2109 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2110 match argument {
2111 Argument::Plain(value) => Ok(value.clone()),
2112 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2113 Argument::SearchPath { kind, path } => {
2114 let text = match self.normalize_path(path) {
2115 Ok(text) => text,
2116 Err(BypassReason::UnmappedAbsolutePath(absolute))
2124 if kind == "native" && self.invocation.native_search_is_inert() =>
2125 {
2126 absolute
2127 .to_str()
2128 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
2129 .to_string()
2130 }
2131 Err(error) => return Err(error),
2132 };
2133 Ok(format!("-L{kind}={text}"))
2134 }
2135 Argument::Extern { name, path } => match path {
2136 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2137 None => Ok(format!("--extern={name}")),
2138 },
2139 Argument::Emit(emits) => Ok(format!(
2140 "--emit={}",
2141 emits
2142 .iter()
2143 .map(|emit| match &emit.path {
2144 Some(path) => self
2145 .normalize_path(path)
2146 .map(|path| format!("{}={path}", emit.kind)),
2147 None => Ok(emit.kind.clone()),
2148 })
2149 .collect::<Result<Vec<_>, _>>()?
2150 .join(",")
2151 )),
2152 Argument::RemapPath { from, to } => Ok(format!(
2153 "--remap-path-prefix={}={}",
2154 self.normalize_path(from)?,
2155 to
2156 )),
2157 Argument::InstallName(name) => Ok(format!(
2158 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2159 )),
2160 Argument::OsoPrefix {
2161 path,
2162 trailing_slash,
2163 } => Ok(format!(
2164 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2165 self.normalize_path(path)?,
2166 if *trailing_slash { "/" } else { "" }
2167 )),
2168 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2169 }
2170 }
2171
2172 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2179 self.context
2180 .environment
2181 .iter()
2182 .map(|(name, value)| {
2183 let value = match value {
2184 Some(value) if self.context.portable_environment.contains(name) => {
2185 Some(self.normalize_path(Path::new(value))?)
2186 }
2187 value => value.clone(),
2188 };
2189 Ok((name.clone(), value))
2190 })
2191 .collect()
2192 }
2193
2194 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2195 normalize_resolved_shared_path_with(
2196 &self.aliases,
2197 path,
2198 &self.context.working_dir,
2199 &self.mappings,
2200 )
2201 .map_err(Into::into)
2202 }
2203}
2204
2205fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2209 directories.iter().any(|directory| {
2210 path.len() > directory.len()
2211 && path.as_bytes()[directory.len()] == b'/'
2212 && path.starts_with(directory)
2213 })
2214}
2215
2216fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2217 for mapping in mappings {
2218 let prefix = format!("${{{}}}", mapping.placeholder);
2219 let suffix = if value == prefix {
2220 ""
2221 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2222 suffix
2223 } else {
2224 continue;
2225 };
2226 if !mapping.root.is_absolute()
2227 || (!suffix.is_empty()
2228 && suffix.split('/').any(|component| {
2229 component.is_empty()
2230 || matches!(component, "." | "..")
2231 || component.contains('\\')
2232 }))
2233 {
2234 return Err(BypassReason::InvalidPredictedInput(value.into()));
2235 }
2236 let mut path = normalize_components(&mapping.root);
2237 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2238 return Ok(path);
2239 }
2240 Err(BypassReason::InvalidPredictedInput(value.into()))
2241}
2242
2243fn normalize_components(path: &Path) -> PathBuf {
2244 let mut normalized = PathBuf::new();
2245 for component in path.components() {
2246 match component {
2247 Component::CurDir => {}
2248 Component::ParentDir => {
2249 normalized.pop();
2250 }
2251 component => normalized.push(component.as_os_str()),
2252 }
2253 }
2254 normalized
2255}
2256
2257fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2258 if path.is_absolute() {
2259 normalize_components(path)
2260 } else {
2261 normalize_components(&working_dir.join(path))
2262 }
2263}
2264
2265#[cfg(test)]
2266#[path = "rustc_cache_tests.rs"]
2267mod tests;