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    reuse_build::{LibdirMapper, PlatformLibdirMapper},
13};
14use camino::{Utf8Path, Utf8PathBuf};
15use indent_write::indentable::Indented;
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    enum ForceBuildTarget {
250        Triple(String),
251        Error,
252    }
253
254    let force_build_target = match std::env::var(FORCE_BUILD_TARGET_VAR).as_deref() {
255        Ok("error") => Some(ForceBuildTarget::Error),
256        Ok(triple) => Some(ForceBuildTarget::Triple(triple.to_owned())),
257        Err(_) => None,
258    };
259
260    let build_target = match force_build_target {
261        Some(ForceBuildTarget::Triple(triple)) => Platform::new(triple, TargetFeatures::Unknown),
262        Some(ForceBuildTarget::Error) => Err(target_spec::Error::RustcVersionVerboseParse(
263            RustcVersionVerboseParseError::MissingHostLine {
264                output: format!(
265                    "({} set to \"error\", forcibly failing build target detection)\n",
266                    FORCE_BUILD_TARGET_VAR
267                ),
268            },
269        )),
270        None => Platform::build_target(),
271    };
272
273    let rustc_vv = RustcCli::version_verbose()
274        .to_expression()
275        .stdout_capture()
276        .stderr_capture()
277        .unchecked();
278    match rustc_vv.run() {
279        Ok(output) => {
280            if output.status.success() {
281                // Neither `rustc` nor `cargo` tell us what target features are
282                // enabled for the host, so we must use
283                // `TargetFeatures::Unknown`.
284                match Platform::from_rustc_version_verbose(output.stdout, TargetFeatures::Unknown) {
285                    Ok(platform) => Ok(platform),
286                    Err(host_platform_error) => {
287                        match build_target {
288                            Ok(build_target) => {
289                                warn!(
290                                    "for host platform, parsing `rustc -vV` failed; \
291                                     falling back to build target `{}`\n\
292                                     - host platform error:\n{}",
293                                    build_target.triple().as_str(),
294                                    DisplayErrorChain::new_with_initial_indent(
295                                        "  ",
296                                        host_platform_error
297                                    ),
298                                );
299                                Ok(build_target)
300                            }
301                            Err(build_target_error) => {
302                                // In this case, we can't do anything.
303                                Err(HostPlatformDetectError::HostPlatformParseError {
304                                    host_platform_error: Box::new(host_platform_error),
305                                    build_target_error: Box::new(build_target_error),
306                                })
307                            }
308                        }
309                    }
310                }
311            } else {
312                match build_target {
313                    Ok(build_target) => {
314                        warn!(
315                            "for host platform, `rustc -vV` failed with {}; \
316                             falling back to build target `{}`\n\
317                             - `rustc -vV` stdout:\n{}\n\
318                             - `rustc -vV` stderr:\n{}",
319                            output.status,
320                            build_target.triple().as_str(),
321                            Indented {
322                                item: String::from_utf8_lossy(&output.stdout),
323                                indent: "  "
324                            },
325                            Indented {
326                                item: String::from_utf8_lossy(&output.stderr),
327                                indent: "  "
328                            },
329                        );
330                        Ok(build_target)
331                    }
332                    Err(build_target_error) => {
333                        // If the build target isn't available either, we
334                        // can't do anything.
335                        Err(HostPlatformDetectError::RustcVvFailed {
336                            status: output.status,
337                            stdout: output.stdout,
338                            stderr: output.stderr,
339                            build_target_error: Box::new(build_target_error),
340                        })
341                    }
342                }
343            }
344        }
345        Err(error) => {
346            match build_target {
347                Ok(build_target) => {
348                    warn!(
349                        "for host platform, failed to spawn `rustc -vV`; \
350                         falling back to build target `{}`\n\
351                         - host platform error:\n{}",
352                        build_target.triple().as_str(),
353                        DisplayErrorChain::new_with_initial_indent("  ", error),
354                    );
355                    Ok(build_target)
356                }
357                Err(build_target_error) => {
358                    // If the build target isn't available either, we
359                    // can't do anything.
360                    Err(HostPlatformDetectError::RustcVvSpawnError {
361                        error,
362                        build_target_error: Box::new(build_target_error),
363                    })
364                }
365            }
366        }
367    }
368}
369
370/// The target platform.
371#[derive(Clone, Debug, Eq, PartialEq)]
372pub struct TargetPlatform {
373    /// The target triple: the platform, along with its source and where it was obtained from.
374    pub triple: TargetTriple,
375
376    /// The target libdir.
377    pub libdir: PlatformLibdir,
378}
379
380impl TargetPlatform {
381    /// Creates a new [`TargetPlatform`].
382    pub fn new(triple: TargetTriple, libdir: PlatformLibdir) -> Self {
383        Self { triple, libdir }
384    }
385
386    /// Converts self to a summary.
387    pub fn to_summary(&self) -> TargetPlatformSummary {
388        TargetPlatformSummary {
389            platform: self.triple.platform.to_summary(),
390            libdir: self.libdir.to_summary(),
391        }
392    }
393
394    /// Converts a summary to a [`TargetPlatform`].
395    pub fn from_summary(summary: TargetPlatformSummary) -> Result<Self, RustBuildMetaParseError> {
396        Ok(Self {
397            triple: TargetTriple::deserialize(Some(summary.platform))
398                .map_err(RustBuildMetaParseError::PlatformDeserializeError)?
399                .expect("the input is not None, so the output must not be None"),
400            libdir: PlatformLibdir::from_summary(summary.libdir),
401        })
402    }
403
404    fn map_libdir(&self, mapper: &PlatformLibdirMapper) -> Self {
405        Self {
406            triple: self.triple.clone(),
407            libdir: mapper.map(&self.libdir),
408        }
409    }
410}
411
412/// A platform libdir.
413#[derive(Clone, Debug, Eq, PartialEq)]
414pub enum PlatformLibdir {
415    /// The libdir is known and available.
416    Available(Utf8PathBuf),
417
418    /// The libdir is unavailable.
419    Unavailable(PlatformLibdirUnavailable),
420}
421
422impl PlatformLibdir {
423    /// Constructs a new `PlatformLibdir` from a `Utf8PathBuf`.
424    pub fn from_path(path: Utf8PathBuf) -> Self {
425        Self::Available(path)
426    }
427
428    /// Constructs a new `PlatformLibdir` from rustc's standard output.
429    ///
430    /// None implies that rustc failed, and will be stored as such.
431    pub fn from_rustc_stdout(rustc_output: Option<Vec<u8>>) -> Self {
432        fn inner(v: Option<Vec<u8>>) -> Result<Utf8PathBuf, PlatformLibdirUnavailable> {
433            let v = v.ok_or(PlatformLibdirUnavailable::RUSTC_FAILED)?;
434
435            let s = String::from_utf8(v).map_err(|e| {
436                debug!("failed to convert the output to a string: {e}");
437                PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR
438            })?;
439
440            let mut lines = s.lines();
441            let Some(out) = lines.next() else {
442                debug!("empty output");
443                return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
444            };
445
446            let trimmed = out.trim();
447            if trimmed.is_empty() {
448                debug!("empty output");
449                return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
450            }
451
452            // If there's another line, it must be empty.
453            for line in lines {
454                if !line.trim().is_empty() {
455                    debug!("unexpected additional output: {line}");
456                    return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
457                }
458            }
459
460            Ok(Utf8PathBuf::from(trimmed))
461        }
462
463        match inner(rustc_output) {
464            Ok(path) => Self::Available(path),
465            Err(error) => Self::Unavailable(error),
466        }
467    }
468
469    /// Constructs a new `PlatformLibdir` from a `PlatformLibdirUnavailable`.
470    pub fn from_unavailable(error: PlatformLibdirUnavailable) -> Self {
471        Self::Unavailable(error)
472    }
473
474    /// Returns self as a path if available.
475    pub fn as_path(&self) -> Option<&Utf8Path> {
476        match self {
477            Self::Available(path) => Some(path),
478            Self::Unavailable(_) => None,
479        }
480    }
481
482    /// Converts self to a summary.
483    pub fn to_summary(&self) -> PlatformLibdirSummary {
484        match self {
485            Self::Available(path) => PlatformLibdirSummary::Available { path: path.clone() },
486            Self::Unavailable(reason) => PlatformLibdirSummary::Unavailable {
487                reason: reason.clone(),
488            },
489        }
490    }
491
492    /// Converts a summary to a [`PlatformLibdir`].
493    pub fn from_summary(summary: PlatformLibdirSummary) -> Self {
494        match summary {
495            PlatformLibdirSummary::Available { path: libdir } => Self::Available(libdir),
496            PlatformLibdirSummary::Unavailable { reason } => Self::Unavailable(reason),
497        }
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504    use test_case::test_case;
505
506    #[test]
507    fn test_from_rustc_output_invalid() {
508        // None.
509        assert_eq!(
510            PlatformLibdir::from_rustc_stdout(None),
511            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_FAILED),
512        );
513
514        // Empty input.
515        assert_eq!(
516            PlatformLibdir::from_rustc_stdout(Some(Vec::new())),
517            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
518        );
519
520        // A single empty line.
521        assert_eq!(
522            PlatformLibdir::from_rustc_stdout(Some(b"\n".to_vec())),
523            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
524        );
525
526        // Multiple lines.
527        assert_eq!(
528            PlatformLibdir::from_rustc_stdout(Some(b"/fake/libdir/1\n/fake/libdir/2\n".to_vec())),
529            PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
530        );
531    }
532
533    #[test_case(b"/fake/libdir/22548", "/fake/libdir/22548"; "single line")]
534    #[test_case(
535        b"\t /fake/libdir\t \n\r",
536        "/fake/libdir";
537        "with leading or trailing whitespace"
538    )]
539    #[test_case(
540        b"/fake/libdir/1\n\n",
541        "/fake/libdir/1";
542        "trailing newlines"
543    )]
544    fn test_read_from_rustc_output_valid(input: &[u8], actual: &str) {
545        assert_eq!(
546            PlatformLibdir::from_rustc_stdout(Some(input.to_vec())),
547            PlatformLibdir::Available(actual.into()),
548        );
549    }
550}