1#![deny(missing_docs)]
19
20use mbx_cache_core::{CacheDigest, 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 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
706 if self.version != 1 {
707 return Err(CcBypassReason::UnsupportedPrediction);
708 }
709 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
710 return Err(CcBypassReason::UnsupportedPrediction);
711 }
712 let mappings = PathMapping::ordered(path_mappings);
713 let mut files = BTreeSet::new();
714 let mut directories = BTreeSet::new();
715 for entry in &self.inputs {
716 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
717 Some(directory) => {
718 directories.insert(denormalize_path(directory, &mappings)?);
719 }
720 None => {
721 files.insert(denormalize_path(entry, &mappings)?);
722 }
723 }
724 }
725 CcDiscoveredInputs::collect(working_dir, files, directories)
726 }
727}
728
729fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
735 for mapping in mappings {
736 let prefix = format!("${{{}}}", mapping.placeholder);
737 let suffix = if value == prefix {
738 ""
739 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
740 suffix
741 } else {
742 continue;
743 };
744 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
745 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
746 }
747 let mut path = normalize_components(&mapping.root);
748 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
749 return Ok(path);
750 }
751 let path = PathBuf::from(value);
755 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
756 return Ok(path);
757 }
758 Err(CcBypassReason::InvalidPredictedInput(value.into()))
759}
760
761fn safe_suffix(suffix: &str) -> bool {
762 suffix.is_empty()
763 || !suffix.split('/').any(|component| {
764 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
765 })
766}
767
768pub fn is_system_path(path: &Path) -> bool {
770 SYSTEM_ROOTS
771 .iter()
772 .any(|root| path.starts_with(Path::new(root)))
773}
774
775fn normalize_components(path: &Path) -> PathBuf {
776 let mut normalized = PathBuf::new();
777 for component in path.components() {
778 match component {
779 Component::CurDir => {}
780 Component::ParentDir => {
781 normalized.pop();
782 }
783 component => normalized.push(component.as_os_str()),
784 }
785 }
786 normalized
787}
788
789pub fn environment_inputs<F>(
792 lookup: F,
793 sysroot: Option<&Path>,
794) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
795where
796 F: Fn(&str) -> Option<String>,
797{
798 for name in BYPASS_ENVIRONMENT {
799 if lookup(name).is_some() {
800 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
801 }
802 }
803 let mut environment = BTreeMap::new();
804 for name in KEYED_ENVIRONMENT {
805 if *name == "SDKROOT" && sysroot.is_some() {
808 continue;
809 }
810 environment.insert((*name).to_string(), lookup(name));
811 }
812 Ok(environment)
813}
814
815struct ActionBuilder<'a> {
816 invocation: &'a CcInvocation,
817 context: CcActionContext,
818 mappings: Vec<PathMapping>,
819}
820
821impl<'a> ActionBuilder<'a> {
822 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
823 context.path_mappings = PathMapping::ordered(&context.path_mappings);
824 let mappings = context.path_mappings.clone();
825 Self {
826 invocation,
827 context,
828 mappings,
829 }
830 }
831
832 fn build(self) -> Result<CcAction, CcBypassReason> {
833 self.validate_mappings()?;
834 let invocation = self.invocation_descriptor()?;
835
836 let mut inputs = BTreeMap::<String, CacheDigest>::new();
837 for input in &self.context.inputs {
838 input.digest.validate().map_err(|_| {
839 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
840 })?;
841 let path = self.normalize_input_path(&input.path)?;
842 if inputs
843 .insert(path.clone(), input.digest.clone())
844 .is_some_and(|existing| existing != input.digest)
845 {
846 return Err(CcBypassReason::ConflictingInput(path));
847 }
848 }
849 let required = self
850 .invocation
851 .required_inputs
852 .iter()
853 .map(|path| self.normalize_path(path))
854 .collect::<Result<BTreeSet<_>, _>>()?;
855 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
856 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
857 }
858 let inputs = inputs
859 .into_iter()
860 .map(|(path, digest)| CcInputDescriptor { path, digest })
861 .collect();
862 let descriptor = CcActionDescriptor {
863 version: ACTION_SCHEMA_VERSION,
864 kind: "cc",
865 adapter_version: ADAPTER_VERSION,
866 compiler: invocation.compiler,
867 arguments: invocation.arguments,
868 environment: self.context.environment.clone(),
869 inputs,
870 };
871 let bytes = canonical_json(&descriptor)
872 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
873 let digest = CacheDigest::blake3(&bytes);
874 Ok(CcAction { digest, bytes })
875 }
876
877 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
878 self.validate_mappings()?;
879 let arguments = self
880 .invocation
881 .arguments
882 .iter()
883 .map(|argument| self.normalize_argument(argument))
884 .collect::<Result<Vec<_>, _>>()?;
885 let required_inputs = self
886 .invocation
887 .required_inputs
888 .iter()
889 .map(|path| self.normalize_path(path))
890 .collect::<Result<BTreeSet<_>, _>>()?
891 .into_iter()
892 .collect();
893 Ok(CcInvocationDescriptor {
894 version: ACTION_SCHEMA_VERSION,
895 kind: "cc",
896 adapter_version: ADAPTER_VERSION,
897 compiler: self.compiler_descriptor(),
898 arguments,
899 required_inputs,
900 })
901 }
902
903 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
904 CcCompilerDescriptor {
905 assembler: self.context.compiler.assembler.clone(),
906 family: self.context.compiler.family.as_str().into(),
907 target: self.context.compiler.target.clone(),
908 version_text: self.context.compiler.version_text.clone(),
909 }
910 }
911
912 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
913 if !self.context.working_dir.is_absolute() {
914 return Err(CcBypassReason::RelativeWorkingDirectory(
915 self.context.working_dir.clone(),
916 ));
917 }
918 let mut roots = BTreeSet::new();
919 let mut placeholders = BTreeSet::new();
920 for mapping in &self.mappings {
921 if !mapping.root.is_absolute() {
922 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
923 }
924 if mapping.placeholder.is_empty()
925 || !mapping
926 .placeholder
927 .bytes()
928 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
929 || !roots.insert(normalize_components(&mapping.root))
930 || !placeholders.insert(&mapping.placeholder)
931 {
932 return Err(CcBypassReason::InvalidPathPlaceholder(
933 mapping.placeholder.clone(),
934 ));
935 }
936 }
937 Ok(())
938 }
939
940 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
941 match argument {
942 Argument::Plain(value) => Ok(value.clone()),
943 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
944 Argument::PrefixMap { flag, from, to } => {
945 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
946 }
947 Argument::Source(path) => Ok(self.normalize_path(path)?),
948 }
949 }
950
951 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
958 match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
959 Ok(normalized) => Ok(normalized),
960 Err(reason) => {
961 let absolute = absolute_path(path, &self.context.working_dir);
962 if is_system_path(&absolute) {
963 return absolute
964 .to_str()
965 .map(ToOwned::to_owned)
966 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
967 }
968 Err(reason.into())
969 }
970 }
971 }
972
973 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
974 match path.to_str().and_then(|path| {
975 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
976 .map(ToOwned::to_owned)
977 }) {
978 Some(directory) => Ok(format!(
979 "{INCLUDE_MANIFEST_PREFIX}{}",
980 self.normalize_path(Path::new(&directory))?
981 )),
982 None => self.normalize_path(path),
983 }
984 }
985}
986
987fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
988 if path.is_absolute() {
989 normalize_components(path)
990 } else {
991 normalize_components(&working_dir.join(path))
992 }
993}
994
995struct Parser<'a> {
996 arguments: &'a [OsString],
997 index: usize,
998 parsed: Vec<Argument>,
999 source: Option<PathBuf>,
1000 output: Option<PathBuf>,
1001 include_dirs: Vec<PathBuf>,
1002 required_inputs: Vec<PathBuf>,
1003 sysroot: Option<PathBuf>,
1004 explicit_language: Option<CcLanguage>,
1005 compiling: bool,
1006}
1007
1008impl<'a> Parser<'a> {
1009 fn new(arguments: &'a [OsString]) -> Self {
1010 Self {
1011 arguments,
1012 index: 0,
1013 parsed: Vec::new(),
1014 source: None,
1015 output: None,
1016 include_dirs: Vec::new(),
1017 required_inputs: Vec::new(),
1018 sysroot: None,
1019 explicit_language: None,
1020 compiling: false,
1021 }
1022 }
1023
1024 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1025 while self.index < self.arguments.len() {
1026 let value = self.current()?.to_string();
1027 self.index += 1;
1028 if value == "-" {
1029 return Err(CcBypassReason::StandardInput);
1030 }
1031 if let Some(argfile) = value.strip_prefix('@') {
1032 return Err(CcBypassReason::ResponseFile(argfile.into()));
1033 }
1034 if value.starts_with('-') {
1035 self.parse_flag(&value)?;
1036 } else {
1037 self.parse_input(&value)?;
1038 }
1039 }
1040
1041 if !self.compiling {
1042 return Err(CcBypassReason::NotACompile);
1043 }
1044 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1045 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1046 let language = self.language(&source)?;
1047 self.required_inputs.push(source.clone());
1048 Ok(CcInvocation {
1049 arguments: self.parsed,
1050 source,
1051 output,
1052 include_dirs: self.include_dirs,
1053 required_inputs: self.required_inputs,
1054 language,
1055 sysroot: self.sysroot,
1056 })
1057 }
1058
1059 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1060 if let Some(language) = self.explicit_language {
1061 return Ok(language);
1062 }
1063 let extension = source
1064 .extension()
1065 .and_then(|extension| extension.to_str())
1066 .unwrap_or_default();
1067 match extension {
1068 "c" => Ok(CcLanguage::C),
1069 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1070 _ => Err(CcBypassReason::UnsupportedLanguage(
1071 source.display().to_string(),
1072 )),
1073 }
1074 }
1075
1076 fn current(&self) -> Result<&str, CcBypassReason> {
1077 self.arguments[self.index]
1078 .to_str()
1079 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1080 }
1081
1082 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1083 if let Some(value) = inline
1084 && !value.is_empty()
1085 {
1086 return Ok(value.into());
1087 }
1088 if self.index >= self.arguments.len() {
1089 return Err(CcBypassReason::MissingValue(flag.into()));
1090 }
1091 let value = self.current()?.to_string();
1092 self.index += 1;
1093 Ok(value)
1094 }
1095
1096 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1097 if self.source.is_some() {
1098 return Err(CcBypassReason::MultipleInputs);
1099 }
1100 let path = PathBuf::from(value);
1101 if self.explicit_language.is_none() {
1105 let extension = path
1106 .extension()
1107 .and_then(|extension| extension.to_str())
1108 .unwrap_or_default();
1109 if !matches!(extension, "c" | "cc" | "cpp" | "cxx" | "c++") {
1110 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1111 }
1112 }
1113 self.source = Some(path.clone());
1114 self.parsed.push(Argument::Source(path));
1115 Ok(())
1116 }
1117
1118 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1119 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1120 return Err(CcBypassReason::CompilerQuery);
1121 }
1122 if matches!(value, "-E" | "-S") {
1123 return Err(CcBypassReason::NonObjectOutput(value.into()));
1124 }
1125 if value.starts_with("-M") {
1126 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1127 }
1128 if value.starts_with("-save-temps") {
1129 return Err(CcBypassReason::SaveTemps(value.into()));
1130 }
1131 if value == "--coverage" {
1132 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1133 }
1134 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1135 || value.starts_with("-Wp,")
1136 || value.starts_with("-Wa,")
1137 || value.starts_with("-Wl,")
1138 {
1139 return Err(CcBypassReason::ToolPassthrough(value.into()));
1142 }
1143 if value.starts_with("-include-pch") || value == "-emit-pch" {
1144 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1145 }
1146
1147 if value == "-c" {
1148 self.compiling = true;
1149 self.parsed.push(Argument::Plain(value.into()));
1150 return Ok(());
1151 }
1152 if SUPPORTED_BARE_FLAGS.contains(&value)
1153 || SUPPORTED_O_FLAGS.contains(&value)
1154 || SUPPORTED_G_FLAGS.contains(&value)
1155 || value.starts_with("-std=")
1156 {
1157 self.parsed.push(Argument::Plain(value.into()));
1158 return Ok(());
1159 }
1160 if let Some(rest) = value.strip_prefix("-o") {
1161 let path = self.take_value("-o", Some(rest))?;
1162 self.output = Some(PathBuf::from(&path));
1165 self.parsed.push(Argument::Path {
1166 flag: "-o".into(),
1167 path: PathBuf::from(path),
1168 });
1169 return Ok(());
1170 }
1171 if let Some(rest) = value.strip_prefix("-I") {
1172 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1173 self.include_dirs.push(path.clone());
1174 self.parsed.push(Argument::Path {
1175 flag: "-I".into(),
1176 path,
1177 });
1178 return Ok(());
1179 }
1180 if SEPARATE_PATH_FLAGS.contains(&value) {
1181 let path = PathBuf::from(self.take_value(value, None)?);
1182 match value {
1183 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1184 "-isysroot" => self.sysroot = Some(path.clone()),
1185 _ => {}
1191 }
1192 self.parsed.push(Argument::Path {
1193 flag: value.into(),
1194 path,
1195 });
1196 return Ok(());
1197 }
1198 if let Some(rest) = value.strip_prefix("--include=") {
1201 let path = PathBuf::from(rest);
1202 self.parsed.push(Argument::Path {
1203 flag: "-include".into(),
1204 path,
1205 });
1206 return Ok(());
1207 }
1208 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1209 value
1210 .strip_prefix(&format!("{flag}="))
1211 .map(|rest| (*flag, rest))
1212 }) {
1213 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1214 self.parsed.push(Argument::PrefixMap {
1215 flag: flag.into(),
1216 from: PathBuf::from(from),
1217 to: to.into(),
1218 });
1219 return Ok(());
1220 }
1221 if value == "--param" {
1223 let parameter = self.take_value("--param", None)?;
1224 self.parsed
1225 .push(Argument::Plain(format!("--param={parameter}")));
1226 return Ok(());
1227 }
1228 if let Some(parameter) = value.strip_prefix("--param=") {
1229 self.parsed
1230 .push(Argument::Plain(format!("--param={parameter}")));
1231 return Ok(());
1232 }
1233 if let Some(rest) = value.strip_prefix("--sysroot=") {
1234 let path = PathBuf::from(rest);
1235 self.sysroot = Some(path.clone());
1236 self.parsed.push(Argument::Path {
1237 flag: "--sysroot".into(),
1238 path,
1239 });
1240 return Ok(());
1241 }
1242 if let Some(rest) = value
1243 .strip_prefix("-D")
1244 .or_else(|| value.strip_prefix("-U"))
1245 {
1246 let flag = &value[..2];
1247 let definition = self.take_value(flag, Some(rest))?;
1248 self.parsed
1249 .push(Argument::Plain(format!("{flag}{definition}")));
1250 return Ok(());
1251 }
1252 if let Some(rest) = value.strip_prefix("-x") {
1253 let language = self.take_value("-x", Some(rest))?;
1254 self.explicit_language = Some(match language.as_str() {
1255 "c" => CcLanguage::C,
1256 "c++" => CcLanguage::Cxx,
1257 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1258 });
1259 self.parsed.push(Argument::Plain(format!("-x{language}")));
1260 return Ok(());
1261 }
1262 if let Some(target) = value.strip_prefix("--target=") {
1263 self.parsed
1264 .push(Argument::Plain(format!("--target={target}")));
1265 return Ok(());
1266 }
1267 if value == "-target" {
1268 let target = self.take_value("-target", None)?;
1269 self.parsed
1270 .push(Argument::Plain(format!("--target={target}")));
1271 return Ok(());
1272 }
1273 if value == "-arch" {
1274 let arch = self.take_value("-arch", None)?;
1275 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1276 return Ok(());
1277 }
1278 if let Some(option) = value.strip_prefix("-f") {
1279 return self.parse_f_flag(value, option);
1280 }
1281 if let Some(option) = value.strip_prefix("-m") {
1282 return self.parse_m_flag(value, option);
1283 }
1284 if value.starts_with("-g") {
1285 return Err(if value.starts_with("-gsplit-dwarf") {
1288 CcBypassReason::SplitDebugOutput(value.into())
1289 } else {
1290 CcBypassReason::UnknownFlag(value.into())
1291 });
1292 }
1293 if value.starts_with("-W") {
1294 self.parsed.push(Argument::Plain(value.into()));
1298 return Ok(());
1299 }
1300 Err(CcBypassReason::UnknownFlag(value.into()))
1301 }
1302
1303 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1304 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1305 return Err(CcBypassReason::Plugin(value.into()));
1306 }
1307 if option.starts_with("profile-") || option == "test-coverage" {
1308 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1309 }
1310 let name = option.split_once('=').map_or(option, |(name, _)| name);
1311 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1312 return Err(CcBypassReason::UnknownFlag(value.into()));
1313 }
1314 self.parsed.push(Argument::Plain(value.into()));
1315 Ok(())
1316 }
1317
1318 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1319 if option == "llvm" {
1320 return Err(CcBypassReason::ToolPassthrough(value.into()));
1321 }
1322 if let Some((name, selection)) = option.split_once('=')
1327 && matches!(name, "arch" | "cpu" | "tune")
1328 && matches!(selection, "native" | "host")
1329 {
1330 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1331 }
1332 let name = option.split_once('=').map_or(option, |(name, _)| name);
1333 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1334 return Err(CcBypassReason::UnknownFlag(value.into()));
1335 }
1336 self.parsed.push(Argument::Plain(value.into()));
1337 Ok(())
1338 }
1339}
1340
1341#[cfg(test)]
1342#[path = "cc_cache_tests.rs"]
1343mod tests;