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    /// The object depends on the machine's own CPU rather than on named inputs.
295    #[error("compiler flag tunes for the local CPU: {0}")]
296    LocalCpuTarget(String),
297    /// The driver is not a gcc-style or clang-style compiler.
298    #[error("compiler driver is not modeled by the cache adapter: {0}")]
299    UnsupportedCompilerDriver(String),
300    /// The identity probe could not be run or parsed.
301    #[error("could not establish compiler identity: {0}")]
302    CompilerIdentityUnavailable(String),
303    /// An environment variable outside the modeled set is set.
304    #[error("environment variable {0} changes the compilation in an unmodeled way")]
305    UnsupportedEnvironment(String),
306    /// The shim could not be told which real compiler to run.
307    #[error("no real compiler was pinned for the cc shim")]
308    RealCompilerUnpinned,
309    /// A read file expands a timestamp macro, so the object is not a function
310    /// of its inputs.
311    #[error("input expands a timestamp macro: {0}")]
312    EmbeddedTimestampMacro(PathBuf),
313    /// The injected depfile could not be parsed exactly.
314    #[error("could not model the compiler depfile: {0}")]
315    MalformedDepfile(String),
316    /// The injected depfile could not be read.
317    #[error("could not read the compiler depfile {path}: {message}")]
318    DepfileRead {
319        /// Depfile that could not be read.
320        path: PathBuf,
321        /// Underlying error text.
322        message: String,
323    },
324    /// The action exceeds an input, byte, or manifest bound.
325    #[error("compilation reads more inputs than the cache adapter models")]
326    TooManyInputs,
327    /// An absolute path lies outside every mapped and system root.
328    #[error("path is outside every modeled root: {0}")]
329    UnmappedAbsolutePath(PathBuf),
330    /// A path cannot be represented in the canonical UTF-8 key.
331    #[error("path is not valid UTF-8: {0}")]
332    NonUtf8Path(PathBuf),
333    /// The compiler working directory is not absolute.
334    #[error("compiler working directory is not absolute: {0}")]
335    RelativeWorkingDirectory(PathBuf),
336    /// A configured path mapping root is not absolute.
337    #[error("path mapping root is not absolute: {0}")]
338    RelativePathMapping(PathBuf),
339    /// A configured placeholder is empty, duplicated, or not a bare name.
340    #[error("invalid path mapping placeholder: {0}")]
341    InvalidPathPlaceholder(String),
342    /// A required input never appeared among the discovered inputs.
343    #[error("required input is missing from the discovered inputs: {0}")]
344    MissingRequiredInput(String),
345    /// An input digest is malformed.
346    #[error("invalid digest for input: {0}")]
347    InvalidInputDigest(String),
348    /// One normalized path carries two different digests.
349    #[error("conflicting digests for input: {0}")]
350    ConflictingInput(String),
351    /// An input could not be read.
352    #[error("could not read input {path}: {message}")]
353    InputRead {
354        /// Input that could not be read.
355        path: PathBuf,
356        /// Underlying error text.
357        message: String,
358    },
359    /// An input changed between discovery and publication.
360    #[error("input changed during the compilation: {0}")]
361    InputChanged(PathBuf),
362    /// An input was written while the compiler ran.
363    #[error("input was modified during the compilation: {0}")]
364    InputModifiedDuringCompilation(PathBuf),
365    /// Discovery and the action disagree about the working directory.
366    #[error("discovered inputs use a different working directory")]
367    DiscoveryWorkingDirectory,
368    /// A prediction uses a schema this adapter version does not model.
369    #[error("action prediction is not modeled by this adapter version")]
370    UnsupportedPrediction,
371    /// A predicted input name cannot be resolved back to a host path.
372    #[error("invalid predicted input: {0}")]
373    InvalidPredictedInput(String),
374    /// Canonical serialization failed.
375    #[error("could not serialize the action descriptor: {0}")]
376    Serialization(String),
377}
378
379impl From<RustcBypassReason> for CcBypassReason {
380    /// Translate the shared path-normalization errors into this adapter's own
381    /// reasons, so a cc bypass never reports a rustc kind.
382    fn from(reason: RustcBypassReason) -> Self {
383        match reason {
384            RustcBypassReason::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
385            RustcBypassReason::NonUtf8Path(path) => Self::NonUtf8Path(path),
386            other => Self::UnknownFlag(other.kind().into()),
387        }
388    }
389}
390
391/// Source language a driver invocation compiles.
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393pub enum CcLanguage {
394    /// C, driven through `CC`.
395    C,
396    /// C++, driven through `CXX`.
397    Cxx,
398}
399
400impl CcLanguage {
401    /// Shim file stem that selects this language.
402    pub fn shim_stem(self) -> &'static str {
403        match self {
404            Self::C => "mbx-cc",
405            Self::Cxx => "mbx-cxx",
406        }
407    }
408
409    /// Default driver name to fall back to when no real compiler is pinned.
410    pub fn default_driver(self) -> &'static str {
411        match self {
412            Self::C => "cc",
413            Self::Cxx => "c++",
414        }
415    }
416}
417
418/// Compiler family, which decides how the identity is assembled.
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub enum CcCompilerFamily {
421    /// GCC, which compiles objects through an external assembler.
422    Gcc,
423    /// Upstream LLVM clang.
424    Clang,
425    /// Apple's clang distribution.
426    AppleClang,
427}
428
429impl CcCompilerFamily {
430    /// Stable name recorded in the action key.
431    pub fn as_str(self) -> &'static str {
432        match self {
433            Self::Gcc => "gcc",
434            Self::Clang => "clang",
435            Self::AppleClang => "apple-clang",
436        }
437    }
438
439    /// Whether objects are produced through a separate assembler binary whose
440    /// version therefore belongs in the identity.
441    pub fn uses_external_assembler(self) -> bool {
442        matches!(self, Self::Gcc)
443    }
444
445    /// Classify a driver from its verbose probe output.
446    pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
447        if probe.contains("Apple clang version") {
448            Ok(Self::AppleClang)
449        } else if probe.contains("clang version") {
450            Ok(Self::Clang)
451        } else if probe.contains("gcc version") {
452            Ok(Self::Gcc)
453        } else {
454            Err(CcBypassReason::UnsupportedCompilerDriver(
455                probe.lines().next().unwrap_or_default().into(),
456            ))
457        }
458    }
459}
460
461/// Compiler properties that distinguish incompatible objects.
462#[derive(Debug, Clone, PartialEq, Eq)]
463pub struct CcCompilerIdentity {
464    /// Driver family.
465    pub family: CcCompilerFamily,
466    /// Complete verbose probe output, verbatim.
467    pub version_text: String,
468    /// Target triple the driver reports.
469    pub target: String,
470    /// Resolved assembler and its version, for families that use one.
471    ///
472    /// GCC assembles through binutils, whose version changes object bytes
473    /// without changing anything `gcc -v` prints. Clang assembles internally,
474    /// so this is empty there.
475    pub assembler: String,
476}
477
478/// One file input paired with the digest used in the action key.
479#[derive(Debug, Clone, PartialEq, Eq)]
480pub struct CcActionInput {
481    /// Absolute host path used to read and verify the input, or an
482    /// include-manifest pseudo-path.
483    pub path: PathBuf,
484    /// Digest of the input contents, or of the directory's name manifest.
485    pub digest: CacheDigest,
486}
487
488/// External information needed to construct a canonical cc action.
489#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct CcActionContext {
491    /// Identity of the compiler that produces the object.
492    pub compiler: CcCompilerIdentity,
493    /// Absolute directory in which the compiler runs.
494    pub working_dir: PathBuf,
495    /// Host roots replaced with stable placeholders in the key.
496    pub path_mappings: Vec<PathMapping>,
497    /// Environment inputs and their observed values.
498    pub environment: BTreeMap<String, Option<String>>,
499    /// Complete set of direct and discovered file inputs.
500    pub inputs: Vec<CcActionInput>,
501}
502
503/// Canonical action descriptor and its content digest.
504#[derive(Debug, Clone, PartialEq, Eq)]
505pub struct CcAction {
506    /// Digest of `bytes`, used as the action-cache key.
507    pub digest: CacheDigest,
508    /// Canonical serialized action descriptor.
509    pub bytes: Vec<u8>,
510}
511
512#[derive(Debug, Serialize)]
513struct CcCompilerDescriptor {
514    assembler: String,
515    family: String,
516    target: String,
517    version_text: String,
518}
519
520#[derive(Debug, Serialize)]
521struct CcInputDescriptor {
522    digest: CacheDigest,
523    path: String,
524}
525
526#[derive(Debug, Serialize)]
527struct CcActionDescriptor {
528    version: u8,
529    kind: &'static str,
530    adapter_version: u8,
531    compiler: CcCompilerDescriptor,
532    arguments: Vec<String>,
533    environment: BTreeMap<String, Option<String>>,
534    inputs: Vec<CcInputDescriptor>,
535}
536
537#[derive(Debug, Serialize)]
538struct CcInvocationDescriptor {
539    version: u8,
540    kind: &'static str,
541    adapter_version: u8,
542    compiler: CcCompilerDescriptor,
543    arguments: Vec<String>,
544    required_inputs: Vec<String>,
545}
546
547/// Normalized input names from the last successful execution of one modeled
548/// compile.
549#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(deny_unknown_fields)]
551pub struct CcInputPrediction {
552    /// Prediction schema version.
553    pub version: u8,
554    /// Normalized input paths, including include-manifest entries.
555    pub inputs: Vec<String>,
556    /// Names of environment variables that entered the key.
557    pub environment: Vec<String>,
558    /// Compiler wall time from the successful invocation that produced this
559    /// prediction. Zero means no timing hint was recorded.
560    #[serde(default, skip_serializing_if = "is_zero")]
561    pub compiler_duration_ns: u64,
562    /// Source file name associated with the timing hint.
563    #[serde(default, skip_serializing_if = "String::is_empty")]
564    pub source_name: String,
565}
566
567fn is_zero(value: &u64) -> bool {
568    *value == 0
569}
570
571/// One parsed and admitted argument.
572#[derive(Debug, Clone, PartialEq, Eq)]
573enum Argument {
574    /// Keyed verbatim.
575    Plain(String),
576    /// Keyed with its path normalized.
577    Path { flag: String, path: PathBuf },
578    /// A prefix rewrite: the source path normalizes, the replacement does not.
579    PrefixMap {
580        flag: String,
581        from: PathBuf,
582        to: String,
583    },
584    /// The source file.
585    Source(PathBuf),
586}
587
588/// A parsed, admitted C or C++ compile.
589#[derive(Debug, Clone, PartialEq, Eq)]
590pub struct CcInvocation {
591    arguments: Vec<Argument>,
592    source: PathBuf,
593    output: PathBuf,
594    include_dirs: Vec<PathBuf>,
595    required_inputs: Vec<PathBuf>,
596    language: CcLanguage,
597    sysroot: Option<PathBuf>,
598}
599
600impl CcInvocation {
601    /// Parse a driver command line, admitting only modeled single-object
602    /// compiles.
603    pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
604        Parser::new(arguments).parse()
605    }
606
607    /// Source file this invocation compiles.
608    pub fn source(&self) -> &Path {
609        &self.source
610    }
611
612    /// Object file this invocation produces.
613    pub fn output(&self) -> &Path {
614        &self.output
615    }
616
617    /// Include search directories named on the command line, in order.
618    pub fn include_dirs(&self) -> &[PathBuf] {
619        &self.include_dirs
620    }
621
622    /// Files that must appear among the discovered inputs.
623    pub fn required_inputs(&self) -> &[PathBuf] {
624        &self.required_inputs
625    }
626
627    /// Language the driver compiles.
628    pub fn language(&self) -> CcLanguage {
629        self.language
630    }
631
632    /// Sysroot named on the command line, if any.
633    pub fn sysroot(&self) -> Option<&Path> {
634        self.sysroot.as_deref()
635    }
636
637    /// Short label used for timing statistics.
638    pub fn source_name(&self) -> String {
639        self.source
640            .file_name()
641            .map(|name| name.to_string_lossy().into_owned())
642            .unwrap_or_default()
643    }
644
645    /// Arguments to append so the driver writes a dependency list beside the
646    /// object.
647    ///
648    /// `-MD` rather than `-MMD`: system headers are exactly the inputs most
649    /// likely to change without any other key component noticing, because the
650    /// compiler identity does not cover the C library or the platform SDK.
651    pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
652        vec!["-MD".into(), "-MF".into(), depfile.into()]
653    }
654
655    /// Digest of the pre-input fingerprint, used to look up a prediction.
656    pub fn invocation_digest(
657        &self,
658        context: &CcActionContext,
659    ) -> Result<CacheDigest, CcBypassReason> {
660        let builder = ActionBuilder::new(self, context.clone());
661        let descriptor = builder.invocation_descriptor()?;
662        let bytes = canonical_json(&descriptor)
663            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
664        Ok(CacheDigest::blake3(&bytes))
665    }
666
667    /// Build the canonical action for this invocation and its discovered
668    /// inputs.
669    pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
670        ActionBuilder::new(self, context).build()
671    }
672
673    /// Record the normalized inputs of a successful compile so the next cold
674    /// invocation can rebuild the same key before compiling.
675    pub fn prediction(
676        &self,
677        context: &CcActionContext,
678        compiler_duration_ns: u64,
679    ) -> Result<CcInputPrediction, CcBypassReason> {
680        let builder = ActionBuilder::new(self, context.clone());
681        let mut inputs = context
682            .inputs
683            .iter()
684            .map(|input| builder.normalize_input_path(&input.path))
685            .collect::<Result<Vec<_>, _>>()?;
686        inputs.sort();
687        inputs.dedup();
688        Ok(CcInputPrediction {
689            version: 1,
690            inputs,
691            environment: context.environment.keys().cloned().collect(),
692            compiler_duration_ns,
693            source_name: self.source_name(),
694        })
695    }
696}
697
698impl CcInputPrediction {
699    /// Rehash the predicted paths and recompute include manifests. The caller
700    /// still recomputes the full action digest, so changed inputs are misses.
701    pub fn discover(
702        &self,
703        working_dir: &Path,
704        path_mappings: &[PathMapping],
705        digests: &dyn FileDigestCache,
706    ) -> Result<CcDiscoveredInputs, CcBypassReason> {
707        if self.version != 1 {
708            return Err(CcBypassReason::UnsupportedPrediction);
709        }
710        if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
711            return Err(CcBypassReason::UnsupportedPrediction);
712        }
713        let mappings = PathMapping::ordered(path_mappings);
714        let mut files = BTreeSet::new();
715        let mut directories = BTreeSet::new();
716        for entry in &self.inputs {
717            match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
718                Some(directory) => {
719                    directories.insert(denormalize_path(directory, &mappings)?);
720                }
721                None => {
722                    files.insert(denormalize_path(entry, &mappings)?);
723                }
724            }
725        }
726        CcDiscoveredInputs::collect(working_dir, files, directories, digests)
727    }
728}
729
730/// Resolve a normalized key path back to a host path.
731///
732/// Placeholder entries expand through their mapping; a verbatim entry is
733/// accepted only when it still lies beneath an admitted system root, so a
734/// prediction cannot name an arbitrary absolute path.
735fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
736    for mapping in mappings {
737        let prefix = format!("${{{}}}", mapping.placeholder);
738        let suffix = if value == prefix {
739            ""
740        } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
741            suffix
742        } else {
743            continue;
744        };
745        if !mapping.root.is_absolute() || !safe_suffix(suffix) {
746            return Err(CcBypassReason::InvalidPredictedInput(value.into()));
747        }
748        let mut path = normalize_components(&mapping.root);
749        path.extend(suffix.split('/').filter(|component| !component.is_empty()));
750        return Ok(path);
751    }
752    // A verbatim entry names a machine path rather than a placeholder. It is
753    // admitted only beneath a system root, and only spelled literally: a
754    // traversal component would let a prediction reach outside that root.
755    let path = PathBuf::from(value);
756    if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
757        return Ok(path);
758    }
759    Err(CcBypassReason::InvalidPredictedInput(value.into()))
760}
761
762fn safe_suffix(suffix: &str) -> bool {
763    suffix.is_empty()
764        || !suffix.split('/').any(|component| {
765            component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
766        })
767}
768
769/// Whether a path lies beneath a root whose location is a machine property.
770pub fn is_system_path(path: &Path) -> bool {
771    SYSTEM_ROOTS
772        .iter()
773        .any(|root| path.starts_with(Path::new(root)))
774}
775
776fn normalize_components(path: &Path) -> PathBuf {
777    let mut normalized = PathBuf::new();
778    for component in path.components() {
779        match component {
780            Component::CurDir => {}
781            Component::ParentDir => {
782                normalized.pop();
783            }
784            component => normalized.push(component.as_os_str()),
785        }
786    }
787    normalized
788}
789
790/// Read the modeled environment, rejecting variables that change the compile in
791/// a way the argv model cannot see.
792pub fn environment_inputs<F>(
793    lookup: F,
794    sysroot: Option<&Path>,
795) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
796where
797    F: Fn(&str) -> Option<String>,
798{
799    for name in BYPASS_ENVIRONMENT {
800        if lookup(name).is_some() {
801            return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
802        }
803    }
804    let mut environment = BTreeMap::new();
805    for name in KEYED_ENVIRONMENT {
806        // An explicit `-isysroot` on the command line already pins the SDK, and
807        // it is what the driver honors, so the variable stops being an input.
808        if *name == "SDKROOT" && sysroot.is_some() {
809            continue;
810        }
811        environment.insert((*name).to_string(), lookup(name));
812    }
813    Ok(environment)
814}
815
816struct ActionBuilder<'a> {
817    invocation: &'a CcInvocation,
818    context: CcActionContext,
819    mappings: Vec<PathMapping>,
820}
821
822impl<'a> ActionBuilder<'a> {
823    fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
824        context.path_mappings = PathMapping::ordered(&context.path_mappings);
825        let mappings = context.path_mappings.clone();
826        Self {
827            invocation,
828            context,
829            mappings,
830        }
831    }
832
833    fn build(self) -> Result<CcAction, CcBypassReason> {
834        self.validate_mappings()?;
835        let invocation = self.invocation_descriptor()?;
836
837        let mut inputs = BTreeMap::<String, CacheDigest>::new();
838        for input in &self.context.inputs {
839            input.digest.validate().map_err(|_| {
840                CcBypassReason::InvalidInputDigest(input.path.display().to_string())
841            })?;
842            let path = self.normalize_input_path(&input.path)?;
843            if inputs
844                .insert(path.clone(), input.digest.clone())
845                .is_some_and(|existing| existing != input.digest)
846            {
847                return Err(CcBypassReason::ConflictingInput(path));
848            }
849        }
850        let required = self
851            .invocation
852            .required_inputs
853            .iter()
854            .map(|path| self.normalize_path(path))
855            .collect::<Result<BTreeSet<_>, _>>()?;
856        if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
857            return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
858        }
859        let inputs = inputs
860            .into_iter()
861            .map(|(path, digest)| CcInputDescriptor { path, digest })
862            .collect();
863        let descriptor = CcActionDescriptor {
864            version: ACTION_SCHEMA_VERSION,
865            kind: "cc",
866            adapter_version: ADAPTER_VERSION,
867            compiler: invocation.compiler,
868            arguments: invocation.arguments,
869            environment: self.context.environment.clone(),
870            inputs,
871        };
872        let bytes = canonical_json(&descriptor)
873            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
874        let digest = CacheDigest::blake3(&bytes);
875        Ok(CcAction { digest, bytes })
876    }
877
878    fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
879        self.validate_mappings()?;
880        let arguments = self
881            .invocation
882            .arguments
883            .iter()
884            .map(|argument| self.normalize_argument(argument))
885            .collect::<Result<Vec<_>, _>>()?;
886        let required_inputs = self
887            .invocation
888            .required_inputs
889            .iter()
890            .map(|path| self.normalize_path(path))
891            .collect::<Result<BTreeSet<_>, _>>()?
892            .into_iter()
893            .collect();
894        Ok(CcInvocationDescriptor {
895            version: ACTION_SCHEMA_VERSION,
896            kind: "cc",
897            adapter_version: ADAPTER_VERSION,
898            compiler: self.compiler_descriptor(),
899            arguments,
900            required_inputs,
901        })
902    }
903
904    fn compiler_descriptor(&self) -> CcCompilerDescriptor {
905        CcCompilerDescriptor {
906            assembler: self.context.compiler.assembler.clone(),
907            family: self.context.compiler.family.as_str().into(),
908            target: self.context.compiler.target.clone(),
909            version_text: self.context.compiler.version_text.clone(),
910        }
911    }
912
913    fn validate_mappings(&self) -> Result<(), CcBypassReason> {
914        if !self.context.working_dir.is_absolute() {
915            return Err(CcBypassReason::RelativeWorkingDirectory(
916                self.context.working_dir.clone(),
917            ));
918        }
919        let mut roots = BTreeSet::new();
920        let mut placeholders = BTreeSet::new();
921        for mapping in &self.mappings {
922            if !mapping.root.is_absolute() {
923                return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
924            }
925            if mapping.placeholder.is_empty()
926                || !mapping
927                    .placeholder
928                    .bytes()
929                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
930                || !roots.insert(normalize_components(&mapping.root))
931                || !placeholders.insert(&mapping.placeholder)
932            {
933                return Err(CcBypassReason::InvalidPathPlaceholder(
934                    mapping.placeholder.clone(),
935                ));
936            }
937        }
938        Ok(())
939    }
940
941    fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
942        match argument {
943            Argument::Plain(value) => Ok(value.clone()),
944            Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
945            Argument::PrefixMap { flag, from, to } => {
946                Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
947            }
948            Argument::Source(path) => Ok(self.normalize_path(path)?),
949        }
950    }
951
952    /// Normalize a path that names a compilation input or search root.
953    ///
954    /// A path beneath a mapped root becomes a placeholder so equivalent
955    /// checkouts agree. A path beneath a system root stays verbatim: its
956    /// location is a property of the machine, and its contents are digested
957    /// like any other input.
958    fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
959        match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
960            Ok(normalized) => Ok(normalized),
961            Err(reason) => {
962                let absolute = absolute_path(path, &self.context.working_dir);
963                if is_system_path(&absolute) {
964                    return absolute
965                        .to_str()
966                        .map(ToOwned::to_owned)
967                        .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
968                }
969                Err(reason.into())
970            }
971        }
972    }
973
974    fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
975        match path.to_str().and_then(|path| {
976            path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
977                .map(ToOwned::to_owned)
978        }) {
979            Some(directory) => Ok(format!(
980                "{INCLUDE_MANIFEST_PREFIX}{}",
981                self.normalize_path(Path::new(&directory))?
982            )),
983            None => self.normalize_path(path),
984        }
985    }
986}
987
988fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
989    if path.is_absolute() {
990        normalize_components(path)
991    } else {
992        normalize_components(&working_dir.join(path))
993    }
994}
995
996struct Parser<'a> {
997    arguments: &'a [OsString],
998    index: usize,
999    parsed: Vec<Argument>,
1000    source: Option<PathBuf>,
1001    output: Option<PathBuf>,
1002    include_dirs: Vec<PathBuf>,
1003    required_inputs: Vec<PathBuf>,
1004    sysroot: Option<PathBuf>,
1005    explicit_language: Option<CcLanguage>,
1006    compiling: bool,
1007}
1008
1009impl<'a> Parser<'a> {
1010    fn new(arguments: &'a [OsString]) -> Self {
1011        Self {
1012            arguments,
1013            index: 0,
1014            parsed: Vec::new(),
1015            source: None,
1016            output: None,
1017            include_dirs: Vec::new(),
1018            required_inputs: Vec::new(),
1019            sysroot: None,
1020            explicit_language: None,
1021            compiling: false,
1022        }
1023    }
1024
1025    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1026        while self.index < self.arguments.len() {
1027            let value = self.current()?.to_string();
1028            self.index += 1;
1029            if value == "-" {
1030                return Err(CcBypassReason::StandardInput);
1031            }
1032            if let Some(argfile) = value.strip_prefix('@') {
1033                return Err(CcBypassReason::ResponseFile(argfile.into()));
1034            }
1035            if value.starts_with('-') {
1036                self.parse_flag(&value)?;
1037            } else {
1038                self.parse_input(&value)?;
1039            }
1040        }
1041
1042        if !self.compiling {
1043            return Err(CcBypassReason::NotACompile);
1044        }
1045        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1046        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1047        let language = self.language(&source)?;
1048        self.required_inputs.push(source.clone());
1049        Ok(CcInvocation {
1050            arguments: self.parsed,
1051            source,
1052            output,
1053            include_dirs: self.include_dirs,
1054            required_inputs: self.required_inputs,
1055            language,
1056            sysroot: self.sysroot,
1057        })
1058    }
1059
1060    fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1061        if let Some(language) = self.explicit_language {
1062            return Ok(language);
1063        }
1064        let extension = source
1065            .extension()
1066            .and_then(|extension| extension.to_str())
1067            .unwrap_or_default();
1068        match extension {
1069            "c" => Ok(CcLanguage::C),
1070            "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1071            _ => Err(CcBypassReason::UnsupportedLanguage(
1072                source.display().to_string(),
1073            )),
1074        }
1075    }
1076
1077    fn current(&self) -> Result<&str, CcBypassReason> {
1078        self.arguments[self.index]
1079            .to_str()
1080            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1081    }
1082
1083    fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1084        if let Some(value) = inline
1085            && !value.is_empty()
1086        {
1087            return Ok(value.into());
1088        }
1089        if self.index >= self.arguments.len() {
1090            return Err(CcBypassReason::MissingValue(flag.into()));
1091        }
1092        let value = self.current()?.to_string();
1093        self.index += 1;
1094        Ok(value)
1095    }
1096
1097    fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1098        if self.source.is_some() {
1099            return Err(CcBypassReason::MultipleInputs);
1100        }
1101        let path = PathBuf::from(value);
1102        // With an explicit `-x`, the driver ignores the extension entirely;
1103        // without one, the extension is the only thing that decides the
1104        // language, so an unmodeled extension has to bypass here.
1105        if self.explicit_language.is_none() {
1106            let extension = path
1107                .extension()
1108                .and_then(|extension| extension.to_str())
1109                .unwrap_or_default();
1110            if !matches!(extension, "c" | "cc" | "cpp" | "cxx" | "c++") {
1111                return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1112            }
1113        }
1114        self.source = Some(path.clone());
1115        self.parsed.push(Argument::Source(path));
1116        Ok(())
1117    }
1118
1119    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1120        if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1121            return Err(CcBypassReason::CompilerQuery);
1122        }
1123        if matches!(value, "-E" | "-S") {
1124            return Err(CcBypassReason::NonObjectOutput(value.into()));
1125        }
1126        if value.starts_with("-M") {
1127            return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1128        }
1129        if value.starts_with("-save-temps") {
1130            return Err(CcBypassReason::SaveTemps(value.into()));
1131        }
1132        if value == "--coverage" {
1133            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1134        }
1135        if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1136            || value.starts_with("-Wp,")
1137            || value.starts_with("-Wa,")
1138            || value.starts_with("-Wl,")
1139        {
1140            // The forwarded options are the compilation's real inputs and they
1141            // are not modeled, so consuming the value would not make this safe.
1142            return Err(CcBypassReason::ToolPassthrough(value.into()));
1143        }
1144        if value.starts_with("-include-pch") || value == "-emit-pch" {
1145            return Err(CcBypassReason::PrecompiledHeader(value.into()));
1146        }
1147
1148        if value == "-c" {
1149            self.compiling = true;
1150            self.parsed.push(Argument::Plain(value.into()));
1151            return Ok(());
1152        }
1153        if SUPPORTED_BARE_FLAGS.contains(&value)
1154            || SUPPORTED_O_FLAGS.contains(&value)
1155            || SUPPORTED_G_FLAGS.contains(&value)
1156            || value.starts_with("-std=")
1157        {
1158            self.parsed.push(Argument::Plain(value.into()));
1159            return Ok(());
1160        }
1161        if let Some(rest) = value.strip_prefix("-o") {
1162            let path = self.take_value("-o", Some(rest))?;
1163            // A repeated `-o` follows the driver: the last one names the file
1164            // that is produced. Every occurrence still enters the key.
1165            self.output = Some(PathBuf::from(&path));
1166            self.parsed.push(Argument::Path {
1167                flag: "-o".into(),
1168                path: PathBuf::from(path),
1169            });
1170            return Ok(());
1171        }
1172        if let Some(rest) = value.strip_prefix("-I") {
1173            let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1174            self.include_dirs.push(path.clone());
1175            self.parsed.push(Argument::Path {
1176                flag: "-I".into(),
1177                path,
1178            });
1179            return Ok(());
1180        }
1181        if SEPARATE_PATH_FLAGS.contains(&value) {
1182            let path = PathBuf::from(self.take_value(value, None)?);
1183            match value {
1184                "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1185                "-isysroot" => self.sysroot = Some(path.clone()),
1186                // `-include` and `-imacros` are deliberately not required
1187                // inputs. The driver resolves the name through the include
1188                // chain, so the file need not exist relative to the working
1189                // directory, and the dependency list names it at whatever path
1190                // it was actually found at.
1191                _ => {}
1192            }
1193            self.parsed.push(Argument::Path {
1194                flag: value.into(),
1195                path,
1196            });
1197            return Ok(());
1198        }
1199        // `--include=<file>` is the long spelling of `-include <file>`; the
1200        // `cc` crate emits it for prefixed headers.
1201        if let Some(rest) = value.strip_prefix("--include=") {
1202            let path = PathBuf::from(rest);
1203            self.parsed.push(Argument::Path {
1204                flag: "-include".into(),
1205                path,
1206            });
1207            return Ok(());
1208        }
1209        if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1210            value
1211                .strip_prefix(&format!("{flag}="))
1212                .map(|rest| (*flag, rest))
1213        }) {
1214            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1215            self.parsed.push(Argument::PrefixMap {
1216                flag: flag.into(),
1217                from: PathBuf::from(from),
1218                to: to.into(),
1219            });
1220            return Ok(());
1221        }
1222        // `--param name=value` tunes the optimizer; its text fully describes it.
1223        if value == "--param" {
1224            let parameter = self.take_value("--param", None)?;
1225            self.parsed
1226                .push(Argument::Plain(format!("--param={parameter}")));
1227            return Ok(());
1228        }
1229        if let Some(parameter) = value.strip_prefix("--param=") {
1230            self.parsed
1231                .push(Argument::Plain(format!("--param={parameter}")));
1232            return Ok(());
1233        }
1234        if let Some(rest) = value.strip_prefix("--sysroot=") {
1235            let path = PathBuf::from(rest);
1236            self.sysroot = Some(path.clone());
1237            self.parsed.push(Argument::Path {
1238                flag: "--sysroot".into(),
1239                path,
1240            });
1241            return Ok(());
1242        }
1243        if let Some(rest) = value
1244            .strip_prefix("-D")
1245            .or_else(|| value.strip_prefix("-U"))
1246        {
1247            let flag = &value[..2];
1248            let definition = self.take_value(flag, Some(rest))?;
1249            self.parsed
1250                .push(Argument::Plain(format!("{flag}{definition}")));
1251            return Ok(());
1252        }
1253        if let Some(rest) = value.strip_prefix("-x") {
1254            let language = self.take_value("-x", Some(rest))?;
1255            self.explicit_language = Some(match language.as_str() {
1256                "c" => CcLanguage::C,
1257                "c++" => CcLanguage::Cxx,
1258                other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1259            });
1260            self.parsed.push(Argument::Plain(format!("-x{language}")));
1261            return Ok(());
1262        }
1263        if let Some(target) = value.strip_prefix("--target=") {
1264            self.parsed
1265                .push(Argument::Plain(format!("--target={target}")));
1266            return Ok(());
1267        }
1268        if value == "-target" {
1269            let target = self.take_value("-target", None)?;
1270            self.parsed
1271                .push(Argument::Plain(format!("--target={target}")));
1272            return Ok(());
1273        }
1274        if value == "-arch" {
1275            let arch = self.take_value("-arch", None)?;
1276            self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1277            return Ok(());
1278        }
1279        if let Some(option) = value.strip_prefix("-f") {
1280            return self.parse_f_flag(value, option);
1281        }
1282        if let Some(option) = value.strip_prefix("-m") {
1283            return self.parse_m_flag(value, option);
1284        }
1285        if value.starts_with("-g") {
1286            // `-gsplit-dwarf` writes a `.dwo` beside the object; every other
1287            // unlisted `-g` spelling is simply unmodeled.
1288            return Err(if value.starts_with("-gsplit-dwarf") {
1289                CcBypassReason::SplitDebugOutput(value.into())
1290            } else {
1291                CcBypassReason::UnknownFlag(value.into())
1292            });
1293        }
1294        if value.starts_with("-W") {
1295            // Warning selection changes only diagnostics, which are replayed
1296            // from the cache, and the exit status, and only successful
1297            // compiles are ever published.
1298            self.parsed.push(Argument::Plain(value.into()));
1299            return Ok(());
1300        }
1301        Err(CcBypassReason::UnknownFlag(value.into()))
1302    }
1303
1304    fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1305        if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1306            return Err(CcBypassReason::Plugin(value.into()));
1307        }
1308        if option.starts_with("profile-") || option == "test-coverage" {
1309            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1310        }
1311        let name = option.split_once('=').map_or(option, |(name, _)| name);
1312        if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1313            return Err(CcBypassReason::UnknownFlag(value.into()));
1314        }
1315        self.parsed.push(Argument::Plain(value.into()));
1316        Ok(())
1317    }
1318
1319    fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1320        if option == "llvm" {
1321            return Err(CcBypassReason::ToolPassthrough(value.into()));
1322        }
1323        // `-march=native` and its relatives resolve against whatever CPU this
1324        // machine has. The resulting object is not a function of the key, so
1325        // another machine could otherwise restore code its processor cannot
1326        // run.
1327        if let Some((name, selection)) = option.split_once('=')
1328            && matches!(name, "arch" | "cpu" | "tune")
1329            && matches!(selection, "native" | "host")
1330        {
1331            return Err(CcBypassReason::LocalCpuTarget(value.into()));
1332        }
1333        let name = option.split_once('=').map_or(option, |(name, _)| name);
1334        if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1335            return Err(CcBypassReason::UnknownFlag(value.into()));
1336        }
1337        self.parsed.push(Argument::Plain(value.into()));
1338        Ok(())
1339    }
1340}
1341
1342#[cfg(test)]
1343#[path = "cc_cache_tests.rs"]
1344mod tests;