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 "cf-protection",
101 "color-diagnostics",
102 "data-sections",
103 "diagnostics-color",
104 "exceptions",
105 "fast-math",
106 "finite-math-only",
107 "function-sections",
108 "lto",
109 "math-errno",
110 "merge-all-constants",
111 "no-asynchronous-unwind-tables",
112 "no-builtin",
113 "no-common",
114 "no-exceptions",
115 "no-fast-math",
116 "no-finite-math-only",
117 "no-lto",
118 "no-math-errno",
119 "no-omit-frame-pointer",
120 "no-plt",
121 "no-reciprocal-math",
122 "no-rtti",
123 "no-semantic-interposition",
124 "no-stack-protector",
125 "no-strict-aliasing",
126 "no-tree-vectorize",
127 "no-unroll-loops",
128 "no-unwind-tables",
129 "omit-frame-pointer",
130 "pic",
131 "pie",
132 "reciprocal-math",
133 "rtti",
134 "sanitize-undefined-strip-path-components",
135 "short-enums",
136 "signed-char",
137 "stack-clash-protection",
138 "stack-protector",
139 "stack-protector-all",
140 "stack-protector-strong",
141 "strict-aliasing",
142 "tls-model",
143 "tree-vectorize",
144 "unroll-loops",
145 "unsigned-char",
146 "unwind-tables",
147 "visibility",
148 "visibility-inlines-hidden",
149 "wrapv",
150];
151
152const SUPPORTED_M_FLAGS: &[&str] = &[
153 "32",
154 "64",
155 "adx",
156 "aes",
157 "arch",
158 "arm",
159 "avx",
160 "avx2",
161 "avx512bf16",
162 "avx512bitalg",
163 "avx512bw",
164 "avx512cd",
165 "avx512dq",
166 "avx512f",
167 "avx512fp16",
168 "avx512ifma",
169 "avx512vbmi",
170 "avx512vbmi2",
171 "avx512vl",
172 "avx512vnni",
173 "avx512vpopcntdq",
174 "avxvnni",
175 "bmi",
176 "bmi2",
177 "cpu",
178 "crc",
179 "crypto",
180 "dotprod",
181 "f16c",
182 "float-abi",
183 "fma",
184 "fpu",
185 "gfni",
186 "iphoneos-version-min",
187 "lse",
188 "lzcnt",
189 "macosx-version-min",
190 "movbe",
191 "no-avx",
192 "no-avx2",
193 "no-avx512f",
194 "no-omit-leaf-frame-pointer",
195 "no-outline-atomics",
196 "no-red-zone",
197 "no-sse",
198 "no-sse2",
199 "omit-leaf-frame-pointer",
200 "outline-atomics",
201 "pclmul",
202 "popcnt",
203 "rdrnd",
204 "rdseed",
205 "red-zone",
206 "sha",
207 "sha512",
208 "soft-float",
209 "sse",
210 "sse2",
211 "sse3",
212 "sse4.1",
213 "sse4.2",
214 "sse4a",
215 "ssse3",
216 "sve",
217 "sve2",
218 "thumb",
219 "tune",
220 "vaes",
221 "vpclmulqdq",
222 "xsave",
223 "xsavec",
224 "xsaveopt",
225 "xsaves",
226];
227
228const SUPPORTED_O_FLAGS: &[&str] = &[
229 "-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Og", "-Os", "-Oz",
230];
231
232const SUPPORTED_G_FLAGS: &[&str] = &[
233 "-g",
234 "-g0",
235 "-g1",
236 "-g2",
237 "-g3",
238 "-gdwarf-2",
239 "-gdwarf-3",
240 "-gdwarf-4",
241 "-gdwarf-5",
242];
243
244const SUPPORTED_BARE_FLAGS: &[&str] = &[
245 "-ansi",
246 "-nostdinc",
247 "-nostdinc++",
248 "-pedantic",
249 "-pedantic-errors",
250 "-pipe",
251 "-pthread",
252 "-w",
253];
254
255const SEPARATE_PATH_FLAGS: &[&str] = &[
256 "-idirafter",
257 "-imacros",
258 "-include",
259 "-iquote",
260 "-isysroot",
261 "-isystem",
262];
263
264const TOOL_PASSTHROUGH_FLAGS: &[&str] = &["-Xassembler", "-Xclang", "-Xlinker", "-Xpreprocessor"];
265
266const SUPPORTED_ASSEMBLER_OPTIONS: &[&str] = &["--noexecstack"];
271
272const COMPILER_QUERY_FLAGS: &[&str] = &[
273 "--help",
274 "--version",
275 "-###",
276 "-?",
279 "-dumpmachine",
280 "-dumpversion",
281 "-v",
282];
283
284const PREFIX_MAP_FLAGS: &[&str] = &[
289 "-fdebug-prefix-map",
290 "-ffile-prefix-map",
291 "-fmacro-prefix-map",
292];
293
294impl CcBypassReason {
295 pub fn kind(&self) -> &'static str {
300 self.into()
301 }
302
303 pub fn remediation(&self) -> Option<&'static str> {
310 match self {
311 Self::UnsupportedEnvironment(_) => Some(
312 "Unset the reported environment variable for this build so the compiler invocation describes all of its inputs.",
313 ),
314 Self::LocalCpuTarget(_) => Some(
315 "Replace the reported local-CPU option with an explicit architecture or CPU name.",
316 ),
317 Self::EmbeddedTimestampMacro(_) => Some(
318 "Remove the reported timestamp macro, or keep this compilation uncached if its changing value is intentional.",
319 ),
320 Self::SearchPathModifiedDuringCompilation(_) => Some(
321 "Generate headers before compilation instead of changing an include directory while the compiler is running.",
322 ),
323 Self::UnknownFlag(_) | Self::ToolPassthrough(_) => Some(
324 "Upgrade mbx or report the unmodeled compiler option. If you control the build script, removing the option can also make the compilation cacheable.",
325 ),
326 Self::UnmappedAbsolutePath(_) => Some(
327 "Move the input under a mapped project or system root, or keep this compilation uncached.",
328 ),
329 _ => None,
330 }
331 }
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
339#[strum(serialize_all = "kebab-case")]
340#[non_exhaustive]
341pub enum CcBypassReason {
342 #[error("compiler argument {index} is not valid UTF-8")]
344 NonUtf8Argument {
345 index: usize,
347 },
348 #[error("compiler response file is not modeled by the cache adapter: {0}")]
350 ResponseFile(String),
351 #[error("compiler flag is not modeled by the cache adapter: {0}")]
353 UnknownFlag(String),
354 #[error("compiler flag {0} is missing its value")]
356 MissingValue(String),
357 #[error("compiler invocation queries the driver instead of compiling")]
359 CompilerQuery,
360 #[error("compiler invocation does not compile with -c")]
362 NotACompile,
363 #[error("compiler invocation emits a non-object output: {0}")]
365 NonObjectOutput(String),
366 #[error("compiler invocation reads its source from standard input")]
368 StandardInput,
369 #[error("compiler invocation names no source file")]
371 MissingInput,
372 #[error("compiler invocation names more than one source file")]
374 MultipleInputs,
375 #[error("compiler invocation names no output file")]
377 MissingOutput,
378 #[error("compiler input language is not modeled by the cache adapter: {0}")]
380 UnsupportedLanguage(String),
381 #[error("compiler invocation requests its own dependency output: {0}")]
383 CallerDependencyFlags(String),
384 #[error("precompiled headers are not modeled by the cache adapter: {0}")]
386 PrecompiledHeader(String),
387 #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
389 CoverageInstrumentation(String),
390 #[error("split debug output is not modeled by the cache adapter: {0}")]
392 SplitDebugOutput(String),
393 #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
395 SaveTemps(String),
396 #[error("compiler flag forwards options to another tool: {0}")]
398 ToolPassthrough(String),
399 #[error("compiler plugins are not modeled by the cache adapter: {0}")]
401 Plugin(String),
402 #[error("include search directory changed during the compilation: {0}")]
405 SearchPathModifiedDuringCompilation(PathBuf),
406
407 #[error("compilation output records a path its key normalized away: {0}")]
414 UnportableOutput(PathBuf),
415 #[error("compiler flag tunes for the local CPU: {0}")]
417 LocalCpuTarget(String),
418 #[error("compiler driver is not modeled by the cache adapter: {0}")]
420 UnsupportedCompilerDriver(String),
421 #[error("could not establish compiler identity: {0}")]
423 CompilerIdentityUnavailable(String),
424 #[error("environment variable {0} changes the compilation in an unmodeled way")]
426 UnsupportedEnvironment(String),
427 #[error("no real compiler was pinned for the cc shim")]
429 RealCompilerUnpinned,
430 #[error("input expands a timestamp macro: {0}")]
433 EmbeddedTimestampMacro(PathBuf),
434 #[error("preprocessed assembly uses an assembler input directive in {0}")]
437 AssemblerInputDirective(PathBuf),
438 #[error("could not model the compiler depfile: {0}")]
440 MalformedDepfile(String),
441 #[error("could not read the compiler depfile {path}: {message}")]
443 DepfileRead {
444 path: PathBuf,
446 message: String,
448 },
449 #[error("compilation reads more inputs than the cache adapter models")]
451 TooManyInputs,
452 #[error("path is outside every modeled root: {0}")]
454 UnmappedAbsolutePath(PathBuf),
455 #[error("path is not valid UTF-8: {0}")]
457 NonUtf8Path(PathBuf),
458 #[error("compiler working directory is not absolute: {0}")]
460 RelativeWorkingDirectory(PathBuf),
461 #[error("path mapping root is not absolute: {0}")]
463 RelativePathMapping(PathBuf),
464 #[error("invalid path mapping placeholder: {0}")]
466 InvalidPathPlaceholder(String),
467 #[error("required input is missing from the discovered inputs: {0}")]
469 MissingRequiredInput(String),
470 #[error("invalid digest for input: {0}")]
472 InvalidInputDigest(String),
473 #[error("conflicting digests for input: {0}")]
475 ConflictingInput(String),
476 #[error("could not read input {path}: {message}")]
478 InputRead {
479 path: PathBuf,
481 message: String,
483 },
484 #[error("input changed during the compilation: {0}")]
486 InputChanged(PathBuf),
487 #[error("input was modified during the compilation: {0}")]
489 InputModifiedDuringCompilation(PathBuf),
490 #[error("discovered inputs use a different working directory")]
492 DiscoveryWorkingDirectory,
493 #[error("action prediction is not modeled by this adapter version")]
495 UnsupportedPrediction,
496 #[error("invalid predicted input: {0}")]
498 InvalidPredictedInput(String),
499 #[error("could not serialize the action descriptor: {0}")]
501 Serialization(String),
502}
503
504impl From<PathNormalizationError> for CcBypassReason {
505 fn from(reason: PathNormalizationError) -> Self {
506 match reason {
507 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
508 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
509 }
510 }
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub enum CcLanguage {
516 C,
518 Cxx,
520}
521
522impl CcLanguage {
523 pub fn shim_stem(self) -> &'static str {
525 match self {
526 Self::C => "mbx-cc",
527 Self::Cxx => "mbx-cxx",
528 }
529 }
530
531 pub fn default_driver(self) -> &'static str {
533 if cfg!(windows) {
534 return "cl.exe";
535 }
536 match self {
537 Self::C => "cc",
538 Self::Cxx => "c++",
539 }
540 }
541}
542
543#[derive(Debug, Clone, Copy, PartialEq, Eq)]
545pub enum CcCompilerFamily {
546 Gcc,
548 Clang,
550 AppleClang,
552 #[cfg(windows)]
554 Msvc,
555}
556
557impl CcCompilerFamily {
558 pub fn as_str(self) -> &'static str {
560 match self {
561 Self::Gcc => "gcc",
562 Self::Clang => "clang",
563 Self::AppleClang => "apple-clang",
564 #[cfg(windows)]
565 Self::Msvc => "msvc",
566 }
567 }
568
569 pub fn uses_external_assembler(self) -> bool {
572 matches!(self, Self::Gcc)
573 }
574
575 pub fn is_msvc(self) -> bool {
577 #[cfg(windows)]
578 {
579 matches!(self, Self::Msvc)
580 }
581 #[cfg(not(windows))]
582 {
583 false
584 }
585 }
586
587 pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
589 #[cfg(windows)]
590 if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
591 return Ok(Self::Msvc);
592 }
593 if probe.contains("Apple clang version") {
594 Ok(Self::AppleClang)
595 } else if probe.contains("clang version") {
596 Ok(Self::Clang)
597 } else if probe.contains("gcc version") {
598 Ok(Self::Gcc)
599 } else {
600 Err(CcBypassReason::UnsupportedCompilerDriver(
601 probe.lines().next().unwrap_or_default().into(),
602 ))
603 }
604 }
605}
606
607#[derive(Debug, Clone, PartialEq, Eq)]
609pub struct CcCompilerIdentity {
610 pub family: CcCompilerFamily,
612 pub version_text: String,
614 pub target: String,
616 pub assembler: String,
622}
623
624#[derive(Debug, Clone, PartialEq, Eq)]
626pub struct CcActionInput {
627 pub path: PathBuf,
630 pub digest: CacheDigest,
632}
633
634#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct CcActionContext {
637 pub compiler: CcCompilerIdentity,
639 pub working_dir: PathBuf,
641 pub path_mappings: Vec<PathMapping>,
643 pub environment: BTreeMap<String, Option<String>>,
645 pub inputs: Vec<CcActionInput>,
647}
648
649#[derive(Debug, Clone, PartialEq, Eq)]
651pub struct CcAction {
652 pub digest: CacheDigest,
654 pub bytes: Vec<u8>,
656}
657
658#[derive(Debug, Serialize)]
659struct CcCompilerDescriptor {
660 assembler: String,
661 family: String,
662 target: String,
663 version_text: String,
664}
665
666#[derive(Debug, Serialize)]
667struct CcInputDescriptor {
668 digest: CacheDigest,
669 path: String,
670}
671
672#[derive(Debug, Serialize)]
673struct CcActionDescriptor {
674 version: u8,
675 kind: &'static str,
676 adapter_version: u8,
677 #[serde(skip_serializing_if = "Option::is_none")]
678 assembly_input_model: Option<u8>,
679 compiler: CcCompilerDescriptor,
680 arguments: Vec<String>,
681 environment: BTreeMap<String, Option<String>>,
682 inputs: Vec<CcInputDescriptor>,
683}
684
685#[derive(Debug, Serialize)]
686struct CcInvocationDescriptor {
687 version: u8,
688 kind: &'static str,
689 adapter_version: u8,
690 #[serde(skip_serializing_if = "Option::is_none")]
691 assembly_input_model: Option<u8>,
692 compiler: CcCompilerDescriptor,
693 arguments: Vec<String>,
694 required_inputs: Vec<String>,
695}
696
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
700#[serde(deny_unknown_fields)]
701pub struct CcInputPrediction {
702 pub version: u8,
704 pub inputs: Vec<String>,
706 pub environment: Vec<String>,
708 #[serde(default, skip_serializing_if = "is_zero")]
711 pub compiler_duration_ns: u64,
712 #[serde(default, skip_serializing_if = "String::is_empty")]
714 pub source_name: String,
715}
716
717fn is_zero(value: &u64) -> bool {
718 *value == 0
719}
720
721#[derive(Debug, Clone, PartialEq, Eq)]
723enum Argument {
724 Plain(String),
726 Path { flag: String, path: PathBuf },
728 PrefixMap {
730 flag: String,
731 from: PathBuf,
732 to: String,
733 },
734 Source(PathBuf),
736}
737
738#[derive(Debug, Clone, PartialEq, Eq)]
740pub struct CcInvocation {
741 arguments: Vec<Argument>,
742 source: PathBuf,
743 output: PathBuf,
744 include_dirs: Vec<PathBuf>,
745 required_inputs: Vec<PathBuf>,
746 language: CcLanguage,
747 preprocessed_assembly: bool,
748 sysroot: Option<PathBuf>,
749 caller_depfile: Option<CallerDepfile>,
750 dependency_argument_indices: Vec<usize>,
753}
754
755#[derive(Debug, Clone, PartialEq, Eq)]
765#[non_exhaustive]
766pub struct CallerDepfile {
767 pub path: PathBuf,
769 pub targets: Vec<DepfileTarget>,
771 pub user_headers_only: bool,
773 pub phony_targets: bool,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq)]
779pub struct DepfileTarget {
780 pub name: String,
782 pub quoted: bool,
785}
786
787impl CcInvocation {
788 pub fn output_in(&self, working_dir: &Path) -> PathBuf {
794 if self.output.is_absolute() {
795 normalize_components(&self.output)
796 } else {
797 normalize_components(&working_dir.join(&self.output))
798 }
799 }
800
801 pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
803 self.caller_depfile.as_ref()
804 }
805
806 pub fn compiler_arguments(&self, arguments: &[OsString]) -> Vec<OsString> {
811 arguments
812 .iter()
813 .enumerate()
814 .filter(|(index, _)| !self.dependency_argument_indices.contains(index))
815 .map(|(_, argument)| argument.clone())
816 .collect()
817 }
818
819 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
822 Parser::new(arguments).parse()
823 }
824
825 pub fn parse_for(
827 arguments: &[OsString],
828 family: CcCompilerFamily,
829 ) -> Result<Self, CcBypassReason> {
830 if family.is_msvc() {
831 MsvcParser::new(arguments).parse()
832 } else {
833 Self::parse(arguments)
834 }
835 }
836
837 pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
839 MsvcParser::new(arguments).parse()
840 }
841
842 pub fn source(&self) -> &Path {
844 &self.source
845 }
846
847 pub fn output(&self) -> &Path {
849 &self.output
850 }
851
852 pub fn include_dirs(&self) -> &[PathBuf] {
854 &self.include_dirs
855 }
856
857 pub fn required_inputs(&self) -> &[PathBuf] {
859 &self.required_inputs
860 }
861
862 pub fn language(&self) -> CcLanguage {
864 self.language
865 }
866
867 pub fn validate_discovered_inputs<'a>(
874 &self,
875 paths: impl IntoIterator<Item = &'a Path>,
876 ) -> Result<(), CcBypassReason> {
877 if !self.preprocessed_assembly {
878 return Ok(());
879 }
880 for path in paths {
881 if depfile::contains_assembler_input_directive(path)? {
882 return Err(CcBypassReason::AssemblerInputDirective(path.to_path_buf()));
883 }
884 }
885 Ok(())
886 }
887
888 pub fn sysroot(&self) -> Option<&Path> {
890 self.sysroot.as_deref()
891 }
892
893 pub fn source_name(&self) -> String {
895 self.source
896 .file_name()
897 .map(|name| name.to_string_lossy().into_owned())
898 .unwrap_or_default()
899 }
900
901 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
908 vec!["-MD".into(), "-MF".into(), depfile.into()]
909 }
910
911 pub fn dependency_arguments_for(
914 &self,
915 depfile: &Path,
916 family: CcCompilerFamily,
917 ) -> Vec<OsString> {
918 if family.is_msvc() {
919 vec!["/sourceDependencies".into(), depfile.into()]
920 } else {
921 self.dependency_arguments(depfile)
922 }
923 }
924
925 pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
927 vec!["/sourceDependencies".into(), depfile.into()]
928 }
929
930 pub fn invocation_digest(
932 &self,
933 context: &CcActionContext,
934 ) -> Result<CacheDigest, CcBypassReason> {
935 let builder = ActionBuilder::new(self, context.clone());
936 let descriptor = builder.invocation_descriptor()?;
937 let bytes = canonical_json(&descriptor)
938 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
939 Ok(CacheDigest::blake3(&bytes))
940 }
941
942 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
945 ActionBuilder::new(self, context).build()
946 }
947
948 pub fn prediction(
951 &self,
952 context: &CcActionContext,
953 compiler_duration_ns: u64,
954 ) -> Result<CcInputPrediction, CcBypassReason> {
955 let builder = ActionBuilder::new(self, context.clone());
956 let mut inputs = context
957 .inputs
958 .iter()
959 .map(|input| builder.normalize_input_path(&input.path))
960 .collect::<Result<Vec<_>, _>>()?;
961 inputs.sort();
962 inputs.dedup();
963 Ok(CcInputPrediction {
964 version: 1,
965 inputs,
966 environment: context.environment.keys().cloned().collect(),
967 compiler_duration_ns,
968 source_name: self.source_name(),
969 })
970 }
971}
972
973impl CcInputPrediction {
974 pub fn discover(
977 &self,
978 working_dir: &Path,
979 path_mappings: &[PathMapping],
980 digests: &dyn FileDigestCache,
981 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
982 if self.version != 1 {
983 return Err(CcBypassReason::UnsupportedPrediction);
984 }
985 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
986 return Err(CcBypassReason::UnsupportedPrediction);
987 }
988 let mappings = PathMapping::ordered(path_mappings);
989 let mut files = BTreeSet::new();
990 let mut directories = BTreeSet::new();
991 for entry in &self.inputs {
992 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
993 Some(directory) => {
994 directories.insert(denormalize_path(directory, &mappings)?);
995 }
996 None => {
997 files.insert(denormalize_path(entry, &mappings)?);
998 }
999 }
1000 }
1001 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
1002 }
1003}
1004
1005fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
1011 for mapping in mappings {
1012 let prefix = format!("${{{}}}", mapping.placeholder);
1013 let suffix = if value == prefix {
1014 ""
1015 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1016 suffix
1017 } else {
1018 continue;
1019 };
1020 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
1021 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
1022 }
1023 let mut path = normalize_components(&mapping.root);
1024 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1025 return Ok(path);
1026 }
1027 let path = PathBuf::from(value);
1031 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
1032 return Ok(path);
1033 }
1034 Err(CcBypassReason::InvalidPredictedInput(value.into()))
1035}
1036
1037fn safe_suffix(suffix: &str) -> bool {
1038 suffix.is_empty()
1039 || !suffix.split('/').any(|component| {
1040 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
1041 })
1042}
1043
1044pub fn is_system_path(path: &Path) -> bool {
1046 SYSTEM_ROOTS
1047 .iter()
1048 .any(|root| path.starts_with(Path::new(root)))
1049}
1050
1051fn normalize_components(path: &Path) -> PathBuf {
1052 let mut normalized = PathBuf::new();
1053 for component in path.components() {
1054 match component {
1055 Component::CurDir => {}
1056 Component::ParentDir => {
1057 normalized.pop();
1058 }
1059 component => normalized.push(component.as_os_str()),
1060 }
1061 }
1062 normalized
1063}
1064
1065pub fn environment_inputs<F>(
1068 lookup: F,
1069 sysroot: Option<&Path>,
1070) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1071where
1072 F: Fn(&str) -> Option<String>,
1073{
1074 environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
1075}
1076
1077pub fn environment_inputs_for<F>(
1079 lookup: F,
1080 sysroot: Option<&Path>,
1081 family: CcCompilerFamily,
1082) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1083where
1084 F: Fn(&str) -> Option<String>,
1085{
1086 for name in BYPASS_ENVIRONMENT {
1087 if lookup(name).is_some() {
1088 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
1089 }
1090 }
1091 let mut environment = BTreeMap::new();
1092 for name in KEYED_ENVIRONMENT {
1093 if *name == "SDKROOT" && sysroot.is_some() {
1096 continue;
1097 }
1098 environment.insert((*name).to_string(), lookup(name));
1099 }
1100 if family.is_msvc() {
1101 for name in [
1105 "INCLUDE",
1106 "VCToolsVersion",
1107 "WindowsSDKVersion",
1108 "UCRTVersion",
1109 ] {
1110 environment.insert(name.into(), lookup(name));
1111 }
1112 for name in ["CL", "_CL_"] {
1113 if lookup(name).is_some() {
1114 return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
1115 }
1116 }
1117 }
1118 Ok(environment)
1119}
1120
1121struct ActionBuilder<'a> {
1122 invocation: &'a CcInvocation,
1123 context: CcActionContext,
1124 mappings: Vec<PathMapping>,
1125}
1126
1127impl<'a> ActionBuilder<'a> {
1128 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
1129 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1130 let mappings = context.path_mappings.clone();
1131 Self {
1132 invocation,
1133 context,
1134 mappings,
1135 }
1136 }
1137
1138 fn build(self) -> Result<CcAction, CcBypassReason> {
1139 self.validate_mappings()?;
1140 let invocation = self.invocation_descriptor()?;
1141
1142 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1143 for input in &self.context.inputs {
1144 input.digest.validate().map_err(|_| {
1145 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
1146 })?;
1147 let path = self.normalize_input_path(&input.path)?;
1148 if inputs
1149 .insert(path.clone(), input.digest.clone())
1150 .is_some_and(|existing| existing != input.digest)
1151 {
1152 return Err(CcBypassReason::ConflictingInput(path));
1153 }
1154 }
1155 let required = self
1156 .invocation
1157 .required_inputs
1158 .iter()
1159 .map(|path| self.normalize_path(path))
1160 .collect::<Result<BTreeSet<_>, _>>()?;
1161 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1162 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
1163 }
1164 let inputs = inputs
1165 .into_iter()
1166 .map(|(path, digest)| CcInputDescriptor { path, digest })
1167 .collect();
1168 let descriptor = CcActionDescriptor {
1169 version: ACTION_SCHEMA_VERSION,
1170 kind: "cc",
1171 adapter_version: ADAPTER_VERSION,
1172 assembly_input_model: invocation.assembly_input_model,
1173 compiler: invocation.compiler,
1174 arguments: invocation.arguments,
1175 environment: self.context.environment.clone(),
1176 inputs,
1177 };
1178 let bytes = canonical_json(&descriptor)
1179 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1180 let digest = CacheDigest::blake3(&bytes);
1181 Ok(CcAction { digest, bytes })
1182 }
1183
1184 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1185 self.validate_mappings()?;
1186 let arguments = self
1187 .invocation
1188 .arguments
1189 .iter()
1190 .map(|argument| self.normalize_argument(argument))
1191 .collect::<Result<Vec<_>, _>>()?;
1192 let required_inputs = self
1193 .invocation
1194 .required_inputs
1195 .iter()
1196 .map(|path| self.normalize_path(path))
1197 .collect::<Result<BTreeSet<_>, _>>()?
1198 .into_iter()
1199 .collect();
1200 Ok(CcInvocationDescriptor {
1201 version: ACTION_SCHEMA_VERSION,
1202 kind: "cc",
1203 adapter_version: ADAPTER_VERSION,
1204 assembly_input_model: self.invocation.preprocessed_assembly.then_some(1),
1207 compiler: self.compiler_descriptor(),
1208 arguments,
1209 required_inputs,
1210 })
1211 }
1212
1213 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1214 CcCompilerDescriptor {
1215 assembler: self.context.compiler.assembler.clone(),
1216 family: self.context.compiler.family.as_str().into(),
1217 target: self.context.compiler.target.clone(),
1218 version_text: self.context.compiler.version_text.clone(),
1219 }
1220 }
1221
1222 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1223 if !self.context.working_dir.is_absolute() {
1224 return Err(CcBypassReason::RelativeWorkingDirectory(
1225 self.context.working_dir.clone(),
1226 ));
1227 }
1228 let mut roots = BTreeSet::new();
1229 let mut placeholders = BTreeSet::new();
1230 for mapping in &self.mappings {
1231 if !mapping.root.is_absolute() {
1232 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1233 }
1234 if mapping.placeholder.is_empty()
1235 || !mapping
1236 .placeholder
1237 .bytes()
1238 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1239 || !roots.insert(normalize_components(&mapping.root))
1240 || !placeholders.insert(&mapping.placeholder)
1241 {
1242 return Err(CcBypassReason::InvalidPathPlaceholder(
1243 mapping.placeholder.clone(),
1244 ));
1245 }
1246 }
1247 Ok(())
1248 }
1249
1250 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1251 match argument {
1252 Argument::Plain(value) => Ok(value.clone()),
1253 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1254 Argument::PrefixMap { flag, from, to } => {
1255 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1256 }
1257 Argument::Source(path) => Ok(self.normalize_path(path)?),
1258 }
1259 }
1260
1261 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1268 match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
1269 Ok(normalized) => Ok(normalized),
1270 Err(reason) => {
1271 let absolute = absolute_path(path, &self.context.working_dir);
1272 if is_system_path(&absolute) {
1273 return absolute
1274 .to_str()
1275 .map(ToOwned::to_owned)
1276 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1277 }
1278 Err(reason.into())
1279 }
1280 }
1281 }
1282
1283 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1284 match path.to_str().and_then(|path| {
1285 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1286 .map(ToOwned::to_owned)
1287 }) {
1288 Some(directory) => Ok(format!(
1289 "{INCLUDE_MANIFEST_PREFIX}{}",
1290 self.normalize_path(Path::new(&directory))?
1291 )),
1292 None => self.normalize_path(path),
1293 }
1294 }
1295}
1296
1297fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1298 if path.is_absolute() {
1299 normalize_components(path)
1300 } else {
1301 normalize_components(&working_dir.join(path))
1302 }
1303}
1304
1305struct Parser<'a> {
1306 arguments: &'a [OsString],
1307 index: usize,
1308 parsed: Vec<Argument>,
1309 source: Option<PathBuf>,
1310 output: Option<PathBuf>,
1311 include_dirs: Vec<PathBuf>,
1312 required_inputs: Vec<PathBuf>,
1313 sysroot: Option<PathBuf>,
1314 explicit_language: Option<CcLanguage>,
1315 preprocessed_assembly: bool,
1316 compiling: bool,
1317 dependency: DependencyRequest,
1318}
1319
1320#[derive(Debug, Default)]
1322struct DependencyRequest {
1323 user_headers_only: Option<bool>,
1325 file: Option<PathBuf>,
1326 targets: Vec<DepfileTarget>,
1327 phony_targets: bool,
1328 modifier: Option<String>,
1331 indices: Vec<usize>,
1332}
1333
1334impl<'a> Parser<'a> {
1335 fn new(arguments: &'a [OsString]) -> Self {
1336 Self {
1337 arguments,
1338 index: 0,
1339 parsed: Vec::new(),
1340 source: None,
1341 output: None,
1342 include_dirs: Vec::new(),
1343 required_inputs: Vec::new(),
1344 sysroot: None,
1345 explicit_language: None,
1346 preprocessed_assembly: false,
1347 compiling: false,
1348 dependency: DependencyRequest::default(),
1349 }
1350 }
1351
1352 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1353 while self.index < self.arguments.len() {
1354 let value = self.current()?.to_string();
1355 self.index += 1;
1356 if value == "-" {
1357 return Err(CcBypassReason::StandardInput);
1358 }
1359 if let Some(argfile) = value.strip_prefix('@') {
1360 return Err(CcBypassReason::ResponseFile(argfile.into()));
1361 }
1362 if value.starts_with('-') {
1363 self.parse_flag(&value)?;
1364 } else {
1365 self.parse_input(&value)?;
1366 }
1367 }
1368
1369 if !self.compiling {
1370 return Err(CcBypassReason::NotACompile);
1371 }
1372 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1373 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1374 let language = self.language(&source)?;
1375 let preprocessed_assembly = self.preprocessed_assembly
1376 || (self.explicit_language.is_none()
1377 && source.extension().and_then(|value| value.to_str()) == Some("S"));
1378 self.required_inputs.push(source.clone());
1379 let caller_depfile = match self.dependency.user_headers_only {
1380 Some(user_headers_only) => Some(CallerDepfile {
1381 path: self
1382 .dependency
1383 .file
1384 .unwrap_or_else(|| output.with_extension("d")),
1385 targets: if self.dependency.targets.is_empty() {
1386 vec![DepfileTarget {
1387 name: output.to_string_lossy().into_owned(),
1388 quoted: true,
1389 }]
1390 } else {
1391 self.dependency.targets
1392 },
1393 user_headers_only,
1394 phony_targets: self.dependency.phony_targets,
1395 }),
1396 None => match self.dependency.modifier {
1400 Some(modifier) => return Err(CcBypassReason::CallerDependencyFlags(modifier)),
1401 None => None,
1402 },
1403 };
1404 Ok(CcInvocation {
1405 arguments: self.parsed,
1406 source,
1407 output,
1408 include_dirs: self.include_dirs,
1409 required_inputs: self.required_inputs,
1410 language,
1411 preprocessed_assembly,
1412 sysroot: self.sysroot,
1413 caller_depfile,
1414 dependency_argument_indices: self.dependency.indices,
1415 })
1416 }
1417
1418 fn parse_dependency_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1426 let start = self.index - 1;
1427 match option {
1428 "D" => self.dependency.user_headers_only = Some(false),
1429 "MD" => self.dependency.user_headers_only = Some(true),
1430 "P" => {
1431 self.dependency.phony_targets = true;
1432 self.dependency.modifier = Some(value.into());
1433 }
1434 _ if option.starts_with('F') => {
1435 let file = self.take_value("-MF", Some(&option[1..]))?;
1436 self.dependency.file = Some(PathBuf::from(file));
1437 self.dependency.modifier = Some("-MF".into());
1438 }
1439 _ if option.starts_with('T') || option.starts_with('Q') => {
1440 let flag = &value[..3];
1441 let name = self.take_value(flag, Some(&option[1..]))?;
1442 self.dependency.targets.push(DepfileTarget {
1443 name,
1444 quoted: option.starts_with('Q'),
1445 });
1446 self.dependency.modifier = Some(flag.into());
1447 }
1448 _ => return Err(CcBypassReason::CallerDependencyFlags(value.into())),
1449 }
1450 self.dependency.indices.extend(start..self.index);
1451 Ok(())
1452 }
1453
1454 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1455 if let Some(language) = self.explicit_language {
1456 return Ok(language);
1457 }
1458 let extension = source
1459 .extension()
1460 .and_then(|extension| extension.to_str())
1461 .unwrap_or_default();
1462 match extension {
1463 "c" | "S" => Ok(CcLanguage::C),
1469 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1470 _ => Err(CcBypassReason::UnsupportedLanguage(
1471 source.display().to_string(),
1472 )),
1473 }
1474 }
1475
1476 fn current(&self) -> Result<&str, CcBypassReason> {
1477 self.arguments[self.index]
1478 .to_str()
1479 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1480 }
1481
1482 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1483 if let Some(value) = inline
1484 && !value.is_empty()
1485 {
1486 return Ok(value.into());
1487 }
1488 if self.index >= self.arguments.len() {
1489 return Err(CcBypassReason::MissingValue(flag.into()));
1490 }
1491 let value = self.current()?.to_string();
1492 self.index += 1;
1493 Ok(value)
1494 }
1495
1496 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1497 if self.source.is_some() {
1498 return Err(CcBypassReason::MultipleInputs);
1499 }
1500 let path = PathBuf::from(value);
1501 if self.explicit_language.is_none() {
1505 let extension = path
1506 .extension()
1507 .and_then(|extension| extension.to_str())
1508 .unwrap_or_default();
1509 if !matches!(extension, "c" | "S" | "cc" | "cpp" | "cxx" | "c++") {
1510 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1511 }
1512 }
1513 self.source = Some(path.clone());
1514 self.parsed.push(Argument::Source(path));
1515 Ok(())
1516 }
1517
1518 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1519 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1520 return Err(CcBypassReason::CompilerQuery);
1521 }
1522 if matches!(value, "-E" | "-S") {
1523 return Err(CcBypassReason::NonObjectOutput(value.into()));
1524 }
1525 if let Some(option) = value.strip_prefix("-M") {
1526 return self.parse_dependency_flag(value, option);
1527 }
1528 if value.starts_with("-save-temps") {
1529 return Err(CcBypassReason::SaveTemps(value.into()));
1530 }
1531 if value == "--coverage" {
1532 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1533 }
1534 if let Some(options) = value.strip_prefix("-Wa,")
1535 && !options.is_empty()
1536 && options
1537 .split(',')
1538 .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1539 {
1540 self.parsed.push(Argument::Plain(value.into()));
1541 return Ok(());
1542 }
1543 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1544 || value.starts_with("-Wp,")
1545 || value.starts_with("-Wa,")
1546 || value.starts_with("-Wl,")
1547 {
1548 return Err(CcBypassReason::ToolPassthrough(value.into()));
1551 }
1552 if value.starts_with("-include-pch") || value == "-emit-pch" {
1553 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1554 }
1555
1556 if value == "-c" {
1557 self.compiling = true;
1558 self.parsed.push(Argument::Plain(value.into()));
1559 return Ok(());
1560 }
1561 if SUPPORTED_BARE_FLAGS.contains(&value)
1562 || SUPPORTED_O_FLAGS.contains(&value)
1563 || SUPPORTED_G_FLAGS.contains(&value)
1564 || value.starts_with("-std=")
1565 {
1566 self.parsed.push(Argument::Plain(value.into()));
1567 return Ok(());
1568 }
1569 if let Some(rest) = value.strip_prefix("-o") {
1570 let path = self.take_value("-o", Some(rest))?;
1571 self.output = Some(PathBuf::from(&path));
1574 self.parsed.push(Argument::Path {
1575 flag: "-o".into(),
1576 path: PathBuf::from(path),
1577 });
1578 return Ok(());
1579 }
1580 if let Some(rest) = value.strip_prefix("-I") {
1581 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1582 self.include_dirs.push(path.clone());
1583 self.parsed.push(Argument::Path {
1584 flag: "-I".into(),
1585 path,
1586 });
1587 return Ok(());
1588 }
1589 if SEPARATE_PATH_FLAGS.contains(&value) {
1590 let path = PathBuf::from(self.take_value(value, None)?);
1591 match value {
1592 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1593 "-isysroot" => self.sysroot = Some(path.clone()),
1594 _ => {}
1600 }
1601 self.parsed.push(Argument::Path {
1602 flag: value.into(),
1603 path,
1604 });
1605 return Ok(());
1606 }
1607 if let Some(rest) = value.strip_prefix("--include=") {
1610 let path = PathBuf::from(rest);
1611 self.parsed.push(Argument::Path {
1612 flag: "-include".into(),
1613 path,
1614 });
1615 return Ok(());
1616 }
1617 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1618 value
1619 .strip_prefix(&format!("{flag}="))
1620 .map(|rest| (*flag, rest))
1621 }) {
1622 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1623 self.parsed.push(Argument::PrefixMap {
1624 flag: flag.into(),
1625 from: PathBuf::from(from),
1626 to: to.into(),
1627 });
1628 return Ok(());
1629 }
1630 if value == "--param" {
1632 let parameter = self.take_value("--param", None)?;
1633 self.parsed
1634 .push(Argument::Plain(format!("--param={parameter}")));
1635 return Ok(());
1636 }
1637 if let Some(parameter) = value.strip_prefix("--param=") {
1638 self.parsed
1639 .push(Argument::Plain(format!("--param={parameter}")));
1640 return Ok(());
1641 }
1642 if let Some(rest) = value.strip_prefix("--sysroot=") {
1643 let path = PathBuf::from(rest);
1644 self.sysroot = Some(path.clone());
1645 self.parsed.push(Argument::Path {
1646 flag: "--sysroot".into(),
1647 path,
1648 });
1649 return Ok(());
1650 }
1651 if let Some(rest) = value
1652 .strip_prefix("-D")
1653 .or_else(|| value.strip_prefix("-U"))
1654 {
1655 let flag = &value[..2];
1656 let definition = self.take_value(flag, Some(rest))?;
1657 self.parsed
1658 .push(Argument::Plain(format!("{flag}{definition}")));
1659 return Ok(());
1660 }
1661 if let Some(rest) = value.strip_prefix("-x") {
1662 let language = self.take_value("-x", Some(rest))?;
1663 self.explicit_language = Some(match language.as_str() {
1664 "c" => CcLanguage::C,
1665 "assembler-with-cpp" => {
1666 self.preprocessed_assembly = true;
1667 CcLanguage::C
1668 }
1669 "c++" => CcLanguage::Cxx,
1670 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1671 });
1672 self.parsed.push(Argument::Plain(format!("-x{language}")));
1673 return Ok(());
1674 }
1675 if let Some(target) = value.strip_prefix("--target=") {
1676 self.parsed
1677 .push(Argument::Plain(format!("--target={target}")));
1678 return Ok(());
1679 }
1680 if value == "-target" {
1681 let target = self.take_value("-target", None)?;
1682 self.parsed
1683 .push(Argument::Plain(format!("--target={target}")));
1684 return Ok(());
1685 }
1686 if value == "-arch" {
1687 let arch = self.take_value("-arch", None)?;
1688 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1689 return Ok(());
1690 }
1691 if let Some(option) = value.strip_prefix("-f") {
1692 return self.parse_f_flag(value, option);
1693 }
1694 if let Some(option) = value.strip_prefix("-m") {
1695 return self.parse_m_flag(value, option);
1696 }
1697 if value.starts_with("-g") {
1698 return Err(if value.starts_with("-gsplit-dwarf") {
1701 CcBypassReason::SplitDebugOutput(value.into())
1702 } else {
1703 CcBypassReason::UnknownFlag(value.into())
1704 });
1705 }
1706 if value.starts_with("-W") {
1707 self.parsed.push(Argument::Plain(value.into()));
1711 return Ok(());
1712 }
1713 Err(CcBypassReason::UnknownFlag(value.into()))
1714 }
1715
1716 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1717 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1718 return Err(CcBypassReason::Plugin(value.into()));
1719 }
1720 if option.starts_with("profile-") || option == "test-coverage" {
1721 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1722 }
1723 let name = option.split_once('=').map_or(option, |(name, _)| name);
1724 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1725 return Err(CcBypassReason::UnknownFlag(value.into()));
1726 }
1727 self.parsed.push(Argument::Plain(value.into()));
1728 Ok(())
1729 }
1730
1731 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1732 if option == "llvm" {
1733 return Err(CcBypassReason::ToolPassthrough(value.into()));
1734 }
1735 if let Some((name, selection)) = option.split_once('=')
1740 && matches!(name, "arch" | "cpu" | "tune")
1741 && matches!(selection, "native" | "host")
1742 {
1743 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1744 }
1745 let name = option.split_once('=').map_or(option, |(name, _)| name);
1746 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1747 return Err(CcBypassReason::UnknownFlag(value.into()));
1748 }
1749 self.parsed.push(Argument::Plain(value.into()));
1750 Ok(())
1751 }
1752}
1753
1754struct MsvcParser<'a> {
1758 arguments: &'a [OsString],
1759 index: usize,
1760 parsed: Vec<Argument>,
1761 source: Option<PathBuf>,
1762 output: Option<PathBuf>,
1763 include_dirs: Vec<PathBuf>,
1764 required_inputs: Vec<PathBuf>,
1765 explicit_language: Option<CcLanguage>,
1766 compiling: bool,
1767}
1768
1769impl<'a> MsvcParser<'a> {
1770 fn new(arguments: &'a [OsString]) -> Self {
1771 Self {
1772 arguments,
1773 index: 0,
1774 parsed: Vec::new(),
1775 source: None,
1776 output: None,
1777 include_dirs: Vec::new(),
1778 required_inputs: Vec::new(),
1779 explicit_language: None,
1780 compiling: false,
1781 }
1782 }
1783
1784 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1785 while self.index < self.arguments.len() {
1786 let value = self.current()?.to_owned();
1787 self.index += 1;
1788 if let Some(file) = value.strip_prefix('@') {
1789 return Err(CcBypassReason::ResponseFile(file.into()));
1790 }
1791 if value.starts_with('/') || value.starts_with('-') {
1792 self.parse_flag(&value)?;
1793 } else {
1794 self.add_source(&value)?;
1795 }
1796 }
1797 if !self.compiling {
1798 return Err(CcBypassReason::NotACompile);
1799 }
1800 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1801 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1802 let language = self.explicit_language.unwrap_or_else(|| {
1803 if source
1804 .extension()
1805 .and_then(|value| value.to_str())
1806 .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1807 {
1808 CcLanguage::C
1809 } else {
1810 CcLanguage::Cxx
1811 }
1812 });
1813 self.required_inputs.push(source.clone());
1814 Ok(CcInvocation {
1815 arguments: self.parsed,
1816 source,
1817 output,
1818 include_dirs: self.include_dirs,
1819 required_inputs: self.required_inputs,
1820 language,
1821 preprocessed_assembly: false,
1822 sysroot: None,
1823 caller_depfile: None,
1824 dependency_argument_indices: Vec::new(),
1825 })
1826 }
1827
1828 fn current(&self) -> Result<&str, CcBypassReason> {
1829 self.arguments[self.index]
1830 .to_str()
1831 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1832 }
1833
1834 fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
1835 if !attached.is_empty() {
1836 return Ok(attached.into());
1837 }
1838 if self.index == self.arguments.len() {
1839 return Err(CcBypassReason::MissingValue(flag.into()));
1840 }
1841 let value = self.current()?.to_owned();
1842 self.index += 1;
1843 Ok(value)
1844 }
1845
1846 fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
1847 if self.source.is_some() {
1848 return Err(CcBypassReason::MultipleInputs);
1849 }
1850 let path = PathBuf::from(value);
1851 if self.explicit_language.is_none()
1852 && !path
1853 .extension()
1854 .and_then(|value| value.to_str())
1855 .is_some_and(|value| {
1856 matches!(
1857 value.to_ascii_lowercase().as_str(),
1858 "c" | "cc" | "cpp" | "cxx"
1859 )
1860 })
1861 {
1862 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1863 }
1864 self.source = Some(path.clone());
1865 self.parsed.push(Argument::Source(path));
1866 Ok(())
1867 }
1868
1869 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1870 let option = value.trim_start_matches(['/', '-']);
1871 let lower = option.to_ascii_lowercase();
1872 if matches!(lower.as_str(), "?" | "help") {
1873 return Err(CcBypassReason::CompilerQuery);
1874 }
1875 if lower == "showincludes" || lower.starts_with("sourcedependencies") {
1876 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1877 }
1878 if matches!(lower.as_str(), "e" | "ep" | "p") {
1879 return Err(CcBypassReason::NonObjectOutput(value.into()));
1880 }
1881 if (lower.starts_with("fa") && !lower.starts_with("favor:"))
1882 || lower.starts_with("fd")
1883 || lower.starts_with("zi")
1884 {
1885 return Err(CcBypassReason::SplitDebugOutput(value.into()));
1886 }
1887 if lower.starts_with("yc")
1888 || lower.starts_with("yu")
1889 || (lower.starts_with("fp") && !lower.starts_with("fp:"))
1890 {
1891 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1892 }
1893 if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
1894 return Err(CcBypassReason::ToolPassthrough(value.into()));
1895 }
1896 if matches!(lower.as_str(), "ld" | "ldd") {
1897 return Err(CcBypassReason::NotACompile);
1898 }
1899 if lower == "c" {
1900 self.compiling = true;
1901 self.parsed.push(Argument::Plain("/c".into()));
1902 return Ok(());
1903 }
1904 for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
1905 if let Some(attached) = option.strip_prefix(prefix) {
1906 let path = PathBuf::from(self.value(canonical, attached)?);
1907 if prefix == "Fo" {
1908 self.output = Some(path.clone());
1909 } else if prefix == "I" {
1910 self.include_dirs.push(path.clone());
1911 }
1912 self.parsed.push(Argument::Path {
1913 flag: canonical.into(),
1914 path,
1915 });
1916 return Ok(());
1917 }
1918 }
1919 if lower.starts_with("external:i") {
1920 let path = PathBuf::from(self.value("/external:I", &option[10..])?);
1921 self.include_dirs.push(path.clone());
1922 self.parsed.push(Argument::Path {
1923 flag: "/external:I".into(),
1924 path,
1925 });
1926 return Ok(());
1927 }
1928 if option.starts_with("Tc") || option.starts_with("Tp") {
1929 let c = option.starts_with("Tc");
1930 let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
1931 self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
1932 return self.add_source(&path);
1933 }
1934 if lower.starts_with("pathmap:") {
1935 let rest = &option[8..];
1936 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1937 self.parsed.push(Argument::PrefixMap {
1938 flag: "/pathmap".into(),
1939 from: PathBuf::from(from),
1940 to: to.into(),
1941 });
1942 return Ok(());
1943 }
1944 if matches!(option, "D" | "U") {
1945 let definition = self.value(value, "")?;
1946 self.parsed
1947 .push(Argument::Plain(format!("/{option}{definition}")));
1948 return Ok(());
1949 }
1950 let definition = option.starts_with('D') || option.starts_with('U');
1953 let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
1954 || lower
1955 .strip_prefix('w')
1956 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1957 || ["wd", "we", "wo"].iter().any(|prefix| {
1958 lower
1959 .strip_prefix(prefix)
1960 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1961 });
1962 let admitted = definition
1963 || warning
1964 || lower.starts_with("std:")
1965 || lower.starts_with("arch:")
1966 || lower.starts_with("favor:")
1967 || lower.starts_with("volatile:")
1968 || lower.starts_with("fp:")
1969 || lower.starts_with("eh")
1970 || lower.starts_with('o')
1971 || lower.starts_with("ob")
1972 || lower.starts_with("oi")
1973 || lower.starts_with("ot")
1974 || lower.starts_with("oy")
1975 || lower.starts_with("gs")
1976 || lower.starts_with("gr")
1977 || lower.starts_with("gy")
1978 || lower.starts_with("gw")
1979 || lower.starts_with("gl")
1980 || lower.starts_with("zc:")
1981 || lower.starts_with("diagnostics:")
1982 || matches!(
1983 lower.as_str(),
1984 "nologo"
1985 | "brepro"
1986 | "bigobj"
1987 | "utf-8"
1988 | "permissive-"
1989 | "z7"
1990 | "md"
1991 | "mdd"
1992 | "mt"
1993 | "mtd"
1994 );
1995 if admitted {
1996 self.parsed.push(Argument::Plain(value.into()));
1997 return Ok(());
1998 }
1999 Err(CcBypassReason::UnknownFlag(value.into()))
2000 }
2001}
2002
2003#[cfg(test)]
2004#[path = "cc_cache_tests.rs"]
2005mod tests;