use anyhow;
use clap;
use log::debug;
use semver::Version;
use which;
use crate::preset;
use crate::runner;
#[derive(clap::Parser, Debug)]
pub struct DoctorArgs {}
fn normalize_version(v: &str) -> String {
let parts: Vec<&str> = v.split('.').collect();
match parts.len() {
1 => format!("{}.0.0", parts[0]),
2 => format!("{}.{}.0", parts[0], parts[1]),
_ => v.to_string(),
}
}
fn check_tool(
name: &str,
min_ver: &str,
optional: bool,
ok: &mut u32,
warn: &mut u32,
fail: &mut u32,
) {
if let Ok(path) = which::which(name) {
let ver = std::process::Command::new(name)
.arg("--version")
.output()
.map(|o| {
let out = String::from_utf8_lossy(&o.stdout).to_string();
out.lines()
.find(|line| line.contains(char::is_numeric))
.unwrap_or("unknown")
.trim()
.to_string()
})
.unwrap_or_else(|_| "unknown".to_string());
let raw_ver = ver
.split_whitespace()
.find_map(|s| {
let s = s.trim_matches(|c: char| !c.is_ascii_digit());
if s.is_empty() {
None
} else {
Some(s.to_string())
}
})
.unwrap_or_else(|| "unknown".to_string());
let min_ver =
Version::parse(&normalize_version(min_ver)).unwrap_or_else(|_| Version::new(0, 0, 0));
let clean_ver = raw_ver.split('-').next().unwrap_or(&raw_ver);
let parsed_ver = Version::parse(&normalize_version(&clean_ver))
.unwrap_or_else(|_| Version::new(0, 0, 0));
debug!(
"Check tool {}: min_ver='{}',raw_tool='{}',parsed_tool='{}'",
name, min_ver, raw_ver, parsed_ver
);
if parsed_ver >= min_ver {
println!(" ✓ {} -> {} ({})", name, path.display(), ver);
*ok += 1;
} else {
println!(
" ✗ {} -> found {} but need >= {}",
name, parsed_ver, min_ver
);
*fail += 1;
}
} else if optional {
println!(" ⚠ {} not found (optional)", name);
*warn += 1;
} else {
println!(" ✗ {} not found", name);
*fail += 1;
}
}
fn check_project_structure(ok: &mut u32, warn: &mut u32) {
for thing in [
"CMakePresets.json",
"CMakeUserPresets.json",
"src",
"include",
"tests",
"docs",
"docs/Doxyfile.in",
"docs/conf.py",
] {
let path = std::path::PathBuf::from(thing);
if path.exists() {
println!(
" ✓ {}{} exists",
thing,
if path.is_dir() { "/" } else { "" }
);
*ok += 1;
} else {
*warn += 1;
println!(
" ⚠ {}{} does not exist",
thing,
if path.is_dir() { "/" } else { "" }
);
}
}
}
pub fn run(ctx: &runner::Context, _args: DoctorArgs) -> anyhow::Result<()> {
preset::ensure_project_root(ctx)?;
println!("Checking LIBRA environment...\n");
println!("Tools:");
let mut ok_count: u32 = 0;
let mut warn_count: u32 = 0;
let mut fail_count: u32 = 0;
let tools = [
("cmake", "3.31", false),
("ninja", "", true),
("make", "", true),
("gcc", "9", true),
("g++", "9", true),
("clang", "17", true),
("clang++", "17", true),
("icx", "2025.0", true),
("icpx", "2025.0", true),
("gcovr", "5.0", true),
("cppcheck", "2.1", true),
("clang-tidy", "17", true),
("clang-format", "17", true),
("ccache", "", true),
];
for (name, min_ver, optional) in tools {
check_tool(
name,
min_ver,
optional,
&mut ok_count,
&mut warn_count,
&mut fail_count,
);
}
println!("\nProject structure:\n");
check_project_structure(&mut ok_count, &mut warn_count);
println!(
"\nChecked {} items: {} errors, {} warnings, {} ok",
ok_count + warn_count + fail_count,
fail_count,
warn_count,
ok_count
);
if fail_count > 0 {
anyhow::bail!("doctor found errors!");
}
Ok(())
}