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
573impl CcCompilerFamily {
574    /// Stable name recorded in the action key.
575    pub fn as_str(self) -> &'static str {
576        match self {
577            Self::Gcc => "gcc",
578            Self::Clang => "clang",
579            Self::AppleClang => "apple-clang",
580            #[cfg(windows)]
581            Self::Msvc => "msvc",
582        }
583    }
584
585    /// Whether objects are produced through a separate assembler binary whose
586    /// version therefore belongs in the identity.
587    pub fn uses_external_assembler(self) -> bool {
588        matches!(self, Self::Gcc)
589    }
590
591    /// Whether this is Microsoft's `cl.exe` driver.
592    pub fn is_msvc(self) -> bool {
593        #[cfg(windows)]
594        {
595            matches!(self, Self::Msvc)
596        }
597        #[cfg(not(windows))]
598        {
599            false
600        }
601    }
602
603    /// Classify a driver from its verbose probe output.
604    pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
605        #[cfg(windows)]
606        if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
607            return Ok(Self::Msvc);
608        }
609        if probe.contains("Apple clang version") {
610            Ok(Self::AppleClang)
611        } else if probe.contains("clang version") {
612            Ok(Self::Clang)
613        } else if probe.contains("gcc version") {
614            Ok(Self::Gcc)
615        } else {
616            Err(CcBypassReason::UnsupportedCompilerDriver(
617                probe.lines().next().unwrap_or_default().into(),
618            ))
619        }
620    }
621}
622
623/// Compiler properties that distinguish incompatible objects.
624#[derive(Debug, Clone, PartialEq, Eq)]
625pub struct CcCompilerIdentity {
626    /// Driver family.
627    pub family: CcCompilerFamily,
628    /// Complete verbose probe output, verbatim.
629    pub version_text: String,
630    /// Target triple the driver reports.
631    pub target: String,
632    /// Resolved assembler and its version, for families that use one.
633    ///
634    /// GCC assembles through binutils, whose version changes object bytes
635    /// without changing anything `gcc -v` prints. Clang assembles internally,
636    /// so this is empty there.
637    pub assembler: String,
638}
639
640/// One file input paired with the digest used in the action key.
641#[derive(Debug, Clone, PartialEq, Eq)]
642pub struct CcActionInput {
643    /// Absolute host path used to read and verify the input, or an
644    /// include-manifest pseudo-path.
645    pub path: PathBuf,
646    /// Digest of the input contents, or of the directory's name manifest.
647    pub digest: CacheDigest,
648}
649
650/// External information needed to construct a canonical cc action.
651#[derive(Debug, Clone, PartialEq, Eq)]
652pub struct CcActionContext {
653    /// Identity of the compiler that produces the object.
654    pub compiler: CcCompilerIdentity,
655    /// Absolute directory in which the compiler runs.
656    pub working_dir: PathBuf,
657    /// Host roots replaced with stable placeholders in the key.
658    pub path_mappings: Vec<PathMapping>,
659    /// Environment inputs and their observed values.
660    pub environment: BTreeMap<String, Option<String>>,
661    /// Complete set of direct and discovered file inputs.
662    pub inputs: Vec<CcActionInput>,
663}
664
665/// Canonical action descriptor and its content digest.
666#[derive(Debug, Clone, PartialEq, Eq)]
667pub struct CcAction {
668    /// Digest of `bytes`, used as the action-cache key.
669    pub digest: CacheDigest,
670    /// Canonical serialized action descriptor.
671    pub bytes: Vec<u8>,
672}
673
674#[derive(Debug, Serialize)]
675struct CcCompilerDescriptor {
676    assembler: String,
677    family: String,
678    target: String,
679    version_text: String,
680}
681
682#[derive(Debug, Serialize)]
683struct CcInputDescriptor {
684    digest: CacheDigest,
685    path: String,
686}
687
688#[derive(Debug, Serialize)]
689struct CcActionDescriptor {
690    #[serde(skip_serializing_if = "Option::is_none")]
691    path_binding: Option<CcPathBinding>,
692    version: u8,
693    kind: &'static str,
694    adapter_version: u8,
695    #[serde(skip_serializing_if = "Option::is_none")]
696    assembly_input_model: Option<u8>,
697    #[serde(skip_serializing_if = "Option::is_none")]
698    preprocessing: Option<PreprocessingContext>,
699    compiler: CcCompilerDescriptor,
700    arguments: Vec<String>,
701    environment: BTreeMap<String, Option<String>>,
702    inputs: Vec<CcInputDescriptor>,
703}
704
705#[derive(Debug, Serialize)]
706struct CcInvocationDescriptor {
707    version: u8,
708    kind: &'static str,
709    adapter_version: u8,
710    #[serde(skip_serializing_if = "Option::is_none")]
711    assembly_input_model: Option<u8>,
712    #[serde(skip_serializing_if = "Option::is_none")]
713    preprocessing: Option<PreprocessingContext>,
714    compiler: CcCompilerDescriptor,
715    arguments: Vec<String>,
716    required_inputs: Vec<String>,
717}
718
719/// Preprocessor text embeds literal paths in line markers and macros. Keep
720/// its invocation tied to those spellings instead of rewriting user output.
721#[derive(Debug, Serialize)]
722struct PreprocessingContext {
723    working_dir: PathBuf,
724    arguments: Vec<String>,
725    roots: BTreeMap<String, PathBuf>,
726}
727
728/// Literal path spellings retained by an object, unlike portable debug paths.
729#[derive(Debug, Serialize)]
730struct CcPathBinding {
731    working_dir: PathBuf,
732    arguments: Vec<Argument>,
733    roots: BTreeMap<String, PathBuf>,
734}
735
736/// Normalized input names from the last successful execution of one modeled
737/// compile.
738#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
739#[serde(deny_unknown_fields)]
740pub struct CcInputPrediction {
741    /// The output retained literal paths and requires a path-specific action key.
742    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
743    pub path_specific: bool,
744    /// Prediction schema version.
745    pub version: u8,
746    /// Normalized input paths, including include-manifest entries.
747    pub inputs: Vec<String>,
748    /// Names of environment variables that entered the key.
749    pub environment: Vec<String>,
750    /// Compiler wall time from the successful invocation that produced this
751    /// prediction. Zero means no timing hint was recorded.
752    #[serde(default, skip_serializing_if = "is_zero")]
753    pub compiler_duration_ns: u64,
754    /// Source file name associated with the timing hint.
755    #[serde(default, skip_serializing_if = "String::is_empty")]
756    pub source_name: String,
757}
758
759fn is_zero(value: &u64) -> bool {
760    *value == 0
761}
762
763/// One parsed and admitted argument.
764#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
765enum Argument {
766    /// Keyed verbatim.
767    Plain(String),
768    /// Keyed with its path normalized.
769    Path { flag: String, path: PathBuf },
770    /// A prefix rewrite: the source path normalizes, the replacement does not.
771    PrefixMap {
772        flag: String,
773        from: PathBuf,
774        to: String,
775    },
776    /// The source file.
777    Source(PathBuf),
778}
779
780/// A parsed, admitted C or C++ compile.
781#[derive(Debug, Clone, PartialEq, Eq)]
782pub struct CcInvocation {
783    arguments: Vec<Argument>,
784    source: PathBuf,
785    output: PathBuf,
786    include_dirs: Vec<PathBuf>,
787    required_inputs: Vec<PathBuf>,
788    language: CcLanguage,
789    preprocessed_assembly: bool,
790    preprocessing_arguments: Option<Vec<String>>,
791    sysroot: Option<PathBuf>,
792    caller_depfile: Option<CallerDepfile>,
793    /// Positions in the parsed command line of the dependency-list flags the
794    /// caller passed, which the shim leaves out when it runs the driver.
795    dependency_argument_indices: Vec<usize>,
796}
797
798/// A dependency list the caller asked the driver to write beside the object.
799///
800/// Build systems driving the compiler through a build script -- OpenSSL's
801/// makefiles under `openssl-src`, CMake under many `-sys` crates -- ask for one
802/// with `-MD` or `-MMD` so their own rebuild logic has something to read. The
803/// flags do not change the object, so they are not key material, and the file
804/// names paths on this machine, so it is never stored. The shim writes it
805/// itself from the dependency list it already collects, whether the object
806/// was compiled or restored.
807#[derive(Debug, Clone, PartialEq, Eq)]
808#[non_exhaustive]
809pub struct CallerDepfile {
810    /// Where the list goes: `-MF`, or the object's path with a `.d` extension.
811    pub path: PathBuf,
812    /// The rule's targets, from `-MT` and `-MQ`, or the object as `-o` named it.
813    pub targets: Vec<DepfileTarget>,
814    /// Whether system headers are left out, as `-MMD` does.
815    pub user_headers_only: bool,
816    /// Whether every header also gets an empty rule of its own, as `-MP` does.
817    pub phony_targets: bool,
818}
819
820/// One target of a caller's dependency rule.
821#[derive(Debug, Clone, PartialEq, Eq)]
822pub struct DepfileTarget {
823    /// The target text as the caller gave it.
824    pub name: String,
825    /// Whether characters special to make are quoted on output, as `-MQ`
826    /// asks; `-MT` writes the name literally.
827    pub quoted: bool,
828}
829
830impl CcInvocation {
831    /// The object's path made absolute against the compiler's working
832    /// directory, for reading and storing it. `-o` is often relative --
833    /// OpenSSL's makefiles name every object that way -- and a path handed to
834    /// the cache agent has to mean the same file from the agent's own
835    /// directory. Key material still normalizes the spelling as given.
836    pub fn output_in(&self, working_dir: &Path) -> PathBuf {
837        if self.output.is_absolute() {
838            normalize_components(&self.output)
839        } else {
840            normalize_components(&working_dir.join(&self.output))
841        }
842    }
843
844    /// The dependency list the caller asked for, if any.
845    pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
846        self.caller_depfile.as_ref()
847    }
848
849    /// The command line to run the driver with: `arguments` as parsed, less
850    /// the caller's dependency-list flags. The shim asks the driver for its
851    /// own list, the driver honors only one `-MF`, and the caller's list is
852    /// written by the shim afterwards.
853    pub fn compiler_arguments(&self, arguments: &[OsString]) -> Vec<OsString> {
854        arguments
855            .iter()
856            .enumerate()
857            .filter(|(index, _)| !self.dependency_argument_indices.contains(index))
858            .map(|(_, argument)| argument.clone())
859            .collect()
860    }
861
862    /// Parse a driver command line, admitting modeled single-source compiles
863    /// and preprocessing to a named output.
864    pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
865        Parser::new(arguments).parse()
866    }
867
868    /// Parse a command line using the syntax of `family`.
869    pub fn parse_for(
870        arguments: &[OsString],
871        family: CcCompilerFamily,
872    ) -> Result<Self, CcBypassReason> {
873        if family.is_msvc() {
874            MsvcParser::new(arguments).parse()
875        } else {
876            let mut parser = Parser::new(arguments);
877            parser.family = family;
878            parser.parse()
879        }
880    }
881
882    /// Parse a command line using Microsoft `cl.exe` syntax.
883    pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
884        MsvcParser::new(arguments).parse()
885    }
886
887    /// Source file this invocation compiles.
888    pub fn source(&self) -> &Path {
889        &self.source
890    }
891
892    /// Whether this invocation writes named preprocessor text.
893    pub fn is_preprocessing(&self) -> bool {
894        self.preprocessing_arguments.is_some()
895    }
896
897    /// Output file this invocation produces.
898    pub fn output(&self) -> &Path {
899        &self.output
900    }
901
902    /// Include search directories named on the command line, in order.
903    pub fn include_dirs(&self) -> &[PathBuf] {
904        &self.include_dirs
905    }
906
907    /// Files that must appear among the discovered inputs.
908    pub fn required_inputs(&self) -> &[PathBuf] {
909        &self.required_inputs
910    }
911
912    /// Language the driver compiles.
913    pub fn language(&self) -> CcLanguage {
914        self.language
915    }
916
917    /// Reject assembler-time file directives that the preprocessor's
918    /// dependency list cannot name.
919    ///
920    /// This is a no-op for C and C++ inputs. For preprocessed assembly, every
921    /// file reported by dependency discovery is scanned conservatively before
922    /// an object can be published.
923    pub fn validate_discovered_inputs<'a>(
924        &self,
925        paths: impl IntoIterator<Item = &'a Path>,
926    ) -> Result<(), CcBypassReason> {
927        if !self.preprocessed_assembly {
928            return Ok(());
929        }
930        for path in paths {
931            if depfile::contains_assembler_input_directive(path)? {
932                return Err(CcBypassReason::AssemblerInputDirective(path.to_path_buf()));
933            }
934        }
935        Ok(())
936    }
937
938    /// Sysroot named on the command line, if any.
939    pub fn sysroot(&self) -> Option<&Path> {
940        self.sysroot.as_deref()
941    }
942
943    /// Short label used for timing statistics.
944    pub fn source_name(&self) -> String {
945        self.source
946            .file_name()
947            .map(|name| name.to_string_lossy().into_owned())
948            .unwrap_or_default()
949    }
950
951    /// Arguments to append so the driver writes a dependency list beside the
952    /// object.
953    ///
954    /// `-MD` rather than `-MMD`: system headers are exactly the inputs most
955    /// likely to change without any other key component noticing, because the
956    /// compiler identity does not cover the C library or the platform SDK.
957    pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
958        vec!["-MD".into(), "-MF".into(), depfile.into()]
959    }
960
961    /// Arguments to append so a driver from `family` writes its dependency
962    /// list beside the object.
963    pub fn dependency_arguments_for(
964        &self,
965        depfile: &Path,
966        family: CcCompilerFamily,
967    ) -> Vec<OsString> {
968        if family.is_msvc() {
969            vec!["/sourceDependencies".into(), depfile.into()]
970        } else {
971            self.dependency_arguments(depfile)
972        }
973    }
974
975    /// Arguments to append so `cl.exe` writes `/sourceDependencies` JSON.
976    pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
977        vec!["/sourceDependencies".into(), depfile.into()]
978    }
979
980    /// Digest of the pre-input fingerprint, used to look up a prediction.
981    pub fn invocation_digest(
982        &self,
983        context: &CcActionContext,
984    ) -> Result<CacheDigest, CcBypassReason> {
985        let builder = ActionBuilder::new(self, context.clone());
986        let descriptor = builder.invocation_descriptor()?;
987        let bytes = canonical_json(&descriptor)
988            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
989        Ok(CacheDigest::blake3(&bytes))
990    }
991
992    /// Build the canonical action for this invocation and its discovered
993    /// inputs.
994    pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
995        self.action_with_path_binding(context, false)
996    }
997
998    /// Build an action tied to literal path spellings when the output retained them.
999    /// Portable actions keep their existing keys.
1000    pub fn action_with_path_binding(
1001        &self,
1002        context: CcActionContext,
1003        path_specific: bool,
1004    ) -> Result<CcAction, CcBypassReason> {
1005        ActionBuilder::new(self, context).build(path_specific)
1006    }
1007
1008    /// Record the normalized inputs of a successful compile so the next cold
1009    /// invocation can rebuild the same key before compiling.
1010    pub fn prediction(
1011        &self,
1012        context: &CcActionContext,
1013        compiler_duration_ns: u64,
1014    ) -> Result<CcInputPrediction, CcBypassReason> {
1015        let builder = ActionBuilder::new(self, context.clone());
1016        let mut inputs = context
1017            .inputs
1018            .iter()
1019            .map(|input| builder.normalize_input_path(&input.path))
1020            .collect::<Result<Vec<_>, _>>()?;
1021        inputs.sort();
1022        inputs.dedup();
1023        Ok(CcInputPrediction {
1024            path_specific: false,
1025            version: 1,
1026            inputs,
1027            environment: context.environment.keys().cloned().collect(),
1028            compiler_duration_ns,
1029            source_name: self.source_name(),
1030        })
1031    }
1032}
1033
1034impl CcInputPrediction {
1035    /// Rehash the predicted paths and recompute include manifests. The caller
1036    /// still recomputes the full action digest, so changed inputs are misses.
1037    pub fn discover(
1038        &self,
1039        working_dir: &Path,
1040        path_mappings: &[PathMapping],
1041        digests: &dyn FileDigestCache,
1042    ) -> Result<CcDiscoveredInputs, CcBypassReason> {
1043        if self.version != 1 {
1044            return Err(CcBypassReason::UnsupportedPrediction);
1045        }
1046        if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1047            return Err(CcBypassReason::UnsupportedPrediction);
1048        }
1049        let mappings = PathMapping::ordered(path_mappings);
1050        let mut files = BTreeSet::new();
1051        let mut directories = BTreeSet::new();
1052        for entry in &self.inputs {
1053            match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
1054                Some(directory) => {
1055                    directories.insert(denormalize_path(directory, &mappings)?);
1056                }
1057                None => {
1058                    files.insert(denormalize_path(entry, &mappings)?);
1059                }
1060            }
1061        }
1062        CcDiscoveredInputs::collect(working_dir, files, directories, digests)
1063    }
1064}
1065
1066/// Resolve a normalized key path back to a host path.
1067///
1068/// Placeholder entries expand through their mapping; a verbatim entry is
1069/// accepted only when it still lies beneath an admitted system root, so a
1070/// prediction cannot name an arbitrary absolute path.
1071fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
1072    for mapping in mappings {
1073        let prefix = format!("${{{}}}", mapping.placeholder);
1074        let suffix = if value == prefix {
1075            ""
1076        } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1077            suffix
1078        } else {
1079            continue;
1080        };
1081        if !mapping.root.is_absolute() || !safe_suffix(suffix) {
1082            return Err(CcBypassReason::InvalidPredictedInput(value.into()));
1083        }
1084        let mut path = normalize_components(&mapping.root);
1085        path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1086        return Ok(path);
1087    }
1088    // A verbatim entry names a machine path rather than a placeholder. It is
1089    // admitted only beneath a system root, and only spelled literally: a
1090    // traversal component would let a prediction reach outside that root.
1091    let path = PathBuf::from(value);
1092    if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
1093        return Ok(path);
1094    }
1095    Err(CcBypassReason::InvalidPredictedInput(value.into()))
1096}
1097
1098fn safe_suffix(suffix: &str) -> bool {
1099    suffix.is_empty()
1100        || !suffix.split('/').any(|component| {
1101            component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
1102        })
1103}
1104
1105/// Whether a path lies beneath a root whose location is a machine property.
1106pub fn is_system_path(path: &Path) -> bool {
1107    SYSTEM_ROOTS
1108        .iter()
1109        .any(|root| path.starts_with(Path::new(root)))
1110        || developer_roots().iter().any(|root| path.starts_with(root))
1111}
1112
1113// Xcode may be renamed or installed outside /Applications. Match the active
1114// developer directory rather than assuming the application bundle's name.
1115// Keep both spellings because compiler depfiles may resolve SDK symlinks.
1116#[cfg(target_os = "macos")]
1117fn developer_roots() -> &'static [PathBuf] {
1118    static ROOTS: std::sync::OnceLock<Vec<PathBuf>> = std::sync::OnceLock::new();
1119    ROOTS.get_or_init(|| {
1120        let selected = std::env::var_os("DEVELOPER_DIR")
1121            .filter(|value| !value.is_empty())
1122            .map(PathBuf::from)
1123            .or_else(|| {
1124                let output = std::process::Command::new("/usr/bin/xcode-select")
1125                    .arg("--print-path")
1126                    .output()
1127                    .ok()?;
1128                if !output.status.success() {
1129                    return None;
1130                }
1131                Some(PathBuf::from(
1132                    std::str::from_utf8(&output.stdout).ok()?.trim(),
1133                ))
1134            });
1135        selected.map(developer_directory_roots).unwrap_or_default()
1136    })
1137}
1138
1139#[cfg(not(target_os = "macos"))]
1140fn developer_roots() -> &'static [PathBuf] {
1141    &[]
1142}
1143
1144#[cfg(any(target_os = "macos", all(test, unix)))]
1145fn developer_directory_roots(mut root: PathBuf) -> Vec<PathBuf> {
1146    if !root.is_absolute() || root == Path::new("/") {
1147        return Vec::new();
1148    }
1149    // xcrun also accepts DEVELOPER_DIR pointing at the application bundle.
1150    if root.extension().is_some_and(|extension| extension == "app") {
1151        root = root.join("Contents/Developer");
1152    }
1153    root = normalize_components(&root);
1154    if root == Path::new("/") {
1155        return Vec::new();
1156    }
1157    let mut roots = vec![root.clone()];
1158    if let Ok(canonical) = std::fs::canonicalize(&root)
1159        && canonical != root
1160    {
1161        roots.push(canonical);
1162    }
1163    roots
1164}
1165
1166fn normalize_components(path: &Path) -> PathBuf {
1167    let mut normalized = PathBuf::new();
1168    for component in path.components() {
1169        match component {
1170            Component::CurDir => {}
1171            Component::ParentDir => {
1172                normalized.pop();
1173            }
1174            component => normalized.push(component.as_os_str()),
1175        }
1176    }
1177    normalized
1178}
1179
1180/// Read the modeled environment, rejecting variables that change the compile in
1181/// a way the argv model cannot see.
1182pub fn environment_inputs<F>(
1183    lookup: F,
1184    sysroot: Option<&Path>,
1185) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1186where
1187    F: Fn(&str) -> Option<String>,
1188{
1189    environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
1190}
1191
1192/// Read the modeled environment for a particular compiler family.
1193pub fn environment_inputs_for<F>(
1194    lookup: F,
1195    sysroot: Option<&Path>,
1196    family: CcCompilerFamily,
1197) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1198where
1199    F: Fn(&str) -> Option<String>,
1200{
1201    for name in BYPASS_ENVIRONMENT {
1202        if lookup(name).is_some() {
1203            return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
1204        }
1205    }
1206    let mut environment = BTreeMap::new();
1207    for name in KEYED_ENVIRONMENT {
1208        // An explicit `-isysroot` on the command line already pins the SDK, and
1209        // it is what the driver honors, so the variable stops being an input.
1210        if *name == "SDKROOT" && sysroot.is_some() {
1211            continue;
1212        }
1213        environment.insert((*name).to_string(), lookup(name));
1214    }
1215    if family.is_msvc() {
1216        // INCLUDE changes header resolution without appearing in argv. The
1217        // toolset and SDK versions make the otherwise machine-local paths
1218        // meaningful when action records move between hosts.
1219        for name in [
1220            "INCLUDE",
1221            "VCToolsVersion",
1222            "WindowsSDKVersion",
1223            "UCRTVersion",
1224        ] {
1225            environment.insert(name.into(), lookup(name));
1226        }
1227        for name in ["CL", "_CL_"] {
1228            if lookup(name).is_some() {
1229                return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
1230            }
1231        }
1232    }
1233    Ok(environment)
1234}
1235
1236struct ActionBuilder<'a> {
1237    invocation: &'a CcInvocation,
1238    context: CcActionContext,
1239    mappings: Vec<PathMapping>,
1240    /// Reuse parent resolution across the inputs of this action only.
1241    aliases: PathAliases,
1242}
1243
1244impl<'a> ActionBuilder<'a> {
1245    fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
1246        context.path_mappings = PathMapping::ordered(&context.path_mappings);
1247        let mappings = resolve_path_mappings(&context.path_mappings);
1248        Self {
1249            invocation,
1250            context,
1251            mappings,
1252            aliases: PathAliases::default(),
1253        }
1254    }
1255
1256    fn build(self, path_specific: bool) -> Result<CcAction, CcBypassReason> {
1257        self.validate_mappings()?;
1258        let invocation = self.invocation_descriptor()?;
1259
1260        let mut inputs = BTreeMap::<String, CacheDigest>::new();
1261        for input in &self.context.inputs {
1262            input.digest.validate().map_err(|_| {
1263                CcBypassReason::InvalidInputDigest(input.path.display().to_string())
1264            })?;
1265            let path = self.normalize_input_path(&input.path)?;
1266            if inputs
1267                .insert(path.clone(), input.digest.clone())
1268                .is_some_and(|existing| existing != input.digest)
1269            {
1270                return Err(CcBypassReason::ConflictingInput(path));
1271            }
1272        }
1273        let required = self
1274            .invocation
1275            .required_inputs
1276            .iter()
1277            .map(|path| self.normalize_path(path))
1278            .collect::<Result<BTreeSet<_>, _>>()?;
1279        if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1280            return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
1281        }
1282        let inputs = inputs
1283            .into_iter()
1284            .map(|(path, digest)| CcInputDescriptor { path, digest })
1285            .collect();
1286        let descriptor = CcActionDescriptor {
1287            path_binding: path_specific.then(|| CcPathBinding {
1288                working_dir: self.context.working_dir.clone(),
1289                arguments: self.invocation.arguments.clone(),
1290                roots: self
1291                    .context
1292                    .path_mappings
1293                    .iter()
1294                    .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1295                    .collect(),
1296            }),
1297            version: ACTION_SCHEMA_VERSION,
1298            kind: "cc",
1299            adapter_version: ADAPTER_VERSION,
1300            assembly_input_model: invocation.assembly_input_model,
1301            preprocessing: invocation.preprocessing,
1302            compiler: invocation.compiler,
1303            arguments: invocation.arguments,
1304            environment: self.context.environment.clone(),
1305            inputs,
1306        };
1307        let bytes = canonical_json(&descriptor)
1308            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1309        let digest = CacheDigest::blake3(&bytes);
1310        Ok(CcAction { digest, bytes })
1311    }
1312
1313    fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1314        self.validate_mappings()?;
1315        let arguments = self
1316            .invocation
1317            .arguments
1318            .iter()
1319            .map(|argument| self.normalize_argument(argument))
1320            .collect::<Result<Vec<_>, _>>()?;
1321        let required_inputs = self
1322            .invocation
1323            .required_inputs
1324            .iter()
1325            .map(|path| self.normalize_path(path))
1326            .collect::<Result<BTreeSet<_>, _>>()?
1327            .into_iter()
1328            .collect();
1329        Ok(CcInvocationDescriptor {
1330            version: ACTION_SCHEMA_VERSION,
1331            kind: "cc",
1332            adapter_version: ADAPTER_VERSION,
1333            // Version the assembly-only safety model independently so adding
1334            // support does not invalidate every existing C and C++ entry.
1335            assembly_input_model: self.invocation.preprocessed_assembly.then_some(1),
1336            preprocessing: self
1337                .invocation
1338                .preprocessing_arguments
1339                .as_ref()
1340                .map(|arguments| PreprocessingContext {
1341                    working_dir: self.context.working_dir.clone(),
1342                    arguments: arguments.clone(),
1343                    roots: self
1344                        .mappings
1345                        .iter()
1346                        .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1347                        .collect(),
1348                }),
1349            compiler: self.compiler_descriptor(),
1350            arguments,
1351            required_inputs,
1352        })
1353    }
1354
1355    fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1356        CcCompilerDescriptor {
1357            assembler: self.context.compiler.assembler.clone(),
1358            family: self.context.compiler.family.as_str().into(),
1359            target: self.context.compiler.target.clone(),
1360            version_text: self.context.compiler.version_text.clone(),
1361        }
1362    }
1363
1364    fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1365        if !self.context.working_dir.is_absolute() {
1366            return Err(CcBypassReason::RelativeWorkingDirectory(
1367                self.context.working_dir.clone(),
1368            ));
1369        }
1370        let mut roots = BTreeSet::new();
1371        let mut placeholders = BTreeSet::new();
1372        for mapping in &self.context.path_mappings {
1373            if !mapping.root.is_absolute() {
1374                return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1375            }
1376            if mapping.placeholder.is_empty()
1377                || !mapping
1378                    .placeholder
1379                    .bytes()
1380                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1381                || !roots.insert(normalize_components(&mapping.root))
1382                || !placeholders.insert(&mapping.placeholder)
1383            {
1384                return Err(CcBypassReason::InvalidPathPlaceholder(
1385                    mapping.placeholder.clone(),
1386                ));
1387            }
1388        }
1389        Ok(())
1390    }
1391
1392    fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1393        match argument {
1394            Argument::Plain(value) => Ok(value.clone()),
1395            Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1396            Argument::PrefixMap { flag, from, to } => {
1397                Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1398            }
1399            Argument::Source(path) => Ok(self.normalize_path(path)?),
1400        }
1401    }
1402
1403    /// Normalize a path that names a compilation input or search root.
1404    ///
1405    /// A path beneath a mapped root becomes a placeholder so equivalent
1406    /// checkouts agree. A path beneath a system root stays verbatim: its
1407    /// location is a property of the machine, and its contents are digested
1408    /// like any other input.
1409    fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1410        match normalize_resolved_mapped_path_with(
1411            &self.aliases,
1412            path,
1413            &self.context.working_dir,
1414            &self.mappings,
1415        ) {
1416            Ok(normalized) => Ok(normalized),
1417            Err(reason) => {
1418                let absolute = absolute_path(path, &self.context.working_dir);
1419                if is_system_path(&absolute) {
1420                    return absolute
1421                        .to_str()
1422                        .map(ToOwned::to_owned)
1423                        .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1424                }
1425                Err(reason.into())
1426            }
1427        }
1428    }
1429
1430    fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1431        match path.to_str().and_then(|path| {
1432            path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1433                .map(ToOwned::to_owned)
1434        }) {
1435            Some(directory) => Ok(format!(
1436                "{INCLUDE_MANIFEST_PREFIX}{}",
1437                self.normalize_path(Path::new(&directory))?
1438            )),
1439            None => self.normalize_path(path),
1440        }
1441    }
1442}
1443
1444fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1445    if path.is_absolute() {
1446        normalize_components(path)
1447    } else {
1448        normalize_components(&working_dir.join(path))
1449    }
1450}
1451
1452struct Parser<'a> {
1453    family: CcCompilerFamily,
1454    arguments: &'a [OsString],
1455    index: usize,
1456    parsed: Vec<Argument>,
1457    source: Option<PathBuf>,
1458    output: Option<PathBuf>,
1459    include_dirs: Vec<PathBuf>,
1460    required_inputs: Vec<PathBuf>,
1461    sysroot: Option<PathBuf>,
1462    explicit_language: Option<CcLanguage>,
1463    preprocessed_assembly: bool,
1464    compiling: bool,
1465    preprocessing: bool,
1466    dependency: DependencyRequest,
1467}
1468
1469/// What the caller's `-M` flags asked for, gathered while parsing.
1470#[derive(Debug, Default)]
1471struct DependencyRequest {
1472    /// `Some(false)` for `-MD`, `Some(true)` for `-MMD`.
1473    user_headers_only: Option<bool>,
1474    file: Option<PathBuf>,
1475    targets: Vec<DepfileTarget>,
1476    phony_targets: bool,
1477    /// The last `-MF`, `-MT`, `-MQ`, or `-MP` seen, for the bypass a request
1478    /// without `-MD` or `-MMD` reports.
1479    modifier: Option<String>,
1480    indices: Vec<usize>,
1481}
1482
1483impl<'a> Parser<'a> {
1484    fn new(arguments: &'a [OsString]) -> Self {
1485        Self {
1486            family: CcCompilerFamily::Gcc,
1487            arguments,
1488            index: 0,
1489            parsed: Vec::new(),
1490            source: None,
1491            output: None,
1492            include_dirs: Vec::new(),
1493            required_inputs: Vec::new(),
1494            sysroot: None,
1495            explicit_language: None,
1496            preprocessed_assembly: false,
1497            compiling: false,
1498            preprocessing: false,
1499            dependency: DependencyRequest::default(),
1500        }
1501    }
1502
1503    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1504        while self.index < self.arguments.len() {
1505            let value = self.current()?.to_string();
1506            self.index += 1;
1507            if value == "-" {
1508                return Err(CcBypassReason::StandardInput);
1509            }
1510            if let Some(argfile) = value.strip_prefix('@') {
1511                return Err(CcBypassReason::ResponseFile(argfile.into()));
1512            }
1513            if value.starts_with('-') {
1514                self.parse_flag(&value)?;
1515            } else {
1516                self.parse_input(&value)?;
1517            }
1518        }
1519
1520        if !self.compiling && !self.preprocessing {
1521            return Err(CcBypassReason::NotACompile);
1522        }
1523        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1524        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1525        if self.preprocessing && output == Path::new("-") {
1526            return Err(CcBypassReason::NonObjectOutput("-E -o -".into()));
1527        }
1528        // GCC interprets -o as the dependency output with -E -MD unless -MF
1529        // is explicit. Do not turn that command into a different operation.
1530        if self.preprocessing
1531            && self.dependency.user_headers_only.is_some()
1532            && self.dependency.file.is_none()
1533        {
1534            return Err(CcBypassReason::CallerDependencyFlags(
1535                "-E -MD/-MMD without -MF".into(),
1536            ));
1537        }
1538        if self.preprocessing && self.dependency.file.as_deref() == Some(Path::new("-")) {
1539            return Err(CcBypassReason::CallerDependencyFlags("-E -MF -".into()));
1540        }
1541        let language = self.language(&source)?;
1542        let preprocessed_assembly = self.preprocessed_assembly
1543            || (self.explicit_language.is_none()
1544                && source.extension().and_then(|value| value.to_str()) == Some("S"));
1545        self.required_inputs.push(source.clone());
1546        let caller_depfile = match self.dependency.user_headers_only {
1547            Some(user_headers_only) => Some(CallerDepfile {
1548                path: self
1549                    .dependency
1550                    .file
1551                    .unwrap_or_else(|| output.with_extension("d")),
1552                targets: if self.dependency.targets.is_empty() {
1553                    vec![DepfileTarget {
1554                        // GCC -E ignores -o when choosing the dependency
1555                        // target; Clang retains it. Neither uses the depfile.
1556                        name: if self.preprocessing && self.family == CcCompilerFamily::Gcc {
1557                            Path::new(source.file_name().ok_or(CcBypassReason::MissingInput)?)
1558                                .with_extension("o")
1559                                .to_string_lossy()
1560                                .into_owned()
1561                        } else {
1562                            output.to_string_lossy().into_owned()
1563                        },
1564                        quoted: true,
1565                    }]
1566                } else {
1567                    self.dependency.targets
1568                },
1569                user_headers_only,
1570                phony_targets: self.dependency.phony_targets,
1571            }),
1572            // `-MF` or `-MT` without `-MD` or `-MMD` writes nothing, but it is
1573            // not a shape the cc crate or a makefile produces, so it is not
1574            // one to guess at.
1575            None => match self.dependency.modifier {
1576                Some(modifier) => return Err(CcBypassReason::CallerDependencyFlags(modifier)),
1577                None => None,
1578            },
1579        };
1580        Ok(CcInvocation {
1581            arguments: self.parsed,
1582            source,
1583            output,
1584            include_dirs: self.include_dirs,
1585            required_inputs: self.required_inputs,
1586            language,
1587            preprocessed_assembly,
1588            preprocessing_arguments: self.preprocessing.then(|| {
1589                self.arguments
1590                    .iter()
1591                    .map(|arg| arg.to_str().expect("arguments were validated").to_owned())
1592                    .collect()
1593            }),
1594            sysroot: self.sysroot,
1595            caller_depfile,
1596            dependency_argument_indices: self.dependency.indices,
1597        })
1598    }
1599
1600    /// A `-M` flag: the caller wants a dependency list beside the object.
1601    ///
1602    /// `-MD` and `-MMD` compile as well as listing, so they are taken, with
1603    /// the `-MF`, `-MT`, `-MQ`, and `-MP` that shape the list. None of them
1604    /// enters the key: the object is the same with or without them. `-M` and
1605    /// `-MM` stop after preprocessing, and `-MG` changes what a missing header
1606    /// means, so those still bypass.
1607    fn parse_dependency_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1608        let start = self.index - 1;
1609        match option {
1610            "D" => self.dependency.user_headers_only = Some(false),
1611            "MD" => self.dependency.user_headers_only = Some(true),
1612            "P" => {
1613                self.dependency.phony_targets = true;
1614                self.dependency.modifier = Some(value.into());
1615            }
1616            _ if option.starts_with('F') => {
1617                let file = self.take_value("-MF", Some(&option[1..]))?;
1618                self.dependency.file = Some(PathBuf::from(file));
1619                self.dependency.modifier = Some("-MF".into());
1620            }
1621            _ if option.starts_with('T') || option.starts_with('Q') => {
1622                let flag = &value[..3];
1623                let name = self.take_value(flag, Some(&option[1..]))?;
1624                self.dependency.targets.push(DepfileTarget {
1625                    name,
1626                    quoted: option.starts_with('Q'),
1627                });
1628                self.dependency.modifier = Some(flag.into());
1629            }
1630            _ => return Err(CcBypassReason::CallerDependencyFlags(value.into())),
1631        }
1632        self.dependency.indices.extend(start..self.index);
1633        Ok(())
1634    }
1635
1636    fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1637        if let Some(language) = self.explicit_language {
1638            return Ok(language);
1639        }
1640        let extension = source
1641            .extension()
1642            .and_then(|extension| extension.to_str())
1643            .unwrap_or_default();
1644        match extension {
1645            // Preprocessed assembly goes through the C driver: the preprocessor
1646            // resolves its includes, which is what dependency discovery reads,
1647            // and the assembler that then produces the object is already part
1648            // of a GCC identity. Plain `.s` is not preprocessed and reports no
1649            // dependencies, so it stays out.
1650            "c" | "S" => Ok(CcLanguage::C),
1651            "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1652            _ => Err(CcBypassReason::UnsupportedLanguage(
1653                source.display().to_string(),
1654            )),
1655        }
1656    }
1657
1658    fn current(&self) -> Result<&str, CcBypassReason> {
1659        self.arguments[self.index]
1660            .to_str()
1661            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1662    }
1663
1664    fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1665        if let Some(value) = inline
1666            && !value.is_empty()
1667        {
1668            return Ok(value.into());
1669        }
1670        if self.index >= self.arguments.len() {
1671            return Err(CcBypassReason::MissingValue(flag.into()));
1672        }
1673        let value = self.current()?.to_string();
1674        self.index += 1;
1675        Ok(value)
1676    }
1677
1678    fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1679        if self.source.is_some() {
1680            return Err(CcBypassReason::MultipleInputs);
1681        }
1682        let path = PathBuf::from(value);
1683        // With an explicit `-x`, the driver ignores the extension entirely;
1684        // without one, the extension is the only thing that decides the
1685        // language, so an unmodeled extension has to bypass here.
1686        if self.explicit_language.is_none() {
1687            let extension = path
1688                .extension()
1689                .and_then(|extension| extension.to_str())
1690                .unwrap_or_default();
1691            if !matches!(extension, "c" | "S" | "cc" | "cpp" | "cxx" | "c++") {
1692                return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1693            }
1694        }
1695        self.source = Some(path.clone());
1696        self.parsed.push(Argument::Source(path));
1697        Ok(())
1698    }
1699
1700    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1701        if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1702            return Err(CcBypassReason::CompilerQuery);
1703        }
1704        if value == "-E" {
1705            self.preprocessing = true;
1706            self.parsed.push(Argument::Plain(value.into()));
1707            return Ok(());
1708        }
1709        if value == "-S" {
1710            return Err(CcBypassReason::NonObjectOutput(value.into()));
1711        }
1712        if let Some(option) = value.strip_prefix("-M") {
1713            return self.parse_dependency_flag(value, option);
1714        }
1715        if value.starts_with("-save-temps") {
1716            return Err(CcBypassReason::SaveTemps(value.into()));
1717        }
1718        if value == "--coverage" {
1719            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1720        }
1721        if let Some(options) = value.strip_prefix("-Wa,")
1722            && !options.is_empty()
1723            && options
1724                .split(',')
1725                .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1726        {
1727            self.parsed.push(Argument::Plain(value.into()));
1728            return Ok(());
1729        }
1730        if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1731            || value.starts_with("-Wp,")
1732            || value.starts_with("-Wa,")
1733            || value.starts_with("-Wl,")
1734        {
1735            // The forwarded options are the compilation's real inputs and they
1736            // are not modeled, so consuming the value would not make this safe.
1737            return Err(CcBypassReason::ToolPassthrough(value.into()));
1738        }
1739        if value.starts_with("-include-pch") || value == "-emit-pch" {
1740            return Err(CcBypassReason::PrecompiledHeader(value.into()));
1741        }
1742
1743        if value == "-c" {
1744            self.compiling = true;
1745            self.parsed.push(Argument::Plain(value.into()));
1746            return Ok(());
1747        }
1748        if SUPPORTED_BARE_FLAGS.contains(&value)
1749            || SUPPORTED_O_FLAGS.contains(&value)
1750            || SUPPORTED_G_FLAGS.contains(&value)
1751            || value.starts_with("-std=")
1752        {
1753            self.parsed.push(Argument::Plain(value.into()));
1754            return Ok(());
1755        }
1756        if let Some(rest) = value.strip_prefix("-o") {
1757            let path = self.take_value("-o", Some(rest))?;
1758            // A repeated `-o` follows the driver: the last one names the file
1759            // that is produced. Every occurrence still enters the key.
1760            self.output = Some(PathBuf::from(&path));
1761            self.parsed.push(Argument::Path {
1762                flag: "-o".into(),
1763                path: PathBuf::from(path),
1764            });
1765            return Ok(());
1766        }
1767        if let Some(rest) = value.strip_prefix("-I") {
1768            let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1769            self.include_dirs.push(path.clone());
1770            self.parsed.push(Argument::Path {
1771                flag: "-I".into(),
1772                path,
1773            });
1774            return Ok(());
1775        }
1776        if SEPARATE_PATH_FLAGS.contains(&value) {
1777            let path = PathBuf::from(self.take_value(value, None)?);
1778            match value {
1779                "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1780                "-isysroot" => self.sysroot = Some(path.clone()),
1781                // `-include` and `-imacros` are deliberately not required
1782                // inputs. The driver resolves the name through the include
1783                // chain, so the file need not exist relative to the working
1784                // directory, and the dependency list names it at whatever path
1785                // it was actually found at.
1786                _ => {}
1787            }
1788            self.parsed.push(Argument::Path {
1789                flag: value.into(),
1790                path,
1791            });
1792            return Ok(());
1793        }
1794        // `--include=<file>` is the long spelling of `-include <file>`; the
1795        // `cc` crate emits it for prefixed headers.
1796        if let Some(rest) = value.strip_prefix("--include=") {
1797            let path = PathBuf::from(rest);
1798            self.parsed.push(Argument::Path {
1799                flag: "-include".into(),
1800                path,
1801            });
1802            return Ok(());
1803        }
1804        if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1805            value
1806                .strip_prefix(&format!("{flag}="))
1807                .map(|rest| (*flag, rest))
1808        }) {
1809            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1810            self.parsed.push(Argument::PrefixMap {
1811                flag: flag.into(),
1812                from: PathBuf::from(from),
1813                to: to.into(),
1814            });
1815            return Ok(());
1816        }
1817        // `--param name=value` tunes the optimizer; its text fully describes it.
1818        if value == "--param" {
1819            let parameter = self.take_value("--param", None)?;
1820            self.parsed
1821                .push(Argument::Plain(format!("--param={parameter}")));
1822            return Ok(());
1823        }
1824        if let Some(parameter) = value.strip_prefix("--param=") {
1825            self.parsed
1826                .push(Argument::Plain(format!("--param={parameter}")));
1827            return Ok(());
1828        }
1829        if let Some(rest) = value.strip_prefix("--sysroot=") {
1830            let path = PathBuf::from(rest);
1831            self.sysroot = Some(path.clone());
1832            self.parsed.push(Argument::Path {
1833                flag: "--sysroot".into(),
1834                path,
1835            });
1836            return Ok(());
1837        }
1838        if let Some(rest) = value
1839            .strip_prefix("-D")
1840            .or_else(|| value.strip_prefix("-U"))
1841        {
1842            let flag = &value[..2];
1843            let definition = self.take_value(flag, Some(rest))?;
1844            self.parsed
1845                .push(Argument::Plain(format!("{flag}{definition}")));
1846            return Ok(());
1847        }
1848        if let Some(rest) = value.strip_prefix("-x") {
1849            let language = self.take_value("-x", Some(rest))?;
1850            self.explicit_language = Some(match language.as_str() {
1851                "c" => CcLanguage::C,
1852                "assembler-with-cpp" => {
1853                    self.preprocessed_assembly = true;
1854                    CcLanguage::C
1855                }
1856                "c++" => CcLanguage::Cxx,
1857                other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1858            });
1859            self.parsed.push(Argument::Plain(format!("-x{language}")));
1860            return Ok(());
1861        }
1862        if let Some(target) = value.strip_prefix("--target=") {
1863            self.parsed
1864                .push(Argument::Plain(format!("--target={target}")));
1865            return Ok(());
1866        }
1867        if value == "-target" {
1868            let target = self.take_value("-target", None)?;
1869            self.parsed
1870                .push(Argument::Plain(format!("--target={target}")));
1871            return Ok(());
1872        }
1873        if value == "-arch" {
1874            let arch = self.take_value("-arch", None)?;
1875            self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1876            return Ok(());
1877        }
1878        if let Some(option) = value.strip_prefix("-f") {
1879            return self.parse_f_flag(value, option);
1880        }
1881        if let Some(option) = value.strip_prefix("-m") {
1882            return self.parse_m_flag(value, option);
1883        }
1884        if value.starts_with("-g") {
1885            // `-gsplit-dwarf` writes a `.dwo` beside the object; every other
1886            // unlisted `-g` spelling is simply unmodeled.
1887            return Err(if value.starts_with("-gsplit-dwarf") {
1888                CcBypassReason::SplitDebugOutput(value.into())
1889            } else {
1890                CcBypassReason::UnknownFlag(value.into())
1891            });
1892        }
1893        if value.starts_with("-W") {
1894            // Warning selection changes only diagnostics, which are replayed
1895            // from the cache, and the exit status, and only successful
1896            // compiles are ever published.
1897            self.parsed.push(Argument::Plain(value.into()));
1898            return Ok(());
1899        }
1900        Err(CcBypassReason::UnknownFlag(value.into()))
1901    }
1902
1903    fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1904        if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1905            return Err(CcBypassReason::Plugin(value.into()));
1906        }
1907        if option.starts_with("profile-") || option == "test-coverage" {
1908            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1909        }
1910        let name = option.split_once('=').map_or(option, |(name, _)| name);
1911        if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1912            return Err(CcBypassReason::UnknownFlag(value.into()));
1913        }
1914        self.parsed.push(Argument::Plain(value.into()));
1915        Ok(())
1916    }
1917
1918    fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1919        if option == "llvm" {
1920            return Err(CcBypassReason::ToolPassthrough(value.into()));
1921        }
1922        // `-march=native` and its relatives resolve against whatever CPU this
1923        // machine has. The resulting object is not a function of the key, so
1924        // another machine could otherwise restore code its processor cannot
1925        // run.
1926        if let Some((name, selection)) = option.split_once('=')
1927            && matches!(name, "arch" | "cpu" | "tune")
1928            && matches!(selection, "native" | "host")
1929        {
1930            return Err(CcBypassReason::LocalCpuTarget(value.into()));
1931        }
1932        let name = option.split_once('=').map_or(option, |(name, _)| name);
1933        if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1934            return Err(CcBypassReason::UnknownFlag(value.into()));
1935        }
1936        self.parsed.push(Argument::Plain(value.into()));
1937        Ok(())
1938    }
1939}
1940
1941/// Conservative parser for the command lines emitted by the `cc` crate for
1942/// Microsoft's compiler. It intentionally admits only flags whose effects are
1943/// either present in argv or covered by dependency discovery.
1944struct MsvcParser<'a> {
1945    arguments: &'a [OsString],
1946    index: usize,
1947    parsed: Vec<Argument>,
1948    source: Option<PathBuf>,
1949    output: Option<PathBuf>,
1950    include_dirs: Vec<PathBuf>,
1951    required_inputs: Vec<PathBuf>,
1952    explicit_language: Option<CcLanguage>,
1953    compiling: bool,
1954}
1955
1956impl<'a> MsvcParser<'a> {
1957    fn new(arguments: &'a [OsString]) -> Self {
1958        Self {
1959            arguments,
1960            index: 0,
1961            parsed: Vec::new(),
1962            source: None,
1963            output: None,
1964            include_dirs: Vec::new(),
1965            required_inputs: Vec::new(),
1966            explicit_language: None,
1967            compiling: false,
1968        }
1969    }
1970
1971    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1972        while self.index < self.arguments.len() {
1973            let value = self.current()?.to_owned();
1974            self.index += 1;
1975            if let Some(file) = value.strip_prefix('@') {
1976                return Err(CcBypassReason::ResponseFile(file.into()));
1977            }
1978            if value.starts_with('/') || value.starts_with('-') {
1979                self.parse_flag(&value)?;
1980            } else {
1981                self.add_source(&value)?;
1982            }
1983        }
1984        if !self.compiling {
1985            return Err(CcBypassReason::NotACompile);
1986        }
1987        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1988        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1989        let language = self.explicit_language.unwrap_or_else(|| {
1990            if source
1991                .extension()
1992                .and_then(|value| value.to_str())
1993                .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1994            {
1995                CcLanguage::C
1996            } else {
1997                CcLanguage::Cxx
1998            }
1999        });
2000        self.required_inputs.push(source.clone());
2001        Ok(CcInvocation {
2002            arguments: self.parsed,
2003            source,
2004            output,
2005            include_dirs: self.include_dirs,
2006            required_inputs: self.required_inputs,
2007            language,
2008            preprocessed_assembly: false,
2009            preprocessing_arguments: None,
2010            sysroot: None,
2011            caller_depfile: None,
2012            dependency_argument_indices: Vec::new(),
2013        })
2014    }
2015
2016    fn current(&self) -> Result<&str, CcBypassReason> {
2017        self.arguments[self.index]
2018            .to_str()
2019            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
2020    }
2021
2022    fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
2023        if !attached.is_empty() {
2024            return Ok(attached.into());
2025        }
2026        if self.index == self.arguments.len() {
2027            return Err(CcBypassReason::MissingValue(flag.into()));
2028        }
2029        let value = self.current()?.to_owned();
2030        self.index += 1;
2031        Ok(value)
2032    }
2033
2034    fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
2035        if self.source.is_some() {
2036            return Err(CcBypassReason::MultipleInputs);
2037        }
2038        let path = PathBuf::from(value);
2039        if self.explicit_language.is_none()
2040            && !path
2041                .extension()
2042                .and_then(|value| value.to_str())
2043                .is_some_and(|value| {
2044                    matches!(
2045                        value.to_ascii_lowercase().as_str(),
2046                        "c" | "cc" | "cpp" | "cxx"
2047                    )
2048                })
2049        {
2050            return Err(CcBypassReason::UnsupportedLanguage(value.into()));
2051        }
2052        self.source = Some(path.clone());
2053        self.parsed.push(Argument::Source(path));
2054        Ok(())
2055    }
2056
2057    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
2058        let option = value.trim_start_matches(['/', '-']);
2059        let lower = option.to_ascii_lowercase();
2060        if matches!(lower.as_str(), "?" | "help") {
2061            return Err(CcBypassReason::CompilerQuery);
2062        }
2063        if lower == "showincludes" || lower.starts_with("sourcedependencies") {
2064            return Err(CcBypassReason::CallerDependencyFlags(value.into()));
2065        }
2066        if matches!(lower.as_str(), "e" | "ep" | "p") {
2067            return Err(CcBypassReason::NonObjectOutput(value.into()));
2068        }
2069        if (lower.starts_with("fa") && !lower.starts_with("favor:"))
2070            || lower.starts_with("fd")
2071            || lower.starts_with("zi")
2072        {
2073            return Err(CcBypassReason::SplitDebugOutput(value.into()));
2074        }
2075        if lower.starts_with("yc")
2076            || lower.starts_with("yu")
2077            || (lower.starts_with("fp") && !lower.starts_with("fp:"))
2078        {
2079            return Err(CcBypassReason::PrecompiledHeader(value.into()));
2080        }
2081        if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
2082            return Err(CcBypassReason::ToolPassthrough(value.into()));
2083        }
2084        if matches!(lower.as_str(), "ld" | "ldd") {
2085            return Err(CcBypassReason::NotACompile);
2086        }
2087        if lower == "c" {
2088            self.compiling = true;
2089            self.parsed.push(Argument::Plain("/c".into()));
2090            return Ok(());
2091        }
2092        for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
2093            if let Some(attached) = option.strip_prefix(prefix) {
2094                let path = PathBuf::from(self.value(canonical, attached)?);
2095                if prefix == "Fo" {
2096                    self.output = Some(path.clone());
2097                } else if prefix == "I" {
2098                    self.include_dirs.push(path.clone());
2099                }
2100                self.parsed.push(Argument::Path {
2101                    flag: canonical.into(),
2102                    path,
2103                });
2104                return Ok(());
2105            }
2106        }
2107        if lower.starts_with("external:i") {
2108            let path = PathBuf::from(self.value("/external:I", &option[10..])?);
2109            self.include_dirs.push(path.clone());
2110            self.parsed.push(Argument::Path {
2111                flag: "/external:I".into(),
2112                path,
2113            });
2114            return Ok(());
2115        }
2116        if option.starts_with("Tc") || option.starts_with("Tp") {
2117            let c = option.starts_with("Tc");
2118            let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
2119            self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
2120            return self.add_source(&path);
2121        }
2122        if lower.starts_with("pathmap:") {
2123            let rest = &option[8..];
2124            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
2125            self.parsed.push(Argument::PrefixMap {
2126                flag: "/pathmap".into(),
2127                from: PathBuf::from(from),
2128                to: to.into(),
2129            });
2130            return Ok(());
2131        }
2132        if matches!(option, "D" | "U") {
2133            let definition = self.value(value, "")?;
2134            self.parsed
2135                .push(Argument::Plain(format!("/{option}{definition}")));
2136            return Ok(());
2137        }
2138        // Definitions and the ordinary code-generation/diagnostic switches
2139        // produced by cc-rs are self-contained text and can be keyed verbatim.
2140        let definition = option.starts_with('D') || option.starts_with('U');
2141        let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
2142            || lower
2143                .strip_prefix('w')
2144                .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2145            || ["wd", "we", "wo"].iter().any(|prefix| {
2146                lower
2147                    .strip_prefix(prefix)
2148                    .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2149            });
2150        let admitted = definition
2151            || warning
2152            || lower.starts_with("std:")
2153            || lower.starts_with("arch:")
2154            || lower.starts_with("favor:")
2155            || lower.starts_with("volatile:")
2156            || lower.starts_with("fp:")
2157            || lower.starts_with("eh")
2158            || lower.starts_with('o')
2159            || lower.starts_with("ob")
2160            || lower.starts_with("oi")
2161            || lower.starts_with("ot")
2162            || lower.starts_with("oy")
2163            || lower.starts_with("gs")
2164            || lower.starts_with("gr")
2165            || lower.starts_with("gy")
2166            || lower.starts_with("gw")
2167            || lower.starts_with("gl")
2168            || lower.starts_with("zc:")
2169            || lower.starts_with("diagnostics:")
2170            || matches!(
2171                lower.as_str(),
2172                "nologo"
2173                    | "brepro"
2174                    | "bigobj"
2175                    | "utf-8"
2176                    | "permissive-"
2177                    | "z7"
2178                    | "md"
2179                    | "mdd"
2180                    | "mt"
2181                    | "mtd"
2182            );
2183        if admitted {
2184            self.parsed.push(Argument::Plain(value.into()));
2185            return Ok(());
2186        }
2187        Err(CcBypassReason::UnknownFlag(value.into()))
2188    }
2189}
2190
2191#[cfg(test)]
2192#[path = "cc_cache_tests.rs"]
2193mod tests;