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}