1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Copyright (c) The nextest Contributors
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::cargo_config::TargetTriple;
use camino::Utf8PathBuf;
use std::{borrow::Cow, path::PathBuf};

/// Create a rustc CLI call.
#[derive(Clone, Debug)]
pub struct RustcCli<'a> {
    rustc_path: Utf8PathBuf,
    args: Vec<Cow<'a, str>>,
}

impl<'a> RustcCli<'a> {
    /// Create a rustc CLI call: `rustc --print target-libdir`.
    pub fn print_host_libdir() -> Self {
        let mut cli = Self::default();
        cli.add_arg("--print").add_arg("target-libdir");
        cli
    }

    /// Create a rustc CLI call: `rustc --print target-libdir --target <triple>`.
    pub fn print_target_libdir(triple: &'a TargetTriple) -> Self {
        let mut cli = Self::default();
        cli.add_arg("--print")
            .add_arg("target-libdir")
            .add_arg("--target")
            .add_arg(triple.platform.triple_str());
        cli
    }

    fn add_arg(&mut self, arg: impl Into<Cow<'a, str>>) -> &mut Self {
        self.args.push(arg.into());
        self
    }

    fn to_expression(&self) -> duct::Expression {
        duct::cmd(
            self.rustc_path.as_str(),
            self.args.iter().map(|arg| arg.as_ref()),
        )
    }

    /// Execute the command, capture its standard output, and return the captured output as a
    /// [`Vec<u8>`].
    pub fn read(&self) -> Option<Vec<u8>> {
        let expression = self.to_expression();
        log::trace!("Executing command: {:?}", expression);
        let output = match expression
            .stdout_capture()
            .stderr_capture()
            .unchecked()
            .run()
        {
            Ok(output) => output,
            Err(e) => {
                log::debug!("Failed to spawn the child process: {}", e);
                return None;
            }
        };
        if !output.status.success() {
            log::debug!("execution failed with {}", output.status);
            log::debug!("stdout:");
            log::debug!("{}", String::from_utf8_lossy(&output.stdout));
            log::debug!("stderr:");
            log::debug!("{}", String::from_utf8_lossy(&output.stderr));
            return None;
        }
        Some(output.stdout)
    }
}

impl<'a> Default for RustcCli<'a> {
    fn default() -> Self {
        Self {
            rustc_path: rustc_path(),
            args: vec![],
        }
    }
}

fn rustc_path() -> Utf8PathBuf {
    match std::env::var_os("RUSTC") {
        Some(rustc_path) => PathBuf::from(rustc_path)
            .try_into()
            .expect("RUSTC env var is not valid UTF-8"),
        None => Utf8PathBuf::from("rustc"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use camino_tempfile::Utf8TempDir;
    use std::env;

    #[test]
    fn test_should_run_rustc_version() {
        let mut cli = RustcCli::default();
        cli.add_arg("--version");
        let output = cli.read().expect("rustc --version should run successfully");
        let output = String::from_utf8(output).expect("the output should be valid utf-8");
        assert!(
            output.starts_with("rustc"),
            "The output should start with rustc, but the actual output is: {}",
            output
        );
    }

    #[test]
    fn test_should_respect_rustc_env() {
        env::set_var("RUSTC", "cargo");
        let mut cli = RustcCli::default();
        cli.add_arg("--version");
        let output = cli.read().expect("cargo --version should run successfully");
        let output = String::from_utf8(output).expect("the output should be valid utf-8");
        assert!(
            output.starts_with("cargo"),
            "The output should start with cargo, but the actual output is: {}",
            output
        );
    }

    #[test]
    fn test_fail_to_spawn() {
        let fake_dir = Utf8TempDir::new().expect("should create the temp dir successfully");
        // No OS will allow executing a directory.
        env::set_var("RUSTC", fake_dir.path());
        let mut cli = RustcCli::default();
        cli.add_arg("--version");
        let output = cli.read();
        assert_eq!(output, None);
    }

    #[test]
    fn test_execute_with_failure() {
        let mut cli = RustcCli::default();
        // rustc --print Y7uDG1HrrY should fail
        cli.add_arg("--print");
        cli.add_arg("Y7uDG1HrrY");
        let output = cli.read();
        assert_eq!(output, None);
    }
}