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