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    /// Creates empty metadata.
87    ///
88    /// Used for replay and testing where actual build metadata is not needed.
89    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    /// Returns the dynamic library paths corresponding to this metadata.
102    ///
103    /// [See this Cargo documentation for
104    /// more.](https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths)
105    ///
106    /// These paths are prepended to the dynamic library environment variable for the current
107    /// platform (e.g. `LD_LIBRARY_PATH` on non-Apple Unix platforms).
108    pub fn dylib_paths(&self) -> Vec<Utf8PathBuf> {
109        // Add rust libdirs to the path if available, so we can run test binaries that depend on
110        // libstd.
111        //
112        // We could be smarter here and only add the host libdir for host binaries and the target
113        // libdir for target binaries, but it's simpler to just add both for now.
114        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        // Cargo puts linked paths before base output directories.
133        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                // Only add the directory to the path if it exists on disk.
140                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                // This is the order paths are added in by Cargo.
148                [with_deps, abs_base]
149            }))
150            .chain(libdirs)
151            .unique()
152            .collect()
153    }
154}
155
156impl<State> RustBuildMeta<State> {
157    /// Creates a `RustBuildMeta` from a serializable summary.
158    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            // Compatibility with metadata generated by older versions of nextest.
163            BuildPlatforms::from_target_summary(summary.clone())?
164        } else {
165            // Compatibility with metadata generated by older versions of nextest.
166            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    /// Converts self to a serializable form.
185    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            // TODO: support multiple --target options
195            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 should be preferred over target_platform
313        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 should be preferred over both target_platform and target_platforms
326        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}