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