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}