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 COMPILER_QUERY_FLAGS: &[&str] = &[
193 "--help",
194 "--version",
195 "-###",
196 "-?",
199 "-dumpmachine",
200 "-dumpversion",
201 "-v",
202];
203
204const PREFIX_MAP_FLAGS: &[&str] = &[
209 "-fdebug-prefix-map",
210 "-ffile-prefix-map",
211 "-fmacro-prefix-map",
212];
213
214impl CcBypassReason {
215 pub fn kind(&self) -> &'static str {
220 self.into()
221 }
222
223 pub fn remediation(&self) -> Option<&'static str> {
230 match self {
231 Self::UnsupportedEnvironment(_) => Some(
232 "Unset the reported environment variable for this build so the compiler invocation describes all of its inputs.",
233 ),
234 Self::LocalCpuTarget(_) => Some(
235 "Replace the reported local-CPU option with an explicit architecture or CPU name.",
236 ),
237 Self::EmbeddedTimestampMacro(_) => Some(
238 "Remove the reported timestamp macro, or keep this compilation uncached if its changing value is intentional.",
239 ),
240 Self::SearchPathModifiedDuringCompilation(_) => Some(
241 "Generate headers before compilation instead of changing an include directory while the compiler is running.",
242 ),
243 Self::UnknownFlag(_) | Self::ToolPassthrough(_) => Some(
244 "Remove the reported compiler option, or upgrade mbx if the option should be modeled.",
245 ),
246 Self::UnmappedAbsolutePath(_) => Some(
247 "Move the input under a mapped project or system root, or keep this compilation uncached.",
248 ),
249 _ => None,
250 }
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
259#[strum(serialize_all = "kebab-case")]
260#[non_exhaustive]
261pub enum CcBypassReason {
262 #[error("compiler argument {index} is not valid UTF-8")]
264 NonUtf8Argument {
265 index: usize,
267 },
268 #[error("compiler response file is not modeled by the cache adapter: {0}")]
270 ResponseFile(String),
271 #[error("compiler flag is not modeled by the cache adapter: {0}")]
273 UnknownFlag(String),
274 #[error("compiler flag {0} is missing its value")]
276 MissingValue(String),
277 #[error("compiler invocation queries the driver instead of compiling")]
279 CompilerQuery,
280 #[error("compiler invocation does not compile with -c")]
282 NotACompile,
283 #[error("compiler invocation emits a non-object output: {0}")]
285 NonObjectOutput(String),
286 #[error("compiler invocation reads its source from standard input")]
288 StandardInput,
289 #[error("compiler invocation names no source file")]
291 MissingInput,
292 #[error("compiler invocation names more than one source file")]
294 MultipleInputs,
295 #[error("compiler invocation names no output file")]
297 MissingOutput,
298 #[error("compiler input language is not modeled by the cache adapter: {0}")]
300 UnsupportedLanguage(String),
301 #[error("compiler invocation requests its own dependency output: {0}")]
303 CallerDependencyFlags(String),
304 #[error("precompiled headers are not modeled by the cache adapter: {0}")]
306 PrecompiledHeader(String),
307 #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
309 CoverageInstrumentation(String),
310 #[error("split debug output is not modeled by the cache adapter: {0}")]
312 SplitDebugOutput(String),
313 #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
315 SaveTemps(String),
316 #[error("compiler flag forwards options to another tool: {0}")]
318 ToolPassthrough(String),
319 #[error("compiler plugins are not modeled by the cache adapter: {0}")]
321 Plugin(String),
322 #[error("include search directory changed during the compilation: {0}")]
325 SearchPathModifiedDuringCompilation(PathBuf),
326
327 #[error("compilation output records a path its key normalized away: {0}")]
334 UnportableOutput(PathBuf),
335 #[error("compiler flag tunes for the local CPU: {0}")]
337 LocalCpuTarget(String),
338 #[error("compiler driver is not modeled by the cache adapter: {0}")]
340 UnsupportedCompilerDriver(String),
341 #[error("could not establish compiler identity: {0}")]
343 CompilerIdentityUnavailable(String),
344 #[error("environment variable {0} changes the compilation in an unmodeled way")]
346 UnsupportedEnvironment(String),
347 #[error("no real compiler was pinned for the cc shim")]
349 RealCompilerUnpinned,
350 #[error("input expands a timestamp macro: {0}")]
353 EmbeddedTimestampMacro(PathBuf),
354 #[error("could not model the compiler depfile: {0}")]
356 MalformedDepfile(String),
357 #[error("could not read the compiler depfile {path}: {message}")]
359 DepfileRead {
360 path: PathBuf,
362 message: String,
364 },
365 #[error("compilation reads more inputs than the cache adapter models")]
367 TooManyInputs,
368 #[error("path is outside every modeled root: {0}")]
370 UnmappedAbsolutePath(PathBuf),
371 #[error("path is not valid UTF-8: {0}")]
373 NonUtf8Path(PathBuf),
374 #[error("compiler working directory is not absolute: {0}")]
376 RelativeWorkingDirectory(PathBuf),
377 #[error("path mapping root is not absolute: {0}")]
379 RelativePathMapping(PathBuf),
380 #[error("invalid path mapping placeholder: {0}")]
382 InvalidPathPlaceholder(String),
383 #[error("required input is missing from the discovered inputs: {0}")]
385 MissingRequiredInput(String),
386 #[error("invalid digest for input: {0}")]
388 InvalidInputDigest(String),
389 #[error("conflicting digests for input: {0}")]
391 ConflictingInput(String),
392 #[error("could not read input {path}: {message}")]
394 InputRead {
395 path: PathBuf,
397 message: String,
399 },
400 #[error("input changed during the compilation: {0}")]
402 InputChanged(PathBuf),
403 #[error("input was modified during the compilation: {0}")]
405 InputModifiedDuringCompilation(PathBuf),
406 #[error("discovered inputs use a different working directory")]
408 DiscoveryWorkingDirectory,
409 #[error("action prediction is not modeled by this adapter version")]
411 UnsupportedPrediction,
412 #[error("invalid predicted input: {0}")]
414 InvalidPredictedInput(String),
415 #[error("could not serialize the action descriptor: {0}")]
417 Serialization(String),
418}
419
420impl From<PathNormalizationError> for CcBypassReason {
421 fn from(reason: PathNormalizationError) -> Self {
422 match reason {
423 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
424 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
425 }
426 }
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
431pub enum CcLanguage {
432 C,
434 Cxx,
436}
437
438impl CcLanguage {
439 pub fn shim_stem(self) -> &'static str {
441 match self {
442 Self::C => "mbx-cc",
443 Self::Cxx => "mbx-cxx",
444 }
445 }
446
447 pub fn default_driver(self) -> &'static str {
449 if cfg!(windows) {
450 return "cl.exe";
451 }
452 match self {
453 Self::C => "cc",
454 Self::Cxx => "c++",
455 }
456 }
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461pub enum CcCompilerFamily {
462 Gcc,
464 Clang,
466 AppleClang,
468 #[cfg(windows)]
470 Msvc,
471}
472
473impl CcCompilerFamily {
474 pub fn as_str(self) -> &'static str {
476 match self {
477 Self::Gcc => "gcc",
478 Self::Clang => "clang",
479 Self::AppleClang => "apple-clang",
480 #[cfg(windows)]
481 Self::Msvc => "msvc",
482 }
483 }
484
485 pub fn uses_external_assembler(self) -> bool {
488 matches!(self, Self::Gcc)
489 }
490
491 pub fn is_msvc(self) -> bool {
493 #[cfg(windows)]
494 {
495 matches!(self, Self::Msvc)
496 }
497 #[cfg(not(windows))]
498 {
499 false
500 }
501 }
502
503 pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
505 #[cfg(windows)]
506 if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
507 return Ok(Self::Msvc);
508 }
509 if probe.contains("Apple clang version") {
510 Ok(Self::AppleClang)
511 } else if probe.contains("clang version") {
512 Ok(Self::Clang)
513 } else if probe.contains("gcc version") {
514 Ok(Self::Gcc)
515 } else {
516 Err(CcBypassReason::UnsupportedCompilerDriver(
517 probe.lines().next().unwrap_or_default().into(),
518 ))
519 }
520 }
521}
522
523#[derive(Debug, Clone, PartialEq, Eq)]
525pub struct CcCompilerIdentity {
526 pub family: CcCompilerFamily,
528 pub version_text: String,
530 pub target: String,
532 pub assembler: String,
538}
539
540#[derive(Debug, Clone, PartialEq, Eq)]
542pub struct CcActionInput {
543 pub path: PathBuf,
546 pub digest: CacheDigest,
548}
549
550#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct CcActionContext {
553 pub compiler: CcCompilerIdentity,
555 pub working_dir: PathBuf,
557 pub path_mappings: Vec<PathMapping>,
559 pub environment: BTreeMap<String, Option<String>>,
561 pub inputs: Vec<CcActionInput>,
563}
564
565#[derive(Debug, Clone, PartialEq, Eq)]
567pub struct CcAction {
568 pub digest: CacheDigest,
570 pub bytes: Vec<u8>,
572}
573
574#[derive(Debug, Serialize)]
575struct CcCompilerDescriptor {
576 assembler: String,
577 family: String,
578 target: String,
579 version_text: String,
580}
581
582#[derive(Debug, Serialize)]
583struct CcInputDescriptor {
584 digest: CacheDigest,
585 path: String,
586}
587
588#[derive(Debug, Serialize)]
589struct CcActionDescriptor {
590 version: u8,
591 kind: &'static str,
592 adapter_version: u8,
593 compiler: CcCompilerDescriptor,
594 arguments: Vec<String>,
595 environment: BTreeMap<String, Option<String>>,
596 inputs: Vec<CcInputDescriptor>,
597}
598
599#[derive(Debug, Serialize)]
600struct CcInvocationDescriptor {
601 version: u8,
602 kind: &'static str,
603 adapter_version: u8,
604 compiler: CcCompilerDescriptor,
605 arguments: Vec<String>,
606 required_inputs: Vec<String>,
607}
608
609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
612#[serde(deny_unknown_fields)]
613pub struct CcInputPrediction {
614 pub version: u8,
616 pub inputs: Vec<String>,
618 pub environment: Vec<String>,
620 #[serde(default, skip_serializing_if = "is_zero")]
623 pub compiler_duration_ns: u64,
624 #[serde(default, skip_serializing_if = "String::is_empty")]
626 pub source_name: String,
627}
628
629fn is_zero(value: &u64) -> bool {
630 *value == 0
631}
632
633#[derive(Debug, Clone, PartialEq, Eq)]
635enum Argument {
636 Plain(String),
638 Path { flag: String, path: PathBuf },
640 PrefixMap {
642 flag: String,
643 from: PathBuf,
644 to: String,
645 },
646 Source(PathBuf),
648}
649
650#[derive(Debug, Clone, PartialEq, Eq)]
652pub struct CcInvocation {
653 arguments: Vec<Argument>,
654 source: PathBuf,
655 output: PathBuf,
656 include_dirs: Vec<PathBuf>,
657 required_inputs: Vec<PathBuf>,
658 language: CcLanguage,
659 sysroot: Option<PathBuf>,
660}
661
662impl CcInvocation {
663 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
666 Parser::new(arguments).parse()
667 }
668
669 pub fn parse_for(
671 arguments: &[OsString],
672 family: CcCompilerFamily,
673 ) -> Result<Self, CcBypassReason> {
674 if family.is_msvc() {
675 MsvcParser::new(arguments).parse()
676 } else {
677 Self::parse(arguments)
678 }
679 }
680
681 pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
683 MsvcParser::new(arguments).parse()
684 }
685
686 pub fn source(&self) -> &Path {
688 &self.source
689 }
690
691 pub fn output(&self) -> &Path {
693 &self.output
694 }
695
696 pub fn include_dirs(&self) -> &[PathBuf] {
698 &self.include_dirs
699 }
700
701 pub fn required_inputs(&self) -> &[PathBuf] {
703 &self.required_inputs
704 }
705
706 pub fn language(&self) -> CcLanguage {
708 self.language
709 }
710
711 pub fn sysroot(&self) -> Option<&Path> {
713 self.sysroot.as_deref()
714 }
715
716 pub fn source_name(&self) -> String {
718 self.source
719 .file_name()
720 .map(|name| name.to_string_lossy().into_owned())
721 .unwrap_or_default()
722 }
723
724 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
731 vec!["-MD".into(), "-MF".into(), depfile.into()]
732 }
733
734 pub fn dependency_arguments_for(
737 &self,
738 depfile: &Path,
739 family: CcCompilerFamily,
740 ) -> Vec<OsString> {
741 if family.is_msvc() {
742 vec!["/sourceDependencies".into(), depfile.into()]
743 } else {
744 self.dependency_arguments(depfile)
745 }
746 }
747
748 pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
750 vec!["/sourceDependencies".into(), depfile.into()]
751 }
752
753 pub fn invocation_digest(
755 &self,
756 context: &CcActionContext,
757 ) -> Result<CacheDigest, CcBypassReason> {
758 let builder = ActionBuilder::new(self, context.clone());
759 let descriptor = builder.invocation_descriptor()?;
760 let bytes = canonical_json(&descriptor)
761 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
762 Ok(CacheDigest::blake3(&bytes))
763 }
764
765 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
768 ActionBuilder::new(self, context).build()
769 }
770
771 pub fn prediction(
774 &self,
775 context: &CcActionContext,
776 compiler_duration_ns: u64,
777 ) -> Result<CcInputPrediction, CcBypassReason> {
778 let builder = ActionBuilder::new(self, context.clone());
779 let mut inputs = context
780 .inputs
781 .iter()
782 .map(|input| builder.normalize_input_path(&input.path))
783 .collect::<Result<Vec<_>, _>>()?;
784 inputs.sort();
785 inputs.dedup();
786 Ok(CcInputPrediction {
787 version: 1,
788 inputs,
789 environment: context.environment.keys().cloned().collect(),
790 compiler_duration_ns,
791 source_name: self.source_name(),
792 })
793 }
794}
795
796impl CcInputPrediction {
797 pub fn discover(
800 &self,
801 working_dir: &Path,
802 path_mappings: &[PathMapping],
803 digests: &dyn FileDigestCache,
804 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
805 if self.version != 1 {
806 return Err(CcBypassReason::UnsupportedPrediction);
807 }
808 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
809 return Err(CcBypassReason::UnsupportedPrediction);
810 }
811 let mappings = PathMapping::ordered(path_mappings);
812 let mut files = BTreeSet::new();
813 let mut directories = BTreeSet::new();
814 for entry in &self.inputs {
815 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
816 Some(directory) => {
817 directories.insert(denormalize_path(directory, &mappings)?);
818 }
819 None => {
820 files.insert(denormalize_path(entry, &mappings)?);
821 }
822 }
823 }
824 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
825 }
826}
827
828fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
834 for mapping in mappings {
835 let prefix = format!("${{{}}}", mapping.placeholder);
836 let suffix = if value == prefix {
837 ""
838 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
839 suffix
840 } else {
841 continue;
842 };
843 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
844 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
845 }
846 let mut path = normalize_components(&mapping.root);
847 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
848 return Ok(path);
849 }
850 let path = PathBuf::from(value);
854 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
855 return Ok(path);
856 }
857 Err(CcBypassReason::InvalidPredictedInput(value.into()))
858}
859
860fn safe_suffix(suffix: &str) -> bool {
861 suffix.is_empty()
862 || !suffix.split('/').any(|component| {
863 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
864 })
865}
866
867pub fn is_system_path(path: &Path) -> bool {
869 SYSTEM_ROOTS
870 .iter()
871 .any(|root| path.starts_with(Path::new(root)))
872}
873
874fn normalize_components(path: &Path) -> PathBuf {
875 let mut normalized = PathBuf::new();
876 for component in path.components() {
877 match component {
878 Component::CurDir => {}
879 Component::ParentDir => {
880 normalized.pop();
881 }
882 component => normalized.push(component.as_os_str()),
883 }
884 }
885 normalized
886}
887
888pub fn environment_inputs<F>(
891 lookup: F,
892 sysroot: Option<&Path>,
893) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
894where
895 F: Fn(&str) -> Option<String>,
896{
897 environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
898}
899
900pub fn environment_inputs_for<F>(
902 lookup: F,
903 sysroot: Option<&Path>,
904 family: CcCompilerFamily,
905) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
906where
907 F: Fn(&str) -> Option<String>,
908{
909 for name in BYPASS_ENVIRONMENT {
910 if lookup(name).is_some() {
911 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
912 }
913 }
914 let mut environment = BTreeMap::new();
915 for name in KEYED_ENVIRONMENT {
916 if *name == "SDKROOT" && sysroot.is_some() {
919 continue;
920 }
921 environment.insert((*name).to_string(), lookup(name));
922 }
923 if family.is_msvc() {
924 for name in [
928 "INCLUDE",
929 "VCToolsVersion",
930 "WindowsSDKVersion",
931 "UCRTVersion",
932 ] {
933 environment.insert(name.into(), lookup(name));
934 }
935 for name in ["CL", "_CL_"] {
936 if lookup(name).is_some() {
937 return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
938 }
939 }
940 }
941 Ok(environment)
942}
943
944struct ActionBuilder<'a> {
945 invocation: &'a CcInvocation,
946 context: CcActionContext,
947 mappings: Vec<PathMapping>,
948}
949
950impl<'a> ActionBuilder<'a> {
951 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
952 context.path_mappings = PathMapping::ordered(&context.path_mappings);
953 let mappings = context.path_mappings.clone();
954 Self {
955 invocation,
956 context,
957 mappings,
958 }
959 }
960
961 fn build(self) -> Result<CcAction, CcBypassReason> {
962 self.validate_mappings()?;
963 let invocation = self.invocation_descriptor()?;
964
965 let mut inputs = BTreeMap::<String, CacheDigest>::new();
966 for input in &self.context.inputs {
967 input.digest.validate().map_err(|_| {
968 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
969 })?;
970 let path = self.normalize_input_path(&input.path)?;
971 if inputs
972 .insert(path.clone(), input.digest.clone())
973 .is_some_and(|existing| existing != input.digest)
974 {
975 return Err(CcBypassReason::ConflictingInput(path));
976 }
977 }
978 let required = self
979 .invocation
980 .required_inputs
981 .iter()
982 .map(|path| self.normalize_path(path))
983 .collect::<Result<BTreeSet<_>, _>>()?;
984 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
985 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
986 }
987 let inputs = inputs
988 .into_iter()
989 .map(|(path, digest)| CcInputDescriptor { path, digest })
990 .collect();
991 let descriptor = CcActionDescriptor {
992 version: ACTION_SCHEMA_VERSION,
993 kind: "cc",
994 adapter_version: ADAPTER_VERSION,
995 compiler: invocation.compiler,
996 arguments: invocation.arguments,
997 environment: self.context.environment.clone(),
998 inputs,
999 };
1000 let bytes = canonical_json(&descriptor)
1001 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1002 let digest = CacheDigest::blake3(&bytes);
1003 Ok(CcAction { digest, bytes })
1004 }
1005
1006 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1007 self.validate_mappings()?;
1008 let arguments = self
1009 .invocation
1010 .arguments
1011 .iter()
1012 .map(|argument| self.normalize_argument(argument))
1013 .collect::<Result<Vec<_>, _>>()?;
1014 let required_inputs = self
1015 .invocation
1016 .required_inputs
1017 .iter()
1018 .map(|path| self.normalize_path(path))
1019 .collect::<Result<BTreeSet<_>, _>>()?
1020 .into_iter()
1021 .collect();
1022 Ok(CcInvocationDescriptor {
1023 version: ACTION_SCHEMA_VERSION,
1024 kind: "cc",
1025 adapter_version: ADAPTER_VERSION,
1026 compiler: self.compiler_descriptor(),
1027 arguments,
1028 required_inputs,
1029 })
1030 }
1031
1032 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1033 CcCompilerDescriptor {
1034 assembler: self.context.compiler.assembler.clone(),
1035 family: self.context.compiler.family.as_str().into(),
1036 target: self.context.compiler.target.clone(),
1037 version_text: self.context.compiler.version_text.clone(),
1038 }
1039 }
1040
1041 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1042 if !self.context.working_dir.is_absolute() {
1043 return Err(CcBypassReason::RelativeWorkingDirectory(
1044 self.context.working_dir.clone(),
1045 ));
1046 }
1047 let mut roots = BTreeSet::new();
1048 let mut placeholders = BTreeSet::new();
1049 for mapping in &self.mappings {
1050 if !mapping.root.is_absolute() {
1051 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1052 }
1053 if mapping.placeholder.is_empty()
1054 || !mapping
1055 .placeholder
1056 .bytes()
1057 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1058 || !roots.insert(normalize_components(&mapping.root))
1059 || !placeholders.insert(&mapping.placeholder)
1060 {
1061 return Err(CcBypassReason::InvalidPathPlaceholder(
1062 mapping.placeholder.clone(),
1063 ));
1064 }
1065 }
1066 Ok(())
1067 }
1068
1069 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1070 match argument {
1071 Argument::Plain(value) => Ok(value.clone()),
1072 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1073 Argument::PrefixMap { flag, from, to } => {
1074 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1075 }
1076 Argument::Source(path) => Ok(self.normalize_path(path)?),
1077 }
1078 }
1079
1080 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1087 match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
1088 Ok(normalized) => Ok(normalized),
1089 Err(reason) => {
1090 let absolute = absolute_path(path, &self.context.working_dir);
1091 if is_system_path(&absolute) {
1092 return absolute
1093 .to_str()
1094 .map(ToOwned::to_owned)
1095 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1096 }
1097 Err(reason.into())
1098 }
1099 }
1100 }
1101
1102 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1103 match path.to_str().and_then(|path| {
1104 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1105 .map(ToOwned::to_owned)
1106 }) {
1107 Some(directory) => Ok(format!(
1108 "{INCLUDE_MANIFEST_PREFIX}{}",
1109 self.normalize_path(Path::new(&directory))?
1110 )),
1111 None => self.normalize_path(path),
1112 }
1113 }
1114}
1115
1116fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1117 if path.is_absolute() {
1118 normalize_components(path)
1119 } else {
1120 normalize_components(&working_dir.join(path))
1121 }
1122}
1123
1124struct Parser<'a> {
1125 arguments: &'a [OsString],
1126 index: usize,
1127 parsed: Vec<Argument>,
1128 source: Option<PathBuf>,
1129 output: Option<PathBuf>,
1130 include_dirs: Vec<PathBuf>,
1131 required_inputs: Vec<PathBuf>,
1132 sysroot: Option<PathBuf>,
1133 explicit_language: Option<CcLanguage>,
1134 compiling: bool,
1135}
1136
1137impl<'a> Parser<'a> {
1138 fn new(arguments: &'a [OsString]) -> Self {
1139 Self {
1140 arguments,
1141 index: 0,
1142 parsed: Vec::new(),
1143 source: None,
1144 output: None,
1145 include_dirs: Vec::new(),
1146 required_inputs: Vec::new(),
1147 sysroot: None,
1148 explicit_language: None,
1149 compiling: false,
1150 }
1151 }
1152
1153 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1154 while self.index < self.arguments.len() {
1155 let value = self.current()?.to_string();
1156 self.index += 1;
1157 if value == "-" {
1158 return Err(CcBypassReason::StandardInput);
1159 }
1160 if let Some(argfile) = value.strip_prefix('@') {
1161 return Err(CcBypassReason::ResponseFile(argfile.into()));
1162 }
1163 if value.starts_with('-') {
1164 self.parse_flag(&value)?;
1165 } else {
1166 self.parse_input(&value)?;
1167 }
1168 }
1169
1170 if !self.compiling {
1171 return Err(CcBypassReason::NotACompile);
1172 }
1173 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1174 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1175 let language = self.language(&source)?;
1176 self.required_inputs.push(source.clone());
1177 Ok(CcInvocation {
1178 arguments: self.parsed,
1179 source,
1180 output,
1181 include_dirs: self.include_dirs,
1182 required_inputs: self.required_inputs,
1183 language,
1184 sysroot: self.sysroot,
1185 })
1186 }
1187
1188 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1189 if let Some(language) = self.explicit_language {
1190 return Ok(language);
1191 }
1192 let extension = source
1193 .extension()
1194 .and_then(|extension| extension.to_str())
1195 .unwrap_or_default();
1196 match extension {
1197 "c" => Ok(CcLanguage::C),
1198 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1199 _ => Err(CcBypassReason::UnsupportedLanguage(
1200 source.display().to_string(),
1201 )),
1202 }
1203 }
1204
1205 fn current(&self) -> Result<&str, CcBypassReason> {
1206 self.arguments[self.index]
1207 .to_str()
1208 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1209 }
1210
1211 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1212 if let Some(value) = inline
1213 && !value.is_empty()
1214 {
1215 return Ok(value.into());
1216 }
1217 if self.index >= self.arguments.len() {
1218 return Err(CcBypassReason::MissingValue(flag.into()));
1219 }
1220 let value = self.current()?.to_string();
1221 self.index += 1;
1222 Ok(value)
1223 }
1224
1225 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1226 if self.source.is_some() {
1227 return Err(CcBypassReason::MultipleInputs);
1228 }
1229 let path = PathBuf::from(value);
1230 if self.explicit_language.is_none() {
1234 let extension = path
1235 .extension()
1236 .and_then(|extension| extension.to_str())
1237 .unwrap_or_default();
1238 if !matches!(extension, "c" | "cc" | "cpp" | "cxx" | "c++") {
1239 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1240 }
1241 }
1242 self.source = Some(path.clone());
1243 self.parsed.push(Argument::Source(path));
1244 Ok(())
1245 }
1246
1247 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1248 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1249 return Err(CcBypassReason::CompilerQuery);
1250 }
1251 if matches!(value, "-E" | "-S") {
1252 return Err(CcBypassReason::NonObjectOutput(value.into()));
1253 }
1254 if value.starts_with("-M") {
1255 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1256 }
1257 if value.starts_with("-save-temps") {
1258 return Err(CcBypassReason::SaveTemps(value.into()));
1259 }
1260 if value == "--coverage" {
1261 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1262 }
1263 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1264 || value.starts_with("-Wp,")
1265 || value.starts_with("-Wa,")
1266 || value.starts_with("-Wl,")
1267 {
1268 return Err(CcBypassReason::ToolPassthrough(value.into()));
1271 }
1272 if value.starts_with("-include-pch") || value == "-emit-pch" {
1273 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1274 }
1275
1276 if value == "-c" {
1277 self.compiling = true;
1278 self.parsed.push(Argument::Plain(value.into()));
1279 return Ok(());
1280 }
1281 if SUPPORTED_BARE_FLAGS.contains(&value)
1282 || SUPPORTED_O_FLAGS.contains(&value)
1283 || SUPPORTED_G_FLAGS.contains(&value)
1284 || value.starts_with("-std=")
1285 {
1286 self.parsed.push(Argument::Plain(value.into()));
1287 return Ok(());
1288 }
1289 if let Some(rest) = value.strip_prefix("-o") {
1290 let path = self.take_value("-o", Some(rest))?;
1291 self.output = Some(PathBuf::from(&path));
1294 self.parsed.push(Argument::Path {
1295 flag: "-o".into(),
1296 path: PathBuf::from(path),
1297 });
1298 return Ok(());
1299 }
1300 if let Some(rest) = value.strip_prefix("-I") {
1301 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1302 self.include_dirs.push(path.clone());
1303 self.parsed.push(Argument::Path {
1304 flag: "-I".into(),
1305 path,
1306 });
1307 return Ok(());
1308 }
1309 if SEPARATE_PATH_FLAGS.contains(&value) {
1310 let path = PathBuf::from(self.take_value(value, None)?);
1311 match value {
1312 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1313 "-isysroot" => self.sysroot = Some(path.clone()),
1314 _ => {}
1320 }
1321 self.parsed.push(Argument::Path {
1322 flag: value.into(),
1323 path,
1324 });
1325 return Ok(());
1326 }
1327 if let Some(rest) = value.strip_prefix("--include=") {
1330 let path = PathBuf::from(rest);
1331 self.parsed.push(Argument::Path {
1332 flag: "-include".into(),
1333 path,
1334 });
1335 return Ok(());
1336 }
1337 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1338 value
1339 .strip_prefix(&format!("{flag}="))
1340 .map(|rest| (*flag, rest))
1341 }) {
1342 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1343 self.parsed.push(Argument::PrefixMap {
1344 flag: flag.into(),
1345 from: PathBuf::from(from),
1346 to: to.into(),
1347 });
1348 return Ok(());
1349 }
1350 if value == "--param" {
1352 let parameter = self.take_value("--param", None)?;
1353 self.parsed
1354 .push(Argument::Plain(format!("--param={parameter}")));
1355 return Ok(());
1356 }
1357 if let Some(parameter) = value.strip_prefix("--param=") {
1358 self.parsed
1359 .push(Argument::Plain(format!("--param={parameter}")));
1360 return Ok(());
1361 }
1362 if let Some(rest) = value.strip_prefix("--sysroot=") {
1363 let path = PathBuf::from(rest);
1364 self.sysroot = Some(path.clone());
1365 self.parsed.push(Argument::Path {
1366 flag: "--sysroot".into(),
1367 path,
1368 });
1369 return Ok(());
1370 }
1371 if let Some(rest) = value
1372 .strip_prefix("-D")
1373 .or_else(|| value.strip_prefix("-U"))
1374 {
1375 let flag = &value[..2];
1376 let definition = self.take_value(flag, Some(rest))?;
1377 self.parsed
1378 .push(Argument::Plain(format!("{flag}{definition}")));
1379 return Ok(());
1380 }
1381 if let Some(rest) = value.strip_prefix("-x") {
1382 let language = self.take_value("-x", Some(rest))?;
1383 self.explicit_language = Some(match language.as_str() {
1384 "c" => CcLanguage::C,
1385 "c++" => CcLanguage::Cxx,
1386 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1387 });
1388 self.parsed.push(Argument::Plain(format!("-x{language}")));
1389 return Ok(());
1390 }
1391 if let Some(target) = value.strip_prefix("--target=") {
1392 self.parsed
1393 .push(Argument::Plain(format!("--target={target}")));
1394 return Ok(());
1395 }
1396 if value == "-target" {
1397 let target = self.take_value("-target", None)?;
1398 self.parsed
1399 .push(Argument::Plain(format!("--target={target}")));
1400 return Ok(());
1401 }
1402 if value == "-arch" {
1403 let arch = self.take_value("-arch", None)?;
1404 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1405 return Ok(());
1406 }
1407 if let Some(option) = value.strip_prefix("-f") {
1408 return self.parse_f_flag(value, option);
1409 }
1410 if let Some(option) = value.strip_prefix("-m") {
1411 return self.parse_m_flag(value, option);
1412 }
1413 if value.starts_with("-g") {
1414 return Err(if value.starts_with("-gsplit-dwarf") {
1417 CcBypassReason::SplitDebugOutput(value.into())
1418 } else {
1419 CcBypassReason::UnknownFlag(value.into())
1420 });
1421 }
1422 if value.starts_with("-W") {
1423 self.parsed.push(Argument::Plain(value.into()));
1427 return Ok(());
1428 }
1429 Err(CcBypassReason::UnknownFlag(value.into()))
1430 }
1431
1432 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1433 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1434 return Err(CcBypassReason::Plugin(value.into()));
1435 }
1436 if option.starts_with("profile-") || option == "test-coverage" {
1437 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1438 }
1439 let name = option.split_once('=').map_or(option, |(name, _)| name);
1440 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1441 return Err(CcBypassReason::UnknownFlag(value.into()));
1442 }
1443 self.parsed.push(Argument::Plain(value.into()));
1444 Ok(())
1445 }
1446
1447 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1448 if option == "llvm" {
1449 return Err(CcBypassReason::ToolPassthrough(value.into()));
1450 }
1451 if let Some((name, selection)) = option.split_once('=')
1456 && matches!(name, "arch" | "cpu" | "tune")
1457 && matches!(selection, "native" | "host")
1458 {
1459 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1460 }
1461 let name = option.split_once('=').map_or(option, |(name, _)| name);
1462 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1463 return Err(CcBypassReason::UnknownFlag(value.into()));
1464 }
1465 self.parsed.push(Argument::Plain(value.into()));
1466 Ok(())
1467 }
1468}
1469
1470struct MsvcParser<'a> {
1474 arguments: &'a [OsString],
1475 index: usize,
1476 parsed: Vec<Argument>,
1477 source: Option<PathBuf>,
1478 output: Option<PathBuf>,
1479 include_dirs: Vec<PathBuf>,
1480 required_inputs: Vec<PathBuf>,
1481 explicit_language: Option<CcLanguage>,
1482 compiling: bool,
1483}
1484
1485impl<'a> MsvcParser<'a> {
1486 fn new(arguments: &'a [OsString]) -> Self {
1487 Self {
1488 arguments,
1489 index: 0,
1490 parsed: Vec::new(),
1491 source: None,
1492 output: None,
1493 include_dirs: Vec::new(),
1494 required_inputs: Vec::new(),
1495 explicit_language: None,
1496 compiling: false,
1497 }
1498 }
1499
1500 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1501 while self.index < self.arguments.len() {
1502 let value = self.current()?.to_owned();
1503 self.index += 1;
1504 if let Some(file) = value.strip_prefix('@') {
1505 return Err(CcBypassReason::ResponseFile(file.into()));
1506 }
1507 if value.starts_with('/') || value.starts_with('-') {
1508 self.parse_flag(&value)?;
1509 } else {
1510 self.add_source(&value)?;
1511 }
1512 }
1513 if !self.compiling {
1514 return Err(CcBypassReason::NotACompile);
1515 }
1516 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1517 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1518 let language = self.explicit_language.unwrap_or_else(|| {
1519 if source
1520 .extension()
1521 .and_then(|value| value.to_str())
1522 .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1523 {
1524 CcLanguage::C
1525 } else {
1526 CcLanguage::Cxx
1527 }
1528 });
1529 self.required_inputs.push(source.clone());
1530 Ok(CcInvocation {
1531 arguments: self.parsed,
1532 source,
1533 output,
1534 include_dirs: self.include_dirs,
1535 required_inputs: self.required_inputs,
1536 language,
1537 sysroot: None,
1538 })
1539 }
1540
1541 fn current(&self) -> Result<&str, CcBypassReason> {
1542 self.arguments[self.index]
1543 .to_str()
1544 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1545 }
1546
1547 fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
1548 if !attached.is_empty() {
1549 return Ok(attached.into());
1550 }
1551 if self.index == self.arguments.len() {
1552 return Err(CcBypassReason::MissingValue(flag.into()));
1553 }
1554 let value = self.current()?.to_owned();
1555 self.index += 1;
1556 Ok(value)
1557 }
1558
1559 fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
1560 if self.source.is_some() {
1561 return Err(CcBypassReason::MultipleInputs);
1562 }
1563 let path = PathBuf::from(value);
1564 if self.explicit_language.is_none()
1565 && !path
1566 .extension()
1567 .and_then(|value| value.to_str())
1568 .is_some_and(|value| {
1569 matches!(
1570 value.to_ascii_lowercase().as_str(),
1571 "c" | "cc" | "cpp" | "cxx"
1572 )
1573 })
1574 {
1575 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1576 }
1577 self.source = Some(path.clone());
1578 self.parsed.push(Argument::Source(path));
1579 Ok(())
1580 }
1581
1582 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1583 let option = value.trim_start_matches(['/', '-']);
1584 let lower = option.to_ascii_lowercase();
1585 if matches!(lower.as_str(), "?" | "help") {
1586 return Err(CcBypassReason::CompilerQuery);
1587 }
1588 if lower == "showincludes" || lower.starts_with("sourcedependencies") {
1589 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1590 }
1591 if matches!(lower.as_str(), "e" | "ep" | "p") {
1592 return Err(CcBypassReason::NonObjectOutput(value.into()));
1593 }
1594 if (lower.starts_with("fa") && !lower.starts_with("favor:"))
1595 || lower.starts_with("fd")
1596 || lower.starts_with("zi")
1597 {
1598 return Err(CcBypassReason::SplitDebugOutput(value.into()));
1599 }
1600 if lower.starts_with("yc")
1601 || lower.starts_with("yu")
1602 || (lower.starts_with("fp") && !lower.starts_with("fp:"))
1603 {
1604 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1605 }
1606 if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
1607 return Err(CcBypassReason::ToolPassthrough(value.into()));
1608 }
1609 if matches!(lower.as_str(), "ld" | "ldd") {
1610 return Err(CcBypassReason::NotACompile);
1611 }
1612 if lower == "c" {
1613 self.compiling = true;
1614 self.parsed.push(Argument::Plain("/c".into()));
1615 return Ok(());
1616 }
1617 for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
1618 if let Some(attached) = option.strip_prefix(prefix) {
1619 let path = PathBuf::from(self.value(canonical, attached)?);
1620 if prefix == "Fo" {
1621 self.output = Some(path.clone());
1622 } else if prefix == "I" {
1623 self.include_dirs.push(path.clone());
1624 }
1625 self.parsed.push(Argument::Path {
1626 flag: canonical.into(),
1627 path,
1628 });
1629 return Ok(());
1630 }
1631 }
1632 if lower.starts_with("external:i") {
1633 let path = PathBuf::from(self.value("/external:I", &option[10..])?);
1634 self.include_dirs.push(path.clone());
1635 self.parsed.push(Argument::Path {
1636 flag: "/external:I".into(),
1637 path,
1638 });
1639 return Ok(());
1640 }
1641 if option.starts_with("Tc") || option.starts_with("Tp") {
1642 let c = option.starts_with("Tc");
1643 let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
1644 self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
1645 return self.add_source(&path);
1646 }
1647 if lower.starts_with("pathmap:") {
1648 let rest = &option[8..];
1649 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1650 self.parsed.push(Argument::PrefixMap {
1651 flag: "/pathmap".into(),
1652 from: PathBuf::from(from),
1653 to: to.into(),
1654 });
1655 return Ok(());
1656 }
1657 if matches!(option, "D" | "U") {
1658 let definition = self.value(value, "")?;
1659 self.parsed
1660 .push(Argument::Plain(format!("/{option}{definition}")));
1661 return Ok(());
1662 }
1663 let definition = option.starts_with('D') || option.starts_with('U');
1666 let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
1667 || lower
1668 .strip_prefix('w')
1669 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1670 || ["wd", "we", "wo"].iter().any(|prefix| {
1671 lower
1672 .strip_prefix(prefix)
1673 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1674 });
1675 let admitted = definition
1676 || warning
1677 || lower.starts_with("std:")
1678 || lower.starts_with("arch:")
1679 || lower.starts_with("favor:")
1680 || lower.starts_with("volatile:")
1681 || lower.starts_with("fp:")
1682 || lower.starts_with("eh")
1683 || lower.starts_with('o')
1684 || lower.starts_with("ob")
1685 || lower.starts_with("oi")
1686 || lower.starts_with("ot")
1687 || lower.starts_with("oy")
1688 || lower.starts_with("gs")
1689 || lower.starts_with("gr")
1690 || lower.starts_with("gy")
1691 || lower.starts_with("gw")
1692 || lower.starts_with("gl")
1693 || lower.starts_with("zc:")
1694 || lower.starts_with("diagnostics:")
1695 || matches!(
1696 lower.as_str(),
1697 "nologo"
1698 | "brepro"
1699 | "bigobj"
1700 | "utf-8"
1701 | "permissive-"
1702 | "z7"
1703 | "md"
1704 | "mdd"
1705 | "mt"
1706 | "mtd"
1707 );
1708 if admitted {
1709 self.parsed.push(Argument::Plain(value.into()));
1710 return Ok(());
1711 }
1712 Err(CcBypassReason::UnknownFlag(value.into()))
1713 }
1714}
1715
1716#[cfg(test)]
1717#[path = "cc_cache_tests.rs"]
1718mod tests;