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    /// A type marker for the state.
50    pub state: PhantomData<State>,
51}
52
53impl RustBuildMeta<BinaryListState> {
54    /// Creates a new [`RustBuildMeta`].
55    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    /// Maps paths using a [`PathMapper`] to convert this to [`TestListState`].
68    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            // Since these are relative paths, they don't need to be mapped.
75            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    /// Empty metadata for tests.
87    #[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    /// Returns the dynamic library paths corresponding to this metadata.
101    ///
102    /// [See this Cargo documentation for
103    /// more.](https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths)
104    ///
105    /// These paths are prepended to the dynamic library environment variable for the current
106    /// platform (e.g. `LD_LIBRARY_PATH` on non-Apple Unix platforms).
107    pub fn dylib_paths(&self) -> Vec<Utf8PathBuf> {
108        // Add rust libdirs to the path if available, so we can run test binaries that depend on
109        // libstd.
110        //
111        // We could be smarter here and only add the host libdir for host binaries and the target
112        // libdir for target binaries, but it's simpler to just add both for now.
113        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        // Cargo puts linked paths before base output directories.
132        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                // Only add the directory to the path if it exists on disk.
139                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                // This is the order paths are added in by Cargo.
147                [with_deps, abs_base]
148            }))
149            .chain(libdirs)
150            .unique()
151            .collect()
152    }
153}
154
155impl<State> RustBuildMeta<State> {
156    /// Creates a `RustBuildMeta` from a serializable summary.
157    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            // Compatibility with metadata generated by older versions of nextest.
162            BuildPlatforms::from_target_summary(summary.clone())?
163        } else {
164            // Compatibility with metadata generated by older versions of nextest.
165            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    /// Converts self to a serializable form.
184    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            // TODO: support multiple --target options
194            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 should be preferred over target_platform
312        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 should be preferred over both target_platform and target_platforms
325        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}