Skip to main content

waterui_cli/toolchain/
windows_arm64_llvm.rs

1//! Windows ARM64 LLVM toolchain support for native assembly dependencies.
2
3use 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/// Toolchain for Windows ARM64 LLVM C/ASM build support.
18///
19/// This is required by native Rust dependencies that ship `.S` sources
20/// (for example `aws-lc-sys` and `rav1e`) when building on `aarch64-pc-windows-msvc`.
21#[derive(Debug, Clone, Default)]
22pub struct WindowsArm64LlvmToolchain;
23
24impl WindowsArm64LlvmToolchain {
25    /// Whether this host requires explicit LLVM tooling for native assembly builds.
26    #[must_use]
27    pub const fn required_on_host() -> bool {
28        cfg!(all(target_os = "windows", target_arch = "aarch64"))
29    }
30
31    /// Build target-scoped cargo environment overrides that force LLVM tools
32    /// for Windows ARM64 C/C++/ASM compilation.
33    ///
34    /// Returns an empty list on hosts where this toolchain is not required.
35    ///
36    /// # Errors
37    /// Returns an error if this host requires LLVM tools and they cannot be located.
38    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/// Installation plan for Windows ARM64 LLVM tooling.
89#[derive(Debug, Clone)]
90pub struct WindowsArm64LlvmInstallation;
91
92/// Errors that can occur when installing Windows ARM64 LLVM tooling.
93#[derive(Debug, thiserror::Error)]
94pub enum FailToInstallWindowsArm64Llvm {
95    /// winget is required for automatic installation.
96    #[error(
97        "winget is required for automatic LLVM installation on Windows. Install App Installer and retry."
98    )]
99    WingetNotFound,
100    /// winget installation failed.
101    #[error("Failed to install LLVM via winget: {0}")]
102    WingetInstallFailed(String),
103    /// LLVM package installed but required binaries are still unavailable.
104    #[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 binary list.
110        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}