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 std::{collections::HashSet, io};
20use tracing::warn;
21
22#[derive(Clone, Debug)]
24pub struct RustTestBinary {
25 pub id: RustBinaryId,
27 pub path: Utf8PathBuf,
29 pub package_id: String,
31 pub kind: RustTestBinaryKind,
33 pub name: String,
35 pub build_platform: BuildPlatform,
38}
39
40#[derive(Clone, Debug)]
42pub struct BinaryList {
43 pub rust_build_meta: RustBuildMeta<BinaryListState>,
45
46 pub rust_binaries: Vec<RustTestBinary>,
48}
49
50impl BinaryList {
51 pub fn from_messages(
53 reader: impl io::BufRead,
54 graph: &PackageGraph,
55 build_platforms: BuildPlatforms,
56 ) -> Result<Self, FromMessagesError> {
57 let mut state = BinaryListBuildState::new(graph, build_platforms);
58
59 for message in Message::parse_stream(reader) {
60 let message = message.map_err(FromMessagesError::ReadMessages)?;
61 state.process_message(message)?;
62 }
63
64 Ok(state.finish())
65 }
66
67 pub fn from_summary(summary: BinaryListSummary) -> Result<Self, RustBuildMetaParseError> {
69 let rust_binaries = summary
70 .rust_binaries
71 .into_values()
72 .map(|bin| RustTestBinary {
73 name: bin.binary_name,
74 path: bin.binary_path,
75 package_id: bin.package_id,
76 kind: bin.kind,
77 id: bin.binary_id,
78 build_platform: bin.build_platform,
79 })
80 .collect();
81 Ok(Self {
82 rust_build_meta: RustBuildMeta::from_summary(summary.rust_build_meta)?,
83 rust_binaries,
84 })
85 }
86
87 pub fn write(
89 &self,
90 output_format: OutputFormat,
91 writer: &mut dyn WriteStr,
92 colorize: bool,
93 ) -> Result<(), WriteTestListError> {
94 match output_format {
95 OutputFormat::Human { verbose } => self
96 .write_human(writer, verbose, colorize)
97 .map_err(WriteTestListError::Io),
98 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
99 }
100 }
101
102 fn to_summary(&self) -> BinaryListSummary {
103 let rust_binaries = self
104 .rust_binaries
105 .iter()
106 .map(|bin| {
107 let summary = RustTestBinarySummary {
108 binary_name: bin.name.clone(),
109 package_id: bin.package_id.clone(),
110 kind: bin.kind.clone(),
111 binary_path: bin.path.clone(),
112 binary_id: bin.id.clone(),
113 build_platform: bin.build_platform,
114 };
115 (bin.id.clone(), summary)
116 })
117 .collect();
118
119 BinaryListSummary {
120 rust_build_meta: self.rust_build_meta.to_summary(),
121 rust_binaries,
122 }
123 }
124
125 fn write_human(
126 &self,
127 writer: &mut dyn WriteStr,
128 verbose: bool,
129 colorize: bool,
130 ) -> io::Result<()> {
131 let mut styles = Styles::default();
132 if colorize {
133 styles.colorize();
134 }
135 for bin in &self.rust_binaries {
136 if verbose {
137 writeln!(writer, "{}:", bin.id.style(styles.binary_id))?;
138 writeln!(writer, " {} {}", "bin:".style(styles.field), bin.path)?;
139 writeln!(
140 writer,
141 " {} {}",
142 "build platform:".style(styles.field),
143 bin.build_platform,
144 )?;
145 } else {
146 writeln!(writer, "{}", bin.id.style(styles.binary_id))?;
147 }
148 }
149 Ok(())
150 }
151
152 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
154 let mut s = String::with_capacity(1024);
155 self.write(output_format, &mut s, false)?;
156 Ok(s)
157 }
158}
159
160#[derive(Debug)]
161struct BinaryListBuildState<'g> {
162 graph: &'g PackageGraph,
163 rust_binaries: Vec<RustTestBinary>,
164 rust_build_meta: RustBuildMeta<BinaryListState>,
165 alt_target_dir: Option<Utf8PathBuf>,
166}
167
168impl<'g> BinaryListBuildState<'g> {
169 fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
170 let rust_target_dir = graph.workspace().target_directory().to_path_buf();
171 let alt_target_dir = std::env::var("__NEXTEST_ALT_TARGET_DIR")
173 .ok()
174 .map(Utf8PathBuf::from);
175
176 Self {
177 graph,
178 rust_binaries: vec![],
179 rust_build_meta: RustBuildMeta::new(rust_target_dir, build_platforms),
180 alt_target_dir,
181 }
182 }
183
184 fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
185 match message {
186 Message::CompilerArtifact(artifact) => {
187 self.process_artifact(artifact)?;
188 }
189 Message::BuildScriptExecuted(build_script) => {
190 self.process_build_script(build_script)?;
191 }
192 _ => {
193 }
195 }
196
197 Ok(())
198 }
199
200 fn process_artifact(&mut self, artifact: Artifact) -> Result<(), FromMessagesError> {
201 if let Some(path) = artifact.executable {
202 self.detect_base_output_dir(&path);
203
204 if artifact.profile.test {
205 let package_id = artifact.package_id.repr;
206
207 let name = artifact.target.name;
210
211 let package = self
212 .graph
213 .metadata(&guppy::PackageId::new(package_id.clone()))
214 .map_err(FromMessagesError::PackageGraph)?;
215
216 let kind = artifact.target.kind;
217 if kind.is_empty() {
218 return Err(FromMessagesError::MissingTargetKind {
219 package_name: package.name().to_owned(),
220 binary_name: name.clone(),
221 });
222 }
223
224 let (computed_kind, platform) = if kind.iter().any(|k| {
225 matches!(
227 k,
228 TargetKind::Lib
229 | TargetKind::RLib
230 | TargetKind::DyLib
231 | TargetKind::CDyLib
232 | TargetKind::StaticLib
233 )
234 }) {
235 (RustTestBinaryKind::LIB, BuildPlatform::Target)
236 } else if let Some(TargetKind::ProcMacro) = kind.first() {
237 (RustTestBinaryKind::PROC_MACRO, BuildPlatform::Host)
238 } else {
239 (
241 RustTestBinaryKind::new(
242 kind.into_iter()
243 .next()
244 .expect("already checked that kind is non-empty")
245 .to_string(),
246 ),
247 BuildPlatform::Target,
248 )
249 };
250
251 let id = RustBinaryId::from_parts(package.name(), &computed_kind, &name);
253
254 self.rust_binaries.push(RustTestBinary {
255 path,
256 package_id,
257 kind: computed_kind,
258 name,
259 id,
260 build_platform: platform,
261 });
262 } else if artifact
263 .target
264 .kind
265 .iter()
266 .any(|x| matches!(x, TargetKind::Bin))
267 {
268 if let Ok(rel_path) = path.strip_prefix(&self.rust_build_meta.target_directory) {
272 let non_test_binary = RustNonTestBinarySummary {
273 name: artifact.target.name,
274 kind: RustNonTestBinaryKind::BIN_EXE,
275 path: convert_rel_path_to_forward_slash(rel_path),
276 };
277
278 self.rust_build_meta
279 .non_test_binaries
280 .entry(artifact.package_id.repr)
281 .or_default()
282 .insert(non_test_binary);
283 };
284 }
285 } else if artifact
286 .target
287 .kind
288 .iter()
289 .any(|x| matches!(x, TargetKind::DyLib | TargetKind::CDyLib))
290 {
291 for filename in artifact.filenames {
293 if let Ok(rel_path) = filename.strip_prefix(&self.rust_build_meta.target_directory)
294 {
295 let non_test_binary = RustNonTestBinarySummary {
296 name: artifact.target.name.clone(),
297 kind: RustNonTestBinaryKind::DYLIB,
298 path: convert_rel_path_to_forward_slash(rel_path),
299 };
300 self.rust_build_meta
301 .non_test_binaries
302 .entry(artifact.package_id.repr.clone())
303 .or_default()
304 .insert(non_test_binary);
305 }
306 }
307 }
308
309 Ok(())
310 }
311
312 fn detect_base_output_dir(&mut self, artifact_path: &Utf8Path) -> Option<()> {
324 let rel_path = artifact_path
326 .strip_prefix(&self.rust_build_meta.target_directory)
327 .ok()?;
328 let parent = rel_path.parent()?;
329 if parent.file_name() == Some("deps") {
330 let base = parent.parent()?;
331 if !self.rust_build_meta.base_output_directories.contains(base) {
332 self.rust_build_meta
333 .base_output_directories
334 .insert(convert_rel_path_to_forward_slash(base));
335 }
336 }
337 Some(())
338 }
339
340 fn process_build_script(&mut self, build_script: BuildScript) -> Result<(), FromMessagesError> {
341 for path in build_script.linked_paths {
342 self.detect_linked_path(&build_script.package_id, &path);
343 }
344
345 let package_id = guppy::PackageId::new(build_script.package_id.repr);
347 let in_workspace = self.graph.metadata(&package_id).map_or_else(
348 |_| {
349 warn!(
351 target: "nextest-runner::list",
352 "warning: saw package ID `{}` which wasn't produced by cargo metadata",
353 package_id
354 );
355 false
356 },
357 |p| p.in_workspace(),
358 );
359 if in_workspace {
360 if let Ok(rel_out_dir) = build_script
362 .out_dir
363 .strip_prefix(&self.rust_build_meta.target_directory)
364 {
365 self.rust_build_meta.build_script_out_dirs.insert(
366 package_id.repr().to_owned(),
367 convert_rel_path_to_forward_slash(rel_out_dir),
368 );
369 }
370 }
371
372 Ok(())
373 }
374
375 fn detect_linked_path(&mut self, package_id: &PackageId, path: &Utf8Path) -> Option<()> {
377 let actual_path = match path.as_str().split_once('=') {
379 Some((_, p)) => p.into(),
380 None => path,
381 };
382
383 let rel_path = match actual_path.strip_prefix(&self.rust_build_meta.target_directory) {
384 Ok(rel) => rel,
385 Err(_) => {
386 if let Some(alt_target_dir) = &self.alt_target_dir {
398 actual_path.strip_prefix(alt_target_dir).ok()?
399 } else {
400 return None;
401 }
402 }
403 };
404
405 self.rust_build_meta
406 .linked_paths
407 .entry(convert_rel_path_to_forward_slash(rel_path))
408 .or_default()
409 .insert(package_id.repr.clone());
410
411 Some(())
412 }
413
414 fn finish(mut self) -> BinaryList {
415 self.rust_binaries.sort_by(|b1, b2| b1.id.cmp(&b2.id));
416
417 let relevant_package_ids = self
419 .rust_binaries
420 .iter()
421 .map(|bin| bin.package_id.clone())
422 .collect::<HashSet<_>>();
423
424 self.rust_build_meta
425 .build_script_out_dirs
426 .retain(|package_id, _| relevant_package_ids.contains(package_id));
427
428 BinaryList {
429 rust_build_meta: self.rust_build_meta,
430 rust_binaries: self.rust_binaries,
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::{
439 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
440 list::SerializableFormat,
441 platform::{HostPlatform, PlatformLibdir, TargetPlatform},
442 };
443 use indoc::indoc;
444 use maplit::btreeset;
445 use nextest_metadata::PlatformLibdirUnavailable;
446 use pretty_assertions::assert_eq;
447 use target_spec::{Platform, TargetFeatures};
448
449 #[test]
450 fn test_parse_binary_list() {
451 let fake_bin_test = RustTestBinary {
452 id: "fake-package::bin/fake-binary".into(),
453 path: "/fake/binary".into(),
454 package_id: "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)"
455 .to_owned(),
456 kind: RustTestBinaryKind::LIB,
457 name: "fake-binary".to_owned(),
458 build_platform: BuildPlatform::Target,
459 };
460 let fake_macro_test = RustTestBinary {
461 id: "fake-macro::proc-macro/fake-macro".into(),
462 path: "/fake/macro".into(),
463 package_id: "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)"
464 .to_owned(),
465 kind: RustTestBinaryKind::PROC_MACRO,
466 name: "fake-macro".to_owned(),
467 build_platform: BuildPlatform::Host,
468 };
469
470 let fake_triple = TargetTriple {
471 platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
472 source: TargetTripleSource::CliOption,
473 location: TargetDefinitionLocation::Builtin,
474 };
475 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
476 let build_platforms = BuildPlatforms {
477 host: HostPlatform {
478 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
479 libdir: PlatformLibdir::Available(Utf8PathBuf::from(fake_host_libdir)),
480 },
481 target: Some(TargetPlatform {
482 triple: fake_triple,
483 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
485 }),
486 };
487
488 let mut rust_build_meta = RustBuildMeta::new("/fake/target", build_platforms);
489 rust_build_meta
490 .base_output_directories
491 .insert("my-profile".into());
492 rust_build_meta.non_test_binaries.insert(
493 "my-package-id".into(),
494 btreeset! {
495 RustNonTestBinarySummary {
496 name: "my-name".into(),
497 kind: RustNonTestBinaryKind::BIN_EXE,
498 path: "my-profile/my-name".into(),
499 },
500 RustNonTestBinarySummary {
501 name: "your-name".into(),
502 kind: RustNonTestBinaryKind::DYLIB,
503 path: "my-profile/your-name.dll".into(),
504 },
505 RustNonTestBinarySummary {
506 name: "your-name".into(),
507 kind: RustNonTestBinaryKind::DYLIB,
508 path: "my-profile/your-name.exp".into(),
509 },
510 },
511 );
512
513 let binary_list = BinaryList {
514 rust_build_meta,
515 rust_binaries: vec![fake_bin_test, fake_macro_test],
516 };
517
518 static EXPECTED_HUMAN: &str = indoc! {"
520 fake-package::bin/fake-binary
521 fake-macro::proc-macro/fake-macro
522 "};
523 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {r"
524 fake-package::bin/fake-binary:
525 bin: /fake/binary
526 build platform: target
527 fake-macro::proc-macro/fake-macro:
528 bin: /fake/macro
529 build platform: host
530 "};
531 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
532 {
533 "rust-build-meta": {
534 "target-directory": "/fake/target",
535 "base-output-directories": [
536 "my-profile"
537 ],
538 "non-test-binaries": {
539 "my-package-id": [
540 {
541 "name": "my-name",
542 "kind": "bin-exe",
543 "path": "my-profile/my-name"
544 },
545 {
546 "name": "your-name",
547 "kind": "dylib",
548 "path": "my-profile/your-name.dll"
549 },
550 {
551 "name": "your-name",
552 "kind": "dylib",
553 "path": "my-profile/your-name.exp"
554 }
555 ]
556 },
557 "build-script-out-dirs": {},
558 "linked-paths": [],
559 "platforms": {
560 "host": {
561 "platform": {
562 "triple": "x86_64-unknown-linux-gnu",
563 "target-features": "unknown"
564 },
565 "libdir": {
566 "status": "available",
567 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
568 }
569 },
570 "targets": [
571 {
572 "platform": {
573 "triple": "aarch64-unknown-linux-gnu",
574 "target-features": "unknown"
575 },
576 "libdir": {
577 "status": "unavailable",
578 "reason": "rustc-output-error"
579 }
580 }
581 ]
582 },
583 "target-platforms": [
584 {
585 "triple": "aarch64-unknown-linux-gnu",
586 "target-features": "unknown"
587 }
588 ],
589 "target-platform": "aarch64-unknown-linux-gnu"
590 },
591 "rust-binaries": {
592 "fake-macro::proc-macro/fake-macro": {
593 "binary-id": "fake-macro::proc-macro/fake-macro",
594 "binary-name": "fake-macro",
595 "package-id": "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)",
596 "kind": "proc-macro",
597 "binary-path": "/fake/macro",
598 "build-platform": "host"
599 },
600 "fake-package::bin/fake-binary": {
601 "binary-id": "fake-package::bin/fake-binary",
602 "binary-name": "fake-binary",
603 "package-id": "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)",
604 "kind": "lib",
605 "binary-path": "/fake/binary",
606 "build-platform": "target"
607 }
608 }
609 }"#};
610
611 assert_eq!(
612 binary_list
613 .to_string(OutputFormat::Human { verbose: false })
614 .expect("human succeeded"),
615 EXPECTED_HUMAN
616 );
617 assert_eq!(
618 binary_list
619 .to_string(OutputFormat::Human { verbose: true })
620 .expect("human succeeded"),
621 EXPECTED_HUMAN_VERBOSE
622 );
623 assert_eq!(
624 binary_list
625 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
626 .expect("json-pretty succeeded"),
627 EXPECTED_JSON_PRETTY
628 );
629 }
630}