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("compiler flag tunes for the local CPU: {0}")]
424 LocalCpuTarget(String),
425 #[error("compiler driver is not modeled by the cache adapter: {0}")]
427 UnsupportedCompilerDriver(String),
428 #[error("could not establish compiler identity: {0}")]
430 CompilerIdentityUnavailable(String),
431 #[error("environment variable {0} changes the compilation in an unmodeled way")]
433 UnsupportedEnvironment(String),
434 #[error("no real compiler was pinned for the cc shim")]
436 RealCompilerUnpinned,
437 #[error("input expands a timestamp macro: {0}")]
440 EmbeddedTimestampMacro(PathBuf),
441 #[error("preprocessed assembly uses an assembler input directive in {0}")]
444 AssemblerInputDirective(PathBuf),
445 #[error("could not model the compiler depfile: {0}")]
447 MalformedDepfile(String),
448 #[error("could not read the compiler depfile {path}: {message}")]
450 DepfileRead {
451 path: PathBuf,
453 message: String,
455 },
456 #[error("compilation reads more inputs than the cache adapter models")]
458 TooManyInputs,
459 #[error("path is outside every modeled root: {0}")]
461 UnmappedAbsolutePath(PathBuf),
462 #[error("path is not valid UTF-8: {0}")]
464 NonUtf8Path(PathBuf),
465 #[error("compiler working directory is not absolute: {0}")]
467 RelativeWorkingDirectory(PathBuf),
468 #[error("path mapping root is not absolute: {0}")]
470 RelativePathMapping(PathBuf),
471 #[error("invalid path mapping placeholder: {0}")]
473 InvalidPathPlaceholder(String),
474 #[error("required input is missing from the discovered inputs: {0}")]
476 MissingRequiredInput(String),
477 #[error("invalid digest for input: {0}")]
479 InvalidInputDigest(String),
480 #[error("conflicting digests for input: {0}")]
482 ConflictingInput(String),
483 #[error("could not read input {path}: {message}")]
485 InputRead {
486 path: PathBuf,
488 message: String,
490 },
491 #[error("input changed during the compilation: {0}")]
493 InputChanged(PathBuf),
494 #[error("input was modified during the compilation: {0}")]
496 InputModifiedDuringCompilation(PathBuf),
497 #[error("discovered inputs use a different working directory")]
499 DiscoveryWorkingDirectory,
500 #[error("action prediction is not modeled by this adapter version")]
502 UnsupportedPrediction,
503 #[error("invalid predicted input: {0}")]
505 InvalidPredictedInput(String),
506 #[error("could not serialize the action descriptor: {0}")]
508 Serialization(String),
509}
510
511impl From<PathNormalizationError> for CcBypassReason {
512 fn from(reason: PathNormalizationError) -> Self {
513 match reason {
514 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
515 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
516 }
517 }
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
522pub enum CcLanguage {
523 C,
525 Cxx,
527}
528
529impl CcLanguage {
530 pub fn shim_stem(self) -> &'static str {
532 match self {
533 Self::C => "mbx-cc",
534 Self::Cxx => "mbx-cxx",
535 }
536 }
537
538 pub fn default_driver(self) -> &'static str {
540 if cfg!(windows) {
541 return "cl.exe";
542 }
543 match self {
544 Self::C => "cc",
545 Self::Cxx => "c++",
546 }
547 }
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
552pub enum CcCompilerFamily {
553 Gcc,
555 Clang,
557 AppleClang,
559 #[cfg(windows)]
561 Msvc,
562}
563
564impl CcCompilerFamily {
565 pub fn as_str(self) -> &'static str {
567 match self {
568 Self::Gcc => "gcc",
569 Self::Clang => "clang",
570 Self::AppleClang => "apple-clang",
571 #[cfg(windows)]
572 Self::Msvc => "msvc",
573 }
574 }
575
576 pub fn uses_external_assembler(self) -> bool {
579 matches!(self, Self::Gcc)
580 }
581
582 pub fn is_msvc(self) -> bool {
584 #[cfg(windows)]
585 {
586 matches!(self, Self::Msvc)
587 }
588 #[cfg(not(windows))]
589 {
590 false
591 }
592 }
593
594 pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
596 #[cfg(windows)]
597 if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
598 return Ok(Self::Msvc);
599 }
600 if probe.contains("Apple clang version") {
601 Ok(Self::AppleClang)
602 } else if probe.contains("clang version") {
603 Ok(Self::Clang)
604 } else if probe.contains("gcc version") {
605 Ok(Self::Gcc)
606 } else {
607 Err(CcBypassReason::UnsupportedCompilerDriver(
608 probe.lines().next().unwrap_or_default().into(),
609 ))
610 }
611 }
612}
613
614#[derive(Debug, Clone, PartialEq, Eq)]
616pub struct CcCompilerIdentity {
617 pub family: CcCompilerFamily,
619 pub version_text: String,
621 pub target: String,
623 pub assembler: String,
629}
630
631#[derive(Debug, Clone, PartialEq, Eq)]
633pub struct CcActionInput {
634 pub path: PathBuf,
637 pub digest: CacheDigest,
639}
640
641#[derive(Debug, Clone, PartialEq, Eq)]
643pub struct CcActionContext {
644 pub compiler: CcCompilerIdentity,
646 pub working_dir: PathBuf,
648 pub path_mappings: Vec<PathMapping>,
650 pub environment: BTreeMap<String, Option<String>>,
652 pub inputs: Vec<CcActionInput>,
654}
655
656#[derive(Debug, Clone, PartialEq, Eq)]
658pub struct CcAction {
659 pub digest: CacheDigest,
661 pub bytes: Vec<u8>,
663}
664
665#[derive(Debug, Serialize)]
666struct CcCompilerDescriptor {
667 assembler: String,
668 family: String,
669 target: String,
670 version_text: String,
671}
672
673#[derive(Debug, Serialize)]
674struct CcInputDescriptor {
675 digest: CacheDigest,
676 path: String,
677}
678
679#[derive(Debug, Serialize)]
680struct CcActionDescriptor {
681 #[serde(skip_serializing_if = "Option::is_none")]
682 path_binding: Option<CcPathBinding>,
683 version: u8,
684 kind: &'static str,
685 adapter_version: u8,
686 #[serde(skip_serializing_if = "Option::is_none")]
687 assembly_input_model: Option<u8>,
688 #[serde(skip_serializing_if = "Option::is_none")]
689 preprocessing: Option<PreprocessingContext>,
690 compiler: CcCompilerDescriptor,
691 arguments: Vec<String>,
692 environment: BTreeMap<String, Option<String>>,
693 inputs: Vec<CcInputDescriptor>,
694}
695
696#[derive(Debug, Serialize)]
697struct CcInvocationDescriptor {
698 version: u8,
699 kind: &'static str,
700 adapter_version: u8,
701 #[serde(skip_serializing_if = "Option::is_none")]
702 assembly_input_model: Option<u8>,
703 #[serde(skip_serializing_if = "Option::is_none")]
704 preprocessing: Option<PreprocessingContext>,
705 compiler: CcCompilerDescriptor,
706 arguments: Vec<String>,
707 required_inputs: Vec<String>,
708}
709
710#[derive(Debug, Serialize)]
713struct PreprocessingContext {
714 working_dir: PathBuf,
715 arguments: Vec<String>,
716 roots: BTreeMap<String, PathBuf>,
717}
718
719#[derive(Debug, Serialize)]
721struct CcPathBinding {
722 working_dir: PathBuf,
723 arguments: Vec<Argument>,
724 roots: BTreeMap<String, PathBuf>,
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
730#[serde(deny_unknown_fields)]
731pub struct CcInputPrediction {
732 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
734 pub path_specific: bool,
735 pub version: u8,
737 pub inputs: Vec<String>,
739 pub environment: Vec<String>,
741 #[serde(default, skip_serializing_if = "is_zero")]
744 pub compiler_duration_ns: u64,
745 #[serde(default, skip_serializing_if = "String::is_empty")]
747 pub source_name: String,
748}
749
750fn is_zero(value: &u64) -> bool {
751 *value == 0
752}
753
754#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
756enum Argument {
757 Plain(String),
759 Path { flag: String, path: PathBuf },
761 PrefixMap {
763 flag: String,
764 from: PathBuf,
765 to: String,
766 },
767 Source(PathBuf),
769}
770
771#[derive(Debug, Clone, PartialEq, Eq)]
773pub struct CcInvocation {
774 arguments: Vec<Argument>,
775 source: PathBuf,
776 output: PathBuf,
777 include_dirs: Vec<PathBuf>,
778 required_inputs: Vec<PathBuf>,
779 language: CcLanguage,
780 preprocessed_assembly: bool,
781 preprocessing_arguments: Option<Vec<String>>,
782 sysroot: Option<PathBuf>,
783 caller_depfile: Option<CallerDepfile>,
784 dependency_argument_indices: Vec<usize>,
787}
788
789#[derive(Debug, Clone, PartialEq, Eq)]
799#[non_exhaustive]
800pub struct CallerDepfile {
801 pub path: PathBuf,
803 pub targets: Vec<DepfileTarget>,
805 pub user_headers_only: bool,
807 pub phony_targets: bool,
809}
810
811#[derive(Debug, Clone, PartialEq, Eq)]
813pub struct DepfileTarget {
814 pub name: String,
816 pub quoted: bool,
819}
820
821impl CcInvocation {
822 pub fn output_in(&self, working_dir: &Path) -> PathBuf {
828 if self.output.is_absolute() {
829 normalize_components(&self.output)
830 } else {
831 normalize_components(&working_dir.join(&self.output))
832 }
833 }
834
835 pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
837 self.caller_depfile.as_ref()
838 }
839
840 pub fn compiler_arguments(&self, arguments: &[OsString]) -> Vec<OsString> {
845 arguments
846 .iter()
847 .enumerate()
848 .filter(|(index, _)| !self.dependency_argument_indices.contains(index))
849 .map(|(_, argument)| argument.clone())
850 .collect()
851 }
852
853 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
856 Parser::new(arguments).parse()
857 }
858
859 pub fn parse_for(
861 arguments: &[OsString],
862 family: CcCompilerFamily,
863 ) -> Result<Self, CcBypassReason> {
864 if family.is_msvc() {
865 MsvcParser::new(arguments).parse()
866 } else {
867 let mut parser = Parser::new(arguments);
868 parser.family = family;
869 parser.parse()
870 }
871 }
872
873 pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
875 MsvcParser::new(arguments).parse()
876 }
877
878 pub fn source(&self) -> &Path {
880 &self.source
881 }
882
883 pub fn is_preprocessing(&self) -> bool {
885 self.preprocessing_arguments.is_some()
886 }
887
888 pub fn output(&self) -> &Path {
890 &self.output
891 }
892
893 pub fn include_dirs(&self) -> &[PathBuf] {
895 &self.include_dirs
896 }
897
898 pub fn required_inputs(&self) -> &[PathBuf] {
900 &self.required_inputs
901 }
902
903 pub fn language(&self) -> CcLanguage {
905 self.language
906 }
907
908 pub fn validate_discovered_inputs<'a>(
915 &self,
916 paths: impl IntoIterator<Item = &'a Path>,
917 ) -> Result<(), CcBypassReason> {
918 if !self.preprocessed_assembly {
919 return Ok(());
920 }
921 for path in paths {
922 if depfile::contains_assembler_input_directive(path)? {
923 return Err(CcBypassReason::AssemblerInputDirective(path.to_path_buf()));
924 }
925 }
926 Ok(())
927 }
928
929 pub fn sysroot(&self) -> Option<&Path> {
931 self.sysroot.as_deref()
932 }
933
934 pub fn source_name(&self) -> String {
936 self.source
937 .file_name()
938 .map(|name| name.to_string_lossy().into_owned())
939 .unwrap_or_default()
940 }
941
942 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
949 vec!["-MD".into(), "-MF".into(), depfile.into()]
950 }
951
952 pub fn dependency_arguments_for(
955 &self,
956 depfile: &Path,
957 family: CcCompilerFamily,
958 ) -> Vec<OsString> {
959 if family.is_msvc() {
960 vec!["/sourceDependencies".into(), depfile.into()]
961 } else {
962 self.dependency_arguments(depfile)
963 }
964 }
965
966 pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
968 vec!["/sourceDependencies".into(), depfile.into()]
969 }
970
971 pub fn invocation_digest(
973 &self,
974 context: &CcActionContext,
975 ) -> Result<CacheDigest, CcBypassReason> {
976 let builder = ActionBuilder::new(self, context.clone());
977 let descriptor = builder.invocation_descriptor()?;
978 let bytes = canonical_json(&descriptor)
979 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
980 Ok(CacheDigest::blake3(&bytes))
981 }
982
983 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
986 self.action_with_path_binding(context, false)
987 }
988
989 pub fn action_with_path_binding(
992 &self,
993 context: CcActionContext,
994 path_specific: bool,
995 ) -> Result<CcAction, CcBypassReason> {
996 ActionBuilder::new(self, context).build(path_specific)
997 }
998
999 pub fn prediction(
1002 &self,
1003 context: &CcActionContext,
1004 compiler_duration_ns: u64,
1005 ) -> Result<CcInputPrediction, CcBypassReason> {
1006 let builder = ActionBuilder::new(self, context.clone());
1007 let mut inputs = context
1008 .inputs
1009 .iter()
1010 .map(|input| builder.normalize_input_path(&input.path))
1011 .collect::<Result<Vec<_>, _>>()?;
1012 inputs.sort();
1013 inputs.dedup();
1014 Ok(CcInputPrediction {
1015 path_specific: false,
1016 version: 1,
1017 inputs,
1018 environment: context.environment.keys().cloned().collect(),
1019 compiler_duration_ns,
1020 source_name: self.source_name(),
1021 })
1022 }
1023}
1024
1025impl CcInputPrediction {
1026 pub fn discover(
1029 &self,
1030 working_dir: &Path,
1031 path_mappings: &[PathMapping],
1032 digests: &dyn FileDigestCache,
1033 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
1034 if self.version != 1 {
1035 return Err(CcBypassReason::UnsupportedPrediction);
1036 }
1037 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1038 return Err(CcBypassReason::UnsupportedPrediction);
1039 }
1040 let mappings = PathMapping::ordered(path_mappings);
1041 let mut files = BTreeSet::new();
1042 let mut directories = BTreeSet::new();
1043 for entry in &self.inputs {
1044 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
1045 Some(directory) => {
1046 directories.insert(denormalize_path(directory, &mappings)?);
1047 }
1048 None => {
1049 files.insert(denormalize_path(entry, &mappings)?);
1050 }
1051 }
1052 }
1053 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
1054 }
1055}
1056
1057fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
1063 for mapping in mappings {
1064 let prefix = format!("${{{}}}", mapping.placeholder);
1065 let suffix = if value == prefix {
1066 ""
1067 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1068 suffix
1069 } else {
1070 continue;
1071 };
1072 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
1073 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
1074 }
1075 let mut path = normalize_components(&mapping.root);
1076 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1077 return Ok(path);
1078 }
1079 let path = PathBuf::from(value);
1083 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
1084 return Ok(path);
1085 }
1086 Err(CcBypassReason::InvalidPredictedInput(value.into()))
1087}
1088
1089fn safe_suffix(suffix: &str) -> bool {
1090 suffix.is_empty()
1091 || !suffix.split('/').any(|component| {
1092 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
1093 })
1094}
1095
1096pub fn is_system_path(path: &Path) -> bool {
1098 SYSTEM_ROOTS
1099 .iter()
1100 .any(|root| path.starts_with(Path::new(root)))
1101 || developer_roots().iter().any(|root| path.starts_with(root))
1102}
1103
1104#[cfg(target_os = "macos")]
1108fn developer_roots() -> &'static [PathBuf] {
1109 static ROOTS: std::sync::OnceLock<Vec<PathBuf>> = std::sync::OnceLock::new();
1110 ROOTS.get_or_init(|| {
1111 let selected = std::env::var_os("DEVELOPER_DIR")
1112 .filter(|value| !value.is_empty())
1113 .map(PathBuf::from)
1114 .or_else(|| {
1115 let output = std::process::Command::new("/usr/bin/xcode-select")
1116 .arg("--print-path")
1117 .output()
1118 .ok()?;
1119 if !output.status.success() {
1120 return None;
1121 }
1122 Some(PathBuf::from(
1123 std::str::from_utf8(&output.stdout).ok()?.trim(),
1124 ))
1125 });
1126 selected.map(developer_directory_roots).unwrap_or_default()
1127 })
1128}
1129
1130#[cfg(not(target_os = "macos"))]
1131fn developer_roots() -> &'static [PathBuf] {
1132 &[]
1133}
1134
1135#[cfg(any(target_os = "macos", all(test, unix)))]
1136fn developer_directory_roots(mut root: PathBuf) -> Vec<PathBuf> {
1137 if !root.is_absolute() || root == Path::new("/") {
1138 return Vec::new();
1139 }
1140 if root.extension().is_some_and(|extension| extension == "app") {
1142 root = root.join("Contents/Developer");
1143 }
1144 root = normalize_components(&root);
1145 if root == Path::new("/") {
1146 return Vec::new();
1147 }
1148 let mut roots = vec![root.clone()];
1149 if let Ok(canonical) = std::fs::canonicalize(&root)
1150 && canonical != root
1151 {
1152 roots.push(canonical);
1153 }
1154 roots
1155}
1156
1157fn normalize_components(path: &Path) -> PathBuf {
1158 let mut normalized = PathBuf::new();
1159 for component in path.components() {
1160 match component {
1161 Component::CurDir => {}
1162 Component::ParentDir => {
1163 normalized.pop();
1164 }
1165 component => normalized.push(component.as_os_str()),
1166 }
1167 }
1168 normalized
1169}
1170
1171pub fn environment_inputs<F>(
1174 lookup: F,
1175 sysroot: Option<&Path>,
1176) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1177where
1178 F: Fn(&str) -> Option<String>,
1179{
1180 environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
1181}
1182
1183pub fn environment_inputs_for<F>(
1185 lookup: F,
1186 sysroot: Option<&Path>,
1187 family: CcCompilerFamily,
1188) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1189where
1190 F: Fn(&str) -> Option<String>,
1191{
1192 for name in BYPASS_ENVIRONMENT {
1193 if lookup(name).is_some() {
1194 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
1195 }
1196 }
1197 let mut environment = BTreeMap::new();
1198 for name in KEYED_ENVIRONMENT {
1199 if *name == "SDKROOT" && sysroot.is_some() {
1202 continue;
1203 }
1204 environment.insert((*name).to_string(), lookup(name));
1205 }
1206 if family.is_msvc() {
1207 for name in [
1211 "INCLUDE",
1212 "VCToolsVersion",
1213 "WindowsSDKVersion",
1214 "UCRTVersion",
1215 ] {
1216 environment.insert(name.into(), lookup(name));
1217 }
1218 for name in ["CL", "_CL_"] {
1219 if lookup(name).is_some() {
1220 return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
1221 }
1222 }
1223 }
1224 Ok(environment)
1225}
1226
1227struct ActionBuilder<'a> {
1228 invocation: &'a CcInvocation,
1229 context: CcActionContext,
1230 mappings: Vec<PathMapping>,
1231 aliases: PathAliases,
1233}
1234
1235impl<'a> ActionBuilder<'a> {
1236 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
1237 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1238 let mappings = resolve_path_mappings(&context.path_mappings);
1239 Self {
1240 invocation,
1241 context,
1242 mappings,
1243 aliases: PathAliases::default(),
1244 }
1245 }
1246
1247 fn build(self, path_specific: bool) -> Result<CcAction, CcBypassReason> {
1248 self.validate_mappings()?;
1249 let invocation = self.invocation_descriptor()?;
1250
1251 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1252 for input in &self.context.inputs {
1253 input.digest.validate().map_err(|_| {
1254 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
1255 })?;
1256 let path = self.normalize_input_path(&input.path)?;
1257 if inputs
1258 .insert(path.clone(), input.digest.clone())
1259 .is_some_and(|existing| existing != input.digest)
1260 {
1261 return Err(CcBypassReason::ConflictingInput(path));
1262 }
1263 }
1264 let required = self
1265 .invocation
1266 .required_inputs
1267 .iter()
1268 .map(|path| self.normalize_path(path))
1269 .collect::<Result<BTreeSet<_>, _>>()?;
1270 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1271 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
1272 }
1273 let inputs = inputs
1274 .into_iter()
1275 .map(|(path, digest)| CcInputDescriptor { path, digest })
1276 .collect();
1277 let descriptor = CcActionDescriptor {
1278 path_binding: path_specific.then(|| CcPathBinding {
1279 working_dir: self.context.working_dir.clone(),
1280 arguments: self.invocation.arguments.clone(),
1281 roots: self
1282 .context
1283 .path_mappings
1284 .iter()
1285 .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1286 .collect(),
1287 }),
1288 version: ACTION_SCHEMA_VERSION,
1289 kind: "cc",
1290 adapter_version: ADAPTER_VERSION,
1291 assembly_input_model: invocation.assembly_input_model,
1292 preprocessing: invocation.preprocessing,
1293 compiler: invocation.compiler,
1294 arguments: invocation.arguments,
1295 environment: self.context.environment.clone(),
1296 inputs,
1297 };
1298 let bytes = canonical_json(&descriptor)
1299 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1300 let digest = CacheDigest::blake3(&bytes);
1301 Ok(CcAction { digest, bytes })
1302 }
1303
1304 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1305 self.validate_mappings()?;
1306 let arguments = self
1307 .invocation
1308 .arguments
1309 .iter()
1310 .map(|argument| self.normalize_argument(argument))
1311 .collect::<Result<Vec<_>, _>>()?;
1312 let required_inputs = self
1313 .invocation
1314 .required_inputs
1315 .iter()
1316 .map(|path| self.normalize_path(path))
1317 .collect::<Result<BTreeSet<_>, _>>()?
1318 .into_iter()
1319 .collect();
1320 Ok(CcInvocationDescriptor {
1321 version: ACTION_SCHEMA_VERSION,
1322 kind: "cc",
1323 adapter_version: ADAPTER_VERSION,
1324 assembly_input_model: self.invocation.preprocessed_assembly.then_some(1),
1327 preprocessing: self
1328 .invocation
1329 .preprocessing_arguments
1330 .as_ref()
1331 .map(|arguments| PreprocessingContext {
1332 working_dir: self.context.working_dir.clone(),
1333 arguments: arguments.clone(),
1334 roots: self
1335 .mappings
1336 .iter()
1337 .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1338 .collect(),
1339 }),
1340 compiler: self.compiler_descriptor(),
1341 arguments,
1342 required_inputs,
1343 })
1344 }
1345
1346 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1347 CcCompilerDescriptor {
1348 assembler: self.context.compiler.assembler.clone(),
1349 family: self.context.compiler.family.as_str().into(),
1350 target: self.context.compiler.target.clone(),
1351 version_text: self.context.compiler.version_text.clone(),
1352 }
1353 }
1354
1355 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1356 if !self.context.working_dir.is_absolute() {
1357 return Err(CcBypassReason::RelativeWorkingDirectory(
1358 self.context.working_dir.clone(),
1359 ));
1360 }
1361 let mut roots = BTreeSet::new();
1362 let mut placeholders = BTreeSet::new();
1363 for mapping in &self.context.path_mappings {
1364 if !mapping.root.is_absolute() {
1365 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1366 }
1367 if mapping.placeholder.is_empty()
1368 || !mapping
1369 .placeholder
1370 .bytes()
1371 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1372 || !roots.insert(normalize_components(&mapping.root))
1373 || !placeholders.insert(&mapping.placeholder)
1374 {
1375 return Err(CcBypassReason::InvalidPathPlaceholder(
1376 mapping.placeholder.clone(),
1377 ));
1378 }
1379 }
1380 Ok(())
1381 }
1382
1383 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1384 match argument {
1385 Argument::Plain(value) => Ok(value.clone()),
1386 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1387 Argument::PrefixMap { flag, from, to } => {
1388 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1389 }
1390 Argument::Source(path) => Ok(self.normalize_path(path)?),
1391 }
1392 }
1393
1394 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1401 match normalize_resolved_mapped_path_with(
1402 &self.aliases,
1403 path,
1404 &self.context.working_dir,
1405 &self.mappings,
1406 ) {
1407 Ok(normalized) => Ok(normalized),
1408 Err(reason) => {
1409 let absolute = absolute_path(path, &self.context.working_dir);
1410 if is_system_path(&absolute) {
1411 return absolute
1412 .to_str()
1413 .map(ToOwned::to_owned)
1414 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1415 }
1416 Err(reason.into())
1417 }
1418 }
1419 }
1420
1421 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1422 match path.to_str().and_then(|path| {
1423 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1424 .map(ToOwned::to_owned)
1425 }) {
1426 Some(directory) => Ok(format!(
1427 "{INCLUDE_MANIFEST_PREFIX}{}",
1428 self.normalize_path(Path::new(&directory))?
1429 )),
1430 None => self.normalize_path(path),
1431 }
1432 }
1433}
1434
1435fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1436 if path.is_absolute() {
1437 normalize_components(path)
1438 } else {
1439 normalize_components(&working_dir.join(path))
1440 }
1441}
1442
1443struct Parser<'a> {
1444 family: CcCompilerFamily,
1445 arguments: &'a [OsString],
1446 index: usize,
1447 parsed: Vec<Argument>,
1448 source: Option<PathBuf>,
1449 output: Option<PathBuf>,
1450 include_dirs: Vec<PathBuf>,
1451 required_inputs: Vec<PathBuf>,
1452 sysroot: Option<PathBuf>,
1453 explicit_language: Option<CcLanguage>,
1454 preprocessed_assembly: bool,
1455 compiling: bool,
1456 preprocessing: bool,
1457 dependency: DependencyRequest,
1458}
1459
1460#[derive(Debug, Default)]
1462struct DependencyRequest {
1463 user_headers_only: Option<bool>,
1465 file: Option<PathBuf>,
1466 targets: Vec<DepfileTarget>,
1467 phony_targets: bool,
1468 modifier: Option<String>,
1471 indices: Vec<usize>,
1472}
1473
1474impl<'a> Parser<'a> {
1475 fn new(arguments: &'a [OsString]) -> Self {
1476 Self {
1477 family: CcCompilerFamily::Gcc,
1478 arguments,
1479 index: 0,
1480 parsed: Vec::new(),
1481 source: None,
1482 output: None,
1483 include_dirs: Vec::new(),
1484 required_inputs: Vec::new(),
1485 sysroot: None,
1486 explicit_language: None,
1487 preprocessed_assembly: false,
1488 compiling: false,
1489 preprocessing: false,
1490 dependency: DependencyRequest::default(),
1491 }
1492 }
1493
1494 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1495 while self.index < self.arguments.len() {
1496 let value = self.current()?.to_string();
1497 self.index += 1;
1498 if value == "-" {
1499 return Err(CcBypassReason::StandardInput);
1500 }
1501 if let Some(argfile) = value.strip_prefix('@') {
1502 return Err(CcBypassReason::ResponseFile(argfile.into()));
1503 }
1504 if value.starts_with('-') {
1505 self.parse_flag(&value)?;
1506 } else {
1507 self.parse_input(&value)?;
1508 }
1509 }
1510
1511 if !self.compiling && !self.preprocessing {
1512 return Err(CcBypassReason::NotACompile);
1513 }
1514 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1515 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1516 if self.preprocessing && output == Path::new("-") {
1517 return Err(CcBypassReason::NonObjectOutput("-E -o -".into()));
1518 }
1519 if self.preprocessing
1522 && self.dependency.user_headers_only.is_some()
1523 && self.dependency.file.is_none()
1524 {
1525 return Err(CcBypassReason::CallerDependencyFlags(
1526 "-E -MD/-MMD without -MF".into(),
1527 ));
1528 }
1529 if self.preprocessing && self.dependency.file.as_deref() == Some(Path::new("-")) {
1530 return Err(CcBypassReason::CallerDependencyFlags("-E -MF -".into()));
1531 }
1532 let language = self.language(&source)?;
1533 let preprocessed_assembly = self.preprocessed_assembly
1534 || (self.explicit_language.is_none()
1535 && source.extension().and_then(|value| value.to_str()) == Some("S"));
1536 self.required_inputs.push(source.clone());
1537 let caller_depfile = match self.dependency.user_headers_only {
1538 Some(user_headers_only) => Some(CallerDepfile {
1539 path: self
1540 .dependency
1541 .file
1542 .unwrap_or_else(|| output.with_extension("d")),
1543 targets: if self.dependency.targets.is_empty() {
1544 vec![DepfileTarget {
1545 name: if self.preprocessing && self.family == CcCompilerFamily::Gcc {
1548 Path::new(source.file_name().ok_or(CcBypassReason::MissingInput)?)
1549 .with_extension("o")
1550 .to_string_lossy()
1551 .into_owned()
1552 } else {
1553 output.to_string_lossy().into_owned()
1554 },
1555 quoted: true,
1556 }]
1557 } else {
1558 self.dependency.targets
1559 },
1560 user_headers_only,
1561 phony_targets: self.dependency.phony_targets,
1562 }),
1563 None => match self.dependency.modifier {
1567 Some(modifier) => return Err(CcBypassReason::CallerDependencyFlags(modifier)),
1568 None => None,
1569 },
1570 };
1571 Ok(CcInvocation {
1572 arguments: self.parsed,
1573 source,
1574 output,
1575 include_dirs: self.include_dirs,
1576 required_inputs: self.required_inputs,
1577 language,
1578 preprocessed_assembly,
1579 preprocessing_arguments: self.preprocessing.then(|| {
1580 self.arguments
1581 .iter()
1582 .map(|arg| arg.to_str().expect("arguments were validated").to_owned())
1583 .collect()
1584 }),
1585 sysroot: self.sysroot,
1586 caller_depfile,
1587 dependency_argument_indices: self.dependency.indices,
1588 })
1589 }
1590
1591 fn parse_dependency_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1599 let start = self.index - 1;
1600 match option {
1601 "D" => self.dependency.user_headers_only = Some(false),
1602 "MD" => self.dependency.user_headers_only = Some(true),
1603 "P" => {
1604 self.dependency.phony_targets = true;
1605 self.dependency.modifier = Some(value.into());
1606 }
1607 _ if option.starts_with('F') => {
1608 let file = self.take_value("-MF", Some(&option[1..]))?;
1609 self.dependency.file = Some(PathBuf::from(file));
1610 self.dependency.modifier = Some("-MF".into());
1611 }
1612 _ if option.starts_with('T') || option.starts_with('Q') => {
1613 let flag = &value[..3];
1614 let name = self.take_value(flag, Some(&option[1..]))?;
1615 self.dependency.targets.push(DepfileTarget {
1616 name,
1617 quoted: option.starts_with('Q'),
1618 });
1619 self.dependency.modifier = Some(flag.into());
1620 }
1621 _ => return Err(CcBypassReason::CallerDependencyFlags(value.into())),
1622 }
1623 self.dependency.indices.extend(start..self.index);
1624 Ok(())
1625 }
1626
1627 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1628 if let Some(language) = self.explicit_language {
1629 return Ok(language);
1630 }
1631 let extension = source
1632 .extension()
1633 .and_then(|extension| extension.to_str())
1634 .unwrap_or_default();
1635 match extension {
1636 "c" | "S" => Ok(CcLanguage::C),
1642 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1643 _ => Err(CcBypassReason::UnsupportedLanguage(
1644 source.display().to_string(),
1645 )),
1646 }
1647 }
1648
1649 fn current(&self) -> Result<&str, CcBypassReason> {
1650 self.arguments[self.index]
1651 .to_str()
1652 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1653 }
1654
1655 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1656 if let Some(value) = inline
1657 && !value.is_empty()
1658 {
1659 return Ok(value.into());
1660 }
1661 if self.index >= self.arguments.len() {
1662 return Err(CcBypassReason::MissingValue(flag.into()));
1663 }
1664 let value = self.current()?.to_string();
1665 self.index += 1;
1666 Ok(value)
1667 }
1668
1669 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1670 if self.source.is_some() {
1671 return Err(CcBypassReason::MultipleInputs);
1672 }
1673 let path = PathBuf::from(value);
1674 if self.explicit_language.is_none() {
1678 let extension = path
1679 .extension()
1680 .and_then(|extension| extension.to_str())
1681 .unwrap_or_default();
1682 if !matches!(extension, "c" | "S" | "cc" | "cpp" | "cxx" | "c++") {
1683 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1684 }
1685 }
1686 self.source = Some(path.clone());
1687 self.parsed.push(Argument::Source(path));
1688 Ok(())
1689 }
1690
1691 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1692 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1693 return Err(CcBypassReason::CompilerQuery);
1694 }
1695 if value == "-E" {
1696 self.preprocessing = true;
1697 self.parsed.push(Argument::Plain(value.into()));
1698 return Ok(());
1699 }
1700 if value == "-S" {
1701 return Err(CcBypassReason::NonObjectOutput(value.into()));
1702 }
1703 if let Some(option) = value.strip_prefix("-M") {
1704 return self.parse_dependency_flag(value, option);
1705 }
1706 if value.starts_with("-save-temps") {
1707 return Err(CcBypassReason::SaveTemps(value.into()));
1708 }
1709 if value == "--coverage" {
1710 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1711 }
1712 if let Some(options) = value.strip_prefix("-Wa,")
1713 && !options.is_empty()
1714 && options
1715 .split(',')
1716 .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1717 {
1718 self.parsed.push(Argument::Plain(value.into()));
1719 return Ok(());
1720 }
1721 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1722 || value.starts_with("-Wp,")
1723 || value.starts_with("-Wa,")
1724 || value.starts_with("-Wl,")
1725 {
1726 return Err(CcBypassReason::ToolPassthrough(value.into()));
1729 }
1730 if value.starts_with("-include-pch") || value == "-emit-pch" {
1731 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1732 }
1733
1734 if value == "-c" {
1735 self.compiling = true;
1736 self.parsed.push(Argument::Plain(value.into()));
1737 return Ok(());
1738 }
1739 if SUPPORTED_BARE_FLAGS.contains(&value)
1740 || SUPPORTED_O_FLAGS.contains(&value)
1741 || SUPPORTED_G_FLAGS.contains(&value)
1742 || value.starts_with("-std=")
1743 {
1744 self.parsed.push(Argument::Plain(value.into()));
1745 return Ok(());
1746 }
1747 if let Some(rest) = value.strip_prefix("-o") {
1748 let path = self.take_value("-o", Some(rest))?;
1749 self.output = Some(PathBuf::from(&path));
1752 self.parsed.push(Argument::Path {
1753 flag: "-o".into(),
1754 path: PathBuf::from(path),
1755 });
1756 return Ok(());
1757 }
1758 if let Some(rest) = value.strip_prefix("-I") {
1759 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1760 self.include_dirs.push(path.clone());
1761 self.parsed.push(Argument::Path {
1762 flag: "-I".into(),
1763 path,
1764 });
1765 return Ok(());
1766 }
1767 if SEPARATE_PATH_FLAGS.contains(&value) {
1768 let path = PathBuf::from(self.take_value(value, None)?);
1769 match value {
1770 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1771 "-isysroot" => self.sysroot = Some(path.clone()),
1772 _ => {}
1778 }
1779 self.parsed.push(Argument::Path {
1780 flag: value.into(),
1781 path,
1782 });
1783 return Ok(());
1784 }
1785 if let Some(rest) = value.strip_prefix("--include=") {
1788 let path = PathBuf::from(rest);
1789 self.parsed.push(Argument::Path {
1790 flag: "-include".into(),
1791 path,
1792 });
1793 return Ok(());
1794 }
1795 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1796 value
1797 .strip_prefix(&format!("{flag}="))
1798 .map(|rest| (*flag, rest))
1799 }) {
1800 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1801 self.parsed.push(Argument::PrefixMap {
1802 flag: flag.into(),
1803 from: PathBuf::from(from),
1804 to: to.into(),
1805 });
1806 return Ok(());
1807 }
1808 if value == "--param" {
1810 let parameter = self.take_value("--param", None)?;
1811 self.parsed
1812 .push(Argument::Plain(format!("--param={parameter}")));
1813 return Ok(());
1814 }
1815 if let Some(parameter) = value.strip_prefix("--param=") {
1816 self.parsed
1817 .push(Argument::Plain(format!("--param={parameter}")));
1818 return Ok(());
1819 }
1820 if let Some(rest) = value.strip_prefix("--sysroot=") {
1821 let path = PathBuf::from(rest);
1822 self.sysroot = Some(path.clone());
1823 self.parsed.push(Argument::Path {
1824 flag: "--sysroot".into(),
1825 path,
1826 });
1827 return Ok(());
1828 }
1829 if let Some(rest) = value
1830 .strip_prefix("-D")
1831 .or_else(|| value.strip_prefix("-U"))
1832 {
1833 let flag = &value[..2];
1834 let definition = self.take_value(flag, Some(rest))?;
1835 self.parsed
1836 .push(Argument::Plain(format!("{flag}{definition}")));
1837 return Ok(());
1838 }
1839 if let Some(rest) = value.strip_prefix("-x") {
1840 let language = self.take_value("-x", Some(rest))?;
1841 self.explicit_language = Some(match language.as_str() {
1842 "c" => CcLanguage::C,
1843 "assembler-with-cpp" => {
1844 self.preprocessed_assembly = true;
1845 CcLanguage::C
1846 }
1847 "c++" => CcLanguage::Cxx,
1848 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1849 });
1850 self.parsed.push(Argument::Plain(format!("-x{language}")));
1851 return Ok(());
1852 }
1853 if let Some(target) = value.strip_prefix("--target=") {
1854 self.parsed
1855 .push(Argument::Plain(format!("--target={target}")));
1856 return Ok(());
1857 }
1858 if value == "-target" {
1859 let target = self.take_value("-target", None)?;
1860 self.parsed
1861 .push(Argument::Plain(format!("--target={target}")));
1862 return Ok(());
1863 }
1864 if value == "-arch" {
1865 let arch = self.take_value("-arch", None)?;
1866 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1867 return Ok(());
1868 }
1869 if let Some(option) = value.strip_prefix("-f") {
1870 return self.parse_f_flag(value, option);
1871 }
1872 if let Some(option) = value.strip_prefix("-m") {
1873 return self.parse_m_flag(value, option);
1874 }
1875 if value.starts_with("-g") {
1876 return Err(if value.starts_with("-gsplit-dwarf") {
1879 CcBypassReason::SplitDebugOutput(value.into())
1880 } else {
1881 CcBypassReason::UnknownFlag(value.into())
1882 });
1883 }
1884 if value.starts_with("-W") {
1885 self.parsed.push(Argument::Plain(value.into()));
1889 return Ok(());
1890 }
1891 Err(CcBypassReason::UnknownFlag(value.into()))
1892 }
1893
1894 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1895 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1896 return Err(CcBypassReason::Plugin(value.into()));
1897 }
1898 if option.starts_with("profile-") || option == "test-coverage" {
1899 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1900 }
1901 let name = option.split_once('=').map_or(option, |(name, _)| name);
1902 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1903 return Err(CcBypassReason::UnknownFlag(value.into()));
1904 }
1905 self.parsed.push(Argument::Plain(value.into()));
1906 Ok(())
1907 }
1908
1909 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1910 if option == "llvm" {
1911 return Err(CcBypassReason::ToolPassthrough(value.into()));
1912 }
1913 if let Some((name, selection)) = option.split_once('=')
1918 && matches!(name, "arch" | "cpu" | "tune")
1919 && matches!(selection, "native" | "host")
1920 {
1921 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1922 }
1923 let name = option.split_once('=').map_or(option, |(name, _)| name);
1924 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1925 return Err(CcBypassReason::UnknownFlag(value.into()));
1926 }
1927 self.parsed.push(Argument::Plain(value.into()));
1928 Ok(())
1929 }
1930}
1931
1932struct MsvcParser<'a> {
1936 arguments: &'a [OsString],
1937 index: usize,
1938 parsed: Vec<Argument>,
1939 source: Option<PathBuf>,
1940 output: Option<PathBuf>,
1941 include_dirs: Vec<PathBuf>,
1942 required_inputs: Vec<PathBuf>,
1943 explicit_language: Option<CcLanguage>,
1944 compiling: bool,
1945}
1946
1947impl<'a> MsvcParser<'a> {
1948 fn new(arguments: &'a [OsString]) -> Self {
1949 Self {
1950 arguments,
1951 index: 0,
1952 parsed: Vec::new(),
1953 source: None,
1954 output: None,
1955 include_dirs: Vec::new(),
1956 required_inputs: Vec::new(),
1957 explicit_language: None,
1958 compiling: false,
1959 }
1960 }
1961
1962 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1963 while self.index < self.arguments.len() {
1964 let value = self.current()?.to_owned();
1965 self.index += 1;
1966 if let Some(file) = value.strip_prefix('@') {
1967 return Err(CcBypassReason::ResponseFile(file.into()));
1968 }
1969 if value.starts_with('/') || value.starts_with('-') {
1970 self.parse_flag(&value)?;
1971 } else {
1972 self.add_source(&value)?;
1973 }
1974 }
1975 if !self.compiling {
1976 return Err(CcBypassReason::NotACompile);
1977 }
1978 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1979 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1980 let language = self.explicit_language.unwrap_or_else(|| {
1981 if source
1982 .extension()
1983 .and_then(|value| value.to_str())
1984 .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1985 {
1986 CcLanguage::C
1987 } else {
1988 CcLanguage::Cxx
1989 }
1990 });
1991 self.required_inputs.push(source.clone());
1992 Ok(CcInvocation {
1993 arguments: self.parsed,
1994 source,
1995 output,
1996 include_dirs: self.include_dirs,
1997 required_inputs: self.required_inputs,
1998 language,
1999 preprocessed_assembly: false,
2000 preprocessing_arguments: None,
2001 sysroot: None,
2002 caller_depfile: None,
2003 dependency_argument_indices: Vec::new(),
2004 })
2005 }
2006
2007 fn current(&self) -> Result<&str, CcBypassReason> {
2008 self.arguments[self.index]
2009 .to_str()
2010 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
2011 }
2012
2013 fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
2014 if !attached.is_empty() {
2015 return Ok(attached.into());
2016 }
2017 if self.index == self.arguments.len() {
2018 return Err(CcBypassReason::MissingValue(flag.into()));
2019 }
2020 let value = self.current()?.to_owned();
2021 self.index += 1;
2022 Ok(value)
2023 }
2024
2025 fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
2026 if self.source.is_some() {
2027 return Err(CcBypassReason::MultipleInputs);
2028 }
2029 let path = PathBuf::from(value);
2030 if self.explicit_language.is_none()
2031 && !path
2032 .extension()
2033 .and_then(|value| value.to_str())
2034 .is_some_and(|value| {
2035 matches!(
2036 value.to_ascii_lowercase().as_str(),
2037 "c" | "cc" | "cpp" | "cxx"
2038 )
2039 })
2040 {
2041 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
2042 }
2043 self.source = Some(path.clone());
2044 self.parsed.push(Argument::Source(path));
2045 Ok(())
2046 }
2047
2048 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
2049 let option = value.trim_start_matches(['/', '-']);
2050 let lower = option.to_ascii_lowercase();
2051 if matches!(lower.as_str(), "?" | "help") {
2052 return Err(CcBypassReason::CompilerQuery);
2053 }
2054 if lower == "showincludes" || lower.starts_with("sourcedependencies") {
2055 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
2056 }
2057 if matches!(lower.as_str(), "e" | "ep" | "p") {
2058 return Err(CcBypassReason::NonObjectOutput(value.into()));
2059 }
2060 if (lower.starts_with("fa") && !lower.starts_with("favor:"))
2061 || lower.starts_with("fd")
2062 || lower.starts_with("zi")
2063 {
2064 return Err(CcBypassReason::SplitDebugOutput(value.into()));
2065 }
2066 if lower.starts_with("yc")
2067 || lower.starts_with("yu")
2068 || (lower.starts_with("fp") && !lower.starts_with("fp:"))
2069 {
2070 return Err(CcBypassReason::PrecompiledHeader(value.into()));
2071 }
2072 if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
2073 return Err(CcBypassReason::ToolPassthrough(value.into()));
2074 }
2075 if matches!(lower.as_str(), "ld" | "ldd") {
2076 return Err(CcBypassReason::NotACompile);
2077 }
2078 if lower == "c" {
2079 self.compiling = true;
2080 self.parsed.push(Argument::Plain("/c".into()));
2081 return Ok(());
2082 }
2083 for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
2084 if let Some(attached) = option.strip_prefix(prefix) {
2085 let path = PathBuf::from(self.value(canonical, attached)?);
2086 if prefix == "Fo" {
2087 self.output = Some(path.clone());
2088 } else if prefix == "I" {
2089 self.include_dirs.push(path.clone());
2090 }
2091 self.parsed.push(Argument::Path {
2092 flag: canonical.into(),
2093 path,
2094 });
2095 return Ok(());
2096 }
2097 }
2098 if lower.starts_with("external:i") {
2099 let path = PathBuf::from(self.value("/external:I", &option[10..])?);
2100 self.include_dirs.push(path.clone());
2101 self.parsed.push(Argument::Path {
2102 flag: "/external:I".into(),
2103 path,
2104 });
2105 return Ok(());
2106 }
2107 if option.starts_with("Tc") || option.starts_with("Tp") {
2108 let c = option.starts_with("Tc");
2109 let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
2110 self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
2111 return self.add_source(&path);
2112 }
2113 if lower.starts_with("pathmap:") {
2114 let rest = &option[8..];
2115 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
2116 self.parsed.push(Argument::PrefixMap {
2117 flag: "/pathmap".into(),
2118 from: PathBuf::from(from),
2119 to: to.into(),
2120 });
2121 return Ok(());
2122 }
2123 if matches!(option, "D" | "U") {
2124 let definition = self.value(value, "")?;
2125 self.parsed
2126 .push(Argument::Plain(format!("/{option}{definition}")));
2127 return Ok(());
2128 }
2129 let definition = option.starts_with('D') || option.starts_with('U');
2132 let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
2133 || lower
2134 .strip_prefix('w')
2135 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2136 || ["wd", "we", "wo"].iter().any(|prefix| {
2137 lower
2138 .strip_prefix(prefix)
2139 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2140 });
2141 let admitted = definition
2142 || warning
2143 || lower.starts_with("std:")
2144 || lower.starts_with("arch:")
2145 || lower.starts_with("favor:")
2146 || lower.starts_with("volatile:")
2147 || lower.starts_with("fp:")
2148 || lower.starts_with("eh")
2149 || lower.starts_with('o')
2150 || lower.starts_with("ob")
2151 || lower.starts_with("oi")
2152 || lower.starts_with("ot")
2153 || lower.starts_with("oy")
2154 || lower.starts_with("gs")
2155 || lower.starts_with("gr")
2156 || lower.starts_with("gy")
2157 || lower.starts_with("gw")
2158 || lower.starts_with("gl")
2159 || lower.starts_with("zc:")
2160 || lower.starts_with("diagnostics:")
2161 || matches!(
2162 lower.as_str(),
2163 "nologo"
2164 | "brepro"
2165 | "bigobj"
2166 | "utf-8"
2167 | "permissive-"
2168 | "z7"
2169 | "md"
2170 | "mdd"
2171 | "mt"
2172 | "mtd"
2173 );
2174 if admitted {
2175 self.parsed.push(Argument::Plain(value.into()));
2176 return Ok(());
2177 }
2178 Err(CcBypassReason::UnknownFlag(value.into()))
2179 }
2180}
2181
2182#[cfg(test)]
2183#[path = "cc_cache_tests.rs"]
2184mod tests;