1#![deny(missing_docs)]
30
31use mbx_cache_core::{CacheDigest, FileDigestCache, canonical_json};
32use serde::{Deserialize, Serialize};
33use std::collections::{BTreeMap, BTreeSet};
34use std::ffi::OsString;
35use std::path::{Component, Path, PathBuf};
36use thiserror::Error;
37
38mod dep_info;
39
40pub use dep_info::{DepInfoCommand, DiscoveredInputs, RustcDepInfo};
41
42pub const ACTION_SCHEMA_VERSION: u8 = 1;
44pub const ADAPTER_VERSION: u8 = 2;
51
52impl BypassReason {
53 pub fn kind(&self) -> &'static str {
58 self.into()
59 }
60}
61
62const SUPPORTED_CODEGEN_OPTIONS: &[&str] = &[
63 "codegen-units",
64 "control-flow-guard",
65 "debug-assertions",
66 "debuginfo",
67 "default-linker-libraries",
68 "embed-bitcode",
69 "extra-filename",
70 "force-frame-pointers",
71 "force-unwind-tables",
72 "instrument-coverage",
73 "link-arg",
74 "link-args",
75 "link-dead-code",
76 "link-self-contained",
77 "lto",
78 "metadata",
79 "no-prepopulate-passes",
80 "opt-level",
81 "overflow-checks",
82 "panic",
83 "prefer-dynamic",
84 "relocation-model",
85 "rpath",
86 "save-temps",
87 "soft-float",
88 "split-debuginfo",
89 "split-dwarf-kind",
90 "strip",
91 "symbol-mangling-version",
92 "target-cpu",
93 "target-feature",
94 "tls-model",
95];
96
97const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
98const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
99const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
100
101const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
104 "wasm32-unknown-unknown",
105 "wasm32-wasip1",
106 "wasm32-wasip1-threads",
107 "wasm32-wasip2",
108 "wasm32v1-none",
109 "wasm64-unknown-unknown",
110];
111
112#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
113#[strum(serialize_all = "kebab-case")]
114#[non_exhaustive]
124pub enum BypassReason {
125 #[error("rustc argument {index} is not valid UTF-8")]
127 NonUtf8Argument {
128 index: usize,
130 },
131 #[error("could not model rustc response file: {0}")]
133 ResponseFile(String),
134 #[error("rustc flag is not modeled by the cache adapter: {0}")]
136 UnknownFlag(String),
137 #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
139 UnknownCodegenOption(String),
140 #[error("rustc flag requires a value: {0}")]
142 MissingValue(String),
143 #[error("rustc invocation is a compiler query, not a compilation")]
145 CompilerQuery,
146 #[error("rustc invocation reads source from standard input")]
148 StandardInput,
149 #[error("rustc invocation has no source input")]
151 MissingInput,
152 #[error("rustc invocation has multiple source inputs")]
154 MultipleInputs,
155 #[error("incremental compilation cannot be combined with action caching")]
157 Incremental,
158 #[error("rustc crate type is not cacheable yet: {0}")]
160 UnsupportedCrateType(String),
161 #[error("rustc output type is not cacheable yet: {0}")]
163 UnsupportedEmit(String),
164 #[error("rustc invocation does not emit a cacheable artifact")]
166 NoCacheableOutput,
167 #[error("rustc invocation does not emit dependency information")]
169 NoDepInfo,
170 #[error("rustc output paths do not share one directory")]
172 SplitOutputDirectories,
173 #[error("rustc output path has no file name: {0}")]
175 InvalidOutputPath(PathBuf),
176 #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
178 ImplicitEmitWithOutputFile(PathBuf),
179 #[error("native library lookup is not cacheable yet")]
181 NativeLibrary,
182 #[error("rustc output name does not distinguish a program from a library: {0}")]
184 AmbiguousOutputName(PathBuf),
185 #[error("native link is not reproducible across checkouts: {0}")]
187 UnportableNativeLink(String),
188 #[error("rustc link argument is not modeled by the cache adapter: {0}")]
190 UnmodeledLinkArgument(String),
191 #[error("rustc search path kind is not cacheable yet: {0}")]
193 UnsupportedSearchPath(String),
194 #[error("rustc extern does not identify an input artifact: {0}")]
196 UnresolvedExtern(String),
197 #[error("absolute path has no stable cache mapping: {0}")]
199 UnmappedAbsolutePath(PathBuf),
200 #[error("cache key paths must be valid UTF-8: {0}")]
202 NonUtf8Path(PathBuf),
203 #[error("cache action working directory must be absolute: {0}")]
205 RelativeWorkingDirectory(PathBuf),
206 #[error("cache path mapping must use an absolute root: {0}")]
208 RelativePathMapping(PathBuf),
209 #[error("cache path mapping placeholder is invalid: {0}")]
211 InvalidPathPlaceholder(String),
212 #[error("required compiler input was not provided: {0}")]
214 MissingRequiredInput(String),
215 #[error("compiler input has an invalid digest: {0}")]
217 InvalidInputDigest(String),
218 #[error("compiler input appears more than once with different content: {0}")]
220 ConflictingInput(String),
221 #[error("rustc dep-info is malformed: {0}")]
223 MalformedDepInfo(String),
224 #[error("failed to read rustc dep-info {path}: {message}")]
226 DepInfoRead {
227 path: PathBuf,
229 message: String,
231 },
232 #[error("rustc dep-info output path must be absolute: {0}")]
234 RelativeDepInfoPath(PathBuf),
235 #[error("rustc dep-info output path cannot contain a comma: {0}")]
237 UnsafeDepInfoPath(PathBuf),
238 #[error("failed to read compiler input {path}: {message}")]
240 InputRead {
241 path: PathBuf,
243 message: String,
245 },
246 #[error("compiler input changed after discovery: {0}")]
248 InputChanged(PathBuf),
249 #[error("compiler input was modified during compilation: {0}")]
251 InputModifiedDuringCompilation(PathBuf),
252 #[error("discovered inputs were collected from a different working directory")]
254 DiscoveryWorkingDirectory,
255 #[error("compiler environment input has conflicting values: {0}")]
257 ConflictingEnvironment(String),
258 #[error("failed to serialize the rustc action: {0}")]
260 Serialization(String),
261 #[error("rustc action prediction is unsupported")]
263 UnsupportedPrediction,
264 #[error("rustc action prediction contains an invalid input path: {0}")]
266 InvalidPredictedInput(String),
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
270enum Argument {
271 Plain(String),
272 Path {
273 flag: String,
274 path: PathBuf,
275 },
276 SearchPath {
277 kind: String,
278 path: PathBuf,
279 },
280 Extern {
281 name: String,
282 path: Option<PathBuf>,
283 },
284 Emit(Vec<Emit>),
285 RemapPath {
286 from: PathBuf,
287 to: String,
288 },
289 OsoPrefix {
292 path: PathBuf,
293 trailing_slash: bool,
294 },
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
298struct Emit {
299 kind: String,
300 path: Option<PathBuf>,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct RustcInvocation {
309 arguments: Vec<Argument>,
310 source: PathBuf,
311 required_inputs: Vec<PathBuf>,
312 crate_name: String,
313 extra_filename: String,
314 out_dir: Option<PathBuf>,
315 explicit_output: Option<PathBuf>,
316 emits: Vec<Emit>,
317 target: Option<String>,
318 link_output: LinkOutput,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322enum LinkOutput {
323 Library,
324 WasmExecutable,
325 NativeExecutable,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
334#[non_exhaustive]
335pub struct ParseOptions {
336 pub cache_native_links: bool,
339}
340
341impl ParseOptions {
342 pub fn caching_native_links(enabled: bool) -> Self {
344 Self {
345 cache_native_links: enabled,
346 }
347 }
348}
349
350#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct RustcOutputs {
353 pub directory: PathBuf,
355 pub files: Vec<PathBuf>,
357 pub dep_info: PathBuf,
359}
360
361impl RustcInvocation {
362 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
371 Self::parse_with(arguments, ParseOptions::default())
372 }
373
374 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
377 let expanded = expand_response_files(arguments)?;
378 Parser::new(&expanded.arguments, options).parse()
379 }
380
381 pub fn links_natively(&self) -> bool {
384 self.link_output == LinkOutput::NativeExecutable
385 }
386
387 fn native_search_is_inert(&self) -> bool {
400 self.link_output == LinkOutput::Library
401 }
402
403 pub fn source(&self) -> &Path {
405 &self.source
406 }
407
408 pub fn target(&self) -> Option<&str> {
410 self.target.as_deref()
411 }
412
413 pub fn crate_name(&self) -> &str {
415 &self.crate_name
416 }
417
418 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
431 let linked = self
432 .arguments
433 .iter()
434 .filter_map(|argument| match argument {
435 Argument::Extern {
436 path: Some(path), ..
437 } => Some(path.as_path()),
438 _ => None,
439 })
440 .collect::<BTreeSet<_>>();
441 let owned = discovered
442 .inputs
443 .iter()
444 .filter(|input| !linked.contains(input.path.as_path()))
445 .map(|input| (input.path.as_path(), &input.digest))
446 .collect::<BTreeMap<_, _>>();
447 let mut bytes = Vec::new();
448 for (path, digest) in owned {
449 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
450 bytes.push(0);
451 bytes.extend_from_slice(digest.key().as_bytes());
452 bytes.push(0);
453 }
454 CacheDigest::blake3(&bytes)
455 }
456
457 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
462 if !working_dir.is_absolute() {
463 return Err(BypassReason::RelativeWorkingDirectory(
464 working_dir.to_path_buf(),
465 ));
466 }
467 let explicit_output = self
468 .explicit_output
469 .as_deref()
470 .map(|path| absolute_path(path, working_dir));
471 let output_directory = explicit_output
472 .as_deref()
473 .and_then(Path::parent)
474 .map(Path::to_path_buf)
475 .or_else(|| {
476 self.out_dir
477 .as_deref()
478 .map(|path| absolute_path(path, working_dir))
479 })
480 .unwrap_or_else(|| normalize_components(working_dir));
481 if let Some(output) = &explicit_output
485 && self.emits.iter().any(|emit| {
486 emit.path.is_none()
487 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
488 })
489 {
490 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
491 }
492 let mut files = BTreeSet::new();
493 let mut dep_info = None;
494 for emit in &self.emits {
495 if emit.kind == "dep-info" {
496 let path = emit.path.as_ref().map_or_else(
497 || {
498 explicit_output.clone().map_or_else(
499 || {
500 output_directory
501 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
502 },
503 |path| path.with_extension("d"),
504 )
505 },
506 |path| absolute_path(path, working_dir),
507 );
508 if path.file_name().is_none() {
509 return Err(BypassReason::InvalidOutputPath(path));
510 }
511 dep_info = Some(path);
512 continue;
513 }
514 let (prefix, extension) = match emit.kind.as_str() {
515 "link" => match self.link_output {
516 LinkOutput::Library => ("lib", "rlib"),
517 LinkOutput::WasmExecutable => ("", "wasm"),
518 LinkOutput::NativeExecutable => ("", ""),
521 },
522 "metadata" => ("lib", "rmeta"),
523 _ => continue,
524 };
525 let path = if let Some(path) = &emit.path {
526 absolute_path(path, working_dir)
527 } else {
528 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
529 output_directory.join(if extension.is_empty() {
530 name
531 } else {
532 format!("{name}.{extension}")
533 })
534 };
535 if path.file_name().is_none() {
536 return Err(BypassReason::InvalidOutputPath(path));
537 }
538 if path.parent() != Some(output_directory.as_path()) {
539 return Err(BypassReason::SplitOutputDirectories);
540 }
541 if emit.kind == "link"
546 && !matches!(self.link_output, LinkOutput::Library)
547 && matches!(
548 path.extension().and_then(|extension| extension.to_str()),
549 Some("rlib" | "rmeta")
550 )
551 {
552 return Err(BypassReason::AmbiguousOutputName(path));
553 }
554 files.insert(path);
555 }
556 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
557 if dep_info.parent() != Some(output_directory.as_path()) {
558 return Err(BypassReason::SplitOutputDirectories);
559 }
560 Ok(RustcOutputs {
561 directory: output_directory,
562 files: files.into_iter().collect(),
563 dep_info,
564 })
565 }
566
567 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
573 self.action_linked_by(context, None)
574 }
575
576 pub fn action_linked_by(
585 &self,
586 context: ActionContext,
587 linker: Option<LinkerIdentity>,
588 ) -> Result<RustcAction, BypassReason> {
589 ActionBuilder::new(self, context).linked_by(linker).build()
590 }
591
592 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
594 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
595 let bytes = canonical_json(&descriptor)
596 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
597 Ok(CacheDigest::blake3(&bytes))
598 }
599
600 pub fn prediction(
603 &self,
604 context: &ActionContext,
605 discovered: &DiscoveredInputs,
606 ) -> Result<RustcInputPrediction, BypassReason> {
607 let builder = ActionBuilder::new(self, context.clone());
608 builder.validate_mappings()?;
609 let mut native_directories = BTreeSet::new();
610 for argument in &self.arguments {
611 if let Argument::SearchPath { kind, path } = argument
612 && kind == "native"
613 {
614 match builder.normalize_path(path) {
615 Ok(normalized) => {
616 native_directories.insert(normalized);
617 }
618 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
624 }
625 Err(error) => return Err(error),
626 }
627 }
628 }
629 let mut inputs = BTreeSet::new();
636 for input in &discovered.inputs {
637 let normalized = builder.normalize_path(&input.path)?;
638 if !under_any_directory(&normalized, &native_directories) {
639 inputs.insert(normalized);
640 }
641 }
642 let has_native_directory = !native_directories.is_empty();
643 inputs.extend(
644 native_directories
645 .into_iter()
646 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
647 );
648 Ok(RustcInputPrediction {
649 version: if has_native_directory { 3 } else { 1 },
650 inputs: inputs.into_iter().collect(),
651 environment: discovered.environment.keys().cloned().collect(),
652 compiler_duration_ns: 0,
653 crate_name: String::new(),
654 })
655 }
656}
657
658impl RustcOutputs {
659 pub fn is_executable(&self, path: &Path) -> bool {
667 self.files.iter().any(|output| output == path)
668 && !matches!(
669 path.extension().and_then(|extension| extension.to_str()),
670 Some("rlib" | "rmeta")
671 )
672 }
673}
674
675#[derive(Debug, Clone, PartialEq, Eq)]
676pub struct PathMapping {
678 pub root: PathBuf,
680 pub placeholder: String,
682}
683
684impl PathMapping {
685 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
687 Self {
688 root: root.into(),
689 placeholder: placeholder.into(),
690 }
691 }
692
693 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
696 let mut ordered = mappings.to_vec();
697 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
698 ordered
699 }
700}
701
702pub fn normalize_mapped_path(
709 path: &Path,
710 working_dir: &Path,
711 mappings: &[PathMapping],
712) -> Result<String, BypassReason> {
713 let mappings = mappings
714 .iter()
715 .map(|mapping| PathMapping {
716 root: resolve_mapping_root(&mapping.root),
717 placeholder: mapping.placeholder.clone(),
718 })
719 .collect::<Vec<_>>();
720 normalize_resolved_mapped_path(path, working_dir, &mappings)
721}
722
723fn normalize_resolved_mapped_path(
724 path: &Path,
725 working_dir: &Path,
726 mappings: &[PathMapping],
727) -> Result<String, BypassReason> {
728 let absolute = if path.is_absolute() {
729 normalize_components(path)
730 } else {
731 normalize_components(&working_dir.join(path))
732 };
733 let resolved = if absolute.is_absolute() {
734 resolve_path_aliases(&absolute)
735 } else {
736 absolute.clone()
737 };
738 for mapping in mappings {
739 if let Ok(relative) = resolved.strip_prefix(&mapping.root) {
740 let suffix = slash_path(relative)?;
741 return Ok(if suffix.is_empty() {
742 format!("${{{}}}", mapping.placeholder)
743 } else {
744 format!("${{{}}}/{suffix}", mapping.placeholder)
745 });
746 }
747 }
748 Err(BypassReason::UnmappedAbsolutePath(absolute))
749}
750
751#[cfg(unix)]
756fn resolve_path_aliases(path: &Path) -> PathBuf {
757 let mut existing = path;
758 let mut missing = Vec::new();
759 loop {
760 match std::fs::canonicalize(existing) {
761 Ok(mut resolved) => {
762 for component in missing.iter().rev() {
763 resolved.push(component);
764 }
765 return normalize_components(&resolved);
766 }
767 Err(_) => {
768 let Some(name) = existing.file_name() else {
769 return path.to_path_buf();
770 };
771 missing.push(name.to_os_string());
772 let Some(parent) = existing.parent() else {
773 return path.to_path_buf();
774 };
775 existing = parent;
776 }
777 }
778 }
779}
780
781#[cfg(not(unix))]
782fn resolve_path_aliases(path: &Path) -> PathBuf {
783 path.to_path_buf()
784}
785
786fn resolve_mapping_root(root: &Path) -> PathBuf {
787 let root = normalize_components(root);
788 if root.is_absolute() {
789 resolve_path_aliases(&root)
790 } else {
791 root
792 }
793}
794
795#[derive(Debug, Clone, PartialEq, Eq)]
796pub struct CompilerIdentity {
798 pub toolchain: String,
800 pub rustc_version: String,
802 pub host: String,
804}
805
806#[derive(Debug, Clone, PartialEq, Eq)]
808pub struct ActionInput {
809 pub path: PathBuf,
811 pub digest: CacheDigest,
813}
814
815#[derive(Debug, Clone, PartialEq, Eq)]
817pub struct ActionContext {
818 pub compiler: CompilerIdentity,
820 pub working_dir: PathBuf,
822 pub path_mappings: Vec<PathMapping>,
824 pub environment: BTreeMap<String, Option<String>>,
826 pub portable_environment: BTreeSet<String>,
833 pub inputs: Vec<ActionInput>,
835}
836
837#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
847#[serde(deny_unknown_fields)]
848pub struct LinkerIdentity {
849 pub driver: String,
851 pub driver_version: String,
853 pub linker_version: String,
855 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
857 pub crt_objects: BTreeMap<String, CacheDigest>,
858 #[serde(skip_serializing_if = "Option::is_none", default)]
860 pub sdk: Option<String>,
861 #[serde(skip_serializing_if = "Option::is_none", default)]
863 pub deployment_target: Option<String>,
864}
865
866#[derive(Debug, Clone, PartialEq, Eq)]
868pub struct RustcAction {
869 pub digest: CacheDigest,
871 pub bytes: Vec<u8>,
873}
874
875#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
878#[serde(deny_unknown_fields)]
879pub struct RustcInputPrediction {
880 pub version: u8,
882 pub inputs: Vec<String>,
884 pub environment: Vec<String>,
886 #[serde(default, skip_serializing_if = "is_zero")]
889 pub compiler_duration_ns: u64,
890 #[serde(default, skip_serializing_if = "String::is_empty")]
892 pub crate_name: String,
893}
894
895fn is_zero(value: &u64) -> bool {
896 *value == 0
897}
898
899impl RustcInputPrediction {
900 pub fn discover(
903 &self,
904 working_dir: &Path,
905 path_mappings: &[PathMapping],
906 digests: &dyn FileDigestCache,
907 ) -> Result<DiscoveredInputs, BypassReason> {
908 if !matches!(self.version, 1..=3) {
909 return Err(BypassReason::UnsupportedPrediction);
910 }
911 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
912 return Err(BypassReason::UnsupportedPrediction);
913 }
914 let mut paths = BTreeSet::new();
915 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
916 let mut native_bytes = 0_u64;
917 for path in &self.inputs {
918 if self.version >= 3
919 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
920 {
921 let directory = denormalize_path(path, path_mappings)?;
922 dep_info::collect_native_directory(
923 &directory,
924 &admitted_roots,
925 &mut paths,
926 &mut native_bytes,
927 )?;
928 } else {
929 paths.insert(denormalize_path(path, path_mappings)?);
930 }
931 }
932 let environment = self
933 .environment
934 .iter()
935 .map(|name| {
936 if name.is_empty() || name.contains(['=', '\0']) {
937 return Err(BypassReason::UnsupportedPrediction);
938 }
939 let value = std::env::var_os(name)
940 .map(|value| {
941 value
942 .into_string()
943 .map_err(|_| BypassReason::UnsupportedPrediction)
944 })
945 .transpose()?;
946 Ok((name.clone(), value))
947 })
948 .collect::<Result<BTreeMap<_, _>, _>>()?;
949 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
950 }
951}
952
953#[derive(Serialize)]
954struct ActionDescriptor {
955 version: u8,
956 kind: &'static str,
957 adapter_version: u8,
958 compiler: CompilerDescriptor,
959 arguments: Vec<String>,
960 environment: BTreeMap<String, Option<String>>,
961 inputs: Vec<InputDescriptor>,
962 #[serde(skip_serializing_if = "Option::is_none")]
965 linker: Option<LinkerIdentity>,
966}
967
968#[derive(Serialize)]
969struct InvocationDescriptor {
970 version: u8,
971 kind: &'static str,
972 adapter_version: u8,
973 compiler: CompilerDescriptor,
974 arguments: Vec<String>,
975 required_inputs: Vec<String>,
976}
977
978#[derive(Serialize)]
979struct CompilerDescriptor {
980 toolchain: String,
981 rustc_version: String,
982 host: String,
983}
984
985#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
986struct InputDescriptor {
987 path: String,
988 digest: CacheDigest,
989}
990
991struct Parser<'a> {
992 arguments: &'a [OsString],
993 index: usize,
994 parsed: Vec<Argument>,
995 source: Option<PathBuf>,
996 crate_types: Vec<String>,
997 emits: Vec<Emit>,
998 required_inputs: Vec<PathBuf>,
999 test: bool,
1000 crate_name: Option<String>,
1001 extra_filename: String,
1002 out_dir: Option<PathBuf>,
1003 explicit_output: Option<PathBuf>,
1004 target: Option<String>,
1005 options: ParseOptions,
1006}
1007
1008struct ExpandedArguments {
1009 arguments: Vec<OsString>,
1010}
1011
1012#[derive(Default)]
1013struct ResponseExpander {
1014 shell_argfiles: bool,
1015 next_is_unstable_option: bool,
1016 arguments: Vec<OsString>,
1017}
1018
1019impl ResponseExpander {
1020 fn push(&mut self, argument: String) {
1021 if self.next_is_unstable_option {
1022 self.shell_argfiles |= argument == "shell-argfiles";
1023 self.next_is_unstable_option = false;
1024 } else if let Some(option) = argument.strip_prefix("-Z") {
1025 if option.is_empty() {
1026 self.next_is_unstable_option = true;
1027 } else {
1028 self.shell_argfiles |= option == "shell-argfiles";
1029 }
1030 }
1031 self.arguments.push(argument.into());
1032 }
1033}
1034
1035fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1038 let mut expanded = ResponseExpander::default();
1039 for (index, argument) in arguments.iter().enumerate() {
1040 let argument = argument
1041 .to_str()
1042 .ok_or(BypassReason::NonUtf8Argument { index })?;
1043 let Some(argfile) = argument.strip_prefix('@') else {
1044 expanded.push(argument.to_string());
1045 continue;
1046 };
1047 let (path, shell) = match argfile.split_once(':') {
1048 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1049 _ => (argfile, false),
1050 };
1051 let contents = std::fs::read_to_string(path).map_err(|error| {
1052 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1053 })?;
1054 if shell {
1055 let arguments = shlex::split(&contents).ok_or_else(|| {
1056 BypassReason::ResponseFile(format!(
1057 "invalid shell-style arguments in {}",
1058 Path::new(path).display()
1059 ))
1060 })?;
1061 for argument in arguments {
1062 expanded.push(argument);
1063 }
1064 } else {
1065 for argument in contents.lines() {
1066 expanded.push(argument.to_string());
1067 }
1068 }
1069 }
1070 Ok(ExpandedArguments {
1071 arguments: expanded.arguments,
1072 })
1073}
1074
1075impl<'a> Parser<'a> {
1076 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1077 Self {
1078 arguments,
1079 options,
1080 index: 0,
1081 parsed: Vec::new(),
1082 source: None,
1083 crate_types: Vec::new(),
1084 emits: Vec::new(),
1085 required_inputs: Vec::new(),
1086 test: false,
1087 crate_name: None,
1088 extra_filename: String::new(),
1089 out_dir: None,
1090 explicit_output: None,
1091 target: None,
1092 }
1093 }
1094
1095 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1096 while self.index < self.arguments.len() {
1097 let value = self.current()?.to_string();
1098 self.index += 1;
1099 if let Some(long) = value.strip_prefix("--") {
1100 self.parse_long(long)?;
1101 } else if value.starts_with('-') && value != "-" {
1102 self.parse_short(&value)?;
1103 } else {
1104 self.parse_input(&value)?;
1105 }
1106 }
1107
1108 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1109 let link_output = self.classify()?;
1110 let crate_name = self.crate_name.clone().map_or_else(
1111 || {
1112 source
1113 .file_stem()
1114 .and_then(|name| name.to_str())
1115 .map(|name| name.replace('-', "_"))
1116 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1117 },
1118 Ok,
1119 )?;
1120 self.required_inputs.push(source.clone());
1121 Ok(RustcInvocation {
1122 arguments: self.parsed,
1123 source,
1124 required_inputs: self.required_inputs,
1125 crate_name,
1126 extra_filename: self.extra_filename,
1127 out_dir: self.out_dir,
1128 explicit_output: self.explicit_output,
1129 emits: self.emits,
1130 target: self.target,
1131 link_output,
1132 })
1133 }
1134
1135 fn current(&self) -> Result<&str, BypassReason> {
1136 self.arguments[self.index]
1137 .to_str()
1138 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1139 }
1140
1141 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1142 if let Some(value) = inline {
1143 if value.is_empty() {
1144 return Err(BypassReason::MissingValue(flag.into()));
1145 }
1146 return Ok(value.into());
1147 }
1148 if self.index >= self.arguments.len() {
1149 return Err(BypassReason::MissingValue(flag.into()));
1150 }
1151 let value = self.current()?.to_string();
1152 self.index += 1;
1153 Ok(value)
1154 }
1155
1156 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1157 let (flag, inline) = value
1158 .split_once('=')
1159 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1160 let rendered_flag = format!("--{flag}");
1161 match flag {
1162 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1163 "test" => {
1164 self.test = true;
1165 self.parsed.push(Argument::Plain(rendered_flag));
1166 Ok(())
1167 }
1168 "verbose" => {
1169 self.parsed.push(Argument::Plain(rendered_flag));
1170 Ok(())
1171 }
1172 "crate-name" => {
1173 let value = self.take_value(&rendered_flag, inline)?;
1174 self.crate_name = Some(value.clone());
1175 self.parsed
1176 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1177 Ok(())
1178 }
1179 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1180 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1181 | "deny" | "forbid" | "cap-lints" => {
1182 let value = self.take_value(&rendered_flag, inline)?;
1183 self.parsed
1184 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1185 Ok(())
1186 }
1187 "target" => {
1188 let value = self.take_value(&rendered_flag, inline)?;
1189 self.target = Some(value.clone());
1190 if value.ends_with(".json") || value.contains(['/', '\\']) {
1191 let path = PathBuf::from(value);
1192 self.required_inputs.push(path.clone());
1193 self.parsed.push(Argument::Path {
1194 flag: rendered_flag,
1195 path,
1196 });
1197 } else {
1198 self.target = Some(value.clone());
1199 self.parsed
1200 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1201 }
1202 Ok(())
1203 }
1204 "crate-type" => {
1205 let value = self.take_value(&rendered_flag, inline)?;
1206 self.crate_types
1207 .extend(value.split(',').map(ToOwned::to_owned));
1208 self.parsed
1209 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1210 Ok(())
1211 }
1212 "emit" => {
1213 let value = self.take_value(&rendered_flag, inline)?;
1214 let emits = parse_emits(&value);
1215 self.emits.extend(emits.clone());
1216 self.parsed.push(Argument::Emit(emits));
1217 Ok(())
1218 }
1219 "out-dir" => {
1220 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1221 self.out_dir = Some(path.clone());
1222 self.parsed.push(Argument::Path {
1223 flag: rendered_flag,
1224 path,
1225 });
1226 Ok(())
1227 }
1228 "sysroot" => {
1229 let path = self.take_value(&rendered_flag, inline)?;
1230 self.parsed.push(Argument::Path {
1231 flag: rendered_flag,
1232 path: path.into(),
1233 });
1234 Ok(())
1235 }
1236 "extern" => {
1237 let value = self.take_value(&rendered_flag, inline)?;
1238 let (name, path) = value
1239 .split_once('=')
1240 .map_or((value.as_str(), None), |(name, path)| {
1241 (name, Some(PathBuf::from(path)))
1242 });
1243 if let Some(path) = &path {
1244 self.required_inputs.push(path.clone());
1245 }
1246 self.parsed.push(Argument::Extern {
1247 name: name.into(),
1248 path,
1249 });
1250 Ok(())
1251 }
1252 "remap-path-prefix" => {
1253 let value = self.take_value(&rendered_flag, inline)?;
1254 let Some((from, to)) = value.split_once('=') else {
1255 return Err(BypassReason::MissingValue(rendered_flag));
1256 };
1257 self.parsed.push(Argument::RemapPath {
1258 from: from.into(),
1259 to: to.into(),
1260 });
1261 Ok(())
1262 }
1263 "codegen" => {
1264 let value = self.take_value(&rendered_flag, inline)?;
1265 self.parse_codegen(&value)
1266 }
1267 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1268 }
1269 }
1270
1271 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1272 if let Some(attached) = value.strip_prefix("-Z") {
1273 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1274 if option == "shell-argfiles" {
1275 self.parsed.push(Argument::Plain("-Zshell-argfiles".into()));
1276 return Ok(());
1277 }
1278 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1279 }
1280 match value {
1281 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1284 "-g" | "-O" | "-v" => {
1285 self.parsed.push(Argument::Plain(value.into()));
1286 return Ok(());
1287 }
1288 _ => {}
1289 }
1290 for (short, long) in [
1291 ("-A", "--allow"),
1292 ("-W", "--warn"),
1293 ("-D", "--deny"),
1294 ("-F", "--forbid"),
1295 ] {
1296 if let Some(attached) = value.strip_prefix(short) {
1297 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1298 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1299 return Ok(());
1300 }
1301 }
1302 if let Some(attached) = value.strip_prefix("-C") {
1303 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1304 return self.parse_codegen(&option);
1305 }
1306 if let Some(attached) = value.strip_prefix("-L") {
1307 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1308 let (kind, path) = search
1309 .split_once('=')
1310 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1311 if !matches!(kind, "dependency" | "native") {
1312 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1313 }
1314 self.parsed.push(Argument::SearchPath {
1315 kind: kind.into(),
1316 path: path.into(),
1317 });
1318 return Ok(());
1319 }
1320 if value == "-l" || value.starts_with("-l") {
1321 return Err(BypassReason::NativeLibrary);
1322 }
1323 if let Some(attached) = value.strip_prefix("-o") {
1324 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1325 self.explicit_output = Some(path.clone().into());
1326 self.parsed.push(Argument::Path {
1327 flag: "-o".into(),
1328 path: path.into(),
1329 });
1330 return Ok(());
1331 }
1332 Err(BypassReason::UnknownFlag(value.into()))
1333 }
1334
1335 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1336 let name = value.split_once('=').map_or(value, |(name, _)| name);
1337 if name == "incremental" {
1338 return Err(BypassReason::Incremental);
1339 }
1340 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() {
1341 return Err(BypassReason::UnknownCodegenOption(name.into()));
1342 }
1343 if matches!(name, "link-arg" | "link-args")
1348 && let Some((_, option)) = value.split_once('=')
1349 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1350 && !prefix.trim_end_matches('/').is_empty()
1351 && !prefix.contains(',')
1352 {
1353 let trailing_slash = prefix.ends_with('/');
1354 self.parsed.push(Argument::OsoPrefix {
1355 path: PathBuf::from(prefix.trim_end_matches('/')),
1356 trailing_slash,
1357 });
1358 return Ok(());
1359 }
1360 self.parsed
1361 .push(Argument::Plain(format!("--codegen={value}")));
1362 if name == "extra-filename" {
1363 self.extra_filename = value
1364 .split_once('=')
1365 .map_or(String::new(), |(_, value)| value.to_string());
1366 }
1367 Ok(())
1368 }
1369
1370 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1371 if value == "-" {
1372 return Err(BypassReason::StandardInput);
1373 }
1374 if self.source.replace(value.into()).is_some() {
1375 return Err(BypassReason::MultipleInputs);
1376 }
1377 Ok(())
1378 }
1379
1380 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1381 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1390 let builds_a_library = !self.test
1391 && !self.crate_types.is_empty()
1392 && self
1393 .crate_types
1394 .iter()
1395 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1396 let link_output = if never_links || builds_a_library {
1397 LinkOutput::Library
1398 } else if self
1399 .target
1400 .as_deref()
1401 .is_some_and(compiler_bundled_wasm_target)
1402 && ((self.test && self.crate_types.is_empty())
1403 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1404 {
1405 if self.parsed.iter().any(|argument| match argument {
1406 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1407 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1408 !matches!(
1409 value.rsplit_once('=').map(|(_, value)| value),
1410 Some("y" | "yes" | "on" | "true")
1411 )
1412 }
1413 _ => false,
1414 }) {
1415 return Err(BypassReason::UnknownCodegenOption(
1416 "link-self-contained".into(),
1417 ));
1418 }
1419 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1420 && self.parsed.iter().any(|argument| {
1421 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1422 })
1423 {
1424 return Err(BypassReason::UnknownCodegenOption(
1425 "target-feature=-crt-static".into(),
1426 ));
1427 }
1428 LinkOutput::WasmExecutable
1432 } else if self.options.cache_native_links && self.links_a_native_program() {
1433 self.check_native_link_is_portable()?;
1434 LinkOutput::NativeExecutable
1435 } else if self.test {
1436 return Err(BypassReason::UnsupportedCrateType("test".into()));
1437 } else {
1438 return Err(BypassReason::UnsupportedCrateType(
1439 self.crate_types
1440 .iter()
1441 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1442 .cloned()
1443 .unwrap_or_else(|| "bin".into()),
1444 ));
1445 };
1446 if link_output != LinkOutput::Library
1455 && let Some(option) = self.first_link_argument()
1456 {
1457 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1458 }
1459 if !matches!(
1463 link_output,
1464 LinkOutput::Library | LinkOutput::NativeExecutable
1465 ) && self
1466 .parsed
1467 .iter()
1468 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1469 {
1470 return Err(BypassReason::UnmodeledLinkArgument(
1471 "link-arg=-Wl,-oso_prefix".into(),
1472 ));
1473 }
1474 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1475 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1476 _ => None,
1477 }) {
1478 return Err(BypassReason::UnresolvedExtern(name.clone()));
1479 }
1480 if let Some(emit) = self
1481 .emits
1482 .iter()
1483 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1484 {
1485 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1486 }
1487 if !self
1488 .emits
1489 .iter()
1490 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1491 {
1492 return Err(BypassReason::NoCacheableOutput);
1493 }
1494 Ok(link_output)
1495 }
1496}
1497
1498impl Parser<'_> {
1499 fn first_link_argument(&self) -> Option<&str> {
1502 self.parsed.iter().find_map(|argument| {
1503 let Argument::Plain(value) = argument else {
1504 return None;
1505 };
1506 let option = value.strip_prefix("--codegen=")?;
1507 let name = option.split_once('=').map_or(option, |(name, _)| name);
1508 matches!(name, "link-arg" | "link-args").then_some(option)
1509 })
1510 }
1511
1512 fn oso_prefix_covers_outputs(&self) -> bool {
1521 let Some(directory) = self
1522 .out_dir
1523 .as_deref()
1524 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
1525 else {
1526 return false;
1527 };
1528 if !directory.is_absolute() {
1529 return false;
1530 }
1531 let directory = normalize_components(directory);
1532 self.parsed.iter().any(|argument| {
1533 let Argument::OsoPrefix { path, .. } = argument else {
1534 return false;
1535 };
1536 path.is_absolute() && directory.starts_with(normalize_components(path))
1537 })
1538 }
1539
1540 fn links_a_native_program(&self) -> bool {
1547 self.target.is_none()
1548 && self.emits.iter().any(|emit| emit.kind == "link")
1553 && ((self.test && self.crate_types.is_empty())
1554 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin"))
1555 }
1556
1557 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
1564 for argument in &self.parsed {
1565 let Argument::Plain(value) = argument else {
1566 continue;
1567 };
1568 let (name, value) = if value == "-g" {
1571 ("debuginfo", Some("2"))
1572 } else if let Some(option) = value.strip_prefix("--codegen=") {
1573 match option.split_once('=') {
1574 Some((name, value)) => (name, Some(value)),
1575 None => (option, None),
1579 }
1580 } else {
1581 continue;
1582 };
1583 let unportable = match name {
1584 "split-debuginfo" => match value {
1590 Some("off") => false,
1591 Some("unpacked") if cfg!(target_os = "macos") => {
1592 !self.oso_prefix_covers_outputs()
1593 }
1594 _ => true,
1595 },
1596 "debuginfo" if cfg!(target_os = "macos") => {
1605 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
1606 }
1607 "rpath" | "prefer-dynamic" => is_enabled(value),
1609 "link-self-contained" => true,
1612 _ => false,
1613 };
1614 if unportable {
1615 return Err(BypassReason::UnportableNativeLink(match value {
1616 Some(value) => format!("{name}={value}"),
1617 None => name.to_owned(),
1618 }));
1619 }
1620 }
1621 Ok(())
1622 }
1623}
1624
1625fn is_enabled(value: Option<&str>) -> bool {
1628 matches!(value, None | Some("y" | "yes" | "on" | "true"))
1629}
1630
1631fn compiler_bundled_wasm_target(target: &str) -> bool {
1632 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
1633}
1634
1635fn parse_emits(value: &str) -> Vec<Emit> {
1636 value
1637 .split(',')
1638 .map(|emit| {
1639 let (kind, path) = emit
1640 .split_once('=')
1641 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
1642 Emit {
1643 kind: kind.into(),
1644 path,
1645 }
1646 })
1647 .collect()
1648}
1649
1650struct ActionBuilder<'a> {
1651 invocation: &'a RustcInvocation,
1652 context: ActionContext,
1653 mappings: Vec<PathMapping>,
1654 linker: Option<LinkerIdentity>,
1655}
1656
1657impl<'a> ActionBuilder<'a> {
1658 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
1659 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1660 let mappings = context
1661 .path_mappings
1662 .iter()
1663 .map(|mapping| PathMapping {
1664 root: resolve_mapping_root(&mapping.root),
1665 placeholder: mapping.placeholder.clone(),
1666 })
1667 .collect();
1668 Self {
1669 linker: None,
1670 invocation,
1671 mappings,
1672 context,
1673 }
1674 }
1675
1676 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
1677 self.linker = linker;
1678 self
1679 }
1680
1681 fn build(self) -> Result<RustcAction, BypassReason> {
1682 self.validate_mappings()?;
1683 let invocation = self.invocation_descriptor()?;
1684 let environment = self.environment_descriptor()?;
1685
1686 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1687 for input in &self.context.inputs {
1688 input
1689 .digest
1690 .validate()
1691 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
1692 let path = self.normalize_path(&input.path)?;
1693 if inputs
1694 .insert(path.clone(), input.digest.clone())
1695 .is_some_and(|existing| existing != input.digest)
1696 {
1697 return Err(BypassReason::ConflictingInput(path));
1698 }
1699 }
1700 let required = self
1701 .invocation
1702 .required_inputs
1703 .iter()
1704 .map(|path| self.normalize_path(path))
1705 .collect::<Result<BTreeSet<_>, _>>()?;
1706 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1707 return Err(BypassReason::MissingRequiredInput(missing.clone()));
1708 }
1709 let inputs = inputs
1710 .into_iter()
1711 .map(|(path, digest)| InputDescriptor { path, digest })
1712 .collect();
1713 if self.invocation.links_natively() && self.linker.is_none() {
1716 return Err(BypassReason::UnportableNativeLink(
1717 "linker identity is unknown".into(),
1718 ));
1719 }
1720 let descriptor = ActionDescriptor {
1721 version: ACTION_SCHEMA_VERSION,
1722 kind: "rustc",
1723 adapter_version: ADAPTER_VERSION,
1724 compiler: invocation.compiler,
1725 arguments: invocation.arguments,
1726 environment,
1727 inputs,
1728 linker: self.linker.clone(),
1729 };
1730 let bytes = canonical_json(&descriptor)
1731 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
1732 let digest = CacheDigest::blake3(&bytes);
1733 Ok(RustcAction { digest, bytes })
1734 }
1735
1736 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
1737 self.validate_mappings()?;
1738 let arguments = self
1739 .invocation
1740 .arguments
1741 .iter()
1742 .map(|argument| self.normalize_argument(argument))
1743 .collect::<Result<Vec<_>, _>>()?;
1744 let required_inputs = self
1745 .invocation
1746 .required_inputs
1747 .iter()
1748 .map(|path| self.normalize_path(path))
1749 .collect::<Result<BTreeSet<_>, _>>()?
1750 .into_iter()
1751 .collect();
1752 Ok(InvocationDescriptor {
1753 version: ACTION_SCHEMA_VERSION,
1754 kind: "rustc",
1755 adapter_version: ADAPTER_VERSION,
1756 compiler: CompilerDescriptor {
1757 toolchain: self.context.compiler.toolchain.clone(),
1758 rustc_version: self.context.compiler.rustc_version.clone(),
1759 host: self.context.compiler.host.clone(),
1760 },
1761 arguments,
1762 required_inputs,
1763 })
1764 }
1765
1766 fn validate_mappings(&self) -> Result<(), BypassReason> {
1767 if !self.context.working_dir.is_absolute() {
1768 return Err(BypassReason::RelativeWorkingDirectory(
1769 self.context.working_dir.clone(),
1770 ));
1771 }
1772 let mut roots = BTreeSet::new();
1773 let mut placeholders = BTreeSet::new();
1774 for mapping in &self.mappings {
1775 if !mapping.root.is_absolute() {
1776 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
1777 }
1778 if mapping.placeholder.is_empty()
1779 || !mapping
1780 .placeholder
1781 .bytes()
1782 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1783 || !roots.insert(normalize_components(&mapping.root))
1784 || !placeholders.insert(&mapping.placeholder)
1785 {
1786 return Err(BypassReason::InvalidPathPlaceholder(
1787 mapping.placeholder.clone(),
1788 ));
1789 }
1790 }
1791 Ok(())
1792 }
1793
1794 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
1795 match argument {
1796 Argument::Plain(value) => Ok(value.clone()),
1797 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1798 Argument::SearchPath { kind, path } => {
1799 let text = match self.normalize_path(path) {
1800 Ok(text) => text,
1801 Err(BypassReason::UnmappedAbsolutePath(absolute))
1809 if kind == "native" && self.invocation.native_search_is_inert() =>
1810 {
1811 absolute
1812 .to_str()
1813 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
1814 .to_string()
1815 }
1816 Err(error) => return Err(error),
1817 };
1818 Ok(format!("-L{kind}={text}"))
1819 }
1820 Argument::Extern { name, path } => match path {
1821 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
1822 None => Ok(format!("--extern={name}")),
1823 },
1824 Argument::Emit(emits) => Ok(format!(
1825 "--emit={}",
1826 emits
1827 .iter()
1828 .map(|emit| match &emit.path {
1829 Some(path) => self
1830 .normalize_path(path)
1831 .map(|path| format!("{}={path}", emit.kind)),
1832 None => Ok(emit.kind.clone()),
1833 })
1834 .collect::<Result<Vec<_>, _>>()?
1835 .join(",")
1836 )),
1837 Argument::RemapPath { from, to } => Ok(format!(
1838 "--remap-path-prefix={}={}",
1839 self.normalize_path(from)?,
1840 to
1841 )),
1842 Argument::OsoPrefix {
1843 path,
1844 trailing_slash,
1845 } => Ok(format!(
1846 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
1847 self.normalize_path(path)?,
1848 if *trailing_slash { "/" } else { "" }
1849 )),
1850 }
1851 }
1852
1853 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
1860 self.context
1861 .environment
1862 .iter()
1863 .map(|(name, value)| {
1864 let value = match value {
1865 Some(value) if self.context.portable_environment.contains(name) => {
1866 Some(self.normalize_path(Path::new(value))?)
1867 }
1868 value => value.clone(),
1869 };
1870 Ok((name.clone(), value))
1871 })
1872 .collect()
1873 }
1874
1875 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
1876 normalize_resolved_mapped_path(path, &self.context.working_dir, &self.mappings)
1877 }
1878}
1879
1880fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
1884 directories.iter().any(|directory| {
1885 path.len() > directory.len()
1886 && path.as_bytes()[directory.len()] == b'/'
1887 && path.starts_with(directory)
1888 })
1889}
1890
1891fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
1892 for mapping in mappings {
1893 let prefix = format!("${{{}}}", mapping.placeholder);
1894 let suffix = if value == prefix {
1895 ""
1896 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1897 suffix
1898 } else {
1899 continue;
1900 };
1901 if !mapping.root.is_absolute()
1902 || (!suffix.is_empty()
1903 && suffix.split('/').any(|component| {
1904 component.is_empty()
1905 || matches!(component, "." | "..")
1906 || component.contains('\\')
1907 }))
1908 {
1909 return Err(BypassReason::InvalidPredictedInput(value.into()));
1910 }
1911 let mut path = normalize_components(&mapping.root);
1912 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1913 return Ok(path);
1914 }
1915 Err(BypassReason::InvalidPredictedInput(value.into()))
1916}
1917
1918fn normalize_components(path: &Path) -> PathBuf {
1919 let mut normalized = PathBuf::new();
1920 for component in path.components() {
1921 match component {
1922 Component::CurDir => {}
1923 Component::ParentDir => {
1924 normalized.pop();
1925 }
1926 component => normalized.push(component.as_os_str()),
1927 }
1928 }
1929 normalized
1930}
1931
1932fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1933 if path.is_absolute() {
1934 normalize_components(path)
1935 } else {
1936 normalize_components(&working_dir.join(path))
1937 }
1938}
1939
1940fn slash_path(path: &Path) -> Result<String, BypassReason> {
1941 path.components()
1942 .filter_map(|component| match component {
1943 Component::Normal(value) => Some(
1944 value
1945 .to_str()
1946 .map(ToOwned::to_owned)
1947 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf())),
1948 ),
1949 _ => None,
1950 })
1951 .collect::<Result<Vec<_>, _>>()
1952 .map(|components| components.join("/"))
1953}
1954
1955#[cfg(test)]
1956#[path = "rustc_cache_tests.rs"]
1957mod tests;