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(
49            self.rustc_path.as_str(),
50            self.args.iter().map(|arg| arg.as_ref()),
51        )
52    }
53
54    /// Execute the command, capture its standard output, and return the captured output as a
55    /// [`Vec<u8>`].
56    pub fn read(&self) -> Option<Vec<u8>> {
57        let expression = self.to_expression();
58        trace!("Executing command: {:?}", expression);
59        let output = match expression
60            .stdout_capture()
61            .stderr_capture()
62            .unchecked()
63            .run()
64        {
65            Ok(output) => output,
66            Err(e) => {
67                debug!("Failed to spawn the child process: {}", e);
68                return None;
69            }
70        };
71        if !output.status.success() {
72            debug!("execution failed with {}", output.status);
73            debug!("stdout:");
74            debug!("{}", String::from_utf8_lossy(&output.stdout));
75            debug!("stderr:");
76            debug!("{}", String::from_utf8_lossy(&output.stderr));
77            return None;
78        }
79        Some(output.stdout)
80    }
81}
82
83impl Default for RustcCli<'_> {
84    fn default() -> Self {
85        Self {
86            rustc_path: rustc_path(),
87            args: vec![],
88        }
89    }
90}
91
92fn rustc_path() -> Utf8PathBuf {
93    match std::env::var_os("RUSTC") {
94        Some(rustc_path) => PathBuf::from(rustc_path)
95            .try_into()
96            .expect("RUSTC env var is not valid UTF-8"),
97        None => Utf8PathBuf::from("rustc"),
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use camino_tempfile::Utf8TempDir;
105    use std::env;
106
107    #[test]
108    fn test_should_run_rustc_version() {
109        let mut cli = RustcCli::default();
110        cli.add_arg("--version");
111        let output = cli.read().expect("rustc --version should run successfully");
112        let output = String::from_utf8(output).expect("the output should be valid utf-8");
113        assert!(
114            output.starts_with("rustc"),
115            "The output should start with rustc, but the actual output is: {output}"
116        );
117    }
118
119    #[test]
120    fn test_should_respect_rustc_env() {
121        env::set_var("RUSTC", "cargo");
122        let mut cli = RustcCli::default();
123        cli.add_arg("--version");
124        let output = cli.read().expect("cargo --version should run successfully");
125        let output = String::from_utf8(output).expect("the output should be valid utf-8");
126        assert!(
127            output.starts_with("cargo"),
128            "The output should start with cargo, but the actual output is: {output}"
129        );
130    }
131
132    #[test]
133    fn test_fail_to_spawn() {
134        let fake_dir = Utf8TempDir::new().expect("should create the temp dir successfully");
135        // No OS will allow executing a directory.
136        env::set_var("RUSTC", fake_dir.path());
137        let mut cli = RustcCli::default();
138        cli.add_arg("--version");
139        let output = cli.read();
140        assert_eq!(output, None);
141    }
142
143    #[test]
144    fn test_execute_with_failure() {
145        let mut cli = RustcCli::default();
146        // rustc --print Y7uDG1HrrY should fail
147        cli.add_arg("--print");
148        cli.add_arg("Y7uDG1HrrY");
149        let output = cli.read();
150        assert_eq!(output, None);
151    }
152}