nextest_runner/
platform.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Platform-related data structures.
5
6use crate::{
7    RustcCli,
8    cargo_config::{CargoTargetArg, TargetTriple},
9    errors::{
10        DisplayErrorChain, HostPlatformDetectError, RustBuildMetaParseError, TargetTripleError,
11    },
12    indenter::DisplayIndented,
13    reuse_build::{LibdirMapper, PlatformLibdirMapper},
14};
15use camino::{Utf8Path, Utf8PathBuf};
16use nextest_metadata::{
17    BuildPlatformsSummary, HostPlatformSummary, PlatformLibdirSummary, PlatformLibdirUnavailable,
18    TargetPlatformSummary,
19};
20pub use target_spec::Platform;
21use target_spec::{
22    TargetFeatures, errors::RustcVersionVerboseParseError, summaries::PlatformSummary,
23};
24use tracing::{debug, warn};
25
26/// A representation of host and target platform.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct BuildPlatforms {
29    /// The host platform.
30    pub host: HostPlatform,
31
32    /// The target platform, if specified.
33    ///
34    /// In the future, this will support multiple targets.
35    pub target: Option<TargetPlatform>,
36}
37
38impl BuildPlatforms {
39    /// Creates a new `BuildPlatforms` with no libdirs or targets.
40    ///
41    /// Used for testing.
42    pub fn new_with_no_target() -> Result<Self, HostPlatformDetectError> {
43        Ok(Self {
44            host: HostPlatform {
45                // Because this is for testing, we just use the build target
46                // rather than `rustc -vV` output.
47                platform: Platform::build_target().map_err(|build_target_error| {
48                    HostPlatformDetectError::BuildTargetError {
49                        build_target_error: Box::new(build_target_error),
50                    }
51                })?,
52                libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
53            },
54            target: None,
55        })
56    }
57
58    /// Maps libdir paths.
59    pub fn map_libdir(&self, mapper: &LibdirMapper) -> Self {
60        Self {
61            host: self.host.map_libdir(&mapper.host),
62            target: self
63                .target
64                .as_ref()
65                .map(|target| target.map_libdir(&mapper.target)),
66        }
67    }
68
69    /// Returns the argument to pass into `cargo metadata --filter-platform <triple>`.
70    pub fn to_cargo_target_arg(&self) -> Result<CargoTargetArg, TargetTripleError> {
71        match &self.target {
72            Some(target) => target.triple.to_cargo_target_arg(),
73            None => {
74                // If there's no target, use the host platform.
75                Ok(CargoTargetArg::Builtin(
76                    self.host.platform.triple_str().to_owned(),
77                ))
78            }
79        }
80    }
81
82    /// Converts self to a summary.
83    pub fn to_summary(&self) -> BuildPlatformsSummary {
84        BuildPlatformsSummary {
85            host: self.host.to_summary(),
86            targets: self
87                .target
88                .as_ref()
89                .map(|target| vec![target.to_summary()])
90                .unwrap_or_default(),
91        }
92    }
93
94    /// Converts self to a single summary.
95    ///
96    /// Pairs with [`Self::from_target_summary`]. Deprecated in favor of [`BuildPlatformsSummary`].
97    pub fn to_target_or_host_summary(&self) -> PlatformSummary {
98        if let Some(target) = &self.target {
99            target.triple.platform.to_summary()
100        } else {
101            self.host.platform.to_summary()
102        }
103    }
104
105    /// Converts a target triple to a [`String`] that can be stored in the build-metadata.
106    ///
107    /// Only for backward compatibility. Deprecated in favor of [`BuildPlatformsSummary`].
108    pub fn to_summary_str(&self) -> Option<String> {
109        self.target
110            .as_ref()
111            .map(|triple| triple.triple.platform.triple_str().to_owned())
112    }
113
114    /// Converts a summary to a [`BuildPlatforms`].
115    pub fn from_summary(summary: BuildPlatformsSummary) -> Result<Self, RustBuildMetaParseError> {
116        Ok(BuildPlatforms {
117            host: HostPlatform::from_summary(summary.host)?,
118            target: {
119                if summary.targets.len() > 1 {
120                    return Err(RustBuildMetaParseError::Unsupported {
121                        message: "multiple build targets is not supported".to_owned(),
122                    });
123                }
124                summary
125                    .targets
126                    .first()
127                    .map(|target| TargetPlatform::from_summary(target.clone()))
128                    .transpose()?
129            },
130        })
131    }
132
133    /// Creates a [`BuildPlatforms`] from a single `PlatformSummary`.
134    ///
135    /// Only for backwards compatibility. Deprecated in favor of [`BuildPlatformsSummary`].
136    pub fn from_target_summary(summary: PlatformSummary) -> Result<Self, RustBuildMetaParseError> {
137        // In this case:
138        //
139        // * no libdirs are available
140        // * the host might be serialized as the target platform as well (we can't detect this case
141        //   reliably, so we treat it as the target platform as well, which isn't a problem in
142        //   practice).
143        let host = HostPlatform {
144            // We don't necessarily have `rustc` available, so we use the build
145            // target instead.
146            platform: Platform::build_target()
147                .map_err(RustBuildMetaParseError::DetectBuildTargetError)?,
148            libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
149        };
150
151        let target = TargetTriple::deserialize(Some(summary))?.map(|triple| {
152            TargetPlatform::new(
153                triple,
154                PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
155            )
156        });
157
158        Ok(Self { host, target })
159    }
160
161    /// Creates a [`BuildPlatforms`] from a target triple.
162    ///
163    /// Only for backward compatibility. Deprecated in favor of [`BuildPlatformsSummary`].
164    pub fn from_summary_str(summary: Option<String>) -> Result<Self, RustBuildMetaParseError> {
165        // In this case:
166        //
167        // * no libdirs are available
168        // * can't represent custom platforms
169        // * the host might be serialized as the target platform as well (we can't detect this case
170        //   reliably, so we treat it as the target platform as well, which isn't a problem in
171        //   practice).
172        let host = HostPlatform {
173            // We don't necessarily have `rustc` available, so we use the build
174            // target instead.
175            platform: Platform::build_target()
176                .map_err(RustBuildMetaParseError::DetectBuildTargetError)?,
177            libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
178        };
179
180        let target = TargetTriple::deserialize_str(summary)?.map(|triple| {
181            TargetPlatform::new(
182                triple,
183                PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
184            )
185        });
186
187        Ok(Self { host, target })
188    }
189}
190
191/// A representation of a host platform during a build.
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct HostPlatform {
194    /// The platform.
195    pub platform: Platform,
196
197    /// The host libdir.
198    pub libdir: PlatformLibdir,
199}
200
201impl HostPlatform {
202    /// Creates a new `HostPlatform` representing the current platform by
203    /// querying rustc.
204    ///
205    /// This may fall back to the build target if `rustc -vV` fails.
206    pub fn detect(libdir: PlatformLibdir) -> Result<Self, HostPlatformDetectError> {
207        let platform = detect_host_platform()?;
208        Ok(Self { platform, libdir })
209    }
210
211    /// Converts self to a summary.
212    pub fn to_summary(&self) -> HostPlatformSummary {
213        HostPlatformSummary {
214            platform: self.platform.to_summary(),
215            libdir: self.libdir.to_summary(),
216        }
217    }
218
219    /// Converts a summary to a [`HostPlatform`].
220    pub fn from_summary(summary: HostPlatformSummary) -> Result<Self, RustBuildMetaParseError> {
221        let platform = summary
222            .platform
223            .to_platform()
224            .map_err(RustBuildMetaParseError::PlatformDeserializeError)?;
225        Ok(Self {
226            platform,
227            libdir: PlatformLibdir::from_summary(summary.libdir),
228        })
229    }
230
231    fn map_libdir(&self, mapper: &PlatformLibdirMapper) -> Self {
232        Self {
233            platform: self.platform.clone(),
234            libdir: mapper.map(&self.libdir),
235        }
236    }
237}
238
239/// Detect the host platform by using `rustc -vV`, and falling back to the build
240/// target.
241///
242/// Returns an error if both of those methods fail, and produces a warning if
243/// `rustc -vV` fails.
244fn detect_host_platform() -> Result<Platform, HostPlatformDetectError> {
245    // A test-only environment variable to always make the build target a fixed
246    // value, or to error out.
247    const FORCE_BUILD_TARGET_VAR: &str = "__NEXTEST_FORCE_BUILD_TARGET";
248
249    let build_target = match std::env::var(FORCE_BUILD_TARGET_VAR).as_deref() {
250        Ok("error") => Err(target_spec::Error::RustcVersionVerboseParse(
251            RustcVersionVerboseParseError::MissingHostLine {
252                output: format!(
253                    "({FORCE_BUILD_TARGET_VAR} set to \"error\", forcibly failing build target detection)\n"
254                ),
255            },
256        )),
257        Ok(triple) => Platform::new(triple.to_owned(), TargetFeatures::Unknown),
258        Err(_) => Platform::build_target(),
259    };
260
261    let rustc_vv = RustcCli::version_verbose()
262        .to_expression()
263        .stdout_capture()
264        .stderr_capture()
265        .unchecked();
266    match rustc_vv.run() {
267        Ok(output) => {
268            if output.status.success() {
269                // Neither `rustc` nor `cargo` tell us what target features are
270                // enabled for the host, so we must use
271                // `TargetFeatures::Unknown`.
272                match Platform::from_rustc_version_verbose(output.stdout, TargetFeatures::Unknown) {
273                    Ok(platform) => Ok(platform),
274                    Err(host_platform_error) => {
275                        match build_target {
276                            Ok(build_target) => {
277                                warn!(
278                                    "for host platform, parsing `rustc -vV` failed; \
279                                     falling back to build target `{}`\n\
280                                     - host platform error:\n{}",
281                                    build_target.triple().as_str(),
282                                    DisplayErrorChain::new_with_initial_indent(
283                                        "  ",
284                                        host_platform_error
285                                    ),
286                                );
287                                Ok(build_target)
288                            }
289                            Err(build_target_error) => {
290                                // In this case, we can't do anything.
291                                Err(HostPlatformDetectError::HostPlatformParseError {
292                                    host_platform_error: Box::new(host_platform_error),
293                                    build_target_error: Box::new(build_target_error),
294                                })
295                            }
296                        }
297                    }
298                }
299            } else {
300                match build_target {
301                    Ok(build_target) => {
302                        warn!(
303                            "for host platform, `rustc -vV` failed with {}; \
304                             falling back to build target `{}`\n\
305                             - `rustc -vV` stdout:\n{}\n\
306                             - `rustc -vV` stderr:\n{}",
307                            output.status,
308                            build_target.triple().as_str(),
309                            DisplayIndented {
310                                item: String::from_utf8_lossy(&output.stdout),
311                                indent: "  "
312                            },
313                            DisplayIndented {
314                                item: String::from_utf8_lossy(&output.stderr),
315                                indent: "  "
316                            },
317                        );
318                        Ok(build_target)
319                    }
320                    Err(build_target_error) => {
321                        // If the build target isn't available either, we
322                        // can't do anything.
323                        Err(HostPlatformDetectError::RustcVvFailed {
324                            status: output.status,
325                            stdout: output.stdout,
326                            stderr: output.stderr,
327                            build_target_error: Box::new(build_target_error),
328                        })
329                    }
330                }
331            }
332        }
333        Err(error) => {
334            match build_target {
335                Ok(build_target) => {
336                    warn!(
337                        "for host platform, failed to spawn `rustc -vV`; \
338                         falling back to build target `{}`\n\
339                         - host platform error:\n{}",
340                        build_target.triple().as_str(),
341                        DisplayErrorChain::new_with_initial_indent("  ", error),
342                    );
343                    Ok(build_target)
344                }
345                Err(build_target_error) => {
346                    // If the build target isn't available either, we
347                    // can't do anything.
348                    Err(HostPlatformDetectError::RustcVvSpawnError {
349                        error,
350                        build_target_error: Box::new(build_target_error),
351                    })
352                }
353            }
354        }
355    }
356}
357
358/// The target platform.
359#[derive(Clone, Debug, Eq, PartialEq)]
360pub struct TargetPlatform {
361    /// The target triple: the platform, along with its source and where it was obtained from.
362    pub triple: TargetTriple,
363
364    /// The target libdir.
365    pub libdir: PlatformLibdir,
366}
367
368impl TargetPlatform {
369    /// Creates a new [`TargetPlatform`].
370    pub fn new(triple: TargetTriple, libdir: PlatformLibdir) -> Self {
371        Self { triple, libdir }
372    }
373
374    /// Converts self to a summary.
375    pub fn to_summary(&self) -> TargetPlatformSummary {
376        TargetPlatformSummary {
377            platform: self.triple.platform.to_summary(),
378            libdir: self.libdir.to_summary(),
379        }
380    }
381
382    /// Converts a summary to a [`TargetPlatform`].
383    pub fn from_summary(summary: TargetPlatformSummary) -> Result<Self, RustBuildMetaParseError> {
384        Ok(Self {
385            triple: TargetTriple::deserialize(Some(summary.platform))
386                .map_err(RustBuildMetaParseError::PlatformDeserializeError)?
387                .expect("the input is not None, so the output must not be None"),
388            libdir: PlatformLibdir::from_summary(summary.libdir),
389        })
390    }
391
392    fn map_libdir(&self, mapper: &PlatformLibdirMapper) -> Self {
393        Self {
394            triple: self.triple.clone(),
395            libdir: mapper.map(&self.libdir),
396        }
397    }
398}
399
400/// A platform libdir.
401#[derive(Clone, Debug, Eq, PartialEq)]
402pub enum PlatformLibdir {
403    /// The libdir is known and available.
404    Available(Utf8PathBuf),
405
406    /// The libdir is unavailable.
407    Unavailable(PlatformLibdirUnavailable),
408}
409
410impl PlatformLibdir {
411    /// Constructs a new `PlatformLibdir` from a `Utf8PathBuf`.
412    pub fn from_path(path: Utf8PathBuf) -> Self {
413        Self::Available(path)
414    }
415
416    /// Constructs a new `PlatformLibdir` from rustc's standard output.
417    ///
418    /// None implies that rustc failed, and will be stored as such.
419    pub fn from_rustc_stdout(rustc_output: Option<Vec<u8>>) -> Self {
420        fn inner(v: Option<Vec<u8>>) -> Result<Utf8PathBuf, PlatformLibdirUnavailable> {
421            let v = v.ok_or(PlatformLibdirUnavailable::RUSTC_FAILED)?;
422
423            let s = String::from_utf8(v).map_err(|e| {
424                debug!("failed to convert the output to a string: {e}");
425                PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR
426            })?;
427
428            let mut lines = s.lines();
429            let Some(out) = lines.next() else {
430                debug!("empty output");
431                return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
432            };
433
434            let trimmed = out.trim();
435            if trimmed.is_empty() {
436                debug!("empty output");
437                return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
438            }
439
440            // If there's another line, it must be empty.
441            for line in lines {
442                if !line.trim().is_empty() {
443                    debug!("unexpected additional output: {line}");
444                    return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
445                }
446            }
447
448            Ok(Utf8PathBuf::from(trimmed))
449        }
450
451        match inner(rustc_output) {
452            Ok(path) => Self::Available(path),
453            Err(error) => Self::Unavailable(error),
454        }
455    }
456
457    /// Constructs a new `PlatformLibdir` from a `PlatformLibdirUnavailable`.
458    pub fn from_unavailable(error: PlatformLibdirUnavailable) -> Self {
459        Self::Unavailable(error)
460    }
461
462    /// Returns self as a path if available.
463    pub fn as_path(&self) -> Option<&Utf8Path> {
464        match self {
465            Self::Available(path) => Some(path),
466            Self::Unavailable(_) => None,
467        }
468    }
469
470    /// Converts self to a summary.
471    pub fn to_summary(&self) -> PlatformLibdirSummary {
472        match self {
473            Self::Available(path) => PlatformLibdirSummary::Available { path: path.clone() },
474            Self::Unavailable(reason) => PlatformLibdirSummary::Unavailable {
475                reason: reason.clone(),
476            },
477        }
478    }
479
480    /// Converts a summary to a [`PlatformLibdir`].
481    pub fn from_summary(summary: PlatformLibdirSummary) -> Self {
482        match summary {
483            PlatformLibdirSummary::Available { path: libdir } => Self::Available(libdir),
484            PlatformLibdirSummary::Unavailable { reason } => Self::Unavailable(reason),
485        }
486    }
487}
488
489/// Detects the host platform for use in tests.
490///
491/// Prefer this over `Platform::build_target()` in tests to ensure we're testing
492/// with the same platform detection logic used in production.
493#[cfg(test)]
494pub(crate) fn detect_host_platform_for_tests() -> Platform {
495    use crate::RustcCli;
496
497    HostPlatform::detect(PlatformLibdir::from_rustc_stdout(
498        RustcCli::print_host_libdir().read(),
499    ))
500    .expect("host platform detection should succeed in tests")
501    .platform
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use test_case::test_case;
508
509    #[test]
510    fn test_from_rustc_output_invalid() {
511        // None.
512        assert_eq!(
513            PlatformLibdir::from_rustc_stdout(None),
514            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_FAILED),
515        );
516
517        // Empty input.
518        assert_eq!(
519            PlatformLibdir::from_rustc_stdout(Some(Vec::new())),
520            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
521        );
522
523        // A single empty line.
524        assert_eq!(
525            PlatformLibdir::from_rustc_stdout(Some(b"\n".to_vec())),
526            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
527        );
528
529        // Multiple lines.
530        assert_eq!(
531            PlatformLibdir::from_rustc_stdout(Some(b"/fake/libdir/1\n/fake/libdir/2\n".to_vec())),
532            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
533        );
534    }
535
536    #[test_case(b"/fake/libdir/22548", "/fake/libdir/22548"; "single line")]
537    #[test_case(
538        b"\t /fake/libdir\t \n\r",
539        "/fake/libdir";
540        "with leading or trailing whitespace"
541    )]
542    #[test_case(
543        b"/fake/libdir/1\n\n",
544        "/fake/libdir/1";
545        "trailing newlines"
546    )]
547    fn test_read_from_rustc_output_valid(input: &[u8], actual: &str) {
548        assert_eq!(
549            PlatformLibdir::from_rustc_stdout(Some(input.to_vec())),
550            PlatformLibdir::Available(actual.into()),
551        );
552    }
553}