Skip to main content

stow_types/
rustc.rs

1//! Parser for the rustc command line the wrapper observes, plus helpers that
2//! decide cacheability and predict output paths from the parsed arguments.
3
4use std::collections::BTreeSet;
5use std::ffi::OsString;
6use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9use target_lexicon::{BinaryFormat, Triple};
10
11use crate::platform::{PanicStrategy, Profile, StripLevel};
12
13/// One `--extern name=path` pair from a rustc invocation.
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15pub struct ParsedExternCrate {
16    /// Crate name as passed to `--extern` (rustc form, underscores).
17    pub crate_name: String,
18    /// Path to the dependency's rlib or rmeta.
19    pub path: PathBuf,
20}
21
22/// The subset of a rustc command line that determines cache identity and
23/// output layout.
24///
25/// Populated by [`ParsedRustcArgs::parse`]; fields are `Option` where the
26/// corresponding flag may be absent.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ParsedRustcArgs {
29    /// `--crate-name` value; a parse without one fails.
30    pub crate_name: String,
31    /// `--crate-type` list, split on `,`.
32    pub crate_types: Vec<String>,
33    /// `feature="…"` values collected from `--cfg` flags.
34    pub features: BTreeSet<String>,
35    /// Every other `--cfg` value: build-script `cargo:rustc-cfg` output and
36    /// `--cfg` flags from `RUSTFLAGS`. They select code at compile time, so
37    /// they are compile identity, kept separate from features because the
38    /// semantic tuple registries index on is features only.
39    pub cfgs: BTreeSet<String>,
40    /// `--emit` kinds, deduplicated.
41    pub emit: BTreeSet<String>,
42    /// `--json` kinds, deduplicated.
43    pub json: BTreeSet<String>,
44    /// The `.rs` input file, when one was passed.
45    pub input_path: Option<PathBuf>,
46    /// `--target` triple, or `None` for an implicit host target.
47    pub target: Option<String>,
48    /// `-C metadata` value.
49    pub c_metadata: Option<String>,
50    /// `--out-dir` directory.
51    pub out_dir: Option<PathBuf>,
52    /// `-C extra-filename` suffix (empty when absent).
53    pub extra_filename: String,
54    /// `-C opt-level` value.
55    pub opt_level: Option<String>,
56    /// `-C debuginfo` value.
57    pub debuginfo: Option<String>,
58    /// `-C panic` value.
59    pub panic_strategy: Option<String>,
60    /// `-C debug-assertions` value.
61    pub debug_assertions: Option<bool>,
62    /// `-C overflow-checks` value.
63    pub overflow_checks: Option<bool>,
64    /// `-C strip` value.
65    pub strip: Option<String>,
66    /// `-L native=…` search paths.
67    pub native_search_paths: Vec<PathBuf>,
68    /// `--extern` pairs, sorted by crate name then path.
69    pub extern_crates: Vec<ParsedExternCrate>,
70    /// `-Z embed-metadata` value. Nightly cargo passes this flag on every
71    /// unit, so it is toolchain identity rather than custom codegen; it
72    /// changes the produced rlib, so it participates in the compile key.
73    pub embed_metadata: Option<bool>,
74    /// Whether the produced object files carry LLVM bitcode. rustc embeds
75    /// bitcode unless `-C embed-bitcode=no`, the value cargo passes to every
76    /// unit that no LTO consumer needs bitcode from; a unit compiled with
77    /// bitcode is a different artifact, so this participates in the compile
78    /// key.
79    pub embed_bitcode: bool,
80    /// Whether the invocation (or `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS`)
81    /// carries codegen flags stow does not model; such builds are never
82    /// served from the public cache.
83    pub has_custom_codegen: bool,
84    /// The `-C` options that steer only the final link step, normalized to
85    /// their `key=value` spelling and sorted. Inert for a unit that never
86    /// reaches the linker, and part of the compile key for one that does —
87    /// see [`ParsedRustcArgs::link_options_reaching_the_linker`]. An
88    /// artifact linked with mold and one linked with lld are different
89    /// bytes, so they are different keys rather than both being refused.
90    pub link_options: BTreeSet<String>,
91}
92
93impl ParsedRustcArgs {
94    /// Parse a captured rustc argv into `ParsedRustcArgs`.
95    ///
96    /// Recognizes the flags cargo passes for dependency compilations. Unknown
97    /// `-C` / `-Z` options set `has_custom_codegen` rather than failing, so
98    /// the invocation is still parsed — it just will not be cacheable.
99    ///
100    /// # Errors
101    /// Returns an error when an argument is not valid UTF-8, a flag that
102    /// requires a value is the last argument, an `--extern` pair or codegen
103    /// boolean is malformed, or `--crate-name` is missing.
104    pub fn parse(args: &[OsString]) -> Result<Self, String> {
105        let env_effect = env_rustflags_effect();
106        let mut parsed = Self {
107            crate_name: String::new(),
108            crate_types: Vec::new(),
109            features: BTreeSet::new(),
110            cfgs: BTreeSet::new(),
111            emit: BTreeSet::new(),
112            json: BTreeSet::new(),
113            input_path: None,
114            target: None,
115            c_metadata: None,
116            out_dir: None,
117            extra_filename: String::new(),
118            opt_level: None,
119            debuginfo: None,
120            panic_strategy: None,
121            debug_assertions: None,
122            overflow_checks: None,
123            strip: None,
124            native_search_paths: Vec::new(),
125            extern_crates: Vec::new(),
126            embed_metadata: None,
127            embed_bitcode: true,
128            has_custom_codegen: env_effect.custom_codegen,
129            link_options: env_effect.link_options,
130        };
131
132        let mut iter = args.iter();
133        while let Some(arg) = iter.next() {
134            let Some(arg) = arg.to_str() else {
135                return Err(format!(
136                    "rustc argument is not valid UTF-8: {}",
137                    arg.display()
138                ));
139            };
140            apply_rustc_arg(arg, &mut iter, &mut parsed)?;
141        }
142
143        if parsed.crate_name.is_empty() {
144            return Err("missing --crate-name in rustc arguments".to_owned());
145        }
146
147        Ok(parsed)
148    }
149
150    /// Whether this invocation may be served from the public cache.
151    ///
152    /// Requires a restorable artifact, no custom codegen flags, and no
153    /// `CARGO_PRIMARY_PACKAGE` (workspace crates are never cached). The
154    /// profile is not a restriction: `opt-level`, `debuginfo`, assertions,
155    /// `panic` and `strip` are all part of the compile identity, so a unit
156    /// built under any profile is served exactly when the pool holds an
157    /// artifact built under the same one.
158    #[must_use]
159    pub fn is_cacheable(&self) -> bool {
160        if !self.is_restorable_artifact() {
161            return false;
162        }
163        if self.has_custom_codegen {
164            return false;
165        }
166        std::env::var_os("CARGO_PRIMARY_PACKAGE").is_none()
167    }
168
169    /// The link-only `-C` options that actually reach a link step here,
170    /// which is what the compile key has to carry.
171    ///
172    /// The cache stores two shapes (see [`Self::is_restorable_artifact`]):
173    /// rlibs, which rustc never links, and dynamic libraries, which it
174    /// does. For an rlib the options are inert — nothing in the archive
175    /// depends on which linker would have been invoked — and that is where
176    /// essentially all of a dependency graph lives, so an rlib's key is
177    /// exactly what it was before link options were modeled.
178    ///
179    /// For a dynamic library they are not inert. `-C link-arg` carries
180    /// arbitrary text: `-L/opt/custom/lib` changes which library is linked,
181    /// `-Wl,-rpath=` changes what is found at run time, `-lfoo` links in
182    /// something else entirely, and `link-self-contained` swaps the bundled
183    /// runtime for the system one. Two such units are different artifacts,
184    /// so they take different keys — serving one for the other would hand
185    /// over a different program, and refusing to cache either would cost
186    /// the cache every proc-macro in a build that chose its own linker.
187    #[must_use]
188    pub fn link_options_reaching_the_linker(&self) -> Vec<String> {
189        if self.invokes_the_linker() {
190            self.link_options.iter().cloned().collect()
191        } else {
192            Vec::new()
193        }
194    }
195
196    /// Whether any `--crate-type` makes rustc run the linker. `lib`/`rlib`
197    /// do not (rustc writes an archive of the crate's own objects and defers
198    /// linking to whatever consumes it), and neither does `staticlib`; every
199    /// other output is a linked image. Cargo compiles all of a lib target's
200    /// declared crate types in one invocation, so a dependency declaring
201    /// `crate-type = ["lib", "cdylib"]` links in the same rustc run that
202    /// produces the cacheable rlib.
203    #[must_use]
204    fn invokes_the_linker(&self) -> bool {
205        self.crate_types
206            .iter()
207            .any(|kind| matches!(kind.as_str(), "proc-macro" | "dylib" | "cdylib" | "bin"))
208    }
209
210    /// Whether this invocation's outputs may be stored in the local artifact
211    /// cache after a successful build.
212    ///
213    /// The gate is deliberately narrower than [`Self::is_cacheable`] in one
214    /// direction and wider in another: the artifacts are self-produced, so
215    /// there is no profile restriction — dev *and* release outputs are worth
216    /// keeping because cross-worktree release rebuilds are exactly where a
217    /// local cache pays. What remains mandatory is a restorable artifact
218    /// shape, no custom codegen flags, and not being the workspace's primary
219    /// package (`CARGO_PRIMARY_PACKAGE`): first-party code is never cached.
220    #[must_use]
221    pub fn is_locally_cacheable(&self) -> bool {
222        self.is_restorable_artifact()
223            && !self.has_custom_codegen
224            && std::env::var_os("CARGO_PRIMARY_PACKAGE").is_none()
225    }
226
227    /// Whether the invocation produces an artifact stow can restore: an rlib
228    /// or dynamic library with both `-C metadata` and `--out-dir` present.
229    #[must_use]
230    pub fn is_restorable_artifact(&self) -> bool {
231        (self.produces_rlib() || self.produces_dynamic_library())
232            && self.c_metadata.is_some()
233            && self.out_dir.is_some()
234    }
235
236    /// Whether any `--crate-type` is `proc-macro`.
237    #[must_use]
238    pub fn is_proc_macro(&self) -> bool {
239        self.crate_types.iter().any(|kind| kind == "proc-macro")
240    }
241
242    /// Whether any `--crate-type` is `lib` or `rlib`.
243    #[must_use]
244    pub fn produces_rlib(&self) -> bool {
245        self.crate_types
246            .iter()
247            .any(|kind| kind == "lib" || kind == "rlib")
248    }
249
250    /// Whether any `--crate-type` is `proc-macro` or `dylib`.
251    #[must_use]
252    pub fn produces_dynamic_library(&self) -> bool {
253        self.crate_types
254            .iter()
255            .any(|kind| kind == "proc-macro" || kind == "dylib")
256    }
257
258    /// Whether any `--crate-type` is `bin`.
259    #[must_use]
260    pub fn is_binary(&self) -> bool {
261        self.crate_types.iter().any(|kind| kind == "bin")
262    }
263
264    /// Whether this is the `build_script_build` binary cargo compiles for
265    /// build scripts.
266    #[must_use]
267    pub fn is_build_script(&self) -> bool {
268        self.is_binary() && self.crate_name == "build_script_build"
269    }
270
271    /// Whether `--json` includes `artifacts` (cargo's artifact-notification
272    /// channel the capture wrapper relies on).
273    #[must_use]
274    pub fn requests_json_artifact_notifications(&self) -> bool {
275        self.json.contains("artifacts")
276    }
277
278    /// Path of the emitted `.rlib` under `--out-dir`
279    /// (`lib<name><extra-filename>.rlib`), or `None` when the invocation does
280    /// not produce an rlib or has no `--out-dir`.
281    #[must_use]
282    pub fn output_rlib_path(&self) -> Option<PathBuf> {
283        let out_dir = self.out_dir.as_ref()?;
284        if !self.produces_rlib() {
285            return None;
286        }
287        Some(out_dir.join(format!(
288            "lib{}{}.rlib",
289            self.crate_name, self.extra_filename
290        )))
291    }
292
293    /// Path of the emitted `.rmeta` under `--out-dir`
294    /// (`lib<name><extra-filename>.rmeta`), or `None` without `--out-dir`.
295    #[must_use]
296    pub fn output_rmeta_path(&self) -> Option<PathBuf> {
297        let out_dir = self.out_dir.as_ref()?;
298        Some(out_dir.join(format!(
299            "lib{}{}.rmeta",
300            self.crate_name, self.extra_filename
301        )))
302    }
303
304    /// Path of the emitted dynamic library under `--out-dir`, named per the
305    /// target's binary format (`lib*.so` / `lib*.dylib` / `*.dll`).
306    ///
307    /// Returns `Ok(None)` when the invocation produces no dynamic library.
308    ///
309    /// # Errors
310    /// Returns an error when a dynamic library is produced but `--out-dir` is
311    /// absent, or when the `--target` triple is unparseable or has an
312    /// unsupported binary format.
313    pub fn output_dynamic_library_path(&self) -> Result<Option<PathBuf>, String> {
314        if !self.produces_dynamic_library() {
315            return Ok(None);
316        }
317        let out_dir = self
318            .out_dir
319            .as_ref()
320            .ok_or_else(|| "dynamic library output requires --out-dir".to_owned())?;
321        let (prefix, extension) = match self.target.as_deref() {
322            Some(target) => dynamic_library_naming(target)?,
323            None => (
324                std::env::consts::DLL_PREFIX,
325                std::env::consts::DLL_SUFFIX
326                    .strip_prefix('.')
327                    .unwrap_or(std::env::consts::DLL_SUFFIX),
328            ),
329        };
330
331        Ok(Some(out_dir.join(format!(
332            "{prefix}{}{}.{}",
333            self.crate_name, self.extra_filename, extension
334        ))))
335    }
336
337    /// Path of the emitted dep-info file under `--out-dir`
338    /// (`<name><extra-filename>.d`), or `None` without `--out-dir`.
339    #[must_use]
340    pub fn output_dep_info_path(&self) -> Option<PathBuf> {
341        let out_dir = self.out_dir.as_ref()?;
342        Some(out_dir.join(format!("{}{}.d", self.crate_name, self.extra_filename)))
343    }
344
345    /// Path of the emitted binary under `--out-dir`
346    /// (`<name><extra-filename><exe-suffix>`), or `None` when the invocation
347    /// is not a `bin` crate or has no `--out-dir`.
348    #[must_use]
349    pub fn output_binary_path(&self) -> Option<PathBuf> {
350        let out_dir = self.out_dir.as_ref()?;
351        if !self.is_binary() {
352            return None;
353        }
354        Some(out_dir.join(format!(
355            "{}{}{}",
356            self.crate_name,
357            self.extra_filename,
358            std::env::consts::EXE_SUFFIX
359        )))
360    }
361
362    /// Path of the artifact a downstream crate would link against: the
363    /// `.rlib`, then the binary, then the dynamic library.
364    ///
365    /// # Errors
366    /// Propagates [`Self::output_dynamic_library_path`]'s error when the
367    /// invocation produces a dynamic library without `--out-dir` or with an
368    /// unparseable target.
369    pub fn output_link_path(&self) -> Result<Option<PathBuf>, String> {
370        if let Some(path) = self.output_rlib_path() {
371            return Ok(Some(path));
372        }
373        if let Some(path) = self.output_binary_path() {
374            return Ok(Some(path));
375        }
376        self.output_dynamic_library_path()
377    }
378
379    /// The `build-script-build` alias cargo creates next to the build script
380    /// binary, or `None` when this is not a build script or `--out-dir` is
381    /// absent.
382    #[must_use]
383    pub fn build_script_alias_path(&self) -> Option<PathBuf> {
384        let out_dir = self.out_dir.as_ref()?;
385        if !self.is_build_script() {
386            return None;
387        }
388        Some(out_dir.join(format!(
389            "build-script-build{}",
390            std::env::consts::EXE_SUFFIX
391        )))
392    }
393
394    /// The cargo profile this invocation compiles with, defaulting absent
395    /// flags to cargo's debug values (`opt-level=0`, debug assertions and
396    /// overflow checks on, `panic=unwind`).
397    ///
398    /// # Errors
399    /// Returns an error when `-C debuginfo`, `-C panic` or `-C strip` carry
400    /// values outside the set rustc documents.
401    pub fn profile(&self) -> Result<Profile, String> {
402        Ok(Profile {
403            opt_level: self.opt_level.clone().unwrap_or_else(|| "0".to_owned()),
404            debuginfo: parse_debuginfo_level(self.debuginfo.as_deref())?,
405            debug_assertions: self.debug_assertions.unwrap_or(true),
406            overflow_checks: self.overflow_checks.unwrap_or(true),
407            panic: parse_panic_strategy(self.panic_strategy.as_deref())?,
408            strip: parse_strip_level(self.strip.as_deref())?,
409        })
410    }
411}
412
413fn apply_rustc_arg<'a>(
414    arg: &str,
415    iter: &mut impl Iterator<Item = &'a OsString>,
416    parsed: &mut ParsedRustcArgs,
417) -> Result<(), String> {
418    match arg {
419        "--crate-name" => apply_crate_name(next_str(iter, "--crate-name")?, parsed),
420        "--crate-type" => apply_crate_types(next_str(iter, "--crate-type")?, parsed),
421        "--target" => apply_target(next_str(iter, "--target")?, parsed),
422        "--cfg" => apply_cfg(next_str(iter, "--cfg")?, parsed),
423        "--out-dir" => parsed.out_dir = Some(PathBuf::from(next_os(iter, "--out-dir")?)),
424        "--extern" => {
425            parse_extern_crate(next_os(iter, "--extern")?.clone(), parsed)?;
426        }
427        "--emit" => {
428            parse_emit_kinds(next_str(iter, "--emit")?, parsed);
429        }
430        "--json" => {
431            parse_json_kinds(next_str(iter, "--json")?, parsed);
432        }
433        "-C" => {
434            parse_codegen_option(next_str(iter, "-C")?, parsed)?;
435        }
436        "-L" => {
437            parse_library_search(next_str(iter, "-L")?, parsed);
438        }
439        _ => apply_attached_arg(arg, iter, parsed)?,
440    }
441    Ok(())
442}
443
444/// Handle a flag carrying its value inline (`--flag=value` or `-Xvalue`).
445///
446/// Every equals spelling shares the space spelling's handler so the two
447/// forms can never drift: cargo emits the space forms, but rustc accepts
448/// both and a unit passed with `=` would otherwise parse as restorable-in-
449/// name-only — recognized by nothing downstream.
450fn apply_attached_arg<'a>(
451    arg: &str,
452    iter: &mut impl Iterator<Item = &'a OsString>,
453    parsed: &mut ParsedRustcArgs,
454) -> Result<(), String> {
455    if let Some(value) = arg.strip_prefix("--crate-name=") {
456        apply_crate_name(value, parsed);
457        return Ok(());
458    }
459    if let Some(value) = arg.strip_prefix("--crate-type=") {
460        apply_crate_types(value, parsed);
461        return Ok(());
462    }
463    if let Some(value) = arg.strip_prefix("--target=") {
464        apply_target(value, parsed);
465        return Ok(());
466    }
467    if let Some(value) = arg.strip_prefix("--cfg=") {
468        apply_cfg(value, parsed);
469        return Ok(());
470    }
471    if let Some(value) = arg.strip_prefix("--out-dir=") {
472        parsed.out_dir = Some(PathBuf::from(value));
473        return Ok(());
474    }
475    if let Some(value) = arg.strip_prefix("--extern=") {
476        return parse_extern_crate(OsString::from(value), parsed);
477    }
478    if let Some(value) = arg.strip_prefix("--emit=") {
479        parse_emit_kinds(value, parsed);
480        return Ok(());
481    }
482    if let Some(value) = arg.strip_prefix("--json=") {
483        parse_json_kinds(value, parsed);
484        return Ok(());
485    }
486    if let Some(option) = arg.strip_prefix("-C") {
487        return parse_codegen_option(option, parsed);
488    }
489    if let Some(option) = arg.strip_prefix("-L") {
490        parse_library_search(option, parsed);
491        return Ok(());
492    }
493    if arg == "-Z" {
494        return parse_unstable_option(next_str(iter, "-Z")?, parsed);
495    }
496    if let Some(option) = arg.strip_prefix("-Z") {
497        return parse_unstable_option(option, parsed);
498    }
499    if !arg.starts_with('-')
500        && parsed.input_path.is_none()
501        && std::path::Path::new(arg)
502            .extension()
503            .and_then(|extension| extension.to_str())
504            == Some("rs")
505    {
506        parsed.input_path = Some(PathBuf::from(arg));
507    }
508    Ok(())
509}
510
511fn apply_crate_name(value: &str, parsed: &mut ParsedRustcArgs) {
512    value.clone_into(&mut parsed.crate_name);
513}
514
515/// Accumulate a `--crate-type` value. rustc takes the flag repeatedly and
516/// unions the results, and cargo spells a multi-type lib target that way
517/// (`--crate-type lib --crate-type cdylib`), so each occurrence extends the
518/// list instead of replacing it: overwriting dropped every type but the last
519/// from an identity the compile key is computed over.
520fn apply_crate_types(value: &str, parsed: &mut ParsedRustcArgs) {
521    for crate_type in value.split(',') {
522        if !parsed.crate_types.iter().any(|seen| seen == crate_type) {
523            parsed.crate_types.push(crate_type.to_owned());
524        }
525    }
526}
527
528fn apply_target(value: &str, parsed: &mut ParsedRustcArgs) {
529    parsed.target = Some(value.to_owned());
530}
531
532fn apply_cfg(value: &str, parsed: &mut ParsedRustcArgs) {
533    match parse_feature_cfg(value) {
534        Some(feature) => {
535            parsed.features.insert(feature);
536        }
537        None => {
538            parsed.cfgs.insert(value.to_owned());
539        }
540    }
541}
542
543fn dynamic_library_naming(target: &str) -> Result<(&'static str, &'static str), String> {
544    let triple: Triple = target
545        .parse()
546        .map_err(|error| format!("parse target triple `{target}`: {error}"))?;
547    match triple.binary_format {
548        BinaryFormat::Elf => Ok(("lib", "so")),
549        BinaryFormat::Macho => Ok(("lib", "dylib")),
550        BinaryFormat::Coff => Ok(("", "dll")),
551        other => Err(format!(
552            "unsupported binary format `{}` for dynamic library output",
553            other.into_str()
554        )),
555    }
556}
557
558fn parse_codegen_option(option: &str, parsed: &mut ParsedRustcArgs) -> Result<(), String> {
559    let Some((key, value)) = option.split_once('=') else {
560        match option {
561            "prefer-dynamic" => {}
562            _ => parsed.has_custom_codegen = true,
563        }
564        return Ok(());
565    };
566
567    match key {
568        "metadata" => parsed.c_metadata = Some(value.to_owned()),
569        "extra-filename" => value.clone_into(&mut parsed.extra_filename),
570        "opt-level" => parsed.opt_level = Some(value.to_owned()),
571        "debuginfo" => parsed.debuginfo = Some(value.to_owned()),
572        "panic" => parsed.panic_strategy = Some(value.to_owned()),
573        "debug-assertions" => parsed.debug_assertions = Some(parse_bool(value)?),
574        "overflow-checks" => parsed.overflow_checks = Some(parse_bool(value)?),
575        "strip" => parsed.strip = Some(value.to_owned()),
576        "embed-bitcode" => parsed.embed_bitcode = parse_bool(value)?,
577        "codegen-units" | "split-debuginfo" => {}
578        _ if is_link_only_codegen_option(option) => {
579            parsed.link_options.insert(option.to_owned());
580        }
581        _ => parsed.has_custom_codegen = true,
582    }
583
584    Ok(())
585}
586
587/// `-C` options that steer only the final link step.
588///
589/// They change nothing an rlib contains, because rustc never invokes a
590/// linker for a unit that does not produce a linked artifact. Mold, lld and
591/// alternate link drivers reach a build through exactly these flags, so
592/// treating them as unmodelled codegen made a mold-configured build serve
593/// nothing at all and recompile its whole dependency graph.
594///
595/// Inert is not the same as harmless, though: what these options mean
596/// depends on whether the unit reaches the linker, which this function
597/// cannot see. [`ParsedRustcArgs::link_options_reach_the_linker`] makes
598/// that call.
599fn is_link_only_codegen_option(option: &str) -> bool {
600    let key = option.split_once('=').map_or(option, |(key, _)| key);
601    matches!(
602        key,
603        "linker" | "linker-flavor" | "link-arg" | "link-args" | "link-self-contained"
604    )
605}
606
607/// Handle a `-Z` option. `embed-metadata` is the flag nightly cargo emits on
608/// every unit and is modeled as compile identity; every other `-Z` option
609/// marks the invocation as custom codegen, same as before.
610fn parse_unstable_option(option: &str, parsed: &mut ParsedRustcArgs) -> Result<(), String> {
611    match option.split_once('=') {
612        Some(("embed-metadata", value)) => {
613            parsed.embed_metadata = Some(parse_bool(value)?);
614        }
615        _ => parsed.has_custom_codegen = true,
616    }
617    Ok(())
618}
619
620fn parse_library_search(option: &str, parsed: &mut ParsedRustcArgs) {
621    if let Some(path) = option.strip_prefix("native=") {
622        parsed.native_search_paths.push(PathBuf::from(path));
623    }
624}
625
626fn parse_extern_crate(arg: OsString, parsed: &mut ParsedRustcArgs) -> Result<(), String> {
627    let arg = arg.into_string().map_err(|value| {
628        format!(
629            "rustc --extern argument is not valid UTF-8: {}",
630            value.display()
631        )
632    })?;
633    let Some((crate_name, path)) = arg.split_once('=') else {
634        return Ok(());
635    };
636    if crate_name.is_empty() || path.is_empty() {
637        return Err(format!("invalid rustc --extern argument: {arg}"));
638    }
639    parsed.extern_crates.push(ParsedExternCrate {
640        crate_name: crate_name.to_owned(),
641        path: PathBuf::from(path),
642    });
643    parsed.extern_crates.sort_by(|left, right| {
644        left.crate_name
645            .cmp(&right.crate_name)
646            .then(left.path.cmp(&right.path))
647    });
648    Ok(())
649}
650
651fn parse_emit_kinds(value: &str, parsed: &mut ParsedRustcArgs) {
652    parsed.emit.extend(
653        value
654            .split(',')
655            .filter(|emit| !emit.is_empty())
656            .map(str::to_owned),
657    );
658}
659
660fn parse_json_kinds(value: &str, parsed: &mut ParsedRustcArgs) {
661    parsed.json.extend(
662        value
663            .split(',')
664            .filter(|json| !json.is_empty())
665            .map(str::to_owned),
666    );
667}
668
669fn parse_bool(value: &str) -> Result<bool, String> {
670    match value {
671        "yes" | "true" | "on" => Ok(true),
672        "no" | "false" | "off" => Ok(false),
673        _ => Err(format!("invalid boolean rustc codegen value: {value}")),
674    }
675}
676
677fn parse_feature_cfg(cfg: &str) -> Option<String> {
678    let feature = cfg.strip_prefix("feature=\"")?;
679    let feature = feature.strip_suffix('"')?;
680    Some(feature.to_owned())
681}
682
683fn parse_debuginfo_level(value: Option<&str>) -> Result<u32, String> {
684    match value {
685        None | Some("none") => Ok(0),
686        Some("line-directives-only" | "line-tables-only" | "limited") => Ok(1),
687        Some("full") => Ok(2),
688        Some(raw) => raw
689            .parse::<u32>()
690            .map_err(|error| format!("invalid debuginfo rustc codegen value `{raw}`: {error}")),
691    }
692}
693
694fn parse_strip_level(value: Option<&str>) -> Result<StripLevel, String> {
695    match value.unwrap_or("none") {
696        "none" => Ok(StripLevel::None),
697        "debuginfo" => Ok(StripLevel::Debuginfo),
698        "symbols" => Ok(StripLevel::Symbols),
699        other => Err(format!("invalid strip rustc codegen value `{other}`")),
700    }
701}
702
703fn parse_panic_strategy(value: Option<&str>) -> Result<PanicStrategy, String> {
704    match value.unwrap_or("unwind") {
705        "unwind" => Ok(PanicStrategy::Unwind),
706        "abort" => Ok(PanicStrategy::Abort),
707        raw => Err(format!("invalid panic rustc codegen value: {raw}")),
708    }
709}
710
711fn next_os<'a>(
712    iter: &mut impl Iterator<Item = &'a OsString>,
713    flag: &str,
714) -> Result<&'a OsString, String> {
715    iter.next()
716        .ok_or_else(|| format!("missing value after {flag}"))
717}
718
719fn next_str<'a>(
720    iter: &mut impl Iterator<Item = &'a OsString>,
721    flag: &str,
722) -> Result<&'a str, String> {
723    next_os(iter, flag)?
724        .to_str()
725        .ok_or_else(|| format!("value after {flag} is not valid UTF-8"))
726}
727
728/// What the process-wide rustflags contribute to cacheability.
729#[derive(Debug, Default, Clone)]
730struct EnvRustflagsEffect {
731    /// A flag that changes the compiled objects: disqualifying outright.
732    custom_codegen: bool,
733    /// Flags that steer only the final link step: inert for a unit that
734    /// never links, part of the key for one that does.
735    link_options: BTreeSet<String>,
736}
737
738/// Classify `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS` into the two effects.
739///
740/// Both are process-wide, so this cannot know the crate type of the unit
741/// being compiled — the caller decides what a link option means for the
742/// unit it holds.
743fn env_rustflags_effect() -> EnvRustflagsEffect {
744    if std::env::var_os("CARGO_ENCODED_RUSTFLAGS").is_none()
745        && std::env::var_os("RUSTFLAGS").is_none()
746    {
747        return EnvRustflagsEffect::default();
748    }
749
750    let mut flags = encoded_rustflags();
751    match split_rustflags_env() {
752        Ok(extra) => flags.extend(extra),
753        Err(error) => {
754            tracing::warn!(
755                %error,
756                "RUSTFLAGS could not be parsed — treating as custom codegen (cache disabled)"
757            );
758            return EnvRustflagsEffect {
759                custom_codegen: true,
760                link_options: BTreeSet::new(),
761            };
762        }
763    }
764
765    let custom = EnvRustflagsEffect {
766        custom_codegen: true,
767        link_options: BTreeSet::new(),
768    };
769    let mut effect = EnvRustflagsEffect::default();
770    let mut iter = flags.into_iter();
771    while let Some(flag) = iter.next() {
772        match flag.as_str() {
773            "--remap-path-prefix" => {
774                if iter.next().is_none() {
775                    return custom;
776                }
777            }
778            _ if flag.starts_with("--remap-path-prefix=") => {}
779            "-C" | "--codegen" => match iter.next() {
780                Some(option) if is_link_only_codegen_option(&option) => {
781                    effect.link_options.insert(option);
782                }
783                _ => return custom,
784            },
785            _ => {
786                let Some(option) = flag
787                    .strip_prefix("-C")
788                    .or_else(|| flag.strip_prefix("--codegen="))
789                    .filter(|option| is_link_only_codegen_option(option))
790                else {
791                    return custom;
792                };
793                effect.link_options.insert(option.to_owned());
794            }
795        }
796    }
797
798    effect
799}
800
801fn encoded_rustflags() -> Vec<String> {
802    std::env::var("CARGO_ENCODED_RUSTFLAGS")
803        .ok()
804        .into_iter()
805        .flat_map(|value| value.split('\x1f').map(str::to_owned).collect::<Vec<_>>())
806        .filter(|value| !value.is_empty())
807        .collect()
808}
809
810fn split_rustflags_env() -> Result<Vec<String>, shell_words::ParseError> {
811    std::env::var("RUSTFLAGS").map_or_else(|_| Ok(Vec::new()), |value| shell_words::split(&value))
812}
813
814#[cfg(test)]
815mod tests {
816    use std::collections::BTreeSet;
817    use std::path::{Path, PathBuf};
818
819    use super::ParsedRustcArgs;
820    use crate::platform::StripLevel;
821
822    fn args(parts: &[&str]) -> Vec<std::ffi::OsString> {
823        parts.iter().map(std::ffi::OsString::from).collect()
824    }
825
826    fn with_clean_rustc_env<T>(f: impl FnOnce() -> T) -> T {
827        let encoded = std::env::var_os("CARGO_ENCODED_RUSTFLAGS");
828        let rustflags = std::env::var_os("RUSTFLAGS");
829        let primary = std::env::var_os("CARGO_PRIMARY_PACKAGE");
830        unsafe {
831            std::env::remove_var("CARGO_ENCODED_RUSTFLAGS");
832            std::env::remove_var("RUSTFLAGS");
833            std::env::remove_var("CARGO_PRIMARY_PACKAGE");
834        }
835        let result = f();
836        unsafe {
837            match encoded {
838                Some(value) => std::env::set_var("CARGO_ENCODED_RUSTFLAGS", value),
839                None => std::env::remove_var("CARGO_ENCODED_RUSTFLAGS"),
840            }
841            match rustflags {
842                Some(value) => std::env::set_var("RUSTFLAGS", value),
843                None => std::env::remove_var("RUSTFLAGS"),
844            }
845            match primary {
846                Some(value) => std::env::set_var("CARGO_PRIMARY_PACKAGE", value),
847                None => std::env::remove_var("CARGO_PRIMARY_PACKAGE"),
848            }
849        }
850        result
851    }
852
853    #[test]
854    #[serial_test::serial]
855    fn parses_debug_rlib_invocation() {
856        with_clean_rustc_env(|| {
857            let parsed = ParsedRustcArgs::parse(&args(&[
858                "--crate-name",
859                "itoa",
860                "--crate-type",
861                "rlib",
862                "--target",
863                "aarch64-apple-darwin",
864                "--cfg",
865                "feature=\"default\"",
866                "--out-dir",
867                "/tmp/out",
868                "-C",
869                "metadata=abc123",
870                "-C",
871                "extra-filename=-abc123",
872                "-C",
873                "opt-level=0",
874                "-C",
875                "debug-assertions=yes",
876            ]))
877            .expect("parser should succeed");
878
879            assert_eq!(parsed.crate_name, "itoa");
880            assert_eq!(parsed.target.as_deref(), Some("aarch64-apple-darwin"));
881            assert_eq!(parsed.c_metadata.as_deref(), Some("abc123"));
882            assert!(parsed.is_cacheable());
883            assert!(parsed.features.contains("default"));
884            assert!(
885                parsed
886                    .output_rlib_path()
887                    .expect("rlib path")
888                    .ends_with("libitoa-abc123.rlib")
889            );
890        });
891    }
892
893    #[test]
894    #[serial_test::serial]
895    fn parses_the_equals_spellings_of_every_flag_cargo_can_emit() {
896        with_clean_rustc_env(|| {
897            let parsed = ParsedRustcArgs::parse(&args(&[
898                "--crate-name=itoa",
899                "--crate-type=rlib",
900                "--target=aarch64-apple-darwin",
901                "--cfg=feature=\"default\"",
902                "--out-dir=/tmp/out",
903                "-C",
904                "metadata=abc123",
905                "-C",
906                "extra-filename=-abc123",
907                "-C",
908                "opt-level=0",
909                "-C",
910                "debug-assertions=yes",
911            ]))
912            .expect("parser should succeed");
913
914            assert_eq!(parsed.crate_name, "itoa");
915            assert_eq!(parsed.crate_types, vec!["rlib"]);
916            assert_eq!(parsed.target.as_deref(), Some("aarch64-apple-darwin"));
917            assert_eq!(parsed.c_metadata.as_deref(), Some("abc123"));
918            assert_eq!(
919                parsed.out_dir.as_deref(),
920                Some(std::path::Path::new("/tmp/out"))
921            );
922            assert!(parsed.is_cacheable());
923            assert!(parsed.features.contains("default"));
924            assert!(
925                parsed
926                    .output_rlib_path()
927                    .expect("rlib path")
928                    .ends_with("libitoa-abc123.rlib")
929            );
930        });
931    }
932
933    #[test]
934    #[serial_test::serial]
935    fn equals_and_space_spellings_produce_the_same_parse() {
936        with_clean_rustc_env(|| {
937            let space = ParsedRustcArgs::parse(&args(&[
938                "--crate-name",
939                "itoa",
940                "--crate-type",
941                "rlib",
942                "--crate-type",
943                "cdylib",
944                "--target",
945                "aarch64-apple-darwin",
946                "--cfg",
947                "feature=\"serde\"",
948                "--cfg",
949                "unix",
950                "--out-dir",
951                "/tmp/out",
952                "-C",
953                "metadata=abc123",
954                "-C",
955                "extra-filename=-abc123",
956            ]))
957            .expect("space spelling parses");
958            let equals = ParsedRustcArgs::parse(&args(&[
959                "--crate-name=itoa",
960                "--crate-type=rlib",
961                "--crate-type=cdylib",
962                "--target=aarch64-apple-darwin",
963                "--cfg=feature=\"serde\"",
964                "--cfg=unix",
965                "--out-dir=/tmp/out",
966                "-C",
967                "metadata=abc123",
968                "-C",
969                "extra-filename=-abc123",
970            ]))
971            .expect("equals spelling parses");
972
973            assert_eq!(space.crate_name, equals.crate_name);
974            assert_eq!(space.crate_types, equals.crate_types);
975            assert_eq!(space.target, equals.target);
976            assert_eq!(space.features, equals.features);
977            assert_eq!(space.cfgs, equals.cfgs);
978            assert_eq!(space.cfgs, BTreeSet::from(["unix".to_owned()]));
979            assert_eq!(space.out_dir, equals.out_dir);
980            assert_eq!(space.c_metadata, equals.c_metadata);
981            assert_eq!(space.extra_filename, equals.extra_filename);
982        });
983    }
984
985    #[test]
986    #[serial_test::serial]
987    fn rejects_custom_codegen() {
988        with_clean_rustc_env(|| {
989            let parsed = ParsedRustcArgs::parse(&args(&[
990                "--crate-name",
991                "itoa",
992                "--crate-type",
993                "rlib",
994                "--target",
995                "aarch64-apple-darwin",
996                "--out-dir",
997                "/tmp/out",
998                "-C",
999                "metadata=abc123",
1000                "-C",
1001                "target-cpu=native",
1002            ]))
1003            .expect("parser should succeed");
1004
1005            assert!(!parsed.is_cacheable());
1006        });
1007    }
1008
1009    #[test]
1010    #[serial_test::serial]
1011    fn proc_macro_is_cacheable_without_debug_profile() {
1012        with_clean_rustc_env(|| {
1013            let parsed = ParsedRustcArgs::parse(&args(&[
1014                "--crate-name",
1015                "serde_derive",
1016                "--crate-type",
1017                "proc-macro",
1018                "--target",
1019                "aarch64-apple-darwin",
1020                "--out-dir",
1021                "/tmp/out",
1022                "-C",
1023                "metadata=pm123",
1024                "-C",
1025                "opt-level=3",
1026            ]))
1027            .expect("parser should succeed");
1028
1029            assert!(parsed.is_cacheable());
1030            assert!(parsed.is_proc_macro());
1031        });
1032    }
1033
1034    #[test]
1035    #[serial_test::serial]
1036    fn release_profile_is_cacheable_because_the_profile_is_identity() {
1037        with_clean_rustc_env(|| {
1038            let parsed = ParsedRustcArgs::parse(&args(&[
1039                "--crate-name",
1040                "itoa",
1041                "--crate-type",
1042                "rlib",
1043                "--target",
1044                "aarch64-apple-darwin",
1045                "--out-dir",
1046                "/tmp/out",
1047                "-C",
1048                "metadata=abc123",
1049                "-C",
1050                "opt-level=3",
1051                "-C",
1052                "debug-assertions=no",
1053            ]))
1054            .expect("parser should succeed");
1055
1056            assert!(parsed.is_cacheable());
1057            assert!(parsed.is_locally_cacheable());
1058            let profile = parsed.profile().expect("profile");
1059            assert_eq!(profile.opt_level, "3");
1060            assert!(!profile.debug_assertions);
1061        });
1062    }
1063
1064    #[test]
1065    #[serial_test::serial]
1066    fn primary_package_is_never_locally_cacheable() {
1067        with_clean_rustc_env(|| {
1068            unsafe { std::env::set_var("CARGO_PRIMARY_PACKAGE", "1") };
1069            let parsed = ParsedRustcArgs::parse(&args(&[
1070                "--crate-name",
1071                "itoa",
1072                "--crate-type",
1073                "rlib",
1074                "--target",
1075                "aarch64-apple-darwin",
1076                "--out-dir",
1077                "/tmp/out",
1078                "-C",
1079                "metadata=abc123",
1080            ]))
1081            .expect("parser should succeed");
1082
1083            assert!(!parsed.is_locally_cacheable());
1084        });
1085    }
1086
1087    #[test]
1088    #[serial_test::serial]
1089    fn custom_codegen_is_never_locally_cacheable() {
1090        with_clean_rustc_env(|| {
1091            let parsed = ParsedRustcArgs::parse(&args(&[
1092                "--crate-name",
1093                "itoa",
1094                "--crate-type",
1095                "rlib",
1096                "--target",
1097                "aarch64-apple-darwin",
1098                "--out-dir",
1099                "/tmp/out",
1100                "-C",
1101                "metadata=abc123",
1102                "-C",
1103                "target-cpu=native",
1104            ]))
1105            .expect("parser should succeed");
1106
1107            assert!(!parsed.is_locally_cacheable());
1108        });
1109    }
1110
1111    #[test]
1112    #[serial_test::serial]
1113    fn link_only_codegen_options_stay_cacheable_for_an_rlib() {
1114        with_clean_rustc_env(|| {
1115            for option in [
1116                "link-arg=-fuse-ld=mold",
1117                "link-args=-fuse-ld=mold -Wl,--as-needed",
1118                "linker=clang",
1119                "linker-flavor=gcc",
1120                "link-self-contained=y",
1121            ] {
1122                let parsed = ParsedRustcArgs::parse(&args(&[
1123                    "--crate-name",
1124                    "itoa",
1125                    "--crate-type",
1126                    "rlib",
1127                    "--target",
1128                    "x86_64-unknown-linux-gnu",
1129                    "--out-dir",
1130                    "/tmp/out",
1131                    "-C",
1132                    "metadata=abc123",
1133                    "-C",
1134                    option,
1135                ]))
1136                .expect("parser should succeed");
1137
1138                assert!(!parsed.has_custom_codegen, "{option} marked custom");
1139                assert!(parsed.is_cacheable(), "{option} made unit uncacheable");
1140            }
1141        });
1142    }
1143
1144    #[test]
1145    #[serial_test::serial]
1146    fn link_options_key_a_unit_that_links_instead_of_disqualifying_it() {
1147        // An rlib is never linked, so these options cannot change it and
1148        // never reach its key. A proc-macro or dylib IS linked, and
1149        // `-C link-arg` carries arbitrary text — a different `-L`, `-rpath`
1150        // or `-l` produces a different `.so`. That makes it a different
1151        // artifact, which is a different key, not a refusal.
1152        with_clean_rustc_env(|| {
1153            for crate_type in ["proc-macro", "dylib"] {
1154                let parsed = ParsedRustcArgs::parse(&args(&[
1155                    "--crate-name",
1156                    "serde_derive",
1157                    "--crate-type",
1158                    crate_type,
1159                    "--target",
1160                    "x86_64-unknown-linux-gnu",
1161                    "--out-dir",
1162                    "/tmp/out",
1163                    "-C",
1164                    "metadata=abc123",
1165                    "-C",
1166                    "link-arg=-L/opt/custom/lib",
1167                ]))
1168                .expect("parser should succeed");
1169
1170                assert_eq!(
1171                    parsed.link_options_reaching_the_linker(),
1172                    vec!["link-arg=-L/opt/custom/lib".to_owned()],
1173                    "{crate_type} lost the link option the key needs"
1174                );
1175                assert!(
1176                    !parsed.has_custom_codegen,
1177                    "{crate_type} was misfiled as custom codegen"
1178                );
1179                assert!(
1180                    parsed.is_cacheable(),
1181                    "{crate_type} with a link option was refused instead of keyed"
1182                );
1183                assert!(
1184                    parsed.is_locally_cacheable(),
1185                    "{crate_type} with a link option was refused by the local cache"
1186                );
1187            }
1188        });
1189    }
1190
1191    #[test]
1192    #[serial_test::serial]
1193    fn an_rlib_never_carries_link_options_into_its_key() {
1194        // The options are inert for an archive rustc never links, and that
1195        // is where a dependency graph lives: keying on them would change
1196        // every existing rlib's identity for nothing.
1197        with_clean_rustc_env(|| {
1198            let parsed = ParsedRustcArgs::parse(&args(&[
1199                "--crate-name",
1200                "serde",
1201                "--crate-type",
1202                "lib",
1203                "--target",
1204                "x86_64-unknown-linux-gnu",
1205                "--out-dir",
1206                "/tmp/out",
1207                "-C",
1208                "metadata=abc123",
1209                "-C",
1210                "link-arg=-fuse-ld=mold",
1211            ]))
1212            .expect("parser should succeed");
1213
1214            assert_eq!(
1215                parsed.link_options,
1216                BTreeSet::from(["link-arg=-fuse-ld=mold".to_owned()]),
1217                "the option was not parsed"
1218            );
1219            assert!(
1220                parsed.link_options_reaching_the_linker().is_empty(),
1221                "an rlib reported a link option as reaching the linker"
1222            );
1223            assert!(parsed.is_cacheable(), "a mold-built rlib stopped serving");
1224        });
1225    }
1226    #[test]
1227    #[serial_test::serial]
1228    fn repeated_crate_type_flags_union_into_one_identity() {
1229        // rustc accepts `--crate-type` more than once and cargo spells a
1230        // multi-type lib target that way. The compile key is computed over
1231        // this list, so dropping all but the last flag both lost the rlib
1232        // and let two different units agree on one key.
1233        with_clean_rustc_env(|| {
1234            let parsed = ParsedRustcArgs::parse(&args(&[
1235                "--crate-name",
1236                "ffi_thing",
1237                "--crate-type",
1238                "lib",
1239                "--crate-type",
1240                "cdylib",
1241                "--target",
1242                "x86_64-unknown-linux-gnu",
1243                "--out-dir",
1244                "/tmp/out",
1245                "-C",
1246                "metadata=abc123",
1247            ]))
1248            .expect("parser should succeed");
1249
1250            assert_eq!(parsed.crate_types, vec!["lib", "cdylib"]);
1251        });
1252    }
1253
1254    #[test]
1255    #[serial_test::serial]
1256    fn a_lib_that_also_produces_a_cdylib_is_a_unit_that_links() {
1257        // Cargo compiles every crate type a lib target declares in one rustc
1258        // invocation, so `crate-type = ["lib", "cdylib"]` — the usual shape
1259        // of an FFI crate — produces the rlib stow would cache in the same
1260        // run that links the `.so`. The link options are not inert there, so
1261        // they enter the key rather than being ignored as they are for a
1262        // pure rlib.
1263        with_clean_rustc_env(|| {
1264            let parsed = ParsedRustcArgs::parse(&args(&[
1265                "--crate-name",
1266                "ffi_thing",
1267                "--crate-type",
1268                "lib",
1269                "--crate-type",
1270                "cdylib",
1271                "--target",
1272                "x86_64-unknown-linux-gnu",
1273                "--out-dir",
1274                "/tmp/out",
1275                "-C",
1276                "metadata=abc123",
1277                "-C",
1278                "link-arg=-L/opt/custom/lib",
1279            ]))
1280            .expect("parser should succeed");
1281
1282            assert!(parsed.produces_rlib(), "the rlib output is what is cached");
1283            assert_eq!(
1284                parsed.link_options_reaching_the_linker(),
1285                vec!["link-arg=-L/opt/custom/lib".to_owned()],
1286                "a lib+cdylib unit dropped the link option from its key"
1287            );
1288            assert!(parsed.is_cacheable(), "a lib+cdylib unit stopped serving");
1289        });
1290    }
1291
1292    #[test]
1293    #[serial_test::serial]
1294    fn link_only_env_rustflags_key_a_unit_that_links() {
1295        // The same split for process-wide rustflags: `RUSTFLAGS` cannot
1296        // know the crate type, so the decision belongs to the unit. The
1297        // linked unit carries the option into its key; the rlib does not
1298        // see it at all.
1299        unsafe {
1300            std::env::set_var("RUSTFLAGS", "-C link-arg=-fuse-ld=mold");
1301        }
1302        let linked = ParsedRustcArgs::parse(&args(&[
1303            "--crate-name",
1304            "serde_derive",
1305            "--crate-type",
1306            "proc-macro",
1307            "--target",
1308            "x86_64-unknown-linux-gnu",
1309            "--out-dir",
1310            "/tmp/out",
1311            "-C",
1312            "metadata=abc123",
1313        ]))
1314        .expect("parser should succeed");
1315        let rlib = ParsedRustcArgs::parse(&args(&[
1316            "--crate-name",
1317            "serde",
1318            "--crate-type",
1319            "rlib",
1320            "--target",
1321            "x86_64-unknown-linux-gnu",
1322            "--out-dir",
1323            "/tmp/out",
1324            "-C",
1325            "metadata=abc123",
1326        ]))
1327        .expect("parser should succeed");
1328        unsafe {
1329            std::env::remove_var("RUSTFLAGS");
1330        }
1331
1332        assert_eq!(
1333            linked.link_options_reaching_the_linker(),
1334            vec!["link-arg=-fuse-ld=mold".to_owned()],
1335            "a linked unit lost the rustflags link option from its key"
1336        );
1337        assert!(
1338            linked.is_cacheable(),
1339            "a mold-linked proc-macro stopped serving"
1340        );
1341        assert!(
1342            rlib.link_options_reaching_the_linker().is_empty(),
1343            "an rlib took a rustflags link option into its key"
1344        );
1345        assert!(rlib.is_cacheable(), "the rlib stopped being cacheable");
1346    }
1347
1348    #[test]
1349    #[serial_test::serial]
1350    fn codegen_flags_that_change_objects_stay_custom() {
1351        with_clean_rustc_env(|| {
1352            for option in ["linker-plugin-lto", "link-dead-code=y", "target-cpu=native"] {
1353                let parsed = ParsedRustcArgs::parse(&args(&[
1354                    "--crate-name",
1355                    "itoa",
1356                    "--crate-type",
1357                    "rlib",
1358                    "--target",
1359                    "x86_64-unknown-linux-gnu",
1360                    "--out-dir",
1361                    "/tmp/out",
1362                    "-C",
1363                    "metadata=abc123",
1364                    "-C",
1365                    option,
1366                ]))
1367                .expect("parser should succeed");
1368
1369                assert!(parsed.has_custom_codegen, "{option} lost its marking");
1370                assert!(!parsed.is_locally_cacheable());
1371            }
1372        });
1373    }
1374
1375    #[test]
1376    #[serial_test::serial]
1377    fn nightly_cargo_embed_metadata_is_not_custom_codegen() {
1378        with_clean_rustc_env(|| {
1379            for spelling in ["-Z embed-metadata=no", "-Zembed-metadata=no"] {
1380                let mut invocation = vec![
1381                    "--crate-name",
1382                    "itoa",
1383                    "--crate-type",
1384                    "rlib",
1385                    "--target",
1386                    "aarch64-apple-darwin",
1387                    "--out-dir",
1388                    "/tmp/out",
1389                    "-C",
1390                    "metadata=abc123",
1391                ];
1392                invocation.extend(spelling.split(' '));
1393                let parsed =
1394                    ParsedRustcArgs::parse(&args(&invocation)).expect("parser should succeed");
1395
1396                assert_eq!(parsed.embed_metadata, Some(false));
1397                assert!(!parsed.has_custom_codegen);
1398                assert!(parsed.is_locally_cacheable());
1399                assert!(parsed.is_cacheable());
1400            }
1401        });
1402    }
1403
1404    #[test]
1405    #[serial_test::serial]
1406    fn parses_embed_metadata_yes() {
1407        with_clean_rustc_env(|| {
1408            let parsed = ParsedRustcArgs::parse(&args(&[
1409                "--crate-name",
1410                "itoa",
1411                "--crate-type",
1412                "rlib",
1413                "--target",
1414                "aarch64-apple-darwin",
1415                "--out-dir",
1416                "/tmp/out",
1417                "-C",
1418                "metadata=abc123",
1419                "-Z",
1420                "embed-metadata=yes",
1421            ]))
1422            .expect("parser should succeed");
1423
1424            assert_eq!(parsed.embed_metadata, Some(true));
1425            assert!(!parsed.has_custom_codegen);
1426        });
1427    }
1428
1429    #[test]
1430    #[serial_test::serial]
1431    fn other_unstable_options_still_count_as_custom_codegen() {
1432        with_clean_rustc_env(|| {
1433            for spelling in ["-Z some-other-flag", "-Zsome-other-flag"] {
1434                let mut invocation = vec![
1435                    "--crate-name",
1436                    "itoa",
1437                    "--crate-type",
1438                    "rlib",
1439                    "--target",
1440                    "aarch64-apple-darwin",
1441                    "--out-dir",
1442                    "/tmp/out",
1443                    "-C",
1444                    "metadata=abc123",
1445                ];
1446                invocation.extend(spelling.split(' '));
1447                let parsed =
1448                    ParsedRustcArgs::parse(&args(&invocation)).expect("parser should succeed");
1449
1450                assert_eq!(parsed.embed_metadata, None);
1451                assert!(parsed.has_custom_codegen);
1452                assert!(!parsed.is_locally_cacheable());
1453            }
1454        });
1455    }
1456
1457    #[test]
1458    #[serial_test::serial]
1459    fn malformed_embed_metadata_value_is_a_parse_error() {
1460        with_clean_rustc_env(|| {
1461            let error = ParsedRustcArgs::parse(&args(&[
1462                "--crate-name",
1463                "itoa",
1464                "--crate-type",
1465                "rlib",
1466                "-Z",
1467                "embed-metadata=banana",
1468            ]))
1469            .expect_err("malformed embed-metadata value must fail parsing");
1470
1471            assert!(error.contains("invalid boolean rustc codegen value"));
1472        });
1473    }
1474
1475    #[test]
1476    #[serial_test::serial]
1477    fn accepts_common_codegen_flag_without_value() {
1478        with_clean_rustc_env(|| {
1479            let parsed = ParsedRustcArgs::parse(&args(&[
1480                "--crate-name",
1481                "zerocopy_derive",
1482                "--crate-type",
1483                "proc-macro",
1484                "--target",
1485                "aarch64-apple-darwin",
1486                "--out-dir",
1487                "/tmp/out",
1488                "-C",
1489                "metadata=pm123",
1490                "-C",
1491                "prefer-dynamic",
1492            ]))
1493            .expect("parser should succeed");
1494
1495            assert!(parsed.is_cacheable());
1496            assert!(parsed.is_proc_macro());
1497            assert!(!parsed.has_custom_codegen);
1498        });
1499    }
1500
1501    #[test]
1502    #[serial_test::serial]
1503    fn unknown_codegen_flag_without_value_is_not_cacheable() {
1504        with_clean_rustc_env(|| {
1505            let parsed = ParsedRustcArgs::parse(&args(&[
1506                "--crate-name",
1507                "itoa",
1508                "--crate-type",
1509                "rlib",
1510                "--target",
1511                "aarch64-apple-darwin",
1512                "--out-dir",
1513                "/tmp/out",
1514                "-C",
1515                "metadata=abc123",
1516                "-C",
1517                "mystery-flag",
1518            ]))
1519            .expect("parser should succeed");
1520
1521            assert!(!parsed.is_cacheable());
1522            assert!(parsed.has_custom_codegen);
1523        });
1524    }
1525
1526    #[test]
1527    #[serial_test::serial]
1528    fn debug_profile_defaults_are_cacheable() {
1529        with_clean_rustc_env(|| {
1530            let parsed = ParsedRustcArgs::parse(&args(&[
1531                "--crate-name",
1532                "unicode_ident",
1533                "--crate-type",
1534                "lib",
1535                "--target",
1536                "aarch64-apple-darwin",
1537                "--out-dir",
1538                "/tmp/out",
1539                "-C",
1540                "metadata=fd828ed38da2eccd",
1541                "-C",
1542                "extra-filename=-d1c4311f6644f7fa",
1543                "-C",
1544                "split-debuginfo=unpacked",
1545            ]))
1546            .expect("parser should succeed");
1547
1548            assert!(parsed.is_cacheable());
1549            assert!(!parsed.has_custom_codegen);
1550        });
1551    }
1552
1553    #[test]
1554    #[serial_test::serial]
1555    fn implicit_host_target_is_cacheable() {
1556        with_clean_rustc_env(|| {
1557            let parsed = ParsedRustcArgs::parse(&args(&[
1558                "--crate-name",
1559                "byteorder",
1560                "--crate-type",
1561                "lib",
1562                "--out-dir",
1563                "/tmp/out",
1564                "-C",
1565                "metadata=979b12f2cebb9d65",
1566                "-C",
1567                "extra-filename=-9c7162ae9201c5e4",
1568            ]))
1569            .expect("parser should succeed");
1570
1571            assert!(parsed.is_cacheable());
1572        });
1573    }
1574
1575    #[test]
1576    #[serial_test::serial]
1577    fn optimized_dependency_is_still_restorable_for_ci_capture() {
1578        with_clean_rustc_env(|| {
1579            let parsed = ParsedRustcArgs::parse(&args(&[
1580                "--crate-name",
1581                "regex",
1582                "--crate-type",
1583                "lib",
1584                "--target",
1585                "aarch64-apple-darwin",
1586                "--out-dir",
1587                "/tmp/out",
1588                "--json",
1589                "diagnostic-rendered-ansi,artifacts,future-incompat",
1590                "-C",
1591                "metadata=regex123",
1592                "-C",
1593                "extra-filename=-regex123",
1594                "-C",
1595                "opt-level=3",
1596                "-C",
1597                "debug-assertions=yes",
1598            ]))
1599            .expect("parser should succeed");
1600
1601            assert!(parsed.is_restorable_artifact());
1602            assert!(parsed.is_cacheable());
1603            assert!(parsed.requests_json_artifact_notifications());
1604        });
1605    }
1606
1607    #[test]
1608    #[serial_test::serial]
1609    fn dylib_invocation_is_cacheable_in_debug_profile() {
1610        with_clean_rustc_env(|| {
1611            let parsed = ParsedRustcArgs::parse(&args(&[
1612                "--crate-name",
1613                "bevy_dylib",
1614                "--crate-type",
1615                "dylib",
1616                "--target",
1617                "aarch64-apple-darwin",
1618                "--out-dir",
1619                "/tmp/out",
1620                "-C",
1621                "metadata=dylib123",
1622                "-C",
1623                "extra-filename=-dylib123",
1624                "-C",
1625                "opt-level=0",
1626                "-C",
1627                "debug-assertions=yes",
1628            ]))
1629            .expect("parser should succeed");
1630
1631            assert!(parsed.is_cacheable());
1632            assert_eq!(
1633                parsed
1634                    .output_dynamic_library_path()
1635                    .expect("dylib path")
1636                    .expect("dynamic library output"),
1637                PathBuf::from("/tmp/out/libbevy_dylib-dylib123.dylib")
1638            );
1639        });
1640    }
1641
1642    #[test]
1643    #[serial_test::serial]
1644    fn proc_macro_metadata_output_uses_rmeta_path() {
1645        with_clean_rustc_env(|| {
1646            let parsed = ParsedRustcArgs::parse(&args(&[
1647                "--crate-name",
1648                "pest_derive",
1649                "--crate-type",
1650                "proc-macro",
1651                "--target",
1652                "aarch64-apple-darwin",
1653                "--out-dir",
1654                "/tmp/out",
1655                "--emit",
1656                "dep-info,metadata",
1657                "-C",
1658                "metadata=pm123",
1659                "-C",
1660                "extra-filename=-pm123",
1661            ]))
1662            .expect("parser should succeed");
1663
1664            assert_eq!(
1665                parsed.output_rmeta_path().expect("proc-macro rmeta path"),
1666                PathBuf::from("/tmp/out/libpest_derive-pm123.rmeta")
1667            );
1668        });
1669    }
1670
1671    #[test]
1672    #[serial_test::serial]
1673    fn static_lib_crate_types_do_not_report_dynamic_library_output() {
1674        with_clean_rustc_env(|| {
1675            let parsed = ParsedRustcArgs::parse(&args(&[
1676                "--crate-name",
1677                "unicode_ident",
1678                "--crate-type",
1679                "lib",
1680                "--target",
1681                "aarch64-apple-darwin",
1682                "--out-dir",
1683                "/tmp/out",
1684                "--emit",
1685                "dep-info,metadata,link",
1686                "-C",
1687                "metadata=lib123",
1688                "-C",
1689                "extra-filename=-lib123",
1690            ]))
1691            .expect("parser should succeed");
1692
1693            assert!(parsed.produces_rlib());
1694            assert!(!parsed.produces_dynamic_library());
1695            assert_eq!(
1696                parsed
1697                    .output_dynamic_library_path()
1698                    .expect("dynamic library path"),
1699                None
1700            );
1701            assert_eq!(
1702                parsed.output_link_path().expect("link path"),
1703                Some(PathBuf::from("/tmp/out/libunicode_ident-lib123.rlib"))
1704            );
1705        });
1706    }
1707
1708    #[test]
1709    #[serial_test::serial]
1710    fn build_script_paths_match_cargo_naming() {
1711        with_clean_rustc_env(|| {
1712            let parsed = ParsedRustcArgs::parse(&args(&[
1713                "--crate-name",
1714                "build_script_build",
1715                "--crate-type",
1716                "bin",
1717                "--out-dir",
1718                "/tmp/out",
1719                "-C",
1720                "metadata=e8a2e5854cc9f44b",
1721                "-C",
1722                "extra-filename=-fbf81822541b190b",
1723            ]))
1724            .expect("parser should succeed");
1725
1726            assert!(parsed.is_build_script());
1727            assert_eq!(
1728                parsed.output_binary_path().expect("build script output"),
1729                Path::new("/tmp/out").join(format!(
1730                    "build_script_build-fbf81822541b190b{}",
1731                    std::env::consts::EXE_SUFFIX
1732                ))
1733            );
1734            assert_eq!(
1735                parsed
1736                    .build_script_alias_path()
1737                    .expect("build script alias"),
1738                Path::new("/tmp/out").join(format!(
1739                    "build-script-build{}",
1740                    std::env::consts::EXE_SUFFIX
1741                ))
1742            );
1743        });
1744    }
1745
1746    #[test]
1747    #[serial_test::serial]
1748    fn remap_rustflags_do_not_disable_cacheability() {
1749        unsafe {
1750            std::env::set_var(
1751                "RUSTFLAGS",
1752                "--remap-path-prefix=/tmp/work=stow-ci://workspace",
1753            );
1754        }
1755        let parsed = ParsedRustcArgs::parse(&args(&[
1756            "--crate-name",
1757            "cfg_if",
1758            "--crate-type",
1759            "lib",
1760            "--target",
1761            "aarch64-apple-darwin",
1762            "--out-dir",
1763            "/tmp/out",
1764            "-C",
1765            "metadata=cfgif123",
1766        ]))
1767        .expect("parser should succeed");
1768        unsafe {
1769            std::env::remove_var("RUSTFLAGS");
1770        }
1771
1772        assert!(parsed.is_cacheable());
1773        assert!(!parsed.has_custom_codegen);
1774    }
1775
1776    #[test]
1777    #[serial_test::serial]
1778    fn link_flag_env_rustflags_do_not_disable_cacheability() {
1779        for value in [
1780            "-C link-arg=-fuse-ld=mold",
1781            "-Clink-arg=-fuse-ld=mold",
1782            "--codegen=link-arg=-fuse-ld=mold",
1783            "--remap-path-prefix=/tmp/work=stow-ci://workspace -C linker=clang",
1784        ] {
1785            unsafe {
1786                std::env::set_var("RUSTFLAGS", value);
1787            }
1788            let parsed = ParsedRustcArgs::parse(&args(&[
1789                "--crate-name",
1790                "cfg_if",
1791                "--crate-type",
1792                "lib",
1793                "--target",
1794                "x86_64-unknown-linux-gnu",
1795                "--out-dir",
1796                "/tmp/out",
1797                "-C",
1798                "metadata=cfgif123",
1799            ]))
1800            .expect("parser should succeed");
1801            unsafe {
1802                std::env::remove_var("RUSTFLAGS");
1803            }
1804
1805            assert!(parsed.is_cacheable(), "{value} made unit uncacheable");
1806            assert!(!parsed.has_custom_codegen, "{value} marked custom");
1807        }
1808    }
1809
1810    #[test]
1811    #[serial_test::serial]
1812    fn mixed_env_rustflags_still_disable_cacheability() {
1813        unsafe {
1814            std::env::set_var(
1815                "RUSTFLAGS",
1816                "-C link-arg=-fuse-ld=mold -C target-cpu=native",
1817            );
1818        }
1819        let parsed = ParsedRustcArgs::parse(&args(&[
1820            "--crate-name",
1821            "cfg_if",
1822            "--crate-type",
1823            "lib",
1824            "--target",
1825            "x86_64-unknown-linux-gnu",
1826            "--out-dir",
1827            "/tmp/out",
1828            "-C",
1829            "metadata=cfgif123",
1830        ]))
1831        .expect("parser should succeed");
1832        unsafe {
1833            std::env::remove_var("RUSTFLAGS");
1834        }
1835
1836        assert!(!parsed.is_cacheable());
1837        assert!(parsed.has_custom_codegen);
1838    }
1839
1840    #[test]
1841    #[serial_test::serial]
1842    fn custom_env_rustflags_disable_cacheability() {
1843        unsafe {
1844            std::env::set_var("RUSTFLAGS", "-C target-cpu=native");
1845        }
1846        let parsed = ParsedRustcArgs::parse(&args(&[
1847            "--crate-name",
1848            "cfg_if",
1849            "--crate-type",
1850            "lib",
1851            "--target",
1852            "aarch64-apple-darwin",
1853            "--out-dir",
1854            "/tmp/out",
1855            "-C",
1856            "metadata=cfgif123",
1857        ]))
1858        .expect("parser should succeed");
1859        unsafe {
1860            std::env::remove_var("RUSTFLAGS");
1861        }
1862
1863        assert!(!parsed.is_cacheable());
1864        assert!(parsed.has_custom_codegen);
1865    }
1866
1867    #[test]
1868    #[serial_test::serial]
1869    fn parses_json_artifact_requests() {
1870        with_clean_rustc_env(|| {
1871            let parsed = ParsedRustcArgs::parse(&args(&[
1872                "--crate-name",
1873                "itoa",
1874                "--crate-type",
1875                "lib",
1876                "--out-dir",
1877                "/tmp/out",
1878                "--json=diagnostic-rendered-ansi,artifacts,future-incompat",
1879                "-C",
1880                "metadata=json123",
1881            ]))
1882            .expect("parser should succeed");
1883
1884            assert!(parsed.requests_json_artifact_notifications());
1885            assert!(parsed.json.contains("diagnostic-rendered-ansi"));
1886            assert!(parsed.json.contains("future-incompat"));
1887        });
1888    }
1889
1890    #[test]
1891    #[serial_test::serial]
1892    fn parses_extern_crates() {
1893        with_clean_rustc_env(|| {
1894            let parsed = ParsedRustcArgs::parse(&args(&[
1895                "--crate-name",
1896                "grep_searcher",
1897                "--crate-type",
1898                "lib",
1899                "--out-dir",
1900                "/tmp/out",
1901                "--extern",
1902                "memchr=/tmp/deps/libmemchr-aaaaaaaaaaaaaaaa.rmeta",
1903                "--extern=regex_automata=/tmp/deps/libregex_automata-bbbbbbbbbbbbbbbb.rmeta",
1904                "-C",
1905                "metadata=grepsearcher123",
1906            ]))
1907            .expect("parser should succeed");
1908
1909            assert_eq!(parsed.extern_crates.len(), 2);
1910            assert_eq!(parsed.extern_crates[0].crate_name, "memchr");
1911            assert_eq!(
1912                parsed.extern_crates[0].path,
1913                PathBuf::from("/tmp/deps/libmemchr-aaaaaaaaaaaaaaaa.rmeta")
1914            );
1915            assert_eq!(parsed.extern_crates[1].crate_name, "regex_automata");
1916        });
1917    }
1918
1919    #[test]
1920    fn strip_level_is_part_of_the_profile() {
1921        let parsed = ParsedRustcArgs::parse(&args(&[
1922            "--crate-name",
1923            "itoa",
1924            "--crate-type",
1925            "rlib",
1926            "--out-dir",
1927            "/tmp/out",
1928            "-C",
1929            "metadata=abc123",
1930            "-C",
1931            "strip=debuginfo",
1932        ]))
1933        .expect("parser should succeed");
1934        assert!(!parsed.has_custom_codegen);
1935        assert_eq!(parsed.strip.as_deref(), Some("debuginfo"));
1936        assert_eq!(
1937            parsed.profile().expect("profile").strip,
1938            StripLevel::Debuginfo
1939        );
1940
1941        let error = ParsedRustcArgs::parse(&args(&["--crate-name", "itoa", "-C", "strip=all"]))
1942            .expect("parser should succeed")
1943            .profile()
1944            .expect_err("unknown strip level must fail");
1945        assert!(error.contains("strip"), "{error}");
1946    }
1947}