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