1#![deny(missing_docs)]
19
20use mbx_cache_core::{
21 CacheDigest, FileDigestCache, PathAliases, PathMapping, PathNormalizationError, canonical_json,
22 normalize_resolved_mapped_path_with, resolve_path_mappings,
23};
24use serde::{Deserialize, Serialize};
25use std::collections::{BTreeMap, BTreeSet};
26use std::ffi::OsString;
27use std::path::{Component, Path, PathBuf};
28use thiserror::Error;
29
30mod depfile;
31
32pub use depfile::{CcDepfile, CcDiscoveredInputs, INCLUDE_MANIFEST_PREFIX, manifest_snapshot};
33
34pub const ACTION_SCHEMA_VERSION: u8 = 1;
36pub const ADAPTER_VERSION: u8 = 1;
38
39pub const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
41pub const MAX_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
43pub const MAX_MANIFEST_ENTRIES: usize = 16 * 1024;
45
46pub const KEYED_ENVIRONMENT: &[&str] = &[
52 "IPHONEOS_DEPLOYMENT_TARGET",
53 "LANG",
54 "LC_ALL",
55 "LC_MESSAGES",
56 "MACOSX_DEPLOYMENT_TARGET",
57 "SDKROOT",
58 "SOURCE_DATE_EPOCH",
59 "TVOS_DEPLOYMENT_TARGET",
60 "WATCHOS_DEPLOYMENT_TARGET",
61 "XROS_DEPLOYMENT_TARGET",
62];
63
64pub const BYPASS_ENVIRONMENT: &[&str] = &[
70 "CPATH",
71 "COMPILER_PATH",
72 "CPLUS_INCLUDE_PATH",
73 "C_INCLUDE_PATH",
74 "DEPENDENCIES_OUTPUT",
75 "GCC_EXEC_PREFIX",
76 "OBJC_INCLUDE_PATH",
77 "SUNPRO_DEPENDENCIES",
78];
79
80pub const SYSTEM_ROOTS: &[&str] = &[
88 "/Applications/Xcode.app",
89 "/Library/Developer",
90 "/nix/store",
91 "/usr/include",
92 "/usr/lib",
93 "/usr/local/include",
94];
95
96const SUPPORTED_F_FLAGS: &[&str] = &[
97 "PIC",
98 "PIE",
99 "asynchronous-unwind-tables",
100 "cf-protection",
101 "color-diagnostics",
102 "data-sections",
103 "diagnostics-color",
104 "exceptions",
105 "fast-math",
106 "finite-math-only",
107 "function-sections",
108 "lto",
109 "math-errno",
110 "merge-all-constants",
111 "no-asynchronous-unwind-tables",
112 "no-builtin",
113 "no-common",
114 "no-exceptions",
115 "no-fast-math",
116 "no-finite-math-only",
117 "no-lto",
118 "no-math-errno",
119 "no-omit-frame-pointer",
120 "no-plt",
121 "no-reciprocal-math",
122 "no-rtti",
123 "no-semantic-interposition",
124 "no-stack-protector",
125 "no-strict-aliasing",
126 "no-tree-vectorize",
127 "no-unroll-loops",
128 "no-unwind-tables",
129 "omit-frame-pointer",
130 "pic",
131 "pie",
132 "reciprocal-math",
133 "rtti",
134 "sanitize-undefined-strip-path-components",
135 "short-enums",
136 "signed-char",
137 "stack-clash-protection",
138 "stack-protector",
139 "stack-protector-all",
140 "stack-protector-strong",
141 "strict-aliasing",
142 "tls-model",
143 "tree-vectorize",
144 "unroll-loops",
145 "unsigned-char",
146 "unwind-tables",
147 "visibility",
148 "visibility-inlines-hidden",
149 "wrapv",
150];
151
152const SUPPORTED_M_FLAGS: &[&str] = &[
153 "32",
154 "64",
155 "adx",
156 "aes",
157 "arch",
158 "arm",
159 "avx",
160 "avx2",
161 "avx512bf16",
162 "avx512bitalg",
163 "avx512bw",
164 "avx512cd",
165 "avx512dq",
166 "avx512f",
167 "avx512fp16",
168 "avx512ifma",
169 "avx512vbmi",
170 "avx512vbmi2",
171 "avx512vl",
172 "avx512vnni",
173 "avx512vpopcntdq",
174 "avxvnni",
175 "bmi",
176 "bmi2",
177 "cpu",
178 "crc",
179 "crypto",
180 "dotprod",
181 "f16c",
182 "float-abi",
183 "fma",
184 "fpu",
185 "gfni",
186 "iphoneos-version-min",
187 "lse",
188 "lzcnt",
189 "macosx-version-min",
190 "movbe",
191 "no-avx",
192 "no-avx2",
193 "no-avx512f",
194 "no-omit-leaf-frame-pointer",
195 "no-outline-atomics",
196 "no-red-zone",
197 "no-sse",
198 "no-sse2",
199 "omit-leaf-frame-pointer",
200 "outline-atomics",
201 "pclmul",
202 "popcnt",
203 "rdrnd",
204 "rdseed",
205 "red-zone",
206 "sha",
207 "sha512",
208 "soft-float",
209 "sse",
210 "sse2",
211 "sse3",
212 "sse4.1",
213 "sse4.2",
214 "sse4a",
215 "ssse3",
216 "sve",
217 "sve2",
218 "thumb",
219 "tune",
220 "vaes",
221 "vpclmulqdq",
222 "xsave",
223 "xsavec",
224 "xsaveopt",
225 "xsaves",
226];
227
228const SUPPORTED_O_FLAGS: &[&str] = &[
229 "-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Og", "-Os", "-Oz",
230];
231
232const SUPPORTED_G_FLAGS: &[&str] = &[
233 "-g",
234 "-g0",
235 "-g1",
236 "-g2",
237 "-g3",
238 "-gdwarf-2",
239 "-gdwarf-3",
240 "-gdwarf-4",
241 "-gdwarf-5",
242 "-gfull",
243 "-ggdb",
244 "-ggdb0",
245 "-ggdb1",
246 "-ggdb2",
247 "-ggdb3",
248];
249
250const SUPPORTED_BARE_FLAGS: &[&str] = &[
251 "-P", "-ansi",
253 "-nostdinc",
254 "-nostdinc++",
255 "-pedantic",
256 "-pedantic-errors",
257 "-pipe",
258 "-pthread",
259 "-w",
260];
261
262const SEPARATE_PATH_FLAGS: &[&str] = &[
263 "-idirafter",
264 "-imacros",
265 "-include",
266 "-iquote",
267 "-isysroot",
268 "-isystem",
269];
270
271const TOOL_PASSTHROUGH_FLAGS: &[&str] = &["-Xassembler", "-Xclang", "-Xlinker", "-Xpreprocessor"];
272
273const SUPPORTED_ASSEMBLER_OPTIONS: &[&str] = &["--noexecstack"];
278
279const COMPILER_QUERY_FLAGS: &[&str] = &[
280 "--help",
281 "--version",
282 "-###",
283 "-?",
286 "-dumpmachine",
287 "-dumpversion",
288 "-v",
289];
290
291const PREFIX_MAP_FLAGS: &[&str] = &[
296 "-fdebug-prefix-map",
297 "-ffile-prefix-map",
298 "-fmacro-prefix-map",
299];
300
301impl CcBypassReason {
302 pub fn kind(&self) -> &'static str {
307 self.into()
308 }
309
310 pub fn remediation(&self) -> Option<&'static str> {
317 match self {
318 Self::UnsupportedEnvironment(_) => Some(
319 "Unset the reported environment variable for this build so the compiler invocation describes all of its inputs.",
320 ),
321 Self::LocalCpuTarget(_) => Some(
322 "Replace the reported local-CPU option with an explicit architecture or CPU name.",
323 ),
324 Self::EmbeddedTimestampMacro(_) => Some(
325 "Remove the reported timestamp macro, or keep this compilation uncached if its changing value is intentional.",
326 ),
327 Self::SearchPathModifiedDuringCompilation(_) => Some(
328 "Generate headers before compilation instead of changing an include directory while the compiler is running.",
329 ),
330 Self::UnknownFlag(_) | Self::ToolPassthrough(_) => Some(
331 "Upgrade mbx or report the unmodeled compiler option. If you control the build script, removing the option can also make the compilation cacheable.",
332 ),
333 Self::UnmappedAbsolutePath(_) => Some(
334 "Move the input under a mapped project or system root, or keep this compilation uncached.",
335 ),
336 _ => None,
337 }
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
346#[strum(serialize_all = "kebab-case")]
347#[non_exhaustive]
348pub enum CcBypassReason {
349 #[error("compiler argument {index} is not valid UTF-8")]
351 NonUtf8Argument {
352 index: usize,
354 },
355 #[error("compiler response file is not modeled by the cache adapter: {0}")]
357 ResponseFile(String),
358 #[error("compiler flag is not modeled by the cache adapter: {0}")]
360 UnknownFlag(String),
361 #[error("compiler flag {0} is missing its value")]
363 MissingValue(String),
364 #[error("compiler invocation queries the driver instead of compiling")]
366 CompilerQuery,
367 #[error("compiler invocation does not compile with -c")]
369 NotACompile,
370 #[error("compiler invocation emits a non-object output: {0}")]
372 NonObjectOutput(String),
373 #[error("compiler invocation reads its source from standard input")]
375 StandardInput,
376 #[error("compiler invocation names no source file")]
378 MissingInput,
379 #[error("compiler invocation names more than one source file")]
381 MultipleInputs,
382 #[error("compiler invocation names no output file")]
384 MissingOutput,
385 #[error("compiler input language is not modeled by the cache adapter: {0}")]
387 UnsupportedLanguage(String),
388 #[error("compiler invocation requests its own dependency output: {0}")]
390 CallerDependencyFlags(String),
391 #[error("precompiled headers are not modeled by the cache adapter: {0}")]
393 PrecompiledHeader(String),
394 #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
396 CoverageInstrumentation(String),
397 #[error("split debug output is not modeled by the cache adapter: {0}")]
399 SplitDebugOutput(String),
400 #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
402 SaveTemps(String),
403 #[error("compiler flag forwards options to another tool: {0}")]
405 ToolPassthrough(String),
406 #[error("compiler plugins are not modeled by the cache adapter: {0}")]
408 Plugin(String),
409 #[error("include search directory changed during the compilation: {0}")]
412 SearchPathModifiedDuringCompilation(PathBuf),
413
414 #[error("compilation output records a path its key normalized away: {0}")]
421 UnportableOutput(PathBuf),
422 #[error("storing path-specific C objects is disabled by MBX_CC_STORE_PATH_SPECIFIC")]
424 PathSpecificStorageDisabled,
425 #[error("compiler flag tunes for the local CPU: {0}")]
427 LocalCpuTarget(String),
428 #[error("compiler driver is not modeled by the cache adapter: {0}")]
430 UnsupportedCompilerDriver(String),
431 #[error("could not establish compiler identity: {0}")]
433 CompilerIdentityUnavailable(String),
434 #[error("environment variable {0} changes the compilation in an unmodeled way")]
436 UnsupportedEnvironment(String),
437 #[error("no real compiler was pinned for the cc shim")]
439 RealCompilerUnpinned,
440 #[error("input expands a timestamp macro: {0}")]
443 EmbeddedTimestampMacro(PathBuf),
444 #[error("preprocessed assembly uses an assembler input directive in {0}")]
447 AssemblerInputDirective(PathBuf),
448 #[error("could not model the compiler depfile: {0}")]
450 MalformedDepfile(String),
451 #[error("could not read the compiler depfile {path}: {message}")]
453 DepfileRead {
454 path: PathBuf,
456 message: String,
458 },
459 #[error("compilation reads more inputs than the cache adapter models")]
461 TooManyInputs,
462 #[error("path is outside every modeled root: {0}")]
464 UnmappedAbsolutePath(PathBuf),
465 #[error("path is not valid UTF-8: {0}")]
467 NonUtf8Path(PathBuf),
468 #[error("compiler working directory is not absolute: {0}")]
470 RelativeWorkingDirectory(PathBuf),
471 #[error("path mapping root is not absolute: {0}")]
473 RelativePathMapping(PathBuf),
474 #[error("invalid path mapping placeholder: {0}")]
476 InvalidPathPlaceholder(String),
477 #[error("required input is missing from the discovered inputs: {0}")]
479 MissingRequiredInput(String),
480 #[error("invalid digest for input: {0}")]
482 InvalidInputDigest(String),
483 #[error("conflicting digests for input: {0}")]
485 ConflictingInput(String),
486 #[error("could not read input {path}: {message}")]
488 InputRead {
489 path: PathBuf,
491 message: String,
493 },
494 #[error("input changed during the compilation: {0}")]
496 InputChanged(PathBuf),
497 #[error("input was modified during the compilation: {0}")]
499 InputModifiedDuringCompilation(PathBuf),
500 #[error("discovered inputs use a different working directory")]
502 DiscoveryWorkingDirectory,
503 #[error("action prediction is not modeled by this adapter version")]
505 UnsupportedPrediction,
506 #[error("invalid predicted input: {0}")]
508 InvalidPredictedInput(String),
509 #[error("could not serialize the action descriptor: {0}")]
511 Serialization(String),
512}
513
514impl From<PathNormalizationError> for CcBypassReason {
515 fn from(reason: PathNormalizationError) -> Self {
516 match reason {
517 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
518 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
519 }
520 }
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum CcLanguage {
526 C,
528 Cxx,
530}
531
532impl CcLanguage {
533 pub fn shim_stem(self) -> &'static str {
541 match self {
542 Self::C => "mbx-c",
543 Self::Cxx => "mbx-cxx",
544 }
545 }
546
547 pub fn default_driver(self) -> &'static str {
549 if cfg!(windows) {
550 return "cl.exe";
551 }
552 match self {
553 Self::C => "cc",
554 Self::Cxx => "c++",
555 }
556 }
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub enum CcCompilerFamily {
562 Gcc,
564 Clang,
566 AppleClang,
568 #[cfg(windows)]
570 Msvc,
571}
572
573pub const MSVC_IDENTITY_SOURCE: &str = "mbx_msc_ver _MSC_VER\nmbx_msc_full_ver _MSC_FULL_VER\nmbx_msc_build _MSC_BUILD\nmbx_clang __clang__\nmbx_x64 _M_X64\nmbx_x86 _M_IX86\nmbx_arm64 _M_ARM64\nmbx_arm64ec _M_ARM64EC\nmbx_arm _M_ARM\n";
578
579pub const MSVC_IDENTITY_PREFIX: &str = "mbx-msvc-identity";
581
582pub fn msvc_identity(preprocessed: &str) -> Option<String> {
588 let value = |name: &str| {
589 preprocessed.lines().find_map(|line| {
590 let (key, rest) = line.trim().split_once(' ')?;
591 (key == name).then(|| rest.trim().to_string())
592 })
593 };
594 let defined = |text: &str, macro_name: &str| {
595 !text.is_empty() && text != macro_name && text.bytes().all(|b| b.is_ascii_digit())
596 };
597 let ver = value("mbx_msc_ver").filter(|v| defined(v, "_MSC_VER"))?;
598 let full = value("mbx_msc_full_ver").filter(|v| defined(v, "_MSC_FULL_VER"))?;
599 let build = value("mbx_msc_build")
600 .filter(|v| defined(v, "_MSC_BUILD"))
601 .unwrap_or_default();
602 if value("mbx_clang").is_some_and(|v| v != "__clang__") {
603 return None;
604 }
605 let architecture = [
608 ("mbx_x64", "_M_X64"),
609 ("mbx_x86", "_M_IX86"),
610 ("mbx_arm64", "_M_ARM64"),
611 ("mbx_arm64ec", "_M_ARM64EC"),
612 ("mbx_arm", "_M_ARM"),
613 ]
614 .iter()
615 .filter(|(key, macro_name)| value(key).is_some_and(|v| v != *macro_name))
616 .map(|(_, macro_name)| *macro_name)
617 .collect::<Vec<_>>()
618 .join(",");
619 Some(format!(
620 "{MSVC_IDENTITY_PREFIX} _MSC_VER={ver} _MSC_FULL_VER={full} _MSC_BUILD={build} ARCH={architecture}\n"
621 ))
622}
623
624impl CcCompilerFamily {
625 pub fn as_str(self) -> &'static str {
627 match self {
628 Self::Gcc => "gcc",
629 Self::Clang => "clang",
630 Self::AppleClang => "apple-clang",
631 #[cfg(windows)]
632 Self::Msvc => "msvc",
633 }
634 }
635
636 pub fn uses_external_assembler(self) -> bool {
639 matches!(self, Self::Gcc)
640 }
641
642 pub fn is_msvc(self) -> bool {
644 #[cfg(windows)]
645 {
646 matches!(self, Self::Msvc)
647 }
648 #[cfg(not(windows))]
649 {
650 false
651 }
652 }
653
654 pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
656 #[cfg(windows)]
657 if probe.starts_with(MSVC_IDENTITY_PREFIX) {
658 return Ok(Self::Msvc);
659 }
660 if probe.contains("Apple clang version") {
661 Ok(Self::AppleClang)
662 } else if probe.contains("clang version") {
663 Ok(Self::Clang)
664 } else if probe.contains("gcc version") {
665 Ok(Self::Gcc)
666 } else {
667 Err(CcBypassReason::UnsupportedCompilerDriver(
668 probe.lines().next().unwrap_or_default().into(),
669 ))
670 }
671 }
672}
673
674#[derive(Debug, Clone, PartialEq, Eq)]
676pub struct CcCompilerIdentity {
677 pub family: CcCompilerFamily,
679 pub version_text: String,
681 pub target: String,
683 pub assembler: String,
689}
690
691#[derive(Debug, Clone, PartialEq, Eq)]
693pub struct CcActionInput {
694 pub path: PathBuf,
697 pub digest: CacheDigest,
699}
700
701#[derive(Debug, Clone, PartialEq, Eq)]
703pub struct CcActionContext {
704 pub compiler: CcCompilerIdentity,
706 pub working_dir: PathBuf,
708 pub path_mappings: Vec<PathMapping>,
710 pub environment: BTreeMap<String, Option<String>>,
712 pub inputs: Vec<CcActionInput>,
714}
715
716#[derive(Debug, Clone, PartialEq, Eq)]
718pub struct CcAction {
719 pub digest: CacheDigest,
721 pub bytes: Vec<u8>,
723}
724
725#[derive(Debug, Serialize)]
726struct CcCompilerDescriptor {
727 assembler: String,
728 family: String,
729 target: String,
730 version_text: String,
731}
732
733#[derive(Debug, Serialize)]
734struct CcInputDescriptor {
735 digest: CacheDigest,
736 path: String,
737}
738
739#[derive(Debug, Serialize)]
740struct CcActionDescriptor {
741 #[serde(skip_serializing_if = "Option::is_none")]
742 path_binding: Option<CcPathBinding>,
743 version: u8,
744 kind: &'static str,
745 adapter_version: u8,
746 #[serde(skip_serializing_if = "Option::is_none")]
747 assembly_input_model: Option<u8>,
748 #[serde(skip_serializing_if = "Option::is_none")]
749 preprocessing: Option<PreprocessingContext>,
750 compiler: CcCompilerDescriptor,
751 arguments: Vec<String>,
752 environment: BTreeMap<String, Option<String>>,
753 inputs: Vec<CcInputDescriptor>,
754}
755
756#[derive(Debug, Serialize)]
757struct CcInvocationDescriptor {
758 version: u8,
759 kind: &'static str,
760 adapter_version: u8,
761 #[serde(skip_serializing_if = "Option::is_none")]
762 assembly_input_model: Option<u8>,
763 #[serde(skip_serializing_if = "Option::is_none")]
764 preprocessing: Option<PreprocessingContext>,
765 compiler: CcCompilerDescriptor,
766 arguments: Vec<String>,
767 required_inputs: Vec<String>,
768}
769
770#[derive(Debug, Serialize)]
773struct PreprocessingContext {
774 working_dir: PathBuf,
775 arguments: Vec<String>,
776 roots: BTreeMap<String, PathBuf>,
777}
778
779#[derive(Debug, Serialize)]
781struct CcPathBinding {
782 working_dir: PathBuf,
783 arguments: Vec<Argument>,
784 roots: BTreeMap<String, PathBuf>,
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
790#[serde(deny_unknown_fields)]
791pub struct CcInputPrediction {
792 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
794 pub path_specific: bool,
795 pub version: u8,
797 pub inputs: Vec<String>,
799 pub environment: Vec<String>,
801 #[serde(default, skip_serializing_if = "is_zero")]
804 pub compiler_duration_ns: u64,
805 #[serde(default, skip_serializing_if = "String::is_empty")]
807 pub source_name: String,
808}
809
810fn is_zero(value: &u64) -> bool {
811 *value == 0
812}
813
814#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
816enum Argument {
817 Plain(String),
819 Path { flag: String, path: PathBuf },
821 PrefixMap {
823 flag: String,
824 from: PathBuf,
825 to: String,
826 },
827 Source(PathBuf),
829}
830
831#[derive(Debug, Clone, PartialEq, Eq)]
833pub struct CcInvocation {
834 arguments: Vec<Argument>,
835 source: PathBuf,
836 output: PathBuf,
837 include_dirs: Vec<PathBuf>,
838 required_inputs: Vec<PathBuf>,
839 language: CcLanguage,
840 preprocessed_assembly: bool,
841 preprocessing_arguments: Option<Vec<String>>,
842 sysroot: Option<PathBuf>,
843 caller_depfile: Option<CallerDepfile>,
844 dependency_argument_indices: Vec<usize>,
847}
848
849#[derive(Debug, Clone, PartialEq, Eq)]
859#[non_exhaustive]
860pub struct CallerDepfile {
861 pub path: PathBuf,
863 pub targets: Vec<DepfileTarget>,
865 pub user_headers_only: bool,
867 pub phony_targets: bool,
869}
870
871#[derive(Debug, Clone, PartialEq, Eq)]
873pub struct DepfileTarget {
874 pub name: String,
876 pub quoted: bool,
879}
880
881impl CcInvocation {
882 pub fn output_in(&self, working_dir: &Path) -> PathBuf {
888 if self.output.is_absolute() {
889 normalize_components(&self.output)
890 } else {
891 normalize_components(&working_dir.join(&self.output))
892 }
893 }
894
895 pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
897 self.caller_depfile.as_ref()
898 }
899
900 pub fn compiler_arguments(&self, arguments: &[OsString]) -> Vec<OsString> {
905 arguments
906 .iter()
907 .enumerate()
908 .filter(|(index, _)| !self.dependency_argument_indices.contains(index))
909 .map(|(_, argument)| argument.clone())
910 .collect()
911 }
912
913 pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
916 Parser::new(arguments).parse()
917 }
918
919 pub fn parse_for(
921 arguments: &[OsString],
922 family: CcCompilerFamily,
923 ) -> Result<Self, CcBypassReason> {
924 if family.is_msvc() {
925 MsvcParser::new(arguments).parse()
926 } else {
927 let mut parser = Parser::new(arguments);
928 parser.family = family;
929 parser.parse()
930 }
931 }
932
933 pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
935 MsvcParser::new(arguments).parse()
936 }
937
938 pub fn source(&self) -> &Path {
940 &self.source
941 }
942
943 pub fn is_preprocessing(&self) -> bool {
945 self.preprocessing_arguments.is_some()
946 }
947
948 pub fn output(&self) -> &Path {
950 &self.output
951 }
952
953 pub fn include_dirs(&self) -> &[PathBuf] {
955 &self.include_dirs
956 }
957
958 pub fn required_inputs(&self) -> &[PathBuf] {
960 &self.required_inputs
961 }
962
963 pub fn language(&self) -> CcLanguage {
965 self.language
966 }
967
968 pub fn validate_discovered_inputs<'a>(
975 &self,
976 paths: impl IntoIterator<Item = &'a Path>,
977 ) -> Result<(), CcBypassReason> {
978 if !self.preprocessed_assembly {
979 return Ok(());
980 }
981 for path in paths {
982 if depfile::contains_assembler_input_directive(path)? {
983 return Err(CcBypassReason::AssemblerInputDirective(path.to_path_buf()));
984 }
985 }
986 Ok(())
987 }
988
989 pub fn sysroot(&self) -> Option<&Path> {
991 self.sysroot.as_deref()
992 }
993
994 pub fn source_name(&self) -> String {
996 self.source
997 .file_name()
998 .map(|name| name.to_string_lossy().into_owned())
999 .unwrap_or_default()
1000 }
1001
1002 pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
1009 vec!["-MD".into(), "-MF".into(), depfile.into()]
1010 }
1011
1012 pub fn dependency_arguments_for(
1015 &self,
1016 depfile: &Path,
1017 family: CcCompilerFamily,
1018 ) -> Vec<OsString> {
1019 if family.is_msvc() {
1020 vec!["/sourceDependencies".into(), depfile.into()]
1021 } else {
1022 self.dependency_arguments(depfile)
1023 }
1024 }
1025
1026 pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
1028 vec!["/sourceDependencies".into(), depfile.into()]
1029 }
1030
1031 pub fn invocation_digest(
1033 &self,
1034 context: &CcActionContext,
1035 ) -> Result<CacheDigest, CcBypassReason> {
1036 let builder = ActionBuilder::new(self, context.clone());
1037 let descriptor = builder.invocation_descriptor()?;
1038 let bytes = canonical_json(&descriptor)
1039 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1040 Ok(CacheDigest::blake3(&bytes))
1041 }
1042
1043 pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
1046 self.action_with_path_binding(context, false)
1047 }
1048
1049 pub fn action_with_path_binding(
1052 &self,
1053 context: CcActionContext,
1054 path_specific: bool,
1055 ) -> Result<CcAction, CcBypassReason> {
1056 ActionBuilder::new(self, context).build(path_specific)
1057 }
1058
1059 pub fn prediction(
1062 &self,
1063 context: &CcActionContext,
1064 compiler_duration_ns: u64,
1065 ) -> Result<CcInputPrediction, CcBypassReason> {
1066 let builder = ActionBuilder::new(self, context.clone());
1067 let mut inputs = context
1068 .inputs
1069 .iter()
1070 .map(|input| builder.normalize_input_path(&input.path))
1071 .collect::<Result<Vec<_>, _>>()?;
1072 inputs.sort();
1073 inputs.dedup();
1074 Ok(CcInputPrediction {
1075 path_specific: false,
1076 version: 1,
1077 inputs,
1078 environment: context.environment.keys().cloned().collect(),
1079 compiler_duration_ns,
1080 source_name: self.source_name(),
1081 })
1082 }
1083}
1084
1085impl CcInputPrediction {
1086 pub fn discover(
1089 &self,
1090 working_dir: &Path,
1091 path_mappings: &[PathMapping],
1092 digests: &dyn FileDigestCache,
1093 ) -> Result<CcDiscoveredInputs, CcBypassReason> {
1094 if self.version != 1 {
1095 return Err(CcBypassReason::UnsupportedPrediction);
1096 }
1097 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1098 return Err(CcBypassReason::UnsupportedPrediction);
1099 }
1100 let mappings = PathMapping::ordered(path_mappings);
1101 let mut files = BTreeSet::new();
1102 let mut directories = BTreeSet::new();
1103 for entry in &self.inputs {
1104 match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
1105 Some(directory) => {
1106 directories.insert(denormalize_path(directory, &mappings)?);
1107 }
1108 None => {
1109 files.insert(denormalize_path(entry, &mappings)?);
1110 }
1111 }
1112 }
1113 CcDiscoveredInputs::collect(working_dir, files, directories, digests)
1114 }
1115}
1116
1117fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
1123 for mapping in mappings {
1124 let prefix = format!("${{{}}}", mapping.placeholder);
1125 let suffix = if value == prefix {
1126 ""
1127 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1128 suffix
1129 } else {
1130 continue;
1131 };
1132 if !mapping.root.is_absolute() || !safe_suffix(suffix) {
1133 return Err(CcBypassReason::InvalidPredictedInput(value.into()));
1134 }
1135 let mut path = normalize_components(&mapping.root);
1136 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1137 return Ok(path);
1138 }
1139 let path = PathBuf::from(value);
1143 if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
1144 return Ok(path);
1145 }
1146 Err(CcBypassReason::InvalidPredictedInput(value.into()))
1147}
1148
1149fn safe_suffix(suffix: &str) -> bool {
1150 suffix.is_empty()
1151 || !suffix.split('/').any(|component| {
1152 component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
1153 })
1154}
1155
1156pub fn is_system_path(path: &Path) -> bool {
1158 SYSTEM_ROOTS
1159 .iter()
1160 .any(|root| path.starts_with(Path::new(root)))
1161 || developer_roots().iter().any(|root| path.starts_with(root))
1162}
1163
1164#[cfg(target_os = "macos")]
1168fn developer_roots() -> &'static [PathBuf] {
1169 static ROOTS: std::sync::OnceLock<Vec<PathBuf>> = std::sync::OnceLock::new();
1170 ROOTS.get_or_init(|| {
1171 let selected = std::env::var_os("DEVELOPER_DIR")
1172 .filter(|value| !value.is_empty())
1173 .map(PathBuf::from)
1174 .or_else(|| {
1175 let output = std::process::Command::new("/usr/bin/xcode-select")
1176 .arg("--print-path")
1177 .output()
1178 .ok()?;
1179 if !output.status.success() {
1180 return None;
1181 }
1182 Some(PathBuf::from(
1183 std::str::from_utf8(&output.stdout).ok()?.trim(),
1184 ))
1185 });
1186 selected.map(developer_directory_roots).unwrap_or_default()
1187 })
1188}
1189
1190#[cfg(not(target_os = "macos"))]
1191fn developer_roots() -> &'static [PathBuf] {
1192 &[]
1193}
1194
1195#[cfg(any(target_os = "macos", all(test, unix)))]
1196fn developer_directory_roots(mut root: PathBuf) -> Vec<PathBuf> {
1197 if !root.is_absolute() || root == Path::new("/") {
1198 return Vec::new();
1199 }
1200 if root.extension().is_some_and(|extension| extension == "app") {
1202 root = root.join("Contents/Developer");
1203 }
1204 root = normalize_components(&root);
1205 if root == Path::new("/") {
1206 return Vec::new();
1207 }
1208 let mut roots = vec![root.clone()];
1209 if let Ok(canonical) = std::fs::canonicalize(&root)
1210 && canonical != root
1211 {
1212 roots.push(canonical);
1213 }
1214 roots
1215}
1216
1217fn normalize_components(path: &Path) -> PathBuf {
1218 let mut normalized = PathBuf::new();
1219 for component in path.components() {
1220 match component {
1221 Component::CurDir => {}
1222 Component::ParentDir => {
1223 normalized.pop();
1224 }
1225 component => normalized.push(component.as_os_str()),
1226 }
1227 }
1228 normalized
1229}
1230
1231pub fn environment_inputs<F>(
1234 lookup: F,
1235 sysroot: Option<&Path>,
1236) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1237where
1238 F: Fn(&str) -> Option<String>,
1239{
1240 environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
1241}
1242
1243pub fn environment_inputs_for<F>(
1245 lookup: F,
1246 sysroot: Option<&Path>,
1247 family: CcCompilerFamily,
1248) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1249where
1250 F: Fn(&str) -> Option<String>,
1251{
1252 for name in BYPASS_ENVIRONMENT {
1253 if lookup(name).is_some() {
1254 return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
1255 }
1256 }
1257 let mut environment = BTreeMap::new();
1258 for name in KEYED_ENVIRONMENT {
1259 if *name == "SDKROOT" && sysroot.is_some() {
1262 continue;
1263 }
1264 environment.insert((*name).to_string(), lookup(name));
1265 }
1266 if family.is_msvc() {
1267 for name in [
1271 "INCLUDE",
1272 "VCToolsVersion",
1273 "WindowsSDKVersion",
1274 "UCRTVersion",
1275 ] {
1276 environment.insert(name.into(), lookup(name));
1277 }
1278 for name in ["CL", "_CL_"] {
1279 if lookup(name).is_some() {
1280 return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
1281 }
1282 }
1283 }
1284 Ok(environment)
1285}
1286
1287struct ActionBuilder<'a> {
1288 invocation: &'a CcInvocation,
1289 context: CcActionContext,
1290 mappings: Vec<PathMapping>,
1291 aliases: PathAliases,
1293}
1294
1295impl<'a> ActionBuilder<'a> {
1296 fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
1297 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1298 let mappings = resolve_path_mappings(&context.path_mappings);
1299 Self {
1300 invocation,
1301 context,
1302 mappings,
1303 aliases: PathAliases::default(),
1304 }
1305 }
1306
1307 fn build(self, path_specific: bool) -> Result<CcAction, CcBypassReason> {
1308 self.validate_mappings()?;
1309 let invocation = self.invocation_descriptor()?;
1310
1311 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1312 for input in &self.context.inputs {
1313 input.digest.validate().map_err(|_| {
1314 CcBypassReason::InvalidInputDigest(input.path.display().to_string())
1315 })?;
1316 let path = self.normalize_input_path(&input.path)?;
1317 if inputs
1318 .insert(path.clone(), input.digest.clone())
1319 .is_some_and(|existing| existing != input.digest)
1320 {
1321 return Err(CcBypassReason::ConflictingInput(path));
1322 }
1323 }
1324 let required = self
1325 .invocation
1326 .required_inputs
1327 .iter()
1328 .map(|path| self.normalize_path(path))
1329 .collect::<Result<BTreeSet<_>, _>>()?;
1330 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1331 return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
1332 }
1333 let inputs = inputs
1334 .into_iter()
1335 .map(|(path, digest)| CcInputDescriptor { path, digest })
1336 .collect();
1337 let descriptor = CcActionDescriptor {
1338 path_binding: path_specific.then(|| CcPathBinding {
1339 working_dir: self.context.working_dir.clone(),
1340 arguments: self.invocation.arguments.clone(),
1341 roots: self
1342 .context
1343 .path_mappings
1344 .iter()
1345 .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1346 .collect(),
1347 }),
1348 version: ACTION_SCHEMA_VERSION,
1349 kind: "cc",
1350 adapter_version: ADAPTER_VERSION,
1351 assembly_input_model: invocation.assembly_input_model,
1352 preprocessing: invocation.preprocessing,
1353 compiler: invocation.compiler,
1354 arguments: invocation.arguments,
1355 environment: self.context.environment.clone(),
1356 inputs,
1357 };
1358 let bytes = canonical_json(&descriptor)
1359 .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1360 let digest = CacheDigest::blake3(&bytes);
1361 Ok(CcAction { digest, bytes })
1362 }
1363
1364 fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1365 self.validate_mappings()?;
1366 let arguments = self
1367 .invocation
1368 .arguments
1369 .iter()
1370 .map(|argument| self.normalize_argument(argument))
1371 .collect::<Result<Vec<_>, _>>()?;
1372 let required_inputs = self
1373 .invocation
1374 .required_inputs
1375 .iter()
1376 .map(|path| self.normalize_path(path))
1377 .collect::<Result<BTreeSet<_>, _>>()?
1378 .into_iter()
1379 .collect();
1380 Ok(CcInvocationDescriptor {
1381 version: ACTION_SCHEMA_VERSION,
1382 kind: "cc",
1383 adapter_version: ADAPTER_VERSION,
1384 assembly_input_model: self.invocation.preprocessed_assembly.then_some(1),
1387 preprocessing: self
1388 .invocation
1389 .preprocessing_arguments
1390 .as_ref()
1391 .map(|arguments| PreprocessingContext {
1392 working_dir: self.context.working_dir.clone(),
1393 arguments: arguments.clone(),
1394 roots: self
1395 .mappings
1396 .iter()
1397 .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1398 .collect(),
1399 }),
1400 compiler: self.compiler_descriptor(),
1401 arguments,
1402 required_inputs,
1403 })
1404 }
1405
1406 fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1407 CcCompilerDescriptor {
1408 assembler: self.context.compiler.assembler.clone(),
1409 family: self.context.compiler.family.as_str().into(),
1410 target: self.context.compiler.target.clone(),
1411 version_text: self.context.compiler.version_text.clone(),
1412 }
1413 }
1414
1415 fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1416 if !self.context.working_dir.is_absolute() {
1417 return Err(CcBypassReason::RelativeWorkingDirectory(
1418 self.context.working_dir.clone(),
1419 ));
1420 }
1421 let mut roots = BTreeSet::new();
1422 let mut placeholders = BTreeSet::new();
1423 for mapping in &self.context.path_mappings {
1424 if !mapping.root.is_absolute() {
1425 return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1426 }
1427 if mapping.placeholder.is_empty()
1428 || !mapping
1429 .placeholder
1430 .bytes()
1431 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1432 || !roots.insert(normalize_components(&mapping.root))
1433 || !placeholders.insert(&mapping.placeholder)
1434 {
1435 return Err(CcBypassReason::InvalidPathPlaceholder(
1436 mapping.placeholder.clone(),
1437 ));
1438 }
1439 }
1440 Ok(())
1441 }
1442
1443 fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1444 match argument {
1445 Argument::Plain(value) => Ok(value.clone()),
1446 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1447 Argument::PrefixMap { flag, from, to } => {
1448 Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1449 }
1450 Argument::Source(path) => Ok(self.normalize_path(path)?),
1451 }
1452 }
1453
1454 fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1461 match normalize_resolved_mapped_path_with(
1462 &self.aliases,
1463 path,
1464 &self.context.working_dir,
1465 &self.mappings,
1466 ) {
1467 Ok(normalized) => Ok(normalized),
1468 Err(reason) => {
1469 let absolute = absolute_path(path, &self.context.working_dir);
1470 if is_system_path(&absolute) {
1471 return absolute
1472 .to_str()
1473 .map(ToOwned::to_owned)
1474 .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1475 }
1476 Err(reason.into())
1477 }
1478 }
1479 }
1480
1481 fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1482 match path.to_str().and_then(|path| {
1483 path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1484 .map(ToOwned::to_owned)
1485 }) {
1486 Some(directory) => Ok(format!(
1487 "{INCLUDE_MANIFEST_PREFIX}{}",
1488 self.normalize_path(Path::new(&directory))?
1489 )),
1490 None => self.normalize_path(path),
1491 }
1492 }
1493}
1494
1495fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1496 if path.is_absolute() {
1497 normalize_components(path)
1498 } else {
1499 normalize_components(&working_dir.join(path))
1500 }
1501}
1502
1503struct Parser<'a> {
1504 family: CcCompilerFamily,
1505 arguments: &'a [OsString],
1506 index: usize,
1507 parsed: Vec<Argument>,
1508 source: Option<PathBuf>,
1509 output: Option<PathBuf>,
1510 include_dirs: Vec<PathBuf>,
1511 required_inputs: Vec<PathBuf>,
1512 sysroot: Option<PathBuf>,
1513 explicit_language: Option<CcLanguage>,
1514 preprocessed_assembly: bool,
1515 compiling: bool,
1516 preprocessing: bool,
1517 dependency: DependencyRequest,
1518}
1519
1520#[derive(Debug, Default)]
1522struct DependencyRequest {
1523 user_headers_only: Option<bool>,
1525 file: Option<PathBuf>,
1526 targets: Vec<DepfileTarget>,
1527 phony_targets: bool,
1528 modifier: Option<String>,
1531 indices: Vec<usize>,
1532}
1533
1534impl<'a> Parser<'a> {
1535 fn new(arguments: &'a [OsString]) -> Self {
1536 Self {
1537 family: CcCompilerFamily::Gcc,
1538 arguments,
1539 index: 0,
1540 parsed: Vec::new(),
1541 source: None,
1542 output: None,
1543 include_dirs: Vec::new(),
1544 required_inputs: Vec::new(),
1545 sysroot: None,
1546 explicit_language: None,
1547 preprocessed_assembly: false,
1548 compiling: false,
1549 preprocessing: false,
1550 dependency: DependencyRequest::default(),
1551 }
1552 }
1553
1554 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1555 while self.index < self.arguments.len() {
1556 let value = self.current()?.to_string();
1557 self.index += 1;
1558 if value == "-" {
1559 return Err(CcBypassReason::StandardInput);
1560 }
1561 if let Some(argfile) = value.strip_prefix('@') {
1562 return Err(CcBypassReason::ResponseFile(argfile.into()));
1563 }
1564 if value.starts_with('-') {
1565 self.parse_flag(&value)?;
1566 } else {
1567 self.parse_input(&value)?;
1568 }
1569 }
1570
1571 if !self.compiling && !self.preprocessing {
1572 return Err(CcBypassReason::NotACompile);
1573 }
1574 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1575 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1576 if self.preprocessing && output == Path::new("-") {
1577 return Err(CcBypassReason::NonObjectOutput("-E -o -".into()));
1578 }
1579 if self.preprocessing
1582 && self.dependency.user_headers_only.is_some()
1583 && self.dependency.file.is_none()
1584 {
1585 return Err(CcBypassReason::CallerDependencyFlags(
1586 "-E -MD/-MMD without -MF".into(),
1587 ));
1588 }
1589 if self.preprocessing && self.dependency.file.as_deref() == Some(Path::new("-")) {
1590 return Err(CcBypassReason::CallerDependencyFlags("-E -MF -".into()));
1591 }
1592 let language = self.language(&source)?;
1593 let preprocessed_assembly = self.preprocessed_assembly
1594 || (self.explicit_language.is_none()
1595 && source.extension().and_then(|value| value.to_str()) == Some("S"));
1596 self.required_inputs.push(source.clone());
1597 let caller_depfile = match self.dependency.user_headers_only {
1598 Some(user_headers_only) => Some(CallerDepfile {
1599 path: self
1600 .dependency
1601 .file
1602 .unwrap_or_else(|| output.with_extension("d")),
1603 targets: if self.dependency.targets.is_empty() {
1604 vec![DepfileTarget {
1605 name: if self.preprocessing && self.family == CcCompilerFamily::Gcc {
1608 Path::new(source.file_name().ok_or(CcBypassReason::MissingInput)?)
1609 .with_extension("o")
1610 .to_string_lossy()
1611 .into_owned()
1612 } else {
1613 output.to_string_lossy().into_owned()
1614 },
1615 quoted: true,
1616 }]
1617 } else {
1618 self.dependency.targets
1619 },
1620 user_headers_only,
1621 phony_targets: self.dependency.phony_targets,
1622 }),
1623 None => match self.dependency.modifier {
1627 Some(modifier) => return Err(CcBypassReason::CallerDependencyFlags(modifier)),
1628 None => None,
1629 },
1630 };
1631 Ok(CcInvocation {
1632 arguments: self.parsed,
1633 source,
1634 output,
1635 include_dirs: self.include_dirs,
1636 required_inputs: self.required_inputs,
1637 language,
1638 preprocessed_assembly,
1639 preprocessing_arguments: self.preprocessing.then(|| {
1640 self.arguments
1641 .iter()
1642 .map(|arg| arg.to_str().expect("arguments were validated").to_owned())
1643 .collect()
1644 }),
1645 sysroot: self.sysroot,
1646 caller_depfile,
1647 dependency_argument_indices: self.dependency.indices,
1648 })
1649 }
1650
1651 fn parse_dependency_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1659 let start = self.index - 1;
1660 match option {
1661 "D" => self.dependency.user_headers_only = Some(false),
1662 "MD" => self.dependency.user_headers_only = Some(true),
1663 "P" => {
1664 self.dependency.phony_targets = true;
1665 self.dependency.modifier = Some(value.into());
1666 }
1667 _ if option.starts_with('F') => {
1668 let file = self.take_value("-MF", Some(&option[1..]))?;
1669 self.dependency.file = Some(PathBuf::from(file));
1670 self.dependency.modifier = Some("-MF".into());
1671 }
1672 _ if option.starts_with('T') || option.starts_with('Q') => {
1673 let flag = &value[..3];
1674 let name = self.take_value(flag, Some(&option[1..]))?;
1675 self.dependency.targets.push(DepfileTarget {
1676 name,
1677 quoted: option.starts_with('Q'),
1678 });
1679 self.dependency.modifier = Some(flag.into());
1680 }
1681 _ => return Err(CcBypassReason::CallerDependencyFlags(value.into())),
1682 }
1683 self.dependency.indices.extend(start..self.index);
1684 Ok(())
1685 }
1686
1687 fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1688 if let Some(language) = self.explicit_language {
1689 return Ok(language);
1690 }
1691 let extension = source
1692 .extension()
1693 .and_then(|extension| extension.to_str())
1694 .unwrap_or_default();
1695 match extension {
1696 "c" | "S" => Ok(CcLanguage::C),
1702 "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1703 _ => Err(CcBypassReason::UnsupportedLanguage(
1704 source.display().to_string(),
1705 )),
1706 }
1707 }
1708
1709 fn current(&self) -> Result<&str, CcBypassReason> {
1710 self.arguments[self.index]
1711 .to_str()
1712 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1713 }
1714
1715 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1716 if let Some(value) = inline
1717 && !value.is_empty()
1718 {
1719 return Ok(value.into());
1720 }
1721 if self.index >= self.arguments.len() {
1722 return Err(CcBypassReason::MissingValue(flag.into()));
1723 }
1724 let value = self.current()?.to_string();
1725 self.index += 1;
1726 Ok(value)
1727 }
1728
1729 fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1730 if self.source.is_some() {
1731 return Err(CcBypassReason::MultipleInputs);
1732 }
1733 let path = PathBuf::from(value);
1734 if self.explicit_language.is_none() {
1738 let extension = path
1739 .extension()
1740 .and_then(|extension| extension.to_str())
1741 .unwrap_or_default();
1742 if !matches!(extension, "c" | "S" | "cc" | "cpp" | "cxx" | "c++") {
1743 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1744 }
1745 }
1746 self.source = Some(path.clone());
1747 self.parsed.push(Argument::Source(path));
1748 Ok(())
1749 }
1750
1751 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1752 if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1753 return Err(CcBypassReason::CompilerQuery);
1754 }
1755 if value == "-E" {
1756 self.preprocessing = true;
1757 self.parsed.push(Argument::Plain(value.into()));
1758 return Ok(());
1759 }
1760 if value == "-S" {
1761 return Err(CcBypassReason::NonObjectOutput(value.into()));
1762 }
1763 if let Some(option) = value.strip_prefix("-M") {
1764 return self.parse_dependency_flag(value, option);
1765 }
1766 if value.starts_with("-save-temps") {
1767 return Err(CcBypassReason::SaveTemps(value.into()));
1768 }
1769 if value == "--coverage" {
1770 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1771 }
1772 if let Some(options) = value.strip_prefix("-Wa,")
1773 && !options.is_empty()
1774 && options
1775 .split(',')
1776 .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1777 {
1778 self.parsed.push(Argument::Plain(value.into()));
1779 return Ok(());
1780 }
1781 if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1782 || value.starts_with("-Wp,")
1783 || value.starts_with("-Wa,")
1784 || value.starts_with("-Wl,")
1785 {
1786 return Err(CcBypassReason::ToolPassthrough(value.into()));
1789 }
1790 if value.starts_with("-include-pch") || value == "-emit-pch" {
1791 return Err(CcBypassReason::PrecompiledHeader(value.into()));
1792 }
1793
1794 if value == "-c" {
1795 self.compiling = true;
1796 self.parsed.push(Argument::Plain(value.into()));
1797 return Ok(());
1798 }
1799 if SUPPORTED_BARE_FLAGS.contains(&value)
1800 || SUPPORTED_O_FLAGS.contains(&value)
1801 || SUPPORTED_G_FLAGS.contains(&value)
1802 || value.starts_with("-std=")
1803 {
1804 self.parsed.push(Argument::Plain(value.into()));
1805 return Ok(());
1806 }
1807 if let Some(rest) = value.strip_prefix("-o") {
1808 let path = self.take_value("-o", Some(rest))?;
1809 self.output = Some(PathBuf::from(&path));
1812 self.parsed.push(Argument::Path {
1813 flag: "-o".into(),
1814 path: PathBuf::from(path),
1815 });
1816 return Ok(());
1817 }
1818 if let Some(rest) = value.strip_prefix("-I") {
1819 let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1820 self.include_dirs.push(path.clone());
1821 self.parsed.push(Argument::Path {
1822 flag: "-I".into(),
1823 path,
1824 });
1825 return Ok(());
1826 }
1827 if SEPARATE_PATH_FLAGS.contains(&value) {
1828 let path = PathBuf::from(self.take_value(value, None)?);
1829 match value {
1830 "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1831 "-isysroot" => self.sysroot = Some(path.clone()),
1832 _ => {}
1838 }
1839 self.parsed.push(Argument::Path {
1840 flag: value.into(),
1841 path,
1842 });
1843 return Ok(());
1844 }
1845 if let Some(rest) = value.strip_prefix("--include=") {
1848 let path = PathBuf::from(rest);
1849 self.parsed.push(Argument::Path {
1850 flag: "-include".into(),
1851 path,
1852 });
1853 return Ok(());
1854 }
1855 if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1856 value
1857 .strip_prefix(&format!("{flag}="))
1858 .map(|rest| (*flag, rest))
1859 }) {
1860 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1861 self.parsed.push(Argument::PrefixMap {
1862 flag: flag.into(),
1863 from: PathBuf::from(from),
1864 to: to.into(),
1865 });
1866 return Ok(());
1867 }
1868 if value == "--param" {
1870 let parameter = self.take_value("--param", None)?;
1871 self.parsed
1872 .push(Argument::Plain(format!("--param={parameter}")));
1873 return Ok(());
1874 }
1875 if let Some(parameter) = value.strip_prefix("--param=") {
1876 self.parsed
1877 .push(Argument::Plain(format!("--param={parameter}")));
1878 return Ok(());
1879 }
1880 if let Some(rest) = value.strip_prefix("--sysroot=") {
1881 let path = PathBuf::from(rest);
1882 self.sysroot = Some(path.clone());
1883 self.parsed.push(Argument::Path {
1884 flag: "--sysroot".into(),
1885 path,
1886 });
1887 return Ok(());
1888 }
1889 if let Some(rest) = value
1890 .strip_prefix("-D")
1891 .or_else(|| value.strip_prefix("-U"))
1892 {
1893 let flag = &value[..2];
1894 let definition = self.take_value(flag, Some(rest))?;
1895 self.parsed
1896 .push(Argument::Plain(format!("{flag}{definition}")));
1897 return Ok(());
1898 }
1899 if let Some(rest) = value.strip_prefix("-x") {
1900 let language = self.take_value("-x", Some(rest))?;
1901 self.explicit_language = Some(match language.as_str() {
1902 "c" => CcLanguage::C,
1903 "assembler-with-cpp" => {
1904 self.preprocessed_assembly = true;
1905 CcLanguage::C
1906 }
1907 "c++" => CcLanguage::Cxx,
1908 other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1909 });
1910 self.parsed.push(Argument::Plain(format!("-x{language}")));
1911 return Ok(());
1912 }
1913 if let Some(target) = value.strip_prefix("--target=") {
1914 self.parsed
1915 .push(Argument::Plain(format!("--target={target}")));
1916 return Ok(());
1917 }
1918 if value == "-target" {
1919 let target = self.take_value("-target", None)?;
1920 self.parsed
1921 .push(Argument::Plain(format!("--target={target}")));
1922 return Ok(());
1923 }
1924 if value == "-arch" {
1925 let arch = self.take_value("-arch", None)?;
1926 self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1927 return Ok(());
1928 }
1929 if let Some(option) = value.strip_prefix("-f") {
1930 return self.parse_f_flag(value, option);
1931 }
1932 if let Some(option) = value.strip_prefix("-m") {
1933 return self.parse_m_flag(value, option);
1934 }
1935 if value.starts_with("-g") {
1936 return Err(if value.starts_with("-gsplit-dwarf") {
1939 CcBypassReason::SplitDebugOutput(value.into())
1940 } else {
1941 CcBypassReason::UnknownFlag(value.into())
1942 });
1943 }
1944 if value.starts_with("-W") {
1945 self.parsed.push(Argument::Plain(value.into()));
1949 return Ok(());
1950 }
1951 Err(CcBypassReason::UnknownFlag(value.into()))
1952 }
1953
1954 fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1955 if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1956 return Err(CcBypassReason::Plugin(value.into()));
1957 }
1958 if option.starts_with("profile-") || option == "test-coverage" {
1959 return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1960 }
1961 let name = option.split_once('=').map_or(option, |(name, _)| name);
1962 if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1963 return Err(CcBypassReason::UnknownFlag(value.into()));
1964 }
1965 self.parsed.push(Argument::Plain(value.into()));
1966 Ok(())
1967 }
1968
1969 fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1970 if option == "llvm" {
1971 return Err(CcBypassReason::ToolPassthrough(value.into()));
1972 }
1973 if let Some((name, selection)) = option.split_once('=')
1978 && matches!(name, "arch" | "cpu" | "tune")
1979 && matches!(selection, "native" | "host")
1980 {
1981 return Err(CcBypassReason::LocalCpuTarget(value.into()));
1982 }
1983 let name = option.split_once('=').map_or(option, |(name, _)| name);
1984 if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1985 return Err(CcBypassReason::UnknownFlag(value.into()));
1986 }
1987 self.parsed.push(Argument::Plain(value.into()));
1988 Ok(())
1989 }
1990}
1991
1992struct MsvcParser<'a> {
1996 arguments: &'a [OsString],
1997 index: usize,
1998 parsed: Vec<Argument>,
1999 source: Option<PathBuf>,
2000 output: Option<PathBuf>,
2001 include_dirs: Vec<PathBuf>,
2002 required_inputs: Vec<PathBuf>,
2003 explicit_language: Option<CcLanguage>,
2004 compiling: bool,
2005}
2006
2007impl<'a> MsvcParser<'a> {
2008 fn new(arguments: &'a [OsString]) -> Self {
2009 Self {
2010 arguments,
2011 index: 0,
2012 parsed: Vec::new(),
2013 source: None,
2014 output: None,
2015 include_dirs: Vec::new(),
2016 required_inputs: Vec::new(),
2017 explicit_language: None,
2018 compiling: false,
2019 }
2020 }
2021
2022 fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
2023 while self.index < self.arguments.len() {
2024 let value = self.current()?.to_owned();
2025 self.index += 1;
2026 if let Some(file) = value.strip_prefix('@') {
2027 return Err(CcBypassReason::ResponseFile(file.into()));
2028 }
2029 if value.starts_with('/') || value.starts_with('-') {
2030 self.parse_flag(&value)?;
2031 } else {
2032 self.add_source(&value)?;
2033 }
2034 }
2035 if !self.compiling {
2036 return Err(CcBypassReason::NotACompile);
2037 }
2038 let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
2039 let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
2040 let language = self.explicit_language.unwrap_or_else(|| {
2041 if source
2042 .extension()
2043 .and_then(|value| value.to_str())
2044 .is_some_and(|value| value.eq_ignore_ascii_case("c"))
2045 {
2046 CcLanguage::C
2047 } else {
2048 CcLanguage::Cxx
2049 }
2050 });
2051 self.required_inputs.push(source.clone());
2052 Ok(CcInvocation {
2053 arguments: self.parsed,
2054 source,
2055 output,
2056 include_dirs: self.include_dirs,
2057 required_inputs: self.required_inputs,
2058 language,
2059 preprocessed_assembly: false,
2060 preprocessing_arguments: None,
2061 sysroot: None,
2062 caller_depfile: None,
2063 dependency_argument_indices: Vec::new(),
2064 })
2065 }
2066
2067 fn current(&self) -> Result<&str, CcBypassReason> {
2068 self.arguments[self.index]
2069 .to_str()
2070 .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
2071 }
2072
2073 fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
2074 if !attached.is_empty() {
2075 return Ok(attached.into());
2076 }
2077 if self.index == self.arguments.len() {
2078 return Err(CcBypassReason::MissingValue(flag.into()));
2079 }
2080 let value = self.current()?.to_owned();
2081 self.index += 1;
2082 Ok(value)
2083 }
2084
2085 fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
2086 if self.source.is_some() {
2087 return Err(CcBypassReason::MultipleInputs);
2088 }
2089 let path = PathBuf::from(value);
2090 if self.explicit_language.is_none()
2091 && !path
2092 .extension()
2093 .and_then(|value| value.to_str())
2094 .is_some_and(|value| {
2095 matches!(
2096 value.to_ascii_lowercase().as_str(),
2097 "c" | "cc" | "cpp" | "cxx"
2098 )
2099 })
2100 {
2101 return Err(CcBypassReason::UnsupportedLanguage(value.into()));
2102 }
2103 self.source = Some(path.clone());
2104 self.parsed.push(Argument::Source(path));
2105 Ok(())
2106 }
2107
2108 fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
2109 let option = value.trim_start_matches(['/', '-']);
2110 let lower = option.to_ascii_lowercase();
2111 if matches!(lower.as_str(), "?" | "help") {
2112 return Err(CcBypassReason::CompilerQuery);
2113 }
2114 if lower == "showincludes" || lower.starts_with("sourcedependencies") {
2115 return Err(CcBypassReason::CallerDependencyFlags(value.into()));
2116 }
2117 if matches!(lower.as_str(), "e" | "ep" | "p") {
2118 return Err(CcBypassReason::NonObjectOutput(value.into()));
2119 }
2120 if (lower.starts_with("fa") && !lower.starts_with("favor:"))
2121 || lower.starts_with("fd")
2122 || lower.starts_with("zi")
2123 {
2124 return Err(CcBypassReason::SplitDebugOutput(value.into()));
2125 }
2126 if lower.starts_with("yc")
2127 || lower.starts_with("yu")
2128 || (lower.starts_with("fp") && !lower.starts_with("fp:"))
2129 {
2130 return Err(CcBypassReason::PrecompiledHeader(value.into()));
2131 }
2132 if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
2133 return Err(CcBypassReason::ToolPassthrough(value.into()));
2134 }
2135 if matches!(lower.as_str(), "ld" | "ldd") {
2136 return Err(CcBypassReason::NotACompile);
2137 }
2138 if lower == "c" {
2139 self.compiling = true;
2140 self.parsed.push(Argument::Plain("/c".into()));
2141 return Ok(());
2142 }
2143 for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
2144 if let Some(attached) = option.strip_prefix(prefix) {
2145 let path = PathBuf::from(self.value(canonical, attached)?);
2146 if prefix == "Fo" {
2147 self.output = Some(path.clone());
2148 } else if prefix == "I" {
2149 self.include_dirs.push(path.clone());
2150 }
2151 self.parsed.push(Argument::Path {
2152 flag: canonical.into(),
2153 path,
2154 });
2155 return Ok(());
2156 }
2157 }
2158 if lower.starts_with("external:i") {
2159 let path = PathBuf::from(self.value("/external:I", &option[10..])?);
2160 self.include_dirs.push(path.clone());
2161 self.parsed.push(Argument::Path {
2162 flag: "/external:I".into(),
2163 path,
2164 });
2165 return Ok(());
2166 }
2167 if option.starts_with("Tc") || option.starts_with("Tp") {
2168 let c = option.starts_with("Tc");
2169 let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
2170 self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
2171 return self.add_source(&path);
2172 }
2173 if lower.starts_with("pathmap:") {
2174 let rest = &option[8..];
2175 let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
2176 self.parsed.push(Argument::PrefixMap {
2177 flag: "/pathmap".into(),
2178 from: PathBuf::from(from),
2179 to: to.into(),
2180 });
2181 return Ok(());
2182 }
2183 if matches!(option, "D" | "U") {
2184 let definition = self.value(value, "")?;
2185 self.parsed
2186 .push(Argument::Plain(format!("/{option}{definition}")));
2187 return Ok(());
2188 }
2189 let definition = option.starts_with('D') || option.starts_with('U');
2192 let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
2193 || lower
2194 .strip_prefix('w')
2195 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2196 || ["wd", "we", "wo"].iter().any(|prefix| {
2197 lower
2198 .strip_prefix(prefix)
2199 .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2200 });
2201 let admitted = definition
2202 || warning
2203 || lower.starts_with("std:")
2204 || lower.starts_with("arch:")
2205 || lower.starts_with("favor:")
2206 || lower.starts_with("volatile:")
2207 || lower.starts_with("fp:")
2208 || lower.starts_with("eh")
2209 || lower.starts_with('o')
2210 || lower.starts_with("ob")
2211 || lower.starts_with("oi")
2212 || lower.starts_with("ot")
2213 || lower.starts_with("oy")
2214 || lower.starts_with("gs")
2215 || lower.starts_with("gr")
2216 || lower.starts_with("gy")
2217 || lower.starts_with("gw")
2218 || lower.starts_with("gl")
2219 || lower.starts_with("zc:")
2220 || lower.starts_with("diagnostics:")
2221 || matches!(
2222 lower.as_str(),
2223 "nologo"
2224 | "brepro"
2225 | "bigobj"
2226 | "utf-8"
2227 | "permissive-"
2228 | "z7"
2229 | "md"
2230 | "mdd"
2231 | "mt"
2232 | "mtd"
2233 );
2234 if admitted {
2235 self.parsed.push(Argument::Plain(value.into()));
2236 return Ok(());
2237 }
2238 Err(CcBypassReason::UnknownFlag(value.into()))
2239 }
2240}
2241
2242#[cfg(test)]
2243#[path = "cc_cache_tests.rs"]
2244mod tests;