1use crate::{
5 errors::RustBuildMetaParseError,
6 helpers::convert_rel_path_to_main_sep,
7 list::{BinaryListState, RustNonTestBinaries, TestListState},
8 platform::{BuildPlatforms, TargetPlatform},
9 reuse_build::PathMapper,
10};
11use camino::Utf8PathBuf;
12use itertools::Itertools;
13use nextest_metadata::{BuildPlatformsSummary, BuildScriptInfoSummary, RustBuildMetaSummary};
14use std::{
15 collections::{BTreeMap, BTreeSet},
16 marker::PhantomData,
17};
18use tracing::warn;
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct RustBuildMeta<State> {
23 pub target_directory: Utf8PathBuf,
26
27 pub build_directory: Utf8PathBuf,
31
32 pub base_output_directories: BTreeSet<Utf8PathBuf>,
34
35 pub(crate) non_test_binaries: RustNonTestBinaries,
38
39 pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
43
44 pub build_script_info: Option<BTreeMap<String, BuildScriptInfo>>,
51
52 pub linked_paths: BTreeMap<Utf8PathBuf, BTreeSet<String>>,
62
63 pub build_platforms: BuildPlatforms,
65
66 pub state: PhantomData<State>,
68}
69
70impl RustBuildMeta<BinaryListState> {
71 pub fn new(
73 target_directory: impl Into<Utf8PathBuf>,
74 build_directory: impl Into<Utf8PathBuf>,
75 build_platforms: BuildPlatforms,
76 ) -> Self {
77 Self {
78 target_directory: target_directory.into(),
79 build_directory: build_directory.into(),
80 base_output_directories: BTreeSet::new(),
81 non_test_binaries: RustNonTestBinaries::default(),
82 build_script_out_dirs: BTreeMap::new(),
83 build_script_info: Some(BTreeMap::new()),
84 linked_paths: BTreeMap::new(),
85 state: PhantomData,
86 build_platforms,
87 }
88 }
89
90 pub fn map_paths(&self, path_mapper: &PathMapper) -> RustBuildMeta<TestListState> {
92 let new_target_directory = path_mapper
93 .new_target_dir()
94 .unwrap_or(&self.target_directory)
95 .to_path_buf();
96 let new_build_directory = path_mapper
102 .new_build_dir()
103 .or(path_mapper.new_target_dir())
104 .unwrap_or(&self.build_directory)
105 .to_path_buf();
106
107 RustBuildMeta {
108 target_directory: new_target_directory,
109 build_directory: new_build_directory,
110 base_output_directories: self.base_output_directories.clone(),
112 non_test_binaries: self.non_test_binaries.clone(),
113 build_script_out_dirs: self.build_script_out_dirs.clone(),
114 build_script_info: self.build_script_info.clone(),
115 linked_paths: self.linked_paths.clone(),
116 state: PhantomData,
117 build_platforms: self.build_platforms.map_libdir(path_mapper.libdir_mapper()),
118 }
119 }
120}
121
122impl RustBuildMeta<TestListState> {
123 pub fn empty() -> Self {
127 Self {
128 target_directory: Utf8PathBuf::new(),
129 build_directory: Utf8PathBuf::new(),
130 base_output_directories: BTreeSet::new(),
131 non_test_binaries: RustNonTestBinaries::default(),
132 build_script_out_dirs: BTreeMap::new(),
133 build_script_info: Some(BTreeMap::new()),
134 linked_paths: BTreeMap::new(),
135 state: PhantomData,
136 build_platforms: BuildPlatforms::new_with_no_target().unwrap(),
137 }
138 }
139
140 pub fn dylib_paths(&self) -> Vec<Utf8PathBuf> {
148 let libdirs = self
154 .build_platforms
155 .host
156 .libdir
157 .as_path()
158 .into_iter()
159 .chain(
160 self.build_platforms
161 .target
162 .as_ref()
163 .and_then(|target| target.libdir.as_path()),
164 )
165 .map(|libdir| libdir.to_path_buf())
166 .collect::<Vec<_>>();
167 if libdirs.is_empty() {
168 warn!("failed to detect the rustc libdir, may fail to list or run tests");
169 }
170
171 self.linked_paths
173 .keys()
174 .filter_map(|rel_path| {
175 let join_path = self
176 .build_directory
177 .join(convert_rel_path_to_main_sep(rel_path));
178 join_path.exists().then_some(join_path)
180 })
181 .chain(self.base_output_directories.iter().flat_map(|base_output| {
182 let rel_base = convert_rel_path_to_main_sep(base_output);
183 let artifact_dir = self.target_directory.join(&rel_base);
188 let legacy_deps_dir = self.build_directory.join(rel_base).join("deps");
190 [artifact_dir, legacy_deps_dir]
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: RustNonTestBinaries::from_summary(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.to_summary(),
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_artifact_dir_is_under_the_target_directory() {
678 let rust_build_meta = RustBuildMeta {
679 target_directory: "/fake/target".into(),
680 build_directory: "/fake/build".into(),
681 base_output_directories: ["debug".into(), "aarch64-unknown-linux-gnu/debug".into()]
682 .into(),
683 build_platforms: BuildPlatforms {
684 host: host_current_with_libdir("/fake/rustc/host/libdir"),
685 target: None,
686 },
687 ..RustBuildMeta::empty()
688 };
689
690 assert_eq!(
691 rust_build_meta.dylib_paths(),
692 vec![
693 Utf8PathBuf::from("/fake/target/aarch64-unknown-linux-gnu/debug"),
694 Utf8PathBuf::from("/fake/build/aarch64-unknown-linux-gnu/debug/deps"),
695 Utf8PathBuf::from("/fake/target/debug"),
696 Utf8PathBuf::from("/fake/build/debug/deps"),
697 Utf8PathBuf::from("/fake/rustc/host/libdir"),
698 ],
699 );
700 }
701
702 #[test]
703 fn test_dylib_paths_should_not_contain_duplicate_paths() {
704 let tmpdir = camino_tempfile::tempdir().expect("should create temp dir successfully");
705 let host_libdir = tmpdir.path().to_path_buf();
706 let target_libdir = host_libdir.clone();
707 let fake_target_dir = tmpdir
708 .path()
709 .parent()
710 .expect("tmp directory should have a parent");
711 let tmpdir_dirname = tmpdir
712 .path()
713 .file_name()
714 .expect("tmp directory should have a file name");
715
716 let rust_build_meta = RustBuildMeta {
717 target_directory: fake_target_dir.to_path_buf(),
718 build_directory: fake_target_dir.to_path_buf(),
719 linked_paths: [(Utf8PathBuf::from(tmpdir_dirname), Default::default())].into(),
720 base_output_directories: [Utf8PathBuf::from(tmpdir_dirname)].into(),
721 build_platforms: BuildPlatforms {
722 host: host_current_with_libdir(host_libdir.as_ref()),
723 target: Some(TargetPlatform::new(
724 TargetTriple::x86_64_unknown_linux_gnu(),
725 PlatformLibdir::Available(target_libdir.clone()),
726 )),
727 },
728 ..RustBuildMeta::empty()
729 };
730 let dylib_paths = rust_build_meta.dylib_paths();
731
732 let expected_abs = fake_target_dir.join(tmpdir_dirname);
736 assert!(
737 dylib_paths.contains(&expected_abs),
738 "{dylib_paths:?} should contain {expected_abs}"
739 );
740 assert!(
741 dylib_paths.clone().into_iter().all_unique(),
742 "{dylib_paths:?} should not contain duplicate paths"
743 );
744 }
745}