1#![deny(missing_docs)]
19
20use mbx_cache_core::{
21 CacheDigest, FileDigestCache, PathMapping, PathNormalizationError, canonical_json,
22 normalize_mapped_path,
23};
24use serde::{Deserialize, Serialize};
25use std::collections::{BTreeMap, BTreeSet};
26use std::ffi::OsString;
27use std::path::{Component, Path, PathBuf};
28use thiserror::Error;
29
30mod depfile;
31
32pub use depfile::{CcDepfile, CcDiscoveredInputs, INCLUDE_MANIFEST_PREFIX, manifest_snapshot};
33
34pub const ACTION_SCHEMA_VERSION: u8 = 1;
36pub const ADAPTER_VERSION: u8 = 1;
38
39pub const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
41pub const MAX_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
43pub const MAX_MANIFEST_ENTRIES: usize = 16 * 1024;
45
46pub const KEYED_ENVIRONMENT: &[&str] = &[
52 "IPHONEOS_DEPLOYMENT_TARGET",
53 "LANG",
54 "LC_ALL",
55 "LC_MESSAGES",
56 "MACOSX_DEPLOYMENT_TARGET",
57 "SDKROOT",
58 "SOURCE_DATE_EPOCH",
59 "TVOS_DEPLOYMENT_TARGET",
60 "WATCHOS_DEPLOYMENT_TARGET",
61 "XROS_DEPLOYMENT_TARGET",
62];
63
64pub const BYPASS_ENVIRONMENT: &[&str] = &[
70 "CPATH",
71 "COMPILER_PATH",
72 "CPLUS_INCLUDE_PATH",
73 "C_INCLUDE_PATH",
74 "DEPENDENCIES_OUTPUT",
75 "GCC_EXEC_PREFIX",
76 "OBJC_INCLUDE_PATH",
77 "SUNPRO_DEPENDENCIES",
78];
79
80pub const SYSTEM_ROOTS: &[&str] = &[
88 "/Applications/Xcode.app",
89 "/Library/Developer",
90 "/nix/store",
91 "/usr/include",
92 "/usr/lib",
93 "/usr/local/include",
94];
95
96const SUPPORTED_F_FLAGS: &[&str] = &[
97 "PIC",
98 "PIE",
99 "asynchronous-unwind-tables",
100 "color-diagnostics",
101 "data-sections",
102 "diagnostics-color",
103 "exceptions",
104 "function-sections",
105 "merge-all-constants",
106 "no-asynchronous-unwind-tables",
107 "no-builtin",
108 "no-common",
109 "no-exceptions",
110 "no-omit-frame-pointer",
111 "no-plt",
112 "no-rtti",
113 "no-strict-aliasing",
114 "omit-frame-pointer",
115 "pic",
116 "pie",
117 "rtti",
118 "short-enums",
119 "signed-char",
120 "stack-protector",
121 "stack-protector-all",
122 "stack-protector-strong",
123 "strict-aliasing",
124 "unsigned-char",
125 "visibility",
126 "visibility-inlines-hidden",
127 "wrapv",
128];
129
130const SUPPORTED_M_FLAGS: &[&str] = &[
131 "32",
132 "64",
133 "arch",
134 "arm",
135 "avx",
136 "avx2",
137 "cpu",
138 "float-abi",
139 "fma",
140 "fpu",
141 "iphoneos-version-min",
142 "macosx-version-min",
143 "no-omit-leaf-frame-pointer",
144 "omit-leaf-frame-pointer",
145 "sse",
146 "sse2",
147 "sse3",
148 "sse4.1",
149 "sse4.2",
150 "thumb",
151 "tune",
152];
153
154const SUPPORTED_O_FLAGS: &[&str] = &[
155 "-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Og", "-Os", "-Oz",
156];
157
158const SUPPORTED_G_FLAGS: &[&str] = &[
159 "-g",
160 "-g0",
161 "-g1",
162 "-g2",
163 "-g3",
164 "-gdwarf-2",
165 "-gdwarf-3",
166 "-gdwarf-4",
167 "-gdwarf-5",
168];
169
170const SUPPORTED_BARE_FLAGS: &[&str] = &[
171 "-ansi",
172 "-nostdinc",
173 "-nostdinc++",
174 "-pedantic",
175 "-pedantic-errors",
176 "-pipe",
177 "-pthread",
178 "-w",
179];
180
181const SEPARATE_PATH_FLAGS: &[&str] = &[
182 "-idirafter",
183 "-imacros",
184 "-include",
185 "-iquote",
186 "-isysroot",
187 "-isystem",
188];
189
190const TOOL_PASSTHROUGH_FLAGS: &[&str] = &["-Xassembler", "-Xclang", "-Xlinker", "-Xpreprocessor"];
191
192const SUPPORTED_ASSEMBLER_OPTIONS: &[&str] = &["--noexecstack"];
197
198const COMPILER_QUERY_FLAGS: &[&str] = &[
199 "--help",
200 "--version",
201 "-###",
202 "-?",
205 "-dumpmachine",
206 "-dumpversion",
207 "-v",
208];
209
210const PREFIX_MAP_FLAGS: &[&str] = &[
215 "-fdebug-prefix-map",
216 "-ffile-prefix-map",
217 "-fmacro-prefix-map",
218];
219
220impl CcBypassReason {
221 pub fn kind(&self) -> &'static str {
226 self.into()
227 }
228
229 pub fn remediation(&self) -> Option<&'static str> {
236 match self {
237 Self::UnsupportedEnvironment(_) => Some(
238 "Unset the reported environment variable for this build so the compiler invocation describes all of its inputs.",
239 ),
240 Self::LocalCpuTarget(_) => Some(
241 "Replace the reported local-CPU option with an explicit architecture or CPU name.",
242 ),
243 Self::EmbeddedTimestampMacro(_) => Some(
244 "Remove the reported timestamp macro, or keep this compilation uncached if its changing value is intentional.",
245 ),
246 Self::SearchPathModifiedDuringCompilation(_) => Some(
247 "Generate headers before compilation instead of changing an include directory while the compiler is running.",
248 ),
249 Self::UnknownFlag(_) | Self::ToolPassthrough(_) => Some(
250 "Upgrade mbx or report the unmodeled compiler option. If you control the build script, removing the option can also make the compilation cacheable.",
251 ),
252 Self::UnmappedAbsolutePath(_) => Some(
253 "Move the input under a mapped project or system root, or keep this compilation uncached.",
254 ),
255 _ => None,
256 }
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
265#[strum(serialize_all = "kebab-case")]
266#[non_exhaustive]
267pub enum CcBypassReason {
268 #[error("compiler argument {index} is not valid UTF-8")]
270 NonUtf8Argument {
271 index: usize,
273 },
274 #[error("compiler response file is not modeled by the cache adapter: {0}")]
276 ResponseFile(String),
277 #[error("compiler flag is not modeled by the cache adapter: {0}")]
279 UnknownFlag(String),
280 #[error("compiler flag {0} is missing its value")]
282 MissingValue(String),
283 #[error("compiler invocation queries the driver instead of compiling")]
285 CompilerQuery,
286 #[error("compiler invocation does not compile with -c")]
288 NotACompile,
289 #[error("compiler invocation emits a non-object output: {0}")]
291 NonObjectOutput(String),
292 #[error("compiler invocation reads its source from standard input")]
294 StandardInput,
295 #[error("compiler invocation names no source file")]
297 MissingInput,
298 #[error("compiler invocation names more than one source file")]
300 MultipleInputs,
301 #[error("compiler invocation names no output file")]
303 MissingOutput,
304 #[error("compiler input language is not modeled by the cache adapter: {0}")]
306 UnsupportedLanguage(String),
307 #[error("compiler invocation requests its own dependency output: {0}")]
309 CallerDependencyFlags(String),
310 #[error("precompiled headers are not modeled by the cache adapter: {0}")]
312 PrecompiledHeader(String),
313 #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
315 CoverageInstrumentation(String),
316 #[error("split debug output is not modeled by the cache adapter: {0}")]
318 SplitDebugOutput(String),
319 #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
321 SaveTemps(String),
322 #[error("compiler flag forwards options to another tool: {0}")]
324 ToolPassthrough(String),
325 #[error("compiler plugins are not modeled by the cache adapter: {0}")]
327 Plugin(String),
328 #[error("include search directory changed during the compilation: {0}")]
331 SearchPathModifiedDuringCompilation(PathBuf),
332
333 #[error("compilation output records a path its key normalized away: {0}")]
340 UnportableOutput(PathBuf),
341 #[error("compiler flag tunes for the local CPU: {0}")]
343 LocalCpuTarget(String),
344 #[error("compiler driver is not modeled by the cache adapter: {0}")]
346 UnsupportedCompilerDriver(String),
347 #[error("could not establish compiler identity: {0}")]
349 CompilerIdentityUnavailable(String),
350 #[error("environment variable {0} changes the compilation in an unmodeled way")]
352 UnsupportedEnvironment(String),
353 #[error("no real compiler was pinned for the cc shim")]
355 RealCompilerUnpinned,
356 #[error("input expands a timestamp macro: {0}")]
359 EmbeddedTimestampMacro(PathBuf),
360 #[error("could not model the compiler depfile: {0}")]
362 MalformedDepfile(String),
363 #[error("could not read the compiler depfile {path}: {message}")]
365 DepfileRead {
366 path: PathBuf,
368 message: String,
370 },
371 #[error("compilation reads more inputs than the cache adapter models")]
373 TooManyInputs,
374 #[error("path is outside every modeled root: {0}")]
376 UnmappedAbsolutePath(PathBuf),
377 #[error("path is not valid UTF-8: {0}")]
379 NonUtf8Path(PathBuf),
380 #[error("compiler working directory is not absolute: {0}")]
382 RelativeWorkingDirectory(PathBuf),
383 #[error("path mapping root is not absolute: {0}")]
385 RelativePathMapping(PathBuf),
386 #[error("invalid path mapping placeholder: {0}")]
388 InvalidPathPlaceholder(String),
389 #[error("required input is missing from the discovered inputs: {0}")]
391 MissingRequiredInput(String),
392 #[error("invalid digest for input: {0}")]
394 InvalidInputDigest(String),
395 #[error("conflicting digests for input: {0}")]
397 ConflictingInput(String),
398 #[error("could not read input {path}: {message}")]
400 InputRead {
401 path: PathBuf,
403 message: String,
405 },
406 #[error("input changed during the compilation: {0}")]
408 InputChanged(PathBuf),
409 #[error("input was modified during the compilation: {0}")]
411 InputModifiedDuringCompilation(PathBuf),
412 #[error("discovered inputs use a different working directory")]
414 DiscoveryWorkingDirectory,
415 #[error("action prediction is not modeled by this adapter version")]
417 UnsupportedPrediction,
418 #[error("invalid predicted input: {0}")]
420 InvalidPredictedInput(String),
421 #[error("could not serialize the action descriptor: {0}")]
423 Serialization(String),
424}
425
426impl From<PathNormalizationError> for CcBypassReason {
427 fn from(reason: PathNormalizationError) -> Self {
428 match reason {
429 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
430 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
431 }
432 }
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub enum CcLanguage {
438 C,
440 Cxx,
442}
443
444impl CcLanguage {
445 pub fn shim_stem(self) -> &'static str {
447 match self {
448 Self::C => "mbx-cc",
449 Self::Cxx => "mbx-cxx",
450 }
451 }
452
453 pub fn default_driver(self) -> &'static str {
455 if cfg!(windows) {
456 return "cl.exe";
457 }
458 match self {
459 Self::C => "cc",
460 Self::Cxx => "c++",
461 }
462 }
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum CcCompilerFamily {
468 Gcc,
470 Clang,
472 AppleClang,
474 #[cfg(windows)]
476 Msvc,
477}
478
479impl CcCompilerFamily {
480 pub fn as_str(self) -> &'static str {
482 match self {
483 Self::Gcc => "gcc",
484 Self::Clang => "clang",
485 Self::AppleClang => "apple-clang",
486 #[cfg(windows)]
487 Self::Msvc => "msvc",
488 }
489 }
490
491 pub fn uses_external_assembler(self) -> bool {
494 matches!(self, Self::Gcc)
495 }
496
497 pub fn is_msvc(self) -> bool {
499 #[cfg(windows)]
500 {
501 matches!(self, Self::Msvc)
502 }
503 #[cfg(not(windows))]
504 {
505 false
506 }
507 }
508
509 pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
511 #[cfg(windows)]
512 if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
513 return Ok(Self::Msvc);
514 }
515 if probe.contains("Apple clang version") {
516 Ok(Self::AppleClang)
517 } else if probe.contains("clang version") {
518 Ok(Self::Clang)
519 } else if probe.contains("gcc version") {
520 Ok(Self::Gcc)
521 } else {
522 Err(CcBypassReason::UnsupportedCompilerDriver(
523 probe.lines().next().unwrap_or_default().into(),
524 ))
525 }
526 }
527}
528
529#[derive(Debug, Clone, PartialEq, Eq)]
531pub struct CcCompilerIdentity {
532 pub family: CcCompilerFamily,
534 pub version_text: String,
536 pub target: String,
538 pub assembler: String,
544}
545
546#[derive(Debug, Clone, PartialEq, Eq)]
548pub struct CcActionInput {
549 pub path: PathBuf,
552 pub digest: CacheDigest,
554}
555
556#[derive(Debug, Clone, PartialEq, Eq)]
558pub struct CcActionContext {
559 pub compiler: CcCompilerIdentity,
561 pub working_dir: PathBuf,
563 pub path_mappings: Vec<PathMapping>,
565 pub environment: BTreeMap<String, Option<String>>,
567 pub inputs: Vec<CcActionInput>,
569}
570
571#[derive(Debug, Clone, PartialEq, Eq)]
573pub struct CcAction {
574 pub digest: CacheDigest,
576 pub bytes: Vec<u8>,
578}
579
580#[derive(Debug, Serialize)]
581struct CcCompilerDescriptor {
582 assembler: String,
583 family: String,
584 target: String,
585 version_text: String,
586}
587
588#[derive(Debug, Serialize)]
589struct CcInputDescriptor {
590 digest: CacheDigest,
591 path: String,
592}
593
594#[derive(Debug, Serialize)]
595struct CcActionDescriptor {
596 version: u8,
597 kind: &'static str,
598 adapter_version: u8,
599 compiler: CcCompilerDescriptor,
600 arguments: Vec<String>,
601 environment: BTreeMap<String, Option<String>>,
602 inputs: Vec<CcInputDescriptor>,
603}
604
605#[derive(Debug, Serialize)]
606struct CcInvocationDescriptor {
607 version: u8,
608 kind: &'static str,
609 adapter_version: u8,
610 compiler: CcCompilerDescriptor,
611 arguments: Vec<String>,
612 required_inputs: Vec<String>,
613}
614
615#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
618#[serde(deny_unknown_fields)]
619pub struct CcInputPrediction {
620 pub version: u8,
622 pub inputs: Vec<String>,
624 pub environment: Vec<String>,
626 #[serde(default, skip_serializing_if = "is_zero")]
629 pub compiler_duration_ns: u64,
630 #[serde(default, skip_serializing_if = "String::is_empty")]
632 pub source_name: String,
633}
634
635fn is_zero(value: &u64) -> bool {
636 *value == 0
637}
638
639#[derive(Debug, Clone, PartialEq, Eq)]
641enum Argument {
642 Plain(String),
644 Path { flag: String, path: PathBuf },
646 PrefixMap {
648 flag: String,
649 from: PathBuf,
650 to: String,
651 },
652 Source(PathBuf),
654}
655
656#[derive(Debug, Clone, PartialEq, Eq)]
658pub struct CcInvocation {
659 arguments: Vec<Argument>,
660 source: PathBuf,
661 output: PathBuf,
662 include_dirs: Vec<PathBuf>,
663 required_inputs: Vec<PathBuf>,
664 language: CcLanguage,
665 sysroot: Option<PathBuf>,
666}
667
668impl CcInvocation {
669 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
672 Parser::new(arguments).parse()
673 }
674
675 pub fn parse_for(
677 arguments: &[OsString],
678 family: CcCompilerFamily,
679 ) -> Result<Self, CcBypassReason> {
680 if family.is_msvc() {
681 MsvcParser::new(arguments).parse()
682 } else {
683 Self::parse(arguments)
684 }
685 }
686
687 pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
689 MsvcParser::new(arguments).parse()
690 }
691
692 pub fn source(&self) -> &Path {
694 &self.source
695 }
696
697 pub fn output(&self) -> &Path {
699 &self.output
700 }
701
702 pub fn include_dirs(&self) -> &[PathBuf] {
704 &self.include_dirs
705 }
706
707 pub fn required_inputs(&self) -> &[PathBuf] {
709 &self.required_inputs
710 }
711
712 pub fn language(&self) -> CcLanguage {
714 self.language
715 }
716
717 pub fn sysroot(&self) -> Option<&Path> {
719 self.sysroot.as_deref()
720 }
721
722 pub fn source_name(&self) -> String {
724 self.source
725 .file_name()
726 .map(|name| name.to_string_lossy().into_owned())
727 .unwrap_or_default()
728 }
729
730 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
737 vec!["-MD".into(), "-MF".into(), depfile.into()]
738 }
739
740 pub fn dependency_arguments_for(
743 &self,
744 depfile: &Path,
745 family: CcCompilerFamily,
746 ) -> Vec<OsString> {
747 if family.is_msvc() {
748 vec!["/sourceDependencies".into(), depfile.into()]
749 } else {
750 self.dependency_arguments(depfile)
751 }
752 }
753
754 pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
756 vec!["/sourceDependencies".into(), depfile.into()]
757 }
758
759 pub fn invocation_digest(
761 &self,
762 context: &CcActionContext,
763 ) -> Result<CacheDigest, CcBypassReason> {
764 let builder = ActionBuilder::new(self, context.clone());
765 let descriptor = builder.invocation_descriptor()?;
766 let bytes = canonical_json(&descriptor)
767 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
768 Ok(CacheDigest::blake3(&bytes))
769 }
770
771 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
774 ActionBuilder::new(self, context).build()
775 }
776
777 pub fn prediction(
780 &self,
781 context: &CcActionContext,
782 compiler_duration_ns: u64,
783 ) -> Result<CcInputPrediction, CcBypassReason> {
784 let builder = ActionBuilder::new(self, context.clone());
785 let mut inputs = context
786 .inputs
787 .iter()
788 .map(|input| builder.normalize_input_path(&input.path))
789 .collect::<Result<Vec<_>, _>>()?;
790 inputs.sort();
791 inputs.dedup();
792 Ok(CcInputPrediction {
793 version: 1,
794 inputs,
795 environment: context.environment.keys().cloned().collect(),
796 compiler_duration_ns,
797 source_name: self.source_name(),
798 })
799 }
800}
801
802impl CcInputPrediction {
803 pub fn discover(
806 &self,
807 working_dir: &Path,
808 path_mappings: &[PathMapping],
809 digests: &dyn FileDigestCache,
810 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
811 if self.version != 1 {
812 return Err(CcBypassReason::UnsupportedPrediction);
813 }
814 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
815 return Err(CcBypassReason::UnsupportedPrediction);
816 }
817 let mappings = PathMapping::ordered(path_mappings);
818 let mut files = BTreeSet::new();
819 let mut directories = BTreeSet::new();
820 for entry in &self.inputs {
821 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
822 Some(directory) => {
823 directories.insert(denormalize_path(directory, &mappings)?);
824 }
825 None => {
826 files.insert(denormalize_path(entry, &mappings)?);
827 }
828 }
829 }
830 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
831 }
832}
833
834fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
840 for mapping in mappings {
841 let prefix = format!("${{{}}}", mapping.placeholder);
842 let suffix = if value == prefix {
843 ""
844 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
845 suffix
846 } else {
847 continue;
848 };
849 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
850 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
851 }
852 let mut path = normalize_components(&mapping.root);
853 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
854 return Ok(path);
855 }
856 let path = PathBuf::from(value);
860 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
861 return Ok(path);
862 }
863 Err(CcBypassReason::InvalidPredictedInput(value.into()))
864}
865
866fn safe_suffix(suffix: &str) -> bool {
867 suffix.is_empty()
868 || !suffix.split('/').any(|component| {
869 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
870 })
871}
872
873pub fn is_system_path(path: &Path) -> bool {
875 SYSTEM_ROOTS
876 .iter()
877 .any(|root| path.starts_with(Path::new(root)))
878}
879
880fn normalize_components(path: &Path) -> PathBuf {
881 let mut normalized = PathBuf::new();
882 for component in path.components() {
883 match component {
884 Component::CurDir => {}
885 Component::ParentDir => {
886 normalized.pop();
887 }
888 component => normalized.push(component.as_os_str()),
889 }
890 }
891 normalized
892}
893
894pub fn environment_inputs<F>(
897 lookup: F,
898 sysroot: Option<&Path>,
899) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
900where
901 F: Fn(&str) -> Option<String>,
902{
903 environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
904}
905
906pub fn environment_inputs_for<F>(
908 lookup: F,
909 sysroot: Option<&Path>,
910 family: CcCompilerFamily,
911) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
912where
913 F: Fn(&str) -> Option<String>,
914{
915 for name in BYPASS_ENVIRONMENT {
916 if lookup(name).is_some() {
917 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
918 }
919 }
920 let mut environment = BTreeMap::new();
921 for name in KEYED_ENVIRONMENT {
922 if *name == "SDKROOT" && sysroot.is_some() {
925 continue;
926 }
927 environment.insert((*name).to_string(), lookup(name));
928 }
929 if family.is_msvc() {
930 for name in [
934 "INCLUDE",
935 "VCToolsVersion",
936 "WindowsSDKVersion",
937 "UCRTVersion",
938 ] {
939 environment.insert(name.into(), lookup(name));
940 }
941 for name in ["CL", "_CL_"] {
942 if lookup(name).is_some() {
943 return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
944 }
945 }
946 }
947 Ok(environment)
948}
949
950struct ActionBuilder<'a> {
951 invocation: &'a CcInvocation,
952 context: CcActionContext,
953 mappings: Vec<PathMapping>,
954}
955
956impl<'a> ActionBuilder<'a> {
957 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
958 context.path_mappings = PathMapping::ordered(&context.path_mappings);
959 let mappings = context.path_mappings.clone();
960 Self {
961 invocation,
962 context,
963 mappings,
964 }
965 }
966
967 fn build(self) -> Result<CcAction, CcBypassReason> {
968 self.validate_mappings()?;
969 let invocation = self.invocation_descriptor()?;
970
971 let mut inputs = BTreeMap::<String, CacheDigest>::new();
972 for input in &self.context.inputs {
973 input.digest.validate().map_err(|_| {
974 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
975 })?;
976 let path = self.normalize_input_path(&input.path)?;
977 if inputs
978 .insert(path.clone(), input.digest.clone())
979 .is_some_and(|existing| existing != input.digest)
980 {
981 return Err(CcBypassReason::ConflictingInput(path));
982 }
983 }
984 let required = self
985 .invocation
986 .required_inputs
987 .iter()
988 .map(|path| self.normalize_path(path))
989 .collect::<Result<BTreeSet<_>, _>>()?;
990 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
991 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
992 }
993 let inputs = inputs
994 .into_iter()
995 .map(|(path, digest)| CcInputDescriptor { path, digest })
996 .collect();
997 let descriptor = CcActionDescriptor {
998 version: ACTION_SCHEMA_VERSION,
999 kind: "cc",
1000 adapter_version: ADAPTER_VERSION,
1001 compiler: invocation.compiler,
1002 arguments: invocation.arguments,
1003 environment: self.context.environment.clone(),
1004 inputs,
1005 };
1006 let bytes = canonical_json(&descriptor)
1007 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1008 let digest = CacheDigest::blake3(&bytes);
1009 Ok(CcAction { digest, bytes })
1010 }
1011
1012 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1013 self.validate_mappings()?;
1014 let arguments = self
1015 .invocation
1016 .arguments
1017 .iter()
1018 .map(|argument| self.normalize_argument(argument))
1019 .collect::<Result<Vec<_>, _>>()?;
1020 let required_inputs = self
1021 .invocation
1022 .required_inputs
1023 .iter()
1024 .map(|path| self.normalize_path(path))
1025 .collect::<Result<BTreeSet<_>, _>>()?
1026 .into_iter()
1027 .collect();
1028 Ok(CcInvocationDescriptor {
1029 version: ACTION_SCHEMA_VERSION,
1030 kind: "cc",
1031 adapter_version: ADAPTER_VERSION,
1032 compiler: self.compiler_descriptor(),
1033 arguments,
1034 required_inputs,
1035 })
1036 }
1037
1038 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1039 CcCompilerDescriptor {
1040 assembler: self.context.compiler.assembler.clone(),
1041 family: self.context.compiler.family.as_str().into(),
1042 target: self.context.compiler.target.clone(),
1043 version_text: self.context.compiler.version_text.clone(),
1044 }
1045 }
1046
1047 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1048 if !self.context.working_dir.is_absolute() {
1049 return Err(CcBypassReason::RelativeWorkingDirectory(
1050 self.context.working_dir.clone(),
1051 ));
1052 }
1053 let mut roots = BTreeSet::new();
1054 let mut placeholders = BTreeSet::new();
1055 for mapping in &self.mappings {
1056 if !mapping.root.is_absolute() {
1057 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1058 }
1059 if mapping.placeholder.is_empty()
1060 || !mapping
1061 .placeholder
1062 .bytes()
1063 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1064 || !roots.insert(normalize_components(&mapping.root))
1065 || !placeholders.insert(&mapping.placeholder)
1066 {
1067 return Err(CcBypassReason::InvalidPathPlaceholder(
1068 mapping.placeholder.clone(),
1069 ));
1070 }
1071 }
1072 Ok(())
1073 }
1074
1075 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1076 match argument {
1077 Argument::Plain(value) => Ok(value.clone()),
1078 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1079 Argument::PrefixMap { flag, from, to } => {
1080 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1081 }
1082 Argument::Source(path) => Ok(self.normalize_path(path)?),
1083 }
1084 }
1085
1086 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1093 match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
1094 Ok(normalized) => Ok(normalized),
1095 Err(reason) => {
1096 let absolute = absolute_path(path, &self.context.working_dir);
1097 if is_system_path(&absolute) {
1098 return absolute
1099 .to_str()
1100 .map(ToOwned::to_owned)
1101 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1102 }
1103 Err(reason.into())
1104 }
1105 }
1106 }
1107
1108 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1109 match path.to_str().and_then(|path| {
1110 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1111 .map(ToOwned::to_owned)
1112 }) {
1113 Some(directory) => Ok(format!(
1114 "{INCLUDE_MANIFEST_PREFIX}{}",
1115 self.normalize_path(Path::new(&directory))?
1116 )),
1117 None => self.normalize_path(path),
1118 }
1119 }
1120}
1121
1122fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1123 if path.is_absolute() {
1124 normalize_components(path)
1125 } else {
1126 normalize_components(&working_dir.join(path))
1127 }
1128}
1129
1130struct Parser<'a> {
1131 arguments: &'a [OsString],
1132 index: usize,
1133 parsed: Vec<Argument>,
1134 source: Option<PathBuf>,
1135 output: Option<PathBuf>,
1136 include_dirs: Vec<PathBuf>,
1137 required_inputs: Vec<PathBuf>,
1138 sysroot: Option<PathBuf>,
1139 explicit_language: Option<CcLanguage>,
1140 compiling: bool,
1141}
1142
1143impl<'a> Parser<'a> {
1144 fn new(arguments: &'a [OsString]) -> Self {
1145 Self {
1146 arguments,
1147 index: 0,
1148 parsed: Vec::new(),
1149 source: None,
1150 output: None,
1151 include_dirs: Vec::new(),
1152 required_inputs: Vec::new(),
1153 sysroot: None,
1154 explicit_language: None,
1155 compiling: false,
1156 }
1157 }
1158
1159 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1160 while self.index < self.arguments.len() {
1161 let value = self.current()?.to_string();
1162 self.index += 1;
1163 if value == "-" {
1164 return Err(CcBypassReason::StandardInput);
1165 }
1166 if let Some(argfile) = value.strip_prefix('@') {
1167 return Err(CcBypassReason::ResponseFile(argfile.into()));
1168 }
1169 if value.starts_with('-') {
1170 self.parse_flag(&value)?;
1171 } else {
1172 self.parse_input(&value)?;
1173 }
1174 }
1175
1176 if !self.compiling {
1177 return Err(CcBypassReason::NotACompile);
1178 }
1179 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1180 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1181 let language = self.language(&source)?;
1182 self.required_inputs.push(source.clone());
1183 Ok(CcInvocation {
1184 arguments: self.parsed,
1185 source,
1186 output,
1187 include_dirs: self.include_dirs,
1188 required_inputs: self.required_inputs,
1189 language,
1190 sysroot: self.sysroot,
1191 })
1192 }
1193
1194 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1195 if let Some(language) = self.explicit_language {
1196 return Ok(language);
1197 }
1198 let extension = source
1199 .extension()
1200 .and_then(|extension| extension.to_str())
1201 .unwrap_or_default();
1202 match extension {
1203 "c" => Ok(CcLanguage::C),
1204 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1205 _ => Err(CcBypassReason::UnsupportedLanguage(
1206 source.display().to_string(),
1207 )),
1208 }
1209 }
1210
1211 fn current(&self) -> Result<&str, CcBypassReason> {
1212 self.arguments[self.index]
1213 .to_str()
1214 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1215 }
1216
1217 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1218 if let Some(value) = inline
1219 && !value.is_empty()
1220 {
1221 return Ok(value.into());
1222 }
1223 if self.index >= self.arguments.len() {
1224 return Err(CcBypassReason::MissingValue(flag.into()));
1225 }
1226 let value = self.current()?.to_string();
1227 self.index += 1;
1228 Ok(value)
1229 }
1230
1231 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1232 if self.source.is_some() {
1233 return Err(CcBypassReason::MultipleInputs);
1234 }
1235 let path = PathBuf::from(value);
1236 if self.explicit_language.is_none() {
1240 let extension = path
1241 .extension()
1242 .and_then(|extension| extension.to_str())
1243 .unwrap_or_default();
1244 if !matches!(extension, "c" | "cc" | "cpp" | "cxx" | "c++") {
1245 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1246 }
1247 }
1248 self.source = Some(path.clone());
1249 self.parsed.push(Argument::Source(path));
1250 Ok(())
1251 }
1252
1253 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1254 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1255 return Err(CcBypassReason::CompilerQuery);
1256 }
1257 if matches!(value, "-E" | "-S") {
1258 return Err(CcBypassReason::NonObjectOutput(value.into()));
1259 }
1260 if value.starts_with("-M") {
1261 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1262 }
1263 if value.starts_with("-save-temps") {
1264 return Err(CcBypassReason::SaveTemps(value.into()));
1265 }
1266 if value == "--coverage" {
1267 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1268 }
1269 if let Some(options) = value.strip_prefix("-Wa,")
1270 && !options.is_empty()
1271 && options
1272 .split(',')
1273 .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1274 {
1275 self.parsed.push(Argument::Plain(value.into()));
1276 return Ok(());
1277 }
1278 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1279 || value.starts_with("-Wp,")
1280 || value.starts_with("-Wa,")
1281 || value.starts_with("-Wl,")
1282 {
1283 return Err(CcBypassReason::ToolPassthrough(value.into()));
1286 }
1287 if value.starts_with("-include-pch") || value == "-emit-pch" {
1288 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1289 }
1290
1291 if value == "-c" {
1292 self.compiling = true;
1293 self.parsed.push(Argument::Plain(value.into()));
1294 return Ok(());
1295 }
1296 if SUPPORTED_BARE_FLAGS.contains(&value)
1297 || SUPPORTED_O_FLAGS.contains(&value)
1298 || SUPPORTED_G_FLAGS.contains(&value)
1299 || value.starts_with("-std=")
1300 {
1301 self.parsed.push(Argument::Plain(value.into()));
1302 return Ok(());
1303 }
1304 if let Some(rest) = value.strip_prefix("-o") {
1305 let path = self.take_value("-o", Some(rest))?;
1306 self.output = Some(PathBuf::from(&path));
1309 self.parsed.push(Argument::Path {
1310 flag: "-o".into(),
1311 path: PathBuf::from(path),
1312 });
1313 return Ok(());
1314 }
1315 if let Some(rest) = value.strip_prefix("-I") {
1316 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1317 self.include_dirs.push(path.clone());
1318 self.parsed.push(Argument::Path {
1319 flag: "-I".into(),
1320 path,
1321 });
1322 return Ok(());
1323 }
1324 if SEPARATE_PATH_FLAGS.contains(&value) {
1325 let path = PathBuf::from(self.take_value(value, None)?);
1326 match value {
1327 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1328 "-isysroot" => self.sysroot = Some(path.clone()),
1329 _ => {}
1335 }
1336 self.parsed.push(Argument::Path {
1337 flag: value.into(),
1338 path,
1339 });
1340 return Ok(());
1341 }
1342 if let Some(rest) = value.strip_prefix("--include=") {
1345 let path = PathBuf::from(rest);
1346 self.parsed.push(Argument::Path {
1347 flag: "-include".into(),
1348 path,
1349 });
1350 return Ok(());
1351 }
1352 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1353 value
1354 .strip_prefix(&format!("{flag}="))
1355 .map(|rest| (*flag, rest))
1356 }) {
1357 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1358 self.parsed.push(Argument::PrefixMap {
1359 flag: flag.into(),
1360 from: PathBuf::from(from),
1361 to: to.into(),
1362 });
1363 return Ok(());
1364 }
1365 if value == "--param" {
1367 let parameter = self.take_value("--param", None)?;
1368 self.parsed
1369 .push(Argument::Plain(format!("--param={parameter}")));
1370 return Ok(());
1371 }
1372 if let Some(parameter) = value.strip_prefix("--param=") {
1373 self.parsed
1374 .push(Argument::Plain(format!("--param={parameter}")));
1375 return Ok(());
1376 }
1377 if let Some(rest) = value.strip_prefix("--sysroot=") {
1378 let path = PathBuf::from(rest);
1379 self.sysroot = Some(path.clone());
1380 self.parsed.push(Argument::Path {
1381 flag: "--sysroot".into(),
1382 path,
1383 });
1384 return Ok(());
1385 }
1386 if let Some(rest) = value
1387 .strip_prefix("-D")
1388 .or_else(|| value.strip_prefix("-U"))
1389 {
1390 let flag = &value[..2];
1391 let definition = self.take_value(flag, Some(rest))?;
1392 self.parsed
1393 .push(Argument::Plain(format!("{flag}{definition}")));
1394 return Ok(());
1395 }
1396 if let Some(rest) = value.strip_prefix("-x") {
1397 let language = self.take_value("-x", Some(rest))?;
1398 self.explicit_language = Some(match language.as_str() {
1399 "c" => CcLanguage::C,
1400 "c++" => CcLanguage::Cxx,
1401 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1402 });
1403 self.parsed.push(Argument::Plain(format!("-x{language}")));
1404 return Ok(());
1405 }
1406 if let Some(target) = value.strip_prefix("--target=") {
1407 self.parsed
1408 .push(Argument::Plain(format!("--target={target}")));
1409 return Ok(());
1410 }
1411 if value == "-target" {
1412 let target = self.take_value("-target", None)?;
1413 self.parsed
1414 .push(Argument::Plain(format!("--target={target}")));
1415 return Ok(());
1416 }
1417 if value == "-arch" {
1418 let arch = self.take_value("-arch", None)?;
1419 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1420 return Ok(());
1421 }
1422 if let Some(option) = value.strip_prefix("-f") {
1423 return self.parse_f_flag(value, option);
1424 }
1425 if let Some(option) = value.strip_prefix("-m") {
1426 return self.parse_m_flag(value, option);
1427 }
1428 if value.starts_with("-g") {
1429 return Err(if value.starts_with("-gsplit-dwarf") {
1432 CcBypassReason::SplitDebugOutput(value.into())
1433 } else {
1434 CcBypassReason::UnknownFlag(value.into())
1435 });
1436 }
1437 if value.starts_with("-W") {
1438 self.parsed.push(Argument::Plain(value.into()));
1442 return Ok(());
1443 }
1444 Err(CcBypassReason::UnknownFlag(value.into()))
1445 }
1446
1447 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1448 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1449 return Err(CcBypassReason::Plugin(value.into()));
1450 }
1451 if option.starts_with("profile-") || option == "test-coverage" {
1452 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1453 }
1454 let name = option.split_once('=').map_or(option, |(name, _)| name);
1455 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1456 return Err(CcBypassReason::UnknownFlag(value.into()));
1457 }
1458 self.parsed.push(Argument::Plain(value.into()));
1459 Ok(())
1460 }
1461
1462 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1463 if option == "llvm" {
1464 return Err(CcBypassReason::ToolPassthrough(value.into()));
1465 }
1466 if let Some((name, selection)) = option.split_once('=')
1471 && matches!(name, "arch" | "cpu" | "tune")
1472 && matches!(selection, "native" | "host")
1473 {
1474 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1475 }
1476 let name = option.split_once('=').map_or(option, |(name, _)| name);
1477 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1478 return Err(CcBypassReason::UnknownFlag(value.into()));
1479 }
1480 self.parsed.push(Argument::Plain(value.into()));
1481 Ok(())
1482 }
1483}
1484
1485struct MsvcParser<'a> {
1489 arguments: &'a [OsString],
1490 index: usize,
1491 parsed: Vec<Argument>,
1492 source: Option<PathBuf>,
1493 output: Option<PathBuf>,
1494 include_dirs: Vec<PathBuf>,
1495 required_inputs: Vec<PathBuf>,
1496 explicit_language: Option<CcLanguage>,
1497 compiling: bool,
1498}
1499
1500impl<'a> MsvcParser<'a> {
1501 fn new(arguments: &'a [OsString]) -> Self {
1502 Self {
1503 arguments,
1504 index: 0,
1505 parsed: Vec::new(),
1506 source: None,
1507 output: None,
1508 include_dirs: Vec::new(),
1509 required_inputs: Vec::new(),
1510 explicit_language: None,
1511 compiling: false,
1512 }
1513 }
1514
1515 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1516 while self.index < self.arguments.len() {
1517 let value = self.current()?.to_owned();
1518 self.index += 1;
1519 if let Some(file) = value.strip_prefix('@') {
1520 return Err(CcBypassReason::ResponseFile(file.into()));
1521 }
1522 if value.starts_with('/') || value.starts_with('-') {
1523 self.parse_flag(&value)?;
1524 } else {
1525 self.add_source(&value)?;
1526 }
1527 }
1528 if !self.compiling {
1529 return Err(CcBypassReason::NotACompile);
1530 }
1531 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1532 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1533 let language = self.explicit_language.unwrap_or_else(|| {
1534 if source
1535 .extension()
1536 .and_then(|value| value.to_str())
1537 .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1538 {
1539 CcLanguage::C
1540 } else {
1541 CcLanguage::Cxx
1542 }
1543 });
1544 self.required_inputs.push(source.clone());
1545 Ok(CcInvocation {
1546 arguments: self.parsed,
1547 source,
1548 output,
1549 include_dirs: self.include_dirs,
1550 required_inputs: self.required_inputs,
1551 language,
1552 sysroot: None,
1553 })
1554 }
1555
1556 fn current(&self) -> Result<&str, CcBypassReason> {
1557 self.arguments[self.index]
1558 .to_str()
1559 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1560 }
1561
1562 fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
1563 if !attached.is_empty() {
1564 return Ok(attached.into());
1565 }
1566 if self.index == self.arguments.len() {
1567 return Err(CcBypassReason::MissingValue(flag.into()));
1568 }
1569 let value = self.current()?.to_owned();
1570 self.index += 1;
1571 Ok(value)
1572 }
1573
1574 fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
1575 if self.source.is_some() {
1576 return Err(CcBypassReason::MultipleInputs);
1577 }
1578 let path = PathBuf::from(value);
1579 if self.explicit_language.is_none()
1580 && !path
1581 .extension()
1582 .and_then(|value| value.to_str())
1583 .is_some_and(|value| {
1584 matches!(
1585 value.to_ascii_lowercase().as_str(),
1586 "c" | "cc" | "cpp" | "cxx"
1587 )
1588 })
1589 {
1590 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1591 }
1592 self.source = Some(path.clone());
1593 self.parsed.push(Argument::Source(path));
1594 Ok(())
1595 }
1596
1597 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1598 let option = value.trim_start_matches(['/', '-']);
1599 let lower = option.to_ascii_lowercase();
1600 if matches!(lower.as_str(), "?" | "help") {
1601 return Err(CcBypassReason::CompilerQuery);
1602 }
1603 if lower == "showincludes" || lower.starts_with("sourcedependencies") {
1604 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1605 }
1606 if matches!(lower.as_str(), "e" | "ep" | "p") {
1607 return Err(CcBypassReason::NonObjectOutput(value.into()));
1608 }
1609 if (lower.starts_with("fa") && !lower.starts_with("favor:"))
1610 || lower.starts_with("fd")
1611 || lower.starts_with("zi")
1612 {
1613 return Err(CcBypassReason::SplitDebugOutput(value.into()));
1614 }
1615 if lower.starts_with("yc")
1616 || lower.starts_with("yu")
1617 || (lower.starts_with("fp") && !lower.starts_with("fp:"))
1618 {
1619 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1620 }
1621 if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
1622 return Err(CcBypassReason::ToolPassthrough(value.into()));
1623 }
1624 if matches!(lower.as_str(), "ld" | "ldd") {
1625 return Err(CcBypassReason::NotACompile);
1626 }
1627 if lower == "c" {
1628 self.compiling = true;
1629 self.parsed.push(Argument::Plain("/c".into()));
1630 return Ok(());
1631 }
1632 for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
1633 if let Some(attached) = option.strip_prefix(prefix) {
1634 let path = PathBuf::from(self.value(canonical, attached)?);
1635 if prefix == "Fo" {
1636 self.output = Some(path.clone());
1637 } else if prefix == "I" {
1638 self.include_dirs.push(path.clone());
1639 }
1640 self.parsed.push(Argument::Path {
1641 flag: canonical.into(),
1642 path,
1643 });
1644 return Ok(());
1645 }
1646 }
1647 if lower.starts_with("external:i") {
1648 let path = PathBuf::from(self.value("/external:I", &option[10..])?);
1649 self.include_dirs.push(path.clone());
1650 self.parsed.push(Argument::Path {
1651 flag: "/external:I".into(),
1652 path,
1653 });
1654 return Ok(());
1655 }
1656 if option.starts_with("Tc") || option.starts_with("Tp") {
1657 let c = option.starts_with("Tc");
1658 let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
1659 self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
1660 return self.add_source(&path);
1661 }
1662 if lower.starts_with("pathmap:") {
1663 let rest = &option[8..];
1664 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1665 self.parsed.push(Argument::PrefixMap {
1666 flag: "/pathmap".into(),
1667 from: PathBuf::from(from),
1668 to: to.into(),
1669 });
1670 return Ok(());
1671 }
1672 if matches!(option, "D" | "U") {
1673 let definition = self.value(value, "")?;
1674 self.parsed
1675 .push(Argument::Plain(format!("/{option}{definition}")));
1676 return Ok(());
1677 }
1678 let definition = option.starts_with('D') || option.starts_with('U');
1681 let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
1682 || lower
1683 .strip_prefix('w')
1684 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1685 || ["wd", "we", "wo"].iter().any(|prefix| {
1686 lower
1687 .strip_prefix(prefix)
1688 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1689 });
1690 let admitted = definition
1691 || warning
1692 || lower.starts_with("std:")
1693 || lower.starts_with("arch:")
1694 || lower.starts_with("favor:")
1695 || lower.starts_with("volatile:")
1696 || lower.starts_with("fp:")
1697 || lower.starts_with("eh")
1698 || lower.starts_with('o')
1699 || lower.starts_with("ob")
1700 || lower.starts_with("oi")
1701 || lower.starts_with("ot")
1702 || lower.starts_with("oy")
1703 || lower.starts_with("gs")
1704 || lower.starts_with("gr")
1705 || lower.starts_with("gy")
1706 || lower.starts_with("gw")
1707 || lower.starts_with("gl")
1708 || lower.starts_with("zc:")
1709 || lower.starts_with("diagnostics:")
1710 || matches!(
1711 lower.as_str(),
1712 "nologo"
1713 | "brepro"
1714 | "bigobj"
1715 | "utf-8"
1716 | "permissive-"
1717 | "z7"
1718 | "md"
1719 | "mdd"
1720 | "mt"
1721 | "mtd"
1722 );
1723 if admitted {
1724 self.parsed.push(Argument::Plain(value.into()));
1725 return Ok(());
1726 }
1727 Err(CcBypassReason::UnknownFlag(value.into()))
1728 }
1729}
1730
1731#[cfg(test)]
1732#[path = "cc_cache_tests.rs"]
1733mod tests;