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