nextest_runner/list/
rust_build_meta.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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/// Rust-related metadata used for builds and test runs.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct RustBuildMeta<State> {
23    /// The target directory for build artifacts.
24    pub target_directory: Utf8PathBuf,
25
26    /// A list of base output directories, relative to the target directory. These directories
27    /// and their "deps" subdirectories are added to the dynamic library path.
28    pub base_output_directories: BTreeSet<Utf8PathBuf>,
29
30    /// Information about non-test executables, keyed by package ID.
31    pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
32
33    /// Build script output directory, relative to the target directory and keyed by package ID.
34    /// Only present for workspace packages that have build scripts.
35    pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
36
37    /// A list of linked paths, relative to the target directory. These directories are
38    /// added to the dynamic library path.
39    ///
40    /// The values are the package IDs of the libraries that requested the linked paths.
41    ///
42    /// Note that the serialized metadata only has the paths for now, not the libraries that
43    /// requested them. We might consider adding a new field with metadata about that.
44    pub linked_paths: BTreeMap<Utf8PathBuf, BTreeSet<String>>,
45
46    /// The build platforms: host and target triple
47    pub build_platforms: BuildPlatforms,
48
49    state: PhantomData<State>,
50}
51
52impl RustBuildMeta<BinaryListState> {
53    /// Creates a new [`RustBuildMeta`].
54    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    /// Maps paths using a [`PathMapper`] to convert this to [`TestListState`].
67    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            // Since these are relative paths, they don't need to be mapped.
74            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    /// Empty metadata for tests.
86    #[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    /// Returns the dynamic library paths corresponding to this metadata.
100    ///
101    /// [See this Cargo documentation for
102    /// more.](https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths)
103    ///
104    /// These paths are prepended to the dynamic library environment variable for the current
105    /// platform (e.g. `LD_LIBRARY_PATH` on non-Apple Unix platforms).
106    pub fn dylib_paths(&self) -> Vec<Utf8PathBuf> {
107        // Add rust libdirs to the path if available, so we can run test binaries that depend on
108        // libstd.
109        //
110        // We could be smarter here and only add the host libdir for host binaries and the target
111        // libdir for target binaries, but it's simpler to just add both for now.
112        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        // Cargo puts linked paths before base output directories.
131        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                // Only add the directory to the path if it exists on disk.
138                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                // This is the order paths are added in by Cargo.
146                [with_deps, abs_base]
147            }))
148            .chain(libdirs)
149            .unique()
150            .collect()
151    }
152}
153
154impl<State> RustBuildMeta<State> {
155    /// Creates a `RustBuildMeta` from a serializable summary.
156    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            // Compatibility with metadata generated by older versions of nextest.
161            BuildPlatforms::from_target_summary(summary.clone())?
162        } else {
163            // Compatibility with metadata generated by older versions of nextest.
164            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    /// Converts self to a serializable form.
183    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            // TODO: support multiple --target options
193            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 should be preferred over target_platform
311        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 should be preferred over both target_platform and target_platforms
324        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}