1use std::ffi::OsString;
4use std::path::PathBuf;
5
6use crate::toolchain::{
7 Host, Installation, Toolchain, ToolchainError,
8 winget::{WingetInstallError, ensure_package_installed},
9};
10
11const LLVM_WINGET_PACKAGE_ID: &str = "LLVM.LLVM";
12const DEFAULT_CLANG_CL_PATH: &str = r"C:\Program Files\LLVM\bin\clang-cl.exe";
13const DEFAULT_LLVM_LIB_PATH: &str = r"C:\Program Files\LLVM\bin\llvm-lib.exe";
14const TARGET_UNDERSCORE: &str = "aarch64_pc_windows_msvc";
15const TARGET_DASHED: &str = "aarch64-pc-windows-msvc";
16
17#[derive(Debug, Clone, Default)]
22pub struct WindowsArm64LlvmToolchain;
23
24impl WindowsArm64LlvmToolchain {
25 #[must_use]
27 pub const fn required_on_host() -> bool {
28 cfg!(all(target_os = "windows", target_arch = "aarch64"))
29 }
30
31 pub async fn cargo_envs(
39 &self,
40 host: &Host,
41 ) -> Result<Vec<(String, OsString)>, ToolchainError<WindowsArm64LlvmInstallation>> {
42 if !Self::required_on_host() {
43 return Ok(Vec::new());
44 }
45
46 let tools = ensure_llvm_tools_available(host).await?;
47 Ok(vec![
48 (
49 format!("CC_{TARGET_UNDERSCORE}"),
50 tools.clang_cl.clone().into_os_string(),
51 ),
52 (
53 format!("CXX_{TARGET_UNDERSCORE}"),
54 tools.clang_cl.clone().into_os_string(),
55 ),
56 (
57 format!("AR_{TARGET_UNDERSCORE}"),
58 tools.llvm_lib.clone().into_os_string(),
59 ),
60 (
61 format!("CC_{TARGET_DASHED}"),
62 tools.clang_cl.clone().into_os_string(),
63 ),
64 (
65 format!("CXX_{TARGET_DASHED}"),
66 tools.clang_cl.clone().into_os_string(),
67 ),
68 (
69 format!("AR_{TARGET_DASHED}"),
70 tools.llvm_lib.into_os_string(),
71 ),
72 ])
73 }
74}
75
76impl Toolchain for WindowsArm64LlvmToolchain {
77 type Installation = WindowsArm64LlvmInstallation;
78
79 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
80 if !Self::required_on_host() {
81 return Ok(());
82 }
83
84 ensure_llvm_tools_available(host).await.map(|_| ())
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct WindowsArm64LlvmInstallation;
91
92#[derive(Debug, thiserror::Error)]
94pub enum FailToInstallWindowsArm64Llvm {
95 #[error(
97 "winget is required for automatic LLVM installation on Windows. Install App Installer and retry."
98 )]
99 WingetNotFound,
100 #[error("Failed to install LLVM via winget: {0}")]
102 WingetInstallFailed(String),
103 #[error(
105 "LLVM was installed, but required binaries are still missing ({missing}). Ensure `{}` is accessible and restart shell/terminal.",
106 DEFAULT_CLANG_CL_PATH
107 )]
108 ToolsNotDetected {
109 missing: String,
111 },
112}
113
114impl Installation for WindowsArm64LlvmInstallation {
115 type Error = FailToInstallWindowsArm64Llvm;
116
117 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
118 ensure_package_installed(host, LLVM_WINGET_PACKAGE_ID)
119 .await
120 .map_err(map_winget_error_for_windows_arm64_llvm)?;
121
122 let tools = resolve_llvm_tools(host).await;
123 if tools.is_complete() {
124 Ok(())
125 } else {
126 Err(FailToInstallWindowsArm64Llvm::ToolsNotDetected {
127 missing: tools.missing_components().join(", "),
128 })
129 }
130 }
131}
132
133#[derive(Debug, Clone)]
134struct CompleteLlvmTools {
135 clang_cl: PathBuf,
136 llvm_lib: PathBuf,
137}
138
139#[derive(Debug, Clone, Default)]
140struct ResolvedLlvmTools {
141 clang_cl: Option<PathBuf>,
142 llvm_lib: Option<PathBuf>,
143}
144
145impl ResolvedLlvmTools {
146 const fn is_complete(&self) -> bool {
147 self.clang_cl.is_some() && self.llvm_lib.is_some()
148 }
149
150 fn missing_components(&self) -> Vec<&'static str> {
151 let mut missing = Vec::new();
152 if self.clang_cl.is_none() {
153 missing.push("clang-cl");
154 }
155 if self.llvm_lib.is_none() {
156 missing.push("llvm-lib");
157 }
158 missing
159 }
160
161 fn into_complete(self) -> Option<CompleteLlvmTools> {
162 Some(CompleteLlvmTools {
163 clang_cl: self.clang_cl?,
164 llvm_lib: self.llvm_lib?,
165 })
166 }
167}
168
169async fn ensure_llvm_tools_available(
170 host: &Host,
171) -> Result<CompleteLlvmTools, ToolchainError<WindowsArm64LlvmInstallation>> {
172 let resolved = resolve_llvm_tools(host).await;
173 if let Some(complete) = resolved.clone().into_complete() {
174 return Ok(complete);
175 }
176
177 if host.which("winget").await.is_ok() {
178 Err(ToolchainError::fixable(WindowsArm64LlvmInstallation))
179 } else {
180 let missing = resolved.missing_components().join(", ");
181 Err(ToolchainError::unfixable(
182 format!("Windows ARM64 LLVM tooling is missing: {missing}"),
183 format!(
184 "Install Microsoft App Installer to enable `winget`, or install LLVM manually and ensure both `{DEFAULT_CLANG_CL_PATH}` and `{DEFAULT_LLVM_LIB_PATH}` are available."
185 ),
186 ))
187 }
188}
189
190async fn resolve_llvm_tools(host: &Host) -> ResolvedLlvmTools {
191 let clang_cl = find_executable(host, "clang-cl", DEFAULT_CLANG_CL_PATH).await;
192 let llvm_lib = find_executable(host, "llvm-lib", DEFAULT_LLVM_LIB_PATH).await;
193 ResolvedLlvmTools { clang_cl, llvm_lib }
194}
195
196async fn find_executable(
197 host: &Host,
198 binary_name: &'static str,
199 fallback_path: &'static str,
200) -> Option<PathBuf> {
201 if let Ok(path) = host.which(binary_name).await {
202 return Some(path);
203 }
204
205 let fallback = PathBuf::from(fallback_path);
206 if fallback.exists() {
207 Some(fallback)
208 } else {
209 None
210 }
211}
212
213fn map_winget_error_for_windows_arm64_llvm(
214 error: WingetInstallError,
215) -> FailToInstallWindowsArm64Llvm {
216 match error {
217 WingetInstallError::WingetNotFound => FailToInstallWindowsArm64Llvm::WingetNotFound,
218 WingetInstallError::CommandFailed(err) => {
219 FailToInstallWindowsArm64Llvm::WingetInstallFailed(err.to_string())
220 }
221 WingetInstallError::NotInstalled { package_id } => {
222 FailToInstallWindowsArm64Llvm::WingetInstallFailed(format!(
223 "Package `{package_id}` is still missing after winget install; verify winget sources and retry."
224 ))
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::{
232 FailToInstallWindowsArm64Llvm, ResolvedLlvmTools, map_winget_error_for_windows_arm64_llvm,
233 };
234 use crate::toolchain::winget::WingetInstallError;
235
236 #[test]
237 fn maps_winget_not_found_to_specific_error() {
238 let mapped = map_winget_error_for_windows_arm64_llvm(WingetInstallError::WingetNotFound);
239 assert!(matches!(
240 mapped,
241 FailToInstallWindowsArm64Llvm::WingetNotFound
242 ));
243 }
244
245 #[test]
246 fn maps_not_installed_error_with_package_context() {
247 let mapped = map_winget_error_for_windows_arm64_llvm(WingetInstallError::NotInstalled {
248 package_id: "LLVM.LLVM",
249 });
250 let message = mapped.to_string();
251 assert!(message.contains("LLVM.LLVM"));
252 assert!(message.contains("still missing"));
253 }
254
255 #[test]
256 fn missing_components_reports_expected_tools() {
257 let missing_both = ResolvedLlvmTools::default();
258 assert_eq!(
259 missing_both.missing_components(),
260 vec!["clang-cl", "llvm-lib"]
261 );
262
263 let missing_llvm_lib = ResolvedLlvmTools {
264 clang_cl: Some("clang-cl".into()),
265 llvm_lib: None,
266 };
267 assert_eq!(missing_llvm_lib.missing_components(), vec!["llvm-lib"]);
268 }
269}
270
271#[cfg(test)]
272mod host_tests {
273 use super::WindowsArm64LlvmToolchain;
274 use crate::toolchain::Toolchain;
275 #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
276 use crate::toolchain::ToolchainError;
277 use crate::toolchain::testing::TestMachine;
278
279 #[test]
280 #[cfg(not(all(target_os = "windows", target_arch = "aarch64")))]
281 fn not_required_outside_windows_arm64() {
282 let machine = TestMachine::new();
283 let host = machine.host(Vec::<(String, String)>::new());
284 smol::block_on(WindowsArm64LlvmToolchain.check(&host))
285 .expect("LLVM tooling is only required on Windows ARM64");
286 let envs = smol::block_on(WindowsArm64LlvmToolchain.cargo_envs(&host))
287 .expect("cargo envs off Windows ARM64 must not probe tools");
288 assert!(envs.is_empty());
289 }
290
291 #[test]
292 #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
293 fn ok_when_llvm_tools_on_path() {
294 let machine = TestMachine::new();
295 machine.install("clang-cl");
296 machine.install("llvm-lib");
297 let host = machine.host(Vec::<(String, String)>::new());
298 smol::block_on(WindowsArm64LlvmToolchain.check(&host))
299 .expect("clang-cl and llvm-lib on PATH must satisfy the check");
300 }
301
302 #[test]
303 #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
304 fn missing_tools_classify_by_winget_presence() {
305 let machine = TestMachine::new();
306 let host = machine.host(Vec::<(String, String)>::new());
307 let result = smol::block_on(WindowsArm64LlvmToolchain.check(&host));
308 assert!(
309 matches!(result, Err(ToolchainError::Unfixable(_))),
310 "missing LLVM tools without winget must be unfixable: {result:?}"
311 );
312 machine.install("winget");
313 let host = machine.host(Vec::<(String, String)>::new());
314 let result = smol::block_on(WindowsArm64LlvmToolchain.check(&host));
315 assert!(
316 matches!(result, Err(ToolchainError::Fixable(_))),
317 "missing LLVM tools with winget must be fixable: {result:?}"
318 );
319 }
320}