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