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
295 #[error("compilation output records a path its key normalized away: {0}")]
302 UnportableOutput(PathBuf),
303 #[error("compiler flag tunes for the local CPU: {0}")]
305 LocalCpuTarget(String),
306 #[error("compiler driver is not modeled by the cache adapter: {0}")]
308 UnsupportedCompilerDriver(String),
309 #[error("could not establish compiler identity: {0}")]
311 CompilerIdentityUnavailable(String),
312 #[error("environment variable {0} changes the compilation in an unmodeled way")]
314 UnsupportedEnvironment(String),
315 #[error("no real compiler was pinned for the cc shim")]
317 RealCompilerUnpinned,
318 #[error("input expands a timestamp macro: {0}")]
321 EmbeddedTimestampMacro(PathBuf),
322 #[error("could not model the compiler depfile: {0}")]
324 MalformedDepfile(String),
325 #[error("could not read the compiler depfile {path}: {message}")]
327 DepfileRead {
328 path: PathBuf,
330 message: String,
332 },
333 #[error("compilation reads more inputs than the cache adapter models")]
335 TooManyInputs,
336 #[error("path is outside every modeled root: {0}")]
338 UnmappedAbsolutePath(PathBuf),
339 #[error("path is not valid UTF-8: {0}")]
341 NonUtf8Path(PathBuf),
342 #[error("compiler working directory is not absolute: {0}")]
344 RelativeWorkingDirectory(PathBuf),
345 #[error("path mapping root is not absolute: {0}")]
347 RelativePathMapping(PathBuf),
348 #[error("invalid path mapping placeholder: {0}")]
350 InvalidPathPlaceholder(String),
351 #[error("required input is missing from the discovered inputs: {0}")]
353 MissingRequiredInput(String),
354 #[error("invalid digest for input: {0}")]
356 InvalidInputDigest(String),
357 #[error("conflicting digests for input: {0}")]
359 ConflictingInput(String),
360 #[error("could not read input {path}: {message}")]
362 InputRead {
363 path: PathBuf,
365 message: String,
367 },
368 #[error("input changed during the compilation: {0}")]
370 InputChanged(PathBuf),
371 #[error("input was modified during the compilation: {0}")]
373 InputModifiedDuringCompilation(PathBuf),
374 #[error("discovered inputs use a different working directory")]
376 DiscoveryWorkingDirectory,
377 #[error("action prediction is not modeled by this adapter version")]
379 UnsupportedPrediction,
380 #[error("invalid predicted input: {0}")]
382 InvalidPredictedInput(String),
383 #[error("could not serialize the action descriptor: {0}")]
385 Serialization(String),
386}
387
388impl From<RustcBypassReason> for CcBypassReason {
389 fn from(reason: RustcBypassReason) -> Self {
392 match reason {
393 RustcBypassReason::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
394 RustcBypassReason::NonUtf8Path(path) => Self::NonUtf8Path(path),
395 other => Self::UnknownFlag(other.kind().into()),
396 }
397 }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402pub enum CcLanguage {
403 C,
405 Cxx,
407}
408
409impl CcLanguage {
410 pub fn shim_stem(self) -> &'static str {
412 match self {
413 Self::C => "mbx-cc",
414 Self::Cxx => "mbx-cxx",
415 }
416 }
417
418 pub fn default_driver(self) -> &'static str {
420 match self {
421 Self::C => "cc",
422 Self::Cxx => "c++",
423 }
424 }
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429pub enum CcCompilerFamily {
430 Gcc,
432 Clang,
434 AppleClang,
436}
437
438impl CcCompilerFamily {
439 pub fn as_str(self) -> &'static str {
441 match self {
442 Self::Gcc => "gcc",
443 Self::Clang => "clang",
444 Self::AppleClang => "apple-clang",
445 }
446 }
447
448 pub fn uses_external_assembler(self) -> bool {
451 matches!(self, Self::Gcc)
452 }
453
454 pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
456 if probe.contains("Apple clang version") {
457 Ok(Self::AppleClang)
458 } else if probe.contains("clang version") {
459 Ok(Self::Clang)
460 } else if probe.contains("gcc version") {
461 Ok(Self::Gcc)
462 } else {
463 Err(CcBypassReason::UnsupportedCompilerDriver(
464 probe.lines().next().unwrap_or_default().into(),
465 ))
466 }
467 }
468}
469
470#[derive(Debug, Clone, PartialEq, Eq)]
472pub struct CcCompilerIdentity {
473 pub family: CcCompilerFamily,
475 pub version_text: String,
477 pub target: String,
479 pub assembler: String,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq)]
489pub struct CcActionInput {
490 pub path: PathBuf,
493 pub digest: CacheDigest,
495}
496
497#[derive(Debug, Clone, PartialEq, Eq)]
499pub struct CcActionContext {
500 pub compiler: CcCompilerIdentity,
502 pub working_dir: PathBuf,
504 pub path_mappings: Vec<PathMapping>,
506 pub environment: BTreeMap<String, Option<String>>,
508 pub inputs: Vec<CcActionInput>,
510}
511
512#[derive(Debug, Clone, PartialEq, Eq)]
514pub struct CcAction {
515 pub digest: CacheDigest,
517 pub bytes: Vec<u8>,
519}
520
521#[derive(Debug, Serialize)]
522struct CcCompilerDescriptor {
523 assembler: String,
524 family: String,
525 target: String,
526 version_text: String,
527}
528
529#[derive(Debug, Serialize)]
530struct CcInputDescriptor {
531 digest: CacheDigest,
532 path: String,
533}
534
535#[derive(Debug, Serialize)]
536struct CcActionDescriptor {
537 version: u8,
538 kind: &'static str,
539 adapter_version: u8,
540 compiler: CcCompilerDescriptor,
541 arguments: Vec<String>,
542 environment: BTreeMap<String, Option<String>>,
543 inputs: Vec<CcInputDescriptor>,
544}
545
546#[derive(Debug, Serialize)]
547struct CcInvocationDescriptor {
548 version: u8,
549 kind: &'static str,
550 adapter_version: u8,
551 compiler: CcCompilerDescriptor,
552 arguments: Vec<String>,
553 required_inputs: Vec<String>,
554}
555
556#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
559#[serde(deny_unknown_fields)]
560pub struct CcInputPrediction {
561 pub version: u8,
563 pub inputs: Vec<String>,
565 pub environment: Vec<String>,
567 #[serde(default, skip_serializing_if = "is_zero")]
570 pub compiler_duration_ns: u64,
571 #[serde(default, skip_serializing_if = "String::is_empty")]
573 pub source_name: String,
574}
575
576fn is_zero(value: &u64) -> bool {
577 *value == 0
578}
579
580#[derive(Debug, Clone, PartialEq, Eq)]
582enum Argument {
583 Plain(String),
585 Path { flag: String, path: PathBuf },
587 PrefixMap {
589 flag: String,
590 from: PathBuf,
591 to: String,
592 },
593 Source(PathBuf),
595}
596
597#[derive(Debug, Clone, PartialEq, Eq)]
599pub struct CcInvocation {
600 arguments: Vec<Argument>,
601 source: PathBuf,
602 output: PathBuf,
603 include_dirs: Vec<PathBuf>,
604 required_inputs: Vec<PathBuf>,
605 language: CcLanguage,
606 sysroot: Option<PathBuf>,
607}
608
609impl CcInvocation {
610 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
613 Parser::new(arguments).parse()
614 }
615
616 pub fn source(&self) -> &Path {
618 &self.source
619 }
620
621 pub fn output(&self) -> &Path {
623 &self.output
624 }
625
626 pub fn include_dirs(&self) -> &[PathBuf] {
628 &self.include_dirs
629 }
630
631 pub fn required_inputs(&self) -> &[PathBuf] {
633 &self.required_inputs
634 }
635
636 pub fn language(&self) -> CcLanguage {
638 self.language
639 }
640
641 pub fn sysroot(&self) -> Option<&Path> {
643 self.sysroot.as_deref()
644 }
645
646 pub fn source_name(&self) -> String {
648 self.source
649 .file_name()
650 .map(|name| name.to_string_lossy().into_owned())
651 .unwrap_or_default()
652 }
653
654 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
661 vec!["-MD".into(), "-MF".into(), depfile.into()]
662 }
663
664 pub fn invocation_digest(
666 &self,
667 context: &CcActionContext,
668 ) -> Result<CacheDigest, CcBypassReason> {
669 let builder = ActionBuilder::new(self, context.clone());
670 let descriptor = builder.invocation_descriptor()?;
671 let bytes = canonical_json(&descriptor)
672 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
673 Ok(CacheDigest::blake3(&bytes))
674 }
675
676 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
679 ActionBuilder::new(self, context).build()
680 }
681
682 pub fn prediction(
685 &self,
686 context: &CcActionContext,
687 compiler_duration_ns: u64,
688 ) -> Result<CcInputPrediction, CcBypassReason> {
689 let builder = ActionBuilder::new(self, context.clone());
690 let mut inputs = context
691 .inputs
692 .iter()
693 .map(|input| builder.normalize_input_path(&input.path))
694 .collect::<Result<Vec<_>, _>>()?;
695 inputs.sort();
696 inputs.dedup();
697 Ok(CcInputPrediction {
698 version: 1,
699 inputs,
700 environment: context.environment.keys().cloned().collect(),
701 compiler_duration_ns,
702 source_name: self.source_name(),
703 })
704 }
705}
706
707impl CcInputPrediction {
708 pub fn discover(
711 &self,
712 working_dir: &Path,
713 path_mappings: &[PathMapping],
714 digests: &dyn FileDigestCache,
715 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
716 if self.version != 1 {
717 return Err(CcBypassReason::UnsupportedPrediction);
718 }
719 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
720 return Err(CcBypassReason::UnsupportedPrediction);
721 }
722 let mappings = PathMapping::ordered(path_mappings);
723 let mut files = BTreeSet::new();
724 let mut directories = BTreeSet::new();
725 for entry in &self.inputs {
726 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
727 Some(directory) => {
728 directories.insert(denormalize_path(directory, &mappings)?);
729 }
730 None => {
731 files.insert(denormalize_path(entry, &mappings)?);
732 }
733 }
734 }
735 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
736 }
737}
738
739fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
745 for mapping in mappings {
746 let prefix = format!("${{{}}}", mapping.placeholder);
747 let suffix = if value == prefix {
748 ""
749 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
750 suffix
751 } else {
752 continue;
753 };
754 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
755 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
756 }
757 let mut path = normalize_components(&mapping.root);
758 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
759 return Ok(path);
760 }
761 let path = PathBuf::from(value);
765 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
766 return Ok(path);
767 }
768 Err(CcBypassReason::InvalidPredictedInput(value.into()))
769}
770
771fn safe_suffix(suffix: &str) -> bool {
772 suffix.is_empty()
773 || !suffix.split('/').any(|component| {
774 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
775 })
776}
777
778pub fn is_system_path(path: &Path) -> bool {
780 SYSTEM_ROOTS
781 .iter()
782 .any(|root| path.starts_with(Path::new(root)))
783}
784
785fn normalize_components(path: &Path) -> PathBuf {
786 let mut normalized = PathBuf::new();
787 for component in path.components() {
788 match component {
789 Component::CurDir => {}
790 Component::ParentDir => {
791 normalized.pop();
792 }
793 component => normalized.push(component.as_os_str()),
794 }
795 }
796 normalized
797}
798
799pub fn environment_inputs<F>(
802 lookup: F,
803 sysroot: Option<&Path>,
804) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
805where
806 F: Fn(&str) -> Option<String>,
807{
808 for name in BYPASS_ENVIRONMENT {
809 if lookup(name).is_some() {
810 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
811 }
812 }
813 let mut environment = BTreeMap::new();
814 for name in KEYED_ENVIRONMENT {
815 if *name == "SDKROOT" && sysroot.is_some() {
818 continue;
819 }
820 environment.insert((*name).to_string(), lookup(name));
821 }
822 Ok(environment)
823}
824
825struct ActionBuilder<'a> {
826 invocation: &'a CcInvocation,
827 context: CcActionContext,
828 mappings: Vec<PathMapping>,
829}
830
831impl<'a> ActionBuilder<'a> {
832 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
833 context.path_mappings = PathMapping::ordered(&context.path_mappings);
834 let mappings = context.path_mappings.clone();
835 Self {
836 invocation,
837 context,
838 mappings,
839 }
840 }
841
842 fn build(self) -> Result<CcAction, CcBypassReason> {
843 self.validate_mappings()?;
844 let invocation = self.invocation_descriptor()?;
845
846 let mut inputs = BTreeMap::<String, CacheDigest>::new();
847 for input in &self.context.inputs {
848 input.digest.validate().map_err(|_| {
849 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
850 })?;
851 let path = self.normalize_input_path(&input.path)?;
852 if inputs
853 .insert(path.clone(), input.digest.clone())
854 .is_some_and(|existing| existing != input.digest)
855 {
856 return Err(CcBypassReason::ConflictingInput(path));
857 }
858 }
859 let required = self
860 .invocation
861 .required_inputs
862 .iter()
863 .map(|path| self.normalize_path(path))
864 .collect::<Result<BTreeSet<_>, _>>()?;
865 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
866 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
867 }
868 let inputs = inputs
869 .into_iter()
870 .map(|(path, digest)| CcInputDescriptor { path, digest })
871 .collect();
872 let descriptor = CcActionDescriptor {
873 version: ACTION_SCHEMA_VERSION,
874 kind: "cc",
875 adapter_version: ADAPTER_VERSION,
876 compiler: invocation.compiler,
877 arguments: invocation.arguments,
878 environment: self.context.environment.clone(),
879 inputs,
880 };
881 let bytes = canonical_json(&descriptor)
882 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
883 let digest = CacheDigest::blake3(&bytes);
884 Ok(CcAction { digest, bytes })
885 }
886
887 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
888 self.validate_mappings()?;
889 let arguments = self
890 .invocation
891 .arguments
892 .iter()
893 .map(|argument| self.normalize_argument(argument))
894 .collect::<Result<Vec<_>, _>>()?;
895 let required_inputs = self
896 .invocation
897 .required_inputs
898 .iter()
899 .map(|path| self.normalize_path(path))
900 .collect::<Result<BTreeSet<_>, _>>()?
901 .into_iter()
902 .collect();
903 Ok(CcInvocationDescriptor {
904 version: ACTION_SCHEMA_VERSION,
905 kind: "cc",
906 adapter_version: ADAPTER_VERSION,
907 compiler: self.compiler_descriptor(),
908 arguments,
909 required_inputs,
910 })
911 }
912
913 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
914 CcCompilerDescriptor {
915 assembler: self.context.compiler.assembler.clone(),
916 family: self.context.compiler.family.as_str().into(),
917 target: self.context.compiler.target.clone(),
918 version_text: self.context.compiler.version_text.clone(),
919 }
920 }
921
922 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
923 if !self.context.working_dir.is_absolute() {
924 return Err(CcBypassReason::RelativeWorkingDirectory(
925 self.context.working_dir.clone(),
926 ));
927 }
928 let mut roots = BTreeSet::new();
929 let mut placeholders = BTreeSet::new();
930 for mapping in &self.mappings {
931 if !mapping.root.is_absolute() {
932 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
933 }
934 if mapping.placeholder.is_empty()
935 || !mapping
936 .placeholder
937 .bytes()
938 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
939 || !roots.insert(normalize_components(&mapping.root))
940 || !placeholders.insert(&mapping.placeholder)
941 {
942 return Err(CcBypassReason::InvalidPathPlaceholder(
943 mapping.placeholder.clone(),
944 ));
945 }
946 }
947 Ok(())
948 }
949
950 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
951 match argument {
952 Argument::Plain(value) => Ok(value.clone()),
953 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
954 Argument::PrefixMap { flag, from, to } => {
955 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
956 }
957 Argument::Source(path) => Ok(self.normalize_path(path)?),
958 }
959 }
960
961 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
968 match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
969 Ok(normalized) => Ok(normalized),
970 Err(reason) => {
971 let absolute = absolute_path(path, &self.context.working_dir);
972 if is_system_path(&absolute) {
973 return absolute
974 .to_str()
975 .map(ToOwned::to_owned)
976 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
977 }
978 Err(reason.into())
979 }
980 }
981 }
982
983 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
984 match path.to_str().and_then(|path| {
985 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
986 .map(ToOwned::to_owned)
987 }) {
988 Some(directory) => Ok(format!(
989 "{INCLUDE_MANIFEST_PREFIX}{}",
990 self.normalize_path(Path::new(&directory))?
991 )),
992 None => self.normalize_path(path),
993 }
994 }
995}
996
997fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
998 if path.is_absolute() {
999 normalize_components(path)
1000 } else {
1001 normalize_components(&working_dir.join(path))
1002 }
1003}
1004
1005struct Parser<'a> {
1006 arguments: &'a [OsString],
1007 index: usize,
1008 parsed: Vec<Argument>,
1009 source: Option<PathBuf>,
1010 output: Option<PathBuf>,
1011 include_dirs: Vec<PathBuf>,
1012 required_inputs: Vec<PathBuf>,
1013 sysroot: Option<PathBuf>,
1014 explicit_language: Option<CcLanguage>,
1015 compiling: bool,
1016}
1017
1018impl<'a> Parser<'a> {
1019 fn new(arguments: &'a [OsString]) -> Self {
1020 Self {
1021 arguments,
1022 index: 0,
1023 parsed: Vec::new(),
1024 source: None,
1025 output: None,
1026 include_dirs: Vec::new(),
1027 required_inputs: Vec::new(),
1028 sysroot: None,
1029 explicit_language: None,
1030 compiling: false,
1031 }
1032 }
1033
1034 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1035 while self.index < self.arguments.len() {
1036 let value = self.current()?.to_string();
1037 self.index += 1;
1038 if value == "-" {
1039 return Err(CcBypassReason::StandardInput);
1040 }
1041 if let Some(argfile) = value.strip_prefix('@') {
1042 return Err(CcBypassReason::ResponseFile(argfile.into()));
1043 }
1044 if value.starts_with('-') {
1045 self.parse_flag(&value)?;
1046 } else {
1047 self.parse_input(&value)?;
1048 }
1049 }
1050
1051 if !self.compiling {
1052 return Err(CcBypassReason::NotACompile);
1053 }
1054 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1055 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1056 let language = self.language(&source)?;
1057 self.required_inputs.push(source.clone());
1058 Ok(CcInvocation {
1059 arguments: self.parsed,
1060 source,
1061 output,
1062 include_dirs: self.include_dirs,
1063 required_inputs: self.required_inputs,
1064 language,
1065 sysroot: self.sysroot,
1066 })
1067 }
1068
1069 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1070 if let Some(language) = self.explicit_language {
1071 return Ok(language);
1072 }
1073 let extension = source
1074 .extension()
1075 .and_then(|extension| extension.to_str())
1076 .unwrap_or_default();
1077 match extension {
1078 "c" => Ok(CcLanguage::C),
1079 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1080 _ => Err(CcBypassReason::UnsupportedLanguage(
1081 source.display().to_string(),
1082 )),
1083 }
1084 }
1085
1086 fn current(&self) -> Result<&str, CcBypassReason> {
1087 self.arguments[self.index]
1088 .to_str()
1089 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1090 }
1091
1092 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1093 if let Some(value) = inline
1094 && !value.is_empty()
1095 {
1096 return Ok(value.into());
1097 }
1098 if self.index >= self.arguments.len() {
1099 return Err(CcBypassReason::MissingValue(flag.into()));
1100 }
1101 let value = self.current()?.to_string();
1102 self.index += 1;
1103 Ok(value)
1104 }
1105
1106 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1107 if self.source.is_some() {
1108 return Err(CcBypassReason::MultipleInputs);
1109 }
1110 let path = PathBuf::from(value);
1111 if self.explicit_language.is_none() {
1115 let extension = path
1116 .extension()
1117 .and_then(|extension| extension.to_str())
1118 .unwrap_or_default();
1119 if !matches!(extension, "c" | "cc" | "cpp" | "cxx" | "c++") {
1120 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1121 }
1122 }
1123 self.source = Some(path.clone());
1124 self.parsed.push(Argument::Source(path));
1125 Ok(())
1126 }
1127
1128 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1129 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1130 return Err(CcBypassReason::CompilerQuery);
1131 }
1132 if matches!(value, "-E" | "-S") {
1133 return Err(CcBypassReason::NonObjectOutput(value.into()));
1134 }
1135 if value.starts_with("-M") {
1136 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1137 }
1138 if value.starts_with("-save-temps") {
1139 return Err(CcBypassReason::SaveTemps(value.into()));
1140 }
1141 if value == "--coverage" {
1142 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1143 }
1144 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1145 || value.starts_with("-Wp,")
1146 || value.starts_with("-Wa,")
1147 || value.starts_with("-Wl,")
1148 {
1149 return Err(CcBypassReason::ToolPassthrough(value.into()));
1152 }
1153 if value.starts_with("-include-pch") || value == "-emit-pch" {
1154 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1155 }
1156
1157 if value == "-c" {
1158 self.compiling = true;
1159 self.parsed.push(Argument::Plain(value.into()));
1160 return Ok(());
1161 }
1162 if SUPPORTED_BARE_FLAGS.contains(&value)
1163 || SUPPORTED_O_FLAGS.contains(&value)
1164 || SUPPORTED_G_FLAGS.contains(&value)
1165 || value.starts_with("-std=")
1166 {
1167 self.parsed.push(Argument::Plain(value.into()));
1168 return Ok(());
1169 }
1170 if let Some(rest) = value.strip_prefix("-o") {
1171 let path = self.take_value("-o", Some(rest))?;
1172 self.output = Some(PathBuf::from(&path));
1175 self.parsed.push(Argument::Path {
1176 flag: "-o".into(),
1177 path: PathBuf::from(path),
1178 });
1179 return Ok(());
1180 }
1181 if let Some(rest) = value.strip_prefix("-I") {
1182 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1183 self.include_dirs.push(path.clone());
1184 self.parsed.push(Argument::Path {
1185 flag: "-I".into(),
1186 path,
1187 });
1188 return Ok(());
1189 }
1190 if SEPARATE_PATH_FLAGS.contains(&value) {
1191 let path = PathBuf::from(self.take_value(value, None)?);
1192 match value {
1193 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1194 "-isysroot" => self.sysroot = Some(path.clone()),
1195 _ => {}
1201 }
1202 self.parsed.push(Argument::Path {
1203 flag: value.into(),
1204 path,
1205 });
1206 return Ok(());
1207 }
1208 if let Some(rest) = value.strip_prefix("--include=") {
1211 let path = PathBuf::from(rest);
1212 self.parsed.push(Argument::Path {
1213 flag: "-include".into(),
1214 path,
1215 });
1216 return Ok(());
1217 }
1218 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1219 value
1220 .strip_prefix(&format!("{flag}="))
1221 .map(|rest| (*flag, rest))
1222 }) {
1223 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1224 self.parsed.push(Argument::PrefixMap {
1225 flag: flag.into(),
1226 from: PathBuf::from(from),
1227 to: to.into(),
1228 });
1229 return Ok(());
1230 }
1231 if value == "--param" {
1233 let parameter = self.take_value("--param", None)?;
1234 self.parsed
1235 .push(Argument::Plain(format!("--param={parameter}")));
1236 return Ok(());
1237 }
1238 if let Some(parameter) = value.strip_prefix("--param=") {
1239 self.parsed
1240 .push(Argument::Plain(format!("--param={parameter}")));
1241 return Ok(());
1242 }
1243 if let Some(rest) = value.strip_prefix("--sysroot=") {
1244 let path = PathBuf::from(rest);
1245 self.sysroot = Some(path.clone());
1246 self.parsed.push(Argument::Path {
1247 flag: "--sysroot".into(),
1248 path,
1249 });
1250 return Ok(());
1251 }
1252 if let Some(rest) = value
1253 .strip_prefix("-D")
1254 .or_else(|| value.strip_prefix("-U"))
1255 {
1256 let flag = &value[..2];
1257 let definition = self.take_value(flag, Some(rest))?;
1258 self.parsed
1259 .push(Argument::Plain(format!("{flag}{definition}")));
1260 return Ok(());
1261 }
1262 if let Some(rest) = value.strip_prefix("-x") {
1263 let language = self.take_value("-x", Some(rest))?;
1264 self.explicit_language = Some(match language.as_str() {
1265 "c" => CcLanguage::C,
1266 "c++" => CcLanguage::Cxx,
1267 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1268 });
1269 self.parsed.push(Argument::Plain(format!("-x{language}")));
1270 return Ok(());
1271 }
1272 if let Some(target) = value.strip_prefix("--target=") {
1273 self.parsed
1274 .push(Argument::Plain(format!("--target={target}")));
1275 return Ok(());
1276 }
1277 if value == "-target" {
1278 let target = self.take_value("-target", None)?;
1279 self.parsed
1280 .push(Argument::Plain(format!("--target={target}")));
1281 return Ok(());
1282 }
1283 if value == "-arch" {
1284 let arch = self.take_value("-arch", None)?;
1285 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1286 return Ok(());
1287 }
1288 if let Some(option) = value.strip_prefix("-f") {
1289 return self.parse_f_flag(value, option);
1290 }
1291 if let Some(option) = value.strip_prefix("-m") {
1292 return self.parse_m_flag(value, option);
1293 }
1294 if value.starts_with("-g") {
1295 return Err(if value.starts_with("-gsplit-dwarf") {
1298 CcBypassReason::SplitDebugOutput(value.into())
1299 } else {
1300 CcBypassReason::UnknownFlag(value.into())
1301 });
1302 }
1303 if value.starts_with("-W") {
1304 self.parsed.push(Argument::Plain(value.into()));
1308 return Ok(());
1309 }
1310 Err(CcBypassReason::UnknownFlag(value.into()))
1311 }
1312
1313 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1314 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1315 return Err(CcBypassReason::Plugin(value.into()));
1316 }
1317 if option.starts_with("profile-") || option == "test-coverage" {
1318 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1319 }
1320 let name = option.split_once('=').map_or(option, |(name, _)| name);
1321 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1322 return Err(CcBypassReason::UnknownFlag(value.into()));
1323 }
1324 self.parsed.push(Argument::Plain(value.into()));
1325 Ok(())
1326 }
1327
1328 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1329 if option == "llvm" {
1330 return Err(CcBypassReason::ToolPassthrough(value.into()));
1331 }
1332 if let Some((name, selection)) = option.split_once('=')
1337 && matches!(name, "arch" | "cpu" | "tune")
1338 && matches!(selection, "native" | "host")
1339 {
1340 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1341 }
1342 let name = option.split_once('=').map_or(option, |(name, _)| name);
1343 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1344 return Err(CcBypassReason::UnknownFlag(value.into()));
1345 }
1346 self.parsed.push(Argument::Plain(value.into()));
1347 Ok(())
1348 }
1349}
1350
1351#[cfg(test)]
1352#[path = "cc_cache_tests.rs"]
1353mod tests;