1use 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#[derive(Clone, Debug)]
28pub struct RustTestBinary {
29 pub id: RustBinaryId,
31 pub path: Utf8PathBuf,
33 pub package_id: String,
35 pub kind: RustTestBinaryKind,
37 pub name: String,
39 pub build_platform: BuildPlatform,
42}
43
44#[derive(Clone, Debug)]
46pub struct BinaryList {
47 pub rust_build_meta: RustBuildMeta<BinaryListState>,
49
50 pub rust_binaries: Vec<RustTestBinary>,
52}
53
54impl BinaryList {
55 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 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 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 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 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 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#[derive(Debug)]
237pub struct BinaryListBuilder<'g> {
238 state: BinaryListBuildState<'g>,
239}
240
241impl<'g> BinaryListBuilder<'g> {
242 pub fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
244 Self {
245 state: BinaryListBuildState::new(graph, build_platforms),
246 }
247 }
248
249 pub fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
251 self.state.process_message(message)
252 }
253
254 pub fn process_message_line(&mut self, line: &str) -> Result<(), FromMessagesError> {
259 self.process_message(parse_message_line(line))
260 }
261
262 pub fn finish(self) -> BinaryList {
264 self.state.finish()
265 }
266}
267
268fn 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 let build_directory = graph
289 .workspace()
290 .build_directory()
291 .unwrap_or_else(|| graph.workspace().target_directory())
292 .to_path_buf();
293 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 }
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 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 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 (
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 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 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 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 fn detect_base_output_dir(&mut self, artifact_path: &Utf8Path) -> Option<()> {
453 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 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 warn!(
507 target: "nextest-runner::list",
508 "warning: saw package ID `{}` which wasn't produced by cargo metadata",
509 package_id
510 );
511 false
512 },
513 |p| p.in_workspace(),
514 );
515 if in_workspace {
516 match build_script
518 .out_dir
519 .strip_prefix(&self.rust_build_meta.build_directory)
520 {
521 Ok(rel_out_dir) => {
522 self.rust_build_meta.build_script_out_dirs.insert(
523 package_id.repr().to_owned(),
524 convert_rel_path_to_forward_slash(rel_out_dir),
525 );
526 }
527 Err(_) => {
528 debug!(
529 target: "nextest-runner::list",
530 "build script out_dir `{}` for package `{}` is not within \
531 the build directory `{}`, skipping",
532 build_script.out_dir, package_id,
533 self.rust_build_meta.build_directory,
534 );
535 }
536 }
537
538 if !build_script.env.is_empty() {
541 self.rust_build_meta
542 .build_script_info
543 .get_or_insert_with(BTreeMap::new)
544 .entry(package_id.repr().to_owned())
545 .or_default()
546 .envs = build_script.env.into_iter().collect();
547 }
548 }
549
550 Ok(())
551 }
552
553 fn detect_linked_path(&mut self, package_id: &PackageId, path: &Utf8Path) -> Option<()> {
555 let actual_path = match path.as_str().split_once('=') {
557 Some((_, p)) => p.into(),
558 None => path,
559 };
560
561 let rel_path = match actual_path.strip_prefix(&self.rust_build_meta.build_directory) {
562 Ok(rel) => rel,
563 Err(_) => {
564 if let Some(alt_target_dir) = &self.alt_target_dir {
576 actual_path.strip_prefix(alt_target_dir).ok()?
577 } else {
578 return None;
579 }
580 }
581 };
582
583 self.rust_build_meta
584 .linked_paths
585 .entry(convert_rel_path_to_forward_slash(rel_path))
586 .or_default()
587 .insert(package_id.repr.clone());
588
589 Some(())
590 }
591
592 fn finish(mut self) -> BinaryList {
593 self.rust_binaries.sort_by(|b1, b2| b1.id.cmp(&b2.id));
594
595 let relevant_package_ids = self
597 .rust_binaries
598 .iter()
599 .map(|bin| bin.package_id.clone())
600 .collect::<HashSet<_>>();
601
602 self.rust_build_meta
603 .build_script_out_dirs
604 .retain(|package_id, _| relevant_package_ids.contains(package_id));
605 if let Some(info) = &mut self.rust_build_meta.build_script_info {
606 info.retain(|package_id, _| relevant_package_ids.contains(package_id));
607 }
608
609 if !self.rust_binaries.is_empty() && self.rust_build_meta.base_output_directories.is_empty()
613 {
614 warn!(
615 target: "nextest-runner::list",
616 "failed to detect any base output directories under the build directory `{}`; \
617 tests that link against dynamic libraries may fail to start. \
618 This usually means Cargo's build directory layout changed -- \
619 please report it at https://github.com/nextest-rs/nextest/issues/new",
620 self.rust_build_meta.build_directory,
621 );
622 }
623
624 BinaryList {
625 rust_build_meta: self.rust_build_meta,
626 rust_binaries: self.rust_binaries,
627 }
628 }
629}
630
631fn artifact_build_platform(
637 artifact_path: &Utf8Path,
638 target_directory: &Utf8Path,
639 build_directory: &Utf8Path,
640 target_triple: Option<&str>,
641) -> Option<BuildPlatform> {
642 let Some(target_triple) = target_triple else {
644 return Some(BuildPlatform::Target);
645 };
646
647 let Some(rel_path) = [target_directory, build_directory]
651 .into_iter()
652 .filter_map(|root| artifact_path.strip_prefix(root).ok())
653 .min_by_key(|rel_path| rel_path.as_str().len())
654 else {
655 debug!(
656 target: "nextest-runner::list",
657 "artifact path `{}` is under neither the target directory `{}` nor the build \
658 directory `{}`, recording its build platform as unknown",
659 artifact_path, target_directory, build_directory,
660 );
661 return None;
662 };
663
664 if rel_path.starts_with(target_triple) {
665 Some(BuildPlatform::Target)
666 } else {
667 Some(BuildPlatform::Host)
668 }
669}
670
671fn base_output_dir(rel_path: &Utf8Path) -> Option<&Utf8Path> {
672 let parent = rel_path.parent()?;
673 let base = match parent.file_name()? {
674 "deps" | "examples" => parent.parent()?,
681 "out" => parent
683 .ancestors()
684 .nth(3)
705 .filter(|dir| dir.file_name() == Some("build"))?
706 .parent()?,
707 _ => return None,
708 };
709
710 (!base.as_str().is_empty()).then_some(base)
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718 use crate::{
719 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
720 list::{
721 SerializableFormat,
722 test_helpers::{PACKAGE_GRAPH_FIXTURE, PACKAGE_METADATA_ID, package_metadata},
723 },
724 platform::{HostPlatform, PlatformLibdir, TargetPlatform},
725 };
726 use indoc::indoc;
727 use maplit::btreeset;
728 use nextest_metadata::PlatformLibdirUnavailable;
729 use pretty_assertions::assert_eq;
730 use serde_json::json;
731 use target_spec::{Platform, TargetFeatures};
732
733 #[test]
734 fn test_parse_binary_list() {
735 let fake_bin_test = RustTestBinary {
736 id: "fake-package::bin/fake-binary".into(),
737 path: "/fake/binary".into(),
738 package_id: "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)"
739 .to_owned(),
740 kind: RustTestBinaryKind::LIB,
741 name: "fake-binary".to_owned(),
742 build_platform: BuildPlatform::Target,
743 };
744 let fake_macro_test = RustTestBinary {
745 id: "fake-macro::proc-macro/fake-macro".into(),
746 path: "/fake/macro".into(),
747 package_id: "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)"
748 .to_owned(),
749 kind: RustTestBinaryKind::PROC_MACRO,
750 name: "fake-macro".to_owned(),
751 build_platform: BuildPlatform::Host,
752 };
753
754 let fake_triple = TargetTriple {
755 platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
756 source: TargetTripleSource::CliOption,
757 location: TargetDefinitionLocation::Builtin,
758 };
759 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
760 let build_platforms = BuildPlatforms {
761 host: HostPlatform {
762 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
763 libdir: PlatformLibdir::Available(Utf8PathBuf::from(fake_host_libdir)),
764 },
765 target: Some(TargetPlatform {
766 triple: fake_triple,
767 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
769 }),
770 };
771
772 let mut rust_build_meta =
773 RustBuildMeta::new("/fake/target", "/fake/target", build_platforms);
774 rust_build_meta
777 .base_output_directories
778 .insert("aarch64-unknown-linux-gnu/my-profile".into());
779 rust_build_meta
780 .base_output_directories
781 .insert("my-profile".into());
782 for non_test_binary in [
783 RustNonTestBinarySummary {
784 name: "my-name".into(),
785 kind: RustNonTestBinaryKind::BIN_EXE,
786 path: "aarch64-unknown-linux-gnu/my-profile/my-name".into(),
787 build_platform: Some(BuildPlatform::Target),
788 },
789 RustNonTestBinarySummary {
790 name: "your-name".into(),
791 kind: RustNonTestBinaryKind::DYLIB,
792 path: "my-profile/your-name.dll".into(),
793 build_platform: Some(BuildPlatform::Host),
794 },
795 RustNonTestBinarySummary {
796 name: "your-name".into(),
797 kind: RustNonTestBinaryKind::DYLIB,
798 path: "my-profile/your-name.exp".into(),
799 build_platform: Some(BuildPlatform::Host),
800 },
801 ] {
802 rust_build_meta
803 .non_test_binaries
804 .insert(guppy::PackageId::new("my-package-id"), non_test_binary);
805 }
806
807 let binary_list = BinaryList {
808 rust_build_meta,
809 rust_binaries: vec![fake_bin_test, fake_macro_test],
810 };
811
812 static EXPECTED_HUMAN: &str = indoc! {"
814 fake-package::bin/fake-binary
815 fake-macro::proc-macro/fake-macro
816 "};
817 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {r"
818 fake-package::bin/fake-binary:
819 bin: /fake/binary
820 build platform: target
821 fake-macro::proc-macro/fake-macro:
822 bin: /fake/macro
823 build platform: host
824 "};
825 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
826 {
827 "rust-build-meta": {
828 "target-directory": "/fake/target",
829 "build-directory": "/fake/target",
830 "base-output-directories": [
831 "aarch64-unknown-linux-gnu/my-profile",
832 "my-profile"
833 ],
834 "non-test-binaries": {
835 "my-package-id": [
836 {
837 "name": "my-name",
838 "kind": "bin-exe",
839 "path": "aarch64-unknown-linux-gnu/my-profile/my-name",
840 "build-platform": "target"
841 },
842 {
843 "name": "your-name",
844 "kind": "dylib",
845 "path": "my-profile/your-name.dll",
846 "build-platform": "host"
847 },
848 {
849 "name": "your-name",
850 "kind": "dylib",
851 "path": "my-profile/your-name.exp",
852 "build-platform": "host"
853 }
854 ]
855 },
856 "build-script-out-dirs": {},
857 "build-script-info": {},
858 "linked-paths": [],
859 "platforms": {
860 "host": {
861 "platform": {
862 "triple": "x86_64-unknown-linux-gnu",
863 "target-features": "unknown"
864 },
865 "libdir": {
866 "status": "available",
867 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
868 }
869 },
870 "targets": [
871 {
872 "platform": {
873 "triple": "aarch64-unknown-linux-gnu",
874 "target-features": "unknown"
875 },
876 "libdir": {
877 "status": "unavailable",
878 "reason": "rustc-output-error"
879 }
880 }
881 ]
882 },
883 "target-platforms": [
884 {
885 "triple": "aarch64-unknown-linux-gnu",
886 "target-features": "unknown"
887 }
888 ],
889 "target-platform": "aarch64-unknown-linux-gnu"
890 },
891 "rust-binaries": {
892 "fake-macro::proc-macro/fake-macro": {
893 "binary-id": "fake-macro::proc-macro/fake-macro",
894 "binary-name": "fake-macro",
895 "package-id": "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)",
896 "kind": "proc-macro",
897 "binary-path": "/fake/macro",
898 "build-platform": "host"
899 },
900 "fake-package::bin/fake-binary": {
901 "binary-id": "fake-package::bin/fake-binary",
902 "binary-name": "fake-binary",
903 "package-id": "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)",
904 "kind": "lib",
905 "binary-path": "/fake/binary",
906 "build-platform": "target"
907 }
908 }
909 }"#};
910 static EXPECTED_ONELINE: &str = indoc! {"
912 fake-package::bin/fake-binary
913 fake-macro::proc-macro/fake-macro
914 "};
915 static EXPECTED_ONELINE_VERBOSE: &str = indoc! {r"
916 fake-package::bin/fake-binary [bin: /fake/binary] [build platform: target]
917 fake-macro::proc-macro/fake-macro [bin: /fake/macro] [build platform: host]
918 "};
919
920 assert_eq!(
921 binary_list
922 .to_string(OutputFormat::Human { verbose: false })
923 .expect("human succeeded"),
924 EXPECTED_HUMAN
925 );
926 assert_eq!(
927 binary_list
928 .to_string(OutputFormat::Human { verbose: true })
929 .expect("human succeeded"),
930 EXPECTED_HUMAN_VERBOSE
931 );
932 assert_eq!(
933 binary_list
934 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
935 .expect("json-pretty succeeded"),
936 EXPECTED_JSON_PRETTY
937 );
938 assert_eq!(
939 binary_list
940 .to_string(OutputFormat::Oneline { verbose: false })
941 .expect("oneline succeeded"),
942 EXPECTED_ONELINE
943 );
944 assert_eq!(
945 binary_list
946 .to_string(OutputFormat::Oneline { verbose: true })
947 .expect("oneline verbose succeeded"),
948 EXPECTED_ONELINE_VERBOSE
949 );
950 }
951
952 #[test]
953 fn test_parse_binary_list_from_message_lines() {
954 let build_platforms = BuildPlatforms {
955 host: HostPlatform {
956 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
957 libdir: PlatformLibdir::Available("/fake/libdir".into()),
958 },
959 target: None,
960 };
961 let package = package_metadata();
962 let artifact_path = PACKAGE_GRAPH_FIXTURE
966 .workspace()
967 .build_directory()
968 .expect("fixture sets build_directory")
969 .join("debug/deps/metadata_helper-test");
970 let compiler_artifact = artifact_json(
971 package.name(),
972 &["lib"],
973 std::slice::from_ref(&artifact_path),
974 Some(&artifact_path),
975 TestTarget::Yes,
976 );
977 let input = format!("this is not JSON\n{}\n\n", compiler_artifact);
978
979 let from_messages = BinaryList::from_messages(
980 input.as_bytes(),
981 &PACKAGE_GRAPH_FIXTURE,
982 build_platforms.clone(),
983 )
984 .expect("parsing from messages succeeds");
985
986 let mut builder = BinaryListBuilder::new(&PACKAGE_GRAPH_FIXTURE, build_platforms);
987 for line in input.lines() {
988 builder
989 .process_message_line(line)
990 .expect("processing line succeeds");
991 }
992 let from_lines = builder.finish();
993
994 assert_eq!(
995 from_lines.rust_build_meta.base_output_directories,
996 btreeset! { Utf8PathBuf::from("debug") },
997 "base output directory detected from the artifact's executable path"
998 );
999
1000 assert_eq!(
1001 from_lines
1002 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1003 .expect("json-pretty succeeds"),
1004 from_messages
1005 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1006 .expect("json-pretty succeeds")
1007 );
1008 }
1009
1010 #[test]
1011 fn test_artifact_build_platform() {
1012 static TARGET_DIR: &str = "/w/target";
1013 static TRIPLE: &str = "aarch64-unknown-linux-gnu";
1014
1015 struct Case {
1016 artifact_path: &'static str,
1017 build_directory: Option<&'static str>,
1020 target_triple: Option<&'static str>,
1021 expected: Option<BuildPlatform>,
1022 description: &'static str,
1023 }
1024
1025 let cases = [
1026 Case {
1027 artifact_path: "/w/target/debug/libfoo.so",
1028 build_directory: None,
1029 target_triple: None,
1030 expected: Some(BuildPlatform::Target),
1031 description: "with no target platform the host and the target coincide, and \
1032 nextest reports target",
1033 },
1034 Case {
1035 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1036 build_directory: None,
1037 target_triple: Some(TRIPLE),
1038 expected: Some(BuildPlatform::Target),
1039 description: "a dylib uplifted into the target platform's artifact directory",
1040 },
1041 Case {
1042 artifact_path: "/w/target/debug/libfoo.so",
1043 build_directory: None,
1044 target_triple: Some(TRIPLE),
1045 expected: Some(BuildPlatform::Host),
1046 description: "the same dylib, built for the host as a build dependency",
1047 },
1048 Case {
1049 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/deps/libfoo.rlib",
1050 build_directory: None,
1051 target_triple: Some(TRIPLE),
1052 expected: Some(BuildPlatform::Target),
1053 description: "an rlib that was never uplifted, legacy layout",
1054 },
1055 Case {
1056 artifact_path: "/w/target/debug/deps/libfoo.rlib",
1057 build_directory: None,
1058 target_triple: Some(TRIPLE),
1059 expected: Some(BuildPlatform::Host),
1060 description: "the same rlib built for the host, legacy layout",
1061 },
1062 Case {
1063 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/build/foo/9e6e6f7b/out/libfoo.rlib",
1064 build_directory: None,
1065 target_triple: Some(TRIPLE),
1066 expected: Some(BuildPlatform::Target),
1067 description: "an rlib that was never uplifted, build-dir layout v2",
1068 },
1069 Case {
1070 artifact_path: "/w/target/debug/build/foo/9e6e6f7b/out/libfoo.rlib",
1071 build_directory: None,
1072 target_triple: Some(TRIPLE),
1073 expected: Some(BuildPlatform::Host),
1074 description: "the same rlib built for the host, build-dir layout v2",
1075 },
1076 Case {
1077 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1078 build_directory: Some("/w/build"),
1079 target_triple: Some(TRIPLE),
1080 expected: Some(BuildPlatform::Target),
1081 description: "a build directory beside the target directory: uplifts are unaffected",
1082 },
1083 Case {
1084 artifact_path: "/w/target/build/aarch64-unknown-linux-gnu/debug/deps/libfoo.rlib",
1085 build_directory: Some("/w/target/build"),
1086 target_triple: Some(TRIPLE),
1087 expected: Some(BuildPlatform::Target),
1088 description: "a build directory inside the target directory: the build directory is the \
1089 innermost root, so the triple is still the first component under it",
1090 },
1091 Case {
1092 artifact_path: "/w/target/build/debug/deps/libfoo.rlib",
1093 build_directory: Some("/w/target/build"),
1094 target_triple: Some(TRIPLE),
1095 expected: Some(BuildPlatform::Host),
1096 description: "a build directory inside the target directory, host artifact",
1097 },
1098 Case {
1099 artifact_path: "/w/target/build/aarch64-unknown-linux-gnu/debug/build/foo/9e6e6f7b/out/libfoo.rlib",
1100 build_directory: Some("/w/target/build"),
1101 target_triple: Some(TRIPLE),
1102 expected: Some(BuildPlatform::Target),
1103 description: "the shape Cargo emits under the build-dir v2 layout for \
1104 `build.build-dir = target/build` plus --target: nesting and v2 at once. \
1105 Verified by hand against a real nightly build",
1106 },
1107 Case {
1108 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1109 build_directory: Some("/w/target/build"),
1110 target_triple: Some(TRIPLE),
1111 expected: Some(BuildPlatform::Target),
1112 description: "a build directory inside the target directory: uplifts still go to the \
1113 target directory, which is the innermost root containing them",
1114 },
1115 Case {
1116 artifact_path: "/w/target/debug/libfoo.so",
1117 build_directory: Some("/w/target/build"),
1118 target_triple: Some(TRIPLE),
1119 expected: Some(BuildPlatform::Host),
1120 description: "a host dylib uplifted alongside a nested build directory. The \
1121 nested build directory does not contain it, so dropping the target directory \
1122 from the candidate roots would leave nothing to strip",
1123 },
1124 Case {
1125 artifact_path: "/w/target/debug/deps/aarch64-unknown-linux-gnu/libfoo.so",
1126 build_directory: None,
1127 target_triple: Some(TRIPLE),
1128 expected: Some(BuildPlatform::Host),
1129 description: "only the first component is the triple slot; a triple-named \
1130 directory deeper in the path means nothing",
1131 },
1132 Case {
1133 artifact_path: "/w/target/debug/libfoo.so",
1134 build_directory: Some("/w"),
1135 target_triple: Some(TRIPLE),
1136 expected: Some(BuildPlatform::Host),
1137 description: "the reverse nesting: `build.build-dir` and `build.target-dir` are \
1138 independent, so the target directory can sit inside the build directory. It \
1139 is then the innermost root",
1140 },
1141 Case {
1142 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1143 build_directory: Some("/w"),
1144 target_triple: Some(TRIPLE),
1145 expected: Some(BuildPlatform::Target),
1146 description: "the reverse nesting, target artifact: resolving against the outer \
1147 build directory would see `target` first and misreport this as host",
1148 },
1149 Case {
1150 artifact_path: "/w/target/aarch64-unknown-linux-gnu-ilp32/debug/libfoo.so",
1151 build_directory: None,
1152 target_triple: Some(TRIPLE),
1153 expected: Some(BuildPlatform::Host),
1154 description: "a directory whose name merely starts with the triple is not the triple",
1155 },
1156 Case {
1157 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1158 build_directory: Some("/w/target/aarch"),
1159 target_triple: Some(TRIPLE),
1160 expected: Some(BuildPlatform::Target),
1161 description: "root selection is component-wise, so a build directory that is only \
1162 a string prefix of the path never wins; a byte-wise prefix test would strip \
1163 `/w/target/aarch` here and misread the artifact as host",
1164 },
1165 Case {
1166 artifact_path: "/w/target/my-custom-target/debug/libfoo.so",
1167 build_directory: None,
1168 target_triple: Some("my-custom-target"),
1169 expected: Some(BuildPlatform::Target),
1170 description: "a custom JSON target's directory is its file stem, which is also its triple \
1171 string in nextest",
1172 },
1173 Case {
1174 artifact_path: "/w/target/aarch64-unknown-linux-gnu/libfoo.so",
1175 build_directory: None,
1176 target_triple: Some(TRIPLE),
1177 expected: Some(BuildPlatform::Target),
1178 description: "KNOWN WRONG, pinned so the limitation stays visible: a custom \
1179 profile named exactly after the triple puts host uplifts at \
1180 `<profile>/libfoo.so` under the target directory, and the profile component \
1181 occupies the `<triple>` slot, so a host artifact reads as target. \
1182 Self-inflicted",
1183 },
1184 Case {
1185 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1186 build_directory: Some("/w/target/aarch64-unknown-linux-gnu"),
1187 target_triple: Some(TRIPLE),
1188 expected: Some(BuildPlatform::Host),
1189 description: "KNOWN WRONG, pinned so the limitation stays visible: a build \
1190 directory nested in the target directory and named after the triple becomes \
1191 the innermost root and swallows the triple component, so an uplifted target \
1192 artifact reads as host. Self-inflicted, like the triple-named custom profile \
1193 above",
1194 },
1195 Case {
1196 artifact_path: "/elsewhere/debug/libfoo.so",
1197 build_directory: None,
1198 target_triple: Some(TRIPLE),
1199 expected: None,
1200 description: "a path under neither root has no `<triple>` slot to read, so the \
1201 platform is unknown",
1202 },
1203 ];
1204
1205 for case in cases {
1206 let build_directory = case.build_directory.unwrap_or(TARGET_DIR);
1207 assert_eq!(
1208 artifact_build_platform(
1209 Utf8Path::new(case.artifact_path),
1210 Utf8Path::new(TARGET_DIR),
1211 Utf8Path::new(build_directory),
1212 case.target_triple,
1213 ),
1214 case.expected,
1215 "{}: build platform for {} (build directory {build_directory})",
1216 case.description,
1217 case.artifact_path,
1218 );
1219 }
1220 }
1221
1222 fn cross_build_platforms() -> BuildPlatforms {
1223 let target_triple = TargetTriple {
1224 platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown)
1225 .expect("aarch64-unknown-linux-gnu is a builtin triple"),
1226 source: TargetTripleSource::CliOption,
1227 location: TargetDefinitionLocation::Builtin,
1228 };
1229 BuildPlatforms {
1230 host: HostPlatform {
1231 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
1232 libdir: PlatformLibdir::Available("/fake/host/libdir".into()),
1233 },
1234 target: Some(TargetPlatform::new(
1235 target_triple,
1236 PlatformLibdir::Available("/fake/target/libdir".into()),
1237 )),
1238 }
1239 }
1240
1241 #[test]
1242 fn test_non_test_build_platform_resolves_against_the_build_directory() {
1243 let mut state = BinaryListBuildState::new(&PACKAGE_GRAPH_FIXTURE, cross_build_platforms());
1244 state.rust_build_meta.target_directory = "/w/target".into();
1245 state.rust_build_meta.build_directory = "/w/target/build".into();
1246
1247 assert_eq!(
1248 state.non_test_build_platform(Utf8Path::new(
1249 "/w/target/build/aarch64-unknown-linux-gnu/debug/deps/libfoo.rlib"
1250 )),
1251 Some(BuildPlatform::Target),
1252 "the nested build directory is the innermost root, so the triple is the first \
1253 component under it; resolving against the target directory instead would see \
1254 `build` first and misreport this as host"
1255 );
1256 }
1257
1258 #[test]
1259 fn test_non_test_binaries_record_the_build_platform() {
1260 let build_platforms = cross_build_platforms();
1261
1262 let workspace = PACKAGE_GRAPH_FIXTURE.workspace();
1263 let target_dir = workspace.target_directory();
1264 let build_dir = workspace
1265 .build_directory()
1266 .expect("fixture sets build_directory");
1267
1268 let input = [
1275 dylib_artifact_json(
1276 "shared",
1277 &[
1278 target_dir.join("debug/libshared.so"),
1279 build_dir.join("debug/deps/libshared.rlib"),
1280 ],
1281 ),
1282 dylib_artifact_json(
1283 "shared",
1284 &[
1285 target_dir.join("aarch64-unknown-linux-gnu/debug/libshared.so"),
1286 build_dir.join("aarch64-unknown-linux-gnu/debug/deps/libshared.rlib"),
1287 ],
1288 ),
1289 bin_artifact_json(
1290 "mainbin",
1291 &target_dir.join("aarch64-unknown-linux-gnu/debug/mainbin"),
1292 ),
1293 ]
1294 .map(|artifact| artifact.to_string())
1295 .join("\n");
1296
1297 let binary_list =
1298 BinaryList::from_messages(input.as_bytes(), &PACKAGE_GRAPH_FIXTURE, build_platforms)
1299 .expect("parsing from messages succeeds");
1300
1301 assert_eq!(
1302 binary_list.rust_build_meta.non_test_binaries.to_summary(),
1303 BTreeMap::from([(
1304 PACKAGE_METADATA_ID.to_owned(),
1305 btreeset! {
1306 RustNonTestBinarySummary {
1307 name: "mainbin".to_owned(),
1308 kind: RustNonTestBinaryKind::BIN_EXE,
1309 path: "aarch64-unknown-linux-gnu/debug/mainbin".into(),
1310 build_platform: Some(BuildPlatform::Target),
1311 },
1312 RustNonTestBinarySummary {
1313 name: "shared".to_owned(),
1314 kind: RustNonTestBinaryKind::DYLIB,
1315 path: "aarch64-unknown-linux-gnu/debug/libshared.so".into(),
1316 build_platform: Some(BuildPlatform::Target),
1317 },
1318 RustNonTestBinarySummary {
1319 name: "shared".to_owned(),
1320 kind: RustNonTestBinaryKind::DYLIB,
1321 path: "debug/libshared.so".into(),
1322 build_platform: Some(BuildPlatform::Host),
1323 },
1324 },
1325 )]),
1326 "the two builds of one dylib target are told apart by the triple component of their \
1327 uplifted paths; the rlibs live in the fixture's build directory, which is not under \
1328 the target directory, so they are not recorded at all"
1329 );
1330 }
1331
1332 #[derive(Clone, Copy)]
1333 enum TestTarget {
1334 Yes,
1335 No,
1336 }
1337
1338 impl TestTarget {
1339 fn as_bool(self) -> bool {
1340 match self {
1341 Self::Yes => true,
1342 Self::No => false,
1343 }
1344 }
1345 }
1346
1347 fn bin_artifact_json(name: &str, path: &Utf8Path) -> serde_json::Value {
1348 artifact_json(
1349 name,
1350 &["bin"],
1351 &[path.to_owned()],
1352 Some(path),
1353 TestTarget::No,
1354 )
1355 }
1356
1357 fn dylib_artifact_json(name: &str, filenames: &[Utf8PathBuf]) -> serde_json::Value {
1358 artifact_json(name, &["dylib", "rlib"], filenames, None, TestTarget::No)
1359 }
1360
1361 fn artifact_json(
1362 name: &str,
1363 kind: &[&str],
1364 filenames: &[Utf8PathBuf],
1365 executable: Option<&Utf8Path>,
1366 test_target: TestTarget,
1367 ) -> serde_json::Value {
1368 let package = package_metadata();
1369 let src_path = package
1370 .manifest_path()
1371 .parent()
1372 .expect("manifest path has a parent")
1373 .join("src/lib.rs");
1374 let is_test = test_target.as_bool();
1375
1376 json!({
1377 "reason": "compiler-artifact",
1378 "package_id": PACKAGE_METADATA_ID,
1379 "manifest_path": package.manifest_path(),
1380 "target": {
1381 "name": name,
1382 "kind": kind,
1383 "crate_types": kind,
1384 "required-features": [],
1385 "src_path": src_path,
1386 "edition": "2021",
1387 "doctest": is_test,
1388 "test": is_test,
1389 "doc": is_test
1390 },
1391 "profile": {
1392 "opt_level": "0",
1393 "debuginfo": 0,
1394 "debug_assertions": true,
1395 "overflow_checks": true,
1396 "test": is_test
1397 },
1398 "features": [],
1399 "filenames": filenames,
1400 "executable": executable,
1401 "fresh": false
1402 })
1403 }
1404
1405 #[test]
1406 fn test_base_output_dir() {
1407 let cases: &[(&str, Option<&str>, &str)] = &[
1408 ("debug/deps/foo-9e6e6f7b", Some("debug"), "legacy layout"),
1409 (
1410 "aarch64-unknown-linux-gnu/debug/deps/foo-9e6e6f7b",
1411 Some("aarch64-unknown-linux-gnu/debug"),
1412 "legacy layout with a target triple",
1413 ),
1414 (
1415 "debug/build/metadata-helper/9e6e6f7b/out/foo",
1416 Some("debug"),
1417 "v2 layout",
1418 ),
1419 (
1420 "aarch64-unknown-linux-gnu/debug/build/metadata-helper/9e6e6f7b/out/foo",
1421 Some("aarch64-unknown-linux-gnu/debug"),
1422 "v2 layout with a target triple",
1423 ),
1424 (
1425 "debug/build/build/9e6e6f7b/out/foo",
1426 Some("debug"),
1427 "v2 layout, package named build",
1428 ),
1429 (
1430 "debug/examples/foo-9e6e6f7b",
1431 Some("debug"),
1432 "legacy layout, example test binary",
1433 ),
1434 (
1435 "aarch64-unknown-linux-gnu/debug/examples/foo-9e6e6f7b",
1436 Some("aarch64-unknown-linux-gnu/debug"),
1437 "legacy layout, example test binary with a target triple",
1438 ),
1439 (
1440 "debug/build/metadata-helper-9e6e6f7b/out/foo",
1441 None,
1442 "legacy build script out dir",
1443 ),
1444 (
1445 "aarch64-unknown-linux-gnu/debug/build/metadata-helper-9e6e6f7b/out/foo",
1446 None,
1447 "legacy build script out dir with a target triple",
1448 ),
1449 ("debug/foo", None, "uplifted into the profile directory"),
1450 (
1451 "out/foo",
1452 None,
1453 "out directory directly under the build directory",
1454 ),
1455 (
1456 "deps/foo",
1457 None,
1458 "deps directory directly under the build directory",
1459 ),
1460 (
1461 "examples/foo",
1462 None,
1463 "examples directory directly under the build directory",
1464 ),
1465 (
1466 "build/metadata-helper/9e6e6f7b/out/foo",
1467 None,
1468 "v2 build directory directly under the build directory",
1469 ),
1470 ];
1471
1472 for (rel_artifact_path, expected, description) in cases {
1473 assert_eq!(
1474 base_output_dir(Utf8Path::new(rel_artifact_path)),
1475 expected.map(Utf8Path::new),
1476 "{description}: base output dir for artifact path {rel_artifact_path}"
1477 );
1478 }
1479 }
1480}