1#![deny(missing_docs)]
19
20use mbx_cache_core::{
21 CacheDigest, FileDigestCache, PathAliases, PathMapping, PathNormalizationError, canonical_json,
22 normalize_resolved_mapped_path_with, resolve_path_mappings,
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 "-gfull",
243 "-ggdb",
244 "-ggdb0",
245 "-ggdb1",
246 "-ggdb2",
247 "-ggdb3",
248];
249
250const SUPPORTED_BARE_FLAGS: &[&str] = &[
251 "-P", "-ansi",
253 "-nostdinc",
254 "-nostdinc++",
255 "-pedantic",
256 "-pedantic-errors",
257 "-pipe",
258 "-pthread",
259 "-w",
260];
261
262const SEPARATE_PATH_FLAGS: &[&str] = &[
263 "-idirafter",
264 "-imacros",
265 "-include",
266 "-iquote",
267 "-isysroot",
268 "-isystem",
269];
270
271const TOOL_PASSTHROUGH_FLAGS: &[&str] = &["-Xassembler", "-Xclang", "-Xlinker", "-Xpreprocessor"];
272
273const SUPPORTED_ASSEMBLER_OPTIONS: &[&str] = &["--noexecstack"];
278
279const COMPILER_QUERY_FLAGS: &[&str] = &[
280 "--help",
281 "--version",
282 "-###",
283 "-?",
286 "-dumpmachine",
287 "-dumpversion",
288 "-v",
289];
290
291const PREFIX_MAP_FLAGS: &[&str] = &[
296 "-fdebug-prefix-map",
297 "-ffile-prefix-map",
298 "-fmacro-prefix-map",
299];
300
301impl CcBypassReason {
302 pub fn kind(&self) -> &'static str {
307 self.into()
308 }
309
310 pub fn remediation(&self) -> Option<&'static str> {
317 match self {
318 Self::UnsupportedEnvironment(_) => Some(
319 "Unset the reported environment variable for this build so the compiler invocation describes all of its inputs.",
320 ),
321 Self::LocalCpuTarget(_) => Some(
322 "Replace the reported local-CPU option with an explicit architecture or CPU name.",
323 ),
324 Self::EmbeddedTimestampMacro(_) => Some(
325 "Remove the reported timestamp macro, or keep this compilation uncached if its changing value is intentional.",
326 ),
327 Self::SearchPathModifiedDuringCompilation(_) => Some(
328 "Generate headers before compilation instead of changing an include directory while the compiler is running.",
329 ),
330 Self::UnknownFlag(_) | Self::ToolPassthrough(_) => Some(
331 "Upgrade mbx or report the unmodeled compiler option. If you control the build script, removing the option can also make the compilation cacheable.",
332 ),
333 Self::UnmappedAbsolutePath(_) => Some(
334 "Move the input under a mapped project or system root, or keep this compilation uncached.",
335 ),
336 _ => None,
337 }
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
346#[strum(serialize_all = "kebab-case")]
347#[non_exhaustive]
348pub enum CcBypassReason {
349 #[error("compiler argument {index} is not valid UTF-8")]
351 NonUtf8Argument {
352 index: usize,
354 },
355 #[error("compiler response file is not modeled by the cache adapter: {0}")]
357 ResponseFile(String),
358 #[error("compiler flag is not modeled by the cache adapter: {0}")]
360 UnknownFlag(String),
361 #[error("compiler flag {0} is missing its value")]
363 MissingValue(String),
364 #[error("compiler invocation queries the driver instead of compiling")]
366 CompilerQuery,
367 #[error("compiler invocation does not compile with -c")]
369 NotACompile,
370 #[error("compiler invocation emits a non-object output: {0}")]
372 NonObjectOutput(String),
373 #[error("compiler invocation reads its source from standard input")]
375 StandardInput,
376 #[error("compiler invocation names no source file")]
378 MissingInput,
379 #[error("compiler invocation names more than one source file")]
381 MultipleInputs,
382 #[error("compiler invocation names no output file")]
384 MissingOutput,
385 #[error("compiler input language is not modeled by the cache adapter: {0}")]
387 UnsupportedLanguage(String),
388 #[error("compiler invocation requests its own dependency output: {0}")]
390 CallerDependencyFlags(String),
391 #[error("precompiled headers are not modeled by the cache adapter: {0}")]
393 PrecompiledHeader(String),
394 #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
396 CoverageInstrumentation(String),
397 #[error("split debug output is not modeled by the cache adapter: {0}")]
399 SplitDebugOutput(String),
400 #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
402 SaveTemps(String),
403 #[error("compiler flag forwards options to another tool: {0}")]
405 ToolPassthrough(String),
406 #[error("compiler plugins are not modeled by the cache adapter: {0}")]
408 Plugin(String),
409 #[error("include search directory changed during the compilation: {0}")]
412 SearchPathModifiedDuringCompilation(PathBuf),
413
414 #[error("compilation output records a path its key normalized away: {0}")]
421 UnportableOutput(PathBuf),
422 #[error("storing path-specific C objects is disabled by MBX_CC_STORE_PATH_SPECIFIC")]
424 PathSpecificStorageDisabled,
425 #[error("compiler flag tunes for the local CPU: {0}")]
427 LocalCpuTarget(String),
428 #[error("compiler driver is not modeled by the cache adapter: {0}")]
430 UnsupportedCompilerDriver(String),
431 #[error("could not establish compiler identity: {0}")]
433 CompilerIdentityUnavailable(String),
434 #[error("environment variable {0} changes the compilation in an unmodeled way")]
436 UnsupportedEnvironment(String),
437 #[error("no real compiler was pinned for the cc shim")]
439 RealCompilerUnpinned,
440 #[error("input expands a timestamp macro: {0}")]
443 EmbeddedTimestampMacro(PathBuf),
444 #[error("preprocessed assembly uses an assembler input directive in {0}")]
447 AssemblerInputDirective(PathBuf),
448 #[error("could not model the compiler depfile: {0}")]
450 MalformedDepfile(String),
451 #[error("could not read the compiler depfile {path}: {message}")]
453 DepfileRead {
454 path: PathBuf,
456 message: String,
458 },
459 #[error("compilation reads more inputs than the cache adapter models")]
461 TooManyInputs,
462 #[error("path is outside every modeled root: {0}")]
464 UnmappedAbsolutePath(PathBuf),
465 #[error("path is not valid UTF-8: {0}")]
467 NonUtf8Path(PathBuf),
468 #[error("compiler working directory is not absolute: {0}")]
470 RelativeWorkingDirectory(PathBuf),
471 #[error("path mapping root is not absolute: {0}")]
473 RelativePathMapping(PathBuf),
474 #[error("invalid path mapping placeholder: {0}")]
476 InvalidPathPlaceholder(String),
477 #[error("required input is missing from the discovered inputs: {0}")]
479 MissingRequiredInput(String),
480 #[error("invalid digest for input: {0}")]
482 InvalidInputDigest(String),
483 #[error("conflicting digests for input: {0}")]
485 ConflictingInput(String),
486 #[error("could not read input {path}: {message}")]
488 InputRead {
489 path: PathBuf,
491 message: String,
493 },
494 #[error("input changed during the compilation: {0}")]
496 InputChanged(PathBuf),
497 #[error("input was modified during the compilation: {0}")]
499 InputModifiedDuringCompilation(PathBuf),
500 #[error("discovered inputs use a different working directory")]
502 DiscoveryWorkingDirectory,
503 #[error("action prediction is not modeled by this adapter version")]
505 UnsupportedPrediction,
506 #[error("invalid predicted input: {0}")]
508 InvalidPredictedInput(String),
509 #[error("could not serialize the action descriptor: {0}")]
511 Serialization(String),
512}
513
514impl From<PathNormalizationError> for CcBypassReason {
515 fn from(reason: PathNormalizationError) -> Self {
516 match reason {
517 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
518 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
519 }
520 }
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum CcLanguage {
526 C,
528 Cxx,
530}
531
532impl CcLanguage {
533 pub fn shim_stem(self) -> &'static str {
535 match self {
536 Self::C => "mbx-cc",
537 Self::Cxx => "mbx-cxx",
538 }
539 }
540
541 pub fn default_driver(self) -> &'static str {
543 if cfg!(windows) {
544 return "cl.exe";
545 }
546 match self {
547 Self::C => "cc",
548 Self::Cxx => "c++",
549 }
550 }
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub enum CcCompilerFamily {
556 Gcc,
558 Clang,
560 AppleClang,
562 #[cfg(windows)]
564 Msvc,
565}
566
567impl CcCompilerFamily {
568 pub fn as_str(self) -> &'static str {
570 match self {
571 Self::Gcc => "gcc",
572 Self::Clang => "clang",
573 Self::AppleClang => "apple-clang",
574 #[cfg(windows)]
575 Self::Msvc => "msvc",
576 }
577 }
578
579 pub fn uses_external_assembler(self) -> bool {
582 matches!(self, Self::Gcc)
583 }
584
585 pub fn is_msvc(self) -> bool {
587 #[cfg(windows)]
588 {
589 matches!(self, Self::Msvc)
590 }
591 #[cfg(not(windows))]
592 {
593 false
594 }
595 }
596
597 pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
599 #[cfg(windows)]
600 if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
601 return Ok(Self::Msvc);
602 }
603 if probe.contains("Apple clang version") {
604 Ok(Self::AppleClang)
605 } else if probe.contains("clang version") {
606 Ok(Self::Clang)
607 } else if probe.contains("gcc version") {
608 Ok(Self::Gcc)
609 } else {
610 Err(CcBypassReason::UnsupportedCompilerDriver(
611 probe.lines().next().unwrap_or_default().into(),
612 ))
613 }
614 }
615}
616
617#[derive(Debug, Clone, PartialEq, Eq)]
619pub struct CcCompilerIdentity {
620 pub family: CcCompilerFamily,
622 pub version_text: String,
624 pub target: String,
626 pub assembler: String,
632}
633
634#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct CcActionInput {
637 pub path: PathBuf,
640 pub digest: CacheDigest,
642}
643
644#[derive(Debug, Clone, PartialEq, Eq)]
646pub struct CcActionContext {
647 pub compiler: CcCompilerIdentity,
649 pub working_dir: PathBuf,
651 pub path_mappings: Vec<PathMapping>,
653 pub environment: BTreeMap<String, Option<String>>,
655 pub inputs: Vec<CcActionInput>,
657}
658
659#[derive(Debug, Clone, PartialEq, Eq)]
661pub struct CcAction {
662 pub digest: CacheDigest,
664 pub bytes: Vec<u8>,
666}
667
668#[derive(Debug, Serialize)]
669struct CcCompilerDescriptor {
670 assembler: String,
671 family: String,
672 target: String,
673 version_text: String,
674}
675
676#[derive(Debug, Serialize)]
677struct CcInputDescriptor {
678 digest: CacheDigest,
679 path: String,
680}
681
682#[derive(Debug, Serialize)]
683struct CcActionDescriptor {
684 #[serde(skip_serializing_if = "Option::is_none")]
685 path_binding: Option<CcPathBinding>,
686 version: u8,
687 kind: &'static str,
688 adapter_version: u8,
689 #[serde(skip_serializing_if = "Option::is_none")]
690 assembly_input_model: Option<u8>,
691 #[serde(skip_serializing_if = "Option::is_none")]
692 preprocessing: Option<PreprocessingContext>,
693 compiler: CcCompilerDescriptor,
694 arguments: Vec<String>,
695 environment: BTreeMap<String, Option<String>>,
696 inputs: Vec<CcInputDescriptor>,
697}
698
699#[derive(Debug, Serialize)]
700struct CcInvocationDescriptor {
701 version: u8,
702 kind: &'static str,
703 adapter_version: u8,
704 #[serde(skip_serializing_if = "Option::is_none")]
705 assembly_input_model: Option<u8>,
706 #[serde(skip_serializing_if = "Option::is_none")]
707 preprocessing: Option<PreprocessingContext>,
708 compiler: CcCompilerDescriptor,
709 arguments: Vec<String>,
710 required_inputs: Vec<String>,
711}
712
713#[derive(Debug, Serialize)]
716struct PreprocessingContext {
717 working_dir: PathBuf,
718 arguments: Vec<String>,
719 roots: BTreeMap<String, PathBuf>,
720}
721
722#[derive(Debug, Serialize)]
724struct CcPathBinding {
725 working_dir: PathBuf,
726 arguments: Vec<Argument>,
727 roots: BTreeMap<String, PathBuf>,
728}
729
730#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
733#[serde(deny_unknown_fields)]
734pub struct CcInputPrediction {
735 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
737 pub path_specific: bool,
738 pub version: u8,
740 pub inputs: Vec<String>,
742 pub environment: Vec<String>,
744 #[serde(default, skip_serializing_if = "is_zero")]
747 pub compiler_duration_ns: u64,
748 #[serde(default, skip_serializing_if = "String::is_empty")]
750 pub source_name: String,
751}
752
753fn is_zero(value: &u64) -> bool {
754 *value == 0
755}
756
757#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
759enum Argument {
760 Plain(String),
762 Path { flag: String, path: PathBuf },
764 PrefixMap {
766 flag: String,
767 from: PathBuf,
768 to: String,
769 },
770 Source(PathBuf),
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
776pub struct CcInvocation {
777 arguments: Vec<Argument>,
778 source: PathBuf,
779 output: PathBuf,
780 include_dirs: Vec<PathBuf>,
781 required_inputs: Vec<PathBuf>,
782 language: CcLanguage,
783 preprocessed_assembly: bool,
784 preprocessing_arguments: Option<Vec<String>>,
785 sysroot: Option<PathBuf>,
786 caller_depfile: Option<CallerDepfile>,
787 dependency_argument_indices: Vec<usize>,
790}
791
792#[derive(Debug, Clone, PartialEq, Eq)]
802#[non_exhaustive]
803pub struct CallerDepfile {
804 pub path: PathBuf,
806 pub targets: Vec<DepfileTarget>,
808 pub user_headers_only: bool,
810 pub phony_targets: bool,
812}
813
814#[derive(Debug, Clone, PartialEq, Eq)]
816pub struct DepfileTarget {
817 pub name: String,
819 pub quoted: bool,
822}
823
824impl CcInvocation {
825 pub fn output_in(&self, working_dir: &Path) -> PathBuf {
831 if self.output.is_absolute() {
832 normalize_components(&self.output)
833 } else {
834 normalize_components(&working_dir.join(&self.output))
835 }
836 }
837
838 pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
840 self.caller_depfile.as_ref()
841 }
842
843 pub fn compiler_arguments(&self, arguments: &[OsString]) -> Vec<OsString> {
848 arguments
849 .iter()
850 .enumerate()
851 .filter(|(index, _)| !self.dependency_argument_indices.contains(index))
852 .map(|(_, argument)| argument.clone())
853 .collect()
854 }
855
856 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
859 Parser::new(arguments).parse()
860 }
861
862 pub fn parse_for(
864 arguments: &[OsString],
865 family: CcCompilerFamily,
866 ) -> Result<Self, CcBypassReason> {
867 if family.is_msvc() {
868 MsvcParser::new(arguments).parse()
869 } else {
870 let mut parser = Parser::new(arguments);
871 parser.family = family;
872 parser.parse()
873 }
874 }
875
876 pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
878 MsvcParser::new(arguments).parse()
879 }
880
881 pub fn source(&self) -> &Path {
883 &self.source
884 }
885
886 pub fn is_preprocessing(&self) -> bool {
888 self.preprocessing_arguments.is_some()
889 }
890
891 pub fn output(&self) -> &Path {
893 &self.output
894 }
895
896 pub fn include_dirs(&self) -> &[PathBuf] {
898 &self.include_dirs
899 }
900
901 pub fn required_inputs(&self) -> &[PathBuf] {
903 &self.required_inputs
904 }
905
906 pub fn language(&self) -> CcLanguage {
908 self.language
909 }
910
911 pub fn validate_discovered_inputs<'a>(
918 &self,
919 paths: impl IntoIterator<Item = &'a Path>,
920 ) -> Result<(), CcBypassReason> {
921 if !self.preprocessed_assembly {
922 return Ok(());
923 }
924 for path in paths {
925 if depfile::contains_assembler_input_directive(path)? {
926 return Err(CcBypassReason::AssemblerInputDirective(path.to_path_buf()));
927 }
928 }
929 Ok(())
930 }
931
932 pub fn sysroot(&self) -> Option<&Path> {
934 self.sysroot.as_deref()
935 }
936
937 pub fn source_name(&self) -> String {
939 self.source
940 .file_name()
941 .map(|name| name.to_string_lossy().into_owned())
942 .unwrap_or_default()
943 }
944
945 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
952 vec!["-MD".into(), "-MF".into(), depfile.into()]
953 }
954
955 pub fn dependency_arguments_for(
958 &self,
959 depfile: &Path,
960 family: CcCompilerFamily,
961 ) -> Vec<OsString> {
962 if family.is_msvc() {
963 vec!["/sourceDependencies".into(), depfile.into()]
964 } else {
965 self.dependency_arguments(depfile)
966 }
967 }
968
969 pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
971 vec!["/sourceDependencies".into(), depfile.into()]
972 }
973
974 pub fn invocation_digest(
976 &self,
977 context: &CcActionContext,
978 ) -> Result<CacheDigest, CcBypassReason> {
979 let builder = ActionBuilder::new(self, context.clone());
980 let descriptor = builder.invocation_descriptor()?;
981 let bytes = canonical_json(&descriptor)
982 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
983 Ok(CacheDigest::blake3(&bytes))
984 }
985
986 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
989 self.action_with_path_binding(context, false)
990 }
991
992 pub fn action_with_path_binding(
995 &self,
996 context: CcActionContext,
997 path_specific: bool,
998 ) -> Result<CcAction, CcBypassReason> {
999 ActionBuilder::new(self, context).build(path_specific)
1000 }
1001
1002 pub fn prediction(
1005 &self,
1006 context: &CcActionContext,
1007 compiler_duration_ns: u64,
1008 ) -> Result<CcInputPrediction, CcBypassReason> {
1009 let builder = ActionBuilder::new(self, context.clone());
1010 let mut inputs = context
1011 .inputs
1012 .iter()
1013 .map(|input| builder.normalize_input_path(&input.path))
1014 .collect::<Result<Vec<_>, _>>()?;
1015 inputs.sort();
1016 inputs.dedup();
1017 Ok(CcInputPrediction {
1018 path_specific: false,
1019 version: 1,
1020 inputs,
1021 environment: context.environment.keys().cloned().collect(),
1022 compiler_duration_ns,
1023 source_name: self.source_name(),
1024 })
1025 }
1026}
1027
1028impl CcInputPrediction {
1029 pub fn discover(
1032 &self,
1033 working_dir: &Path,
1034 path_mappings: &[PathMapping],
1035 digests: &dyn FileDigestCache,
1036 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
1037 if self.version != 1 {
1038 return Err(CcBypassReason::UnsupportedPrediction);
1039 }
1040 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1041 return Err(CcBypassReason::UnsupportedPrediction);
1042 }
1043 let mappings = PathMapping::ordered(path_mappings);
1044 let mut files = BTreeSet::new();
1045 let mut directories = BTreeSet::new();
1046 for entry in &self.inputs {
1047 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
1048 Some(directory) => {
1049 directories.insert(denormalize_path(directory, &mappings)?);
1050 }
1051 None => {
1052 files.insert(denormalize_path(entry, &mappings)?);
1053 }
1054 }
1055 }
1056 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
1057 }
1058}
1059
1060fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
1066 for mapping in mappings {
1067 let prefix = format!("${{{}}}", mapping.placeholder);
1068 let suffix = if value == prefix {
1069 ""
1070 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1071 suffix
1072 } else {
1073 continue;
1074 };
1075 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
1076 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
1077 }
1078 let mut path = normalize_components(&mapping.root);
1079 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1080 return Ok(path);
1081 }
1082 let path = PathBuf::from(value);
1086 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
1087 return Ok(path);
1088 }
1089 Err(CcBypassReason::InvalidPredictedInput(value.into()))
1090}
1091
1092fn safe_suffix(suffix: &str) -> bool {
1093 suffix.is_empty()
1094 || !suffix.split('/').any(|component| {
1095 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
1096 })
1097}
1098
1099pub fn is_system_path(path: &Path) -> bool {
1101 SYSTEM_ROOTS
1102 .iter()
1103 .any(|root| path.starts_with(Path::new(root)))
1104 || developer_roots().iter().any(|root| path.starts_with(root))
1105}
1106
1107#[cfg(target_os = "macos")]
1111fn developer_roots() -> &'static [PathBuf] {
1112 static ROOTS: std::sync::OnceLock<Vec<PathBuf>> = std::sync::OnceLock::new();
1113 ROOTS.get_or_init(|| {
1114 let selected = std::env::var_os("DEVELOPER_DIR")
1115 .filter(|value| !value.is_empty())
1116 .map(PathBuf::from)
1117 .or_else(|| {
1118 let output = std::process::Command::new("/usr/bin/xcode-select")
1119 .arg("--print-path")
1120 .output()
1121 .ok()?;
1122 if !output.status.success() {
1123 return None;
1124 }
1125 Some(PathBuf::from(
1126 std::str::from_utf8(&output.stdout).ok()?.trim(),
1127 ))
1128 });
1129 selected.map(developer_directory_roots).unwrap_or_default()
1130 })
1131}
1132
1133#[cfg(not(target_os = "macos"))]
1134fn developer_roots() -> &'static [PathBuf] {
1135 &[]
1136}
1137
1138#[cfg(any(target_os = "macos", all(test, unix)))]
1139fn developer_directory_roots(mut root: PathBuf) -> Vec<PathBuf> {
1140 if !root.is_absolute() || root == Path::new("/") {
1141 return Vec::new();
1142 }
1143 if root.extension().is_some_and(|extension| extension == "app") {
1145 root = root.join("Contents/Developer");
1146 }
1147 root = normalize_components(&root);
1148 if root == Path::new("/") {
1149 return Vec::new();
1150 }
1151 let mut roots = vec![root.clone()];
1152 if let Ok(canonical) = std::fs::canonicalize(&root)
1153 && canonical != root
1154 {
1155 roots.push(canonical);
1156 }
1157 roots
1158}
1159
1160fn normalize_components(path: &Path) -> PathBuf {
1161 let mut normalized = PathBuf::new();
1162 for component in path.components() {
1163 match component {
1164 Component::CurDir => {}
1165 Component::ParentDir => {
1166 normalized.pop();
1167 }
1168 component => normalized.push(component.as_os_str()),
1169 }
1170 }
1171 normalized
1172}
1173
1174pub fn environment_inputs<F>(
1177 lookup: F,
1178 sysroot: Option<&Path>,
1179) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1180where
1181 F: Fn(&str) -> Option<String>,
1182{
1183 environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
1184}
1185
1186pub fn environment_inputs_for<F>(
1188 lookup: F,
1189 sysroot: Option<&Path>,
1190 family: CcCompilerFamily,
1191) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1192where
1193 F: Fn(&str) -> Option<String>,
1194{
1195 for name in BYPASS_ENVIRONMENT {
1196 if lookup(name).is_some() {
1197 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
1198 }
1199 }
1200 let mut environment = BTreeMap::new();
1201 for name in KEYED_ENVIRONMENT {
1202 if *name == "SDKROOT" && sysroot.is_some() {
1205 continue;
1206 }
1207 environment.insert((*name).to_string(), lookup(name));
1208 }
1209 if family.is_msvc() {
1210 for name in [
1214 "INCLUDE",
1215 "VCToolsVersion",
1216 "WindowsSDKVersion",
1217 "UCRTVersion",
1218 ] {
1219 environment.insert(name.into(), lookup(name));
1220 }
1221 for name in ["CL", "_CL_"] {
1222 if lookup(name).is_some() {
1223 return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
1224 }
1225 }
1226 }
1227 Ok(environment)
1228}
1229
1230struct ActionBuilder<'a> {
1231 invocation: &'a CcInvocation,
1232 context: CcActionContext,
1233 mappings: Vec<PathMapping>,
1234 aliases: PathAliases,
1236}
1237
1238impl<'a> ActionBuilder<'a> {
1239 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
1240 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1241 let mappings = resolve_path_mappings(&context.path_mappings);
1242 Self {
1243 invocation,
1244 context,
1245 mappings,
1246 aliases: PathAliases::default(),
1247 }
1248 }
1249
1250 fn build(self, path_specific: bool) -> Result<CcAction, CcBypassReason> {
1251 self.validate_mappings()?;
1252 let invocation = self.invocation_descriptor()?;
1253
1254 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1255 for input in &self.context.inputs {
1256 input.digest.validate().map_err(|_| {
1257 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
1258 })?;
1259 let path = self.normalize_input_path(&input.path)?;
1260 if inputs
1261 .insert(path.clone(), input.digest.clone())
1262 .is_some_and(|existing| existing != input.digest)
1263 {
1264 return Err(CcBypassReason::ConflictingInput(path));
1265 }
1266 }
1267 let required = self
1268 .invocation
1269 .required_inputs
1270 .iter()
1271 .map(|path| self.normalize_path(path))
1272 .collect::<Result<BTreeSet<_>, _>>()?;
1273 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1274 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
1275 }
1276 let inputs = inputs
1277 .into_iter()
1278 .map(|(path, digest)| CcInputDescriptor { path, digest })
1279 .collect();
1280 let descriptor = CcActionDescriptor {
1281 path_binding: path_specific.then(|| CcPathBinding {
1282 working_dir: self.context.working_dir.clone(),
1283 arguments: self.invocation.arguments.clone(),
1284 roots: self
1285 .context
1286 .path_mappings
1287 .iter()
1288 .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1289 .collect(),
1290 }),
1291 version: ACTION_SCHEMA_VERSION,
1292 kind: "cc",
1293 adapter_version: ADAPTER_VERSION,
1294 assembly_input_model: invocation.assembly_input_model,
1295 preprocessing: invocation.preprocessing,
1296 compiler: invocation.compiler,
1297 arguments: invocation.arguments,
1298 environment: self.context.environment.clone(),
1299 inputs,
1300 };
1301 let bytes = canonical_json(&descriptor)
1302 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1303 let digest = CacheDigest::blake3(&bytes);
1304 Ok(CcAction { digest, bytes })
1305 }
1306
1307 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1308 self.validate_mappings()?;
1309 let arguments = self
1310 .invocation
1311 .arguments
1312 .iter()
1313 .map(|argument| self.normalize_argument(argument))
1314 .collect::<Result<Vec<_>, _>>()?;
1315 let required_inputs = self
1316 .invocation
1317 .required_inputs
1318 .iter()
1319 .map(|path| self.normalize_path(path))
1320 .collect::<Result<BTreeSet<_>, _>>()?
1321 .into_iter()
1322 .collect();
1323 Ok(CcInvocationDescriptor {
1324 version: ACTION_SCHEMA_VERSION,
1325 kind: "cc",
1326 adapter_version: ADAPTER_VERSION,
1327 assembly_input_model: self.invocation.preprocessed_assembly.then_some(1),
1330 preprocessing: self
1331 .invocation
1332 .preprocessing_arguments
1333 .as_ref()
1334 .map(|arguments| PreprocessingContext {
1335 working_dir: self.context.working_dir.clone(),
1336 arguments: arguments.clone(),
1337 roots: self
1338 .mappings
1339 .iter()
1340 .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1341 .collect(),
1342 }),
1343 compiler: self.compiler_descriptor(),
1344 arguments,
1345 required_inputs,
1346 })
1347 }
1348
1349 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1350 CcCompilerDescriptor {
1351 assembler: self.context.compiler.assembler.clone(),
1352 family: self.context.compiler.family.as_str().into(),
1353 target: self.context.compiler.target.clone(),
1354 version_text: self.context.compiler.version_text.clone(),
1355 }
1356 }
1357
1358 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1359 if !self.context.working_dir.is_absolute() {
1360 return Err(CcBypassReason::RelativeWorkingDirectory(
1361 self.context.working_dir.clone(),
1362 ));
1363 }
1364 let mut roots = BTreeSet::new();
1365 let mut placeholders = BTreeSet::new();
1366 for mapping in &self.context.path_mappings {
1367 if !mapping.root.is_absolute() {
1368 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1369 }
1370 if mapping.placeholder.is_empty()
1371 || !mapping
1372 .placeholder
1373 .bytes()
1374 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1375 || !roots.insert(normalize_components(&mapping.root))
1376 || !placeholders.insert(&mapping.placeholder)
1377 {
1378 return Err(CcBypassReason::InvalidPathPlaceholder(
1379 mapping.placeholder.clone(),
1380 ));
1381 }
1382 }
1383 Ok(())
1384 }
1385
1386 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1387 match argument {
1388 Argument::Plain(value) => Ok(value.clone()),
1389 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1390 Argument::PrefixMap { flag, from, to } => {
1391 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1392 }
1393 Argument::Source(path) => Ok(self.normalize_path(path)?),
1394 }
1395 }
1396
1397 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1404 match normalize_resolved_mapped_path_with(
1405 &self.aliases,
1406 path,
1407 &self.context.working_dir,
1408 &self.mappings,
1409 ) {
1410 Ok(normalized) => Ok(normalized),
1411 Err(reason) => {
1412 let absolute = absolute_path(path, &self.context.working_dir);
1413 if is_system_path(&absolute) {
1414 return absolute
1415 .to_str()
1416 .map(ToOwned::to_owned)
1417 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1418 }
1419 Err(reason.into())
1420 }
1421 }
1422 }
1423
1424 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1425 match path.to_str().and_then(|path| {
1426 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1427 .map(ToOwned::to_owned)
1428 }) {
1429 Some(directory) => Ok(format!(
1430 "{INCLUDE_MANIFEST_PREFIX}{}",
1431 self.normalize_path(Path::new(&directory))?
1432 )),
1433 None => self.normalize_path(path),
1434 }
1435 }
1436}
1437
1438fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1439 if path.is_absolute() {
1440 normalize_components(path)
1441 } else {
1442 normalize_components(&working_dir.join(path))
1443 }
1444}
1445
1446struct Parser<'a> {
1447 family: CcCompilerFamily,
1448 arguments: &'a [OsString],
1449 index: usize,
1450 parsed: Vec<Argument>,
1451 source: Option<PathBuf>,
1452 output: Option<PathBuf>,
1453 include_dirs: Vec<PathBuf>,
1454 required_inputs: Vec<PathBuf>,
1455 sysroot: Option<PathBuf>,
1456 explicit_language: Option<CcLanguage>,
1457 preprocessed_assembly: bool,
1458 compiling: bool,
1459 preprocessing: bool,
1460 dependency: DependencyRequest,
1461}
1462
1463#[derive(Debug, Default)]
1465struct DependencyRequest {
1466 user_headers_only: Option<bool>,
1468 file: Option<PathBuf>,
1469 targets: Vec<DepfileTarget>,
1470 phony_targets: bool,
1471 modifier: Option<String>,
1474 indices: Vec<usize>,
1475}
1476
1477impl<'a> Parser<'a> {
1478 fn new(arguments: &'a [OsString]) -> Self {
1479 Self {
1480 family: CcCompilerFamily::Gcc,
1481 arguments,
1482 index: 0,
1483 parsed: Vec::new(),
1484 source: None,
1485 output: None,
1486 include_dirs: Vec::new(),
1487 required_inputs: Vec::new(),
1488 sysroot: None,
1489 explicit_language: None,
1490 preprocessed_assembly: false,
1491 compiling: false,
1492 preprocessing: false,
1493 dependency: DependencyRequest::default(),
1494 }
1495 }
1496
1497 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1498 while self.index < self.arguments.len() {
1499 let value = self.current()?.to_string();
1500 self.index += 1;
1501 if value == "-" {
1502 return Err(CcBypassReason::StandardInput);
1503 }
1504 if let Some(argfile) = value.strip_prefix('@') {
1505 return Err(CcBypassReason::ResponseFile(argfile.into()));
1506 }
1507 if value.starts_with('-') {
1508 self.parse_flag(&value)?;
1509 } else {
1510 self.parse_input(&value)?;
1511 }
1512 }
1513
1514 if !self.compiling && !self.preprocessing {
1515 return Err(CcBypassReason::NotACompile);
1516 }
1517 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1518 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1519 if self.preprocessing && output == Path::new("-") {
1520 return Err(CcBypassReason::NonObjectOutput("-E -o -".into()));
1521 }
1522 if self.preprocessing
1525 && self.dependency.user_headers_only.is_some()
1526 && self.dependency.file.is_none()
1527 {
1528 return Err(CcBypassReason::CallerDependencyFlags(
1529 "-E -MD/-MMD without -MF".into(),
1530 ));
1531 }
1532 if self.preprocessing && self.dependency.file.as_deref() == Some(Path::new("-")) {
1533 return Err(CcBypassReason::CallerDependencyFlags("-E -MF -".into()));
1534 }
1535 let language = self.language(&source)?;
1536 let preprocessed_assembly = self.preprocessed_assembly
1537 || (self.explicit_language.is_none()
1538 && source.extension().and_then(|value| value.to_str()) == Some("S"));
1539 self.required_inputs.push(source.clone());
1540 let caller_depfile = match self.dependency.user_headers_only {
1541 Some(user_headers_only) => Some(CallerDepfile {
1542 path: self
1543 .dependency
1544 .file
1545 .unwrap_or_else(|| output.with_extension("d")),
1546 targets: if self.dependency.targets.is_empty() {
1547 vec![DepfileTarget {
1548 name: if self.preprocessing && self.family == CcCompilerFamily::Gcc {
1551 Path::new(source.file_name().ok_or(CcBypassReason::MissingInput)?)
1552 .with_extension("o")
1553 .to_string_lossy()
1554 .into_owned()
1555 } else {
1556 output.to_string_lossy().into_owned()
1557 },
1558 quoted: true,
1559 }]
1560 } else {
1561 self.dependency.targets
1562 },
1563 user_headers_only,
1564 phony_targets: self.dependency.phony_targets,
1565 }),
1566 None => match self.dependency.modifier {
1570 Some(modifier) => return Err(CcBypassReason::CallerDependencyFlags(modifier)),
1571 None => None,
1572 },
1573 };
1574 Ok(CcInvocation {
1575 arguments: self.parsed,
1576 source,
1577 output,
1578 include_dirs: self.include_dirs,
1579 required_inputs: self.required_inputs,
1580 language,
1581 preprocessed_assembly,
1582 preprocessing_arguments: self.preprocessing.then(|| {
1583 self.arguments
1584 .iter()
1585 .map(|arg| arg.to_str().expect("arguments were validated").to_owned())
1586 .collect()
1587 }),
1588 sysroot: self.sysroot,
1589 caller_depfile,
1590 dependency_argument_indices: self.dependency.indices,
1591 })
1592 }
1593
1594 fn parse_dependency_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1602 let start = self.index - 1;
1603 match option {
1604 "D" => self.dependency.user_headers_only = Some(false),
1605 "MD" => self.dependency.user_headers_only = Some(true),
1606 "P" => {
1607 self.dependency.phony_targets = true;
1608 self.dependency.modifier = Some(value.into());
1609 }
1610 _ if option.starts_with('F') => {
1611 let file = self.take_value("-MF", Some(&option[1..]))?;
1612 self.dependency.file = Some(PathBuf::from(file));
1613 self.dependency.modifier = Some("-MF".into());
1614 }
1615 _ if option.starts_with('T') || option.starts_with('Q') => {
1616 let flag = &value[..3];
1617 let name = self.take_value(flag, Some(&option[1..]))?;
1618 self.dependency.targets.push(DepfileTarget {
1619 name,
1620 quoted: option.starts_with('Q'),
1621 });
1622 self.dependency.modifier = Some(flag.into());
1623 }
1624 _ => return Err(CcBypassReason::CallerDependencyFlags(value.into())),
1625 }
1626 self.dependency.indices.extend(start..self.index);
1627 Ok(())
1628 }
1629
1630 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1631 if let Some(language) = self.explicit_language {
1632 return Ok(language);
1633 }
1634 let extension = source
1635 .extension()
1636 .and_then(|extension| extension.to_str())
1637 .unwrap_or_default();
1638 match extension {
1639 "c" | "S" => Ok(CcLanguage::C),
1645 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1646 _ => Err(CcBypassReason::UnsupportedLanguage(
1647 source.display().to_string(),
1648 )),
1649 }
1650 }
1651
1652 fn current(&self) -> Result<&str, CcBypassReason> {
1653 self.arguments[self.index]
1654 .to_str()
1655 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1656 }
1657
1658 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1659 if let Some(value) = inline
1660 && !value.is_empty()
1661 {
1662 return Ok(value.into());
1663 }
1664 if self.index >= self.arguments.len() {
1665 return Err(CcBypassReason::MissingValue(flag.into()));
1666 }
1667 let value = self.current()?.to_string();
1668 self.index += 1;
1669 Ok(value)
1670 }
1671
1672 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1673 if self.source.is_some() {
1674 return Err(CcBypassReason::MultipleInputs);
1675 }
1676 let path = PathBuf::from(value);
1677 if self.explicit_language.is_none() {
1681 let extension = path
1682 .extension()
1683 .and_then(|extension| extension.to_str())
1684 .unwrap_or_default();
1685 if !matches!(extension, "c" | "S" | "cc" | "cpp" | "cxx" | "c++") {
1686 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1687 }
1688 }
1689 self.source = Some(path.clone());
1690 self.parsed.push(Argument::Source(path));
1691 Ok(())
1692 }
1693
1694 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1695 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1696 return Err(CcBypassReason::CompilerQuery);
1697 }
1698 if value == "-E" {
1699 self.preprocessing = true;
1700 self.parsed.push(Argument::Plain(value.into()));
1701 return Ok(());
1702 }
1703 if value == "-S" {
1704 return Err(CcBypassReason::NonObjectOutput(value.into()));
1705 }
1706 if let Some(option) = value.strip_prefix("-M") {
1707 return self.parse_dependency_flag(value, option);
1708 }
1709 if value.starts_with("-save-temps") {
1710 return Err(CcBypassReason::SaveTemps(value.into()));
1711 }
1712 if value == "--coverage" {
1713 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1714 }
1715 if let Some(options) = value.strip_prefix("-Wa,")
1716 && !options.is_empty()
1717 && options
1718 .split(',')
1719 .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1720 {
1721 self.parsed.push(Argument::Plain(value.into()));
1722 return Ok(());
1723 }
1724 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1725 || value.starts_with("-Wp,")
1726 || value.starts_with("-Wa,")
1727 || value.starts_with("-Wl,")
1728 {
1729 return Err(CcBypassReason::ToolPassthrough(value.into()));
1732 }
1733 if value.starts_with("-include-pch") || value == "-emit-pch" {
1734 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1735 }
1736
1737 if value == "-c" {
1738 self.compiling = true;
1739 self.parsed.push(Argument::Plain(value.into()));
1740 return Ok(());
1741 }
1742 if SUPPORTED_BARE_FLAGS.contains(&value)
1743 || SUPPORTED_O_FLAGS.contains(&value)
1744 || SUPPORTED_G_FLAGS.contains(&value)
1745 || value.starts_with("-std=")
1746 {
1747 self.parsed.push(Argument::Plain(value.into()));
1748 return Ok(());
1749 }
1750 if let Some(rest) = value.strip_prefix("-o") {
1751 let path = self.take_value("-o", Some(rest))?;
1752 self.output = Some(PathBuf::from(&path));
1755 self.parsed.push(Argument::Path {
1756 flag: "-o".into(),
1757 path: PathBuf::from(path),
1758 });
1759 return Ok(());
1760 }
1761 if let Some(rest) = value.strip_prefix("-I") {
1762 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1763 self.include_dirs.push(path.clone());
1764 self.parsed.push(Argument::Path {
1765 flag: "-I".into(),
1766 path,
1767 });
1768 return Ok(());
1769 }
1770 if SEPARATE_PATH_FLAGS.contains(&value) {
1771 let path = PathBuf::from(self.take_value(value, None)?);
1772 match value {
1773 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1774 "-isysroot" => self.sysroot = Some(path.clone()),
1775 _ => {}
1781 }
1782 self.parsed.push(Argument::Path {
1783 flag: value.into(),
1784 path,
1785 });
1786 return Ok(());
1787 }
1788 if let Some(rest) = value.strip_prefix("--include=") {
1791 let path = PathBuf::from(rest);
1792 self.parsed.push(Argument::Path {
1793 flag: "-include".into(),
1794 path,
1795 });
1796 return Ok(());
1797 }
1798 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1799 value
1800 .strip_prefix(&format!("{flag}="))
1801 .map(|rest| (*flag, rest))
1802 }) {
1803 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1804 self.parsed.push(Argument::PrefixMap {
1805 flag: flag.into(),
1806 from: PathBuf::from(from),
1807 to: to.into(),
1808 });
1809 return Ok(());
1810 }
1811 if value == "--param" {
1813 let parameter = self.take_value("--param", None)?;
1814 self.parsed
1815 .push(Argument::Plain(format!("--param={parameter}")));
1816 return Ok(());
1817 }
1818 if let Some(parameter) = value.strip_prefix("--param=") {
1819 self.parsed
1820 .push(Argument::Plain(format!("--param={parameter}")));
1821 return Ok(());
1822 }
1823 if let Some(rest) = value.strip_prefix("--sysroot=") {
1824 let path = PathBuf::from(rest);
1825 self.sysroot = Some(path.clone());
1826 self.parsed.push(Argument::Path {
1827 flag: "--sysroot".into(),
1828 path,
1829 });
1830 return Ok(());
1831 }
1832 if let Some(rest) = value
1833 .strip_prefix("-D")
1834 .or_else(|| value.strip_prefix("-U"))
1835 {
1836 let flag = &value[..2];
1837 let definition = self.take_value(flag, Some(rest))?;
1838 self.parsed
1839 .push(Argument::Plain(format!("{flag}{definition}")));
1840 return Ok(());
1841 }
1842 if let Some(rest) = value.strip_prefix("-x") {
1843 let language = self.take_value("-x", Some(rest))?;
1844 self.explicit_language = Some(match language.as_str() {
1845 "c" => CcLanguage::C,
1846 "assembler-with-cpp" => {
1847 self.preprocessed_assembly = true;
1848 CcLanguage::C
1849 }
1850 "c++" => CcLanguage::Cxx,
1851 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1852 });
1853 self.parsed.push(Argument::Plain(format!("-x{language}")));
1854 return Ok(());
1855 }
1856 if let Some(target) = value.strip_prefix("--target=") {
1857 self.parsed
1858 .push(Argument::Plain(format!("--target={target}")));
1859 return Ok(());
1860 }
1861 if value == "-target" {
1862 let target = self.take_value("-target", None)?;
1863 self.parsed
1864 .push(Argument::Plain(format!("--target={target}")));
1865 return Ok(());
1866 }
1867 if value == "-arch" {
1868 let arch = self.take_value("-arch", None)?;
1869 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1870 return Ok(());
1871 }
1872 if let Some(option) = value.strip_prefix("-f") {
1873 return self.parse_f_flag(value, option);
1874 }
1875 if let Some(option) = value.strip_prefix("-m") {
1876 return self.parse_m_flag(value, option);
1877 }
1878 if value.starts_with("-g") {
1879 return Err(if value.starts_with("-gsplit-dwarf") {
1882 CcBypassReason::SplitDebugOutput(value.into())
1883 } else {
1884 CcBypassReason::UnknownFlag(value.into())
1885 });
1886 }
1887 if value.starts_with("-W") {
1888 self.parsed.push(Argument::Plain(value.into()));
1892 return Ok(());
1893 }
1894 Err(CcBypassReason::UnknownFlag(value.into()))
1895 }
1896
1897 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1898 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1899 return Err(CcBypassReason::Plugin(value.into()));
1900 }
1901 if option.starts_with("profile-") || option == "test-coverage" {
1902 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1903 }
1904 let name = option.split_once('=').map_or(option, |(name, _)| name);
1905 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1906 return Err(CcBypassReason::UnknownFlag(value.into()));
1907 }
1908 self.parsed.push(Argument::Plain(value.into()));
1909 Ok(())
1910 }
1911
1912 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1913 if option == "llvm" {
1914 return Err(CcBypassReason::ToolPassthrough(value.into()));
1915 }
1916 if let Some((name, selection)) = option.split_once('=')
1921 && matches!(name, "arch" | "cpu" | "tune")
1922 && matches!(selection, "native" | "host")
1923 {
1924 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1925 }
1926 let name = option.split_once('=').map_or(option, |(name, _)| name);
1927 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1928 return Err(CcBypassReason::UnknownFlag(value.into()));
1929 }
1930 self.parsed.push(Argument::Plain(value.into()));
1931 Ok(())
1932 }
1933}
1934
1935struct MsvcParser<'a> {
1939 arguments: &'a [OsString],
1940 index: usize,
1941 parsed: Vec<Argument>,
1942 source: Option<PathBuf>,
1943 output: Option<PathBuf>,
1944 include_dirs: Vec<PathBuf>,
1945 required_inputs: Vec<PathBuf>,
1946 explicit_language: Option<CcLanguage>,
1947 compiling: bool,
1948}
1949
1950impl<'a> MsvcParser<'a> {
1951 fn new(arguments: &'a [OsString]) -> Self {
1952 Self {
1953 arguments,
1954 index: 0,
1955 parsed: Vec::new(),
1956 source: None,
1957 output: None,
1958 include_dirs: Vec::new(),
1959 required_inputs: Vec::new(),
1960 explicit_language: None,
1961 compiling: false,
1962 }
1963 }
1964
1965 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1966 while self.index < self.arguments.len() {
1967 let value = self.current()?.to_owned();
1968 self.index += 1;
1969 if let Some(file) = value.strip_prefix('@') {
1970 return Err(CcBypassReason::ResponseFile(file.into()));
1971 }
1972 if value.starts_with('/') || value.starts_with('-') {
1973 self.parse_flag(&value)?;
1974 } else {
1975 self.add_source(&value)?;
1976 }
1977 }
1978 if !self.compiling {
1979 return Err(CcBypassReason::NotACompile);
1980 }
1981 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1982 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1983 let language = self.explicit_language.unwrap_or_else(|| {
1984 if source
1985 .extension()
1986 .and_then(|value| value.to_str())
1987 .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1988 {
1989 CcLanguage::C
1990 } else {
1991 CcLanguage::Cxx
1992 }
1993 });
1994 self.required_inputs.push(source.clone());
1995 Ok(CcInvocation {
1996 arguments: self.parsed,
1997 source,
1998 output,
1999 include_dirs: self.include_dirs,
2000 required_inputs: self.required_inputs,
2001 language,
2002 preprocessed_assembly: false,
2003 preprocessing_arguments: None,
2004 sysroot: None,
2005 caller_depfile: None,
2006 dependency_argument_indices: Vec::new(),
2007 })
2008 }
2009
2010 fn current(&self) -> Result<&str, CcBypassReason> {
2011 self.arguments[self.index]
2012 .to_str()
2013 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
2014 }
2015
2016 fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
2017 if !attached.is_empty() {
2018 return Ok(attached.into());
2019 }
2020 if self.index == self.arguments.len() {
2021 return Err(CcBypassReason::MissingValue(flag.into()));
2022 }
2023 let value = self.current()?.to_owned();
2024 self.index += 1;
2025 Ok(value)
2026 }
2027
2028 fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
2029 if self.source.is_some() {
2030 return Err(CcBypassReason::MultipleInputs);
2031 }
2032 let path = PathBuf::from(value);
2033 if self.explicit_language.is_none()
2034 && !path
2035 .extension()
2036 .and_then(|value| value.to_str())
2037 .is_some_and(|value| {
2038 matches!(
2039 value.to_ascii_lowercase().as_str(),
2040 "c" | "cc" | "cpp" | "cxx"
2041 )
2042 })
2043 {
2044 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
2045 }
2046 self.source = Some(path.clone());
2047 self.parsed.push(Argument::Source(path));
2048 Ok(())
2049 }
2050
2051 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
2052 let option = value.trim_start_matches(['/', '-']);
2053 let lower = option.to_ascii_lowercase();
2054 if matches!(lower.as_str(), "?" | "help") {
2055 return Err(CcBypassReason::CompilerQuery);
2056 }
2057 if lower == "showincludes" || lower.starts_with("sourcedependencies") {
2058 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
2059 }
2060 if matches!(lower.as_str(), "e" | "ep" | "p") {
2061 return Err(CcBypassReason::NonObjectOutput(value.into()));
2062 }
2063 if (lower.starts_with("fa") && !lower.starts_with("favor:"))
2064 || lower.starts_with("fd")
2065 || lower.starts_with("zi")
2066 {
2067 return Err(CcBypassReason::SplitDebugOutput(value.into()));
2068 }
2069 if lower.starts_with("yc")
2070 || lower.starts_with("yu")
2071 || (lower.starts_with("fp") && !lower.starts_with("fp:"))
2072 {
2073 return Err(CcBypassReason::PrecompiledHeader(value.into()));
2074 }
2075 if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
2076 return Err(CcBypassReason::ToolPassthrough(value.into()));
2077 }
2078 if matches!(lower.as_str(), "ld" | "ldd") {
2079 return Err(CcBypassReason::NotACompile);
2080 }
2081 if lower == "c" {
2082 self.compiling = true;
2083 self.parsed.push(Argument::Plain("/c".into()));
2084 return Ok(());
2085 }
2086 for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
2087 if let Some(attached) = option.strip_prefix(prefix) {
2088 let path = PathBuf::from(self.value(canonical, attached)?);
2089 if prefix == "Fo" {
2090 self.output = Some(path.clone());
2091 } else if prefix == "I" {
2092 self.include_dirs.push(path.clone());
2093 }
2094 self.parsed.push(Argument::Path {
2095 flag: canonical.into(),
2096 path,
2097 });
2098 return Ok(());
2099 }
2100 }
2101 if lower.starts_with("external:i") {
2102 let path = PathBuf::from(self.value("/external:I", &option[10..])?);
2103 self.include_dirs.push(path.clone());
2104 self.parsed.push(Argument::Path {
2105 flag: "/external:I".into(),
2106 path,
2107 });
2108 return Ok(());
2109 }
2110 if option.starts_with("Tc") || option.starts_with("Tp") {
2111 let c = option.starts_with("Tc");
2112 let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
2113 self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
2114 return self.add_source(&path);
2115 }
2116 if lower.starts_with("pathmap:") {
2117 let rest = &option[8..];
2118 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
2119 self.parsed.push(Argument::PrefixMap {
2120 flag: "/pathmap".into(),
2121 from: PathBuf::from(from),
2122 to: to.into(),
2123 });
2124 return Ok(());
2125 }
2126 if matches!(option, "D" | "U") {
2127 let definition = self.value(value, "")?;
2128 self.parsed
2129 .push(Argument::Plain(format!("/{option}{definition}")));
2130 return Ok(());
2131 }
2132 let definition = option.starts_with('D') || option.starts_with('U');
2135 let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
2136 || lower
2137 .strip_prefix('w')
2138 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2139 || ["wd", "we", "wo"].iter().any(|prefix| {
2140 lower
2141 .strip_prefix(prefix)
2142 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2143 });
2144 let admitted = definition
2145 || warning
2146 || lower.starts_with("std:")
2147 || lower.starts_with("arch:")
2148 || lower.starts_with("favor:")
2149 || lower.starts_with("volatile:")
2150 || lower.starts_with("fp:")
2151 || lower.starts_with("eh")
2152 || lower.starts_with('o')
2153 || lower.starts_with("ob")
2154 || lower.starts_with("oi")
2155 || lower.starts_with("ot")
2156 || lower.starts_with("oy")
2157 || lower.starts_with("gs")
2158 || lower.starts_with("gr")
2159 || lower.starts_with("gy")
2160 || lower.starts_with("gw")
2161 || lower.starts_with("gl")
2162 || lower.starts_with("zc:")
2163 || lower.starts_with("diagnostics:")
2164 || matches!(
2165 lower.as_str(),
2166 "nologo"
2167 | "brepro"
2168 | "bigobj"
2169 | "utf-8"
2170 | "permissive-"
2171 | "z7"
2172 | "md"
2173 | "mdd"
2174 | "mt"
2175 | "mtd"
2176 );
2177 if admitted {
2178 self.parsed.push(Argument::Plain(value.into()));
2179 return Ok(());
2180 }
2181 Err(CcBypassReason::UnknownFlag(value.into()))
2182 }
2183}
2184
2185#[cfg(test)]
2186#[path = "cc_cache_tests.rs"]
2187mod tests;