Skip to main content

mbx_cache_cc/
lib.rs

1//! Conservative parsing and action-key construction for C and C++ compiles.
2//!
3//! Cargo build scripts using the `cc` crate compile C and C++ through a
4//! gcc-style driver. This adapter models the narrow shape those build scripts
5//! produce -- one source, one object, `-c` -- plus named `-E` output. As
6//! in the rustc adapter, callers should treat [`CcBypassReason`] as a safe
7//! cache bypass: run the real compiler and publish nothing.
8//!
9//! Two properties separate this adapter from a traditional compiler cache.
10//! Preprocessor inputs are discovered from a depfile the adapter injects
11//! itself, so the key names the headers the compilation actually read; and the
12//! directories those headers were searched from contribute a name manifest, so
13//! a header that newly *shadows* one of them changes the key even though every
14//! previously-read file is byte-identical.
15//!
16//! Path mappings are shared with the rustc adapter so both agree on which host
17//! roots are checkout-specific.
18#![deny(missing_docs)]
19
20use mbx_cache_core::{
21    CacheDigest, FileDigestCache, PathAliases, PathMapping, PathNormalizationError, canonical_json,
22    normalize_resolved_mapped_path_with, resolve_path_mappings,
23};
24use serde::{Deserialize, Serialize};
25use std::collections::{BTreeMap, BTreeSet};
26use std::ffi::OsString;
27use std::path::{Component, Path, PathBuf};
28use thiserror::Error;
29
30mod depfile;
31
32pub use depfile::{CcDepfile, CcDiscoveredInputs, INCLUDE_MANIFEST_PREFIX, manifest_snapshot};
33
34/// Schema version embedded in canonical cc action descriptors.
35pub const ACTION_SCHEMA_VERSION: u8 = 1;
36/// Version of the cc argument and input model used to construct keys.
37pub const ADAPTER_VERSION: u8 = 1;
38
39/// Maximum discovered inputs, including include-manifest entries.
40pub const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
41/// Maximum total bytes digested for one action.
42pub const MAX_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
43/// Maximum file names summarized across all include manifests.
44pub const MAX_MANIFEST_ENTRIES: usize = 16 * 1024;
45
46/// Environment variables whose values enter every cc action key.
47///
48/// These change the compiler's own behavior without appearing in argv. They
49/// are recorded even when unset, so setting one is distinguishable from
50/// leaving it unset.
51pub const KEYED_ENVIRONMENT: &[&str] = &[
52    "IPHONEOS_DEPLOYMENT_TARGET",
53    "LANG",
54    "LC_ALL",
55    "LC_MESSAGES",
56    "MACOSX_DEPLOYMENT_TARGET",
57    "SDKROOT",
58    "SOURCE_DATE_EPOCH",
59    "TVOS_DEPLOYMENT_TARGET",
60    "WATCHOS_DEPLOYMENT_TARGET",
61    "XROS_DEPLOYMENT_TARGET",
62];
63
64/// Environment variables that force a bypass when set.
65///
66/// Each one either injects search paths the argv model cannot see, redirects
67/// sub-tool resolution beneath the identity probe, or makes the driver write an
68/// output the adapter does not model.
69pub const BYPASS_ENVIRONMENT: &[&str] = &[
70    "CPATH",
71    "COMPILER_PATH",
72    "CPLUS_INCLUDE_PATH",
73    "C_INCLUDE_PATH",
74    "DEPENDENCIES_OUTPUT",
75    "GCC_EXEC_PREFIX",
76    "OBJC_INCLUDE_PATH",
77    "SUNPRO_DEPENDENCIES",
78];
79
80/// Absolute roots whose contents are keyed verbatim rather than through a
81/// placeholder.
82///
83/// Files beneath these roots are still digested; keying the path verbatim only
84/// declares that the path itself is a machine property rather than a
85/// checkout-specific one, which is what makes system headers shareable between
86/// worktrees on one machine.
87pub const SYSTEM_ROOTS: &[&str] = &[
88    "/Applications/Xcode.app",
89    "/Library/Developer",
90    "/nix/store",
91    "/usr/include",
92    "/usr/lib",
93    "/usr/local/include",
94];
95
96const SUPPORTED_F_FLAGS: &[&str] = &[
97    "PIC",
98    "PIE",
99    "asynchronous-unwind-tables",
100    "cf-protection",
101    "color-diagnostics",
102    "data-sections",
103    "diagnostics-color",
104    "exceptions",
105    "fast-math",
106    "finite-math-only",
107    "function-sections",
108    "lto",
109    "math-errno",
110    "merge-all-constants",
111    "no-asynchronous-unwind-tables",
112    "no-builtin",
113    "no-common",
114    "no-exceptions",
115    "no-fast-math",
116    "no-finite-math-only",
117    "no-lto",
118    "no-math-errno",
119    "no-omit-frame-pointer",
120    "no-plt",
121    "no-reciprocal-math",
122    "no-rtti",
123    "no-semantic-interposition",
124    "no-stack-protector",
125    "no-strict-aliasing",
126    "no-tree-vectorize",
127    "no-unroll-loops",
128    "no-unwind-tables",
129    "omit-frame-pointer",
130    "pic",
131    "pie",
132    "reciprocal-math",
133    "rtti",
134    "sanitize-undefined-strip-path-components",
135    "short-enums",
136    "signed-char",
137    "stack-clash-protection",
138    "stack-protector",
139    "stack-protector-all",
140    "stack-protector-strong",
141    "strict-aliasing",
142    "tls-model",
143    "tree-vectorize",
144    "unroll-loops",
145    "unsigned-char",
146    "unwind-tables",
147    "visibility",
148    "visibility-inlines-hidden",
149    "wrapv",
150];
151
152const SUPPORTED_M_FLAGS: &[&str] = &[
153    "32",
154    "64",
155    "adx",
156    "aes",
157    "arch",
158    "arm",
159    "avx",
160    "avx2",
161    "avx512bf16",
162    "avx512bitalg",
163    "avx512bw",
164    "avx512cd",
165    "avx512dq",
166    "avx512f",
167    "avx512fp16",
168    "avx512ifma",
169    "avx512vbmi",
170    "avx512vbmi2",
171    "avx512vl",
172    "avx512vnni",
173    "avx512vpopcntdq",
174    "avxvnni",
175    "bmi",
176    "bmi2",
177    "cpu",
178    "crc",
179    "crypto",
180    "dotprod",
181    "f16c",
182    "float-abi",
183    "fma",
184    "fpu",
185    "gfni",
186    "iphoneos-version-min",
187    "lse",
188    "lzcnt",
189    "macosx-version-min",
190    "movbe",
191    "no-avx",
192    "no-avx2",
193    "no-avx512f",
194    "no-omit-leaf-frame-pointer",
195    "no-outline-atomics",
196    "no-red-zone",
197    "no-sse",
198    "no-sse2",
199    "omit-leaf-frame-pointer",
200    "outline-atomics",
201    "pclmul",
202    "popcnt",
203    "rdrnd",
204    "rdseed",
205    "red-zone",
206    "sha",
207    "sha512",
208    "soft-float",
209    "sse",
210    "sse2",
211    "sse3",
212    "sse4.1",
213    "sse4.2",
214    "sse4a",
215    "ssse3",
216    "sve",
217    "sve2",
218    "thumb",
219    "tune",
220    "vaes",
221    "vpclmulqdq",
222    "xsave",
223    "xsavec",
224    "xsaveopt",
225    "xsaves",
226];
227
228const SUPPORTED_O_FLAGS: &[&str] = &[
229    "-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Og", "-Os", "-Oz",
230];
231
232const SUPPORTED_G_FLAGS: &[&str] = &[
233    "-g",
234    "-g0",
235    "-g1",
236    "-g2",
237    "-g3",
238    "-gdwarf-2",
239    "-gdwarf-3",
240    "-gdwarf-4",
241    "-gdwarf-5",
242    "-gfull",
243    "-ggdb",
244    "-ggdb0",
245    "-ggdb1",
246    "-ggdb2",
247    "-ggdb3",
248];
249
250const SUPPORTED_BARE_FLAGS: &[&str] = &[
251    "-P", // Suppress preprocessor line markers; keyed even without -E.
252    "-ansi",
253    "-nostdinc",
254    "-nostdinc++",
255    "-pedantic",
256    "-pedantic-errors",
257    "-pipe",
258    "-pthread",
259    "-w",
260];
261
262const SEPARATE_PATH_FLAGS: &[&str] = &[
263    "-idirafter",
264    "-imacros",
265    "-include",
266    "-iquote",
267    "-isysroot",
268    "-isystem",
269];
270
271const TOOL_PASSTHROUGH_FLAGS: &[&str] = &["-Xassembler", "-Xclang", "-Xlinker", "-Xpreprocessor"];
272
273/// Assembler options whose effects are completely described by their text.
274///
275/// Other assembler options can name files that dependency discovery does not
276/// report, so they remain conservative passthrough bypasses.
277const SUPPORTED_ASSEMBLER_OPTIONS: &[&str] = &["--noexecstack"];
278
279const COMPILER_QUERY_FLAGS: &[&str] = &[
280    "--help",
281    "--version",
282    "-###",
283    // The `cc` crate probes with `-?` to tell an MSVC-style driver from a
284    // gcc-style one; neither answer is a compilation.
285    "-?",
286    "-dumpmachine",
287    "-dumpversion",
288    "-v",
289];
290
291/// Flags that rewrite a path prefix in the compiler's own output.
292///
293/// The left side is a real path and normalizes like any other; the right side
294/// is the text it is replaced with and enters the key verbatim.
295const PREFIX_MAP_FLAGS: &[&str] = &[
296    "-fdebug-prefix-map",
297    "-ffile-prefix-map",
298    "-fmacro-prefix-map",
299];
300
301impl CcBypassReason {
302    /// A stable, low-cardinality name for this reason.
303    ///
304    /// Many variants carry a path or a flag, so `Display` text cannot be
305    /// aggregated; statistics group by this instead.
306    pub fn kind(&self) -> &'static str {
307        self.into()
308    }
309
310    /// A concrete change that can make this invocation cacheable, when one is
311    /// available.
312    ///
313    /// Expected compiler probes and failures that require adapter support
314    /// return `None`; callers can still explain those from
315    /// [`CcBypassReason::kind`].
316    pub fn remediation(&self) -> Option<&'static str> {
317        match self {
318            Self::UnsupportedEnvironment(_) => Some(
319                "Unset the reported environment variable for this build so the compiler invocation describes all of its inputs.",
320            ),
321            Self::LocalCpuTarget(_) => Some(
322                "Replace the reported local-CPU option with an explicit architecture or CPU name.",
323            ),
324            Self::EmbeddedTimestampMacro(_) => Some(
325                "Remove the reported timestamp macro, or keep this compilation uncached if its changing value is intentional.",
326            ),
327            Self::SearchPathModifiedDuringCompilation(_) => Some(
328                "Generate headers before compilation instead of changing an include directory while the compiler is running.",
329            ),
330            Self::UnknownFlag(_) | Self::ToolPassthrough(_) => Some(
331                "Upgrade mbx or report the unmodeled compiler option. If you control the build script, removing the option can also make the compilation cacheable.",
332            ),
333            Self::UnmappedAbsolutePath(_) => Some(
334                "Move the input under a mapped project or system root, or keep this compilation uncached.",
335            ),
336            _ => None,
337        }
338    }
339}
340
341/// Reason a C or C++ invocation cannot safely use the action cache.
342///
343/// A bypass is an expected conservative outcome, not a compiler error. Match on
344/// [`CcBypassReason::kind`] for aggregation rather than on the variants.
345#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
346#[strum(serialize_all = "kebab-case")]
347#[non_exhaustive]
348pub enum CcBypassReason {
349    /// An argument cannot be represented in the canonical UTF-8 key.
350    #[error("compiler argument {index} is not valid UTF-8")]
351    NonUtf8Argument {
352        /// Zero-based index in the argument slice.
353        index: usize,
354    },
355    /// The driver was handed an argument file.
356    #[error("compiler response file is not modeled by the cache adapter: {0}")]
357    ResponseFile(String),
358    /// A compiler flag is not modeled by this adapter version.
359    #[error("compiler flag is not modeled by the cache adapter: {0}")]
360    UnknownFlag(String),
361    /// A recognized flag was given without its value.
362    #[error("compiler flag {0} is missing its value")]
363    MissingValue(String),
364    /// The invocation asks the driver about itself rather than compiling.
365    #[error("compiler invocation queries the driver instead of compiling")]
366    CompilerQuery,
367    /// The invocation is not a single-object compile.
368    #[error("compiler invocation does not compile with -c")]
369    NotACompile,
370    /// The invocation emits preprocessed source or assembly.
371    #[error("compiler invocation emits a non-object output: {0}")]
372    NonObjectOutput(String),
373    /// The source arrives on standard input and cannot be rediscovered.
374    #[error("compiler invocation reads its source from standard input")]
375    StandardInput,
376    /// No source file was given.
377    #[error("compiler invocation names no source file")]
378    MissingInput,
379    /// More than one source file was given.
380    #[error("compiler invocation names more than one source file")]
381    MultipleInputs,
382    /// No `-o` was given, so the object name follows driver defaults.
383    #[error("compiler invocation names no output file")]
384    MissingOutput,
385    /// The source language is outside the modeled set.
386    #[error("compiler input language is not modeled by the cache adapter: {0}")]
387    UnsupportedLanguage(String),
388    /// The caller asked for its own dependency output.
389    #[error("compiler invocation requests its own dependency output: {0}")]
390    CallerDependencyFlags(String),
391    /// Precompiled headers are not byte-hermetic key material.
392    #[error("precompiled headers are not modeled by the cache adapter: {0}")]
393    PrecompiledHeader(String),
394    /// Coverage instrumentation writes outputs beside the object.
395    #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
396    CoverageInstrumentation(String),
397    /// Split debug info writes a `.dwo` beside the object.
398    #[error("split debug output is not modeled by the cache adapter: {0}")]
399    SplitDebugOutput(String),
400    /// Temporary files are preserved beside the object.
401    #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
402    SaveTemps(String),
403    /// An option is smuggled to a sub-tool the adapter cannot model.
404    #[error("compiler flag forwards options to another tool: {0}")]
405    ToolPassthrough(String),
406    /// A compiler plugin makes the output depend on unmodeled code.
407    #[error("compiler plugins are not modeled by the cache adapter: {0}")]
408    Plugin(String),
409    /// An include search directory gained or lost a header while the compiler
410    /// ran, so the manifest recorded after it is not what the compilation saw.
411    #[error("include search directory changed during the compilation: {0}")]
412    SearchPathModifiedDuringCompilation(PathBuf),
413
414    /// The object kept a path the key normalized away.
415    ///
416    /// Remapping covers what the compiler records itself; a path the source
417    /// keeps as a string survives it, and publishing such an object would
418    /// share this checkout's directory under a key that says it does not
419    /// matter.
420    #[error("compilation output records a path its key normalized away: {0}")]
421    UnportableOutput(PathBuf),
422    /// The 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    #[serde(skip_serializing_if = "Option::is_none")]
682    path_binding: Option<CcPathBinding>,
683    version: u8,
684    kind: &'static str,
685    adapter_version: u8,
686    #[serde(skip_serializing_if = "Option::is_none")]
687    assembly_input_model: Option<u8>,
688    #[serde(skip_serializing_if = "Option::is_none")]
689    preprocessing: Option<PreprocessingContext>,
690    compiler: CcCompilerDescriptor,
691    arguments: Vec<String>,
692    environment: BTreeMap<String, Option<String>>,
693    inputs: Vec<CcInputDescriptor>,
694}
695
696#[derive(Debug, Serialize)]
697struct CcInvocationDescriptor {
698    version: u8,
699    kind: &'static str,
700    adapter_version: u8,
701    #[serde(skip_serializing_if = "Option::is_none")]
702    assembly_input_model: Option<u8>,
703    #[serde(skip_serializing_if = "Option::is_none")]
704    preprocessing: Option<PreprocessingContext>,
705    compiler: CcCompilerDescriptor,
706    arguments: Vec<String>,
707    required_inputs: Vec<String>,
708}
709
710/// Preprocessor text embeds literal paths in line markers and macros. Keep
711/// its invocation tied to those spellings instead of rewriting user output.
712#[derive(Debug, Serialize)]
713struct PreprocessingContext {
714    working_dir: PathBuf,
715    arguments: Vec<String>,
716    roots: BTreeMap<String, PathBuf>,
717}
718
719/// Literal path spellings retained by an object, unlike portable debug paths.
720#[derive(Debug, Serialize)]
721struct CcPathBinding {
722    working_dir: PathBuf,
723    arguments: Vec<Argument>,
724    roots: BTreeMap<String, PathBuf>,
725}
726
727/// Normalized input names from the last successful execution of one modeled
728/// compile.
729#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
730#[serde(deny_unknown_fields)]
731pub struct CcInputPrediction {
732    /// The output retained literal paths and requires a path-specific action key.
733    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
734    pub path_specific: bool,
735    /// Prediction schema version.
736    pub version: u8,
737    /// Normalized input paths, including include-manifest entries.
738    pub inputs: Vec<String>,
739    /// Names of environment variables that entered the key.
740    pub environment: Vec<String>,
741    /// Compiler wall time from the successful invocation that produced this
742    /// prediction. Zero means no timing hint was recorded.
743    #[serde(default, skip_serializing_if = "is_zero")]
744    pub compiler_duration_ns: u64,
745    /// Source file name associated with the timing hint.
746    #[serde(default, skip_serializing_if = "String::is_empty")]
747    pub source_name: String,
748}
749
750fn is_zero(value: &u64) -> bool {
751    *value == 0
752}
753
754/// One parsed and admitted argument.
755#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
756enum Argument {
757    /// Keyed verbatim.
758    Plain(String),
759    /// Keyed with its path normalized.
760    Path { flag: String, path: PathBuf },
761    /// A prefix rewrite: the source path normalizes, the replacement does not.
762    PrefixMap {
763        flag: String,
764        from: PathBuf,
765        to: String,
766    },
767    /// The source file.
768    Source(PathBuf),
769}
770
771/// A parsed, admitted C or C++ compile.
772#[derive(Debug, Clone, PartialEq, Eq)]
773pub struct CcInvocation {
774    arguments: Vec<Argument>,
775    source: PathBuf,
776    output: PathBuf,
777    include_dirs: Vec<PathBuf>,
778    required_inputs: Vec<PathBuf>,
779    language: CcLanguage,
780    preprocessed_assembly: bool,
781    preprocessing_arguments: Option<Vec<String>>,
782    sysroot: Option<PathBuf>,
783    caller_depfile: Option<CallerDepfile>,
784    /// Positions in the parsed command line of the dependency-list flags the
785    /// caller passed, which the shim leaves out when it runs the driver.
786    dependency_argument_indices: Vec<usize>,
787}
788
789/// A dependency list the caller asked the driver to write beside the object.
790///
791/// Build systems driving the compiler through a build script -- OpenSSL's
792/// makefiles under `openssl-src`, CMake under many `-sys` crates -- ask for one
793/// with `-MD` or `-MMD` so their own rebuild logic has something to read. The
794/// flags do not change the object, so they are not key material, and the file
795/// names paths on this machine, so it is never stored. The shim writes it
796/// itself from the dependency list it already collects, whether the object
797/// was compiled or restored.
798#[derive(Debug, Clone, PartialEq, Eq)]
799#[non_exhaustive]
800pub struct CallerDepfile {
801    /// Where the list goes: `-MF`, or the object's path with a `.d` extension.
802    pub path: PathBuf,
803    /// The rule's targets, from `-MT` and `-MQ`, or the object as `-o` named it.
804    pub targets: Vec<DepfileTarget>,
805    /// Whether system headers are left out, as `-MMD` does.
806    pub user_headers_only: bool,
807    /// Whether every header also gets an empty rule of its own, as `-MP` does.
808    pub phony_targets: bool,
809}
810
811/// One target of a caller's dependency rule.
812#[derive(Debug, Clone, PartialEq, Eq)]
813pub struct DepfileTarget {
814    /// The target text as the caller gave it.
815    pub name: String,
816    /// Whether characters special to make are quoted on output, as `-MQ`
817    /// asks; `-MT` writes the name literally.
818    pub quoted: bool,
819}
820
821impl CcInvocation {
822    /// The object's path made absolute against the compiler's working
823    /// directory, for reading and storing it. `-o` is often relative --
824    /// OpenSSL's makefiles name every object that way -- and a path handed to
825    /// the cache agent has to mean the same file from the agent's own
826    /// directory. Key material still normalizes the spelling as given.
827    pub fn output_in(&self, working_dir: &Path) -> PathBuf {
828        if self.output.is_absolute() {
829            normalize_components(&self.output)
830        } else {
831            normalize_components(&working_dir.join(&self.output))
832        }
833    }
834
835    /// The dependency list the caller asked for, if any.
836    pub fn caller_depfile(&self) -> Option<&CallerDepfile> {
837        self.caller_depfile.as_ref()
838    }
839
840    /// The command line to run the driver with: `arguments` as parsed, less
841    /// the caller's dependency-list flags. The shim asks the driver for its
842    /// own list, the driver honors only one `-MF`, and the caller's list is
843    /// written by the shim afterwards.
844    pub fn compiler_arguments(&self, arguments: &[OsString]) -> Vec<OsString> {
845        arguments
846            .iter()
847            .enumerate()
848            .filter(|(index, _)| !self.dependency_argument_indices.contains(index))
849            .map(|(_, argument)| argument.clone())
850            .collect()
851    }
852
853    /// Parse a driver command line, admitting modeled single-source compiles
854    /// and preprocessing to a named output.
855    pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
856        Parser::new(arguments).parse()
857    }
858
859    /// Parse a command line using the syntax of `family`.
860    pub fn parse_for(
861        arguments: &[OsString],
862        family: CcCompilerFamily,
863    ) -> Result<Self, CcBypassReason> {
864        if family.is_msvc() {
865            MsvcParser::new(arguments).parse()
866        } else {
867            let mut parser = Parser::new(arguments);
868            parser.family = family;
869            parser.parse()
870        }
871    }
872
873    /// Parse a command line using Microsoft `cl.exe` syntax.
874    pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
875        MsvcParser::new(arguments).parse()
876    }
877
878    /// Source file this invocation compiles.
879    pub fn source(&self) -> &Path {
880        &self.source
881    }
882
883    /// Whether this invocation writes named preprocessor text.
884    pub fn is_preprocessing(&self) -> bool {
885        self.preprocessing_arguments.is_some()
886    }
887
888    /// Output file this invocation produces.
889    pub fn output(&self) -> &Path {
890        &self.output
891    }
892
893    /// Include search directories named on the command line, in order.
894    pub fn include_dirs(&self) -> &[PathBuf] {
895        &self.include_dirs
896    }
897
898    /// Files that must appear among the discovered inputs.
899    pub fn required_inputs(&self) -> &[PathBuf] {
900        &self.required_inputs
901    }
902
903    /// Language the driver compiles.
904    pub fn language(&self) -> CcLanguage {
905        self.language
906    }
907
908    /// Reject assembler-time file directives that the preprocessor's
909    /// dependency list cannot name.
910    ///
911    /// This is a no-op for C and C++ inputs. For preprocessed assembly, every
912    /// file reported by dependency discovery is scanned conservatively before
913    /// an object can be published.
914    pub fn validate_discovered_inputs<'a>(
915        &self,
916        paths: impl IntoIterator<Item = &'a Path>,
917    ) -> Result<(), CcBypassReason> {
918        if !self.preprocessed_assembly {
919            return Ok(());
920        }
921        for path in paths {
922            if depfile::contains_assembler_input_directive(path)? {
923                return Err(CcBypassReason::AssemblerInputDirective(path.to_path_buf()));
924            }
925        }
926        Ok(())
927    }
928
929    /// Sysroot named on the command line, if any.
930    pub fn sysroot(&self) -> Option<&Path> {
931        self.sysroot.as_deref()
932    }
933
934    /// Short label used for timing statistics.
935    pub fn source_name(&self) -> String {
936        self.source
937            .file_name()
938            .map(|name| name.to_string_lossy().into_owned())
939            .unwrap_or_default()
940    }
941
942    /// Arguments to append so the driver writes a dependency list beside the
943    /// object.
944    ///
945    /// `-MD` rather than `-MMD`: system headers are exactly the inputs most
946    /// likely to change without any other key component noticing, because the
947    /// compiler identity does not cover the C library or the platform SDK.
948    pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
949        vec!["-MD".into(), "-MF".into(), depfile.into()]
950    }
951
952    /// Arguments to append so a driver from `family` writes its dependency
953    /// list beside the object.
954    pub fn dependency_arguments_for(
955        &self,
956        depfile: &Path,
957        family: CcCompilerFamily,
958    ) -> Vec<OsString> {
959        if family.is_msvc() {
960            vec!["/sourceDependencies".into(), depfile.into()]
961        } else {
962            self.dependency_arguments(depfile)
963        }
964    }
965
966    /// Arguments to append so `cl.exe` writes `/sourceDependencies` JSON.
967    pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
968        vec!["/sourceDependencies".into(), depfile.into()]
969    }
970
971    /// Digest of the pre-input fingerprint, used to look up a prediction.
972    pub fn invocation_digest(
973        &self,
974        context: &CcActionContext,
975    ) -> Result<CacheDigest, CcBypassReason> {
976        let builder = ActionBuilder::new(self, context.clone());
977        let descriptor = builder.invocation_descriptor()?;
978        let bytes = canonical_json(&descriptor)
979            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
980        Ok(CacheDigest::blake3(&bytes))
981    }
982
983    /// Build the canonical action for this invocation and its discovered
984    /// inputs.
985    pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
986        self.action_with_path_binding(context, false)
987    }
988
989    /// Build an action tied to literal path spellings when the output retained them.
990    /// Portable actions keep their existing keys.
991    pub fn action_with_path_binding(
992        &self,
993        context: CcActionContext,
994        path_specific: bool,
995    ) -> Result<CcAction, CcBypassReason> {
996        ActionBuilder::new(self, context).build(path_specific)
997    }
998
999    /// Record the normalized inputs of a successful compile so the next cold
1000    /// invocation can rebuild the same key before compiling.
1001    pub fn prediction(
1002        &self,
1003        context: &CcActionContext,
1004        compiler_duration_ns: u64,
1005    ) -> Result<CcInputPrediction, CcBypassReason> {
1006        let builder = ActionBuilder::new(self, context.clone());
1007        let mut inputs = context
1008            .inputs
1009            .iter()
1010            .map(|input| builder.normalize_input_path(&input.path))
1011            .collect::<Result<Vec<_>, _>>()?;
1012        inputs.sort();
1013        inputs.dedup();
1014        Ok(CcInputPrediction {
1015            path_specific: false,
1016            version: 1,
1017            inputs,
1018            environment: context.environment.keys().cloned().collect(),
1019            compiler_duration_ns,
1020            source_name: self.source_name(),
1021        })
1022    }
1023}
1024
1025impl CcInputPrediction {
1026    /// Rehash the predicted paths and recompute include manifests. The caller
1027    /// still recomputes the full action digest, so changed inputs are misses.
1028    pub fn discover(
1029        &self,
1030        working_dir: &Path,
1031        path_mappings: &[PathMapping],
1032        digests: &dyn FileDigestCache,
1033    ) -> Result<CcDiscoveredInputs, CcBypassReason> {
1034        if self.version != 1 {
1035            return Err(CcBypassReason::UnsupportedPrediction);
1036        }
1037        if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1038            return Err(CcBypassReason::UnsupportedPrediction);
1039        }
1040        let mappings = PathMapping::ordered(path_mappings);
1041        let mut files = BTreeSet::new();
1042        let mut directories = BTreeSet::new();
1043        for entry in &self.inputs {
1044            match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
1045                Some(directory) => {
1046                    directories.insert(denormalize_path(directory, &mappings)?);
1047                }
1048                None => {
1049                    files.insert(denormalize_path(entry, &mappings)?);
1050                }
1051            }
1052        }
1053        CcDiscoveredInputs::collect(working_dir, files, directories, digests)
1054    }
1055}
1056
1057/// Resolve a normalized key path back to a host path.
1058///
1059/// Placeholder entries expand through their mapping; a verbatim entry is
1060/// accepted only when it still lies beneath an admitted system root, so a
1061/// prediction cannot name an arbitrary absolute path.
1062fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
1063    for mapping in mappings {
1064        let prefix = format!("${{{}}}", mapping.placeholder);
1065        let suffix = if value == prefix {
1066            ""
1067        } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1068            suffix
1069        } else {
1070            continue;
1071        };
1072        if !mapping.root.is_absolute() || !safe_suffix(suffix) {
1073            return Err(CcBypassReason::InvalidPredictedInput(value.into()));
1074        }
1075        let mut path = normalize_components(&mapping.root);
1076        path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1077        return Ok(path);
1078    }
1079    // A verbatim entry names a machine path rather than a placeholder. It is
1080    // admitted only beneath a system root, and only spelled literally: a
1081    // traversal component would let a prediction reach outside that root.
1082    let path = PathBuf::from(value);
1083    if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
1084        return Ok(path);
1085    }
1086    Err(CcBypassReason::InvalidPredictedInput(value.into()))
1087}
1088
1089fn safe_suffix(suffix: &str) -> bool {
1090    suffix.is_empty()
1091        || !suffix.split('/').any(|component| {
1092            component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
1093        })
1094}
1095
1096/// Whether a path lies beneath a root whose location is a machine property.
1097pub fn is_system_path(path: &Path) -> bool {
1098    SYSTEM_ROOTS
1099        .iter()
1100        .any(|root| path.starts_with(Path::new(root)))
1101        || developer_roots().iter().any(|root| path.starts_with(root))
1102}
1103
1104// Xcode may be renamed or installed outside /Applications. Match the active
1105// developer directory rather than assuming the application bundle's name.
1106// Keep both spellings because compiler depfiles may resolve SDK symlinks.
1107#[cfg(target_os = "macos")]
1108fn developer_roots() -> &'static [PathBuf] {
1109    static ROOTS: std::sync::OnceLock<Vec<PathBuf>> = std::sync::OnceLock::new();
1110    ROOTS.get_or_init(|| {
1111        let selected = std::env::var_os("DEVELOPER_DIR")
1112            .filter(|value| !value.is_empty())
1113            .map(PathBuf::from)
1114            .or_else(|| {
1115                let output = std::process::Command::new("/usr/bin/xcode-select")
1116                    .arg("--print-path")
1117                    .output()
1118                    .ok()?;
1119                if !output.status.success() {
1120                    return None;
1121                }
1122                Some(PathBuf::from(
1123                    std::str::from_utf8(&output.stdout).ok()?.trim(),
1124                ))
1125            });
1126        selected.map(developer_directory_roots).unwrap_or_default()
1127    })
1128}
1129
1130#[cfg(not(target_os = "macos"))]
1131fn developer_roots() -> &'static [PathBuf] {
1132    &[]
1133}
1134
1135#[cfg(any(target_os = "macos", all(test, unix)))]
1136fn developer_directory_roots(mut root: PathBuf) -> Vec<PathBuf> {
1137    if !root.is_absolute() || root == Path::new("/") {
1138        return Vec::new();
1139    }
1140    // xcrun also accepts DEVELOPER_DIR pointing at the application bundle.
1141    if root.extension().is_some_and(|extension| extension == "app") {
1142        root = root.join("Contents/Developer");
1143    }
1144    root = normalize_components(&root);
1145    if root == Path::new("/") {
1146        return Vec::new();
1147    }
1148    let mut roots = vec![root.clone()];
1149    if let Ok(canonical) = std::fs::canonicalize(&root)
1150        && canonical != root
1151    {
1152        roots.push(canonical);
1153    }
1154    roots
1155}
1156
1157fn normalize_components(path: &Path) -> PathBuf {
1158    let mut normalized = PathBuf::new();
1159    for component in path.components() {
1160        match component {
1161            Component::CurDir => {}
1162            Component::ParentDir => {
1163                normalized.pop();
1164            }
1165            component => normalized.push(component.as_os_str()),
1166        }
1167    }
1168    normalized
1169}
1170
1171/// Read the modeled environment, rejecting variables that change the compile in
1172/// a way the argv model cannot see.
1173pub fn environment_inputs<F>(
1174    lookup: F,
1175    sysroot: Option<&Path>,
1176) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1177where
1178    F: Fn(&str) -> Option<String>,
1179{
1180    environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
1181}
1182
1183/// Read the modeled environment for a particular compiler family.
1184pub fn environment_inputs_for<F>(
1185    lookup: F,
1186    sysroot: Option<&Path>,
1187    family: CcCompilerFamily,
1188) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
1189where
1190    F: Fn(&str) -> Option<String>,
1191{
1192    for name in BYPASS_ENVIRONMENT {
1193        if lookup(name).is_some() {
1194            return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
1195        }
1196    }
1197    let mut environment = BTreeMap::new();
1198    for name in KEYED_ENVIRONMENT {
1199        // An explicit `-isysroot` on the command line already pins the SDK, and
1200        // it is what the driver honors, so the variable stops being an input.
1201        if *name == "SDKROOT" && sysroot.is_some() {
1202            continue;
1203        }
1204        environment.insert((*name).to_string(), lookup(name));
1205    }
1206    if family.is_msvc() {
1207        // INCLUDE changes header resolution without appearing in argv. The
1208        // toolset and SDK versions make the otherwise machine-local paths
1209        // meaningful when action records move between hosts.
1210        for name in [
1211            "INCLUDE",
1212            "VCToolsVersion",
1213            "WindowsSDKVersion",
1214            "UCRTVersion",
1215        ] {
1216            environment.insert(name.into(), lookup(name));
1217        }
1218        for name in ["CL", "_CL_"] {
1219            if lookup(name).is_some() {
1220                return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
1221            }
1222        }
1223    }
1224    Ok(environment)
1225}
1226
1227struct ActionBuilder<'a> {
1228    invocation: &'a CcInvocation,
1229    context: CcActionContext,
1230    mappings: Vec<PathMapping>,
1231    /// Reuse parent resolution across the inputs of this action only.
1232    aliases: PathAliases,
1233}
1234
1235impl<'a> ActionBuilder<'a> {
1236    fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
1237        context.path_mappings = PathMapping::ordered(&context.path_mappings);
1238        let mappings = resolve_path_mappings(&context.path_mappings);
1239        Self {
1240            invocation,
1241            context,
1242            mappings,
1243            aliases: PathAliases::default(),
1244        }
1245    }
1246
1247    fn build(self, path_specific: bool) -> Result<CcAction, CcBypassReason> {
1248        self.validate_mappings()?;
1249        let invocation = self.invocation_descriptor()?;
1250
1251        let mut inputs = BTreeMap::<String, CacheDigest>::new();
1252        for input in &self.context.inputs {
1253            input.digest.validate().map_err(|_| {
1254                CcBypassReason::InvalidInputDigest(input.path.display().to_string())
1255            })?;
1256            let path = self.normalize_input_path(&input.path)?;
1257            if inputs
1258                .insert(path.clone(), input.digest.clone())
1259                .is_some_and(|existing| existing != input.digest)
1260            {
1261                return Err(CcBypassReason::ConflictingInput(path));
1262            }
1263        }
1264        let required = self
1265            .invocation
1266            .required_inputs
1267            .iter()
1268            .map(|path| self.normalize_path(path))
1269            .collect::<Result<BTreeSet<_>, _>>()?;
1270        if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1271            return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
1272        }
1273        let inputs = inputs
1274            .into_iter()
1275            .map(|(path, digest)| CcInputDescriptor { path, digest })
1276            .collect();
1277        let descriptor = CcActionDescriptor {
1278            path_binding: path_specific.then(|| CcPathBinding {
1279                working_dir: self.context.working_dir.clone(),
1280                arguments: self.invocation.arguments.clone(),
1281                roots: self
1282                    .context
1283                    .path_mappings
1284                    .iter()
1285                    .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1286                    .collect(),
1287            }),
1288            version: ACTION_SCHEMA_VERSION,
1289            kind: "cc",
1290            adapter_version: ADAPTER_VERSION,
1291            assembly_input_model: invocation.assembly_input_model,
1292            preprocessing: invocation.preprocessing,
1293            compiler: invocation.compiler,
1294            arguments: invocation.arguments,
1295            environment: self.context.environment.clone(),
1296            inputs,
1297        };
1298        let bytes = canonical_json(&descriptor)
1299            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1300        let digest = CacheDigest::blake3(&bytes);
1301        Ok(CcAction { digest, bytes })
1302    }
1303
1304    fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1305        self.validate_mappings()?;
1306        let arguments = self
1307            .invocation
1308            .arguments
1309            .iter()
1310            .map(|argument| self.normalize_argument(argument))
1311            .collect::<Result<Vec<_>, _>>()?;
1312        let required_inputs = self
1313            .invocation
1314            .required_inputs
1315            .iter()
1316            .map(|path| self.normalize_path(path))
1317            .collect::<Result<BTreeSet<_>, _>>()?
1318            .into_iter()
1319            .collect();
1320        Ok(CcInvocationDescriptor {
1321            version: ACTION_SCHEMA_VERSION,
1322            kind: "cc",
1323            adapter_version: ADAPTER_VERSION,
1324            // Version the assembly-only safety model independently so adding
1325            // support does not invalidate every existing C and C++ entry.
1326            assembly_input_model: self.invocation.preprocessed_assembly.then_some(1),
1327            preprocessing: self
1328                .invocation
1329                .preprocessing_arguments
1330                .as_ref()
1331                .map(|arguments| PreprocessingContext {
1332                    working_dir: self.context.working_dir.clone(),
1333                    arguments: arguments.clone(),
1334                    roots: self
1335                        .mappings
1336                        .iter()
1337                        .map(|mapping| (mapping.placeholder.clone(), mapping.root.clone()))
1338                        .collect(),
1339                }),
1340            compiler: self.compiler_descriptor(),
1341            arguments,
1342            required_inputs,
1343        })
1344    }
1345
1346    fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1347        CcCompilerDescriptor {
1348            assembler: self.context.compiler.assembler.clone(),
1349            family: self.context.compiler.family.as_str().into(),
1350            target: self.context.compiler.target.clone(),
1351            version_text: self.context.compiler.version_text.clone(),
1352        }
1353    }
1354
1355    fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1356        if !self.context.working_dir.is_absolute() {
1357            return Err(CcBypassReason::RelativeWorkingDirectory(
1358                self.context.working_dir.clone(),
1359            ));
1360        }
1361        let mut roots = BTreeSet::new();
1362        let mut placeholders = BTreeSet::new();
1363        for mapping in &self.context.path_mappings {
1364            if !mapping.root.is_absolute() {
1365                return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1366            }
1367            if mapping.placeholder.is_empty()
1368                || !mapping
1369                    .placeholder
1370                    .bytes()
1371                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1372                || !roots.insert(normalize_components(&mapping.root))
1373                || !placeholders.insert(&mapping.placeholder)
1374            {
1375                return Err(CcBypassReason::InvalidPathPlaceholder(
1376                    mapping.placeholder.clone(),
1377                ));
1378            }
1379        }
1380        Ok(())
1381    }
1382
1383    fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1384        match argument {
1385            Argument::Plain(value) => Ok(value.clone()),
1386            Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1387            Argument::PrefixMap { flag, from, to } => {
1388                Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1389            }
1390            Argument::Source(path) => Ok(self.normalize_path(path)?),
1391        }
1392    }
1393
1394    /// Normalize a path that names a compilation input or search root.
1395    ///
1396    /// A path beneath a mapped root becomes a placeholder so equivalent
1397    /// checkouts agree. A path beneath a system root stays verbatim: its
1398    /// location is a property of the machine, and its contents are digested
1399    /// like any other input.
1400    fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1401        match normalize_resolved_mapped_path_with(
1402            &self.aliases,
1403            path,
1404            &self.context.working_dir,
1405            &self.mappings,
1406        ) {
1407            Ok(normalized) => Ok(normalized),
1408            Err(reason) => {
1409                let absolute = absolute_path(path, &self.context.working_dir);
1410                if is_system_path(&absolute) {
1411                    return absolute
1412                        .to_str()
1413                        .map(ToOwned::to_owned)
1414                        .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1415                }
1416                Err(reason.into())
1417            }
1418        }
1419    }
1420
1421    fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1422        match path.to_str().and_then(|path| {
1423            path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1424                .map(ToOwned::to_owned)
1425        }) {
1426            Some(directory) => Ok(format!(
1427                "{INCLUDE_MANIFEST_PREFIX}{}",
1428                self.normalize_path(Path::new(&directory))?
1429            )),
1430            None => self.normalize_path(path),
1431        }
1432    }
1433}
1434
1435fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1436    if path.is_absolute() {
1437        normalize_components(path)
1438    } else {
1439        normalize_components(&working_dir.join(path))
1440    }
1441}
1442
1443struct Parser<'a> {
1444    family: CcCompilerFamily,
1445    arguments: &'a [OsString],
1446    index: usize,
1447    parsed: Vec<Argument>,
1448    source: Option<PathBuf>,
1449    output: Option<PathBuf>,
1450    include_dirs: Vec<PathBuf>,
1451    required_inputs: Vec<PathBuf>,
1452    sysroot: Option<PathBuf>,
1453    explicit_language: Option<CcLanguage>,
1454    preprocessed_assembly: bool,
1455    compiling: bool,
1456    preprocessing: bool,
1457    dependency: DependencyRequest,
1458}
1459
1460/// What the caller's `-M` flags asked for, gathered while parsing.
1461#[derive(Debug, Default)]
1462struct DependencyRequest {
1463    /// `Some(false)` for `-MD`, `Some(true)` for `-MMD`.
1464    user_headers_only: Option<bool>,
1465    file: Option<PathBuf>,
1466    targets: Vec<DepfileTarget>,
1467    phony_targets: bool,
1468    /// The last `-MF`, `-MT`, `-MQ`, or `-MP` seen, for the bypass a request
1469    /// without `-MD` or `-MMD` reports.
1470    modifier: Option<String>,
1471    indices: Vec<usize>,
1472}
1473
1474impl<'a> Parser<'a> {
1475    fn new(arguments: &'a [OsString]) -> Self {
1476        Self {
1477            family: CcCompilerFamily::Gcc,
1478            arguments,
1479            index: 0,
1480            parsed: Vec::new(),
1481            source: None,
1482            output: None,
1483            include_dirs: Vec::new(),
1484            required_inputs: Vec::new(),
1485            sysroot: None,
1486            explicit_language: None,
1487            preprocessed_assembly: false,
1488            compiling: false,
1489            preprocessing: false,
1490            dependency: DependencyRequest::default(),
1491        }
1492    }
1493
1494    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1495        while self.index < self.arguments.len() {
1496            let value = self.current()?.to_string();
1497            self.index += 1;
1498            if value == "-" {
1499                return Err(CcBypassReason::StandardInput);
1500            }
1501            if let Some(argfile) = value.strip_prefix('@') {
1502                return Err(CcBypassReason::ResponseFile(argfile.into()));
1503            }
1504            if value.starts_with('-') {
1505                self.parse_flag(&value)?;
1506            } else {
1507                self.parse_input(&value)?;
1508            }
1509        }
1510
1511        if !self.compiling && !self.preprocessing {
1512            return Err(CcBypassReason::NotACompile);
1513        }
1514        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1515        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1516        if self.preprocessing && output == Path::new("-") {
1517            return Err(CcBypassReason::NonObjectOutput("-E -o -".into()));
1518        }
1519        // GCC interprets -o as the dependency output with -E -MD unless -MF
1520        // is explicit. Do not turn that command into a different operation.
1521        if self.preprocessing
1522            && self.dependency.user_headers_only.is_some()
1523            && self.dependency.file.is_none()
1524        {
1525            return Err(CcBypassReason::CallerDependencyFlags(
1526                "-E -MD/-MMD without -MF".into(),
1527            ));
1528        }
1529        if self.preprocessing && self.dependency.file.as_deref() == Some(Path::new("-")) {
1530            return Err(CcBypassReason::CallerDependencyFlags("-E -MF -".into()));
1531        }
1532        let language = self.language(&source)?;
1533        let preprocessed_assembly = self.preprocessed_assembly
1534            || (self.explicit_language.is_none()
1535                && source.extension().and_then(|value| value.to_str()) == Some("S"));
1536        self.required_inputs.push(source.clone());
1537        let caller_depfile = match self.dependency.user_headers_only {
1538            Some(user_headers_only) => Some(CallerDepfile {
1539                path: self
1540                    .dependency
1541                    .file
1542                    .unwrap_or_else(|| output.with_extension("d")),
1543                targets: if self.dependency.targets.is_empty() {
1544                    vec![DepfileTarget {
1545                        // GCC -E ignores -o when choosing the dependency
1546                        // target; Clang retains it. Neither uses the depfile.
1547                        name: if self.preprocessing && self.family == CcCompilerFamily::Gcc {
1548                            Path::new(source.file_name().ok_or(CcBypassReason::MissingInput)?)
1549                                .with_extension("o")
1550                                .to_string_lossy()
1551                                .into_owned()
1552                        } else {
1553                            output.to_string_lossy().into_owned()
1554                        },
1555                        quoted: true,
1556                    }]
1557                } else {
1558                    self.dependency.targets
1559                },
1560                user_headers_only,
1561                phony_targets: self.dependency.phony_targets,
1562            }),
1563            // `-MF` or `-MT` without `-MD` or `-MMD` writes nothing, but it is
1564            // not a shape the cc crate or a makefile produces, so it is not
1565            // one to guess at.
1566            None => match self.dependency.modifier {
1567                Some(modifier) => return Err(CcBypassReason::CallerDependencyFlags(modifier)),
1568                None => None,
1569            },
1570        };
1571        Ok(CcInvocation {
1572            arguments: self.parsed,
1573            source,
1574            output,
1575            include_dirs: self.include_dirs,
1576            required_inputs: self.required_inputs,
1577            language,
1578            preprocessed_assembly,
1579            preprocessing_arguments: self.preprocessing.then(|| {
1580                self.arguments
1581                    .iter()
1582                    .map(|arg| arg.to_str().expect("arguments were validated").to_owned())
1583                    .collect()
1584            }),
1585            sysroot: self.sysroot,
1586            caller_depfile,
1587            dependency_argument_indices: self.dependency.indices,
1588        })
1589    }
1590
1591    /// A `-M` flag: the caller wants a dependency list beside the object.
1592    ///
1593    /// `-MD` and `-MMD` compile as well as listing, so they are taken, with
1594    /// the `-MF`, `-MT`, `-MQ`, and `-MP` that shape the list. None of them
1595    /// enters the key: the object is the same with or without them. `-M` and
1596    /// `-MM` stop after preprocessing, and `-MG` changes what a missing header
1597    /// means, so those still bypass.
1598    fn parse_dependency_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1599        let start = self.index - 1;
1600        match option {
1601            "D" => self.dependency.user_headers_only = Some(false),
1602            "MD" => self.dependency.user_headers_only = Some(true),
1603            "P" => {
1604                self.dependency.phony_targets = true;
1605                self.dependency.modifier = Some(value.into());
1606            }
1607            _ if option.starts_with('F') => {
1608                let file = self.take_value("-MF", Some(&option[1..]))?;
1609                self.dependency.file = Some(PathBuf::from(file));
1610                self.dependency.modifier = Some("-MF".into());
1611            }
1612            _ if option.starts_with('T') || option.starts_with('Q') => {
1613                let flag = &value[..3];
1614                let name = self.take_value(flag, Some(&option[1..]))?;
1615                self.dependency.targets.push(DepfileTarget {
1616                    name,
1617                    quoted: option.starts_with('Q'),
1618                });
1619                self.dependency.modifier = Some(flag.into());
1620            }
1621            _ => return Err(CcBypassReason::CallerDependencyFlags(value.into())),
1622        }
1623        self.dependency.indices.extend(start..self.index);
1624        Ok(())
1625    }
1626
1627    fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1628        if let Some(language) = self.explicit_language {
1629            return Ok(language);
1630        }
1631        let extension = source
1632            .extension()
1633            .and_then(|extension| extension.to_str())
1634            .unwrap_or_default();
1635        match extension {
1636            // Preprocessed assembly goes through the C driver: the preprocessor
1637            // resolves its includes, which is what dependency discovery reads,
1638            // and the assembler that then produces the object is already part
1639            // of a GCC identity. Plain `.s` is not preprocessed and reports no
1640            // dependencies, so it stays out.
1641            "c" | "S" => Ok(CcLanguage::C),
1642            "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1643            _ => Err(CcBypassReason::UnsupportedLanguage(
1644                source.display().to_string(),
1645            )),
1646        }
1647    }
1648
1649    fn current(&self) -> Result<&str, CcBypassReason> {
1650        self.arguments[self.index]
1651            .to_str()
1652            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1653    }
1654
1655    fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1656        if let Some(value) = inline
1657            && !value.is_empty()
1658        {
1659            return Ok(value.into());
1660        }
1661        if self.index >= self.arguments.len() {
1662            return Err(CcBypassReason::MissingValue(flag.into()));
1663        }
1664        let value = self.current()?.to_string();
1665        self.index += 1;
1666        Ok(value)
1667    }
1668
1669    fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1670        if self.source.is_some() {
1671            return Err(CcBypassReason::MultipleInputs);
1672        }
1673        let path = PathBuf::from(value);
1674        // With an explicit `-x`, the driver ignores the extension entirely;
1675        // without one, the extension is the only thing that decides the
1676        // language, so an unmodeled extension has to bypass here.
1677        if self.explicit_language.is_none() {
1678            let extension = path
1679                .extension()
1680                .and_then(|extension| extension.to_str())
1681                .unwrap_or_default();
1682            if !matches!(extension, "c" | "S" | "cc" | "cpp" | "cxx" | "c++") {
1683                return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1684            }
1685        }
1686        self.source = Some(path.clone());
1687        self.parsed.push(Argument::Source(path));
1688        Ok(())
1689    }
1690
1691    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1692        if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1693            return Err(CcBypassReason::CompilerQuery);
1694        }
1695        if value == "-E" {
1696            self.preprocessing = true;
1697            self.parsed.push(Argument::Plain(value.into()));
1698            return Ok(());
1699        }
1700        if value == "-S" {
1701            return Err(CcBypassReason::NonObjectOutput(value.into()));
1702        }
1703        if let Some(option) = value.strip_prefix("-M") {
1704            return self.parse_dependency_flag(value, option);
1705        }
1706        if value.starts_with("-save-temps") {
1707            return Err(CcBypassReason::SaveTemps(value.into()));
1708        }
1709        if value == "--coverage" {
1710            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1711        }
1712        if let Some(options) = value.strip_prefix("-Wa,")
1713            && !options.is_empty()
1714            && options
1715                .split(',')
1716                .all(|option| SUPPORTED_ASSEMBLER_OPTIONS.contains(&option))
1717        {
1718            self.parsed.push(Argument::Plain(value.into()));
1719            return Ok(());
1720        }
1721        if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1722            || value.starts_with("-Wp,")
1723            || value.starts_with("-Wa,")
1724            || value.starts_with("-Wl,")
1725        {
1726            // The forwarded options are the compilation's real inputs and they
1727            // are not modeled, so consuming the value would not make this safe.
1728            return Err(CcBypassReason::ToolPassthrough(value.into()));
1729        }
1730        if value.starts_with("-include-pch") || value == "-emit-pch" {
1731            return Err(CcBypassReason::PrecompiledHeader(value.into()));
1732        }
1733
1734        if value == "-c" {
1735            self.compiling = true;
1736            self.parsed.push(Argument::Plain(value.into()));
1737            return Ok(());
1738        }
1739        if SUPPORTED_BARE_FLAGS.contains(&value)
1740            || SUPPORTED_O_FLAGS.contains(&value)
1741            || SUPPORTED_G_FLAGS.contains(&value)
1742            || value.starts_with("-std=")
1743        {
1744            self.parsed.push(Argument::Plain(value.into()));
1745            return Ok(());
1746        }
1747        if let Some(rest) = value.strip_prefix("-o") {
1748            let path = self.take_value("-o", Some(rest))?;
1749            // A repeated `-o` follows the driver: the last one names the file
1750            // that is produced. Every occurrence still enters the key.
1751            self.output = Some(PathBuf::from(&path));
1752            self.parsed.push(Argument::Path {
1753                flag: "-o".into(),
1754                path: PathBuf::from(path),
1755            });
1756            return Ok(());
1757        }
1758        if let Some(rest) = value.strip_prefix("-I") {
1759            let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1760            self.include_dirs.push(path.clone());
1761            self.parsed.push(Argument::Path {
1762                flag: "-I".into(),
1763                path,
1764            });
1765            return Ok(());
1766        }
1767        if SEPARATE_PATH_FLAGS.contains(&value) {
1768            let path = PathBuf::from(self.take_value(value, None)?);
1769            match value {
1770                "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1771                "-isysroot" => self.sysroot = Some(path.clone()),
1772                // `-include` and `-imacros` are deliberately not required
1773                // inputs. The driver resolves the name through the include
1774                // chain, so the file need not exist relative to the working
1775                // directory, and the dependency list names it at whatever path
1776                // it was actually found at.
1777                _ => {}
1778            }
1779            self.parsed.push(Argument::Path {
1780                flag: value.into(),
1781                path,
1782            });
1783            return Ok(());
1784        }
1785        // `--include=<file>` is the long spelling of `-include <file>`; the
1786        // `cc` crate emits it for prefixed headers.
1787        if let Some(rest) = value.strip_prefix("--include=") {
1788            let path = PathBuf::from(rest);
1789            self.parsed.push(Argument::Path {
1790                flag: "-include".into(),
1791                path,
1792            });
1793            return Ok(());
1794        }
1795        if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1796            value
1797                .strip_prefix(&format!("{flag}="))
1798                .map(|rest| (*flag, rest))
1799        }) {
1800            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1801            self.parsed.push(Argument::PrefixMap {
1802                flag: flag.into(),
1803                from: PathBuf::from(from),
1804                to: to.into(),
1805            });
1806            return Ok(());
1807        }
1808        // `--param name=value` tunes the optimizer; its text fully describes it.
1809        if value == "--param" {
1810            let parameter = self.take_value("--param", None)?;
1811            self.parsed
1812                .push(Argument::Plain(format!("--param={parameter}")));
1813            return Ok(());
1814        }
1815        if let Some(parameter) = value.strip_prefix("--param=") {
1816            self.parsed
1817                .push(Argument::Plain(format!("--param={parameter}")));
1818            return Ok(());
1819        }
1820        if let Some(rest) = value.strip_prefix("--sysroot=") {
1821            let path = PathBuf::from(rest);
1822            self.sysroot = Some(path.clone());
1823            self.parsed.push(Argument::Path {
1824                flag: "--sysroot".into(),
1825                path,
1826            });
1827            return Ok(());
1828        }
1829        if let Some(rest) = value
1830            .strip_prefix("-D")
1831            .or_else(|| value.strip_prefix("-U"))
1832        {
1833            let flag = &value[..2];
1834            let definition = self.take_value(flag, Some(rest))?;
1835            self.parsed
1836                .push(Argument::Plain(format!("{flag}{definition}")));
1837            return Ok(());
1838        }
1839        if let Some(rest) = value.strip_prefix("-x") {
1840            let language = self.take_value("-x", Some(rest))?;
1841            self.explicit_language = Some(match language.as_str() {
1842                "c" => CcLanguage::C,
1843                "assembler-with-cpp" => {
1844                    self.preprocessed_assembly = true;
1845                    CcLanguage::C
1846                }
1847                "c++" => CcLanguage::Cxx,
1848                other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1849            });
1850            self.parsed.push(Argument::Plain(format!("-x{language}")));
1851            return Ok(());
1852        }
1853        if let Some(target) = value.strip_prefix("--target=") {
1854            self.parsed
1855                .push(Argument::Plain(format!("--target={target}")));
1856            return Ok(());
1857        }
1858        if value == "-target" {
1859            let target = self.take_value("-target", None)?;
1860            self.parsed
1861                .push(Argument::Plain(format!("--target={target}")));
1862            return Ok(());
1863        }
1864        if value == "-arch" {
1865            let arch = self.take_value("-arch", None)?;
1866            self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1867            return Ok(());
1868        }
1869        if let Some(option) = value.strip_prefix("-f") {
1870            return self.parse_f_flag(value, option);
1871        }
1872        if let Some(option) = value.strip_prefix("-m") {
1873            return self.parse_m_flag(value, option);
1874        }
1875        if value.starts_with("-g") {
1876            // `-gsplit-dwarf` writes a `.dwo` beside the object; every other
1877            // unlisted `-g` spelling is simply unmodeled.
1878            return Err(if value.starts_with("-gsplit-dwarf") {
1879                CcBypassReason::SplitDebugOutput(value.into())
1880            } else {
1881                CcBypassReason::UnknownFlag(value.into())
1882            });
1883        }
1884        if value.starts_with("-W") {
1885            // Warning selection changes only diagnostics, which are replayed
1886            // from the cache, and the exit status, and only successful
1887            // compiles are ever published.
1888            self.parsed.push(Argument::Plain(value.into()));
1889            return Ok(());
1890        }
1891        Err(CcBypassReason::UnknownFlag(value.into()))
1892    }
1893
1894    fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1895        if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1896            return Err(CcBypassReason::Plugin(value.into()));
1897        }
1898        if option.starts_with("profile-") || option == "test-coverage" {
1899            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1900        }
1901        let name = option.split_once('=').map_or(option, |(name, _)| name);
1902        if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1903            return Err(CcBypassReason::UnknownFlag(value.into()));
1904        }
1905        self.parsed.push(Argument::Plain(value.into()));
1906        Ok(())
1907    }
1908
1909    fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1910        if option == "llvm" {
1911            return Err(CcBypassReason::ToolPassthrough(value.into()));
1912        }
1913        // `-march=native` and its relatives resolve against whatever CPU this
1914        // machine has. The resulting object is not a function of the key, so
1915        // another machine could otherwise restore code its processor cannot
1916        // run.
1917        if let Some((name, selection)) = option.split_once('=')
1918            && matches!(name, "arch" | "cpu" | "tune")
1919            && matches!(selection, "native" | "host")
1920        {
1921            return Err(CcBypassReason::LocalCpuTarget(value.into()));
1922        }
1923        let name = option.split_once('=').map_or(option, |(name, _)| name);
1924        if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1925            return Err(CcBypassReason::UnknownFlag(value.into()));
1926        }
1927        self.parsed.push(Argument::Plain(value.into()));
1928        Ok(())
1929    }
1930}
1931
1932/// Conservative parser for the command lines emitted by the `cc` crate for
1933/// Microsoft's compiler. It intentionally admits only flags whose effects are
1934/// either present in argv or covered by dependency discovery.
1935struct MsvcParser<'a> {
1936    arguments: &'a [OsString],
1937    index: usize,
1938    parsed: Vec<Argument>,
1939    source: Option<PathBuf>,
1940    output: Option<PathBuf>,
1941    include_dirs: Vec<PathBuf>,
1942    required_inputs: Vec<PathBuf>,
1943    explicit_language: Option<CcLanguage>,
1944    compiling: bool,
1945}
1946
1947impl<'a> MsvcParser<'a> {
1948    fn new(arguments: &'a [OsString]) -> Self {
1949        Self {
1950            arguments,
1951            index: 0,
1952            parsed: Vec::new(),
1953            source: None,
1954            output: None,
1955            include_dirs: Vec::new(),
1956            required_inputs: Vec::new(),
1957            explicit_language: None,
1958            compiling: false,
1959        }
1960    }
1961
1962    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1963        while self.index < self.arguments.len() {
1964            let value = self.current()?.to_owned();
1965            self.index += 1;
1966            if let Some(file) = value.strip_prefix('@') {
1967                return Err(CcBypassReason::ResponseFile(file.into()));
1968            }
1969            if value.starts_with('/') || value.starts_with('-') {
1970                self.parse_flag(&value)?;
1971            } else {
1972                self.add_source(&value)?;
1973            }
1974        }
1975        if !self.compiling {
1976            return Err(CcBypassReason::NotACompile);
1977        }
1978        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1979        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1980        let language = self.explicit_language.unwrap_or_else(|| {
1981            if source
1982                .extension()
1983                .and_then(|value| value.to_str())
1984                .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1985            {
1986                CcLanguage::C
1987            } else {
1988                CcLanguage::Cxx
1989            }
1990        });
1991        self.required_inputs.push(source.clone());
1992        Ok(CcInvocation {
1993            arguments: self.parsed,
1994            source,
1995            output,
1996            include_dirs: self.include_dirs,
1997            required_inputs: self.required_inputs,
1998            language,
1999            preprocessed_assembly: false,
2000            preprocessing_arguments: None,
2001            sysroot: None,
2002            caller_depfile: None,
2003            dependency_argument_indices: Vec::new(),
2004        })
2005    }
2006
2007    fn current(&self) -> Result<&str, CcBypassReason> {
2008        self.arguments[self.index]
2009            .to_str()
2010            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
2011    }
2012
2013    fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
2014        if !attached.is_empty() {
2015            return Ok(attached.into());
2016        }
2017        if self.index == self.arguments.len() {
2018            return Err(CcBypassReason::MissingValue(flag.into()));
2019        }
2020        let value = self.current()?.to_owned();
2021        self.index += 1;
2022        Ok(value)
2023    }
2024
2025    fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
2026        if self.source.is_some() {
2027            return Err(CcBypassReason::MultipleInputs);
2028        }
2029        let path = PathBuf::from(value);
2030        if self.explicit_language.is_none()
2031            && !path
2032                .extension()
2033                .and_then(|value| value.to_str())
2034                .is_some_and(|value| {
2035                    matches!(
2036                        value.to_ascii_lowercase().as_str(),
2037                        "c" | "cc" | "cpp" | "cxx"
2038                    )
2039                })
2040        {
2041            return Err(CcBypassReason::UnsupportedLanguage(value.into()));
2042        }
2043        self.source = Some(path.clone());
2044        self.parsed.push(Argument::Source(path));
2045        Ok(())
2046    }
2047
2048    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
2049        let option = value.trim_start_matches(['/', '-']);
2050        let lower = option.to_ascii_lowercase();
2051        if matches!(lower.as_str(), "?" | "help") {
2052            return Err(CcBypassReason::CompilerQuery);
2053        }
2054        if lower == "showincludes" || lower.starts_with("sourcedependencies") {
2055            return Err(CcBypassReason::CallerDependencyFlags(value.into()));
2056        }
2057        if matches!(lower.as_str(), "e" | "ep" | "p") {
2058            return Err(CcBypassReason::NonObjectOutput(value.into()));
2059        }
2060        if (lower.starts_with("fa") && !lower.starts_with("favor:"))
2061            || lower.starts_with("fd")
2062            || lower.starts_with("zi")
2063        {
2064            return Err(CcBypassReason::SplitDebugOutput(value.into()));
2065        }
2066        if lower.starts_with("yc")
2067            || lower.starts_with("yu")
2068            || (lower.starts_with("fp") && !lower.starts_with("fp:"))
2069        {
2070            return Err(CcBypassReason::PrecompiledHeader(value.into()));
2071        }
2072        if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
2073            return Err(CcBypassReason::ToolPassthrough(value.into()));
2074        }
2075        if matches!(lower.as_str(), "ld" | "ldd") {
2076            return Err(CcBypassReason::NotACompile);
2077        }
2078        if lower == "c" {
2079            self.compiling = true;
2080            self.parsed.push(Argument::Plain("/c".into()));
2081            return Ok(());
2082        }
2083        for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
2084            if let Some(attached) = option.strip_prefix(prefix) {
2085                let path = PathBuf::from(self.value(canonical, attached)?);
2086                if prefix == "Fo" {
2087                    self.output = Some(path.clone());
2088                } else if prefix == "I" {
2089                    self.include_dirs.push(path.clone());
2090                }
2091                self.parsed.push(Argument::Path {
2092                    flag: canonical.into(),
2093                    path,
2094                });
2095                return Ok(());
2096            }
2097        }
2098        if lower.starts_with("external:i") {
2099            let path = PathBuf::from(self.value("/external:I", &option[10..])?);
2100            self.include_dirs.push(path.clone());
2101            self.parsed.push(Argument::Path {
2102                flag: "/external:I".into(),
2103                path,
2104            });
2105            return Ok(());
2106        }
2107        if option.starts_with("Tc") || option.starts_with("Tp") {
2108            let c = option.starts_with("Tc");
2109            let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
2110            self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
2111            return self.add_source(&path);
2112        }
2113        if lower.starts_with("pathmap:") {
2114            let rest = &option[8..];
2115            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
2116            self.parsed.push(Argument::PrefixMap {
2117                flag: "/pathmap".into(),
2118                from: PathBuf::from(from),
2119                to: to.into(),
2120            });
2121            return Ok(());
2122        }
2123        if matches!(option, "D" | "U") {
2124            let definition = self.value(value, "")?;
2125            self.parsed
2126                .push(Argument::Plain(format!("/{option}{definition}")));
2127            return Ok(());
2128        }
2129        // Definitions and the ordinary code-generation/diagnostic switches
2130        // produced by cc-rs are self-contained text and can be keyed verbatim.
2131        let definition = option.starts_with('D') || option.starts_with('U');
2132        let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
2133            || lower
2134                .strip_prefix('w')
2135                .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2136            || ["wd", "we", "wo"].iter().any(|prefix| {
2137                lower
2138                    .strip_prefix(prefix)
2139                    .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
2140            });
2141        let admitted = definition
2142            || warning
2143            || lower.starts_with("std:")
2144            || lower.starts_with("arch:")
2145            || lower.starts_with("favor:")
2146            || lower.starts_with("volatile:")
2147            || lower.starts_with("fp:")
2148            || lower.starts_with("eh")
2149            || lower.starts_with('o')
2150            || lower.starts_with("ob")
2151            || lower.starts_with("oi")
2152            || lower.starts_with("ot")
2153            || lower.starts_with("oy")
2154            || lower.starts_with("gs")
2155            || lower.starts_with("gr")
2156            || lower.starts_with("gy")
2157            || lower.starts_with("gw")
2158            || lower.starts_with("gl")
2159            || lower.starts_with("zc:")
2160            || lower.starts_with("diagnostics:")
2161            || matches!(
2162                lower.as_str(),
2163                "nologo"
2164                    | "brepro"
2165                    | "bigobj"
2166                    | "utf-8"
2167                    | "permissive-"
2168                    | "z7"
2169                    | "md"
2170                    | "mdd"
2171                    | "mt"
2172                    | "mtd"
2173            );
2174        if admitted {
2175            self.parsed.push(Argument::Plain(value.into()));
2176            return Ok(());
2177        }
2178        Err(CcBypassReason::UnknownFlag(value.into()))
2179    }
2180}
2181
2182#[cfg(test)]
2183#[path = "cc_cache_tests.rs"]
2184mod tests;