use std::path::{Path, PathBuf};
use waterui_assets_core::{AssetError, download_remote_bytes, write_bytes_atomically};
use crate::toolchain::{Host, Installation, Toolchain, ToolchainError};
const VS_BUILD_TOOLS_URL: &str = "https://aka.ms/vs/17/release/vs_BuildTools.exe";
const VC_TOOLS_COMPONENT: &str = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64";
const EXIT_SUCCESS_REBOOT_REQUIRED: i32 = 0xBC2;
#[derive(Debug, Clone, Copy, Default)]
pub struct MsvcBuildTools;
impl MsvcBuildTools {
async fn vswhere(host: &Host) -> Option<PathBuf> {
if let Ok(path) = host.which("vswhere").await {
return Some(path);
}
let program_files_x86 = host.env("ProgramFiles(x86)")?;
let path = Path::new(&program_files_x86)
.join("Microsoft Visual Studio")
.join("Installer")
.join("vswhere.exe");
path.is_file().then_some(path)
}
async fn vswhere_reports_vc_tools(host: &Host) -> bool {
let Some(vswhere) = Self::vswhere(host).await else {
return false;
};
let Ok(output) = host
.output(
&vswhere,
[
"-products",
"*",
"-requires",
VC_TOOLS_COMPONENT,
"-property",
"installationPath",
"-latest",
],
)
.await
else {
return false;
};
output.status.success() && !output.stdout.trim_ascii().is_empty()
}
}
impl Toolchain for MsvcBuildTools {
type Installation = MsvcBuildToolsInstallation;
async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
if host.which("link.exe").await.is_ok() {
return Ok(());
}
if Self::vswhere_reports_vc_tools(host).await {
return Ok(());
}
Err(ToolchainError::fixable(MsvcBuildToolsInstallation))
}
}
#[derive(Debug, Clone, Copy)]
pub struct MsvcBuildToolsInstallation;
#[derive(Debug, thiserror::Error)]
pub enum FailToInstallMsvcBuildTools {
#[error(transparent)]
Asset(#[from] AssetError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Command(#[from] crate::utils::CommandError),
#[error("Visual Studio Build Tools installer exited with {0}")]
InstallerFailed(std::process::ExitStatus),
#[error("Visual Studio Build Tools can only be installed on Windows")]
UnsupportedPlatform,
}
impl Installation for MsvcBuildToolsInstallation {
type Error = FailToInstallMsvcBuildTools;
fn modifies_system(&self) -> bool {
true
}
async fn install(&self, host: &Host) -> Result<(), Self::Error> {
if !cfg!(target_os = "windows") {
return Err(FailToInstallMsvcBuildTools::UnsupportedPlatform);
}
let bytes = download_remote_bytes(VS_BUILD_TOOLS_URL).await?;
let staging = smol::unblock(tempfile::tempdir).await?;
let bootstrapper = staging.path().join("vs_BuildTools.exe");
write_bytes_atomically(&bootstrapper, &bytes).await?;
let output = host
.output(
&bootstrapper,
[
"--quiet",
"--wait",
"--norestart",
"--add",
"Microsoft.VisualStudio.Workload.VCTools",
"--includeRecommended",
],
)
.await?;
if output.status.success() || output.status.code() == Some(EXIT_SUCCESS_REBOOT_REQUIRED) {
return Ok(());
}
Err(FailToInstallMsvcBuildTools::InstallerFailed(output.status))
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::MsvcBuildTools;
use crate::toolchain::testing::TestMachine;
use crate::toolchain::{Toolchain, ToolchainError};
#[test]
fn missing_reports_fixable() {
let machine = TestMachine::new();
let host = machine.host(Vec::<(String, String)>::new());
let result = smol::block_on(MsvcBuildTools.check(&host));
assert!(
matches!(result, Err(ToolchainError::Fixable(_))),
"a host without MSVC build tools must report fixable: {result:?}"
);
}
#[test]
fn ok_when_link_exe_on_path() {
let machine = TestMachine::new();
machine.executable(Path::new("bin").join("link.exe"));
let host = machine.host(Vec::<(String, String)>::new());
smol::block_on(MsvcBuildTools.check(&host)).expect("link.exe on PATH must be ok");
}
#[test]
fn ok_when_vswhere_reports_vc_tools() {
let machine = TestMachine::new();
machine.install("vswhere");
machine.respond(
"VSWHERE",
"C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools",
);
let host = machine.host(Vec::<(String, String)>::new());
smol::block_on(MsvcBuildTools.check(&host))
.expect("vswhere reporting a VC.Tools install must be ok");
}
#[test]
#[cfg(unix)]
fn ok_when_vswhere_at_installer_path_reports_vc_tools() {
let machine = TestMachine::new();
let program_files = machine.dir("Program Files (x86)");
machine.executable(
Path::new("Program Files (x86)")
.join("Microsoft Visual Studio")
.join("Installer")
.join("vswhere.exe"),
);
machine.respond(
"VSWHERE",
"C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools",
);
let host = machine.host([("ProgramFiles(x86)", program_files.as_os_str())]);
smol::block_on(MsvcBuildTools.check(&host))
.expect("a vswhere at the installer path reporting VC.Tools must be ok");
}
#[test]
fn missing_when_vswhere_finds_nothing() {
let machine = TestMachine::new();
machine.install("vswhere");
let host = machine.host(Vec::<(String, String)>::new());
let result = smol::block_on(MsvcBuildTools.check(&host));
assert!(
matches!(result, Err(ToolchainError::Fixable(_))),
"vswhere with no VC.Tools install must report fixable: {result:?}"
);
}
}