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("compiler input metadata changed during compilation: {0}")]
329 InputMetadataChangedDuringCompilation(PathBuf),
330 #[error("discovered inputs were collected from a different working directory")]
332 DiscoveryWorkingDirectory,
333 #[error("compiler environment input has conflicting values: {0}")]
335 ConflictingEnvironment(String),
336 #[error("failed to serialize the rustc action: {0}")]
338 Serialization(String),
339 #[error("rustc action prediction is unsupported")]
341 UnsupportedPrediction,
342 #[error("rustc action prediction contains an invalid input path: {0}")]
344 InvalidPredictedInput(String),
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348enum Argument {
349 Plain(String),
350 Path {
351 flag: String,
352 path: PathBuf,
353 },
354 SearchPath {
355 kind: String,
356 path: PathBuf,
357 },
358 Extern {
359 name: String,
360 path: Option<PathBuf>,
361 },
362 Emit(Vec<Emit>),
363 RemapPath {
364 from: PathBuf,
365 to: String,
366 },
367 InstallName(String),
369 OsoPrefix {
372 path: PathBuf,
373 trailing_slash: bool,
374 },
375 FuseLd(String),
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
382struct Emit {
383 kind: String,
384 path: Option<PathBuf>,
385}
386
387#[derive(Debug, Clone, PartialEq, Eq)]
388pub struct RustcInvocation {
393 arguments: Vec<Argument>,
394 source: PathBuf,
395 required_inputs: Vec<PathBuf>,
396 crate_name: String,
397 extra_filename: String,
398 out_dir: Option<PathBuf>,
399 explicit_output: Option<PathBuf>,
400 emits: Vec<Emit>,
401 target: Option<String>,
402 link_output: LinkOutput,
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406enum LinkOutput {
407 Library,
408 WasmExecutable,
409 NativeExecutable,
410 NativeProcMacro,
411 NativeSharedLibrary,
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
424#[non_exhaustive]
425pub struct ParseOptions {
426 pub cache_native_links: bool,
429 pub custom_target_search: bool,
435}
436
437impl ParseOptions {
438 pub fn caching_native_links(enabled: bool) -> Self {
440 Self {
441 cache_native_links: enabled,
442 ..Self::default()
443 }
444 }
445
446 pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
449 self.custom_target_search = enabled;
450 self
451 }
452}
453
454#[derive(Debug, Clone, PartialEq, Eq)]
456pub struct RustcOutputs {
457 pub directory: PathBuf,
459 pub files: Vec<PathBuf>,
461 pub dep_info: PathBuf,
463}
464
465impl RustcInvocation {
466 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
475 Self::parse_with(arguments, ParseOptions::default())
476 }
477
478 pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
483 Ok(expand_response_files(arguments)?.arguments)
484 }
485
486 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
489 let expanded = expand_response_files(arguments)?;
490 Parser::new(&expanded.arguments, options).parse()
491 }
492
493 pub fn links_natively(&self) -> bool {
496 matches!(
497 self.link_output,
498 LinkOutput::NativeExecutable
499 | LinkOutput::NativeProcMacro
500 | LinkOutput::NativeSharedLibrary
501 )
502 }
503
504 pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
508 if !cfg!(target_os = "macos")
509 || self.link_output != LinkOutput::NativeProcMacro
510 || self
511 .arguments
512 .iter()
513 .any(|arg| matches!(arg, Argument::InstallName(_)))
514 {
515 return None;
516 }
517 let file = outputs
518 .files
519 .iter()
520 .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
521 let name = file.file_name()?.to_str()?;
522 if !safe_install_name(name) {
523 return None;
524 }
525 Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
526 }
527
528 pub fn linker_override(&self) -> Option<&Path> {
531 self.arguments.iter().find_map(|argument| {
532 let Argument::Plain(value) = argument else {
533 return None;
534 };
535 value.strip_prefix("--codegen=linker=").map(Path::new)
536 })
537 }
538
539 pub fn fuse_ld(&self) -> Option<&str> {
542 self.arguments.iter().find_map(|argument| {
543 let Argument::FuseLd(selection) = argument else {
544 return None;
545 };
546 Some(selection.as_str())
547 })
548 }
549
550 fn emits_windows_pdb(&self) -> bool {
551 if !cfg!(windows) || !self.links_natively() {
552 return false;
553 }
554 let mut enabled = false;
555 for argument in &self.arguments {
556 let Argument::Plain(value) = argument else {
557 continue;
558 };
559 if value == "-g" {
560 enabled = true;
561 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
562 enabled = !matches!(value, "0" | "none");
563 }
564 }
565 enabled
566 }
567
568 pub fn native_search_paths(&self) -> impl Iterator<Item = &Path> {
572 self.arguments.iter().filter_map(|argument| match argument {
573 Argument::SearchPath { kind, path } if kind == "native" => Some(path.as_path()),
574 _ => None,
575 })
576 }
577
578 pub fn source(&self) -> &Path {
580 &self.source
581 }
582
583 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
589 self.required_inputs
590 .iter()
591 .map(|path| absolute_path(path, working_dir))
592 .collect()
593 }
594
595 pub fn target(&self) -> Option<&str> {
597 self.target.as_deref()
598 }
599
600 pub fn crate_name(&self) -> &str {
602 &self.crate_name
603 }
604
605 pub fn remaps_working_directory(&self) -> bool {
614 self.arguments.iter().any(|argument| {
615 matches!(argument, Argument::Plain(flag) if flag.starts_with("-Zremap-cwd-prefix="))
616 })
617 }
618
619 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
632 let linked = self
633 .arguments
634 .iter()
635 .filter_map(|argument| match argument {
636 Argument::Extern {
637 path: Some(path), ..
638 } => Some(path.as_path()),
639 _ => None,
640 })
641 .collect::<BTreeSet<_>>();
642 let owned = discovered
643 .inputs
644 .iter()
645 .filter(|input| {
646 !linked.contains(input.path.as_path()) && !discovered.is_native_only(&input.path)
647 })
648 .map(|input| (input.path.as_path(), &input.digest))
649 .collect::<BTreeMap<_, _>>();
650 let mut bytes = Vec::new();
651 for (path, digest) in owned {
652 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
653 bytes.push(0);
654 bytes.extend_from_slice(digest.key().as_bytes());
655 bytes.push(0);
656 }
657 CacheDigest::blake3(&bytes)
658 }
659
660 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
665 if !working_dir.is_absolute() {
666 return Err(BypassReason::RelativeWorkingDirectory(
667 working_dir.to_path_buf(),
668 ));
669 }
670 let explicit_output = self
671 .explicit_output
672 .as_deref()
673 .map(|path| absolute_path(path, working_dir));
674 let output_directory = explicit_output
675 .as_deref()
676 .and_then(Path::parent)
677 .map(Path::to_path_buf)
678 .or_else(|| {
679 self.out_dir
680 .as_deref()
681 .map(|path| absolute_path(path, working_dir))
682 })
683 .unwrap_or_else(|| normalize_components(working_dir));
684 if let Some(output) = &explicit_output
688 && self.emits.iter().any(|emit| {
689 emit.path.is_none()
690 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
691 })
692 {
693 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
694 }
695 let mut files = BTreeSet::new();
696 let mut dep_info = None;
697 for emit in &self.emits {
698 if emit.kind == "dep-info" {
699 let path = emit.path.as_ref().map_or_else(
700 || {
701 explicit_output.clone().map_or_else(
702 || {
703 output_directory
704 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
705 },
706 |path| path.with_extension("d"),
707 )
708 },
709 |path| absolute_path(path, working_dir),
710 );
711 if path.file_name().is_none() {
712 return Err(BypassReason::InvalidOutputPath(path));
713 }
714 dep_info = Some(path);
715 continue;
716 }
717 let (prefix, extension) = match emit.kind.as_str() {
718 "link" => match self.link_output {
719 LinkOutput::Library => ("lib", "rlib"),
720 LinkOutput::WasmExecutable => ("", "wasm"),
721 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
722 LinkOutput::NativeProcMacro | LinkOutput::NativeSharedLibrary => (
723 std::env::consts::DLL_PREFIX,
724 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
725 ),
726 },
727 "metadata" => ("lib", "rmeta"),
728 _ => continue,
729 };
730 let path = if let Some(path) = &emit.path {
731 absolute_path(path, working_dir)
732 } else {
733 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
734 output_directory.join(if extension.is_empty() {
735 name
736 } else {
737 format!("{name}.{extension}")
738 })
739 };
740 if path.file_name().is_none() {
741 return Err(BypassReason::InvalidOutputPath(path));
742 }
743 if path.parent() != Some(output_directory.as_path()) {
744 return Err(BypassReason::SplitOutputDirectories);
745 }
746 let has_library_extension = path
751 .extension()
752 .and_then(|extension| extension.to_str())
753 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
754 || (cfg!(windows)
755 && path
756 .file_stem()
757 .and_then(|stem| Path::new(stem).extension())
758 .and_then(|extension| extension.to_str())
759 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
760 if emit.kind == "link"
761 && !matches!(self.link_output, LinkOutput::Library)
762 && has_library_extension
763 {
764 return Err(BypassReason::AmbiguousOutputName(path));
765 }
766 if emit.kind == "link" && self.emits_windows_pdb() {
767 files.insert(path.with_extension("pdb"));
768 }
769 files.insert(path);
770 }
771 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
772 if dep_info.parent() != Some(output_directory.as_path()) {
773 return Err(BypassReason::SplitOutputDirectories);
774 }
775 Ok(RustcOutputs {
776 directory: output_directory,
777 files: files.into_iter().collect(),
778 dep_info,
779 })
780 }
781
782 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
788 self.action_linked_by(context, None)
789 }
790
791 pub fn action_linked_by(
800 &self,
801 context: ActionContext,
802 linker: Option<LinkerIdentity>,
803 ) -> Result<RustcAction, BypassReason> {
804 ActionBuilder::new(self, context).linked_by(linker).build()
805 }
806
807 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
809 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
810 let bytes = canonical_json(&descriptor)
811 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
812 Ok(CacheDigest::blake3(&bytes))
813 }
814
815 pub fn prediction(
818 &self,
819 context: &ActionContext,
820 discovered: &DiscoveredInputs,
821 ) -> Result<RustcInputPrediction, BypassReason> {
822 let builder = ActionBuilder::new(self, context.clone());
823 builder.validate_mappings()?;
824 let mut native_directories = BTreeSet::new();
825 for argument in &self.arguments {
826 if let Argument::SearchPath { kind, path } = argument
827 && kind == "native"
828 {
829 native_directories.insert(builder.normalize_path(path)?);
830 }
831 }
832 let mut inputs = BTreeSet::new();
844 for input in &discovered.inputs {
845 let normalized = builder.normalize_path(&input.path)?;
846 if !under_any_directory(&normalized, &native_directories)
847 || !discovered.is_native_only(&input.path)
848 {
849 inputs.insert(normalized);
850 }
851 }
852 inputs.extend(
853 native_directories
854 .into_iter()
855 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
856 );
857 Ok(RustcInputPrediction {
858 version: 4,
859 inputs: inputs.into_iter().collect(),
860 environment: discovered.environment.keys().cloned().collect(),
861 compiler_duration_ns: 0,
862 crate_name: String::new(),
863 })
864 }
865}
866
867pub fn is_build_script_crate_name(crate_name: &str) -> bool {
871 crate_name
872 .strip_prefix("build_script_")
873 .is_some_and(|stem| !stem.is_empty())
874}
875
876impl RustcOutputs {
877 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
887 is_build_script_crate_name(crate_name)
888 .then(|| self.files.iter().find(|path| self.is_executable(path)))
889 .flatten()
890 .map(PathBuf::as_path)
891 }
892
893 pub fn is_executable(&self, path: &Path) -> bool {
901 self.files.iter().any(|output| output == path)
902 && !matches!(
903 path.extension().and_then(|extension| extension.to_str()),
904 Some("rlib" | "rmeta")
905 )
906 }
907}
908
909#[derive(Debug, Clone, PartialEq, Eq)]
910pub struct PathMapping {
912 pub root: PathBuf,
914 pub placeholder: String,
916}
917
918impl PathMapping {
919 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
921 Self {
922 root: root.into(),
923 placeholder: placeholder.into(),
924 }
925 }
926
927 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
930 let mut ordered = mappings.to_vec();
931 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
932 ordered
933 }
934}
935
936fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
937 mappings
938 .iter()
939 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
940 .collect()
941}
942
943pub fn normalize_mapped_path(
950 path: &Path,
951 working_dir: &Path,
952 mappings: &[PathMapping],
953) -> Result<String, BypassReason> {
954 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
955}
956
957#[derive(Debug, Clone, PartialEq, Eq)]
958pub struct CompilerIdentity {
960 pub toolchain: String,
962 pub rustc_version: String,
964 pub host: String,
966 pub driver: Option<String>,
968}
969
970#[derive(Debug, Clone, PartialEq, Eq)]
972pub struct ActionInput {
973 pub path: PathBuf,
975 pub digest: CacheDigest,
977}
978
979#[derive(Debug, Clone, PartialEq, Eq)]
981pub struct ActionContext {
982 pub compiler: CompilerIdentity,
984 pub working_dir: PathBuf,
986 pub path_mappings: Vec<PathMapping>,
988 pub environment: BTreeMap<String, Option<String>>,
990 pub portable_environment: BTreeSet<String>,
997 pub inputs: Vec<ActionInput>,
999}
1000
1001#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1011#[serde(deny_unknown_fields)]
1012pub struct LinkerIdentity {
1013 pub driver: String,
1015 pub driver_version: String,
1017 pub linker_version: String,
1019 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
1021 pub crt_objects: BTreeMap<String, CacheDigest>,
1022 #[serde(skip_serializing_if = "Option::is_none", default)]
1024 pub sdk: Option<String>,
1025 #[serde(skip_serializing_if = "Option::is_none", default)]
1027 pub deployment_target: Option<String>,
1028}
1029
1030#[derive(Debug, Clone, PartialEq, Eq)]
1032pub struct RustcAction {
1033 pub digest: CacheDigest,
1035 pub bytes: Vec<u8>,
1037}
1038
1039#[derive(Debug, Clone, PartialEq, Eq)]
1042pub struct RustcInputPrediction {
1043 pub version: u8,
1045 pub inputs: Vec<String>,
1047 pub environment: Vec<String>,
1049 pub compiler_duration_ns: u64,
1052 pub crate_name: String,
1054}
1055
1056#[derive(Serialize, Deserialize)]
1057#[serde(deny_unknown_fields)]
1058struct RustcInputPredictionWire {
1059 version: u8,
1060 inputs: Vec<String>,
1061 environment: Vec<String>,
1062 #[serde(default, skip_serializing_if = "is_zero")]
1063 compiler_duration_ns: u64,
1064 #[serde(default, skip_serializing_if = "String::is_empty")]
1065 crate_name: String,
1066}
1067
1068impl Serialize for RustcInputPrediction {
1069 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1070 where
1071 S: Serializer,
1072 {
1073 RustcInputPredictionWire {
1074 version: self.version,
1075 inputs: if self.version == 4 {
1076 compact_prediction_inputs(&self.inputs)
1077 } else {
1078 self.inputs.clone()
1079 },
1080 environment: self.environment.clone(),
1081 compiler_duration_ns: self.compiler_duration_ns,
1082 crate_name: self.crate_name.clone(),
1083 }
1084 .serialize(serializer)
1085 }
1086}
1087
1088impl<'de> Deserialize<'de> for RustcInputPrediction {
1089 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1090 where
1091 D: Deserializer<'de>,
1092 {
1093 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1094 let inputs = if wire.version == 4 {
1095 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1096 } else {
1097 wire.inputs
1098 };
1099 Ok(Self {
1100 version: wire.version,
1101 inputs,
1102 environment: wire.environment,
1103 compiler_duration_ns: wire.compiler_duration_ns,
1104 crate_name: wire.crate_name,
1105 })
1106 }
1107}
1108
1109fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1110 let mut previous = "";
1111 inputs
1112 .iter()
1113 .map(|input| {
1114 let mut shared = previous
1115 .bytes()
1116 .zip(input.bytes())
1117 .take_while(|(left, right)| left == right)
1118 .count();
1119 while !input.is_char_boundary(shared) {
1120 shared -= 1;
1121 }
1122 let compact = format!("{shared}:{}", &input[shared..]);
1123 previous = input;
1124 compact
1125 })
1126 .collect()
1127}
1128
1129fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1130 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1131 let mut decoded_bytes = 0_usize;
1132 for input in inputs {
1133 let (shared_text, suffix) = input
1134 .split_once(':')
1135 .ok_or("compact rustc prediction input has no prefix length")?;
1136 let shared: usize = shared_text
1137 .parse()
1138 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1139 if shared.to_string() != shared_text
1140 || shared > expanded.last().map_or(0, String::len)
1141 || expanded
1142 .last()
1143 .is_some_and(|previous| !previous.is_char_boundary(shared))
1144 {
1145 return Err("compact rustc prediction prefix is not canonical");
1146 }
1147 let mut path = expanded
1148 .last()
1149 .map_or_else(String::new, |previous| previous[..shared].to_string());
1150 path.push_str(suffix);
1151 decoded_bytes = decoded_bytes
1152 .checked_add(path.len())
1153 .ok_or("compact rustc prediction is too large")?;
1154 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1155 return Err("compact rustc prediction is too large");
1156 }
1157 expanded.push(path);
1158 }
1159 Ok(expanded)
1160}
1161
1162fn is_zero(value: &u64) -> bool {
1163 *value == 0
1164}
1165
1166impl RustcInputPrediction {
1167 pub fn discover(
1170 &self,
1171 working_dir: &Path,
1172 path_mappings: &[PathMapping],
1173 digests: &dyn FileDigestCache,
1174 ) -> Result<DiscoveredInputs, BypassReason> {
1175 if !matches!(self.version, 2..=4) {
1176 return Err(BypassReason::UnsupportedPrediction);
1177 }
1178 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1179 return Err(BypassReason::UnsupportedPrediction);
1180 }
1181 let mut paths = BTreeSet::new();
1182 let mut named = BTreeSet::new();
1183 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1184 let mut native_bytes = 0_u64;
1185 let mut counted = BTreeSet::new();
1186 for path in &self.inputs {
1187 if self.version >= 3
1188 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1189 {
1190 let directory = denormalize_path(path, path_mappings)?;
1191 dep_info::collect_native_directory(
1192 &directory,
1193 &admitted_roots,
1194 &mut paths,
1195 &mut native_bytes,
1196 &mut counted,
1197 )?;
1198 } else {
1199 let path = denormalize_path(path, path_mappings)?;
1200 named.insert(path.clone());
1201 paths.insert(path);
1202 }
1203 }
1204 let native_only = paths.difference(&named).cloned().collect();
1205 let environment = self
1206 .environment
1207 .iter()
1208 .map(|name| {
1209 if name.is_empty() || name.contains(['=', '\0']) {
1210 return Err(BypassReason::UnsupportedPrediction);
1211 }
1212 let value = std::env::var_os(name)
1213 .map(|value| {
1214 value
1215 .into_string()
1216 .map_err(|_| BypassReason::UnsupportedPrediction)
1217 })
1218 .transpose()?;
1219 Ok((name.clone(), value))
1220 })
1221 .collect::<Result<BTreeMap<_, _>, _>>()?;
1222 Ok(
1223 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)?
1224 .with_native_only(native_only),
1225 )
1226 }
1227}
1228
1229#[derive(Serialize)]
1230struct ActionDescriptor {
1231 version: u8,
1232 kind: &'static str,
1233 adapter_version: u8,
1234 compiler: CompilerDescriptor,
1235 arguments: Vec<String>,
1236 environment: BTreeMap<String, Option<String>>,
1237 inputs: Vec<InputDescriptor>,
1238 #[serde(skip_serializing_if = "Option::is_none")]
1241 linker: Option<LinkerIdentity>,
1242}
1243
1244#[derive(Serialize)]
1245struct InvocationDescriptor {
1246 version: u8,
1247 kind: &'static str,
1248 adapter_version: u8,
1249 compiler: CompilerDescriptor,
1250 arguments: Vec<String>,
1251 required_inputs: Vec<String>,
1252}
1253
1254#[derive(Serialize)]
1255struct CompilerDescriptor {
1256 toolchain: String,
1257 rustc_version: String,
1258 host: String,
1259 #[serde(skip_serializing_if = "Option::is_none")]
1260 driver: Option<String>,
1261}
1262
1263#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1264struct InputDescriptor {
1265 path: String,
1266 digest: CacheDigest,
1267}
1268
1269#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1271enum StaticLibraryNaming {
1272 Msvc,
1274 Unix,
1276}
1277
1278struct Parser<'a> {
1279 arguments: &'a [OsString],
1280 index: usize,
1281 parsed: Vec<Argument>,
1282 source: Option<PathBuf>,
1283 crate_types: Vec<String>,
1284 emits: Vec<Emit>,
1285 required_inputs: Vec<PathBuf>,
1286 native_libraries: Vec<String>,
1287 test: bool,
1288 crate_name: Option<String>,
1289 extra_filename: String,
1290 out_dir: Option<PathBuf>,
1291 explicit_output: Option<PathBuf>,
1292 target: Option<String>,
1293 sysroot_backend: Option<String>,
1294 options: ParseOptions,
1295}
1296
1297struct ExpandedArguments {
1298 arguments: Vec<OsString>,
1299}
1300
1301#[derive(Default)]
1302struct ResponseExpander {
1303 shell_argfiles: bool,
1304 next_is_unstable_option: bool,
1305 arguments: Vec<OsString>,
1306}
1307
1308impl ResponseExpander {
1309 fn push(&mut self, argument: String) {
1310 if self.next_is_unstable_option {
1311 self.shell_argfiles |= argument == "shell-argfiles";
1312 self.next_is_unstable_option = false;
1313 } else if let Some(option) = argument.strip_prefix("-Z") {
1314 if option.is_empty() {
1315 self.next_is_unstable_option = true;
1316 } else {
1317 self.shell_argfiles |= option == "shell-argfiles";
1318 }
1319 }
1320 self.arguments.push(argument.into());
1321 }
1322}
1323
1324fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1327 let mut expanded = ResponseExpander::default();
1328 for (index, argument) in arguments.iter().enumerate() {
1329 let argument = argument
1330 .to_str()
1331 .ok_or(BypassReason::NonUtf8Argument { index })?;
1332 let Some(argfile) = argument.strip_prefix('@') else {
1333 expanded.push(argument.to_string());
1334 continue;
1335 };
1336 let (path, shell) = match argfile.split_once(':') {
1337 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1338 _ => (argfile, false),
1339 };
1340 let contents = std::fs::read_to_string(path).map_err(|error| {
1341 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1342 })?;
1343 if shell {
1344 let arguments = shlex::split(&contents).ok_or_else(|| {
1345 BypassReason::ResponseFile(format!(
1346 "invalid shell-style arguments in {}",
1347 Path::new(path).display()
1348 ))
1349 })?;
1350 for argument in arguments {
1351 expanded.push(argument);
1352 }
1353 } else {
1354 for argument in contents.lines() {
1355 expanded.push(argument.to_string());
1356 }
1357 }
1358 }
1359 Ok(ExpandedArguments {
1360 arguments: expanded.arguments,
1361 })
1362}
1363
1364impl<'a> Parser<'a> {
1365 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1366 Self {
1367 arguments,
1368 options,
1369 index: 0,
1370 parsed: Vec::new(),
1371 source: None,
1372 crate_types: Vec::new(),
1373 emits: Vec::new(),
1374 required_inputs: Vec::new(),
1375 native_libraries: Vec::new(),
1376 test: false,
1377 crate_name: None,
1378 extra_filename: String::new(),
1379 out_dir: None,
1380 explicit_output: None,
1381 target: None,
1382 sysroot_backend: None,
1383 }
1384 }
1385
1386 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1387 while self.index < self.arguments.len() {
1388 let value = self.current()?.to_string();
1389 self.index += 1;
1390 if let Some(long) = value.strip_prefix("--") {
1391 self.parse_long(long)?;
1392 } else if value.starts_with('-') && value != "-" {
1393 self.parse_short(&value)?;
1394 } else {
1395 self.parse_input(&value)?;
1396 }
1397 }
1398
1399 if let Some(backend) = self.sysroot_backend.take()
1401 && self.parsed.iter().any(
1402 |argument| matches!(argument, Argument::Path { flag, .. } if flag == "--sysroot"),
1403 )
1404 {
1405 return Err(BypassReason::UnknownFlag(backend));
1406 }
1407 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1408 let link_output = self.classify()?;
1409 self.require_native_libraries(link_output)?;
1410 let crate_name = self.crate_name.clone().map_or_else(
1411 || {
1412 source
1413 .file_stem()
1414 .and_then(|name| name.to_str())
1415 .map(|name| name.replace('-', "_"))
1416 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1417 },
1418 Ok,
1419 )?;
1420 self.required_inputs.push(source.clone());
1421 self.drop_unobservable_diagnostic_width();
1422 Ok(RustcInvocation {
1423 arguments: self.parsed,
1424 source,
1425 required_inputs: self.required_inputs,
1426 crate_name,
1427 extra_filename: self.extra_filename,
1428 out_dir: self.out_dir,
1429 explicit_output: self.explicit_output,
1430 emits: self.emits,
1431 target: self.target,
1432 link_output,
1433 })
1434 }
1435
1436 fn current(&self) -> Result<&str, BypassReason> {
1437 self.arguments[self.index]
1438 .to_str()
1439 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1440 }
1441
1442 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1443 if let Some(value) = inline {
1444 if value.is_empty() {
1445 return Err(BypassReason::MissingValue(flag.into()));
1446 }
1447 return Ok(value.into());
1448 }
1449 if self.index >= self.arguments.len() {
1450 return Err(BypassReason::MissingValue(flag.into()));
1451 }
1452 let value = self.current()?.to_string();
1453 self.index += 1;
1454 Ok(value)
1455 }
1456
1457 fn drop_unobservable_diagnostic_width(&mut self) {
1464 let capped = self
1466 .parsed
1467 .iter()
1468 .rev()
1469 .find_map(|argument| match argument {
1470 Argument::Plain(value) => value.strip_prefix("--cap-lints="),
1471 _ => None,
1472 })
1473 .is_some_and(|level| level == "allow");
1474 let forced = self
1476 .parsed
1477 .iter()
1478 .any(|argument| matches!(argument, Argument::Plain(value) if value.starts_with("--force-warn=")));
1479 if capped && !forced {
1480 self.parsed.retain(|argument| {
1481 !matches!(argument, Argument::Plain(value) if value.starts_with("--diagnostic-width="))
1482 });
1483 }
1484 }
1485
1486 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1487 let (flag, inline) = value
1488 .split_once('=')
1489 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1490 let rendered_flag = format!("--{flag}");
1491 match flag {
1492 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1493 "test" => {
1494 self.test = true;
1495 self.parsed.push(Argument::Plain(rendered_flag));
1496 Ok(())
1497 }
1498 "verbose" => {
1499 self.parsed.push(Argument::Plain(rendered_flag));
1500 Ok(())
1501 }
1502 "crate-name" => {
1503 let value = self.take_value(&rendered_flag, inline)?;
1504 self.crate_name = Some(value.clone());
1505 self.parsed
1506 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1507 Ok(())
1508 }
1509 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1510 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1511 | "deny" | "forbid" | "cap-lints" => {
1512 let value = self.take_value(&rendered_flag, inline)?;
1513 self.parsed
1514 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1515 Ok(())
1516 }
1517 "target" => {
1518 let value = self.take_value(&rendered_flag, inline)?;
1519 let effective = self.target.is_none();
1523 if effective {
1524 self.target = Some(value.clone());
1525 }
1526 if value.ends_with(".json") || value.contains(['/', '\\']) {
1527 let path = PathBuf::from(value);
1528 if effective {
1529 self.required_inputs.push(path.clone());
1530 }
1531 self.parsed.push(Argument::Path {
1532 flag: rendered_flag,
1533 path,
1534 });
1535 } else {
1536 self.parsed
1537 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1538 }
1539 Ok(())
1540 }
1541 "crate-type" => {
1542 let value = self.take_value(&rendered_flag, inline)?;
1543 self.crate_types
1544 .extend(value.split(',').map(ToOwned::to_owned));
1545 self.parsed
1546 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1547 Ok(())
1548 }
1549 "emit" => {
1550 let value = self.take_value(&rendered_flag, inline)?;
1551 let emits = parse_emits(&value);
1552 self.emits.extend(emits.clone());
1553 self.parsed.push(Argument::Emit(emits));
1554 Ok(())
1555 }
1556 "out-dir" => {
1557 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1558 self.out_dir = Some(path.clone());
1559 self.parsed.push(Argument::Path {
1560 flag: rendered_flag,
1561 path,
1562 });
1563 Ok(())
1564 }
1565 "sysroot" => {
1566 let path = self.take_value(&rendered_flag, inline)?;
1567 self.parsed.push(Argument::Path {
1568 flag: rendered_flag,
1569 path: path.into(),
1570 });
1571 Ok(())
1572 }
1573 "extern" => {
1574 let value = self.take_value(&rendered_flag, inline)?;
1575 let (name, path) = value
1576 .split_once('=')
1577 .map_or((value.as_str(), None), |(name, path)| {
1578 (name, Some(PathBuf::from(path)))
1579 });
1580 if let Some(path) = &path {
1581 self.required_inputs.push(path.clone());
1582 }
1583 self.parsed.push(Argument::Extern {
1584 name: name.into(),
1585 path,
1586 });
1587 Ok(())
1588 }
1589 "remap-path-prefix" => {
1590 let value = self.take_value(&rendered_flag, inline)?;
1591 let Some((from, to)) = value.split_once('=') else {
1592 return Err(BypassReason::MissingValue(rendered_flag));
1593 };
1594 self.parsed.push(Argument::RemapPath {
1595 from: from.into(),
1596 to: to.into(),
1597 });
1598 Ok(())
1599 }
1600 "codegen" => {
1601 let value = self.take_value(&rendered_flag, inline)?;
1602 self.parse_codegen(&value)
1603 }
1604 "jobs-frontend" => {
1605 let value = self.take_value(&rendered_flag, inline)?;
1606 self.parsed
1607 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1608 Ok(())
1609 }
1610 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1611 }
1612 }
1613
1614 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1615 if let Some(attached) = value.strip_prefix("-Z") {
1616 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1617 match option.as_str() {
1618 "embed-metadata=no"
1622 | "embed-metadata=yes"
1623 | "force-unstable-if-unmarked"
1624 | "shell-argfiles"
1625 | "unstable-options" => {
1626 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1627 return Ok(());
1628 }
1629 "use-sync-unwind=yes" | "use-sync-unwind=no" => {
1632 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1633 return Ok(());
1634 }
1635 "threads" | "threads=" => {
1636 return Err(BypassReason::MissingValue("-Zthreads".into()));
1637 }
1638 "remap-cwd-prefix" | "remap-cwd-prefix=" => {
1639 return Err(BypassReason::MissingValue("-Zremap-cwd-prefix".into()));
1640 }
1641 _ => {}
1642 }
1643 if option.starts_with("remap-cwd-prefix=") {
1649 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1650 return Ok(());
1651 }
1652 if option == "share-generics" {
1653 return Err(BypassReason::MissingValue("-Zshare-generics".into()));
1654 }
1655 if let Some(value) = option.strip_prefix("share-generics=") {
1656 if matches!(
1657 value,
1658 "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1659 ) {
1660 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1661 return Ok(());
1662 }
1663 return Err(if value.is_empty() {
1664 BypassReason::MissingValue("-Zshare-generics".into())
1665 } else {
1666 BypassReason::UnknownFlag(format!("-Z{option}"))
1667 });
1668 }
1669 if option.starts_with("threads=") {
1670 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1671 return Ok(());
1672 }
1673 if let Some(backend) = option.strip_prefix("codegen-backend=")
1675 && !backend.is_empty()
1676 && !backend.contains(['.', '/', '\\'])
1677 {
1678 self.sysroot_backend = Some(format!("-Z{option}"));
1679 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1680 return Ok(());
1681 }
1682 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1683 }
1684 match value {
1685 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1688 "-g" | "-O" | "-v" => {
1689 self.parsed.push(Argument::Plain(value.into()));
1690 return Ok(());
1691 }
1692 _ => {}
1693 }
1694 for (short, long) in [
1695 ("-A", "--allow"),
1696 ("-W", "--warn"),
1697 ("-D", "--deny"),
1698 ("-F", "--forbid"),
1699 ] {
1700 if let Some(attached) = value.strip_prefix(short) {
1701 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1702 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1703 return Ok(());
1704 }
1705 }
1706 if let Some(attached) = value.strip_prefix("-C") {
1707 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1708 return self.parse_codegen(&option);
1709 }
1710 if let Some(attached) = value.strip_prefix("-L") {
1711 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1712 let (kind, path) = search
1713 .split_once('=')
1714 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1715 if !matches!(kind, "dependency" | "native") {
1716 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1717 }
1718 self.parsed.push(Argument::SearchPath {
1719 kind: kind.into(),
1720 path: path.into(),
1721 });
1722 return Ok(());
1723 }
1724 if let Some(attached) = value.strip_prefix("-l") {
1725 let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1726 self.parsed.push(Argument::Plain(format!("-l{library}")));
1727 self.native_libraries.push(library);
1728 return Ok(());
1729 }
1730 if let Some(attached) = value.strip_prefix("-o") {
1731 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1732 self.explicit_output = Some(path.clone().into());
1733 self.parsed.push(Argument::Path {
1734 flag: "-o".into(),
1735 path: path.into(),
1736 });
1737 return Ok(());
1738 }
1739 Err(BypassReason::UnknownFlag(value.into()))
1740 }
1741
1742 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1743 let name = value.split_once('=').map_or(value, |(name, _)| name);
1744 if name == "incremental" {
1745 return Err(BypassReason::Incremental);
1746 }
1747 if name == "profile-use" {
1748 let Some((_, profile)) = value.split_once('=') else {
1749 return Err(BypassReason::MissingValue("-Cprofile-use".into()));
1750 };
1751 if profile.is_empty() {
1752 return Err(BypassReason::MissingValue("-Cprofile-use".into()));
1753 }
1754 let path = PathBuf::from(profile);
1755 self.required_inputs.push(path.clone());
1756 self.parsed.push(Argument::Path {
1757 flag: "-Cprofile-use".into(),
1758 path,
1759 });
1760 return Ok(());
1761 }
1762 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err()
1763 && !matches!(name, "linker" | "linker-flavor")
1764 {
1765 return Err(BypassReason::UnknownCodegenOption(name.into()));
1766 }
1767 if name == "linker-plugin-lto"
1773 && let Some((_, selection)) = value.split_once('=')
1774 && !matches!(
1775 selection,
1776 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1777 )
1778 {
1779 return Err(BypassReason::UnknownCodegenOption(format!(
1780 "linker-plugin-lto={selection}"
1781 )));
1782 }
1783 if name == "link-arg"
1790 && let Some((_, option)) = value.split_once('=')
1791 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1792 && !selection.is_empty()
1793 && !selection.contains(',')
1794 && !self.parsed.iter().any(
1795 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1796 )
1797 {
1798 if !self.parsed.iter().any(
1799 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1800 ) {
1801 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1802 }
1803 return Ok(());
1804 }
1805 if name == "link-arg"
1808 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1809 && safe_install_name(option)
1810 {
1811 self.parsed.push(Argument::InstallName(option.to_owned()));
1812 return Ok(());
1813 }
1814 if matches!(name, "link-arg" | "link-args")
1819 && let Some((_, option)) = value.split_once('=')
1820 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1821 && !prefix.trim_end_matches('/').is_empty()
1822 && !prefix.contains(',')
1823 {
1824 let trailing_slash = prefix.ends_with('/');
1825 self.parsed.push(Argument::OsoPrefix {
1826 path: PathBuf::from(prefix.trim_end_matches('/')),
1827 trailing_slash,
1828 });
1829 return Ok(());
1830 }
1831 self.parsed
1832 .push(Argument::Plain(format!("--codegen={value}")));
1833 if name == "extra-filename" {
1834 self.extra_filename = value
1835 .split_once('=')
1836 .map_or(String::new(), |(_, value)| value.to_string());
1837 }
1838 Ok(())
1839 }
1840
1841 fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1856 for library in std::mem::take(&mut self.native_libraries) {
1857 if link_output != LinkOutput::Library {
1858 return Err(BypassReason::NativeLibrary(library));
1859 }
1860 let Some((kind, name)) = library.split_once('=') else {
1868 continue;
1869 };
1870 let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1871 match kind {
1872 "static" => {}
1873 "dylib" | "framework" | "link-arg" => continue,
1874 _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1875 }
1876 let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1879 let (mut verbatim, mut bundle) = (false, true);
1880 for modifier in modifiers.split(',') {
1881 match modifier {
1882 "+verbatim" => verbatim = true,
1883 "-verbatim" => verbatim = false,
1884 "+bundle" => bundle = true,
1885 "-bundle" => bundle = false,
1886 _ => {}
1887 }
1888 }
1889 if !bundle {
1890 continue;
1891 }
1892 let archive = self
1893 .find_native_static_library(name, verbatim)
1894 .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1895 .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1896 self.required_inputs.push(archive);
1897 }
1898 Ok(())
1899 }
1900
1901 fn find_native_static_library(
1913 &self,
1914 name: &str,
1915 verbatim: bool,
1916 ) -> Result<Option<PathBuf>, ()> {
1917 let file_names = if verbatim {
1918 vec![name.to_owned()]
1919 } else {
1920 match self.static_library_naming() {
1921 Some(StaticLibraryNaming::Msvc) => {
1922 vec![format!("{name}.lib"), format!("lib{name}.a")]
1923 }
1924 Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1925 None => return Err(()),
1926 }
1927 };
1928 Ok(self
1929 .parsed
1930 .iter()
1931 .filter_map(|argument| match argument {
1932 Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1933 _ => None,
1934 })
1935 .flat_map(|directory| {
1936 file_names
1937 .iter()
1938 .map(move |file_name| directory.join(file_name))
1939 })
1940 .find(|candidate| candidate.is_file()))
1941 }
1942
1943 fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1951 let Some(target) = self.target.as_deref() else {
1952 return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1953 StaticLibraryNaming::Msvc
1954 } else {
1955 StaticLibraryNaming::Unix
1956 });
1957 };
1958 if target.ends_with(".json")
1959 || target.contains(['/', '\\'])
1960 || self.options.custom_target_search
1961 {
1962 return None;
1963 }
1964 Some(if target.contains("msvc") || target.ends_with("-uefi") {
1967 StaticLibraryNaming::Msvc
1968 } else {
1969 StaticLibraryNaming::Unix
1970 })
1971 }
1972
1973 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1974 if value == "-" {
1975 return Err(BypassReason::StandardInput);
1976 }
1977 if self.source.replace(value.into()).is_some() {
1978 return Err(BypassReason::MultipleInputs);
1979 }
1980 Ok(())
1981 }
1982
1983 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1984 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1993 let builds_a_library = !self.test
1994 && !self.crate_types.is_empty()
1995 && self
1996 .crate_types
1997 .iter()
1998 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1999 let link_output = if never_links || builds_a_library {
2000 LinkOutput::Library
2001 } else if self
2002 .target
2003 .as_deref()
2004 .is_some_and(compiler_bundled_wasm_target)
2005 && ((self.test && self.crate_types.is_empty())
2006 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
2007 {
2008 if self.parsed.iter().any(|argument| match argument {
2009 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
2010 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
2011 !matches!(
2012 value.rsplit_once('=').map(|(_, value)| value),
2013 Some("y" | "yes" | "on" | "true")
2014 )
2015 }
2016 _ => false,
2017 }) {
2018 return Err(BypassReason::UnknownCodegenOption(
2019 "link-self-contained".into(),
2020 ));
2021 }
2022 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
2023 && self.parsed.iter().any(|argument| {
2024 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
2025 })
2026 {
2027 return Err(BypassReason::UnknownCodegenOption(
2028 "target-feature=-crt-static".into(),
2029 ));
2030 }
2031 LinkOutput::WasmExecutable
2035 } else if self.options.cache_native_links && self.links_a_native_artifact() {
2036 self.check_native_link_is_portable()?;
2037 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
2038 LinkOutput::NativeProcMacro
2039 } else if matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib") {
2040 LinkOutput::NativeSharedLibrary
2041 } else {
2042 LinkOutput::NativeExecutable
2043 }
2044 } else if self.test {
2045 return Err(BypassReason::UnsupportedCrateType("test".into()));
2046 } else {
2047 return Err(BypassReason::UnsupportedCrateType(
2048 self.crate_types
2049 .iter()
2050 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
2051 .cloned()
2052 .unwrap_or_else(|| "bin".into()),
2053 ));
2054 };
2055 if link_output != LinkOutput::Library
2064 && let Some(option) = self.first_link_argument(link_output)
2065 {
2066 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
2067 }
2068 if self
2069 .parsed
2070 .iter()
2071 .any(|arg| matches!(arg, Argument::InstallName(_)))
2072 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
2073 {
2074 return Err(BypassReason::UnmodeledLinkArgument(
2075 "link-arg=-Wl,-install_name".into(),
2076 ));
2077 }
2078 if let Some(selection) = self.parsed.iter().find_map(|argument| {
2085 let Argument::FuseLd(selection) = argument else {
2086 return None;
2087 };
2088 Some(selection.as_str())
2089 }) && !matches!(
2090 link_output,
2091 LinkOutput::Library
2092 | LinkOutput::NativeExecutable
2093 | LinkOutput::NativeProcMacro
2094 | LinkOutput::NativeSharedLibrary
2095 ) {
2096 return Err(BypassReason::UnmodeledLinkArgument(format!(
2097 "link-arg=-fuse-ld={selection}"
2098 )));
2099 }
2100 if self.parsed.iter().any(|argument| {
2101 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
2102 }) && !matches!(
2103 link_output,
2104 LinkOutput::Library
2105 | LinkOutput::NativeExecutable
2106 | LinkOutput::NativeProcMacro
2107 | LinkOutput::NativeSharedLibrary
2108 ) {
2109 return Err(BypassReason::UnknownCodegenOption("linker".into()));
2110 }
2111 if link_output != LinkOutput::Library
2116 && self.parsed.iter().any(|argument| {
2117 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker-flavor="))
2118 })
2119 {
2120 return Err(BypassReason::UnknownCodegenOption("linker-flavor".into()));
2121 }
2122 if !matches!(
2126 link_output,
2127 LinkOutput::Library
2128 | LinkOutput::NativeExecutable
2129 | LinkOutput::NativeProcMacro
2130 | LinkOutput::NativeSharedLibrary
2131 ) && self
2132 .parsed
2133 .iter()
2134 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
2135 {
2136 return Err(BypassReason::UnmodeledLinkArgument(
2137 "link-arg=-Wl,-oso_prefix".into(),
2138 ));
2139 }
2140 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
2141 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
2142 _ => None,
2143 }) {
2144 return Err(BypassReason::UnresolvedExtern(name.clone()));
2145 }
2146 if let Some(emit) = self
2147 .emits
2148 .iter()
2149 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
2150 {
2151 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
2152 }
2153 if !self
2154 .emits
2155 .iter()
2156 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
2157 {
2158 return Err(BypassReason::NoCacheableOutput);
2159 }
2160 Ok(link_output)
2161 }
2162}
2163
2164impl Parser<'_> {
2165 fn first_link_argument(&self, link_output: LinkOutput) -> Option<&str> {
2168 self.parsed.iter().find_map(|argument| {
2169 let Argument::Plain(value) = argument else {
2170 return None;
2171 };
2172 let option = value.strip_prefix("--codegen=")?;
2173 let (name, value) = option.split_once('=').unwrap_or((option, ""));
2174 (matches!(name, "link-arg" | "link-args")
2175 && !(name == "link-arg" && elf_keyword_is_modeled(link_output, value))
2176 && !msvc_options_are_modeled(link_output, name, value))
2177 .then_some(option)
2178 })
2179 }
2180
2181 fn oso_prefix_covers_outputs(&self) -> bool {
2190 let Some(directory) = self
2191 .out_dir
2192 .as_deref()
2193 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2194 else {
2195 return false;
2196 };
2197 if !directory.is_absolute() {
2198 return false;
2199 }
2200 let directory = normalize_components(directory);
2201 self.parsed.iter().any(|argument| {
2202 let Argument::OsoPrefix { path, .. } = argument else {
2203 return false;
2204 };
2205 path.is_absolute()
2206 && (directory.starts_with(normalize_components(path))
2207 || std::fs::canonicalize(&directory)
2208 .ok()
2209 .zip(std::fs::canonicalize(path).ok())
2210 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2211 })
2212 }
2213
2214 fn links_a_native_artifact(&self) -> bool {
2221 self.target.is_none()
2222 && self.emits.iter().any(|emit| emit.kind == "link")
2227 && ((self.test && self.crate_types.is_empty())
2228 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro"))
2229 || (cfg!(target_os = "linux")
2230 && matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib")))
2231 }
2232
2233 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2240 for argument in &self.parsed {
2241 let Argument::Plain(value) = argument else {
2242 continue;
2243 };
2244 let (name, value) = if value == "-g" {
2247 ("debuginfo", Some("2"))
2248 } else if let Some(option) = value.strip_prefix("--codegen=") {
2249 match option.split_once('=') {
2250 Some((name, value)) => (name, Some(value)),
2251 None => (option, None),
2255 }
2256 } else {
2257 continue;
2258 };
2259 let unportable = match name {
2260 "split-debuginfo" => match value {
2266 Some("off") => false,
2267 Some("unpacked") if cfg!(target_os = "macos") => {
2268 !self.oso_prefix_covers_outputs()
2269 }
2270 _ => true,
2271 },
2272 "debuginfo" if cfg!(target_os = "macos") => {
2281 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2282 }
2283 "rpath" => is_enabled(value),
2291 "prefer-dynamic" => {
2292 is_enabled(value)
2293 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2294 }
2295 "link-self-contained" => true,
2298 _ => false,
2299 };
2300 if unportable {
2301 return Err(BypassReason::UnportableNativeLink(match value {
2302 Some(value) => format!("{name}={value}"),
2303 None => name.to_owned(),
2304 }));
2305 }
2306 }
2307 Ok(())
2308 }
2309}
2310
2311fn safe_install_name(name: &str) -> bool {
2312 !name.is_empty()
2313 && name
2314 .bytes()
2315 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2316}
2317
2318fn is_enabled(value: Option<&str>) -> bool {
2321 matches!(value, None | Some("y" | "yes" | "on" | "true"))
2322}
2323
2324fn elf_keyword_is_modeled(link_output: LinkOutput, value: &str) -> bool {
2334 cfg!(target_os = "linux")
2335 && matches!(
2336 link_output,
2337 LinkOutput::NativeExecutable
2338 | LinkOutput::NativeProcMacro
2339 | LinkOutput::NativeSharedLibrary
2340 )
2341 && value.strip_prefix("-Wl,-z,").is_some_and(|keyword| {
2342 matches!(
2343 keyword,
2344 "defs"
2345 | "lazy"
2346 | "nodelete"
2347 | "nodlopen"
2348 | "noexecstack"
2349 | "norelro"
2350 | "now"
2351 | "origin"
2352 | "relro"
2353 )
2354 })
2355}
2356
2357fn msvc_options_are_modeled(link_output: LinkOutput, name: &str, value: &str) -> bool {
2373 cfg!(target_env = "msvc")
2374 && matches!(
2375 link_output,
2376 LinkOutput::NativeExecutable
2377 | LinkOutput::NativeProcMacro
2378 | LinkOutput::NativeSharedLibrary
2379 )
2380 && match name {
2381 "link-arg" => msvc_option_reads_no_file(value),
2382 "link-args" => {
2385 let mut options = value.split_ascii_whitespace().peekable();
2386 options.peek().is_some() && options.all(msvc_option_reads_no_file)
2387 }
2388 _ => false,
2389 }
2390}
2391
2392fn msvc_option_reads_no_file(option: &str) -> bool {
2396 let Some(option) = option.strip_prefix(['/', '-']) else {
2397 return false;
2398 };
2399 if option.eq_ignore_ascii_case("brepro") {
2400 return true;
2401 }
2402 let Some((name, sizes)) = option.split_once(':') else {
2403 return false;
2404 };
2405 let is_size = |size: &str| match size.strip_prefix("0x").or_else(|| size.strip_prefix("0X")) {
2408 Some(hex) => !hex.is_empty() && hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
2409 None => !size.is_empty() && size.bytes().all(|byte| byte.is_ascii_digit()),
2410 };
2411 name.eq_ignore_ascii_case("stack")
2412 && sizes.split(',').count() <= 2
2413 && sizes.split(',').all(is_size)
2414}
2415
2416fn compiler_bundled_wasm_target(target: &str) -> bool {
2417 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2418}
2419
2420fn parse_emits(value: &str) -> Vec<Emit> {
2421 value
2422 .split(',')
2423 .map(|emit| {
2424 let (kind, path) = emit
2425 .split_once('=')
2426 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2427 Emit {
2428 kind: kind.into(),
2429 path,
2430 }
2431 })
2432 .collect()
2433}
2434
2435struct ActionBuilder<'a> {
2436 invocation: &'a RustcInvocation,
2437 context: ActionContext,
2438 mappings: Vec<SharedPathMapping>,
2439 aliases: PathAliases,
2443 linker: Option<LinkerIdentity>,
2444}
2445
2446impl<'a> ActionBuilder<'a> {
2447 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2448 context.path_mappings = PathMapping::ordered(&context.path_mappings);
2449 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2450 Self {
2451 linker: None,
2452 invocation,
2453 mappings,
2454 aliases: PathAliases::default(),
2455 context,
2456 }
2457 }
2458
2459 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2460 self.linker = linker;
2461 self
2462 }
2463
2464 fn build(self) -> Result<RustcAction, BypassReason> {
2465 self.validate_mappings()?;
2466 let invocation = self.invocation_descriptor()?;
2467 let environment = self.environment_descriptor()?;
2468
2469 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2470 for input in &self.context.inputs {
2471 input
2472 .digest
2473 .validate()
2474 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2475 let path = self.normalize_input_path(&input.path)?;
2476 if inputs
2477 .insert(path.clone(), input.digest.clone())
2478 .is_some_and(|existing| existing != input.digest)
2479 {
2480 return Err(BypassReason::ConflictingInput(path));
2481 }
2482 }
2483 let required = self
2484 .invocation
2485 .required_inputs
2486 .iter()
2487 .map(|path| self.normalize_input_path(path))
2488 .collect::<Result<BTreeSet<_>, _>>()?;
2489 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2490 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2491 }
2492 let inputs = inputs
2493 .into_iter()
2494 .map(|(path, digest)| InputDescriptor { path, digest })
2495 .collect();
2496 if self.invocation.links_natively() && self.linker.is_none() {
2499 return Err(BypassReason::UnportableNativeLink(
2500 "linker identity is unknown".into(),
2501 ));
2502 }
2503 let descriptor = ActionDescriptor {
2504 version: ACTION_SCHEMA_VERSION,
2505 kind: "rustc",
2506 adapter_version: ADAPTER_VERSION,
2507 compiler: invocation.compiler,
2508 arguments: invocation.arguments,
2509 environment,
2510 inputs,
2511 linker: self.linker.clone(),
2512 };
2513 let bytes = canonical_json(&descriptor)
2514 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2515 let digest = CacheDigest::blake3(&bytes);
2516 Ok(RustcAction { digest, bytes })
2517 }
2518
2519 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2520 self.validate_mappings()?;
2521 let arguments = self
2522 .invocation
2523 .arguments
2524 .iter()
2525 .map(|argument| self.normalize_argument(argument))
2526 .collect::<Result<Vec<_>, _>>()?;
2527 let required_inputs = self
2528 .invocation
2529 .required_inputs
2530 .iter()
2531 .map(|path| self.normalize_path(path))
2532 .collect::<Result<BTreeSet<_>, _>>()?
2533 .into_iter()
2534 .collect();
2535 Ok(InvocationDescriptor {
2536 version: ACTION_SCHEMA_VERSION,
2537 kind: "rustc",
2538 adapter_version: ADAPTER_VERSION,
2539 compiler: CompilerDescriptor {
2540 toolchain: self.context.compiler.toolchain.clone(),
2541 rustc_version: self.context.compiler.rustc_version.clone(),
2542 host: self.context.compiler.host.clone(),
2543 driver: self.context.compiler.driver.clone(),
2544 },
2545 arguments,
2546 required_inputs,
2547 })
2548 }
2549
2550 fn validate_mappings(&self) -> Result<(), BypassReason> {
2551 if !self.context.working_dir.is_absolute() {
2552 return Err(BypassReason::RelativeWorkingDirectory(
2553 self.context.working_dir.clone(),
2554 ));
2555 }
2556 let mut roots = BTreeSet::new();
2557 let mut placeholders = BTreeSet::new();
2558 for mapping in &self.mappings {
2559 if !mapping.root.is_absolute() {
2560 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2561 }
2562 if mapping.placeholder.is_empty()
2563 || !mapping
2564 .placeholder
2565 .bytes()
2566 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2567 || !roots.insert(normalize_components(&mapping.root))
2568 || !placeholders.insert(&mapping.placeholder)
2569 {
2570 return Err(BypassReason::InvalidPathPlaceholder(
2571 mapping.placeholder.clone(),
2572 ));
2573 }
2574 }
2575 Ok(())
2576 }
2577
2578 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2579 match argument {
2580 Argument::Plain(value) => Ok(value.clone()),
2581 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2582 Argument::SearchPath { kind, path } => {
2583 let text = self.normalize_path(path)?;
2584 Ok(format!("-L{kind}={text}"))
2585 }
2586 Argument::Extern { name, path } => match path {
2587 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2588 None => Ok(format!("--extern={name}")),
2589 },
2590 Argument::Emit(emits) => Ok(format!(
2591 "--emit={}",
2592 emits
2593 .iter()
2594 .map(|emit| match &emit.path {
2595 Some(path) => self
2596 .normalize_path(path)
2597 .map(|path| format!("{}={path}", emit.kind)),
2598 None => Ok(emit.kind.clone()),
2599 })
2600 .collect::<Result<Vec<_>, _>>()?
2601 .join(",")
2602 )),
2603 Argument::RemapPath { from, to } => Ok(format!(
2604 "--remap-path-prefix={}={}",
2605 self.normalize_path(from)?,
2606 to
2607 )),
2608 Argument::InstallName(name) => Ok(format!(
2609 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2610 )),
2611 Argument::OsoPrefix {
2612 path,
2613 trailing_slash,
2614 } => Ok(format!(
2615 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2616 self.normalize_path(path)?,
2617 if *trailing_slash { "/" } else { "" }
2618 )),
2619 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2620 }
2621 }
2622
2623 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2630 self.context
2631 .environment
2632 .iter()
2633 .map(|(name, value)| {
2634 let value = match value {
2635 Some(value) if self.context.portable_environment.contains(name) => {
2636 Some(self.normalize_path(Path::new(value))?)
2637 }
2638 value => value.clone(),
2639 };
2640 Ok((name.clone(), value))
2641 })
2642 .collect()
2643 }
2644
2645 fn normalize_input_path(&self, path: &Path) -> Result<String, BypassReason> {
2646 let absolute = normalize_components(&self.context.working_dir.join(path));
2647 for directory in self.invocation.native_search_paths() {
2648 let directory = normalize_components(&self.context.working_dir.join(directory));
2649 if let Ok(relative) = absolute.strip_prefix(&directory) {
2650 let suffix = relative
2656 .components()
2657 .map(|component| {
2658 component
2659 .as_os_str()
2660 .to_str()
2661 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf()))
2662 })
2663 .collect::<Result<Vec<_>, _>>()?
2664 .join("/");
2665 if !suffix.is_empty() {
2666 return Ok(format!("{}/{}", self.normalize_path(&directory)?, suffix));
2667 }
2668 }
2669 }
2670 self.normalize_path(path)
2671 }
2672
2673 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2674 normalize_resolved_shared_path_with(
2675 &self.aliases,
2676 path,
2677 &self.context.working_dir,
2678 &self.mappings,
2679 )
2680 .map_err(Into::into)
2681 }
2682}
2683
2684fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2688 directories.iter().any(|directory| {
2689 path.len() > directory.len()
2690 && path.as_bytes()[directory.len()] == b'/'
2691 && path.starts_with(directory)
2692 })
2693}
2694
2695fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2696 for mapping in mappings {
2697 let prefix = format!("${{{}}}", mapping.placeholder);
2698 let suffix = if value == prefix {
2699 ""
2700 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2701 suffix
2702 } else {
2703 continue;
2704 };
2705 if !mapping.root.is_absolute()
2706 || (!suffix.is_empty()
2707 && suffix.split('/').any(|component| {
2708 component.is_empty()
2709 || matches!(component, "." | "..")
2710 || component.contains('\\')
2711 }))
2712 {
2713 return Err(BypassReason::InvalidPredictedInput(value.into()));
2714 }
2715 let mut path = normalize_components(&mapping.root);
2716 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2717 return Ok(path);
2718 }
2719 Err(BypassReason::InvalidPredictedInput(value.into()))
2720}
2721
2722fn normalize_components(path: &Path) -> PathBuf {
2723 let mut normalized = PathBuf::new();
2724 for component in path.components() {
2725 match component {
2726 Component::CurDir => {}
2727 Component::ParentDir => {
2728 normalized.pop();
2729 }
2730 component => normalized.push(component.as_os_str()),
2731 }
2732 }
2733 normalized
2734}
2735
2736fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2737 if path.is_absolute() {
2738 normalize_components(path)
2739 } else {
2740 normalize_components(&working_dir.join(path))
2741 }
2742}
2743
2744#[cfg(test)]
2745#[path = "rustc_cache_tests.rs"]
2746mod tests;