Skip to main content

nextest_runner/list/
binary_list.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    errors::{FromMessagesError, RustBuildMetaParseError, WriteTestListError},
6    helpers::convert_rel_path_to_forward_slash,
7    list::{BinaryListState, OutputFormat, RustBuildMeta, Styles},
8    platform::BuildPlatforms,
9    write_str::WriteStr,
10};
11use camino::{Utf8Path, Utf8PathBuf};
12use cargo_metadata::{Artifact, BuildScript, Message, PackageId, TargetKind};
13use guppy::graph::PackageGraph;
14use nextest_metadata::{
15    BinaryListSummary, BuildPlatform, RustBinaryId, RustNonTestBinaryKind,
16    RustNonTestBinarySummary, RustTestBinaryKind, RustTestBinarySummary,
17};
18use owo_colors::OwoColorize;
19use serde::Deserialize;
20use std::{
21    collections::{BTreeMap, HashSet},
22    io,
23};
24use tracing::{debug, warn};
25
26/// A Rust test binary built by Cargo.
27#[derive(Clone, Debug)]
28pub struct RustTestBinary {
29    /// A unique ID.
30    pub id: RustBinaryId,
31    /// The path to the binary artifact.
32    pub path: Utf8PathBuf,
33    /// The package this artifact belongs to.
34    pub package_id: String,
35    /// The kind of Rust test binary this is.
36    pub kind: RustTestBinaryKind,
37    /// The unique binary name defined in `Cargo.toml` or inferred by the filename.
38    pub name: String,
39    /// Platform for which this binary was built.
40    /// (Proc-macro tests are built for the host.)
41    pub build_platform: BuildPlatform,
42}
43
44/// The list of Rust test binaries built by Cargo.
45#[derive(Clone, Debug)]
46pub struct BinaryList {
47    /// Rust-related metadata.
48    pub rust_build_meta: RustBuildMeta<BinaryListState>,
49
50    /// The list of test binaries.
51    pub rust_binaries: Vec<RustTestBinary>,
52}
53
54impl BinaryList {
55    /// Parses Cargo messages from the given `BufRead` and returns a list of test binaries.
56    pub fn from_messages(
57        reader: impl io::BufRead,
58        graph: &PackageGraph,
59        build_platforms: BuildPlatforms,
60    ) -> Result<Self, FromMessagesError> {
61        let mut builder = BinaryListBuilder::new(graph, build_platforms);
62
63        for message in Message::parse_stream(reader) {
64            let message = message.map_err(FromMessagesError::ReadMessages)?;
65            builder.process_message(message)?;
66        }
67
68        Ok(builder.finish())
69    }
70
71    /// Constructs the list from its summary format
72    pub fn from_summary(summary: BinaryListSummary) -> Result<Self, RustBuildMetaParseError> {
73        let rust_binaries = summary
74            .rust_binaries
75            .into_values()
76            .map(|bin| RustTestBinary {
77                name: bin.binary_name,
78                path: bin.binary_path,
79                package_id: bin.package_id,
80                kind: bin.kind,
81                id: bin.binary_id,
82                build_platform: bin.build_platform,
83            })
84            .collect();
85        Ok(Self {
86            rust_build_meta: RustBuildMeta::from_summary(summary.rust_build_meta)?,
87            rust_binaries,
88        })
89    }
90
91    /// Outputs this list to the given writer.
92    pub fn write(
93        &self,
94        output_format: OutputFormat,
95        writer: &mut dyn WriteStr,
96        colorize: bool,
97    ) -> Result<(), WriteTestListError> {
98        match output_format {
99            OutputFormat::Human { verbose } => self
100                .write_human(writer, verbose, colorize)
101                .map_err(WriteTestListError::Io),
102            OutputFormat::Oneline { verbose } => self
103                .write_oneline(writer, verbose, colorize)
104                .map_err(WriteTestListError::Io),
105            OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
106        }
107    }
108
109    fn to_summary(&self) -> BinaryListSummary {
110        BinaryListSummary {
111            rust_build_meta: self.rust_build_meta.to_summary(),
112            rust_binaries: self.binary_summaries(),
113        }
114    }
115
116    /// Produces a summary suitable for archive metadata.
117    ///
118    /// * `build_directory` is omitted so it defaults to `target_directory` on
119    ///   extraction.
120    /// * Binary paths under `build_directory` are remapped to `target_directory`
121    ///   so the `PathMapper` can remap them correctly on extraction.
122    pub(crate) fn to_archive_summary(&self) -> BinaryListSummary {
123        let target_dir = &self.rust_build_meta.target_directory;
124        let build_directory = &self.rust_build_meta.build_directory;
125
126        let rust_binaries = self
127            .rust_binaries
128            .iter()
129            .map(|bin| {
130                // In the archive, test binaries are stored under target/.
131                // Remap paths from build_directory to target_directory so the PathMapper
132                // can relocate them on extraction.
133                let binary_path = target_dir.join(
134                    bin.path
135                        .strip_prefix(build_directory)
136                        .expect("test binary paths must be within the build directory"),
137                );
138                let summary = RustTestBinarySummary {
139                    binary_name: bin.name.clone(),
140                    package_id: bin.package_id.clone(),
141                    kind: bin.kind.clone(),
142                    binary_path,
143                    binary_id: bin.id.clone(),
144                    build_platform: bin.build_platform,
145                };
146                (bin.id.clone(), summary)
147            })
148            .collect();
149
150        BinaryListSummary {
151            rust_build_meta: self.rust_build_meta.to_archive_summary(),
152            rust_binaries,
153        }
154    }
155
156    fn binary_summaries(&self) -> BTreeMap<RustBinaryId, RustTestBinarySummary> {
157        self.rust_binaries
158            .iter()
159            .map(|bin| {
160                let summary = RustTestBinarySummary {
161                    binary_name: bin.name.clone(),
162                    package_id: bin.package_id.clone(),
163                    kind: bin.kind.clone(),
164                    binary_path: bin.path.clone(),
165                    binary_id: bin.id.clone(),
166                    build_platform: bin.build_platform,
167                };
168                (bin.id.clone(), summary)
169            })
170            .collect()
171    }
172
173    fn write_human(
174        &self,
175        writer: &mut dyn WriteStr,
176        verbose: bool,
177        colorize: bool,
178    ) -> io::Result<()> {
179        let mut styles = Styles::default();
180        if colorize {
181            styles.colorize();
182        }
183        for bin in &self.rust_binaries {
184            if verbose {
185                writeln!(writer, "{}:", bin.id.style(styles.binary_id))?;
186                writeln!(writer, "  {} {}", "bin:".style(styles.field), bin.path)?;
187                writeln!(
188                    writer,
189                    "  {} {}",
190                    "build platform:".style(styles.field),
191                    bin.build_platform,
192                )?;
193            } else {
194                writeln!(writer, "{}", bin.id.style(styles.binary_id))?;
195            }
196        }
197        Ok(())
198    }
199
200    fn write_oneline(
201        &self,
202        writer: &mut dyn WriteStr,
203        verbose: bool,
204        colorize: bool,
205    ) -> io::Result<()> {
206        let mut styles = Styles::default();
207        if colorize {
208            styles.colorize();
209        }
210        for bin in &self.rust_binaries {
211            write!(writer, "{}", bin.id.style(styles.binary_id))?;
212            if verbose {
213                write!(
214                    writer,
215                    " [{}{}] [{}{}]",
216                    "bin: ".style(styles.field),
217                    bin.path,
218                    "build platform: ".style(styles.field),
219                    bin.build_platform,
220                )?;
221            }
222            writeln!(writer)?;
223        }
224        Ok(())
225    }
226
227    /// Outputs this list as a string with the given format.
228    pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
229        let mut s = String::with_capacity(1024);
230        self.write(output_format, &mut s, false)?;
231        Ok(s)
232    }
233}
234
235/// Incrementally builds a [`BinaryList`] from Cargo messages.
236#[derive(Debug)]
237pub struct BinaryListBuilder<'g> {
238    state: BinaryListBuildState<'g>,
239}
240
241impl<'g> BinaryListBuilder<'g> {
242    /// Creates a new builder for Cargo messages.
243    pub fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
244        Self {
245            state: BinaryListBuildState::new(graph, build_platforms),
246        }
247    }
248
249    /// Processes a single Cargo message.
250    pub fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
251        self.state.process_message(message)
252    }
253
254    /// Processes a single line of Cargo output.
255    ///
256    /// This uses the same single-line parsing behavior as
257    /// [`cargo_metadata::Message::parse_stream`].
258    pub fn process_message_line(&mut self, line: &str) -> Result<(), FromMessagesError> {
259        self.process_message(parse_message_line(line))
260    }
261
262    /// Finishes building the binary list.
263    pub fn finish(self) -> BinaryList {
264        self.state.finish()
265    }
266}
267
268// Adapted from cargo_metadata::MessageIter::next (cargo_metadata 0.23.1).
269fn parse_message_line(line: &str) -> Message {
270    let mut deserializer = serde_json::Deserializer::from_str(line);
271    deserializer.disable_recursion_limit();
272    Message::deserialize(&mut deserializer).unwrap_or_else(|_| Message::TextLine(line.to_owned()))
273}
274
275#[derive(Debug)]
276struct BinaryListBuildState<'g> {
277    graph: &'g PackageGraph,
278    rust_binaries: Vec<RustTestBinary>,
279    rust_build_meta: RustBuildMeta<BinaryListState>,
280    alt_target_dir: Option<Utf8PathBuf>,
281}
282
283impl<'g> BinaryListBuildState<'g> {
284    fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
285        let rust_target_dir = graph.workspace().target_directory().to_path_buf();
286        // Use the build directory if on Cargo 1.91 or newer. Fall back to
287        // the target directory for older Cargo versions.
288        let build_directory = graph
289            .workspace()
290            .build_directory()
291            .unwrap_or_else(|| graph.workspace().target_directory())
292            .to_path_buf();
293        // For testing only, not part of the public API.
294        let alt_target_dir = std::env::var("__NEXTEST_ALT_TARGET_DIR")
295            .ok()
296            .map(Utf8PathBuf::from);
297
298        Self {
299            graph,
300            rust_binaries: vec![],
301            rust_build_meta: RustBuildMeta::new(rust_target_dir, build_directory, build_platforms),
302            alt_target_dir,
303        }
304    }
305
306    fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
307        match message {
308            Message::CompilerArtifact(artifact) => {
309                self.process_artifact(artifact)?;
310            }
311            Message::BuildScriptExecuted(build_script) => {
312                self.process_build_script(build_script)?;
313            }
314            _ => {
315                // Ignore all other messages.
316            }
317        }
318
319        Ok(())
320    }
321
322    fn process_artifact(&mut self, artifact: Artifact) -> Result<(), FromMessagesError> {
323        if let Some(path) = artifact.executable {
324            self.detect_base_output_dir(&path);
325
326            if artifact.profile.test {
327                let package_id = artifact.package_id.repr;
328
329                // Look up the executable by package ID.
330
331                let name = artifact.target.name;
332
333                let package = self
334                    .graph
335                    .metadata(&guppy::PackageId::new(package_id.clone()))
336                    .map_err(FromMessagesError::PackageGraph)?;
337
338                let kind = artifact.target.kind;
339                if kind.is_empty() {
340                    return Err(FromMessagesError::MissingTargetKind {
341                        package_name: package.name().to_owned(),
342                        binary_name: name.clone(),
343                    });
344                }
345
346                let (computed_kind, platform) = if kind.iter().any(|k| {
347                    // https://doc.rust-lang.org/nightly/cargo/reference/cargo-targets.html#the-crate-type-field
348                    matches!(
349                        k,
350                        TargetKind::Lib
351                            | TargetKind::RLib
352                            | TargetKind::DyLib
353                            | TargetKind::CDyLib
354                            | TargetKind::StaticLib
355                    )
356                }) {
357                    (RustTestBinaryKind::LIB, BuildPlatform::Target)
358                } else if let Some(TargetKind::ProcMacro) = kind.first() {
359                    (RustTestBinaryKind::PROC_MACRO, BuildPlatform::Host)
360                } else {
361                    // Non-lib kinds should always have just one element. Grab the first one.
362                    (
363                        RustTestBinaryKind::new(
364                            kind.into_iter()
365                                .next()
366                                .expect("already checked that kind is non-empty")
367                                .to_string(),
368                        ),
369                        BuildPlatform::Target,
370                    )
371                };
372
373                // Construct the binary ID from the package and build target.
374                let id = RustBinaryId::from_parts(package.name(), &computed_kind, &name);
375
376                self.rust_binaries.push(RustTestBinary {
377                    path,
378                    package_id,
379                    kind: computed_kind,
380                    name,
381                    id,
382                    build_platform: platform,
383                });
384            } else if artifact
385                .target
386                .kind
387                .iter()
388                .any(|x| matches!(x, TargetKind::Bin))
389            {
390                // This is a non-test binary -- add it to the map.
391                // Error case here implies that the returned path wasn't in the target directory -- ignore it
392                // since it shouldn't happen in normal use.
393                if let Ok(rel_path) = path.strip_prefix(&self.rust_build_meta.target_directory) {
394                    let non_test_binary = RustNonTestBinarySummary {
395                        name: artifact.target.name,
396                        kind: RustNonTestBinaryKind::BIN_EXE,
397                        path: convert_rel_path_to_forward_slash(rel_path),
398                        build_platform: self.non_test_build_platform(&path),
399                    };
400
401                    self.rust_build_meta.non_test_binaries.insert(
402                        guppy::PackageId::new(artifact.package_id.repr),
403                        non_test_binary,
404                    );
405                };
406            }
407        } else if artifact
408            .target
409            .kind
410            .iter()
411            .any(|x| matches!(x, TargetKind::DyLib | TargetKind::CDyLib))
412        {
413            // Also look for and grab dynamic libraries to store in archives.
414            for filename in artifact.filenames {
415                if let Ok(rel_path) = filename.strip_prefix(&self.rust_build_meta.target_directory)
416                {
417                    let non_test_binary = RustNonTestBinarySummary {
418                        name: artifact.target.name.clone(),
419                        kind: RustNonTestBinaryKind::DYLIB,
420                        path: convert_rel_path_to_forward_slash(rel_path),
421                        build_platform: self.non_test_build_platform(&filename),
422                    };
423                    self.rust_build_meta.non_test_binaries.insert(
424                        guppy::PackageId::new(artifact.package_id.repr.clone()),
425                        non_test_binary,
426                    );
427                }
428            }
429        }
430
431        Ok(())
432    }
433
434    /// Records the base output directory an artifact was built into.
435    ///
436    /// A base output directory is the `[<triple>/]<profile>` prefix of an
437    /// artifact's path, relative to the build directory. Only these shapes are
438    /// recognized:
439    ///
440    /// * the legacy layout: `debug/deps/test-binary`
441    /// * the legacy layout, for `[[example]]` targets: `debug/examples/test-binary`
442    /// * the build-dir layout v2: `debug/build/my-package/f3694e28990a9310/out/test-binary`
443    ///
444    /// In each case, the base output directory is `debug`. For each one, nextest
445    /// adds two directories to the dynamic library path:
446    ///
447    /// * the Cargo artifact directory, where Cargo uplifts final artifacts to
448    ///   (the base output directory under the *target* directory)
449    /// * the legacy `deps` directory (under the *build* directory)
450    ///
451    /// The `Option` in the return value is to let ? work.
452    fn detect_base_output_dir(&mut self, artifact_path: &Utf8Path) -> Option<()> {
453        // Artifact paths must be relative to the build directory (which
454        // equals the target directory unless Cargo's build.build-dir is
455        // configured).
456        //
457        // Unlike `artifact_build_platform`, which picks whichever is the
458        // innermost of the build and target directories, this resolves against
459        // the build directory and nothing else, because its result goes into
460        // `base_output_directories`, which is relative to the build directory.
461        let rel_path = match artifact_path.strip_prefix(&self.rust_build_meta.build_directory) {
462            Ok(rel) => rel,
463            Err(_) => {
464                debug!(
465                    target: "nextest-runner::list",
466                    "artifact path `{}` is not within the build directory `{}`, \
467                     skipping base output directory detection",
468                    artifact_path, self.rust_build_meta.build_directory,
469                );
470                return None;
471            }
472        };
473
474        let base = base_output_dir(rel_path)?;
475        if !self.rust_build_meta.base_output_directories.contains(base) {
476            self.rust_build_meta
477                .base_output_directories
478                .insert(convert_rel_path_to_forward_slash(base));
479        }
480        Some(())
481    }
482
483    fn non_test_build_platform(&self, artifact_path: &Utf8Path) -> Option<BuildPlatform> {
484        artifact_build_platform(
485            artifact_path,
486            &self.rust_build_meta.target_directory,
487            &self.rust_build_meta.build_directory,
488            self.rust_build_meta
489                .build_platforms
490                .target
491                .as_ref()
492                .map(|target| target.triple.platform.triple_str()),
493        )
494    }
495
496    fn process_build_script(&mut self, build_script: BuildScript) -> Result<(), FromMessagesError> {
497        for path in build_script.linked_paths {
498            self.detect_linked_path(&build_script.package_id, &path);
499        }
500
501        // We only care about build scripts for workspace packages.
502        let package_id = guppy::PackageId::new(build_script.package_id.repr);
503        let in_workspace = self.graph.metadata(&package_id).map_or_else(
504            |_| {
505                debug!(
506                    target: "nextest-runner::list",
507                    "package ID `{}` isn't in cargo metadata, so its build \
508                     script's out_dir and env won't be recorded (this is \
509                     expected for the standard library and its dependencies \
510                     under -Zbuild-std)",
511                    package_id
512                );
513                // Absent packages can't be workspace members.
514                false
515            },
516            |p| p.in_workspace(),
517        );
518        if in_workspace {
519            // Build script out_dirs are relative to the build directory.
520            match build_script
521                .out_dir
522                .strip_prefix(&self.rust_build_meta.build_directory)
523            {
524                Ok(rel_out_dir) => {
525                    self.rust_build_meta.build_script_out_dirs.insert(
526                        package_id.repr().to_owned(),
527                        convert_rel_path_to_forward_slash(rel_out_dir),
528                    );
529                }
530                Err(_) => {
531                    debug!(
532                        target: "nextest-runner::list",
533                        "build script out_dir `{}` for package `{}` is not within \
534                         the build directory `{}`, skipping",
535                        build_script.out_dir, package_id,
536                        self.rust_build_meta.build_directory,
537                    );
538                }
539            }
540
541            // Capture build script environment variables from the structured
542            // cargo message, avoiding the need to parse the raw output file.
543            if !build_script.env.is_empty() {
544                self.rust_build_meta
545                    .build_script_info
546                    .get_or_insert_with(BTreeMap::new)
547                    .entry(package_id.repr().to_owned())
548                    .or_default()
549                    .envs = build_script.env.into_iter().collect();
550            }
551        }
552
553        Ok(())
554    }
555
556    /// The `Option` in the return value is to let ? work.
557    fn detect_linked_path(&mut self, package_id: &PackageId, path: &Utf8Path) -> Option<()> {
558        // Remove anything up to the first "=" (e.g. "native=").
559        let actual_path = match path.as_str().split_once('=') {
560            Some((_, p)) => p.into(),
561            None => path,
562        };
563
564        let rel_path = match actual_path.strip_prefix(&self.rust_build_meta.build_directory) {
565            Ok(rel) => rel,
566            Err(_) => {
567                // For a seeded build (like in our test suite), Cargo will
568                // return:
569                //
570                // * the new path if the linked path exists
571                // * the original path if the linked path does not exist
572                //
573                // Linked paths not existing is not an ordinary condition, but
574                // we want to test it within nextest. We filter out paths if
575                // they're not a subdirectory of the target directory. With
576                // __NEXTEST_ALT_TARGET_DIR, we can simulate that for an
577                // alternate target directory.
578                if let Some(alt_target_dir) = &self.alt_target_dir {
579                    actual_path.strip_prefix(alt_target_dir).ok()?
580                } else {
581                    return None;
582                }
583            }
584        };
585
586        self.rust_build_meta
587            .linked_paths
588            .entry(convert_rel_path_to_forward_slash(rel_path))
589            .or_default()
590            .insert(package_id.repr.clone());
591
592        Some(())
593    }
594
595    fn finish(mut self) -> BinaryList {
596        self.rust_binaries.sort_by(|b1, b2| b1.id.cmp(&b2.id));
597
598        // Clean out any build script output directories for which there's no corresponding binary.
599        let relevant_package_ids = self
600            .rust_binaries
601            .iter()
602            .map(|bin| bin.package_id.clone())
603            .collect::<HashSet<_>>();
604
605        self.rust_build_meta
606            .build_script_out_dirs
607            .retain(|package_id, _| relevant_package_ids.contains(package_id));
608        if let Some(info) = &mut self.rust_build_meta.build_script_info {
609            info.retain(|package_id, _| relevant_package_ids.contains(package_id));
610        }
611
612        // All test binaries live inside a base output dir under both Cargo
613        // layouts, so an empty set suggests that we didn't recognize the
614        // layout. It's worth warning about this.
615        if !self.rust_binaries.is_empty() && self.rust_build_meta.base_output_directories.is_empty()
616        {
617            warn!(
618                target: "nextest-runner::list",
619                "failed to detect any base output directories under the build directory `{}`; \
620                 tests that link against dynamic libraries may fail to start. \
621                 This usually means Cargo's build directory layout changed -- \
622                 please report it at https://github.com/nextest-rs/nextest/issues/new",
623                self.rust_build_meta.build_directory,
624            );
625        }
626
627        BinaryList {
628            rust_build_meta: self.rust_build_meta,
629            rust_binaries: self.rust_binaries,
630        }
631    }
632}
633
634/// Determines the build platform for a particular artifact.
635///
636/// This is a heuristic due to Cargo's lack of platform information in build
637/// message output. See <https://github.com/rust-lang/cargo/issues/12869> for
638/// the Cargo issue.
639fn artifact_build_platform(
640    artifact_path: &Utf8Path,
641    target_directory: &Utf8Path,
642    build_directory: &Utf8Path,
643    target_triple: Option<&str>,
644) -> Option<BuildPlatform> {
645    // If we're not cross compiling, we must be building for the target.
646    let Some(target_triple) = target_triple else {
647        return Some(BuildPlatform::Target);
648    };
649
650    // As of this writing (2026-08), callers ensure that the artifact path is
651    // under either the target directory or the build directory. But we can
652    // reasonably return None here if that assumption doesn't hold.
653    let Some(rel_path) = [target_directory, build_directory]
654        .into_iter()
655        .filter_map(|root| artifact_path.strip_prefix(root).ok())
656        .min_by_key(|rel_path| rel_path.as_str().len())
657    else {
658        debug!(
659            target: "nextest-runner::list",
660            "artifact path `{}` is under neither the target directory `{}` nor the build \
661             directory `{}`, recording its build platform as unknown",
662            artifact_path, target_directory, build_directory,
663        );
664        return None;
665    };
666
667    if rel_path.starts_with(target_triple) {
668        Some(BuildPlatform::Target)
669    } else {
670        Some(BuildPlatform::Host)
671    }
672}
673
674fn base_output_dir(rel_path: &Utf8Path) -> Option<&Utf8Path> {
675    let parent = rel_path.parent()?;
676    let base = match parent.file_name()? {
677        // The legacy layout.
678        //
679        // Test binaries built from `[[example]]` targets go to `examples`
680        // rather than `deps` -- see Cargo's `CompilationFiles::output_dir`.
681        // (Under the v2 layout they go to the `out` directory below, like every
682        // other compilation unit.)
683        "deps" | "examples" => parent.parent()?,
684        // The build-dir v2 layout.
685        "out" => parent
686            .ancestors()
687            // There is a subtle point here: we want to restrict this branch to
688            // the v2 layout.
689            //
690            // With the legacy layout, build script out dirs are of the form
691            // `<base>/build/<package>-<hash>/out`. `nth(3)` would return
692            // `<base>`, whose last component is the profile's directory name.
693            // That name can never be `build`, because:
694            //
695            // * Cargo does not allow profiles to have the name `build`
696            // * `profile.<name>.dir-name`, the only potential way to decouple a
697            //   profile's directory name from the profile name, is currently
698            //   disallowed as of 2026-07.
699            //
700            // So the filter below discards legacy build script out dirs without
701            // also discarding any real base output directory.
702            //
703            // With the v2 layout, out dirs are of the form
704            // `<base>/build/<package>/<hash>/out`. `nth(3)` returns `build`.
705            // This is also the shape of build script out dirs under v2, which
706            // is harmless: they resolve to the same `<base>`.
707            .nth(3)
708            .filter(|dir| dir.file_name() == Some("build"))?
709            .parent()?,
710        _ => return None,
711    };
712
713    // A base output dir is always `[<triple>/]<profile>`, so an empty one means
714    // the path didn't have the shape we expected.
715    (!base.as_str().is_empty()).then_some(base)
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721    use crate::{
722        cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
723        list::{
724            BuildScriptInfo, SerializableFormat,
725            test_helpers::{
726                PACKAGE_GRAPH_FIXTURE, PACKAGE_METADATA_ID, WORKSPACE_PACKAGE_ID, package_metadata,
727            },
728        },
729        platform::{HostPlatform, PlatformLibdir, TargetPlatform},
730    };
731    use indoc::indoc;
732    use maplit::btreeset;
733    use nextest_metadata::PlatformLibdirUnavailable;
734    use pretty_assertions::assert_eq;
735    use serde_json::json;
736    use target_spec::{Platform, TargetFeatures};
737
738    #[test]
739    fn test_parse_binary_list() {
740        let fake_bin_test = RustTestBinary {
741            id: "fake-package::bin/fake-binary".into(),
742            path: "/fake/binary".into(),
743            package_id: "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)"
744                .to_owned(),
745            kind: RustTestBinaryKind::LIB,
746            name: "fake-binary".to_owned(),
747            build_platform: BuildPlatform::Target,
748        };
749        let fake_macro_test = RustTestBinary {
750            id: "fake-macro::proc-macro/fake-macro".into(),
751            path: "/fake/macro".into(),
752            package_id: "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)"
753                .to_owned(),
754            kind: RustTestBinaryKind::PROC_MACRO,
755            name: "fake-macro".to_owned(),
756            build_platform: BuildPlatform::Host,
757        };
758
759        let fake_triple = TargetTriple {
760            platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
761            source: TargetTripleSource::CliOption,
762            location: TargetDefinitionLocation::Builtin,
763        };
764        let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
765        let build_platforms = BuildPlatforms {
766            host: HostPlatform {
767                platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
768                libdir: PlatformLibdir::Available(Utf8PathBuf::from(fake_host_libdir)),
769            },
770            target: Some(TargetPlatform {
771                triple: fake_triple,
772                // Test out the error case for unavailable libdirs.
773                libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
774            }),
775        };
776
777        let mut rust_build_meta =
778            RustBuildMeta::new("/fake/target", "/fake/target", build_platforms);
779        // With a target triple set, a binary built for the target platform
780        // lives under `<triple>/<profile>`, otherwise just under `<profile>`.
781        rust_build_meta
782            .base_output_directories
783            .insert("aarch64-unknown-linux-gnu/my-profile".into());
784        rust_build_meta
785            .base_output_directories
786            .insert("my-profile".into());
787        for non_test_binary in [
788            RustNonTestBinarySummary {
789                name: "my-name".into(),
790                kind: RustNonTestBinaryKind::BIN_EXE,
791                path: "aarch64-unknown-linux-gnu/my-profile/my-name".into(),
792                build_platform: Some(BuildPlatform::Target),
793            },
794            RustNonTestBinarySummary {
795                name: "your-name".into(),
796                kind: RustNonTestBinaryKind::DYLIB,
797                path: "my-profile/your-name.dll".into(),
798                build_platform: Some(BuildPlatform::Host),
799            },
800            RustNonTestBinarySummary {
801                name: "your-name".into(),
802                kind: RustNonTestBinaryKind::DYLIB,
803                path: "my-profile/your-name.exp".into(),
804                build_platform: Some(BuildPlatform::Host),
805            },
806        ] {
807            rust_build_meta
808                .non_test_binaries
809                .insert(guppy::PackageId::new("my-package-id"), non_test_binary);
810        }
811
812        let binary_list = BinaryList {
813            rust_build_meta,
814            rust_binaries: vec![fake_bin_test, fake_macro_test],
815        };
816
817        // Check that the expected outputs are valid.
818        static EXPECTED_HUMAN: &str = indoc! {"
819        fake-package::bin/fake-binary
820        fake-macro::proc-macro/fake-macro
821        "};
822        static EXPECTED_HUMAN_VERBOSE: &str = indoc! {r"
823        fake-package::bin/fake-binary:
824          bin: /fake/binary
825          build platform: target
826        fake-macro::proc-macro/fake-macro:
827          bin: /fake/macro
828          build platform: host
829        "};
830        static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
831        {
832          "rust-build-meta": {
833            "target-directory": "/fake/target",
834            "build-directory": "/fake/target",
835            "base-output-directories": [
836              "aarch64-unknown-linux-gnu/my-profile",
837              "my-profile"
838            ],
839            "non-test-binaries": {
840              "my-package-id": [
841                {
842                  "name": "my-name",
843                  "kind": "bin-exe",
844                  "path": "aarch64-unknown-linux-gnu/my-profile/my-name",
845                  "build-platform": "target"
846                },
847                {
848                  "name": "your-name",
849                  "kind": "dylib",
850                  "path": "my-profile/your-name.dll",
851                  "build-platform": "host"
852                },
853                {
854                  "name": "your-name",
855                  "kind": "dylib",
856                  "path": "my-profile/your-name.exp",
857                  "build-platform": "host"
858                }
859              ]
860            },
861            "build-script-out-dirs": {},
862            "build-script-info": {},
863            "linked-paths": [],
864            "platforms": {
865              "host": {
866                "platform": {
867                  "triple": "x86_64-unknown-linux-gnu",
868                  "target-features": "unknown"
869                },
870                "libdir": {
871                  "status": "available",
872                  "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
873                }
874              },
875              "targets": [
876                {
877                  "platform": {
878                    "triple": "aarch64-unknown-linux-gnu",
879                    "target-features": "unknown"
880                  },
881                  "libdir": {
882                    "status": "unavailable",
883                    "reason": "rustc-output-error"
884                  }
885                }
886              ]
887            },
888            "target-platforms": [
889              {
890                "triple": "aarch64-unknown-linux-gnu",
891                "target-features": "unknown"
892              }
893            ],
894            "target-platform": "aarch64-unknown-linux-gnu"
895          },
896          "rust-binaries": {
897            "fake-macro::proc-macro/fake-macro": {
898              "binary-id": "fake-macro::proc-macro/fake-macro",
899              "binary-name": "fake-macro",
900              "package-id": "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)",
901              "kind": "proc-macro",
902              "binary-path": "/fake/macro",
903              "build-platform": "host"
904            },
905            "fake-package::bin/fake-binary": {
906              "binary-id": "fake-package::bin/fake-binary",
907              "binary-name": "fake-binary",
908              "package-id": "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)",
909              "kind": "lib",
910              "binary-path": "/fake/binary",
911              "build-platform": "target"
912            }
913          }
914        }"#};
915        // Non-verbose oneline is the same as non-verbose human.
916        static EXPECTED_ONELINE: &str = indoc! {"
917            fake-package::bin/fake-binary
918            fake-macro::proc-macro/fake-macro
919        "};
920        static EXPECTED_ONELINE_VERBOSE: &str = indoc! {r"
921            fake-package::bin/fake-binary [bin: /fake/binary] [build platform: target]
922            fake-macro::proc-macro/fake-macro [bin: /fake/macro] [build platform: host]
923        "};
924
925        assert_eq!(
926            binary_list
927                .to_string(OutputFormat::Human { verbose: false })
928                .expect("human succeeded"),
929            EXPECTED_HUMAN
930        );
931        assert_eq!(
932            binary_list
933                .to_string(OutputFormat::Human { verbose: true })
934                .expect("human succeeded"),
935            EXPECTED_HUMAN_VERBOSE
936        );
937        assert_eq!(
938            binary_list
939                .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
940                .expect("json-pretty succeeded"),
941            EXPECTED_JSON_PRETTY
942        );
943        assert_eq!(
944            binary_list
945                .to_string(OutputFormat::Oneline { verbose: false })
946                .expect("oneline succeeded"),
947            EXPECTED_ONELINE
948        );
949        assert_eq!(
950            binary_list
951                .to_string(OutputFormat::Oneline { verbose: true })
952                .expect("oneline verbose succeeded"),
953            EXPECTED_ONELINE_VERBOSE
954        );
955    }
956
957    #[test]
958    fn test_parse_binary_list_from_message_lines() {
959        let build_platforms = BuildPlatforms {
960            host: HostPlatform {
961                platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
962                libdir: PlatformLibdir::Available("/fake/libdir".into()),
963            },
964            target: None,
965        };
966        let package = package_metadata();
967        // The fixture sets build_directory separately from target_directory, so
968        // an artifact resolved against the wrong root fails to produce a base
969        // output directory.
970        let artifact_path = PACKAGE_GRAPH_FIXTURE
971            .workspace()
972            .build_directory()
973            .expect("fixture sets build_directory")
974            .join("debug/deps/metadata_helper-test");
975        let compiler_artifact = artifact_json(
976            package.name(),
977            &["lib"],
978            std::slice::from_ref(&artifact_path),
979            Some(&artifact_path),
980            TestTarget::Yes,
981        );
982        let input = format!("this is not JSON\n{}\n\n", compiler_artifact);
983
984        let from_messages = BinaryList::from_messages(
985            input.as_bytes(),
986            &PACKAGE_GRAPH_FIXTURE,
987            build_platforms.clone(),
988        )
989        .expect("parsing from messages succeeds");
990
991        let mut builder = BinaryListBuilder::new(&PACKAGE_GRAPH_FIXTURE, build_platforms);
992        for line in input.lines() {
993            builder
994                .process_message_line(line)
995                .expect("processing line succeeds");
996        }
997        let from_lines = builder.finish();
998
999        assert_eq!(
1000            from_lines.rust_build_meta.base_output_directories,
1001            btreeset! { Utf8PathBuf::from("debug") },
1002            "base output directory detected from the artifact's executable path"
1003        );
1004
1005        assert_eq!(
1006            from_lines
1007                .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1008                .expect("json-pretty succeeds"),
1009            from_messages
1010                .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1011                .expect("json-pretty succeeds")
1012        );
1013    }
1014
1015    #[test]
1016    fn test_artifact_build_platform() {
1017        static TARGET_DIR: &str = "/w/target";
1018        static TRIPLE: &str = "aarch64-unknown-linux-gnu";
1019
1020        struct Case {
1021            artifact_path: &'static str,
1022            // None here means Cargo's `build.build-dir` is unset, so it equals
1023            // the target directory.
1024            build_directory: Option<&'static str>,
1025            target_triple: Option<&'static str>,
1026            expected: Option<BuildPlatform>,
1027            description: &'static str,
1028        }
1029
1030        let cases = [
1031            Case {
1032                artifact_path: "/w/target/debug/libfoo.so",
1033                build_directory: None,
1034                target_triple: None,
1035                expected: Some(BuildPlatform::Target),
1036                description: "with no target platform the host and the target coincide, and \
1037                    nextest reports target",
1038            },
1039            Case {
1040                artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1041                build_directory: None,
1042                target_triple: Some(TRIPLE),
1043                expected: Some(BuildPlatform::Target),
1044                description: "a dylib uplifted into the target platform's artifact directory",
1045            },
1046            Case {
1047                artifact_path: "/w/target/debug/libfoo.so",
1048                build_directory: None,
1049                target_triple: Some(TRIPLE),
1050                expected: Some(BuildPlatform::Host),
1051                description: "the same dylib, built for the host as a build dependency",
1052            },
1053            Case {
1054                artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/deps/libfoo.rlib",
1055                build_directory: None,
1056                target_triple: Some(TRIPLE),
1057                expected: Some(BuildPlatform::Target),
1058                description: "an rlib that was never uplifted, legacy layout",
1059            },
1060            Case {
1061                artifact_path: "/w/target/debug/deps/libfoo.rlib",
1062                build_directory: None,
1063                target_triple: Some(TRIPLE),
1064                expected: Some(BuildPlatform::Host),
1065                description: "the same rlib built for the host, legacy layout",
1066            },
1067            Case {
1068                artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/build/foo/9e6e6f7b/out/libfoo.rlib",
1069                build_directory: None,
1070                target_triple: Some(TRIPLE),
1071                expected: Some(BuildPlatform::Target),
1072                description: "an rlib that was never uplifted, build-dir layout v2",
1073            },
1074            Case {
1075                artifact_path: "/w/target/debug/build/foo/9e6e6f7b/out/libfoo.rlib",
1076                build_directory: None,
1077                target_triple: Some(TRIPLE),
1078                expected: Some(BuildPlatform::Host),
1079                description: "the same rlib built for the host, build-dir layout v2",
1080            },
1081            Case {
1082                artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1083                build_directory: Some("/w/build"),
1084                target_triple: Some(TRIPLE),
1085                expected: Some(BuildPlatform::Target),
1086                description: "a build directory beside the target directory: uplifts are unaffected",
1087            },
1088            Case {
1089                artifact_path: "/w/target/build/aarch64-unknown-linux-gnu/debug/deps/libfoo.rlib",
1090                build_directory: Some("/w/target/build"),
1091                target_triple: Some(TRIPLE),
1092                expected: Some(BuildPlatform::Target),
1093                description: "a build directory inside the target directory: the build directory is the \
1094                    innermost root, so the triple is still the first component under it",
1095            },
1096            Case {
1097                artifact_path: "/w/target/build/debug/deps/libfoo.rlib",
1098                build_directory: Some("/w/target/build"),
1099                target_triple: Some(TRIPLE),
1100                expected: Some(BuildPlatform::Host),
1101                description: "a build directory inside the target directory, host artifact",
1102            },
1103            Case {
1104                artifact_path: "/w/target/build/aarch64-unknown-linux-gnu/debug/build/foo/9e6e6f7b/out/libfoo.rlib",
1105                build_directory: Some("/w/target/build"),
1106                target_triple: Some(TRIPLE),
1107                expected: Some(BuildPlatform::Target),
1108                description: "the shape Cargo emits under the build-dir v2 layout for \
1109                    `build.build-dir = target/build` plus --target: nesting and v2 at once. \
1110                    Verified by hand against a real nightly build",
1111            },
1112            Case {
1113                artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1114                build_directory: Some("/w/target/build"),
1115                target_triple: Some(TRIPLE),
1116                expected: Some(BuildPlatform::Target),
1117                description: "a build directory inside the target directory: uplifts still go to the \
1118                    target directory, which is the innermost root containing them",
1119            },
1120            Case {
1121                artifact_path: "/w/target/debug/libfoo.so",
1122                build_directory: Some("/w/target/build"),
1123                target_triple: Some(TRIPLE),
1124                expected: Some(BuildPlatform::Host),
1125                description: "a host dylib uplifted alongside a nested build directory. The \
1126                    nested build directory does not contain it, so dropping the target directory \
1127                    from the candidate roots would leave nothing to strip",
1128            },
1129            Case {
1130                artifact_path: "/w/target/debug/deps/aarch64-unknown-linux-gnu/libfoo.so",
1131                build_directory: None,
1132                target_triple: Some(TRIPLE),
1133                expected: Some(BuildPlatform::Host),
1134                description: "only the first component is the triple slot; a triple-named \
1135                    directory deeper in the path means nothing",
1136            },
1137            Case {
1138                artifact_path: "/w/target/debug/libfoo.so",
1139                build_directory: Some("/w"),
1140                target_triple: Some(TRIPLE),
1141                expected: Some(BuildPlatform::Host),
1142                description: "the reverse nesting: `build.build-dir` and `build.target-dir` are \
1143                    independent, so the target directory can sit inside the build directory. It \
1144                    is then the innermost root",
1145            },
1146            Case {
1147                artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1148                build_directory: Some("/w"),
1149                target_triple: Some(TRIPLE),
1150                expected: Some(BuildPlatform::Target),
1151                description: "the reverse nesting, target artifact: resolving against the outer \
1152                    build directory would see `target` first and misreport this as host",
1153            },
1154            Case {
1155                artifact_path: "/w/target/aarch64-unknown-linux-gnu-ilp32/debug/libfoo.so",
1156                build_directory: None,
1157                target_triple: Some(TRIPLE),
1158                expected: Some(BuildPlatform::Host),
1159                description: "a directory whose name merely starts with the triple is not the triple",
1160            },
1161            Case {
1162                artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1163                build_directory: Some("/w/target/aarch"),
1164                target_triple: Some(TRIPLE),
1165                expected: Some(BuildPlatform::Target),
1166                description: "root selection is component-wise, so a build directory that is only \
1167                    a string prefix of the path never wins; a byte-wise prefix test would strip \
1168                    `/w/target/aarch` here and misread the artifact as host",
1169            },
1170            Case {
1171                artifact_path: "/w/target/my-custom-target/debug/libfoo.so",
1172                build_directory: None,
1173                target_triple: Some("my-custom-target"),
1174                expected: Some(BuildPlatform::Target),
1175                description: "a custom JSON target's directory is its file stem, which is also its triple \
1176                    string in nextest",
1177            },
1178            Case {
1179                artifact_path: "/w/target/aarch64-unknown-linux-gnu/libfoo.so",
1180                build_directory: None,
1181                target_triple: Some(TRIPLE),
1182                expected: Some(BuildPlatform::Target),
1183                description: "KNOWN WRONG, pinned so the limitation stays visible: a custom \
1184                    profile named exactly after the triple puts host uplifts at \
1185                    `<profile>/libfoo.so` under the target directory, and the profile component \
1186                    occupies the `<triple>` slot, so a host artifact reads as target. \
1187                    Self-inflicted",
1188            },
1189            Case {
1190                artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1191                build_directory: Some("/w/target/aarch64-unknown-linux-gnu"),
1192                target_triple: Some(TRIPLE),
1193                expected: Some(BuildPlatform::Host),
1194                description: "KNOWN WRONG, pinned so the limitation stays visible: a build \
1195                    directory nested in the target directory and named after the triple becomes \
1196                    the innermost root and swallows the triple component, so an uplifted target \
1197                    artifact reads as host. Self-inflicted, like the triple-named custom profile \
1198                    above",
1199            },
1200            Case {
1201                artifact_path: "/elsewhere/debug/libfoo.so",
1202                build_directory: None,
1203                target_triple: Some(TRIPLE),
1204                expected: None,
1205                description: "a path under neither root has no `<triple>` slot to read, so the \
1206                    platform is unknown",
1207            },
1208        ];
1209
1210        for case in cases {
1211            let build_directory = case.build_directory.unwrap_or(TARGET_DIR);
1212            assert_eq!(
1213                artifact_build_platform(
1214                    Utf8Path::new(case.artifact_path),
1215                    Utf8Path::new(TARGET_DIR),
1216                    Utf8Path::new(build_directory),
1217                    case.target_triple,
1218                ),
1219                case.expected,
1220                "{}: build platform for {} (build directory {build_directory})",
1221                case.description,
1222                case.artifact_path,
1223            );
1224        }
1225    }
1226
1227    fn cross_build_platforms() -> BuildPlatforms {
1228        let target_triple = TargetTriple {
1229            platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown)
1230                .expect("aarch64-unknown-linux-gnu is a builtin triple"),
1231            source: TargetTripleSource::CliOption,
1232            location: TargetDefinitionLocation::Builtin,
1233        };
1234        BuildPlatforms {
1235            host: HostPlatform {
1236                platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
1237                libdir: PlatformLibdir::Available("/fake/host/libdir".into()),
1238            },
1239            target: Some(TargetPlatform::new(
1240                target_triple,
1241                PlatformLibdir::Available("/fake/target/libdir".into()),
1242            )),
1243        }
1244    }
1245
1246    #[test]
1247    fn test_non_test_build_platform_resolves_against_the_build_directory() {
1248        let mut state = BinaryListBuildState::new(&PACKAGE_GRAPH_FIXTURE, cross_build_platforms());
1249        state.rust_build_meta.target_directory = "/w/target".into();
1250        state.rust_build_meta.build_directory = "/w/target/build".into();
1251
1252        assert_eq!(
1253            state.non_test_build_platform(Utf8Path::new(
1254                "/w/target/build/aarch64-unknown-linux-gnu/debug/deps/libfoo.rlib"
1255            )),
1256            Some(BuildPlatform::Target),
1257            "the nested build directory is the innermost root, so the triple is the first \
1258             component under it; resolving against the target directory instead would see \
1259             `build` first and misreport this as host"
1260        );
1261    }
1262
1263    #[test]
1264    fn test_non_test_binaries_record_the_build_platform() {
1265        let build_platforms = cross_build_platforms();
1266
1267        let workspace = PACKAGE_GRAPH_FIXTURE.workspace();
1268        let target_dir = workspace.target_directory();
1269        let build_dir = workspace
1270            .build_directory()
1271            .expect("fixture sets build_directory");
1272
1273        // This simulates `cargo test --no-run --target
1274        // aarch64-unknown-linux-gnu` for a crate with `crate-type = ["dylib",
1275        // "rlib"]` reachable through both a build dependency (i.e. host) and a
1276        // regular dependency (target). Cargo compiles such libraries twice, and
1277        // uplifts each build's dylib into the build's artifact directory, but
1278        // leaves the rlibs in the build directory.
1279        let input = [
1280            dylib_artifact_json(
1281                "shared",
1282                &[
1283                    target_dir.join("debug/libshared.so"),
1284                    build_dir.join("debug/deps/libshared.rlib"),
1285                ],
1286            ),
1287            dylib_artifact_json(
1288                "shared",
1289                &[
1290                    target_dir.join("aarch64-unknown-linux-gnu/debug/libshared.so"),
1291                    build_dir.join("aarch64-unknown-linux-gnu/debug/deps/libshared.rlib"),
1292                ],
1293            ),
1294            bin_artifact_json(
1295                "mainbin",
1296                &target_dir.join("aarch64-unknown-linux-gnu/debug/mainbin"),
1297            ),
1298        ]
1299        .map(|artifact| artifact.to_string())
1300        .join("\n");
1301
1302        let binary_list =
1303            BinaryList::from_messages(input.as_bytes(), &PACKAGE_GRAPH_FIXTURE, build_platforms)
1304                .expect("parsing from messages succeeds");
1305
1306        assert_eq!(
1307            binary_list.rust_build_meta.non_test_binaries.to_summary(),
1308            BTreeMap::from([(
1309                PACKAGE_METADATA_ID.to_owned(),
1310                btreeset! {
1311                    RustNonTestBinarySummary {
1312                        name: "mainbin".to_owned(),
1313                        kind: RustNonTestBinaryKind::BIN_EXE,
1314                        path: "aarch64-unknown-linux-gnu/debug/mainbin".into(),
1315                        build_platform: Some(BuildPlatform::Target),
1316                    },
1317                    RustNonTestBinarySummary {
1318                        name: "shared".to_owned(),
1319                        kind: RustNonTestBinaryKind::DYLIB,
1320                        path: "aarch64-unknown-linux-gnu/debug/libshared.so".into(),
1321                        build_platform: Some(BuildPlatform::Target),
1322                    },
1323                    RustNonTestBinarySummary {
1324                        name: "shared".to_owned(),
1325                        kind: RustNonTestBinaryKind::DYLIB,
1326                        path: "debug/libshared.so".into(),
1327                        build_platform: Some(BuildPlatform::Host),
1328                    },
1329                },
1330            )]),
1331            "the two builds of one dylib target are told apart by the triple component of their \
1332             uplifted paths; the rlibs live in the fixture's build directory, which is not under \
1333             the target directory, so they are not recorded at all"
1334        );
1335    }
1336
1337    #[test]
1338    fn test_build_scripts_recorded_only_for_workspace_packages() {
1339        let build_dir = PACKAGE_GRAPH_FIXTURE
1340            .workspace()
1341            .build_directory()
1342            .expect("fixture sets build_directory");
1343        let std_id = "path+file:///fake/sysroot/lib/rustlib/src/rust/library/std#0.0.0";
1344
1345        let build_platforms =
1346            BuildPlatforms::new_with_no_target().expect("host platform is detected");
1347        let mut state = BinaryListBuildState::new(&PACKAGE_GRAPH_FIXTURE, build_platforms);
1348        for (package_id, name) in [
1349            (WORKSPACE_PACKAGE_ID, "workspace"),
1350            (PACKAGE_METADATA_ID, "non-workspace"),
1351            (std_id, "not-in-graph"),
1352        ] {
1353            let linked_path = build_dir.join(format!("debug/build/{name}-link"));
1354            let message = json!({
1355                "reason": "build-script-executed",
1356                "package_id": package_id,
1357                "linked_libs": [],
1358                "linked_paths": [format!("native={linked_path}")],
1359                "cfgs": [],
1360                "env": [["BUILD_SCRIPT_ENV", name]],
1361                "out_dir": build_dir.join(format!("debug/build/{name}/out")),
1362            });
1363            let message = serde_json::from_value::<Message>(message)
1364                .expect("message is a valid build-script-executed message");
1365            state
1366                .process_message(message)
1367                .expect("processing build script message succeeds");
1368        }
1369
1370        let expected_out_dirs = BTreeMap::from([(
1371            WORKSPACE_PACKAGE_ID.to_owned(),
1372            Utf8PathBuf::from("debug/build/workspace/out"),
1373        )]);
1374        let expected_info = BTreeMap::from([(
1375            WORKSPACE_PACKAGE_ID.to_owned(),
1376            BuildScriptInfo {
1377                envs: BTreeMap::from([("BUILD_SCRIPT_ENV".to_owned(), "workspace".to_owned())]),
1378            },
1379        )]);
1380        let expected_linked_paths = BTreeMap::from([
1381            (
1382                Utf8PathBuf::from("debug/build/workspace-link"),
1383                btreeset! { WORKSPACE_PACKAGE_ID.to_owned() },
1384            ),
1385            (
1386                Utf8PathBuf::from("debug/build/non-workspace-link"),
1387                btreeset! { PACKAGE_METADATA_ID.to_owned() },
1388            ),
1389            (
1390                Utf8PathBuf::from("debug/build/not-in-graph-link"),
1391                btreeset! { std_id.to_owned() },
1392            ),
1393        ]);
1394
1395        let meta = &state.rust_build_meta;
1396        assert_eq!(
1397            meta.build_script_out_dirs, expected_out_dirs,
1398            "only the workspace package has its out_dir recorded"
1399        );
1400        assert_eq!(
1401            meta.build_script_info,
1402            Some(expected_info),
1403            "only the workspace package has its env recorded"
1404        );
1405        assert_eq!(
1406            meta.linked_paths, expected_linked_paths,
1407            "linked paths are recorded for every package, including ones absent from \
1408             cargo metadata (e.g. standard library crates built with -Zbuild-std)"
1409        );
1410    }
1411
1412    #[derive(Clone, Copy)]
1413    enum TestTarget {
1414        Yes,
1415        No,
1416    }
1417
1418    impl TestTarget {
1419        fn as_bool(self) -> bool {
1420            match self {
1421                Self::Yes => true,
1422                Self::No => false,
1423            }
1424        }
1425    }
1426
1427    fn bin_artifact_json(name: &str, path: &Utf8Path) -> serde_json::Value {
1428        artifact_json(
1429            name,
1430            &["bin"],
1431            &[path.to_owned()],
1432            Some(path),
1433            TestTarget::No,
1434        )
1435    }
1436
1437    fn dylib_artifact_json(name: &str, filenames: &[Utf8PathBuf]) -> serde_json::Value {
1438        artifact_json(name, &["dylib", "rlib"], filenames, None, TestTarget::No)
1439    }
1440
1441    fn artifact_json(
1442        name: &str,
1443        kind: &[&str],
1444        filenames: &[Utf8PathBuf],
1445        executable: Option<&Utf8Path>,
1446        test_target: TestTarget,
1447    ) -> serde_json::Value {
1448        let package = package_metadata();
1449        let src_path = package
1450            .manifest_path()
1451            .parent()
1452            .expect("manifest path has a parent")
1453            .join("src/lib.rs");
1454        let is_test = test_target.as_bool();
1455
1456        json!({
1457            "reason": "compiler-artifact",
1458            "package_id": PACKAGE_METADATA_ID,
1459            "manifest_path": package.manifest_path(),
1460            "target": {
1461                "name": name,
1462                "kind": kind,
1463                "crate_types": kind,
1464                "required-features": [],
1465                "src_path": src_path,
1466                "edition": "2021",
1467                "doctest": is_test,
1468                "test": is_test,
1469                "doc": is_test
1470            },
1471            "profile": {
1472                "opt_level": "0",
1473                "debuginfo": 0,
1474                "debug_assertions": true,
1475                "overflow_checks": true,
1476                "test": is_test
1477            },
1478            "features": [],
1479            "filenames": filenames,
1480            "executable": executable,
1481            "fresh": false
1482        })
1483    }
1484
1485    #[test]
1486    fn test_base_output_dir() {
1487        let cases: &[(&str, Option<&str>, &str)] = &[
1488            ("debug/deps/foo-9e6e6f7b", Some("debug"), "legacy layout"),
1489            (
1490                "aarch64-unknown-linux-gnu/debug/deps/foo-9e6e6f7b",
1491                Some("aarch64-unknown-linux-gnu/debug"),
1492                "legacy layout with a target triple",
1493            ),
1494            (
1495                "debug/build/metadata-helper/9e6e6f7b/out/foo",
1496                Some("debug"),
1497                "v2 layout",
1498            ),
1499            (
1500                "aarch64-unknown-linux-gnu/debug/build/metadata-helper/9e6e6f7b/out/foo",
1501                Some("aarch64-unknown-linux-gnu/debug"),
1502                "v2 layout with a target triple",
1503            ),
1504            (
1505                "debug/build/build/9e6e6f7b/out/foo",
1506                Some("debug"),
1507                "v2 layout, package named build",
1508            ),
1509            (
1510                "debug/examples/foo-9e6e6f7b",
1511                Some("debug"),
1512                "legacy layout, example test binary",
1513            ),
1514            (
1515                "aarch64-unknown-linux-gnu/debug/examples/foo-9e6e6f7b",
1516                Some("aarch64-unknown-linux-gnu/debug"),
1517                "legacy layout, example test binary with a target triple",
1518            ),
1519            (
1520                "debug/build/metadata-helper-9e6e6f7b/out/foo",
1521                None,
1522                "legacy build script out dir",
1523            ),
1524            (
1525                "aarch64-unknown-linux-gnu/debug/build/metadata-helper-9e6e6f7b/out/foo",
1526                None,
1527                "legacy build script out dir with a target triple",
1528            ),
1529            ("debug/foo", None, "uplifted into the profile directory"),
1530            (
1531                "out/foo",
1532                None,
1533                "out directory directly under the build directory",
1534            ),
1535            (
1536                "deps/foo",
1537                None,
1538                "deps directory directly under the build directory",
1539            ),
1540            (
1541                "examples/foo",
1542                None,
1543                "examples directory directly under the build directory",
1544            ),
1545            (
1546                "build/metadata-helper/9e6e6f7b/out/foo",
1547                None,
1548                "v2 build directory directly under the build directory",
1549            ),
1550        ];
1551
1552        for (rel_artifact_path, expected, description) in cases {
1553            assert_eq!(
1554                base_output_dir(Utf8Path::new(rel_artifact_path)),
1555                expected.map(Utf8Path::new),
1556                "{description}: base output dir for artifact path {rel_artifact_path}"
1557            );
1558        }
1559    }
1560}