Skip to main content

nextest_runner/
rustc_cli.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::cargo_config::TargetTriple;
5use camino::Utf8PathBuf;
6use std::{borrow::Cow, path::PathBuf};
7use tracing::{debug, trace};
8
9/// Create a rustc CLI call.
10#[derive(Clone, Debug)]
11pub struct RustcCli<'a> {
12    rustc_path: Utf8PathBuf,
13    args: Vec<Cow<'a, str>>,
14}
15
16impl<'a> RustcCli<'a> {
17    /// Create a rustc CLI call: `rustc --version --verbose`.
18    pub fn version_verbose() -> Self {
19        let mut cli = Self::default();
20        cli.add_arg("--version").add_arg("--verbose");
21        cli
22    }
23
24    /// Create a rustc CLI call: `rustc --print target-libdir`.
25    pub fn print_host_libdir() -> Self {
26        let mut cli = Self::default();
27        cli.add_arg("--print").add_arg("target-libdir");
28        cli
29    }
30
31    /// Create a rustc CLI call: `rustc --print target-libdir --target <triple>`.
32    pub fn print_target_libdir(triple: &'a TargetTriple) -> Self {
33        let mut cli = Self::default();
34        cli.add_arg("--print")
35            .add_arg("target-libdir")
36            .add_arg("--target")
37            .add_arg(triple.platform.triple_str());
38        cli
39    }
40
41    fn add_arg(&mut self, arg: impl Into<Cow<'a, str>>) -> &mut Self {
42        self.args.push(arg.into());
43        self
44    }
45
46    /// Convert the command to a [`duct::Expression`].
47    pub fn to_expression(&self) -> duct::Expression {
48        duct::cmd(self.rustc_path.as_str(), self.args.iter().map(|arg| &**arg))
49    }
50
51    /// Execute the command, capture its standard output, and return the captured output as a
52    /// [`Vec<u8>`].
53    pub fn read(&self) -> Option<Vec<u8>> {
54        let expression = self.to_expression();
55        trace!("Executing command: {:?}", expression);
56        let output = match expression
57            .stdout_capture()
58            .stderr_capture()
59            .unchecked()
60            .run()
61        {
62            Ok(output) => output,
63            Err(e) => {
64                debug!("Failed to spawn the child process: {}", e);
65                return None;
66            }
67        };
68        if !output.status.success() {
69            debug!("execution failed with {}", output.status);
70            debug!("stdout:");
71            debug!("{}", String::from_utf8_lossy(&output.stdout));
72            debug!("stderr:");
73            debug!("{}", String::from_utf8_lossy(&output.stderr));
74            return None;
75        }
76        Some(output.stdout)
77    }
78}
79
80impl Default for RustcCli<'_> {
81    fn default() -> Self {
82        Self {
83            rustc_path: rustc_path(),
84            args: vec![],
85        }
86    }
87}
88
89fn rustc_path() -> Utf8PathBuf {
90    match std::env::var_os("RUSTC") {
91        Some(rustc_path) => PathBuf::from(rustc_path)
92            .try_into()
93            .expect("RUSTC env var is not valid UTF-8"),
94        None => Utf8PathBuf::from("rustc"),
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use camino_tempfile::Utf8TempDir;
102    use std::env;
103
104    #[test]
105    fn test_should_run_rustc_version() {
106        let mut cli = RustcCli::default();
107        cli.add_arg("--version");
108        let output = cli.read().expect("rustc --version should run successfully");
109        let output = String::from_utf8(output).expect("the output should be valid utf-8");
110        assert!(
111            output.starts_with("rustc"),
112            "The output should start with rustc, but the actual output is: {output}"
113        );
114    }
115
116    #[test]
117    fn test_should_respect_rustc_env() {
118        // SAFETY:
119        // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
120        unsafe { env::set_var("RUSTC", "cargo") };
121        let mut cli = RustcCli::default();
122        cli.add_arg("--version");
123        let output = cli.read().expect("cargo --version should run successfully");
124        let output = String::from_utf8(output).expect("the output should be valid utf-8");
125        assert!(
126            output.starts_with("cargo"),
127            "The output should start with cargo, but the actual output is: {output}"
128        );
129    }
130
131    #[test]
132    fn test_fail_to_spawn() {
133        let fake_dir = Utf8TempDir::new().expect("should create the temp dir successfully");
134        // No OS will allow executing a directory.
135        // SAFETY:
136        // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
137        unsafe { env::set_var("RUSTC", fake_dir.path()) };
138        let mut cli = RustcCli::default();
139        cli.add_arg("--version");
140        let output = cli.read();
141        assert_eq!(output, None);
142    }
143
144    #[test]
145    fn test_execute_with_failure() {
146        let mut cli = RustcCli::default();
147        // rustc --print Y7uDG1HrrY should fail
148        cli.add_arg("--print");
149        cli.add_arg("Y7uDG1HrrY");
150        let output = cli.read();
151        assert_eq!(output, None);
152    }
153}