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` -- and rejects everything else. 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, PathMapping, PathNormalizationError, canonical_json,
22    normalize_mapped_path,
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];
243
244const SUPPORTED_BARE_FLAGS: &[&str] = &[
245    "-ansi",
246    "-nostdinc",
247    "-nostdinc++",
248    "-pedantic",
249    "-pedantic-errors",
250    "-pipe",
251    "-pthread",
252    "-w",
253];
254
255const SEPARATE_PATH_FLAGS: &[&str] = &[
256    "-idirafter",
257    "-imacros",
258    "-include",
259    "-iquote",
260    "-isysroot",
261    "-isystem",
262];
263
264const TOOL_PASSTHROUGH_FLAGS: &[&str] = &["-Xassembler", "-Xclang", "-Xlinker", "-Xpreprocessor"];
265
266/// Assembler options whose effects are completely described by their text.
267///
268/// Other assembler options can name files that dependency discovery does not
269/// report, so they remain conservative passthrough bypasses.
270const SUPPORTED_ASSEMBLER_OPTIONS: &[&str] = &["--noexecstack"];
271
272const COMPILER_QUERY_FLAGS: &[&str] = &[
273    "--help",
274    "--version",
275    "-###",
276    // The `cc` crate probes with `-?` to tell an MSVC-style driver from a
277    // gcc-style one; neither answer is a compilation.
278    "-?",
279    "-dumpmachine",
280    "-dumpversion",
281    "-v",
282];
283
284/// Flags that rewrite a path prefix in the compiler's own output.
285///
286/// The left side is a real path and normalizes like any other; the right side
287/// is the text it is replaced with and enters the key verbatim.
288const PREFIX_MAP_FLAGS: &[&str] = &[
289    "-fdebug-prefix-map",
290    "-ffile-prefix-map",
291    "-fmacro-prefix-map",
292];
293
294impl CcBypassReason {
295    /// A stable, low-cardinality name for this reason.
296    ///
297    /// Many variants carry a path or a flag, so `Display` text cannot be
298    /// aggregated; statistics group by this instead.
299    pub fn kind(&self) -> &'static str {
300        self.into()
301    }
302
303    /// A concrete change that can make this invocation cacheable, when one is
304    /// available.
305    ///
306    /// Expected compiler probes and failures that require adapter support
307    /// return `None`; callers can still explain those from
308    /// [`CcBypassReason::kind`].
309    pub fn remediation(&self) -> Option<&'static str> {
310        match self {
311            Self::UnsupportedEnvironment(_) => Some(
312                "Unset the reported environment variable for this build so the compiler invocation describes all of its inputs.",
313            ),
314            Self::LocalCpuTarget(_) => Some(
315                "Replace the reported local-CPU option with an explicit architecture or CPU name.",
316            ),
317            Self::EmbeddedTimestampMacro(_) => Some(
318                "Remove the reported timestamp macro, or keep this compilation uncached if its changing value is intentional.",
319            ),
320            Self::SearchPathModifiedDuringCompilation(_) => Some(
321                "Generate headers before compilation instead of changing an include directory while the compiler is running.",
322            ),
323            Self::UnknownFlag(_) | Self::ToolPassthrough(_) => Some(
324                "Upgrade mbx or report the unmodeled compiler option. If you control the build script, removing the option can also make the compilation cacheable.",
325            ),
326            Self::UnmappedAbsolutePath(_) => Some(
327                "Move the input under a mapped project or system root, or keep this compilation uncached.",
328            ),
329            _ => None,
330        }
331    }
332}
333
334/// Reason a C or C++ invocation cannot safely use the action cache.
335///
336/// A bypass is an expected conservative outcome, not a compiler error. Match on
337/// [`CcBypassReason::kind`] for aggregation rather than on the variants.
338#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
339#[strum(serialize_all = "kebab-case")]
340#[non_exhaustive]
341pub enum CcBypassReason {
342    /// An argument cannot be represented in the canonical UTF-8 key.
343    #[error("compiler argument {index} is not valid UTF-8")]
344    NonUtf8Argument {
345        /// Zero-based index in the argument slice.
346        index: usize,
347    },
348    /// The driver was handed an argument file.
349    #[error("compiler response file is not modeled by the cache adapter: {0}")]
350    ResponseFile(String),
351    /// A compiler flag is not modeled by this adapter version.
352    #[error("compiler flag is not modeled by the cache adapter: {0}")]
353    UnknownFlag(String),
354    /// A recognized flag was given without its value.
355    #[error("compiler flag {0} is missing its value")]
356    MissingValue(String),
357    /// The invocation asks the driver about itself rather than compiling.
358    #[error("compiler invocation queries the driver instead of compiling")]
359    CompilerQuery,
360    /// The invocation is not a single-object compile.
361    #[error("compiler invocation does not compile with -c")]
362    NotACompile,
363    /// The invocation emits preprocessed source or assembly.
364    #[error("compiler invocation emits a non-object output: {0}")]
365    NonObjectOutput(String),
366    /// The source arrives on standard input and cannot be rediscovered.
367    #[error("compiler invocation reads its source from standard input")]
368    StandardInput,
369    /// No source file was given.
370    #[error("compiler invocation names no source file")]
371    MissingInput,
372    /// More than one source file was given.
373    #[error("compiler invocation names more than one source file")]
374    MultipleInputs,
375    /// No `-o` was given, so the object name follows driver defaults.
376    #[error("compiler invocation names no output file")]
377    MissingOutput,
378    /// The source language is outside the modeled set.
379    #[error("compiler input language is not modeled by the cache adapter: {0}")]
380    UnsupportedLanguage(String),
381    /// The caller asked for its own dependency output.
382    #[error("compiler invocation requests its own dependency output: {0}")]
383    CallerDependencyFlags(String),
384    /// Precompiled headers are not byte-hermetic key material.
385    #[error("precompiled headers are not modeled by the cache adapter: {0}")]
386    PrecompiledHeader(String),
387    /// Coverage instrumentation writes outputs beside the object.
388    #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
389    CoverageInstrumentation(String),
390    /// Split debug info writes a `.dwo` beside the object.
391    #[error("split debug output is not modeled by the cache adapter: {0}")]
392    SplitDebugOutput(String),
393    /// Temporary files are preserved beside the object.
394    #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
395    SaveTemps(String),
396    /// An option is smuggled to a sub-tool the adapter cannot model.
397    #[error("compiler flag forwards options to another tool: {0}")]
398    ToolPassthrough(String),
399    /// A compiler plugin makes the output depend on unmodeled code.
400    #[error("compiler plugins are not modeled by the cache adapter: {0}")]
401    Plugin(String),
402    /// An include search directory gained or lost a header while the compiler
403    /// ran, so the manifest recorded after it is not what the compilation saw.
404    #[error("include search directory changed during the compilation: {0}")]
405    SearchPathModifiedDuringCompilation(PathBuf),
406
407    /// The object kept a path the key normalized away.
408    ///
409    /// Remapping covers what the compiler records itself; a path the source
410    /// keeps as a string survives it, and publishing such an object would
411    /// share this checkout's directory under a key that says it does not
412    /// matter.
413    #[error("compilation output records a path its key normalized away: {0}")]
414    UnportableOutput(PathBuf),
415    /// The object depends on the machine's own CPU rather than on named inputs.
416    #[error("compiler flag tunes for the local CPU: {0}")]
417    LocalCpuTarget(String),
418    /// The driver is not a gcc-style or clang-style compiler.
419    #[error("compiler driver is not modeled by the cache adapter: {0}")]
420    UnsupportedCompilerDriver(String),
421    /// The identity probe could not be run or parsed.
422    #[error("could not establish compiler identity: {0}")]
423    CompilerIdentityUnavailable(String),
424    /// An environment variable outside the modeled set is set.
425    #[error("environment variable {0} changes the compilation in an unmodeled way")]
426    UnsupportedEnvironment(String),
427    /// The shim could not be told which real compiler to run.
428    #[error("no real compiler was pinned for the cc shim")]
429    RealCompilerUnpinned,
430    /// A read file expands a timestamp macro, so the object is not a function
431    /// of its inputs.
432    #[error("input expands a timestamp macro: {0}")]
433    EmbeddedTimestampMacro(PathBuf),
434    /// Preprocessed assembly names an input that compiler dependency output
435    /// does not report.
436    #[error("preprocessed assembly uses an assembler input directive in {0}")]
437    AssemblerInputDirective(PathBuf),
438    /// The injected depfile could not be parsed exactly.
439    #[error("could not model the compiler depfile: {0}")]
440    MalformedDepfile(String),
441    /// The injected depfile could not be read.
442    #[error("could not read the compiler depfile {path}: {message}")]
443    DepfileRead {
444        /// Depfile that could not be read.
445        path: PathBuf,
446        /// Underlying error text.
447        message: String,
448    },
449    /// The action exceeds an input, byte, or manifest bound.
450    #[error("compilation reads more inputs than the cache adapter models")]
451    TooManyInputs,
452    /// An absolute path lies outside every mapped and system root.
453    #[error("path is outside every modeled root: {0}")]
454    UnmappedAbsolutePath(PathBuf),
455    /// A path cannot be represented in the canonical UTF-8 key.
456    #[error("path is not valid UTF-8: {0}")]
457    NonUtf8Path(PathBuf),
458    /// The compiler working directory is not absolute.
459    #[error("compiler working directory is not absolute: {0}")]
460    RelativeWorkingDirectory(PathBuf),
461    /// A configured path mapping root is not absolute.
462    #[error("path mapping root is not absolute: {0}")]
463    RelativePathMapping(PathBuf),
464    /// A configured placeholder is empty, duplicated, or not a bare name.
465    #[error("invalid path mapping placeholder: {0}")]
466    InvalidPathPlaceholder(String),
467    /// A required input never appeared among the discovered inputs.
468    #[error("required input is missing from the discovered inputs: {0}")]
469    MissingRequiredInput(String),
470    /// An input digest is malformed.
471    #[error("invalid digest for input: {0}")]
472    InvalidInputDigest(String),
473    /// One normalized path carries two different digests.
474    #[error("conflicting digests for input: {0}")]
475    ConflictingInput(String),
476    /// An input could not be read.
477    #[error("could not read input {path}: {message}")]
478    InputRead {
479        /// Input that could not be read.
480        path: PathBuf,
481        /// Underlying error text.
482        message: String,
483    },
484    /// An input changed between discovery and publication.
485    #[error("input changed during the compilation: {0}")]
486    InputChanged(PathBuf),
487    /// An input was written while the compiler ran.
488    #[error("input was modified during the compilation: {0}")]
489    InputModifiedDuringCompilation(PathBuf),
490    /// Discovery and the action disagree about the working directory.
491    #[error("discovered inputs use a different working directory")]
492    DiscoveryWorkingDirectory,
493    /// A prediction uses a schema this adapter version does not model.
494    #[error("action prediction is not modeled by this adapter version")]
495    UnsupportedPrediction,
496    /// A predicted input name cannot be resolved back to a host path.
497    #[error("invalid predicted input: {0}")]
498    InvalidPredictedInput(String),
499    /// Canonical serialization failed.
500    #[error("could not serialize the action descriptor: {0}")]
501    Serialization(String),
502}
503
504impl From<PathNormalizationError> for CcBypassReason {
505    fn from(reason: PathNormalizationError) -> Self {
506        match reason {
507            PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
508            PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
509        }
510    }
511}
512
513/// Source language a driver invocation compiles.
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub enum CcLanguage {
516    /// C, driven through `CC`.
517    C,
518    /// C++, driven through `CXX`.
519    Cxx,
520}
521
522impl CcLanguage {
523    /// Shim file stem that selects this language.
524    pub fn shim_stem(self) -> &'static str {
525        match self {
526            Self::C => "mbx-cc",
527            Self::Cxx => "mbx-cxx",
528        }
529    }
530
531    /// Default driver name to fall back to when no real compiler is pinned.
532    pub fn default_driver(self) -> &'static str {
533        if cfg!(windows) {
534            return "cl.exe";
535        }
536        match self {
537            Self::C => "cc",
538            Self::Cxx => "c++",
539        }
540    }
541}
542
543/// Compiler family, which decides how the identity is assembled.
544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
545pub enum CcCompilerFamily {
546    /// GCC, which compiles objects through an external assembler.
547    Gcc,
548    /// Upstream LLVM clang.
549    Clang,
550    /// Apple's clang distribution.
551    AppleClang,
552    /// Microsoft's `cl.exe` driver.
553    #[cfg(windows)]
554    Msvc,
555}
556
557impl CcCompilerFamily {
558    /// Stable name recorded in the action key.
559    pub fn as_str(self) -> &'static str {
560        match self {
561            Self::Gcc => "gcc",
562            Self::Clang => "clang",
563            Self::AppleClang => "apple-clang",
564            #[cfg(windows)]
565            Self::Msvc => "msvc",
566        }
567    }
568
569    /// Whether objects are produced through a separate assembler binary whose
570    /// version therefore belongs in the identity.
571    pub fn uses_external_assembler(self) -> bool {
572        matches!(self, Self::Gcc)
573    }
574
575    /// Whether this is Microsoft's `cl.exe` driver.
576    pub fn is_msvc(self) -> bool {
577        #[cfg(windows)]
578        {
579            matches!(self, Self::Msvc)
580        }
581        #[cfg(not(windows))]
582        {
583            false
584        }
585    }
586
587    /// Classify a driver from its verbose probe output.
588    pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
589        #[cfg(windows)]
590        if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
591            return Ok(Self::Msvc);
592        }
593        if probe.contains("Apple clang version") {
594            Ok(Self::AppleClang)
595        } else if probe.contains("clang version") {
596            Ok(Self::Clang)
597        } else if probe.contains("gcc version") {
598            Ok(Self::Gcc)
599        } else {
600            Err(CcBypassReason::UnsupportedCompilerDriver(
601                probe.lines().next().unwrap_or_default().into(),
602            ))
603        }
604    }
605}
606
607/// Compiler properties that distinguish incompatible objects.
608#[derive(Debug, Clone, PartialEq, Eq)]
609pub struct CcCompilerIdentity {
610    /// Driver family.
611    pub family: CcCompilerFamily,
612    /// Complete verbose probe output, verbatim.
613    pub version_text: String,
614    /// Target triple the driver reports.
615    pub target: String,
616    /// Resolved assembler and its version, for families that use one.
617    ///
618    /// GCC assembles through binutils, whose version changes object bytes
619    /// without changing anything `gcc -v` prints. Clang assembles internally,
620    /// so this is empty there.
621    pub assembler: String,
622}
623
624/// One file input paired with the digest used in the action key.
625#[derive(Debug, Clone, PartialEq, Eq)]
626pub struct CcActionInput {
627    /// Absolute host path used to read and verify the input, or an
628    /// include-manifest pseudo-path.
629    pub path: PathBuf,
630    /// Digest of the input contents, or of the directory's name manifest.
631    pub digest: CacheDigest,
632}
633
634/// External information needed to construct a canonical cc action.
635#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct CcActionContext {
637    /// Identity of the compiler that produces the object.
638    pub compiler: CcCompilerIdentity,
639    /// Absolute directory in which the compiler runs.
640    pub working_dir: PathBuf,
641    /// Host roots replaced with stable placeholders in the key.
642    pub path_mappings: Vec<PathMapping>,
643    /// Environment inputs and their observed values.
644    pub environment: BTreeMap<String, Option<String>>,
645    /// Complete set of direct and discovered file inputs.
646    pub inputs: Vec<CcActionInput>,
647}
648
649/// Canonical action descriptor and its content digest.
650#[derive(Debug, Clone, PartialEq, Eq)]
651pub struct CcAction {
652    /// Digest of `bytes`, used as the action-cache key.
653    pub digest: CacheDigest,
654    /// Canonical serialized action descriptor.
655    pub bytes: Vec<u8>,
656}
657
658#[derive(Debug, Serialize)]
659struct CcCompilerDescriptor {
660    assembler: String,
661    family: String,
662    target: String,
663    version_text: String,
664}
665
666#[derive(Debug, Serialize)]
667struct CcInputDescriptor {
668    digest: CacheDigest,
669    path: String,
670}
671
672#[derive(Debug, Serialize)]
673struct CcActionDescriptor {
674    version: u8,
675    kind: &'static str,
676    adapter_version: u8,
677    #[serde(skip_serializing_if = "Option::is_none")]
678    assembly_input_model: Option<u8>,
679    compiler: CcCompilerDescriptor,
680    arguments: Vec<String>,
681    environment: BTreeMap<String, Option<String>>,
682    inputs: Vec<CcInputDescriptor>,
683}
684
685#[derive(Debug, Serialize)]
686struct CcInvocationDescriptor {
687    version: u8,
688    kind: &'static str,
689    adapter_version: u8,
690    #[serde(skip_serializing_if = "Option::is_none")]
691    assembly_input_model: Option<u8>,
692    compiler: CcCompilerDescriptor,
693    arguments: Vec<String>,
694    required_inputs: Vec<String>,
695}
696
697/// Normalized input names from the last successful execution of one modeled
698/// compile.
699#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
700#[serde(deny_unknown_fields)]
701pub struct CcInputPrediction {
702    /// Prediction schema version.
703    pub version: u8,
704    /// Normalized input paths, including include-manifest entries.
705    pub inputs: Vec<String>,
706    /// Names of environment variables that entered the key.
707    pub environment: Vec<String>,
708    /// Compiler wall time from the successful invocation that produced this
709    /// prediction. Zero means no timing hint was recorded.
710    #[serde(default, skip_serializing_if = "is_zero")]
711    pub compiler_duration_ns: u64,
712    /// Source file name associated with the timing hint.
713    #[serde(default, skip_serializing_if = "String::is_empty")]
714    pub source_name: String,
715}
716
717fn is_zero(value: &u64) -> bool {
718    *value == 0
719}
720
721/// One parsed and admitted argument.
722#[derive(Debug, Clone, PartialEq, Eq)]
723enum Argument {
724    /// Keyed verbatim.
725    Plain(String),
726    /// Keyed with its path normalized.
727    Path { flag: String, path: PathBuf },
728    /// A prefix rewrite: the source path normalizes, the replacement does not.
729    PrefixMap {
730        flag: String,
731        from: PathBuf,
732        to: String,
733    },
734    /// The source file.
735    Source(PathBuf),
736}
737
738/// A parsed, admitted C or C++ compile.
739#[derive(Debug, Clone, PartialEq, Eq)]
740pub struct CcInvocation {
741    arguments: Vec<Argument>,
742    source: PathBuf,
743    output: PathBuf,
744    include_dirs: Vec<PathBuf>,
745    required_inputs: Vec<PathBuf>,
746    language: CcLanguage,
747    preprocessed_assembly: bool,
748    sysroot: Option<PathBuf>,
749    caller_depfile: Option<CallerDepfile>,
750    /// Positions in the parsed command line of the dependency-list flags the
751    /// caller passed, which the shim leaves out when it runs the driver.
752    dependency_argument_indices: Vec<usize>,
753}
754
755/// A dependency list the caller asked the driver to write beside the object.
756///
757/// Build systems driving the compiler through a build script -- OpenSSL's
758/// makefiles under `openssl-src`, CMake under many `-sys` crates -- ask for one
759/// with `-MD` or `-MMD` so their own rebuild logic has something to read. The
760/// flags do not change the object, so they are not key material, and the file
761/// names paths on this machine, so it is never stored. The shim writes it
762/// itself from the dependency list it already collects, whether the object
763/// was compiled or restored.
764#[derive(Debug, Clone, PartialEq, Eq)]
765#[non_exhaustive]
766pub struct CallerDepfile {
767    /// Where the list goes: `-MF`, or the object's path with a `.d` extension.
768    pub path: PathBuf,
769    /// The rule's targets, from `-MT` and `-MQ`, or the object as `-o` named it.
770    pub targets: Vec<DepfileTarget>,
771    /// Whether system headers are left out, as `-MMD` does.
772    pub user_headers_only: bool,
773    /// Whether every header also gets an empty rule of its own, as `-MP` does.
774    pub phony_targets: bool,
775}
776
777/// One target of a caller's dependency rule.
778#[derive(Debug, Clone, PartialEq, Eq)]
779pub struct DepfileTarget {
780    /// The target text as the caller gave it.
781    pub name: String,
782    /// Whether characters special to make are quoted on output, as `-MQ`
783    /// asks; `-MT` writes the name literally.
784    pub quoted: bool,
785}
786
787impl CcInvocation {
788    /// The object's path made absolute against the compiler's working
789    /// directory, for reading and storing it. `-o` is often relative --
790    /// OpenSSL's makefiles name every object that way -- and a path handed to
791    /// the cache agent has to mean the same file from the agent's own
792    /// directory. Key material still normalizes the spelling as given.
793    pub fn output_in(&self, working_dir: &Path) -> PathBuf {
794        if self.output.is_absolute() {
795            normalize_components(&self.output)
796        } else {
797            normalize_components(&working_dir.join(&self.output))
798        }
799    }
800
801    /// The dependency list the caller asked for, if any.
802    pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
803        self.caller_depfile.as_ref()
804    }
805
806    /// The command line to run the driver with: `arguments` as parsed, less
807    /// the caller's dependency-list flags. The shim asks the driver for its
808    /// own list, the driver honors only one `-MF`, and the caller's list is
809    /// written by the shim afterwards.
810    pub fn compiler_arguments(&self, arguments: &[OsString]) -> Vec<OsString> {
811        arguments
812            .iter()
813            .enumerate()
814            .filter(|(index, _)| !self.dependency_argument_indices.contains(index))
815            .map(|(_, argument)| argument.clone())
816            .collect()
817    }
818
819    /// Parse a driver command line, admitting only modeled single-object
820    /// compiles.
821    pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
822        Parser::new(arguments).parse()
823    }
824
825    /// Parse a command line using the syntax of `family`.
826    pub fn parse_for(
827        arguments: &[OsString],
828        family: CcCompilerFamily,
829    ) -> Result<Self, CcBypassReason> {
830        if family.is_msvc() {
831            MsvcParser::new(arguments).parse()
832        } else {
833            Self::parse(arguments)
834        }
835    }
836
837    /// Parse a command line using Microsoft `cl.exe` syntax.
838    pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
839        MsvcParser::new(arguments).parse()
840    }
841
842    /// Source file this invocation compiles.
843    pub fn source(&self) -> &Path {
844        &self.source
845    }
846
847    /// Object file this invocation produces.
848    pub fn output(&self) -> &Path {
849        &self.output
850    }
851
852    /// Include search directories named on the command line, in order.
853    pub fn include_dirs(&self) -> &[PathBuf] {
854        &self.include_dirs
855    }
856
857    /// Files that must appear among the discovered inputs.
858    pub fn required_inputs(&self) -> &[PathBuf] {
859        &self.required_inputs
860    }
861
862    /// Language the driver compiles.
863    pub fn language(&self) -> CcLanguage {
864        self.language
865    }
866
867    /// Reject assembler-time file directives that the preprocessor's
868    /// dependency list cannot name.
869    ///
870    /// This is a no-op for C and C++ inputs. For preprocessed assembly, every
871    /// file reported by dependency discovery is scanned conservatively before
872    /// an object can be published.
873    pub fn validate_discovered_inputs<'a>(
874        &self,
875        paths: impl IntoIterator<Item = &'a Path>,
876    ) -> Result<(), CcBypassReason> {
877        if !self.preprocessed_assembly {
878            return Ok(());
879        }
880        for path in paths {
881            if depfile::contains_assembler_input_directive(path)? {
882                return Err(CcBypassReason::AssemblerInputDirective(path.to_path_buf()));
883            }
884        }
885        Ok(())
886    }
887
888    /// Sysroot named on the command line, if any.
889    pub fn sysroot(&self) -> Option<&Path> {
890        self.sysroot.as_deref()
891    }
892
893    /// Short label used for timing statistics.
894    pub fn source_name(&self) -> String {
895        self.source
896            .file_name()
897            .map(|name| name.to_string_lossy().into_owned())
898            .unwrap_or_default()
899    }
900
901    /// Arguments to append so the driver writes a dependency list beside the
902    /// object.
903    ///
904    /// `-MD` rather than `-MMD`: system headers are exactly the inputs most
905    /// likely to change without any other key component noticing, because the
906    /// compiler identity does not cover the C library or the platform SDK.
907    pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
908        vec!["-MD".into(), "-MF".into(), depfile.into()]
909    }
910
911    /// Arguments to append so a driver from `family` writes its dependency
912    /// list beside the object.
913    pub fn dependency_arguments_for(
914        &self,
915        depfile: &Path,
916        family: CcCompilerFamily,
917    ) -> Vec<OsString> {
918        if family.is_msvc() {
919            vec!["/sourceDependencies".into(), depfile.into()]
920        } else {
921            self.dependency_arguments(depfile)
922        }
923    }
924
925    /// Arguments to append so `cl.exe` writes `/sourceDependencies` JSON.
926    pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
927        vec!["/sourceDependencies".into(), depfile.into()]
928    }
929
930    /// Digest of the pre-input fingerprint, used to look up a prediction.
931    pub fn invocation_digest(
932        &self,
933        context: &CcActionContext,
934    ) -> Result<CacheDigest, CcBypassReason> {
935        let builder = ActionBuilder::new(self, context.clone());
936        let descriptor = builder.invocation_descriptor()?;
937        let bytes = canonical_json(&descriptor)
938            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
939        Ok(CacheDigest::blake3(&bytes))
940    }
941
942    /// Build the canonical action for this invocation and its discovered
943    /// inputs.
944    pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
945        ActionBuilder::new(self, context).build()
946    }
947
948    /// Record the normalized inputs of a successful compile so the next cold
949    /// invocation can rebuild the same key before compiling.
950    pub fn prediction(
951        &self,
952        context: &CcActionContext,
953        compiler_duration_ns: u64,
954    ) -> Result<CcInputPrediction, CcBypassReason> {
955        let builder = ActionBuilder::new(self, context.clone());
956        let mut inputs = context
957            .inputs
958            .iter()
959            .map(|input| builder.normalize_input_path(&input.path))
960            .collect::<Result<Vec<_>, _>>()?;
961        inputs.sort();
962        inputs.dedup();
963        Ok(CcInputPrediction {
964            version: 1,
965            inputs,
966            environment: context.environment.keys().cloned().collect(),
967            compiler_duration_ns,
968            source_name: self.source_name(),
969        })
970    }
971}
972
973impl CcInputPrediction {
974    /// Rehash the predicted paths and recompute include manifests. The caller
975    /// still recomputes the full action digest, so changed inputs are misses.
976    pub fn discover(
977        &self,
978        working_dir: &Path,
979        path_mappings: &[PathMapping],
980        digests: &dyn FileDigestCache,
981    ) -> Result<CcDiscoveredInputs, CcBypassReason> {
982        if self.version != 1 {
983            return Err(CcBypassReason::UnsupportedPrediction);
984        }
985        if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
986            return Err(CcBypassReason::UnsupportedPrediction);
987        }
988        let mappings = PathMapping::ordered(path_mappings);
989        let mut files = BTreeSet::new();
990        let mut directories = BTreeSet::new();
991        for entry in &self.inputs {
992            match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
993                Some(directory) => {
994                    directories.insert(denormalize_path(directory, &mappings)?);
995                }
996                None => {
997                    files.insert(denormalize_path(entry, &mappings)?);
998                }
999            }
1000        }
1001        CcDiscoveredInputs::collect(working_dir, files, directories, digests)
1002    }
1003}
1004
1005/// Resolve a normalized key path back to a host path.
1006///
1007/// Placeholder entries expand through their mapping; a verbatim entry is
1008/// accepted only when it still lies beneath an admitted system root, so a
1009/// prediction cannot name an arbitrary absolute path.
1010fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
1011    for mapping in mappings {
1012        let prefix = format!("${{{}}}", mapping.placeholder);
1013        let suffix = if value == prefix {
1014            ""
1015        } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1016            suffix
1017        } else {
1018            continue;
1019        };
1020        if !mapping.root.is_absolute() || !safe_suffix(suffix) {
1021            return Err(CcBypassReason::InvalidPredictedInput(value.into()));
1022        }
1023        let mut path = normalize_components(&mapping.root);
1024        path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1025        return Ok(path);
1026    }
1027    // A verbatim entry names a machine path rather than a placeholder. It is
1028    // admitted only beneath a system root, and only spelled literally: a
1029    // traversal component would let a prediction reach outside that root.
1030    let path = PathBuf::from(value);
1031    if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
1032        return Ok(path);
1033    }
1034    Err(CcBypassReason::InvalidPredictedInput(value.into()))
1035}
1036
1037fn safe_suffix(suffix: &str) -> bool {
1038    suffix.is_empty()
1039        || !suffix.split('/').any(|component| {
1040            component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
1041        })
1042}
1043
1044/// Whether a path lies beneath a root whose location is a machine property.
1045pub fn is_system_path(path: &Path) -> bool {
1046    SYSTEM_ROOTS
1047        .iter()
1048        .any(|root| path.starts_with(Path::new(root)))
1049}
1050
1051fn normalize_components(path: &Path) -> PathBuf {
1052    let mut normalized = PathBuf::new();
1053    for component in path.components() {
1054        match component {
1055            Component::CurDir => {}
1056            Component::ParentDir => {
1057                normalized.pop();
1058            }
1059            component => normalized.push(component.as_os_str()),
1060        }
1061    }
1062    normalized
1063}
1064
1065/// Read the modeled environment, rejecting variables that change the compile in
1066/// a way the argv model cannot see.
1067pub fn environment_inputs<F>(
1068    lookup: F,
1069    sysroot: Option<&Path>,
1070) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1071where
1072    F: Fn(&str) -> Option<String>,
1073{
1074    environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
1075}
1076
1077/// Read the modeled environment for a particular compiler family.
1078pub fn environment_inputs_for<F>(
1079    lookup: F,
1080    sysroot: Option<&Path>,
1081    family: CcCompilerFamily,
1082) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1083where
1084    F: Fn(&str) -> Option<String>,
1085{
1086    for name in BYPASS_ENVIRONMENT {
1087        if lookup(name).is_some() {
1088            return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
1089        }
1090    }
1091    let mut environment = BTreeMap::new();
1092    for name in KEYED_ENVIRONMENT {
1093        // An explicit `-isysroot` on the command line already pins the SDK, and
1094        // it is what the driver honors, so the variable stops being an input.
1095        if *name == "SDKROOT" && sysroot.is_some() {
1096            continue;
1097        }
1098        environment.insert((*name).to_string(), lookup(name));
1099    }
1100    if family.is_msvc() {
1101        // INCLUDE changes header resolution without appearing in argv. The
1102        // toolset and SDK versions make the otherwise machine-local paths
1103        // meaningful when action records move between hosts.
1104        for name in [
1105            "INCLUDE",
1106            "VCToolsVersion",
1107            "WindowsSDKVersion",
1108            "UCRTVersion",
1109        ] {
1110            environment.insert(name.into(), lookup(name));
1111        }
1112        for name in ["CL", "_CL_"] {
1113            if lookup(name).is_some() {
1114                return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
1115            }
1116        }
1117    }
1118    Ok(environment)
1119}
1120
1121struct ActionBuilder<'a> {
1122    invocation: &'a CcInvocation,
1123    context: CcActionContext,
1124    mappings: Vec<PathMapping>,
1125}
1126
1127impl<'a> ActionBuilder<'a> {
1128    fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
1129        context.path_mappings = PathMapping::ordered(&context.path_mappings);
1130        let mappings = context.path_mappings.clone();
1131        Self {
1132            invocation,
1133            context,
1134            mappings,
1135        }
1136    }
1137
1138    fn build(self) -> Result<CcAction, CcBypassReason> {
1139        self.validate_mappings()?;
1140        let invocation = self.invocation_descriptor()?;
1141
1142        let mut inputs = BTreeMap::<String, CacheDigest>::new();
1143        for input in &self.context.inputs {
1144            input.digest.validate().map_err(|_| {
1145                CcBypassReason::InvalidInputDigest(input.path.display().to_string())
1146            })?;
1147            let path = self.normalize_input_path(&input.path)?;
1148            if inputs
1149                .insert(path.clone(), input.digest.clone())
1150                .is_some_and(|existing| existing != input.digest)
1151            {
1152                return Err(CcBypassReason::ConflictingInput(path));
1153            }
1154        }
1155        let required = self
1156            .invocation
1157            .required_inputs
1158            .iter()
1159            .map(|path| self.normalize_path(path))
1160            .collect::<Result<BTreeSet<_>, _>>()?;
1161        if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1162            return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
1163        }
1164        let inputs = inputs
1165            .into_iter()
1166            .map(|(path, digest)| CcInputDescriptor { path, digest })
1167            .collect();
1168        let descriptor = CcActionDescriptor {
1169            version: ACTION_SCHEMA_VERSION,
1170            kind: "cc",
1171            adapter_version: ADAPTER_VERSION,
1172            assembly_input_model: invocation.assembly_input_model,
1173            compiler: invocation.compiler,
1174            arguments: invocation.arguments,
1175            environment: self.context.environment.clone(),
1176            inputs,
1177        };
1178        let bytes = canonical_json(&descriptor)
1179            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1180        let digest = CacheDigest::blake3(&bytes);
1181        Ok(CcAction { digest, bytes })
1182    }
1183
1184    fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1185        self.validate_mappings()?;
1186        let arguments = self
1187            .invocation
1188            .arguments
1189            .iter()
1190            .map(|argument| self.normalize_argument(argument))
1191            .collect::<Result<Vec<_>, _>>()?;
1192        let required_inputs = self
1193            .invocation
1194            .required_inputs
1195            .iter()
1196            .map(|path| self.normalize_path(path))
1197            .collect::<Result<BTreeSet<_>, _>>()?
1198            .into_iter()
1199            .collect();
1200        Ok(CcInvocationDescriptor {
1201            version: ACTION_SCHEMA_VERSION,
1202            kind: "cc",
1203            adapter_version: ADAPTER_VERSION,
1204            // Version the assembly-only safety model independently so adding
1205            // support does not invalidate every existing C and C++ entry.
1206            assembly_input_model: self.invocation.preprocessed_assembly.then_some(1),
1207            compiler: self.compiler_descriptor(),
1208            arguments,
1209            required_inputs,
1210        })
1211    }
1212
1213    fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1214        CcCompilerDescriptor {
1215            assembler: self.context.compiler.assembler.clone(),
1216            family: self.context.compiler.family.as_str().into(),
1217            target: self.context.compiler.target.clone(),
1218            version_text: self.context.compiler.version_text.clone(),
1219        }
1220    }
1221
1222    fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1223        if !self.context.working_dir.is_absolute() {
1224            return Err(CcBypassReason::RelativeWorkingDirectory(
1225                self.context.working_dir.clone(),
1226            ));
1227        }
1228        let mut roots = BTreeSet::new();
1229        let mut placeholders = BTreeSet::new();
1230        for mapping in &self.mappings {
1231            if !mapping.root.is_absolute() {
1232                return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1233            }
1234            if mapping.placeholder.is_empty()
1235                || !mapping
1236                    .placeholder
1237                    .bytes()
1238                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1239                || !roots.insert(normalize_components(&mapping.root))
1240                || !placeholders.insert(&mapping.placeholder)
1241            {
1242                return Err(CcBypassReason::InvalidPathPlaceholder(
1243                    mapping.placeholder.clone(),
1244                ));
1245            }
1246        }
1247        Ok(())
1248    }
1249
1250    fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1251        match argument {
1252            Argument::Plain(value) => Ok(value.clone()),
1253            Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1254            Argument::PrefixMap { flag, from, to } => {
1255                Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1256            }
1257            Argument::Source(path) => Ok(self.normalize_path(path)?),
1258        }
1259    }
1260
1261    /// Normalize a path that names a compilation input or search root.
1262    ///
1263    /// A path beneath a mapped root becomes a placeholder so equivalent
1264    /// checkouts agree. A path beneath a system root stays verbatim: its
1265    /// location is a property of the machine, and its contents are digested
1266    /// like any other input.
1267    fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1268        match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
1269            Ok(normalized) => Ok(normalized),
1270            Err(reason) => {
1271                let absolute = absolute_path(path, &self.context.working_dir);
1272                if is_system_path(&absolute) {
1273                    return absolute
1274                        .to_str()
1275                        .map(ToOwned::to_owned)
1276                        .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1277                }
1278                Err(reason.into())
1279            }
1280        }
1281    }
1282
1283    fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1284        match path.to_str().and_then(|path| {
1285            path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1286                .map(ToOwned::to_owned)
1287        }) {
1288            Some(directory) => Ok(format!(
1289                "{INCLUDE_MANIFEST_PREFIX}{}",
1290                self.normalize_path(Path::new(&directory))?
1291            )),
1292            None => self.normalize_path(path),
1293        }
1294    }
1295}
1296
1297fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1298    if path.is_absolute() {
1299        normalize_components(path)
1300    } else {
1301        normalize_components(&working_dir.join(path))
1302    }
1303}
1304
1305struct Parser<'a> {
1306    arguments: &'a [OsString],
1307    index: usize,
1308    parsed: Vec<Argument>,
1309    source: Option<PathBuf>,
1310    output: Option<PathBuf>,
1311    include_dirs: Vec<PathBuf>,
1312    required_inputs: Vec<PathBuf>,
1313    sysroot: Option<PathBuf>,
1314    explicit_language: Option<CcLanguage>,
1315    preprocessed_assembly: bool,
1316    compiling: bool,
1317    dependency: DependencyRequest,
1318}
1319
1320/// What the caller's `-M` flags asked for, gathered while parsing.
1321#[derive(Debug, Default)]
1322struct DependencyRequest {
1323    /// `Some(false)` for `-MD`, `Some(true)` for `-MMD`.
1324    user_headers_only: Option<bool>,
1325    file: Option<PathBuf>,
1326    targets: Vec<DepfileTarget>,
1327    phony_targets: bool,
1328    /// The last `-MF`, `-MT`, `-MQ`, or `-MP` seen, for the bypass a request
1329    /// without `-MD` or `-MMD` reports.
1330    modifier: Option<String>,
1331    indices: Vec<usize>,
1332}
1333
1334impl<'a> Parser<'a> {
1335    fn new(arguments: &'a [OsString]) -> Self {
1336        Self {
1337            arguments,
1338            index: 0,
1339            parsed: Vec::new(),
1340            source: None,
1341            output: None,
1342            include_dirs: Vec::new(),
1343            required_inputs: Vec::new(),
1344            sysroot: None,
1345            explicit_language: None,
1346            preprocessed_assembly: false,
1347            compiling: false,
1348            dependency: DependencyRequest::default(),
1349        }
1350    }
1351
1352    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1353        while self.index < self.arguments.len() {
1354            let value = self.current()?.to_string();
1355            self.index += 1;
1356            if value == "-" {
1357                return Err(CcBypassReason::StandardInput);
1358            }
1359            if let Some(argfile) = value.strip_prefix('@') {
1360                return Err(CcBypassReason::ResponseFile(argfile.into()));
1361            }
1362            if value.starts_with('-') {
1363                self.parse_flag(&value)?;
1364            } else {
1365                self.parse_input(&value)?;
1366            }
1367        }
1368
1369        if !self.compiling {
1370            return Err(CcBypassReason::NotACompile);
1371        }
1372        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1373        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1374        let language = self.language(&source)?;
1375        let preprocessed_assembly = self.preprocessed_assembly
1376            || (self.explicit_language.is_none()
1377                && source.extension().and_then(|value| value.to_str()) == Some("S"));
1378        self.required_inputs.push(source.clone());
1379        let caller_depfile = match self.dependency.user_headers_only {
1380            Some(user_headers_only) => Some(CallerDepfile {
1381                path: self
1382                    .dependency
1383                    .file
1384                    .unwrap_or_else(|| output.with_extension("d")),
1385                targets: if self.dependency.targets.is_empty() {
1386                    vec![DepfileTarget {
1387                        name: output.to_string_lossy().into_owned(),
1388                        quoted: true,
1389                    }]
1390                } else {
1391                    self.dependency.targets
1392                },
1393                user_headers_only,
1394                phony_targets: self.dependency.phony_targets,
1395            }),
1396            // `-MF` or `-MT` without `-MD` or `-MMD` writes nothing, but it is
1397            // not a shape the cc crate or a makefile produces, so it is not
1398            // one to guess at.
1399            None => match self.dependency.modifier {
1400                Some(modifier) => return Err(CcBypassReason::CallerDependencyFlags(modifier)),
1401                None => None,
1402            },
1403        };
1404        Ok(CcInvocation {
1405            arguments: self.parsed,
1406            source,
1407            output,
1408            include_dirs: self.include_dirs,
1409            required_inputs: self.required_inputs,
1410            language,
1411            preprocessed_assembly,
1412            sysroot: self.sysroot,
1413            caller_depfile,
1414            dependency_argument_indices: self.dependency.indices,
1415        })
1416    }
1417
1418    /// A `-M` flag: the caller wants a dependency list beside the object.
1419    ///
1420    /// `-MD` and `-MMD` compile as well as listing, so they are taken, with
1421    /// the `-MF`, `-MT`, `-MQ`, and `-MP` that shape the list. None of them
1422    /// enters the key: the object is the same with or without them. `-M` and
1423    /// `-MM` stop after preprocessing, and `-MG` changes what a missing header
1424    /// means, so those still bypass.
1425    fn parse_dependency_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1426        let start = self.index - 1;
1427        match option {
1428            "D" => self.dependency.user_headers_only = Some(false),
1429            "MD" => self.dependency.user_headers_only = Some(true),
1430            "P" => {
1431                self.dependency.phony_targets = true;
1432                self.dependency.modifier = Some(value.into());
1433            }
1434            _ if option.starts_with('F') => {
1435                let file = self.take_value("-MF", Some(&option[1..]))?;
1436                self.dependency.file = Some(PathBuf::from(file));
1437                self.dependency.modifier = Some("-MF".into());
1438            }
1439            _ if option.starts_with('T') || option.starts_with('Q') => {
1440                let flag = &value[..3];
1441                let name = self.take_value(flag, Some(&option[1..]))?;
1442                self.dependency.targets.push(DepfileTarget {
1443                    name,
1444                    quoted: option.starts_with('Q'),
1445                });
1446                self.dependency.modifier = Some(flag.into());
1447            }
1448            _ => return Err(CcBypassReason::CallerDependencyFlags(value.into())),
1449        }
1450        self.dependency.indices.extend(start..self.index);
1451        Ok(())
1452    }
1453
1454    fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1455        if let Some(language) = self.explicit_language {
1456            return Ok(language);
1457        }
1458        let extension = source
1459            .extension()
1460            .and_then(|extension| extension.to_str())
1461            .unwrap_or_default();
1462        match extension {
1463            // Preprocessed assembly goes through the C driver: the preprocessor
1464            // resolves its includes, which is what dependency discovery reads,
1465            // and the assembler that then produces the object is already part
1466            // of a GCC identity. Plain `.s` is not preprocessed and reports no
1467            // dependencies, so it stays out.
1468            "c" | "S" => Ok(CcLanguage::C),
1469            "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1470            _ => Err(CcBypassReason::UnsupportedLanguage(
1471                source.display().to_string(),
1472            )),
1473        }
1474    }
1475
1476    fn current(&self) -> Result<&str, CcBypassReason> {
1477        self.arguments[self.index]
1478            .to_str()
1479            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1480    }
1481
1482    fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1483        if let Some(value) = inline
1484            && !value.is_empty()
1485        {
1486            return Ok(value.into());
1487        }
1488        if self.index >= self.arguments.len() {
1489            return Err(CcBypassReason::MissingValue(flag.into()));
1490        }
1491        let value = self.current()?.to_string();
1492        self.index += 1;
1493        Ok(value)
1494    }
1495
1496    fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1497        if self.source.is_some() {
1498            return Err(CcBypassReason::MultipleInputs);
1499        }
1500        let path = PathBuf::from(value);
1501        // With an explicit `-x`, the driver ignores the extension entirely;
1502        // without one, the extension is the only thing that decides the
1503        // language, so an unmodeled extension has to bypass here.
1504        if self.explicit_language.is_none() {
1505            let extension = path
1506                .extension()
1507                .and_then(|extension| extension.to_str())
1508                .unwrap_or_default();
1509            if !matches!(extension, "c" | "S" | "cc" | "cpp" | "cxx" | "c++") {
1510                return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1511            }
1512        }
1513        self.source = Some(path.clone());
1514        self.parsed.push(Argument::Source(path));
1515        Ok(())
1516    }
1517
1518    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1519        if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1520            return Err(CcBypassReason::CompilerQuery);
1521        }
1522        if matches!(value, "-E" | "-S") {
1523            return Err(CcBypassReason::NonObjectOutput(value.into()));
1524        }
1525        if let Some(option) = value.strip_prefix("-M") {
1526            return self.parse_dependency_flag(value, option);
1527        }
1528        if value.starts_with("-save-temps") {
1529            return Err(CcBypassReason::SaveTemps(value.into()));
1530        }
1531        if value == "--coverage" {
1532            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1533        }
1534        if let Some(options) = value.strip_prefix("-Wa,")
1535            && !options.is_empty()
1536            && options
1537                .split(',')
1538                .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1539        {
1540            self.parsed.push(Argument::Plain(value.into()));
1541            return Ok(());
1542        }
1543        if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1544            || value.starts_with("-Wp,")
1545            || value.starts_with("-Wa,")
1546            || value.starts_with("-Wl,")
1547        {
1548            // The forwarded options are the compilation's real inputs and they
1549            // are not modeled, so consuming the value would not make this safe.
1550            return Err(CcBypassReason::ToolPassthrough(value.into()));
1551        }
1552        if value.starts_with("-include-pch") || value == "-emit-pch" {
1553            return Err(CcBypassReason::PrecompiledHeader(value.into()));
1554        }
1555
1556        if value == "-c" {
1557            self.compiling = true;
1558            self.parsed.push(Argument::Plain(value.into()));
1559            return Ok(());
1560        }
1561        if SUPPORTED_BARE_FLAGS.contains(&value)
1562            || SUPPORTED_O_FLAGS.contains(&value)
1563            || SUPPORTED_G_FLAGS.contains(&value)
1564            || value.starts_with("-std=")
1565        {
1566            self.parsed.push(Argument::Plain(value.into()));
1567            return Ok(());
1568        }
1569        if let Some(rest) = value.strip_prefix("-o") {
1570            let path = self.take_value("-o", Some(rest))?;
1571            // A repeated `-o` follows the driver: the last one names the file
1572            // that is produced. Every occurrence still enters the key.
1573            self.output = Some(PathBuf::from(&path));
1574            self.parsed.push(Argument::Path {
1575                flag: "-o".into(),
1576                path: PathBuf::from(path),
1577            });
1578            return Ok(());
1579        }
1580        if let Some(rest) = value.strip_prefix("-I") {
1581            let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1582            self.include_dirs.push(path.clone());
1583            self.parsed.push(Argument::Path {
1584                flag: "-I".into(),
1585                path,
1586            });
1587            return Ok(());
1588        }
1589        if SEPARATE_PATH_FLAGS.contains(&value) {
1590            let path = PathBuf::from(self.take_value(value, None)?);
1591            match value {
1592                "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1593                "-isysroot" => self.sysroot = Some(path.clone()),
1594                // `-include` and `-imacros` are deliberately not required
1595                // inputs. The driver resolves the name through the include
1596                // chain, so the file need not exist relative to the working
1597                // directory, and the dependency list names it at whatever path
1598                // it was actually found at.
1599                _ => {}
1600            }
1601            self.parsed.push(Argument::Path {
1602                flag: value.into(),
1603                path,
1604            });
1605            return Ok(());
1606        }
1607        // `--include=<file>` is the long spelling of `-include <file>`; the
1608        // `cc` crate emits it for prefixed headers.
1609        if let Some(rest) = value.strip_prefix("--include=") {
1610            let path = PathBuf::from(rest);
1611            self.parsed.push(Argument::Path {
1612                flag: "-include".into(),
1613                path,
1614            });
1615            return Ok(());
1616        }
1617        if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1618            value
1619                .strip_prefix(&format!("{flag}="))
1620                .map(|rest| (*flag, rest))
1621        }) {
1622            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1623            self.parsed.push(Argument::PrefixMap {
1624                flag: flag.into(),
1625                from: PathBuf::from(from),
1626                to: to.into(),
1627            });
1628            return Ok(());
1629        }
1630        // `--param name=value` tunes the optimizer; its text fully describes it.
1631        if value == "--param" {
1632            let parameter = self.take_value("--param", None)?;
1633            self.parsed
1634                .push(Argument::Plain(format!("--param={parameter}")));
1635            return Ok(());
1636        }
1637        if let Some(parameter) = value.strip_prefix("--param=") {
1638            self.parsed
1639                .push(Argument::Plain(format!("--param={parameter}")));
1640            return Ok(());
1641        }
1642        if let Some(rest) = value.strip_prefix("--sysroot=") {
1643            let path = PathBuf::from(rest);
1644            self.sysroot = Some(path.clone());
1645            self.parsed.push(Argument::Path {
1646                flag: "--sysroot".into(),
1647                path,
1648            });
1649            return Ok(());
1650        }
1651        if let Some(rest) = value
1652            .strip_prefix("-D")
1653            .or_else(|| value.strip_prefix("-U"))
1654        {
1655            let flag = &value[..2];
1656            let definition = self.take_value(flag, Some(rest))?;
1657            self.parsed
1658                .push(Argument::Plain(format!("{flag}{definition}")));
1659            return Ok(());
1660        }
1661        if let Some(rest) = value.strip_prefix("-x") {
1662            let language = self.take_value("-x", Some(rest))?;
1663            self.explicit_language = Some(match language.as_str() {
1664                "c" => CcLanguage::C,
1665                "assembler-with-cpp" => {
1666                    self.preprocessed_assembly = true;
1667                    CcLanguage::C
1668                }
1669                "c++" => CcLanguage::Cxx,
1670                other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1671            });
1672            self.parsed.push(Argument::Plain(format!("-x{language}")));
1673            return Ok(());
1674        }
1675        if let Some(target) = value.strip_prefix("--target=") {
1676            self.parsed
1677                .push(Argument::Plain(format!("--target={target}")));
1678            return Ok(());
1679        }
1680        if value == "-target" {
1681            let target = self.take_value("-target", None)?;
1682            self.parsed
1683                .push(Argument::Plain(format!("--target={target}")));
1684            return Ok(());
1685        }
1686        if value == "-arch" {
1687            let arch = self.take_value("-arch", None)?;
1688            self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1689            return Ok(());
1690        }
1691        if let Some(option) = value.strip_prefix("-f") {
1692            return self.parse_f_flag(value, option);
1693        }
1694        if let Some(option) = value.strip_prefix("-m") {
1695            return self.parse_m_flag(value, option);
1696        }
1697        if value.starts_with("-g") {
1698            // `-gsplit-dwarf` writes a `.dwo` beside the object; every other
1699            // unlisted `-g` spelling is simply unmodeled.
1700            return Err(if value.starts_with("-gsplit-dwarf") {
1701                CcBypassReason::SplitDebugOutput(value.into())
1702            } else {
1703                CcBypassReason::UnknownFlag(value.into())
1704            });
1705        }
1706        if value.starts_with("-W") {
1707            // Warning selection changes only diagnostics, which are replayed
1708            // from the cache, and the exit status, and only successful
1709            // compiles are ever published.
1710            self.parsed.push(Argument::Plain(value.into()));
1711            return Ok(());
1712        }
1713        Err(CcBypassReason::UnknownFlag(value.into()))
1714    }
1715
1716    fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1717        if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1718            return Err(CcBypassReason::Plugin(value.into()));
1719        }
1720        if option.starts_with("profile-") || option == "test-coverage" {
1721            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1722        }
1723        let name = option.split_once('=').map_or(option, |(name, _)| name);
1724        if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1725            return Err(CcBypassReason::UnknownFlag(value.into()));
1726        }
1727        self.parsed.push(Argument::Plain(value.into()));
1728        Ok(())
1729    }
1730
1731    fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1732        if option == "llvm" {
1733            return Err(CcBypassReason::ToolPassthrough(value.into()));
1734        }
1735        // `-march=native` and its relatives resolve against whatever CPU this
1736        // machine has. The resulting object is not a function of the key, so
1737        // another machine could otherwise restore code its processor cannot
1738        // run.
1739        if let Some((name, selection)) = option.split_once('=')
1740            && matches!(name, "arch" | "cpu" | "tune")
1741            && matches!(selection, "native" | "host")
1742        {
1743            return Err(CcBypassReason::LocalCpuTarget(value.into()));
1744        }
1745        let name = option.split_once('=').map_or(option, |(name, _)| name);
1746        if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1747            return Err(CcBypassReason::UnknownFlag(value.into()));
1748        }
1749        self.parsed.push(Argument::Plain(value.into()));
1750        Ok(())
1751    }
1752}
1753
1754/// Conservative parser for the command lines emitted by the `cc` crate for
1755/// Microsoft's compiler. It intentionally admits only flags whose effects are
1756/// either present in argv or covered by dependency discovery.
1757struct MsvcParser<'a> {
1758    arguments: &'a [OsString],
1759    index: usize,
1760    parsed: Vec<Argument>,
1761    source: Option<PathBuf>,
1762    output: Option<PathBuf>,
1763    include_dirs: Vec<PathBuf>,
1764    required_inputs: Vec<PathBuf>,
1765    explicit_language: Option<CcLanguage>,
1766    compiling: bool,
1767}
1768
1769impl<'a> MsvcParser<'a> {
1770    fn new(arguments: &'a [OsString]) -> Self {
1771        Self {
1772            arguments,
1773            index: 0,
1774            parsed: Vec::new(),
1775            source: None,
1776            output: None,
1777            include_dirs: Vec::new(),
1778            required_inputs: Vec::new(),
1779            explicit_language: None,
1780            compiling: false,
1781        }
1782    }
1783
1784    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1785        while self.index < self.arguments.len() {
1786            let value = self.current()?.to_owned();
1787            self.index += 1;
1788            if let Some(file) = value.strip_prefix('@') {
1789                return Err(CcBypassReason::ResponseFile(file.into()));
1790            }
1791            if value.starts_with('/') || value.starts_with('-') {
1792                self.parse_flag(&value)?;
1793            } else {
1794                self.add_source(&value)?;
1795            }
1796        }
1797        if !self.compiling {
1798            return Err(CcBypassReason::NotACompile);
1799        }
1800        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1801        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1802        let language = self.explicit_language.unwrap_or_else(|| {
1803            if source
1804                .extension()
1805                .and_then(|value| value.to_str())
1806                .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1807            {
1808                CcLanguage::C
1809            } else {
1810                CcLanguage::Cxx
1811            }
1812        });
1813        self.required_inputs.push(source.clone());
1814        Ok(CcInvocation {
1815            arguments: self.parsed,
1816            source,
1817            output,
1818            include_dirs: self.include_dirs,
1819            required_inputs: self.required_inputs,
1820            language,
1821            preprocessed_assembly: false,
1822            sysroot: None,
1823            caller_depfile: None,
1824            dependency_argument_indices: Vec::new(),
1825        })
1826    }
1827
1828    fn current(&self) -> Result<&str, CcBypassReason> {
1829        self.arguments[self.index]
1830            .to_str()
1831            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1832    }
1833
1834    fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
1835        if !attached.is_empty() {
1836            return Ok(attached.into());
1837        }
1838        if self.index == self.arguments.len() {
1839            return Err(CcBypassReason::MissingValue(flag.into()));
1840        }
1841        let value = self.current()?.to_owned();
1842        self.index += 1;
1843        Ok(value)
1844    }
1845
1846    fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
1847        if self.source.is_some() {
1848            return Err(CcBypassReason::MultipleInputs);
1849        }
1850        let path = PathBuf::from(value);
1851        if self.explicit_language.is_none()
1852            && !path
1853                .extension()
1854                .and_then(|value| value.to_str())
1855                .is_some_and(|value| {
1856                    matches!(
1857                        value.to_ascii_lowercase().as_str(),
1858                        "c" | "cc" | "cpp" | "cxx"
1859                    )
1860                })
1861        {
1862            return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1863        }
1864        self.source = Some(path.clone());
1865        self.parsed.push(Argument::Source(path));
1866        Ok(())
1867    }
1868
1869    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1870        let option = value.trim_start_matches(['/', '-']);
1871        let lower = option.to_ascii_lowercase();
1872        if matches!(lower.as_str(), "?" | "help") {
1873            return Err(CcBypassReason::CompilerQuery);
1874        }
1875        if lower == "showincludes" || lower.starts_with("sourcedependencies") {
1876            return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1877        }
1878        if matches!(lower.as_str(), "e" | "ep" | "p") {
1879            return Err(CcBypassReason::NonObjectOutput(value.into()));
1880        }
1881        if (lower.starts_with("fa") && !lower.starts_with("favor:"))
1882            || lower.starts_with("fd")
1883            || lower.starts_with("zi")
1884        {
1885            return Err(CcBypassReason::SplitDebugOutput(value.into()));
1886        }
1887        if lower.starts_with("yc")
1888            || lower.starts_with("yu")
1889            || (lower.starts_with("fp") && !lower.starts_with("fp:"))
1890        {
1891            return Err(CcBypassReason::PrecompiledHeader(value.into()));
1892        }
1893        if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
1894            return Err(CcBypassReason::ToolPassthrough(value.into()));
1895        }
1896        if matches!(lower.as_str(), "ld" | "ldd") {
1897            return Err(CcBypassReason::NotACompile);
1898        }
1899        if lower == "c" {
1900            self.compiling = true;
1901            self.parsed.push(Argument::Plain("/c".into()));
1902            return Ok(());
1903        }
1904        for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
1905            if let Some(attached) = option.strip_prefix(prefix) {
1906                let path = PathBuf::from(self.value(canonical, attached)?);
1907                if prefix == "Fo" {
1908                    self.output = Some(path.clone());
1909                } else if prefix == "I" {
1910                    self.include_dirs.push(path.clone());
1911                }
1912                self.parsed.push(Argument::Path {
1913                    flag: canonical.into(),
1914                    path,
1915                });
1916                return Ok(());
1917            }
1918        }
1919        if lower.starts_with("external:i") {
1920            let path = PathBuf::from(self.value("/external:I", &option[10..])?);
1921            self.include_dirs.push(path.clone());
1922            self.parsed.push(Argument::Path {
1923                flag: "/external:I".into(),
1924                path,
1925            });
1926            return Ok(());
1927        }
1928        if option.starts_with("Tc") || option.starts_with("Tp") {
1929            let c = option.starts_with("Tc");
1930            let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
1931            self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
1932            return self.add_source(&path);
1933        }
1934        if lower.starts_with("pathmap:") {
1935            let rest = &option[8..];
1936            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1937            self.parsed.push(Argument::PrefixMap {
1938                flag: "/pathmap".into(),
1939                from: PathBuf::from(from),
1940                to: to.into(),
1941            });
1942            return Ok(());
1943        }
1944        if matches!(option, "D" | "U") {
1945            let definition = self.value(value, "")?;
1946            self.parsed
1947                .push(Argument::Plain(format!("/{option}{definition}")));
1948            return Ok(());
1949        }
1950        // Definitions and the ordinary code-generation/diagnostic switches
1951        // produced by cc-rs are self-contained text and can be keyed verbatim.
1952        let definition = option.starts_with('D') || option.starts_with('U');
1953        let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
1954            || lower
1955                .strip_prefix('w')
1956                .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1957            || ["wd", "we", "wo"].iter().any(|prefix| {
1958                lower
1959                    .strip_prefix(prefix)
1960                    .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1961            });
1962        let admitted = definition
1963            || warning
1964            || lower.starts_with("std:")
1965            || lower.starts_with("arch:")
1966            || lower.starts_with("favor:")
1967            || lower.starts_with("volatile:")
1968            || lower.starts_with("fp:")
1969            || lower.starts_with("eh")
1970            || lower.starts_with('o')
1971            || lower.starts_with("ob")
1972            || lower.starts_with("oi")
1973            || lower.starts_with("ot")
1974            || lower.starts_with("oy")
1975            || lower.starts_with("gs")
1976            || lower.starts_with("gr")
1977            || lower.starts_with("gy")
1978            || lower.starts_with("gw")
1979            || lower.starts_with("gl")
1980            || lower.starts_with("zc:")
1981            || lower.starts_with("diagnostics:")
1982            || matches!(
1983                lower.as_str(),
1984                "nologo"
1985                    | "brepro"
1986                    | "bigobj"
1987                    | "utf-8"
1988                    | "permissive-"
1989                    | "z7"
1990                    | "md"
1991                    | "mdd"
1992                    | "mt"
1993                    | "mtd"
1994            );
1995        if admitted {
1996            self.parsed.push(Argument::Plain(value.into()));
1997            return Ok(());
1998        }
1999        Err(CcBypassReason::UnknownFlag(value.into()))
2000    }
2001}
2002
2003#[cfg(test)]
2004#[path = "cc_cache_tests.rs"]
2005mod tests;