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/// Environment variables that force a bypass when set and the compiler is MSVC.
81///
82/// `cl.exe` prepends their contents to the command line, so argv no longer
83/// describes the compile.
84pub const MSVC_BYPASS_ENVIRONMENT: &[&str] = &["CL", "_CL_"];
85
86/// Absolute roots whose contents are keyed verbatim rather than through a
87/// placeholder.
88///
89/// Files beneath these roots are still digested; keying the path verbatim only
90/// declares that the path itself is a machine property rather than a
91/// checkout-specific one, which is what makes system headers shareable between
92/// worktrees on one machine.
93pub 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", // Suppress preprocessor line markers; keyed even without -E.
258    "-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
279/// Assembler options whose effects are completely described by their text.
280///
281/// Other assembler options can name files that dependency discovery does not
282/// report, so they remain conservative passthrough bypasses.
283const SUPPORTED_ASSEMBLER_OPTIONS: &[&str] = &["--noexecstack"];
284
285const COMPILER_QUERY_FLAGS: &[&str] = &[
286    "--help",
287    "--version",
288    "-###",
289    // The `cc` crate probes with `-?` to tell an MSVC-style driver from a
290    // gcc-style one; neither answer is a compilation.
291    "-?",
292    "-dumpmachine",
293    "-dumpversion",
294    "-v",
295];
296
297/// Flags that rewrite a path prefix in the compiler's own output.
298///
299/// The left side is a real path and normalizes like any other; the right side
300/// is the text it is replaced with and enters the key verbatim.
301const PREFIX_MAP_FLAGS: &[&str] = &[
302    "-fdebug-prefix-map",
303    "-ffile-prefix-map",
304    "-fmacro-prefix-map",
305];
306
307impl CcBypassReason {
308    /// A stable, low-cardinality name for this reason.
309    ///
310    /// Many variants carry a path or a flag, so `Display` text cannot be
311    /// aggregated; statistics group by this instead.
312    pub fn kind(&self) -> &'static str {
313        self.into()
314    }
315
316    /// A concrete change that can make this invocation cacheable, when one is
317    /// available.
318    ///
319    /// Expected compiler probes and failures that require adapter support
320    /// return `None`; callers can still explain those from
321    /// [`CcBypassReason::kind`].
322    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/// Reason a C or C++ invocation cannot safely use the action cache.
348///
349/// A bypass is an expected conservative outcome, not a compiler error. Match on
350/// [`CcBypassReason::kind`] for aggregation rather than on the variants.
351#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
352#[strum(serialize_all = "kebab-case")]
353#[non_exhaustive]
354pub enum CcBypassReason {
355    /// An argument cannot be represented in the canonical UTF-8 key.
356    #[error("compiler argument {index} is not valid UTF-8")]
357    NonUtf8Argument {
358        /// Zero-based index in the argument slice.
359        index: usize,
360    },
361    /// The driver was handed an argument file.
362    #[error("compiler response file is not modeled by the cache adapter: {0}")]
363    ResponseFile(String),
364    /// A compiler flag is not modeled by this adapter version.
365    #[error("compiler flag is not modeled by the cache adapter: {0}")]
366    UnknownFlag(String),
367    /// A recognized flag was given without its value.
368    #[error("compiler flag {0} is missing its value")]
369    MissingValue(String),
370    /// The invocation asks the driver about itself rather than compiling.
371    #[error("compiler invocation queries the driver instead of compiling")]
372    CompilerQuery,
373    /// The invocation is not a single-object compile.
374    #[error("compiler invocation does not compile with -c")]
375    NotACompile,
376    /// The invocation emits preprocessed source or assembly.
377    #[error("compiler invocation emits a non-object output: {0}")]
378    NonObjectOutput(String),
379    /// The source arrives on standard input and cannot be rediscovered.
380    #[error("compiler invocation reads its source from standard input")]
381    StandardInput,
382    /// No source file was given.
383    #[error("compiler invocation names no source file")]
384    MissingInput,
385    /// More than one source file was given.
386    #[error("compiler invocation names more than one source file")]
387    MultipleInputs,
388    /// No `-o` was given, so the object name follows driver defaults.
389    #[error("compiler invocation names no output file")]
390    MissingOutput,
391    /// The source language is outside the modeled set.
392    #[error("compiler input language is not modeled by the cache adapter: {0}")]
393    UnsupportedLanguage(String),
394    /// The caller asked for its own dependency output.
395    #[error("compiler invocation requests its own dependency output: {0}")]
396    CallerDependencyFlags(String),
397    /// Precompiled headers are not byte-hermetic key material.
398    #[error("precompiled headers are not modeled by the cache adapter: {0}")]
399    PrecompiledHeader(String),
400    /// Coverage instrumentation writes outputs beside the object.
401    #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
402    CoverageInstrumentation(String),
403    /// Split debug info writes a `.dwo` beside the object.
404    #[error("split debug output is not modeled by the cache adapter: {0}")]
405    SplitDebugOutput(String),
406    /// Temporary files are preserved beside the object.
407    #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
408    SaveTemps(String),
409    /// An option is smuggled to a sub-tool the adapter cannot model.
410    #[error("compiler flag forwards options to another tool: {0}")]
411    ToolPassthrough(String),
412    /// A compiler plugin makes the output depend on unmodeled code.
413    #[error("compiler plugins are not modeled by the cache adapter: {0}")]
414    Plugin(String),
415    /// An include search directory gained or lost a header while the compiler
416    /// ran, so the manifest recorded after it is not what the compilation saw.
417    #[error("include search directory changed during the compilation: {0}")]
418    SearchPathModifiedDuringCompilation(PathBuf),
419
420    /// The object kept a path the key normalized away.
421    ///
422    /// Remapping covers what the compiler records itself; a path the source
423    /// keeps as a string survives it, and publishing such an object would
424    /// share this checkout's directory under a key that says it does not
425    /// matter.
426    #[error("compilation output records a path its key normalized away: {0}")]
427    UnportableOutput(PathBuf),
428    /// The caller chose not to store objects bound to this checkout's paths.
429    #[error("storing path-specific C objects is disabled by MBX_CC_STORE_PATH_SPECIFIC")]
430    PathSpecificStorageDisabled,
431    /// The object depends on the machine's own CPU rather than on named inputs.
432    #[error("compiler flag tunes for the local CPU: {0}")]
433    LocalCpuTarget(String),
434    /// The driver is not a gcc-style or clang-style compiler.
435    #[error("compiler driver is not modeled by the cache adapter: {0}")]
436    UnsupportedCompilerDriver(String),
437    /// The identity probe could not be run or parsed.
438    #[error("could not establish compiler identity: {0}")]
439    CompilerIdentityUnavailable(String),
440    /// An environment variable outside the modeled set is set.
441    #[error("environment variable {0} changes the compilation in an unmodeled way")]
442    UnsupportedEnvironment(String),
443    /// The shim could not be told which real compiler to run.
444    #[error("no real compiler was pinned for the cc shim")]
445    RealCompilerUnpinned,
446    /// A read file expands a timestamp macro, so the object is not a function
447    /// of its inputs.
448    #[error("input expands a timestamp macro: {0}")]
449    EmbeddedTimestampMacro(PathBuf),
450    /// Preprocessed assembly names an input that compiler dependency output
451    /// does not report.
452    #[error("preprocessed assembly uses an assembler input directive in {0}")]
453    AssemblerInputDirective(PathBuf),
454    /// The injected depfile could not be parsed exactly.
455    #[error("could not model the compiler depfile: {0}")]
456    MalformedDepfile(String),
457    /// The injected depfile could not be read.
458    #[error("could not read the compiler depfile {path}: {message}")]
459    DepfileRead {
460        /// Depfile that could not be read.
461        path: PathBuf,
462        /// Underlying error text.
463        message: String,
464    },
465    /// The action exceeds an input, byte, or manifest bound.
466    #[error("compilation reads more inputs than the cache adapter models")]
467    TooManyInputs,
468    /// An absolute path lies outside every mapped and system root.
469    #[error("path is outside every modeled root: {0}")]
470    UnmappedAbsolutePath(PathBuf),
471    /// A path cannot be represented in the canonical UTF-8 key.
472    #[error("path is not valid UTF-8: {0}")]
473    NonUtf8Path(PathBuf),
474    /// The compiler working directory is not absolute.
475    #[error("compiler working directory is not absolute: {0}")]
476    RelativeWorkingDirectory(PathBuf),
477    /// A configured path mapping root is not absolute.
478    #[error("path mapping root is not absolute: {0}")]
479    RelativePathMapping(PathBuf),
480    /// A configured placeholder is empty, duplicated, or not a bare name.
481    #[error("invalid path mapping placeholder: {0}")]
482    InvalidPathPlaceholder(String),
483    /// A required input never appeared among the discovered inputs.
484    #[error("required input is missing from the discovered inputs: {0}")]
485    MissingRequiredInput(String),
486    /// An input digest is malformed.
487    #[error("invalid digest for input: {0}")]
488    InvalidInputDigest(String),
489    /// One normalized path carries two different digests.
490    #[error("conflicting digests for input: {0}")]
491    ConflictingInput(String),
492    /// An input could not be read.
493    #[error("could not read input {path}: {message}")]
494    InputRead {
495        /// Input that could not be read.
496        path: PathBuf,
497        /// Underlying error text.
498        message: String,
499    },
500    /// An input changed between discovery and publication.
501    #[error("input changed during the compilation: {0}")]
502    InputChanged(PathBuf),
503    /// An input was written while the compiler ran.
504    #[error("input was modified during the compilation: {0}")]
505    InputModifiedDuringCompilation(PathBuf),
506    /// Discovery and the action disagree about the working directory.
507    #[error("discovered inputs use a different working directory")]
508    DiscoveryWorkingDirectory,
509    /// A prediction uses a schema this adapter version does not model.
510    #[error("action prediction is not modeled by this adapter version")]
511    UnsupportedPrediction,
512    /// A predicted input name cannot be resolved back to a host path.
513    #[error("invalid predicted input: {0}")]
514    InvalidPredictedInput(String),
515    /// Canonical serialization failed.
516    #[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/// Source language a driver invocation compiles.
530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
531pub enum CcLanguage {
532    /// C, driven through `CC`.
533    C,
534    /// C++, driven through `CXX`.
535    Cxx,
536}
537
538impl CcLanguage {
539    /// Shim file stem that selects this language.
540    ///
541    /// Neither stem ends in `-cc` or `-gcc`. Build tooling reads those
542    /// suffixes off `CC` as a cross-compiler prefix -- the `autotools` crate
543    /// strips one and hands the remainder to `configure` as `--host` -- so a
544    /// shim named `mbx-cc` made an absolute shim path look like a machine
545    /// triple.
546    pub fn shim_stem(self) -> &'static str {
547        match self {
548            Self::C => "mbx-c",
549            Self::Cxx => "mbx-cxx",
550        }
551    }
552
553    /// Default driver name to fall back to when no real compiler is pinned.
554    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/// Compiler family, which decides how the identity is assembled.
566#[derive(Debug, Clone, Copy, PartialEq, Eq)]
567pub enum CcCompilerFamily {
568    /// GCC, which compiles objects through an external assembler.
569    Gcc,
570    /// Upstream LLVM clang.
571    Clang,
572    /// Apple's clang distribution.
573    AppleClang,
574    /// Microsoft's `cl.exe` driver.
575    #[cfg(windows)]
576    Msvc,
577}
578
579/// Source preprocessed to ask `cl.exe` who it is.
580///
581/// The macros survive unexpanded in any compiler that does not define them, so
582/// the answer does not depend on banner wording or the console locale.
583pub 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
585/// Leading text of an identity built by [`msvc_identity`].
586pub const MSVC_IDENTITY_PREFIX: &str = "mbx-msvc-identity";
587
588/// Build the identity text for `cl.exe` from its preprocessed
589/// [`MSVC_IDENTITY_SOURCE`], or `None` when the compiler is not MSVC.
590///
591/// `clang-cl` defines `_MSC_VER` too, so `__clang__` rejects it: it is a
592/// different compiler with its own version line.
593pub 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    // The version macros match across the x86, x64 and ARM64 toolset binaries,
612    // so the architecture the compiler targets has to be keyed separately.
613    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    /// Stable name recorded in the action key.
632    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    /// Whether objects are produced through a separate assembler binary whose
643    /// version therefore belongs in the identity.
644    pub fn uses_external_assembler(self) -> bool {
645        matches!(self, Self::Gcc)
646    }
647
648    /// Whether this is Microsoft's `cl.exe` driver.
649    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    /// Classify a driver from its verbose probe output.
661    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/// Compiler properties that distinguish incompatible objects.
681#[derive(Debug, Clone, PartialEq, Eq)]
682pub struct CcCompilerIdentity {
683    /// Driver family.
684    pub family: CcCompilerFamily,
685    /// Complete verbose probe output, verbatim.
686    pub version_text: String,
687    /// Target triple the driver reports.
688    pub target: String,
689    /// Resolved assembler and its version, for families that use one.
690    ///
691    /// GCC assembles through binutils, whose version changes object bytes
692    /// without changing anything `gcc -v` prints. Clang assembles internally,
693    /// so this is empty there.
694    pub assembler: String,
695}
696
697/// One file input paired with the digest used in the action key.
698#[derive(Debug, Clone, PartialEq, Eq)]
699pub struct CcActionInput {
700    /// Absolute host path used to read and verify the input, or an
701    /// include-manifest pseudo-path.
702    pub path: PathBuf,
703    /// Digest of the input contents, or of the directory's name manifest.
704    pub digest: CacheDigest,
705}
706
707/// External information needed to construct a canonical cc action.
708#[derive(Debug, Clone, PartialEq, Eq)]
709pub struct CcActionContext {
710    /// Identity of the compiler that produces the object.
711    pub compiler: CcCompilerIdentity,
712    /// Absolute directory in which the compiler runs.
713    pub working_dir: PathBuf,
714    /// Host roots replaced with stable placeholders in the key.
715    pub path_mappings: Vec<PathMapping>,
716    /// Environment inputs and their observed values.
717    pub environment: BTreeMap<String, Option<String>>,
718    /// Complete set of direct and discovered file inputs.
719    pub inputs: Vec<CcActionInput>,
720}
721
722/// Canonical action descriptor and its content digest.
723#[derive(Debug, Clone, PartialEq, Eq)]
724pub struct CcAction {
725    /// Digest of `bytes`, used as the action-cache key.
726    pub digest: CacheDigest,
727    /// Canonical serialized action descriptor.
728    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/// Preprocessor text embeds literal paths in line markers and macros. Keep
777/// its invocation tied to those spellings instead of rewriting user output.
778#[derive(Debug, Serialize)]
779struct PreprocessingContext {
780    working_dir: PathBuf,
781    arguments: Vec<String>,
782    roots: BTreeMap<String, PathBuf>,
783}
784
785/// Literal path spellings retained by an object, unlike portable debug paths.
786#[derive(Debug, Serialize)]
787struct CcPathBinding {
788    working_dir: PathBuf,
789    arguments: Vec<Argument>,
790    roots: BTreeMap<String, PathBuf>,
791}
792
793/// Normalized input names from the last successful execution of one modeled
794/// compile.
795#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
796#[serde(deny_unknown_fields)]
797pub struct CcInputPrediction {
798    /// The output retained literal paths and requires a path-specific action key.
799    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
800    pub path_specific: bool,
801    /// Prediction schema version.
802    pub version: u8,
803    /// Normalized input paths, including include-manifest entries.
804    pub inputs: Vec<String>,
805    /// Names of environment variables that entered the key.
806    pub environment: Vec<String>,
807    /// Compiler wall time from the successful invocation that produced this
808    /// prediction. Zero means no timing hint was recorded.
809    #[serde(default, skip_serializing_if = "is_zero")]
810    pub compiler_duration_ns: u64,
811    /// Source file name associated with the timing hint.
812    #[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/// One parsed and admitted argument.
821#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
822enum Argument {
823    /// Keyed verbatim.
824    Plain(String),
825    /// Keyed with its path normalized.
826    Path { flag: String, path: PathBuf },
827    /// A prefix rewrite: the source path normalizes, the replacement does not.
828    PrefixMap {
829        flag: String,
830        from: PathBuf,
831        to: String,
832    },
833    /// The source file.
834    Source(PathBuf),
835}
836
837/// A parsed, admitted C or C++ compile.
838#[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    /// Positions in the parsed command line of the dependency-list flags the
851    /// caller passed, which the shim leaves out when it runs the driver.
852    dependency_argument_indices: Vec<usize>,
853}
854
855/// A dependency list the caller asked the driver to write beside the object.
856///
857/// Build systems driving the compiler through a build script -- OpenSSL's
858/// makefiles under `openssl-src`, CMake under many `-sys` crates -- ask for one
859/// with `-MD` or `-MMD` so their own rebuild logic has something to read. The
860/// flags do not change the object, so they are not key material, and the file
861/// names paths on this machine, so it is never stored. The shim writes it
862/// itself from the dependency list it already collects, whether the object
863/// was compiled or restored.
864#[derive(Debug, Clone, PartialEq, Eq)]
865#[non_exhaustive]
866pub struct CallerDepfile {
867    /// Where the list goes: `-MF`, or the object's path with a `.d` extension.
868    pub path: PathBuf,
869    /// The rule's targets, from `-MT` and `-MQ`, or the object as `-o` named it.
870    pub targets: Vec<DepfileTarget>,
871    /// Whether system headers are left out, as `-MMD` does.
872    pub user_headers_only: bool,
873    /// Whether every header also gets an empty rule of its own, as `-MP` does.
874    pub phony_targets: bool,
875}
876
877/// One target of a caller's dependency rule.
878#[derive(Debug, Clone, PartialEq, Eq)]
879pub struct DepfileTarget {
880    /// The target text as the caller gave it.
881    pub name: String,
882    /// Whether characters special to make are quoted on output, as `-MQ`
883    /// asks; `-MT` writes the name literally.
884    pub quoted: bool,
885}
886
887impl CcInvocation {
888    /// The object's path made absolute against the compiler's working
889    /// directory, for reading and storing it. `-o` is often relative --
890    /// OpenSSL's makefiles name every object that way -- and a path handed to
891    /// the cache agent has to mean the same file from the agent's own
892    /// directory. Key material still normalizes the spelling as given.
893    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    /// The dependency list the caller asked for, if any.
902    pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
903        self.caller_depfile.as_ref()
904    }
905
906    /// The command line to run the driver with: `arguments` as parsed, less
907    /// the caller's dependency-list flags. The shim asks the driver for its
908    /// own list, the driver honors only one `-MF`, and the caller's list is
909    /// written by the shim afterwards.
910    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    /// Parse a driver command line, admitting modeled single-source compiles
920    /// and preprocessing to a named output.
921    pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
922        Parser::new(arguments).parse()
923    }
924
925    /// Parse a command line using the syntax of `family`.
926    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    /// Parse a command line using Microsoft `cl.exe` syntax.
940    pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
941        MsvcParser::new(arguments).parse()
942    }
943
944    /// Source file this invocation compiles.
945    pub fn source(&self) -> &Path {
946        &self.source
947    }
948
949    /// Whether this invocation writes named preprocessor text.
950    pub fn is_preprocessing(&self) -> bool {
951        self.preprocessing_arguments.is_some()
952    }
953
954    /// Output file this invocation produces.
955    pub fn output(&self) -> &Path {
956        &self.output
957    }
958
959    /// Include search directories named on the command line, in order.
960    pub fn include_dirs(&self) -> &[PathBuf] {
961        &self.include_dirs
962    }
963
964    /// Files that must appear among the discovered inputs.
965    pub fn required_inputs(&self) -> &[PathBuf] {
966        &self.required_inputs
967    }
968
969    /// Language the driver compiles.
970    pub fn language(&self) -> CcLanguage {
971        self.language
972    }
973
974    /// Reject assembler-time file directives that the preprocessor's
975    /// dependency list cannot name.
976    ///
977    /// This is a no-op for C and C++ inputs. For preprocessed assembly, every
978    /// file reported by dependency discovery is scanned conservatively before
979    /// an object can be published.
980    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    /// Sysroot named on the command line, if any.
996    pub fn sysroot(&self) -> Option<&Path> {
997        self.sysroot.as_deref()
998    }
999
1000    /// Short label used for timing statistics.
1001    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    /// Arguments to append so the driver writes a dependency list beside the
1009    /// object.
1010    ///
1011    /// `-MD` rather than `-MMD`: system headers are exactly the inputs most
1012    /// likely to change without any other key component noticing, because the
1013    /// compiler identity does not cover the C library or the platform SDK.
1014    pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
1015        vec!["-MD".into(), "-MF".into(), depfile.into()]
1016    }
1017
1018    /// Arguments to append so a driver from `family` writes its dependency
1019    /// list beside the object.
1020    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    /// Arguments to append so `cl.exe` writes `/sourceDependencies` JSON.
1033    pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
1034        vec!["/sourceDependencies".into(), depfile.into()]
1035    }
1036
1037    /// Digest of the pre-input fingerprint, used to look up a prediction.
1038    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    /// Build the canonical action for this invocation and its discovered
1050    /// inputs.
1051    pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
1052        self.action_with_path_binding(context, false)
1053    }
1054
1055    /// Build an action tied to literal path spellings when the output retained them.
1056    /// Portable actions keep their existing keys.
1057    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    /// Record the normalized inputs of a successful compile so the next cold
1066    /// invocation can rebuild the same key before compiling.
1067    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    /// Rehash the predicted paths and recompute include manifests. The caller
1093    /// still recomputes the full action digest, so changed inputs are misses.
1094    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
1123/// Resolve a normalized key path back to a host path.
1124///
1125/// Placeholder entries expand through their mapping; a verbatim entry is
1126/// accepted only when it still lies beneath an admitted system root, so a
1127/// prediction cannot name an arbitrary absolute path.
1128fn 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    // A verbatim entry names a machine path rather than a placeholder. It is
1146    // admitted only beneath a system root, and only spelled literally: a
1147    // traversal component would let a prediction reach outside that root.
1148    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
1162/// Whether a path lies beneath a root whose location is a machine property.
1163pub 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// Xcode may be renamed or installed outside /Applications. Match the active
1171// developer directory rather than assuming the application bundle's name.
1172// Keep both spellings because compiler depfiles may resolve SDK symlinks.
1173#[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    // xcrun also accepts DEVELOPER_DIR pointing at the application bundle.
1207    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
1237/// Read the modeled environment, rejecting variables that change the compile in
1238/// a way the argv model cannot see.
1239pub 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
1249/// Read the modeled environment for a particular compiler family.
1250pub 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        // An explicit `-isysroot` on the command line already pins the SDK, and
1266        // it is what the driver honors, so the variable stops being an input.
1267        if *name == "SDKROOT" && sysroot.is_some() {
1268            continue;
1269        }
1270        environment.insert((*name).to_string(), lookup(name));
1271    }
1272    if family.is_msvc() {
1273        // INCLUDE changes header resolution without appearing in argv. The
1274        // toolset and SDK versions make the otherwise machine-local paths
1275        // meaningful when action records move between hosts.
1276        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    /// Reuse parent resolution across the inputs of this action only.
1298    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            // Version the assembly-only safety model independently so adding
1391            // support does not invalidate every existing C and C++ entry.
1392            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    /// Normalize a path that names a compilation input or search root.
1461    ///
1462    /// A path beneath a mapped root becomes a placeholder so equivalent
1463    /// checkouts agree. A path beneath a system root stays verbatim: its
1464    /// location is a property of the machine, and its contents are digested
1465    /// like any other input.
1466    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/// What the caller's `-M` flags asked for, gathered while parsing.
1527#[derive(Debug, Default)]
1528struct DependencyRequest {
1529    /// `Some(false)` for `-MD`, `Some(true)` for `-MMD`.
1530    user_headers_only: Option<bool>,
1531    file: Option<PathBuf>,
1532    targets: Vec<DepfileTarget>,
1533    phony_targets: bool,
1534    /// The last `-MF`, `-MT`, `-MQ`, or `-MP` seen, for the bypass a request
1535    /// without `-MD` or `-MMD` reports.
1536    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        // GCC interprets -o as the dependency output with -E -MD unless -MF
1586        // is explicit. Do not turn that command into a different operation.
1587        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                        // GCC -E ignores -o when choosing the dependency
1612                        // target; Clang retains it. Neither uses the depfile.
1613                        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            // `-MF` or `-MT` without `-MD` or `-MMD` writes nothing, but it is
1630            // not a shape the cc crate or a makefile produces, so it is not
1631            // one to guess at.
1632            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    /// A `-M` flag: the caller wants a dependency list beside the object.
1658    ///
1659    /// `-MD` and `-MMD` compile as well as listing, so they are taken, with
1660    /// the `-MF`, `-MT`, `-MQ`, and `-MP` that shape the list. None of them
1661    /// enters the key: the object is the same with or without them. `-M` and
1662    /// `-MM` stop after preprocessing, and `-MG` changes what a missing header
1663    /// means, so those still bypass.
1664    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            // Preprocessed assembly goes through the C driver: the preprocessor
1703            // resolves its includes, which is what dependency discovery reads,
1704            // and the assembler that then produces the object is already part
1705            // of a GCC identity. Plain `.s` is not preprocessed and reports no
1706            // dependencies, so it stays out.
1707            "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        // With an explicit `-x`, the driver ignores the extension entirely;
1741        // without one, the extension is the only thing that decides the
1742        // language, so an unmodeled extension has to bypass here.
1743        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            // The forwarded options are the compilation's real inputs and they
1793            // are not modeled, so consuming the value would not make this safe.
1794            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            // A repeated `-o` follows the driver: the last one names the file
1816            // that is produced. Every occurrence still enters the key.
1817            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                // `-include` and `-imacros` are deliberately not required
1839                // inputs. The driver resolves the name through the include
1840                // chain, so the file need not exist relative to the working
1841                // directory, and the dependency list names it at whatever path
1842                // it was actually found at.
1843                _ => {}
1844            }
1845            self.parsed.push(Argument::Path {
1846                flag: value.into(),
1847                path,
1848            });
1849            return Ok(());
1850        }
1851        // `--include=<file>` is the long spelling of `-include <file>`; the
1852        // `cc` crate emits it for prefixed headers.
1853        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        // `--param name=value` tunes the optimizer; its text fully describes it.
1875        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            // `-gsplit-dwarf` writes a `.dwo` beside the object; every other
1943            // unlisted `-g` spelling is simply unmodeled.
1944            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            // Warning selection changes only diagnostics, which are replayed
1952            // from the cache, and the exit status, and only successful
1953            // compiles are ever published.
1954            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        // `-march=native` and its relatives resolve against whatever CPU this
1980        // machine has. The resulting object is not a function of the key, so
1981        // another machine could otherwise restore code its processor cannot
1982        // run.
1983        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
1998/// Conservative parser for the command lines emitted by the `cc` crate for
1999/// Microsoft's compiler. It intentionally admits only flags whose effects are
2000/// either present in argv or covered by dependency discovery.
2001struct 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        // Definitions and the ordinary code-generation/diagnostic switches
2196        // produced by cc-rs are self-contained text and can be keyed verbatim.
2197        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;