use super::types::{CompilerType, Toolchain, ToolchainError, VSInstallation};
use colored::*;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
const VSWHERE_PATHS: &[&str] = &[
r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe",
r"C:\Program Files\Microsoft Visual Studio\Installer\vswhere.exe",
];
const LLVM_PATHS: &[&str] = &[
r"C:\Program Files\LLVM\bin",
r"C:\Program Files (x86)\LLVM\bin",
];
pub fn find_vswhere() -> Option<PathBuf> {
for path in VSWHERE_PATHS {
let p = PathBuf::from(path);
if p.exists() {
return Some(p);
}
}
None
}
pub fn detect_vs_installations() -> Result<Vec<VSInstallation>, ToolchainError> {
let vswhere = find_vswhere().ok_or_else(|| {
ToolchainError::VsWhereError(
"vswhere.exe not found. Please install Visual Studio or Build Tools.".to_string(),
)
})?;
let output = Command::new(&vswhere)
.args([
"-all",
"-format",
"json",
"-utf8",
"-products",
"*",
"-requires",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
])
.output()?;
if !output.status.success() {
return Err(ToolchainError::VsWhereError(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
let json_str = String::from_utf8_lossy(&output.stdout);
parse_vswhere_output(&json_str)
}
fn parse_vswhere_output(json_str: &str) -> Result<Vec<VSInstallation>, ToolchainError> {
let installations: Vec<serde_json::Value> = serde_json::from_str(json_str).map_err(|e| {
ToolchainError::VsWhereError(format!("Failed to parse vswhere output: {}", e))
})?;
let mut result = Vec::new();
let mut seen_paths = std::collections::HashSet::new();
for inst in installations {
if let (Some(path), Some(name), Some(version), Some(product)) = (
inst.get("installationPath").and_then(|v| v.as_str()),
inst.get("displayName").and_then(|v| v.as_str()),
inst.get("installationVersion").and_then(|v| v.as_str()),
inst.get("productId").and_then(|v| v.as_str()),
) {
let path_buf = PathBuf::from(path);
if seen_paths.contains(&path_buf) {
continue;
}
seen_paths.insert(path_buf.clone());
result.push(VSInstallation {
install_path: path_buf,
display_name: name.to_string(),
version: version.to_string(),
product_id: product.to_string(),
});
}
}
Ok(result)
}
pub fn find_msvc_toolset(vs_path: &Path) -> Option<(PathBuf, String)> {
let vc_tools_path = vs_path.join("VC").join("Tools").join("MSVC");
if !vc_tools_path.exists() {
return None;
}
let mut versions: Vec<_> = std::fs::read_dir(&vc_tools_path)
.ok()?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
versions.sort();
let latest = versions.pop()?;
let toolset_path = vc_tools_path.join(&latest);
Some((toolset_path, latest))
}
pub fn find_cl_exe(toolset_path: &Path) -> Option<PathBuf> {
for host in ["Hostx64", "Hostx86"] {
for target in ["x64", "x86"] {
let cl_path = toolset_path
.join("bin")
.join(host)
.join(target)
.join("cl.exe");
if cl_path.exists() {
return Some(cl_path);
}
}
}
None
}
pub fn find_bundled_clang_cl(vs_path: &Path) -> Option<PathBuf> {
let paths = [
vs_path
.join("VC")
.join("Tools")
.join("Llvm")
.join("x64")
.join("bin")
.join("clang-cl.exe"),
vs_path
.join("VC")
.join("Tools")
.join("Llvm")
.join("bin")
.join("clang-cl.exe"),
];
paths.into_iter().find(|p| p.exists())
}
pub fn find_bundled_clang(vs_path: &Path) -> Option<PathBuf> {
let paths = [
vs_path
.join("VC")
.join("Tools")
.join("Llvm")
.join("x64")
.join("bin")
.join("clang++.exe"),
vs_path
.join("VC")
.join("Tools")
.join("Llvm")
.join("bin")
.join("clang++.exe"),
];
paths.into_iter().find(|p| p.exists())
}
pub fn find_standalone_llvm() -> Option<PathBuf> {
for path in LLVM_PATHS {
let clang_cl = PathBuf::from(path).join("clang-cl.exe");
if clang_cl.exists() {
return Some(clang_cl);
}
}
#[cfg(windows)]
{
use winreg::RegKey;
use winreg::enums::*;
if let Ok(hklm) = RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey(r"SOFTWARE\LLVM\LLVM")
&& let Ok(path) = hklm.get_value::<String, _>("")
{
let clang_cl = PathBuf::from(&path).join("bin").join("clang-cl.exe");
if clang_cl.exists() {
return Some(clang_cl);
}
}
}
None
}
pub fn find_standalone_clang() -> Option<PathBuf> {
for path in LLVM_PATHS {
let clang_pp = PathBuf::from(path).join("clang++.exe");
if clang_pp.exists() {
return Some(clang_pp);
}
}
#[cfg(windows)]
{
use winreg::RegKey;
use winreg::enums::*;
if let Ok(hklm) = RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey(r"SOFTWARE\LLVM\LLVM")
&& let Ok(path) = hklm.get_value::<String, _>("")
{
let clang_pp = PathBuf::from(&path).join("bin").join("clang++.exe");
if clang_pp.exists() {
return Some(clang_pp);
}
}
}
None
}
pub fn load_vcvars_env(vs_path: &Path) -> Result<HashMap<String, String>, ToolchainError> {
let vcvars_path = vs_path
.join("VC")
.join("Auxiliary")
.join("Build")
.join("vcvars64.bat");
if !vcvars_path.exists() {
return Err(ToolchainError::VcVarsError(format!(
"vcvars64.bat not found at {}",
vcvars_path.display()
)));
}
let vcvars_str = vcvars_path.to_string_lossy();
let cmd_str = format!("call \"{}\" && set", vcvars_str);
#[cfg(windows)]
let output = {
use std::os::windows::process::CommandExt;
Command::new("cmd")
.raw_arg(format!("/C {}", cmd_str))
.output()?
};
#[cfg(not(windows))]
let output = Command::new("cmd").args(["/C", &cmd_str]).output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
if output_str.is_empty() || !output_str.contains("Path=") {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ToolchainError::VcVarsError(format!(
"Failed to load vcvars environment. Exit code: {:?}, stderr: {}",
output.status.code(),
stderr
)));
}
let mut env_vars = HashMap::new();
for line in output_str.lines() {
if let Some((key, value)) = line.split_once('=') {
let key_upper = key.to_uppercase();
if key_upper == "PATH"
|| key_upper == "INCLUDE"
|| key_upper == "LIB"
|| key_upper == "LIBPATH"
|| key_upper.starts_with("VS")
|| key_upper.starts_with("VSCMD")
|| key_upper.starts_with("WINDOWS")
|| key_upper == "UCRTVERSION"
|| key_upper == "VCTOOLSVERSION"
{
env_vars.insert(key.to_string(), value.to_string());
}
}
}
Ok(env_vars)
}
fn get_compiler_version(compiler_path: &Path, is_msvc: bool) -> String {
let output = if is_msvc {
Command::new(compiler_path).output()
} else {
Command::new(compiler_path).arg("--version").output()
};
match output {
Ok(out) => {
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
for line in combined.lines() {
let text = line.trim();
if text.is_empty() {
continue;
}
if is_msvc {
if text.starts_with("Copyright") || text.starts_with("usage:") {
continue;
}
if text.contains("Microsoft (R)") && text.contains("Version") {
return text.to_string();
}
if text.contains("Version") {
return text.to_string();
}
} else {
return text.to_string();
}
}
if is_msvc {
for line in combined.lines() {
let text = line.trim();
if !text.is_empty()
&& !text.starts_with("usage:")
&& !text.starts_with("Copyright")
{
return text.to_string();
}
}
}
"unknown".to_string()
}
Err(_) => "unknown".to_string(),
}
}
pub fn detect_toolchain_from_source(
compiler_type: CompilerType,
source: &str,
) -> Result<Toolchain, ToolchainError> {
let vs_installations = detect_vs_installations().unwrap_or_default();
let vs = vs_installations
.iter()
.find(|vs| source.contains(&vs.display_name) || vs.display_name.contains(source))
.ok_or_else(|| {
ToolchainError::NotFound(format!(
"Visual Studio installation '{}' not found. Run 'cx toolchain select' to choose again.",
source
))
})?;
let env_vars = load_vcvars_env(&vs.install_path)?;
let (toolset_path, toolset_version) = find_msvc_toolset(&vs.install_path).ok_or_else(|| {
ToolchainError::NotFound("MSVC toolset not found in selected VS installation".to_string())
})?;
let windows_sdk_version = env_vars.get("WINDOWSSDKVERSION").cloned();
let (final_type, cxx_path, cc_path) = match compiler_type {
CompilerType::ClangCL => {
if let Some(clang_cl) = find_bundled_clang_cl(&vs.install_path) {
(CompilerType::ClangCL, clang_cl.clone(), clang_cl)
} else if let Some(clang_cl) = find_standalone_llvm() {
(CompilerType::ClangCL, clang_cl.clone(), clang_cl)
} else {
let cl = find_cl_exe(&toolset_path)
.ok_or_else(|| ToolchainError::NotFound("cl.exe not found".to_string()))?;
(CompilerType::MSVC, cl.clone(), cl)
}
}
CompilerType::Clang => {
if let Some(clang) = find_bundled_clang(&vs.install_path) {
let cc = clang.with_file_name("clang.exe");
(CompilerType::Clang, clang, cc)
} else if let Some(clang) = find_standalone_clang() {
let cc = clang.with_file_name("clang.exe");
(CompilerType::Clang, clang, cc)
} else {
let cl = find_cl_exe(&toolset_path)
.ok_or_else(|| ToolchainError::NotFound("cl.exe not found".to_string()))?;
(CompilerType::MSVC, cl.clone(), cl)
}
}
CompilerType::MSVC | CompilerType::GCC => {
let cl = find_cl_exe(&toolset_path)
.ok_or_else(|| ToolchainError::NotFound("cl.exe not found".to_string()))?;
(CompilerType::MSVC, cl.clone(), cl)
}
};
let linker_path = toolset_path
.join("bin")
.join("Hostx64")
.join("x64")
.join("link.exe");
let version = get_compiler_version(&cxx_path, final_type == CompilerType::MSVC);
Ok(Toolchain {
compiler_type: final_type,
cc_path,
cxx_path,
linker_path,
version,
msvc_toolset_version: Some(toolset_version),
windows_sdk_version,
vs_install_path: Some(vs.install_path.clone()),
env_vars,
})
}
pub fn detect_toolchain(preferred: Option<CompilerType>) -> Result<Toolchain, ToolchainError> {
let vs_installations = detect_vs_installations().unwrap_or_default();
let mut mingw_path = None;
if let Some(home) = dirs::home_dir() {
let p = home
.join(".cx")
.join("tools")
.join("mingw64")
.join("bin")
.join("g++.exe");
if p.exists() {
mingw_path = Some(p);
}
}
if mingw_path.is_none() {
if let Ok(output) = std::process::Command::new("where").arg("g++").output()
&& output.status.success()
&& let Some(line) = String::from_utf8_lossy(&output.stdout).lines().next()
{
mingw_path = Some(PathBuf::from(line.trim()));
}
}
match preferred {
Some(CompilerType::GCC) => {
if let Some(gxx) = mingw_path {
let version = get_compiler_version(&gxx, false);
return Ok(Toolchain {
compiler_type: CompilerType::GCC,
cc_path: gxx.with_file_name("gcc.exe"), cxx_path: gxx,
linker_path: PathBuf::new(), version,
msvc_toolset_version: None,
windows_sdk_version: None,
vs_install_path: None,
env_vars: HashMap::new(),
});
} else {
return Err(ToolchainError::NotFound(
"GCC/MinGW not found. Run 'cx toolchain install mingw'.".to_string(),
));
}
}
Some(CompilerType::MSVC) => {
if vs_installations.is_empty() {
return Err(ToolchainError::NotFound(
"Visual Studio not found.".to_string(),
));
}
}
_ => {
if vs_installations.is_empty() {
if let Some(gxx) = mingw_path {
println!(
"{} Visual Studio not found, falling back to MinGW.",
"!".yellow()
);
let version = get_compiler_version(&gxx, false);
return Ok(Toolchain {
compiler_type: CompilerType::GCC,
cc_path: gxx.with_file_name("gcc.exe"),
cxx_path: gxx,
linker_path: PathBuf::new(),
version,
msvc_toolset_version: None,
windows_sdk_version: None,
vs_install_path: None,
env_vars: HashMap::new(),
});
}
return Err(ToolchainError::NotFound(
"No suitable compiler found (VS or MinGW).".to_string(),
));
}
}
}
let vs = &vs_installations[0];
let env_vars = load_vcvars_env(&vs.install_path)?;
let (toolset_path, toolset_version) = find_msvc_toolset(&vs.install_path).ok_or_else(|| {
ToolchainError::NotFound("MSVC toolset not found in VS installation".to_string())
})?;
let windows_sdk_version = env_vars.get("WINDOWSSDKVERSION").cloned();
let (compiler_type, cxx_path, cc_path) = match preferred {
Some(CompilerType::ClangCL) => {
let mut clang_cl_path = None;
for vs_inst in &vs_installations {
if let Some(path) = find_bundled_clang_cl(&vs_inst.install_path) {
clang_cl_path = Some(path);
break;
}
}
if clang_cl_path.is_none() {
clang_cl_path = find_standalone_llvm();
}
if let Some(clang_cl) = clang_cl_path {
(CompilerType::ClangCL, clang_cl.clone(), clang_cl)
} else {
let cl = find_cl_exe(&toolset_path)
.ok_or_else(|| ToolchainError::NotFound("cl.exe not found".to_string()))?;
(CompilerType::MSVC, cl.clone(), cl)
}
}
Some(CompilerType::MSVC) | None => {
let cl = find_cl_exe(&toolset_path)
.ok_or_else(|| ToolchainError::NotFound("cl.exe not found".to_string()))?;
(CompilerType::MSVC, cl.clone(), cl)
}
Some(CompilerType::Clang) => {
let mut clang_path = None;
if let Some(path) = find_standalone_clang() {
clang_path = Some(path);
}
if clang_path.is_none() {
for vs_inst in &vs_installations {
if let Some(path) = find_bundled_clang(&vs_inst.install_path) {
clang_path = Some(path);
break;
}
}
}
if let Some(clang) = clang_path {
let cc_path = clang.with_file_name("clang.exe");
(CompilerType::Clang, clang, cc_path)
} else {
let cl = find_cl_exe(&toolset_path)
.ok_or_else(|| ToolchainError::NotFound("cl.exe not found".to_string()))?;
(CompilerType::MSVC, cl.clone(), cl)
}
}
Some(CompilerType::GCC) => unreachable!(), };
let linker_path = toolset_path
.join("bin")
.join("Hostx64")
.join("x64")
.join("link.exe");
let version = get_compiler_version(&cxx_path, compiler_type == CompilerType::MSVC);
Ok(Toolchain {
compiler_type,
cc_path,
cxx_path,
linker_path,
version,
msvc_toolset_version: Some(toolset_version),
windows_sdk_version,
vs_install_path: Some(vs.install_path.clone()),
env_vars,
})
}
#[derive(Debug, Clone)]
pub struct AvailableToolchain {
pub display_name: String,
pub compiler_type: CompilerType,
pub path: PathBuf,
pub version: String,
pub source: String, }
impl std::fmt::Display for AvailableToolchain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} ({}) - {}",
self.display_name, self.version, self.source
)
}
}
pub fn discover_all_toolchains() -> Vec<AvailableToolchain> {
let mut toolchains = Vec::new();
if let Ok(vs_installations) = detect_vs_installations() {
for vs in &vs_installations {
if let Some((toolset_path, _version)) = find_msvc_toolset(&vs.install_path)
&& let Some(cl) = find_cl_exe(&toolset_path)
{
let version = get_compiler_version(&cl, true);
toolchains.push(AvailableToolchain {
display_name: "MSVC (cl.exe)".to_string(),
compiler_type: CompilerType::MSVC,
path: cl,
version,
source: vs.display_name.clone(),
});
}
if let Some(clang_cl) = find_bundled_clang_cl(&vs.install_path) {
let version = get_compiler_version(&clang_cl, false);
toolchains.push(AvailableToolchain {
display_name: "Clang-CL (clang-cl.exe)".to_string(),
compiler_type: CompilerType::ClangCL,
path: clang_cl,
version,
source: format!("{} bundled", vs.display_name),
});
}
if let Some(clang) = find_bundled_clang(&vs.install_path) {
let version = get_compiler_version(&clang, false);
toolchains.push(AvailableToolchain {
display_name: "Clang (clang++.exe)".to_string(),
compiler_type: CompilerType::Clang,
path: clang,
version,
source: format!("{} bundled", vs.display_name),
});
}
}
}
if let Some(clang_cl) = find_standalone_llvm() {
let version = get_compiler_version(&clang_cl, false);
toolchains.push(AvailableToolchain {
display_name: "Clang-CL (clang-cl.exe)".to_string(),
compiler_type: CompilerType::ClangCL,
path: clang_cl,
version,
source: "Standalone LLVM".to_string(),
});
}
if let Some(clang) = find_standalone_clang() {
let version = get_compiler_version(&clang, false);
toolchains.push(AvailableToolchain {
display_name: "Clang (clang++.exe)".to_string(),
compiler_type: CompilerType::Clang,
path: clang,
version,
source: "Standalone LLVM".to_string(),
});
}
if let Some(home) = dirs::home_dir() {
let mingw_bin = home
.join(".cx")
.join("tools")
.join("mingw64")
.join("bin")
.join("g++.exe");
if mingw_bin.exists() {
let version = get_compiler_version(&mingw_bin, false);
toolchains.push(AvailableToolchain {
display_name: "GCC (g++.exe)".to_string(),
compiler_type: CompilerType::GCC,
path: mingw_bin,
version,
source: "Max/MinGW (WinLibs)".to_string(),
});
}
}
if let Ok(output) = std::process::Command::new("where").arg("g++").output()
&& output.status.success()
{
let paths = String::from_utf8_lossy(&output.stdout);
for line in paths.lines() {
let path = PathBuf::from(line.trim());
if toolchains.iter().any(|t| t.path == path) {
continue;
}
if path.exists() {
let version = get_compiler_version(&path, false);
let source = if line.contains("msys64") {
"MSYS2/MinGW"
} else if line.contains("mingw") {
"MinGW"
} else {
"PATH"
};
toolchains.push(AvailableToolchain {
display_name: "GCC (g++.exe)".to_string(),
compiler_type: CompilerType::GCC,
path,
version,
source: source.to_string(),
});
break; }
}
}
toolchains
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_vswhere() {
if let Some(path) = find_vswhere() {
assert!(path.exists());
assert!(path.to_string_lossy().contains("vswhere"));
}
}
}