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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
use std::{
env::current_dir,
fmt::{self, Display, Formatter},
fs,
path::{Path, PathBuf},
process::Command,
};
use regex::Regex;
use semver::Version;
use which::{which, which_in};
use crate::RequestedVersion;
/// Error that occur when trying to get the path to a clang tool executable.
#[derive(Debug, thiserror::Error)]
pub enum GetClangPathError {
/// Failed to access current working directory.
#[error("Failed to access current working directory: {0}")]
InvalidCurrentDirectory(#[from] std::io::Error),
/// Failed to find the clang tool binary by searching for the provided name.
#[error("Failed to find the {0} binary by searching for the provided name: {1}")]
NotFoundByName(ClangTool, which::Error),
/// Failed to find the clang tool binary by searching for the provided version requirement.
#[error("Failed to find the {0} binary by searching for the provided version requirement: {1}")]
NotFoundByVersion(ClangTool, which::Error),
/// Failed to find the clang tool binary by searching for the provided path.
#[error("Failed to find the {0} binary by searching for the provided path: {1}")]
NotFoundByPath(ClangTool, which::Error),
}
/// Error that occur when trying to get the version number of a clang tool executable's output.
#[derive(Debug, thiserror::Error)]
pub enum GetClangVersionError {
/// Failed to run the clang tool executable with `--version` flag.
#[error("Failed to run `{0} --version` flag: {1}")]
Command(PathBuf, std::io::Error),
/// Regex pattern failed to compile.
#[error("Regex pattern failed to compile: {0}")]
RegexCompile(#[from] regex::Error),
/// Failed to parse the version number from the output of `clang-tool --version`.
#[error("Failed to parse the version number from the `--version` output: {0}")]
VersionParse(String),
/// Failed to parse the version number from the output of `clang-tool --version` into a [`semver::Version`].
#[error("Failed to parse the version number from the `--version` output: {0}")]
SemVerParse(#[from] semver::Error),
}
/// A enumeration of supported clang tools.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "bin", derive(clap::ValueEnum))]
pub enum ClangTool {
ClangTidy,
ClangFormat,
}
impl Display for ClangTool {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl ClangTool {
/// Get the string representation of the clang tool's name.
pub const fn as_str(&self) -> &'static str {
match self {
ClangTool::ClangTidy => "clang-tidy",
ClangTool::ClangFormat => "clang-format",
}
}
/// Fetch the path to an executable clang tool for the specified `version`.
///
/// If the executable is not found using the specified `version`, then the tool is
/// sought only by it's name ([`Self::as_str()`]).
///
/// The only reason this function would return an error is if the specified tool is not
/// installed or present on the system (nor in the `PATH` environment variable).
pub fn get_exe_path(&self, version: &RequestedVersion) -> Result<PathBuf, GetClangPathError> {
let name = self.as_str();
match version {
RequestedVersion::Path(path_buf) => which_in(
name,
Some(path_buf),
current_dir().map_err(GetClangPathError::InvalidCurrentDirectory)?,
)
.map_err(|e| GetClangPathError::NotFoundByPath(*self, e)),
// Thus, we should use whatever is installed and added to $PATH.
RequestedVersion::SystemDefault | RequestedVersion::NoValue => {
which(name).map_err(|e| GetClangPathError::NotFoundByName(*self, e))
}
RequestedVersion::Requirement(req) => {
// `req.comparators` has at least a major version number for each comparator.
// We need to start with the highest major version number first, then
// decrement to the lowest that satisfies the requirement.
// find the highest major version from requirement's boundaries.
let mut it = req.comparators.iter();
let mut highest_major = it.next().map(|v| v.major).unwrap_or_default() + 1;
for n in it {
if n.major > highest_major {
// +1 because we aren't checking the comparator's operator here.
highest_major = n.major + 1;
}
}
// aggregate by decrementing through major versions that satisfy the requirement.
let mut majors = vec![];
while highest_major > 0 {
// check if the current major version satisfies the requirement.
if req.matches(&Version::new(highest_major, 0, 0)) {
majors.push(highest_major);
}
highest_major -= 1;
}
// now we're ready to search for the binary exe with the major version suffixed.
for major in majors {
if let Ok(cmd) = which(format!("{self}-{major}")) {
return Ok(cmd);
}
}
// failed to find a binary where the major version number is suffixed to the tool name.
// This line essentially ignores the version specified as a fail-safe.
//
// On Windows, the version's major number is typically not appended to the name of
// the executable (or symlink for executable), so this is useful in that scenario.
//
// On Unix systems, this line is not likely reached. Typically, installing clang
// will produce a symlink to the executable with the major version appended to the
// name.
which(name).map_err(|e| GetClangPathError::NotFoundByVersion(*self, e))
}
}
}
/// Run `clang-tool --version`, then extract and return the version number.
pub fn capture_version(&self, path: &Path) -> Result<Version, GetClangVersionError> {
let output = Command::new(path)
.arg("--version")
.output()
.map_err(|e| GetClangVersionError::Command(path.to_path_buf(), e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let version_pattern = Regex::new(r"(?i)version[^\d]*([\d.]+)")?;
if let Some(captures) = version_pattern.captures(&stdout)
&& let Some(result) = captures.get(1)
{
let version = Version::parse(result.as_str())?;
return Ok(version);
}
Err(GetClangVersionError::VersionParse(stdout.to_string()))
}
pub fn symlink_bin(
&self,
bin: &PathBuf,
symlink_path: &PathBuf,
force: bool,
) -> std::io::Result<()> {
if symlink_path.exists() && !force {
log::warn!(
"Symlink for {self} already exists at {}. Use --force to overwrite it.",
symlink_path.to_string_lossy()
);
} else {
if symlink_path.exists() {
log::info!(
"Overwriting existing symlink for {self} at {}",
symlink_path.to_string_lossy()
);
fs::remove_file(symlink_path)?;
}
log::info!("Creating symlink for {self} at {symlink_path:?}");
#[cfg(windows)]
std::os::windows::fs::symlink_file(bin, symlink_path)?;
#[cfg(unix)]
std::os::unix::fs::symlink(bin, symlink_path)?;
}
// check if the symlink is valid and points to an existing file.
let symlink = fs::read_link(symlink_path)?;
log::info!(
"Symlink {} points to {}",
symlink_path.to_string_lossy(),
symlink.to_string_lossy()
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{path::PathBuf, str::FromStr};
use which::which;
use super::ClangTool;
use crate::RequestedVersion;
const CLANG_FORMAT: ClangTool = ClangTool::ClangFormat;
#[test]
fn get_exe_by_version() {
let requirement = ">=9, <22";
let req_version = RequestedVersion::from_str(requirement).unwrap();
let tool_exe = CLANG_FORMAT.get_exe_path(&req_version);
println!("tool_exe: {:?}", tool_exe);
assert!(tool_exe.is_ok_and(|val| {
val.file_name()
.unwrap()
.to_string_lossy()
.to_string()
.contains(CLANG_FORMAT.as_str())
}));
}
#[test]
fn get_exe_by_default() {
let tool_exe = CLANG_FORMAT.get_exe_path(&RequestedVersion::from_str("").unwrap());
println!("tool_exe: {:?}", tool_exe);
assert!(tool_exe.is_ok_and(|val| {
val.file_name()
.unwrap()
.to_string_lossy()
.to_string()
.contains(CLANG_FORMAT.as_str())
}));
}
#[test]
fn get_exe_by_path() {
static TOOL_NAME: &str = CLANG_FORMAT.as_str();
let clang_version = which(TOOL_NAME).unwrap();
let bin_path = clang_version.parent().unwrap().to_str().unwrap();
println!("binary exe path: {bin_path}");
let tool_exe = CLANG_FORMAT
.get_exe_path(&RequestedVersion::from_str(bin_path).unwrap())
.unwrap();
println!("tool_exe: {:?}", tool_exe);
assert!(
tool_exe
.file_name()
.unwrap()
.to_string_lossy()
.to_string()
.contains(TOOL_NAME)
);
}
#[test]
fn get_exe_by_invalid_path() {
let tool_exe =
CLANG_FORMAT.get_exe_path(&RequestedVersion::Path(PathBuf::from("non-existent-path")));
assert!(tool_exe.is_err());
}
}