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::{
14    BuildPlatformsSummary, BuildScriptInfoSummary, RustBuildMetaSummary, RustNonTestBinarySummary,
15};
16use std::{
17    collections::{BTreeMap, BTreeSet},
18    marker::PhantomData,
19};
20use tracing::warn;
21
22/// Rust-related metadata used for builds and test runs.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct RustBuildMeta<State> {
25    /// The target directory for Rust artifacts. Non-test binaries are uplifted
26    /// here by Cargo.
27    pub target_directory: Utf8PathBuf,
28
29    /// The build directory for intermediate Cargo artifacts (test binaries,
30    /// build script outputs, deps, etc.). Equals `target_directory` unless
31    /// Cargo's `build.build-dir` is configured.
32    pub build_directory: Utf8PathBuf,
33
34    /// A list of base output directories, relative to the build directory.
35    /// These directories and their "deps" subdirectories are added to the
36    /// dynamic library path.
37    pub base_output_directories: BTreeSet<Utf8PathBuf>,
38
39    /// Information about non-test executables, keyed by package ID. Paths are
40    /// relative to `target_directory` (non-test binaries are uplifted by Cargo).
41    pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
42
43    /// Build script output directory, relative to the build directory and keyed
44    /// by package ID. Only present for workspace packages that have build
45    /// scripts.
46    pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
47
48    /// Extended build script information, keyed by package ID. Only present for
49    /// workspace packages that have build scripts.
50    ///
51    /// `None` means this field was absent (old archive/metadata that predates
52    /// this field). `Some(map)` means the field was present, even if the map is
53    /// empty.
54    pub build_script_info: Option<BTreeMap<String, BuildScriptInfo>>,
55
56    /// A list of linked paths, relative to the build directory. These
57    /// directories are added to the dynamic library path.
58    ///
59    /// The values are the package IDs of the libraries that requested the
60    /// linked paths.
61    ///
62    /// Note that the serialized metadata only has the paths for now, not the
63    /// libraries that requested them. We might consider adding a new field with
64    /// metadata about that.
65    pub linked_paths: BTreeMap<Utf8PathBuf, BTreeSet<String>>,
66
67    /// The build platforms: host and target triple.
68    pub build_platforms: BuildPlatforms,
69
70    /// A type marker for the state.
71    pub state: PhantomData<State>,
72}
73
74impl RustBuildMeta<BinaryListState> {
75    /// Creates a new [`RustBuildMeta`].
76    pub fn new(
77        target_directory: impl Into<Utf8PathBuf>,
78        build_directory: impl Into<Utf8PathBuf>,
79        build_platforms: BuildPlatforms,
80    ) -> Self {
81        Self {
82            target_directory: target_directory.into(),
83            build_directory: build_directory.into(),
84            base_output_directories: BTreeSet::new(),
85            non_test_binaries: BTreeMap::new(),
86            build_script_out_dirs: BTreeMap::new(),
87            build_script_info: Some(BTreeMap::new()),
88            linked_paths: BTreeMap::new(),
89            state: PhantomData,
90            build_platforms,
91        }
92    }
93
94    /// Maps paths using a [`PathMapper`] to convert this to [`TestListState`].
95    pub fn map_paths(&self, path_mapper: &PathMapper) -> RustBuildMeta<TestListState> {
96        let new_target_directory = path_mapper
97            .new_target_dir()
98            .unwrap_or(&self.target_directory)
99            .to_path_buf();
100        // Use the build dir remap if set, otherwise fall back to the target
101        // dir remap. This fallback is for programmatic callers that construct
102        // PathMapper directly without the make_path_mapper helper. (The CLI
103        // path goes through make_path_mapper, which already applies this
104        // fallback when constructing the PathMapper.)
105        let new_build_directory = path_mapper
106            .new_build_dir()
107            .or(path_mapper.new_target_dir())
108            .unwrap_or(&self.build_directory)
109            .to_path_buf();
110
111        RustBuildMeta {
112            target_directory: new_target_directory,
113            build_directory: new_build_directory,
114            // Since these are relative paths, they don't need to be mapped.
115            base_output_directories: self.base_output_directories.clone(),
116            non_test_binaries: self.non_test_binaries.clone(),
117            build_script_out_dirs: self.build_script_out_dirs.clone(),
118            build_script_info: self.build_script_info.clone(),
119            linked_paths: self.linked_paths.clone(),
120            state: PhantomData,
121            build_platforms: self.build_platforms.map_libdir(path_mapper.libdir_mapper()),
122        }
123    }
124}
125
126impl RustBuildMeta<TestListState> {
127    /// Creates empty metadata.
128    ///
129    /// Used for replay and testing where actual build metadata is not needed.
130    pub fn empty() -> Self {
131        Self {
132            target_directory: Utf8PathBuf::new(),
133            build_directory: Utf8PathBuf::new(),
134            base_output_directories: BTreeSet::new(),
135            non_test_binaries: BTreeMap::new(),
136            build_script_out_dirs: BTreeMap::new(),
137            build_script_info: Some(BTreeMap::new()),
138            linked_paths: BTreeMap::new(),
139            state: PhantomData,
140            build_platforms: BuildPlatforms::new_with_no_target().unwrap(),
141        }
142    }
143
144    /// Returns the dynamic library paths corresponding to this metadata.
145    ///
146    /// [See this Cargo documentation for
147    /// more.](https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths)
148    ///
149    /// These paths are prepended to the dynamic library environment variable for the current
150    /// platform (e.g. `LD_LIBRARY_PATH` on non-Apple Unix platforms).
151    pub fn dylib_paths(&self) -> Vec<Utf8PathBuf> {
152        // Add rust libdirs to the path if available, so we can run test binaries that depend on
153        // libstd.
154        //
155        // We could be smarter here and only add the host libdir for host binaries and the target
156        // libdir for target binaries, but it's simpler to just add both for now.
157        let libdirs = self
158            .build_platforms
159            .host
160            .libdir
161            .as_path()
162            .into_iter()
163            .chain(
164                self.build_platforms
165                    .target
166                    .as_ref()
167                    .and_then(|target| target.libdir.as_path()),
168            )
169            .map(|libdir| libdir.to_path_buf())
170            .collect::<Vec<_>>();
171        if libdirs.is_empty() {
172            warn!("failed to detect the rustc libdir, may fail to list or run tests");
173        }
174
175        // Cargo puts linked paths before base output directories. Both
176        // linked paths and base output dirs are relative to the build
177        // directory.
178        self.linked_paths
179            .keys()
180            .filter_map(|rel_path| {
181                let join_path = self
182                    .build_directory
183                    .join(convert_rel_path_to_main_sep(rel_path));
184                // Only add the directory to the path if it exists on disk.
185                join_path.exists().then_some(join_path)
186            })
187            .chain(self.base_output_directories.iter().flat_map(|base_output| {
188                let abs_base = self
189                    .build_directory
190                    .join(convert_rel_path_to_main_sep(base_output));
191                let with_deps = abs_base.join("deps");
192                // This is the order paths are added in by Cargo.
193                [with_deps, abs_base]
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: 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.clone(),
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_should_not_contain_duplicate_paths() {
678        let tmpdir = camino_tempfile::tempdir().expect("should create temp dir successfully");
679        let host_libdir = tmpdir.path().to_path_buf();
680        let target_libdir = host_libdir.clone();
681        let fake_target_dir = tmpdir
682            .path()
683            .parent()
684            .expect("tmp directory should have a parent");
685        let tmpdir_dirname = tmpdir
686            .path()
687            .file_name()
688            .expect("tmp directory should have a file name");
689
690        let rust_build_meta = RustBuildMeta {
691            target_directory: fake_target_dir.to_path_buf(),
692            build_directory: fake_target_dir.to_path_buf(),
693            linked_paths: [(Utf8PathBuf::from(tmpdir_dirname), Default::default())].into(),
694            base_output_directories: [Utf8PathBuf::from(tmpdir_dirname)].into(),
695            build_platforms: BuildPlatforms {
696                host: host_current_with_libdir(host_libdir.as_ref()),
697                target: Some(TargetPlatform::new(
698                    TargetTriple::x86_64_unknown_linux_gnu(),
699                    PlatformLibdir::Available(target_libdir.clone()),
700                )),
701            },
702            ..RustBuildMeta::empty()
703        };
704        let dylib_paths = rust_build_meta.dylib_paths();
705
706        // The linked path resolves to the same directory as the base output
707        // directory (both are tmpdir_dirname relative to the build directory).
708        // Verify the result contains the expected path and has no duplicates.
709        let expected_abs = fake_target_dir.join(tmpdir_dirname);
710        assert!(
711            dylib_paths.contains(&expected_abs),
712            "{dylib_paths:?} should contain {expected_abs}"
713        );
714        assert!(
715            dylib_paths.clone().into_iter().all_unique(),
716            "{dylib_paths:?} should not contain duplicate paths"
717        );
718    }
719}