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