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("Cargo has not recorded the build script's declarations yet: {0}")]
314 BuildScriptDeclarationsPending(PathBuf),
315 #[error("build-script inputs changed during compilation (declarations: {0})")]
318 BuildScriptInputsChanged(PathBuf),
319 #[error("compiler input changed after discovery: {0}")]
321 InputChanged(PathBuf),
322 #[error("compiler input was modified during compilation: {0}")]
324 InputModifiedDuringCompilation(PathBuf),
325 #[error("discovered inputs were collected from a different working directory")]
327 DiscoveryWorkingDirectory,
328 #[error("compiler environment input has conflicting values: {0}")]
330 ConflictingEnvironment(String),
331 #[error("failed to serialize the rustc action: {0}")]
333 Serialization(String),
334 #[error("rustc action prediction is unsupported")]
336 UnsupportedPrediction,
337 #[error("rustc action prediction contains an invalid input path: {0}")]
339 InvalidPredictedInput(String),
340}
341
342#[derive(Debug, Clone, PartialEq, Eq)]
343enum Argument {
344 Plain(String),
345 Path {
346 flag: String,
347 path: PathBuf,
348 },
349 SearchPath {
350 kind: String,
351 path: PathBuf,
352 },
353 Extern {
354 name: String,
355 path: Option<PathBuf>,
356 },
357 Emit(Vec<Emit>),
358 RemapPath {
359 from: PathBuf,
360 to: String,
361 },
362 InstallName(String),
364 OsoPrefix {
367 path: PathBuf,
368 trailing_slash: bool,
369 },
370 FuseLd(String),
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
377struct Emit {
378 kind: String,
379 path: Option<PathBuf>,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct RustcInvocation {
388 arguments: Vec<Argument>,
389 source: PathBuf,
390 required_inputs: Vec<PathBuf>,
391 crate_name: String,
392 extra_filename: String,
393 out_dir: Option<PathBuf>,
394 explicit_output: Option<PathBuf>,
395 emits: Vec<Emit>,
396 target: Option<String>,
397 link_output: LinkOutput,
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401enum LinkOutput {
402 Library,
403 WasmExecutable,
404 NativeExecutable,
405 NativeProcMacro,
406 NativeSharedLibrary,
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
419#[non_exhaustive]
420pub struct ParseOptions {
421 pub cache_native_links: bool,
424 pub custom_target_search: bool,
430}
431
432impl ParseOptions {
433 pub fn caching_native_links(enabled: bool) -> Self {
435 Self {
436 cache_native_links: enabled,
437 ..Self::default()
438 }
439 }
440
441 pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
444 self.custom_target_search = enabled;
445 self
446 }
447}
448
449#[derive(Debug, Clone, PartialEq, Eq)]
451pub struct RustcOutputs {
452 pub directory: PathBuf,
454 pub files: Vec<PathBuf>,
456 pub dep_info: PathBuf,
458}
459
460impl RustcInvocation {
461 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
470 Self::parse_with(arguments, ParseOptions::default())
471 }
472
473 pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
478 Ok(expand_response_files(arguments)?.arguments)
479 }
480
481 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
484 let expanded = expand_response_files(arguments)?;
485 Parser::new(&expanded.arguments, options).parse()
486 }
487
488 pub fn links_natively(&self) -> bool {
491 matches!(
492 self.link_output,
493 LinkOutput::NativeExecutable
494 | LinkOutput::NativeProcMacro
495 | LinkOutput::NativeSharedLibrary
496 )
497 }
498
499 pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
503 if !cfg!(target_os = "macos")
504 || self.link_output != LinkOutput::NativeProcMacro
505 || self
506 .arguments
507 .iter()
508 .any(|arg| matches!(arg, Argument::InstallName(_)))
509 {
510 return None;
511 }
512 let file = outputs
513 .files
514 .iter()
515 .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
516 let name = file.file_name()?.to_str()?;
517 if !safe_install_name(name) {
518 return None;
519 }
520 Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
521 }
522
523 pub fn linker_override(&self) -> Option<&Path> {
526 self.arguments.iter().find_map(|argument| {
527 let Argument::Plain(value) = argument else {
528 return None;
529 };
530 value.strip_prefix("--codegen=linker=").map(Path::new)
531 })
532 }
533
534 pub fn fuse_ld(&self) -> Option<&str> {
537 self.arguments.iter().find_map(|argument| {
538 let Argument::FuseLd(selection) = argument else {
539 return None;
540 };
541 Some(selection.as_str())
542 })
543 }
544
545 fn emits_windows_pdb(&self) -> bool {
546 if !cfg!(windows) || !self.links_natively() {
547 return false;
548 }
549 let mut enabled = false;
550 for argument in &self.arguments {
551 let Argument::Plain(value) = argument else {
552 continue;
553 };
554 if value == "-g" {
555 enabled = true;
556 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
557 enabled = !matches!(value, "0" | "none");
558 }
559 }
560 enabled
561 }
562
563 pub fn native_search_paths(&self) -> impl Iterator<Item = &Path> {
567 self.arguments.iter().filter_map(|argument| match argument {
568 Argument::SearchPath { kind, path } if kind == "native" => Some(path.as_path()),
569 _ => None,
570 })
571 }
572
573 pub fn source(&self) -> &Path {
575 &self.source
576 }
577
578 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
584 self.required_inputs
585 .iter()
586 .map(|path| absolute_path(path, working_dir))
587 .collect()
588 }
589
590 pub fn target(&self) -> Option<&str> {
592 self.target.as_deref()
593 }
594
595 pub fn crate_name(&self) -> &str {
597 &self.crate_name
598 }
599
600 pub fn remaps_working_directory(&self) -> bool {
609 self.arguments.iter().any(|argument| {
610 matches!(argument, Argument::Plain(flag) if flag.starts_with("-Zremap-cwd-prefix="))
611 })
612 }
613
614 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
627 let linked = self
628 .arguments
629 .iter()
630 .filter_map(|argument| match argument {
631 Argument::Extern {
632 path: Some(path), ..
633 } => Some(path.as_path()),
634 _ => None,
635 })
636 .collect::<BTreeSet<_>>();
637 let owned = discovered
638 .inputs
639 .iter()
640 .filter(|input| {
641 !linked.contains(input.path.as_path()) && !discovered.is_native_only(&input.path)
642 })
643 .map(|input| (input.path.as_path(), &input.digest))
644 .collect::<BTreeMap<_, _>>();
645 let mut bytes = Vec::new();
646 for (path, digest) in owned {
647 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
648 bytes.push(0);
649 bytes.extend_from_slice(digest.key().as_bytes());
650 bytes.push(0);
651 }
652 CacheDigest::blake3(&bytes)
653 }
654
655 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
660 if !working_dir.is_absolute() {
661 return Err(BypassReason::RelativeWorkingDirectory(
662 working_dir.to_path_buf(),
663 ));
664 }
665 let explicit_output = self
666 .explicit_output
667 .as_deref()
668 .map(|path| absolute_path(path, working_dir));
669 let output_directory = explicit_output
670 .as_deref()
671 .and_then(Path::parent)
672 .map(Path::to_path_buf)
673 .or_else(|| {
674 self.out_dir
675 .as_deref()
676 .map(|path| absolute_path(path, working_dir))
677 })
678 .unwrap_or_else(|| normalize_components(working_dir));
679 if let Some(output) = &explicit_output
683 && self.emits.iter().any(|emit| {
684 emit.path.is_none()
685 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
686 })
687 {
688 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
689 }
690 let mut files = BTreeSet::new();
691 let mut dep_info = None;
692 for emit in &self.emits {
693 if emit.kind == "dep-info" {
694 let path = emit.path.as_ref().map_or_else(
695 || {
696 explicit_output.clone().map_or_else(
697 || {
698 output_directory
699 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
700 },
701 |path| path.with_extension("d"),
702 )
703 },
704 |path| absolute_path(path, working_dir),
705 );
706 if path.file_name().is_none() {
707 return Err(BypassReason::InvalidOutputPath(path));
708 }
709 dep_info = Some(path);
710 continue;
711 }
712 let (prefix, extension) = match emit.kind.as_str() {
713 "link" => match self.link_output {
714 LinkOutput::Library => ("lib", "rlib"),
715 LinkOutput::WasmExecutable => ("", "wasm"),
716 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
717 LinkOutput::NativeProcMacro | LinkOutput::NativeSharedLibrary => (
718 std::env::consts::DLL_PREFIX,
719 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
720 ),
721 },
722 "metadata" => ("lib", "rmeta"),
723 _ => continue,
724 };
725 let path = if let Some(path) = &emit.path {
726 absolute_path(path, working_dir)
727 } else {
728 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
729 output_directory.join(if extension.is_empty() {
730 name
731 } else {
732 format!("{name}.{extension}")
733 })
734 };
735 if path.file_name().is_none() {
736 return Err(BypassReason::InvalidOutputPath(path));
737 }
738 if path.parent() != Some(output_directory.as_path()) {
739 return Err(BypassReason::SplitOutputDirectories);
740 }
741 let has_library_extension = path
746 .extension()
747 .and_then(|extension| extension.to_str())
748 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
749 || (cfg!(windows)
750 && path
751 .file_stem()
752 .and_then(|stem| Path::new(stem).extension())
753 .and_then(|extension| extension.to_str())
754 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
755 if emit.kind == "link"
756 && !matches!(self.link_output, LinkOutput::Library)
757 && has_library_extension
758 {
759 return Err(BypassReason::AmbiguousOutputName(path));
760 }
761 if emit.kind == "link" && self.emits_windows_pdb() {
762 files.insert(path.with_extension("pdb"));
763 }
764 files.insert(path);
765 }
766 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
767 if dep_info.parent() != Some(output_directory.as_path()) {
768 return Err(BypassReason::SplitOutputDirectories);
769 }
770 Ok(RustcOutputs {
771 directory: output_directory,
772 files: files.into_iter().collect(),
773 dep_info,
774 })
775 }
776
777 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
783 self.action_linked_by(context, None)
784 }
785
786 pub fn action_linked_by(
795 &self,
796 context: ActionContext,
797 linker: Option<LinkerIdentity>,
798 ) -> Result<RustcAction, BypassReason> {
799 ActionBuilder::new(self, context).linked_by(linker).build()
800 }
801
802 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
804 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
805 let bytes = canonical_json(&descriptor)
806 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
807 Ok(CacheDigest::blake3(&bytes))
808 }
809
810 pub fn prediction(
813 &self,
814 context: &ActionContext,
815 discovered: &DiscoveredInputs,
816 ) -> Result<RustcInputPrediction, BypassReason> {
817 let builder = ActionBuilder::new(self, context.clone());
818 builder.validate_mappings()?;
819 let mut native_directories = BTreeSet::new();
820 for argument in &self.arguments {
821 if let Argument::SearchPath { kind, path } = argument
822 && kind == "native"
823 {
824 native_directories.insert(builder.normalize_path(path)?);
825 }
826 }
827 let mut inputs = BTreeSet::new();
839 for input in &discovered.inputs {
840 let normalized = builder.normalize_path(&input.path)?;
841 if !under_any_directory(&normalized, &native_directories)
842 || !discovered.is_native_only(&input.path)
843 {
844 inputs.insert(normalized);
845 }
846 }
847 inputs.extend(
848 native_directories
849 .into_iter()
850 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
851 );
852 Ok(RustcInputPrediction {
853 version: 4,
854 inputs: inputs.into_iter().collect(),
855 environment: discovered.environment.keys().cloned().collect(),
856 compiler_duration_ns: 0,
857 crate_name: String::new(),
858 })
859 }
860}
861
862pub fn is_build_script_crate_name(crate_name: &str) -> bool {
866 crate_name
867 .strip_prefix("build_script_")
868 .is_some_and(|stem| !stem.is_empty())
869}
870
871impl RustcOutputs {
872 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
882 is_build_script_crate_name(crate_name)
883 .then(|| self.files.iter().find(|path| self.is_executable(path)))
884 .flatten()
885 .map(PathBuf::as_path)
886 }
887
888 pub fn is_executable(&self, path: &Path) -> bool {
896 self.files.iter().any(|output| output == path)
897 && !matches!(
898 path.extension().and_then(|extension| extension.to_str()),
899 Some("rlib" | "rmeta")
900 )
901 }
902}
903
904#[derive(Debug, Clone, PartialEq, Eq)]
905pub struct PathMapping {
907 pub root: PathBuf,
909 pub placeholder: String,
911}
912
913impl PathMapping {
914 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
916 Self {
917 root: root.into(),
918 placeholder: placeholder.into(),
919 }
920 }
921
922 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
925 let mut ordered = mappings.to_vec();
926 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
927 ordered
928 }
929}
930
931fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
932 mappings
933 .iter()
934 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
935 .collect()
936}
937
938pub fn normalize_mapped_path(
945 path: &Path,
946 working_dir: &Path,
947 mappings: &[PathMapping],
948) -> Result<String, BypassReason> {
949 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
950}
951
952#[derive(Debug, Clone, PartialEq, Eq)]
953pub struct CompilerIdentity {
955 pub toolchain: String,
957 pub rustc_version: String,
959 pub host: String,
961 pub driver: Option<String>,
963}
964
965#[derive(Debug, Clone, PartialEq, Eq)]
967pub struct ActionInput {
968 pub path: PathBuf,
970 pub digest: CacheDigest,
972}
973
974#[derive(Debug, Clone, PartialEq, Eq)]
976pub struct ActionContext {
977 pub compiler: CompilerIdentity,
979 pub working_dir: PathBuf,
981 pub path_mappings: Vec<PathMapping>,
983 pub environment: BTreeMap<String, Option<String>>,
985 pub portable_environment: BTreeSet<String>,
992 pub inputs: Vec<ActionInput>,
994}
995
996#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1006#[serde(deny_unknown_fields)]
1007pub struct LinkerIdentity {
1008 pub driver: String,
1010 pub driver_version: String,
1012 pub linker_version: String,
1014 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
1016 pub crt_objects: BTreeMap<String, CacheDigest>,
1017 #[serde(skip_serializing_if = "Option::is_none", default)]
1019 pub sdk: Option<String>,
1020 #[serde(skip_serializing_if = "Option::is_none", default)]
1022 pub deployment_target: Option<String>,
1023}
1024
1025#[derive(Debug, Clone, PartialEq, Eq)]
1027pub struct RustcAction {
1028 pub digest: CacheDigest,
1030 pub bytes: Vec<u8>,
1032}
1033
1034#[derive(Debug, Clone, PartialEq, Eq)]
1037pub struct RustcInputPrediction {
1038 pub version: u8,
1040 pub inputs: Vec<String>,
1042 pub environment: Vec<String>,
1044 pub compiler_duration_ns: u64,
1047 pub crate_name: String,
1049}
1050
1051#[derive(Serialize, Deserialize)]
1052#[serde(deny_unknown_fields)]
1053struct RustcInputPredictionWire {
1054 version: u8,
1055 inputs: Vec<String>,
1056 environment: Vec<String>,
1057 #[serde(default, skip_serializing_if = "is_zero")]
1058 compiler_duration_ns: u64,
1059 #[serde(default, skip_serializing_if = "String::is_empty")]
1060 crate_name: String,
1061}
1062
1063impl Serialize for RustcInputPrediction {
1064 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1065 where
1066 S: Serializer,
1067 {
1068 RustcInputPredictionWire {
1069 version: self.version,
1070 inputs: if self.version == 4 {
1071 compact_prediction_inputs(&self.inputs)
1072 } else {
1073 self.inputs.clone()
1074 },
1075 environment: self.environment.clone(),
1076 compiler_duration_ns: self.compiler_duration_ns,
1077 crate_name: self.crate_name.clone(),
1078 }
1079 .serialize(serializer)
1080 }
1081}
1082
1083impl<'de> Deserialize<'de> for RustcInputPrediction {
1084 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1085 where
1086 D: Deserializer<'de>,
1087 {
1088 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1089 let inputs = if wire.version == 4 {
1090 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1091 } else {
1092 wire.inputs
1093 };
1094 Ok(Self {
1095 version: wire.version,
1096 inputs,
1097 environment: wire.environment,
1098 compiler_duration_ns: wire.compiler_duration_ns,
1099 crate_name: wire.crate_name,
1100 })
1101 }
1102}
1103
1104fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1105 let mut previous = "";
1106 inputs
1107 .iter()
1108 .map(|input| {
1109 let mut shared = previous
1110 .bytes()
1111 .zip(input.bytes())
1112 .take_while(|(left, right)| left == right)
1113 .count();
1114 while !input.is_char_boundary(shared) {
1115 shared -= 1;
1116 }
1117 let compact = format!("{shared}:{}", &input[shared..]);
1118 previous = input;
1119 compact
1120 })
1121 .collect()
1122}
1123
1124fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1125 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1126 let mut decoded_bytes = 0_usize;
1127 for input in inputs {
1128 let (shared_text, suffix) = input
1129 .split_once(':')
1130 .ok_or("compact rustc prediction input has no prefix length")?;
1131 let shared: usize = shared_text
1132 .parse()
1133 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1134 if shared.to_string() != shared_text
1135 || shared > expanded.last().map_or(0, String::len)
1136 || expanded
1137 .last()
1138 .is_some_and(|previous| !previous.is_char_boundary(shared))
1139 {
1140 return Err("compact rustc prediction prefix is not canonical");
1141 }
1142 let mut path = expanded
1143 .last()
1144 .map_or_else(String::new, |previous| previous[..shared].to_string());
1145 path.push_str(suffix);
1146 decoded_bytes = decoded_bytes
1147 .checked_add(path.len())
1148 .ok_or("compact rustc prediction is too large")?;
1149 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1150 return Err("compact rustc prediction is too large");
1151 }
1152 expanded.push(path);
1153 }
1154 Ok(expanded)
1155}
1156
1157fn is_zero(value: &u64) -> bool {
1158 *value == 0
1159}
1160
1161impl RustcInputPrediction {
1162 pub fn discover(
1165 &self,
1166 working_dir: &Path,
1167 path_mappings: &[PathMapping],
1168 digests: &dyn FileDigestCache,
1169 ) -> Result<DiscoveredInputs, BypassReason> {
1170 if !matches!(self.version, 2..=4) {
1171 return Err(BypassReason::UnsupportedPrediction);
1172 }
1173 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1174 return Err(BypassReason::UnsupportedPrediction);
1175 }
1176 let mut paths = BTreeSet::new();
1177 let mut named = BTreeSet::new();
1178 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1179 let mut native_bytes = 0_u64;
1180 let mut counted = BTreeSet::new();
1181 for path in &self.inputs {
1182 if self.version >= 3
1183 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1184 {
1185 let directory = denormalize_path(path, path_mappings)?;
1186 dep_info::collect_native_directory(
1187 &directory,
1188 &admitted_roots,
1189 &mut paths,
1190 &mut native_bytes,
1191 &mut counted,
1192 )?;
1193 } else {
1194 let path = denormalize_path(path, path_mappings)?;
1195 named.insert(path.clone());
1196 paths.insert(path);
1197 }
1198 }
1199 let native_only = paths.difference(&named).cloned().collect();
1200 let environment = self
1201 .environment
1202 .iter()
1203 .map(|name| {
1204 if name.is_empty() || name.contains(['=', '\0']) {
1205 return Err(BypassReason::UnsupportedPrediction);
1206 }
1207 let value = std::env::var_os(name)
1208 .map(|value| {
1209 value
1210 .into_string()
1211 .map_err(|_| BypassReason::UnsupportedPrediction)
1212 })
1213 .transpose()?;
1214 Ok((name.clone(), value))
1215 })
1216 .collect::<Result<BTreeMap<_, _>, _>>()?;
1217 Ok(
1218 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)?
1219 .with_native_only(native_only),
1220 )
1221 }
1222}
1223
1224#[derive(Serialize)]
1225struct ActionDescriptor {
1226 version: u8,
1227 kind: &'static str,
1228 adapter_version: u8,
1229 compiler: CompilerDescriptor,
1230 arguments: Vec<String>,
1231 environment: BTreeMap<String, Option<String>>,
1232 inputs: Vec<InputDescriptor>,
1233 #[serde(skip_serializing_if = "Option::is_none")]
1236 linker: Option<LinkerIdentity>,
1237}
1238
1239#[derive(Serialize)]
1240struct InvocationDescriptor {
1241 version: u8,
1242 kind: &'static str,
1243 adapter_version: u8,
1244 compiler: CompilerDescriptor,
1245 arguments: Vec<String>,
1246 required_inputs: Vec<String>,
1247}
1248
1249#[derive(Serialize)]
1250struct CompilerDescriptor {
1251 toolchain: String,
1252 rustc_version: String,
1253 host: String,
1254 #[serde(skip_serializing_if = "Option::is_none")]
1255 driver: Option<String>,
1256}
1257
1258#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1259struct InputDescriptor {
1260 path: String,
1261 digest: CacheDigest,
1262}
1263
1264#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1266enum StaticLibraryNaming {
1267 Msvc,
1269 Unix,
1271}
1272
1273struct Parser<'a> {
1274 arguments: &'a [OsString],
1275 index: usize,
1276 parsed: Vec<Argument>,
1277 source: Option<PathBuf>,
1278 crate_types: Vec<String>,
1279 emits: Vec<Emit>,
1280 required_inputs: Vec<PathBuf>,
1281 native_libraries: Vec<String>,
1282 test: bool,
1283 crate_name: Option<String>,
1284 extra_filename: String,
1285 out_dir: Option<PathBuf>,
1286 explicit_output: Option<PathBuf>,
1287 target: Option<String>,
1288 sysroot_backend: Option<String>,
1289 options: ParseOptions,
1290}
1291
1292struct ExpandedArguments {
1293 arguments: Vec<OsString>,
1294}
1295
1296#[derive(Default)]
1297struct ResponseExpander {
1298 shell_argfiles: bool,
1299 next_is_unstable_option: bool,
1300 arguments: Vec<OsString>,
1301}
1302
1303impl ResponseExpander {
1304 fn push(&mut self, argument: String) {
1305 if self.next_is_unstable_option {
1306 self.shell_argfiles |= argument == "shell-argfiles";
1307 self.next_is_unstable_option = false;
1308 } else if let Some(option) = argument.strip_prefix("-Z") {
1309 if option.is_empty() {
1310 self.next_is_unstable_option = true;
1311 } else {
1312 self.shell_argfiles |= option == "shell-argfiles";
1313 }
1314 }
1315 self.arguments.push(argument.into());
1316 }
1317}
1318
1319fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1322 let mut expanded = ResponseExpander::default();
1323 for (index, argument) in arguments.iter().enumerate() {
1324 let argument = argument
1325 .to_str()
1326 .ok_or(BypassReason::NonUtf8Argument { index })?;
1327 let Some(argfile) = argument.strip_prefix('@') else {
1328 expanded.push(argument.to_string());
1329 continue;
1330 };
1331 let (path, shell) = match argfile.split_once(':') {
1332 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1333 _ => (argfile, false),
1334 };
1335 let contents = std::fs::read_to_string(path).map_err(|error| {
1336 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1337 })?;
1338 if shell {
1339 let arguments = shlex::split(&contents).ok_or_else(|| {
1340 BypassReason::ResponseFile(format!(
1341 "invalid shell-style arguments in {}",
1342 Path::new(path).display()
1343 ))
1344 })?;
1345 for argument in arguments {
1346 expanded.push(argument);
1347 }
1348 } else {
1349 for argument in contents.lines() {
1350 expanded.push(argument.to_string());
1351 }
1352 }
1353 }
1354 Ok(ExpandedArguments {
1355 arguments: expanded.arguments,
1356 })
1357}
1358
1359impl<'a> Parser<'a> {
1360 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1361 Self {
1362 arguments,
1363 options,
1364 index: 0,
1365 parsed: Vec::new(),
1366 source: None,
1367 crate_types: Vec::new(),
1368 emits: Vec::new(),
1369 required_inputs: Vec::new(),
1370 native_libraries: Vec::new(),
1371 test: false,
1372 crate_name: None,
1373 extra_filename: String::new(),
1374 out_dir: None,
1375 explicit_output: None,
1376 target: None,
1377 sysroot_backend: None,
1378 }
1379 }
1380
1381 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1382 while self.index < self.arguments.len() {
1383 let value = self.current()?.to_string();
1384 self.index += 1;
1385 if let Some(long) = value.strip_prefix("--") {
1386 self.parse_long(long)?;
1387 } else if value.starts_with('-') && value != "-" {
1388 self.parse_short(&value)?;
1389 } else {
1390 self.parse_input(&value)?;
1391 }
1392 }
1393
1394 if let Some(backend) = self.sysroot_backend.take()
1396 && self.parsed.iter().any(
1397 |argument| matches!(argument, Argument::Path { flag, .. } if flag == "--sysroot"),
1398 )
1399 {
1400 return Err(BypassReason::UnknownFlag(backend));
1401 }
1402 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1403 let link_output = self.classify()?;
1404 self.require_native_libraries(link_output)?;
1405 let crate_name = self.crate_name.clone().map_or_else(
1406 || {
1407 source
1408 .file_stem()
1409 .and_then(|name| name.to_str())
1410 .map(|name| name.replace('-', "_"))
1411 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1412 },
1413 Ok,
1414 )?;
1415 self.required_inputs.push(source.clone());
1416 self.drop_unobservable_diagnostic_width();
1417 Ok(RustcInvocation {
1418 arguments: self.parsed,
1419 source,
1420 required_inputs: self.required_inputs,
1421 crate_name,
1422 extra_filename: self.extra_filename,
1423 out_dir: self.out_dir,
1424 explicit_output: self.explicit_output,
1425 emits: self.emits,
1426 target: self.target,
1427 link_output,
1428 })
1429 }
1430
1431 fn current(&self) -> Result<&str, BypassReason> {
1432 self.arguments[self.index]
1433 .to_str()
1434 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1435 }
1436
1437 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1438 if let Some(value) = inline {
1439 if value.is_empty() {
1440 return Err(BypassReason::MissingValue(flag.into()));
1441 }
1442 return Ok(value.into());
1443 }
1444 if self.index >= self.arguments.len() {
1445 return Err(BypassReason::MissingValue(flag.into()));
1446 }
1447 let value = self.current()?.to_string();
1448 self.index += 1;
1449 Ok(value)
1450 }
1451
1452 fn drop_unobservable_diagnostic_width(&mut self) {
1459 let capped = self
1461 .parsed
1462 .iter()
1463 .rev()
1464 .find_map(|argument| match argument {
1465 Argument::Plain(value) => value.strip_prefix("--cap-lints="),
1466 _ => None,
1467 })
1468 .is_some_and(|level| level == "allow");
1469 let forced = self
1471 .parsed
1472 .iter()
1473 .any(|argument| matches!(argument, Argument::Plain(value) if value.starts_with("--force-warn=")));
1474 if capped && !forced {
1475 self.parsed.retain(|argument| {
1476 !matches!(argument, Argument::Plain(value) if value.starts_with("--diagnostic-width="))
1477 });
1478 }
1479 }
1480
1481 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1482 let (flag, inline) = value
1483 .split_once('=')
1484 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1485 let rendered_flag = format!("--{flag}");
1486 match flag {
1487 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1488 "test" => {
1489 self.test = true;
1490 self.parsed.push(Argument::Plain(rendered_flag));
1491 Ok(())
1492 }
1493 "verbose" => {
1494 self.parsed.push(Argument::Plain(rendered_flag));
1495 Ok(())
1496 }
1497 "crate-name" => {
1498 let value = self.take_value(&rendered_flag, inline)?;
1499 self.crate_name = Some(value.clone());
1500 self.parsed
1501 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1502 Ok(())
1503 }
1504 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1505 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1506 | "deny" | "forbid" | "cap-lints" => {
1507 let value = self.take_value(&rendered_flag, inline)?;
1508 self.parsed
1509 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1510 Ok(())
1511 }
1512 "target" => {
1513 let value = self.take_value(&rendered_flag, inline)?;
1514 let effective = self.target.is_none();
1518 if effective {
1519 self.target = Some(value.clone());
1520 }
1521 if value.ends_with(".json") || value.contains(['/', '\\']) {
1522 let path = PathBuf::from(value);
1523 if effective {
1524 self.required_inputs.push(path.clone());
1525 }
1526 self.parsed.push(Argument::Path {
1527 flag: rendered_flag,
1528 path,
1529 });
1530 } else {
1531 self.parsed
1532 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1533 }
1534 Ok(())
1535 }
1536 "crate-type" => {
1537 let value = self.take_value(&rendered_flag, inline)?;
1538 self.crate_types
1539 .extend(value.split(',').map(ToOwned::to_owned));
1540 self.parsed
1541 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1542 Ok(())
1543 }
1544 "emit" => {
1545 let value = self.take_value(&rendered_flag, inline)?;
1546 let emits = parse_emits(&value);
1547 self.emits.extend(emits.clone());
1548 self.parsed.push(Argument::Emit(emits));
1549 Ok(())
1550 }
1551 "out-dir" => {
1552 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1553 self.out_dir = Some(path.clone());
1554 self.parsed.push(Argument::Path {
1555 flag: rendered_flag,
1556 path,
1557 });
1558 Ok(())
1559 }
1560 "sysroot" => {
1561 let path = self.take_value(&rendered_flag, inline)?;
1562 self.parsed.push(Argument::Path {
1563 flag: rendered_flag,
1564 path: path.into(),
1565 });
1566 Ok(())
1567 }
1568 "extern" => {
1569 let value = self.take_value(&rendered_flag, inline)?;
1570 let (name, path) = value
1571 .split_once('=')
1572 .map_or((value.as_str(), None), |(name, path)| {
1573 (name, Some(PathBuf::from(path)))
1574 });
1575 if let Some(path) = &path {
1576 self.required_inputs.push(path.clone());
1577 }
1578 self.parsed.push(Argument::Extern {
1579 name: name.into(),
1580 path,
1581 });
1582 Ok(())
1583 }
1584 "remap-path-prefix" => {
1585 let value = self.take_value(&rendered_flag, inline)?;
1586 let Some((from, to)) = value.split_once('=') else {
1587 return Err(BypassReason::MissingValue(rendered_flag));
1588 };
1589 self.parsed.push(Argument::RemapPath {
1590 from: from.into(),
1591 to: to.into(),
1592 });
1593 Ok(())
1594 }
1595 "codegen" => {
1596 let value = self.take_value(&rendered_flag, inline)?;
1597 self.parse_codegen(&value)
1598 }
1599 "jobs-frontend" => {
1600 let value = self.take_value(&rendered_flag, inline)?;
1601 self.parsed
1602 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1603 Ok(())
1604 }
1605 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1606 }
1607 }
1608
1609 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1610 if let Some(attached) = value.strip_prefix("-Z") {
1611 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1612 match option.as_str() {
1613 "embed-metadata=no"
1617 | "embed-metadata=yes"
1618 | "force-unstable-if-unmarked"
1619 | "shell-argfiles"
1620 | "unstable-options" => {
1621 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1622 return Ok(());
1623 }
1624 "use-sync-unwind=yes" | "use-sync-unwind=no" => {
1627 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1628 return Ok(());
1629 }
1630 "threads" | "threads=" => {
1631 return Err(BypassReason::MissingValue("-Zthreads".into()));
1632 }
1633 "remap-cwd-prefix" | "remap-cwd-prefix=" => {
1634 return Err(BypassReason::MissingValue("-Zremap-cwd-prefix".into()));
1635 }
1636 _ => {}
1637 }
1638 if option.starts_with("remap-cwd-prefix=") {
1644 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1645 return Ok(());
1646 }
1647 if option == "share-generics" {
1648 return Err(BypassReason::MissingValue("-Zshare-generics".into()));
1649 }
1650 if let Some(value) = option.strip_prefix("share-generics=") {
1651 if matches!(
1652 value,
1653 "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1654 ) {
1655 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1656 return Ok(());
1657 }
1658 return Err(if value.is_empty() {
1659 BypassReason::MissingValue("-Zshare-generics".into())
1660 } else {
1661 BypassReason::UnknownFlag(format!("-Z{option}"))
1662 });
1663 }
1664 if option.starts_with("threads=") {
1665 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1666 return Ok(());
1667 }
1668 if let Some(backend) = option.strip_prefix("codegen-backend=")
1670 && !backend.is_empty()
1671 && !backend.contains(['.', '/', '\\'])
1672 {
1673 self.sysroot_backend = Some(format!("-Z{option}"));
1674 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1675 return Ok(());
1676 }
1677 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1678 }
1679 match value {
1680 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1683 "-g" | "-O" | "-v" => {
1684 self.parsed.push(Argument::Plain(value.into()));
1685 return Ok(());
1686 }
1687 _ => {}
1688 }
1689 for (short, long) in [
1690 ("-A", "--allow"),
1691 ("-W", "--warn"),
1692 ("-D", "--deny"),
1693 ("-F", "--forbid"),
1694 ] {
1695 if let Some(attached) = value.strip_prefix(short) {
1696 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1697 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1698 return Ok(());
1699 }
1700 }
1701 if let Some(attached) = value.strip_prefix("-C") {
1702 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1703 return self.parse_codegen(&option);
1704 }
1705 if let Some(attached) = value.strip_prefix("-L") {
1706 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1707 let (kind, path) = search
1708 .split_once('=')
1709 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1710 if !matches!(kind, "dependency" | "native") {
1711 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1712 }
1713 self.parsed.push(Argument::SearchPath {
1714 kind: kind.into(),
1715 path: path.into(),
1716 });
1717 return Ok(());
1718 }
1719 if let Some(attached) = value.strip_prefix("-l") {
1720 let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1721 self.parsed.push(Argument::Plain(format!("-l{library}")));
1722 self.native_libraries.push(library);
1723 return Ok(());
1724 }
1725 if let Some(attached) = value.strip_prefix("-o") {
1726 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1727 self.explicit_output = Some(path.clone().into());
1728 self.parsed.push(Argument::Path {
1729 flag: "-o".into(),
1730 path: path.into(),
1731 });
1732 return Ok(());
1733 }
1734 Err(BypassReason::UnknownFlag(value.into()))
1735 }
1736
1737 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1738 let name = value.split_once('=').map_or(value, |(name, _)| name);
1739 if name == "incremental" {
1740 return Err(BypassReason::Incremental);
1741 }
1742 if name == "profile-use" {
1743 let Some((_, profile)) = value.split_once('=') else {
1744 return Err(BypassReason::MissingValue("-Cprofile-use".into()));
1745 };
1746 if profile.is_empty() {
1747 return Err(BypassReason::MissingValue("-Cprofile-use".into()));
1748 }
1749 let path = PathBuf::from(profile);
1750 self.required_inputs.push(path.clone());
1751 self.parsed.push(Argument::Path {
1752 flag: "-Cprofile-use".into(),
1753 path,
1754 });
1755 return Ok(());
1756 }
1757 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err()
1758 && !matches!(name, "linker" | "linker-flavor")
1759 {
1760 return Err(BypassReason::UnknownCodegenOption(name.into()));
1761 }
1762 if name == "linker-plugin-lto"
1768 && let Some((_, selection)) = value.split_once('=')
1769 && !matches!(
1770 selection,
1771 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1772 )
1773 {
1774 return Err(BypassReason::UnknownCodegenOption(format!(
1775 "linker-plugin-lto={selection}"
1776 )));
1777 }
1778 if name == "link-arg"
1785 && let Some((_, option)) = value.split_once('=')
1786 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1787 && !selection.is_empty()
1788 && !selection.contains(',')
1789 && !self.parsed.iter().any(
1790 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1791 )
1792 {
1793 if !self.parsed.iter().any(
1794 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1795 ) {
1796 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1797 }
1798 return Ok(());
1799 }
1800 if name == "link-arg"
1803 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1804 && safe_install_name(option)
1805 {
1806 self.parsed.push(Argument::InstallName(option.to_owned()));
1807 return Ok(());
1808 }
1809 if matches!(name, "link-arg" | "link-args")
1814 && let Some((_, option)) = value.split_once('=')
1815 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1816 && !prefix.trim_end_matches('/').is_empty()
1817 && !prefix.contains(',')
1818 {
1819 let trailing_slash = prefix.ends_with('/');
1820 self.parsed.push(Argument::OsoPrefix {
1821 path: PathBuf::from(prefix.trim_end_matches('/')),
1822 trailing_slash,
1823 });
1824 return Ok(());
1825 }
1826 self.parsed
1827 .push(Argument::Plain(format!("--codegen={value}")));
1828 if name == "extra-filename" {
1829 self.extra_filename = value
1830 .split_once('=')
1831 .map_or(String::new(), |(_, value)| value.to_string());
1832 }
1833 Ok(())
1834 }
1835
1836 fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1851 for library in std::mem::take(&mut self.native_libraries) {
1852 if link_output != LinkOutput::Library {
1853 return Err(BypassReason::NativeLibrary(library));
1854 }
1855 let Some((kind, name)) = library.split_once('=') else {
1863 continue;
1864 };
1865 let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1866 match kind {
1867 "static" => {}
1868 "dylib" | "framework" | "link-arg" => continue,
1869 _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1870 }
1871 let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1874 let (mut verbatim, mut bundle) = (false, true);
1875 for modifier in modifiers.split(',') {
1876 match modifier {
1877 "+verbatim" => verbatim = true,
1878 "-verbatim" => verbatim = false,
1879 "+bundle" => bundle = true,
1880 "-bundle" => bundle = false,
1881 _ => {}
1882 }
1883 }
1884 if !bundle {
1885 continue;
1886 }
1887 let archive = self
1888 .find_native_static_library(name, verbatim)
1889 .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1890 .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1891 self.required_inputs.push(archive);
1892 }
1893 Ok(())
1894 }
1895
1896 fn find_native_static_library(
1908 &self,
1909 name: &str,
1910 verbatim: bool,
1911 ) -> Result<Option<PathBuf>, ()> {
1912 let file_names = if verbatim {
1913 vec![name.to_owned()]
1914 } else {
1915 match self.static_library_naming() {
1916 Some(StaticLibraryNaming::Msvc) => {
1917 vec![format!("{name}.lib"), format!("lib{name}.a")]
1918 }
1919 Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1920 None => return Err(()),
1921 }
1922 };
1923 Ok(self
1924 .parsed
1925 .iter()
1926 .filter_map(|argument| match argument {
1927 Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1928 _ => None,
1929 })
1930 .flat_map(|directory| {
1931 file_names
1932 .iter()
1933 .map(move |file_name| directory.join(file_name))
1934 })
1935 .find(|candidate| candidate.is_file()))
1936 }
1937
1938 fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1946 let Some(target) = self.target.as_deref() else {
1947 return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1948 StaticLibraryNaming::Msvc
1949 } else {
1950 StaticLibraryNaming::Unix
1951 });
1952 };
1953 if target.ends_with(".json")
1954 || target.contains(['/', '\\'])
1955 || self.options.custom_target_search
1956 {
1957 return None;
1958 }
1959 Some(if target.contains("msvc") || target.ends_with("-uefi") {
1962 StaticLibraryNaming::Msvc
1963 } else {
1964 StaticLibraryNaming::Unix
1965 })
1966 }
1967
1968 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1969 if value == "-" {
1970 return Err(BypassReason::StandardInput);
1971 }
1972 if self.source.replace(value.into()).is_some() {
1973 return Err(BypassReason::MultipleInputs);
1974 }
1975 Ok(())
1976 }
1977
1978 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1979 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1988 let builds_a_library = !self.test
1989 && !self.crate_types.is_empty()
1990 && self
1991 .crate_types
1992 .iter()
1993 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1994 let link_output = if never_links || builds_a_library {
1995 LinkOutput::Library
1996 } else if self
1997 .target
1998 .as_deref()
1999 .is_some_and(compiler_bundled_wasm_target)
2000 && ((self.test && self.crate_types.is_empty())
2001 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
2002 {
2003 if self.parsed.iter().any(|argument| match argument {
2004 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
2005 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
2006 !matches!(
2007 value.rsplit_once('=').map(|(_, value)| value),
2008 Some("y" | "yes" | "on" | "true")
2009 )
2010 }
2011 _ => false,
2012 }) {
2013 return Err(BypassReason::UnknownCodegenOption(
2014 "link-self-contained".into(),
2015 ));
2016 }
2017 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
2018 && self.parsed.iter().any(|argument| {
2019 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
2020 })
2021 {
2022 return Err(BypassReason::UnknownCodegenOption(
2023 "target-feature=-crt-static".into(),
2024 ));
2025 }
2026 LinkOutput::WasmExecutable
2030 } else if self.options.cache_native_links && self.links_a_native_artifact() {
2031 self.check_native_link_is_portable()?;
2032 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
2033 LinkOutput::NativeProcMacro
2034 } else if matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib") {
2035 LinkOutput::NativeSharedLibrary
2036 } else {
2037 LinkOutput::NativeExecutable
2038 }
2039 } else if self.test {
2040 return Err(BypassReason::UnsupportedCrateType("test".into()));
2041 } else {
2042 return Err(BypassReason::UnsupportedCrateType(
2043 self.crate_types
2044 .iter()
2045 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
2046 .cloned()
2047 .unwrap_or_else(|| "bin".into()),
2048 ));
2049 };
2050 if link_output != LinkOutput::Library
2059 && let Some(option) = self.first_link_argument(link_output)
2060 {
2061 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
2062 }
2063 if self
2064 .parsed
2065 .iter()
2066 .any(|arg| matches!(arg, Argument::InstallName(_)))
2067 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
2068 {
2069 return Err(BypassReason::UnmodeledLinkArgument(
2070 "link-arg=-Wl,-install_name".into(),
2071 ));
2072 }
2073 if let Some(selection) = self.parsed.iter().find_map(|argument| {
2080 let Argument::FuseLd(selection) = argument else {
2081 return None;
2082 };
2083 Some(selection.as_str())
2084 }) && !matches!(
2085 link_output,
2086 LinkOutput::Library
2087 | LinkOutput::NativeExecutable
2088 | LinkOutput::NativeProcMacro
2089 | LinkOutput::NativeSharedLibrary
2090 ) {
2091 return Err(BypassReason::UnmodeledLinkArgument(format!(
2092 "link-arg=-fuse-ld={selection}"
2093 )));
2094 }
2095 if self.parsed.iter().any(|argument| {
2096 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
2097 }) && !matches!(
2098 link_output,
2099 LinkOutput::Library
2100 | LinkOutput::NativeExecutable
2101 | LinkOutput::NativeProcMacro
2102 | LinkOutput::NativeSharedLibrary
2103 ) {
2104 return Err(BypassReason::UnknownCodegenOption("linker".into()));
2105 }
2106 if link_output != LinkOutput::Library
2111 && self.parsed.iter().any(|argument| {
2112 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker-flavor="))
2113 })
2114 {
2115 return Err(BypassReason::UnknownCodegenOption("linker-flavor".into()));
2116 }
2117 if !matches!(
2121 link_output,
2122 LinkOutput::Library
2123 | LinkOutput::NativeExecutable
2124 | LinkOutput::NativeProcMacro
2125 | LinkOutput::NativeSharedLibrary
2126 ) && self
2127 .parsed
2128 .iter()
2129 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
2130 {
2131 return Err(BypassReason::UnmodeledLinkArgument(
2132 "link-arg=-Wl,-oso_prefix".into(),
2133 ));
2134 }
2135 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
2136 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
2137 _ => None,
2138 }) {
2139 return Err(BypassReason::UnresolvedExtern(name.clone()));
2140 }
2141 if let Some(emit) = self
2142 .emits
2143 .iter()
2144 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
2145 {
2146 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
2147 }
2148 if !self
2149 .emits
2150 .iter()
2151 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
2152 {
2153 return Err(BypassReason::NoCacheableOutput);
2154 }
2155 Ok(link_output)
2156 }
2157}
2158
2159impl Parser<'_> {
2160 fn first_link_argument(&self, link_output: LinkOutput) -> Option<&str> {
2163 self.parsed.iter().find_map(|argument| {
2164 let Argument::Plain(value) = argument else {
2165 return None;
2166 };
2167 let option = value.strip_prefix("--codegen=")?;
2168 let (name, value) = option.split_once('=').unwrap_or((option, ""));
2169 (matches!(name, "link-arg" | "link-args")
2170 && !(name == "link-arg" && elf_keyword_is_modeled(link_output, value))
2171 && !msvc_options_are_modeled(link_output, name, value))
2172 .then_some(option)
2173 })
2174 }
2175
2176 fn oso_prefix_covers_outputs(&self) -> bool {
2185 let Some(directory) = self
2186 .out_dir
2187 .as_deref()
2188 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2189 else {
2190 return false;
2191 };
2192 if !directory.is_absolute() {
2193 return false;
2194 }
2195 let directory = normalize_components(directory);
2196 self.parsed.iter().any(|argument| {
2197 let Argument::OsoPrefix { path, .. } = argument else {
2198 return false;
2199 };
2200 path.is_absolute()
2201 && (directory.starts_with(normalize_components(path))
2202 || std::fs::canonicalize(&directory)
2203 .ok()
2204 .zip(std::fs::canonicalize(path).ok())
2205 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2206 })
2207 }
2208
2209 fn links_a_native_artifact(&self) -> bool {
2216 self.target.is_none()
2217 && self.emits.iter().any(|emit| emit.kind == "link")
2222 && ((self.test && self.crate_types.is_empty())
2223 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro"))
2224 || (cfg!(target_os = "linux")
2225 && matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib")))
2226 }
2227
2228 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2235 for argument in &self.parsed {
2236 let Argument::Plain(value) = argument else {
2237 continue;
2238 };
2239 let (name, value) = if value == "-g" {
2242 ("debuginfo", Some("2"))
2243 } else if let Some(option) = value.strip_prefix("--codegen=") {
2244 match option.split_once('=') {
2245 Some((name, value)) => (name, Some(value)),
2246 None => (option, None),
2250 }
2251 } else {
2252 continue;
2253 };
2254 let unportable = match name {
2255 "split-debuginfo" => match value {
2261 Some("off") => false,
2262 Some("unpacked") if cfg!(target_os = "macos") => {
2263 !self.oso_prefix_covers_outputs()
2264 }
2265 _ => true,
2266 },
2267 "debuginfo" if cfg!(target_os = "macos") => {
2276 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2277 }
2278 "rpath" => is_enabled(value),
2286 "prefer-dynamic" => {
2287 is_enabled(value)
2288 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2289 }
2290 "link-self-contained" => true,
2293 _ => false,
2294 };
2295 if unportable {
2296 return Err(BypassReason::UnportableNativeLink(match value {
2297 Some(value) => format!("{name}={value}"),
2298 None => name.to_owned(),
2299 }));
2300 }
2301 }
2302 Ok(())
2303 }
2304}
2305
2306fn safe_install_name(name: &str) -> bool {
2307 !name.is_empty()
2308 && name
2309 .bytes()
2310 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2311}
2312
2313fn is_enabled(value: Option<&str>) -> bool {
2316 matches!(value, None | Some("y" | "yes" | "on" | "true"))
2317}
2318
2319fn elf_keyword_is_modeled(link_output: LinkOutput, value: &str) -> bool {
2329 cfg!(target_os = "linux")
2330 && matches!(
2331 link_output,
2332 LinkOutput::NativeExecutable
2333 | LinkOutput::NativeProcMacro
2334 | LinkOutput::NativeSharedLibrary
2335 )
2336 && value.strip_prefix("-Wl,-z,").is_some_and(|keyword| {
2337 matches!(
2338 keyword,
2339 "defs"
2340 | "lazy"
2341 | "nodelete"
2342 | "nodlopen"
2343 | "noexecstack"
2344 | "norelro"
2345 | "now"
2346 | "origin"
2347 | "relro"
2348 )
2349 })
2350}
2351
2352fn msvc_options_are_modeled(link_output: LinkOutput, name: &str, value: &str) -> bool {
2368 cfg!(target_env = "msvc")
2369 && matches!(
2370 link_output,
2371 LinkOutput::NativeExecutable
2372 | LinkOutput::NativeProcMacro
2373 | LinkOutput::NativeSharedLibrary
2374 )
2375 && match name {
2376 "link-arg" => msvc_option_reads_no_file(value),
2377 "link-args" => {
2380 let mut options = value.split_ascii_whitespace().peekable();
2381 options.peek().is_some() && options.all(msvc_option_reads_no_file)
2382 }
2383 _ => false,
2384 }
2385}
2386
2387fn msvc_option_reads_no_file(option: &str) -> bool {
2391 let Some(option) = option.strip_prefix(['/', '-']) else {
2392 return false;
2393 };
2394 if option.eq_ignore_ascii_case("brepro") {
2395 return true;
2396 }
2397 let Some((name, sizes)) = option.split_once(':') else {
2398 return false;
2399 };
2400 let is_size = |size: &str| match size.strip_prefix("0x").or_else(|| size.strip_prefix("0X")) {
2403 Some(hex) => !hex.is_empty() && hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
2404 None => !size.is_empty() && size.bytes().all(|byte| byte.is_ascii_digit()),
2405 };
2406 name.eq_ignore_ascii_case("stack")
2407 && sizes.split(',').count() <= 2
2408 && sizes.split(',').all(is_size)
2409}
2410
2411fn compiler_bundled_wasm_target(target: &str) -> bool {
2412 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2413}
2414
2415fn parse_emits(value: &str) -> Vec<Emit> {
2416 value
2417 .split(',')
2418 .map(|emit| {
2419 let (kind, path) = emit
2420 .split_once('=')
2421 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2422 Emit {
2423 kind: kind.into(),
2424 path,
2425 }
2426 })
2427 .collect()
2428}
2429
2430struct ActionBuilder<'a> {
2431 invocation: &'a RustcInvocation,
2432 context: ActionContext,
2433 mappings: Vec<SharedPathMapping>,
2434 aliases: PathAliases,
2438 linker: Option<LinkerIdentity>,
2439}
2440
2441impl<'a> ActionBuilder<'a> {
2442 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2443 context.path_mappings = PathMapping::ordered(&context.path_mappings);
2444 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2445 Self {
2446 linker: None,
2447 invocation,
2448 mappings,
2449 aliases: PathAliases::default(),
2450 context,
2451 }
2452 }
2453
2454 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2455 self.linker = linker;
2456 self
2457 }
2458
2459 fn build(self) -> Result<RustcAction, BypassReason> {
2460 self.validate_mappings()?;
2461 let invocation = self.invocation_descriptor()?;
2462 let environment = self.environment_descriptor()?;
2463
2464 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2465 for input in &self.context.inputs {
2466 input
2467 .digest
2468 .validate()
2469 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2470 let path = self.normalize_input_path(&input.path)?;
2471 if inputs
2472 .insert(path.clone(), input.digest.clone())
2473 .is_some_and(|existing| existing != input.digest)
2474 {
2475 return Err(BypassReason::ConflictingInput(path));
2476 }
2477 }
2478 let required = self
2479 .invocation
2480 .required_inputs
2481 .iter()
2482 .map(|path| self.normalize_input_path(path))
2483 .collect::<Result<BTreeSet<_>, _>>()?;
2484 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2485 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2486 }
2487 let inputs = inputs
2488 .into_iter()
2489 .map(|(path, digest)| InputDescriptor { path, digest })
2490 .collect();
2491 if self.invocation.links_natively() && self.linker.is_none() {
2494 return Err(BypassReason::UnportableNativeLink(
2495 "linker identity is unknown".into(),
2496 ));
2497 }
2498 let descriptor = ActionDescriptor {
2499 version: ACTION_SCHEMA_VERSION,
2500 kind: "rustc",
2501 adapter_version: ADAPTER_VERSION,
2502 compiler: invocation.compiler,
2503 arguments: invocation.arguments,
2504 environment,
2505 inputs,
2506 linker: self.linker.clone(),
2507 };
2508 let bytes = canonical_json(&descriptor)
2509 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2510 let digest = CacheDigest::blake3(&bytes);
2511 Ok(RustcAction { digest, bytes })
2512 }
2513
2514 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2515 self.validate_mappings()?;
2516 let arguments = self
2517 .invocation
2518 .arguments
2519 .iter()
2520 .map(|argument| self.normalize_argument(argument))
2521 .collect::<Result<Vec<_>, _>>()?;
2522 let required_inputs = self
2523 .invocation
2524 .required_inputs
2525 .iter()
2526 .map(|path| self.normalize_path(path))
2527 .collect::<Result<BTreeSet<_>, _>>()?
2528 .into_iter()
2529 .collect();
2530 Ok(InvocationDescriptor {
2531 version: ACTION_SCHEMA_VERSION,
2532 kind: "rustc",
2533 adapter_version: ADAPTER_VERSION,
2534 compiler: CompilerDescriptor {
2535 toolchain: self.context.compiler.toolchain.clone(),
2536 rustc_version: self.context.compiler.rustc_version.clone(),
2537 host: self.context.compiler.host.clone(),
2538 driver: self.context.compiler.driver.clone(),
2539 },
2540 arguments,
2541 required_inputs,
2542 })
2543 }
2544
2545 fn validate_mappings(&self) -> Result<(), BypassReason> {
2546 if !self.context.working_dir.is_absolute() {
2547 return Err(BypassReason::RelativeWorkingDirectory(
2548 self.context.working_dir.clone(),
2549 ));
2550 }
2551 let mut roots = BTreeSet::new();
2552 let mut placeholders = BTreeSet::new();
2553 for mapping in &self.mappings {
2554 if !mapping.root.is_absolute() {
2555 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2556 }
2557 if mapping.placeholder.is_empty()
2558 || !mapping
2559 .placeholder
2560 .bytes()
2561 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2562 || !roots.insert(normalize_components(&mapping.root))
2563 || !placeholders.insert(&mapping.placeholder)
2564 {
2565 return Err(BypassReason::InvalidPathPlaceholder(
2566 mapping.placeholder.clone(),
2567 ));
2568 }
2569 }
2570 Ok(())
2571 }
2572
2573 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2574 match argument {
2575 Argument::Plain(value) => Ok(value.clone()),
2576 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2577 Argument::SearchPath { kind, path } => {
2578 let text = self.normalize_path(path)?;
2579 Ok(format!("-L{kind}={text}"))
2580 }
2581 Argument::Extern { name, path } => match path {
2582 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2583 None => Ok(format!("--extern={name}")),
2584 },
2585 Argument::Emit(emits) => Ok(format!(
2586 "--emit={}",
2587 emits
2588 .iter()
2589 .map(|emit| match &emit.path {
2590 Some(path) => self
2591 .normalize_path(path)
2592 .map(|path| format!("{}={path}", emit.kind)),
2593 None => Ok(emit.kind.clone()),
2594 })
2595 .collect::<Result<Vec<_>, _>>()?
2596 .join(",")
2597 )),
2598 Argument::RemapPath { from, to } => Ok(format!(
2599 "--remap-path-prefix={}={}",
2600 self.normalize_path(from)?,
2601 to
2602 )),
2603 Argument::InstallName(name) => Ok(format!(
2604 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2605 )),
2606 Argument::OsoPrefix {
2607 path,
2608 trailing_slash,
2609 } => Ok(format!(
2610 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2611 self.normalize_path(path)?,
2612 if *trailing_slash { "/" } else { "" }
2613 )),
2614 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2615 }
2616 }
2617
2618 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2625 self.context
2626 .environment
2627 .iter()
2628 .map(|(name, value)| {
2629 let value = match value {
2630 Some(value) if self.context.portable_environment.contains(name) => {
2631 Some(self.normalize_path(Path::new(value))?)
2632 }
2633 value => value.clone(),
2634 };
2635 Ok((name.clone(), value))
2636 })
2637 .collect()
2638 }
2639
2640 fn normalize_input_path(&self, path: &Path) -> Result<String, BypassReason> {
2641 let absolute = normalize_components(&self.context.working_dir.join(path));
2642 for directory in self.invocation.native_search_paths() {
2643 let directory = normalize_components(&self.context.working_dir.join(directory));
2644 if let Ok(relative) = absolute.strip_prefix(&directory) {
2645 let suffix = relative
2651 .components()
2652 .map(|component| {
2653 component
2654 .as_os_str()
2655 .to_str()
2656 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf()))
2657 })
2658 .collect::<Result<Vec<_>, _>>()?
2659 .join("/");
2660 if !suffix.is_empty() {
2661 return Ok(format!("{}/{}", self.normalize_path(&directory)?, suffix));
2662 }
2663 }
2664 }
2665 self.normalize_path(path)
2666 }
2667
2668 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2669 normalize_resolved_shared_path_with(
2670 &self.aliases,
2671 path,
2672 &self.context.working_dir,
2673 &self.mappings,
2674 )
2675 .map_err(Into::into)
2676 }
2677}
2678
2679fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2683 directories.iter().any(|directory| {
2684 path.len() > directory.len()
2685 && path.as_bytes()[directory.len()] == b'/'
2686 && path.starts_with(directory)
2687 })
2688}
2689
2690fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2691 for mapping in mappings {
2692 let prefix = format!("${{{}}}", mapping.placeholder);
2693 let suffix = if value == prefix {
2694 ""
2695 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2696 suffix
2697 } else {
2698 continue;
2699 };
2700 if !mapping.root.is_absolute()
2701 || (!suffix.is_empty()
2702 && suffix.split('/').any(|component| {
2703 component.is_empty()
2704 || matches!(component, "." | "..")
2705 || component.contains('\\')
2706 }))
2707 {
2708 return Err(BypassReason::InvalidPredictedInput(value.into()));
2709 }
2710 let mut path = normalize_components(&mapping.root);
2711 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2712 return Ok(path);
2713 }
2714 Err(BypassReason::InvalidPredictedInput(value.into()))
2715}
2716
2717fn normalize_components(path: &Path) -> PathBuf {
2718 let mut normalized = PathBuf::new();
2719 for component in path.components() {
2720 match component {
2721 Component::CurDir => {}
2722 Component::ParentDir => {
2723 normalized.pop();
2724 }
2725 component => normalized.push(component.as_os_str()),
2726 }
2727 }
2728 normalized
2729}
2730
2731fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2732 if path.is_absolute() {
2733 normalize_components(path)
2734 } else {
2735 normalize_components(&working_dir.join(path))
2736 }
2737}
2738
2739#[cfg(test)]
2740#[path = "rustc_cache_tests.rs"]
2741mod tests;