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, 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    ) -> Result<CcDiscoveredInputs, CcBypassReason> {
706        if self.version != 1 {
707            return Err(CcBypassReason::UnsupportedPrediction);
708        }
709        if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
710            return Err(CcBypassReason::UnsupportedPrediction);
711        }
712        let mappings = PathMapping::ordered(path_mappings);
713        let mut files = BTreeSet::new();
714        let mut directories = BTreeSet::new();
715        for entry in &self.inputs {
716            match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
717                Some(directory) => {
718                    directories.insert(denormalize_path(directory, &mappings)?);
719                }
720                None => {
721                    files.insert(denormalize_path(entry, &mappings)?);
722                }
723            }
724        }
725        CcDiscoveredInputs::collect(working_dir, files, directories)
726    }
727}
728
729/// Resolve a normalized key path back to a host path.
730///
731/// Placeholder entries expand through their mapping; a verbatim entry is
732/// accepted only when it still lies beneath an admitted system root, so a
733/// prediction cannot name an arbitrary absolute path.
734fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
735    for mapping in mappings {
736        let prefix = format!("${{{}}}", mapping.placeholder);
737        let suffix = if value == prefix {
738            ""
739        } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
740            suffix
741        } else {
742            continue;
743        };
744        if !mapping.root.is_absolute() || !safe_suffix(suffix) {
745            return Err(CcBypassReason::InvalidPredictedInput(value.into()));
746        }
747        let mut path = normalize_components(&mapping.root);
748        path.extend(suffix.split('/').filter(|component| !component.is_empty()));
749        return Ok(path);
750    }
751    // A verbatim entry names a machine path rather than a placeholder. It is
752    // admitted only beneath a system root, and only spelled literally: a
753    // traversal component would let a prediction reach outside that root.
754    let path = PathBuf::from(value);
755    if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
756        return Ok(path);
757    }
758    Err(CcBypassReason::InvalidPredictedInput(value.into()))
759}
760
761fn safe_suffix(suffix: &str) -> bool {
762    suffix.is_empty()
763        || !suffix.split('/').any(|component| {
764            component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
765        })
766}
767
768/// Whether a path lies beneath a root whose location is a machine property.
769pub fn is_system_path(path: &Path) -> bool {
770    SYSTEM_ROOTS
771        .iter()
772        .any(|root| path.starts_with(Path::new(root)))
773}
774
775fn normalize_components(path: &Path) -> PathBuf {
776    let mut normalized = PathBuf::new();
777    for component in path.components() {
778        match component {
779            Component::CurDir => {}
780            Component::ParentDir => {
781                normalized.pop();
782            }
783            component => normalized.push(component.as_os_str()),
784        }
785    }
786    normalized
787}
788
789/// Read the modeled environment, rejecting variables that change the compile in
790/// a way the argv model cannot see.
791pub fn environment_inputs<F>(
792    lookup: F,
793    sysroot: Option<&Path>,
794) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
795where
796    F: Fn(&str) -> Option<String>,
797{
798    for name in BYPASS_ENVIRONMENT {
799        if lookup(name).is_some() {
800            return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
801        }
802    }
803    let mut environment = BTreeMap::new();
804    for name in KEYED_ENVIRONMENT {
805        // An explicit `-isysroot` on the command line already pins the SDK, and
806        // it is what the driver honors, so the variable stops being an input.
807        if *name == "SDKROOT" && sysroot.is_some() {
808            continue;
809        }
810        environment.insert((*name).to_string(), lookup(name));
811    }
812    Ok(environment)
813}
814
815struct ActionBuilder<'a> {
816    invocation: &'a CcInvocation,
817    context: CcActionContext,
818    mappings: Vec<PathMapping>,
819}
820
821impl<'a> ActionBuilder<'a> {
822    fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
823        context.path_mappings = PathMapping::ordered(&context.path_mappings);
824        let mappings = context.path_mappings.clone();
825        Self {
826            invocation,
827            context,
828            mappings,
829        }
830    }
831
832    fn build(self) -> Result<CcAction, CcBypassReason> {
833        self.validate_mappings()?;
834        let invocation = self.invocation_descriptor()?;
835
836        let mut inputs = BTreeMap::<String, CacheDigest>::new();
837        for input in &self.context.inputs {
838            input.digest.validate().map_err(|_| {
839                CcBypassReason::InvalidInputDigest(input.path.display().to_string())
840            })?;
841            let path = self.normalize_input_path(&input.path)?;
842            if inputs
843                .insert(path.clone(), input.digest.clone())
844                .is_some_and(|existing| existing != input.digest)
845            {
846                return Err(CcBypassReason::ConflictingInput(path));
847            }
848        }
849        let required = self
850            .invocation
851            .required_inputs
852            .iter()
853            .map(|path| self.normalize_path(path))
854            .collect::<Result<BTreeSet<_>, _>>()?;
855        if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
856            return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
857        }
858        let inputs = inputs
859            .into_iter()
860            .map(|(path, digest)| CcInputDescriptor { path, digest })
861            .collect();
862        let descriptor = CcActionDescriptor {
863            version: ACTION_SCHEMA_VERSION,
864            kind: "cc",
865            adapter_version: ADAPTER_VERSION,
866            compiler: invocation.compiler,
867            arguments: invocation.arguments,
868            environment: self.context.environment.clone(),
869            inputs,
870        };
871        let bytes = canonical_json(&descriptor)
872            .map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
873        let digest = CacheDigest::blake3(&bytes);
874        Ok(CcAction { digest, bytes })
875    }
876
877    fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
878        self.validate_mappings()?;
879        let arguments = self
880            .invocation
881            .arguments
882            .iter()
883            .map(|argument| self.normalize_argument(argument))
884            .collect::<Result<Vec<_>, _>>()?;
885        let required_inputs = self
886            .invocation
887            .required_inputs
888            .iter()
889            .map(|path| self.normalize_path(path))
890            .collect::<Result<BTreeSet<_>, _>>()?
891            .into_iter()
892            .collect();
893        Ok(CcInvocationDescriptor {
894            version: ACTION_SCHEMA_VERSION,
895            kind: "cc",
896            adapter_version: ADAPTER_VERSION,
897            compiler: self.compiler_descriptor(),
898            arguments,
899            required_inputs,
900        })
901    }
902
903    fn compiler_descriptor(&self) -> CcCompilerDescriptor {
904        CcCompilerDescriptor {
905            assembler: self.context.compiler.assembler.clone(),
906            family: self.context.compiler.family.as_str().into(),
907            target: self.context.compiler.target.clone(),
908            version_text: self.context.compiler.version_text.clone(),
909        }
910    }
911
912    fn validate_mappings(&self) -> Result<(), CcBypassReason> {
913        if !self.context.working_dir.is_absolute() {
914            return Err(CcBypassReason::RelativeWorkingDirectory(
915                self.context.working_dir.clone(),
916            ));
917        }
918        let mut roots = BTreeSet::new();
919        let mut placeholders = BTreeSet::new();
920        for mapping in &self.mappings {
921            if !mapping.root.is_absolute() {
922                return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
923            }
924            if mapping.placeholder.is_empty()
925                || !mapping
926                    .placeholder
927                    .bytes()
928                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
929                || !roots.insert(normalize_components(&mapping.root))
930                || !placeholders.insert(&mapping.placeholder)
931            {
932                return Err(CcBypassReason::InvalidPathPlaceholder(
933                    mapping.placeholder.clone(),
934                ));
935            }
936        }
937        Ok(())
938    }
939
940    fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
941        match argument {
942            Argument::Plain(value) => Ok(value.clone()),
943            Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
944            Argument::PrefixMap { flag, from, to } => {
945                Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
946            }
947            Argument::Source(path) => Ok(self.normalize_path(path)?),
948        }
949    }
950
951    /// Normalize a path that names a compilation input or search root.
952    ///
953    /// A path beneath a mapped root becomes a placeholder so equivalent
954    /// checkouts agree. A path beneath a system root stays verbatim: its
955    /// location is a property of the machine, and its contents are digested
956    /// like any other input.
957    fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
958        match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
959            Ok(normalized) => Ok(normalized),
960            Err(reason) => {
961                let absolute = absolute_path(path, &self.context.working_dir);
962                if is_system_path(&absolute) {
963                    return absolute
964                        .to_str()
965                        .map(ToOwned::to_owned)
966                        .ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
967                }
968                Err(reason.into())
969            }
970        }
971    }
972
973    fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
974        match path.to_str().and_then(|path| {
975            path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
976                .map(ToOwned::to_owned)
977        }) {
978            Some(directory) => Ok(format!(
979                "{INCLUDE_MANIFEST_PREFIX}{}",
980                self.normalize_path(Path::new(&directory))?
981            )),
982            None => self.normalize_path(path),
983        }
984    }
985}
986
987fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
988    if path.is_absolute() {
989        normalize_components(path)
990    } else {
991        normalize_components(&working_dir.join(path))
992    }
993}
994
995struct Parser<'a> {
996    arguments: &'a [OsString],
997    index: usize,
998    parsed: Vec<Argument>,
999    source: Option<PathBuf>,
1000    output: Option<PathBuf>,
1001    include_dirs: Vec<PathBuf>,
1002    required_inputs: Vec<PathBuf>,
1003    sysroot: Option<PathBuf>,
1004    explicit_language: Option<CcLanguage>,
1005    compiling: bool,
1006}
1007
1008impl<'a> Parser<'a> {
1009    fn new(arguments: &'a [OsString]) -> Self {
1010        Self {
1011            arguments,
1012            index: 0,
1013            parsed: Vec::new(),
1014            source: None,
1015            output: None,
1016            include_dirs: Vec::new(),
1017            required_inputs: Vec::new(),
1018            sysroot: None,
1019            explicit_language: None,
1020            compiling: false,
1021        }
1022    }
1023
1024    fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
1025        while self.index < self.arguments.len() {
1026            let value = self.current()?.to_string();
1027            self.index += 1;
1028            if value == "-" {
1029                return Err(CcBypassReason::StandardInput);
1030            }
1031            if let Some(argfile) = value.strip_prefix('@') {
1032                return Err(CcBypassReason::ResponseFile(argfile.into()));
1033            }
1034            if value.starts_with('-') {
1035                self.parse_flag(&value)?;
1036            } else {
1037                self.parse_input(&value)?;
1038            }
1039        }
1040
1041        if !self.compiling {
1042            return Err(CcBypassReason::NotACompile);
1043        }
1044        let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
1045        let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
1046        let language = self.language(&source)?;
1047        self.required_inputs.push(source.clone());
1048        Ok(CcInvocation {
1049            arguments: self.parsed,
1050            source,
1051            output,
1052            include_dirs: self.include_dirs,
1053            required_inputs: self.required_inputs,
1054            language,
1055            sysroot: self.sysroot,
1056        })
1057    }
1058
1059    fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
1060        if let Some(language) = self.explicit_language {
1061            return Ok(language);
1062        }
1063        let extension = source
1064            .extension()
1065            .and_then(|extension| extension.to_str())
1066            .unwrap_or_default();
1067        match extension {
1068            "c" => Ok(CcLanguage::C),
1069            "cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
1070            _ => Err(CcBypassReason::UnsupportedLanguage(
1071                source.display().to_string(),
1072            )),
1073        }
1074    }
1075
1076    fn current(&self) -> Result<&str, CcBypassReason> {
1077        self.arguments[self.index]
1078            .to_str()
1079            .ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
1080    }
1081
1082    fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
1083        if let Some(value) = inline
1084            && !value.is_empty()
1085        {
1086            return Ok(value.into());
1087        }
1088        if self.index >= self.arguments.len() {
1089            return Err(CcBypassReason::MissingValue(flag.into()));
1090        }
1091        let value = self.current()?.to_string();
1092        self.index += 1;
1093        Ok(value)
1094    }
1095
1096    fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
1097        if self.source.is_some() {
1098            return Err(CcBypassReason::MultipleInputs);
1099        }
1100        let path = PathBuf::from(value);
1101        // With an explicit `-x`, the driver ignores the extension entirely;
1102        // without one, the extension is the only thing that decides the
1103        // language, so an unmodeled extension has to bypass here.
1104        if self.explicit_language.is_none() {
1105            let extension = path
1106                .extension()
1107                .and_then(|extension| extension.to_str())
1108                .unwrap_or_default();
1109            if !matches!(extension, "c" | "cc" | "cpp" | "cxx" | "c++") {
1110                return Err(CcBypassReason::UnsupportedLanguage(value.into()));
1111            }
1112        }
1113        self.source = Some(path.clone());
1114        self.parsed.push(Argument::Source(path));
1115        Ok(())
1116    }
1117
1118    fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
1119        if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
1120            return Err(CcBypassReason::CompilerQuery);
1121        }
1122        if matches!(value, "-E" | "-S") {
1123            return Err(CcBypassReason::NonObjectOutput(value.into()));
1124        }
1125        if value.starts_with("-M") {
1126            return Err(CcBypassReason::CallerDependencyFlags(value.into()));
1127        }
1128        if value.starts_with("-save-temps") {
1129            return Err(CcBypassReason::SaveTemps(value.into()));
1130        }
1131        if value == "--coverage" {
1132            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1133        }
1134        if TOOL_PASSTHROUGH_FLAGS.contains(&value)
1135            || value.starts_with("-Wp,")
1136            || value.starts_with("-Wa,")
1137            || value.starts_with("-Wl,")
1138        {
1139            // The forwarded options are the compilation's real inputs and they
1140            // are not modeled, so consuming the value would not make this safe.
1141            return Err(CcBypassReason::ToolPassthrough(value.into()));
1142        }
1143        if value.starts_with("-include-pch") || value == "-emit-pch" {
1144            return Err(CcBypassReason::PrecompiledHeader(value.into()));
1145        }
1146
1147        if value == "-c" {
1148            self.compiling = true;
1149            self.parsed.push(Argument::Plain(value.into()));
1150            return Ok(());
1151        }
1152        if SUPPORTED_BARE_FLAGS.contains(&value)
1153            || SUPPORTED_O_FLAGS.contains(&value)
1154            || SUPPORTED_G_FLAGS.contains(&value)
1155            || value.starts_with("-std=")
1156        {
1157            self.parsed.push(Argument::Plain(value.into()));
1158            return Ok(());
1159        }
1160        if let Some(rest) = value.strip_prefix("-o") {
1161            let path = self.take_value("-o", Some(rest))?;
1162            // A repeated `-o` follows the driver: the last one names the file
1163            // that is produced. Every occurrence still enters the key.
1164            self.output = Some(PathBuf::from(&path));
1165            self.parsed.push(Argument::Path {
1166                flag: "-o".into(),
1167                path: PathBuf::from(path),
1168            });
1169            return Ok(());
1170        }
1171        if let Some(rest) = value.strip_prefix("-I") {
1172            let path = PathBuf::from(self.take_value("-I", Some(rest))?);
1173            self.include_dirs.push(path.clone());
1174            self.parsed.push(Argument::Path {
1175                flag: "-I".into(),
1176                path,
1177            });
1178            return Ok(());
1179        }
1180        if SEPARATE_PATH_FLAGS.contains(&value) {
1181            let path = PathBuf::from(self.take_value(value, None)?);
1182            match value {
1183                "-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
1184                "-isysroot" => self.sysroot = Some(path.clone()),
1185                // `-include` and `-imacros` are deliberately not required
1186                // inputs. The driver resolves the name through the include
1187                // chain, so the file need not exist relative to the working
1188                // directory, and the dependency list names it at whatever path
1189                // it was actually found at.
1190                _ => {}
1191            }
1192            self.parsed.push(Argument::Path {
1193                flag: value.into(),
1194                path,
1195            });
1196            return Ok(());
1197        }
1198        // `--include=<file>` is the long spelling of `-include <file>`; the
1199        // `cc` crate emits it for prefixed headers.
1200        if let Some(rest) = value.strip_prefix("--include=") {
1201            let path = PathBuf::from(rest);
1202            self.parsed.push(Argument::Path {
1203                flag: "-include".into(),
1204                path,
1205            });
1206            return Ok(());
1207        }
1208        if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
1209            value
1210                .strip_prefix(&format!("{flag}="))
1211                .map(|rest| (*flag, rest))
1212        }) {
1213            let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
1214            self.parsed.push(Argument::PrefixMap {
1215                flag: flag.into(),
1216                from: PathBuf::from(from),
1217                to: to.into(),
1218            });
1219            return Ok(());
1220        }
1221        // `--param name=value` tunes the optimizer; its text fully describes it.
1222        if value == "--param" {
1223            let parameter = self.take_value("--param", None)?;
1224            self.parsed
1225                .push(Argument::Plain(format!("--param={parameter}")));
1226            return Ok(());
1227        }
1228        if let Some(parameter) = value.strip_prefix("--param=") {
1229            self.parsed
1230                .push(Argument::Plain(format!("--param={parameter}")));
1231            return Ok(());
1232        }
1233        if let Some(rest) = value.strip_prefix("--sysroot=") {
1234            let path = PathBuf::from(rest);
1235            self.sysroot = Some(path.clone());
1236            self.parsed.push(Argument::Path {
1237                flag: "--sysroot".into(),
1238                path,
1239            });
1240            return Ok(());
1241        }
1242        if let Some(rest) = value
1243            .strip_prefix("-D")
1244            .or_else(|| value.strip_prefix("-U"))
1245        {
1246            let flag = &value[..2];
1247            let definition = self.take_value(flag, Some(rest))?;
1248            self.parsed
1249                .push(Argument::Plain(format!("{flag}{definition}")));
1250            return Ok(());
1251        }
1252        if let Some(rest) = value.strip_prefix("-x") {
1253            let language = self.take_value("-x", Some(rest))?;
1254            self.explicit_language = Some(match language.as_str() {
1255                "c" => CcLanguage::C,
1256                "c++" => CcLanguage::Cxx,
1257                other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
1258            });
1259            self.parsed.push(Argument::Plain(format!("-x{language}")));
1260            return Ok(());
1261        }
1262        if let Some(target) = value.strip_prefix("--target=") {
1263            self.parsed
1264                .push(Argument::Plain(format!("--target={target}")));
1265            return Ok(());
1266        }
1267        if value == "-target" {
1268            let target = self.take_value("-target", None)?;
1269            self.parsed
1270                .push(Argument::Plain(format!("--target={target}")));
1271            return Ok(());
1272        }
1273        if value == "-arch" {
1274            let arch = self.take_value("-arch", None)?;
1275            self.parsed.push(Argument::Plain(format!("-arch={arch}")));
1276            return Ok(());
1277        }
1278        if let Some(option) = value.strip_prefix("-f") {
1279            return self.parse_f_flag(value, option);
1280        }
1281        if let Some(option) = value.strip_prefix("-m") {
1282            return self.parse_m_flag(value, option);
1283        }
1284        if value.starts_with("-g") {
1285            // `-gsplit-dwarf` writes a `.dwo` beside the object; every other
1286            // unlisted `-g` spelling is simply unmodeled.
1287            return Err(if value.starts_with("-gsplit-dwarf") {
1288                CcBypassReason::SplitDebugOutput(value.into())
1289            } else {
1290                CcBypassReason::UnknownFlag(value.into())
1291            });
1292        }
1293        if value.starts_with("-W") {
1294            // Warning selection changes only diagnostics, which are replayed
1295            // from the cache, and the exit status, and only successful
1296            // compiles are ever published.
1297            self.parsed.push(Argument::Plain(value.into()));
1298            return Ok(());
1299        }
1300        Err(CcBypassReason::UnknownFlag(value.into()))
1301    }
1302
1303    fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1304        if option.starts_with("plugin") || option.starts_with("pass-plugin") {
1305            return Err(CcBypassReason::Plugin(value.into()));
1306        }
1307        if option.starts_with("profile-") || option == "test-coverage" {
1308            return Err(CcBypassReason::CoverageInstrumentation(value.into()));
1309        }
1310        let name = option.split_once('=').map_or(option, |(name, _)| name);
1311        if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
1312            return Err(CcBypassReason::UnknownFlag(value.into()));
1313        }
1314        self.parsed.push(Argument::Plain(value.into()));
1315        Ok(())
1316    }
1317
1318    fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
1319        if option == "llvm" {
1320            return Err(CcBypassReason::ToolPassthrough(value.into()));
1321        }
1322        // `-march=native` and its relatives resolve against whatever CPU this
1323        // machine has. The resulting object is not a function of the key, so
1324        // another machine could otherwise restore code its processor cannot
1325        // run.
1326        if let Some((name, selection)) = option.split_once('=')
1327            && matches!(name, "arch" | "cpu" | "tune")
1328            && matches!(selection, "native" | "host")
1329        {
1330            return Err(CcBypassReason::LocalCpuTarget(value.into()));
1331        }
1332        let name = option.split_once('=').map_or(option, |(name, _)| name);
1333        if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
1334            return Err(CcBypassReason::UnknownFlag(value.into()));
1335        }
1336        self.parsed.push(Argument::Plain(value.into()));
1337        Ok(())
1338    }
1339}
1340
1341#[cfg(test)]
1342#[path = "cc_cache_tests.rs"]
1343mod tests;