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