use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone)]
pub struct VsInstance {
pub install_path: PathBuf,
pub version: String,
pub label: String,
}
#[derive(Debug)]
pub struct VsInstallResult {
pub vs_instance: VsInstance,
pub registered_versions: Vec<String>,
pub installed_source: Option<PathBuf>,
}
pub fn find_vs_instances() -> Vec<VsInstance> {
let mut instances = Vec::new();
if let Ok(vs) = find_via_vswhere() {
instances.extend(vs);
}
#[cfg(windows)]
if instances.is_empty() {
if let Ok(vs) = find_via_registry() {
instances.extend(vs);
}
}
if instances.is_empty() {
instances.extend(find_via_common_paths());
}
instances
}
fn find_via_vswhere() -> Result<Vec<VsInstance>, String> {
let vswhere_paths = [
Path::new(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"),
Path::new(r"C:\Program Files\Microsoft Visual Studio\Installer\vswhere.exe"),
];
let vswhere = vswhere_paths.iter().find(|p| p.exists());
let vswhere = match vswhere {
Some(p) => p,
None => return Err("vswhere.exe not found".into()),
};
let output = Command::new(vswhere)
.args([
"-products",
"*",
"-requires",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-format",
"json",
"-utf8",
])
.output()
.map_err(|e| format!("failed to run vswhere: {}", e))?;
if !output.status.success() {
return Err(format!("vswhere exited with code {}", output.status));
}
#[derive(serde::Deserialize)]
struct VsWhereInstance {
#[serde(rename = "installationPath")]
installation_path: String,
#[serde(rename = "installationVersion")]
installation_version: String,
#[serde(rename = "displayName", default)]
display_name: Option<String>,
}
let parsed: Vec<VsWhereInstance> =
serde_json::from_slice(&output.stdout).map_err(|e| format!("json parse: {}", e))?;
Ok(parsed
.into_iter()
.map(|inst| {
let label = inst
.display_name
.unwrap_or_else(|| format!("VS {}", &inst.installation_version[..4]));
VsInstance {
install_path: PathBuf::from(inst.installation_path),
version: inst.installation_version,
label,
}
})
.collect())
}
#[cfg(windows)]
fn find_via_registry() -> Result<Vec<VsInstance>, String> {
use std::io::ErrorKind;
use winreg::enums::*;
use winreg::RegKey;
let mut instances = Vec::new();
let setup_key = match RegKey::predef(HKEY_LOCAL_MACHINE)
.open_subkey_with_flags(r"SOFTWARE\Microsoft\VisualStudio\Setup\VS", KEY_READ)
.or_else(|_| {
RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey_with_flags(
r"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\Setup\VS",
KEY_READ,
)
}) {
Ok(k) => k,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(instances),
Err(e) => return Err(format!("registry: {}", e)),
};
for name in setup_key.enum_keys().filter_map(|r| r.ok()) {
match setup_key.open_subkey_with_flags(&name, KEY_READ) {
Ok(inst_key) => {
let install_dir: Option<String> = inst_key.get_value("InstallDir").ok();
if let Some(dir) = install_dir {
let path = PathBuf::from(dir);
let msvc_dir = path.join("VC").join("Tools").join("MSVC");
if msvc_dir.exists() {
let version: String = inst_key
.get_value("InstallationVersion")
.or_else(|_| inst_key.get_value("Version"))
.unwrap_or_else(|_| "unknown".into());
instances.push(VsInstance {
install_path: path,
version,
label: name.clone(),
});
}
}
}
Err(_) => continue,
}
}
Ok(instances)
}
fn find_via_common_paths() -> Vec<VsInstance> {
let candidates = [
(
r"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools",
"VS 2022 BuildTools",
),
(
r"C:\Program Files\Microsoft Visual Studio\2022\BuildTools",
"VS 2022 BuildTools",
),
(
r"C:\Program Files (x86)\Microsoft Visual Studio\2022\Community",
"VS 2022 Community",
),
(
r"C:\Program Files\Microsoft Visual Studio\2022\Community",
"VS 2022 Community",
),
(
r"C:\Program Files (x86)\Microsoft Visual Studio\2022\Professional",
"VS 2022 Professional",
),
(
r"C:\Program Files\Microsoft Visual Studio\2022\Professional",
"VS 2022 Professional",
),
(
r"C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise",
"VS 2022 Enterprise",
),
(
r"C:\Program Files\Microsoft Visual Studio\2022\Enterprise",
"VS 2022 Enterprise",
),
];
candidates
.iter()
.filter(|(p, _)| Path::new(p).join("VC").join("Tools").join("MSVC").exists())
.map(|(p, label)| VsInstance {
install_path: PathBuf::from(p),
version: "17.0".into(),
label: label.to_string(),
})
.collect()
}
pub fn list_vs_msvc_versions(vs_path: &Path) -> Vec<String> {
let msvc_dir = vs_path.join("VC").join("Tools").join("MSVC");
if !msvc_dir.exists() {
return vec![];
}
let mut versions: Vec<String> = std::fs::read_dir(&msvc_dir)
.ok()
.into_iter()
.flatten()
.filter_map(|entry| {
let entry = entry.ok()?;
if entry.file_type().ok()?.is_dir() {
Some(entry.file_name().to_string_lossy().to_string())
} else {
None
}
})
.collect();
versions.sort_by(|a, b| {
let a_parts: Vec<u32> = a.split('.').filter_map(|s| s.parse().ok()).collect();
let b_parts: Vec<u32> = b.split('.').filter_map(|s| s.parse().ok()).collect();
b_parts.cmp(&a_parts)
});
versions
}
pub fn find_msvc_kit_version(source_dir: &Path) -> Option<PathBuf> {
let msvc_dir = source_dir.join("VC").join("Tools").join("MSVC");
if !msvc_dir.exists() {
return None;
}
std::fs::read_dir(&msvc_dir)
.ok()?
.filter_map(|e| e.ok())
.find(|e| e.file_type().ok().is_some_and(|t| t.is_dir()))
.map(|e| e.path())
}
pub fn can_write_to_vs(vs_path: &Path) -> bool {
let candidates = [
vs_path.join("VC").join("Tools").join("MSVC"),
vs_path.join("VC").join("Tools"),
vs_path.to_path_buf(),
];
for candidate in &candidates {
let test_file = candidate.join(".msvc_kit_write_test");
match std::fs::write(&test_file, b"test") {
Ok(_) => {
let _ = std::fs::remove_file(&test_file);
return true;
}
Err(_) => continue,
}
}
false
}
pub fn install_into_vs(
source_dir: &Path,
vs_instance: &VsInstance,
) -> Result<VsInstallResult, String> {
let version_dir = find_msvc_kit_version(source_dir).ok_or_else(|| {
format!(
"No MSVC toolchain found in {}/VC/Tools/MSVC/",
source_dir.display()
)
})?;
let version_name = version_dir
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| "Invalid version directory name".to_string())?;
let target_dir = vs_instance
.install_path
.join("VC")
.join("Tools")
.join("MSVC")
.join(version_name);
if target_dir.exists() {
return Err(format!(
"MSVC {} is already installed at {}",
version_name,
target_dir.display()
));
}
let parent = target_dir
.parent()
.ok_or_else(|| "Invalid target path".to_string())?;
std::fs::create_dir_all(parent)
.map_err(|e| format!("Cannot create {}: {}", parent.display(), e))?;
fn copy_recursively(src: &Path, dst: &Path) -> Result<(), String> {
if src.is_dir() {
std::fs::create_dir_all(dst)
.map_err(|e| format!("Cannot create {}: {}", dst.display(), e))?;
for entry in std::fs::read_dir(src)
.map_err(|e| format!("Cannot read {}: {}", src.display(), e))?
{
let entry = entry.map_err(|e| format!("Entry error: {}", e))?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
copy_recursively(&src_path, &dst_path)?;
}
Ok(())
} else if src.is_file() || src.is_symlink() {
std::fs::copy(src, dst).map_err(|e| {
format!("Cannot copy {} -> {}: {}", src.display(), dst.display(), e)
})?;
Ok(())
} else {
Ok(())
}
}
copy_recursively(&version_dir, &target_dir)?;
let cl_path = target_dir
.join("bin")
.join("Hostx64")
.join("x64")
.join("cl.exe");
if !cl_path.exists() {
let alt_cl = target_dir
.join("bin")
.join("Hostx86")
.join("x64")
.join("cl.exe");
if !alt_cl.exists() {
return Err(format!(
"Toolchain installed but cl.exe not found at expected path. \
Files copied to: {}. Verify manually.",
target_dir.display()
));
}
}
let registered = list_vs_msvc_versions(&vs_instance.install_path);
Ok(VsInstallResult {
vs_instance: vs_instance.clone(),
registered_versions: registered,
installed_source: Some(version_dir),
})
}