Skip to main content

lux_lib/
toolchains.rs

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