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