Skip to main content

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, RustNonTestBinaries, TestListState},
8    platform::{BuildPlatforms, TargetPlatform},
9    reuse_build::PathMapper,
10};
11use camino::Utf8PathBuf;
12use itertools::Itertools;
13use nextest_metadata::{BuildPlatformsSummary, BuildScriptInfoSummary, RustBuildMetaSummary};
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 Rust artifacts. Non-test binaries are uplifted
24    /// here by Cargo.
25    pub target_directory: Utf8PathBuf,
26
27    /// The build directory for intermediate Cargo artifacts (test binaries,
28    /// build script outputs, deps, etc.). Equals `target_directory` unless
29    /// Cargo's `build.build-dir` is configured.
30    pub build_directory: Utf8PathBuf,
31
32    /// A list of base output directories of the form `[<triple>/]<profile>`.
33    pub base_output_directories: BTreeSet<Utf8PathBuf>,
34
35    /// Information about non-test executables, keyed by package ID. Paths are
36    /// relative to `target_directory` (non-test binaries are uplifted by Cargo).
37    pub(crate) non_test_binaries: RustNonTestBinaries,
38
39    /// Build script output directory, relative to the build directory and keyed
40    /// by package ID. Only present for workspace packages that have build
41    /// scripts.
42    pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
43
44    /// Extended build script information, keyed by package ID. Only present for
45    /// workspace packages that have build scripts.
46    ///
47    /// `None` means this field was absent (old archive/metadata that predates
48    /// this field). `Some(map)` means the field was present, even if the map is
49    /// empty.
50    pub build_script_info: Option<BTreeMap<String, BuildScriptInfo>>,
51
52    /// A list of linked paths, relative to the build directory. These
53    /// directories are added to the dynamic library path.
54    ///
55    /// The values are the package IDs of the libraries that requested the
56    /// linked paths.
57    ///
58    /// Note that the serialized metadata only has the paths for now, not the
59    /// libraries that requested them. We might consider adding a new field with
60    /// metadata about that.
61    pub linked_paths: BTreeMap<Utf8PathBuf, BTreeSet<String>>,
62
63    /// The build platforms: host and target triple.
64    pub build_platforms: BuildPlatforms,
65
66    /// A type marker for the state.
67    pub state: PhantomData<State>,
68}
69
70impl RustBuildMeta<BinaryListState> {
71    /// Creates a new [`RustBuildMeta`].
72    pub fn new(
73        target_directory: impl Into<Utf8PathBuf>,
74        build_directory: impl Into<Utf8PathBuf>,
75        build_platforms: BuildPlatforms,
76    ) -> Self {
77        Self {
78            target_directory: target_directory.into(),
79            build_directory: build_directory.into(),
80            base_output_directories: BTreeSet::new(),
81            non_test_binaries: RustNonTestBinaries::default(),
82            build_script_out_dirs: BTreeMap::new(),
83            build_script_info: Some(BTreeMap::new()),
84            linked_paths: BTreeMap::new(),
85            state: PhantomData,
86            build_platforms,
87        }
88    }
89
90    /// Maps paths using a [`PathMapper`] to convert this to [`TestListState`].
91    pub fn map_paths(&self, path_mapper: &PathMapper) -> RustBuildMeta<TestListState> {
92        let new_target_directory = path_mapper
93            .new_target_dir()
94            .unwrap_or(&self.target_directory)
95            .to_path_buf();
96        // Use the build dir remap if set, otherwise fall back to the target
97        // dir remap. This fallback is for programmatic callers that construct
98        // PathMapper directly without the make_path_mapper helper. (The CLI
99        // path goes through make_path_mapper, which already applies this
100        // fallback when constructing the PathMapper.)
101        let new_build_directory = path_mapper
102            .new_build_dir()
103            .or(path_mapper.new_target_dir())
104            .unwrap_or(&self.build_directory)
105            .to_path_buf();
106
107        RustBuildMeta {
108            target_directory: new_target_directory,
109            build_directory: new_build_directory,
110            // Since these are relative paths, they don't need to be mapped.
111            base_output_directories: self.base_output_directories.clone(),
112            non_test_binaries: self.non_test_binaries.clone(),
113            build_script_out_dirs: self.build_script_out_dirs.clone(),
114            build_script_info: self.build_script_info.clone(),
115            linked_paths: self.linked_paths.clone(),
116            state: PhantomData,
117            build_platforms: self.build_platforms.map_libdir(path_mapper.libdir_mapper()),
118        }
119    }
120}
121
122impl RustBuildMeta<TestListState> {
123    /// Creates empty metadata.
124    ///
125    /// Used for replay and testing where actual build metadata is not needed.
126    pub fn empty() -> Self {
127        Self {
128            target_directory: Utf8PathBuf::new(),
129            build_directory: Utf8PathBuf::new(),
130            base_output_directories: BTreeSet::new(),
131            non_test_binaries: RustNonTestBinaries::default(),
132            build_script_out_dirs: BTreeMap::new(),
133            build_script_info: Some(BTreeMap::new()),
134            linked_paths: BTreeMap::new(),
135            state: PhantomData,
136            build_platforms: BuildPlatforms::new_with_no_target().unwrap(),
137        }
138    }
139
140    /// Returns the dynamic library paths corresponding to this metadata.
141    ///
142    /// [See this Cargo documentation for
143    /// more.](https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths)
144    ///
145    /// These paths are prepended to the dynamic library environment variable for the current
146    /// platform (e.g. `LD_LIBRARY_PATH` on non-Apple Unix platforms).
147    pub fn dylib_paths(&self) -> Vec<Utf8PathBuf> {
148        // Add rust libdirs to the path if available, so we can run test binaries that depend on
149        // libstd.
150        //
151        // We could be smarter here and only add the host libdir for host binaries and the target
152        // libdir for target binaries, but it's simpler to just add both for now.
153        let libdirs = self
154            .build_platforms
155            .host
156            .libdir
157            .as_path()
158            .into_iter()
159            .chain(
160                self.build_platforms
161                    .target
162                    .as_ref()
163                    .and_then(|target| target.libdir.as_path()),
164            )
165            .map(|libdir| libdir.to_path_buf())
166            .collect::<Vec<_>>();
167        if libdirs.is_empty() {
168            warn!("failed to detect the rustc libdir, may fail to list or run tests");
169        }
170
171        // Cargo puts linked paths before base output directories.
172        self.linked_paths
173            .keys()
174            .filter_map(|rel_path| {
175                let join_path = self
176                    .build_directory
177                    .join(convert_rel_path_to_main_sep(rel_path));
178                // Only add the directory to the path if it exists on disk.
179                join_path.exists().then_some(join_path)
180            })
181            .chain(self.base_output_directories.iter().flat_map(|base_output| {
182                let rel_base = convert_rel_path_to_main_sep(base_output);
183                // The Cargo artifact directory, where Cargo uplifts final
184                // artifacts to, is under the target directory. (Remember that
185                // recent Cargo versions make a separation between the target
186                // and build directories, even though they are often the same.)
187                let artifact_dir = self.target_directory.join(&rel_base);
188                // For the legacy layout, the deps directory is under the build directory.
189                let legacy_deps_dir = self.build_directory.join(rel_base).join("deps");
190                // This is the order paths are added in by Cargo. (Note that
191                // Cargo 1.93 changed the order slightly here, putting
192                // artifact_dir before legacy_deps_dir.)
193                [artifact_dir, legacy_deps_dir]
194            }))
195            .chain(libdirs)
196            .unique()
197            .collect()
198    }
199}
200
201impl<State> RustBuildMeta<State> {
202    /// Creates a `RustBuildMeta` from a serializable summary.
203    pub fn from_summary(summary: RustBuildMetaSummary) -> Result<Self, RustBuildMetaParseError> {
204        let build_platforms = if let Some(summary) = summary.platforms {
205            BuildPlatforms::from_summary(summary.clone())?
206        } else if let Some(summary) = summary.target_platforms.first() {
207            // Compatibility with metadata generated by older versions of nextest.
208            BuildPlatforms::from_target_summary(summary.clone())?
209        } else {
210            // Compatibility with metadata generated by older versions of nextest.
211            BuildPlatforms::from_summary_str(summary.target_platform.clone())?
212        };
213
214        // If build_directory is absent (old archive or metadata), default to
215        // target_directory.
216        let build_directory = summary
217            .build_directory
218            .unwrap_or_else(|| summary.target_directory.clone());
219
220        Ok(Self {
221            target_directory: summary.target_directory,
222            build_directory,
223            base_output_directories: summary.base_output_directories,
224            build_script_out_dirs: summary.build_script_out_dirs,
225            build_script_info: summary.build_script_info.map(|info| {
226                info.into_iter()
227                    .map(|(k, v)| (k, BuildScriptInfo::from_summary(v)))
228                    .collect()
229            }),
230            non_test_binaries: RustNonTestBinaries::from_summary(summary.non_test_binaries),
231            linked_paths: summary
232                .linked_paths
233                .into_iter()
234                .map(|linked_path| (linked_path, BTreeSet::new()))
235                .collect(),
236            state: PhantomData,
237            build_platforms,
238        })
239    }
240
241    /// Converts self to a serializable form.
242    pub fn to_summary(&self) -> RustBuildMetaSummary {
243        RustBuildMetaSummary {
244            target_directory: self.target_directory.clone(),
245            build_directory: Some(self.build_directory.clone()),
246            base_output_directories: self.base_output_directories.clone(),
247            non_test_binaries: self.non_test_binaries.to_summary(),
248            build_script_out_dirs: self.build_script_out_dirs.clone(),
249            build_script_info: self.build_script_info.as_ref().map(|info| {
250                info.iter()
251                    .map(|(k, v)| (k.clone(), v.to_summary()))
252                    .collect()
253            }),
254            linked_paths: self.linked_paths.keys().cloned().collect(),
255            target_platform: self.build_platforms.to_summary_str(),
256            target_platforms: vec![self.build_platforms.to_target_or_host_summary()],
257            // TODO: support multiple --target options
258            platforms: Some(BuildPlatformsSummary {
259                host: self.build_platforms.host.to_summary(),
260                targets: self
261                    .build_platforms
262                    .target
263                    .as_ref()
264                    .into_iter()
265                    .map(TargetPlatform::to_summary)
266                    .collect(),
267            }),
268        }
269    }
270
271    /// Converts self to a serializable form suitable for archive metadata.
272    ///
273    /// Archives are portable, so `build_directory` is omitted (it will default to
274    /// `target_directory` on extraction).
275    pub fn to_archive_summary(&self) -> RustBuildMetaSummary {
276        let mut summary = self.to_summary();
277        // In archives, build artifacts are stored under target/, so build_directory
278        // should not be present (it defaults to target_directory).
279        summary.build_directory = None;
280        summary
281    }
282}
283
284/// Extended build script information for a single package.
285#[derive(Clone, Debug, Default, Eq, PartialEq)]
286pub struct BuildScriptInfo {
287    /// Environment variables set by the build script via `cargo::rustc-env` directives.
288    pub envs: BTreeMap<String, String>,
289}
290
291impl BuildScriptInfo {
292    fn from_summary(summary: BuildScriptInfoSummary) -> Self {
293        Self { envs: summary.envs }
294    }
295
296    fn to_summary(&self) -> BuildScriptInfoSummary {
297        BuildScriptInfoSummary {
298            envs: self.envs.clone(),
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use crate::{
307        cargo_config::TargetTriple,
308        platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
309    };
310    use nextest_metadata::{
311        BuildPlatformsSummary, HostPlatformSummary, PlatformLibdirSummary,
312        PlatformLibdirUnavailable,
313    };
314    use target_spec::{Platform, summaries::PlatformSummary};
315    use test_case::test_case;
316
317    impl Default for RustBuildMeta<BinaryListState> {
318        fn default() -> Self {
319            RustBuildMeta::<BinaryListState>::new(
320                Utf8PathBuf::default(),
321                Utf8PathBuf::default(),
322                BuildPlatforms::new_with_no_target()
323                    .expect("creating BuildPlatforms without target triple should succeed"),
324            )
325        }
326    }
327
328    fn x86_64_pc_windows_msvc_triple() -> TargetTriple {
329        TargetTriple::deserialize_str(Some("x86_64-pc-windows-msvc".to_owned()))
330            .expect("creating TargetTriple should succeed")
331            .expect("the output of deserialize_str shouldn't be None")
332    }
333
334    fn host_current() -> HostPlatform {
335        HostPlatform {
336            platform: Platform::build_target()
337                .expect("should detect the build target successfully"),
338            libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
339        }
340    }
341
342    fn host_current_with_libdir(libdir: &str) -> HostPlatform {
343        HostPlatform {
344            platform: Platform::build_target()
345                .expect("should detect the build target successfully"),
346            libdir: PlatformLibdir::Available(libdir.into()),
347        }
348    }
349
350    fn host_not_current_with_libdir(libdir: &str) -> HostPlatform {
351        cfg_if::cfg_if! {
352            if #[cfg(windows)] {
353                let triple = TargetTriple::x86_64_unknown_linux_gnu();
354            } else {
355                let triple = x86_64_pc_windows_msvc_triple();
356            }
357        };
358
359        HostPlatform {
360            platform: triple.platform,
361            libdir: PlatformLibdir::Available(libdir.into()),
362        }
363    }
364
365    fn target_linux() -> TargetPlatform {
366        TargetPlatform::new(
367            TargetTriple::x86_64_unknown_linux_gnu(),
368            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
369        )
370    }
371
372    fn target_linux_with_libdir(libdir: &str) -> TargetPlatform {
373        TargetPlatform::new(
374            TargetTriple::x86_64_unknown_linux_gnu(),
375            PlatformLibdir::Available(libdir.into()),
376        )
377    }
378
379    fn target_windows() -> TargetPlatform {
380        TargetPlatform::new(
381            x86_64_pc_windows_msvc_triple(),
382            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
383        )
384    }
385
386    #[test_case(RustBuildMetaSummary {
387        ..Default::default()
388    }, RustBuildMeta::<BinaryListState> {
389        build_platforms: BuildPlatforms {
390            host: host_current(),
391            target: None,
392        },
393        // Summary has no build_script_info field, so from_summary produces None.
394        build_script_info: None,
395        ..Default::default()
396    }; "no target platforms")]
397    #[test_case(RustBuildMetaSummary {
398        target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
399        ..Default::default()
400    }, RustBuildMeta::<BinaryListState> {
401        build_platforms: BuildPlatforms {
402            host: host_current(),
403            target: Some(target_linux()),
404        },
405        build_script_info: None,
406        ..Default::default()
407    }; "only target platform field")]
408    #[test_case(RustBuildMetaSummary {
409        target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
410        // target_platforms should be preferred over target_platform
411        target_platforms: vec![PlatformSummary::new("x86_64-pc-windows-msvc")],
412        ..Default::default()
413    }, RustBuildMeta::<BinaryListState> {
414        build_platforms: BuildPlatforms {
415            host: host_current(),
416            target: Some(target_windows()),
417        },
418        build_script_info: None,
419        ..Default::default()
420    }; "target platform and target platforms field")]
421    #[test_case(RustBuildMetaSummary {
422        target_platform: Some("aarch64-unknown-linux-gnu".to_owned()),
423        target_platforms: vec![PlatformSummary::new("x86_64-pc-windows-msvc")],
424        // platforms should be preferred over both target_platform and target_platforms
425        platforms: Some(BuildPlatformsSummary {
426            host: host_not_current_with_libdir("/fake/test/libdir/281").to_summary(),
427            targets: vec![target_linux_with_libdir("/fake/test/libdir/837").to_summary()],
428        }),
429        ..Default::default()
430    }, RustBuildMeta::<BinaryListState> {
431        build_platforms: BuildPlatforms {
432            host: host_not_current_with_libdir("/fake/test/libdir/281"),
433            target: Some(target_linux_with_libdir("/fake/test/libdir/837")),
434        },
435        build_script_info: None,
436        ..Default::default()
437    }; "target platform and target platforms and platforms field")]
438    #[test_case(RustBuildMetaSummary {
439        platforms: Some(BuildPlatformsSummary {
440            host: host_current().to_summary(),
441            targets: vec![],
442        }),
443        ..Default::default()
444    }, RustBuildMeta::<BinaryListState> {
445        build_platforms: BuildPlatforms {
446            host: host_current(),
447            target: None,
448        },
449        build_script_info: None,
450        ..Default::default()
451    }; "platforms with zero targets")]
452    #[test_case(RustBuildMetaSummary {
453        target_directory: "/fake/target".into(),
454        build_directory: Some("/fake/build".into()),
455        platforms: Some(BuildPlatformsSummary {
456            host: host_current().to_summary(),
457            targets: vec![],
458        }),
459        ..Default::default()
460    }, RustBuildMeta::<BinaryListState> {
461        target_directory: "/fake/target".into(),
462        build_directory: "/fake/build".into(),
463        build_platforms: BuildPlatforms {
464            host: host_current(),
465            target: None,
466        },
467        build_script_info: None,
468        ..Default::default()
469    }; "build directory differs from target directory")]
470    #[test_case(RustBuildMetaSummary {
471        target_directory: "/fake/target".into(),
472        build_directory: None,
473        platforms: Some(BuildPlatformsSummary {
474            host: host_current().to_summary(),
475            targets: vec![],
476        }),
477        ..Default::default()
478    }, RustBuildMeta::<BinaryListState> {
479        target_directory: "/fake/target".into(),
480        // When build_directory is absent, it defaults to target_directory.
481        build_directory: "/fake/target".into(),
482        build_platforms: BuildPlatforms {
483            host: host_current(),
484            target: None,
485        },
486        build_script_info: None,
487        ..Default::default()
488    }; "build directory absent defaults to target directory")]
489    fn test_from_summary(summary: RustBuildMetaSummary, expected: RustBuildMeta<BinaryListState>) {
490        let actual = RustBuildMeta::<BinaryListState>::from_summary(summary)
491            .expect("RustBuildMeta should deserialize from summary with success.");
492        assert_eq!(actual, expected);
493    }
494
495    #[test]
496    fn test_from_summary_error_multiple_targets() {
497        let summary = RustBuildMetaSummary {
498            platforms: Some(BuildPlatformsSummary {
499                host: host_current().to_summary(),
500                targets: vec![target_linux().to_summary(), target_windows().to_summary()],
501            }),
502            ..Default::default()
503        };
504        let actual = RustBuildMeta::<BinaryListState>::from_summary(summary);
505        assert!(
506            matches!(actual, Err(RustBuildMetaParseError::Unsupported { .. })),
507            "Expect the parse result to be an error of RustBuildMetaParseError::Unsupported, actual {actual:?}"
508        );
509    }
510
511    #[test]
512    fn test_from_summary_error_invalid_host_platform_summary() {
513        let summary = RustBuildMetaSummary {
514            platforms: Some(BuildPlatformsSummary {
515                host: HostPlatformSummary {
516                    platform: PlatformSummary::new("invalid-platform-triple"),
517                    libdir: PlatformLibdirSummary::Unavailable {
518                        reason: PlatformLibdirUnavailable::RUSTC_FAILED,
519                    },
520                },
521                targets: vec![],
522            }),
523            ..Default::default()
524        };
525        let actual = RustBuildMeta::<BinaryListState>::from_summary(summary);
526        actual.expect_err("parse result should be an error");
527    }
528
529    #[test_case(RustBuildMeta::<BinaryListState> {
530        build_platforms: BuildPlatforms {
531            host: host_current(),
532            target: None,
533        },
534        ..Default::default()
535    }, RustBuildMetaSummary {
536        target_platform: None,
537        target_platforms: vec![host_current().to_summary().platform],
538        platforms: Some(BuildPlatformsSummary {
539            host: host_current().to_summary(),
540            targets: vec![],
541        }),
542        build_script_info: Some(BTreeMap::new()),
543        build_directory: Some(Utf8PathBuf::new()),
544        ..Default::default()
545    }; "build platforms without target")]
546    #[test_case(RustBuildMeta::<BinaryListState> {
547        build_platforms: BuildPlatforms {
548            host: host_current_with_libdir("/fake/test/libdir/736"),
549            target: Some(target_linux_with_libdir("/fake/test/libdir/873")),
550        },
551        ..Default::default()
552    }, RustBuildMetaSummary {
553        target_platform: Some(
554            target_linux_with_libdir("/fake/test/libdir/873")
555                .triple
556                .platform
557                .triple_str()
558                .to_owned(),
559        ),
560        target_platforms: vec![target_linux_with_libdir("/fake/test/libdir/873").triple.platform.to_summary()],
561        platforms: Some(BuildPlatformsSummary {
562            host: host_current_with_libdir("/fake/test/libdir/736").to_summary(),
563            targets: vec![target_linux_with_libdir("/fake/test/libdir/873").to_summary()],
564        }),
565        build_script_info: Some(BTreeMap::new()),
566        build_directory: Some(Utf8PathBuf::new()),
567        ..Default::default()
568    }; "build platforms with target")]
569    #[test_case(RustBuildMeta::<BinaryListState> {
570        target_directory: "/fake/target".into(),
571        build_directory: "/fake/build".into(),
572        build_platforms: BuildPlatforms {
573            host: host_current(),
574            target: None,
575        },
576        ..Default::default()
577    }, RustBuildMetaSummary {
578        target_directory: "/fake/target".into(),
579        // build_directory is emitted when it differs from target_directory.
580        build_directory: Some("/fake/build".into()),
581        target_platform: None,
582        target_platforms: vec![host_current().to_summary().platform],
583        platforms: Some(BuildPlatformsSummary {
584            host: host_current().to_summary(),
585            targets: vec![],
586        }),
587        build_script_info: Some(BTreeMap::new()),
588        ..Default::default()
589    }; "build directory differs from target directory")]
590    #[test_case(RustBuildMeta::<BinaryListState> {
591        target_directory: "/fake/target".into(),
592        build_directory: "/fake/target".into(),
593        build_platforms: BuildPlatforms {
594            host: host_current(),
595            target: None,
596        },
597        ..Default::default()
598    }, RustBuildMetaSummary {
599        target_directory: "/fake/target".into(),
600        // build_directory is always emitted by to_summary().
601        build_directory: Some("/fake/target".into()),
602        target_platform: None,
603        target_platforms: vec![host_current().to_summary().platform],
604        platforms: Some(BuildPlatformsSummary {
605            host: host_current().to_summary(),
606            targets: vec![],
607        }),
608        build_script_info: Some(BTreeMap::new()),
609        ..Default::default()
610    }; "build directory equals target directory")]
611    fn test_to_summary(meta: RustBuildMeta<BinaryListState>, expected: RustBuildMetaSummary) {
612        let actual = meta.to_summary();
613        assert_eq!(actual, expected);
614    }
615
616    #[test]
617    fn test_to_archive_summary_omits_build_directory() {
618        let meta = RustBuildMeta::<BinaryListState> {
619            target_directory: "/fake/target".into(),
620            build_directory: "/fake/build".into(),
621            build_platforms: BuildPlatforms {
622                host: host_current(),
623                target: None,
624            },
625            ..Default::default()
626        };
627
628        let archive_summary = meta.to_archive_summary();
629
630        // Archive summaries always omit build_directory so it defaults to
631        // target_directory on extraction.
632        assert_eq!(
633            archive_summary.build_directory, None,
634            "to_archive_summary should always set build_directory to None"
635        );
636        assert_eq!(archive_summary.target_directory, meta.target_directory);
637
638        // Verify round-trip: from_summary with the archive summary should
639        // produce a RustBuildMeta where build_directory == target_directory.
640        let round_tripped = RustBuildMeta::<BinaryListState>::from_summary(archive_summary)
641            .expect("from_summary should succeed");
642        assert_eq!(
643            round_tripped.build_directory, round_tripped.target_directory,
644            "after round-trip through archive summary, \
645             build_directory should equal target_directory"
646        );
647    }
648
649    #[test]
650    fn test_dylib_paths_should_include_rustc_dir() {
651        let host_libdir = Utf8PathBuf::from("/fake/rustc/host/libdir");
652        let target_libdir = Utf8PathBuf::from("/fake/rustc/target/libdir");
653
654        let rust_build_meta = RustBuildMeta {
655            build_platforms: BuildPlatforms {
656                host: host_current_with_libdir(host_libdir.as_ref()),
657                target: Some(TargetPlatform::new(
658                    TargetTriple::x86_64_unknown_linux_gnu(),
659                    PlatformLibdir::Available(target_libdir.clone()),
660                )),
661            },
662            ..RustBuildMeta::empty()
663        };
664        let dylib_paths = rust_build_meta.dylib_paths();
665
666        assert!(
667            dylib_paths.contains(&host_libdir),
668            "{dylib_paths:?} should contain {host_libdir}"
669        );
670        assert!(
671            dylib_paths.contains(&target_libdir),
672            "{dylib_paths:?} should contain {target_libdir}"
673        );
674    }
675
676    #[test]
677    fn test_dylib_paths_artifact_dir_is_under_the_target_directory() {
678        let rust_build_meta = RustBuildMeta {
679            target_directory: "/fake/target".into(),
680            build_directory: "/fake/build".into(),
681            base_output_directories: ["debug".into(), "aarch64-unknown-linux-gnu/debug".into()]
682                .into(),
683            build_platforms: BuildPlatforms {
684                host: host_current_with_libdir("/fake/rustc/host/libdir"),
685                target: None,
686            },
687            ..RustBuildMeta::empty()
688        };
689
690        assert_eq!(
691            rust_build_meta.dylib_paths(),
692            vec![
693                Utf8PathBuf::from("/fake/target/aarch64-unknown-linux-gnu/debug"),
694                Utf8PathBuf::from("/fake/build/aarch64-unknown-linux-gnu/debug/deps"),
695                Utf8PathBuf::from("/fake/target/debug"),
696                Utf8PathBuf::from("/fake/build/debug/deps"),
697                Utf8PathBuf::from("/fake/rustc/host/libdir"),
698            ],
699        );
700    }
701
702    #[test]
703    fn test_dylib_paths_should_not_contain_duplicate_paths() {
704        let tmpdir = camino_tempfile::tempdir().expect("should create temp dir successfully");
705        let host_libdir = tmpdir.path().to_path_buf();
706        let target_libdir = host_libdir.clone();
707        let fake_target_dir = tmpdir
708            .path()
709            .parent()
710            .expect("tmp directory should have a parent");
711        let tmpdir_dirname = tmpdir
712            .path()
713            .file_name()
714            .expect("tmp directory should have a file name");
715
716        let rust_build_meta = RustBuildMeta {
717            target_directory: fake_target_dir.to_path_buf(),
718            build_directory: fake_target_dir.to_path_buf(),
719            linked_paths: [(Utf8PathBuf::from(tmpdir_dirname), Default::default())].into(),
720            base_output_directories: [Utf8PathBuf::from(tmpdir_dirname)].into(),
721            build_platforms: BuildPlatforms {
722                host: host_current_with_libdir(host_libdir.as_ref()),
723                target: Some(TargetPlatform::new(
724                    TargetTriple::x86_64_unknown_linux_gnu(),
725                    PlatformLibdir::Available(target_libdir.clone()),
726                )),
727            },
728            ..RustBuildMeta::empty()
729        };
730        let dylib_paths = rust_build_meta.dylib_paths();
731
732        // The linked path resolves to the same directory as the base output
733        // directory (both are tmpdir_dirname relative to the build directory).
734        // Verify the result contains the expected path and has no duplicates.
735        let expected_abs = fake_target_dir.join(tmpdir_dirname);
736        assert!(
737            dylib_paths.contains(&expected_abs),
738            "{dylib_paths:?} should contain {expected_abs}"
739        );
740        assert!(
741            dylib_paths.clone().into_iter().all_unique(),
742            "{dylib_paths:?} should not contain duplicate paths"
743        );
744    }
745}