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::{BuildPlatformsSummary, RustBuildMetaSummary, RustNonTestBinarySummary};
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,
25
26 pub base_output_directories: BTreeSet<Utf8PathBuf>,
29
30 pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
32
33 pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
36
37 pub linked_paths: BTreeMap<Utf8PathBuf, BTreeSet<String>>,
45
46 pub build_platforms: BuildPlatforms,
48
49 state: PhantomData<State>,
50}
51
52impl RustBuildMeta<BinaryListState> {
53 pub fn new(target_directory: impl Into<Utf8PathBuf>, build_platforms: BuildPlatforms) -> Self {
55 Self {
56 target_directory: target_directory.into(),
57 base_output_directories: BTreeSet::new(),
58 non_test_binaries: BTreeMap::new(),
59 build_script_out_dirs: BTreeMap::new(),
60 linked_paths: BTreeMap::new(),
61 state: PhantomData,
62 build_platforms,
63 }
64 }
65
66 pub fn map_paths(&self, path_mapper: &PathMapper) -> RustBuildMeta<TestListState> {
68 RustBuildMeta {
69 target_directory: path_mapper
70 .new_target_dir()
71 .unwrap_or(&self.target_directory)
72 .to_path_buf(),
73 base_output_directories: self.base_output_directories.clone(),
75 non_test_binaries: self.non_test_binaries.clone(),
76 build_script_out_dirs: self.build_script_out_dirs.clone(),
77 linked_paths: self.linked_paths.clone(),
78 state: PhantomData,
79 build_platforms: self.build_platforms.map_libdir(path_mapper.libdir_mapper()),
80 }
81 }
82}
83
84impl RustBuildMeta<TestListState> {
85 #[cfg(test)]
87 pub(crate) fn empty() -> Self {
88 Self {
89 target_directory: Utf8PathBuf::new(),
90 base_output_directories: BTreeSet::new(),
91 non_test_binaries: BTreeMap::new(),
92 build_script_out_dirs: BTreeMap::new(),
93 linked_paths: BTreeMap::new(),
94 state: PhantomData,
95 build_platforms: BuildPlatforms::new_with_no_target().unwrap(),
96 }
97 }
98
99 pub fn dylib_paths(&self) -> Vec<Utf8PathBuf> {
107 let libdirs = self
113 .build_platforms
114 .host
115 .libdir
116 .as_path()
117 .into_iter()
118 .chain(
119 self.build_platforms
120 .target
121 .as_ref()
122 .and_then(|target| target.libdir.as_path()),
123 )
124 .map(|libdir| libdir.to_path_buf())
125 .collect::<Vec<_>>();
126 if libdirs.is_empty() {
127 warn!("failed to detect the rustc libdir, may fail to list or run tests");
128 }
129
130 self.linked_paths
132 .keys()
133 .filter_map(|rel_path| {
134 let join_path = self
135 .target_directory
136 .join(convert_rel_path_to_main_sep(rel_path));
137 join_path.exists().then_some(join_path)
139 })
140 .chain(self.base_output_directories.iter().flat_map(|base_output| {
141 let abs_base = self
142 .target_directory
143 .join(convert_rel_path_to_main_sep(base_output));
144 let with_deps = abs_base.join("deps");
145 [with_deps, abs_base]
147 }))
148 .chain(libdirs)
149 .unique()
150 .collect()
151 }
152}
153
154impl<State> RustBuildMeta<State> {
155 pub fn from_summary(summary: RustBuildMetaSummary) -> Result<Self, RustBuildMetaParseError> {
157 let build_platforms = if let Some(summary) = summary.platforms {
158 BuildPlatforms::from_summary(summary.clone())?
159 } else if let Some(summary) = summary.target_platforms.first() {
160 BuildPlatforms::from_target_summary(summary.clone())?
162 } else {
163 BuildPlatforms::from_summary_str(summary.target_platform.clone())?
165 };
166
167 Ok(Self {
168 target_directory: summary.target_directory,
169 base_output_directories: summary.base_output_directories,
170 build_script_out_dirs: summary.build_script_out_dirs,
171 non_test_binaries: summary.non_test_binaries,
172 linked_paths: summary
173 .linked_paths
174 .into_iter()
175 .map(|linked_path| (linked_path, BTreeSet::new()))
176 .collect(),
177 state: PhantomData,
178 build_platforms,
179 })
180 }
181
182 pub fn to_summary(&self) -> RustBuildMetaSummary {
184 RustBuildMetaSummary {
185 target_directory: self.target_directory.clone(),
186 base_output_directories: self.base_output_directories.clone(),
187 non_test_binaries: self.non_test_binaries.clone(),
188 build_script_out_dirs: self.build_script_out_dirs.clone(),
189 linked_paths: self.linked_paths.keys().cloned().collect(),
190 target_platform: self.build_platforms.to_summary_str(),
191 target_platforms: vec![self.build_platforms.to_target_or_host_summary()],
192 platforms: Some(BuildPlatformsSummary {
194 host: self.build_platforms.host.to_summary(),
195 targets: self
196 .build_platforms
197 .target
198 .as_ref()
199 .into_iter()
200 .map(TargetPlatform::to_summary)
201 .collect(),
202 }),
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::{
211 cargo_config::TargetTriple,
212 platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
213 };
214 use nextest_metadata::{
215 BuildPlatformsSummary, HostPlatformSummary, PlatformLibdirSummary,
216 PlatformLibdirUnavailable,
217 };
218 use target_spec::{Platform, summaries::PlatformSummary};
219 use test_case::test_case;
220
221 impl Default for RustBuildMeta<BinaryListState> {
222 fn default() -> Self {
223 RustBuildMeta::<BinaryListState>::new(
224 Utf8PathBuf::default(),
225 BuildPlatforms::new_with_no_target()
226 .expect("creating BuildPlatforms without target triple should succeed"),
227 )
228 }
229 }
230
231 fn x86_64_pc_windows_msvc_triple() -> TargetTriple {
232 TargetTriple::deserialize_str(Some("x86_64-pc-windows-msvc".to_owned()))
233 .expect("creating TargetTriple should succeed")
234 .expect("the output of deserialize_str shouldn't be None")
235 }
236
237 fn host_current() -> HostPlatform {
238 HostPlatform {
239 platform: Platform::build_target()
240 .expect("should detect the build target successfully"),
241 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
242 }
243 }
244
245 fn host_current_with_libdir(libdir: &str) -> HostPlatform {
246 HostPlatform {
247 platform: Platform::build_target()
248 .expect("should detect the build target successfully"),
249 libdir: PlatformLibdir::Available(libdir.into()),
250 }
251 }
252
253 fn host_not_current_with_libdir(libdir: &str) -> HostPlatform {
254 cfg_if::cfg_if! {
255 if #[cfg(windows)] {
256 let triple = TargetTriple::x86_64_unknown_linux_gnu();
257 } else {
258 let triple = x86_64_pc_windows_msvc_triple();
259 }
260 };
261
262 HostPlatform {
263 platform: triple.platform,
264 libdir: PlatformLibdir::Available(libdir.into()),
265 }
266 }
267
268 fn target_linux() -> TargetPlatform {
269 TargetPlatform::new(
270 TargetTriple::x86_64_unknown_linux_gnu(),
271 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
272 )
273 }
274
275 fn target_linux_with_libdir(libdir: &str) -> TargetPlatform {
276 TargetPlatform::new(
277 TargetTriple::x86_64_unknown_linux_gnu(),
278 PlatformLibdir::Available(libdir.into()),
279 )
280 }
281
282 fn target_windows() -> TargetPlatform {
283 TargetPlatform::new(
284 x86_64_pc_windows_msvc_triple(),
285 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
286 )
287 }
288
289 #[test_case(RustBuildMetaSummary {
290 ..Default::default()
291 }, RustBuildMeta::<BinaryListState> {
292 build_platforms: BuildPlatforms {
293 host: host_current(),
294 target: None,
295 },
296 ..Default::default()
297 }; "no target platforms")]
298 #[test_case(RustBuildMetaSummary {
299 target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
300 ..Default::default()
301 }, RustBuildMeta::<BinaryListState> {
302 build_platforms: BuildPlatforms {
303 host: host_current(),
304 target: Some(target_linux()),
305 },
306 ..Default::default()
307 }; "only target platform field")]
308 #[test_case(RustBuildMetaSummary {
309 target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
310 target_platforms: vec![PlatformSummary::new("x86_64-pc-windows-msvc")],
312 ..Default::default()
313 }, RustBuildMeta::<BinaryListState> {
314 build_platforms: BuildPlatforms {
315 host: host_current(),
316 target: Some(target_windows()),
317 },
318 ..Default::default()
319 }; "target platform and target platforms field")]
320 #[test_case(RustBuildMetaSummary {
321 target_platform: Some("aarch64-unknown-linux-gnu".to_owned()),
322 target_platforms: vec![PlatformSummary::new("x86_64-pc-windows-msvc")],
323 platforms: Some(BuildPlatformsSummary {
325 host: host_not_current_with_libdir("/fake/test/libdir/281").to_summary(),
326 targets: vec![target_linux_with_libdir("/fake/test/libdir/837").to_summary()],
327 }),
328 ..Default::default()
329 }, RustBuildMeta::<BinaryListState> {
330 build_platforms: BuildPlatforms {
331 host: host_not_current_with_libdir("/fake/test/libdir/281"),
332 target: Some(target_linux_with_libdir("/fake/test/libdir/837")),
333 },
334 ..Default::default()
335 }; "target platform and target platforms and platforms field")]
336 #[test_case(RustBuildMetaSummary {
337 platforms: Some(BuildPlatformsSummary {
338 host: host_current().to_summary(),
339 targets: vec![],
340 }),
341 ..Default::default()
342 }, RustBuildMeta::<BinaryListState> {
343 build_platforms: BuildPlatforms {
344 host: host_current(),
345 target: None,
346 },
347 ..Default::default()
348 }; "platforms with zero targets")]
349 fn test_from_summary(summary: RustBuildMetaSummary, expected: RustBuildMeta<BinaryListState>) {
350 let actual = RustBuildMeta::<BinaryListState>::from_summary(summary)
351 .expect("RustBuildMeta should deserialize from summary with success.");
352 assert_eq!(actual, expected);
353 }
354
355 #[test]
356 fn test_from_summary_error_multiple_targets() {
357 let summary = RustBuildMetaSummary {
358 platforms: Some(BuildPlatformsSummary {
359 host: host_current().to_summary(),
360 targets: vec![target_linux().to_summary(), target_windows().to_summary()],
361 }),
362 ..Default::default()
363 };
364 let actual = RustBuildMeta::<BinaryListState>::from_summary(summary);
365 assert!(
366 matches!(actual, Err(RustBuildMetaParseError::Unsupported { .. })),
367 "Expect the parse result to be an error of RustBuildMetaParseError::Unsupported, actual {actual:?}"
368 );
369 }
370
371 #[test]
372 fn test_from_summary_error_invalid_host_platform_summary() {
373 let summary = RustBuildMetaSummary {
374 platforms: Some(BuildPlatformsSummary {
375 host: HostPlatformSummary {
376 platform: PlatformSummary::new("invalid-platform-triple"),
377 libdir: PlatformLibdirSummary::Unavailable {
378 reason: PlatformLibdirUnavailable::RUSTC_FAILED,
379 },
380 },
381 targets: vec![],
382 }),
383 ..Default::default()
384 };
385 let actual = RustBuildMeta::<BinaryListState>::from_summary(summary);
386 actual.expect_err("parse result should be an error");
387 }
388
389 #[test_case(RustBuildMeta::<BinaryListState> {
390 build_platforms: BuildPlatforms {
391 host: host_current(),
392 target: None,
393 },
394 ..Default::default()
395 }, RustBuildMetaSummary {
396 target_platform: None,
397 target_platforms: vec![host_current().to_summary().platform],
398 platforms: Some(BuildPlatformsSummary {
399 host: host_current().to_summary(),
400 targets: vec![],
401 }),
402 ..Default::default()
403 }; "build platforms without target")]
404 #[test_case(RustBuildMeta::<BinaryListState> {
405 build_platforms: BuildPlatforms {
406 host: host_current_with_libdir("/fake/test/libdir/736"),
407 target: Some(target_linux_with_libdir("/fake/test/libdir/873")),
408 },
409 ..Default::default()
410 }, RustBuildMetaSummary {
411 target_platform: Some(
412 target_linux_with_libdir("/fake/test/libdir/873")
413 .triple
414 .platform
415 .triple_str()
416 .to_owned(),
417 ),
418 target_platforms: vec![target_linux_with_libdir("/fake/test/libdir/873").triple.platform.to_summary()],
419 platforms: Some(BuildPlatformsSummary {
420 host: host_current_with_libdir("/fake/test/libdir/736").to_summary(),
421 targets: vec![target_linux_with_libdir("/fake/test/libdir/873").to_summary()],
422 }),
423 ..Default::default()
424 }; "build platforms with target")]
425 fn test_to_summary(meta: RustBuildMeta<BinaryListState>, expected: RustBuildMetaSummary) {
426 let actual = meta.to_summary();
427 assert_eq!(actual, expected);
428 }
429
430 #[test]
431 fn test_dylib_paths_should_include_rustc_dir() {
432 let host_libdir = Utf8PathBuf::from("/fake/rustc/host/libdir");
433 let target_libdir = Utf8PathBuf::from("/fake/rustc/target/libdir");
434
435 let rust_build_meta = RustBuildMeta {
436 build_platforms: BuildPlatforms {
437 host: host_current_with_libdir(host_libdir.as_ref()),
438 target: Some(TargetPlatform::new(
439 TargetTriple::x86_64_unknown_linux_gnu(),
440 PlatformLibdir::Available(target_libdir.clone()),
441 )),
442 },
443 ..RustBuildMeta::empty()
444 };
445 let dylib_paths = rust_build_meta.dylib_paths();
446
447 assert!(
448 dylib_paths.contains(&host_libdir),
449 "{dylib_paths:?} should contain {host_libdir}"
450 );
451 assert!(
452 dylib_paths.contains(&target_libdir),
453 "{dylib_paths:?} should contain {target_libdir}"
454 );
455 }
456
457 #[test]
458 fn test_dylib_paths_should_not_contain_duplicate_paths() {
459 let tmpdir = camino_tempfile::tempdir().expect("should create temp dir successfully");
460 let host_libdir = tmpdir.path().to_path_buf();
461 let target_libdir = host_libdir.clone();
462 let fake_target_dir = tmpdir
463 .path()
464 .parent()
465 .expect("tmp directory should have a parent");
466 let tmpdir_dirname = tmpdir
467 .path()
468 .file_name()
469 .expect("tmp directory should have a file name");
470
471 let rust_build_meta = RustBuildMeta {
472 target_directory: fake_target_dir.to_path_buf(),
473 linked_paths: [(Utf8PathBuf::from(tmpdir_dirname), Default::default())].into(),
474 base_output_directories: [Utf8PathBuf::from(tmpdir_dirname)].into(),
475 build_platforms: BuildPlatforms {
476 host: host_current_with_libdir(host_libdir.as_ref()),
477 target: Some(TargetPlatform::new(
478 TargetTriple::x86_64_unknown_linux_gnu(),
479 PlatformLibdir::Available(target_libdir.clone()),
480 )),
481 },
482 ..RustBuildMeta::empty()
483 };
484 let dylib_paths = rust_build_meta.dylib_paths();
485
486 assert!(
487 dylib_paths.clone().into_iter().all_unique(),
488 "{dylib_paths:?} should not contain duplicate paths"
489 );
490 }
491}