waterui_cli/toolchain/
msvc.rs1use std::path::{Path, PathBuf};
5
6use waterui_assets_core::{AssetError, download_remote_bytes, write_bytes_atomically};
7
8use crate::toolchain::{Host, Installation, Toolchain, ToolchainError};
9
10const VS_BUILD_TOOLS_URL: &str = "https://aka.ms/vs/17/release/vs_BuildTools.exe";
14
15const VC_TOOLS_COMPONENT: &str = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64";
17
18const EXIT_SUCCESS_REBOOT_REQUIRED: i32 = 0xBC2;
21
22#[derive(Debug, Clone, Copy, Default)]
24pub struct MsvcBuildTools;
25
26impl MsvcBuildTools {
27 async fn vswhere(host: &Host) -> Option<PathBuf> {
30 if let Ok(path) = host.which("vswhere").await {
31 return Some(path);
32 }
33 let program_files_x86 = host.env("ProgramFiles(x86)")?;
34 let path = Path::new(&program_files_x86)
35 .join("Microsoft Visual Studio")
36 .join("Installer")
37 .join("vswhere.exe");
38 path.is_file().then_some(path)
39 }
40
41 async fn vswhere_reports_vc_tools(host: &Host) -> bool {
44 let Some(vswhere) = Self::vswhere(host).await else {
45 return false;
46 };
47 let Ok(output) = host
48 .output(
49 &vswhere,
50 [
51 "-products",
52 "*",
53 "-requires",
54 VC_TOOLS_COMPONENT,
55 "-property",
56 "installationPath",
57 "-latest",
58 ],
59 )
60 .await
61 else {
62 return false;
63 };
64 output.status.success() && !output.stdout.trim_ascii().is_empty()
65 }
66}
67
68impl Toolchain for MsvcBuildTools {
69 type Installation = MsvcBuildToolsInstallation;
70
71 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
72 if host.which("link.exe").await.is_ok() {
73 return Ok(());
74 }
75 if Self::vswhere_reports_vc_tools(host).await {
76 return Ok(());
77 }
78 Err(ToolchainError::fixable(MsvcBuildToolsInstallation))
79 }
80}
81
82#[derive(Debug, Clone, Copy)]
88pub struct MsvcBuildToolsInstallation;
89
90#[derive(Debug, thiserror::Error)]
92pub enum FailToInstallMsvcBuildTools {
93 #[error(transparent)]
95 Asset(#[from] AssetError),
96
97 #[error(transparent)]
99 Io(#[from] std::io::Error),
100
101 #[error(transparent)]
103 Command(#[from] crate::utils::CommandError),
104
105 #[error("Visual Studio Build Tools installer exited with {0}")]
107 InstallerFailed(std::process::ExitStatus),
108
109 #[error("Visual Studio Build Tools can only be installed on Windows")]
111 UnsupportedPlatform,
112}
113
114impl Installation for MsvcBuildToolsInstallation {
115 type Error = FailToInstallMsvcBuildTools;
116
117 fn modifies_system(&self) -> bool {
118 true
119 }
120
121 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
122 if !cfg!(target_os = "windows") {
123 return Err(FailToInstallMsvcBuildTools::UnsupportedPlatform);
124 }
125 let bytes = download_remote_bytes(VS_BUILD_TOOLS_URL).await?;
126 let staging = smol::unblock(tempfile::tempdir).await?;
127 let bootstrapper = staging.path().join("vs_BuildTools.exe");
128 write_bytes_atomically(&bootstrapper, &bytes).await?;
129 let output = host
130 .output(
131 &bootstrapper,
132 [
133 "--quiet",
134 "--wait",
135 "--norestart",
136 "--add",
137 "Microsoft.VisualStudio.Workload.VCTools",
138 "--includeRecommended",
139 ],
140 )
141 .await?;
142 if output.status.success() || output.status.code() == Some(EXIT_SUCCESS_REBOOT_REQUIRED) {
143 return Ok(());
144 }
145 Err(FailToInstallMsvcBuildTools::InstallerFailed(output.status))
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use std::path::Path;
152
153 use super::MsvcBuildTools;
154 use crate::toolchain::testing::TestMachine;
155 use crate::toolchain::{Toolchain, ToolchainError};
156
157 #[test]
158 fn missing_reports_fixable() {
159 let machine = TestMachine::new();
160 let host = machine.host(Vec::<(String, String)>::new());
161 let result = smol::block_on(MsvcBuildTools.check(&host));
162 assert!(
163 matches!(result, Err(ToolchainError::Fixable(_))),
164 "a host without MSVC build tools must report fixable: {result:?}"
165 );
166 }
167
168 #[test]
169 fn ok_when_link_exe_on_path() {
170 let machine = TestMachine::new();
171 machine.executable(Path::new("bin").join("link.exe"));
175 let host = machine.host(Vec::<(String, String)>::new());
176 smol::block_on(MsvcBuildTools.check(&host)).expect("link.exe on PATH must be ok");
177 }
178
179 #[test]
182 fn ok_when_vswhere_reports_vc_tools() {
183 let machine = TestMachine::new();
184 machine.install("vswhere");
185 machine.respond(
186 "VSWHERE",
187 "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools",
188 );
189 let host = machine.host(Vec::<(String, String)>::new());
190 smol::block_on(MsvcBuildTools.check(&host))
191 .expect("vswhere reporting a VC.Tools install must be ok");
192 }
193
194 #[test]
198 #[cfg(unix)]
199 fn ok_when_vswhere_at_installer_path_reports_vc_tools() {
200 let machine = TestMachine::new();
201 let program_files = machine.dir("Program Files (x86)");
202 machine.executable(
203 Path::new("Program Files (x86)")
204 .join("Microsoft Visual Studio")
205 .join("Installer")
206 .join("vswhere.exe"),
207 );
208 machine.respond(
209 "VSWHERE",
210 "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools",
211 );
212 let host = machine.host([("ProgramFiles(x86)", program_files.as_os_str())]);
213 smol::block_on(MsvcBuildTools.check(&host))
214 .expect("a vswhere at the installer path reporting VC.Tools must be ok");
215 }
216
217 #[test]
218 fn missing_when_vswhere_finds_nothing() {
219 let machine = TestMachine::new();
220 machine.install("vswhere");
221 let host = machine.host(Vec::<(String, String)>::new());
223 let result = smol::block_on(MsvcBuildTools.check(&host));
224 assert!(
225 matches!(result, Err(ToolchainError::Fixable(_))),
226 "vswhere with no VC.Tools install must report fixable: {result:?}"
227 );
228 }
229}