Skip to main content

lux_lib/
toolchains.rs

1use serde::{Deserialize, Serialize};
2use std::path::{Path, PathBuf};
3use std::process::Command;
4use target_lexicon::Triple;
5use which::which;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ToolInfo {
9    path: PathBuf,
10    version: Option<String>,
11}
12
13impl ToolInfo {
14    pub fn path(&self) -> &Path {
15        &self.path
16    }
17
18    pub fn version(&self) -> Option<&str> {
19        self.version.as_deref()
20    }
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Tool {
25    name: String,
26    info: Option<ToolInfo>,
27}
28
29impl Tool {
30    pub fn name(&self) -> &String {
31        &self.name
32    }
33
34    pub fn info(&self) -> Option<&ToolInfo> {
35        self.info.as_ref()
36    }
37
38    pub fn is_found(&self) -> bool {
39        self.info.is_some()
40    }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ToolchainReport {
45    c_compiler: Tool,
46    make: Tool,
47    cmake: Tool,
48    cargo: Tool,
49    pkg_config: Tool,
50}
51
52impl ToolchainReport {
53    pub fn c_compiler(&self) -> &Tool {
54        &self.c_compiler
55    }
56    pub fn make(&self) -> &Tool {
57        &self.make
58    }
59    pub fn cmake(&self) -> &Tool {
60        &self.cmake
61    }
62    pub fn cargo(&self) -> &Tool {
63        &self.cargo
64    }
65    pub fn pkg_config(&self) -> &Tool {
66        &self.pkg_config
67    }
68
69    pub fn generate() -> Self {
70        Self {
71            c_compiler: check_c_compiler(),
72            make: check_executable("make"),
73            cmake: check_executable("cmake"),
74            cargo: check_executable("cargo"),
75            pkg_config: check_executable("pkg-config"),
76        }
77    }
78}
79
80fn check_executable(name: &str) -> Tool {
81    match which(name) {
82        Ok(path) => {
83            let version = try_get_version(Command::new(&path));
84
85            Tool {
86                name: name.to_string(),
87                info: Some(ToolInfo { path, version }),
88            }
89        }
90        Err(_) => Tool {
91            name: name.to_string(),
92            info: None,
93        },
94    }
95}
96
97fn check_c_compiler() -> Tool {
98    let host = Triple::host();
99    match cc::Build::new()
100        .host(&host.to_string())
101        .target(&host.to_string())
102        .opt_level(2)
103        .try_get_compiler()
104    {
105        Ok(compiler) => {
106            let path = compiler.path().to_path_buf();
107
108            let binary = path
109                .file_name()
110                .map(|n| n.to_string_lossy().to_string())
111                .unwrap_or_else(|| "unknown".to_string());
112
113            let name = format!("C compiler ({})", binary);
114            let version = try_get_version(Command::new(&path));
115
116            Tool {
117                name,
118                info: Some(ToolInfo { path, version }),
119            }
120        }
121
122        Err(_) => Tool {
123            name: "C compiler".to_string(),
124            info: None,
125        },
126    }
127}
128
129fn try_get_version(mut cmd: Command) -> Option<String> {
130    cmd.arg("--version")
131        .output()
132        .ok()
133        .map(|output| String::from_utf8_lossy(&output.stdout).to_string())
134        .and_then(|stdout| try_parse_version(&stdout))
135}
136
137fn try_parse_version(stdout: &str) -> Option<String> {
138    stdout
139        .lines()
140        .find(|line| !line.trim().is_empty())
141        .map(|line| line.trim().to_string())
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_try_parse_version() {
150        let gcc_output = "gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0\nCopyright (C) 2021 Free Software Foundation, Inc.\nThis is free software...";
151        assert_eq!(
152            try_parse_version(gcc_output),
153            Some("gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0".to_string())
154        );
155
156        let loose_output = "\n\n   cmake version 3.22.1   \nConfigured safely";
157        assert_eq!(
158            try_parse_version(loose_output),
159            Some("cmake version 3.22.1".to_string())
160        );
161
162        assert_eq!(try_parse_version("   \n\n  "), None);
163    }
164
165    #[test]
166    fn test_live_environment_smoke() {
167        let report = ToolchainReport::generate();
168
169        let tools = [
170            report.c_compiler(),
171            report.make(),
172            report.cmake(),
173            report.cargo(),
174            report.pkg_config(),
175        ];
176
177        for tool in tools {
178            if let Some(info) = tool.info() {
179                assert!(!info.path().as_os_str().is_empty());
180            }
181        }
182    }
183}