1#![deny(missing_docs)]
19
20use mbx_cache_core::{
21 CacheDigest, FileDigestCache, PathMapping, PathNormalizationError, canonical_json,
22 normalize_mapped_path,
23};
24use serde::{Deserialize, Serialize};
25use std::collections::{BTreeMap, BTreeSet};
26use std::ffi::OsString;
27use std::path::{Component, Path, PathBuf};
28use thiserror::Error;
29
30mod depfile;
31
32pub use depfile::{CcDepfile, CcDiscoveredInputs, INCLUDE_MANIFEST_PREFIX, manifest_snapshot};
33
34pub const ACTION_SCHEMA_VERSION: u8 = 1;
36pub const ADAPTER_VERSION: u8 = 1;
38
39pub const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
41pub const MAX_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
43pub const MAX_MANIFEST_ENTRIES: usize = 16 * 1024;
45
46pub const KEYED_ENVIRONMENT: &[&str] = &[
52 "IPHONEOS_DEPLOYMENT_TARGET",
53 "LANG",
54 "LC_ALL",
55 "LC_MESSAGES",
56 "MACOSX_DEPLOYMENT_TARGET",
57 "SDKROOT",
58 "SOURCE_DATE_EPOCH",
59 "TVOS_DEPLOYMENT_TARGET",
60 "WATCHOS_DEPLOYMENT_TARGET",
61 "XROS_DEPLOYMENT_TARGET",
62];
63
64pub const BYPASS_ENVIRONMENT: &[&str] = &[
70 "CPATH",
71 "COMPILER_PATH",
72 "CPLUS_INCLUDE_PATH",
73 "C_INCLUDE_PATH",
74 "DEPENDENCIES_OUTPUT",
75 "GCC_EXEC_PREFIX",
76 "OBJC_INCLUDE_PATH",
77 "SUNPRO_DEPENDENCIES",
78];
79
80pub const SYSTEM_ROOTS: &[&str] = &[
88 "/Applications/Xcode.app",
89 "/Library/Developer",
90 "/nix/store",
91 "/usr/include",
92 "/usr/lib",
93 "/usr/local/include",
94];
95
96const SUPPORTED_F_FLAGS: &[&str] = &[
97 "PIC",
98 "PIE",
99 "asynchronous-unwind-tables",
100 "cf-protection",
101 "color-diagnostics",
102 "data-sections",
103 "diagnostics-color",
104 "exceptions",
105 "fast-math",
106 "finite-math-only",
107 "function-sections",
108 "lto",
109 "math-errno",
110 "merge-all-constants",
111 "no-asynchronous-unwind-tables",
112 "no-builtin",
113 "no-common",
114 "no-exceptions",
115 "no-fast-math",
116 "no-finite-math-only",
117 "no-lto",
118 "no-math-errno",
119 "no-omit-frame-pointer",
120 "no-plt",
121 "no-reciprocal-math",
122 "no-rtti",
123 "no-semantic-interposition",
124 "no-stack-protector",
125 "no-strict-aliasing",
126 "no-tree-vectorize",
127 "no-unroll-loops",
128 "no-unwind-tables",
129 "omit-frame-pointer",
130 "pic",
131 "pie",
132 "reciprocal-math",
133 "rtti",
134 "sanitize-undefined-strip-path-components",
135 "short-enums",
136 "signed-char",
137 "stack-clash-protection",
138 "stack-protector",
139 "stack-protector-all",
140 "stack-protector-strong",
141 "strict-aliasing",
142 "tls-model",
143 "tree-vectorize",
144 "unroll-loops",
145 "unsigned-char",
146 "unwind-tables",
147 "visibility",
148 "visibility-inlines-hidden",
149 "wrapv",
150];
151
152const SUPPORTED_M_FLAGS: &[&str] = &[
153 "32",
154 "64",
155 "adx",
156 "aes",
157 "arch",
158 "arm",
159 "avx",
160 "avx2",
161 "avx512bf16",
162 "avx512bitalg",
163 "avx512bw",
164 "avx512cd",
165 "avx512dq",
166 "avx512f",
167 "avx512fp16",
168 "avx512ifma",
169 "avx512vbmi",
170 "avx512vbmi2",
171 "avx512vl",
172 "avx512vnni",
173 "avx512vpopcntdq",
174 "avxvnni",
175 "bmi",
176 "bmi2",
177 "cpu",
178 "crc",
179 "crypto",
180 "dotprod",
181 "f16c",
182 "float-abi",
183 "fma",
184 "fpu",
185 "gfni",
186 "iphoneos-version-min",
187 "lse",
188 "lzcnt",
189 "macosx-version-min",
190 "movbe",
191 "no-avx",
192 "no-avx2",
193 "no-avx512f",
194 "no-omit-leaf-frame-pointer",
195 "no-outline-atomics",
196 "no-red-zone",
197 "no-sse",
198 "no-sse2",
199 "omit-leaf-frame-pointer",
200 "outline-atomics",
201 "pclmul",
202 "popcnt",
203 "rdrnd",
204 "rdseed",
205 "red-zone",
206 "sha",
207 "sha512",
208 "soft-float",
209 "sse",
210 "sse2",
211 "sse3",
212 "sse4.1",
213 "sse4.2",
214 "sse4a",
215 "ssse3",
216 "sve",
217 "sve2",
218 "thumb",
219 "tune",
220 "vaes",
221 "vpclmulqdq",
222 "xsave",
223 "xsavec",
224 "xsaveopt",
225 "xsaves",
226];
227
228const SUPPORTED_O_FLAGS: &[&str] = &[
229 "-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Og", "-Os", "-Oz",
230];
231
232const SUPPORTED_G_FLAGS: &[&str] = &[
233 "-g",
234 "-g0",
235 "-g1",
236 "-g2",
237 "-g3",
238 "-gdwarf-2",
239 "-gdwarf-3",
240 "-gdwarf-4",
241 "-gdwarf-5",
242 "-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 version: u8,
682 kind: &'static str,
683 adapter_version: u8,
684 #[serde(skip_serializing_if = "Option::is_none")]
685 assembly_input_model: Option<u8>,
686 #[serde(skip_serializing_if = "Option::is_none")]
687 preprocessing: Option<PreprocessingContext>,
688 compiler: CcCompilerDescriptor,
689 arguments: Vec<String>,
690 environment: BTreeMap<String, Option<String>>,
691 inputs: Vec<CcInputDescriptor>,
692}
693
694#[derive(Debug, Serialize)]
695struct CcInvocationDescriptor {
696 version: u8,
697 kind: &'static str,
698 adapter_version: u8,
699 #[serde(skip_serializing_if = "Option::is_none")]
700 assembly_input_model: Option<u8>,
701 #[serde(skip_serializing_if = "Option::is_none")]
702 preprocessing: Option<PreprocessingContext>,
703 compiler: CcCompilerDescriptor,
704 arguments: Vec<String>,
705 required_inputs: Vec<String>,
706}
707
708#[derive(Debug, Serialize)]
711struct PreprocessingContext {
712 working_dir: PathBuf,
713 arguments: Vec<String>,
714 roots: BTreeMap<String, PathBuf>,
715}
716
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
720#[serde(deny_unknown_fields)]
721pub struct CcInputPrediction {
722 pub version: u8,
724 pub inputs: Vec<String>,
726 pub environment: Vec<String>,
728 #[serde(default, skip_serializing_if = "is_zero")]
731 pub compiler_duration_ns: u64,
732 #[serde(default, skip_serializing_if = "String::is_empty")]
734 pub source_name: String,
735}
736
737fn is_zero(value: &u64) -> bool {
738 *value == 0
739}
740
741#[derive(Debug, Clone, PartialEq, Eq)]
743enum Argument {
744 Plain(String),
746 Path { flag: String, path: PathBuf },
748 PrefixMap {
750 flag: String,
751 from: PathBuf,
752 to: String,
753 },
754 Source(PathBuf),
756}
757
758#[derive(Debug, Clone, PartialEq, Eq)]
760pub struct CcInvocation {
761 arguments: Vec<Argument>,
762 source: PathBuf,
763 output: PathBuf,
764 include_dirs: Vec<PathBuf>,
765 required_inputs: Vec<PathBuf>,
766 language: CcLanguage,
767 preprocessed_assembly: bool,
768 preprocessing_arguments: Option<Vec<String>>,
769 sysroot: Option<PathBuf>,
770 caller_depfile: Option<CallerDepfile>,
771 dependency_argument_indices: Vec<usize>,
774}
775
776#[derive(Debug, Clone, PartialEq, Eq)]
786#[non_exhaustive]
787pub struct CallerDepfile {
788 pub path: PathBuf,
790 pub targets: Vec<DepfileTarget>,
792 pub user_headers_only: bool,
794 pub phony_targets: bool,
796}
797
798#[derive(Debug, Clone, PartialEq, Eq)]
800pub struct DepfileTarget {
801 pub name: String,
803 pub quoted: bool,
806}
807
808impl CcInvocation {
809 pub fn output_in(&self, working_dir: &Path) -> PathBuf {
815 if self.output.is_absolute() {
816 normalize_components(&self.output)
817 } else {
818 normalize_components(&working_dir.join(&self.output))
819 }
820 }
821
822 pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
824 self.caller_depfile.as_ref()
825 }
826
827 pub fn compiler_arguments(&self, arguments: &[OsString]) -> Vec<OsString> {
832 arguments
833 .iter()
834 .enumerate()
835 .filter(|(index, _)| !self.dependency_argument_indices.contains(index))
836 .map(|(_, argument)| argument.clone())
837 .collect()
838 }
839
840 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
843 Parser::new(arguments).parse()
844 }
845
846 pub fn parse_for(
848 arguments: &[OsString],
849 family: CcCompilerFamily,
850 ) -> Result<Self, CcBypassReason> {
851 if family.is_msvc() {
852 MsvcParser::new(arguments).parse()
853 } else {
854 let mut parser = Parser::new(arguments);
855 parser.family = family;
856 parser.parse()
857 }
858 }
859
860 pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
862 MsvcParser::new(arguments).parse()
863 }
864
865 pub fn source(&self) -> &Path {
867 &self.source
868 }
869
870 pub fn is_preprocessing(&self) -> bool {
872 self.preprocessing_arguments.is_some()
873 }
874
875 pub fn output(&self) -> &Path {
877 &self.output
878 }
879
880 pub fn include_dirs(&self) -> &[PathBuf] {
882 &self.include_dirs
883 }
884
885 pub fn required_inputs(&self) -> &[PathBuf] {
887 &self.required_inputs
888 }
889
890 pub fn language(&self) -> CcLanguage {
892 self.language
893 }
894
895 pub fn validate_discovered_inputs<'a>(
902 &self,
903 paths: impl IntoIterator<Item = &'a Path>,
904 ) -> Result<(), CcBypassReason> {
905 if !self.preprocessed_assembly {
906 return Ok(());
907 }
908 for path in paths {
909 if depfile::contains_assembler_input_directive(path)? {
910 return Err(CcBypassReason::AssemblerInputDirective(path.to_path_buf()));
911 }
912 }
913 Ok(())
914 }
915
916 pub fn sysroot(&self) -> Option<&Path> {
918 self.sysroot.as_deref()
919 }
920
921 pub fn source_name(&self) -> String {
923 self.source
924 .file_name()
925 .map(|name| name.to_string_lossy().into_owned())
926 .unwrap_or_default()
927 }
928
929 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
936 vec!["-MD".into(), "-MF".into(), depfile.into()]
937 }
938
939 pub fn dependency_arguments_for(
942 &self,
943 depfile: &Path,
944 family: CcCompilerFamily,
945 ) -> Vec<OsString> {
946 if family.is_msvc() {
947 vec!["/sourceDependencies".into(), depfile.into()]
948 } else {
949 self.dependency_arguments(depfile)
950 }
951 }
952
953 pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
955 vec!["/sourceDependencies".into(), depfile.into()]
956 }
957
958 pub fn invocation_digest(
960 &self,
961 context: &CcActionContext,
962 ) -> Result<CacheDigest, CcBypassReason> {
963 let builder = ActionBuilder::new(self, context.clone());
964 let descriptor = builder.invocation_descriptor()?;
965 let bytes = canonical_json(&descriptor)
966 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
967 Ok(CacheDigest::blake3(&bytes))
968 }
969
970 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
973 ActionBuilder::new(self, context).build()
974 }
975
976 pub fn prediction(
979 &self,
980 context: &CcActionContext,
981 compiler_duration_ns: u64,
982 ) -> Result<CcInputPrediction, CcBypassReason> {
983 let builder = ActionBuilder::new(self, context.clone());
984 let mut inputs = context
985 .inputs
986 .iter()
987 .map(|input| builder.normalize_input_path(&input.path))
988 .collect::<Result<Vec<_>, _>>()?;
989 inputs.sort();
990 inputs.dedup();
991 Ok(CcInputPrediction {
992 version: 1,
993 inputs,
994 environment: context.environment.keys().cloned().collect(),
995 compiler_duration_ns,
996 source_name: self.source_name(),
997 })
998 }
999}
1000
1001impl CcInputPrediction {
1002 pub fn discover(
1005 &self,
1006 working_dir: &Path,
1007 path_mappings: &[PathMapping],
1008 digests: &dyn FileDigestCache,
1009 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
1010 if self.version != 1 {
1011 return Err(CcBypassReason::UnsupportedPrediction);
1012 }
1013 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1014 return Err(CcBypassReason::UnsupportedPrediction);
1015 }
1016 let mappings = PathMapping::ordered(path_mappings);
1017 let mut files = BTreeSet::new();
1018 let mut directories = BTreeSet::new();
1019 for entry in &self.inputs {
1020 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
1021 Some(directory) => {
1022 directories.insert(denormalize_path(directory, &mappings)?);
1023 }
1024 None => {
1025 files.insert(denormalize_path(entry, &mappings)?);
1026 }
1027 }
1028 }
1029 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
1030 }
1031}
1032
1033fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
1039 for mapping in mappings {
1040 let prefix = format!("${{{}}}", mapping.placeholder);
1041 let suffix = if value == prefix {
1042 ""
1043 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1044 suffix
1045 } else {
1046 continue;
1047 };
1048 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
1049 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
1050 }
1051 let mut path = normalize_components(&mapping.root);
1052 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1053 return Ok(path);
1054 }
1055 let path = PathBuf::from(value);
1059 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
1060 return Ok(path);
1061 }
1062 Err(CcBypassReason::InvalidPredictedInput(value.into()))
1063}
1064
1065fn safe_suffix(suffix: &str) -> bool {
1066 suffix.is_empty()
1067 || !suffix.split('/').any(|component| {
1068 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
1069 })
1070}
1071
1072pub fn is_system_path(path: &Path) -> bool {
1074 SYSTEM_ROOTS
1075 .iter()
1076 .any(|root| path.starts_with(Path::new(root)))
1077}
1078
1079fn normalize_components(path: &Path) -> PathBuf {
1080 let mut normalized = PathBuf::new();
1081 for component in path.components() {
1082 match component {
1083 Component::CurDir => {}
1084 Component::ParentDir => {
1085 normalized.pop();
1086 }
1087 component => normalized.push(component.as_os_str()),
1088 }
1089 }
1090 normalized
1091}
1092
1093pub fn environment_inputs<F>(
1096 lookup: F,
1097 sysroot: Option<&Path>,
1098) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1099where
1100 F: Fn(&str) -> Option<String>,
1101{
1102 environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
1103}
1104
1105pub fn environment_inputs_for<F>(
1107 lookup: F,
1108 sysroot: Option<&Path>,
1109 family: CcCompilerFamily,
1110) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1111where
1112 F: Fn(&str) -> Option<String>,
1113{
1114 for name in BYPASS_ENVIRONMENT {
1115 if lookup(name).is_some() {
1116 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
1117 }
1118 }
1119 let mut environment = BTreeMap::new();
1120 for name in KEYED_ENVIRONMENT {
1121 if *name == "SDKROOT" && sysroot.is_some() {
1124 continue;
1125 }
1126 environment.insert((*name).to_string(), lookup(name));
1127 }
1128 if family.is_msvc() {
1129 for name in [
1133 "INCLUDE",
1134 "VCToolsVersion",
1135 "WindowsSDKVersion",
1136 "UCRTVersion",
1137 ] {
1138 environment.insert(name.into(), lookup(name));
1139 }
1140 for name in ["CL", "_CL_"] {
1141 if lookup(name).is_some() {
1142 return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
1143 }
1144 }
1145 }
1146 Ok(environment)
1147}
1148
1149struct ActionBuilder<'a> {
1150 invocation: &'a CcInvocation,
1151 context: CcActionContext,
1152 mappings: Vec<PathMapping>,
1153}
1154
1155impl<'a> ActionBuilder<'a> {
1156 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
1157 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1158 let mappings = context.path_mappings.clone();
1159 Self {
1160 invocation,
1161 context,
1162 mappings,
1163 }
1164 }
1165
1166 fn build(self) -> Result<CcAction, CcBypassReason> {
1167 self.validate_mappings()?;
1168 let invocation = self.invocation_descriptor()?;
1169
1170 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1171 for input in &self.context.inputs {
1172 input.digest.validate().map_err(|_| {
1173 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
1174 })?;
1175 let path = self.normalize_input_path(&input.path)?;
1176 if inputs
1177 .insert(path.clone(), input.digest.clone())
1178 .is_some_and(|existing| existing != input.digest)
1179 {
1180 return Err(CcBypassReason::ConflictingInput(path));
1181 }
1182 }
1183 let required = self
1184 .invocation
1185 .required_inputs
1186 .iter()
1187 .map(|path| self.normalize_path(path))
1188 .collect::<Result<BTreeSet<_>, _>>()?;
1189 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1190 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
1191 }
1192 let inputs = inputs
1193 .into_iter()
1194 .map(|(path, digest)| CcInputDescriptor { path, digest })
1195 .collect();
1196 let descriptor = CcActionDescriptor {
1197 version: ACTION_SCHEMA_VERSION,
1198 kind: "cc",
1199 adapter_version: ADAPTER_VERSION,
1200 assembly_input_model: invocation.assembly_input_model,
1201 preprocessing: invocation.preprocessing,
1202 compiler: invocation.compiler,
1203 arguments: invocation.arguments,
1204 environment: self.context.environment.clone(),
1205 inputs,
1206 };
1207 let bytes = canonical_json(&descriptor)
1208 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1209 let digest = CacheDigest::blake3(&bytes);
1210 Ok(CcAction { digest, bytes })
1211 }
1212
1213 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1214 self.validate_mappings()?;
1215 let arguments = self
1216 .invocation
1217 .arguments
1218 .iter()
1219 .map(|argument| self.normalize_argument(argument))
1220 .collect::<Result<Vec<_>, _>>()?;
1221 let required_inputs = self
1222 .invocation
1223 .required_inputs
1224 .iter()
1225 .map(|path| self.normalize_path(path))
1226 .collect::<Result<BTreeSet<_>, _>>()?
1227 .into_iter()
1228 .collect();
1229 Ok(CcInvocationDescriptor {
1230 version: ACTION_SCHEMA_VERSION,
1231 kind: "cc",
1232 adapter_version: ADAPTER_VERSION,
1233 assembly_input_model: self.invocation.preprocessed_assembly.then_some(1),
1236 preprocessing: self
1237 .invocation
1238 .preprocessing_arguments
1239 .as_ref()
1240 .map(|arguments| PreprocessingContext {
1241 working_dir: self.context.working_dir.clone(),
1242 arguments: arguments.clone(),
1243 roots: self
1244 .mappings
1245 .iter()
1246 .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1247 .collect(),
1248 }),
1249 compiler: self.compiler_descriptor(),
1250 arguments,
1251 required_inputs,
1252 })
1253 }
1254
1255 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1256 CcCompilerDescriptor {
1257 assembler: self.context.compiler.assembler.clone(),
1258 family: self.context.compiler.family.as_str().into(),
1259 target: self.context.compiler.target.clone(),
1260 version_text: self.context.compiler.version_text.clone(),
1261 }
1262 }
1263
1264 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1265 if !self.context.working_dir.is_absolute() {
1266 return Err(CcBypassReason::RelativeWorkingDirectory(
1267 self.context.working_dir.clone(),
1268 ));
1269 }
1270 let mut roots = BTreeSet::new();
1271 let mut placeholders = BTreeSet::new();
1272 for mapping in &self.mappings {
1273 if !mapping.root.is_absolute() {
1274 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1275 }
1276 if mapping.placeholder.is_empty()
1277 || !mapping
1278 .placeholder
1279 .bytes()
1280 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1281 || !roots.insert(normalize_components(&mapping.root))
1282 || !placeholders.insert(&mapping.placeholder)
1283 {
1284 return Err(CcBypassReason::InvalidPathPlaceholder(
1285 mapping.placeholder.clone(),
1286 ));
1287 }
1288 }
1289 Ok(())
1290 }
1291
1292 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1293 match argument {
1294 Argument::Plain(value) => Ok(value.clone()),
1295 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1296 Argument::PrefixMap { flag, from, to } => {
1297 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1298 }
1299 Argument::Source(path) => Ok(self.normalize_path(path)?),
1300 }
1301 }
1302
1303 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1310 match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
1311 Ok(normalized) => Ok(normalized),
1312 Err(reason) => {
1313 let absolute = absolute_path(path, &self.context.working_dir);
1314 if is_system_path(&absolute) {
1315 return absolute
1316 .to_str()
1317 .map(ToOwned::to_owned)
1318 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1319 }
1320 Err(reason.into())
1321 }
1322 }
1323 }
1324
1325 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1326 match path.to_str().and_then(|path| {
1327 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1328 .map(ToOwned::to_owned)
1329 }) {
1330 Some(directory) => Ok(format!(
1331 "{INCLUDE_MANIFEST_PREFIX}{}",
1332 self.normalize_path(Path::new(&directory))?
1333 )),
1334 None => self.normalize_path(path),
1335 }
1336 }
1337}
1338
1339fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1340 if path.is_absolute() {
1341 normalize_components(path)
1342 } else {
1343 normalize_components(&working_dir.join(path))
1344 }
1345}
1346
1347struct Parser<'a> {
1348 family: CcCompilerFamily,
1349 arguments: &'a [OsString],
1350 index: usize,
1351 parsed: Vec<Argument>,
1352 source: Option<PathBuf>,
1353 output: Option<PathBuf>,
1354 include_dirs: Vec<PathBuf>,
1355 required_inputs: Vec<PathBuf>,
1356 sysroot: Option<PathBuf>,
1357 explicit_language: Option<CcLanguage>,
1358 preprocessed_assembly: bool,
1359 compiling: bool,
1360 preprocessing: bool,
1361 dependency: DependencyRequest,
1362}
1363
1364#[derive(Debug, Default)]
1366struct DependencyRequest {
1367 user_headers_only: Option<bool>,
1369 file: Option<PathBuf>,
1370 targets: Vec<DepfileTarget>,
1371 phony_targets: bool,
1372 modifier: Option<String>,
1375 indices: Vec<usize>,
1376}
1377
1378impl<'a> Parser<'a> {
1379 fn new(arguments: &'a [OsString]) -> Self {
1380 Self {
1381 family: CcCompilerFamily::Gcc,
1382 arguments,
1383 index: 0,
1384 parsed: Vec::new(),
1385 source: None,
1386 output: None,
1387 include_dirs: Vec::new(),
1388 required_inputs: Vec::new(),
1389 sysroot: None,
1390 explicit_language: None,
1391 preprocessed_assembly: false,
1392 compiling: false,
1393 preprocessing: false,
1394 dependency: DependencyRequest::default(),
1395 }
1396 }
1397
1398 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1399 while self.index < self.arguments.len() {
1400 let value = self.current()?.to_string();
1401 self.index += 1;
1402 if value == "-" {
1403 return Err(CcBypassReason::StandardInput);
1404 }
1405 if let Some(argfile) = value.strip_prefix('@') {
1406 return Err(CcBypassReason::ResponseFile(argfile.into()));
1407 }
1408 if value.starts_with('-') {
1409 self.parse_flag(&value)?;
1410 } else {
1411 self.parse_input(&value)?;
1412 }
1413 }
1414
1415 if !self.compiling && !self.preprocessing {
1416 return Err(CcBypassReason::NotACompile);
1417 }
1418 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1419 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1420 if self.preprocessing && output == Path::new("-") {
1421 return Err(CcBypassReason::NonObjectOutput("-E -o -".into()));
1422 }
1423 if self.preprocessing
1426 && self.dependency.user_headers_only.is_some()
1427 && self.dependency.file.is_none()
1428 {
1429 return Err(CcBypassReason::CallerDependencyFlags(
1430 "-E -MD/-MMD without -MF".into(),
1431 ));
1432 }
1433 if self.preprocessing && self.dependency.file.as_deref() == Some(Path::new("-")) {
1434 return Err(CcBypassReason::CallerDependencyFlags("-E -MF -".into()));
1435 }
1436 let language = self.language(&source)?;
1437 let preprocessed_assembly = self.preprocessed_assembly
1438 || (self.explicit_language.is_none()
1439 && source.extension().and_then(|value| value.to_str()) == Some("S"));
1440 self.required_inputs.push(source.clone());
1441 let caller_depfile = match self.dependency.user_headers_only {
1442 Some(user_headers_only) => Some(CallerDepfile {
1443 path: self
1444 .dependency
1445 .file
1446 .unwrap_or_else(|| output.with_extension("d")),
1447 targets: if self.dependency.targets.is_empty() {
1448 vec![DepfileTarget {
1449 name: if self.preprocessing && self.family == CcCompilerFamily::Gcc {
1452 Path::new(source.file_name().ok_or(CcBypassReason::MissingInput)?)
1453 .with_extension("o")
1454 .to_string_lossy()
1455 .into_owned()
1456 } else {
1457 output.to_string_lossy().into_owned()
1458 },
1459 quoted: true,
1460 }]
1461 } else {
1462 self.dependency.targets
1463 },
1464 user_headers_only,
1465 phony_targets: self.dependency.phony_targets,
1466 }),
1467 None => match self.dependency.modifier {
1471 Some(modifier) => return Err(CcBypassReason::CallerDependencyFlags(modifier)),
1472 None => None,
1473 },
1474 };
1475 Ok(CcInvocation {
1476 arguments: self.parsed,
1477 source,
1478 output,
1479 include_dirs: self.include_dirs,
1480 required_inputs: self.required_inputs,
1481 language,
1482 preprocessed_assembly,
1483 preprocessing_arguments: self.preprocessing.then(|| {
1484 self.arguments
1485 .iter()
1486 .map(|arg| arg.to_str().expect("arguments were validated").to_owned())
1487 .collect()
1488 }),
1489 sysroot: self.sysroot,
1490 caller_depfile,
1491 dependency_argument_indices: self.dependency.indices,
1492 })
1493 }
1494
1495 fn parse_dependency_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1503 let start = self.index - 1;
1504 match option {
1505 "D" => self.dependency.user_headers_only = Some(false),
1506 "MD" => self.dependency.user_headers_only = Some(true),
1507 "P" => {
1508 self.dependency.phony_targets = true;
1509 self.dependency.modifier = Some(value.into());
1510 }
1511 _ if option.starts_with('F') => {
1512 let file = self.take_value("-MF", Some(&option[1..]))?;
1513 self.dependency.file = Some(PathBuf::from(file));
1514 self.dependency.modifier = Some("-MF".into());
1515 }
1516 _ if option.starts_with('T') || option.starts_with('Q') => {
1517 let flag = &value[..3];
1518 let name = self.take_value(flag, Some(&option[1..]))?;
1519 self.dependency.targets.push(DepfileTarget {
1520 name,
1521 quoted: option.starts_with('Q'),
1522 });
1523 self.dependency.modifier = Some(flag.into());
1524 }
1525 _ => return Err(CcBypassReason::CallerDependencyFlags(value.into())),
1526 }
1527 self.dependency.indices.extend(start..self.index);
1528 Ok(())
1529 }
1530
1531 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1532 if let Some(language) = self.explicit_language {
1533 return Ok(language);
1534 }
1535 let extension = source
1536 .extension()
1537 .and_then(|extension| extension.to_str())
1538 .unwrap_or_default();
1539 match extension {
1540 "c" | "S" => Ok(CcLanguage::C),
1546 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1547 _ => Err(CcBypassReason::UnsupportedLanguage(
1548 source.display().to_string(),
1549 )),
1550 }
1551 }
1552
1553 fn current(&self) -> Result<&str, CcBypassReason> {
1554 self.arguments[self.index]
1555 .to_str()
1556 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1557 }
1558
1559 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1560 if let Some(value) = inline
1561 && !value.is_empty()
1562 {
1563 return Ok(value.into());
1564 }
1565 if self.index >= self.arguments.len() {
1566 return Err(CcBypassReason::MissingValue(flag.into()));
1567 }
1568 let value = self.current()?.to_string();
1569 self.index += 1;
1570 Ok(value)
1571 }
1572
1573 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1574 if self.source.is_some() {
1575 return Err(CcBypassReason::MultipleInputs);
1576 }
1577 let path = PathBuf::from(value);
1578 if self.explicit_language.is_none() {
1582 let extension = path
1583 .extension()
1584 .and_then(|extension| extension.to_str())
1585 .unwrap_or_default();
1586 if !matches!(extension, "c" | "S" | "cc" | "cpp" | "cxx" | "c++") {
1587 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1588 }
1589 }
1590 self.source = Some(path.clone());
1591 self.parsed.push(Argument::Source(path));
1592 Ok(())
1593 }
1594
1595 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1596 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1597 return Err(CcBypassReason::CompilerQuery);
1598 }
1599 if value == "-E" {
1600 self.preprocessing = true;
1601 self.parsed.push(Argument::Plain(value.into()));
1602 return Ok(());
1603 }
1604 if value == "-S" {
1605 return Err(CcBypassReason::NonObjectOutput(value.into()));
1606 }
1607 if let Some(option) = value.strip_prefix("-M") {
1608 return self.parse_dependency_flag(value, option);
1609 }
1610 if value.starts_with("-save-temps") {
1611 return Err(CcBypassReason::SaveTemps(value.into()));
1612 }
1613 if value == "--coverage" {
1614 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1615 }
1616 if let Some(options) = value.strip_prefix("-Wa,")
1617 && !options.is_empty()
1618 && options
1619 .split(',')
1620 .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1621 {
1622 self.parsed.push(Argument::Plain(value.into()));
1623 return Ok(());
1624 }
1625 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1626 || value.starts_with("-Wp,")
1627 || value.starts_with("-Wa,")
1628 || value.starts_with("-Wl,")
1629 {
1630 return Err(CcBypassReason::ToolPassthrough(value.into()));
1633 }
1634 if value.starts_with("-include-pch") || value == "-emit-pch" {
1635 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1636 }
1637
1638 if value == "-c" {
1639 self.compiling = true;
1640 self.parsed.push(Argument::Plain(value.into()));
1641 return Ok(());
1642 }
1643 if SUPPORTED_BARE_FLAGS.contains(&value)
1644 || SUPPORTED_O_FLAGS.contains(&value)
1645 || SUPPORTED_G_FLAGS.contains(&value)
1646 || value.starts_with("-std=")
1647 {
1648 self.parsed.push(Argument::Plain(value.into()));
1649 return Ok(());
1650 }
1651 if let Some(rest) = value.strip_prefix("-o") {
1652 let path = self.take_value("-o", Some(rest))?;
1653 self.output = Some(PathBuf::from(&path));
1656 self.parsed.push(Argument::Path {
1657 flag: "-o".into(),
1658 path: PathBuf::from(path),
1659 });
1660 return Ok(());
1661 }
1662 if let Some(rest) = value.strip_prefix("-I") {
1663 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1664 self.include_dirs.push(path.clone());
1665 self.parsed.push(Argument::Path {
1666 flag: "-I".into(),
1667 path,
1668 });
1669 return Ok(());
1670 }
1671 if SEPARATE_PATH_FLAGS.contains(&value) {
1672 let path = PathBuf::from(self.take_value(value, None)?);
1673 match value {
1674 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1675 "-isysroot" => self.sysroot = Some(path.clone()),
1676 _ => {}
1682 }
1683 self.parsed.push(Argument::Path {
1684 flag: value.into(),
1685 path,
1686 });
1687 return Ok(());
1688 }
1689 if let Some(rest) = value.strip_prefix("--include=") {
1692 let path = PathBuf::from(rest);
1693 self.parsed.push(Argument::Path {
1694 flag: "-include".into(),
1695 path,
1696 });
1697 return Ok(());
1698 }
1699 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1700 value
1701 .strip_prefix(&format!("{flag}="))
1702 .map(|rest| (*flag, rest))
1703 }) {
1704 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1705 self.parsed.push(Argument::PrefixMap {
1706 flag: flag.into(),
1707 from: PathBuf::from(from),
1708 to: to.into(),
1709 });
1710 return Ok(());
1711 }
1712 if value == "--param" {
1714 let parameter = self.take_value("--param", None)?;
1715 self.parsed
1716 .push(Argument::Plain(format!("--param={parameter}")));
1717 return Ok(());
1718 }
1719 if let Some(parameter) = value.strip_prefix("--param=") {
1720 self.parsed
1721 .push(Argument::Plain(format!("--param={parameter}")));
1722 return Ok(());
1723 }
1724 if let Some(rest) = value.strip_prefix("--sysroot=") {
1725 let path = PathBuf::from(rest);
1726 self.sysroot = Some(path.clone());
1727 self.parsed.push(Argument::Path {
1728 flag: "--sysroot".into(),
1729 path,
1730 });
1731 return Ok(());
1732 }
1733 if let Some(rest) = value
1734 .strip_prefix("-D")
1735 .or_else(|| value.strip_prefix("-U"))
1736 {
1737 let flag = &value[..2];
1738 let definition = self.take_value(flag, Some(rest))?;
1739 self.parsed
1740 .push(Argument::Plain(format!("{flag}{definition}")));
1741 return Ok(());
1742 }
1743 if let Some(rest) = value.strip_prefix("-x") {
1744 let language = self.take_value("-x", Some(rest))?;
1745 self.explicit_language = Some(match language.as_str() {
1746 "c" => CcLanguage::C,
1747 "assembler-with-cpp" => {
1748 self.preprocessed_assembly = true;
1749 CcLanguage::C
1750 }
1751 "c++" => CcLanguage::Cxx,
1752 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1753 });
1754 self.parsed.push(Argument::Plain(format!("-x{language}")));
1755 return Ok(());
1756 }
1757 if let Some(target) = value.strip_prefix("--target=") {
1758 self.parsed
1759 .push(Argument::Plain(format!("--target={target}")));
1760 return Ok(());
1761 }
1762 if value == "-target" {
1763 let target = self.take_value("-target", None)?;
1764 self.parsed
1765 .push(Argument::Plain(format!("--target={target}")));
1766 return Ok(());
1767 }
1768 if value == "-arch" {
1769 let arch = self.take_value("-arch", None)?;
1770 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1771 return Ok(());
1772 }
1773 if let Some(option) = value.strip_prefix("-f") {
1774 return self.parse_f_flag(value, option);
1775 }
1776 if let Some(option) = value.strip_prefix("-m") {
1777 return self.parse_m_flag(value, option);
1778 }
1779 if value.starts_with("-g") {
1780 return Err(if value.starts_with("-gsplit-dwarf") {
1783 CcBypassReason::SplitDebugOutput(value.into())
1784 } else {
1785 CcBypassReason::UnknownFlag(value.into())
1786 });
1787 }
1788 if value.starts_with("-W") {
1789 self.parsed.push(Argument::Plain(value.into()));
1793 return Ok(());
1794 }
1795 Err(CcBypassReason::UnknownFlag(value.into()))
1796 }
1797
1798 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1799 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1800 return Err(CcBypassReason::Plugin(value.into()));
1801 }
1802 if option.starts_with("profile-") || option == "test-coverage" {
1803 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1804 }
1805 let name = option.split_once('=').map_or(option, |(name, _)| name);
1806 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1807 return Err(CcBypassReason::UnknownFlag(value.into()));
1808 }
1809 self.parsed.push(Argument::Plain(value.into()));
1810 Ok(())
1811 }
1812
1813 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1814 if option == "llvm" {
1815 return Err(CcBypassReason::ToolPassthrough(value.into()));
1816 }
1817 if let Some((name, selection)) = option.split_once('=')
1822 && matches!(name, "arch" | "cpu" | "tune")
1823 && matches!(selection, "native" | "host")
1824 {
1825 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1826 }
1827 let name = option.split_once('=').map_or(option, |(name, _)| name);
1828 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1829 return Err(CcBypassReason::UnknownFlag(value.into()));
1830 }
1831 self.parsed.push(Argument::Plain(value.into()));
1832 Ok(())
1833 }
1834}
1835
1836struct MsvcParser<'a> {
1840 arguments: &'a [OsString],
1841 index: usize,
1842 parsed: Vec<Argument>,
1843 source: Option<PathBuf>,
1844 output: Option<PathBuf>,
1845 include_dirs: Vec<PathBuf>,
1846 required_inputs: Vec<PathBuf>,
1847 explicit_language: Option<CcLanguage>,
1848 compiling: bool,
1849}
1850
1851impl<'a> MsvcParser<'a> {
1852 fn new(arguments: &'a [OsString]) -> Self {
1853 Self {
1854 arguments,
1855 index: 0,
1856 parsed: Vec::new(),
1857 source: None,
1858 output: None,
1859 include_dirs: Vec::new(),
1860 required_inputs: Vec::new(),
1861 explicit_language: None,
1862 compiling: false,
1863 }
1864 }
1865
1866 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1867 while self.index < self.arguments.len() {
1868 let value = self.current()?.to_owned();
1869 self.index += 1;
1870 if let Some(file) = value.strip_prefix('@') {
1871 return Err(CcBypassReason::ResponseFile(file.into()));
1872 }
1873 if value.starts_with('/') || value.starts_with('-') {
1874 self.parse_flag(&value)?;
1875 } else {
1876 self.add_source(&value)?;
1877 }
1878 }
1879 if !self.compiling {
1880 return Err(CcBypassReason::NotACompile);
1881 }
1882 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1883 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1884 let language = self.explicit_language.unwrap_or_else(|| {
1885 if source
1886 .extension()
1887 .and_then(|value| value.to_str())
1888 .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1889 {
1890 CcLanguage::C
1891 } else {
1892 CcLanguage::Cxx
1893 }
1894 });
1895 self.required_inputs.push(source.clone());
1896 Ok(CcInvocation {
1897 arguments: self.parsed,
1898 source,
1899 output,
1900 include_dirs: self.include_dirs,
1901 required_inputs: self.required_inputs,
1902 language,
1903 preprocessed_assembly: false,
1904 preprocessing_arguments: None,
1905 sysroot: None,
1906 caller_depfile: None,
1907 dependency_argument_indices: Vec::new(),
1908 })
1909 }
1910
1911 fn current(&self) -> Result<&str, CcBypassReason> {
1912 self.arguments[self.index]
1913 .to_str()
1914 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1915 }
1916
1917 fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
1918 if !attached.is_empty() {
1919 return Ok(attached.into());
1920 }
1921 if self.index == self.arguments.len() {
1922 return Err(CcBypassReason::MissingValue(flag.into()));
1923 }
1924 let value = self.current()?.to_owned();
1925 self.index += 1;
1926 Ok(value)
1927 }
1928
1929 fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
1930 if self.source.is_some() {
1931 return Err(CcBypassReason::MultipleInputs);
1932 }
1933 let path = PathBuf::from(value);
1934 if self.explicit_language.is_none()
1935 && !path
1936 .extension()
1937 .and_then(|value| value.to_str())
1938 .is_some_and(|value| {
1939 matches!(
1940 value.to_ascii_lowercase().as_str(),
1941 "c" | "cc" | "cpp" | "cxx"
1942 )
1943 })
1944 {
1945 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1946 }
1947 self.source = Some(path.clone());
1948 self.parsed.push(Argument::Source(path));
1949 Ok(())
1950 }
1951
1952 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1953 let option = value.trim_start_matches(['/', '-']);
1954 let lower = option.to_ascii_lowercase();
1955 if matches!(lower.as_str(), "?" | "help") {
1956 return Err(CcBypassReason::CompilerQuery);
1957 }
1958 if lower == "showincludes" || lower.starts_with("sourcedependencies") {
1959 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1960 }
1961 if matches!(lower.as_str(), "e" | "ep" | "p") {
1962 return Err(CcBypassReason::NonObjectOutput(value.into()));
1963 }
1964 if (lower.starts_with("fa") && !lower.starts_with("favor:"))
1965 || lower.starts_with("fd")
1966 || lower.starts_with("zi")
1967 {
1968 return Err(CcBypassReason::SplitDebugOutput(value.into()));
1969 }
1970 if lower.starts_with("yc")
1971 || lower.starts_with("yu")
1972 || (lower.starts_with("fp") && !lower.starts_with("fp:"))
1973 {
1974 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1975 }
1976 if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
1977 return Err(CcBypassReason::ToolPassthrough(value.into()));
1978 }
1979 if matches!(lower.as_str(), "ld" | "ldd") {
1980 return Err(CcBypassReason::NotACompile);
1981 }
1982 if lower == "c" {
1983 self.compiling = true;
1984 self.parsed.push(Argument::Plain("/c".into()));
1985 return Ok(());
1986 }
1987 for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
1988 if let Some(attached) = option.strip_prefix(prefix) {
1989 let path = PathBuf::from(self.value(canonical, attached)?);
1990 if prefix == "Fo" {
1991 self.output = Some(path.clone());
1992 } else if prefix == "I" {
1993 self.include_dirs.push(path.clone());
1994 }
1995 self.parsed.push(Argument::Path {
1996 flag: canonical.into(),
1997 path,
1998 });
1999 return Ok(());
2000 }
2001 }
2002 if lower.starts_with("external:i") {
2003 let path = PathBuf::from(self.value("/external:I", &option[10..])?);
2004 self.include_dirs.push(path.clone());
2005 self.parsed.push(Argument::Path {
2006 flag: "/external:I".into(),
2007 path,
2008 });
2009 return Ok(());
2010 }
2011 if option.starts_with("Tc") || option.starts_with("Tp") {
2012 let c = option.starts_with("Tc");
2013 let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
2014 self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
2015 return self.add_source(&path);
2016 }
2017 if lower.starts_with("pathmap:") {
2018 let rest = &option[8..];
2019 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
2020 self.parsed.push(Argument::PrefixMap {
2021 flag: "/pathmap".into(),
2022 from: PathBuf::from(from),
2023 to: to.into(),
2024 });
2025 return Ok(());
2026 }
2027 if matches!(option, "D" | "U") {
2028 let definition = self.value(value, "")?;
2029 self.parsed
2030 .push(Argument::Plain(format!("/{option}{definition}")));
2031 return Ok(());
2032 }
2033 let definition = option.starts_with('D') || option.starts_with('U');
2036 let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
2037 || lower
2038 .strip_prefix('w')
2039 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2040 || ["wd", "we", "wo"].iter().any(|prefix| {
2041 lower
2042 .strip_prefix(prefix)
2043 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2044 });
2045 let admitted = definition
2046 || warning
2047 || lower.starts_with("std:")
2048 || lower.starts_with("arch:")
2049 || lower.starts_with("favor:")
2050 || lower.starts_with("volatile:")
2051 || lower.starts_with("fp:")
2052 || lower.starts_with("eh")
2053 || lower.starts_with('o')
2054 || lower.starts_with("ob")
2055 || lower.starts_with("oi")
2056 || lower.starts_with("ot")
2057 || lower.starts_with("oy")
2058 || lower.starts_with("gs")
2059 || lower.starts_with("gr")
2060 || lower.starts_with("gy")
2061 || lower.starts_with("gw")
2062 || lower.starts_with("gl")
2063 || lower.starts_with("zc:")
2064 || lower.starts_with("diagnostics:")
2065 || matches!(
2066 lower.as_str(),
2067 "nologo"
2068 | "brepro"
2069 | "bigobj"
2070 | "utf-8"
2071 | "permissive-"
2072 | "z7"
2073 | "md"
2074 | "mdd"
2075 | "mt"
2076 | "mtd"
2077 );
2078 if admitted {
2079 self.parsed.push(Argument::Plain(value.into()));
2080 return Ok(());
2081 }
2082 Err(CcBypassReason::UnknownFlag(value.into()))
2083 }
2084}
2085
2086#[cfg(test)]
2087#[path = "cc_cache_tests.rs"]
2088mod tests;