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    /// A `cdylib` linked for the host. Admitted on Linux only: an ELF shared
391    /// object names neither its own path nor an import library beside it,
392    /// where a Mach-O dylib records its output path as its install name and
393    /// a Windows DLL leaves an import library and export file next to it.
394    NativeSharedLibrary,
395}
396
397/// What the caller is prepared to model beyond the default tier.
398///
399/// The parser itself stays pure: whether the host can describe its linker
400/// precisely enough to key a native link is the caller's question, and the
401/// answer arrives here rather than being read out of the environment.
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
403#[non_exhaustive]
404pub struct ParseOptions {
405    /// Admit natively linked test binaries, executables, and proc macros,
406    /// given a linker identity in the action key. Off by default.
407    pub cache_native_links: bool,
408    /// rustc may resolve this invocation's bare `--target NAME` to a custom
409    /// specification: a `NAME.json` under `RUST_TARGET_PATH` or a
410    /// `lib/rustlib/NAME/target.json` in the sysroot exists. Such a target
411    /// chooses its own static-library file names, which the parser cannot
412    /// know, so `-l static` bypasses.
413    pub custom_target_search: bool,
414}
415
416impl ParseOptions {
417    /// Options that admit native links when `enabled`.
418    pub fn caching_native_links(enabled: bool) -> Self {
419        Self {
420            cache_native_links: enabled,
421            ..Self::default()
422        }
423    }
424
425    /// Options that also say whether rustc could resolve a bare target name
426    /// to a custom specification.
427    pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
428        self.custom_target_search = enabled;
429        self
430    }
431}
432
433/// The cacheable files and dependency manifest produced by a rustc invocation.
434#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct RustcOutputs {
436    /// Common directory containing all modeled outputs.
437    pub directory: PathBuf,
438    /// Cacheable library, metadata, and/or linked program files.
439    pub files: Vec<PathBuf>,
440    /// Dep-info file used for precise input discovery.
441    pub dep_info: PathBuf,
442}
443
444impl RustcInvocation {
445    /// Parse rustc's arguments, excluding the compiler executable supplied as
446    /// the first argument to `RUSTC_WRAPPER`.
447    ///
448    /// Any flag whose cache semantics are not modeled returns a bypass reason
449    /// instead of guessing. A successful parse admits the rlib/rmeta tier,
450    /// every compilation that links nothing at all -- what `cargo check` and
451    /// clippy run, whatever the crate type -- and binaries linked by
452    /// compiler-bundled WebAssembly toolchains.
453    pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
454        Self::parse_with(arguments, ParseOptions::default())
455    }
456
457    /// The command line with `@argfile` response files expanded, as the
458    /// parser sees it. A caller that scans the arguments before parsing, for a
459    /// `--target` a response file may carry, reads this rather than the raw
460    /// list.
461    pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
462        Ok(expand_response_files(arguments)?.arguments)
463    }
464
465    /// Parse as [`RustcInvocation::parse`] does, admitting what `options`
466    /// says the caller can model.
467    pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
468        let expanded = expand_response_files(arguments)?;
469        Parser::new(&expanded.arguments, options).parse()
470    }
471
472    /// Whether this invocation links a native artifact, whose key must
473    /// therefore describe the linker that produced it.
474    pub fn links_natively(&self) -> bool {
475        matches!(
476            self.link_output,
477            LinkOutput::NativeExecutable
478                | LinkOutput::NativeProcMacro
479                | LinkOutput::NativeSharedLibrary
480        )
481    }
482
483    /// Stable Mach-O identity for a cached macOS proc macro. The compiler loads
484    /// these by filename; embedding the checkout's output path only makes the
485    /// bytes (and dependent action keys) vary between otherwise identical builds.
486    pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
487        if !cfg!(target_os = "macos")
488            || self.link_output != LinkOutput::NativeProcMacro
489            || self
490                .arguments
491                .iter()
492                .any(|arg| matches!(arg, Argument::InstallName(_)))
493        {
494            return None;
495        }
496        let file = outputs
497            .files
498            .iter()
499            .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
500        let name = file.file_name()?.to_str()?;
501        if !safe_install_name(name) {
502            return None;
503        }
504        Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
505    }
506
507    /// Linker selected with `-C linker`, if the invocation overrides rustc's
508    /// platform default.
509    pub fn linker_override(&self) -> Option<&Path> {
510        self.arguments.iter().find_map(|argument| {
511            let Argument::Plain(value) = argument else {
512                return None;
513            };
514            value.strip_prefix("--codegen=linker=").map(Path::new)
515        })
516    }
517
518    /// Linker selected with `-C link-arg=-fuse-ld=<name>`, if the invocation
519    /// passes one the adapter models.
520    pub fn fuse_ld(&self) -> Option<&str> {
521        self.arguments.iter().find_map(|argument| {
522            let Argument::FuseLd(selection) = argument else {
523                return None;
524            };
525            Some(selection.as_str())
526        })
527    }
528
529    fn emits_windows_pdb(&self) -> bool {
530        if !cfg!(windows) || !self.links_natively() {
531            return false;
532        }
533        let mut enabled = false;
534        for argument in &self.arguments {
535            let Argument::Plain(value) = argument else {
536                continue;
537            };
538            if value == "-g" {
539                enabled = true;
540            } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
541                enabled = !matches!(value, "0" | "none");
542            }
543        }
544        enabled
545    }
546
547    /// Native directories explicitly searched by this invocation. Callers can
548    /// map external installations so their contents participate in discovery
549    /// and prediction, rather than treating their paths as immutable inputs.
550    pub fn native_search_paths(&self) -> impl Iterator<Item = &Path> {
551        self.arguments.iter().filter_map(|argument| match argument {
552            Argument::SearchPath { kind, path } if kind == "native" => Some(path.as_path()),
553            _ => None,
554        })
555    }
556
557    /// Return the source input passed to rustc.
558    pub fn source(&self) -> &Path {
559        &self.source
560    }
561
562    /// Absolute files named directly by the invocation that rustc must read.
563    ///
564    /// This includes the source, explicit extern artifacts, and custom target
565    /// specifications. Callers can snapshot these before rustc starts even
566    /// though the complete input set is only known from dep-info afterwards.
567    pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
568        self.required_inputs
569            .iter()
570            .map(|path| absolute_path(path, working_dir))
571            .collect()
572    }
573
574    /// Return the explicitly selected compilation target, if any.
575    pub fn target(&self) -> Option<&str> {
576        self.target.as_deref()
577    }
578
579    /// Return the crate name rustc will assign to this compilation.
580    pub fn crate_name(&self) -> &str {
581        &self.crate_name
582    }
583
584    /// Digest of the inputs this compilation owns: its sources and whatever
585    /// they read, but not the artifacts it merely links against.
586    ///
587    /// This is what separates a crate someone is editing from one sitting
588    /// above it in the graph. Rebuilding a dependency changes the action key of
589    /// every crate that links it, because their keys hash its artifact; it does
590    /// not change this. A caller watching for churn has to watch this instead,
591    /// or a single edited crate would drag its whole dependent cone along.
592    ///
593    /// Host paths enter the digest as they are. This describes one checkout to
594    /// itself rather than to the cache, so there is nothing here to make
595    /// portable.
596    pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
597        let linked = self
598            .arguments
599            .iter()
600            .filter_map(|argument| match argument {
601                Argument::Extern {
602                    path: Some(path), ..
603                } => Some(path.as_path()),
604                _ => None,
605            })
606            .collect::<BTreeSet<_>>();
607        let owned = discovered
608            .inputs
609            .iter()
610            .filter(|input| {
611                !linked.contains(input.path.as_path()) && !discovered.is_native_only(&input.path)
612            })
613            .map(|input| (input.path.as_path(), &input.digest))
614            .collect::<BTreeMap<_, _>>();
615        let mut bytes = Vec::new();
616        for (path, digest) in owned {
617            bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
618            bytes.push(0);
619            bytes.extend_from_slice(digest.key().as_bytes());
620            bytes.push(0);
621        }
622        CacheDigest::blake3(&bytes)
623    }
624
625    /// Resolve the files produced by this invocation.
626    ///
627    /// The initial cache tier requires one output directory so its artifact can
628    /// be represented by one protocol directory and restored atomically later.
629    pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
630        if !working_dir.is_absolute() {
631            return Err(BypassReason::RelativeWorkingDirectory(
632                working_dir.to_path_buf(),
633            ));
634        }
635        let explicit_output = self
636            .explicit_output
637            .as_deref()
638            .map(|path| absolute_path(path, working_dir));
639        let output_directory = explicit_output
640            .as_deref()
641            .and_then(Path::parent)
642            .map(Path::to_path_buf)
643            .or_else(|| {
644                self.out_dir
645                    .as_deref()
646                    .map(|path| absolute_path(path, working_dir))
647            })
648            .unwrap_or_else(|| normalize_components(working_dir));
649        // rustc applies `-o` to every emit that has no path of its own, so the
650        // file names cannot be derived from the crate name here. Cargo always
651        // uses --out-dir instead, so refusing to model this costs nothing.
652        if let Some(output) = &explicit_output
653            && self.emits.iter().any(|emit| {
654                emit.path.is_none()
655                    && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
656            })
657        {
658            return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
659        }
660        let mut files = BTreeSet::new();
661        let mut dep_info = None;
662        for emit in &self.emits {
663            if emit.kind == "dep-info" {
664                let path = emit.path.as_ref().map_or_else(
665                    || {
666                        explicit_output.clone().map_or_else(
667                            || {
668                                output_directory
669                                    .join(format!("{}{}.d", self.crate_name, self.extra_filename))
670                            },
671                            |path| path.with_extension("d"),
672                        )
673                    },
674                    |path| absolute_path(path, working_dir),
675                );
676                if path.file_name().is_none() {
677                    return Err(BypassReason::InvalidOutputPath(path));
678                }
679                dep_info = Some(path);
680                continue;
681            }
682            let (prefix, extension) = match emit.kind.as_str() {
683                "link" => match self.link_output {
684                    LinkOutput::Library => ("lib", "rlib"),
685                    LinkOutput::WasmExecutable => ("", "wasm"),
686                    LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
687                    LinkOutput::NativeProcMacro | LinkOutput::NativeSharedLibrary => (
688                        std::env::consts::DLL_PREFIX,
689                        std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
690                    ),
691                },
692                "metadata" => ("lib", "rmeta"),
693                _ => continue,
694            };
695            let path = if let Some(path) = &emit.path {
696                absolute_path(path, working_dir)
697            } else {
698                let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
699                output_directory.join(if extension.is_empty() {
700                    name
701                } else {
702                    format!("{name}.{extension}")
703                })
704            };
705            if path.file_name().is_none() {
706                return Err(BypassReason::InvalidOutputPath(path));
707            }
708            if path.parent() != Some(output_directory.as_path()) {
709                return Err(BypassReason::SplitOutputDirectories);
710            }
711            // Whether a restored output is a program is read back off its name,
712            // so a program that answers to a library's name would be restored
713            // without the permission that makes it runnable. Nothing cargo
714            // emits looks like this; a hand-built invocation could.
715            let has_library_extension = path
716                .extension()
717                .and_then(|extension| extension.to_str())
718                .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
719                || (cfg!(windows)
720                    && path
721                        .file_stem()
722                        .and_then(|stem| Path::new(stem).extension())
723                        .and_then(|extension| extension.to_str())
724                        .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
725            if emit.kind == "link"
726                && !matches!(self.link_output, LinkOutput::Library)
727                && has_library_extension
728            {
729                return Err(BypassReason::AmbiguousOutputName(path));
730            }
731            if emit.kind == "link" && self.emits_windows_pdb() {
732                files.insert(path.with_extension("pdb"));
733            }
734            files.insert(path);
735        }
736        let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
737        if dep_info.parent() != Some(output_directory.as_path()) {
738            return Err(BypassReason::SplitOutputDirectories);
739        }
740        Ok(RustcOutputs {
741            directory: output_directory,
742            files: files.into_iter().collect(),
743            dep_info,
744        })
745    }
746
747    /// Build canonical action bytes after precise input discovery has run.
748    ///
749    /// `context.inputs` must contain the source, every explicit extern, and
750    /// every additional source or environment-generated input discovered from
751    /// dep-info.
752    pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
753        self.action_linked_by(context, None)
754    }
755
756    /// Build canonical action bytes for an invocation that links a native
757    /// program.
758    ///
759    /// A `linker` is required whenever [`RustcInvocation::links_natively`]
760    /// holds: the linker, its startup objects, and the platform SDK are inputs
761    /// rustc dep-info does not enumerate, so a key without them would claim
762    /// more than it can support. Passing one for anything else is ignored,
763    /// since nothing else depends on a linker.
764    pub fn action_linked_by(
765        &self,
766        context: ActionContext,
767        linker: Option<LinkerIdentity>,
768    ) -> Result<RustcAction, BypassReason> {
769        ActionBuilder::new(self, context).linked_by(linker).build()
770    }
771
772    /// Fingerprint the modeled invocation before dependency contents are known.
773    pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
774        let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
775        let bytes = canonical_json(&descriptor)
776            .map_err(|error| BypassReason::Serialization(error.to_string()))?;
777        Ok(CacheDigest::blake3(&bytes))
778    }
779
780    /// Capture normalized dependency paths for a future invocation that has no
781    /// dep-info file yet.
782    pub fn prediction(
783        &self,
784        context: &ActionContext,
785        discovered: &DiscoveredInputs,
786    ) -> Result<RustcInputPrediction, BypassReason> {
787        let builder = ActionBuilder::new(self, context.clone());
788        builder.validate_mappings()?;
789        let mut native_directories = BTreeSet::new();
790        for argument in &self.arguments {
791            if let Argument::SearchPath { kind, path } = argument
792                && kind == "native"
793            {
794                native_directories.insert(builder.normalize_path(path)?);
795            }
796        }
797        // A file beneath a recorded directory is rediscovered by walking that
798        // directory, so naming it as well only makes the payload grow with the
799        // object tree a C dependency leaves in its `OUT_DIR`. `aws-lc-sys`
800        // leaves thousands of files there, which was enough to push the
801        // serialized prediction past the protocol's payload limit and lose the
802        // prediction entirely for the crate that most needed one.
803        //
804        // A file dep-info itself names stays, even beneath a recorded
805        // directory. It is a source of the crate, and rediscovery can only tell
806        // it from the objects around it if the prediction says which it is.
807        // There are few of those; the thousands of objects are not named.
808        let mut inputs = BTreeSet::new();
809        for input in &discovered.inputs {
810            let normalized = builder.normalize_path(&input.path)?;
811            if !under_any_directory(&normalized, &native_directories)
812                || !discovered.is_native_only(&input.path)
813            {
814                inputs.insert(normalized);
815            }
816        }
817        inputs.extend(
818            native_directories
819                .into_iter()
820                .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
821        );
822        Ok(RustcInputPrediction {
823            version: 4,
824            inputs: inputs.into_iter().collect(),
825            environment: discovered.environment.keys().cloned().collect(),
826            compiler_duration_ns: 0,
827            crate_name: String::new(),
828        })
829    }
830}
831
832/// Whether a crate name has the form Cargo gives every build script.
833///
834/// The name alone is not proof: see [`RustcOutputs::build_script_executable`].
835pub fn is_build_script_crate_name(crate_name: &str) -> bool {
836    crate_name
837        .strip_prefix("build_script_")
838        .is_some_and(|stem| !stem.is_empty())
839}
840
841impl RustcOutputs {
842    /// The linked executable Cargo will run as a build script, when this is a
843    /// build-script compilation.
844    ///
845    /// Cargo names a build script's crate `build_script_` plus the stem of its
846    /// source: `build_script_build` for `build.rs`, `build_script_main` for
847    /// `build = "builder/main.rs"`. A `[[bin]]` target can take the same name,
848    /// and Cargo's output directory does not tell them apart, so the caller
849    /// has to confirm from Cargo's environment that this is a build script:
850    /// Cargo sets `CARGO_BIN_NAME` for binary targets and never for one.
851    pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
852        is_build_script_crate_name(crate_name)
853            .then(|| self.files.iter().find(|path| self.is_executable(path)))
854            .flatten()
855            .map(PathBuf::as_path)
856    }
857
858    /// Whether `path` is a linked program whose executable permission is part
859    /// of the declared output contract.
860    /// A program is whatever this invocation emitted that is not a library
861    /// artifact. Every tier the adapter admits distinguishes the two by
862    /// extension -- `rlib` and `rmeta` are the compiler's own, and a linked
863    /// program carries either the target's (`wasm`) or none at all -- so the
864    /// name is enough and no separate list has to be carried alongside.
865    pub fn is_executable(&self, path: &Path) -> bool {
866        self.files.iter().any(|output| output == path)
867            && !matches!(
868                path.extension().and_then(|extension| extension.to_str()),
869                Some("rlib" | "rmeta")
870            )
871    }
872}
873
874#[derive(Debug, Clone, PartialEq, Eq)]
875/// Mapping from a host-specific absolute root to a stable key placeholder.
876pub struct PathMapping {
877    /// Absolute host path to replace.
878    pub root: PathBuf,
879    /// Placeholder name without the surrounding `${...}` syntax.
880    pub placeholder: String,
881}
882
883impl PathMapping {
884    /// Map an absolute host path to a stable cache-key placeholder.
885    pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
886        Self {
887            root: root.into(),
888            placeholder: placeholder.into(),
889        }
890    }
891
892    /// Order mappings deepest root first, which is what normalization needs:
893    /// a target directory inside the workspace has to win over the workspace.
894    pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
895        let mut ordered = mappings.to_vec();
896        ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
897        ordered
898    }
899}
900
901fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
902    mappings
903        .iter()
904        .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
905        .collect()
906}
907
908/// Map an absolute path to its cache-key placeholder form.
909///
910/// `mappings` must already be ordered by [`PathMapping::ordered`]. Exposed for
911/// callers that need the placeholder text before an action exists -- notably to
912/// build the `--remap-path-prefix` flag that makes a compilation independent of
913/// a path in its environment.
914pub fn normalize_mapped_path(
915    path: &Path,
916    working_dir: &Path,
917    mappings: &[PathMapping],
918) -> Result<String, BypassReason> {
919    normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
920}
921
922#[derive(Debug, Clone, PartialEq, Eq)]
923/// Compiler properties that distinguish incompatible action outputs.
924pub struct CompilerIdentity {
925    /// Toolchain selector or installation identity.
926    pub toolchain: String,
927    /// Complete verbose rustc version string.
928    pub rustc_version: String,
929    /// Compiler host target triple.
930    pub host: String,
931    /// Identity of a compiler driver layered over rustc, when one is in use.
932    pub driver: Option<String>,
933}
934
935/// One file input paired with the digest used in the action key.
936#[derive(Debug, Clone, PartialEq, Eq)]
937pub struct ActionInput {
938    /// Absolute host path used to read and verify the input.
939    pub path: PathBuf,
940    /// Digest of the input contents.
941    pub digest: CacheDigest,
942}
943
944/// External information needed to construct a canonical rustc action.
945#[derive(Debug, Clone, PartialEq, Eq)]
946pub struct ActionContext {
947    /// Identity of the compiler that produces the outputs.
948    pub compiler: CompilerIdentity,
949    /// Absolute directory in which rustc runs.
950    pub working_dir: PathBuf,
951    /// Host roots replaced with stable placeholders in the key.
952    pub path_mappings: Vec<PathMapping>,
953    /// Environment inputs and their observed values.
954    pub environment: BTreeMap<String, Option<String>>,
955    /// Environment inputs whose absolute values the compilation has been made
956    /// independent of, and whose values the key therefore normalizes.
957    ///
958    /// Naming one here is a claim about the compilation, not a preference: the
959    /// caller must both neutralize the value inside it (with
960    /// `--remap-path-prefix`) and confirm no output carries the value anyway.
961    pub portable_environment: BTreeSet<String>,
962    /// Complete set of direct and discovered file inputs.
963    pub inputs: Vec<ActionInput>,
964}
965
966/// What produced a linked native program, beyond the compiler itself.
967///
968/// The fields are identity rather than content wherever a compiler's own
969/// identity is: a driver's version output names its toolchain more cheaply than
970/// hashing a hundred megabytes of it, and matches how rustc is identified. The
971/// CRT objects are hashed, because nothing else pins the libc a link resolves
972/// against. Nothing here is placeholder-mapped -- these paths are host
973/// locations rather than checkout locations, and two hosts that differ should
974/// miss rather than share.
975#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
976#[serde(deny_unknown_fields)]
977pub struct LinkerIdentity {
978    /// Resolved absolute path of the linker driver rustc will invoke.
979    pub driver: String,
980    /// Version output of that driver.
981    pub driver_version: String,
982    /// Version of the linker the driver selects.
983    pub linker_version: String,
984    /// Startup objects and libc the driver resolves, by probe name.
985    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
986    pub crt_objects: BTreeMap<String, CacheDigest>,
987    /// Platform SDK identity, where the platform has one.
988    #[serde(skip_serializing_if = "Option::is_none", default)]
989    pub sdk: Option<String>,
990    /// Deployment target the link was made against, where one applies.
991    #[serde(skip_serializing_if = "Option::is_none", default)]
992    pub deployment_target: Option<String>,
993}
994
995/// Canonical action descriptor and its content digest.
996#[derive(Debug, Clone, PartialEq, Eq)]
997pub struct RustcAction {
998    /// Digest of `bytes`, used as the action-cache key.
999    pub digest: CacheDigest,
1000    /// Canonical serialized action descriptor.
1001    pub bytes: Vec<u8>,
1002}
1003
1004/// Normalized input names from the last successful execution of one modeled
1005/// rustc invocation.
1006#[derive(Debug, Clone, PartialEq, Eq)]
1007pub struct RustcInputPrediction {
1008    /// Prediction schema version.
1009    pub version: u8,
1010    /// Normalized input paths observed during the successful invocation.
1011    pub inputs: Vec<String>,
1012    /// Names of environment variables read by the compilation.
1013    pub environment: Vec<String>,
1014    /// Compiler wall time from the successful invocation that produced this
1015    /// prediction. Zero means no timing hint was recorded.
1016    pub compiler_duration_ns: u64,
1017    /// Crate name associated with the timing hint.
1018    pub crate_name: String,
1019}
1020
1021#[derive(Serialize, Deserialize)]
1022#[serde(deny_unknown_fields)]
1023struct RustcInputPredictionWire {
1024    version: u8,
1025    inputs: Vec<String>,
1026    environment: Vec<String>,
1027    #[serde(default, skip_serializing_if = "is_zero")]
1028    compiler_duration_ns: u64,
1029    #[serde(default, skip_serializing_if = "String::is_empty")]
1030    crate_name: String,
1031}
1032
1033impl Serialize for RustcInputPrediction {
1034    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1035    where
1036        S: Serializer,
1037    {
1038        RustcInputPredictionWire {
1039            version: self.version,
1040            inputs: if self.version == 4 {
1041                compact_prediction_inputs(&self.inputs)
1042            } else {
1043                self.inputs.clone()
1044            },
1045            environment: self.environment.clone(),
1046            compiler_duration_ns: self.compiler_duration_ns,
1047            crate_name: self.crate_name.clone(),
1048        }
1049        .serialize(serializer)
1050    }
1051}
1052
1053impl<'de> Deserialize<'de> for RustcInputPrediction {
1054    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1055    where
1056        D: Deserializer<'de>,
1057    {
1058        let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1059        let inputs = if wire.version == 4 {
1060            expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1061        } else {
1062            wire.inputs
1063        };
1064        Ok(Self {
1065            version: wire.version,
1066            inputs,
1067            environment: wire.environment,
1068            compiler_duration_ns: wire.compiler_duration_ns,
1069            crate_name: wire.crate_name,
1070        })
1071    }
1072}
1073
1074fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1075    let mut previous = "";
1076    inputs
1077        .iter()
1078        .map(|input| {
1079            let mut shared = previous
1080                .bytes()
1081                .zip(input.bytes())
1082                .take_while(|(left, right)| left == right)
1083                .count();
1084            while !input.is_char_boundary(shared) {
1085                shared -= 1;
1086            }
1087            let compact = format!("{shared}:{}", &input[shared..]);
1088            previous = input;
1089            compact
1090        })
1091        .collect()
1092}
1093
1094fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1095    let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1096    let mut decoded_bytes = 0_usize;
1097    for input in inputs {
1098        let (shared_text, suffix) = input
1099            .split_once(':')
1100            .ok_or("compact rustc prediction input has no prefix length")?;
1101        let shared: usize = shared_text
1102            .parse()
1103            .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1104        if shared.to_string() != shared_text
1105            || shared > expanded.last().map_or(0, String::len)
1106            || expanded
1107                .last()
1108                .is_some_and(|previous| !previous.is_char_boundary(shared))
1109        {
1110            return Err("compact rustc prediction prefix is not canonical");
1111        }
1112        let mut path = expanded
1113            .last()
1114            .map_or_else(String::new, |previous| previous[..shared].to_string());
1115        path.push_str(suffix);
1116        decoded_bytes = decoded_bytes
1117            .checked_add(path.len())
1118            .ok_or("compact rustc prediction is too large")?;
1119        if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1120            return Err("compact rustc prediction is too large");
1121        }
1122        expanded.push(path);
1123    }
1124    Ok(expanded)
1125}
1126
1127fn is_zero(value: &u64) -> bool {
1128    *value == 0
1129}
1130
1131impl RustcInputPrediction {
1132    /// Rehash the predicted paths and read the current environment. The caller
1133    /// still recomputes the full action digest, so changed inputs are misses.
1134    pub fn discover(
1135        &self,
1136        working_dir: &Path,
1137        path_mappings: &[PathMapping],
1138        digests: &dyn FileDigestCache,
1139    ) -> Result<DiscoveredInputs, BypassReason> {
1140        if !matches!(self.version, 2..=4) {
1141            return Err(BypassReason::UnsupportedPrediction);
1142        }
1143        if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1144            return Err(BypassReason::UnsupportedPrediction);
1145        }
1146        let mut paths = BTreeSet::new();
1147        let mut named = BTreeSet::new();
1148        let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1149        let mut native_bytes = 0_u64;
1150        for path in &self.inputs {
1151            if self.version >= 3
1152                && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1153            {
1154                let directory = denormalize_path(path, path_mappings)?;
1155                dep_info::collect_native_directory(
1156                    &directory,
1157                    &admitted_roots,
1158                    &mut paths,
1159                    &mut native_bytes,
1160                )?;
1161            } else {
1162                let path = denormalize_path(path, path_mappings)?;
1163                named.insert(path.clone());
1164                paths.insert(path);
1165            }
1166        }
1167        let native_only = paths.difference(&named).cloned().collect();
1168        let environment = self
1169            .environment
1170            .iter()
1171            .map(|name| {
1172                if name.is_empty() || name.contains(['=', '\0']) {
1173                    return Err(BypassReason::UnsupportedPrediction);
1174                }
1175                let value = std::env::var_os(name)
1176                    .map(|value| {
1177                        value
1178                            .into_string()
1179                            .map_err(|_| BypassReason::UnsupportedPrediction)
1180                    })
1181                    .transpose()?;
1182                Ok((name.clone(), value))
1183            })
1184            .collect::<Result<BTreeMap<_, _>, _>>()?;
1185        Ok(
1186            DiscoveredInputs::from_paths(working_dir, paths, environment, digests)?
1187                .with_native_only(native_only),
1188        )
1189    }
1190}
1191
1192#[derive(Serialize)]
1193struct ActionDescriptor {
1194    version: u8,
1195    kind: &'static str,
1196    adapter_version: u8,
1197    compiler: CompilerDescriptor,
1198    arguments: Vec<String>,
1199    environment: BTreeMap<String, Option<String>>,
1200    inputs: Vec<InputDescriptor>,
1201    /// Omitted entirely unless the invocation links natively, so every key
1202    /// written before this field existed still serializes to the same bytes.
1203    #[serde(skip_serializing_if = "Option::is_none")]
1204    linker: Option<LinkerIdentity>,
1205}
1206
1207#[derive(Serialize)]
1208struct InvocationDescriptor {
1209    version: u8,
1210    kind: &'static str,
1211    adapter_version: u8,
1212    compiler: CompilerDescriptor,
1213    arguments: Vec<String>,
1214    required_inputs: Vec<String>,
1215}
1216
1217#[derive(Serialize)]
1218struct CompilerDescriptor {
1219    toolchain: String,
1220    rustc_version: String,
1221    host: String,
1222    #[serde(skip_serializing_if = "Option::is_none")]
1223    driver: Option<String>,
1224}
1225
1226#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1227struct InputDescriptor {
1228    path: String,
1229    digest: CacheDigest,
1230}
1231
1232/// The static-library file names a built-in target uses.
1233#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1234enum StaticLibraryNaming {
1235    /// `NAME.lib`, then the Unix spelling as a fallback.
1236    Msvc,
1237    /// `libNAME.a`.
1238    Unix,
1239}
1240
1241struct Parser<'a> {
1242    arguments: &'a [OsString],
1243    index: usize,
1244    parsed: Vec<Argument>,
1245    source: Option<PathBuf>,
1246    crate_types: Vec<String>,
1247    emits: Vec<Emit>,
1248    required_inputs: Vec<PathBuf>,
1249    native_libraries: Vec<String>,
1250    test: bool,
1251    crate_name: Option<String>,
1252    extra_filename: String,
1253    out_dir: Option<PathBuf>,
1254    explicit_output: Option<PathBuf>,
1255    target: Option<String>,
1256    sysroot_backend: Option<String>,
1257    options: ParseOptions,
1258}
1259
1260struct ExpandedArguments {
1261    arguments: Vec<OsString>,
1262}
1263
1264#[derive(Default)]
1265struct ResponseExpander {
1266    shell_argfiles: bool,
1267    next_is_unstable_option: bool,
1268    arguments: Vec<OsString>,
1269}
1270
1271impl ResponseExpander {
1272    fn push(&mut self, argument: String) {
1273        if self.next_is_unstable_option {
1274            self.shell_argfiles |= argument == "shell-argfiles";
1275            self.next_is_unstable_option = false;
1276        } else if let Some(option) = argument.strip_prefix("-Z") {
1277            if option.is_empty() {
1278                self.next_is_unstable_option = true;
1279            } else {
1280                self.shell_argfiles |= option == "shell-argfiles";
1281            }
1282        }
1283        self.arguments.push(argument.into());
1284    }
1285}
1286
1287/// Match rustc's argfile expansion: UTF-8, one option per line, no recursive
1288/// expansion, with the nightly shell form enabled only after its `-Z` flag.
1289fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1290    let mut expanded = ResponseExpander::default();
1291    for (index, argument) in arguments.iter().enumerate() {
1292        let argument = argument
1293            .to_str()
1294            .ok_or(BypassReason::NonUtf8Argument { index })?;
1295        let Some(argfile) = argument.strip_prefix('@') else {
1296            expanded.push(argument.to_string());
1297            continue;
1298        };
1299        let (path, shell) = match argfile.split_once(':') {
1300            Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1301            _ => (argfile, false),
1302        };
1303        let contents = std::fs::read_to_string(path).map_err(|error| {
1304            BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1305        })?;
1306        if shell {
1307            let arguments = shlex::split(&contents).ok_or_else(|| {
1308                BypassReason::ResponseFile(format!(
1309                    "invalid shell-style arguments in {}",
1310                    Path::new(path).display()
1311                ))
1312            })?;
1313            for argument in arguments {
1314                expanded.push(argument);
1315            }
1316        } else {
1317            for argument in contents.lines() {
1318                expanded.push(argument.to_string());
1319            }
1320        }
1321    }
1322    Ok(ExpandedArguments {
1323        arguments: expanded.arguments,
1324    })
1325}
1326
1327impl<'a> Parser<'a> {
1328    fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1329        Self {
1330            arguments,
1331            options,
1332            index: 0,
1333            parsed: Vec::new(),
1334            source: None,
1335            crate_types: Vec::new(),
1336            emits: Vec::new(),
1337            required_inputs: Vec::new(),
1338            native_libraries: Vec::new(),
1339            test: false,
1340            crate_name: None,
1341            extra_filename: String::new(),
1342            out_dir: None,
1343            explicit_output: None,
1344            target: None,
1345            sysroot_backend: None,
1346        }
1347    }
1348
1349    fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1350        while self.index < self.arguments.len() {
1351            let value = self.current()?.to_string();
1352            self.index += 1;
1353            if let Some(long) = value.strip_prefix("--") {
1354                self.parse_long(long)?;
1355            } else if value.starts_with('-') && value != "-" {
1356                self.parse_short(&value)?;
1357            } else {
1358                self.parse_input(&value)?;
1359            }
1360        }
1361
1362        // rustc searches an explicit sysroot for the backend first, and only the compiler's own is hashed.
1363        if let Some(backend) = self.sysroot_backend.take()
1364            && self.parsed.iter().any(
1365                |argument| matches!(argument, Argument::Path { flag, .. } if flag == "--sysroot"),
1366            )
1367        {
1368            return Err(BypassReason::UnknownFlag(backend));
1369        }
1370        let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1371        let link_output = self.classify()?;
1372        self.require_native_libraries(link_output)?;
1373        let crate_name = self.crate_name.clone().map_or_else(
1374            || {
1375                source
1376                    .file_stem()
1377                    .and_then(|name| name.to_str())
1378                    .map(|name| name.replace('-', "_"))
1379                    .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1380            },
1381            Ok,
1382        )?;
1383        self.required_inputs.push(source.clone());
1384        Ok(RustcInvocation {
1385            arguments: self.parsed,
1386            source,
1387            required_inputs: self.required_inputs,
1388            crate_name,
1389            extra_filename: self.extra_filename,
1390            out_dir: self.out_dir,
1391            explicit_output: self.explicit_output,
1392            emits: self.emits,
1393            target: self.target,
1394            link_output,
1395        })
1396    }
1397
1398    fn current(&self) -> Result<&str, BypassReason> {
1399        self.arguments[self.index]
1400            .to_str()
1401            .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1402    }
1403
1404    fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1405        if let Some(value) = inline {
1406            if value.is_empty() {
1407                return Err(BypassReason::MissingValue(flag.into()));
1408            }
1409            return Ok(value.into());
1410        }
1411        if self.index >= self.arguments.len() {
1412            return Err(BypassReason::MissingValue(flag.into()));
1413        }
1414        let value = self.current()?.to_string();
1415        self.index += 1;
1416        Ok(value)
1417    }
1418
1419    fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1420        let (flag, inline) = value
1421            .split_once('=')
1422            .map_or((value, None), |(flag, value)| (flag, Some(value)));
1423        let rendered_flag = format!("--{flag}");
1424        match flag {
1425            "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1426            "test" => {
1427                self.test = true;
1428                self.parsed.push(Argument::Plain(rendered_flag));
1429                Ok(())
1430            }
1431            "verbose" => {
1432                self.parsed.push(Argument::Plain(rendered_flag));
1433                Ok(())
1434            }
1435            "crate-name" => {
1436                let value = self.take_value(&rendered_flag, inline)?;
1437                self.crate_name = Some(value.clone());
1438                self.parsed
1439                    .push(Argument::Plain(format!("{rendered_flag}={value}")));
1440                Ok(())
1441            }
1442            "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1443            | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1444            | "deny" | "forbid" | "cap-lints" => {
1445                let value = self.take_value(&rendered_flag, inline)?;
1446                self.parsed
1447                    .push(Argument::Plain(format!("{rendered_flag}={value}")));
1448                Ok(())
1449            }
1450            "target" => {
1451                let value = self.take_value(&rendered_flag, inline)?;
1452                // rustc reads the first `--target`, as getopts does; a later
1453                // one is still keyed as an argument but does not decide the
1454                // target, so it is not read and not a required input either.
1455                let effective = self.target.is_none();
1456                if effective {
1457                    self.target = Some(value.clone());
1458                }
1459                if value.ends_with(".json") || value.contains(['/', '\\']) {
1460                    let path = PathBuf::from(value);
1461                    if effective {
1462                        self.required_inputs.push(path.clone());
1463                    }
1464                    self.parsed.push(Argument::Path {
1465                        flag: rendered_flag,
1466                        path,
1467                    });
1468                } else {
1469                    self.parsed
1470                        .push(Argument::Plain(format!("{rendered_flag}={value}")));
1471                }
1472                Ok(())
1473            }
1474            "crate-type" => {
1475                let value = self.take_value(&rendered_flag, inline)?;
1476                self.crate_types
1477                    .extend(value.split(',').map(ToOwned::to_owned));
1478                self.parsed
1479                    .push(Argument::Plain(format!("{rendered_flag}={value}")));
1480                Ok(())
1481            }
1482            "emit" => {
1483                let value = self.take_value(&rendered_flag, inline)?;
1484                let emits = parse_emits(&value);
1485                self.emits.extend(emits.clone());
1486                self.parsed.push(Argument::Emit(emits));
1487                Ok(())
1488            }
1489            "out-dir" => {
1490                let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1491                self.out_dir = Some(path.clone());
1492                self.parsed.push(Argument::Path {
1493                    flag: rendered_flag,
1494                    path,
1495                });
1496                Ok(())
1497            }
1498            "sysroot" => {
1499                let path = self.take_value(&rendered_flag, inline)?;
1500                self.parsed.push(Argument::Path {
1501                    flag: rendered_flag,
1502                    path: path.into(),
1503                });
1504                Ok(())
1505            }
1506            "extern" => {
1507                let value = self.take_value(&rendered_flag, inline)?;
1508                let (name, path) = value
1509                    .split_once('=')
1510                    .map_or((value.as_str(), None), |(name, path)| {
1511                        (name, Some(PathBuf::from(path)))
1512                    });
1513                if let Some(path) = &path {
1514                    self.required_inputs.push(path.clone());
1515                }
1516                self.parsed.push(Argument::Extern {
1517                    name: name.into(),
1518                    path,
1519                });
1520                Ok(())
1521            }
1522            "remap-path-prefix" => {
1523                let value = self.take_value(&rendered_flag, inline)?;
1524                let Some((from, to)) = value.split_once('=') else {
1525                    return Err(BypassReason::MissingValue(rendered_flag));
1526                };
1527                self.parsed.push(Argument::RemapPath {
1528                    from: from.into(),
1529                    to: to.into(),
1530                });
1531                Ok(())
1532            }
1533            "codegen" => {
1534                let value = self.take_value(&rendered_flag, inline)?;
1535                self.parse_codegen(&value)
1536            }
1537            "jobs-frontend" => {
1538                let value = self.take_value(&rendered_flag, inline)?;
1539                self.parsed
1540                    .push(Argument::Plain(format!("{rendered_flag}={value}")));
1541                Ok(())
1542            }
1543            _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1544        }
1545    }
1546
1547    fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1548        if let Some(attached) = value.strip_prefix("-Z") {
1549            let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1550            match option.as_str() {
1551                // Cargo passes `force-unstable-if-unmarked` to every standard
1552                // library unit under `-Zbuild-std`. It changes the stability
1553                // recorded in the crate's metadata, so it stays in the key.
1554                "embed-metadata=no"
1555                | "embed-metadata=yes"
1556                | "force-unstable-if-unmarked"
1557                | "shell-argfiles"
1558                | "unstable-options" => {
1559                    self.parsed.push(Argument::Plain(format!("-Z{option}")));
1560                    return Ok(());
1561                }
1562                "threads" | "threads=" => {
1563                    return Err(BypassReason::MissingValue("-Zthreads".into()));
1564                }
1565                _ => {}
1566            }
1567            if option.starts_with("threads=") {
1568                self.parsed.push(Argument::Plain(format!("-Z{option}")));
1569                return Ok(());
1570            }
1571            // rustc loads a value containing a `.` as a library path, which the key doesn't hash.
1572            if let Some(backend) = option.strip_prefix("codegen-backend=")
1573                && !backend.is_empty()
1574                && !backend.contains(['.', '/', '\\'])
1575            {
1576                self.sysroot_backend = Some(format!("-Z{option}"));
1577                self.parsed.push(Argument::Plain(format!("-Z{option}")));
1578                return Ok(());
1579            }
1580            return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1581        }
1582        match value {
1583            // `-vV` is how cargo and build scripts ask for the verbose
1584            // version, so it is a query rather than a flag left unmodeled.
1585            "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1586            "-g" | "-O" | "-v" => {
1587                self.parsed.push(Argument::Plain(value.into()));
1588                return Ok(());
1589            }
1590            _ => {}
1591        }
1592        for (short, long) in [
1593            ("-A", "--allow"),
1594            ("-W", "--warn"),
1595            ("-D", "--deny"),
1596            ("-F", "--forbid"),
1597        ] {
1598            if let Some(attached) = value.strip_prefix(short) {
1599                let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1600                self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1601                return Ok(());
1602            }
1603        }
1604        if let Some(attached) = value.strip_prefix("-C") {
1605            let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1606            return self.parse_codegen(&option);
1607        }
1608        if let Some(attached) = value.strip_prefix("-L") {
1609            let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1610            let (kind, path) = search
1611                .split_once('=')
1612                .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1613            if !matches!(kind, "dependency" | "native") {
1614                return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1615            }
1616            self.parsed.push(Argument::SearchPath {
1617                kind: kind.into(),
1618                path: path.into(),
1619            });
1620            return Ok(());
1621        }
1622        if let Some(attached) = value.strip_prefix("-l") {
1623            let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1624            self.parsed.push(Argument::Plain(format!("-l{library}")));
1625            self.native_libraries.push(library);
1626            return Ok(());
1627        }
1628        if let Some(attached) = value.strip_prefix("-o") {
1629            let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1630            self.explicit_output = Some(path.clone().into());
1631            self.parsed.push(Argument::Path {
1632                flag: "-o".into(),
1633                path: path.into(),
1634            });
1635            return Ok(());
1636        }
1637        Err(BypassReason::UnknownFlag(value.into()))
1638    }
1639
1640    fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1641        let name = value.split_once('=').map_or(value, |(name, _)| name);
1642        if name == "incremental" {
1643            return Err(BypassReason::Incremental);
1644        }
1645        if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1646            return Err(BypassReason::UnknownCodegenOption(name.into()));
1647        }
1648        // Cargo compiles every dependency of an LTO build with this flag, so
1649        // the rlibs carry bitcode for the final link to optimize across. The
1650        // switch is keyed as text like any other option. A value naming the
1651        // linker plugin is a host file the key does not describe, so that
1652        // spelling still bypasses.
1653        if name == "linker-plugin-lto"
1654            && let Some((_, selection)) = value.split_once('=')
1655            && !matches!(
1656                selection,
1657                "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1658            )
1659        {
1660            return Err(BypassReason::UnknownCodegenOption(format!(
1661                "linker-plugin-lto={selection}"
1662            )));
1663        }
1664        // `-fuse-ld=<name>` selects the linker the driver invokes. Parsed
1665        // rather than keyed as text so the linker identity probe can pin the
1666        // selected linker; a repeated selection dedups, and a conflicting
1667        // second selection falls through to Plain, which native-link handling
1668        // declines to model. The driver honors the last selection, so a
1669        // conflict is a link whose linker this adapter cannot name.
1670        if name == "link-arg"
1671            && let Some((_, option)) = value.split_once('=')
1672            && let Some(selection) = option.strip_prefix("-fuse-ld=")
1673            && !selection.is_empty()
1674            && !selection.contains(',')
1675            && !self.parsed.iter().any(
1676                |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1677            )
1678        {
1679            if !self.parsed.iter().any(
1680                |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1681            ) {
1682                self.parsed.push(Argument::FuseLd(selection.to_owned()));
1683            }
1684            return Ok(());
1685        }
1686        // Only a single filename under @rpath is modeled. Other spellings,
1687        // combined linker options, and explicit absolute names still bypass.
1688        if name == "link-arg"
1689            && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1690            && safe_install_name(option)
1691        {
1692            self.parsed.push(Argument::InstallName(option.to_owned()));
1693            return Ok(());
1694        }
1695        // `-oso_prefix` names a checkout-specific path, so it is parsed
1696        // rather than keyed as text: the path normalizes like every other
1697        // path in the key, which is what lets two checkouts passing their own
1698        // prefixes agree on one action.
1699        if matches!(name, "link-arg" | "link-args")
1700            && let Some((_, option)) = value.split_once('=')
1701            && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1702            && !prefix.trim_end_matches('/').is_empty()
1703            && !prefix.contains(',')
1704        {
1705            let trailing_slash = prefix.ends_with('/');
1706            self.parsed.push(Argument::OsoPrefix {
1707                path: PathBuf::from(prefix.trim_end_matches('/')),
1708                trailing_slash,
1709            });
1710            return Ok(());
1711        }
1712        self.parsed
1713            .push(Argument::Plain(format!("--codegen={value}")));
1714        if name == "extra-filename" {
1715            self.extra_filename = value
1716                .split_once('=')
1717                .map_or(String::new(), |(_, value)| value.to_string());
1718        }
1719        Ok(())
1720    }
1721
1722    /// Model what rustc reads for each `-l`, now that the emit is known.
1723    ///
1724    /// A library emit runs no linker. For `dylib`, `framework`, `link-arg`,
1725    /// and an unspecified kind, rustc only records the name in the crate's
1726    /// metadata, so the flag text already in the key is the whole input. A
1727    /// `static` library is different: rustc opens the archive and bundles its
1728    /// members into the rlib, so the archive's content decides the output. It
1729    /// is found the way rustc finds it and named as a required input, keyed
1730    /// by content exactly like an `--extern` artifact. With `-bundle` rustc
1731    /// records the name for a downstream link instead of reading the archive,
1732    /// so that flag is text like the name-only kinds.
1733    ///
1734    /// A linked output hands every `-l` to its linker, whose search is not
1735    /// modeled here, so those still bypass whatever the kind.
1736    fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1737        for library in std::mem::take(&mut self.native_libraries) {
1738            if link_output != LinkOutput::Library {
1739                return Err(BypassReason::NativeLibrary(library));
1740            }
1741            // `-l [KIND[:MODIFIERS]=]NAME[:RENAME]`. Without a kind the
1742            // library is rustc's "unspecified" kind: it is handed to the
1743            // linker of whatever finally links, and an rlib only records the
1744            // name. rustc bundles into an rlib only what `-l static` names
1745            // (`link_rlib` takes `NativeLibKind::Static` with bundling on),
1746            // so a `libNAME.a` that later appears beside a plain `-l NAME`
1747            // changes that final link, not this compilation.
1748            let Some((kind, name)) = library.split_once('=') else {
1749                continue;
1750            };
1751            let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1752            match kind {
1753                "static" => {}
1754                "dylib" | "framework" | "link-arg" => continue,
1755                _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1756            }
1757            // A rename links RENAME in place of the NAME a `#[link]` attribute
1758            // gave, so RENAME is the archive on disk.
1759            let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1760            let (mut verbatim, mut bundle) = (false, true);
1761            for modifier in modifiers.split(',') {
1762                match modifier {
1763                    "+verbatim" => verbatim = true,
1764                    "-verbatim" => verbatim = false,
1765                    "+bundle" => bundle = true,
1766                    "-bundle" => bundle = false,
1767                    _ => {}
1768                }
1769            }
1770            if !bundle {
1771                continue;
1772            }
1773            let archive = self
1774                .find_native_static_library(name, verbatim)
1775                .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1776                .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1777            self.required_inputs.push(archive);
1778        }
1779        Ok(())
1780    }
1781
1782    /// Resolve a static library the way rustc does: the first `-L native`
1783    /// directory, in command-line order, holding the target's file name for
1784    /// it. rustc would also search a plain `-L` directory, but that kind
1785    /// already bypasses as unsupported before it gets here. Targets with
1786    /// MSVC-style libraries (Windows MSVC and UEFI) accept `NAME.lib` before
1787    /// falling back to the `libNAME.a` every built-in target otherwise uses;
1788    /// `+verbatim` names the file literally. A custom target specification
1789    /// sets its own prefix and suffix, which are not read here, so it is an
1790    /// `Err` rather than a guess that could hash a different archive than
1791    /// rustc bundles. A relative search directory resolves against the
1792    /// process working directory, as it does for rustc.
1793    fn find_native_static_library(
1794        &self,
1795        name: &str,
1796        verbatim: bool,
1797    ) -> Result<Option<PathBuf>, ()> {
1798        let file_names = if verbatim {
1799            vec![name.to_owned()]
1800        } else {
1801            match self.static_library_naming() {
1802                Some(StaticLibraryNaming::Msvc) => {
1803                    vec![format!("{name}.lib"), format!("lib{name}.a")]
1804                }
1805                Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1806                None => return Err(()),
1807            }
1808        };
1809        Ok(self
1810            .parsed
1811            .iter()
1812            .filter_map(|argument| match argument {
1813                Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1814                _ => None,
1815            })
1816            .flat_map(|directory| {
1817                file_names
1818                    .iter()
1819                    .map(move |file_name| directory.join(file_name))
1820            })
1821            .find(|candidate| candidate.is_file()))
1822    }
1823
1824    /// How the target names a static library, or `None` for a custom target
1825    /// specification, whose names only the specification knows.
1826    ///
1827    /// A bare name is a built-in target unless a specification file for it
1828    /// exists where rustc would look, under `RUST_TARGET_PATH` or in the
1829    /// sysroot, which the caller reports through
1830    /// [`ParseOptions::custom_target_search`].
1831    fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1832        let Some(target) = self.target.as_deref() else {
1833            return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1834                StaticLibraryNaming::Msvc
1835            } else {
1836                StaticLibraryNaming::Unix
1837            });
1838        };
1839        if target.ends_with(".json")
1840            || target.contains(['/', '\\'])
1841            || self.options.custom_target_search
1842        {
1843            return None;
1844        }
1845        // rustc's `is_like_msvc` targets: `*-windows-msvc`, `*-win7-windows-msvc`,
1846        // and the UEFI targets, which borrow the MSVC toolchain's conventions.
1847        Some(if target.contains("msvc") || target.ends_with("-uefi") {
1848            StaticLibraryNaming::Msvc
1849        } else {
1850            StaticLibraryNaming::Unix
1851        })
1852    }
1853
1854    fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1855        if value == "-" {
1856            return Err(BypassReason::StandardInput);
1857        }
1858        if self.source.replace(value.into()).is_some() {
1859            return Err(BypassReason::MultipleInputs);
1860        }
1861        Ok(())
1862    }
1863
1864    fn classify(&self) -> Result<LinkOutput, BypassReason> {
1865        // A compilation that emits no `link` produces exactly what an rlib's
1866        // metadata emit does -- dep-info and `lib<name>.rmeta` -- whatever it
1867        // calls its crate type. `cargo check` and clippy compile every binary
1868        // and test target this way, and those were bypassing as an
1869        // unsupported crate type for a linked artifact none of them asked
1870        // for. No linker runs, so nothing downstream needs a linker identity
1871        // to describe, and rustc names the metadata the same way for every
1872        // crate type.
1873        let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1874        let builds_a_library = !self.test
1875            && !self.crate_types.is_empty()
1876            && self
1877                .crate_types
1878                .iter()
1879                .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1880        let link_output = if never_links || builds_a_library {
1881            LinkOutput::Library
1882        } else if self
1883            .target
1884            .as_deref()
1885            .is_some_and(compiler_bundled_wasm_target)
1886            && ((self.test && self.crate_types.is_empty())
1887                || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1888        {
1889            if self.parsed.iter().any(|argument| match argument {
1890                Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1891                Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1892                    !matches!(
1893                        value.rsplit_once('=').map(|(_, value)| value),
1894                        Some("y" | "yes" | "on" | "true")
1895                    )
1896                }
1897                _ => false,
1898            }) {
1899                return Err(BypassReason::UnknownCodegenOption(
1900                    "link-self-contained".into(),
1901                ));
1902            }
1903            if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1904                && self.parsed.iter().any(|argument| {
1905                    matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1906                })
1907            {
1908                return Err(BypassReason::UnknownCodegenOption(
1909                    "target-feature=-crt-static".into(),
1910                ));
1911            }
1912            // These targets use a linker and, where applicable, CRT objects
1913            // and libc shipped in the Rust toolchain. Unlike native linking,
1914            // there are no implicit host inputs outside compiler identity.
1915            LinkOutput::WasmExecutable
1916        } else if self.options.cache_native_links && self.links_a_native_artifact() {
1917            self.check_native_link_is_portable()?;
1918            if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1919                LinkOutput::NativeProcMacro
1920            } else if matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib") {
1921                LinkOutput::NativeSharedLibrary
1922            } else {
1923                LinkOutput::NativeExecutable
1924            }
1925        } else if self.test {
1926            return Err(BypassReason::UnsupportedCrateType("test".into()));
1927        } else {
1928            return Err(BypassReason::UnsupportedCrateType(
1929                self.crate_types
1930                    .iter()
1931                    .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1932                    .cloned()
1933                    .unwrap_or_else(|| "bin".into()),
1934            ));
1935        };
1936        // A link argument is handed to the linker verbatim, and its text is all
1937        // the adapter has to go on. `-Clink-arg=-Tlink.x` names a linker script
1938        // resolved off the search path, `-Clink-arg=-fuse-ld=lld` replaces the
1939        // linker the identity in the key describes, and neither is told apart
1940        // from `/STACK:8000000` by any rule short of knowing the linker's own
1941        // grammar. Where nothing links there is nothing to tell apart: an rlib
1942        // or an rmeta is produced without a linker invocation, so the option is
1943        // inert and the key carries its text like any other codegen option.
1944        if link_output != LinkOutput::Library
1945            && let Some(option) = self.first_link_argument(link_output)
1946        {
1947            return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1948        }
1949        if self
1950            .parsed
1951            .iter()
1952            .any(|arg| matches!(arg, Argument::InstallName(_)))
1953            && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
1954        {
1955            return Err(BypassReason::UnmodeledLinkArgument(
1956                "link-arg=-Wl,-install_name".into(),
1957            ));
1958        }
1959        // `-fuse-ld` is modeled only for native links, where the linker
1960        // identity probe resolves and pins the linker it names. A wasm or
1961        // other non-native link consuming the selection has no identity that
1962        // describes it, so it declines the action like any other link
1963        // argument. Where nothing links, the selection is inert and the key
1964        // carries its text.
1965        if let Some(selection) = self.parsed.iter().find_map(|argument| {
1966            let Argument::FuseLd(selection) = argument else {
1967                return None;
1968            };
1969            Some(selection.as_str())
1970        }) && !matches!(
1971            link_output,
1972            LinkOutput::Library
1973                | LinkOutput::NativeExecutable
1974                | LinkOutput::NativeProcMacro
1975                | LinkOutput::NativeSharedLibrary
1976        ) {
1977            return Err(BypassReason::UnmodeledLinkArgument(format!(
1978                "link-arg=-fuse-ld={selection}"
1979            )));
1980        }
1981        if self.parsed.iter().any(|argument| {
1982            matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1983        }) && !matches!(
1984            link_output,
1985            LinkOutput::NativeExecutable
1986                | LinkOutput::NativeProcMacro
1987                | LinkOutput::NativeSharedLibrary
1988        ) {
1989            return Err(BypassReason::UnknownCodegenOption("linker".into()));
1990        }
1991        // `-oso_prefix` is modeled, but only where ld64 is the linker reading
1992        // it: a native macOS link. Any other linker sees an option this
1993        // adapter cannot vouch for, exactly like the arguments above.
1994        if !matches!(
1995            link_output,
1996            LinkOutput::Library
1997                | LinkOutput::NativeExecutable
1998                | LinkOutput::NativeProcMacro
1999                | LinkOutput::NativeSharedLibrary
2000        ) && self
2001            .parsed
2002            .iter()
2003            .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
2004        {
2005            return Err(BypassReason::UnmodeledLinkArgument(
2006                "link-arg=-Wl,-oso_prefix".into(),
2007            ));
2008        }
2009        if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
2010            Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
2011            _ => None,
2012        }) {
2013            return Err(BypassReason::UnresolvedExtern(name.clone()));
2014        }
2015        if let Some(emit) = self
2016            .emits
2017            .iter()
2018            .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
2019        {
2020            return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
2021        }
2022        if !self
2023            .emits
2024            .iter()
2025            .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
2026        {
2027            return Err(BypassReason::NoCacheableOutput);
2028        }
2029        Ok(link_output)
2030    }
2031}
2032
2033impl Parser<'_> {
2034    /// The first `-C link-arg` or `-C link-args` option in the invocation,
2035    /// rendered as it appears in the action key.
2036    fn first_link_argument(&self, link_output: LinkOutput) -> Option<&str> {
2037        self.parsed.iter().find_map(|argument| {
2038            let Argument::Plain(value) = argument else {
2039                return None;
2040            };
2041            let option = value.strip_prefix("--codegen=")?;
2042            let (name, value) = option.split_once('=').unwrap_or((option, ""));
2043            (matches!(name, "link-arg" | "link-args")
2044                && !(name == "link-arg" && elf_keyword_is_modeled(link_output, value))
2045                && !msvc_options_are_modeled(link_output, name, value))
2046            .then_some(option)
2047        })
2048    }
2049
2050    /// Whether an `-oso_prefix` strips the directory this link's objects live
2051    /// in from the debug map ld64 records.
2052    ///
2053    /// The objects a native link reads -- this unit's own compiled objects
2054    /// and the rlibs it links -- sit in the output directory, so a prefix
2055    /// covering that directory leaves the debug map naming spellings every
2056    /// checkout shares. Toolchain objects stay absolute, as they do in the
2057    /// paths rustc itself embeds on every platform.
2058    fn oso_prefix_covers_outputs(&self) -> bool {
2059        let Some(directory) = self
2060            .out_dir
2061            .as_deref()
2062            .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2063        else {
2064            return false;
2065        };
2066        if !directory.is_absolute() {
2067            return false;
2068        }
2069        let directory = normalize_components(directory);
2070        self.parsed.iter().any(|argument| {
2071            let Argument::OsoPrefix { path, .. } = argument else {
2072                return false;
2073            };
2074            path.is_absolute()
2075                && (directory.starts_with(normalize_components(path))
2076                    || std::fs::canonicalize(&directory)
2077                        .ok()
2078                        .zip(std::fs::canonicalize(path).ok())
2079                        .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2080        })
2081    }
2082
2083    /// Whether this invocation links an artifact for the host.
2084    ///
2085    /// An explicit `--target` bypasses even when it spells the host triple:
2086    /// rustc without one links for the host by construction, which is the
2087    /// cheapest description of "the linker this adapter can identify", and
2088    /// cargo omits it for host builds anyway.
2089    fn links_a_native_artifact(&self) -> bool {
2090        self.target.is_none()
2091            // A compilation that emits no linked artifact did not link, so it
2092            // needs no linker to describe it. `cargo check --tests` asks for
2093            // metadata alone, and reading that as a link would send it looking
2094            // for a linker identity and refusing flags no linker ever saw.
2095            && self.emits.iter().any(|emit| emit.kind == "link")
2096            && ((self.test && self.crate_types.is_empty())
2097                || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro"))
2098                || (cfg!(target_os = "linux")
2099                    && matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib")))
2100    }
2101
2102    /// Reject a native link whose result depends on something the key cannot
2103    /// describe, or that leaves artifacts beside the ones mbx would store.
2104    ///
2105    /// Each check names a value rather than a flag: an option absent from the
2106    /// invocation keeps rustc's default, which the compiler identity already
2107    /// pins, while any other spelling is refused rather than guessed at.
2108    fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2109        for argument in &self.parsed {
2110            let Argument::Plain(value) = argument else {
2111                continue;
2112            };
2113            // `-g` is rustc's shorthand for debug info, and arrives as itself
2114            // rather than as a codegen option.
2115            let (name, value) = if value == "-g" {
2116                ("debuginfo", Some("2"))
2117            } else if let Some(option) = value.strip_prefix("--codegen=") {
2118                match option.split_once('=') {
2119                    Some((name, value)) => (name, Some(value)),
2120                    // A codegen flag with no value asks for its enabled form,
2121                    // which is what cargo passes for `-Crpath`. Reading it as
2122                    // "nothing to check" is how these slipped through.
2123                    None => (option, None),
2124                }
2125            } else {
2126                continue;
2127            };
2128            let unportable = match name {
2129                // Packed debug info leaves a .dSYM bundle or .dwp file beside
2130                // the binary, and mbx stores neither. Unpacked is macOS's
2131                // debug-map arrangement -- debug info stays in the objects and
2132                // nothing lands beside the binary -- so the same covering
2133                // `-oso_prefix` that makes the debug map portable covers it.
2134                "split-debuginfo" => match value {
2135                    Some("off") => false,
2136                    Some("unpacked") if cfg!(target_os = "macos") => {
2137                        !self.oso_prefix_covers_outputs()
2138                    }
2139                    _ => true,
2140                },
2141                // ld64 records absolute object paths and their timestamps in
2142                // the binary's debug map, so the same source links to
2143                // different bytes in another checkout -- or the same one
2144                // twice. An `-oso_prefix` covering the output directory
2145                // strips those paths down to spellings every checkout
2146                // shares, which is the same leniency a Linux link already
2147                // gets for the paths rustc itself embeds; what remains
2148                // (object timestamps) only shows up under verification.
2149                "debuginfo" if cfg!(target_os = "macos") => {
2150                    !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2151                }
2152                // An rpath and a dynamically linked native program embed this
2153                // checkout's absolute target directory. Cargo also passes
2154                // `prefer-dynamic` to proc macros, but their dynamic runtime
2155                // is rustc's own sysroot (already pinned by compiler identity),
2156                // while Cargo's dependencies are linked into the macro. There
2157                // is no checkout rpath to carry; keep rejecting an explicit
2158                // `rpath` independently.
2159                "rpath" => is_enabled(value),
2160                "prefer-dynamic" => {
2161                    is_enabled(value)
2162                        && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2163                }
2164                // The CRT objects a self-contained link uses come from
2165                // somewhere other than where the driver reports.
2166                "link-self-contained" => true,
2167                _ => false,
2168            };
2169            if unportable {
2170                return Err(BypassReason::UnportableNativeLink(match value {
2171                    Some(value) => format!("{name}={value}"),
2172                    None => name.to_owned(),
2173                }));
2174            }
2175        }
2176        Ok(())
2177    }
2178}
2179
2180fn safe_install_name(name: &str) -> bool {
2181    !name.is_empty()
2182        && name
2183            .bytes()
2184            .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2185}
2186
2187/// Whether a boolean codegen option is asking for its enabled form. Absent a
2188/// value, rustc reads the flag itself as the request.
2189fn is_enabled(value: Option<&str>) -> bool {
2190    matches!(value, None | Some("y" | "yes" | "on" | "true"))
2191}
2192
2193/// Whether `-Wl,-z,KEYWORD` is a native Linux link option the key can carry
2194/// as text.
2195///
2196/// A link argument is refused because its text cannot say whether it names a
2197/// file. These keywords are the exception: each sets a flag or a dynamic
2198/// section entry in the ELF output and reads nothing, in every linker the
2199/// identity probe resolves (GNU ld, gold, lld, and mold all spell them the
2200/// same). napi-build passes `-z nodelete` to every Node-API addon, so without
2201/// it no such addon caches.
2202fn elf_keyword_is_modeled(link_output: LinkOutput, value: &str) -> bool {
2203    cfg!(target_os = "linux")
2204        && matches!(
2205            link_output,
2206            LinkOutput::NativeExecutable
2207                | LinkOutput::NativeProcMacro
2208                | LinkOutput::NativeSharedLibrary
2209        )
2210        && value.strip_prefix("-Wl,-z,").is_some_and(|keyword| {
2211            matches!(
2212                keyword,
2213                "defs"
2214                    | "lazy"
2215                    | "nodelete"
2216                    | "nodlopen"
2217                    | "noexecstack"
2218                    | "norelro"
2219                    | "now"
2220                    | "origin"
2221                    | "relro"
2222            )
2223        })
2224}
2225
2226/// `-C link-arg` / `-C link-args` values that `link.exe` and `lld-link` read
2227/// as a plain switch or number and never as a file name.
2228///
2229/// `-C link-arg=/STACK:8000000` is how projects raise the main thread's stack
2230/// for Windows, usually through a `rustflags` entry in `.cargo/config.toml`
2231/// that reaches every proc macro, build script, and binary the workspace
2232/// builds. Refusing it leaves each of those linking on every build, and a MSVC
2233/// link is not reproducible (the PE header and PDB carry a timestamp and a
2234/// GUID), so each fresh proc-macro DLL hashes differently and every crate
2235/// compiled against it misses too.
2236///
2237/// Only spellings whose grammar is closed are listed: a number after
2238/// `/STACK:`, and `/Brepro`, which makes the link reproducible. Anything else
2239/// (`/DEF:`, `/MANIFESTINPUT:`, `/LIBPATH:`, a bare file name) can name a file
2240/// the key never hashes, so it still bypasses.
2241fn msvc_options_are_modeled(link_output: LinkOutput, name: &str, value: &str) -> bool {
2242    cfg!(target_env = "msvc")
2243        && matches!(
2244            link_output,
2245            LinkOutput::NativeExecutable
2246                | LinkOutput::NativeProcMacro
2247                | LinkOutput::NativeSharedLibrary
2248        )
2249        && match name {
2250            "link-arg" => msvc_option_reads_no_file(value),
2251            // The plural spelling is a whitespace-separated list, each entry
2252            // of which has to qualify.
2253            "link-args" => {
2254                let mut options = value.split_ascii_whitespace().peekable();
2255                options.peek().is_some() && options.all(msvc_option_reads_no_file)
2256            }
2257            _ => false,
2258        }
2259}
2260
2261/// One MSVC linker option from the closed list in [`msvc_options_are_modeled`].
2262/// The linker takes `-` for `/` and ignores the case of option names, and so
2263/// does this.
2264fn msvc_option_reads_no_file(option: &str) -> bool {
2265    let Some(option) = option.strip_prefix(['/', '-']) else {
2266        return false;
2267    };
2268    if option.eq_ignore_ascii_case("brepro") {
2269        return true;
2270    }
2271    let Some((name, sizes)) = option.split_once(':') else {
2272        return false;
2273    };
2274    // `/STACK:reserve[,commit]`. Sizes are decimal or `0x` hexadecimal, and
2275    // nothing else, so a value cannot smuggle in a path.
2276    let is_size = |size: &str| match size.strip_prefix("0x").or_else(|| size.strip_prefix("0X")) {
2277        Some(hex) => !hex.is_empty() && hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
2278        None => !size.is_empty() && size.bytes().all(|byte| byte.is_ascii_digit()),
2279    };
2280    name.eq_ignore_ascii_case("stack")
2281        && sizes.split(',').count() <= 2
2282        && sizes.split(',').all(is_size)
2283}
2284
2285fn compiler_bundled_wasm_target(target: &str) -> bool {
2286    COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2287}
2288
2289fn parse_emits(value: &str) -> Vec<Emit> {
2290    value
2291        .split(',')
2292        .map(|emit| {
2293            let (kind, path) = emit
2294                .split_once('=')
2295                .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2296            Emit {
2297                kind: kind.into(),
2298                path,
2299            }
2300        })
2301        .collect()
2302}
2303
2304struct ActionBuilder<'a> {
2305    invocation: &'a RustcInvocation,
2306    context: ActionContext,
2307    mappings: Vec<SharedPathMapping>,
2308    /// The directories resolved while building this one action. Its inputs
2309    /// share a few parents, and resolving each once is what keeps a crate
2310    /// with hundreds of them from paying a `realpath` walk per input.
2311    aliases: PathAliases,
2312    linker: Option<LinkerIdentity>,
2313}
2314
2315impl<'a> ActionBuilder<'a> {
2316    fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2317        context.path_mappings = PathMapping::ordered(&context.path_mappings);
2318        let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2319        Self {
2320            linker: None,
2321            invocation,
2322            mappings,
2323            aliases: PathAliases::default(),
2324            context,
2325        }
2326    }
2327
2328    fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2329        self.linker = linker;
2330        self
2331    }
2332
2333    fn build(self) -> Result<RustcAction, BypassReason> {
2334        self.validate_mappings()?;
2335        let invocation = self.invocation_descriptor()?;
2336        let environment = self.environment_descriptor()?;
2337
2338        let mut inputs = BTreeMap::<String, CacheDigest>::new();
2339        for input in &self.context.inputs {
2340            input
2341                .digest
2342                .validate()
2343                .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2344            let path = self.normalize_input_path(&input.path)?;
2345            if inputs
2346                .insert(path.clone(), input.digest.clone())
2347                .is_some_and(|existing| existing != input.digest)
2348            {
2349                return Err(BypassReason::ConflictingInput(path));
2350            }
2351        }
2352        let required = self
2353            .invocation
2354            .required_inputs
2355            .iter()
2356            .map(|path| self.normalize_input_path(path))
2357            .collect::<Result<BTreeSet<_>, _>>()?;
2358        if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2359            return Err(BypassReason::MissingRequiredInput(missing.clone()));
2360        }
2361        let inputs = inputs
2362            .into_iter()
2363            .map(|(path, digest)| InputDescriptor { path, digest })
2364            .collect();
2365        // A native link without a linker identity would be keyed as though the
2366        // host did not matter. Refuse rather than publish that claim.
2367        if self.invocation.links_natively() && self.linker.is_none() {
2368            return Err(BypassReason::UnportableNativeLink(
2369                "linker identity is unknown".into(),
2370            ));
2371        }
2372        let descriptor = ActionDescriptor {
2373            version: ACTION_SCHEMA_VERSION,
2374            kind: "rustc",
2375            adapter_version: ADAPTER_VERSION,
2376            compiler: invocation.compiler,
2377            arguments: invocation.arguments,
2378            environment,
2379            inputs,
2380            linker: self.linker.clone(),
2381        };
2382        let bytes = canonical_json(&descriptor)
2383            .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2384        let digest = CacheDigest::blake3(&bytes);
2385        Ok(RustcAction { digest, bytes })
2386    }
2387
2388    fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2389        self.validate_mappings()?;
2390        let arguments = self
2391            .invocation
2392            .arguments
2393            .iter()
2394            .map(|argument| self.normalize_argument(argument))
2395            .collect::<Result<Vec<_>, _>>()?;
2396        let required_inputs = self
2397            .invocation
2398            .required_inputs
2399            .iter()
2400            .map(|path| self.normalize_path(path))
2401            .collect::<Result<BTreeSet<_>, _>>()?
2402            .into_iter()
2403            .collect();
2404        Ok(InvocationDescriptor {
2405            version: ACTION_SCHEMA_VERSION,
2406            kind: "rustc",
2407            adapter_version: ADAPTER_VERSION,
2408            compiler: CompilerDescriptor {
2409                toolchain: self.context.compiler.toolchain.clone(),
2410                rustc_version: self.context.compiler.rustc_version.clone(),
2411                host: self.context.compiler.host.clone(),
2412                driver: self.context.compiler.driver.clone(),
2413            },
2414            arguments,
2415            required_inputs,
2416        })
2417    }
2418
2419    fn validate_mappings(&self) -> Result<(), BypassReason> {
2420        if !self.context.working_dir.is_absolute() {
2421            return Err(BypassReason::RelativeWorkingDirectory(
2422                self.context.working_dir.clone(),
2423            ));
2424        }
2425        let mut roots = BTreeSet::new();
2426        let mut placeholders = BTreeSet::new();
2427        for mapping in &self.mappings {
2428            if !mapping.root.is_absolute() {
2429                return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2430            }
2431            if mapping.placeholder.is_empty()
2432                || !mapping
2433                    .placeholder
2434                    .bytes()
2435                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2436                || !roots.insert(normalize_components(&mapping.root))
2437                || !placeholders.insert(&mapping.placeholder)
2438            {
2439                return Err(BypassReason::InvalidPathPlaceholder(
2440                    mapping.placeholder.clone(),
2441                ));
2442            }
2443        }
2444        Ok(())
2445    }
2446
2447    fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2448        match argument {
2449            Argument::Plain(value) => Ok(value.clone()),
2450            Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2451            Argument::SearchPath { kind, path } => {
2452                let text = self.normalize_path(path)?;
2453                Ok(format!("-L{kind}={text}"))
2454            }
2455            Argument::Extern { name, path } => match path {
2456                Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2457                None => Ok(format!("--extern={name}")),
2458            },
2459            Argument::Emit(emits) => Ok(format!(
2460                "--emit={}",
2461                emits
2462                    .iter()
2463                    .map(|emit| match &emit.path {
2464                        Some(path) => self
2465                            .normalize_path(path)
2466                            .map(|path| format!("{}={path}", emit.kind)),
2467                        None => Ok(emit.kind.clone()),
2468                    })
2469                    .collect::<Result<Vec<_>, _>>()?
2470                    .join(",")
2471            )),
2472            Argument::RemapPath { from, to } => Ok(format!(
2473                "--remap-path-prefix={}={}",
2474                self.normalize_path(from)?,
2475                to
2476            )),
2477            Argument::InstallName(name) => Ok(format!(
2478                "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2479            )),
2480            Argument::OsoPrefix {
2481                path,
2482                trailing_slash,
2483            } => Ok(format!(
2484                "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2485                self.normalize_path(path)?,
2486                if *trailing_slash { "/" } else { "" }
2487            )),
2488            Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2489        }
2490    }
2491
2492    /// Environment values enter the key verbatim, because rustc may embed one
2493    /// through `env!`: unlike a path used to locate an input, changing the value
2494    /// changes the artifact.
2495    ///
2496    /// A name in `portable_environment` is the exception the caller has earned.
2497    /// Its value normalizes like any other path, so two checkouts agree on it.
2498    fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2499        self.context
2500            .environment
2501            .iter()
2502            .map(|(name, value)| {
2503                let value = match value {
2504                    Some(value) if self.context.portable_environment.contains(name) => {
2505                        Some(self.normalize_path(Path::new(value))?)
2506                    }
2507                    value => value.clone(),
2508                };
2509                Ok((name.clone(), value))
2510            })
2511            .collect()
2512    }
2513
2514    fn normalize_input_path(&self, path: &Path) -> Result<String, BypassReason> {
2515        let absolute = normalize_components(&self.context.working_dir.join(path));
2516        for directory in self.invocation.native_search_paths() {
2517            let directory = normalize_components(&self.context.working_dir.join(directory));
2518            if let Ok(relative) = absolute.strip_prefix(&directory) {
2519                // Preserve the searched filename. Resolving a symlink here
2520                // would collapse libssl.dylib into its versioned referent, so
2521                // retargeting that link between two existing files could leave
2522                // the input map unchanged. Discovery rejects directory links
2523                // inside the tree and hashes regular-file links by content.
2524                let suffix = relative
2525                    .components()
2526                    .map(|component| {
2527                        component
2528                            .as_os_str()
2529                            .to_str()
2530                            .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf()))
2531                    })
2532                    .collect::<Result<Vec<_>, _>>()?
2533                    .join("/");
2534                if !suffix.is_empty() {
2535                    return Ok(format!("{}/{}", self.normalize_path(&directory)?, suffix));
2536                }
2537            }
2538        }
2539        self.normalize_path(path)
2540    }
2541
2542    fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2543        normalize_resolved_shared_path_with(
2544            &self.aliases,
2545            path,
2546            &self.context.working_dir,
2547            &self.mappings,
2548        )
2549        .map_err(Into::into)
2550    }
2551}
2552
2553/// Whether a normalized path names something strictly beneath one of
2554/// `directories`, which are normalized the same way and so share its `/`
2555/// separator regardless of platform.
2556fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2557    directories.iter().any(|directory| {
2558        path.len() > directory.len()
2559            && path.as_bytes()[directory.len()] == b'/'
2560            && path.starts_with(directory)
2561    })
2562}
2563
2564fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2565    for mapping in mappings {
2566        let prefix = format!("${{{}}}", mapping.placeholder);
2567        let suffix = if value == prefix {
2568            ""
2569        } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2570            suffix
2571        } else {
2572            continue;
2573        };
2574        if !mapping.root.is_absolute()
2575            || (!suffix.is_empty()
2576                && suffix.split('/').any(|component| {
2577                    component.is_empty()
2578                        || matches!(component, "." | "..")
2579                        || component.contains('\\')
2580                }))
2581        {
2582            return Err(BypassReason::InvalidPredictedInput(value.into()));
2583        }
2584        let mut path = normalize_components(&mapping.root);
2585        path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2586        return Ok(path);
2587    }
2588    Err(BypassReason::InvalidPredictedInput(value.into()))
2589}
2590
2591fn normalize_components(path: &Path) -> PathBuf {
2592    let mut normalized = PathBuf::new();
2593    for component in path.components() {
2594        match component {
2595            Component::CurDir => {}
2596            Component::ParentDir => {
2597                normalized.pop();
2598            }
2599            component => normalized.push(component.as_os_str()),
2600        }
2601    }
2602    normalized
2603}
2604
2605fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2606    if path.is_absolute() {
2607        normalize_components(path)
2608    } else {
2609        normalize_components(&working_dir.join(path))
2610    }
2611}
2612
2613#[cfg(test)]
2614#[path = "rustc_cache_tests.rs"]
2615mod tests;