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 {
541 match self {
542 Self::C => "mbx-c",
543 Self::Cxx => "mbx-cxx",
544 }
545 }
546
547 pub fn default_driver(self) -> &'static str {
549 if cfg!(windows) {
550 return "cl.exe";
551 }
552 match self {
553 Self::C => "cc",
554 Self::Cxx => "c++",
555 }
556 }
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub enum CcCompilerFamily {
562 Gcc,
564 Clang,
566 AppleClang,
568 #[cfg(windows)]
570 Msvc,
571}
572
573impl CcCompilerFamily {
574 pub fn as_str(self) -> &'static str {
576 match self {
577 Self::Gcc => "gcc",
578 Self::Clang => "clang",
579 Self::AppleClang => "apple-clang",
580 #[cfg(windows)]
581 Self::Msvc => "msvc",
582 }
583 }
584
585 pub fn uses_external_assembler(self) -> bool {
588 matches!(self, Self::Gcc)
589 }
590
591 pub fn is_msvc(self) -> bool {
593 #[cfg(windows)]
594 {
595 matches!(self, Self::Msvc)
596 }
597 #[cfg(not(windows))]
598 {
599 false
600 }
601 }
602
603 pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
605 #[cfg(windows)]
606 if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
607 return Ok(Self::Msvc);
608 }
609 if probe.contains("Apple clang version") {
610 Ok(Self::AppleClang)
611 } else if probe.contains("clang version") {
612 Ok(Self::Clang)
613 } else if probe.contains("gcc version") {
614 Ok(Self::Gcc)
615 } else {
616 Err(CcBypassReason::UnsupportedCompilerDriver(
617 probe.lines().next().unwrap_or_default().into(),
618 ))
619 }
620 }
621}
622
623#[derive(Debug, Clone, PartialEq, Eq)]
625pub struct CcCompilerIdentity {
626 pub family: CcCompilerFamily,
628 pub version_text: String,
630 pub target: String,
632 pub assembler: String,
638}
639
640#[derive(Debug, Clone, PartialEq, Eq)]
642pub struct CcActionInput {
643 pub path: PathBuf,
646 pub digest: CacheDigest,
648}
649
650#[derive(Debug, Clone, PartialEq, Eq)]
652pub struct CcActionContext {
653 pub compiler: CcCompilerIdentity,
655 pub working_dir: PathBuf,
657 pub path_mappings: Vec<PathMapping>,
659 pub environment: BTreeMap<String, Option<String>>,
661 pub inputs: Vec<CcActionInput>,
663}
664
665#[derive(Debug, Clone, PartialEq, Eq)]
667pub struct CcAction {
668 pub digest: CacheDigest,
670 pub bytes: Vec<u8>,
672}
673
674#[derive(Debug, Serialize)]
675struct CcCompilerDescriptor {
676 assembler: String,
677 family: String,
678 target: String,
679 version_text: String,
680}
681
682#[derive(Debug, Serialize)]
683struct CcInputDescriptor {
684 digest: CacheDigest,
685 path: String,
686}
687
688#[derive(Debug, Serialize)]
689struct CcActionDescriptor {
690 #[serde(skip_serializing_if = "Option::is_none")]
691 path_binding: Option<CcPathBinding>,
692 version: u8,
693 kind: &'static str,
694 adapter_version: u8,
695 #[serde(skip_serializing_if = "Option::is_none")]
696 assembly_input_model: Option<u8>,
697 #[serde(skip_serializing_if = "Option::is_none")]
698 preprocessing: Option<PreprocessingContext>,
699 compiler: CcCompilerDescriptor,
700 arguments: Vec<String>,
701 environment: BTreeMap<String, Option<String>>,
702 inputs: Vec<CcInputDescriptor>,
703}
704
705#[derive(Debug, Serialize)]
706struct CcInvocationDescriptor {
707 version: u8,
708 kind: &'static str,
709 adapter_version: u8,
710 #[serde(skip_serializing_if = "Option::is_none")]
711 assembly_input_model: Option<u8>,
712 #[serde(skip_serializing_if = "Option::is_none")]
713 preprocessing: Option<PreprocessingContext>,
714 compiler: CcCompilerDescriptor,
715 arguments: Vec<String>,
716 required_inputs: Vec<String>,
717}
718
719#[derive(Debug, Serialize)]
722struct PreprocessingContext {
723 working_dir: PathBuf,
724 arguments: Vec<String>,
725 roots: BTreeMap<String, PathBuf>,
726}
727
728#[derive(Debug, Serialize)]
730struct CcPathBinding {
731 working_dir: PathBuf,
732 arguments: Vec<Argument>,
733 roots: BTreeMap<String, PathBuf>,
734}
735
736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
739#[serde(deny_unknown_fields)]
740pub struct CcInputPrediction {
741 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
743 pub path_specific: bool,
744 pub version: u8,
746 pub inputs: Vec<String>,
748 pub environment: Vec<String>,
750 #[serde(default, skip_serializing_if = "is_zero")]
753 pub compiler_duration_ns: u64,
754 #[serde(default, skip_serializing_if = "String::is_empty")]
756 pub source_name: String,
757}
758
759fn is_zero(value: &u64) -> bool {
760 *value == 0
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
765enum Argument {
766 Plain(String),
768 Path { flag: String, path: PathBuf },
770 PrefixMap {
772 flag: String,
773 from: PathBuf,
774 to: String,
775 },
776 Source(PathBuf),
778}
779
780#[derive(Debug, Clone, PartialEq, Eq)]
782pub struct CcInvocation {
783 arguments: Vec<Argument>,
784 source: PathBuf,
785 output: PathBuf,
786 include_dirs: Vec<PathBuf>,
787 required_inputs: Vec<PathBuf>,
788 language: CcLanguage,
789 preprocessed_assembly: bool,
790 preprocessing_arguments: Option<Vec<String>>,
791 sysroot: Option<PathBuf>,
792 caller_depfile: Option<CallerDepfile>,
793 dependency_argument_indices: Vec<usize>,
796}
797
798#[derive(Debug, Clone, PartialEq, Eq)]
808#[non_exhaustive]
809pub struct CallerDepfile {
810 pub path: PathBuf,
812 pub targets: Vec<DepfileTarget>,
814 pub user_headers_only: bool,
816 pub phony_targets: bool,
818}
819
820#[derive(Debug, Clone, PartialEq, Eq)]
822pub struct DepfileTarget {
823 pub name: String,
825 pub quoted: bool,
828}
829
830impl CcInvocation {
831 pub fn output_in(&self, working_dir: &Path) -> PathBuf {
837 if self.output.is_absolute() {
838 normalize_components(&self.output)
839 } else {
840 normalize_components(&working_dir.join(&self.output))
841 }
842 }
843
844 pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
846 self.caller_depfile.as_ref()
847 }
848
849 pub fn compiler_arguments(&self, arguments: &[OsString]) -> Vec<OsString> {
854 arguments
855 .iter()
856 .enumerate()
857 .filter(|(index, _)| !self.dependency_argument_indices.contains(index))
858 .map(|(_, argument)| argument.clone())
859 .collect()
860 }
861
862 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
865 Parser::new(arguments).parse()
866 }
867
868 pub fn parse_for(
870 arguments: &[OsString],
871 family: CcCompilerFamily,
872 ) -> Result<Self, CcBypassReason> {
873 if family.is_msvc() {
874 MsvcParser::new(arguments).parse()
875 } else {
876 let mut parser = Parser::new(arguments);
877 parser.family = family;
878 parser.parse()
879 }
880 }
881
882 pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
884 MsvcParser::new(arguments).parse()
885 }
886
887 pub fn source(&self) -> &Path {
889 &self.source
890 }
891
892 pub fn is_preprocessing(&self) -> bool {
894 self.preprocessing_arguments.is_some()
895 }
896
897 pub fn output(&self) -> &Path {
899 &self.output
900 }
901
902 pub fn include_dirs(&self) -> &[PathBuf] {
904 &self.include_dirs
905 }
906
907 pub fn required_inputs(&self) -> &[PathBuf] {
909 &self.required_inputs
910 }
911
912 pub fn language(&self) -> CcLanguage {
914 self.language
915 }
916
917 pub fn validate_discovered_inputs<'a>(
924 &self,
925 paths: impl IntoIterator<Item = &'a Path>,
926 ) -> Result<(), CcBypassReason> {
927 if !self.preprocessed_assembly {
928 return Ok(());
929 }
930 for path in paths {
931 if depfile::contains_assembler_input_directive(path)? {
932 return Err(CcBypassReason::AssemblerInputDirective(path.to_path_buf()));
933 }
934 }
935 Ok(())
936 }
937
938 pub fn sysroot(&self) -> Option<&Path> {
940 self.sysroot.as_deref()
941 }
942
943 pub fn source_name(&self) -> String {
945 self.source
946 .file_name()
947 .map(|name| name.to_string_lossy().into_owned())
948 .unwrap_or_default()
949 }
950
951 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
958 vec!["-MD".into(), "-MF".into(), depfile.into()]
959 }
960
961 pub fn dependency_arguments_for(
964 &self,
965 depfile: &Path,
966 family: CcCompilerFamily,
967 ) -> Vec<OsString> {
968 if family.is_msvc() {
969 vec!["/sourceDependencies".into(), depfile.into()]
970 } else {
971 self.dependency_arguments(depfile)
972 }
973 }
974
975 pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
977 vec!["/sourceDependencies".into(), depfile.into()]
978 }
979
980 pub fn invocation_digest(
982 &self,
983 context: &CcActionContext,
984 ) -> Result<CacheDigest, CcBypassReason> {
985 let builder = ActionBuilder::new(self, context.clone());
986 let descriptor = builder.invocation_descriptor()?;
987 let bytes = canonical_json(&descriptor)
988 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
989 Ok(CacheDigest::blake3(&bytes))
990 }
991
992 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
995 self.action_with_path_binding(context, false)
996 }
997
998 pub fn action_with_path_binding(
1001 &self,
1002 context: CcActionContext,
1003 path_specific: bool,
1004 ) -> Result<CcAction, CcBypassReason> {
1005 ActionBuilder::new(self, context).build(path_specific)
1006 }
1007
1008 pub fn prediction(
1011 &self,
1012 context: &CcActionContext,
1013 compiler_duration_ns: u64,
1014 ) -> Result<CcInputPrediction, CcBypassReason> {
1015 let builder = ActionBuilder::new(self, context.clone());
1016 let mut inputs = context
1017 .inputs
1018 .iter()
1019 .map(|input| builder.normalize_input_path(&input.path))
1020 .collect::<Result<Vec<_>, _>>()?;
1021 inputs.sort();
1022 inputs.dedup();
1023 Ok(CcInputPrediction {
1024 path_specific: false,
1025 version: 1,
1026 inputs,
1027 environment: context.environment.keys().cloned().collect(),
1028 compiler_duration_ns,
1029 source_name: self.source_name(),
1030 })
1031 }
1032}
1033
1034impl CcInputPrediction {
1035 pub fn discover(
1038 &self,
1039 working_dir: &Path,
1040 path_mappings: &[PathMapping],
1041 digests: &dyn FileDigestCache,
1042 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
1043 if self.version != 1 {
1044 return Err(CcBypassReason::UnsupportedPrediction);
1045 }
1046 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1047 return Err(CcBypassReason::UnsupportedPrediction);
1048 }
1049 let mappings = PathMapping::ordered(path_mappings);
1050 let mut files = BTreeSet::new();
1051 let mut directories = BTreeSet::new();
1052 for entry in &self.inputs {
1053 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
1054 Some(directory) => {
1055 directories.insert(denormalize_path(directory, &mappings)?);
1056 }
1057 None => {
1058 files.insert(denormalize_path(entry, &mappings)?);
1059 }
1060 }
1061 }
1062 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
1063 }
1064}
1065
1066fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
1072 for mapping in mappings {
1073 let prefix = format!("${{{}}}", mapping.placeholder);
1074 let suffix = if value == prefix {
1075 ""
1076 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1077 suffix
1078 } else {
1079 continue;
1080 };
1081 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
1082 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
1083 }
1084 let mut path = normalize_components(&mapping.root);
1085 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1086 return Ok(path);
1087 }
1088 let path = PathBuf::from(value);
1092 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
1093 return Ok(path);
1094 }
1095 Err(CcBypassReason::InvalidPredictedInput(value.into()))
1096}
1097
1098fn safe_suffix(suffix: &str) -> bool {
1099 suffix.is_empty()
1100 || !suffix.split('/').any(|component| {
1101 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
1102 })
1103}
1104
1105pub fn is_system_path(path: &Path) -> bool {
1107 SYSTEM_ROOTS
1108 .iter()
1109 .any(|root| path.starts_with(Path::new(root)))
1110 || developer_roots().iter().any(|root| path.starts_with(root))
1111}
1112
1113#[cfg(target_os = "macos")]
1117fn developer_roots() -> &'static [PathBuf] {
1118 static ROOTS: std::sync::OnceLock<Vec<PathBuf>> = std::sync::OnceLock::new();
1119 ROOTS.get_or_init(|| {
1120 let selected = std::env::var_os("DEVELOPER_DIR")
1121 .filter(|value| !value.is_empty())
1122 .map(PathBuf::from)
1123 .or_else(|| {
1124 let output = std::process::Command::new("/usr/bin/xcode-select")
1125 .arg("--print-path")
1126 .output()
1127 .ok()?;
1128 if !output.status.success() {
1129 return None;
1130 }
1131 Some(PathBuf::from(
1132 std::str::from_utf8(&output.stdout).ok()?.trim(),
1133 ))
1134 });
1135 selected.map(developer_directory_roots).unwrap_or_default()
1136 })
1137}
1138
1139#[cfg(not(target_os = "macos"))]
1140fn developer_roots() -> &'static [PathBuf] {
1141 &[]
1142}
1143
1144#[cfg(any(target_os = "macos", all(test, unix)))]
1145fn developer_directory_roots(mut root: PathBuf) -> Vec<PathBuf> {
1146 if !root.is_absolute() || root == Path::new("/") {
1147 return Vec::new();
1148 }
1149 if root.extension().is_some_and(|extension| extension == "app") {
1151 root = root.join("Contents/Developer");
1152 }
1153 root = normalize_components(&root);
1154 if root == Path::new("/") {
1155 return Vec::new();
1156 }
1157 let mut roots = vec![root.clone()];
1158 if let Ok(canonical) = std::fs::canonicalize(&root)
1159 && canonical != root
1160 {
1161 roots.push(canonical);
1162 }
1163 roots
1164}
1165
1166fn normalize_components(path: &Path) -> PathBuf {
1167 let mut normalized = PathBuf::new();
1168 for component in path.components() {
1169 match component {
1170 Component::CurDir => {}
1171 Component::ParentDir => {
1172 normalized.pop();
1173 }
1174 component => normalized.push(component.as_os_str()),
1175 }
1176 }
1177 normalized
1178}
1179
1180pub fn environment_inputs<F>(
1183 lookup: F,
1184 sysroot: Option<&Path>,
1185) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1186where
1187 F: Fn(&str) -> Option<String>,
1188{
1189 environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
1190}
1191
1192pub fn environment_inputs_for<F>(
1194 lookup: F,
1195 sysroot: Option<&Path>,
1196 family: CcCompilerFamily,
1197) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1198where
1199 F: Fn(&str) -> Option<String>,
1200{
1201 for name in BYPASS_ENVIRONMENT {
1202 if lookup(name).is_some() {
1203 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
1204 }
1205 }
1206 let mut environment = BTreeMap::new();
1207 for name in KEYED_ENVIRONMENT {
1208 if *name == "SDKROOT" && sysroot.is_some() {
1211 continue;
1212 }
1213 environment.insert((*name).to_string(), lookup(name));
1214 }
1215 if family.is_msvc() {
1216 for name in [
1220 "INCLUDE",
1221 "VCToolsVersion",
1222 "WindowsSDKVersion",
1223 "UCRTVersion",
1224 ] {
1225 environment.insert(name.into(), lookup(name));
1226 }
1227 for name in ["CL", "_CL_"] {
1228 if lookup(name).is_some() {
1229 return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
1230 }
1231 }
1232 }
1233 Ok(environment)
1234}
1235
1236struct ActionBuilder<'a> {
1237 invocation: &'a CcInvocation,
1238 context: CcActionContext,
1239 mappings: Vec<PathMapping>,
1240 aliases: PathAliases,
1242}
1243
1244impl<'a> ActionBuilder<'a> {
1245 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
1246 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1247 let mappings = resolve_path_mappings(&context.path_mappings);
1248 Self {
1249 invocation,
1250 context,
1251 mappings,
1252 aliases: PathAliases::default(),
1253 }
1254 }
1255
1256 fn build(self, path_specific: bool) -> Result<CcAction, CcBypassReason> {
1257 self.validate_mappings()?;
1258 let invocation = self.invocation_descriptor()?;
1259
1260 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1261 for input in &self.context.inputs {
1262 input.digest.validate().map_err(|_| {
1263 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
1264 })?;
1265 let path = self.normalize_input_path(&input.path)?;
1266 if inputs
1267 .insert(path.clone(), input.digest.clone())
1268 .is_some_and(|existing| existing != input.digest)
1269 {
1270 return Err(CcBypassReason::ConflictingInput(path));
1271 }
1272 }
1273 let required = self
1274 .invocation
1275 .required_inputs
1276 .iter()
1277 .map(|path| self.normalize_path(path))
1278 .collect::<Result<BTreeSet<_>, _>>()?;
1279 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1280 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
1281 }
1282 let inputs = inputs
1283 .into_iter()
1284 .map(|(path, digest)| CcInputDescriptor { path, digest })
1285 .collect();
1286 let descriptor = CcActionDescriptor {
1287 path_binding: path_specific.then(|| CcPathBinding {
1288 working_dir: self.context.working_dir.clone(),
1289 arguments: self.invocation.arguments.clone(),
1290 roots: self
1291 .context
1292 .path_mappings
1293 .iter()
1294 .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1295 .collect(),
1296 }),
1297 version: ACTION_SCHEMA_VERSION,
1298 kind: "cc",
1299 adapter_version: ADAPTER_VERSION,
1300 assembly_input_model: invocation.assembly_input_model,
1301 preprocessing: invocation.preprocessing,
1302 compiler: invocation.compiler,
1303 arguments: invocation.arguments,
1304 environment: self.context.environment.clone(),
1305 inputs,
1306 };
1307 let bytes = canonical_json(&descriptor)
1308 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1309 let digest = CacheDigest::blake3(&bytes);
1310 Ok(CcAction { digest, bytes })
1311 }
1312
1313 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1314 self.validate_mappings()?;
1315 let arguments = self
1316 .invocation
1317 .arguments
1318 .iter()
1319 .map(|argument| self.normalize_argument(argument))
1320 .collect::<Result<Vec<_>, _>>()?;
1321 let required_inputs = self
1322 .invocation
1323 .required_inputs
1324 .iter()
1325 .map(|path| self.normalize_path(path))
1326 .collect::<Result<BTreeSet<_>, _>>()?
1327 .into_iter()
1328 .collect();
1329 Ok(CcInvocationDescriptor {
1330 version: ACTION_SCHEMA_VERSION,
1331 kind: "cc",
1332 adapter_version: ADAPTER_VERSION,
1333 assembly_input_model: self.invocation.preprocessed_assembly.then_some(1),
1336 preprocessing: self
1337 .invocation
1338 .preprocessing_arguments
1339 .as_ref()
1340 .map(|arguments| PreprocessingContext {
1341 working_dir: self.context.working_dir.clone(),
1342 arguments: arguments.clone(),
1343 roots: self
1344 .mappings
1345 .iter()
1346 .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1347 .collect(),
1348 }),
1349 compiler: self.compiler_descriptor(),
1350 arguments,
1351 required_inputs,
1352 })
1353 }
1354
1355 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1356 CcCompilerDescriptor {
1357 assembler: self.context.compiler.assembler.clone(),
1358 family: self.context.compiler.family.as_str().into(),
1359 target: self.context.compiler.target.clone(),
1360 version_text: self.context.compiler.version_text.clone(),
1361 }
1362 }
1363
1364 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1365 if !self.context.working_dir.is_absolute() {
1366 return Err(CcBypassReason::RelativeWorkingDirectory(
1367 self.context.working_dir.clone(),
1368 ));
1369 }
1370 let mut roots = BTreeSet::new();
1371 let mut placeholders = BTreeSet::new();
1372 for mapping in &self.context.path_mappings {
1373 if !mapping.root.is_absolute() {
1374 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1375 }
1376 if mapping.placeholder.is_empty()
1377 || !mapping
1378 .placeholder
1379 .bytes()
1380 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1381 || !roots.insert(normalize_components(&mapping.root))
1382 || !placeholders.insert(&mapping.placeholder)
1383 {
1384 return Err(CcBypassReason::InvalidPathPlaceholder(
1385 mapping.placeholder.clone(),
1386 ));
1387 }
1388 }
1389 Ok(())
1390 }
1391
1392 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1393 match argument {
1394 Argument::Plain(value) => Ok(value.clone()),
1395 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1396 Argument::PrefixMap { flag, from, to } => {
1397 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1398 }
1399 Argument::Source(path) => Ok(self.normalize_path(path)?),
1400 }
1401 }
1402
1403 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1410 match normalize_resolved_mapped_path_with(
1411 &self.aliases,
1412 path,
1413 &self.context.working_dir,
1414 &self.mappings,
1415 ) {
1416 Ok(normalized) => Ok(normalized),
1417 Err(reason) => {
1418 let absolute = absolute_path(path, &self.context.working_dir);
1419 if is_system_path(&absolute) {
1420 return absolute
1421 .to_str()
1422 .map(ToOwned::to_owned)
1423 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1424 }
1425 Err(reason.into())
1426 }
1427 }
1428 }
1429
1430 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1431 match path.to_str().and_then(|path| {
1432 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1433 .map(ToOwned::to_owned)
1434 }) {
1435 Some(directory) => Ok(format!(
1436 "{INCLUDE_MANIFEST_PREFIX}{}",
1437 self.normalize_path(Path::new(&directory))?
1438 )),
1439 None => self.normalize_path(path),
1440 }
1441 }
1442}
1443
1444fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1445 if path.is_absolute() {
1446 normalize_components(path)
1447 } else {
1448 normalize_components(&working_dir.join(path))
1449 }
1450}
1451
1452struct Parser<'a> {
1453 family: CcCompilerFamily,
1454 arguments: &'a [OsString],
1455 index: usize,
1456 parsed: Vec<Argument>,
1457 source: Option<PathBuf>,
1458 output: Option<PathBuf>,
1459 include_dirs: Vec<PathBuf>,
1460 required_inputs: Vec<PathBuf>,
1461 sysroot: Option<PathBuf>,
1462 explicit_language: Option<CcLanguage>,
1463 preprocessed_assembly: bool,
1464 compiling: bool,
1465 preprocessing: bool,
1466 dependency: DependencyRequest,
1467}
1468
1469#[derive(Debug, Default)]
1471struct DependencyRequest {
1472 user_headers_only: Option<bool>,
1474 file: Option<PathBuf>,
1475 targets: Vec<DepfileTarget>,
1476 phony_targets: bool,
1477 modifier: Option<String>,
1480 indices: Vec<usize>,
1481}
1482
1483impl<'a> Parser<'a> {
1484 fn new(arguments: &'a [OsString]) -> Self {
1485 Self {
1486 family: CcCompilerFamily::Gcc,
1487 arguments,
1488 index: 0,
1489 parsed: Vec::new(),
1490 source: None,
1491 output: None,
1492 include_dirs: Vec::new(),
1493 required_inputs: Vec::new(),
1494 sysroot: None,
1495 explicit_language: None,
1496 preprocessed_assembly: false,
1497 compiling: false,
1498 preprocessing: false,
1499 dependency: DependencyRequest::default(),
1500 }
1501 }
1502
1503 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1504 while self.index < self.arguments.len() {
1505 let value = self.current()?.to_string();
1506 self.index += 1;
1507 if value == "-" {
1508 return Err(CcBypassReason::StandardInput);
1509 }
1510 if let Some(argfile) = value.strip_prefix('@') {
1511 return Err(CcBypassReason::ResponseFile(argfile.into()));
1512 }
1513 if value.starts_with('-') {
1514 self.parse_flag(&value)?;
1515 } else {
1516 self.parse_input(&value)?;
1517 }
1518 }
1519
1520 if !self.compiling && !self.preprocessing {
1521 return Err(CcBypassReason::NotACompile);
1522 }
1523 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1524 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1525 if self.preprocessing && output == Path::new("-") {
1526 return Err(CcBypassReason::NonObjectOutput("-E -o -".into()));
1527 }
1528 if self.preprocessing
1531 && self.dependency.user_headers_only.is_some()
1532 && self.dependency.file.is_none()
1533 {
1534 return Err(CcBypassReason::CallerDependencyFlags(
1535 "-E -MD/-MMD without -MF".into(),
1536 ));
1537 }
1538 if self.preprocessing && self.dependency.file.as_deref() == Some(Path::new("-")) {
1539 return Err(CcBypassReason::CallerDependencyFlags("-E -MF -".into()));
1540 }
1541 let language = self.language(&source)?;
1542 let preprocessed_assembly = self.preprocessed_assembly
1543 || (self.explicit_language.is_none()
1544 && source.extension().and_then(|value| value.to_str()) == Some("S"));
1545 self.required_inputs.push(source.clone());
1546 let caller_depfile = match self.dependency.user_headers_only {
1547 Some(user_headers_only) => Some(CallerDepfile {
1548 path: self
1549 .dependency
1550 .file
1551 .unwrap_or_else(|| output.with_extension("d")),
1552 targets: if self.dependency.targets.is_empty() {
1553 vec![DepfileTarget {
1554 name: if self.preprocessing && self.family == CcCompilerFamily::Gcc {
1557 Path::new(source.file_name().ok_or(CcBypassReason::MissingInput)?)
1558 .with_extension("o")
1559 .to_string_lossy()
1560 .into_owned()
1561 } else {
1562 output.to_string_lossy().into_owned()
1563 },
1564 quoted: true,
1565 }]
1566 } else {
1567 self.dependency.targets
1568 },
1569 user_headers_only,
1570 phony_targets: self.dependency.phony_targets,
1571 }),
1572 None => match self.dependency.modifier {
1576 Some(modifier) => return Err(CcBypassReason::CallerDependencyFlags(modifier)),
1577 None => None,
1578 },
1579 };
1580 Ok(CcInvocation {
1581 arguments: self.parsed,
1582 source,
1583 output,
1584 include_dirs: self.include_dirs,
1585 required_inputs: self.required_inputs,
1586 language,
1587 preprocessed_assembly,
1588 preprocessing_arguments: self.preprocessing.then(|| {
1589 self.arguments
1590 .iter()
1591 .map(|arg| arg.to_str().expect("arguments were validated").to_owned())
1592 .collect()
1593 }),
1594 sysroot: self.sysroot,
1595 caller_depfile,
1596 dependency_argument_indices: self.dependency.indices,
1597 })
1598 }
1599
1600 fn parse_dependency_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1608 let start = self.index - 1;
1609 match option {
1610 "D" => self.dependency.user_headers_only = Some(false),
1611 "MD" => self.dependency.user_headers_only = Some(true),
1612 "P" => {
1613 self.dependency.phony_targets = true;
1614 self.dependency.modifier = Some(value.into());
1615 }
1616 _ if option.starts_with('F') => {
1617 let file = self.take_value("-MF", Some(&option[1..]))?;
1618 self.dependency.file = Some(PathBuf::from(file));
1619 self.dependency.modifier = Some("-MF".into());
1620 }
1621 _ if option.starts_with('T') || option.starts_with('Q') => {
1622 let flag = &value[..3];
1623 let name = self.take_value(flag, Some(&option[1..]))?;
1624 self.dependency.targets.push(DepfileTarget {
1625 name,
1626 quoted: option.starts_with('Q'),
1627 });
1628 self.dependency.modifier = Some(flag.into());
1629 }
1630 _ => return Err(CcBypassReason::CallerDependencyFlags(value.into())),
1631 }
1632 self.dependency.indices.extend(start..self.index);
1633 Ok(())
1634 }
1635
1636 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1637 if let Some(language) = self.explicit_language {
1638 return Ok(language);
1639 }
1640 let extension = source
1641 .extension()
1642 .and_then(|extension| extension.to_str())
1643 .unwrap_or_default();
1644 match extension {
1645 "c" | "S" => Ok(CcLanguage::C),
1651 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1652 _ => Err(CcBypassReason::UnsupportedLanguage(
1653 source.display().to_string(),
1654 )),
1655 }
1656 }
1657
1658 fn current(&self) -> Result<&str, CcBypassReason> {
1659 self.arguments[self.index]
1660 .to_str()
1661 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1662 }
1663
1664 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1665 if let Some(value) = inline
1666 && !value.is_empty()
1667 {
1668 return Ok(value.into());
1669 }
1670 if self.index >= self.arguments.len() {
1671 return Err(CcBypassReason::MissingValue(flag.into()));
1672 }
1673 let value = self.current()?.to_string();
1674 self.index += 1;
1675 Ok(value)
1676 }
1677
1678 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1679 if self.source.is_some() {
1680 return Err(CcBypassReason::MultipleInputs);
1681 }
1682 let path = PathBuf::from(value);
1683 if self.explicit_language.is_none() {
1687 let extension = path
1688 .extension()
1689 .and_then(|extension| extension.to_str())
1690 .unwrap_or_default();
1691 if !matches!(extension, "c" | "S" | "cc" | "cpp" | "cxx" | "c++") {
1692 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1693 }
1694 }
1695 self.source = Some(path.clone());
1696 self.parsed.push(Argument::Source(path));
1697 Ok(())
1698 }
1699
1700 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1701 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1702 return Err(CcBypassReason::CompilerQuery);
1703 }
1704 if value == "-E" {
1705 self.preprocessing = true;
1706 self.parsed.push(Argument::Plain(value.into()));
1707 return Ok(());
1708 }
1709 if value == "-S" {
1710 return Err(CcBypassReason::NonObjectOutput(value.into()));
1711 }
1712 if let Some(option) = value.strip_prefix("-M") {
1713 return self.parse_dependency_flag(value, option);
1714 }
1715 if value.starts_with("-save-temps") {
1716 return Err(CcBypassReason::SaveTemps(value.into()));
1717 }
1718 if value == "--coverage" {
1719 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1720 }
1721 if let Some(options) = value.strip_prefix("-Wa,")
1722 && !options.is_empty()
1723 && options
1724 .split(',')
1725 .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1726 {
1727 self.parsed.push(Argument::Plain(value.into()));
1728 return Ok(());
1729 }
1730 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1731 || value.starts_with("-Wp,")
1732 || value.starts_with("-Wa,")
1733 || value.starts_with("-Wl,")
1734 {
1735 return Err(CcBypassReason::ToolPassthrough(value.into()));
1738 }
1739 if value.starts_with("-include-pch") || value == "-emit-pch" {
1740 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1741 }
1742
1743 if value == "-c" {
1744 self.compiling = true;
1745 self.parsed.push(Argument::Plain(value.into()));
1746 return Ok(());
1747 }
1748 if SUPPORTED_BARE_FLAGS.contains(&value)
1749 || SUPPORTED_O_FLAGS.contains(&value)
1750 || SUPPORTED_G_FLAGS.contains(&value)
1751 || value.starts_with("-std=")
1752 {
1753 self.parsed.push(Argument::Plain(value.into()));
1754 return Ok(());
1755 }
1756 if let Some(rest) = value.strip_prefix("-o") {
1757 let path = self.take_value("-o", Some(rest))?;
1758 self.output = Some(PathBuf::from(&path));
1761 self.parsed.push(Argument::Path {
1762 flag: "-o".into(),
1763 path: PathBuf::from(path),
1764 });
1765 return Ok(());
1766 }
1767 if let Some(rest) = value.strip_prefix("-I") {
1768 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1769 self.include_dirs.push(path.clone());
1770 self.parsed.push(Argument::Path {
1771 flag: "-I".into(),
1772 path,
1773 });
1774 return Ok(());
1775 }
1776 if SEPARATE_PATH_FLAGS.contains(&value) {
1777 let path = PathBuf::from(self.take_value(value, None)?);
1778 match value {
1779 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1780 "-isysroot" => self.sysroot = Some(path.clone()),
1781 _ => {}
1787 }
1788 self.parsed.push(Argument::Path {
1789 flag: value.into(),
1790 path,
1791 });
1792 return Ok(());
1793 }
1794 if let Some(rest) = value.strip_prefix("--include=") {
1797 let path = PathBuf::from(rest);
1798 self.parsed.push(Argument::Path {
1799 flag: "-include".into(),
1800 path,
1801 });
1802 return Ok(());
1803 }
1804 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1805 value
1806 .strip_prefix(&format!("{flag}="))
1807 .map(|rest| (*flag, rest))
1808 }) {
1809 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1810 self.parsed.push(Argument::PrefixMap {
1811 flag: flag.into(),
1812 from: PathBuf::from(from),
1813 to: to.into(),
1814 });
1815 return Ok(());
1816 }
1817 if value == "--param" {
1819 let parameter = self.take_value("--param", None)?;
1820 self.parsed
1821 .push(Argument::Plain(format!("--param={parameter}")));
1822 return Ok(());
1823 }
1824 if let Some(parameter) = value.strip_prefix("--param=") {
1825 self.parsed
1826 .push(Argument::Plain(format!("--param={parameter}")));
1827 return Ok(());
1828 }
1829 if let Some(rest) = value.strip_prefix("--sysroot=") {
1830 let path = PathBuf::from(rest);
1831 self.sysroot = Some(path.clone());
1832 self.parsed.push(Argument::Path {
1833 flag: "--sysroot".into(),
1834 path,
1835 });
1836 return Ok(());
1837 }
1838 if let Some(rest) = value
1839 .strip_prefix("-D")
1840 .or_else(|| value.strip_prefix("-U"))
1841 {
1842 let flag = &value[..2];
1843 let definition = self.take_value(flag, Some(rest))?;
1844 self.parsed
1845 .push(Argument::Plain(format!("{flag}{definition}")));
1846 return Ok(());
1847 }
1848 if let Some(rest) = value.strip_prefix("-x") {
1849 let language = self.take_value("-x", Some(rest))?;
1850 self.explicit_language = Some(match language.as_str() {
1851 "c" => CcLanguage::C,
1852 "assembler-with-cpp" => {
1853 self.preprocessed_assembly = true;
1854 CcLanguage::C
1855 }
1856 "c++" => CcLanguage::Cxx,
1857 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1858 });
1859 self.parsed.push(Argument::Plain(format!("-x{language}")));
1860 return Ok(());
1861 }
1862 if let Some(target) = value.strip_prefix("--target=") {
1863 self.parsed
1864 .push(Argument::Plain(format!("--target={target}")));
1865 return Ok(());
1866 }
1867 if value == "-target" {
1868 let target = self.take_value("-target", None)?;
1869 self.parsed
1870 .push(Argument::Plain(format!("--target={target}")));
1871 return Ok(());
1872 }
1873 if value == "-arch" {
1874 let arch = self.take_value("-arch", None)?;
1875 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1876 return Ok(());
1877 }
1878 if let Some(option) = value.strip_prefix("-f") {
1879 return self.parse_f_flag(value, option);
1880 }
1881 if let Some(option) = value.strip_prefix("-m") {
1882 return self.parse_m_flag(value, option);
1883 }
1884 if value.starts_with("-g") {
1885 return Err(if value.starts_with("-gsplit-dwarf") {
1888 CcBypassReason::SplitDebugOutput(value.into())
1889 } else {
1890 CcBypassReason::UnknownFlag(value.into())
1891 });
1892 }
1893 if value.starts_with("-W") {
1894 self.parsed.push(Argument::Plain(value.into()));
1898 return Ok(());
1899 }
1900 Err(CcBypassReason::UnknownFlag(value.into()))
1901 }
1902
1903 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1904 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1905 return Err(CcBypassReason::Plugin(value.into()));
1906 }
1907 if option.starts_with("profile-") || option == "test-coverage" {
1908 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1909 }
1910 let name = option.split_once('=').map_or(option, |(name, _)| name);
1911 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1912 return Err(CcBypassReason::UnknownFlag(value.into()));
1913 }
1914 self.parsed.push(Argument::Plain(value.into()));
1915 Ok(())
1916 }
1917
1918 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1919 if option == "llvm" {
1920 return Err(CcBypassReason::ToolPassthrough(value.into()));
1921 }
1922 if let Some((name, selection)) = option.split_once('=')
1927 && matches!(name, "arch" | "cpu" | "tune")
1928 && matches!(selection, "native" | "host")
1929 {
1930 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1931 }
1932 let name = option.split_once('=').map_or(option, |(name, _)| name);
1933 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1934 return Err(CcBypassReason::UnknownFlag(value.into()));
1935 }
1936 self.parsed.push(Argument::Plain(value.into()));
1937 Ok(())
1938 }
1939}
1940
1941struct MsvcParser<'a> {
1945 arguments: &'a [OsString],
1946 index: usize,
1947 parsed: Vec<Argument>,
1948 source: Option<PathBuf>,
1949 output: Option<PathBuf>,
1950 include_dirs: Vec<PathBuf>,
1951 required_inputs: Vec<PathBuf>,
1952 explicit_language: Option<CcLanguage>,
1953 compiling: bool,
1954}
1955
1956impl<'a> MsvcParser<'a> {
1957 fn new(arguments: &'a [OsString]) -> Self {
1958 Self {
1959 arguments,
1960 index: 0,
1961 parsed: Vec::new(),
1962 source: None,
1963 output: None,
1964 include_dirs: Vec::new(),
1965 required_inputs: Vec::new(),
1966 explicit_language: None,
1967 compiling: false,
1968 }
1969 }
1970
1971 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1972 while self.index < self.arguments.len() {
1973 let value = self.current()?.to_owned();
1974 self.index += 1;
1975 if let Some(file) = value.strip_prefix('@') {
1976 return Err(CcBypassReason::ResponseFile(file.into()));
1977 }
1978 if value.starts_with('/') || value.starts_with('-') {
1979 self.parse_flag(&value)?;
1980 } else {
1981 self.add_source(&value)?;
1982 }
1983 }
1984 if !self.compiling {
1985 return Err(CcBypassReason::NotACompile);
1986 }
1987 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1988 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1989 let language = self.explicit_language.unwrap_or_else(|| {
1990 if source
1991 .extension()
1992 .and_then(|value| value.to_str())
1993 .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1994 {
1995 CcLanguage::C
1996 } else {
1997 CcLanguage::Cxx
1998 }
1999 });
2000 self.required_inputs.push(source.clone());
2001 Ok(CcInvocation {
2002 arguments: self.parsed,
2003 source,
2004 output,
2005 include_dirs: self.include_dirs,
2006 required_inputs: self.required_inputs,
2007 language,
2008 preprocessed_assembly: false,
2009 preprocessing_arguments: None,
2010 sysroot: None,
2011 caller_depfile: None,
2012 dependency_argument_indices: Vec::new(),
2013 })
2014 }
2015
2016 fn current(&self) -> Result<&str, CcBypassReason> {
2017 self.arguments[self.index]
2018 .to_str()
2019 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
2020 }
2021
2022 fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
2023 if !attached.is_empty() {
2024 return Ok(attached.into());
2025 }
2026 if self.index == self.arguments.len() {
2027 return Err(CcBypassReason::MissingValue(flag.into()));
2028 }
2029 let value = self.current()?.to_owned();
2030 self.index += 1;
2031 Ok(value)
2032 }
2033
2034 fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
2035 if self.source.is_some() {
2036 return Err(CcBypassReason::MultipleInputs);
2037 }
2038 let path = PathBuf::from(value);
2039 if self.explicit_language.is_none()
2040 && !path
2041 .extension()
2042 .and_then(|value| value.to_str())
2043 .is_some_and(|value| {
2044 matches!(
2045 value.to_ascii_lowercase().as_str(),
2046 "c" | "cc" | "cpp" | "cxx"
2047 )
2048 })
2049 {
2050 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
2051 }
2052 self.source = Some(path.clone());
2053 self.parsed.push(Argument::Source(path));
2054 Ok(())
2055 }
2056
2057 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
2058 let option = value.trim_start_matches(['/', '-']);
2059 let lower = option.to_ascii_lowercase();
2060 if matches!(lower.as_str(), "?" | "help") {
2061 return Err(CcBypassReason::CompilerQuery);
2062 }
2063 if lower == "showincludes" || lower.starts_with("sourcedependencies") {
2064 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
2065 }
2066 if matches!(lower.as_str(), "e" | "ep" | "p") {
2067 return Err(CcBypassReason::NonObjectOutput(value.into()));
2068 }
2069 if (lower.starts_with("fa") && !lower.starts_with("favor:"))
2070 || lower.starts_with("fd")
2071 || lower.starts_with("zi")
2072 {
2073 return Err(CcBypassReason::SplitDebugOutput(value.into()));
2074 }
2075 if lower.starts_with("yc")
2076 || lower.starts_with("yu")
2077 || (lower.starts_with("fp") && !lower.starts_with("fp:"))
2078 {
2079 return Err(CcBypassReason::PrecompiledHeader(value.into()));
2080 }
2081 if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
2082 return Err(CcBypassReason::ToolPassthrough(value.into()));
2083 }
2084 if matches!(lower.as_str(), "ld" | "ldd") {
2085 return Err(CcBypassReason::NotACompile);
2086 }
2087 if lower == "c" {
2088 self.compiling = true;
2089 self.parsed.push(Argument::Plain("/c".into()));
2090 return Ok(());
2091 }
2092 for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
2093 if let Some(attached) = option.strip_prefix(prefix) {
2094 let path = PathBuf::from(self.value(canonical, attached)?);
2095 if prefix == "Fo" {
2096 self.output = Some(path.clone());
2097 } else if prefix == "I" {
2098 self.include_dirs.push(path.clone());
2099 }
2100 self.parsed.push(Argument::Path {
2101 flag: canonical.into(),
2102 path,
2103 });
2104 return Ok(());
2105 }
2106 }
2107 if lower.starts_with("external:i") {
2108 let path = PathBuf::from(self.value("/external:I", &option[10..])?);
2109 self.include_dirs.push(path.clone());
2110 self.parsed.push(Argument::Path {
2111 flag: "/external:I".into(),
2112 path,
2113 });
2114 return Ok(());
2115 }
2116 if option.starts_with("Tc") || option.starts_with("Tp") {
2117 let c = option.starts_with("Tc");
2118 let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
2119 self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
2120 return self.add_source(&path);
2121 }
2122 if lower.starts_with("pathmap:") {
2123 let rest = &option[8..];
2124 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
2125 self.parsed.push(Argument::PrefixMap {
2126 flag: "/pathmap".into(),
2127 from: PathBuf::from(from),
2128 to: to.into(),
2129 });
2130 return Ok(());
2131 }
2132 if matches!(option, "D" | "U") {
2133 let definition = self.value(value, "")?;
2134 self.parsed
2135 .push(Argument::Plain(format!("/{option}{definition}")));
2136 return Ok(());
2137 }
2138 let definition = option.starts_with('D') || option.starts_with('U');
2141 let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
2142 || lower
2143 .strip_prefix('w')
2144 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2145 || ["wd", "we", "wo"].iter().any(|prefix| {
2146 lower
2147 .strip_prefix(prefix)
2148 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2149 });
2150 let admitted = definition
2151 || warning
2152 || lower.starts_with("std:")
2153 || lower.starts_with("arch:")
2154 || lower.starts_with("favor:")
2155 || lower.starts_with("volatile:")
2156 || lower.starts_with("fp:")
2157 || lower.starts_with("eh")
2158 || lower.starts_with('o')
2159 || lower.starts_with("ob")
2160 || lower.starts_with("oi")
2161 || lower.starts_with("ot")
2162 || lower.starts_with("oy")
2163 || lower.starts_with("gs")
2164 || lower.starts_with("gr")
2165 || lower.starts_with("gy")
2166 || lower.starts_with("gw")
2167 || lower.starts_with("gl")
2168 || lower.starts_with("zc:")
2169 || lower.starts_with("diagnostics:")
2170 || matches!(
2171 lower.as_str(),
2172 "nologo"
2173 | "brepro"
2174 | "bigobj"
2175 | "utf-8"
2176 | "permissive-"
2177 | "z7"
2178 | "md"
2179 | "mdd"
2180 | "mt"
2181 | "mtd"
2182 );
2183 if admitted {
2184 self.parsed.push(Argument::Plain(value.into()));
2185 return Ok(());
2186 }
2187 Err(CcBypassReason::UnknownFlag(value.into()))
2188 }
2189}
2190
2191#[cfg(test)]
2192#[path = "cc_cache_tests.rs"]
2193mod tests;