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