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 Self::BuildScriptInputOverlapsOutputs(_) => Some(
92 "Keep compiler output directories outside watched trees, or narrow `rerun-if-changed` to source and asset paths.",
93 ),
94 _ => None,
95 }
96 }
97}
98
99impl From<PathNormalizationError> for BypassReason {
100 fn from(reason: PathNormalizationError) -> Self {
101 match reason {
102 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
103 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
104 }
105 }
106}
107
108const SUPPORTED_CODEGEN_OPTIONS: &[&str] = &[
109 "codegen-units",
110 "control-flow-guard",
111 "debug-assertions",
112 "debuginfo",
113 "default-linker-libraries",
114 "embed-bitcode",
115 "extra-filename",
116 "force-frame-pointers",
117 "force-unwind-tables",
118 "instrument-coverage",
119 "link-arg",
120 "link-args",
121 "link-dead-code",
122 "link-self-contained",
123 "linker-plugin-lto",
124 "lto",
125 "metadata",
126 "no-prepopulate-passes",
127 "opt-level",
128 "overflow-checks",
129 "panic",
130 "prefer-dynamic",
131 "relocation-model",
132 "rpath",
133 "save-temps",
134 "soft-float",
135 "split-debuginfo",
136 "split-dwarf-kind",
137 "strip",
138 "symbol-mangling-version",
139 "target-cpu",
140 "target-feature",
141 "tls-model",
142];
143
144const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
145const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
146const MAX_DECODED_PREDICTION_BYTES: usize = 16 * 1024 * 1024;
150const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
151
152const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
155 "wasm32-unknown-unknown",
156 "wasm32-wasip1",
157 "wasm32-wasip1-threads",
158 "wasm32-wasip2",
159 "wasm32v1-none",
160 "wasm64-unknown-unknown",
161];
162
163#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
164#[strum(serialize_all = "kebab-case")]
165#[non_exhaustive]
175pub enum BypassReason {
176 #[error("rustc argument {index} is not valid UTF-8")]
178 NonUtf8Argument {
179 index: usize,
181 },
182 #[error("could not model rustc response file: {0}")]
184 ResponseFile(String),
185 #[error("rustc flag is not modeled by the cache adapter: {0}")]
187 UnknownFlag(String),
188 #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
190 UnknownCodegenOption(String),
191 #[error("rustc flag requires a value: {0}")]
193 MissingValue(String),
194 #[error("rustc invocation is a compiler query, not a compilation")]
196 CompilerQuery,
197 #[error("rustc invocation reads source from standard input")]
199 StandardInput,
200 #[error("rustc invocation has no source input")]
202 MissingInput,
203 #[error("rustc invocation has multiple source inputs")]
205 MultipleInputs,
206 #[error("incremental compilation cannot be combined with action caching")]
208 Incremental,
209 #[error("rustc crate type is not cacheable yet: {0}")]
211 UnsupportedCrateType(String),
212 #[error("rustc output type is not cacheable yet: {0}")]
214 UnsupportedEmit(String),
215 #[error("rustc invocation does not emit a cacheable artifact")]
217 NoCacheableOutput,
218 #[error("rustc invocation does not emit dependency information")]
220 NoDepInfo,
221 #[error("rustc output paths do not share one directory")]
223 SplitOutputDirectories,
224 #[error("rustc output path has no file name: {0}")]
226 InvalidOutputPath(PathBuf),
227 #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
229 ImplicitEmitWithOutputFile(PathBuf),
230 #[error("native library on a linked output is not cacheable yet: {0}")]
233 NativeLibrary(String),
234 #[error("native static library was not found on any search path: {0}")]
237 MissingNativeLibrary(String),
238 #[error("static library file names are not known for a custom target: {0}")]
241 CustomTargetNativeLibrary(String),
242 #[error("rustc output name does not distinguish a program from a library: {0}")]
244 AmbiguousOutputName(PathBuf),
245 #[error("native link is not reproducible across checkouts: {0}")]
247 UnportableNativeLink(String),
248 #[error("rustc link argument is not modeled by the cache adapter: {0}")]
250 UnmodeledLinkArgument(String),
251 #[error("rustc search path kind is not cacheable yet: {0}")]
253 UnsupportedSearchPath(String),
254 #[error("rustc extern does not identify an input artifact: {0}")]
256 UnresolvedExtern(String),
257 #[error("absolute path has no stable cache mapping: {0}")]
259 UnmappedAbsolutePath(PathBuf),
260 #[error("cache key paths must be valid UTF-8: {0}")]
262 NonUtf8Path(PathBuf),
263 #[error("cache action working directory must be absolute: {0}")]
265 RelativeWorkingDirectory(PathBuf),
266 #[error("cache path mapping must use an absolute root: {0}")]
268 RelativePathMapping(PathBuf),
269 #[error("cache path mapping placeholder is invalid: {0}")]
271 InvalidPathPlaceholder(String),
272 #[error("required compiler input was not provided: {0}")]
274 MissingRequiredInput(String),
275 #[error("compiler input has an invalid digest: {0}")]
277 InvalidInputDigest(String),
278 #[error("compiler input appears more than once with different content: {0}")]
280 ConflictingInput(String),
281 #[error("rustc dep-info is malformed: {0}")]
283 MalformedDepInfo(String),
284 #[error("failed to read rustc dep-info {path}: {message}")]
286 DepInfoRead {
287 path: PathBuf,
289 message: String,
291 },
292 #[error("rustc dep-info output path must be absolute: {0}")]
294 RelativeDepInfoPath(PathBuf),
295 #[error("rustc dep-info output path cannot contain a comma: {0}")]
297 UnsafeDepInfoPath(PathBuf),
298 #[error("failed to read compiler input {path}: {message}")]
300 InputRead {
301 path: PathBuf,
303 message: String,
305 },
306 #[error("declared build-script input overlaps compiler outputs: {0}")]
308 BuildScriptInputOverlapsOutputs(PathBuf),
309 #[error("build-script inputs changed during compilation (declarations: {0})")]
312 BuildScriptInputsChanged(PathBuf),
313 #[error("compiler input changed after discovery: {0}")]
315 InputChanged(PathBuf),
316 #[error("compiler input was modified during compilation: {0}")]
318 InputModifiedDuringCompilation(PathBuf),
319 #[error("discovered inputs were collected from a different working directory")]
321 DiscoveryWorkingDirectory,
322 #[error("compiler environment input has conflicting values: {0}")]
324 ConflictingEnvironment(String),
325 #[error("failed to serialize the rustc action: {0}")]
327 Serialization(String),
328 #[error("rustc action prediction is unsupported")]
330 UnsupportedPrediction,
331 #[error("rustc action prediction contains an invalid input path: {0}")]
333 InvalidPredictedInput(String),
334}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
337enum Argument {
338 Plain(String),
339 Path {
340 flag: String,
341 path: PathBuf,
342 },
343 SearchPath {
344 kind: String,
345 path: PathBuf,
346 },
347 Extern {
348 name: String,
349 path: Option<PathBuf>,
350 },
351 Emit(Vec<Emit>),
352 RemapPath {
353 from: PathBuf,
354 to: String,
355 },
356 InstallName(String),
358 OsoPrefix {
361 path: PathBuf,
362 trailing_slash: bool,
363 },
364 FuseLd(String),
368}
369
370#[derive(Debug, Clone, PartialEq, Eq)]
371struct Emit {
372 kind: String,
373 path: Option<PathBuf>,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
377pub struct RustcInvocation {
382 arguments: Vec<Argument>,
383 source: PathBuf,
384 required_inputs: Vec<PathBuf>,
385 crate_name: String,
386 extra_filename: String,
387 out_dir: Option<PathBuf>,
388 explicit_output: Option<PathBuf>,
389 emits: Vec<Emit>,
390 target: Option<String>,
391 link_output: LinkOutput,
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
395enum LinkOutput {
396 Library,
397 WasmExecutable,
398 NativeExecutable,
399 NativeProcMacro,
400 NativeSharedLibrary,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
413#[non_exhaustive]
414pub struct ParseOptions {
415 pub cache_native_links: bool,
418 pub custom_target_search: bool,
424}
425
426impl ParseOptions {
427 pub fn caching_native_links(enabled: bool) -> Self {
429 Self {
430 cache_native_links: enabled,
431 ..Self::default()
432 }
433 }
434
435 pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
438 self.custom_target_search = enabled;
439 self
440 }
441}
442
443#[derive(Debug, Clone, PartialEq, Eq)]
445pub struct RustcOutputs {
446 pub directory: PathBuf,
448 pub files: Vec<PathBuf>,
450 pub dep_info: PathBuf,
452}
453
454impl RustcInvocation {
455 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
464 Self::parse_with(arguments, ParseOptions::default())
465 }
466
467 pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
472 Ok(expand_response_files(arguments)?.arguments)
473 }
474
475 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
478 let expanded = expand_response_files(arguments)?;
479 Parser::new(&expanded.arguments, options).parse()
480 }
481
482 pub fn links_natively(&self) -> bool {
485 matches!(
486 self.link_output,
487 LinkOutput::NativeExecutable
488 | LinkOutput::NativeProcMacro
489 | LinkOutput::NativeSharedLibrary
490 )
491 }
492
493 pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
497 if !cfg!(target_os = "macos")
498 || self.link_output != LinkOutput::NativeProcMacro
499 || self
500 .arguments
501 .iter()
502 .any(|arg| matches!(arg, Argument::InstallName(_)))
503 {
504 return None;
505 }
506 let file = outputs
507 .files
508 .iter()
509 .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
510 let name = file.file_name()?.to_str()?;
511 if !safe_install_name(name) {
512 return None;
513 }
514 Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
515 }
516
517 pub fn linker_override(&self) -> Option<&Path> {
520 self.arguments.iter().find_map(|argument| {
521 let Argument::Plain(value) = argument else {
522 return None;
523 };
524 value.strip_prefix("--codegen=linker=").map(Path::new)
525 })
526 }
527
528 pub fn fuse_ld(&self) -> Option<&str> {
531 self.arguments.iter().find_map(|argument| {
532 let Argument::FuseLd(selection) = argument else {
533 return None;
534 };
535 Some(selection.as_str())
536 })
537 }
538
539 fn emits_windows_pdb(&self) -> bool {
540 if !cfg!(windows) || !self.links_natively() {
541 return false;
542 }
543 let mut enabled = false;
544 for argument in &self.arguments {
545 let Argument::Plain(value) = argument else {
546 continue;
547 };
548 if value == "-g" {
549 enabled = true;
550 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
551 enabled = !matches!(value, "0" | "none");
552 }
553 }
554 enabled
555 }
556
557 pub fn native_search_paths(&self) -> impl Iterator<Item = &Path> {
561 self.arguments.iter().filter_map(|argument| match argument {
562 Argument::SearchPath { kind, path } if kind == "native" => Some(path.as_path()),
563 _ => None,
564 })
565 }
566
567 pub fn source(&self) -> &Path {
569 &self.source
570 }
571
572 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
578 self.required_inputs
579 .iter()
580 .map(|path| absolute_path(path, working_dir))
581 .collect()
582 }
583
584 pub fn target(&self) -> Option<&str> {
586 self.target.as_deref()
587 }
588
589 pub fn crate_name(&self) -> &str {
591 &self.crate_name
592 }
593
594 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
607 let linked = self
608 .arguments
609 .iter()
610 .filter_map(|argument| match argument {
611 Argument::Extern {
612 path: Some(path), ..
613 } => Some(path.as_path()),
614 _ => None,
615 })
616 .collect::<BTreeSet<_>>();
617 let owned = discovered
618 .inputs
619 .iter()
620 .filter(|input| {
621 !linked.contains(input.path.as_path()) && !discovered.is_native_only(&input.path)
622 })
623 .map(|input| (input.path.as_path(), &input.digest))
624 .collect::<BTreeMap<_, _>>();
625 let mut bytes = Vec::new();
626 for (path, digest) in owned {
627 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
628 bytes.push(0);
629 bytes.extend_from_slice(digest.key().as_bytes());
630 bytes.push(0);
631 }
632 CacheDigest::blake3(&bytes)
633 }
634
635 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
640 if !working_dir.is_absolute() {
641 return Err(BypassReason::RelativeWorkingDirectory(
642 working_dir.to_path_buf(),
643 ));
644 }
645 let explicit_output = self
646 .explicit_output
647 .as_deref()
648 .map(|path| absolute_path(path, working_dir));
649 let output_directory = explicit_output
650 .as_deref()
651 .and_then(Path::parent)
652 .map(Path::to_path_buf)
653 .or_else(|| {
654 self.out_dir
655 .as_deref()
656 .map(|path| absolute_path(path, working_dir))
657 })
658 .unwrap_or_else(|| normalize_components(working_dir));
659 if let Some(output) = &explicit_output
663 && self.emits.iter().any(|emit| {
664 emit.path.is_none()
665 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
666 })
667 {
668 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
669 }
670 let mut files = BTreeSet::new();
671 let mut dep_info = None;
672 for emit in &self.emits {
673 if emit.kind == "dep-info" {
674 let path = emit.path.as_ref().map_or_else(
675 || {
676 explicit_output.clone().map_or_else(
677 || {
678 output_directory
679 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
680 },
681 |path| path.with_extension("d"),
682 )
683 },
684 |path| absolute_path(path, working_dir),
685 );
686 if path.file_name().is_none() {
687 return Err(BypassReason::InvalidOutputPath(path));
688 }
689 dep_info = Some(path);
690 continue;
691 }
692 let (prefix, extension) = match emit.kind.as_str() {
693 "link" => match self.link_output {
694 LinkOutput::Library => ("lib", "rlib"),
695 LinkOutput::WasmExecutable => ("", "wasm"),
696 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
697 LinkOutput::NativeProcMacro | LinkOutput::NativeSharedLibrary => (
698 std::env::consts::DLL_PREFIX,
699 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
700 ),
701 },
702 "metadata" => ("lib", "rmeta"),
703 _ => continue,
704 };
705 let path = if let Some(path) = &emit.path {
706 absolute_path(path, working_dir)
707 } else {
708 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
709 output_directory.join(if extension.is_empty() {
710 name
711 } else {
712 format!("{name}.{extension}")
713 })
714 };
715 if path.file_name().is_none() {
716 return Err(BypassReason::InvalidOutputPath(path));
717 }
718 if path.parent() != Some(output_directory.as_path()) {
719 return Err(BypassReason::SplitOutputDirectories);
720 }
721 let has_library_extension = path
726 .extension()
727 .and_then(|extension| extension.to_str())
728 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
729 || (cfg!(windows)
730 && path
731 .file_stem()
732 .and_then(|stem| Path::new(stem).extension())
733 .and_then(|extension| extension.to_str())
734 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
735 if emit.kind == "link"
736 && !matches!(self.link_output, LinkOutput::Library)
737 && has_library_extension
738 {
739 return Err(BypassReason::AmbiguousOutputName(path));
740 }
741 if emit.kind == "link" && self.emits_windows_pdb() {
742 files.insert(path.with_extension("pdb"));
743 }
744 files.insert(path);
745 }
746 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
747 if dep_info.parent() != Some(output_directory.as_path()) {
748 return Err(BypassReason::SplitOutputDirectories);
749 }
750 Ok(RustcOutputs {
751 directory: output_directory,
752 files: files.into_iter().collect(),
753 dep_info,
754 })
755 }
756
757 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
763 self.action_linked_by(context, None)
764 }
765
766 pub fn action_linked_by(
775 &self,
776 context: ActionContext,
777 linker: Option<LinkerIdentity>,
778 ) -> Result<RustcAction, BypassReason> {
779 ActionBuilder::new(self, context).linked_by(linker).build()
780 }
781
782 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
784 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
785 let bytes = canonical_json(&descriptor)
786 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
787 Ok(CacheDigest::blake3(&bytes))
788 }
789
790 pub fn prediction(
793 &self,
794 context: &ActionContext,
795 discovered: &DiscoveredInputs,
796 ) -> Result<RustcInputPrediction, BypassReason> {
797 let builder = ActionBuilder::new(self, context.clone());
798 builder.validate_mappings()?;
799 let mut native_directories = BTreeSet::new();
800 for argument in &self.arguments {
801 if let Argument::SearchPath { kind, path } = argument
802 && kind == "native"
803 {
804 native_directories.insert(builder.normalize_path(path)?);
805 }
806 }
807 let mut inputs = BTreeSet::new();
819 for input in &discovered.inputs {
820 let normalized = builder.normalize_path(&input.path)?;
821 if !under_any_directory(&normalized, &native_directories)
822 || !discovered.is_native_only(&input.path)
823 {
824 inputs.insert(normalized);
825 }
826 }
827 inputs.extend(
828 native_directories
829 .into_iter()
830 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
831 );
832 Ok(RustcInputPrediction {
833 version: 4,
834 inputs: inputs.into_iter().collect(),
835 environment: discovered.environment.keys().cloned().collect(),
836 compiler_duration_ns: 0,
837 crate_name: String::new(),
838 })
839 }
840}
841
842pub fn is_build_script_crate_name(crate_name: &str) -> bool {
846 crate_name
847 .strip_prefix("build_script_")
848 .is_some_and(|stem| !stem.is_empty())
849}
850
851impl RustcOutputs {
852 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
862 is_build_script_crate_name(crate_name)
863 .then(|| self.files.iter().find(|path| self.is_executable(path)))
864 .flatten()
865 .map(PathBuf::as_path)
866 }
867
868 pub fn is_executable(&self, path: &Path) -> bool {
876 self.files.iter().any(|output| output == path)
877 && !matches!(
878 path.extension().and_then(|extension| extension.to_str()),
879 Some("rlib" | "rmeta")
880 )
881 }
882}
883
884#[derive(Debug, Clone, PartialEq, Eq)]
885pub struct PathMapping {
887 pub root: PathBuf,
889 pub placeholder: String,
891}
892
893impl PathMapping {
894 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
896 Self {
897 root: root.into(),
898 placeholder: placeholder.into(),
899 }
900 }
901
902 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
905 let mut ordered = mappings.to_vec();
906 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
907 ordered
908 }
909}
910
911fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
912 mappings
913 .iter()
914 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
915 .collect()
916}
917
918pub fn normalize_mapped_path(
925 path: &Path,
926 working_dir: &Path,
927 mappings: &[PathMapping],
928) -> Result<String, BypassReason> {
929 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
930}
931
932#[derive(Debug, Clone, PartialEq, Eq)]
933pub struct CompilerIdentity {
935 pub toolchain: String,
937 pub rustc_version: String,
939 pub host: String,
941 pub driver: Option<String>,
943}
944
945#[derive(Debug, Clone, PartialEq, Eq)]
947pub struct ActionInput {
948 pub path: PathBuf,
950 pub digest: CacheDigest,
952}
953
954#[derive(Debug, Clone, PartialEq, Eq)]
956pub struct ActionContext {
957 pub compiler: CompilerIdentity,
959 pub working_dir: PathBuf,
961 pub path_mappings: Vec<PathMapping>,
963 pub environment: BTreeMap<String, Option<String>>,
965 pub portable_environment: BTreeSet<String>,
972 pub inputs: Vec<ActionInput>,
974}
975
976#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
986#[serde(deny_unknown_fields)]
987pub struct LinkerIdentity {
988 pub driver: String,
990 pub driver_version: String,
992 pub linker_version: String,
994 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
996 pub crt_objects: BTreeMap<String, CacheDigest>,
997 #[serde(skip_serializing_if = "Option::is_none", default)]
999 pub sdk: Option<String>,
1000 #[serde(skip_serializing_if = "Option::is_none", default)]
1002 pub deployment_target: Option<String>,
1003}
1004
1005#[derive(Debug, Clone, PartialEq, Eq)]
1007pub struct RustcAction {
1008 pub digest: CacheDigest,
1010 pub bytes: Vec<u8>,
1012}
1013
1014#[derive(Debug, Clone, PartialEq, Eq)]
1017pub struct RustcInputPrediction {
1018 pub version: u8,
1020 pub inputs: Vec<String>,
1022 pub environment: Vec<String>,
1024 pub compiler_duration_ns: u64,
1027 pub crate_name: String,
1029}
1030
1031#[derive(Serialize, Deserialize)]
1032#[serde(deny_unknown_fields)]
1033struct RustcInputPredictionWire {
1034 version: u8,
1035 inputs: Vec<String>,
1036 environment: Vec<String>,
1037 #[serde(default, skip_serializing_if = "is_zero")]
1038 compiler_duration_ns: u64,
1039 #[serde(default, skip_serializing_if = "String::is_empty")]
1040 crate_name: String,
1041}
1042
1043impl Serialize for RustcInputPrediction {
1044 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1045 where
1046 S: Serializer,
1047 {
1048 RustcInputPredictionWire {
1049 version: self.version,
1050 inputs: if self.version == 4 {
1051 compact_prediction_inputs(&self.inputs)
1052 } else {
1053 self.inputs.clone()
1054 },
1055 environment: self.environment.clone(),
1056 compiler_duration_ns: self.compiler_duration_ns,
1057 crate_name: self.crate_name.clone(),
1058 }
1059 .serialize(serializer)
1060 }
1061}
1062
1063impl<'de> Deserialize<'de> for RustcInputPrediction {
1064 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1065 where
1066 D: Deserializer<'de>,
1067 {
1068 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1069 let inputs = if wire.version == 4 {
1070 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1071 } else {
1072 wire.inputs
1073 };
1074 Ok(Self {
1075 version: wire.version,
1076 inputs,
1077 environment: wire.environment,
1078 compiler_duration_ns: wire.compiler_duration_ns,
1079 crate_name: wire.crate_name,
1080 })
1081 }
1082}
1083
1084fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1085 let mut previous = "";
1086 inputs
1087 .iter()
1088 .map(|input| {
1089 let mut shared = previous
1090 .bytes()
1091 .zip(input.bytes())
1092 .take_while(|(left, right)| left == right)
1093 .count();
1094 while !input.is_char_boundary(shared) {
1095 shared -= 1;
1096 }
1097 let compact = format!("{shared}:{}", &input[shared..]);
1098 previous = input;
1099 compact
1100 })
1101 .collect()
1102}
1103
1104fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1105 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1106 let mut decoded_bytes = 0_usize;
1107 for input in inputs {
1108 let (shared_text, suffix) = input
1109 .split_once(':')
1110 .ok_or("compact rustc prediction input has no prefix length")?;
1111 let shared: usize = shared_text
1112 .parse()
1113 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1114 if shared.to_string() != shared_text
1115 || shared > expanded.last().map_or(0, String::len)
1116 || expanded
1117 .last()
1118 .is_some_and(|previous| !previous.is_char_boundary(shared))
1119 {
1120 return Err("compact rustc prediction prefix is not canonical");
1121 }
1122 let mut path = expanded
1123 .last()
1124 .map_or_else(String::new, |previous| previous[..shared].to_string());
1125 path.push_str(suffix);
1126 decoded_bytes = decoded_bytes
1127 .checked_add(path.len())
1128 .ok_or("compact rustc prediction is too large")?;
1129 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1130 return Err("compact rustc prediction is too large");
1131 }
1132 expanded.push(path);
1133 }
1134 Ok(expanded)
1135}
1136
1137fn is_zero(value: &u64) -> bool {
1138 *value == 0
1139}
1140
1141impl RustcInputPrediction {
1142 pub fn discover(
1145 &self,
1146 working_dir: &Path,
1147 path_mappings: &[PathMapping],
1148 digests: &dyn FileDigestCache,
1149 ) -> Result<DiscoveredInputs, BypassReason> {
1150 if !matches!(self.version, 2..=4) {
1151 return Err(BypassReason::UnsupportedPrediction);
1152 }
1153 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1154 return Err(BypassReason::UnsupportedPrediction);
1155 }
1156 let mut paths = BTreeSet::new();
1157 let mut named = BTreeSet::new();
1158 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1159 let mut native_bytes = 0_u64;
1160 let mut counted = BTreeSet::new();
1161 for path in &self.inputs {
1162 if self.version >= 3
1163 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1164 {
1165 let directory = denormalize_path(path, path_mappings)?;
1166 dep_info::collect_native_directory(
1167 &directory,
1168 &admitted_roots,
1169 &mut paths,
1170 &mut native_bytes,
1171 &mut counted,
1172 )?;
1173 } else {
1174 let path = denormalize_path(path, path_mappings)?;
1175 named.insert(path.clone());
1176 paths.insert(path);
1177 }
1178 }
1179 let native_only = paths.difference(&named).cloned().collect();
1180 let environment = self
1181 .environment
1182 .iter()
1183 .map(|name| {
1184 if name.is_empty() || name.contains(['=', '\0']) {
1185 return Err(BypassReason::UnsupportedPrediction);
1186 }
1187 let value = std::env::var_os(name)
1188 .map(|value| {
1189 value
1190 .into_string()
1191 .map_err(|_| BypassReason::UnsupportedPrediction)
1192 })
1193 .transpose()?;
1194 Ok((name.clone(), value))
1195 })
1196 .collect::<Result<BTreeMap<_, _>, _>>()?;
1197 Ok(
1198 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)?
1199 .with_native_only(native_only),
1200 )
1201 }
1202}
1203
1204#[derive(Serialize)]
1205struct ActionDescriptor {
1206 version: u8,
1207 kind: &'static str,
1208 adapter_version: u8,
1209 compiler: CompilerDescriptor,
1210 arguments: Vec<String>,
1211 environment: BTreeMap<String, Option<String>>,
1212 inputs: Vec<InputDescriptor>,
1213 #[serde(skip_serializing_if = "Option::is_none")]
1216 linker: Option<LinkerIdentity>,
1217}
1218
1219#[derive(Serialize)]
1220struct InvocationDescriptor {
1221 version: u8,
1222 kind: &'static str,
1223 adapter_version: u8,
1224 compiler: CompilerDescriptor,
1225 arguments: Vec<String>,
1226 required_inputs: Vec<String>,
1227}
1228
1229#[derive(Serialize)]
1230struct CompilerDescriptor {
1231 toolchain: String,
1232 rustc_version: String,
1233 host: String,
1234 #[serde(skip_serializing_if = "Option::is_none")]
1235 driver: Option<String>,
1236}
1237
1238#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1239struct InputDescriptor {
1240 path: String,
1241 digest: CacheDigest,
1242}
1243
1244#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1246enum StaticLibraryNaming {
1247 Msvc,
1249 Unix,
1251}
1252
1253struct Parser<'a> {
1254 arguments: &'a [OsString],
1255 index: usize,
1256 parsed: Vec<Argument>,
1257 source: Option<PathBuf>,
1258 crate_types: Vec<String>,
1259 emits: Vec<Emit>,
1260 required_inputs: Vec<PathBuf>,
1261 native_libraries: Vec<String>,
1262 test: bool,
1263 crate_name: Option<String>,
1264 extra_filename: String,
1265 out_dir: Option<PathBuf>,
1266 explicit_output: Option<PathBuf>,
1267 target: Option<String>,
1268 sysroot_backend: Option<String>,
1269 options: ParseOptions,
1270}
1271
1272struct ExpandedArguments {
1273 arguments: Vec<OsString>,
1274}
1275
1276#[derive(Default)]
1277struct ResponseExpander {
1278 shell_argfiles: bool,
1279 next_is_unstable_option: bool,
1280 arguments: Vec<OsString>,
1281}
1282
1283impl ResponseExpander {
1284 fn push(&mut self, argument: String) {
1285 if self.next_is_unstable_option {
1286 self.shell_argfiles |= argument == "shell-argfiles";
1287 self.next_is_unstable_option = false;
1288 } else if let Some(option) = argument.strip_prefix("-Z") {
1289 if option.is_empty() {
1290 self.next_is_unstable_option = true;
1291 } else {
1292 self.shell_argfiles |= option == "shell-argfiles";
1293 }
1294 }
1295 self.arguments.push(argument.into());
1296 }
1297}
1298
1299fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1302 let mut expanded = ResponseExpander::default();
1303 for (index, argument) in arguments.iter().enumerate() {
1304 let argument = argument
1305 .to_str()
1306 .ok_or(BypassReason::NonUtf8Argument { index })?;
1307 let Some(argfile) = argument.strip_prefix('@') else {
1308 expanded.push(argument.to_string());
1309 continue;
1310 };
1311 let (path, shell) = match argfile.split_once(':') {
1312 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1313 _ => (argfile, false),
1314 };
1315 let contents = std::fs::read_to_string(path).map_err(|error| {
1316 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1317 })?;
1318 if shell {
1319 let arguments = shlex::split(&contents).ok_or_else(|| {
1320 BypassReason::ResponseFile(format!(
1321 "invalid shell-style arguments in {}",
1322 Path::new(path).display()
1323 ))
1324 })?;
1325 for argument in arguments {
1326 expanded.push(argument);
1327 }
1328 } else {
1329 for argument in contents.lines() {
1330 expanded.push(argument.to_string());
1331 }
1332 }
1333 }
1334 Ok(ExpandedArguments {
1335 arguments: expanded.arguments,
1336 })
1337}
1338
1339impl<'a> Parser<'a> {
1340 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1341 Self {
1342 arguments,
1343 options,
1344 index: 0,
1345 parsed: Vec::new(),
1346 source: None,
1347 crate_types: Vec::new(),
1348 emits: Vec::new(),
1349 required_inputs: Vec::new(),
1350 native_libraries: Vec::new(),
1351 test: false,
1352 crate_name: None,
1353 extra_filename: String::new(),
1354 out_dir: None,
1355 explicit_output: None,
1356 target: None,
1357 sysroot_backend: None,
1358 }
1359 }
1360
1361 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1362 while self.index < self.arguments.len() {
1363 let value = self.current()?.to_string();
1364 self.index += 1;
1365 if let Some(long) = value.strip_prefix("--") {
1366 self.parse_long(long)?;
1367 } else if value.starts_with('-') && value != "-" {
1368 self.parse_short(&value)?;
1369 } else {
1370 self.parse_input(&value)?;
1371 }
1372 }
1373
1374 if let Some(backend) = self.sysroot_backend.take()
1376 && self.parsed.iter().any(
1377 |argument| matches!(argument, Argument::Path { flag, .. } if flag == "--sysroot"),
1378 )
1379 {
1380 return Err(BypassReason::UnknownFlag(backend));
1381 }
1382 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1383 let link_output = self.classify()?;
1384 self.require_native_libraries(link_output)?;
1385 let crate_name = self.crate_name.clone().map_or_else(
1386 || {
1387 source
1388 .file_stem()
1389 .and_then(|name| name.to_str())
1390 .map(|name| name.replace('-', "_"))
1391 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1392 },
1393 Ok,
1394 )?;
1395 self.required_inputs.push(source.clone());
1396 Ok(RustcInvocation {
1397 arguments: self.parsed,
1398 source,
1399 required_inputs: self.required_inputs,
1400 crate_name,
1401 extra_filename: self.extra_filename,
1402 out_dir: self.out_dir,
1403 explicit_output: self.explicit_output,
1404 emits: self.emits,
1405 target: self.target,
1406 link_output,
1407 })
1408 }
1409
1410 fn current(&self) -> Result<&str, BypassReason> {
1411 self.arguments[self.index]
1412 .to_str()
1413 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1414 }
1415
1416 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1417 if let Some(value) = inline {
1418 if value.is_empty() {
1419 return Err(BypassReason::MissingValue(flag.into()));
1420 }
1421 return Ok(value.into());
1422 }
1423 if self.index >= self.arguments.len() {
1424 return Err(BypassReason::MissingValue(flag.into()));
1425 }
1426 let value = self.current()?.to_string();
1427 self.index += 1;
1428 Ok(value)
1429 }
1430
1431 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1432 let (flag, inline) = value
1433 .split_once('=')
1434 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1435 let rendered_flag = format!("--{flag}");
1436 match flag {
1437 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1438 "test" => {
1439 self.test = true;
1440 self.parsed.push(Argument::Plain(rendered_flag));
1441 Ok(())
1442 }
1443 "verbose" => {
1444 self.parsed.push(Argument::Plain(rendered_flag));
1445 Ok(())
1446 }
1447 "crate-name" => {
1448 let value = self.take_value(&rendered_flag, inline)?;
1449 self.crate_name = Some(value.clone());
1450 self.parsed
1451 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1452 Ok(())
1453 }
1454 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1455 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1456 | "deny" | "forbid" | "cap-lints" => {
1457 let value = self.take_value(&rendered_flag, inline)?;
1458 self.parsed
1459 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1460 Ok(())
1461 }
1462 "target" => {
1463 let value = self.take_value(&rendered_flag, inline)?;
1464 let effective = self.target.is_none();
1468 if effective {
1469 self.target = Some(value.clone());
1470 }
1471 if value.ends_with(".json") || value.contains(['/', '\\']) {
1472 let path = PathBuf::from(value);
1473 if effective {
1474 self.required_inputs.push(path.clone());
1475 }
1476 self.parsed.push(Argument::Path {
1477 flag: rendered_flag,
1478 path,
1479 });
1480 } else {
1481 self.parsed
1482 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1483 }
1484 Ok(())
1485 }
1486 "crate-type" => {
1487 let value = self.take_value(&rendered_flag, inline)?;
1488 self.crate_types
1489 .extend(value.split(',').map(ToOwned::to_owned));
1490 self.parsed
1491 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1492 Ok(())
1493 }
1494 "emit" => {
1495 let value = self.take_value(&rendered_flag, inline)?;
1496 let emits = parse_emits(&value);
1497 self.emits.extend(emits.clone());
1498 self.parsed.push(Argument::Emit(emits));
1499 Ok(())
1500 }
1501 "out-dir" => {
1502 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1503 self.out_dir = Some(path.clone());
1504 self.parsed.push(Argument::Path {
1505 flag: rendered_flag,
1506 path,
1507 });
1508 Ok(())
1509 }
1510 "sysroot" => {
1511 let path = self.take_value(&rendered_flag, inline)?;
1512 self.parsed.push(Argument::Path {
1513 flag: rendered_flag,
1514 path: path.into(),
1515 });
1516 Ok(())
1517 }
1518 "extern" => {
1519 let value = self.take_value(&rendered_flag, inline)?;
1520 let (name, path) = value
1521 .split_once('=')
1522 .map_or((value.as_str(), None), |(name, path)| {
1523 (name, Some(PathBuf::from(path)))
1524 });
1525 if let Some(path) = &path {
1526 self.required_inputs.push(path.clone());
1527 }
1528 self.parsed.push(Argument::Extern {
1529 name: name.into(),
1530 path,
1531 });
1532 Ok(())
1533 }
1534 "remap-path-prefix" => {
1535 let value = self.take_value(&rendered_flag, inline)?;
1536 let Some((from, to)) = value.split_once('=') else {
1537 return Err(BypassReason::MissingValue(rendered_flag));
1538 };
1539 self.parsed.push(Argument::RemapPath {
1540 from: from.into(),
1541 to: to.into(),
1542 });
1543 Ok(())
1544 }
1545 "codegen" => {
1546 let value = self.take_value(&rendered_flag, inline)?;
1547 self.parse_codegen(&value)
1548 }
1549 "jobs-frontend" => {
1550 let value = self.take_value(&rendered_flag, inline)?;
1551 self.parsed
1552 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1553 Ok(())
1554 }
1555 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1556 }
1557 }
1558
1559 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1560 if let Some(attached) = value.strip_prefix("-Z") {
1561 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1562 match option.as_str() {
1563 "embed-metadata=no"
1567 | "embed-metadata=yes"
1568 | "force-unstable-if-unmarked"
1569 | "shell-argfiles"
1570 | "unstable-options" => {
1571 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1572 return Ok(());
1573 }
1574 "use-sync-unwind=yes" | "use-sync-unwind=no" => {
1577 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1578 return Ok(());
1579 }
1580 "threads" | "threads=" => {
1581 return Err(BypassReason::MissingValue("-Zthreads".into()));
1582 }
1583 _ => {}
1584 }
1585 if option == "share-generics" {
1586 return Err(BypassReason::MissingValue("-Zshare-generics".into()));
1587 }
1588 if let Some(value) = option.strip_prefix("share-generics=") {
1589 if matches!(
1590 value,
1591 "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1592 ) {
1593 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1594 return Ok(());
1595 }
1596 return Err(if value.is_empty() {
1597 BypassReason::MissingValue("-Zshare-generics".into())
1598 } else {
1599 BypassReason::UnknownFlag(format!("-Z{option}"))
1600 });
1601 }
1602 if option.starts_with("threads=") {
1603 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1604 return Ok(());
1605 }
1606 if let Some(backend) = option.strip_prefix("codegen-backend=")
1608 && !backend.is_empty()
1609 && !backend.contains(['.', '/', '\\'])
1610 {
1611 self.sysroot_backend = Some(format!("-Z{option}"));
1612 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1613 return Ok(());
1614 }
1615 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1616 }
1617 match value {
1618 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1621 "-g" | "-O" | "-v" => {
1622 self.parsed.push(Argument::Plain(value.into()));
1623 return Ok(());
1624 }
1625 _ => {}
1626 }
1627 for (short, long) in [
1628 ("-A", "--allow"),
1629 ("-W", "--warn"),
1630 ("-D", "--deny"),
1631 ("-F", "--forbid"),
1632 ] {
1633 if let Some(attached) = value.strip_prefix(short) {
1634 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1635 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1636 return Ok(());
1637 }
1638 }
1639 if let Some(attached) = value.strip_prefix("-C") {
1640 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1641 return self.parse_codegen(&option);
1642 }
1643 if let Some(attached) = value.strip_prefix("-L") {
1644 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1645 let (kind, path) = search
1646 .split_once('=')
1647 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1648 if !matches!(kind, "dependency" | "native") {
1649 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1650 }
1651 self.parsed.push(Argument::SearchPath {
1652 kind: kind.into(),
1653 path: path.into(),
1654 });
1655 return Ok(());
1656 }
1657 if let Some(attached) = value.strip_prefix("-l") {
1658 let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1659 self.parsed.push(Argument::Plain(format!("-l{library}")));
1660 self.native_libraries.push(library);
1661 return Ok(());
1662 }
1663 if let Some(attached) = value.strip_prefix("-o") {
1664 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1665 self.explicit_output = Some(path.clone().into());
1666 self.parsed.push(Argument::Path {
1667 flag: "-o".into(),
1668 path: path.into(),
1669 });
1670 return Ok(());
1671 }
1672 Err(BypassReason::UnknownFlag(value.into()))
1673 }
1674
1675 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1676 let name = value.split_once('=').map_or(value, |(name, _)| name);
1677 if name == "incremental" {
1678 return Err(BypassReason::Incremental);
1679 }
1680 if name == "profile-use" {
1681 let Some((_, profile)) = value.split_once('=') else {
1682 return Err(BypassReason::MissingValue("-Cprofile-use".into()));
1683 };
1684 if profile.is_empty() {
1685 return Err(BypassReason::MissingValue("-Cprofile-use".into()));
1686 }
1687 let path = PathBuf::from(profile);
1688 self.required_inputs.push(path.clone());
1689 self.parsed.push(Argument::Path {
1690 flag: "-Cprofile-use".into(),
1691 path,
1692 });
1693 return Ok(());
1694 }
1695 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err()
1696 && !matches!(name, "linker" | "linker-flavor")
1697 {
1698 return Err(BypassReason::UnknownCodegenOption(name.into()));
1699 }
1700 if name == "linker-plugin-lto"
1706 && let Some((_, selection)) = value.split_once('=')
1707 && !matches!(
1708 selection,
1709 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1710 )
1711 {
1712 return Err(BypassReason::UnknownCodegenOption(format!(
1713 "linker-plugin-lto={selection}"
1714 )));
1715 }
1716 if name == "link-arg"
1723 && let Some((_, option)) = value.split_once('=')
1724 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1725 && !selection.is_empty()
1726 && !selection.contains(',')
1727 && !self.parsed.iter().any(
1728 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1729 )
1730 {
1731 if !self.parsed.iter().any(
1732 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1733 ) {
1734 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1735 }
1736 return Ok(());
1737 }
1738 if name == "link-arg"
1741 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1742 && safe_install_name(option)
1743 {
1744 self.parsed.push(Argument::InstallName(option.to_owned()));
1745 return Ok(());
1746 }
1747 if matches!(name, "link-arg" | "link-args")
1752 && let Some((_, option)) = value.split_once('=')
1753 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1754 && !prefix.trim_end_matches('/').is_empty()
1755 && !prefix.contains(',')
1756 {
1757 let trailing_slash = prefix.ends_with('/');
1758 self.parsed.push(Argument::OsoPrefix {
1759 path: PathBuf::from(prefix.trim_end_matches('/')),
1760 trailing_slash,
1761 });
1762 return Ok(());
1763 }
1764 self.parsed
1765 .push(Argument::Plain(format!("--codegen={value}")));
1766 if name == "extra-filename" {
1767 self.extra_filename = value
1768 .split_once('=')
1769 .map_or(String::new(), |(_, value)| value.to_string());
1770 }
1771 Ok(())
1772 }
1773
1774 fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1789 for library in std::mem::take(&mut self.native_libraries) {
1790 if link_output != LinkOutput::Library {
1791 return Err(BypassReason::NativeLibrary(library));
1792 }
1793 let Some((kind, name)) = library.split_once('=') else {
1801 continue;
1802 };
1803 let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1804 match kind {
1805 "static" => {}
1806 "dylib" | "framework" | "link-arg" => continue,
1807 _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1808 }
1809 let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1812 let (mut verbatim, mut bundle) = (false, true);
1813 for modifier in modifiers.split(',') {
1814 match modifier {
1815 "+verbatim" => verbatim = true,
1816 "-verbatim" => verbatim = false,
1817 "+bundle" => bundle = true,
1818 "-bundle" => bundle = false,
1819 _ => {}
1820 }
1821 }
1822 if !bundle {
1823 continue;
1824 }
1825 let archive = self
1826 .find_native_static_library(name, verbatim)
1827 .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1828 .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1829 self.required_inputs.push(archive);
1830 }
1831 Ok(())
1832 }
1833
1834 fn find_native_static_library(
1846 &self,
1847 name: &str,
1848 verbatim: bool,
1849 ) -> Result<Option<PathBuf>, ()> {
1850 let file_names = if verbatim {
1851 vec![name.to_owned()]
1852 } else {
1853 match self.static_library_naming() {
1854 Some(StaticLibraryNaming::Msvc) => {
1855 vec![format!("{name}.lib"), format!("lib{name}.a")]
1856 }
1857 Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1858 None => return Err(()),
1859 }
1860 };
1861 Ok(self
1862 .parsed
1863 .iter()
1864 .filter_map(|argument| match argument {
1865 Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1866 _ => None,
1867 })
1868 .flat_map(|directory| {
1869 file_names
1870 .iter()
1871 .map(move |file_name| directory.join(file_name))
1872 })
1873 .find(|candidate| candidate.is_file()))
1874 }
1875
1876 fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1884 let Some(target) = self.target.as_deref() else {
1885 return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1886 StaticLibraryNaming::Msvc
1887 } else {
1888 StaticLibraryNaming::Unix
1889 });
1890 };
1891 if target.ends_with(".json")
1892 || target.contains(['/', '\\'])
1893 || self.options.custom_target_search
1894 {
1895 return None;
1896 }
1897 Some(if target.contains("msvc") || target.ends_with("-uefi") {
1900 StaticLibraryNaming::Msvc
1901 } else {
1902 StaticLibraryNaming::Unix
1903 })
1904 }
1905
1906 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1907 if value == "-" {
1908 return Err(BypassReason::StandardInput);
1909 }
1910 if self.source.replace(value.into()).is_some() {
1911 return Err(BypassReason::MultipleInputs);
1912 }
1913 Ok(())
1914 }
1915
1916 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1917 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1926 let builds_a_library = !self.test
1927 && !self.crate_types.is_empty()
1928 && self
1929 .crate_types
1930 .iter()
1931 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1932 let link_output = if never_links || builds_a_library {
1933 LinkOutput::Library
1934 } else if self
1935 .target
1936 .as_deref()
1937 .is_some_and(compiler_bundled_wasm_target)
1938 && ((self.test && self.crate_types.is_empty())
1939 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1940 {
1941 if self.parsed.iter().any(|argument| match argument {
1942 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1943 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1944 !matches!(
1945 value.rsplit_once('=').map(|(_, value)| value),
1946 Some("y" | "yes" | "on" | "true")
1947 )
1948 }
1949 _ => false,
1950 }) {
1951 return Err(BypassReason::UnknownCodegenOption(
1952 "link-self-contained".into(),
1953 ));
1954 }
1955 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1956 && self.parsed.iter().any(|argument| {
1957 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1958 })
1959 {
1960 return Err(BypassReason::UnknownCodegenOption(
1961 "target-feature=-crt-static".into(),
1962 ));
1963 }
1964 LinkOutput::WasmExecutable
1968 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1969 self.check_native_link_is_portable()?;
1970 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1971 LinkOutput::NativeProcMacro
1972 } else if matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib") {
1973 LinkOutput::NativeSharedLibrary
1974 } else {
1975 LinkOutput::NativeExecutable
1976 }
1977 } else if self.test {
1978 return Err(BypassReason::UnsupportedCrateType("test".into()));
1979 } else {
1980 return Err(BypassReason::UnsupportedCrateType(
1981 self.crate_types
1982 .iter()
1983 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1984 .cloned()
1985 .unwrap_or_else(|| "bin".into()),
1986 ));
1987 };
1988 if link_output != LinkOutput::Library
1997 && let Some(option) = self.first_link_argument(link_output)
1998 {
1999 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
2000 }
2001 if self
2002 .parsed
2003 .iter()
2004 .any(|arg| matches!(arg, Argument::InstallName(_)))
2005 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
2006 {
2007 return Err(BypassReason::UnmodeledLinkArgument(
2008 "link-arg=-Wl,-install_name".into(),
2009 ));
2010 }
2011 if let Some(selection) = self.parsed.iter().find_map(|argument| {
2018 let Argument::FuseLd(selection) = argument else {
2019 return None;
2020 };
2021 Some(selection.as_str())
2022 }) && !matches!(
2023 link_output,
2024 LinkOutput::Library
2025 | LinkOutput::NativeExecutable
2026 | LinkOutput::NativeProcMacro
2027 | LinkOutput::NativeSharedLibrary
2028 ) {
2029 return Err(BypassReason::UnmodeledLinkArgument(format!(
2030 "link-arg=-fuse-ld={selection}"
2031 )));
2032 }
2033 if self.parsed.iter().any(|argument| {
2034 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
2035 }) && !matches!(
2036 link_output,
2037 LinkOutput::Library
2038 | LinkOutput::NativeExecutable
2039 | LinkOutput::NativeProcMacro
2040 | LinkOutput::NativeSharedLibrary
2041 ) {
2042 return Err(BypassReason::UnknownCodegenOption("linker".into()));
2043 }
2044 if link_output != LinkOutput::Library
2049 && self.parsed.iter().any(|argument| {
2050 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker-flavor="))
2051 })
2052 {
2053 return Err(BypassReason::UnknownCodegenOption("linker-flavor".into()));
2054 }
2055 if !matches!(
2059 link_output,
2060 LinkOutput::Library
2061 | LinkOutput::NativeExecutable
2062 | LinkOutput::NativeProcMacro
2063 | LinkOutput::NativeSharedLibrary
2064 ) && self
2065 .parsed
2066 .iter()
2067 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
2068 {
2069 return Err(BypassReason::UnmodeledLinkArgument(
2070 "link-arg=-Wl,-oso_prefix".into(),
2071 ));
2072 }
2073 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
2074 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
2075 _ => None,
2076 }) {
2077 return Err(BypassReason::UnresolvedExtern(name.clone()));
2078 }
2079 if let Some(emit) = self
2080 .emits
2081 .iter()
2082 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
2083 {
2084 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
2085 }
2086 if !self
2087 .emits
2088 .iter()
2089 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
2090 {
2091 return Err(BypassReason::NoCacheableOutput);
2092 }
2093 Ok(link_output)
2094 }
2095}
2096
2097impl Parser<'_> {
2098 fn first_link_argument(&self, link_output: LinkOutput) -> Option<&str> {
2101 self.parsed.iter().find_map(|argument| {
2102 let Argument::Plain(value) = argument else {
2103 return None;
2104 };
2105 let option = value.strip_prefix("--codegen=")?;
2106 let (name, value) = option.split_once('=').unwrap_or((option, ""));
2107 (matches!(name, "link-arg" | "link-args")
2108 && !(name == "link-arg" && elf_keyword_is_modeled(link_output, value))
2109 && !msvc_options_are_modeled(link_output, name, value))
2110 .then_some(option)
2111 })
2112 }
2113
2114 fn oso_prefix_covers_outputs(&self) -> bool {
2123 let Some(directory) = self
2124 .out_dir
2125 .as_deref()
2126 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2127 else {
2128 return false;
2129 };
2130 if !directory.is_absolute() {
2131 return false;
2132 }
2133 let directory = normalize_components(directory);
2134 self.parsed.iter().any(|argument| {
2135 let Argument::OsoPrefix { path, .. } = argument else {
2136 return false;
2137 };
2138 path.is_absolute()
2139 && (directory.starts_with(normalize_components(path))
2140 || std::fs::canonicalize(&directory)
2141 .ok()
2142 .zip(std::fs::canonicalize(path).ok())
2143 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2144 })
2145 }
2146
2147 fn links_a_native_artifact(&self) -> bool {
2154 self.target.is_none()
2155 && self.emits.iter().any(|emit| emit.kind == "link")
2160 && ((self.test && self.crate_types.is_empty())
2161 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro"))
2162 || (cfg!(target_os = "linux")
2163 && matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib")))
2164 }
2165
2166 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2173 for argument in &self.parsed {
2174 let Argument::Plain(value) = argument else {
2175 continue;
2176 };
2177 let (name, value) = if value == "-g" {
2180 ("debuginfo", Some("2"))
2181 } else if let Some(option) = value.strip_prefix("--codegen=") {
2182 match option.split_once('=') {
2183 Some((name, value)) => (name, Some(value)),
2184 None => (option, None),
2188 }
2189 } else {
2190 continue;
2191 };
2192 let unportable = match name {
2193 "split-debuginfo" => match value {
2199 Some("off") => false,
2200 Some("unpacked") if cfg!(target_os = "macos") => {
2201 !self.oso_prefix_covers_outputs()
2202 }
2203 _ => true,
2204 },
2205 "debuginfo" if cfg!(target_os = "macos") => {
2214 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2215 }
2216 "rpath" => is_enabled(value),
2224 "prefer-dynamic" => {
2225 is_enabled(value)
2226 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2227 }
2228 "link-self-contained" => true,
2231 _ => false,
2232 };
2233 if unportable {
2234 return Err(BypassReason::UnportableNativeLink(match value {
2235 Some(value) => format!("{name}={value}"),
2236 None => name.to_owned(),
2237 }));
2238 }
2239 }
2240 Ok(())
2241 }
2242}
2243
2244fn safe_install_name(name: &str) -> bool {
2245 !name.is_empty()
2246 && name
2247 .bytes()
2248 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2249}
2250
2251fn is_enabled(value: Option<&str>) -> bool {
2254 matches!(value, None | Some("y" | "yes" | "on" | "true"))
2255}
2256
2257fn elf_keyword_is_modeled(link_output: LinkOutput, value: &str) -> bool {
2267 cfg!(target_os = "linux")
2268 && matches!(
2269 link_output,
2270 LinkOutput::NativeExecutable
2271 | LinkOutput::NativeProcMacro
2272 | LinkOutput::NativeSharedLibrary
2273 )
2274 && value.strip_prefix("-Wl,-z,").is_some_and(|keyword| {
2275 matches!(
2276 keyword,
2277 "defs"
2278 | "lazy"
2279 | "nodelete"
2280 | "nodlopen"
2281 | "noexecstack"
2282 | "norelro"
2283 | "now"
2284 | "origin"
2285 | "relro"
2286 )
2287 })
2288}
2289
2290fn msvc_options_are_modeled(link_output: LinkOutput, name: &str, value: &str) -> bool {
2306 cfg!(target_env = "msvc")
2307 && matches!(
2308 link_output,
2309 LinkOutput::NativeExecutable
2310 | LinkOutput::NativeProcMacro
2311 | LinkOutput::NativeSharedLibrary
2312 )
2313 && match name {
2314 "link-arg" => msvc_option_reads_no_file(value),
2315 "link-args" => {
2318 let mut options = value.split_ascii_whitespace().peekable();
2319 options.peek().is_some() && options.all(msvc_option_reads_no_file)
2320 }
2321 _ => false,
2322 }
2323}
2324
2325fn msvc_option_reads_no_file(option: &str) -> bool {
2329 let Some(option) = option.strip_prefix(['/', '-']) else {
2330 return false;
2331 };
2332 if option.eq_ignore_ascii_case("brepro") {
2333 return true;
2334 }
2335 let Some((name, sizes)) = option.split_once(':') else {
2336 return false;
2337 };
2338 let is_size = |size: &str| match size.strip_prefix("0x").or_else(|| size.strip_prefix("0X")) {
2341 Some(hex) => !hex.is_empty() && hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
2342 None => !size.is_empty() && size.bytes().all(|byte| byte.is_ascii_digit()),
2343 };
2344 name.eq_ignore_ascii_case("stack")
2345 && sizes.split(',').count() <= 2
2346 && sizes.split(',').all(is_size)
2347}
2348
2349fn compiler_bundled_wasm_target(target: &str) -> bool {
2350 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2351}
2352
2353fn parse_emits(value: &str) -> Vec<Emit> {
2354 value
2355 .split(',')
2356 .map(|emit| {
2357 let (kind, path) = emit
2358 .split_once('=')
2359 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2360 Emit {
2361 kind: kind.into(),
2362 path,
2363 }
2364 })
2365 .collect()
2366}
2367
2368struct ActionBuilder<'a> {
2369 invocation: &'a RustcInvocation,
2370 context: ActionContext,
2371 mappings: Vec<SharedPathMapping>,
2372 aliases: PathAliases,
2376 linker: Option<LinkerIdentity>,
2377}
2378
2379impl<'a> ActionBuilder<'a> {
2380 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2381 context.path_mappings = PathMapping::ordered(&context.path_mappings);
2382 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2383 Self {
2384 linker: None,
2385 invocation,
2386 mappings,
2387 aliases: PathAliases::default(),
2388 context,
2389 }
2390 }
2391
2392 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2393 self.linker = linker;
2394 self
2395 }
2396
2397 fn build(self) -> Result<RustcAction, BypassReason> {
2398 self.validate_mappings()?;
2399 let invocation = self.invocation_descriptor()?;
2400 let environment = self.environment_descriptor()?;
2401
2402 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2403 for input in &self.context.inputs {
2404 input
2405 .digest
2406 .validate()
2407 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2408 let path = self.normalize_input_path(&input.path)?;
2409 if inputs
2410 .insert(path.clone(), input.digest.clone())
2411 .is_some_and(|existing| existing != input.digest)
2412 {
2413 return Err(BypassReason::ConflictingInput(path));
2414 }
2415 }
2416 let required = self
2417 .invocation
2418 .required_inputs
2419 .iter()
2420 .map(|path| self.normalize_input_path(path))
2421 .collect::<Result<BTreeSet<_>, _>>()?;
2422 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2423 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2424 }
2425 let inputs = inputs
2426 .into_iter()
2427 .map(|(path, digest)| InputDescriptor { path, digest })
2428 .collect();
2429 if self.invocation.links_natively() && self.linker.is_none() {
2432 return Err(BypassReason::UnportableNativeLink(
2433 "linker identity is unknown".into(),
2434 ));
2435 }
2436 let descriptor = ActionDescriptor {
2437 version: ACTION_SCHEMA_VERSION,
2438 kind: "rustc",
2439 adapter_version: ADAPTER_VERSION,
2440 compiler: invocation.compiler,
2441 arguments: invocation.arguments,
2442 environment,
2443 inputs,
2444 linker: self.linker.clone(),
2445 };
2446 let bytes = canonical_json(&descriptor)
2447 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2448 let digest = CacheDigest::blake3(&bytes);
2449 Ok(RustcAction { digest, bytes })
2450 }
2451
2452 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2453 self.validate_mappings()?;
2454 let arguments = self
2455 .invocation
2456 .arguments
2457 .iter()
2458 .map(|argument| self.normalize_argument(argument))
2459 .collect::<Result<Vec<_>, _>>()?;
2460 let required_inputs = self
2461 .invocation
2462 .required_inputs
2463 .iter()
2464 .map(|path| self.normalize_path(path))
2465 .collect::<Result<BTreeSet<_>, _>>()?
2466 .into_iter()
2467 .collect();
2468 Ok(InvocationDescriptor {
2469 version: ACTION_SCHEMA_VERSION,
2470 kind: "rustc",
2471 adapter_version: ADAPTER_VERSION,
2472 compiler: CompilerDescriptor {
2473 toolchain: self.context.compiler.toolchain.clone(),
2474 rustc_version: self.context.compiler.rustc_version.clone(),
2475 host: self.context.compiler.host.clone(),
2476 driver: self.context.compiler.driver.clone(),
2477 },
2478 arguments,
2479 required_inputs,
2480 })
2481 }
2482
2483 fn validate_mappings(&self) -> Result<(), BypassReason> {
2484 if !self.context.working_dir.is_absolute() {
2485 return Err(BypassReason::RelativeWorkingDirectory(
2486 self.context.working_dir.clone(),
2487 ));
2488 }
2489 let mut roots = BTreeSet::new();
2490 let mut placeholders = BTreeSet::new();
2491 for mapping in &self.mappings {
2492 if !mapping.root.is_absolute() {
2493 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2494 }
2495 if mapping.placeholder.is_empty()
2496 || !mapping
2497 .placeholder
2498 .bytes()
2499 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2500 || !roots.insert(normalize_components(&mapping.root))
2501 || !placeholders.insert(&mapping.placeholder)
2502 {
2503 return Err(BypassReason::InvalidPathPlaceholder(
2504 mapping.placeholder.clone(),
2505 ));
2506 }
2507 }
2508 Ok(())
2509 }
2510
2511 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2512 match argument {
2513 Argument::Plain(value) => Ok(value.clone()),
2514 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2515 Argument::SearchPath { kind, path } => {
2516 let text = self.normalize_path(path)?;
2517 Ok(format!("-L{kind}={text}"))
2518 }
2519 Argument::Extern { name, path } => match path {
2520 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2521 None => Ok(format!("--extern={name}")),
2522 },
2523 Argument::Emit(emits) => Ok(format!(
2524 "--emit={}",
2525 emits
2526 .iter()
2527 .map(|emit| match &emit.path {
2528 Some(path) => self
2529 .normalize_path(path)
2530 .map(|path| format!("{}={path}", emit.kind)),
2531 None => Ok(emit.kind.clone()),
2532 })
2533 .collect::<Result<Vec<_>, _>>()?
2534 .join(",")
2535 )),
2536 Argument::RemapPath { from, to } => Ok(format!(
2537 "--remap-path-prefix={}={}",
2538 self.normalize_path(from)?,
2539 to
2540 )),
2541 Argument::InstallName(name) => Ok(format!(
2542 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2543 )),
2544 Argument::OsoPrefix {
2545 path,
2546 trailing_slash,
2547 } => Ok(format!(
2548 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2549 self.normalize_path(path)?,
2550 if *trailing_slash { "/" } else { "" }
2551 )),
2552 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2553 }
2554 }
2555
2556 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2563 self.context
2564 .environment
2565 .iter()
2566 .map(|(name, value)| {
2567 let value = match value {
2568 Some(value) if self.context.portable_environment.contains(name) => {
2569 Some(self.normalize_path(Path::new(value))?)
2570 }
2571 value => value.clone(),
2572 };
2573 Ok((name.clone(), value))
2574 })
2575 .collect()
2576 }
2577
2578 fn normalize_input_path(&self, path: &Path) -> Result<String, BypassReason> {
2579 let absolute = normalize_components(&self.context.working_dir.join(path));
2580 for directory in self.invocation.native_search_paths() {
2581 let directory = normalize_components(&self.context.working_dir.join(directory));
2582 if let Ok(relative) = absolute.strip_prefix(&directory) {
2583 let suffix = relative
2589 .components()
2590 .map(|component| {
2591 component
2592 .as_os_str()
2593 .to_str()
2594 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf()))
2595 })
2596 .collect::<Result<Vec<_>, _>>()?
2597 .join("/");
2598 if !suffix.is_empty() {
2599 return Ok(format!("{}/{}", self.normalize_path(&directory)?, suffix));
2600 }
2601 }
2602 }
2603 self.normalize_path(path)
2604 }
2605
2606 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2607 normalize_resolved_shared_path_with(
2608 &self.aliases,
2609 path,
2610 &self.context.working_dir,
2611 &self.mappings,
2612 )
2613 .map_err(Into::into)
2614 }
2615}
2616
2617fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2621 directories.iter().any(|directory| {
2622 path.len() > directory.len()
2623 && path.as_bytes()[directory.len()] == b'/'
2624 && path.starts_with(directory)
2625 })
2626}
2627
2628fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2629 for mapping in mappings {
2630 let prefix = format!("${{{}}}", mapping.placeholder);
2631 let suffix = if value == prefix {
2632 ""
2633 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2634 suffix
2635 } else {
2636 continue;
2637 };
2638 if !mapping.root.is_absolute()
2639 || (!suffix.is_empty()
2640 && suffix.split('/').any(|component| {
2641 component.is_empty()
2642 || matches!(component, "." | "..")
2643 || component.contains('\\')
2644 }))
2645 {
2646 return Err(BypassReason::InvalidPredictedInput(value.into()));
2647 }
2648 let mut path = normalize_components(&mapping.root);
2649 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2650 return Ok(path);
2651 }
2652 Err(BypassReason::InvalidPredictedInput(value.into()))
2653}
2654
2655fn normalize_components(path: &Path) -> PathBuf {
2656 let mut normalized = PathBuf::new();
2657 for component in path.components() {
2658 match component {
2659 Component::CurDir => {}
2660 Component::ParentDir => {
2661 normalized.pop();
2662 }
2663 component => normalized.push(component.as_os_str()),
2664 }
2665 }
2666 normalized
2667}
2668
2669fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2670 if path.is_absolute() {
2671 normalize_components(path)
2672 } else {
2673 normalize_components(&working_dir.join(path))
2674 }
2675}
2676
2677#[cfg(test)]
2678#[path = "rustc_cache_tests.rs"]
2679mod tests;