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 NativeProcMacro,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
335#[non_exhaustive]
336pub struct ParseOptions {
337 pub cache_native_links: bool,
340}
341
342impl ParseOptions {
343 pub fn caching_native_links(enabled: bool) -> Self {
345 Self {
346 cache_native_links: enabled,
347 }
348 }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
353pub struct RustcOutputs {
354 pub directory: PathBuf,
356 pub files: Vec<PathBuf>,
358 pub dep_info: PathBuf,
360}
361
362impl RustcInvocation {
363 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
372 Self::parse_with(arguments, ParseOptions::default())
373 }
374
375 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
378 let expanded = expand_response_files(arguments)?;
379 Parser::new(&expanded.arguments, options).parse()
380 }
381
382 pub fn links_natively(&self) -> bool {
385 matches!(
386 self.link_output,
387 LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
388 )
389 }
390
391 fn native_search_is_inert(&self) -> bool {
404 self.link_output == LinkOutput::Library
405 }
406
407 pub fn source(&self) -> &Path {
409 &self.source
410 }
411
412 pub fn target(&self) -> Option<&str> {
414 self.target.as_deref()
415 }
416
417 pub fn crate_name(&self) -> &str {
419 &self.crate_name
420 }
421
422 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
435 let linked = self
436 .arguments
437 .iter()
438 .filter_map(|argument| match argument {
439 Argument::Extern {
440 path: Some(path), ..
441 } => Some(path.as_path()),
442 _ => None,
443 })
444 .collect::<BTreeSet<_>>();
445 let owned = discovered
446 .inputs
447 .iter()
448 .filter(|input| !linked.contains(input.path.as_path()))
449 .map(|input| (input.path.as_path(), &input.digest))
450 .collect::<BTreeMap<_, _>>();
451 let mut bytes = Vec::new();
452 for (path, digest) in owned {
453 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
454 bytes.push(0);
455 bytes.extend_from_slice(digest.key().as_bytes());
456 bytes.push(0);
457 }
458 CacheDigest::blake3(&bytes)
459 }
460
461 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
466 if !working_dir.is_absolute() {
467 return Err(BypassReason::RelativeWorkingDirectory(
468 working_dir.to_path_buf(),
469 ));
470 }
471 let explicit_output = self
472 .explicit_output
473 .as_deref()
474 .map(|path| absolute_path(path, working_dir));
475 let output_directory = explicit_output
476 .as_deref()
477 .and_then(Path::parent)
478 .map(Path::to_path_buf)
479 .or_else(|| {
480 self.out_dir
481 .as_deref()
482 .map(|path| absolute_path(path, working_dir))
483 })
484 .unwrap_or_else(|| normalize_components(working_dir));
485 if let Some(output) = &explicit_output
489 && self.emits.iter().any(|emit| {
490 emit.path.is_none()
491 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
492 })
493 {
494 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
495 }
496 let mut files = BTreeSet::new();
497 let mut dep_info = None;
498 for emit in &self.emits {
499 if emit.kind == "dep-info" {
500 let path = emit.path.as_ref().map_or_else(
501 || {
502 explicit_output.clone().map_or_else(
503 || {
504 output_directory
505 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
506 },
507 |path| path.with_extension("d"),
508 )
509 },
510 |path| absolute_path(path, working_dir),
511 );
512 if path.file_name().is_none() {
513 return Err(BypassReason::InvalidOutputPath(path));
514 }
515 dep_info = Some(path);
516 continue;
517 }
518 let (prefix, extension) = match emit.kind.as_str() {
519 "link" => match self.link_output {
520 LinkOutput::Library => ("lib", "rlib"),
521 LinkOutput::WasmExecutable => ("", "wasm"),
522 LinkOutput::NativeExecutable => ("", ""),
523 LinkOutput::NativeProcMacro => (
524 std::env::consts::DLL_PREFIX,
525 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
526 ),
527 },
528 "metadata" => ("lib", "rmeta"),
529 _ => continue,
530 };
531 let path = if let Some(path) = &emit.path {
532 absolute_path(path, working_dir)
533 } else {
534 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
535 output_directory.join(if extension.is_empty() {
536 name
537 } else {
538 format!("{name}.{extension}")
539 })
540 };
541 if path.file_name().is_none() {
542 return Err(BypassReason::InvalidOutputPath(path));
543 }
544 if path.parent() != Some(output_directory.as_path()) {
545 return Err(BypassReason::SplitOutputDirectories);
546 }
547 if emit.kind == "link"
552 && !matches!(self.link_output, LinkOutput::Library)
553 && matches!(
554 path.extension().and_then(|extension| extension.to_str()),
555 Some("rlib" | "rmeta")
556 )
557 {
558 return Err(BypassReason::AmbiguousOutputName(path));
559 }
560 files.insert(path);
561 }
562 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
563 if dep_info.parent() != Some(output_directory.as_path()) {
564 return Err(BypassReason::SplitOutputDirectories);
565 }
566 Ok(RustcOutputs {
567 directory: output_directory,
568 files: files.into_iter().collect(),
569 dep_info,
570 })
571 }
572
573 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
579 self.action_linked_by(context, None)
580 }
581
582 pub fn action_linked_by(
591 &self,
592 context: ActionContext,
593 linker: Option<LinkerIdentity>,
594 ) -> Result<RustcAction, BypassReason> {
595 ActionBuilder::new(self, context).linked_by(linker).build()
596 }
597
598 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
600 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
601 let bytes = canonical_json(&descriptor)
602 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
603 Ok(CacheDigest::blake3(&bytes))
604 }
605
606 pub fn prediction(
609 &self,
610 context: &ActionContext,
611 discovered: &DiscoveredInputs,
612 ) -> Result<RustcInputPrediction, BypassReason> {
613 let builder = ActionBuilder::new(self, context.clone());
614 builder.validate_mappings()?;
615 let mut native_directories = BTreeSet::new();
616 for argument in &self.arguments {
617 if let Argument::SearchPath { kind, path } = argument
618 && kind == "native"
619 {
620 match builder.normalize_path(path) {
621 Ok(normalized) => {
622 native_directories.insert(normalized);
623 }
624 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
630 }
631 Err(error) => return Err(error),
632 }
633 }
634 }
635 let mut inputs = BTreeSet::new();
642 for input in &discovered.inputs {
643 let normalized = builder.normalize_path(&input.path)?;
644 if !under_any_directory(&normalized, &native_directories) {
645 inputs.insert(normalized);
646 }
647 }
648 let has_native_directory = !native_directories.is_empty();
649 inputs.extend(
650 native_directories
651 .into_iter()
652 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
653 );
654 Ok(RustcInputPrediction {
655 version: if has_native_directory { 3 } else { 1 },
656 inputs: inputs.into_iter().collect(),
657 environment: discovered.environment.keys().cloned().collect(),
658 compiler_duration_ns: 0,
659 crate_name: String::new(),
660 })
661 }
662}
663
664impl RustcOutputs {
665 pub fn is_executable(&self, path: &Path) -> bool {
673 self.files.iter().any(|output| output == path)
674 && !matches!(
675 path.extension().and_then(|extension| extension.to_str()),
676 Some("rlib" | "rmeta")
677 )
678 }
679}
680
681#[derive(Debug, Clone, PartialEq, Eq)]
682pub struct PathMapping {
684 pub root: PathBuf,
686 pub placeholder: String,
688}
689
690impl PathMapping {
691 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
693 Self {
694 root: root.into(),
695 placeholder: placeholder.into(),
696 }
697 }
698
699 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
702 let mut ordered = mappings.to_vec();
703 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
704 ordered
705 }
706}
707
708pub fn normalize_mapped_path(
715 path: &Path,
716 working_dir: &Path,
717 mappings: &[PathMapping],
718) -> Result<String, BypassReason> {
719 let mappings = mappings
720 .iter()
721 .map(|mapping| PathMapping {
722 root: resolve_mapping_root(&mapping.root),
723 placeholder: mapping.placeholder.clone(),
724 })
725 .collect::<Vec<_>>();
726 normalize_resolved_mapped_path(path, working_dir, &mappings)
727}
728
729fn normalize_resolved_mapped_path(
730 path: &Path,
731 working_dir: &Path,
732 mappings: &[PathMapping],
733) -> Result<String, BypassReason> {
734 let absolute = if path.is_absolute() {
735 normalize_components(path)
736 } else {
737 normalize_components(&working_dir.join(path))
738 };
739 let resolved = if absolute.is_absolute() {
740 resolve_path_aliases(&absolute)
741 } else {
742 absolute.clone()
743 };
744 for mapping in mappings {
745 if let Ok(relative) = resolved.strip_prefix(&mapping.root) {
746 let suffix = slash_path(relative)?;
747 return Ok(if suffix.is_empty() {
748 format!("${{{}}}", mapping.placeholder)
749 } else {
750 format!("${{{}}}/{suffix}", mapping.placeholder)
751 });
752 }
753 }
754 Err(BypassReason::UnmappedAbsolutePath(absolute))
755}
756
757#[cfg(unix)]
762fn resolve_path_aliases(path: &Path) -> PathBuf {
763 let mut existing = path;
764 let mut missing = Vec::new();
765 loop {
766 match std::fs::canonicalize(existing) {
767 Ok(mut resolved) => {
768 for component in missing.iter().rev() {
769 resolved.push(component);
770 }
771 return normalize_components(&resolved);
772 }
773 Err(_) => {
774 let Some(name) = existing.file_name() else {
775 return path.to_path_buf();
776 };
777 missing.push(name.to_os_string());
778 let Some(parent) = existing.parent() else {
779 return path.to_path_buf();
780 };
781 existing = parent;
782 }
783 }
784 }
785}
786
787#[cfg(not(unix))]
788fn resolve_path_aliases(path: &Path) -> PathBuf {
789 path.to_path_buf()
790}
791
792fn resolve_mapping_root(root: &Path) -> PathBuf {
793 let root = normalize_components(root);
794 if root.is_absolute() {
795 resolve_path_aliases(&root)
796 } else {
797 root
798 }
799}
800
801#[derive(Debug, Clone, PartialEq, Eq)]
802pub struct CompilerIdentity {
804 pub toolchain: String,
806 pub rustc_version: String,
808 pub host: String,
810}
811
812#[derive(Debug, Clone, PartialEq, Eq)]
814pub struct ActionInput {
815 pub path: PathBuf,
817 pub digest: CacheDigest,
819}
820
821#[derive(Debug, Clone, PartialEq, Eq)]
823pub struct ActionContext {
824 pub compiler: CompilerIdentity,
826 pub working_dir: PathBuf,
828 pub path_mappings: Vec<PathMapping>,
830 pub environment: BTreeMap<String, Option<String>>,
832 pub portable_environment: BTreeSet<String>,
839 pub inputs: Vec<ActionInput>,
841}
842
843#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
853#[serde(deny_unknown_fields)]
854pub struct LinkerIdentity {
855 pub driver: String,
857 pub driver_version: String,
859 pub linker_version: String,
861 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
863 pub crt_objects: BTreeMap<String, CacheDigest>,
864 #[serde(skip_serializing_if = "Option::is_none", default)]
866 pub sdk: Option<String>,
867 #[serde(skip_serializing_if = "Option::is_none", default)]
869 pub deployment_target: Option<String>,
870}
871
872#[derive(Debug, Clone, PartialEq, Eq)]
874pub struct RustcAction {
875 pub digest: CacheDigest,
877 pub bytes: Vec<u8>,
879}
880
881#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
884#[serde(deny_unknown_fields)]
885pub struct RustcInputPrediction {
886 pub version: u8,
888 pub inputs: Vec<String>,
890 pub environment: Vec<String>,
892 #[serde(default, skip_serializing_if = "is_zero")]
895 pub compiler_duration_ns: u64,
896 #[serde(default, skip_serializing_if = "String::is_empty")]
898 pub crate_name: String,
899}
900
901fn is_zero(value: &u64) -> bool {
902 *value == 0
903}
904
905impl RustcInputPrediction {
906 pub fn discover(
909 &self,
910 working_dir: &Path,
911 path_mappings: &[PathMapping],
912 digests: &dyn FileDigestCache,
913 ) -> Result<DiscoveredInputs, BypassReason> {
914 if !matches!(self.version, 1..=3) {
915 return Err(BypassReason::UnsupportedPrediction);
916 }
917 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
918 return Err(BypassReason::UnsupportedPrediction);
919 }
920 let mut paths = BTreeSet::new();
921 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
922 let mut native_bytes = 0_u64;
923 for path in &self.inputs {
924 if self.version >= 3
925 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
926 {
927 let directory = denormalize_path(path, path_mappings)?;
928 dep_info::collect_native_directory(
929 &directory,
930 &admitted_roots,
931 &mut paths,
932 &mut native_bytes,
933 )?;
934 } else {
935 paths.insert(denormalize_path(path, path_mappings)?);
936 }
937 }
938 let environment = self
939 .environment
940 .iter()
941 .map(|name| {
942 if name.is_empty() || name.contains(['=', '\0']) {
943 return Err(BypassReason::UnsupportedPrediction);
944 }
945 let value = std::env::var_os(name)
946 .map(|value| {
947 value
948 .into_string()
949 .map_err(|_| BypassReason::UnsupportedPrediction)
950 })
951 .transpose()?;
952 Ok((name.clone(), value))
953 })
954 .collect::<Result<BTreeMap<_, _>, _>>()?;
955 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
956 }
957}
958
959#[derive(Serialize)]
960struct ActionDescriptor {
961 version: u8,
962 kind: &'static str,
963 adapter_version: u8,
964 compiler: CompilerDescriptor,
965 arguments: Vec<String>,
966 environment: BTreeMap<String, Option<String>>,
967 inputs: Vec<InputDescriptor>,
968 #[serde(skip_serializing_if = "Option::is_none")]
971 linker: Option<LinkerIdentity>,
972}
973
974#[derive(Serialize)]
975struct InvocationDescriptor {
976 version: u8,
977 kind: &'static str,
978 adapter_version: u8,
979 compiler: CompilerDescriptor,
980 arguments: Vec<String>,
981 required_inputs: Vec<String>,
982}
983
984#[derive(Serialize)]
985struct CompilerDescriptor {
986 toolchain: String,
987 rustc_version: String,
988 host: String,
989}
990
991#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
992struct InputDescriptor {
993 path: String,
994 digest: CacheDigest,
995}
996
997struct Parser<'a> {
998 arguments: &'a [OsString],
999 index: usize,
1000 parsed: Vec<Argument>,
1001 source: Option<PathBuf>,
1002 crate_types: Vec<String>,
1003 emits: Vec<Emit>,
1004 required_inputs: Vec<PathBuf>,
1005 test: bool,
1006 crate_name: Option<String>,
1007 extra_filename: String,
1008 out_dir: Option<PathBuf>,
1009 explicit_output: Option<PathBuf>,
1010 target: Option<String>,
1011 options: ParseOptions,
1012}
1013
1014struct ExpandedArguments {
1015 arguments: Vec<OsString>,
1016}
1017
1018#[derive(Default)]
1019struct ResponseExpander {
1020 shell_argfiles: bool,
1021 next_is_unstable_option: bool,
1022 arguments: Vec<OsString>,
1023}
1024
1025impl ResponseExpander {
1026 fn push(&mut self, argument: String) {
1027 if self.next_is_unstable_option {
1028 self.shell_argfiles |= argument == "shell-argfiles";
1029 self.next_is_unstable_option = false;
1030 } else if let Some(option) = argument.strip_prefix("-Z") {
1031 if option.is_empty() {
1032 self.next_is_unstable_option = true;
1033 } else {
1034 self.shell_argfiles |= option == "shell-argfiles";
1035 }
1036 }
1037 self.arguments.push(argument.into());
1038 }
1039}
1040
1041fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1044 let mut expanded = ResponseExpander::default();
1045 for (index, argument) in arguments.iter().enumerate() {
1046 let argument = argument
1047 .to_str()
1048 .ok_or(BypassReason::NonUtf8Argument { index })?;
1049 let Some(argfile) = argument.strip_prefix('@') else {
1050 expanded.push(argument.to_string());
1051 continue;
1052 };
1053 let (path, shell) = match argfile.split_once(':') {
1054 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1055 _ => (argfile, false),
1056 };
1057 let contents = std::fs::read_to_string(path).map_err(|error| {
1058 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1059 })?;
1060 if shell {
1061 let arguments = shlex::split(&contents).ok_or_else(|| {
1062 BypassReason::ResponseFile(format!(
1063 "invalid shell-style arguments in {}",
1064 Path::new(path).display()
1065 ))
1066 })?;
1067 for argument in arguments {
1068 expanded.push(argument);
1069 }
1070 } else {
1071 for argument in contents.lines() {
1072 expanded.push(argument.to_string());
1073 }
1074 }
1075 }
1076 Ok(ExpandedArguments {
1077 arguments: expanded.arguments,
1078 })
1079}
1080
1081impl<'a> Parser<'a> {
1082 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1083 Self {
1084 arguments,
1085 options,
1086 index: 0,
1087 parsed: Vec::new(),
1088 source: None,
1089 crate_types: Vec::new(),
1090 emits: Vec::new(),
1091 required_inputs: Vec::new(),
1092 test: false,
1093 crate_name: None,
1094 extra_filename: String::new(),
1095 out_dir: None,
1096 explicit_output: None,
1097 target: None,
1098 }
1099 }
1100
1101 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1102 while self.index < self.arguments.len() {
1103 let value = self.current()?.to_string();
1104 self.index += 1;
1105 if let Some(long) = value.strip_prefix("--") {
1106 self.parse_long(long)?;
1107 } else if value.starts_with('-') && value != "-" {
1108 self.parse_short(&value)?;
1109 } else {
1110 self.parse_input(&value)?;
1111 }
1112 }
1113
1114 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1115 let link_output = self.classify()?;
1116 let crate_name = self.crate_name.clone().map_or_else(
1117 || {
1118 source
1119 .file_stem()
1120 .and_then(|name| name.to_str())
1121 .map(|name| name.replace('-', "_"))
1122 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1123 },
1124 Ok,
1125 )?;
1126 self.required_inputs.push(source.clone());
1127 Ok(RustcInvocation {
1128 arguments: self.parsed,
1129 source,
1130 required_inputs: self.required_inputs,
1131 crate_name,
1132 extra_filename: self.extra_filename,
1133 out_dir: self.out_dir,
1134 explicit_output: self.explicit_output,
1135 emits: self.emits,
1136 target: self.target,
1137 link_output,
1138 })
1139 }
1140
1141 fn current(&self) -> Result<&str, BypassReason> {
1142 self.arguments[self.index]
1143 .to_str()
1144 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1145 }
1146
1147 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1148 if let Some(value) = inline {
1149 if value.is_empty() {
1150 return Err(BypassReason::MissingValue(flag.into()));
1151 }
1152 return Ok(value.into());
1153 }
1154 if self.index >= self.arguments.len() {
1155 return Err(BypassReason::MissingValue(flag.into()));
1156 }
1157 let value = self.current()?.to_string();
1158 self.index += 1;
1159 Ok(value)
1160 }
1161
1162 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1163 let (flag, inline) = value
1164 .split_once('=')
1165 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1166 let rendered_flag = format!("--{flag}");
1167 match flag {
1168 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1169 "test" => {
1170 self.test = true;
1171 self.parsed.push(Argument::Plain(rendered_flag));
1172 Ok(())
1173 }
1174 "verbose" => {
1175 self.parsed.push(Argument::Plain(rendered_flag));
1176 Ok(())
1177 }
1178 "crate-name" => {
1179 let value = self.take_value(&rendered_flag, inline)?;
1180 self.crate_name = Some(value.clone());
1181 self.parsed
1182 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1183 Ok(())
1184 }
1185 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1186 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1187 | "deny" | "forbid" | "cap-lints" => {
1188 let value = self.take_value(&rendered_flag, inline)?;
1189 self.parsed
1190 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1191 Ok(())
1192 }
1193 "target" => {
1194 let value = self.take_value(&rendered_flag, inline)?;
1195 self.target = Some(value.clone());
1196 if value.ends_with(".json") || value.contains(['/', '\\']) {
1197 let path = PathBuf::from(value);
1198 self.required_inputs.push(path.clone());
1199 self.parsed.push(Argument::Path {
1200 flag: rendered_flag,
1201 path,
1202 });
1203 } else {
1204 self.target = Some(value.clone());
1205 self.parsed
1206 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1207 }
1208 Ok(())
1209 }
1210 "crate-type" => {
1211 let value = self.take_value(&rendered_flag, inline)?;
1212 self.crate_types
1213 .extend(value.split(',').map(ToOwned::to_owned));
1214 self.parsed
1215 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1216 Ok(())
1217 }
1218 "emit" => {
1219 let value = self.take_value(&rendered_flag, inline)?;
1220 let emits = parse_emits(&value);
1221 self.emits.extend(emits.clone());
1222 self.parsed.push(Argument::Emit(emits));
1223 Ok(())
1224 }
1225 "out-dir" => {
1226 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1227 self.out_dir = Some(path.clone());
1228 self.parsed.push(Argument::Path {
1229 flag: rendered_flag,
1230 path,
1231 });
1232 Ok(())
1233 }
1234 "sysroot" => {
1235 let path = self.take_value(&rendered_flag, inline)?;
1236 self.parsed.push(Argument::Path {
1237 flag: rendered_flag,
1238 path: path.into(),
1239 });
1240 Ok(())
1241 }
1242 "extern" => {
1243 let value = self.take_value(&rendered_flag, inline)?;
1244 let (name, path) = value
1245 .split_once('=')
1246 .map_or((value.as_str(), None), |(name, path)| {
1247 (name, Some(PathBuf::from(path)))
1248 });
1249 if let Some(path) = &path {
1250 self.required_inputs.push(path.clone());
1251 }
1252 self.parsed.push(Argument::Extern {
1253 name: name.into(),
1254 path,
1255 });
1256 Ok(())
1257 }
1258 "remap-path-prefix" => {
1259 let value = self.take_value(&rendered_flag, inline)?;
1260 let Some((from, to)) = value.split_once('=') else {
1261 return Err(BypassReason::MissingValue(rendered_flag));
1262 };
1263 self.parsed.push(Argument::RemapPath {
1264 from: from.into(),
1265 to: to.into(),
1266 });
1267 Ok(())
1268 }
1269 "codegen" => {
1270 let value = self.take_value(&rendered_flag, inline)?;
1271 self.parse_codegen(&value)
1272 }
1273 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1274 }
1275 }
1276
1277 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1278 if let Some(attached) = value.strip_prefix("-Z") {
1279 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1280 if option == "shell-argfiles" {
1281 self.parsed.push(Argument::Plain("-Zshell-argfiles".into()));
1282 return Ok(());
1283 }
1284 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1285 }
1286 match value {
1287 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1290 "-g" | "-O" | "-v" => {
1291 self.parsed.push(Argument::Plain(value.into()));
1292 return Ok(());
1293 }
1294 _ => {}
1295 }
1296 for (short, long) in [
1297 ("-A", "--allow"),
1298 ("-W", "--warn"),
1299 ("-D", "--deny"),
1300 ("-F", "--forbid"),
1301 ] {
1302 if let Some(attached) = value.strip_prefix(short) {
1303 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1304 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1305 return Ok(());
1306 }
1307 }
1308 if let Some(attached) = value.strip_prefix("-C") {
1309 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1310 return self.parse_codegen(&option);
1311 }
1312 if let Some(attached) = value.strip_prefix("-L") {
1313 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1314 let (kind, path) = search
1315 .split_once('=')
1316 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1317 if !matches!(kind, "dependency" | "native") {
1318 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1319 }
1320 self.parsed.push(Argument::SearchPath {
1321 kind: kind.into(),
1322 path: path.into(),
1323 });
1324 return Ok(());
1325 }
1326 if value == "-l" || value.starts_with("-l") {
1327 return Err(BypassReason::NativeLibrary);
1328 }
1329 if let Some(attached) = value.strip_prefix("-o") {
1330 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1331 self.explicit_output = Some(path.clone().into());
1332 self.parsed.push(Argument::Path {
1333 flag: "-o".into(),
1334 path: path.into(),
1335 });
1336 return Ok(());
1337 }
1338 Err(BypassReason::UnknownFlag(value.into()))
1339 }
1340
1341 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1342 let name = value.split_once('=').map_or(value, |(name, _)| name);
1343 if name == "incremental" {
1344 return Err(BypassReason::Incremental);
1345 }
1346 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() {
1347 return Err(BypassReason::UnknownCodegenOption(name.into()));
1348 }
1349 if matches!(name, "link-arg" | "link-args")
1354 && let Some((_, option)) = value.split_once('=')
1355 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1356 && !prefix.trim_end_matches('/').is_empty()
1357 && !prefix.contains(',')
1358 {
1359 let trailing_slash = prefix.ends_with('/');
1360 self.parsed.push(Argument::OsoPrefix {
1361 path: PathBuf::from(prefix.trim_end_matches('/')),
1362 trailing_slash,
1363 });
1364 return Ok(());
1365 }
1366 self.parsed
1367 .push(Argument::Plain(format!("--codegen={value}")));
1368 if name == "extra-filename" {
1369 self.extra_filename = value
1370 .split_once('=')
1371 .map_or(String::new(), |(_, value)| value.to_string());
1372 }
1373 Ok(())
1374 }
1375
1376 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1377 if value == "-" {
1378 return Err(BypassReason::StandardInput);
1379 }
1380 if self.source.replace(value.into()).is_some() {
1381 return Err(BypassReason::MultipleInputs);
1382 }
1383 Ok(())
1384 }
1385
1386 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1387 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1396 let builds_a_library = !self.test
1397 && !self.crate_types.is_empty()
1398 && self
1399 .crate_types
1400 .iter()
1401 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1402 let link_output = if never_links || builds_a_library {
1403 LinkOutput::Library
1404 } else if self
1405 .target
1406 .as_deref()
1407 .is_some_and(compiler_bundled_wasm_target)
1408 && ((self.test && self.crate_types.is_empty())
1409 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1410 {
1411 if self.parsed.iter().any(|argument| match argument {
1412 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1413 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1414 !matches!(
1415 value.rsplit_once('=').map(|(_, value)| value),
1416 Some("y" | "yes" | "on" | "true")
1417 )
1418 }
1419 _ => false,
1420 }) {
1421 return Err(BypassReason::UnknownCodegenOption(
1422 "link-self-contained".into(),
1423 ));
1424 }
1425 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1426 && self.parsed.iter().any(|argument| {
1427 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1428 })
1429 {
1430 return Err(BypassReason::UnknownCodegenOption(
1431 "target-feature=-crt-static".into(),
1432 ));
1433 }
1434 LinkOutput::WasmExecutable
1438 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1439 self.check_native_link_is_portable()?;
1440 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1441 LinkOutput::NativeProcMacro
1442 } else {
1443 LinkOutput::NativeExecutable
1444 }
1445 } else if self.test {
1446 return Err(BypassReason::UnsupportedCrateType("test".into()));
1447 } else {
1448 return Err(BypassReason::UnsupportedCrateType(
1449 self.crate_types
1450 .iter()
1451 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1452 .cloned()
1453 .unwrap_or_else(|| "bin".into()),
1454 ));
1455 };
1456 if link_output != LinkOutput::Library
1465 && let Some(option) = self.first_link_argument()
1466 {
1467 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1468 }
1469 if !matches!(
1473 link_output,
1474 LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1475 ) && self
1476 .parsed
1477 .iter()
1478 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1479 {
1480 return Err(BypassReason::UnmodeledLinkArgument(
1481 "link-arg=-Wl,-oso_prefix".into(),
1482 ));
1483 }
1484 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1485 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1486 _ => None,
1487 }) {
1488 return Err(BypassReason::UnresolvedExtern(name.clone()));
1489 }
1490 if let Some(emit) = self
1491 .emits
1492 .iter()
1493 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1494 {
1495 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1496 }
1497 if !self
1498 .emits
1499 .iter()
1500 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1501 {
1502 return Err(BypassReason::NoCacheableOutput);
1503 }
1504 Ok(link_output)
1505 }
1506}
1507
1508impl Parser<'_> {
1509 fn first_link_argument(&self) -> Option<&str> {
1512 self.parsed.iter().find_map(|argument| {
1513 let Argument::Plain(value) = argument else {
1514 return None;
1515 };
1516 let option = value.strip_prefix("--codegen=")?;
1517 let name = option.split_once('=').map_or(option, |(name, _)| name);
1518 matches!(name, "link-arg" | "link-args").then_some(option)
1519 })
1520 }
1521
1522 fn oso_prefix_covers_outputs(&self) -> bool {
1531 let Some(directory) = self
1532 .out_dir
1533 .as_deref()
1534 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
1535 else {
1536 return false;
1537 };
1538 if !directory.is_absolute() {
1539 return false;
1540 }
1541 let directory = normalize_components(directory);
1542 self.parsed.iter().any(|argument| {
1543 let Argument::OsoPrefix { path, .. } = argument else {
1544 return false;
1545 };
1546 path.is_absolute() && directory.starts_with(normalize_components(path))
1547 })
1548 }
1549
1550 fn links_a_native_artifact(&self) -> bool {
1557 self.target.is_none()
1558 && self.emits.iter().any(|emit| emit.kind == "link")
1563 && ((self.test && self.crate_types.is_empty())
1564 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro")))
1565 }
1566
1567 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
1574 for argument in &self.parsed {
1575 let Argument::Plain(value) = argument else {
1576 continue;
1577 };
1578 let (name, value) = if value == "-g" {
1581 ("debuginfo", Some("2"))
1582 } else if let Some(option) = value.strip_prefix("--codegen=") {
1583 match option.split_once('=') {
1584 Some((name, value)) => (name, Some(value)),
1585 None => (option, None),
1589 }
1590 } else {
1591 continue;
1592 };
1593 let unportable = match name {
1594 "split-debuginfo" => match value {
1600 Some("off") => false,
1601 Some("unpacked") if cfg!(target_os = "macos") => {
1602 !self.oso_prefix_covers_outputs()
1603 }
1604 _ => true,
1605 },
1606 "debuginfo" if cfg!(target_os = "macos") => {
1615 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
1616 }
1617 "rpath" => is_enabled(value),
1625 "prefer-dynamic" => {
1626 is_enabled(value)
1627 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
1628 }
1629 "link-self-contained" => true,
1632 _ => false,
1633 };
1634 if unportable {
1635 return Err(BypassReason::UnportableNativeLink(match value {
1636 Some(value) => format!("{name}={value}"),
1637 None => name.to_owned(),
1638 }));
1639 }
1640 }
1641 Ok(())
1642 }
1643}
1644
1645fn is_enabled(value: Option<&str>) -> bool {
1648 matches!(value, None | Some("y" | "yes" | "on" | "true"))
1649}
1650
1651fn compiler_bundled_wasm_target(target: &str) -> bool {
1652 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
1653}
1654
1655fn parse_emits(value: &str) -> Vec<Emit> {
1656 value
1657 .split(',')
1658 .map(|emit| {
1659 let (kind, path) = emit
1660 .split_once('=')
1661 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
1662 Emit {
1663 kind: kind.into(),
1664 path,
1665 }
1666 })
1667 .collect()
1668}
1669
1670struct ActionBuilder<'a> {
1671 invocation: &'a RustcInvocation,
1672 context: ActionContext,
1673 mappings: Vec<PathMapping>,
1674 linker: Option<LinkerIdentity>,
1675}
1676
1677impl<'a> ActionBuilder<'a> {
1678 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
1679 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1680 let mappings = context
1681 .path_mappings
1682 .iter()
1683 .map(|mapping| PathMapping {
1684 root: resolve_mapping_root(&mapping.root),
1685 placeholder: mapping.placeholder.clone(),
1686 })
1687 .collect();
1688 Self {
1689 linker: None,
1690 invocation,
1691 mappings,
1692 context,
1693 }
1694 }
1695
1696 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
1697 self.linker = linker;
1698 self
1699 }
1700
1701 fn build(self) -> Result<RustcAction, BypassReason> {
1702 self.validate_mappings()?;
1703 let invocation = self.invocation_descriptor()?;
1704 let environment = self.environment_descriptor()?;
1705
1706 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1707 for input in &self.context.inputs {
1708 input
1709 .digest
1710 .validate()
1711 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
1712 let path = self.normalize_path(&input.path)?;
1713 if inputs
1714 .insert(path.clone(), input.digest.clone())
1715 .is_some_and(|existing| existing != input.digest)
1716 {
1717 return Err(BypassReason::ConflictingInput(path));
1718 }
1719 }
1720 let required = self
1721 .invocation
1722 .required_inputs
1723 .iter()
1724 .map(|path| self.normalize_path(path))
1725 .collect::<Result<BTreeSet<_>, _>>()?;
1726 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1727 return Err(BypassReason::MissingRequiredInput(missing.clone()));
1728 }
1729 let inputs = inputs
1730 .into_iter()
1731 .map(|(path, digest)| InputDescriptor { path, digest })
1732 .collect();
1733 if self.invocation.links_natively() && self.linker.is_none() {
1736 return Err(BypassReason::UnportableNativeLink(
1737 "linker identity is unknown".into(),
1738 ));
1739 }
1740 let descriptor = ActionDescriptor {
1741 version: ACTION_SCHEMA_VERSION,
1742 kind: "rustc",
1743 adapter_version: ADAPTER_VERSION,
1744 compiler: invocation.compiler,
1745 arguments: invocation.arguments,
1746 environment,
1747 inputs,
1748 linker: self.linker.clone(),
1749 };
1750 let bytes = canonical_json(&descriptor)
1751 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
1752 let digest = CacheDigest::blake3(&bytes);
1753 Ok(RustcAction { digest, bytes })
1754 }
1755
1756 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
1757 self.validate_mappings()?;
1758 let arguments = self
1759 .invocation
1760 .arguments
1761 .iter()
1762 .map(|argument| self.normalize_argument(argument))
1763 .collect::<Result<Vec<_>, _>>()?;
1764 let required_inputs = self
1765 .invocation
1766 .required_inputs
1767 .iter()
1768 .map(|path| self.normalize_path(path))
1769 .collect::<Result<BTreeSet<_>, _>>()?
1770 .into_iter()
1771 .collect();
1772 Ok(InvocationDescriptor {
1773 version: ACTION_SCHEMA_VERSION,
1774 kind: "rustc",
1775 adapter_version: ADAPTER_VERSION,
1776 compiler: CompilerDescriptor {
1777 toolchain: self.context.compiler.toolchain.clone(),
1778 rustc_version: self.context.compiler.rustc_version.clone(),
1779 host: self.context.compiler.host.clone(),
1780 },
1781 arguments,
1782 required_inputs,
1783 })
1784 }
1785
1786 fn validate_mappings(&self) -> Result<(), BypassReason> {
1787 if !self.context.working_dir.is_absolute() {
1788 return Err(BypassReason::RelativeWorkingDirectory(
1789 self.context.working_dir.clone(),
1790 ));
1791 }
1792 let mut roots = BTreeSet::new();
1793 let mut placeholders = BTreeSet::new();
1794 for mapping in &self.mappings {
1795 if !mapping.root.is_absolute() {
1796 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
1797 }
1798 if mapping.placeholder.is_empty()
1799 || !mapping
1800 .placeholder
1801 .bytes()
1802 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1803 || !roots.insert(normalize_components(&mapping.root))
1804 || !placeholders.insert(&mapping.placeholder)
1805 {
1806 return Err(BypassReason::InvalidPathPlaceholder(
1807 mapping.placeholder.clone(),
1808 ));
1809 }
1810 }
1811 Ok(())
1812 }
1813
1814 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
1815 match argument {
1816 Argument::Plain(value) => Ok(value.clone()),
1817 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1818 Argument::SearchPath { kind, path } => {
1819 let text = match self.normalize_path(path) {
1820 Ok(text) => text,
1821 Err(BypassReason::UnmappedAbsolutePath(absolute))
1829 if kind == "native" && self.invocation.native_search_is_inert() =>
1830 {
1831 absolute
1832 .to_str()
1833 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
1834 .to_string()
1835 }
1836 Err(error) => return Err(error),
1837 };
1838 Ok(format!("-L{kind}={text}"))
1839 }
1840 Argument::Extern { name, path } => match path {
1841 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
1842 None => Ok(format!("--extern={name}")),
1843 },
1844 Argument::Emit(emits) => Ok(format!(
1845 "--emit={}",
1846 emits
1847 .iter()
1848 .map(|emit| match &emit.path {
1849 Some(path) => self
1850 .normalize_path(path)
1851 .map(|path| format!("{}={path}", emit.kind)),
1852 None => Ok(emit.kind.clone()),
1853 })
1854 .collect::<Result<Vec<_>, _>>()?
1855 .join(",")
1856 )),
1857 Argument::RemapPath { from, to } => Ok(format!(
1858 "--remap-path-prefix={}={}",
1859 self.normalize_path(from)?,
1860 to
1861 )),
1862 Argument::OsoPrefix {
1863 path,
1864 trailing_slash,
1865 } => Ok(format!(
1866 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
1867 self.normalize_path(path)?,
1868 if *trailing_slash { "/" } else { "" }
1869 )),
1870 }
1871 }
1872
1873 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
1880 self.context
1881 .environment
1882 .iter()
1883 .map(|(name, value)| {
1884 let value = match value {
1885 Some(value) if self.context.portable_environment.contains(name) => {
1886 Some(self.normalize_path(Path::new(value))?)
1887 }
1888 value => value.clone(),
1889 };
1890 Ok((name.clone(), value))
1891 })
1892 .collect()
1893 }
1894
1895 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
1896 normalize_resolved_mapped_path(path, &self.context.working_dir, &self.mappings)
1897 }
1898}
1899
1900fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
1904 directories.iter().any(|directory| {
1905 path.len() > directory.len()
1906 && path.as_bytes()[directory.len()] == b'/'
1907 && path.starts_with(directory)
1908 })
1909}
1910
1911fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
1912 for mapping in mappings {
1913 let prefix = format!("${{{}}}", mapping.placeholder);
1914 let suffix = if value == prefix {
1915 ""
1916 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1917 suffix
1918 } else {
1919 continue;
1920 };
1921 if !mapping.root.is_absolute()
1922 || (!suffix.is_empty()
1923 && suffix.split('/').any(|component| {
1924 component.is_empty()
1925 || matches!(component, "." | "..")
1926 || component.contains('\\')
1927 }))
1928 {
1929 return Err(BypassReason::InvalidPredictedInput(value.into()));
1930 }
1931 let mut path = normalize_components(&mapping.root);
1932 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1933 return Ok(path);
1934 }
1935 Err(BypassReason::InvalidPredictedInput(value.into()))
1936}
1937
1938fn normalize_components(path: &Path) -> PathBuf {
1939 let mut normalized = PathBuf::new();
1940 for component in path.components() {
1941 match component {
1942 Component::CurDir => {}
1943 Component::ParentDir => {
1944 normalized.pop();
1945 }
1946 component => normalized.push(component.as_os_str()),
1947 }
1948 }
1949 normalized
1950}
1951
1952fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1953 if path.is_absolute() {
1954 normalize_components(path)
1955 } else {
1956 normalize_components(&working_dir.join(path))
1957 }
1958}
1959
1960fn slash_path(path: &Path) -> Result<String, BypassReason> {
1961 path.components()
1962 .filter_map(|component| match component {
1963 Component::Normal(value) => Some(
1964 value
1965 .to_str()
1966 .map(ToOwned::to_owned)
1967 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf())),
1968 ),
1969 _ => None,
1970 })
1971 .collect::<Result<Vec<_>, _>>()
1972 .map(|components| components.join("/"))
1973}
1974
1975#[cfg(test)]
1976#[path = "rustc_cache_tests.rs"]
1977mod tests;