1#![deny(missing_docs)]
19
20use mbx_cache_core::{CacheDigest, FileDigestCache, canonical_json};
21use mbx_cache_rustc::{BypassReason as RustcBypassReason, PathMapping, normalize_mapped_path};
22use serde::{Deserialize, Serialize};
23use std::collections::{BTreeMap, BTreeSet};
24use std::ffi::OsString;
25use std::path::{Component, Path, PathBuf};
26use thiserror::Error;
27
28mod depfile;
29
30pub use depfile::{CcDepfile, CcDiscoveredInputs, INCLUDE_MANIFEST_PREFIX, manifest_snapshot};
31
32pub const ACTION_SCHEMA_VERSION: u8 = 1;
34pub const ADAPTER_VERSION: u8 = 1;
36
37pub const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
39pub const MAX_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
41pub const MAX_MANIFEST_ENTRIES: usize = 16 * 1024;
43
44pub const KEYED_ENVIRONMENT: &[&str] = &[
50 "IPHONEOS_DEPLOYMENT_TARGET",
51 "LANG",
52 "LC_ALL",
53 "LC_MESSAGES",
54 "MACOSX_DEPLOYMENT_TARGET",
55 "SDKROOT",
56 "SOURCE_DATE_EPOCH",
57 "TVOS_DEPLOYMENT_TARGET",
58 "WATCHOS_DEPLOYMENT_TARGET",
59 "XROS_DEPLOYMENT_TARGET",
60];
61
62pub const BYPASS_ENVIRONMENT: &[&str] = &[
68 "CPATH",
69 "COMPILER_PATH",
70 "CPLUS_INCLUDE_PATH",
71 "C_INCLUDE_PATH",
72 "DEPENDENCIES_OUTPUT",
73 "GCC_EXEC_PREFIX",
74 "OBJC_INCLUDE_PATH",
75 "SUNPRO_DEPENDENCIES",
76];
77
78pub const SYSTEM_ROOTS: &[&str] = &[
86 "/Applications/Xcode.app",
87 "/Library/Developer",
88 "/nix/store",
89 "/usr/include",
90 "/usr/lib",
91 "/usr/local/include",
92];
93
94const SUPPORTED_F_FLAGS: &[&str] = &[
95 "PIC",
96 "PIE",
97 "asynchronous-unwind-tables",
98 "color-diagnostics",
99 "data-sections",
100 "diagnostics-color",
101 "exceptions",
102 "function-sections",
103 "merge-all-constants",
104 "no-asynchronous-unwind-tables",
105 "no-builtin",
106 "no-common",
107 "no-exceptions",
108 "no-omit-frame-pointer",
109 "no-plt",
110 "no-rtti",
111 "no-strict-aliasing",
112 "omit-frame-pointer",
113 "pic",
114 "pie",
115 "rtti",
116 "short-enums",
117 "signed-char",
118 "stack-protector",
119 "stack-protector-all",
120 "stack-protector-strong",
121 "strict-aliasing",
122 "unsigned-char",
123 "visibility",
124 "visibility-inlines-hidden",
125 "wrapv",
126];
127
128const SUPPORTED_M_FLAGS: &[&str] = &[
129 "32",
130 "64",
131 "arch",
132 "arm",
133 "avx",
134 "avx2",
135 "cpu",
136 "float-abi",
137 "fma",
138 "fpu",
139 "iphoneos-version-min",
140 "macosx-version-min",
141 "no-omit-leaf-frame-pointer",
142 "omit-leaf-frame-pointer",
143 "sse",
144 "sse2",
145 "sse3",
146 "sse4.1",
147 "sse4.2",
148 "thumb",
149 "tune",
150];
151
152const SUPPORTED_O_FLAGS: &[&str] = &[
153 "-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Og", "-Os", "-Oz",
154];
155
156const SUPPORTED_G_FLAGS: &[&str] = &[
157 "-g",
158 "-g0",
159 "-g1",
160 "-g2",
161 "-g3",
162 "-gdwarf-2",
163 "-gdwarf-3",
164 "-gdwarf-4",
165 "-gdwarf-5",
166];
167
168const SUPPORTED_BARE_FLAGS: &[&str] = &[
169 "-ansi",
170 "-nostdinc",
171 "-nostdinc++",
172 "-pedantic",
173 "-pedantic-errors",
174 "-pipe",
175 "-pthread",
176 "-w",
177];
178
179const SEPARATE_PATH_FLAGS: &[&str] = &[
180 "-idirafter",
181 "-imacros",
182 "-include",
183 "-iquote",
184 "-isysroot",
185 "-isystem",
186];
187
188const TOOL_PASSTHROUGH_FLAGS: &[&str] = &["-Xassembler", "-Xclang", "-Xlinker", "-Xpreprocessor"];
189
190const COMPILER_QUERY_FLAGS: &[&str] = &[
191 "--help",
192 "--version",
193 "-###",
194 "-?",
197 "-dumpmachine",
198 "-dumpversion",
199 "-v",
200];
201
202const PREFIX_MAP_FLAGS: &[&str] = &[
207 "-fdebug-prefix-map",
208 "-ffile-prefix-map",
209 "-fmacro-prefix-map",
210];
211
212impl CcBypassReason {
213 pub fn kind(&self) -> &'static str {
218 self.into()
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
227#[strum(serialize_all = "kebab-case")]
228#[non_exhaustive]
229pub enum CcBypassReason {
230 #[error("compiler argument {index} is not valid UTF-8")]
232 NonUtf8Argument {
233 index: usize,
235 },
236 #[error("compiler response file is not modeled by the cache adapter: {0}")]
238 ResponseFile(String),
239 #[error("compiler flag is not modeled by the cache adapter: {0}")]
241 UnknownFlag(String),
242 #[error("compiler flag {0} is missing its value")]
244 MissingValue(String),
245 #[error("compiler invocation queries the driver instead of compiling")]
247 CompilerQuery,
248 #[error("compiler invocation does not compile with -c")]
250 NotACompile,
251 #[error("compiler invocation emits a non-object output: {0}")]
253 NonObjectOutput(String),
254 #[error("compiler invocation reads its source from standard input")]
256 StandardInput,
257 #[error("compiler invocation names no source file")]
259 MissingInput,
260 #[error("compiler invocation names more than one source file")]
262 MultipleInputs,
263 #[error("compiler invocation names no output file")]
265 MissingOutput,
266 #[error("compiler input language is not modeled by the cache adapter: {0}")]
268 UnsupportedLanguage(String),
269 #[error("compiler invocation requests its own dependency output: {0}")]
271 CallerDependencyFlags(String),
272 #[error("precompiled headers are not modeled by the cache adapter: {0}")]
274 PrecompiledHeader(String),
275 #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
277 CoverageInstrumentation(String),
278 #[error("split debug output is not modeled by the cache adapter: {0}")]
280 SplitDebugOutput(String),
281 #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
283 SaveTemps(String),
284 #[error("compiler flag forwards options to another tool: {0}")]
286 ToolPassthrough(String),
287 #[error("compiler plugins are not modeled by the cache adapter: {0}")]
289 Plugin(String),
290 #[error("include search directory changed during the compilation: {0}")]
293 SearchPathModifiedDuringCompilation(PathBuf),
294 #[error("compiler flag tunes for the local CPU: {0}")]
296 LocalCpuTarget(String),
297 #[error("compiler driver is not modeled by the cache adapter: {0}")]
299 UnsupportedCompilerDriver(String),
300 #[error("could not establish compiler identity: {0}")]
302 CompilerIdentityUnavailable(String),
303 #[error("environment variable {0} changes the compilation in an unmodeled way")]
305 UnsupportedEnvironment(String),
306 #[error("no real compiler was pinned for the cc shim")]
308 RealCompilerUnpinned,
309 #[error("input expands a timestamp macro: {0}")]
312 EmbeddedTimestampMacro(PathBuf),
313 #[error("could not model the compiler depfile: {0}")]
315 MalformedDepfile(String),
316 #[error("could not read the compiler depfile {path}: {message}")]
318 DepfileRead {
319 path: PathBuf,
321 message: String,
323 },
324 #[error("compilation reads more inputs than the cache adapter models")]
326 TooManyInputs,
327 #[error("path is outside every modeled root: {0}")]
329 UnmappedAbsolutePath(PathBuf),
330 #[error("path is not valid UTF-8: {0}")]
332 NonUtf8Path(PathBuf),
333 #[error("compiler working directory is not absolute: {0}")]
335 RelativeWorkingDirectory(PathBuf),
336 #[error("path mapping root is not absolute: {0}")]
338 RelativePathMapping(PathBuf),
339 #[error("invalid path mapping placeholder: {0}")]
341 InvalidPathPlaceholder(String),
342 #[error("required input is missing from the discovered inputs: {0}")]
344 MissingRequiredInput(String),
345 #[error("invalid digest for input: {0}")]
347 InvalidInputDigest(String),
348 #[error("conflicting digests for input: {0}")]
350 ConflictingInput(String),
351 #[error("could not read input {path}: {message}")]
353 InputRead {
354 path: PathBuf,
356 message: String,
358 },
359 #[error("input changed during the compilation: {0}")]
361 InputChanged(PathBuf),
362 #[error("input was modified during the compilation: {0}")]
364 InputModifiedDuringCompilation(PathBuf),
365 #[error("discovered inputs use a different working directory")]
367 DiscoveryWorkingDirectory,
368 #[error("action prediction is not modeled by this adapter version")]
370 UnsupportedPrediction,
371 #[error("invalid predicted input: {0}")]
373 InvalidPredictedInput(String),
374 #[error("could not serialize the action descriptor: {0}")]
376 Serialization(String),
377}
378
379impl From<RustcBypassReason> for CcBypassReason {
380 fn from(reason: RustcBypassReason) -> Self {
383 match reason {
384 RustcBypassReason::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
385 RustcBypassReason::NonUtf8Path(path) => Self::NonUtf8Path(path),
386 other => Self::UnknownFlag(other.kind().into()),
387 }
388 }
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393pub enum CcLanguage {
394 C,
396 Cxx,
398}
399
400impl CcLanguage {
401 pub fn shim_stem(self) -> &'static str {
403 match self {
404 Self::C => "mbx-cc",
405 Self::Cxx => "mbx-cxx",
406 }
407 }
408
409 pub fn default_driver(self) -> &'static str {
411 match self {
412 Self::C => "cc",
413 Self::Cxx => "c++",
414 }
415 }
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub enum CcCompilerFamily {
421 Gcc,
423 Clang,
425 AppleClang,
427}
428
429impl CcCompilerFamily {
430 pub fn as_str(self) -> &'static str {
432 match self {
433 Self::Gcc => "gcc",
434 Self::Clang => "clang",
435 Self::AppleClang => "apple-clang",
436 }
437 }
438
439 pub fn uses_external_assembler(self) -> bool {
442 matches!(self, Self::Gcc)
443 }
444
445 pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
447 if probe.contains("Apple clang version") {
448 Ok(Self::AppleClang)
449 } else if probe.contains("clang version") {
450 Ok(Self::Clang)
451 } else if probe.contains("gcc version") {
452 Ok(Self::Gcc)
453 } else {
454 Err(CcBypassReason::UnsupportedCompilerDriver(
455 probe.lines().next().unwrap_or_default().into(),
456 ))
457 }
458 }
459}
460
461#[derive(Debug, Clone, PartialEq, Eq)]
463pub struct CcCompilerIdentity {
464 pub family: CcCompilerFamily,
466 pub version_text: String,
468 pub target: String,
470 pub assembler: String,
476}
477
478#[derive(Debug, Clone, PartialEq, Eq)]
480pub struct CcActionInput {
481 pub path: PathBuf,
484 pub digest: CacheDigest,
486}
487
488#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct CcActionContext {
491 pub compiler: CcCompilerIdentity,
493 pub working_dir: PathBuf,
495 pub path_mappings: Vec<PathMapping>,
497 pub environment: BTreeMap<String, Option<String>>,
499 pub inputs: Vec<CcActionInput>,
501}
502
503#[derive(Debug, Clone, PartialEq, Eq)]
505pub struct CcAction {
506 pub digest: CacheDigest,
508 pub bytes: Vec<u8>,
510}
511
512#[derive(Debug, Serialize)]
513struct CcCompilerDescriptor {
514 assembler: String,
515 family: String,
516 target: String,
517 version_text: String,
518}
519
520#[derive(Debug, Serialize)]
521struct CcInputDescriptor {
522 digest: CacheDigest,
523 path: String,
524}
525
526#[derive(Debug, Serialize)]
527struct CcActionDescriptor {
528 version: u8,
529 kind: &'static str,
530 adapter_version: u8,
531 compiler: CcCompilerDescriptor,
532 arguments: Vec<String>,
533 environment: BTreeMap<String, Option<String>>,
534 inputs: Vec<CcInputDescriptor>,
535}
536
537#[derive(Debug, Serialize)]
538struct CcInvocationDescriptor {
539 version: u8,
540 kind: &'static str,
541 adapter_version: u8,
542 compiler: CcCompilerDescriptor,
543 arguments: Vec<String>,
544 required_inputs: Vec<String>,
545}
546
547#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(deny_unknown_fields)]
551pub struct CcInputPrediction {
552 pub version: u8,
554 pub inputs: Vec<String>,
556 pub environment: Vec<String>,
558 #[serde(default, skip_serializing_if = "is_zero")]
561 pub compiler_duration_ns: u64,
562 #[serde(default, skip_serializing_if = "String::is_empty")]
564 pub source_name: String,
565}
566
567fn is_zero(value: &u64) -> bool {
568 *value == 0
569}
570
571#[derive(Debug, Clone, PartialEq, Eq)]
573enum Argument {
574 Plain(String),
576 Path { flag: String, path: PathBuf },
578 PrefixMap {
580 flag: String,
581 from: PathBuf,
582 to: String,
583 },
584 Source(PathBuf),
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
590pub struct CcInvocation {
591 arguments: Vec<Argument>,
592 source: PathBuf,
593 output: PathBuf,
594 include_dirs: Vec<PathBuf>,
595 required_inputs: Vec<PathBuf>,
596 language: CcLanguage,
597 sysroot: Option<PathBuf>,
598}
599
600impl CcInvocation {
601 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
604 Parser::new(arguments).parse()
605 }
606
607 pub fn source(&self) -> &Path {
609 &self.source
610 }
611
612 pub fn output(&self) -> &Path {
614 &self.output
615 }
616
617 pub fn include_dirs(&self) -> &[PathBuf] {
619 &self.include_dirs
620 }
621
622 pub fn required_inputs(&self) -> &[PathBuf] {
624 &self.required_inputs
625 }
626
627 pub fn language(&self) -> CcLanguage {
629 self.language
630 }
631
632 pub fn sysroot(&self) -> Option<&Path> {
634 self.sysroot.as_deref()
635 }
636
637 pub fn source_name(&self) -> String {
639 self.source
640 .file_name()
641 .map(|name| name.to_string_lossy().into_owned())
642 .unwrap_or_default()
643 }
644
645 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
652 vec!["-MD".into(), "-MF".into(), depfile.into()]
653 }
654
655 pub fn invocation_digest(
657 &self,
658 context: &CcActionContext,
659 ) -> Result<CacheDigest, CcBypassReason> {
660 let builder = ActionBuilder::new(self, context.clone());
661 let descriptor = builder.invocation_descriptor()?;
662 let bytes = canonical_json(&descriptor)
663 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
664 Ok(CacheDigest::blake3(&bytes))
665 }
666
667 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
670 ActionBuilder::new(self, context).build()
671 }
672
673 pub fn prediction(
676 &self,
677 context: &CcActionContext,
678 compiler_duration_ns: u64,
679 ) -> Result<CcInputPrediction, CcBypassReason> {
680 let builder = ActionBuilder::new(self, context.clone());
681 let mut inputs = context
682 .inputs
683 .iter()
684 .map(|input| builder.normalize_input_path(&input.path))
685 .collect::<Result<Vec<_>, _>>()?;
686 inputs.sort();
687 inputs.dedup();
688 Ok(CcInputPrediction {
689 version: 1,
690 inputs,
691 environment: context.environment.keys().cloned().collect(),
692 compiler_duration_ns,
693 source_name: self.source_name(),
694 })
695 }
696}
697
698impl CcInputPrediction {
699 pub fn discover(
702 &self,
703 working_dir: &Path,
704 path_mappings: &[PathMapping],
705 digests: &dyn FileDigestCache,
706 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
707 if self.version != 1 {
708 return Err(CcBypassReason::UnsupportedPrediction);
709 }
710 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
711 return Err(CcBypassReason::UnsupportedPrediction);
712 }
713 let mappings = PathMapping::ordered(path_mappings);
714 let mut files = BTreeSet::new();
715 let mut directories = BTreeSet::new();
716 for entry in &self.inputs {
717 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
718 Some(directory) => {
719 directories.insert(denormalize_path(directory, &mappings)?);
720 }
721 None => {
722 files.insert(denormalize_path(entry, &mappings)?);
723 }
724 }
725 }
726 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
727 }
728}
729
730fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
736 for mapping in mappings {
737 let prefix = format!("${{{}}}", mapping.placeholder);
738 let suffix = if value == prefix {
739 ""
740 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
741 suffix
742 } else {
743 continue;
744 };
745 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
746 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
747 }
748 let mut path = normalize_components(&mapping.root);
749 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
750 return Ok(path);
751 }
752 let path = PathBuf::from(value);
756 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
757 return Ok(path);
758 }
759 Err(CcBypassReason::InvalidPredictedInput(value.into()))
760}
761
762fn safe_suffix(suffix: &str) -> bool {
763 suffix.is_empty()
764 || !suffix.split('/').any(|component| {
765 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
766 })
767}
768
769pub fn is_system_path(path: &Path) -> bool {
771 SYSTEM_ROOTS
772 .iter()
773 .any(|root| path.starts_with(Path::new(root)))
774}
775
776fn normalize_components(path: &Path) -> PathBuf {
777 let mut normalized = PathBuf::new();
778 for component in path.components() {
779 match component {
780 Component::CurDir => {}
781 Component::ParentDir => {
782 normalized.pop();
783 }
784 component => normalized.push(component.as_os_str()),
785 }
786 }
787 normalized
788}
789
790pub fn environment_inputs<F>(
793 lookup: F,
794 sysroot: Option<&Path>,
795) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
796where
797 F: Fn(&str) -> Option<String>,
798{
799 for name in BYPASS_ENVIRONMENT {
800 if lookup(name).is_some() {
801 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
802 }
803 }
804 let mut environment = BTreeMap::new();
805 for name in KEYED_ENVIRONMENT {
806 if *name == "SDKROOT" && sysroot.is_some() {
809 continue;
810 }
811 environment.insert((*name).to_string(), lookup(name));
812 }
813 Ok(environment)
814}
815
816struct ActionBuilder<'a> {
817 invocation: &'a CcInvocation,
818 context: CcActionContext,
819 mappings: Vec<PathMapping>,
820}
821
822impl<'a> ActionBuilder<'a> {
823 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
824 context.path_mappings = PathMapping::ordered(&context.path_mappings);
825 let mappings = context.path_mappings.clone();
826 Self {
827 invocation,
828 context,
829 mappings,
830 }
831 }
832
833 fn build(self) -> Result<CcAction, CcBypassReason> {
834 self.validate_mappings()?;
835 let invocation = self.invocation_descriptor()?;
836
837 let mut inputs = BTreeMap::<String, CacheDigest>::new();
838 for input in &self.context.inputs {
839 input.digest.validate().map_err(|_| {
840 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
841 })?;
842 let path = self.normalize_input_path(&input.path)?;
843 if inputs
844 .insert(path.clone(), input.digest.clone())
845 .is_some_and(|existing| existing != input.digest)
846 {
847 return Err(CcBypassReason::ConflictingInput(path));
848 }
849 }
850 let required = self
851 .invocation
852 .required_inputs
853 .iter()
854 .map(|path| self.normalize_path(path))
855 .collect::<Result<BTreeSet<_>, _>>()?;
856 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
857 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
858 }
859 let inputs = inputs
860 .into_iter()
861 .map(|(path, digest)| CcInputDescriptor { path, digest })
862 .collect();
863 let descriptor = CcActionDescriptor {
864 version: ACTION_SCHEMA_VERSION,
865 kind: "cc",
866 adapter_version: ADAPTER_VERSION,
867 compiler: invocation.compiler,
868 arguments: invocation.arguments,
869 environment: self.context.environment.clone(),
870 inputs,
871 };
872 let bytes = canonical_json(&descriptor)
873 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
874 let digest = CacheDigest::blake3(&bytes);
875 Ok(CcAction { digest, bytes })
876 }
877
878 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
879 self.validate_mappings()?;
880 let arguments = self
881 .invocation
882 .arguments
883 .iter()
884 .map(|argument| self.normalize_argument(argument))
885 .collect::<Result<Vec<_>, _>>()?;
886 let required_inputs = self
887 .invocation
888 .required_inputs
889 .iter()
890 .map(|path| self.normalize_path(path))
891 .collect::<Result<BTreeSet<_>, _>>()?
892 .into_iter()
893 .collect();
894 Ok(CcInvocationDescriptor {
895 version: ACTION_SCHEMA_VERSION,
896 kind: "cc",
897 adapter_version: ADAPTER_VERSION,
898 compiler: self.compiler_descriptor(),
899 arguments,
900 required_inputs,
901 })
902 }
903
904 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
905 CcCompilerDescriptor {
906 assembler: self.context.compiler.assembler.clone(),
907 family: self.context.compiler.family.as_str().into(),
908 target: self.context.compiler.target.clone(),
909 version_text: self.context.compiler.version_text.clone(),
910 }
911 }
912
913 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
914 if !self.context.working_dir.is_absolute() {
915 return Err(CcBypassReason::RelativeWorkingDirectory(
916 self.context.working_dir.clone(),
917 ));
918 }
919 let mut roots = BTreeSet::new();
920 let mut placeholders = BTreeSet::new();
921 for mapping in &self.mappings {
922 if !mapping.root.is_absolute() {
923 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
924 }
925 if mapping.placeholder.is_empty()
926 || !mapping
927 .placeholder
928 .bytes()
929 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
930 || !roots.insert(normalize_components(&mapping.root))
931 || !placeholders.insert(&mapping.placeholder)
932 {
933 return Err(CcBypassReason::InvalidPathPlaceholder(
934 mapping.placeholder.clone(),
935 ));
936 }
937 }
938 Ok(())
939 }
940
941 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
942 match argument {
943 Argument::Plain(value) => Ok(value.clone()),
944 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
945 Argument::PrefixMap { flag, from, to } => {
946 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
947 }
948 Argument::Source(path) => Ok(self.normalize_path(path)?),
949 }
950 }
951
952 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
959 match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
960 Ok(normalized) => Ok(normalized),
961 Err(reason) => {
962 let absolute = absolute_path(path, &self.context.working_dir);
963 if is_system_path(&absolute) {
964 return absolute
965 .to_str()
966 .map(ToOwned::to_owned)
967 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
968 }
969 Err(reason.into())
970 }
971 }
972 }
973
974 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
975 match path.to_str().and_then(|path| {
976 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
977 .map(ToOwned::to_owned)
978 }) {
979 Some(directory) => Ok(format!(
980 "{INCLUDE_MANIFEST_PREFIX}{}",
981 self.normalize_path(Path::new(&directory))?
982 )),
983 None => self.normalize_path(path),
984 }
985 }
986}
987
988fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
989 if path.is_absolute() {
990 normalize_components(path)
991 } else {
992 normalize_components(&working_dir.join(path))
993 }
994}
995
996struct Parser<'a> {
997 arguments: &'a [OsString],
998 index: usize,
999 parsed: Vec<Argument>,
1000 source: Option<PathBuf>,
1001 output: Option<PathBuf>,
1002 include_dirs: Vec<PathBuf>,
1003 required_inputs: Vec<PathBuf>,
1004 sysroot: Option<PathBuf>,
1005 explicit_language: Option<CcLanguage>,
1006 compiling: bool,
1007}
1008
1009impl<'a> Parser<'a> {
1010 fn new(arguments: &'a [OsString]) -> Self {
1011 Self {
1012 arguments,
1013 index: 0,
1014 parsed: Vec::new(),
1015 source: None,
1016 output: None,
1017 include_dirs: Vec::new(),
1018 required_inputs: Vec::new(),
1019 sysroot: None,
1020 explicit_language: None,
1021 compiling: false,
1022 }
1023 }
1024
1025 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1026 while self.index < self.arguments.len() {
1027 let value = self.current()?.to_string();
1028 self.index += 1;
1029 if value == "-" {
1030 return Err(CcBypassReason::StandardInput);
1031 }
1032 if let Some(argfile) = value.strip_prefix('@') {
1033 return Err(CcBypassReason::ResponseFile(argfile.into()));
1034 }
1035 if value.starts_with('-') {
1036 self.parse_flag(&value)?;
1037 } else {
1038 self.parse_input(&value)?;
1039 }
1040 }
1041
1042 if !self.compiling {
1043 return Err(CcBypassReason::NotACompile);
1044 }
1045 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1046 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1047 let language = self.language(&source)?;
1048 self.required_inputs.push(source.clone());
1049 Ok(CcInvocation {
1050 arguments: self.parsed,
1051 source,
1052 output,
1053 include_dirs: self.include_dirs,
1054 required_inputs: self.required_inputs,
1055 language,
1056 sysroot: self.sysroot,
1057 })
1058 }
1059
1060 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1061 if let Some(language) = self.explicit_language {
1062 return Ok(language);
1063 }
1064 let extension = source
1065 .extension()
1066 .and_then(|extension| extension.to_str())
1067 .unwrap_or_default();
1068 match extension {
1069 "c" => Ok(CcLanguage::C),
1070 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1071 _ => Err(CcBypassReason::UnsupportedLanguage(
1072 source.display().to_string(),
1073 )),
1074 }
1075 }
1076
1077 fn current(&self) -> Result<&str, CcBypassReason> {
1078 self.arguments[self.index]
1079 .to_str()
1080 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1081 }
1082
1083 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1084 if let Some(value) = inline
1085 && !value.is_empty()
1086 {
1087 return Ok(value.into());
1088 }
1089 if self.index >= self.arguments.len() {
1090 return Err(CcBypassReason::MissingValue(flag.into()));
1091 }
1092 let value = self.current()?.to_string();
1093 self.index += 1;
1094 Ok(value)
1095 }
1096
1097 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1098 if self.source.is_some() {
1099 return Err(CcBypassReason::MultipleInputs);
1100 }
1101 let path = PathBuf::from(value);
1102 if self.explicit_language.is_none() {
1106 let extension = path
1107 .extension()
1108 .and_then(|extension| extension.to_str())
1109 .unwrap_or_default();
1110 if !matches!(extension, "c" | "cc" | "cpp" | "cxx" | "c++") {
1111 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1112 }
1113 }
1114 self.source = Some(path.clone());
1115 self.parsed.push(Argument::Source(path));
1116 Ok(())
1117 }
1118
1119 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1120 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1121 return Err(CcBypassReason::CompilerQuery);
1122 }
1123 if matches!(value, "-E" | "-S") {
1124 return Err(CcBypassReason::NonObjectOutput(value.into()));
1125 }
1126 if value.starts_with("-M") {
1127 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1128 }
1129 if value.starts_with("-save-temps") {
1130 return Err(CcBypassReason::SaveTemps(value.into()));
1131 }
1132 if value == "--coverage" {
1133 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1134 }
1135 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1136 || value.starts_with("-Wp,")
1137 || value.starts_with("-Wa,")
1138 || value.starts_with("-Wl,")
1139 {
1140 return Err(CcBypassReason::ToolPassthrough(value.into()));
1143 }
1144 if value.starts_with("-include-pch") || value == "-emit-pch" {
1145 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1146 }
1147
1148 if value == "-c" {
1149 self.compiling = true;
1150 self.parsed.push(Argument::Plain(value.into()));
1151 return Ok(());
1152 }
1153 if SUPPORTED_BARE_FLAGS.contains(&value)
1154 || SUPPORTED_O_FLAGS.contains(&value)
1155 || SUPPORTED_G_FLAGS.contains(&value)
1156 || value.starts_with("-std=")
1157 {
1158 self.parsed.push(Argument::Plain(value.into()));
1159 return Ok(());
1160 }
1161 if let Some(rest) = value.strip_prefix("-o") {
1162 let path = self.take_value("-o", Some(rest))?;
1163 self.output = Some(PathBuf::from(&path));
1166 self.parsed.push(Argument::Path {
1167 flag: "-o".into(),
1168 path: PathBuf::from(path),
1169 });
1170 return Ok(());
1171 }
1172 if let Some(rest) = value.strip_prefix("-I") {
1173 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1174 self.include_dirs.push(path.clone());
1175 self.parsed.push(Argument::Path {
1176 flag: "-I".into(),
1177 path,
1178 });
1179 return Ok(());
1180 }
1181 if SEPARATE_PATH_FLAGS.contains(&value) {
1182 let path = PathBuf::from(self.take_value(value, None)?);
1183 match value {
1184 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1185 "-isysroot" => self.sysroot = Some(path.clone()),
1186 _ => {}
1192 }
1193 self.parsed.push(Argument::Path {
1194 flag: value.into(),
1195 path,
1196 });
1197 return Ok(());
1198 }
1199 if let Some(rest) = value.strip_prefix("--include=") {
1202 let path = PathBuf::from(rest);
1203 self.parsed.push(Argument::Path {
1204 flag: "-include".into(),
1205 path,
1206 });
1207 return Ok(());
1208 }
1209 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1210 value
1211 .strip_prefix(&format!("{flag}="))
1212 .map(|rest| (*flag, rest))
1213 }) {
1214 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1215 self.parsed.push(Argument::PrefixMap {
1216 flag: flag.into(),
1217 from: PathBuf::from(from),
1218 to: to.into(),
1219 });
1220 return Ok(());
1221 }
1222 if value == "--param" {
1224 let parameter = self.take_value("--param", None)?;
1225 self.parsed
1226 .push(Argument::Plain(format!("--param={parameter}")));
1227 return Ok(());
1228 }
1229 if let Some(parameter) = value.strip_prefix("--param=") {
1230 self.parsed
1231 .push(Argument::Plain(format!("--param={parameter}")));
1232 return Ok(());
1233 }
1234 if let Some(rest) = value.strip_prefix("--sysroot=") {
1235 let path = PathBuf::from(rest);
1236 self.sysroot = Some(path.clone());
1237 self.parsed.push(Argument::Path {
1238 flag: "--sysroot".into(),
1239 path,
1240 });
1241 return Ok(());
1242 }
1243 if let Some(rest) = value
1244 .strip_prefix("-D")
1245 .or_else(|| value.strip_prefix("-U"))
1246 {
1247 let flag = &value[..2];
1248 let definition = self.take_value(flag, Some(rest))?;
1249 self.parsed
1250 .push(Argument::Plain(format!("{flag}{definition}")));
1251 return Ok(());
1252 }
1253 if let Some(rest) = value.strip_prefix("-x") {
1254 let language = self.take_value("-x", Some(rest))?;
1255 self.explicit_language = Some(match language.as_str() {
1256 "c" => CcLanguage::C,
1257 "c++" => CcLanguage::Cxx,
1258 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1259 });
1260 self.parsed.push(Argument::Plain(format!("-x{language}")));
1261 return Ok(());
1262 }
1263 if let Some(target) = value.strip_prefix("--target=") {
1264 self.parsed
1265 .push(Argument::Plain(format!("--target={target}")));
1266 return Ok(());
1267 }
1268 if value == "-target" {
1269 let target = self.take_value("-target", None)?;
1270 self.parsed
1271 .push(Argument::Plain(format!("--target={target}")));
1272 return Ok(());
1273 }
1274 if value == "-arch" {
1275 let arch = self.take_value("-arch", None)?;
1276 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1277 return Ok(());
1278 }
1279 if let Some(option) = value.strip_prefix("-f") {
1280 return self.parse_f_flag(value, option);
1281 }
1282 if let Some(option) = value.strip_prefix("-m") {
1283 return self.parse_m_flag(value, option);
1284 }
1285 if value.starts_with("-g") {
1286 return Err(if value.starts_with("-gsplit-dwarf") {
1289 CcBypassReason::SplitDebugOutput(value.into())
1290 } else {
1291 CcBypassReason::UnknownFlag(value.into())
1292 });
1293 }
1294 if value.starts_with("-W") {
1295 self.parsed.push(Argument::Plain(value.into()));
1299 return Ok(());
1300 }
1301 Err(CcBypassReason::UnknownFlag(value.into()))
1302 }
1303
1304 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1305 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1306 return Err(CcBypassReason::Plugin(value.into()));
1307 }
1308 if option.starts_with("profile-") || option == "test-coverage" {
1309 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1310 }
1311 let name = option.split_once('=').map_or(option, |(name, _)| name);
1312 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1313 return Err(CcBypassReason::UnknownFlag(value.into()));
1314 }
1315 self.parsed.push(Argument::Plain(value.into()));
1316 Ok(())
1317 }
1318
1319 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1320 if option == "llvm" {
1321 return Err(CcBypassReason::ToolPassthrough(value.into()));
1322 }
1323 if let Some((name, selection)) = option.split_once('=')
1328 && matches!(name, "arch" | "cpu" | "tune")
1329 && matches!(selection, "native" | "host")
1330 {
1331 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1332 }
1333 let name = option.split_once('=').map_or(option, |(name, _)| name);
1334 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1335 return Err(CcBypassReason::UnknownFlag(value.into()));
1336 }
1337 self.parsed.push(Argument::Plain(value.into()));
1338 Ok(())
1339 }
1340}
1341
1342#[cfg(test)]
1343#[path = "cc_cache_tests.rs"]
1344mod tests;