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 on a linked output is not cacheable yet: {0}")]
230 NativeLibrary(String),
231 #[error("native static library was not found on any search path: {0}")]
234 MissingNativeLibrary(String),
235 #[error("static library file names are not known for a custom target: {0}")]
238 CustomTargetNativeLibrary(String),
239 #[error("rustc output name does not distinguish a program from a library: {0}")]
241 AmbiguousOutputName(PathBuf),
242 #[error("native link is not reproducible across checkouts: {0}")]
244 UnportableNativeLink(String),
245 #[error("rustc link argument is not modeled by the cache adapter: {0}")]
247 UnmodeledLinkArgument(String),
248 #[error("rustc search path kind is not cacheable yet: {0}")]
250 UnsupportedSearchPath(String),
251 #[error("rustc extern does not identify an input artifact: {0}")]
253 UnresolvedExtern(String),
254 #[error("absolute path has no stable cache mapping: {0}")]
256 UnmappedAbsolutePath(PathBuf),
257 #[error("cache key paths must be valid UTF-8: {0}")]
259 NonUtf8Path(PathBuf),
260 #[error("cache action working directory must be absolute: {0}")]
262 RelativeWorkingDirectory(PathBuf),
263 #[error("cache path mapping must use an absolute root: {0}")]
265 RelativePathMapping(PathBuf),
266 #[error("cache path mapping placeholder is invalid: {0}")]
268 InvalidPathPlaceholder(String),
269 #[error("required compiler input was not provided: {0}")]
271 MissingRequiredInput(String),
272 #[error("compiler input has an invalid digest: {0}")]
274 InvalidInputDigest(String),
275 #[error("compiler input appears more than once with different content: {0}")]
277 ConflictingInput(String),
278 #[error("rustc dep-info is malformed: {0}")]
280 MalformedDepInfo(String),
281 #[error("failed to read rustc dep-info {path}: {message}")]
283 DepInfoRead {
284 path: PathBuf,
286 message: String,
288 },
289 #[error("rustc dep-info output path must be absolute: {0}")]
291 RelativeDepInfoPath(PathBuf),
292 #[error("rustc dep-info output path cannot contain a comma: {0}")]
294 UnsafeDepInfoPath(PathBuf),
295 #[error("failed to read compiler input {path}: {message}")]
297 InputRead {
298 path: PathBuf,
300 message: String,
302 },
303 #[error("compiler input changed after discovery: {0}")]
305 InputChanged(PathBuf),
306 #[error("compiler input was modified during compilation: {0}")]
308 InputModifiedDuringCompilation(PathBuf),
309 #[error("discovered inputs were collected from a different working directory")]
311 DiscoveryWorkingDirectory,
312 #[error("compiler environment input has conflicting values: {0}")]
314 ConflictingEnvironment(String),
315 #[error("failed to serialize the rustc action: {0}")]
317 Serialization(String),
318 #[error("rustc action prediction is unsupported")]
320 UnsupportedPrediction,
321 #[error("rustc action prediction contains an invalid input path: {0}")]
323 InvalidPredictedInput(String),
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
327enum Argument {
328 Plain(String),
329 Path {
330 flag: String,
331 path: PathBuf,
332 },
333 SearchPath {
334 kind: String,
335 path: PathBuf,
336 },
337 Extern {
338 name: String,
339 path: Option<PathBuf>,
340 },
341 Emit(Vec<Emit>),
342 RemapPath {
343 from: PathBuf,
344 to: String,
345 },
346 InstallName(String),
348 OsoPrefix {
351 path: PathBuf,
352 trailing_slash: bool,
353 },
354 FuseLd(String),
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361struct Emit {
362 kind: String,
363 path: Option<PathBuf>,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct RustcInvocation {
372 arguments: Vec<Argument>,
373 source: PathBuf,
374 required_inputs: Vec<PathBuf>,
375 crate_name: String,
376 extra_filename: String,
377 out_dir: Option<PathBuf>,
378 explicit_output: Option<PathBuf>,
379 emits: Vec<Emit>,
380 target: Option<String>,
381 link_output: LinkOutput,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385enum LinkOutput {
386 Library,
387 WasmExecutable,
388 NativeExecutable,
389 NativeProcMacro,
390 NativeSharedLibrary,
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
403#[non_exhaustive]
404pub struct ParseOptions {
405 pub cache_native_links: bool,
408 pub custom_target_search: bool,
414}
415
416impl ParseOptions {
417 pub fn caching_native_links(enabled: bool) -> Self {
419 Self {
420 cache_native_links: enabled,
421 ..Self::default()
422 }
423 }
424
425 pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
428 self.custom_target_search = enabled;
429 self
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct RustcOutputs {
436 pub directory: PathBuf,
438 pub files: Vec<PathBuf>,
440 pub dep_info: PathBuf,
442}
443
444impl RustcInvocation {
445 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
454 Self::parse_with(arguments, ParseOptions::default())
455 }
456
457 pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
462 Ok(expand_response_files(arguments)?.arguments)
463 }
464
465 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
468 let expanded = expand_response_files(arguments)?;
469 Parser::new(&expanded.arguments, options).parse()
470 }
471
472 pub fn links_natively(&self) -> bool {
475 matches!(
476 self.link_output,
477 LinkOutput::NativeExecutable
478 | LinkOutput::NativeProcMacro
479 | LinkOutput::NativeSharedLibrary
480 )
481 }
482
483 pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
487 if !cfg!(target_os = "macos")
488 || self.link_output != LinkOutput::NativeProcMacro
489 || self
490 .arguments
491 .iter()
492 .any(|arg| matches!(arg, Argument::InstallName(_)))
493 {
494 return None;
495 }
496 let file = outputs
497 .files
498 .iter()
499 .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
500 let name = file.file_name()?.to_str()?;
501 if !safe_install_name(name) {
502 return None;
503 }
504 Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
505 }
506
507 pub fn linker_override(&self) -> Option<&Path> {
510 self.arguments.iter().find_map(|argument| {
511 let Argument::Plain(value) = argument else {
512 return None;
513 };
514 value.strip_prefix("--codegen=linker=").map(Path::new)
515 })
516 }
517
518 pub fn fuse_ld(&self) -> Option<&str> {
521 self.arguments.iter().find_map(|argument| {
522 let Argument::FuseLd(selection) = argument else {
523 return None;
524 };
525 Some(selection.as_str())
526 })
527 }
528
529 fn emits_windows_pdb(&self) -> bool {
530 if !cfg!(windows) || !self.links_natively() {
531 return false;
532 }
533 let mut enabled = false;
534 for argument in &self.arguments {
535 let Argument::Plain(value) = argument else {
536 continue;
537 };
538 if value == "-g" {
539 enabled = true;
540 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
541 enabled = !matches!(value, "0" | "none");
542 }
543 }
544 enabled
545 }
546
547 pub fn native_search_paths(&self) -> impl Iterator<Item = &Path> {
551 self.arguments.iter().filter_map(|argument| match argument {
552 Argument::SearchPath { kind, path } if kind == "native" => Some(path.as_path()),
553 _ => None,
554 })
555 }
556
557 pub fn source(&self) -> &Path {
559 &self.source
560 }
561
562 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
568 self.required_inputs
569 .iter()
570 .map(|path| absolute_path(path, working_dir))
571 .collect()
572 }
573
574 pub fn target(&self) -> Option<&str> {
576 self.target.as_deref()
577 }
578
579 pub fn crate_name(&self) -> &str {
581 &self.crate_name
582 }
583
584 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
597 let linked = self
598 .arguments
599 .iter()
600 .filter_map(|argument| match argument {
601 Argument::Extern {
602 path: Some(path), ..
603 } => Some(path.as_path()),
604 _ => None,
605 })
606 .collect::<BTreeSet<_>>();
607 let owned = discovered
608 .inputs
609 .iter()
610 .filter(|input| !linked.contains(input.path.as_path()))
611 .map(|input| (input.path.as_path(), &input.digest))
612 .collect::<BTreeMap<_, _>>();
613 let mut bytes = Vec::new();
614 for (path, digest) in owned {
615 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
616 bytes.push(0);
617 bytes.extend_from_slice(digest.key().as_bytes());
618 bytes.push(0);
619 }
620 CacheDigest::blake3(&bytes)
621 }
622
623 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
628 if !working_dir.is_absolute() {
629 return Err(BypassReason::RelativeWorkingDirectory(
630 working_dir.to_path_buf(),
631 ));
632 }
633 let explicit_output = self
634 .explicit_output
635 .as_deref()
636 .map(|path| absolute_path(path, working_dir));
637 let output_directory = explicit_output
638 .as_deref()
639 .and_then(Path::parent)
640 .map(Path::to_path_buf)
641 .or_else(|| {
642 self.out_dir
643 .as_deref()
644 .map(|path| absolute_path(path, working_dir))
645 })
646 .unwrap_or_else(|| normalize_components(working_dir));
647 if let Some(output) = &explicit_output
651 && self.emits.iter().any(|emit| {
652 emit.path.is_none()
653 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
654 })
655 {
656 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
657 }
658 let mut files = BTreeSet::new();
659 let mut dep_info = None;
660 for emit in &self.emits {
661 if emit.kind == "dep-info" {
662 let path = emit.path.as_ref().map_or_else(
663 || {
664 explicit_output.clone().map_or_else(
665 || {
666 output_directory
667 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
668 },
669 |path| path.with_extension("d"),
670 )
671 },
672 |path| absolute_path(path, working_dir),
673 );
674 if path.file_name().is_none() {
675 return Err(BypassReason::InvalidOutputPath(path));
676 }
677 dep_info = Some(path);
678 continue;
679 }
680 let (prefix, extension) = match emit.kind.as_str() {
681 "link" => match self.link_output {
682 LinkOutput::Library => ("lib", "rlib"),
683 LinkOutput::WasmExecutable => ("", "wasm"),
684 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
685 LinkOutput::NativeProcMacro | LinkOutput::NativeSharedLibrary => (
686 std::env::consts::DLL_PREFIX,
687 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
688 ),
689 },
690 "metadata" => ("lib", "rmeta"),
691 _ => continue,
692 };
693 let path = if let Some(path) = &emit.path {
694 absolute_path(path, working_dir)
695 } else {
696 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
697 output_directory.join(if extension.is_empty() {
698 name
699 } else {
700 format!("{name}.{extension}")
701 })
702 };
703 if path.file_name().is_none() {
704 return Err(BypassReason::InvalidOutputPath(path));
705 }
706 if path.parent() != Some(output_directory.as_path()) {
707 return Err(BypassReason::SplitOutputDirectories);
708 }
709 let has_library_extension = path
714 .extension()
715 .and_then(|extension| extension.to_str())
716 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
717 || (cfg!(windows)
718 && path
719 .file_stem()
720 .and_then(|stem| Path::new(stem).extension())
721 .and_then(|extension| extension.to_str())
722 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
723 if emit.kind == "link"
724 && !matches!(self.link_output, LinkOutput::Library)
725 && has_library_extension
726 {
727 return Err(BypassReason::AmbiguousOutputName(path));
728 }
729 if emit.kind == "link" && self.emits_windows_pdb() {
730 files.insert(path.with_extension("pdb"));
731 }
732 files.insert(path);
733 }
734 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
735 if dep_info.parent() != Some(output_directory.as_path()) {
736 return Err(BypassReason::SplitOutputDirectories);
737 }
738 Ok(RustcOutputs {
739 directory: output_directory,
740 files: files.into_iter().collect(),
741 dep_info,
742 })
743 }
744
745 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
751 self.action_linked_by(context, None)
752 }
753
754 pub fn action_linked_by(
763 &self,
764 context: ActionContext,
765 linker: Option<LinkerIdentity>,
766 ) -> Result<RustcAction, BypassReason> {
767 ActionBuilder::new(self, context).linked_by(linker).build()
768 }
769
770 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
772 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
773 let bytes = canonical_json(&descriptor)
774 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
775 Ok(CacheDigest::blake3(&bytes))
776 }
777
778 pub fn prediction(
781 &self,
782 context: &ActionContext,
783 discovered: &DiscoveredInputs,
784 ) -> Result<RustcInputPrediction, BypassReason> {
785 let builder = ActionBuilder::new(self, context.clone());
786 builder.validate_mappings()?;
787 let mut native_directories = BTreeSet::new();
788 for argument in &self.arguments {
789 if let Argument::SearchPath { kind, path } = argument
790 && kind == "native"
791 {
792 native_directories.insert(builder.normalize_path(path)?);
793 }
794 }
795 let mut inputs = BTreeSet::new();
802 for input in &discovered.inputs {
803 let normalized = builder.normalize_path(&input.path)?;
804 if !under_any_directory(&normalized, &native_directories) {
805 inputs.insert(normalized);
806 }
807 }
808 inputs.extend(
809 native_directories
810 .into_iter()
811 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
812 );
813 Ok(RustcInputPrediction {
814 version: 4,
815 inputs: inputs.into_iter().collect(),
816 environment: discovered.environment.keys().cloned().collect(),
817 compiler_duration_ns: 0,
818 crate_name: String::new(),
819 })
820 }
821}
822
823pub fn is_build_script_crate_name(crate_name: &str) -> bool {
827 crate_name
828 .strip_prefix("build_script_")
829 .is_some_and(|stem| !stem.is_empty())
830}
831
832impl RustcOutputs {
833 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
843 is_build_script_crate_name(crate_name)
844 .then(|| self.files.iter().find(|path| self.is_executable(path)))
845 .flatten()
846 .map(PathBuf::as_path)
847 }
848
849 pub fn is_executable(&self, path: &Path) -> bool {
857 self.files.iter().any(|output| output == path)
858 && !matches!(
859 path.extension().and_then(|extension| extension.to_str()),
860 Some("rlib" | "rmeta")
861 )
862 }
863}
864
865#[derive(Debug, Clone, PartialEq, Eq)]
866pub struct PathMapping {
868 pub root: PathBuf,
870 pub placeholder: String,
872}
873
874impl PathMapping {
875 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
877 Self {
878 root: root.into(),
879 placeholder: placeholder.into(),
880 }
881 }
882
883 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
886 let mut ordered = mappings.to_vec();
887 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
888 ordered
889 }
890}
891
892fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
893 mappings
894 .iter()
895 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
896 .collect()
897}
898
899pub fn normalize_mapped_path(
906 path: &Path,
907 working_dir: &Path,
908 mappings: &[PathMapping],
909) -> Result<String, BypassReason> {
910 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
911}
912
913#[derive(Debug, Clone, PartialEq, Eq)]
914pub struct CompilerIdentity {
916 pub toolchain: String,
918 pub rustc_version: String,
920 pub host: String,
922 pub driver: Option<String>,
924}
925
926#[derive(Debug, Clone, PartialEq, Eq)]
928pub struct ActionInput {
929 pub path: PathBuf,
931 pub digest: CacheDigest,
933}
934
935#[derive(Debug, Clone, PartialEq, Eq)]
937pub struct ActionContext {
938 pub compiler: CompilerIdentity,
940 pub working_dir: PathBuf,
942 pub path_mappings: Vec<PathMapping>,
944 pub environment: BTreeMap<String, Option<String>>,
946 pub portable_environment: BTreeSet<String>,
953 pub inputs: Vec<ActionInput>,
955}
956
957#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
967#[serde(deny_unknown_fields)]
968pub struct LinkerIdentity {
969 pub driver: String,
971 pub driver_version: String,
973 pub linker_version: String,
975 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
977 pub crt_objects: BTreeMap<String, CacheDigest>,
978 #[serde(skip_serializing_if = "Option::is_none", default)]
980 pub sdk: Option<String>,
981 #[serde(skip_serializing_if = "Option::is_none", default)]
983 pub deployment_target: Option<String>,
984}
985
986#[derive(Debug, Clone, PartialEq, Eq)]
988pub struct RustcAction {
989 pub digest: CacheDigest,
991 pub bytes: Vec<u8>,
993}
994
995#[derive(Debug, Clone, PartialEq, Eq)]
998pub struct RustcInputPrediction {
999 pub version: u8,
1001 pub inputs: Vec<String>,
1003 pub environment: Vec<String>,
1005 pub compiler_duration_ns: u64,
1008 pub crate_name: String,
1010}
1011
1012#[derive(Serialize, Deserialize)]
1013#[serde(deny_unknown_fields)]
1014struct RustcInputPredictionWire {
1015 version: u8,
1016 inputs: Vec<String>,
1017 environment: Vec<String>,
1018 #[serde(default, skip_serializing_if = "is_zero")]
1019 compiler_duration_ns: u64,
1020 #[serde(default, skip_serializing_if = "String::is_empty")]
1021 crate_name: String,
1022}
1023
1024impl Serialize for RustcInputPrediction {
1025 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1026 where
1027 S: Serializer,
1028 {
1029 RustcInputPredictionWire {
1030 version: self.version,
1031 inputs: if self.version == 4 {
1032 compact_prediction_inputs(&self.inputs)
1033 } else {
1034 self.inputs.clone()
1035 },
1036 environment: self.environment.clone(),
1037 compiler_duration_ns: self.compiler_duration_ns,
1038 crate_name: self.crate_name.clone(),
1039 }
1040 .serialize(serializer)
1041 }
1042}
1043
1044impl<'de> Deserialize<'de> for RustcInputPrediction {
1045 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1046 where
1047 D: Deserializer<'de>,
1048 {
1049 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1050 let inputs = if wire.version == 4 {
1051 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1052 } else {
1053 wire.inputs
1054 };
1055 Ok(Self {
1056 version: wire.version,
1057 inputs,
1058 environment: wire.environment,
1059 compiler_duration_ns: wire.compiler_duration_ns,
1060 crate_name: wire.crate_name,
1061 })
1062 }
1063}
1064
1065fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1066 let mut previous = "";
1067 inputs
1068 .iter()
1069 .map(|input| {
1070 let mut shared = previous
1071 .bytes()
1072 .zip(input.bytes())
1073 .take_while(|(left, right)| left == right)
1074 .count();
1075 while !input.is_char_boundary(shared) {
1076 shared -= 1;
1077 }
1078 let compact = format!("{shared}:{}", &input[shared..]);
1079 previous = input;
1080 compact
1081 })
1082 .collect()
1083}
1084
1085fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1086 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1087 let mut decoded_bytes = 0_usize;
1088 for input in inputs {
1089 let (shared_text, suffix) = input
1090 .split_once(':')
1091 .ok_or("compact rustc prediction input has no prefix length")?;
1092 let shared: usize = shared_text
1093 .parse()
1094 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1095 if shared.to_string() != shared_text
1096 || shared > expanded.last().map_or(0, String::len)
1097 || expanded
1098 .last()
1099 .is_some_and(|previous| !previous.is_char_boundary(shared))
1100 {
1101 return Err("compact rustc prediction prefix is not canonical");
1102 }
1103 let mut path = expanded
1104 .last()
1105 .map_or_else(String::new, |previous| previous[..shared].to_string());
1106 path.push_str(suffix);
1107 decoded_bytes = decoded_bytes
1108 .checked_add(path.len())
1109 .ok_or("compact rustc prediction is too large")?;
1110 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1111 return Err("compact rustc prediction is too large");
1112 }
1113 expanded.push(path);
1114 }
1115 Ok(expanded)
1116}
1117
1118fn is_zero(value: &u64) -> bool {
1119 *value == 0
1120}
1121
1122impl RustcInputPrediction {
1123 pub fn discover(
1126 &self,
1127 working_dir: &Path,
1128 path_mappings: &[PathMapping],
1129 digests: &dyn FileDigestCache,
1130 ) -> Result<DiscoveredInputs, BypassReason> {
1131 if !matches!(self.version, 2..=4) {
1132 return Err(BypassReason::UnsupportedPrediction);
1133 }
1134 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1135 return Err(BypassReason::UnsupportedPrediction);
1136 }
1137 let mut paths = BTreeSet::new();
1138 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1139 let mut native_bytes = 0_u64;
1140 for path in &self.inputs {
1141 if self.version >= 3
1142 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1143 {
1144 let directory = denormalize_path(path, path_mappings)?;
1145 dep_info::collect_native_directory(
1146 &directory,
1147 &admitted_roots,
1148 &mut paths,
1149 &mut native_bytes,
1150 )?;
1151 } else {
1152 paths.insert(denormalize_path(path, path_mappings)?);
1153 }
1154 }
1155 let environment = self
1156 .environment
1157 .iter()
1158 .map(|name| {
1159 if name.is_empty() || name.contains(['=', '\0']) {
1160 return Err(BypassReason::UnsupportedPrediction);
1161 }
1162 let value = std::env::var_os(name)
1163 .map(|value| {
1164 value
1165 .into_string()
1166 .map_err(|_| BypassReason::UnsupportedPrediction)
1167 })
1168 .transpose()?;
1169 Ok((name.clone(), value))
1170 })
1171 .collect::<Result<BTreeMap<_, _>, _>>()?;
1172 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1173 }
1174}
1175
1176#[derive(Serialize)]
1177struct ActionDescriptor {
1178 version: u8,
1179 kind: &'static str,
1180 adapter_version: u8,
1181 compiler: CompilerDescriptor,
1182 arguments: Vec<String>,
1183 environment: BTreeMap<String, Option<String>>,
1184 inputs: Vec<InputDescriptor>,
1185 #[serde(skip_serializing_if = "Option::is_none")]
1188 linker: Option<LinkerIdentity>,
1189}
1190
1191#[derive(Serialize)]
1192struct InvocationDescriptor {
1193 version: u8,
1194 kind: &'static str,
1195 adapter_version: u8,
1196 compiler: CompilerDescriptor,
1197 arguments: Vec<String>,
1198 required_inputs: Vec<String>,
1199}
1200
1201#[derive(Serialize)]
1202struct CompilerDescriptor {
1203 toolchain: String,
1204 rustc_version: String,
1205 host: String,
1206 #[serde(skip_serializing_if = "Option::is_none")]
1207 driver: Option<String>,
1208}
1209
1210#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1211struct InputDescriptor {
1212 path: String,
1213 digest: CacheDigest,
1214}
1215
1216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1218enum StaticLibraryNaming {
1219 Msvc,
1221 Unix,
1223}
1224
1225struct Parser<'a> {
1226 arguments: &'a [OsString],
1227 index: usize,
1228 parsed: Vec<Argument>,
1229 source: Option<PathBuf>,
1230 crate_types: Vec<String>,
1231 emits: Vec<Emit>,
1232 required_inputs: Vec<PathBuf>,
1233 native_libraries: Vec<String>,
1234 test: bool,
1235 crate_name: Option<String>,
1236 extra_filename: String,
1237 out_dir: Option<PathBuf>,
1238 explicit_output: Option<PathBuf>,
1239 target: Option<String>,
1240 sysroot_backend: Option<String>,
1241 options: ParseOptions,
1242}
1243
1244struct ExpandedArguments {
1245 arguments: Vec<OsString>,
1246}
1247
1248#[derive(Default)]
1249struct ResponseExpander {
1250 shell_argfiles: bool,
1251 next_is_unstable_option: bool,
1252 arguments: Vec<OsString>,
1253}
1254
1255impl ResponseExpander {
1256 fn push(&mut self, argument: String) {
1257 if self.next_is_unstable_option {
1258 self.shell_argfiles |= argument == "shell-argfiles";
1259 self.next_is_unstable_option = false;
1260 } else if let Some(option) = argument.strip_prefix("-Z") {
1261 if option.is_empty() {
1262 self.next_is_unstable_option = true;
1263 } else {
1264 self.shell_argfiles |= option == "shell-argfiles";
1265 }
1266 }
1267 self.arguments.push(argument.into());
1268 }
1269}
1270
1271fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1274 let mut expanded = ResponseExpander::default();
1275 for (index, argument) in arguments.iter().enumerate() {
1276 let argument = argument
1277 .to_str()
1278 .ok_or(BypassReason::NonUtf8Argument { index })?;
1279 let Some(argfile) = argument.strip_prefix('@') else {
1280 expanded.push(argument.to_string());
1281 continue;
1282 };
1283 let (path, shell) = match argfile.split_once(':') {
1284 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1285 _ => (argfile, false),
1286 };
1287 let contents = std::fs::read_to_string(path).map_err(|error| {
1288 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1289 })?;
1290 if shell {
1291 let arguments = shlex::split(&contents).ok_or_else(|| {
1292 BypassReason::ResponseFile(format!(
1293 "invalid shell-style arguments in {}",
1294 Path::new(path).display()
1295 ))
1296 })?;
1297 for argument in arguments {
1298 expanded.push(argument);
1299 }
1300 } else {
1301 for argument in contents.lines() {
1302 expanded.push(argument.to_string());
1303 }
1304 }
1305 }
1306 Ok(ExpandedArguments {
1307 arguments: expanded.arguments,
1308 })
1309}
1310
1311impl<'a> Parser<'a> {
1312 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1313 Self {
1314 arguments,
1315 options,
1316 index: 0,
1317 parsed: Vec::new(),
1318 source: None,
1319 crate_types: Vec::new(),
1320 emits: Vec::new(),
1321 required_inputs: Vec::new(),
1322 native_libraries: Vec::new(),
1323 test: false,
1324 crate_name: None,
1325 extra_filename: String::new(),
1326 out_dir: None,
1327 explicit_output: None,
1328 target: None,
1329 sysroot_backend: None,
1330 }
1331 }
1332
1333 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1334 while self.index < self.arguments.len() {
1335 let value = self.current()?.to_string();
1336 self.index += 1;
1337 if let Some(long) = value.strip_prefix("--") {
1338 self.parse_long(long)?;
1339 } else if value.starts_with('-') && value != "-" {
1340 self.parse_short(&value)?;
1341 } else {
1342 self.parse_input(&value)?;
1343 }
1344 }
1345
1346 if let Some(backend) = self.sysroot_backend.take()
1348 && self.parsed.iter().any(
1349 |argument| matches!(argument, Argument::Path { flag, .. } if flag == "--sysroot"),
1350 )
1351 {
1352 return Err(BypassReason::UnknownFlag(backend));
1353 }
1354 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1355 let link_output = self.classify()?;
1356 self.require_native_libraries(link_output)?;
1357 let crate_name = self.crate_name.clone().map_or_else(
1358 || {
1359 source
1360 .file_stem()
1361 .and_then(|name| name.to_str())
1362 .map(|name| name.replace('-', "_"))
1363 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1364 },
1365 Ok,
1366 )?;
1367 self.required_inputs.push(source.clone());
1368 Ok(RustcInvocation {
1369 arguments: self.parsed,
1370 source,
1371 required_inputs: self.required_inputs,
1372 crate_name,
1373 extra_filename: self.extra_filename,
1374 out_dir: self.out_dir,
1375 explicit_output: self.explicit_output,
1376 emits: self.emits,
1377 target: self.target,
1378 link_output,
1379 })
1380 }
1381
1382 fn current(&self) -> Result<&str, BypassReason> {
1383 self.arguments[self.index]
1384 .to_str()
1385 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1386 }
1387
1388 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1389 if let Some(value) = inline {
1390 if value.is_empty() {
1391 return Err(BypassReason::MissingValue(flag.into()));
1392 }
1393 return Ok(value.into());
1394 }
1395 if self.index >= self.arguments.len() {
1396 return Err(BypassReason::MissingValue(flag.into()));
1397 }
1398 let value = self.current()?.to_string();
1399 self.index += 1;
1400 Ok(value)
1401 }
1402
1403 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1404 let (flag, inline) = value
1405 .split_once('=')
1406 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1407 let rendered_flag = format!("--{flag}");
1408 match flag {
1409 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1410 "test" => {
1411 self.test = true;
1412 self.parsed.push(Argument::Plain(rendered_flag));
1413 Ok(())
1414 }
1415 "verbose" => {
1416 self.parsed.push(Argument::Plain(rendered_flag));
1417 Ok(())
1418 }
1419 "crate-name" => {
1420 let value = self.take_value(&rendered_flag, inline)?;
1421 self.crate_name = Some(value.clone());
1422 self.parsed
1423 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1424 Ok(())
1425 }
1426 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1427 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1428 | "deny" | "forbid" | "cap-lints" => {
1429 let value = self.take_value(&rendered_flag, inline)?;
1430 self.parsed
1431 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1432 Ok(())
1433 }
1434 "target" => {
1435 let value = self.take_value(&rendered_flag, inline)?;
1436 let effective = self.target.is_none();
1440 if effective {
1441 self.target = Some(value.clone());
1442 }
1443 if value.ends_with(".json") || value.contains(['/', '\\']) {
1444 let path = PathBuf::from(value);
1445 if effective {
1446 self.required_inputs.push(path.clone());
1447 }
1448 self.parsed.push(Argument::Path {
1449 flag: rendered_flag,
1450 path,
1451 });
1452 } else {
1453 self.parsed
1454 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1455 }
1456 Ok(())
1457 }
1458 "crate-type" => {
1459 let value = self.take_value(&rendered_flag, inline)?;
1460 self.crate_types
1461 .extend(value.split(',').map(ToOwned::to_owned));
1462 self.parsed
1463 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1464 Ok(())
1465 }
1466 "emit" => {
1467 let value = self.take_value(&rendered_flag, inline)?;
1468 let emits = parse_emits(&value);
1469 self.emits.extend(emits.clone());
1470 self.parsed.push(Argument::Emit(emits));
1471 Ok(())
1472 }
1473 "out-dir" => {
1474 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1475 self.out_dir = Some(path.clone());
1476 self.parsed.push(Argument::Path {
1477 flag: rendered_flag,
1478 path,
1479 });
1480 Ok(())
1481 }
1482 "sysroot" => {
1483 let path = self.take_value(&rendered_flag, inline)?;
1484 self.parsed.push(Argument::Path {
1485 flag: rendered_flag,
1486 path: path.into(),
1487 });
1488 Ok(())
1489 }
1490 "extern" => {
1491 let value = self.take_value(&rendered_flag, inline)?;
1492 let (name, path) = value
1493 .split_once('=')
1494 .map_or((value.as_str(), None), |(name, path)| {
1495 (name, Some(PathBuf::from(path)))
1496 });
1497 if let Some(path) = &path {
1498 self.required_inputs.push(path.clone());
1499 }
1500 self.parsed.push(Argument::Extern {
1501 name: name.into(),
1502 path,
1503 });
1504 Ok(())
1505 }
1506 "remap-path-prefix" => {
1507 let value = self.take_value(&rendered_flag, inline)?;
1508 let Some((from, to)) = value.split_once('=') else {
1509 return Err(BypassReason::MissingValue(rendered_flag));
1510 };
1511 self.parsed.push(Argument::RemapPath {
1512 from: from.into(),
1513 to: to.into(),
1514 });
1515 Ok(())
1516 }
1517 "codegen" => {
1518 let value = self.take_value(&rendered_flag, inline)?;
1519 self.parse_codegen(&value)
1520 }
1521 "jobs-frontend" => {
1522 let value = self.take_value(&rendered_flag, inline)?;
1523 self.parsed
1524 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1525 Ok(())
1526 }
1527 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1528 }
1529 }
1530
1531 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1532 if let Some(attached) = value.strip_prefix("-Z") {
1533 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1534 match option.as_str() {
1535 "embed-metadata=no"
1539 | "embed-metadata=yes"
1540 | "force-unstable-if-unmarked"
1541 | "shell-argfiles"
1542 | "unstable-options" => {
1543 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1544 return Ok(());
1545 }
1546 "threads" | "threads=" => {
1547 return Err(BypassReason::MissingValue("-Zthreads".into()));
1548 }
1549 _ => {}
1550 }
1551 if option.starts_with("threads=") {
1552 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1553 return Ok(());
1554 }
1555 if let Some(backend) = option.strip_prefix("codegen-backend=")
1557 && !backend.is_empty()
1558 && !backend.contains(['.', '/', '\\'])
1559 {
1560 self.sysroot_backend = Some(format!("-Z{option}"));
1561 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1562 return Ok(());
1563 }
1564 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1565 }
1566 match value {
1567 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1570 "-g" | "-O" | "-v" => {
1571 self.parsed.push(Argument::Plain(value.into()));
1572 return Ok(());
1573 }
1574 _ => {}
1575 }
1576 for (short, long) in [
1577 ("-A", "--allow"),
1578 ("-W", "--warn"),
1579 ("-D", "--deny"),
1580 ("-F", "--forbid"),
1581 ] {
1582 if let Some(attached) = value.strip_prefix(short) {
1583 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1584 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1585 return Ok(());
1586 }
1587 }
1588 if let Some(attached) = value.strip_prefix("-C") {
1589 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1590 return self.parse_codegen(&option);
1591 }
1592 if let Some(attached) = value.strip_prefix("-L") {
1593 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1594 let (kind, path) = search
1595 .split_once('=')
1596 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1597 if !matches!(kind, "dependency" | "native") {
1598 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1599 }
1600 self.parsed.push(Argument::SearchPath {
1601 kind: kind.into(),
1602 path: path.into(),
1603 });
1604 return Ok(());
1605 }
1606 if let Some(attached) = value.strip_prefix("-l") {
1607 let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1608 self.parsed.push(Argument::Plain(format!("-l{library}")));
1609 self.native_libraries.push(library);
1610 return Ok(());
1611 }
1612 if let Some(attached) = value.strip_prefix("-o") {
1613 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1614 self.explicit_output = Some(path.clone().into());
1615 self.parsed.push(Argument::Path {
1616 flag: "-o".into(),
1617 path: path.into(),
1618 });
1619 return Ok(());
1620 }
1621 Err(BypassReason::UnknownFlag(value.into()))
1622 }
1623
1624 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1625 let name = value.split_once('=').map_or(value, |(name, _)| name);
1626 if name == "incremental" {
1627 return Err(BypassReason::Incremental);
1628 }
1629 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1630 return Err(BypassReason::UnknownCodegenOption(name.into()));
1631 }
1632 if name == "linker-plugin-lto"
1638 && let Some((_, selection)) = value.split_once('=')
1639 && !matches!(
1640 selection,
1641 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1642 )
1643 {
1644 return Err(BypassReason::UnknownCodegenOption(format!(
1645 "linker-plugin-lto={selection}"
1646 )));
1647 }
1648 if name == "link-arg"
1655 && let Some((_, option)) = value.split_once('=')
1656 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1657 && !selection.is_empty()
1658 && !selection.contains(',')
1659 && !self.parsed.iter().any(
1660 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1661 )
1662 {
1663 if !self.parsed.iter().any(
1664 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1665 ) {
1666 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1667 }
1668 return Ok(());
1669 }
1670 if name == "link-arg"
1673 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1674 && safe_install_name(option)
1675 {
1676 self.parsed.push(Argument::InstallName(option.to_owned()));
1677 return Ok(());
1678 }
1679 if matches!(name, "link-arg" | "link-args")
1684 && let Some((_, option)) = value.split_once('=')
1685 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1686 && !prefix.trim_end_matches('/').is_empty()
1687 && !prefix.contains(',')
1688 {
1689 let trailing_slash = prefix.ends_with('/');
1690 self.parsed.push(Argument::OsoPrefix {
1691 path: PathBuf::from(prefix.trim_end_matches('/')),
1692 trailing_slash,
1693 });
1694 return Ok(());
1695 }
1696 self.parsed
1697 .push(Argument::Plain(format!("--codegen={value}")));
1698 if name == "extra-filename" {
1699 self.extra_filename = value
1700 .split_once('=')
1701 .map_or(String::new(), |(_, value)| value.to_string());
1702 }
1703 Ok(())
1704 }
1705
1706 fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1721 for library in std::mem::take(&mut self.native_libraries) {
1722 if link_output != LinkOutput::Library {
1723 return Err(BypassReason::NativeLibrary(library));
1724 }
1725 let Some((kind, name)) = library.split_once('=') else {
1733 continue;
1734 };
1735 let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1736 match kind {
1737 "static" => {}
1738 "dylib" | "framework" | "link-arg" => continue,
1739 _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1740 }
1741 let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1744 let (mut verbatim, mut bundle) = (false, true);
1745 for modifier in modifiers.split(',') {
1746 match modifier {
1747 "+verbatim" => verbatim = true,
1748 "-verbatim" => verbatim = false,
1749 "+bundle" => bundle = true,
1750 "-bundle" => bundle = false,
1751 _ => {}
1752 }
1753 }
1754 if !bundle {
1755 continue;
1756 }
1757 let archive = self
1758 .find_native_static_library(name, verbatim)
1759 .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1760 .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1761 self.required_inputs.push(archive);
1762 }
1763 Ok(())
1764 }
1765
1766 fn find_native_static_library(
1778 &self,
1779 name: &str,
1780 verbatim: bool,
1781 ) -> Result<Option<PathBuf>, ()> {
1782 let file_names = if verbatim {
1783 vec![name.to_owned()]
1784 } else {
1785 match self.static_library_naming() {
1786 Some(StaticLibraryNaming::Msvc) => {
1787 vec![format!("{name}.lib"), format!("lib{name}.a")]
1788 }
1789 Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1790 None => return Err(()),
1791 }
1792 };
1793 Ok(self
1794 .parsed
1795 .iter()
1796 .filter_map(|argument| match argument {
1797 Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1798 _ => None,
1799 })
1800 .flat_map(|directory| {
1801 file_names
1802 .iter()
1803 .map(move |file_name| directory.join(file_name))
1804 })
1805 .find(|candidate| candidate.is_file()))
1806 }
1807
1808 fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1816 let Some(target) = self.target.as_deref() else {
1817 return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1818 StaticLibraryNaming::Msvc
1819 } else {
1820 StaticLibraryNaming::Unix
1821 });
1822 };
1823 if target.ends_with(".json")
1824 || target.contains(['/', '\\'])
1825 || self.options.custom_target_search
1826 {
1827 return None;
1828 }
1829 Some(if target.contains("msvc") || target.ends_with("-uefi") {
1832 StaticLibraryNaming::Msvc
1833 } else {
1834 StaticLibraryNaming::Unix
1835 })
1836 }
1837
1838 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1839 if value == "-" {
1840 return Err(BypassReason::StandardInput);
1841 }
1842 if self.source.replace(value.into()).is_some() {
1843 return Err(BypassReason::MultipleInputs);
1844 }
1845 Ok(())
1846 }
1847
1848 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1849 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1858 let builds_a_library = !self.test
1859 && !self.crate_types.is_empty()
1860 && self
1861 .crate_types
1862 .iter()
1863 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1864 let link_output = if never_links || builds_a_library {
1865 LinkOutput::Library
1866 } else if self
1867 .target
1868 .as_deref()
1869 .is_some_and(compiler_bundled_wasm_target)
1870 && ((self.test && self.crate_types.is_empty())
1871 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1872 {
1873 if self.parsed.iter().any(|argument| match argument {
1874 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1875 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1876 !matches!(
1877 value.rsplit_once('=').map(|(_, value)| value),
1878 Some("y" | "yes" | "on" | "true")
1879 )
1880 }
1881 _ => false,
1882 }) {
1883 return Err(BypassReason::UnknownCodegenOption(
1884 "link-self-contained".into(),
1885 ));
1886 }
1887 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1888 && self.parsed.iter().any(|argument| {
1889 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1890 })
1891 {
1892 return Err(BypassReason::UnknownCodegenOption(
1893 "target-feature=-crt-static".into(),
1894 ));
1895 }
1896 LinkOutput::WasmExecutable
1900 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1901 self.check_native_link_is_portable()?;
1902 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1903 LinkOutput::NativeProcMacro
1904 } else if matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib") {
1905 LinkOutput::NativeSharedLibrary
1906 } else {
1907 LinkOutput::NativeExecutable
1908 }
1909 } else if self.test {
1910 return Err(BypassReason::UnsupportedCrateType("test".into()));
1911 } else {
1912 return Err(BypassReason::UnsupportedCrateType(
1913 self.crate_types
1914 .iter()
1915 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1916 .cloned()
1917 .unwrap_or_else(|| "bin".into()),
1918 ));
1919 };
1920 if link_output != LinkOutput::Library
1929 && let Some(option) = self.first_link_argument(link_output)
1930 {
1931 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1932 }
1933 if self
1934 .parsed
1935 .iter()
1936 .any(|arg| matches!(arg, Argument::InstallName(_)))
1937 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
1938 {
1939 return Err(BypassReason::UnmodeledLinkArgument(
1940 "link-arg=-Wl,-install_name".into(),
1941 ));
1942 }
1943 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1950 let Argument::FuseLd(selection) = argument else {
1951 return None;
1952 };
1953 Some(selection.as_str())
1954 }) && !matches!(
1955 link_output,
1956 LinkOutput::Library
1957 | LinkOutput::NativeExecutable
1958 | LinkOutput::NativeProcMacro
1959 | LinkOutput::NativeSharedLibrary
1960 ) {
1961 return Err(BypassReason::UnmodeledLinkArgument(format!(
1962 "link-arg=-fuse-ld={selection}"
1963 )));
1964 }
1965 if self.parsed.iter().any(|argument| {
1966 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1967 }) && !matches!(
1968 link_output,
1969 LinkOutput::NativeExecutable
1970 | LinkOutput::NativeProcMacro
1971 | LinkOutput::NativeSharedLibrary
1972 ) {
1973 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1974 }
1975 if !matches!(
1979 link_output,
1980 LinkOutput::Library
1981 | LinkOutput::NativeExecutable
1982 | LinkOutput::NativeProcMacro
1983 | LinkOutput::NativeSharedLibrary
1984 ) && self
1985 .parsed
1986 .iter()
1987 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1988 {
1989 return Err(BypassReason::UnmodeledLinkArgument(
1990 "link-arg=-Wl,-oso_prefix".into(),
1991 ));
1992 }
1993 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1994 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1995 _ => None,
1996 }) {
1997 return Err(BypassReason::UnresolvedExtern(name.clone()));
1998 }
1999 if let Some(emit) = self
2000 .emits
2001 .iter()
2002 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
2003 {
2004 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
2005 }
2006 if !self
2007 .emits
2008 .iter()
2009 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
2010 {
2011 return Err(BypassReason::NoCacheableOutput);
2012 }
2013 Ok(link_output)
2014 }
2015}
2016
2017impl Parser<'_> {
2018 fn first_link_argument(&self, link_output: LinkOutput) -> Option<&str> {
2021 self.parsed.iter().find_map(|argument| {
2022 let Argument::Plain(value) = argument else {
2023 return None;
2024 };
2025 let option = value.strip_prefix("--codegen=")?;
2026 let (name, value) = option.split_once('=').unwrap_or((option, ""));
2027 (matches!(name, "link-arg" | "link-args")
2028 && !(name == "link-arg" && elf_keyword_is_modeled(link_output, value)))
2029 .then_some(option)
2030 })
2031 }
2032
2033 fn oso_prefix_covers_outputs(&self) -> bool {
2042 let Some(directory) = self
2043 .out_dir
2044 .as_deref()
2045 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2046 else {
2047 return false;
2048 };
2049 if !directory.is_absolute() {
2050 return false;
2051 }
2052 let directory = normalize_components(directory);
2053 self.parsed.iter().any(|argument| {
2054 let Argument::OsoPrefix { path, .. } = argument else {
2055 return false;
2056 };
2057 path.is_absolute()
2058 && (directory.starts_with(normalize_components(path))
2059 || std::fs::canonicalize(&directory)
2060 .ok()
2061 .zip(std::fs::canonicalize(path).ok())
2062 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2063 })
2064 }
2065
2066 fn links_a_native_artifact(&self) -> bool {
2073 self.target.is_none()
2074 && self.emits.iter().any(|emit| emit.kind == "link")
2079 && ((self.test && self.crate_types.is_empty())
2080 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro"))
2081 || (cfg!(target_os = "linux")
2082 && matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib")))
2083 }
2084
2085 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2092 for argument in &self.parsed {
2093 let Argument::Plain(value) = argument else {
2094 continue;
2095 };
2096 let (name, value) = if value == "-g" {
2099 ("debuginfo", Some("2"))
2100 } else if let Some(option) = value.strip_prefix("--codegen=") {
2101 match option.split_once('=') {
2102 Some((name, value)) => (name, Some(value)),
2103 None => (option, None),
2107 }
2108 } else {
2109 continue;
2110 };
2111 let unportable = match name {
2112 "split-debuginfo" => match value {
2118 Some("off") => false,
2119 Some("unpacked") if cfg!(target_os = "macos") => {
2120 !self.oso_prefix_covers_outputs()
2121 }
2122 _ => true,
2123 },
2124 "debuginfo" if cfg!(target_os = "macos") => {
2133 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2134 }
2135 "rpath" => is_enabled(value),
2143 "prefer-dynamic" => {
2144 is_enabled(value)
2145 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2146 }
2147 "link-self-contained" => true,
2150 _ => false,
2151 };
2152 if unportable {
2153 return Err(BypassReason::UnportableNativeLink(match value {
2154 Some(value) => format!("{name}={value}"),
2155 None => name.to_owned(),
2156 }));
2157 }
2158 }
2159 Ok(())
2160 }
2161}
2162
2163fn safe_install_name(name: &str) -> bool {
2164 !name.is_empty()
2165 && name
2166 .bytes()
2167 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2168}
2169
2170fn is_enabled(value: Option<&str>) -> bool {
2173 matches!(value, None | Some("y" | "yes" | "on" | "true"))
2174}
2175
2176fn elf_keyword_is_modeled(link_output: LinkOutput, value: &str) -> bool {
2186 cfg!(target_os = "linux")
2187 && matches!(
2188 link_output,
2189 LinkOutput::NativeExecutable
2190 | LinkOutput::NativeProcMacro
2191 | LinkOutput::NativeSharedLibrary
2192 )
2193 && value.strip_prefix("-Wl,-z,").is_some_and(|keyword| {
2194 matches!(
2195 keyword,
2196 "defs"
2197 | "lazy"
2198 | "nodelete"
2199 | "nodlopen"
2200 | "noexecstack"
2201 | "norelro"
2202 | "now"
2203 | "origin"
2204 | "relro"
2205 )
2206 })
2207}
2208
2209fn compiler_bundled_wasm_target(target: &str) -> bool {
2210 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2211}
2212
2213fn parse_emits(value: &str) -> Vec<Emit> {
2214 value
2215 .split(',')
2216 .map(|emit| {
2217 let (kind, path) = emit
2218 .split_once('=')
2219 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2220 Emit {
2221 kind: kind.into(),
2222 path,
2223 }
2224 })
2225 .collect()
2226}
2227
2228struct ActionBuilder<'a> {
2229 invocation: &'a RustcInvocation,
2230 context: ActionContext,
2231 mappings: Vec<SharedPathMapping>,
2232 aliases: PathAliases,
2236 linker: Option<LinkerIdentity>,
2237}
2238
2239impl<'a> ActionBuilder<'a> {
2240 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2241 context.path_mappings = PathMapping::ordered(&context.path_mappings);
2242 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2243 Self {
2244 linker: None,
2245 invocation,
2246 mappings,
2247 aliases: PathAliases::default(),
2248 context,
2249 }
2250 }
2251
2252 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2253 self.linker = linker;
2254 self
2255 }
2256
2257 fn build(self) -> Result<RustcAction, BypassReason> {
2258 self.validate_mappings()?;
2259 let invocation = self.invocation_descriptor()?;
2260 let environment = self.environment_descriptor()?;
2261
2262 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2263 for input in &self.context.inputs {
2264 input
2265 .digest
2266 .validate()
2267 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2268 let path = self.normalize_input_path(&input.path)?;
2269 if inputs
2270 .insert(path.clone(), input.digest.clone())
2271 .is_some_and(|existing| existing != input.digest)
2272 {
2273 return Err(BypassReason::ConflictingInput(path));
2274 }
2275 }
2276 let required = self
2277 .invocation
2278 .required_inputs
2279 .iter()
2280 .map(|path| self.normalize_input_path(path))
2281 .collect::<Result<BTreeSet<_>, _>>()?;
2282 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2283 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2284 }
2285 let inputs = inputs
2286 .into_iter()
2287 .map(|(path, digest)| InputDescriptor { path, digest })
2288 .collect();
2289 if self.invocation.links_natively() && self.linker.is_none() {
2292 return Err(BypassReason::UnportableNativeLink(
2293 "linker identity is unknown".into(),
2294 ));
2295 }
2296 let descriptor = ActionDescriptor {
2297 version: ACTION_SCHEMA_VERSION,
2298 kind: "rustc",
2299 adapter_version: ADAPTER_VERSION,
2300 compiler: invocation.compiler,
2301 arguments: invocation.arguments,
2302 environment,
2303 inputs,
2304 linker: self.linker.clone(),
2305 };
2306 let bytes = canonical_json(&descriptor)
2307 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2308 let digest = CacheDigest::blake3(&bytes);
2309 Ok(RustcAction { digest, bytes })
2310 }
2311
2312 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2313 self.validate_mappings()?;
2314 let arguments = self
2315 .invocation
2316 .arguments
2317 .iter()
2318 .map(|argument| self.normalize_argument(argument))
2319 .collect::<Result<Vec<_>, _>>()?;
2320 let required_inputs = self
2321 .invocation
2322 .required_inputs
2323 .iter()
2324 .map(|path| self.normalize_path(path))
2325 .collect::<Result<BTreeSet<_>, _>>()?
2326 .into_iter()
2327 .collect();
2328 Ok(InvocationDescriptor {
2329 version: ACTION_SCHEMA_VERSION,
2330 kind: "rustc",
2331 adapter_version: ADAPTER_VERSION,
2332 compiler: CompilerDescriptor {
2333 toolchain: self.context.compiler.toolchain.clone(),
2334 rustc_version: self.context.compiler.rustc_version.clone(),
2335 host: self.context.compiler.host.clone(),
2336 driver: self.context.compiler.driver.clone(),
2337 },
2338 arguments,
2339 required_inputs,
2340 })
2341 }
2342
2343 fn validate_mappings(&self) -> Result<(), BypassReason> {
2344 if !self.context.working_dir.is_absolute() {
2345 return Err(BypassReason::RelativeWorkingDirectory(
2346 self.context.working_dir.clone(),
2347 ));
2348 }
2349 let mut roots = BTreeSet::new();
2350 let mut placeholders = BTreeSet::new();
2351 for mapping in &self.mappings {
2352 if !mapping.root.is_absolute() {
2353 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2354 }
2355 if mapping.placeholder.is_empty()
2356 || !mapping
2357 .placeholder
2358 .bytes()
2359 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2360 || !roots.insert(normalize_components(&mapping.root))
2361 || !placeholders.insert(&mapping.placeholder)
2362 {
2363 return Err(BypassReason::InvalidPathPlaceholder(
2364 mapping.placeholder.clone(),
2365 ));
2366 }
2367 }
2368 Ok(())
2369 }
2370
2371 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2372 match argument {
2373 Argument::Plain(value) => Ok(value.clone()),
2374 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2375 Argument::SearchPath { kind, path } => {
2376 let text = self.normalize_path(path)?;
2377 Ok(format!("-L{kind}={text}"))
2378 }
2379 Argument::Extern { name, path } => match path {
2380 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2381 None => Ok(format!("--extern={name}")),
2382 },
2383 Argument::Emit(emits) => Ok(format!(
2384 "--emit={}",
2385 emits
2386 .iter()
2387 .map(|emit| match &emit.path {
2388 Some(path) => self
2389 .normalize_path(path)
2390 .map(|path| format!("{}={path}", emit.kind)),
2391 None => Ok(emit.kind.clone()),
2392 })
2393 .collect::<Result<Vec<_>, _>>()?
2394 .join(",")
2395 )),
2396 Argument::RemapPath { from, to } => Ok(format!(
2397 "--remap-path-prefix={}={}",
2398 self.normalize_path(from)?,
2399 to
2400 )),
2401 Argument::InstallName(name) => Ok(format!(
2402 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2403 )),
2404 Argument::OsoPrefix {
2405 path,
2406 trailing_slash,
2407 } => Ok(format!(
2408 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2409 self.normalize_path(path)?,
2410 if *trailing_slash { "/" } else { "" }
2411 )),
2412 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2413 }
2414 }
2415
2416 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2423 self.context
2424 .environment
2425 .iter()
2426 .map(|(name, value)| {
2427 let value = match value {
2428 Some(value) if self.context.portable_environment.contains(name) => {
2429 Some(self.normalize_path(Path::new(value))?)
2430 }
2431 value => value.clone(),
2432 };
2433 Ok((name.clone(), value))
2434 })
2435 .collect()
2436 }
2437
2438 fn normalize_input_path(&self, path: &Path) -> Result<String, BypassReason> {
2439 let absolute = normalize_components(&self.context.working_dir.join(path));
2440 for directory in self.invocation.native_search_paths() {
2441 let directory = normalize_components(&self.context.working_dir.join(directory));
2442 if let Ok(relative) = absolute.strip_prefix(&directory) {
2443 let suffix = relative
2449 .components()
2450 .map(|component| {
2451 component
2452 .as_os_str()
2453 .to_str()
2454 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf()))
2455 })
2456 .collect::<Result<Vec<_>, _>>()?
2457 .join("/");
2458 if !suffix.is_empty() {
2459 return Ok(format!("{}/{}", self.normalize_path(&directory)?, suffix));
2460 }
2461 }
2462 }
2463 self.normalize_path(path)
2464 }
2465
2466 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2467 normalize_resolved_shared_path_with(
2468 &self.aliases,
2469 path,
2470 &self.context.working_dir,
2471 &self.mappings,
2472 )
2473 .map_err(Into::into)
2474 }
2475}
2476
2477fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2481 directories.iter().any(|directory| {
2482 path.len() > directory.len()
2483 && path.as_bytes()[directory.len()] == b'/'
2484 && path.starts_with(directory)
2485 })
2486}
2487
2488fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2489 for mapping in mappings {
2490 let prefix = format!("${{{}}}", mapping.placeholder);
2491 let suffix = if value == prefix {
2492 ""
2493 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2494 suffix
2495 } else {
2496 continue;
2497 };
2498 if !mapping.root.is_absolute()
2499 || (!suffix.is_empty()
2500 && suffix.split('/').any(|component| {
2501 component.is_empty()
2502 || matches!(component, "." | "..")
2503 || component.contains('\\')
2504 }))
2505 {
2506 return Err(BypassReason::InvalidPredictedInput(value.into()));
2507 }
2508 let mut path = normalize_components(&mapping.root);
2509 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2510 return Ok(path);
2511 }
2512 Err(BypassReason::InvalidPredictedInput(value.into()))
2513}
2514
2515fn normalize_components(path: &Path) -> PathBuf {
2516 let mut normalized = PathBuf::new();
2517 for component in path.components() {
2518 match component {
2519 Component::CurDir => {}
2520 Component::ParentDir => {
2521 normalized.pop();
2522 }
2523 component => normalized.push(component.as_os_str()),
2524 }
2525 }
2526 normalized
2527}
2528
2529fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2530 if path.is_absolute() {
2531 normalize_components(path)
2532 } else {
2533 normalize_components(&working_dir.join(path))
2534 }
2535}
2536
2537#[cfg(test)]
2538#[path = "rustc_cache_tests.rs"]
2539mod tests;