Skip to main content

mbx_cache_cc/
lib.rs

1//! Conservative parsing and action-key construction for C and C++ compiles.
2//!
3//! Cargo build scripts using the `cc` crate compile C and C++ through a
4//! gcc-style driver. This adapter models the narrow shape those build scripts
5//! produce -- one source, one object, `-c` -- and rejects everything else. As
6//! in the rustc adapter, callers should treat [`CcBypassReason`] as a safe
7//! cache bypass: run the real compiler and publish nothing.
8//!
9//! Two properties separate this adapter from a traditional compiler cache.
10//! Preprocessor inputs are discovered from a depfile the adapter injects
11//! itself, so the key names the headers the compilation actually read; and the
12//! directories those headers were searched from contribute a name manifest, so
13//! a header that newly *shadows* one of them changes the key even though every
14//! previously-read file is byte-identical.
15//!
16//! Path mappings are shared with the rustc adapter so both agree on which host
17//! roots are checkout-specific.
18#![deny(missing_docs)]
19
20use mbx_cache_core::{
21    CacheDigest, FileDigestCache, PathMapping, PathNormalizationError, canonical_json,
22    normalize_mapped_path,
23};
24use serde::{Deserialize, Serialize};
25use std::collections::{BTreeMap, BTreeSet};
26use std::ffi::OsString;
27use std::path::{Component, Path, PathBuf};
28use thiserror::Error;
29
30mod depfile;
31
32pub use depfile::{CcDepfile, CcDiscoveredInputs, INCLUDE_MANIFEST_PREFIX, manifest_snapshot};
33
34/// Schema version embedded in canonical cc action descriptors.
35pub const ACTION_SCHEMA_VERSION: u8 = 1;
36/// Version of the cc argument and input model used to construct keys.
37pub const ADAPTER_VERSION: u8 = 1;
38
39/// Maximum discovered inputs, including include-manifest entries.
40pub const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
41/// Maximum total bytes digested for one action.
42pub const MAX_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
43/// Maximum file names summarized across all include manifests.
44pub const MAX_MANIFEST_ENTRIES: usize = 16 * 1024;
45
46/// Environment variables whose values enter every cc action key.
47///
48/// These change the compiler's own behavior without appearing in argv. They
49/// are recorded even when unset, so setting one is distinguishable from
50/// leaving it unset.
51pub const KEYED_ENVIRONMENT: &[&str] = &[
52    "IPHONEOS_DEPLOYMENT_TARGET",
53    "LANG",
54    "LC_ALL",
55    "LC_MESSAGES",
56    "MACOSX_DEPLOYMENT_TARGET",
57    "SDKROOT",
58    "SOURCE_DATE_EPOCH",
59    "TVOS_DEPLOYMENT_TARGET",
60    "WATCHOS_DEPLOYMENT_TARGET",
61    "XROS_DEPLOYMENT_TARGET",
62];
63
64/// Environment variables that force a bypass when set.
65///
66/// Each one either injects search paths the argv model cannot see, redirects
67/// sub-tool resolution beneath the identity probe, or makes the driver write an
68/// output the adapter does not model.
69pub const BYPASS_ENVIRONMENT: &[&str] = &[
70    "CPATH",
71    "COMPILER_PATH",
72    "CPLUS_INCLUDE_PATH",
73    "C_INCLUDE_PATH",
74    "DEPENDENCIES_OUTPUT",
75    "GCC_EXEC_PREFIX",
76    "OBJC_INCLUDE_PATH",
77    "SUNPRO_DEPENDENCIES",
78];
79
80/// Absolute roots whose contents are keyed verbatim rather than through a
81/// placeholder.
82///
83/// Files beneath these roots are still digested; keying the path verbatim only
84/// declares that the path itself is a machine property rather than a
85/// checkout-specific one, which is what makes system headers shareable between
86/// worktrees on one machine.
87pub const SYSTEM_ROOTS: &[&str] = &[
88    "/Applications/Xcode.app",
89    "/Library/Developer",
90    "/nix/store",
91    "/usr/include",
92    "/usr/lib",
93    "/usr/local/include",
94];
95
96const SUPPORTED_F_FLAGS: &[&str] = &[
97    "PIC",
98    "PIE",
99    "asynchronous-unwind-tables",
100    "color-diagnostics",
101    "data-sections",
102    "diagnostics-color",
103    "exceptions",
104    "function-sections",
105    "merge-all-constants",
106    "no-asynchronous-unwind-tables",
107    "no-builtin",
108    "no-common",
109    "no-exceptions",
110    "no-omit-frame-pointer",
111    "no-plt",
112    "no-rtti",
113    "no-strict-aliasing",
114    "omit-frame-pointer",
115    "pic",
116    "pie",
117    "rtti",
118    "short-enums",
119    "signed-char",
120    "stack-protector",
121    "stack-protector-all",
122    "stack-protector-strong",
123    "strict-aliasing",
124    "unsigned-char",
125    "visibility",
126    "visibility-inlines-hidden",
127    "wrapv",
128];
129
130const SUPPORTED_M_FLAGS: &[&str] = &[
131    "32",
132    "64",
133    "arch",
134    "arm",
135    "avx",
136    "avx2",
137    "cpu",
138    "float-abi",
139    "fma",
140    "fpu",
141    "iphoneos-version-min",
142    "macosx-version-min",
143    "no-omit-leaf-frame-pointer",
144    "omit-leaf-frame-pointer",
145    "sse",
146    "sse2",
147    "sse3",
148    "sse4.1",
149    "sse4.2",
150    "thumb",
151    "tune",
152];
153
154const SUPPORTED_O_FLAGS: &[&str] = &[
155    "-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Og", "-Os", "-Oz",
156];
157
158const SUPPORTED_G_FLAGS: &[&str] = &[
159    "-g",
160    "-g0",
161    "-g1",
162    "-g2",
163    "-g3",
164    "-gdwarf-2",
165    "-gdwarf-3",
166    "-gdwarf-4",
167    "-gdwarf-5",
168];
169
170const SUPPORTED_BARE_FLAGS: &[&str] = &[
171    "-ansi",
172    "-nostdinc",
173    "-nostdinc++",
174    "-pedantic",
175    "-pedantic-errors",
176    "-pipe",
177    "-pthread",
178    "-w",
179];
180
181const SEPARATE_PATH_FLAGS: &[&str] = &[
182    "-idirafter",
183    "-imacros",
184    "-include",
185    "-iquote",
186    "-isysroot",
187    "-isystem",
188];
189
190const TOOL_PASSTHROUGH_FLAGS: &[&str] = &["-Xassembler", "-Xclang", "-Xlinker", "-Xpreprocessor"];
191
192const COMPILER_QUERY_FLAGS: &[&str] = &[
193    "--help",
194    "--version",
195    "-###",
196    // The `cc` crate probes with `-?` to tell an MSVC-style driver from a
197    // gcc-style one; neither answer is a compilation.
198    "-?",
199    "-dumpmachine",
200    "-dumpversion",
201    "-v",
202];
203
204/// Flags that rewrite a path prefix in the compiler's own output.
205///
206/// The left side is a real path and normalizes like any other; the right side
207/// is the text it is replaced with and enters the key verbatim.
208const PREFIX_MAP_FLAGS: &[&str] = &[
209    "-fdebug-prefix-map",
210    "-ffile-prefix-map",
211    "-fmacro-prefix-map",
212];
213
214impl CcBypassReason {
215    /// A stable, low-cardinality name for this reason.
216    ///
217    /// Many variants carry a path or a flag, so `Display` text cannot be
218    /// aggregated; statistics group by this instead.
219    pub fn kind(&self) -> &'static str {
220        self.into()
221    }
222
223    /// A concrete change that can make this invocation cacheable, when one is
224    /// available.
225    ///
226    /// Expected compiler probes and failures that require adapter support
227    /// return `None`; callers can still explain those from
228    /// [`CcBypassReason::kind`].
229    pub fn remediation(&self) -> Option<&'static str> {
230        match self {
231            Self::UnsupportedEnvironment(_) => Some(
232                "Unset the reported environment variable for this build so the compiler invocation describes all of its inputs.",
233            ),
234            Self::LocalCpuTarget(_) => Some(
235                "Replace the reported local-CPU option with an explicit architecture or CPU name.",
236            ),
237            Self::EmbeddedTimestampMacro(_) => Some(
238                "Remove the reported timestamp macro, or keep this compilation uncached if its changing value is intentional.",
239            ),
240            Self::SearchPathModifiedDuringCompilation(_) => Some(
241                "Generate headers before compilation instead of changing an include directory while the compiler is running.",
242            ),
243            Self::UnknownFlag(_) | Self::ToolPassthrough(_) => Some(
244                "Remove the reported compiler option, or upgrade mbx if the option should be modeled.",
245            ),
246            Self::UnmappedAbsolutePath(_) => Some(
247                "Move the input under a mapped project or system root, or keep this compilation uncached.",
248            ),
249            _ => None,
250        }
251    }
252}
253
254/// Reason a C or C++ invocation cannot safely use the action cache.
255///
256/// A bypass is an expected conservative outcome, not a compiler error. Match on
257/// [`CcBypassReason::kind`] for aggregation rather than on the variants.
258#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
259#[strum(serialize_all = "kebab-case")]
260#[non_exhaustive]
261pub enum CcBypassReason {
262    /// An argument cannot be represented in the canonical UTF-8 key.
263    #[error("compiler argument {index} is not valid UTF-8")]
264    NonUtf8Argument {
265        /// Zero-based index in the argument slice.
266        index: usize,
267    },
268    /// The driver was handed an argument file.
269    #[error("compiler response file is not modeled by the cache adapter: {0}")]
270    ResponseFile(String),
271    /// A compiler flag is not modeled by this adapter version.
272    #[error("compiler flag is not modeled by the cache adapter: {0}")]
273    UnknownFlag(String),
274    /// A recognized flag was given without its value.
275    #[error("compiler flag {0} is missing its value")]
276    MissingValue(String),
277    /// The invocation asks the driver about itself rather than compiling.
278    #[error("compiler invocation queries the driver instead of compiling")]
279    CompilerQuery,
280    /// The invocation is not a single-object compile.
281    #[error("compiler invocation does not compile with -c")]
282    NotACompile,
283    /// The invocation emits preprocessed source or assembly.
284    #[error("compiler invocation emits a non-object output: {0}")]
285    NonObjectOutput(String),
286    /// The source arrives on standard input and cannot be rediscovered.
287    #[error("compiler invocation reads its source from standard input")]
288    StandardInput,
289    /// No source file was given.
290    #[error("compiler invocation names no source file")]
291    MissingInput,
292    /// More than one source file was given.
293    #[error("compiler invocation names more than one source file")]
294    MultipleInputs,
295    /// No `-o` was given, so the object name follows driver defaults.
296    #[error("compiler invocation names no output file")]
297    MissingOutput,
298    /// The source language is outside the modeled set.
299    #[error("compiler input language is not modeled by the cache adapter: {0}")]
300    UnsupportedLanguage(String),
301    /// The caller asked for its own dependency output.
302    #[error("compiler invocation requests its own dependency output: {0}")]
303    CallerDependencyFlags(String),
304    /// Precompiled headers are not byte-hermetic key material.
305    #[error("precompiled headers are not modeled by the cache adapter: {0}")]
306    PrecompiledHeader(String),
307    /// Coverage instrumentation writes outputs beside the object.
308    #[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
309    CoverageInstrumentation(String),
310    /// Split debug info writes a `.dwo` beside the object.
311    #[error("split debug output is not modeled by the cache adapter: {0}")]
312    SplitDebugOutput(String),
313    /// Temporary files are preserved beside the object.
314    #[error("preserved temporaries are not modeled by the cache adapter: {0}")]
315    SaveTemps(String),
316    /// An option is smuggled to a sub-tool the adapter cannot model.
317    #[error("compiler flag forwards options to another tool: {0}")]
318    ToolPassthrough(String),
319    /// A compiler plugin makes the output depend on unmodeled code.
320    #[error("compiler plugins are not modeled by the cache adapter: {0}")]
321    Plugin(String),
322    /// An include search directory gained or lost a header while the compiler
323    /// ran, so the manifest recorded after it is not what the compilation saw.
324    #[error("include search directory changed during the compilation: {0}")]
325    SearchPathModifiedDuringCompilation(PathBuf),
326
327    /// The object kept a path the key normalized away.
328    ///
329    /// Remapping covers what the compiler records itself; a path the source
330    /// keeps as a string survives it, and publishing such an object would
331    /// share this checkout's directory under a key that says it does not
332    /// matter.
333    #[error("compilation output records a path its key normalized away: {0}")]
334    UnportableOutput(PathBuf),
335    /// The object depends on the machine's own CPU rather than on named inputs.
336    #[error("compiler flag tunes for the local CPU: {0}")]
337    LocalCpuTarget(String),
338    /// The driver is not a gcc-style or clang-style compiler.
339    #[error("compiler driver is not modeled by the cache adapter: {0}")]
340    UnsupportedCompilerDriver(String),
341    /// The identity probe could not be run or parsed.
342    #[error("could not establish compiler identity: {0}")]
343    CompilerIdentityUnavailable(String),
344    /// An environment variable outside the modeled set is set.
345    #[error("environment variable {0} changes the compilation in an unmodeled way")]
346    UnsupportedEnvironment(String),
347    /// The shim could not be told which real compiler to run.
348    #[error("no real compiler was pinned for the cc shim")]
349    RealCompilerUnpinned,
350    /// A read file expands a timestamp macro, so the object is not a function
351    /// of its inputs.
352    #[error("input expands a timestamp macro: {0}")]
353    EmbeddedTimestampMacro(PathBuf),
354    /// The injected depfile could not be parsed exactly.
355    #[error("could not model the compiler depfile: {0}")]
356    MalformedDepfile(String),
357    /// The injected depfile could not be read.
358    #[error("could not read the compiler depfile {path}: {message}")]
359    DepfileRead {
360        /// Depfile that could not be read.
361        path: PathBuf,
362        /// Underlying error text.
363        message: String,
364    },
365    /// The action exceeds an input, byte, or manifest bound.
366    #[error("compilation reads more inputs than the cache adapter models")]
367    TooManyInputs,
368    /// An absolute path lies outside every mapped and system root.
369    #[error("path is outside every modeled root: {0}")]
370    UnmappedAbsolutePath(PathBuf),
371    /// A path cannot be represented in the canonical UTF-8 key.
372    #[error("path is not valid UTF-8: {0}")]
373    NonUtf8Path(PathBuf),
374    /// The compiler working directory is not absolute.
375    #[error("compiler working directory is not absolute: {0}")]
376    RelativeWorkingDirectory(PathBuf),
377    /// A configured path mapping root is not absolute.
378    #[error("path mapping root is not absolute: {0}")]
379    RelativePathMapping(PathBuf),
380    /// A configured placeholder is empty, duplicated, or not a bare name.
381    #[error("invalid path mapping placeholder: {0}")]
382    InvalidPathPlaceholder(String),
383    /// A required input never appeared among the discovered inputs.
384    #[error("required input is missing from the discovered inputs: {0}")]
385    MissingRequiredInput(String),
386    /// An input digest is malformed.
387    #[error("invalid digest for input: {0}")]
388    InvalidInputDigest(String),
389    /// One normalized path carries two different digests.
390    #[error("conflicting digests for input: {0}")]
391    ConflictingInput(String),
392    /// An input could not be read.
393    #[error("could not read input {path}: {message}")]
394    InputRead {
395        /// Input that could not be read.
396        path: PathBuf,
397        /// Underlying error text.
398        message: String,
399    },
400    /// An input changed between discovery and publication.
401    #[error("input changed during the compilation: {0}")]
402    InputChanged(PathBuf),
403    /// An input was written while the compiler ran.
404    #[error("input was modified during the compilation: {0}")]
405    InputModifiedDuringCompilation(PathBuf),
406    /// Discovery and the action disagree about the working directory.
407    #[error("discovered inputs use a different working directory")]
408    DiscoveryWorkingDirectory,
409    /// A prediction uses a schema this adapter version does not model.
410    #[error("action prediction is not modeled by this adapter version")]
411    UnsupportedPrediction,
412    /// A predicted input name cannot be resolved back to a host path.
413    #[error("invalid predicted input: {0}")]
414    InvalidPredictedInput(String),
415    /// Canonical serialization failed.
416    #[error("could not serialize the action descriptor: {0}")]
417    Serialization(String),
418}
419
420impl From<PathNormalizationError> for CcBypassReason {
421    fn from(reason: PathNormalizationError) -> Self {
422        match reason {
423            PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
424            PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
425        }
426    }
427}
428
429/// Source language a driver invocation compiles.
430#[derive(Debug, Clone, Copy, PartialEq, Eq)]
431pub enum CcLanguage {
432    /// C, driven through `CC`.
433    C,
434    /// C++, driven through `CXX`.
435    Cxx,
436}
437
438impl CcLanguage {
439    /// Shim file stem that selects this language.
440    pub fn shim_stem(self) -> &'static str {
441        match self {
442            Self::C => "mbx-cc",
443            Self::Cxx => "mbx-cxx",
444        }
445    }
446
447    /// Default driver name to fall back to when no real compiler is pinned.
448    pub fn default_driver(self) -> &'static str {
449        if cfg!(windows) {
450            return "cl.exe";
451        }
452        match self {
453            Self::C => "cc",
454            Self::Cxx => "c++",
455        }
456    }
457}
458
459/// Compiler family, which decides how the identity is assembled.
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461pub enum CcCompilerFamily {
462    /// GCC, which compiles objects through an external assembler.
463    Gcc,
464    /// Upstream LLVM clang.
465    Clang,
466    /// Apple's clang distribution.
467    AppleClang,
468    /// Microsoft's `cl.exe` driver.
469    #[cfg(windows)]
470    Msvc,
471}
472
473impl CcCompilerFamily {
474    /// Stable name recorded in the action key.
475    pub fn as_str(self) -> &'static str {
476        match self {
477            Self::Gcc => "gcc",
478            Self::Clang => "clang",
479            Self::AppleClang => "apple-clang",
480            #[cfg(windows)]
481            Self::Msvc => "msvc",
482        }
483    }
484
485    /// Whether objects are produced through a separate assembler binary whose
486    /// version therefore belongs in the identity.
487    pub fn uses_external_assembler(self) -> bool {
488        matches!(self, Self::Gcc)
489    }
490
491    /// Whether this is Microsoft's `cl.exe` driver.
492    pub fn is_msvc(self) -> bool {
493        #[cfg(windows)]
494        {
495            matches!(self, Self::Msvc)
496        }
497        #[cfg(not(windows))]
498        {
499            false
500        }
501    }
502
503    /// Classify a driver from its verbose probe output.
504    pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
505        #[cfg(windows)]
506        if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
507            return Ok(Self::Msvc);
508        }
509        if probe.contains("Apple clang version") {
510            Ok(Self::AppleClang)
511        } else if probe.contains("clang version") {
512            Ok(Self::Clang)
513        } else if probe.contains("gcc version") {
514            Ok(Self::Gcc)
515        } else {
516            Err(CcBypassReason::UnsupportedCompilerDriver(
517                probe.lines().next().unwrap_or_default().into(),
518            ))
519        }
520    }
521}
522
523/// Compiler properties that distinguish incompatible objects.
524#[derive(Debug, Clone, PartialEq, Eq)]
525pub struct CcCompilerIdentity {
526    /// Driver family.
527    pub family: CcCompilerFamily,
528    /// Complete verbose probe output, verbatim.
529    pub version_text: String,
530    /// Target triple the driver reports.
531    pub target: String,
532    /// Resolved assembler and its version, for families that use one.
533    ///
534    /// GCC assembles through binutils, whose version changes object bytes
535    /// without changing anything `gcc -v` prints. Clang assembles internally,
536    /// so this is empty there.
537    pub assembler: String,
538}
539
540/// One file input paired with the digest used in the action key.
541#[derive(Debug, Clone, PartialEq, Eq)]
542pub struct CcActionInput {
543    /// Absolute host path used to read and verify the input, or an
544    /// include-manifest pseudo-path.
545    pub path: PathBuf,
546    /// Digest of the input contents, or of the directory's name manifest.
547    pub digest: CacheDigest,
548}
549
550/// External information needed to construct a canonical cc action.
551#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct CcActionContext {
553    /// Identity of the compiler that produces the object.
554    pub compiler: CcCompilerIdentity,
555    /// Absolute directory in which the compiler runs.
556    pub working_dir: PathBuf,
557    /// Host roots replaced with stable placeholders in the key.
558    pub path_mappings: Vec<PathMapping>,
559    /// Environment inputs and their observed values.
560    pub environment: BTreeMap<String, Option<String>>,
561    /// Complete set of direct and discovered file inputs.
562    pub inputs: Vec<CcActionInput>,
563}
564
565/// Canonical action descriptor and its content digest.
566#[derive(Debug, Clone, PartialEq, Eq)]
567pub struct CcAction {
568    /// Digest of `bytes`, used as the action-cache key.
569    pub digest: CacheDigest,
570    /// Canonical serialized action descriptor.
571    pub bytes: Vec<u8>,
572}
573
574#[derive(Debug, Serialize)]
575struct CcCompilerDescriptor {
576    assembler: String,
577    family: String,
578    target: String,
579    version_text: String,
580}
581
582#[derive(Debug, Serialize)]
583struct CcInputDescriptor {
584    digest: CacheDigest,
585    path: String,
586}
587
588#[derive(Debug, Serialize)]
589struct CcActionDescriptor {
590    version: u8,
591    kind: &'static str,
592    adapter_version: u8,
593    compiler: CcCompilerDescriptor,
594    arguments: Vec<String>,
595    environment: BTreeMap<String, Option<String>>,
596    inputs: Vec<CcInputDescriptor>,
597}
598
599#[derive(Debug, Serialize)]
600struct CcInvocationDescriptor {
601    version: u8,
602    kind: &'static str,
603    adapter_version: u8,
604    compiler: CcCompilerDescriptor,
605    arguments: Vec<String>,
606    required_inputs: Vec<String>,
607}
608
609/// Normalized input names from the last successful execution of one modeled
610/// compile.
611#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
612#[serde(deny_unknown_fields)]
613pub struct CcInputPrediction {
614    /// Prediction schema version.
615    pub version: u8,
616    /// Normalized input paths, including include-manifest entries.
617    pub inputs: Vec<String>,
618    /// Names of environment variables that entered the key.
619    pub environment: Vec<String>,
620    /// Compiler wall time from the successful invocation that produced this
621    /// prediction. Zero means no timing hint was recorded.
622    #[serde(default, skip_serializing_if = "is_zero")]
623    pub compiler_duration_ns: u64,
624    /// Source file name associated with the timing hint.
625    #[serde(default, skip_serializing_if = "String::is_empty")]
626    pub source_name: String,
627}
628
629fn is_zero(value: &u64) -> bool {
630    *value == 0
631}
632
633/// One parsed and admitted argument.
634#[derive(Debug, Clone, PartialEq, Eq)]
635enum Argument {
636    /// Keyed verbatim.
637    Plain(String),
638    /// Keyed with its path normalized.
639    Path { flag: String, path: PathBuf },
640    /// A prefix rewrite: the source path normalizes, the replacement does not.
641    PrefixMap {
642        flag: String,
643        from: PathBuf,
644        to: String,
645    },
646    /// The source file.
647    Source(PathBuf),
648}
649
650/// A parsed, admitted C or C++ compile.
651#[derive(Debug, Clone, PartialEq, Eq)]
652pub struct CcInvocation {
653    arguments: Vec<Argument>,
654    source: PathBuf,
655    output: PathBuf,
656    include_dirs: Vec<PathBuf>,
657    required_inputs: Vec<PathBuf>,
658    language: CcLanguage,
659    sysroot: Option<PathBuf>,
660}
661
662impl CcInvocation {
663    /// Parse a driver command line, admitting only modeled single-object
664    /// compiles.
665    pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
666        Parser::new(arguments).parse()
667    }
668
669    /// Parse a command line using the syntax of `family`.
670    pub fn parse_for(
671        arguments: &[OsString],
672        family: CcCompilerFamily,
673    ) -> Result<Self, CcBypassReason> {
674        if family.is_msvc() {
675            MsvcParser::new(arguments).parse()
676        } else {
677            Self::parse(arguments)
678        }
679    }
680
681    /// Parse a command line using Microsoft `cl.exe` syntax.
682    pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
683        MsvcParser::new(arguments).parse()
684    }
685
686    /// Source file this invocation compiles.
687    pub fn source(&self) -> &Path {
688        &self.source
689    }
690
691    /// Object file this invocation produces.
692    pub fn output(&self) -> &Path {
693        &self.output
694    }
695
696    /// Include search directories named on the command line, in order.
697    pub fn include_dirs(&self) -> &[PathBuf] {
698        &self.include_dirs
699    }
700
701    /// Files that must appear among the discovered inputs.
702    pub fn required_inputs(&self) -> &[PathBuf] {
703        &self.required_inputs
704    }
705
706    /// Language the driver compiles.
707    pub fn language(&self) -> CcLanguage {
708        self.language
709    }
710
711    /// Sysroot named on the command line, if any.
712    pub fn sysroot(&self) -> Option<&Path> {
713        self.sysroot.as_deref()
714    }
715
716    /// Short label used for timing statistics.
717    pub fn source_name(&self) -> String {
718        self.source
719            .file_name()
720            .map(|name| name.to_string_lossy().into_owned())
721            .unwrap_or_default()
722    }
723
724    /// Arguments to append so the driver writes a dependency list beside the
725    /// object.
726    ///
727    /// `-MD` rather than `-MMD`: system headers are exactly the inputs most
728    /// likely to change without any other key component noticing, because the
729    /// compiler identity does not cover the C library or the platform SDK.
730    pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
731        vec!["-MD".into(), "-MF".into(), depfile.into()]
732    }
733
734    /// Arguments to append so a driver from `family` writes its dependency
735    /// list beside the object.
736    pub fn dependency_arguments_for(
737        &self,
738        depfile: &Path,
739        family: CcCompilerFamily,
740    ) -> Vec<OsString> {
741        if family.is_msvc() {
742            vec!["/sourceDependencies".into(), depfile.into()]
743        } else {
744            self.dependency_arguments(depfile)
745        }
746    }
747
748    /// Arguments to append so `cl.exe` writes `/sourceDependencies` JSON.
749    pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
750        vec!["/sourceDependencies".into(), depfile.into()]
751    }
752
753    /// Digest of the pre-input fingerprint, used to look up a prediction.
754    pub fn invocation_digest(
755        &self,
756        context: &CcActionContext,
757    ) -> Result<CacheDigest, CcBypassReason> {
758        let builder = ActionBuilder::new(self, context.clone());
759        let descriptor = builder.invocation_descriptor()?;
760        let bytes = canonical_json(&descriptor)
761            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
762        Ok(CacheDigest::blake3(&bytes))
763    }
764
765    /// Build the canonical action for this invocation and its discovered
766    /// inputs.
767    pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
768        ActionBuilder::new(self, context).build()
769    }
770
771    /// Record the normalized inputs of a successful compile so the next cold
772    /// invocation can rebuild the same key before compiling.
773    pub fn prediction(
774        &self,
775        context: &CcActionContext,
776        compiler_duration_ns: u64,
777    ) -> Result<CcInputPrediction, CcBypassReason> {
778        let builder = ActionBuilder::new(self, context.clone());
779        let mut inputs = context
780            .inputs
781            .iter()
782            .map(|input| builder.normalize_input_path(&input.path))
783            .collect::<Result<Vec<_>, _>>()?;
784        inputs.sort();
785        inputs.dedup();
786        Ok(CcInputPrediction {
787            version: 1,
788            inputs,
789            environment: context.environment.keys().cloned().collect(),
790            compiler_duration_ns,
791            source_name: self.source_name(),
792        })
793    }
794}
795
796impl CcInputPrediction {
797    /// Rehash the predicted paths and recompute include manifests. The caller
798    /// still recomputes the full action digest, so changed inputs are misses.
799    pub fn discover(
800        &self,
801        working_dir: &Path,
802        path_mappings: &[PathMapping],
803        digests: &dyn FileDigestCache,
804    ) -> Result<CcDiscoveredInputs, CcBypassReason> {
805        if self.version != 1 {
806            return Err(CcBypassReason::UnsupportedPrediction);
807        }
808        if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
809            return Err(CcBypassReason::UnsupportedPrediction);
810        }
811        let mappings = PathMapping::ordered(path_mappings);
812        let mut files = BTreeSet::new();
813        let mut directories = BTreeSet::new();
814        for entry in &self.inputs {
815            match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
816                Some(directory) => {
817                    directories.insert(denormalize_path(directory, &mappings)?);
818                }
819                None => {
820                    files.insert(denormalize_path(entry, &mappings)?);
821                }
822            }
823        }
824        CcDiscoveredInputs::collect(working_dir, files, directories, digests)
825    }
826}
827
828/// Resolve a normalized key path back to a host path.
829///
830/// Placeholder entries expand through their mapping; a verbatim entry is
831/// accepted only when it still lies beneath an admitted system root, so a
832/// prediction cannot name an arbitrary absolute path.
833fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
834    for mapping in mappings {
835        let prefix = format!("${{{}}}", mapping.placeholder);
836        let suffix = if value == prefix {
837            ""
838        } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
839            suffix
840        } else {
841            continue;
842        };
843        if !mapping.root.is_absolute() || !safe_suffix(suffix) {
844            return Err(CcBypassReason::InvalidPredictedInput(value.into()));
845        }
846        let mut path = normalize_components(&mapping.root);
847        path.extend(suffix.split('/').filter(|component| !component.is_empty()));
848        return Ok(path);
849    }
850    // A verbatim entry names a machine path rather than a placeholder. It is
851    // admitted only beneath a system root, and only spelled literally: a
852    // traversal component would let a prediction reach outside that root.
853    let path = PathBuf::from(value);
854    if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
855        return Ok(path);
856    }
857    Err(CcBypassReason::InvalidPredictedInput(value.into()))
858}
859
860fn safe_suffix(suffix: &str) -> bool {
861    suffix.is_empty()
862        || !suffix.split('/').any(|component| {
863            component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
864        })
865}
866
867/// Whether a path lies beneath a root whose location is a machine property.
868pub fn is_system_path(path: &Path) -> bool {
869    SYSTEM_ROOTS
870        .iter()
871        .any(|root| path.starts_with(Path::new(root)))
872}
873
874fn normalize_components(path: &Path) -> PathBuf {
875    let mut normalized = PathBuf::new();
876    for component in path.components() {
877        match component {
878            Component::CurDir => {}
879            Component::ParentDir => {
880                normalized.pop();
881            }
882            component => normalized.push(component.as_os_str()),
883        }
884    }
885    normalized
886}
887
888/// Read the modeled environment, rejecting variables that change the compile in
889/// a way the argv model cannot see.
890pub fn environment_inputs<F>(
891    lookup: F,
892    sysroot: Option<&Path>,
893) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
894where
895    F: Fn(&str) -> Option<String>,
896{
897    environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
898}
899
900/// Read the modeled environment for a particular compiler family.
901pub fn environment_inputs_for<F>(
902    lookup: F,
903    sysroot: Option<&Path>,
904    family: CcCompilerFamily,
905) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
906where
907    F: Fn(&str) -> Option<String>,
908{
909    for name in BYPASS_ENVIRONMENT {
910        if lookup(name).is_some() {
911            return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
912        }
913    }
914    let mut environment = BTreeMap::new();
915    for name in KEYED_ENVIRONMENT {
916        // An explicit `-isysroot` on the command line already pins the SDK, and
917        // it is what the driver honors, so the variable stops being an input.
918        if *name == "SDKROOT" && sysroot.is_some() {
919            continue;
920        }
921        environment.insert((*name).to_string(), lookup(name));
922    }
923    if family.is_msvc() {
924        // INCLUDE changes header resolution without appearing in argv. The
925        // toolset and SDK versions make the otherwise machine-local paths
926        // meaningful when action records move between hosts.
927        for name in [
928            "INCLUDE",
929            "VCToolsVersion",
930            "WindowsSDKVersion",
931            "UCRTVersion",
932        ] {
933            environment.insert(name.into(), lookup(name));
934        }
935        for name in ["CL", "_CL_"] {
936            if lookup(name).is_some() {
937                return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
938            }
939        }
940    }
941    Ok(environment)
942}
943
944struct ActionBuilder<'a> {
945    invocation: &'a CcInvocation,
946    context: CcActionContext,
947    mappings: Vec<PathMapping>,
948}
949
950impl<'a> ActionBuilder<'a> {
951    fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
952        context.path_mappings = PathMapping::ordered(&context.path_mappings);
953        let mappings = context.path_mappings.clone();
954        Self {
955            invocation,
956            context,
957            mappings,
958        }
959    }
960
961    fn build(self) -> Result<CcAction, CcBypassReason> {
962        self.validate_mappings()?;
963        let invocation = self.invocation_descriptor()?;
964
965        let mut inputs = BTreeMap::<String, CacheDigest>::new();
966        for input in &self.context.inputs {
967            input.digest.validate().map_err(|_| {
968                CcBypassReason::InvalidInputDigest(input.path.display().to_string())
969            })?;
970            let path = self.normalize_input_path(&input.path)?;
971            if inputs
972                .insert(path.clone(), input.digest.clone())
973                .is_some_and(|existing| existing != input.digest)
974            {
975                return Err(CcBypassReason::ConflictingInput(path));
976            }
977        }
978        let required = self
979            .invocation
980            .required_inputs
981            .iter()
982            .map(|path| self.normalize_path(path))
983            .collect::<Result<BTreeSet<_>, _>>()?;
984        if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
985            return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
986        }
987        let inputs = inputs
988            .into_iter()
989            .map(|(path, digest)| CcInputDescriptor { path, digest })
990            .collect();
991        let descriptor = CcActionDescriptor {
992            version: ACTION_SCHEMA_VERSION,
993            kind: "cc",
994            adapter_version: ADAPTER_VERSION,
995            compiler: invocation.compiler,
996            arguments: invocation.arguments,
997            environment: self.context.environment.clone(),
998            inputs,
999        };
1000        let bytes = canonical_json(&descriptor)
1001            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
1002        let digest = CacheDigest::blake3(&bytes);
1003        Ok(CcAction { digest, bytes })
1004    }
1005
1006    fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
1007        self.validate_mappings()?;
1008        let arguments = self
1009            .invocation
1010            .arguments
1011            .iter()
1012            .map(|argument| self.normalize_argument(argument))
1013            .collect::<Result<Vec<_>, _>>()?;
1014        let required_inputs = self
1015            .invocation
1016            .required_inputs
1017            .iter()
1018            .map(|path| self.normalize_path(path))
1019            .collect::<Result<BTreeSet<_>, _>>()?
1020            .into_iter()
1021            .collect();
1022        Ok(CcInvocationDescriptor {
1023            version: ACTION_SCHEMA_VERSION,
1024            kind: "cc",
1025            adapter_version: ADAPTER_VERSION,
1026            compiler: self.compiler_descriptor(),
1027            arguments,
1028            required_inputs,
1029        })
1030    }
1031
1032    fn compiler_descriptor(&self) -> CcCompilerDescriptor {
1033        CcCompilerDescriptor {
1034            assembler: self.context.compiler.assembler.clone(),
1035            family: self.context.compiler.family.as_str().into(),
1036            target: self.context.compiler.target.clone(),
1037            version_text: self.context.compiler.version_text.clone(),
1038        }
1039    }
1040
1041    fn validate_mappings(&self) -> Result<(), CcBypassReason> {
1042        if !self.context.working_dir.is_absolute() {
1043            return Err(CcBypassReason::RelativeWorkingDirectory(
1044                self.context.working_dir.clone(),
1045            ));
1046        }
1047        let mut roots = BTreeSet::new();
1048        let mut placeholders = BTreeSet::new();
1049        for mapping in &self.mappings {
1050            if !mapping.root.is_absolute() {
1051                return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
1052            }
1053            if mapping.placeholder.is_empty()
1054                || !mapping
1055                    .placeholder
1056                    .bytes()
1057                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1058                || !roots.insert(normalize_components(&mapping.root))
1059                || !placeholders.insert(&mapping.placeholder)
1060            {
1061                return Err(CcBypassReason::InvalidPathPlaceholder(
1062                    mapping.placeholder.clone(),
1063                ));
1064            }
1065        }
1066        Ok(())
1067    }
1068
1069    fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
1070        match argument {
1071            Argument::Plain(value) => Ok(value.clone()),
1072            Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1073            Argument::PrefixMap { flag, from, to } => {
1074                Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
1075            }
1076            Argument::Source(path) => Ok(self.normalize_path(path)?),
1077        }
1078    }
1079
1080    /// Normalize a path that names a compilation input or search root.
1081    ///
1082    /// A path beneath a mapped root becomes a placeholder so equivalent
1083    /// checkouts agree. A path beneath a system root stays verbatim: its
1084    /// location is a property of the machine, and its contents are digested
1085    /// like any other input.
1086    fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1087        match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
1088            Ok(normalized) => Ok(normalized),
1089            Err(reason) => {
1090                let absolute = absolute_path(path, &self.context.working_dir);
1091                if is_system_path(&absolute) {
1092                    return absolute
1093                        .to_str()
1094                        .map(ToOwned::to_owned)
1095                        .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
1096                }
1097                Err(reason.into())
1098            }
1099        }
1100    }
1101
1102    fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
1103        match path.to_str().and_then(|path| {
1104            path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
1105                .map(ToOwned::to_owned)
1106        }) {
1107            Some(directory) => Ok(format!(
1108                "{INCLUDE_MANIFEST_PREFIX}{}",
1109                self.normalize_path(Path::new(&directory))?
1110            )),
1111            None => self.normalize_path(path),
1112        }
1113    }
1114}
1115
1116fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1117    if path.is_absolute() {
1118        normalize_components(path)
1119    } else {
1120        normalize_components(&working_dir.join(path))
1121    }
1122}
1123
1124struct Parser<'a> {
1125    arguments: &'a [OsString],
1126    index: usize,
1127    parsed: Vec<Argument>,
1128    source: Option<PathBuf>,
1129    output: Option<PathBuf>,
1130    include_dirs: Vec<PathBuf>,
1131    required_inputs: Vec<PathBuf>,
1132    sysroot: Option<PathBuf>,
1133    explicit_language: Option<CcLanguage>,
1134    compiling: bool,
1135}
1136
1137impl<'a> Parser<'a> {
1138    fn new(arguments: &'a [OsString]) -> Self {
1139        Self {
1140            arguments,
1141            index: 0,
1142            parsed: Vec::new(),
1143            source: None,
1144            output: None,
1145            include_dirs: Vec::new(),
1146            required_inputs: Vec::new(),
1147            sysroot: None,
1148            explicit_language: None,
1149            compiling: false,
1150        }
1151    }
1152
1153    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1154        while self.index < self.arguments.len() {
1155            let value = self.current()?.to_string();
1156            self.index += 1;
1157            if value == "-" {
1158                return Err(CcBypassReason::StandardInput);
1159            }
1160            if let Some(argfile) = value.strip_prefix('@') {
1161                return Err(CcBypassReason::ResponseFile(argfile.into()));
1162            }
1163            if value.starts_with('-') {
1164                self.parse_flag(&value)?;
1165            } else {
1166                self.parse_input(&value)?;
1167            }
1168        }
1169
1170        if !self.compiling {
1171            return Err(CcBypassReason::NotACompile);
1172        }
1173        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1174        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1175        let language = self.language(&source)?;
1176        self.required_inputs.push(source.clone());
1177        Ok(CcInvocation {
1178            arguments: self.parsed,
1179            source,
1180            output,
1181            include_dirs: self.include_dirs,
1182            required_inputs: self.required_inputs,
1183            language,
1184            sysroot: self.sysroot,
1185        })
1186    }
1187
1188    fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1189        if let Some(language) = self.explicit_language {
1190            return Ok(language);
1191        }
1192        let extension = source
1193            .extension()
1194            .and_then(|extension| extension.to_str())
1195            .unwrap_or_default();
1196        match extension {
1197            "c" => Ok(CcLanguage::C),
1198            "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1199            _ => Err(CcBypassReason::UnsupportedLanguage(
1200                source.display().to_string(),
1201            )),
1202        }
1203    }
1204
1205    fn current(&self) -> Result<&str, CcBypassReason> {
1206        self.arguments[self.index]
1207            .to_str()
1208            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1209    }
1210
1211    fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1212        if let Some(value) = inline
1213            && !value.is_empty()
1214        {
1215            return Ok(value.into());
1216        }
1217        if self.index >= self.arguments.len() {
1218            return Err(CcBypassReason::MissingValue(flag.into()));
1219        }
1220        let value = self.current()?.to_string();
1221        self.index += 1;
1222        Ok(value)
1223    }
1224
1225    fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1226        if self.source.is_some() {
1227            return Err(CcBypassReason::MultipleInputs);
1228        }
1229        let path = PathBuf::from(value);
1230        // With an explicit `-x`, the driver ignores the extension entirely;
1231        // without one, the extension is the only thing that decides the
1232        // language, so an unmodeled extension has to bypass here.
1233        if self.explicit_language.is_none() {
1234            let extension = path
1235                .extension()
1236                .and_then(|extension| extension.to_str())
1237                .unwrap_or_default();
1238            if !matches!(extension, "c" | "cc" | "cpp" | "cxx" | "c++") {
1239                return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1240            }
1241        }
1242        self.source = Some(path.clone());
1243        self.parsed.push(Argument::Source(path));
1244        Ok(())
1245    }
1246
1247    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1248        if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1249            return Err(CcBypassReason::CompilerQuery);
1250        }
1251        if matches!(value, "-E" | "-S") {
1252            return Err(CcBypassReason::NonObjectOutput(value.into()));
1253        }
1254        if value.starts_with("-M") {
1255            return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1256        }
1257        if value.starts_with("-save-temps") {
1258            return Err(CcBypassReason::SaveTemps(value.into()));
1259        }
1260        if value == "--coverage" {
1261            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1262        }
1263        if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1264            || value.starts_with("-Wp,")
1265            || value.starts_with("-Wa,")
1266            || value.starts_with("-Wl,")
1267        {
1268            // The forwarded options are the compilation's real inputs and they
1269            // are not modeled, so consuming the value would not make this safe.
1270            return Err(CcBypassReason::ToolPassthrough(value.into()));
1271        }
1272        if value.starts_with("-include-pch") || value == "-emit-pch" {
1273            return Err(CcBypassReason::PrecompiledHeader(value.into()));
1274        }
1275
1276        if value == "-c" {
1277            self.compiling = true;
1278            self.parsed.push(Argument::Plain(value.into()));
1279            return Ok(());
1280        }
1281        if SUPPORTED_BARE_FLAGS.contains(&value)
1282            || SUPPORTED_O_FLAGS.contains(&value)
1283            || SUPPORTED_G_FLAGS.contains(&value)
1284            || value.starts_with("-std=")
1285        {
1286            self.parsed.push(Argument::Plain(value.into()));
1287            return Ok(());
1288        }
1289        if let Some(rest) = value.strip_prefix("-o") {
1290            let path = self.take_value("-o", Some(rest))?;
1291            // A repeated `-o` follows the driver: the last one names the file
1292            // that is produced. Every occurrence still enters the key.
1293            self.output = Some(PathBuf::from(&path));
1294            self.parsed.push(Argument::Path {
1295                flag: "-o".into(),
1296                path: PathBuf::from(path),
1297            });
1298            return Ok(());
1299        }
1300        if let Some(rest) = value.strip_prefix("-I") {
1301            let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1302            self.include_dirs.push(path.clone());
1303            self.parsed.push(Argument::Path {
1304                flag: "-I".into(),
1305                path,
1306            });
1307            return Ok(());
1308        }
1309        if SEPARATE_PATH_FLAGS.contains(&value) {
1310            let path = PathBuf::from(self.take_value(value, None)?);
1311            match value {
1312                "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1313                "-isysroot" => self.sysroot = Some(path.clone()),
1314                // `-include` and `-imacros` are deliberately not required
1315                // inputs. The driver resolves the name through the include
1316                // chain, so the file need not exist relative to the working
1317                // directory, and the dependency list names it at whatever path
1318                // it was actually found at.
1319                _ => {}
1320            }
1321            self.parsed.push(Argument::Path {
1322                flag: value.into(),
1323                path,
1324            });
1325            return Ok(());
1326        }
1327        // `--include=<file>` is the long spelling of `-include <file>`; the
1328        // `cc` crate emits it for prefixed headers.
1329        if let Some(rest) = value.strip_prefix("--include=") {
1330            let path = PathBuf::from(rest);
1331            self.parsed.push(Argument::Path {
1332                flag: "-include".into(),
1333                path,
1334            });
1335            return Ok(());
1336        }
1337        if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1338            value
1339                .strip_prefix(&format!("{flag}="))
1340                .map(|rest| (*flag, rest))
1341        }) {
1342            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1343            self.parsed.push(Argument::PrefixMap {
1344                flag: flag.into(),
1345                from: PathBuf::from(from),
1346                to: to.into(),
1347            });
1348            return Ok(());
1349        }
1350        // `--param name=value` tunes the optimizer; its text fully describes it.
1351        if value == "--param" {
1352            let parameter = self.take_value("--param", None)?;
1353            self.parsed
1354                .push(Argument::Plain(format!("--param={parameter}")));
1355            return Ok(());
1356        }
1357        if let Some(parameter) = value.strip_prefix("--param=") {
1358            self.parsed
1359                .push(Argument::Plain(format!("--param={parameter}")));
1360            return Ok(());
1361        }
1362        if let Some(rest) = value.strip_prefix("--sysroot=") {
1363            let path = PathBuf::from(rest);
1364            self.sysroot = Some(path.clone());
1365            self.parsed.push(Argument::Path {
1366                flag: "--sysroot".into(),
1367                path,
1368            });
1369            return Ok(());
1370        }
1371        if let Some(rest) = value
1372            .strip_prefix("-D")
1373            .or_else(|| value.strip_prefix("-U"))
1374        {
1375            let flag = &value[..2];
1376            let definition = self.take_value(flag, Some(rest))?;
1377            self.parsed
1378                .push(Argument::Plain(format!("{flag}{definition}")));
1379            return Ok(());
1380        }
1381        if let Some(rest) = value.strip_prefix("-x") {
1382            let language = self.take_value("-x", Some(rest))?;
1383            self.explicit_language = Some(match language.as_str() {
1384                "c" => CcLanguage::C,
1385                "c++" => CcLanguage::Cxx,
1386                other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1387            });
1388            self.parsed.push(Argument::Plain(format!("-x{language}")));
1389            return Ok(());
1390        }
1391        if let Some(target) = value.strip_prefix("--target=") {
1392            self.parsed
1393                .push(Argument::Plain(format!("--target={target}")));
1394            return Ok(());
1395        }
1396        if value == "-target" {
1397            let target = self.take_value("-target", None)?;
1398            self.parsed
1399                .push(Argument::Plain(format!("--target={target}")));
1400            return Ok(());
1401        }
1402        if value == "-arch" {
1403            let arch = self.take_value("-arch", None)?;
1404            self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1405            return Ok(());
1406        }
1407        if let Some(option) = value.strip_prefix("-f") {
1408            return self.parse_f_flag(value, option);
1409        }
1410        if let Some(option) = value.strip_prefix("-m") {
1411            return self.parse_m_flag(value, option);
1412        }
1413        if value.starts_with("-g") {
1414            // `-gsplit-dwarf` writes a `.dwo` beside the object; every other
1415            // unlisted `-g` spelling is simply unmodeled.
1416            return Err(if value.starts_with("-gsplit-dwarf") {
1417                CcBypassReason::SplitDebugOutput(value.into())
1418            } else {
1419                CcBypassReason::UnknownFlag(value.into())
1420            });
1421        }
1422        if value.starts_with("-W") {
1423            // Warning selection changes only diagnostics, which are replayed
1424            // from the cache, and the exit status, and only successful
1425            // compiles are ever published.
1426            self.parsed.push(Argument::Plain(value.into()));
1427            return Ok(());
1428        }
1429        Err(CcBypassReason::UnknownFlag(value.into()))
1430    }
1431
1432    fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1433        if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1434            return Err(CcBypassReason::Plugin(value.into()));
1435        }
1436        if option.starts_with("profile-") || option == "test-coverage" {
1437            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1438        }
1439        let name = option.split_once('=').map_or(option, |(name, _)| name);
1440        if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1441            return Err(CcBypassReason::UnknownFlag(value.into()));
1442        }
1443        self.parsed.push(Argument::Plain(value.into()));
1444        Ok(())
1445    }
1446
1447    fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1448        if option == "llvm" {
1449            return Err(CcBypassReason::ToolPassthrough(value.into()));
1450        }
1451        // `-march=native` and its relatives resolve against whatever CPU this
1452        // machine has. The resulting object is not a function of the key, so
1453        // another machine could otherwise restore code its processor cannot
1454        // run.
1455        if let Some((name, selection)) = option.split_once('=')
1456            && matches!(name, "arch" | "cpu" | "tune")
1457            && matches!(selection, "native" | "host")
1458        {
1459            return Err(CcBypassReason::LocalCpuTarget(value.into()));
1460        }
1461        let name = option.split_once('=').map_or(option, |(name, _)| name);
1462        if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1463            return Err(CcBypassReason::UnknownFlag(value.into()));
1464        }
1465        self.parsed.push(Argument::Plain(value.into()));
1466        Ok(())
1467    }
1468}
1469
1470/// Conservative parser for the command lines emitted by the `cc` crate for
1471/// Microsoft's compiler. It intentionally admits only flags whose effects are
1472/// either present in argv or covered by dependency discovery.
1473struct MsvcParser<'a> {
1474    arguments: &'a [OsString],
1475    index: usize,
1476    parsed: Vec<Argument>,
1477    source: Option<PathBuf>,
1478    output: Option<PathBuf>,
1479    include_dirs: Vec<PathBuf>,
1480    required_inputs: Vec<PathBuf>,
1481    explicit_language: Option<CcLanguage>,
1482    compiling: bool,
1483}
1484
1485impl<'a> MsvcParser<'a> {
1486    fn new(arguments: &'a [OsString]) -> Self {
1487        Self {
1488            arguments,
1489            index: 0,
1490            parsed: Vec::new(),
1491            source: None,
1492            output: None,
1493            include_dirs: Vec::new(),
1494            required_inputs: Vec::new(),
1495            explicit_language: None,
1496            compiling: false,
1497        }
1498    }
1499
1500    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1501        while self.index < self.arguments.len() {
1502            let value = self.current()?.to_owned();
1503            self.index += 1;
1504            if let Some(file) = value.strip_prefix('@') {
1505                return Err(CcBypassReason::ResponseFile(file.into()));
1506            }
1507            if value.starts_with('/') || value.starts_with('-') {
1508                self.parse_flag(&value)?;
1509            } else {
1510                self.add_source(&value)?;
1511            }
1512        }
1513        if !self.compiling {
1514            return Err(CcBypassReason::NotACompile);
1515        }
1516        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1517        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1518        let language = self.explicit_language.unwrap_or_else(|| {
1519            if source
1520                .extension()
1521                .and_then(|value| value.to_str())
1522                .is_some_and(|value| value.eq_ignore_ascii_case("c"))
1523            {
1524                CcLanguage::C
1525            } else {
1526                CcLanguage::Cxx
1527            }
1528        });
1529        self.required_inputs.push(source.clone());
1530        Ok(CcInvocation {
1531            arguments: self.parsed,
1532            source,
1533            output,
1534            include_dirs: self.include_dirs,
1535            required_inputs: self.required_inputs,
1536            language,
1537            sysroot: None,
1538        })
1539    }
1540
1541    fn current(&self) -> Result<&str, CcBypassReason> {
1542        self.arguments[self.index]
1543            .to_str()
1544            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1545    }
1546
1547    fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
1548        if !attached.is_empty() {
1549            return Ok(attached.into());
1550        }
1551        if self.index == self.arguments.len() {
1552            return Err(CcBypassReason::MissingValue(flag.into()));
1553        }
1554        let value = self.current()?.to_owned();
1555        self.index += 1;
1556        Ok(value)
1557    }
1558
1559    fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
1560        if self.source.is_some() {
1561            return Err(CcBypassReason::MultipleInputs);
1562        }
1563        let path = PathBuf::from(value);
1564        if self.explicit_language.is_none()
1565            && !path
1566                .extension()
1567                .and_then(|value| value.to_str())
1568                .is_some_and(|value| {
1569                    matches!(
1570                        value.to_ascii_lowercase().as_str(),
1571                        "c" | "cc" | "cpp" | "cxx"
1572                    )
1573                })
1574        {
1575            return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1576        }
1577        self.source = Some(path.clone());
1578        self.parsed.push(Argument::Source(path));
1579        Ok(())
1580    }
1581
1582    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1583        let option = value.trim_start_matches(['/', '-']);
1584        let lower = option.to_ascii_lowercase();
1585        if matches!(lower.as_str(), "?" | "help") {
1586            return Err(CcBypassReason::CompilerQuery);
1587        }
1588        if lower == "showincludes" || lower.starts_with("sourcedependencies") {
1589            return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1590        }
1591        if matches!(lower.as_str(), "e" | "ep" | "p") {
1592            return Err(CcBypassReason::NonObjectOutput(value.into()));
1593        }
1594        if (lower.starts_with("fa") && !lower.starts_with("favor:"))
1595            || lower.starts_with("fd")
1596            || lower.starts_with("zi")
1597        {
1598            return Err(CcBypassReason::SplitDebugOutput(value.into()));
1599        }
1600        if lower.starts_with("yc")
1601            || lower.starts_with("yu")
1602            || (lower.starts_with("fp") && !lower.starts_with("fp:"))
1603        {
1604            return Err(CcBypassReason::PrecompiledHeader(value.into()));
1605        }
1606        if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
1607            return Err(CcBypassReason::ToolPassthrough(value.into()));
1608        }
1609        if matches!(lower.as_str(), "ld" | "ldd") {
1610            return Err(CcBypassReason::NotACompile);
1611        }
1612        if lower == "c" {
1613            self.compiling = true;
1614            self.parsed.push(Argument::Plain("/c".into()));
1615            return Ok(());
1616        }
1617        for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
1618            if let Some(attached) = option.strip_prefix(prefix) {
1619                let path = PathBuf::from(self.value(canonical, attached)?);
1620                if prefix == "Fo" {
1621                    self.output = Some(path.clone());
1622                } else if prefix == "I" {
1623                    self.include_dirs.push(path.clone());
1624                }
1625                self.parsed.push(Argument::Path {
1626                    flag: canonical.into(),
1627                    path,
1628                });
1629                return Ok(());
1630            }
1631        }
1632        if lower.starts_with("external:i") {
1633            let path = PathBuf::from(self.value("/external:I", &option[10..])?);
1634            self.include_dirs.push(path.clone());
1635            self.parsed.push(Argument::Path {
1636                flag: "/external:I".into(),
1637                path,
1638            });
1639            return Ok(());
1640        }
1641        if option.starts_with("Tc") || option.starts_with("Tp") {
1642            let c = option.starts_with("Tc");
1643            let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
1644            self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
1645            return self.add_source(&path);
1646        }
1647        if lower.starts_with("pathmap:") {
1648            let rest = &option[8..];
1649            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1650            self.parsed.push(Argument::PrefixMap {
1651                flag: "/pathmap".into(),
1652                from: PathBuf::from(from),
1653                to: to.into(),
1654            });
1655            return Ok(());
1656        }
1657        if matches!(option, "D" | "U") {
1658            let definition = self.value(value, "")?;
1659            self.parsed
1660                .push(Argument::Plain(format!("/{option}{definition}")));
1661            return Ok(());
1662        }
1663        // Definitions and the ordinary code-generation/diagnostic switches
1664        // produced by cc-rs are self-contained text and can be keyed verbatim.
1665        let definition = option.starts_with('D') || option.starts_with('U');
1666        let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
1667            || lower
1668                .strip_prefix('w')
1669                .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1670            || ["wd", "we", "wo"].iter().any(|prefix| {
1671                lower
1672                    .strip_prefix(prefix)
1673                    .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
1674            });
1675        let admitted = definition
1676            || warning
1677            || lower.starts_with("std:")
1678            || lower.starts_with("arch:")
1679            || lower.starts_with("favor:")
1680            || lower.starts_with("volatile:")
1681            || lower.starts_with("fp:")
1682            || lower.starts_with("eh")
1683            || lower.starts_with('o')
1684            || lower.starts_with("ob")
1685            || lower.starts_with("oi")
1686            || lower.starts_with("ot")
1687            || lower.starts_with("oy")
1688            || lower.starts_with("gs")
1689            || lower.starts_with("gr")
1690            || lower.starts_with("gy")
1691            || lower.starts_with("gw")
1692            || lower.starts_with("gl")
1693            || lower.starts_with("zc:")
1694            || lower.starts_with("diagnostics:")
1695            || matches!(
1696                lower.as_str(),
1697                "nologo"
1698                    | "brepro"
1699                    | "bigobj"
1700                    | "utf-8"
1701                    | "permissive-"
1702                    | "z7"
1703                    | "md"
1704                    | "mdd"
1705                    | "mt"
1706                    | "mtd"
1707            );
1708        if admitted {
1709            self.parsed.push(Argument::Plain(value.into()));
1710            return Ok(());
1711        }
1712        Err(CcBypassReason::UnknownFlag(value.into()))
1713    }
1714}
1715
1716#[cfg(test)]
1717#[path = "cc_cache_tests.rs"]
1718mod tests;