waterui_cli/android/
toolchain.rs

1use std::{env, path::PathBuf};
2
3use crate::{
4    toolchain::{Installation, Toolchain, cmake::Cmake},
5    utils::which,
6};
7
8/// Complete Android toolchain including SDK, NDK, and `CMake`.
9pub type AndroidToolchain = (AndroidSdk, AndroidNdk, Cmake);
10
11/// Android SDK toolchain component.
12#[derive(Debug, Clone, Default)]
13pub struct AndroidSdk;
14
15impl AndroidSdk {
16    /// Detect the path to the Android SDK installation.
17    #[must_use]
18    pub fn detect_path() -> Option<PathBuf> {
19        // Check ANDROID_HOME environment variable
20        if let Ok(android_home) = env::var("ANDROID_HOME") {
21            let sdk_path = PathBuf::from(android_home);
22            if sdk_path.exists() {
23                return Some(sdk_path);
24            }
25        }
26
27        if cfg!(target_os = "macos") {
28            let home_sdk = PathBuf::from(env::var("HOME").ok()?).join("Library/Android/sdk");
29            if home_sdk.exists() {
30                return Some(home_sdk);
31            }
32        }
33
34        if cfg!(target_os = "linux") {
35            let home_sdk = PathBuf::from(env::var("HOME").ok()?).join("Android/Sdk");
36            if home_sdk.exists() {
37                return Some(home_sdk);
38            }
39        }
40
41        if cfg!(target_os = "windows") {
42            if let Ok(localappdata) = env::var("LOCALAPPDATA") {
43                let sdk_path = PathBuf::from(localappdata).join("Android/Sdk");
44                if sdk_path.exists() {
45                    return Some(sdk_path);
46                }
47            }
48        }
49
50        None
51    }
52
53    /// Get the path to the `adb` executable.
54    #[must_use]
55    pub fn adb_path() -> Option<PathBuf> {
56        let sdk_path = Self::detect_path()?;
57        let adb = sdk_path
58            .join("platform-tools")
59            .join(if cfg!(target_os = "windows") {
60                "adb.exe"
61            } else {
62                "adb"
63            });
64        if adb.exists() { Some(adb) } else { None }
65    }
66
67    /// Get the path to the `emulator` executable.
68    #[must_use]
69    pub fn emulator_path() -> Option<PathBuf> {
70        let sdk_path = Self::detect_path()?;
71        let emulator = sdk_path
72            .join("emulator")
73            .join(if cfg!(target_os = "windows") {
74                "emulator.exe"
75            } else {
76                "emulator"
77            });
78        if emulator.exists() {
79            Some(emulator)
80        } else {
81            None
82        }
83    }
84}
85
86/// Installation procedure for the Android SDK.
87#[derive(Debug, Clone, Default)]
88pub struct AndroidSdkInstallation;
89
90/// An Android NDK toolchain component.
91#[derive(Debug, Clone, Default)]
92pub struct AndroidNdk;
93
94/// Java toolchain component for Android development.
95#[derive(Debug)]
96pub struct Java;
97
98impl Java {
99    /// Detect the path to the Java installation.
100    pub async fn detect_path() -> Option<PathBuf> {
101        // Check JAVA_HOME first
102        if let Ok(home) = env::var("JAVA_HOME") {
103            let java_path = PathBuf::from(home).join("bin/java");
104            if java_path.exists() {
105                return Some(java_path);
106            }
107        }
108
109        // Check Android Studio's bundled JBR on macOS
110        if cfg!(target_os = "macos") {
111            const ANDROID_STUDIO_JBRS: &[&str] = &[
112                "/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java",
113                "/Applications/Android Studio Preview.app/Contents/jbr/Contents/Home/bin/java",
114            ];
115            for path in ANDROID_STUDIO_JBRS {
116                let java_path = PathBuf::from(path);
117                if java_path.exists() {
118                    return Some(java_path);
119                }
120            }
121        }
122
123        // Check PATH
124        which("java").await.ok()
125    }
126}
127
128impl AndroidNdk {
129    /// Detect the Android NDK path from environment variables or standard locations.
130    #[must_use]
131    pub fn detect_path() -> Option<PathBuf> {
132        // Check ANDROID_NDK_ROOT environment variable
133        if let Ok(ndk_root) = env::var("ANDROID_NDK_ROOT") {
134            let ndk_path = PathBuf::from(ndk_root);
135            if ndk_path.exists() {
136                return Some(ndk_path);
137            }
138        }
139
140        // Check ANDROID_NDK_HOME environment variable
141        if let Ok(ndk_home) = env::var("ANDROID_NDK_HOME") {
142            let ndk_path = PathBuf::from(ndk_home);
143            if ndk_path.exists() {
144                return Some(ndk_path);
145            }
146        }
147
148        // Check in the Android SDK path
149        let sdk_path = AndroidSdk::detect_path()?;
150
151        let ndk_dir = sdk_path.join("ndk");
152        if ndk_dir.exists() {
153            // Find the latest NDK version
154            if let Ok(entries) = std::fs::read_dir(&ndk_dir) {
155                let mut versions: Vec<PathBuf> = entries
156                    .filter_map(std::result::Result::ok)
157                    .map(|e| e.path())
158                    .filter(|p| p.is_dir())
159                    .collect();
160
161                versions.sort();
162                if let Some(latest) = versions.last() {
163                    return Some(latest.clone());
164                }
165            }
166        }
167
168        None
169    }
170}
171
172/// Errors that can occur when installing the Android SDK.
173#[derive(Debug, thiserror::Error)]
174pub enum FailToInstallAndroidSdk {}
175
176impl Toolchain for AndroidSdk {
177    type Installation = AndroidSdkInstallation;
178
179    async fn check(&self) -> Result<(), crate::toolchain::ToolchainError<Self::Installation>> {
180        use crate::toolchain::ToolchainError;
181
182        if Self::detect_path().is_none() {
183            return Err(ToolchainError::unfixable(
184                "Android SDK not found",
185                "Install Android Studio from https://developer.android.com/studio \
186                 or set ANDROID_HOME environment variable to your SDK path.",
187            ));
188        }
189
190        // Check for adb executable
191        if Self::adb_path().is_none() {
192            return Err(ToolchainError::unfixable(
193                "Android SDK platform-tools not found",
194                "Open Android Studio -> SDK Manager -> SDK Tools -> check 'Android SDK Platform-Tools'",
195            ));
196        }
197
198        Ok(())
199    }
200}
201
202impl Installation for AndroidSdkInstallation {
203    type Error = FailToInstallAndroidSdk;
204
205    async fn install(&self) -> Result<(), Self::Error> {
206        // Android SDK installation is complex and platform-specific
207        // We guide the user to install it manually via Android Studio
208        // This is intentionally a no-op as the check returns unfixable errors
209        Ok(())
210    }
211}
212
213/// Android NDK installation handler.
214#[derive(Debug)]
215pub struct AndroidNdkInstallation;
216
217impl Toolchain for AndroidNdk {
218    type Installation = AndroidNdkInstallation;
219
220    async fn check(&self) -> Result<(), crate::toolchain::ToolchainError<Self::Installation>> {
221        use crate::toolchain::ToolchainError;
222
223        let ndk_path = Self::detect_path().ok_or_else(|| {
224            ToolchainError::unfixable(
225                "Android NDK not found",
226                "Open Android Studio -> SDK Manager -> SDK Tools -> check 'NDK (Side by side)' \
227                 or set ANDROID_NDK_ROOT environment variable.",
228            )
229        })?;
230
231        // Check for LLVM toolchain (required for building)
232        let llvm_dir = ndk_path.join("toolchains/llvm/prebuilt");
233        if !llvm_dir.exists() {
234            return Err(ToolchainError::unfixable(
235                "Android NDK LLVM toolchain not found",
236                "The NDK installation appears to be incomplete. \
237                 Try reinstalling the NDK via Android Studio SDK Manager.",
238            ));
239        }
240
241        Ok(())
242    }
243}
244
245impl Installation for AndroidNdkInstallation {
246    type Error = FailToInstallAndroidNdk;
247
248    async fn install(&self) -> Result<(), Self::Error> {
249        // NDK installation is handled via Android Studio SDK Manager
250        // This is intentionally a no-op as the check returns unfixable errors
251        Ok(())
252    }
253}
254
255/// Errors that can occur when installing the Android NDK.
256#[derive(Debug, thiserror::Error)]
257pub enum FailToInstallAndroidNdk {}