1use std::ffi::OsString;
4use std::path::PathBuf;
5
6use crate::toolchain::{
7 Host, Installation, Toolchain, ToolchainError,
8 managed_tool::{LLVM_ARM64_MSI_SHA256, LLVM_ARM64_MSI_URL, ManagedToolError, fetch_pinned},
9 winget::{WingetInstallError, ensure_package_installed},
10};
11
12const LLVM_WINGET_PACKAGE_ID: &str = "LLVM.LLVM";
13const DEFAULT_CLANG_CL_PATH: &str = r"C:\Program Files\LLVM\bin\clang-cl.exe";
14const DEFAULT_LLVM_LIB_PATH: &str = r"C:\Program Files\LLVM\bin\llvm-lib.exe";
15const TARGET_UNDERSCORE: &str = "aarch64_pc_windows_msvc";
16const TARGET_DASHED: &str = "aarch64-pc-windows-msvc";
17
18#[derive(Debug, Clone, Default)]
23pub struct WindowsArm64LlvmToolchain;
24
25impl WindowsArm64LlvmToolchain {
26 #[must_use]
28 pub const fn required_on_host() -> bool {
29 cfg!(all(target_os = "windows", target_arch = "aarch64"))
30 }
31
32 pub async fn cargo_envs(
40 &self,
41 host: &Host,
42 ) -> Result<Vec<(String, OsString)>, ToolchainError<WindowsArm64LlvmInstallation>> {
43 if !Self::required_on_host() {
44 return Ok(Vec::new());
45 }
46
47 let tools = ensure_llvm_tools_available(host).await?;
48 Ok(vec![
49 (
50 format!("CC_{TARGET_UNDERSCORE}"),
51 tools.clang_cl.clone().into_os_string(),
52 ),
53 (
54 format!("CXX_{TARGET_UNDERSCORE}"),
55 tools.clang_cl.clone().into_os_string(),
56 ),
57 (
58 format!("AR_{TARGET_UNDERSCORE}"),
59 tools.llvm_lib.clone().into_os_string(),
60 ),
61 (
62 format!("CC_{TARGET_DASHED}"),
63 tools.clang_cl.clone().into_os_string(),
64 ),
65 (
66 format!("CXX_{TARGET_DASHED}"),
67 tools.clang_cl.clone().into_os_string(),
68 ),
69 (
70 format!("AR_{TARGET_DASHED}"),
71 tools.llvm_lib.into_os_string(),
72 ),
73 ])
74 }
75}
76
77impl Toolchain for WindowsArm64LlvmToolchain {
78 type Installation = WindowsArm64LlvmInstallation;
79
80 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
81 if !Self::required_on_host() {
82 return Ok(());
83 }
84
85 ensure_llvm_tools_available(host).await.map(|_| ())
86 }
87}
88
89#[derive(Debug, Clone)]
92pub enum WindowsArm64LlvmInstallation {
93 Winget,
95 Msi,
99}
100
101#[derive(Debug, thiserror::Error)]
103pub enum FailToInstallWindowsArm64Llvm {
104 #[error(
106 "winget is required for automatic LLVM installation on Windows. Install App Installer and retry."
107 )]
108 WingetNotFound,
109 #[error("Failed to install LLVM via winget: {0}")]
111 WingetInstallFailed(String),
112 #[error(transparent)]
114 Managed(#[from] ManagedToolError),
115 #[error(transparent)]
117 Command(#[from] crate::utils::CommandError),
118 #[error(transparent)]
120 Io(#[from] std::io::Error),
121 #[error("`msiexec` for the LLVM installer exited with {0}")]
123 MsiexecFailed(std::process::ExitStatus),
124 #[error(
126 "LLVM was installed, but required binaries are still missing ({missing}). Ensure `{}` is accessible and restart shell/terminal.",
127 DEFAULT_CLANG_CL_PATH
128 )]
129 ToolsNotDetected {
130 missing: String,
132 },
133}
134
135impl Installation for WindowsArm64LlvmInstallation {
136 type Error = FailToInstallWindowsArm64Llvm;
137
138 fn modifies_system(&self) -> bool {
141 matches!(self, Self::Msi)
142 }
143
144 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
145 match self {
146 Self::Winget => {
147 ensure_package_installed(host, LLVM_WINGET_PACKAGE_ID)
148 .await
149 .map_err(map_winget_error_for_windows_arm64_llvm)?;
150 }
151 Self::Msi => {
152 use std::ffi::OsStr;
153 let staging = smol::unblock(tempfile::tempdir).await?;
154 let msi = staging.path().join("LLVM-23.1.1-woa64.msi");
155 fetch_pinned(LLVM_ARM64_MSI_URL, LLVM_ARM64_MSI_SHA256, &msi).await?;
156 let output = host
157 .output(
158 "msiexec",
159 [
160 OsStr::new("/i"),
161 msi.as_os_str(),
162 OsStr::new("/quiet"),
163 OsStr::new("/norestart"),
164 ],
165 )
166 .await?;
167 if !output.status.success() {
168 return Err(FailToInstallWindowsArm64Llvm::MsiexecFailed(output.status));
169 }
170 }
171 }
172
173 let tools = resolve_llvm_tools(host).await;
174 if tools.is_complete() {
175 Ok(())
176 } else {
177 Err(FailToInstallWindowsArm64Llvm::ToolsNotDetected {
178 missing: tools.missing_components().join(", "),
179 })
180 }
181 }
182}
183
184#[derive(Debug, Clone)]
185struct CompleteLlvmTools {
186 clang_cl: PathBuf,
187 llvm_lib: PathBuf,
188}
189
190#[derive(Debug, Clone, Default)]
191struct ResolvedLlvmTools {
192 clang_cl: Option<PathBuf>,
193 llvm_lib: Option<PathBuf>,
194}
195
196impl ResolvedLlvmTools {
197 const fn is_complete(&self) -> bool {
198 self.clang_cl.is_some() && self.llvm_lib.is_some()
199 }
200
201 fn missing_components(&self) -> Vec<&'static str> {
202 let mut missing = Vec::new();
203 if self.clang_cl.is_none() {
204 missing.push("clang-cl");
205 }
206 if self.llvm_lib.is_none() {
207 missing.push("llvm-lib");
208 }
209 missing
210 }
211
212 fn into_complete(self) -> Option<CompleteLlvmTools> {
213 Some(CompleteLlvmTools {
214 clang_cl: self.clang_cl?,
215 llvm_lib: self.llvm_lib?,
216 })
217 }
218}
219
220async fn ensure_llvm_tools_available(
221 host: &Host,
222) -> Result<CompleteLlvmTools, ToolchainError<WindowsArm64LlvmInstallation>> {
223 let resolved = resolve_llvm_tools(host).await;
224 if let Some(complete) = resolved.clone().into_complete() {
225 return Ok(complete);
226 }
227
228 if host.which("winget").await.is_ok() {
229 Err(ToolchainError::fixable(
230 WindowsArm64LlvmInstallation::Winget,
231 ))
232 } else {
233 Err(ToolchainError::fixable(WindowsArm64LlvmInstallation::Msi))
236 }
237}
238
239async fn resolve_llvm_tools(host: &Host) -> ResolvedLlvmTools {
240 let clang_cl = find_executable(host, "clang-cl", DEFAULT_CLANG_CL_PATH).await;
241 let llvm_lib = find_executable(host, "llvm-lib", DEFAULT_LLVM_LIB_PATH).await;
242 ResolvedLlvmTools { clang_cl, llvm_lib }
243}
244
245async fn find_executable(
246 host: &Host,
247 binary_name: &'static str,
248 fallback_path: &'static str,
249) -> Option<PathBuf> {
250 if let Ok(path) = host.which(binary_name).await {
251 return Some(path);
252 }
253
254 let fallback = PathBuf::from(fallback_path);
255 if fallback.exists() {
256 Some(fallback)
257 } else {
258 None
259 }
260}
261
262fn map_winget_error_for_windows_arm64_llvm(
263 error: WingetInstallError,
264) -> FailToInstallWindowsArm64Llvm {
265 match error {
266 WingetInstallError::WingetNotFound => FailToInstallWindowsArm64Llvm::WingetNotFound,
267 WingetInstallError::CommandFailed(err) => {
268 FailToInstallWindowsArm64Llvm::WingetInstallFailed(err.to_string())
269 }
270 WingetInstallError::NotInstalled { package_id } => {
271 FailToInstallWindowsArm64Llvm::WingetInstallFailed(format!(
272 "Package `{package_id}` is still missing after winget install; verify winget sources and retry."
273 ))
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::{
281 FailToInstallWindowsArm64Llvm, ResolvedLlvmTools, map_winget_error_for_windows_arm64_llvm,
282 };
283 use crate::toolchain::winget::WingetInstallError;
284
285 #[test]
286 fn maps_winget_not_found_to_specific_error() {
287 let mapped = map_winget_error_for_windows_arm64_llvm(WingetInstallError::WingetNotFound);
288 assert!(matches!(
289 mapped,
290 FailToInstallWindowsArm64Llvm::WingetNotFound
291 ));
292 }
293
294 #[test]
295 fn maps_not_installed_error_with_package_context() {
296 let mapped = map_winget_error_for_windows_arm64_llvm(WingetInstallError::NotInstalled {
297 package_id: "LLVM.LLVM",
298 });
299 let message = mapped.to_string();
300 assert!(message.contains("LLVM.LLVM"));
301 assert!(message.contains("still missing"));
302 }
303
304 #[test]
305 fn missing_components_reports_expected_tools() {
306 let missing_both = ResolvedLlvmTools::default();
307 assert_eq!(
308 missing_both.missing_components(),
309 vec!["clang-cl", "llvm-lib"]
310 );
311
312 let missing_llvm_lib = ResolvedLlvmTools {
313 clang_cl: Some("clang-cl".into()),
314 llvm_lib: None,
315 };
316 assert_eq!(missing_llvm_lib.missing_components(), vec!["llvm-lib"]);
317 }
318}
319
320#[cfg(test)]
321mod host_tests {
322 use super::WindowsArm64LlvmToolchain;
323 use crate::toolchain::Toolchain;
324 #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
325 use crate::toolchain::ToolchainError;
326 use crate::toolchain::testing::TestMachine;
327
328 #[test]
329 #[cfg(not(all(target_os = "windows", target_arch = "aarch64")))]
330 fn not_required_outside_windows_arm64() {
331 let machine = TestMachine::new();
332 let host = machine.host(Vec::<(String, String)>::new());
333 smol::block_on(WindowsArm64LlvmToolchain.check(&host))
334 .expect("LLVM tooling is only required on Windows ARM64");
335 let envs = smol::block_on(WindowsArm64LlvmToolchain.cargo_envs(&host))
336 .expect("cargo envs off Windows ARM64 must not probe tools");
337 assert!(envs.is_empty());
338 }
339
340 #[test]
341 #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
342 fn ok_when_llvm_tools_on_path() {
343 let machine = TestMachine::new();
344 machine.install("clang-cl");
345 machine.install("llvm-lib");
346 let host = machine.host(Vec::<(String, String)>::new());
347 smol::block_on(WindowsArm64LlvmToolchain.check(&host))
348 .expect("clang-cl and llvm-lib on PATH must satisfy the check");
349 }
350
351 #[test]
352 #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
353 fn missing_tools_classify_by_winget_presence() {
354 let machine = TestMachine::new();
355 let host = machine.host(Vec::<(String, String)>::new());
356 let result = smol::block_on(WindowsArm64LlvmToolchain.check(&host));
357 assert!(
358 matches!(
359 result,
360 Err(ToolchainError::Fixable(
361 super::WindowsArm64LlvmInstallation::Msi
362 ))
363 ),
364 "missing LLVM tools without winget falls back to the pinned MSI: {result:?}"
365 );
366 machine.install("winget");
367 let host = machine.host(Vec::<(String, String)>::new());
368 let result = smol::block_on(WindowsArm64LlvmToolchain.check(&host));
369 assert!(
370 matches!(
371 result,
372 Err(ToolchainError::Fixable(
373 super::WindowsArm64LlvmInstallation::Winget
374 ))
375 ),
376 "missing LLVM tools with winget must stay on winget: {result:?}"
377 );
378 }
379}