Skip to main content

mbx_cache_rustc/
lib.rs

1//! Conservative parsing and action-key construction for `rustc` invocations.
2//!
3//! The adapter deliberately rejects any compiler option whose effect on the
4//! action key is unknown. Callers should treat [`BypassReason`] as a safe cache
5//! bypass, run the real compiler, and avoid publishing an action result.
6//!
7//! A typical integration parses an invocation, discovers precise inputs from
8//! rustc dep-info with [`RustcInvocation::discover_inputs`], adds them to an
9//! [`ActionContext`], and finally calls [`RustcInvocation::action`]. Path
10//! mappings make workspace-local absolute paths stable across machines.
11//!
12//! ```
13//! use mbx_cache_rustc::{PathMapping, normalize_mapped_path};
14//! use std::path::Path;
15//!
16//! let mappings = PathMapping::ordered(&[
17//!     PathMapping::new("/work/project", "workspace"),
18//! ]);
19//! assert_eq!(
20//!     normalize_mapped_path(
21//!         Path::new("src/lib.rs"),
22//!         Path::new("/work/project"),
23//!         &mappings,
24//!     )?,
25//!     "${workspace}/src/lib.rs",
26//! );
27//! # Ok::<(), mbx_cache_rustc::BypassReason>(())
28//! ```
29#![deny(missing_docs)]
30
31use mbx_cache_core::{
32    CacheDigest, FileDigestCache, PathAliases, PathMapping as SharedPathMapping,
33    PathNormalizationError, canonical_json, normalize_mapped_path as normalize_shared_path,
34    normalize_resolved_mapped_path_with as normalize_resolved_shared_path_with,
35    resolve_path_mappings,
36};
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38use std::collections::{BTreeMap, BTreeSet};
39use std::ffi::OsString;
40use std::path::{Component, Path, PathBuf};
41use thiserror::Error;
42
43mod dep_info;
44
45pub use dep_info::{DepInfoCommand, DiscoveredInputs, RustcDepInfo};
46
47/// Schema version embedded in canonical rustc action descriptors.
48pub const ACTION_SCHEMA_VERSION: u8 = 1;
49/// Version of the rustc argument and input model used to construct keys.
50///
51/// Bumped to 2 when the dep-info and diagnostics stored with a result stopped
52/// carrying the publishing checkout's absolute paths. Entries written before
53/// that hold the old spelling, and nothing in them says so, so they are retired
54/// by the key rather than restored into a checkout they do not describe.
55pub const ADAPTER_VERSION: u8 = 2;
56
57impl BypassReason {
58    /// A stable, low-cardinality name for this reason.
59    ///
60    /// Many variants carry a path or a flag, so `Display` text cannot be
61    /// aggregated; statistics group by this instead.
62    pub fn kind(&self) -> &'static str {
63        self.into()
64    }
65
66    /// A concrete change that can make this invocation cacheable, when one is
67    /// available.
68    ///
69    /// Expected probes and failures that require adapter support return
70    /// `None`; callers can still explain those from [`BypassReason::kind`].
71    pub fn remediation(&self) -> Option<&'static str> {
72        match self {
73            Self::Incremental => Some(
74                "Set `MBX_INCREMENTAL=0`; mbx will then disable Cargo incremental state and cache the compilation.",
75            ),
76            Self::UnportableNativeLink(detail) if detail.contains("linker") => Some(
77                "Make the native linker resolvable on `PATH`, or configure a linker mbx can identify for this target.",
78            ),
79            Self::UnportableNativeLink(_) => Some(
80                "Remove the reported `-C` option from the active Cargo profile or `RUSTFLAGS` to make these links cacheable.",
81            ),
82            Self::UnknownFlag(_) | Self::UnknownCodegenOption(_) => Some(
83                "Remove the reported compiler option, or upgrade mbx if the option should be modeled.",
84            ),
85            Self::UnmodeledLinkArgument(_) => Some(
86                "Remove the reported linker argument, or keep these links uncached if the argument is required.",
87            ),
88            Self::UnmappedAbsolutePath(_) => Some(
89                "Move the input under the workspace, target, Cargo, toolchain, or home roots so mbx can give it a portable cache name.",
90            ),
91            _ => None,
92        }
93    }
94}
95
96impl From<PathNormalizationError> for BypassReason {
97    fn from(reason: PathNormalizationError) -> Self {
98        match reason {
99            PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
100            PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
101        }
102    }
103}
104
105const SUPPORTED_CODEGEN_OPTIONS: &[&str] = &[
106    "codegen-units",
107    "control-flow-guard",
108    "debug-assertions",
109    "debuginfo",
110    "default-linker-libraries",
111    "embed-bitcode",
112    "extra-filename",
113    "force-frame-pointers",
114    "force-unwind-tables",
115    "instrument-coverage",
116    "link-arg",
117    "link-args",
118    "link-dead-code",
119    "link-self-contained",
120    "linker-plugin-lto",
121    "lto",
122    "metadata",
123    "no-prepopulate-passes",
124    "opt-level",
125    "overflow-checks",
126    "panic",
127    "prefer-dynamic",
128    "relocation-model",
129    "rpath",
130    "save-temps",
131    "soft-float",
132    "split-debuginfo",
133    "split-dwarf-kind",
134    "strip",
135    "symbol-mangling-version",
136    "target-cpu",
137    "target-feature",
138    "tls-model",
139];
140
141const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
142const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
143// A compact payload is capped on the wire by the protocol. Bound its expanded
144// form too, so a hostile prefix chain cannot turn a small manifest into an
145// unreasonable allocation while it is being decoded.
146const MAX_DECODED_PREDICTION_BYTES: usize = 16 * 1024 * 1024;
147const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
148
149/// Built-in WebAssembly targets whose default linkers and CRT inputs ship in
150/// the Rust toolchain. Custom target specs never enter this list.
151const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
152    "wasm32-unknown-unknown",
153    "wasm32-wasip1",
154    "wasm32-wasip1-threads",
155    "wasm32-wasip2",
156    "wasm32v1-none",
157    "wasm64-unknown-unknown",
158];
159
160#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
161#[strum(serialize_all = "kebab-case")]
162/// Reason an invocation cannot safely use the action cache.
163///
164/// A bypass is an expected conservative outcome, not necessarily a compiler
165/// error. Variants carry diagnostic context while [`BypassReason::kind`]
166/// provides a stable aggregation key.
167///
168/// This set exists to shrink: every invocation the adapter learns to model
169/// retires a variant, and every construct it learns to reject adds one. Match
170/// on [`BypassReason::kind`] for aggregation rather than on the variants.
171#[non_exhaustive]
172pub enum BypassReason {
173    /// An argument cannot be represented in the canonical UTF-8 key.
174    #[error("rustc argument {index} is not valid UTF-8")]
175    NonUtf8Argument {
176        /// Zero-based index in the argument slice.
177        index: usize,
178    },
179    /// A rustc response file could not be read or parsed exactly.
180    #[error("could not model rustc response file: {0}")]
181    ResponseFile(String),
182    /// A compiler flag is not modeled by this adapter version.
183    #[error("rustc flag is not modeled by the cache adapter: {0}")]
184    UnknownFlag(String),
185    /// A `-C` option is not modeled by this adapter version.
186    #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
187    UnknownCodegenOption(String),
188    /// A recognized flag was not followed by its required value.
189    #[error("rustc flag requires a value: {0}")]
190    MissingValue(String),
191    /// The invocation queries compiler information instead of compiling.
192    #[error("rustc invocation is a compiler query, not a compilation")]
193    CompilerQuery,
194    /// Source would be read from standard input and cannot be rediscovered.
195    #[error("rustc invocation reads source from standard input")]
196    StandardInput,
197    /// No Rust source input was supplied.
198    #[error("rustc invocation has no source input")]
199    MissingInput,
200    /// More than one source input was supplied.
201    #[error("rustc invocation has multiple source inputs")]
202    MultipleInputs,
203    /// Incremental state makes the outputs unsuitable for action caching.
204    #[error("incremental compilation cannot be combined with action caching")]
205    Incremental,
206    /// The requested crate type is outside the supported cacheability tier.
207    #[error("rustc crate type is not cacheable yet: {0}")]
208    UnsupportedCrateType(String),
209    /// The requested emit kind is outside the supported cacheability tier.
210    #[error("rustc output type is not cacheable yet: {0}")]
211    UnsupportedEmit(String),
212    /// The invocation emits no artifact in the supported cacheability tier.
213    #[error("rustc invocation does not emit a cacheable artifact")]
214    NoCacheableOutput,
215    /// The invocation does not emit the dep-info needed for input discovery.
216    #[error("rustc invocation does not emit dependency information")]
217    NoDepInfo,
218    /// Outputs cannot be represented as one cache directory.
219    #[error("rustc output paths do not share one directory")]
220    SplitOutputDirectories,
221    /// An output path does not name a file.
222    #[error("rustc output path has no file name: {0}")]
223    InvalidOutputPath(PathBuf),
224    /// `-o` leaves the name of an implicit emit ambiguous.
225    #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
226    ImplicitEmitWithOutputFile(PathBuf),
227    /// A linked output hands its native libraries to a linker whose search
228    /// is not modeled; only a library emit can name what rustc reads.
229    #[error("native library on a linked output is not cacheable yet: {0}")]
230    NativeLibrary(String),
231    /// A static library named by `-l` is not in any directory rustc would
232    /// read it from, so there is no archive to hash.
233    #[error("native static library was not found on any search path: {0}")]
234    MissingNativeLibrary(String),
235    /// A custom target specification chooses its own static-library file
236    /// names, so the archive rustc would read cannot be named without it.
237    #[error("static library file names are not known for a custom target: {0}")]
238    CustomTargetNativeLibrary(String),
239    /// An output's name does not say whether it is a program or a library.
240    #[error("rustc output name does not distinguish a program from a library: {0}")]
241    AmbiguousOutputName(PathBuf),
242    /// A native link would embed something no other checkout can reproduce.
243    #[error("native link is not reproducible across checkouts: {0}")]
244    UnportableNativeLink(String),
245    /// A linked output was given an argument to pass on to its linker.
246    #[error("rustc link argument is not modeled by the cache adapter: {0}")]
247    UnmodeledLinkArgument(String),
248    /// A library search-path kind is not modeled.
249    #[error("rustc search path kind is not cacheable yet: {0}")]
250    UnsupportedSearchPath(String),
251    /// An extern name does not resolve to a concrete input artifact.
252    #[error("rustc extern does not identify an input artifact: {0}")]
253    UnresolvedExtern(String),
254    /// An absolute path has no stable placeholder mapping.
255    #[error("absolute path has no stable cache mapping: {0}")]
256    UnmappedAbsolutePath(PathBuf),
257    /// A path cannot be represented in a canonical UTF-8 action key.
258    #[error("cache key paths must be valid UTF-8: {0}")]
259    NonUtf8Path(PathBuf),
260    /// The action working directory is not absolute.
261    #[error("cache action working directory must be absolute: {0}")]
262    RelativeWorkingDirectory(PathBuf),
263    /// A path-mapping root is not absolute.
264    #[error("cache path mapping must use an absolute root: {0}")]
265    RelativePathMapping(PathBuf),
266    /// A mapping placeholder is empty or contains unsafe characters.
267    #[error("cache path mapping placeholder is invalid: {0}")]
268    InvalidPathPlaceholder(String),
269    /// An input referenced by compiler arguments is missing from the context.
270    #[error("required compiler input was not provided: {0}")]
271    MissingRequiredInput(String),
272    /// A supplied compiler-input digest is malformed.
273    #[error("compiler input has an invalid digest: {0}")]
274    InvalidInputDigest(String),
275    /// One normalized input path has multiple distinct digests.
276    #[error("compiler input appears more than once with different content: {0}")]
277    ConflictingInput(String),
278    /// rustc dep-info does not follow the supported format.
279    #[error("rustc dep-info is malformed: {0}")]
280    MalformedDepInfo(String),
281    /// A dep-info file could not be read as UTF-8 text.
282    #[error("failed to read rustc dep-info {path}: {message}")]
283    DepInfoRead {
284        /// Dep-info file path.
285        path: PathBuf,
286        /// Underlying I/O or decoding error.
287        message: String,
288    },
289    /// The requested dep-info output path is not absolute.
290    #[error("rustc dep-info output path must be absolute: {0}")]
291    RelativeDepInfoPath(PathBuf),
292    /// A dep-info output path contains a comma and cannot be safely rendered.
293    #[error("rustc dep-info output path cannot contain a comma: {0}")]
294    UnsafeDepInfoPath(PathBuf),
295    /// A discovered compiler input could not be read or was not a file.
296    #[error("failed to read compiler input {path}: {message}")]
297    InputRead {
298        /// Compiler-input path.
299        path: PathBuf,
300        /// Underlying filesystem error.
301        message: String,
302    },
303    /// Input contents changed after they were hashed.
304    #[error("compiler input changed after discovery: {0}")]
305    InputChanged(PathBuf),
306    /// An input's modification time overlaps the compiler execution.
307    #[error("compiler input was modified during compilation: {0}")]
308    InputModifiedDuringCompilation(PathBuf),
309    /// Discovered inputs and the action use different working directories.
310    #[error("discovered inputs were collected from a different working directory")]
311    DiscoveryWorkingDirectory,
312    /// One observed environment input has conflicting values.
313    #[error("compiler environment input has conflicting values: {0}")]
314    ConflictingEnvironment(String),
315    /// Canonical action serialization failed.
316    #[error("failed to serialize the rustc action: {0}")]
317    Serialization(String),
318    /// The stored prediction uses an unsupported version or exceeds limits.
319    #[error("rustc action prediction is unsupported")]
320    UnsupportedPrediction,
321    /// A normalized predicted path cannot be mapped back to the host.
322    #[error("rustc action prediction contains an invalid input path: {0}")]
323    InvalidPredictedInput(String),
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
327enum Argument {
328    Plain(String),
329    Path {
330        flag: String,
331        path: PathBuf,
332    },
333    SearchPath {
334        kind: String,
335        path: PathBuf,
336    },
337    Extern {
338        name: String,
339        path: Option<PathBuf>,
340    },
341    Emit(Vec<Emit>),
342    RemapPath {
343        from: PathBuf,
344        to: String,
345    },
346    /// A single @rpath filename used as a macOS proc macro install name.
347    InstallName(String),
348    /// An `-oso_prefix` handed to ld64, whose path strips checkout-specific
349    /// prefixes from the debug map the linker records.
350    OsoPrefix {
351        path: PathBuf,
352        trailing_slash: bool,
353    },
354    /// A `-fuse-ld=<name>` handed to the driver, selecting which linker a
355    /// link invokes. Modeled for native links only, where the linker identity
356    /// probe resolves and pins the linker it names.
357    FuseLd(String),
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361struct Emit {
362    kind: String,
363    path: Option<PathBuf>,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
367/// Parsed, cache-safe model of one `rustc` command line.
368///
369/// Fields are intentionally private so new modeled flags can be added without
370/// exposing the adapter's internal representation.
371pub struct RustcInvocation {
372    arguments: Vec<Argument>,
373    source: PathBuf,
374    required_inputs: Vec<PathBuf>,
375    crate_name: String,
376    extra_filename: String,
377    out_dir: Option<PathBuf>,
378    explicit_output: Option<PathBuf>,
379    emits: Vec<Emit>,
380    target: Option<String>,
381    link_output: LinkOutput,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385enum LinkOutput {
386    Library,
387    WasmExecutable,
388    NativeExecutable,
389    NativeProcMacro,
390}
391
392/// What the caller is prepared to model beyond the default tier.
393///
394/// The parser itself stays pure: whether the host can describe its linker
395/// precisely enough to key a native link is the caller's question, and the
396/// answer arrives here rather than being read out of the environment.
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
398#[non_exhaustive]
399pub struct ParseOptions {
400    /// Admit natively linked test binaries, executables, and proc macros,
401    /// given a linker identity in the action key. Off by default.
402    pub cache_native_links: bool,
403    /// rustc may resolve this invocation's bare `--target NAME` to a custom
404    /// specification: a `NAME.json` under `RUST_TARGET_PATH` or a
405    /// `lib/rustlib/NAME/target.json` in the sysroot exists. Such a target
406    /// chooses its own static-library file names, which the parser cannot
407    /// know, so `-l static` bypasses.
408    pub custom_target_search: bool,
409}
410
411impl ParseOptions {
412    /// Options that admit native links when `enabled`.
413    pub fn caching_native_links(enabled: bool) -> Self {
414        Self {
415            cache_native_links: enabled,
416            ..Self::default()
417        }
418    }
419
420    /// Options that also say whether rustc could resolve a bare target name
421    /// to a custom specification.
422    pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
423        self.custom_target_search = enabled;
424        self
425    }
426}
427
428/// The cacheable files and dependency manifest produced by a rustc invocation.
429#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct RustcOutputs {
431    /// Common directory containing all modeled outputs.
432    pub directory: PathBuf,
433    /// Cacheable library, metadata, and/or linked program files.
434    pub files: Vec<PathBuf>,
435    /// Dep-info file used for precise input discovery.
436    pub dep_info: PathBuf,
437}
438
439impl RustcInvocation {
440    /// Parse rustc's arguments, excluding the compiler executable supplied as
441    /// the first argument to `RUSTC_WRAPPER`.
442    ///
443    /// Any flag whose cache semantics are not modeled returns a bypass reason
444    /// instead of guessing. A successful parse admits the rlib/rmeta tier,
445    /// every compilation that links nothing at all -- what `cargo check` and
446    /// clippy run, whatever the crate type -- and binaries linked by
447    /// compiler-bundled WebAssembly toolchains.
448    pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
449        Self::parse_with(arguments, ParseOptions::default())
450    }
451
452    /// The command line with `@argfile` response files expanded, as the
453    /// parser sees it. A caller that scans the arguments before parsing, for a
454    /// `--target` a response file may carry, reads this rather than the raw
455    /// list.
456    pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
457        Ok(expand_response_files(arguments)?.arguments)
458    }
459
460    /// Parse as [`RustcInvocation::parse`] does, admitting what `options`
461    /// says the caller can model.
462    pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
463        let expanded = expand_response_files(arguments)?;
464        Parser::new(&expanded.arguments, options).parse()
465    }
466
467    /// Whether this invocation links a native artifact, whose key must
468    /// therefore describe the linker that produced it.
469    pub fn links_natively(&self) -> bool {
470        matches!(
471            self.link_output,
472            LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
473        )
474    }
475
476    /// Stable Mach-O identity for a cached macOS proc macro. The compiler loads
477    /// these by filename; embedding the checkout's output path only makes the
478    /// bytes (and dependent action keys) vary between otherwise identical builds.
479    pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
480        if !cfg!(target_os = "macos")
481            || self.link_output != LinkOutput::NativeProcMacro
482            || self
483                .arguments
484                .iter()
485                .any(|arg| matches!(arg, Argument::InstallName(_)))
486        {
487            return None;
488        }
489        let file = outputs
490            .files
491            .iter()
492            .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
493        let name = file.file_name()?.to_str()?;
494        if !safe_install_name(name) {
495            return None;
496        }
497        Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
498    }
499
500    /// Linker selected with `-C linker`, if the invocation overrides rustc's
501    /// platform default.
502    pub fn linker_override(&self) -> Option<&Path> {
503        self.arguments.iter().find_map(|argument| {
504            let Argument::Plain(value) = argument else {
505                return None;
506            };
507            value.strip_prefix("--codegen=linker=").map(Path::new)
508        })
509    }
510
511    /// Linker selected with `-C link-arg=-fuse-ld=<name>`, if the invocation
512    /// passes one the adapter models.
513    pub fn fuse_ld(&self) -> Option<&str> {
514        self.arguments.iter().find_map(|argument| {
515            let Argument::FuseLd(selection) = argument else {
516                return None;
517            };
518            Some(selection.as_str())
519        })
520    }
521
522    fn emits_windows_pdb(&self) -> bool {
523        if !cfg!(windows) || !self.links_natively() {
524            return false;
525        }
526        let mut enabled = false;
527        for argument in &self.arguments {
528            let Argument::Plain(value) = argument else {
529                continue;
530            };
531            if value == "-g" {
532                enabled = true;
533            } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
534                enabled = !matches!(value, "0" | "none");
535            }
536        }
537        enabled
538    }
539
540    /// Whether the contents of native search directories cannot reach this
541    /// invocation's outputs.
542    ///
543    /// A library emit never runs a linker, so the only file rustc reads out of
544    /// a `-L native` directory is a static archive it bundles into the rlib.
545    /// One named by `-l static` is resolved at parse time and hashed as a
546    /// required input, so the directory it came from need not be walked for
547    /// it. On Windows every crate downstream of a `cc`-built dependency
548    /// carries the MSVC toolset's `-L native` directories, which sit outside
549    /// every checkout root; treating them as content inputs would leave all of
550    /// those compilations permanently uncacheable. A `#[link]` attribute
551    /// naming a bundled static library could in principle reach through such a
552    /// directory without an `-l` flag, but toolchain directories are
553    /// version-stamped by their path, which the key still carries verbatim.
554    fn native_search_is_inert(&self) -> bool {
555        self.link_output == LinkOutput::Library
556    }
557
558    /// Return the source input passed to rustc.
559    pub fn source(&self) -> &Path {
560        &self.source
561    }
562
563    /// Absolute files named directly by the invocation that rustc must read.
564    ///
565    /// This includes the source, explicit extern artifacts, and custom target
566    /// specifications. Callers can snapshot these before rustc starts even
567    /// though the complete input set is only known from dep-info afterwards.
568    pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
569        self.required_inputs
570            .iter()
571            .map(|path| absolute_path(path, working_dir))
572            .collect()
573    }
574
575    /// Return the explicitly selected compilation target, if any.
576    pub fn target(&self) -> Option<&str> {
577        self.target.as_deref()
578    }
579
580    /// Return the crate name rustc will assign to this compilation.
581    pub fn crate_name(&self) -> &str {
582        &self.crate_name
583    }
584
585    /// Digest of the inputs this compilation owns: its sources and whatever
586    /// they read, but not the artifacts it merely links against.
587    ///
588    /// This is what separates a crate someone is editing from one sitting
589    /// above it in the graph. Rebuilding a dependency changes the action key of
590    /// every crate that links it, because their keys hash its artifact; it does
591    /// not change this. A caller watching for churn has to watch this instead,
592    /// or a single edited crate would drag its whole dependent cone along.
593    ///
594    /// Host paths enter the digest as they are. This describes one checkout to
595    /// itself rather than to the cache, so there is nothing here to make
596    /// portable.
597    pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
598        let linked = self
599            .arguments
600            .iter()
601            .filter_map(|argument| match argument {
602                Argument::Extern {
603                    path: Some(path), ..
604                } => Some(path.as_path()),
605                _ => None,
606            })
607            .collect::<BTreeSet<_>>();
608        let owned = discovered
609            .inputs
610            .iter()
611            .filter(|input| !linked.contains(input.path.as_path()))
612            .map(|input| (input.path.as_path(), &input.digest))
613            .collect::<BTreeMap<_, _>>();
614        let mut bytes = Vec::new();
615        for (path, digest) in owned {
616            bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
617            bytes.push(0);
618            bytes.extend_from_slice(digest.key().as_bytes());
619            bytes.push(0);
620        }
621        CacheDigest::blake3(&bytes)
622    }
623
624    /// Resolve the files produced by this invocation.
625    ///
626    /// The initial cache tier requires one output directory so its artifact can
627    /// be represented by one protocol directory and restored atomically later.
628    pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
629        if !working_dir.is_absolute() {
630            return Err(BypassReason::RelativeWorkingDirectory(
631                working_dir.to_path_buf(),
632            ));
633        }
634        let explicit_output = self
635            .explicit_output
636            .as_deref()
637            .map(|path| absolute_path(path, working_dir));
638        let output_directory = explicit_output
639            .as_deref()
640            .and_then(Path::parent)
641            .map(Path::to_path_buf)
642            .or_else(|| {
643                self.out_dir
644                    .as_deref()
645                    .map(|path| absolute_path(path, working_dir))
646            })
647            .unwrap_or_else(|| normalize_components(working_dir));
648        // rustc applies `-o` to every emit that has no path of its own, so the
649        // file names cannot be derived from the crate name here. Cargo always
650        // uses --out-dir instead, so refusing to model this costs nothing.
651        if let Some(output) = &explicit_output
652            && self.emits.iter().any(|emit| {
653                emit.path.is_none()
654                    && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
655            })
656        {
657            return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
658        }
659        let mut files = BTreeSet::new();
660        let mut dep_info = None;
661        for emit in &self.emits {
662            if emit.kind == "dep-info" {
663                let path = emit.path.as_ref().map_or_else(
664                    || {
665                        explicit_output.clone().map_or_else(
666                            || {
667                                output_directory
668                                    .join(format!("{}{}.d", self.crate_name, self.extra_filename))
669                            },
670                            |path| path.with_extension("d"),
671                        )
672                    },
673                    |path| absolute_path(path, working_dir),
674                );
675                if path.file_name().is_none() {
676                    return Err(BypassReason::InvalidOutputPath(path));
677                }
678                dep_info = Some(path);
679                continue;
680            }
681            let (prefix, extension) = match emit.kind.as_str() {
682                "link" => match self.link_output {
683                    LinkOutput::Library => ("lib", "rlib"),
684                    LinkOutput::WasmExecutable => ("", "wasm"),
685                    LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
686                    LinkOutput::NativeProcMacro => (
687                        std::env::consts::DLL_PREFIX,
688                        std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
689                    ),
690                },
691                "metadata" => ("lib", "rmeta"),
692                _ => continue,
693            };
694            let path = if let Some(path) = &emit.path {
695                absolute_path(path, working_dir)
696            } else {
697                let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
698                output_directory.join(if extension.is_empty() {
699                    name
700                } else {
701                    format!("{name}.{extension}")
702                })
703            };
704            if path.file_name().is_none() {
705                return Err(BypassReason::InvalidOutputPath(path));
706            }
707            if path.parent() != Some(output_directory.as_path()) {
708                return Err(BypassReason::SplitOutputDirectories);
709            }
710            // Whether a restored output is a program is read back off its name,
711            // so a program that answers to a library's name would be restored
712            // without the permission that makes it runnable. Nothing cargo
713            // emits looks like this; a hand-built invocation could.
714            let has_library_extension = path
715                .extension()
716                .and_then(|extension| extension.to_str())
717                .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
718                || (cfg!(windows)
719                    && path
720                        .file_stem()
721                        .and_then(|stem| Path::new(stem).extension())
722                        .and_then(|extension| extension.to_str())
723                        .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
724            if emit.kind == "link"
725                && !matches!(self.link_output, LinkOutput::Library)
726                && has_library_extension
727            {
728                return Err(BypassReason::AmbiguousOutputName(path));
729            }
730            if emit.kind == "link" && self.emits_windows_pdb() {
731                files.insert(path.with_extension("pdb"));
732            }
733            files.insert(path);
734        }
735        let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
736        if dep_info.parent() != Some(output_directory.as_path()) {
737            return Err(BypassReason::SplitOutputDirectories);
738        }
739        Ok(RustcOutputs {
740            directory: output_directory,
741            files: files.into_iter().collect(),
742            dep_info,
743        })
744    }
745
746    /// Build canonical action bytes after precise input discovery has run.
747    ///
748    /// `context.inputs` must contain the source, every explicit extern, and
749    /// every additional source or environment-generated input discovered from
750    /// dep-info.
751    pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
752        self.action_linked_by(context, None)
753    }
754
755    /// Build canonical action bytes for an invocation that links a native
756    /// program.
757    ///
758    /// A `linker` is required whenever [`RustcInvocation::links_natively`]
759    /// holds: the linker, its startup objects, and the platform SDK are inputs
760    /// rustc dep-info does not enumerate, so a key without them would claim
761    /// more than it can support. Passing one for anything else is ignored,
762    /// since nothing else depends on a linker.
763    pub fn action_linked_by(
764        &self,
765        context: ActionContext,
766        linker: Option<LinkerIdentity>,
767    ) -> Result<RustcAction, BypassReason> {
768        ActionBuilder::new(self, context).linked_by(linker).build()
769    }
770
771    /// Fingerprint the modeled invocation before dependency contents are known.
772    pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
773        let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
774        let bytes = canonical_json(&descriptor)
775            .map_err(|error| BypassReason::Serialization(error.to_string()))?;
776        Ok(CacheDigest::blake3(&bytes))
777    }
778
779    /// Capture normalized dependency paths for a future invocation that has no
780    /// dep-info file yet.
781    pub fn prediction(
782        &self,
783        context: &ActionContext,
784        discovered: &DiscoveredInputs,
785    ) -> Result<RustcInputPrediction, BypassReason> {
786        let builder = ActionBuilder::new(self, context.clone());
787        builder.validate_mappings()?;
788        let mut native_directories = BTreeSet::new();
789        for argument in &self.arguments {
790            if let Argument::SearchPath { kind, path } = argument
791                && kind == "native"
792            {
793                match builder.normalize_path(path) {
794                    Ok(normalized) => {
795                        native_directories.insert(normalized);
796                    }
797                    // Inert and outside every mapped root: the directory
798                    // contributes no content inputs, so the prediction has
799                    // nothing to replay for it. Input discovery skips it the
800                    // same way, which is what keeps the predicted action key
801                    // equal to the one dep-info discovery builds.
802                    Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
803                    }
804                    Err(error) => return Err(error),
805                }
806            }
807        }
808        // A file beneath a recorded directory is rediscovered by walking that
809        // directory, so naming it as well only makes the payload grow with the
810        // object tree a C dependency leaves in its `OUT_DIR`. `aws-lc-sys`
811        // leaves thousands of files there, which was enough to push the
812        // serialized prediction past the protocol's payload limit and lose the
813        // prediction entirely for the crate that most needed one.
814        let mut inputs = BTreeSet::new();
815        for input in &discovered.inputs {
816            let normalized = builder.normalize_path(&input.path)?;
817            if !under_any_directory(&normalized, &native_directories) {
818                inputs.insert(normalized);
819            }
820        }
821        inputs.extend(
822            native_directories
823                .into_iter()
824                .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
825        );
826        Ok(RustcInputPrediction {
827            version: 4,
828            inputs: inputs.into_iter().collect(),
829            environment: discovered.environment.keys().cloned().collect(),
830            compiler_duration_ns: 0,
831            crate_name: String::new(),
832        })
833    }
834}
835
836impl RustcOutputs {
837    /// The linked executable Cargo will run as a build script, when this is a
838    /// build-script compilation.
839    pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
840        (crate_name == "build_script_build")
841            .then(|| self.files.iter().find(|path| self.is_executable(path)))
842            .flatten()
843            .map(PathBuf::as_path)
844    }
845
846    /// Whether `path` is a linked program whose executable permission is part
847    /// of the declared output contract.
848    /// A program is whatever this invocation emitted that is not a library
849    /// artifact. Every tier the adapter admits distinguishes the two by
850    /// extension -- `rlib` and `rmeta` are the compiler's own, and a linked
851    /// program carries either the target's (`wasm`) or none at all -- so the
852    /// name is enough and no separate list has to be carried alongside.
853    pub fn is_executable(&self, path: &Path) -> bool {
854        self.files.iter().any(|output| output == path)
855            && !matches!(
856                path.extension().and_then(|extension| extension.to_str()),
857                Some("rlib" | "rmeta")
858            )
859    }
860}
861
862#[derive(Debug, Clone, PartialEq, Eq)]
863/// Mapping from a host-specific absolute root to a stable key placeholder.
864pub struct PathMapping {
865    /// Absolute host path to replace.
866    pub root: PathBuf,
867    /// Placeholder name without the surrounding `${...}` syntax.
868    pub placeholder: String,
869}
870
871impl PathMapping {
872    /// Map an absolute host path to a stable cache-key placeholder.
873    pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
874        Self {
875            root: root.into(),
876            placeholder: placeholder.into(),
877        }
878    }
879
880    /// Order mappings deepest root first, which is what normalization needs:
881    /// a target directory inside the workspace has to win over the workspace.
882    pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
883        let mut ordered = mappings.to_vec();
884        ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
885        ordered
886    }
887}
888
889fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
890    mappings
891        .iter()
892        .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
893        .collect()
894}
895
896/// Map an absolute path to its cache-key placeholder form.
897///
898/// `mappings` must already be ordered by [`PathMapping::ordered`]. Exposed for
899/// callers that need the placeholder text before an action exists -- notably to
900/// build the `--remap-path-prefix` flag that makes a compilation independent of
901/// a path in its environment.
902pub fn normalize_mapped_path(
903    path: &Path,
904    working_dir: &Path,
905    mappings: &[PathMapping],
906) -> Result<String, BypassReason> {
907    normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
908}
909
910#[derive(Debug, Clone, PartialEq, Eq)]
911/// Compiler properties that distinguish incompatible action outputs.
912pub struct CompilerIdentity {
913    /// Toolchain selector or installation identity.
914    pub toolchain: String,
915    /// Complete verbose rustc version string.
916    pub rustc_version: String,
917    /// Compiler host target triple.
918    pub host: String,
919    /// Identity of a compiler driver layered over rustc, when one is in use.
920    pub driver: Option<String>,
921}
922
923/// One file input paired with the digest used in the action key.
924#[derive(Debug, Clone, PartialEq, Eq)]
925pub struct ActionInput {
926    /// Absolute host path used to read and verify the input.
927    pub path: PathBuf,
928    /// Digest of the input contents.
929    pub digest: CacheDigest,
930}
931
932/// External information needed to construct a canonical rustc action.
933#[derive(Debug, Clone, PartialEq, Eq)]
934pub struct ActionContext {
935    /// Identity of the compiler that produces the outputs.
936    pub compiler: CompilerIdentity,
937    /// Absolute directory in which rustc runs.
938    pub working_dir: PathBuf,
939    /// Host roots replaced with stable placeholders in the key.
940    pub path_mappings: Vec<PathMapping>,
941    /// Environment inputs and their observed values.
942    pub environment: BTreeMap<String, Option<String>>,
943    /// Environment inputs whose absolute values the compilation has been made
944    /// independent of, and whose values the key therefore normalizes.
945    ///
946    /// Naming one here is a claim about the compilation, not a preference: the
947    /// caller must both neutralize the value inside it (with
948    /// `--remap-path-prefix`) and confirm no output carries the value anyway.
949    pub portable_environment: BTreeSet<String>,
950    /// Complete set of direct and discovered file inputs.
951    pub inputs: Vec<ActionInput>,
952}
953
954/// What produced a linked native program, beyond the compiler itself.
955///
956/// The fields are identity rather than content wherever a compiler's own
957/// identity is: a driver's version output names its toolchain more cheaply than
958/// hashing a hundred megabytes of it, and matches how rustc is identified. The
959/// CRT objects are hashed, because nothing else pins the libc a link resolves
960/// against. Nothing here is placeholder-mapped -- these paths are host
961/// locations rather than checkout locations, and two hosts that differ should
962/// miss rather than share.
963#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
964#[serde(deny_unknown_fields)]
965pub struct LinkerIdentity {
966    /// Resolved absolute path of the linker driver rustc will invoke.
967    pub driver: String,
968    /// Version output of that driver.
969    pub driver_version: String,
970    /// Version of the linker the driver selects.
971    pub linker_version: String,
972    /// Startup objects and libc the driver resolves, by probe name.
973    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
974    pub crt_objects: BTreeMap<String, CacheDigest>,
975    /// Platform SDK identity, where the platform has one.
976    #[serde(skip_serializing_if = "Option::is_none", default)]
977    pub sdk: Option<String>,
978    /// Deployment target the link was made against, where one applies.
979    #[serde(skip_serializing_if = "Option::is_none", default)]
980    pub deployment_target: Option<String>,
981}
982
983/// Canonical action descriptor and its content digest.
984#[derive(Debug, Clone, PartialEq, Eq)]
985pub struct RustcAction {
986    /// Digest of `bytes`, used as the action-cache key.
987    pub digest: CacheDigest,
988    /// Canonical serialized action descriptor.
989    pub bytes: Vec<u8>,
990}
991
992/// Normalized input names from the last successful execution of one modeled
993/// rustc invocation.
994#[derive(Debug, Clone, PartialEq, Eq)]
995pub struct RustcInputPrediction {
996    /// Prediction schema version.
997    pub version: u8,
998    /// Normalized input paths observed during the successful invocation.
999    pub inputs: Vec<String>,
1000    /// Names of environment variables read by the compilation.
1001    pub environment: Vec<String>,
1002    /// Compiler wall time from the successful invocation that produced this
1003    /// prediction. Zero means no timing hint was recorded.
1004    pub compiler_duration_ns: u64,
1005    /// Crate name associated with the timing hint.
1006    pub crate_name: String,
1007}
1008
1009#[derive(Serialize, Deserialize)]
1010#[serde(deny_unknown_fields)]
1011struct RustcInputPredictionWire {
1012    version: u8,
1013    inputs: Vec<String>,
1014    environment: Vec<String>,
1015    #[serde(default, skip_serializing_if = "is_zero")]
1016    compiler_duration_ns: u64,
1017    #[serde(default, skip_serializing_if = "String::is_empty")]
1018    crate_name: String,
1019}
1020
1021impl Serialize for RustcInputPrediction {
1022    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1023    where
1024        S: Serializer,
1025    {
1026        RustcInputPredictionWire {
1027            version: self.version,
1028            inputs: if self.version == 4 {
1029                compact_prediction_inputs(&self.inputs)
1030            } else {
1031                self.inputs.clone()
1032            },
1033            environment: self.environment.clone(),
1034            compiler_duration_ns: self.compiler_duration_ns,
1035            crate_name: self.crate_name.clone(),
1036        }
1037        .serialize(serializer)
1038    }
1039}
1040
1041impl<'de> Deserialize<'de> for RustcInputPrediction {
1042    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1043    where
1044        D: Deserializer<'de>,
1045    {
1046        let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1047        let inputs = if wire.version == 4 {
1048            expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1049        } else {
1050            wire.inputs
1051        };
1052        Ok(Self {
1053            version: wire.version,
1054            inputs,
1055            environment: wire.environment,
1056            compiler_duration_ns: wire.compiler_duration_ns,
1057            crate_name: wire.crate_name,
1058        })
1059    }
1060}
1061
1062fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1063    let mut previous = "";
1064    inputs
1065        .iter()
1066        .map(|input| {
1067            let mut shared = previous
1068                .bytes()
1069                .zip(input.bytes())
1070                .take_while(|(left, right)| left == right)
1071                .count();
1072            while !input.is_char_boundary(shared) {
1073                shared -= 1;
1074            }
1075            let compact = format!("{shared}:{}", &input[shared..]);
1076            previous = input;
1077            compact
1078        })
1079        .collect()
1080}
1081
1082fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1083    let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1084    let mut decoded_bytes = 0_usize;
1085    for input in inputs {
1086        let (shared_text, suffix) = input
1087            .split_once(':')
1088            .ok_or("compact rustc prediction input has no prefix length")?;
1089        let shared: usize = shared_text
1090            .parse()
1091            .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1092        if shared.to_string() != shared_text
1093            || shared > expanded.last().map_or(0, String::len)
1094            || expanded
1095                .last()
1096                .is_some_and(|previous| !previous.is_char_boundary(shared))
1097        {
1098            return Err("compact rustc prediction prefix is not canonical");
1099        }
1100        let mut path = expanded
1101            .last()
1102            .map_or_else(String::new, |previous| previous[..shared].to_string());
1103        path.push_str(suffix);
1104        decoded_bytes = decoded_bytes
1105            .checked_add(path.len())
1106            .ok_or("compact rustc prediction is too large")?;
1107        if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1108            return Err("compact rustc prediction is too large");
1109        }
1110        expanded.push(path);
1111    }
1112    Ok(expanded)
1113}
1114
1115fn is_zero(value: &u64) -> bool {
1116    *value == 0
1117}
1118
1119impl RustcInputPrediction {
1120    /// Rehash the predicted paths and read the current environment. The caller
1121    /// still recomputes the full action digest, so changed inputs are misses.
1122    pub fn discover(
1123        &self,
1124        working_dir: &Path,
1125        path_mappings: &[PathMapping],
1126        digests: &dyn FileDigestCache,
1127    ) -> Result<DiscoveredInputs, BypassReason> {
1128        if !matches!(self.version, 2..=4) {
1129            return Err(BypassReason::UnsupportedPrediction);
1130        }
1131        if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1132            return Err(BypassReason::UnsupportedPrediction);
1133        }
1134        let mut paths = BTreeSet::new();
1135        let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1136        let mut native_bytes = 0_u64;
1137        for path in &self.inputs {
1138            if self.version >= 3
1139                && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1140            {
1141                let directory = denormalize_path(path, path_mappings)?;
1142                dep_info::collect_native_directory(
1143                    &directory,
1144                    &admitted_roots,
1145                    &mut paths,
1146                    &mut native_bytes,
1147                )?;
1148            } else {
1149                paths.insert(denormalize_path(path, path_mappings)?);
1150            }
1151        }
1152        let environment = self
1153            .environment
1154            .iter()
1155            .map(|name| {
1156                if name.is_empty() || name.contains(['=', '\0']) {
1157                    return Err(BypassReason::UnsupportedPrediction);
1158                }
1159                let value = std::env::var_os(name)
1160                    .map(|value| {
1161                        value
1162                            .into_string()
1163                            .map_err(|_| BypassReason::UnsupportedPrediction)
1164                    })
1165                    .transpose()?;
1166                Ok((name.clone(), value))
1167            })
1168            .collect::<Result<BTreeMap<_, _>, _>>()?;
1169        DiscoveredInputs::from_paths(working_dir, paths, environment, digests)
1170    }
1171}
1172
1173#[derive(Serialize)]
1174struct ActionDescriptor {
1175    version: u8,
1176    kind: &'static str,
1177    adapter_version: u8,
1178    compiler: CompilerDescriptor,
1179    arguments: Vec<String>,
1180    environment: BTreeMap<String, Option<String>>,
1181    inputs: Vec<InputDescriptor>,
1182    /// Omitted entirely unless the invocation links natively, so every key
1183    /// written before this field existed still serializes to the same bytes.
1184    #[serde(skip_serializing_if = "Option::is_none")]
1185    linker: Option<LinkerIdentity>,
1186}
1187
1188#[derive(Serialize)]
1189struct InvocationDescriptor {
1190    version: u8,
1191    kind: &'static str,
1192    adapter_version: u8,
1193    compiler: CompilerDescriptor,
1194    arguments: Vec<String>,
1195    required_inputs: Vec<String>,
1196}
1197
1198#[derive(Serialize)]
1199struct CompilerDescriptor {
1200    toolchain: String,
1201    rustc_version: String,
1202    host: String,
1203    #[serde(skip_serializing_if = "Option::is_none")]
1204    driver: Option<String>,
1205}
1206
1207#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1208struct InputDescriptor {
1209    path: String,
1210    digest: CacheDigest,
1211}
1212
1213/// The static-library file names a built-in target uses.
1214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1215enum StaticLibraryNaming {
1216    /// `NAME.lib`, then the Unix spelling as a fallback.
1217    Msvc,
1218    /// `libNAME.a`.
1219    Unix,
1220}
1221
1222struct Parser<'a> {
1223    arguments: &'a [OsString],
1224    index: usize,
1225    parsed: Vec<Argument>,
1226    source: Option<PathBuf>,
1227    crate_types: Vec<String>,
1228    emits: Vec<Emit>,
1229    required_inputs: Vec<PathBuf>,
1230    native_libraries: Vec<String>,
1231    test: bool,
1232    crate_name: Option<String>,
1233    extra_filename: String,
1234    out_dir: Option<PathBuf>,
1235    explicit_output: Option<PathBuf>,
1236    target: Option<String>,
1237    options: ParseOptions,
1238}
1239
1240struct ExpandedArguments {
1241    arguments: Vec<OsString>,
1242}
1243
1244#[derive(Default)]
1245struct ResponseExpander {
1246    shell_argfiles: bool,
1247    next_is_unstable_option: bool,
1248    arguments: Vec<OsString>,
1249}
1250
1251impl ResponseExpander {
1252    fn push(&mut self, argument: String) {
1253        if self.next_is_unstable_option {
1254            self.shell_argfiles |= argument == "shell-argfiles";
1255            self.next_is_unstable_option = false;
1256        } else if let Some(option) = argument.strip_prefix("-Z") {
1257            if option.is_empty() {
1258                self.next_is_unstable_option = true;
1259            } else {
1260                self.shell_argfiles |= option == "shell-argfiles";
1261            }
1262        }
1263        self.arguments.push(argument.into());
1264    }
1265}
1266
1267/// Match rustc's argfile expansion: UTF-8, one option per line, no recursive
1268/// expansion, with the nightly shell form enabled only after its `-Z` flag.
1269fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1270    let mut expanded = ResponseExpander::default();
1271    for (index, argument) in arguments.iter().enumerate() {
1272        let argument = argument
1273            .to_str()
1274            .ok_or(BypassReason::NonUtf8Argument { index })?;
1275        let Some(argfile) = argument.strip_prefix('@') else {
1276            expanded.push(argument.to_string());
1277            continue;
1278        };
1279        let (path, shell) = match argfile.split_once(':') {
1280            Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1281            _ => (argfile, false),
1282        };
1283        let contents = std::fs::read_to_string(path).map_err(|error| {
1284            BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1285        })?;
1286        if shell {
1287            let arguments = shlex::split(&contents).ok_or_else(|| {
1288                BypassReason::ResponseFile(format!(
1289                    "invalid shell-style arguments in {}",
1290                    Path::new(path).display()
1291                ))
1292            })?;
1293            for argument in arguments {
1294                expanded.push(argument);
1295            }
1296        } else {
1297            for argument in contents.lines() {
1298                expanded.push(argument.to_string());
1299            }
1300        }
1301    }
1302    Ok(ExpandedArguments {
1303        arguments: expanded.arguments,
1304    })
1305}
1306
1307impl<'a> Parser<'a> {
1308    fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1309        Self {
1310            arguments,
1311            options,
1312            index: 0,
1313            parsed: Vec::new(),
1314            source: None,
1315            crate_types: Vec::new(),
1316            emits: Vec::new(),
1317            required_inputs: Vec::new(),
1318            native_libraries: Vec::new(),
1319            test: false,
1320            crate_name: None,
1321            extra_filename: String::new(),
1322            out_dir: None,
1323            explicit_output: None,
1324            target: None,
1325        }
1326    }
1327
1328    fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1329        while self.index < self.arguments.len() {
1330            let value = self.current()?.to_string();
1331            self.index += 1;
1332            if let Some(long) = value.strip_prefix("--") {
1333                self.parse_long(long)?;
1334            } else if value.starts_with('-') && value != "-" {
1335                self.parse_short(&value)?;
1336            } else {
1337                self.parse_input(&value)?;
1338            }
1339        }
1340
1341        let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1342        let link_output = self.classify()?;
1343        self.require_native_libraries(link_output)?;
1344        let crate_name = self.crate_name.clone().map_or_else(
1345            || {
1346                source
1347                    .file_stem()
1348                    .and_then(|name| name.to_str())
1349                    .map(|name| name.replace('-', "_"))
1350                    .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1351            },
1352            Ok,
1353        )?;
1354        self.required_inputs.push(source.clone());
1355        Ok(RustcInvocation {
1356            arguments: self.parsed,
1357            source,
1358            required_inputs: self.required_inputs,
1359            crate_name,
1360            extra_filename: self.extra_filename,
1361            out_dir: self.out_dir,
1362            explicit_output: self.explicit_output,
1363            emits: self.emits,
1364            target: self.target,
1365            link_output,
1366        })
1367    }
1368
1369    fn current(&self) -> Result<&str, BypassReason> {
1370        self.arguments[self.index]
1371            .to_str()
1372            .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1373    }
1374
1375    fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1376        if let Some(value) = inline {
1377            if value.is_empty() {
1378                return Err(BypassReason::MissingValue(flag.into()));
1379            }
1380            return Ok(value.into());
1381        }
1382        if self.index >= self.arguments.len() {
1383            return Err(BypassReason::MissingValue(flag.into()));
1384        }
1385        let value = self.current()?.to_string();
1386        self.index += 1;
1387        Ok(value)
1388    }
1389
1390    fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1391        let (flag, inline) = value
1392            .split_once('=')
1393            .map_or((value, None), |(flag, value)| (flag, Some(value)));
1394        let rendered_flag = format!("--{flag}");
1395        match flag {
1396            "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1397            "test" => {
1398                self.test = true;
1399                self.parsed.push(Argument::Plain(rendered_flag));
1400                Ok(())
1401            }
1402            "verbose" => {
1403                self.parsed.push(Argument::Plain(rendered_flag));
1404                Ok(())
1405            }
1406            "crate-name" => {
1407                let value = self.take_value(&rendered_flag, inline)?;
1408                self.crate_name = Some(value.clone());
1409                self.parsed
1410                    .push(Argument::Plain(format!("{rendered_flag}={value}")));
1411                Ok(())
1412            }
1413            "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1414            | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1415            | "deny" | "forbid" | "cap-lints" => {
1416                let value = self.take_value(&rendered_flag, inline)?;
1417                self.parsed
1418                    .push(Argument::Plain(format!("{rendered_flag}={value}")));
1419                Ok(())
1420            }
1421            "target" => {
1422                let value = self.take_value(&rendered_flag, inline)?;
1423                // rustc reads the first `--target`, as getopts does; a later
1424                // one is still keyed as an argument but does not decide the
1425                // target, so it is not read and not a required input either.
1426                let effective = self.target.is_none();
1427                if effective {
1428                    self.target = Some(value.clone());
1429                }
1430                if value.ends_with(".json") || value.contains(['/', '\\']) {
1431                    let path = PathBuf::from(value);
1432                    if effective {
1433                        self.required_inputs.push(path.clone());
1434                    }
1435                    self.parsed.push(Argument::Path {
1436                        flag: rendered_flag,
1437                        path,
1438                    });
1439                } else {
1440                    self.parsed
1441                        .push(Argument::Plain(format!("{rendered_flag}={value}")));
1442                }
1443                Ok(())
1444            }
1445            "crate-type" => {
1446                let value = self.take_value(&rendered_flag, inline)?;
1447                self.crate_types
1448                    .extend(value.split(',').map(ToOwned::to_owned));
1449                self.parsed
1450                    .push(Argument::Plain(format!("{rendered_flag}={value}")));
1451                Ok(())
1452            }
1453            "emit" => {
1454                let value = self.take_value(&rendered_flag, inline)?;
1455                let emits = parse_emits(&value);
1456                self.emits.extend(emits.clone());
1457                self.parsed.push(Argument::Emit(emits));
1458                Ok(())
1459            }
1460            "out-dir" => {
1461                let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1462                self.out_dir = Some(path.clone());
1463                self.parsed.push(Argument::Path {
1464                    flag: rendered_flag,
1465                    path,
1466                });
1467                Ok(())
1468            }
1469            "sysroot" => {
1470                let path = self.take_value(&rendered_flag, inline)?;
1471                self.parsed.push(Argument::Path {
1472                    flag: rendered_flag,
1473                    path: path.into(),
1474                });
1475                Ok(())
1476            }
1477            "extern" => {
1478                let value = self.take_value(&rendered_flag, inline)?;
1479                let (name, path) = value
1480                    .split_once('=')
1481                    .map_or((value.as_str(), None), |(name, path)| {
1482                        (name, Some(PathBuf::from(path)))
1483                    });
1484                if let Some(path) = &path {
1485                    self.required_inputs.push(path.clone());
1486                }
1487                self.parsed.push(Argument::Extern {
1488                    name: name.into(),
1489                    path,
1490                });
1491                Ok(())
1492            }
1493            "remap-path-prefix" => {
1494                let value = self.take_value(&rendered_flag, inline)?;
1495                let Some((from, to)) = value.split_once('=') else {
1496                    return Err(BypassReason::MissingValue(rendered_flag));
1497                };
1498                self.parsed.push(Argument::RemapPath {
1499                    from: from.into(),
1500                    to: to.into(),
1501                });
1502                Ok(())
1503            }
1504            "codegen" => {
1505                let value = self.take_value(&rendered_flag, inline)?;
1506                self.parse_codegen(&value)
1507            }
1508            "jobs-frontend" => {
1509                let value = self.take_value(&rendered_flag, inline)?;
1510                self.parsed
1511                    .push(Argument::Plain(format!("{rendered_flag}={value}")));
1512                Ok(())
1513            }
1514            _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1515        }
1516    }
1517
1518    fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1519        if let Some(attached) = value.strip_prefix("-Z") {
1520            let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1521            match option.as_str() {
1522                "embed-metadata=no" | "embed-metadata=yes" | "shell-argfiles"
1523                | "unstable-options" => {
1524                    self.parsed.push(Argument::Plain(format!("-Z{option}")));
1525                    return Ok(());
1526                }
1527                "threads" | "threads=" => {
1528                    return Err(BypassReason::MissingValue("-Zthreads".into()));
1529                }
1530                _ => {}
1531            }
1532            if option.starts_with("threads=") {
1533                self.parsed.push(Argument::Plain(format!("-Z{option}")));
1534                return Ok(());
1535            }
1536            return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1537        }
1538        match value {
1539            // `-vV` is how cargo and build scripts ask for the verbose
1540            // version, so it is a query rather than a flag left unmodeled.
1541            "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1542            "-g" | "-O" | "-v" => {
1543                self.parsed.push(Argument::Plain(value.into()));
1544                return Ok(());
1545            }
1546            _ => {}
1547        }
1548        for (short, long) in [
1549            ("-A", "--allow"),
1550            ("-W", "--warn"),
1551            ("-D", "--deny"),
1552            ("-F", "--forbid"),
1553        ] {
1554            if let Some(attached) = value.strip_prefix(short) {
1555                let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1556                self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1557                return Ok(());
1558            }
1559        }
1560        if let Some(attached) = value.strip_prefix("-C") {
1561            let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1562            return self.parse_codegen(&option);
1563        }
1564        if let Some(attached) = value.strip_prefix("-L") {
1565            let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1566            let (kind, path) = search
1567                .split_once('=')
1568                .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1569            if !matches!(kind, "dependency" | "native") {
1570                return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1571            }
1572            self.parsed.push(Argument::SearchPath {
1573                kind: kind.into(),
1574                path: path.into(),
1575            });
1576            return Ok(());
1577        }
1578        if let Some(attached) = value.strip_prefix("-l") {
1579            let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1580            self.parsed.push(Argument::Plain(format!("-l{library}")));
1581            self.native_libraries.push(library);
1582            return Ok(());
1583        }
1584        if let Some(attached) = value.strip_prefix("-o") {
1585            let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1586            self.explicit_output = Some(path.clone().into());
1587            self.parsed.push(Argument::Path {
1588                flag: "-o".into(),
1589                path: path.into(),
1590            });
1591            return Ok(());
1592        }
1593        Err(BypassReason::UnknownFlag(value.into()))
1594    }
1595
1596    fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1597        let name = value.split_once('=').map_or(value, |(name, _)| name);
1598        if name == "incremental" {
1599            return Err(BypassReason::Incremental);
1600        }
1601        if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1602            return Err(BypassReason::UnknownCodegenOption(name.into()));
1603        }
1604        // Cargo compiles every dependency of an LTO build with this flag, so
1605        // the rlibs carry bitcode for the final link to optimize across. The
1606        // switch is keyed as text like any other option. A value naming the
1607        // linker plugin is a host file the key does not describe, so that
1608        // spelling still bypasses.
1609        if name == "linker-plugin-lto"
1610            && let Some((_, selection)) = value.split_once('=')
1611            && !matches!(
1612                selection,
1613                "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1614            )
1615        {
1616            return Err(BypassReason::UnknownCodegenOption(format!(
1617                "linker-plugin-lto={selection}"
1618            )));
1619        }
1620        // `-fuse-ld=<name>` selects the linker the driver invokes. Parsed
1621        // rather than keyed as text so the linker identity probe can pin the
1622        // selected linker; a repeated selection dedups, and a conflicting
1623        // second selection falls through to Plain, which native-link handling
1624        // declines to model. The driver honors the last selection, so a
1625        // conflict is a link whose linker this adapter cannot name.
1626        if name == "link-arg"
1627            && let Some((_, option)) = value.split_once('=')
1628            && let Some(selection) = option.strip_prefix("-fuse-ld=")
1629            && !selection.is_empty()
1630            && !selection.contains(',')
1631            && !self.parsed.iter().any(
1632                |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1633            )
1634        {
1635            if !self.parsed.iter().any(
1636                |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1637            ) {
1638                self.parsed.push(Argument::FuseLd(selection.to_owned()));
1639            }
1640            return Ok(());
1641        }
1642        // Only a single filename under @rpath is modeled. Other spellings,
1643        // combined linker options, and explicit absolute names still bypass.
1644        if name == "link-arg"
1645            && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1646            && safe_install_name(option)
1647        {
1648            self.parsed.push(Argument::InstallName(option.to_owned()));
1649            return Ok(());
1650        }
1651        // `-oso_prefix` names a checkout-specific path, so it is parsed
1652        // rather than keyed as text: the path normalizes like every other
1653        // path in the key, which is what lets two checkouts passing their own
1654        // prefixes agree on one action.
1655        if matches!(name, "link-arg" | "link-args")
1656            && let Some((_, option)) = value.split_once('=')
1657            && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1658            && !prefix.trim_end_matches('/').is_empty()
1659            && !prefix.contains(',')
1660        {
1661            let trailing_slash = prefix.ends_with('/');
1662            self.parsed.push(Argument::OsoPrefix {
1663                path: PathBuf::from(prefix.trim_end_matches('/')),
1664                trailing_slash,
1665            });
1666            return Ok(());
1667        }
1668        self.parsed
1669            .push(Argument::Plain(format!("--codegen={value}")));
1670        if name == "extra-filename" {
1671            self.extra_filename = value
1672                .split_once('=')
1673                .map_or(String::new(), |(_, value)| value.to_string());
1674        }
1675        Ok(())
1676    }
1677
1678    /// Model what rustc reads for each `-l`, now that the emit is known.
1679    ///
1680    /// A library emit runs no linker. For `dylib`, `framework`, `link-arg`,
1681    /// and an unspecified kind, rustc only records the name in the crate's
1682    /// metadata, so the flag text already in the key is the whole input. A
1683    /// `static` library is different: rustc opens the archive and bundles its
1684    /// members into the rlib, so the archive's content decides the output. It
1685    /// is found the way rustc finds it and named as a required input, keyed
1686    /// by content exactly like an `--extern` artifact. With `-bundle` rustc
1687    /// records the name for a downstream link instead of reading the archive,
1688    /// so that flag is text like the name-only kinds.
1689    ///
1690    /// A linked output hands every `-l` to its linker, whose search is not
1691    /// modeled here, so those still bypass whatever the kind.
1692    fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1693        for library in std::mem::take(&mut self.native_libraries) {
1694            if link_output != LinkOutput::Library {
1695                return Err(BypassReason::NativeLibrary(library));
1696            }
1697            // `-l [KIND[:MODIFIERS]=]NAME[:RENAME]`. Without a kind the
1698            // library is rustc's "unspecified" kind: it is handed to the
1699            // linker of whatever finally links, and an rlib only records the
1700            // name. rustc bundles into an rlib only what `-l static` names
1701            // (`link_rlib` takes `NativeLibKind::Static` with bundling on),
1702            // so a `libNAME.a` that later appears beside a plain `-l NAME`
1703            // changes that final link, not this compilation.
1704            let Some((kind, name)) = library.split_once('=') else {
1705                continue;
1706            };
1707            let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1708            match kind {
1709                "static" => {}
1710                "dylib" | "framework" | "link-arg" => continue,
1711                _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1712            }
1713            // A rename links RENAME in place of the NAME a `#[link]` attribute
1714            // gave, so RENAME is the archive on disk.
1715            let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1716            let (mut verbatim, mut bundle) = (false, true);
1717            for modifier in modifiers.split(',') {
1718                match modifier {
1719                    "+verbatim" => verbatim = true,
1720                    "-verbatim" => verbatim = false,
1721                    "+bundle" => bundle = true,
1722                    "-bundle" => bundle = false,
1723                    _ => {}
1724                }
1725            }
1726            if !bundle {
1727                continue;
1728            }
1729            let archive = self
1730                .find_native_static_library(name, verbatim)
1731                .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1732                .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1733            self.required_inputs.push(archive);
1734        }
1735        Ok(())
1736    }
1737
1738    /// Resolve a static library the way rustc does: the first `-L native`
1739    /// directory, in command-line order, holding the target's file name for
1740    /// it. rustc would also search a plain `-L` directory, but that kind
1741    /// already bypasses as unsupported before it gets here. Targets with
1742    /// MSVC-style libraries (Windows MSVC and UEFI) accept `NAME.lib` before
1743    /// falling back to the `libNAME.a` every built-in target otherwise uses;
1744    /// `+verbatim` names the file literally. A custom target specification
1745    /// sets its own prefix and suffix, which are not read here, so it is an
1746    /// `Err` rather than a guess that could hash a different archive than
1747    /// rustc bundles. A relative search directory resolves against the
1748    /// process working directory, as it does for rustc.
1749    fn find_native_static_library(
1750        &self,
1751        name: &str,
1752        verbatim: bool,
1753    ) -> Result<Option<PathBuf>, ()> {
1754        let file_names = if verbatim {
1755            vec![name.to_owned()]
1756        } else {
1757            match self.static_library_naming() {
1758                Some(StaticLibraryNaming::Msvc) => {
1759                    vec![format!("{name}.lib"), format!("lib{name}.a")]
1760                }
1761                Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1762                None => return Err(()),
1763            }
1764        };
1765        Ok(self
1766            .parsed
1767            .iter()
1768            .filter_map(|argument| match argument {
1769                Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1770                _ => None,
1771            })
1772            .flat_map(|directory| {
1773                file_names
1774                    .iter()
1775                    .map(move |file_name| directory.join(file_name))
1776            })
1777            .find(|candidate| candidate.is_file()))
1778    }
1779
1780    /// How the target names a static library, or `None` for a custom target
1781    /// specification, whose names only the specification knows.
1782    ///
1783    /// A bare name is a built-in target unless a specification file for it
1784    /// exists where rustc would look, under `RUST_TARGET_PATH` or in the
1785    /// sysroot, which the caller reports through
1786    /// [`ParseOptions::custom_target_search`].
1787    fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1788        let Some(target) = self.target.as_deref() else {
1789            return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1790                StaticLibraryNaming::Msvc
1791            } else {
1792                StaticLibraryNaming::Unix
1793            });
1794        };
1795        if target.ends_with(".json")
1796            || target.contains(['/', '\\'])
1797            || self.options.custom_target_search
1798        {
1799            return None;
1800        }
1801        // rustc's `is_like_msvc` targets: `*-windows-msvc`, `*-win7-windows-msvc`,
1802        // and the UEFI targets, which borrow the MSVC toolchain's conventions.
1803        Some(if target.contains("msvc") || target.ends_with("-uefi") {
1804            StaticLibraryNaming::Msvc
1805        } else {
1806            StaticLibraryNaming::Unix
1807        })
1808    }
1809
1810    fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1811        if value == "-" {
1812            return Err(BypassReason::StandardInput);
1813        }
1814        if self.source.replace(value.into()).is_some() {
1815            return Err(BypassReason::MultipleInputs);
1816        }
1817        Ok(())
1818    }
1819
1820    fn classify(&self) -> Result<LinkOutput, BypassReason> {
1821        // A compilation that emits no `link` produces exactly what an rlib's
1822        // metadata emit does -- dep-info and `lib<name>.rmeta` -- whatever it
1823        // calls its crate type. `cargo check` and clippy compile every binary
1824        // and test target this way, and those were bypassing as an
1825        // unsupported crate type for a linked artifact none of them asked
1826        // for. No linker runs, so nothing downstream needs a linker identity
1827        // to describe, and rustc names the metadata the same way for every
1828        // crate type.
1829        let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1830        let builds_a_library = !self.test
1831            && !self.crate_types.is_empty()
1832            && self
1833                .crate_types
1834                .iter()
1835                .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1836        let link_output = if never_links || builds_a_library {
1837            LinkOutput::Library
1838        } else if self
1839            .target
1840            .as_deref()
1841            .is_some_and(compiler_bundled_wasm_target)
1842            && ((self.test && self.crate_types.is_empty())
1843                || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1844        {
1845            if self.parsed.iter().any(|argument| match argument {
1846                Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1847                Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1848                    !matches!(
1849                        value.rsplit_once('=').map(|(_, value)| value),
1850                        Some("y" | "yes" | "on" | "true")
1851                    )
1852                }
1853                _ => false,
1854            }) {
1855                return Err(BypassReason::UnknownCodegenOption(
1856                    "link-self-contained".into(),
1857                ));
1858            }
1859            if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1860                && self.parsed.iter().any(|argument| {
1861                    matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1862                })
1863            {
1864                return Err(BypassReason::UnknownCodegenOption(
1865                    "target-feature=-crt-static".into(),
1866                ));
1867            }
1868            // These targets use a linker and, where applicable, CRT objects
1869            // and libc shipped in the Rust toolchain. Unlike native linking,
1870            // there are no implicit host inputs outside compiler identity.
1871            LinkOutput::WasmExecutable
1872        } else if self.options.cache_native_links && self.links_a_native_artifact() {
1873            self.check_native_link_is_portable()?;
1874            if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1875                LinkOutput::NativeProcMacro
1876            } else {
1877                LinkOutput::NativeExecutable
1878            }
1879        } else if self.test {
1880            return Err(BypassReason::UnsupportedCrateType("test".into()));
1881        } else {
1882            return Err(BypassReason::UnsupportedCrateType(
1883                self.crate_types
1884                    .iter()
1885                    .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1886                    .cloned()
1887                    .unwrap_or_else(|| "bin".into()),
1888            ));
1889        };
1890        // A link argument is handed to the linker verbatim, and its text is all
1891        // the adapter has to go on. `-Clink-arg=-Tlink.x` names a linker script
1892        // resolved off the search path, `-Clink-arg=-fuse-ld=lld` replaces the
1893        // linker the identity in the key describes, and neither is told apart
1894        // from `/STACK:8000000` by any rule short of knowing the linker's own
1895        // grammar. Where nothing links there is nothing to tell apart: an rlib
1896        // or an rmeta is produced without a linker invocation, so the option is
1897        // inert and the key carries its text like any other codegen option.
1898        if link_output != LinkOutput::Library
1899            && let Some(option) = self.first_link_argument()
1900        {
1901            return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1902        }
1903        if self
1904            .parsed
1905            .iter()
1906            .any(|arg| matches!(arg, Argument::InstallName(_)))
1907            && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
1908        {
1909            return Err(BypassReason::UnmodeledLinkArgument(
1910                "link-arg=-Wl,-install_name".into(),
1911            ));
1912        }
1913        // `-fuse-ld` is modeled only for native links, where the linker
1914        // identity probe resolves and pins the linker it names. A wasm or
1915        // other non-native link consuming the selection has no identity that
1916        // describes it, so it declines the action like any other link
1917        // argument. Where nothing links, the selection is inert and the key
1918        // carries its text.
1919        if let Some(selection) = self.parsed.iter().find_map(|argument| {
1920            let Argument::FuseLd(selection) = argument else {
1921                return None;
1922            };
1923            Some(selection.as_str())
1924        }) && !matches!(
1925            link_output,
1926            LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1927        ) {
1928            return Err(BypassReason::UnmodeledLinkArgument(format!(
1929                "link-arg=-fuse-ld={selection}"
1930            )));
1931        }
1932        if self.parsed.iter().any(|argument| {
1933            matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1934        }) && !matches!(
1935            link_output,
1936            LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1937        ) {
1938            return Err(BypassReason::UnknownCodegenOption("linker".into()));
1939        }
1940        // `-oso_prefix` is modeled, but only where ld64 is the linker reading
1941        // it: a native macOS link. Any other linker sees an option this
1942        // adapter cannot vouch for, exactly like the arguments above.
1943        if !matches!(
1944            link_output,
1945            LinkOutput::Library | LinkOutput::NativeExecutable | LinkOutput::NativeProcMacro
1946        ) && self
1947            .parsed
1948            .iter()
1949            .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
1950        {
1951            return Err(BypassReason::UnmodeledLinkArgument(
1952                "link-arg=-Wl,-oso_prefix".into(),
1953            ));
1954        }
1955        if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1956            Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1957            _ => None,
1958        }) {
1959            return Err(BypassReason::UnresolvedExtern(name.clone()));
1960        }
1961        if let Some(emit) = self
1962            .emits
1963            .iter()
1964            .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1965        {
1966            return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1967        }
1968        if !self
1969            .emits
1970            .iter()
1971            .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1972        {
1973            return Err(BypassReason::NoCacheableOutput);
1974        }
1975        Ok(link_output)
1976    }
1977}
1978
1979impl Parser<'_> {
1980    /// The first `-C link-arg` or `-C link-args` option in the invocation,
1981    /// rendered as it appears in the action key.
1982    fn first_link_argument(&self) -> Option<&str> {
1983        self.parsed.iter().find_map(|argument| {
1984            let Argument::Plain(value) = argument else {
1985                return None;
1986            };
1987            let option = value.strip_prefix("--codegen=")?;
1988            let name = option.split_once('=').map_or(option, |(name, _)| name);
1989            matches!(name, "link-arg" | "link-args").then_some(option)
1990        })
1991    }
1992
1993    /// Whether an `-oso_prefix` strips the directory this link's objects live
1994    /// in from the debug map ld64 records.
1995    ///
1996    /// The objects a native link reads -- this unit's own compiled objects
1997    /// and the rlibs it links -- sit in the output directory, so a prefix
1998    /// covering that directory leaves the debug map naming spellings every
1999    /// checkout shares. Toolchain objects stay absolute, as they do in the
2000    /// paths rustc itself embeds on every platform.
2001    fn oso_prefix_covers_outputs(&self) -> bool {
2002        let Some(directory) = self
2003            .out_dir
2004            .as_deref()
2005            .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2006        else {
2007            return false;
2008        };
2009        if !directory.is_absolute() {
2010            return false;
2011        }
2012        let directory = normalize_components(directory);
2013        self.parsed.iter().any(|argument| {
2014            let Argument::OsoPrefix { path, .. } = argument else {
2015                return false;
2016            };
2017            path.is_absolute()
2018                && (directory.starts_with(normalize_components(path))
2019                    || std::fs::canonicalize(&directory)
2020                        .ok()
2021                        .zip(std::fs::canonicalize(path).ok())
2022                        .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2023        })
2024    }
2025
2026    /// Whether this invocation links an artifact for the host.
2027    ///
2028    /// An explicit `--target` bypasses even when it spells the host triple:
2029    /// rustc without one links for the host by construction, which is the
2030    /// cheapest description of "the linker this adapter can identify", and
2031    /// cargo omits it for host builds anyway.
2032    fn links_a_native_artifact(&self) -> bool {
2033        self.target.is_none()
2034            // A compilation that emits no linked artifact did not link, so it
2035            // needs no linker to describe it. `cargo check --tests` asks for
2036            // metadata alone, and reading that as a link would send it looking
2037            // for a linker identity and refusing flags no linker ever saw.
2038            && self.emits.iter().any(|emit| emit.kind == "link")
2039            && ((self.test && self.crate_types.is_empty())
2040                || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro")))
2041    }
2042
2043    /// Reject a native link whose result depends on something the key cannot
2044    /// describe, or that leaves artifacts beside the ones mbx would store.
2045    ///
2046    /// Each check names a value rather than a flag: an option absent from the
2047    /// invocation keeps rustc's default, which the compiler identity already
2048    /// pins, while any other spelling is refused rather than guessed at.
2049    fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2050        for argument in &self.parsed {
2051            let Argument::Plain(value) = argument else {
2052                continue;
2053            };
2054            // `-g` is rustc's shorthand for debug info, and arrives as itself
2055            // rather than as a codegen option.
2056            let (name, value) = if value == "-g" {
2057                ("debuginfo", Some("2"))
2058            } else if let Some(option) = value.strip_prefix("--codegen=") {
2059                match option.split_once('=') {
2060                    Some((name, value)) => (name, Some(value)),
2061                    // A codegen flag with no value asks for its enabled form,
2062                    // which is what cargo passes for `-Crpath`. Reading it as
2063                    // "nothing to check" is how these slipped through.
2064                    None => (option, None),
2065                }
2066            } else {
2067                continue;
2068            };
2069            let unportable = match name {
2070                // Packed debug info leaves a .dSYM bundle or .dwp file beside
2071                // the binary, and mbx stores neither. Unpacked is macOS's
2072                // debug-map arrangement -- debug info stays in the objects and
2073                // nothing lands beside the binary -- so the same covering
2074                // `-oso_prefix` that makes the debug map portable covers it.
2075                "split-debuginfo" => match value {
2076                    Some("off") => false,
2077                    Some("unpacked") if cfg!(target_os = "macos") => {
2078                        !self.oso_prefix_covers_outputs()
2079                    }
2080                    _ => true,
2081                },
2082                // ld64 records absolute object paths and their timestamps in
2083                // the binary's debug map, so the same source links to
2084                // different bytes in another checkout -- or the same one
2085                // twice. An `-oso_prefix` covering the output directory
2086                // strips those paths down to spellings every checkout
2087                // shares, which is the same leniency a Linux link already
2088                // gets for the paths rustc itself embeds; what remains
2089                // (object timestamps) only shows up under verification.
2090                "debuginfo" if cfg!(target_os = "macos") => {
2091                    !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2092                }
2093                // An rpath and a dynamically linked native program embed this
2094                // checkout's absolute target directory. Cargo also passes
2095                // `prefer-dynamic` to proc macros, but their dynamic runtime
2096                // is rustc's own sysroot (already pinned by compiler identity),
2097                // while Cargo's dependencies are linked into the macro. There
2098                // is no checkout rpath to carry; keep rejecting an explicit
2099                // `rpath` independently.
2100                "rpath" => is_enabled(value),
2101                "prefer-dynamic" => {
2102                    is_enabled(value)
2103                        && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2104                }
2105                // The CRT objects a self-contained link uses come from
2106                // somewhere other than where the driver reports.
2107                "link-self-contained" => true,
2108                _ => false,
2109            };
2110            if unportable {
2111                return Err(BypassReason::UnportableNativeLink(match value {
2112                    Some(value) => format!("{name}={value}"),
2113                    None => name.to_owned(),
2114                }));
2115            }
2116        }
2117        Ok(())
2118    }
2119}
2120
2121fn safe_install_name(name: &str) -> bool {
2122    !name.is_empty()
2123        && name
2124            .bytes()
2125            .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2126}
2127
2128/// Whether a boolean codegen option is asking for its enabled form. Absent a
2129/// value, rustc reads the flag itself as the request.
2130fn is_enabled(value: Option<&str>) -> bool {
2131    matches!(value, None | Some("y" | "yes" | "on" | "true"))
2132}
2133
2134fn compiler_bundled_wasm_target(target: &str) -> bool {
2135    COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2136}
2137
2138fn parse_emits(value: &str) -> Vec<Emit> {
2139    value
2140        .split(',')
2141        .map(|emit| {
2142            let (kind, path) = emit
2143                .split_once('=')
2144                .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2145            Emit {
2146                kind: kind.into(),
2147                path,
2148            }
2149        })
2150        .collect()
2151}
2152
2153struct ActionBuilder<'a> {
2154    invocation: &'a RustcInvocation,
2155    context: ActionContext,
2156    mappings: Vec<SharedPathMapping>,
2157    /// The directories resolved while building this one action. Its inputs
2158    /// share a few parents, and resolving each once is what keeps a crate
2159    /// with hundreds of them from paying a `realpath` walk per input.
2160    aliases: PathAliases,
2161    linker: Option<LinkerIdentity>,
2162}
2163
2164impl<'a> ActionBuilder<'a> {
2165    fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2166        context.path_mappings = PathMapping::ordered(&context.path_mappings);
2167        let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2168        Self {
2169            linker: None,
2170            invocation,
2171            mappings,
2172            aliases: PathAliases::default(),
2173            context,
2174        }
2175    }
2176
2177    fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2178        self.linker = linker;
2179        self
2180    }
2181
2182    fn build(self) -> Result<RustcAction, BypassReason> {
2183        self.validate_mappings()?;
2184        let invocation = self.invocation_descriptor()?;
2185        let environment = self.environment_descriptor()?;
2186
2187        let mut inputs = BTreeMap::<String, CacheDigest>::new();
2188        for input in &self.context.inputs {
2189            input
2190                .digest
2191                .validate()
2192                .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2193            let path = self.normalize_path(&input.path)?;
2194            if inputs
2195                .insert(path.clone(), input.digest.clone())
2196                .is_some_and(|existing| existing != input.digest)
2197            {
2198                return Err(BypassReason::ConflictingInput(path));
2199            }
2200        }
2201        let required = self
2202            .invocation
2203            .required_inputs
2204            .iter()
2205            .map(|path| self.normalize_path(path))
2206            .collect::<Result<BTreeSet<_>, _>>()?;
2207        if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2208            return Err(BypassReason::MissingRequiredInput(missing.clone()));
2209        }
2210        let inputs = inputs
2211            .into_iter()
2212            .map(|(path, digest)| InputDescriptor { path, digest })
2213            .collect();
2214        // A native link without a linker identity would be keyed as though the
2215        // host did not matter. Refuse rather than publish that claim.
2216        if self.invocation.links_natively() && self.linker.is_none() {
2217            return Err(BypassReason::UnportableNativeLink(
2218                "linker identity is unknown".into(),
2219            ));
2220        }
2221        let descriptor = ActionDescriptor {
2222            version: ACTION_SCHEMA_VERSION,
2223            kind: "rustc",
2224            adapter_version: ADAPTER_VERSION,
2225            compiler: invocation.compiler,
2226            arguments: invocation.arguments,
2227            environment,
2228            inputs,
2229            linker: self.linker.clone(),
2230        };
2231        let bytes = canonical_json(&descriptor)
2232            .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2233        let digest = CacheDigest::blake3(&bytes);
2234        Ok(RustcAction { digest, bytes })
2235    }
2236
2237    fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2238        self.validate_mappings()?;
2239        let arguments = self
2240            .invocation
2241            .arguments
2242            .iter()
2243            .map(|argument| self.normalize_argument(argument))
2244            .collect::<Result<Vec<_>, _>>()?;
2245        let required_inputs = self
2246            .invocation
2247            .required_inputs
2248            .iter()
2249            .map(|path| self.normalize_path(path))
2250            .collect::<Result<BTreeSet<_>, _>>()?
2251            .into_iter()
2252            .collect();
2253        Ok(InvocationDescriptor {
2254            version: ACTION_SCHEMA_VERSION,
2255            kind: "rustc",
2256            adapter_version: ADAPTER_VERSION,
2257            compiler: CompilerDescriptor {
2258                toolchain: self.context.compiler.toolchain.clone(),
2259                rustc_version: self.context.compiler.rustc_version.clone(),
2260                host: self.context.compiler.host.clone(),
2261                driver: self.context.compiler.driver.clone(),
2262            },
2263            arguments,
2264            required_inputs,
2265        })
2266    }
2267
2268    fn validate_mappings(&self) -> Result<(), BypassReason> {
2269        if !self.context.working_dir.is_absolute() {
2270            return Err(BypassReason::RelativeWorkingDirectory(
2271                self.context.working_dir.clone(),
2272            ));
2273        }
2274        let mut roots = BTreeSet::new();
2275        let mut placeholders = BTreeSet::new();
2276        for mapping in &self.mappings {
2277            if !mapping.root.is_absolute() {
2278                return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2279            }
2280            if mapping.placeholder.is_empty()
2281                || !mapping
2282                    .placeholder
2283                    .bytes()
2284                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2285                || !roots.insert(normalize_components(&mapping.root))
2286                || !placeholders.insert(&mapping.placeholder)
2287            {
2288                return Err(BypassReason::InvalidPathPlaceholder(
2289                    mapping.placeholder.clone(),
2290                ));
2291            }
2292        }
2293        Ok(())
2294    }
2295
2296    fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2297        match argument {
2298            Argument::Plain(value) => Ok(value.clone()),
2299            Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2300            Argument::SearchPath { kind, path } => {
2301                let text = match self.normalize_path(path) {
2302                    Ok(text) => text,
2303                    // A native directory outside every mapped root is a host
2304                    // toolchain installation, not a checkout location. Its
2305                    // literal path is the key material: the path is
2306                    // version-stamped on the platforms that pass one (the MSVC
2307                    // toolset, the Windows SDK), so hosts that differ miss,
2308                    // which is the same identity-over-content stance
2309                    // [`LinkerIdentity`] takes.
2310                    Err(BypassReason::UnmappedAbsolutePath(absolute))
2311                        if kind == "native" && self.invocation.native_search_is_inert() =>
2312                    {
2313                        absolute
2314                            .to_str()
2315                            .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
2316                            .to_string()
2317                    }
2318                    Err(error) => return Err(error),
2319                };
2320                Ok(format!("-L{kind}={text}"))
2321            }
2322            Argument::Extern { name, path } => match path {
2323                Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2324                None => Ok(format!("--extern={name}")),
2325            },
2326            Argument::Emit(emits) => Ok(format!(
2327                "--emit={}",
2328                emits
2329                    .iter()
2330                    .map(|emit| match &emit.path {
2331                        Some(path) => self
2332                            .normalize_path(path)
2333                            .map(|path| format!("{}={path}", emit.kind)),
2334                        None => Ok(emit.kind.clone()),
2335                    })
2336                    .collect::<Result<Vec<_>, _>>()?
2337                    .join(",")
2338            )),
2339            Argument::RemapPath { from, to } => Ok(format!(
2340                "--remap-path-prefix={}={}",
2341                self.normalize_path(from)?,
2342                to
2343            )),
2344            Argument::InstallName(name) => Ok(format!(
2345                "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2346            )),
2347            Argument::OsoPrefix {
2348                path,
2349                trailing_slash,
2350            } => Ok(format!(
2351                "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2352                self.normalize_path(path)?,
2353                if *trailing_slash { "/" } else { "" }
2354            )),
2355            Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2356        }
2357    }
2358
2359    /// Environment values enter the key verbatim, because rustc may embed one
2360    /// through `env!`: unlike a path used to locate an input, changing the value
2361    /// changes the artifact.
2362    ///
2363    /// A name in `portable_environment` is the exception the caller has earned.
2364    /// Its value normalizes like any other path, so two checkouts agree on it.
2365    fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2366        self.context
2367            .environment
2368            .iter()
2369            .map(|(name, value)| {
2370                let value = match value {
2371                    Some(value) if self.context.portable_environment.contains(name) => {
2372                        Some(self.normalize_path(Path::new(value))?)
2373                    }
2374                    value => value.clone(),
2375                };
2376                Ok((name.clone(), value))
2377            })
2378            .collect()
2379    }
2380
2381    fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2382        normalize_resolved_shared_path_with(
2383            &self.aliases,
2384            path,
2385            &self.context.working_dir,
2386            &self.mappings,
2387        )
2388        .map_err(Into::into)
2389    }
2390}
2391
2392/// Whether a normalized path names something strictly beneath one of
2393/// `directories`, which are normalized the same way and so share its `/`
2394/// separator regardless of platform.
2395fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2396    directories.iter().any(|directory| {
2397        path.len() > directory.len()
2398            && path.as_bytes()[directory.len()] == b'/'
2399            && path.starts_with(directory)
2400    })
2401}
2402
2403fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2404    for mapping in mappings {
2405        let prefix = format!("${{{}}}", mapping.placeholder);
2406        let suffix = if value == prefix {
2407            ""
2408        } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2409            suffix
2410        } else {
2411            continue;
2412        };
2413        if !mapping.root.is_absolute()
2414            || (!suffix.is_empty()
2415                && suffix.split('/').any(|component| {
2416                    component.is_empty()
2417                        || matches!(component, "." | "..")
2418                        || component.contains('\\')
2419                }))
2420        {
2421            return Err(BypassReason::InvalidPredictedInput(value.into()));
2422        }
2423        let mut path = normalize_components(&mapping.root);
2424        path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2425        return Ok(path);
2426    }
2427    Err(BypassReason::InvalidPredictedInput(value.into()))
2428}
2429
2430fn normalize_components(path: &Path) -> PathBuf {
2431    let mut normalized = PathBuf::new();
2432    for component in path.components() {
2433        match component {
2434            Component::CurDir => {}
2435            Component::ParentDir => {
2436                normalized.pop();
2437            }
2438            component => normalized.push(component.as_os_str()),
2439        }
2440    }
2441    normalized
2442}
2443
2444fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2445    if path.is_absolute() {
2446        normalize_components(path)
2447    } else {
2448        normalize_components(&working_dir.join(path))
2449    }
2450}
2451
2452#[cfg(test)]
2453#[path = "rustc_cache_tests.rs"]
2454mod tests;