Skip to main content

mbx_cache_cc/
lib.rs

1//! Conservative parsing and action-key construction for C and C++ compiles.
2//!
3//! Cargo build scripts using the `cc` crate compile C and C++ through a
4//! gcc-style driver. This adapter models the narrow shape those build scripts
5//! produce -- one source, one object, `-c` -- plus named `-E` output. As
6//! in the rustc adapter, callers should treat [`CcBypassReason`] as a safe
7//! cache bypass: run the real compiler and publish nothing.
8//!
9//! Two properties separate this adapter from a traditional compiler cache.
10//! Preprocessor inputs are discovered from a depfile the adapter injects
11//! itself, so the key names the headers the compilation actually read; and the
12//! directories those headers were searched from contribute a name manifest, so
13//! a header that newly *shadows* one of them changes the key even though every
14//! previously-read file is byte-identical.
15//!
16//! Path mappings are shared with the rustc adapter so both agree on which host
17//! roots are checkout-specific.
18#![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
34/// Schema version embedded in canonical cc action descriptors.
35pub const ACTION_SCHEMA_VERSION: u8 = 1;
36/// Version of the cc argument and input model used to construct keys.
37pub const ADAPTER_VERSION: u8 = 1;
38
39/// Maximum discovered inputs, including include-manifest entries.
40pub const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
41/// Maximum total bytes digested for one action.
42pub const MAX_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
43/// Maximum file names summarized across all include manifests.
44pub const MAX_MANIFEST_ENTRIES: usize = 16 * 1024;
45
46/// Environment variables whose values enter every cc action key.
47///
48/// These change the compiler's own behavior without appearing in argv. They
49/// are recorded even when unset, so setting one is distinguishable from
50/// leaving it unset.
51pub 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
64/// Environment variables that force a bypass when set.
65///
66/// Each one either injects search paths the argv model cannot see, redirects
67/// sub-tool resolution beneath the identity probe, or makes the driver write an
68/// output the adapter does not model.
69pub 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
80/// Absolute roots whose contents are keyed verbatim rather than through a
81/// placeholder.
82///
83/// Files beneath these roots are still digested; keying the path verbatim only
84/// declares that the path itself is a machine property rather than a
85/// checkout-specific one, which is what makes system headers shareable between
86/// worktrees on one machine.
87pub 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", // Suppress preprocessor line markers; keyed even without -E.
252    "-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
273/// Assembler options whose effects are completely described by their text.
274///
275/// Other assembler options can name files that dependency discovery does not
276/// report, so they remain conservative passthrough bypasses.
277const SUPPORTED_ASSEMBLER_OPTIONS: &[&str] = &["--noexecstack"];
278
279const COMPILER_QUERY_FLAGS: &[&str] = &[
280    "--help",
281    "--version",
282    "-###",
283    // The `cc` crate probes with `-?` to tell an MSVC-style driver from a
284    // gcc-style one; neither answer is a compilation.
285    "-?",
286    "-dumpmachine",
287    "-dumpversion",
288    "-v",
289];
290
291/// Flags that rewrite a path prefix in the compiler's own output.
292///
293/// The left side is a real path and normalizes like any other; the right side
294/// is the text it is replaced with and enters the key verbatim.
295const PREFIX_MAP_FLAGS: &[&str] = &[
296    "-fdebug-prefix-map",
297    "-ffile-prefix-map",
298    "-fmacro-prefix-map",
299];
300
301impl CcBypassReason {
302    /// A stable, low-cardinality name for this reason.
303    ///
304    /// Many variants carry a path or a flag, so `Display` text cannot be
305    /// aggregated; statistics group by this instead.
306    pub fn kind(&self) -> &'static str {
307        self.into()
308    }
309
310    /// A concrete change that can make this invocation cacheable, when one is
311    /// available.
312    ///
313    /// Expected compiler probes and failures that require adapter support
314    /// return `None`; callers can still explain those from
315    /// [`CcBypassReason::kind`].
316    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/// Reason a C or C++ invocation cannot safely use the action cache.
342///
343/// A bypass is an expected conservative outcome, not a compiler error. Match on
344/// [`CcBypassReason::kind`] for aggregation rather than on the variants.
345#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
346#[strum(serialize_all = "kebab-case")]
347#[non_exhaustive]
348pub enum CcBypassReason {
349    /// An argument cannot be represented in the canonical UTF-8 key.
350    #[error("compiler argument {index} is not valid UTF-8")]
351    NonUtf8Argument {
352        /// Zero-based index in the argument slice.
353        index: usize,
354    },
355    /// The driver was handed an argument file.
356    #[error("compiler response file is not modeled by the cache adapter: {0}")]
357    ResponseFile(String),
358    /// A compiler flag is not modeled by this adapter version.
359    #[error("compiler flag is not modeled by the cache adapter: {0}")]
360    UnknownFlag(String),
361    /// A recognized flag was given without its value.
362    #[error("compiler flag {0} is missing its value")]
363    MissingValue(String),
364    /// The invocation asks the driver about itself rather than compiling.
365    #[error("compiler invocation queries the driver instead of compiling")]
366    CompilerQuery,
367    /// The invocation is not a single-object compile.
368    #[error("compiler invocation does not compile with -c")]
369    NotACompile,
370    /// The invocation emits preprocessed source or assembly.
371    #[error("compiler invocation emits a non-object output: {0}")]
372    NonObjectOutput(String),
373    /// The source arrives on standard input and cannot be rediscovered.
374    #[error("compiler invocation reads its source from standard input")]
375    StandardInput,
376    /// No source file was given.
377    #[error("compiler invocation names no source file")]
378    MissingInput,
379    /// More than one source file was given.
380    #[error("compiler invocation names more than one source file")]
381    MultipleInputs,
382    /// No `-o` was given, so the object name follows driver defaults.
383    #[error("compiler invocation names no output file")]
384    MissingOutput,
385    /// The source language is outside the modeled set.
386    #[error("compiler input language is not modeled by the cache adapter: {0}")]
387    UnsupportedLanguage(String),
388    /// The caller asked for its own dependency output.
389    #[error("compiler invocation requests its own dependency output: {0}")]
390    CallerDependencyFlags(String),
391    /// Precompiled headers are not byte-hermetic key material.
392    #[error("precompiled headers are not modeled by the cache adapter: {0}")]
393    PrecompiledHeader(String),
394    /// Coverage instrumentation writes outputs beside the object.
395    #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
396    CoverageInstrumentation(String),
397    /// Split debug info writes a `.dwo` beside the object.
398    #[error("split debug output is not modeled by the cache adapter: {0}")]
399    SplitDebugOutput(String),
400    /// Temporary files are preserved beside the object.
401    #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
402    SaveTemps(String),
403    /// An option is smuggled to a sub-tool the adapter cannot model.
404    #[error("compiler flag forwards options to another tool: {0}")]
405    ToolPassthrough(String),
406    /// A compiler plugin makes the output depend on unmodeled code.
407    #[error("compiler plugins are not modeled by the cache adapter: {0}")]
408    Plugin(String),
409    /// An include search directory gained or lost a header while the compiler
410    /// ran, so the manifest recorded after it is not what the compilation saw.
411    #[error("include search directory changed during the compilation: {0}")]
412    SearchPathModifiedDuringCompilation(PathBuf),
413
414    /// The object kept a path the key normalized away.
415    ///
416    /// Remapping covers what the compiler records itself; a path the source
417    /// keeps as a string survives it, and publishing such an object would
418    /// share this checkout's directory under a key that says it does not
419    /// matter.
420    #[error("compilation output records a path its key normalized away: {0}")]
421    UnportableOutput(PathBuf),
422    /// The caller chose not to store objects bound to this checkout's paths.
423    #[error("storing path-specific C objects is disabled by MBX_CC_STORE_PATH_SPECIFIC")]
424    PathSpecificStorageDisabled,
425    /// The object depends on the machine's own CPU rather than on named inputs.
426    #[error("compiler flag tunes for the local CPU: {0}")]
427    LocalCpuTarget(String),
428    /// The driver is not a gcc-style or clang-style compiler.
429    #[error("compiler driver is not modeled by the cache adapter: {0}")]
430    UnsupportedCompilerDriver(String),
431    /// The identity probe could not be run or parsed.
432    #[error("could not establish compiler identity: {0}")]
433    CompilerIdentityUnavailable(String),
434    /// An environment variable outside the modeled set is set.
435    #[error("environment variable {0} changes the compilation in an unmodeled way")]
436    UnsupportedEnvironment(String),
437    /// The shim could not be told which real compiler to run.
438    #[error("no real compiler was pinned for the cc shim")]
439    RealCompilerUnpinned,
440    /// A read file expands a timestamp macro, so the object is not a function
441    /// of its inputs.
442    #[error("input expands a timestamp macro: {0}")]
443    EmbeddedTimestampMacro(PathBuf),
444    /// Preprocessed assembly names an input that compiler dependency output
445    /// does not report.
446    #[error("preprocessed assembly uses an assembler input directive in {0}")]
447    AssemblerInputDirective(PathBuf),
448    /// The injected depfile could not be parsed exactly.
449    #[error("could not model the compiler depfile: {0}")]
450    MalformedDepfile(String),
451    /// The injected depfile could not be read.
452    #[error("could not read the compiler depfile {path}: {message}")]
453    DepfileRead {
454        /// Depfile that could not be read.
455        path: PathBuf,
456        /// Underlying error text.
457        message: String,
458    },
459    /// The action exceeds an input, byte, or manifest bound.
460    #[error("compilation reads more inputs than the cache adapter models")]
461    TooManyInputs,
462    /// An absolute path lies outside every mapped and system root.
463    #[error("path is outside every modeled root: {0}")]
464    UnmappedAbsolutePath(PathBuf),
465    /// A path cannot be represented in the canonical UTF-8 key.
466    #[error("path is not valid UTF-8: {0}")]
467    NonUtf8Path(PathBuf),
468    /// The compiler working directory is not absolute.
469    #[error("compiler working directory is not absolute: {0}")]
470    RelativeWorkingDirectory(PathBuf),
471    /// A configured path mapping root is not absolute.
472    #[error("path mapping root is not absolute: {0}")]
473    RelativePathMapping(PathBuf),
474    /// A configured placeholder is empty, duplicated, or not a bare name.
475    #[error("invalid path mapping placeholder: {0}")]
476    InvalidPathPlaceholder(String),
477    /// A required input never appeared among the discovered inputs.
478    #[error("required input is missing from the discovered inputs: {0}")]
479    MissingRequiredInput(String),
480    /// An input digest is malformed.
481    #[error("invalid digest for input: {0}")]
482    InvalidInputDigest(String),
483    /// One normalized path carries two different digests.
484    #[error("conflicting digests for input: {0}")]
485    ConflictingInput(String),
486    /// An input could not be read.
487    #[error("could not read input {path}: {message}")]
488    InputRead {
489        /// Input that could not be read.
490        path: PathBuf,
491        /// Underlying error text.
492        message: String,
493    },
494    /// An input changed between discovery and publication.
495    #[error("input changed during the compilation: {0}")]
496    InputChanged(PathBuf),
497    /// An input was written while the compiler ran.
498    #[error("input was modified during the compilation: {0}")]
499    InputModifiedDuringCompilation(PathBuf),
500    /// Discovery and the action disagree about the working directory.
501    #[error("discovered inputs use a different working directory")]
502    DiscoveryWorkingDirectory,
503    /// A prediction uses a schema this adapter version does not model.
504    #[error("action prediction is not modeled by this adapter version")]
505    UnsupportedPrediction,
506    /// A predicted input name cannot be resolved back to a host path.
507    #[error("invalid predicted input: {0}")]
508    InvalidPredictedInput(String),
509    /// Canonical serialization failed.
510    #[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/// Source language a driver invocation compiles.
524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum CcLanguage {
526    /// C, driven through `CC`.
527    C,
528    /// C++, driven through `CXX`.
529    Cxx,
530}
531
532impl CcLanguage {
533    /// Shim file stem that selects this language.
534    ///
535    /// Neither stem ends in `-cc` or `-gcc`. Build tooling reads those
536    /// suffixes off `CC` as a cross-compiler prefix -- the `autotools` crate
537    /// strips one and hands the remainder to `configure` as `--host` -- so a
538    /// shim named `mbx-cc` made an absolute shim path look like a machine
539    /// triple.
540    pub fn shim_stem(self) -> &'static str {
541        match self {
542            Self::C => "mbx-c",
543            Self::Cxx => "mbx-cxx",
544        }
545    }
546
547    /// Default driver name to fall back to when no real compiler is pinned.
548    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/// Compiler family, which decides how the identity is assembled.
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub enum CcCompilerFamily {
562    /// GCC, which compiles objects through an external assembler.
563    Gcc,
564    /// Upstream LLVM clang.
565    Clang,
566    /// Apple's clang distribution.
567    AppleClang,
568    /// Microsoft's `cl.exe` driver.
569    #[cfg(windows)]
570    Msvc,
571}
572
573/// Source preprocessed to ask `cl.exe` who it is.
574///
575/// The macros survive unexpanded in any compiler that does not define them, so
576/// the answer does not depend on banner wording or the console locale.
577pub 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
579/// Leading text of an identity built by [`msvc_identity`].
580pub const MSVC_IDENTITY_PREFIX: &str = "mbx-msvc-identity";
581
582/// Build the identity text for `cl.exe` from its preprocessed
583/// [`MSVC_IDENTITY_SOURCE`], or `None` when the compiler is not MSVC.
584///
585/// `clang-cl` defines `_MSC_VER` too, so `__clang__` rejects it: it is a
586/// different compiler with its own version line.
587pub 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    // The version macros match across the x86, x64 and ARM64 toolset binaries,
606    // so the architecture the compiler targets has to be keyed separately.
607    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    /// Stable name recorded in the action key.
626    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    /// Whether objects are produced through a separate assembler binary whose
637    /// version therefore belongs in the identity.
638    pub fn uses_external_assembler(self) -> bool {
639        matches!(self, Self::Gcc)
640    }
641
642    /// Whether this is Microsoft's `cl.exe` driver.
643    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    /// Classify a driver from its verbose probe output.
655    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/// Compiler properties that distinguish incompatible objects.
675#[derive(Debug, Clone, PartialEq, Eq)]
676pub struct CcCompilerIdentity {
677    /// Driver family.
678    pub family: CcCompilerFamily,
679    /// Complete verbose probe output, verbatim.
680    pub version_text: String,
681    /// Target triple the driver reports.
682    pub target: String,
683    /// Resolved assembler and its version, for families that use one.
684    ///
685    /// GCC assembles through binutils, whose version changes object bytes
686    /// without changing anything `gcc -v` prints. Clang assembles internally,
687    /// so this is empty there.
688    pub assembler: String,
689}
690
691/// One file input paired with the digest used in the action key.
692#[derive(Debug, Clone, PartialEq, Eq)]
693pub struct CcActionInput {
694    /// Absolute host path used to read and verify the input, or an
695    /// include-manifest pseudo-path.
696    pub path: PathBuf,
697    /// Digest of the input contents, or of the directory's name manifest.
698    pub digest: CacheDigest,
699}
700
701/// External information needed to construct a canonical cc action.
702#[derive(Debug, Clone, PartialEq, Eq)]
703pub struct CcActionContext {
704    /// Identity of the compiler that produces the object.
705    pub compiler: CcCompilerIdentity,
706    /// Absolute directory in which the compiler runs.
707    pub working_dir: PathBuf,
708    /// Host roots replaced with stable placeholders in the key.
709    pub path_mappings: Vec<PathMapping>,
710    /// Environment inputs and their observed values.
711    pub environment: BTreeMap<String, Option<String>>,
712    /// Complete set of direct and discovered file inputs.
713    pub inputs: Vec<CcActionInput>,
714}
715
716/// Canonical action descriptor and its content digest.
717#[derive(Debug, Clone, PartialEq, Eq)]
718pub struct CcAction {
719    /// Digest of `bytes`, used as the action-cache key.
720    pub digest: CacheDigest,
721    /// Canonical serialized action descriptor.
722    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/// Preprocessor text embeds literal paths in line markers and macros. Keep
771/// its invocation tied to those spellings instead of rewriting user output.
772#[derive(Debug, Serialize)]
773struct PreprocessingContext {
774    working_dir: PathBuf,
775    arguments: Vec<String>,
776    roots: BTreeMap<String, PathBuf>,
777}
778
779/// Literal path spellings retained by an object, unlike portable debug paths.
780#[derive(Debug, Serialize)]
781struct CcPathBinding {
782    working_dir: PathBuf,
783    arguments: Vec<Argument>,
784    roots: BTreeMap<String, PathBuf>,
785}
786
787/// Normalized input names from the last successful execution of one modeled
788/// compile.
789#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
790#[serde(deny_unknown_fields)]
791pub struct CcInputPrediction {
792    /// The output retained literal paths and requires a path-specific action key.
793    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
794    pub path_specific: bool,
795    /// Prediction schema version.
796    pub version: u8,
797    /// Normalized input paths, including include-manifest entries.
798    pub inputs: Vec<String>,
799    /// Names of environment variables that entered the key.
800    pub environment: Vec<String>,
801    /// Compiler wall time from the successful invocation that produced this
802    /// prediction. Zero means no timing hint was recorded.
803    #[serde(default, skip_serializing_if = "is_zero")]
804    pub compiler_duration_ns: u64,
805    /// Source file name associated with the timing hint.
806    #[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/// One parsed and admitted argument.
815#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
816enum Argument {
817    /// Keyed verbatim.
818    Plain(String),
819    /// Keyed with its path normalized.
820    Path { flag: String, path: PathBuf },
821    /// A prefix rewrite: the source path normalizes, the replacement does not.
822    PrefixMap {
823        flag: String,
824        from: PathBuf,
825        to: String,
826    },
827    /// The source file.
828    Source(PathBuf),
829}
830
831/// A parsed, admitted C or C++ compile.
832#[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    /// Positions in the parsed command line of the dependency-list flags the
845    /// caller passed, which the shim leaves out when it runs the driver.
846    dependency_argument_indices: Vec<usize>,
847}
848
849/// A dependency list the caller asked the driver to write beside the object.
850///
851/// Build systems driving the compiler through a build script -- OpenSSL's
852/// makefiles under `openssl-src`, CMake under many `-sys` crates -- ask for one
853/// with `-MD` or `-MMD` so their own rebuild logic has something to read. The
854/// flags do not change the object, so they are not key material, and the file
855/// names paths on this machine, so it is never stored. The shim writes it
856/// itself from the dependency list it already collects, whether the object
857/// was compiled or restored.
858#[derive(Debug, Clone, PartialEq, Eq)]
859#[non_exhaustive]
860pub struct CallerDepfile {
861    /// Where the list goes: `-MF`, or the object's path with a `.d` extension.
862    pub path: PathBuf,
863    /// The rule's targets, from `-MT` and `-MQ`, or the object as `-o` named it.
864    pub targets: Vec<DepfileTarget>,
865    /// Whether system headers are left out, as `-MMD` does.
866    pub user_headers_only: bool,
867    /// Whether every header also gets an empty rule of its own, as `-MP` does.
868    pub phony_targets: bool,
869}
870
871/// One target of a caller's dependency rule.
872#[derive(Debug, Clone, PartialEq, Eq)]
873pub struct DepfileTarget {
874    /// The target text as the caller gave it.
875    pub name: String,
876    /// Whether characters special to make are quoted on output, as `-MQ`
877    /// asks; `-MT` writes the name literally.
878    pub quoted: bool,
879}
880
881impl CcInvocation {
882    /// The object's path made absolute against the compiler's working
883    /// directory, for reading and storing it. `-o` is often relative --
884    /// OpenSSL's makefiles name every object that way -- and a path handed to
885    /// the cache agent has to mean the same file from the agent's own
886    /// directory. Key material still normalizes the spelling as given.
887    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    /// The dependency list the caller asked for, if any.
896    pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
897        self.caller_depfile.as_ref()
898    }
899
900    /// The command line to run the driver with: `arguments` as parsed, less
901    /// the caller's dependency-list flags. The shim asks the driver for its
902    /// own list, the driver honors only one `-MF`, and the caller's list is
903    /// written by the shim afterwards.
904    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    /// Parse a driver command line, admitting modeled single-source compiles
914    /// and preprocessing to a named output.
915    pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
916        Parser::new(arguments).parse()
917    }
918
919    /// Parse a command line using the syntax of `family`.
920    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    /// Parse a command line using Microsoft `cl.exe` syntax.
934    pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
935        MsvcParser::new(arguments).parse()
936    }
937
938    /// Source file this invocation compiles.
939    pub fn source(&self) -> &Path {
940        &self.source
941    }
942
943    /// Whether this invocation writes named preprocessor text.
944    pub fn is_preprocessing(&self) -> bool {
945        self.preprocessing_arguments.is_some()
946    }
947
948    /// Output file this invocation produces.
949    pub fn output(&self) -> &Path {
950        &self.output
951    }
952
953    /// Include search directories named on the command line, in order.
954    pub fn include_dirs(&self) -> &[PathBuf] {
955        &self.include_dirs
956    }
957
958    /// Files that must appear among the discovered inputs.
959    pub fn required_inputs(&self) -> &[PathBuf] {
960        &self.required_inputs
961    }
962
963    /// Language the driver compiles.
964    pub fn language(&self) -> CcLanguage {
965        self.language
966    }
967
968    /// Reject assembler-time file directives that the preprocessor's
969    /// dependency list cannot name.
970    ///
971    /// This is a no-op for C and C++ inputs. For preprocessed assembly, every
972    /// file reported by dependency discovery is scanned conservatively before
973    /// an object can be published.
974    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    /// Sysroot named on the command line, if any.
990    pub fn sysroot(&self) -> Option<&Path> {
991        self.sysroot.as_deref()
992    }
993
994    /// Short label used for timing statistics.
995    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    /// Arguments to append so the driver writes a dependency list beside the
1003    /// object.
1004    ///
1005    /// `-MD` rather than `-MMD`: system headers are exactly the inputs most
1006    /// likely to change without any other key component noticing, because the
1007    /// compiler identity does not cover the C library or the platform SDK.
1008    pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
1009        vec!["-MD".into(), "-MF".into(), depfile.into()]
1010    }
1011
1012    /// Arguments to append so a driver from `family` writes its dependency
1013    /// list beside the object.
1014    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    /// Arguments to append so `cl.exe` writes `/sourceDependencies` JSON.
1027    pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
1028        vec!["/sourceDependencies".into(), depfile.into()]
1029    }
1030
1031    /// Digest of the pre-input fingerprint, used to look up a prediction.
1032    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    /// Build the canonical action for this invocation and its discovered
1044    /// inputs.
1045    pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
1046        self.action_with_path_binding(context, false)
1047    }
1048
1049    /// Build an action tied to literal path spellings when the output retained them.
1050    /// Portable actions keep their existing keys.
1051    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    /// Record the normalized inputs of a successful compile so the next cold
1060    /// invocation can rebuild the same key before compiling.
1061    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    /// Rehash the predicted paths and recompute include manifests. The caller
1087    /// still recomputes the full action digest, so changed inputs are misses.
1088    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
1117/// Resolve a normalized key path back to a host path.
1118///
1119/// Placeholder entries expand through their mapping; a verbatim entry is
1120/// accepted only when it still lies beneath an admitted system root, so a
1121/// prediction cannot name an arbitrary absolute path.
1122fn 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    // A verbatim entry names a machine path rather than a placeholder. It is
1140    // admitted only beneath a system root, and only spelled literally: a
1141    // traversal component would let a prediction reach outside that root.
1142    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
1156/// Whether a path lies beneath a root whose location is a machine property.
1157pub 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// Xcode may be renamed or installed outside /Applications. Match the active
1165// developer directory rather than assuming the application bundle's name.
1166// Keep both spellings because compiler depfiles may resolve SDK symlinks.
1167#[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    // xcrun also accepts DEVELOPER_DIR pointing at the application bundle.
1201    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
1231/// Read the modeled environment, rejecting variables that change the compile in
1232/// a way the argv model cannot see.
1233pub 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
1243/// Read the modeled environment for a particular compiler family.
1244pub 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        // An explicit `-isysroot` on the command line already pins the SDK, and
1260        // it is what the driver honors, so the variable stops being an input.
1261        if *name == "SDKROOT" && sysroot.is_some() {
1262            continue;
1263        }
1264        environment.insert((*name).to_string(), lookup(name));
1265    }
1266    if family.is_msvc() {
1267        // INCLUDE changes header resolution without appearing in argv. The
1268        // toolset and SDK versions make the otherwise machine-local paths
1269        // meaningful when action records move between hosts.
1270        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    /// Reuse parent resolution across the inputs of this action only.
1292    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            // Version the assembly-only safety model independently so adding
1385            // support does not invalidate every existing C and C++ entry.
1386            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    /// Normalize a path that names a compilation input or search root.
1455    ///
1456    /// A path beneath a mapped root becomes a placeholder so equivalent
1457    /// checkouts agree. A path beneath a system root stays verbatim: its
1458    /// location is a property of the machine, and its contents are digested
1459    /// like any other input.
1460    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/// What the caller's `-M` flags asked for, gathered while parsing.
1521#[derive(Debug, Default)]
1522struct DependencyRequest {
1523    /// `Some(false)` for `-MD`, `Some(true)` for `-MMD`.
1524    user_headers_only: Option<bool>,
1525    file: Option<PathBuf>,
1526    targets: Vec<DepfileTarget>,
1527    phony_targets: bool,
1528    /// The last `-MF`, `-MT`, `-MQ`, or `-MP` seen, for the bypass a request
1529    /// without `-MD` or `-MMD` reports.
1530    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        // GCC interprets -o as the dependency output with -E -MD unless -MF
1580        // is explicit. Do not turn that command into a different operation.
1581        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                        // GCC -E ignores -o when choosing the dependency
1606                        // target; Clang retains it. Neither uses the depfile.
1607                        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            // `-MF` or `-MT` without `-MD` or `-MMD` writes nothing, but it is
1624            // not a shape the cc crate or a makefile produces, so it is not
1625            // one to guess at.
1626            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    /// A `-M` flag: the caller wants a dependency list beside the object.
1652    ///
1653    /// `-MD` and `-MMD` compile as well as listing, so they are taken, with
1654    /// the `-MF`, `-MT`, `-MQ`, and `-MP` that shape the list. None of them
1655    /// enters the key: the object is the same with or without them. `-M` and
1656    /// `-MM` stop after preprocessing, and `-MG` changes what a missing header
1657    /// means, so those still bypass.
1658    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            // Preprocessed assembly goes through the C driver: the preprocessor
1697            // resolves its includes, which is what dependency discovery reads,
1698            // and the assembler that then produces the object is already part
1699            // of a GCC identity. Plain `.s` is not preprocessed and reports no
1700            // dependencies, so it stays out.
1701            "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        // With an explicit `-x`, the driver ignores the extension entirely;
1735        // without one, the extension is the only thing that decides the
1736        // language, so an unmodeled extension has to bypass here.
1737        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            // The forwarded options are the compilation's real inputs and they
1787            // are not modeled, so consuming the value would not make this safe.
1788            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            // A repeated `-o` follows the driver: the last one names the file
1810            // that is produced. Every occurrence still enters the key.
1811            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                // `-include` and `-imacros` are deliberately not required
1833                // inputs. The driver resolves the name through the include
1834                // chain, so the file need not exist relative to the working
1835                // directory, and the dependency list names it at whatever path
1836                // it was actually found at.
1837                _ => {}
1838            }
1839            self.parsed.push(Argument::Path {
1840                flag: value.into(),
1841                path,
1842            });
1843            return Ok(());
1844        }
1845        // `--include=<file>` is the long spelling of `-include <file>`; the
1846        // `cc` crate emits it for prefixed headers.
1847        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        // `--param name=value` tunes the optimizer; its text fully describes it.
1869        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            // `-gsplit-dwarf` writes a `.dwo` beside the object; every other
1937            // unlisted `-g` spelling is simply unmodeled.
1938            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            // Warning selection changes only diagnostics, which are replayed
1946            // from the cache, and the exit status, and only successful
1947            // compiles are ever published.
1948            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        // `-march=native` and its relatives resolve against whatever CPU this
1974        // machine has. The resulting object is not a function of the key, so
1975        // another machine could otherwise restore code its processor cannot
1976        // run.
1977        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
1992/// Conservative parser for the command lines emitted by the `cc` crate for
1993/// Microsoft's compiler. It intentionally admits only flags whose effects are
1994/// either present in argv or covered by dependency discovery.
1995struct 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        // Definitions and the ordinary code-generation/diagnostic switches
2190        // produced by cc-rs are self-contained text and can be keyed verbatim.
2191        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;