1use crate::{
5 errors::RustBuildMetaParseError,
6 helpers::convert_rel_path_to_main_sep,
7 list::{BinaryListState, TestListState},
8 platform::{BuildPlatforms, TargetPlatform},
9 reuse_build::PathMapper,
10};
11use camino::Utf8PathBuf;
12use itertools::Itertools;
13use nextest_metadata::{
14 BuildPlatformsSummary, BuildScriptInfoSummary, RustBuildMetaSummary, RustNonTestBinarySummary,
15};
16use std::{
17 collections::{BTreeMap, BTreeSet},
18 marker::PhantomData,
19};
20use tracing::warn;
21
22#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct RustBuildMeta<State> {
25 pub target_directory: Utf8PathBuf,
28
29 pub build_directory: Utf8PathBuf,
33
34 pub base_output_directories: BTreeSet<Utf8PathBuf>,
38
39 pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
42
43 pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
47
48 pub build_script_info: Option<BTreeMap<String, BuildScriptInfo>>,
55
56 pub linked_paths: BTreeMap<Utf8PathBuf, BTreeSet<String>>,
66
67 pub build_platforms: BuildPlatforms,
69
70 pub state: PhantomData<State>,
72}
73
74impl RustBuildMeta<BinaryListState> {
75 pub fn new(
77 target_directory: impl Into<Utf8PathBuf>,
78 build_directory: impl Into<Utf8PathBuf>,
79 build_platforms: BuildPlatforms,
80 ) -> Self {
81 Self {
82 target_directory: target_directory.into(),
83 build_directory: build_directory.into(),
84 base_output_directories: BTreeSet::new(),
85 non_test_binaries: BTreeMap::new(),
86 build_script_out_dirs: BTreeMap::new(),
87 build_script_info: Some(BTreeMap::new()),
88 linked_paths: BTreeMap::new(),
89 state: PhantomData,
90 build_platforms,
91 }
92 }
93
94 pub fn map_paths(&self, path_mapper: &PathMapper) -> RustBuildMeta<TestListState> {
96 let new_target_directory = path_mapper
97 .new_target_dir()
98 .unwrap_or(&self.target_directory)
99 .to_path_buf();
100 let new_build_directory = path_mapper
106 .new_build_dir()
107 .or(path_mapper.new_target_dir())
108 .unwrap_or(&self.build_directory)
109 .to_path_buf();
110
111 RustBuildMeta {
112 target_directory: new_target_directory,
113 build_directory: new_build_directory,
114 base_output_directories: self.base_output_directories.clone(),
116 non_test_binaries: self.non_test_binaries.clone(),
117 build_script_out_dirs: self.build_script_out_dirs.clone(),
118 build_script_info: self.build_script_info.clone(),
119 linked_paths: self.linked_paths.clone(),
120 state: PhantomData,
121 build_platforms: self.build_platforms.map_libdir(path_mapper.libdir_mapper()),
122 }
123 }
124}
125
126impl RustBuildMeta<TestListState> {
127 pub fn empty() -> Self {
131 Self {
132 target_directory: Utf8PathBuf::new(),
133 build_directory: Utf8PathBuf::new(),
134 base_output_directories: BTreeSet::new(),
135 non_test_binaries: BTreeMap::new(),
136 build_script_out_dirs: BTreeMap::new(),
137 build_script_info: Some(BTreeMap::new()),
138 linked_paths: BTreeMap::new(),
139 state: PhantomData,
140 build_platforms: BuildPlatforms::new_with_no_target().unwrap(),
141 }
142 }
143
144 pub fn dylib_paths(&self) -> Vec<Utf8PathBuf> {
152 let libdirs = self
158 .build_platforms
159 .host
160 .libdir
161 .as_path()
162 .into_iter()
163 .chain(
164 self.build_platforms
165 .target
166 .as_ref()
167 .and_then(|target| target.libdir.as_path()),
168 )
169 .map(|libdir| libdir.to_path_buf())
170 .collect::<Vec<_>>();
171 if libdirs.is_empty() {
172 warn!("failed to detect the rustc libdir, may fail to list or run tests");
173 }
174
175 self.linked_paths
179 .keys()
180 .filter_map(|rel_path| {
181 let join_path = self
182 .build_directory
183 .join(convert_rel_path_to_main_sep(rel_path));
184 join_path.exists().then_some(join_path)
186 })
187 .chain(self.base_output_directories.iter().flat_map(|base_output| {
188 let abs_base = self
189 .build_directory
190 .join(convert_rel_path_to_main_sep(base_output));
191 let with_deps = abs_base.join("deps");
192 [with_deps, abs_base]
194 }))
195 .chain(libdirs)
196 .unique()
197 .collect()
198 }
199}
200
201impl<State> RustBuildMeta<State> {
202 pub fn from_summary(summary: RustBuildMetaSummary) -> Result<Self, RustBuildMetaParseError> {
204 let build_platforms = if let Some(summary) = summary.platforms {
205 BuildPlatforms::from_summary(summary.clone())?
206 } else if let Some(summary) = summary.target_platforms.first() {
207 BuildPlatforms::from_target_summary(summary.clone())?
209 } else {
210 BuildPlatforms::from_summary_str(summary.target_platform.clone())?
212 };
213
214 let build_directory = summary
217 .build_directory
218 .unwrap_or_else(|| summary.target_directory.clone());
219
220 Ok(Self {
221 target_directory: summary.target_directory,
222 build_directory,
223 base_output_directories: summary.base_output_directories,
224 build_script_out_dirs: summary.build_script_out_dirs,
225 build_script_info: summary.build_script_info.map(|info| {
226 info.into_iter()
227 .map(|(k, v)| (k, BuildScriptInfo::from_summary(v)))
228 .collect()
229 }),
230 non_test_binaries: summary.non_test_binaries,
231 linked_paths: summary
232 .linked_paths
233 .into_iter()
234 .map(|linked_path| (linked_path, BTreeSet::new()))
235 .collect(),
236 state: PhantomData,
237 build_platforms,
238 })
239 }
240
241 pub fn to_summary(&self) -> RustBuildMetaSummary {
243 RustBuildMetaSummary {
244 target_directory: self.target_directory.clone(),
245 build_directory: Some(self.build_directory.clone()),
246 base_output_directories: self.base_output_directories.clone(),
247 non_test_binaries: self.non_test_binaries.clone(),
248 build_script_out_dirs: self.build_script_out_dirs.clone(),
249 build_script_info: self.build_script_info.as_ref().map(|info| {
250 info.iter()
251 .map(|(k, v)| (k.clone(), v.to_summary()))
252 .collect()
253 }),
254 linked_paths: self.linked_paths.keys().cloned().collect(),
255 target_platform: self.build_platforms.to_summary_str(),
256 target_platforms: vec![self.build_platforms.to_target_or_host_summary()],
257 platforms: Some(BuildPlatformsSummary {
259 host: self.build_platforms.host.to_summary(),
260 targets: self
261 .build_platforms
262 .target
263 .as_ref()
264 .into_iter()
265 .map(TargetPlatform::to_summary)
266 .collect(),
267 }),
268 }
269 }
270
271 pub fn to_archive_summary(&self) -> RustBuildMetaSummary {
276 let mut summary = self.to_summary();
277 summary.build_directory = None;
280 summary
281 }
282}
283
284#[derive(Clone, Debug, Default, Eq, PartialEq)]
286pub struct BuildScriptInfo {
287 pub envs: BTreeMap<String, String>,
289}
290
291impl BuildScriptInfo {
292 fn from_summary(summary: BuildScriptInfoSummary) -> Self {
293 Self { envs: summary.envs }
294 }
295
296 fn to_summary(&self) -> BuildScriptInfoSummary {
297 BuildScriptInfoSummary {
298 envs: self.envs.clone(),
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::{
307 cargo_config::TargetTriple,
308 platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
309 };
310 use nextest_metadata::{
311 BuildPlatformsSummary, HostPlatformSummary, PlatformLibdirSummary,
312 PlatformLibdirUnavailable,
313 };
314 use target_spec::{Platform, summaries::PlatformSummary};
315 use test_case::test_case;
316
317 impl Default for RustBuildMeta<BinaryListState> {
318 fn default() -> Self {
319 RustBuildMeta::<BinaryListState>::new(
320 Utf8PathBuf::default(),
321 Utf8PathBuf::default(),
322 BuildPlatforms::new_with_no_target()
323 .expect("creating BuildPlatforms without target triple should succeed"),
324 )
325 }
326 }
327
328 fn x86_64_pc_windows_msvc_triple() -> TargetTriple {
329 TargetTriple::deserialize_str(Some("x86_64-pc-windows-msvc".to_owned()))
330 .expect("creating TargetTriple should succeed")
331 .expect("the output of deserialize_str shouldn't be None")
332 }
333
334 fn host_current() -> HostPlatform {
335 HostPlatform {
336 platform: Platform::build_target()
337 .expect("should detect the build target successfully"),
338 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
339 }
340 }
341
342 fn host_current_with_libdir(libdir: &str) -> HostPlatform {
343 HostPlatform {
344 platform: Platform::build_target()
345 .expect("should detect the build target successfully"),
346 libdir: PlatformLibdir::Available(libdir.into()),
347 }
348 }
349
350 fn host_not_current_with_libdir(libdir: &str) -> HostPlatform {
351 cfg_if::cfg_if! {
352 if #[cfg(windows)] {
353 let triple = TargetTriple::x86_64_unknown_linux_gnu();
354 } else {
355 let triple = x86_64_pc_windows_msvc_triple();
356 }
357 };
358
359 HostPlatform {
360 platform: triple.platform,
361 libdir: PlatformLibdir::Available(libdir.into()),
362 }
363 }
364
365 fn target_linux() -> TargetPlatform {
366 TargetPlatform::new(
367 TargetTriple::x86_64_unknown_linux_gnu(),
368 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
369 )
370 }
371
372 fn target_linux_with_libdir(libdir: &str) -> TargetPlatform {
373 TargetPlatform::new(
374 TargetTriple::x86_64_unknown_linux_gnu(),
375 PlatformLibdir::Available(libdir.into()),
376 )
377 }
378
379 fn target_windows() -> TargetPlatform {
380 TargetPlatform::new(
381 x86_64_pc_windows_msvc_triple(),
382 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
383 )
384 }
385
386 #[test_case(RustBuildMetaSummary {
387 ..Default::default()
388 }, RustBuildMeta::<BinaryListState> {
389 build_platforms: BuildPlatforms {
390 host: host_current(),
391 target: None,
392 },
393 build_script_info: None,
395 ..Default::default()
396 }; "no target platforms")]
397 #[test_case(RustBuildMetaSummary {
398 target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
399 ..Default::default()
400 }, RustBuildMeta::<BinaryListState> {
401 build_platforms: BuildPlatforms {
402 host: host_current(),
403 target: Some(target_linux()),
404 },
405 build_script_info: None,
406 ..Default::default()
407 }; "only target platform field")]
408 #[test_case(RustBuildMetaSummary {
409 target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
410 target_platforms: vec![PlatformSummary::new("x86_64-pc-windows-msvc")],
412 ..Default::default()
413 }, RustBuildMeta::<BinaryListState> {
414 build_platforms: BuildPlatforms {
415 host: host_current(),
416 target: Some(target_windows()),
417 },
418 build_script_info: None,
419 ..Default::default()
420 }; "target platform and target platforms field")]
421 #[test_case(RustBuildMetaSummary {
422 target_platform: Some("aarch64-unknown-linux-gnu".to_owned()),
423 target_platforms: vec![PlatformSummary::new("x86_64-pc-windows-msvc")],
424 platforms: Some(BuildPlatformsSummary {
426 host: host_not_current_with_libdir("/fake/test/libdir/281").to_summary(),
427 targets: vec![target_linux_with_libdir("/fake/test/libdir/837").to_summary()],
428 }),
429 ..Default::default()
430 }, RustBuildMeta::<BinaryListState> {
431 build_platforms: BuildPlatforms {
432 host: host_not_current_with_libdir("/fake/test/libdir/281"),
433 target: Some(target_linux_with_libdir("/fake/test/libdir/837")),
434 },
435 build_script_info: None,
436 ..Default::default()
437 }; "target platform and target platforms and platforms field")]
438 #[test_case(RustBuildMetaSummary {
439 platforms: Some(BuildPlatformsSummary {
440 host: host_current().to_summary(),
441 targets: vec![],
442 }),
443 ..Default::default()
444 }, RustBuildMeta::<BinaryListState> {
445 build_platforms: BuildPlatforms {
446 host: host_current(),
447 target: None,
448 },
449 build_script_info: None,
450 ..Default::default()
451 }; "platforms with zero targets")]
452 #[test_case(RustBuildMetaSummary {
453 target_directory: "/fake/target".into(),
454 build_directory: Some("/fake/build".into()),
455 platforms: Some(BuildPlatformsSummary {
456 host: host_current().to_summary(),
457 targets: vec![],
458 }),
459 ..Default::default()
460 }, RustBuildMeta::<BinaryListState> {
461 target_directory: "/fake/target".into(),
462 build_directory: "/fake/build".into(),
463 build_platforms: BuildPlatforms {
464 host: host_current(),
465 target: None,
466 },
467 build_script_info: None,
468 ..Default::default()
469 }; "build directory differs from target directory")]
470 #[test_case(RustBuildMetaSummary {
471 target_directory: "/fake/target".into(),
472 build_directory: None,
473 platforms: Some(BuildPlatformsSummary {
474 host: host_current().to_summary(),
475 targets: vec![],
476 }),
477 ..Default::default()
478 }, RustBuildMeta::<BinaryListState> {
479 target_directory: "/fake/target".into(),
480 build_directory: "/fake/target".into(),
482 build_platforms: BuildPlatforms {
483 host: host_current(),
484 target: None,
485 },
486 build_script_info: None,
487 ..Default::default()
488 }; "build directory absent defaults to target directory")]
489 fn test_from_summary(summary: RustBuildMetaSummary, expected: RustBuildMeta<BinaryListState>) {
490 let actual = RustBuildMeta::<BinaryListState>::from_summary(summary)
491 .expect("RustBuildMeta should deserialize from summary with success.");
492 assert_eq!(actual, expected);
493 }
494
495 #[test]
496 fn test_from_summary_error_multiple_targets() {
497 let summary = RustBuildMetaSummary {
498 platforms: Some(BuildPlatformsSummary {
499 host: host_current().to_summary(),
500 targets: vec![target_linux().to_summary(), target_windows().to_summary()],
501 }),
502 ..Default::default()
503 };
504 let actual = RustBuildMeta::<BinaryListState>::from_summary(summary);
505 assert!(
506 matches!(actual, Err(RustBuildMetaParseError::Unsupported { .. })),
507 "Expect the parse result to be an error of RustBuildMetaParseError::Unsupported, actual {actual:?}"
508 );
509 }
510
511 #[test]
512 fn test_from_summary_error_invalid_host_platform_summary() {
513 let summary = RustBuildMetaSummary {
514 platforms: Some(BuildPlatformsSummary {
515 host: HostPlatformSummary {
516 platform: PlatformSummary::new("invalid-platform-triple"),
517 libdir: PlatformLibdirSummary::Unavailable {
518 reason: PlatformLibdirUnavailable::RUSTC_FAILED,
519 },
520 },
521 targets: vec![],
522 }),
523 ..Default::default()
524 };
525 let actual = RustBuildMeta::<BinaryListState>::from_summary(summary);
526 actual.expect_err("parse result should be an error");
527 }
528
529 #[test_case(RustBuildMeta::<BinaryListState> {
530 build_platforms: BuildPlatforms {
531 host: host_current(),
532 target: None,
533 },
534 ..Default::default()
535 }, RustBuildMetaSummary {
536 target_platform: None,
537 target_platforms: vec![host_current().to_summary().platform],
538 platforms: Some(BuildPlatformsSummary {
539 host: host_current().to_summary(),
540 targets: vec![],
541 }),
542 build_script_info: Some(BTreeMap::new()),
543 build_directory: Some(Utf8PathBuf::new()),
544 ..Default::default()
545 }; "build platforms without target")]
546 #[test_case(RustBuildMeta::<BinaryListState> {
547 build_platforms: BuildPlatforms {
548 host: host_current_with_libdir("/fake/test/libdir/736"),
549 target: Some(target_linux_with_libdir("/fake/test/libdir/873")),
550 },
551 ..Default::default()
552 }, RustBuildMetaSummary {
553 target_platform: Some(
554 target_linux_with_libdir("/fake/test/libdir/873")
555 .triple
556 .platform
557 .triple_str()
558 .to_owned(),
559 ),
560 target_platforms: vec![target_linux_with_libdir("/fake/test/libdir/873").triple.platform.to_summary()],
561 platforms: Some(BuildPlatformsSummary {
562 host: host_current_with_libdir("/fake/test/libdir/736").to_summary(),
563 targets: vec![target_linux_with_libdir("/fake/test/libdir/873").to_summary()],
564 }),
565 build_script_info: Some(BTreeMap::new()),
566 build_directory: Some(Utf8PathBuf::new()),
567 ..Default::default()
568 }; "build platforms with target")]
569 #[test_case(RustBuildMeta::<BinaryListState> {
570 target_directory: "/fake/target".into(),
571 build_directory: "/fake/build".into(),
572 build_platforms: BuildPlatforms {
573 host: host_current(),
574 target: None,
575 },
576 ..Default::default()
577 }, RustBuildMetaSummary {
578 target_directory: "/fake/target".into(),
579 build_directory: Some("/fake/build".into()),
581 target_platform: None,
582 target_platforms: vec![host_current().to_summary().platform],
583 platforms: Some(BuildPlatformsSummary {
584 host: host_current().to_summary(),
585 targets: vec![],
586 }),
587 build_script_info: Some(BTreeMap::new()),
588 ..Default::default()
589 }; "build directory differs from target directory")]
590 #[test_case(RustBuildMeta::<BinaryListState> {
591 target_directory: "/fake/target".into(),
592 build_directory: "/fake/target".into(),
593 build_platforms: BuildPlatforms {
594 host: host_current(),
595 target: None,
596 },
597 ..Default::default()
598 }, RustBuildMetaSummary {
599 target_directory: "/fake/target".into(),
600 build_directory: Some("/fake/target".into()),
602 target_platform: None,
603 target_platforms: vec![host_current().to_summary().platform],
604 platforms: Some(BuildPlatformsSummary {
605 host: host_current().to_summary(),
606 targets: vec![],
607 }),
608 build_script_info: Some(BTreeMap::new()),
609 ..Default::default()
610 }; "build directory equals target directory")]
611 fn test_to_summary(meta: RustBuildMeta<BinaryListState>, expected: RustBuildMetaSummary) {
612 let actual = meta.to_summary();
613 assert_eq!(actual, expected);
614 }
615
616 #[test]
617 fn test_to_archive_summary_omits_build_directory() {
618 let meta = RustBuildMeta::<BinaryListState> {
619 target_directory: "/fake/target".into(),
620 build_directory: "/fake/build".into(),
621 build_platforms: BuildPlatforms {
622 host: host_current(),
623 target: None,
624 },
625 ..Default::default()
626 };
627
628 let archive_summary = meta.to_archive_summary();
629
630 assert_eq!(
633 archive_summary.build_directory, None,
634 "to_archive_summary should always set build_directory to None"
635 );
636 assert_eq!(archive_summary.target_directory, meta.target_directory);
637
638 let round_tripped = RustBuildMeta::<BinaryListState>::from_summary(archive_summary)
641 .expect("from_summary should succeed");
642 assert_eq!(
643 round_tripped.build_directory, round_tripped.target_directory,
644 "after round-trip through archive summary, \
645 build_directory should equal target_directory"
646 );
647 }
648
649 #[test]
650 fn test_dylib_paths_should_include_rustc_dir() {
651 let host_libdir = Utf8PathBuf::from("/fake/rustc/host/libdir");
652 let target_libdir = Utf8PathBuf::from("/fake/rustc/target/libdir");
653
654 let rust_build_meta = RustBuildMeta {
655 build_platforms: BuildPlatforms {
656 host: host_current_with_libdir(host_libdir.as_ref()),
657 target: Some(TargetPlatform::new(
658 TargetTriple::x86_64_unknown_linux_gnu(),
659 PlatformLibdir::Available(target_libdir.clone()),
660 )),
661 },
662 ..RustBuildMeta::empty()
663 };
664 let dylib_paths = rust_build_meta.dylib_paths();
665
666 assert!(
667 dylib_paths.contains(&host_libdir),
668 "{dylib_paths:?} should contain {host_libdir}"
669 );
670 assert!(
671 dylib_paths.contains(&target_libdir),
672 "{dylib_paths:?} should contain {target_libdir}"
673 );
674 }
675
676 #[test]
677 fn test_dylib_paths_should_not_contain_duplicate_paths() {
678 let tmpdir = camino_tempfile::tempdir().expect("should create temp dir successfully");
679 let host_libdir = tmpdir.path().to_path_buf();
680 let target_libdir = host_libdir.clone();
681 let fake_target_dir = tmpdir
682 .path()
683 .parent()
684 .expect("tmp directory should have a parent");
685 let tmpdir_dirname = tmpdir
686 .path()
687 .file_name()
688 .expect("tmp directory should have a file name");
689
690 let rust_build_meta = RustBuildMeta {
691 target_directory: fake_target_dir.to_path_buf(),
692 build_directory: fake_target_dir.to_path_buf(),
693 linked_paths: [(Utf8PathBuf::from(tmpdir_dirname), Default::default())].into(),
694 base_output_directories: [Utf8PathBuf::from(tmpdir_dirname)].into(),
695 build_platforms: BuildPlatforms {
696 host: host_current_with_libdir(host_libdir.as_ref()),
697 target: Some(TargetPlatform::new(
698 TargetTriple::x86_64_unknown_linux_gnu(),
699 PlatformLibdir::Available(target_libdir.clone()),
700 )),
701 },
702 ..RustBuildMeta::empty()
703 };
704 let dylib_paths = rust_build_meta.dylib_paths();
705
706 let expected_abs = fake_target_dir.join(tmpdir_dirname);
710 assert!(
711 dylib_paths.contains(&expected_abs),
712 "{dylib_paths:?} should contain {expected_abs}"
713 );
714 assert!(
715 dylib_paths.clone().into_iter().all_unique(),
716 "{dylib_paths:?} should not contain duplicate paths"
717 );
718 }
719}