1use crate::types::{BenchError, BuildConfig, BuildProfile, BuildResult, Target};
7use std::env;
8use std::path::PathBuf;
9use std::process::Command;
10
11pub struct AndroidBuilder {
13 project_root: PathBuf,
15 crate_name: String,
17 verbose: bool,
19}
20
21impl AndroidBuilder {
22 pub fn new(project_root: impl Into<PathBuf>, crate_name: impl Into<String>) -> Self {
29 Self {
30 project_root: project_root.into(),
31 crate_name: crate_name.into(),
32 verbose: false,
33 }
34 }
35
36 pub fn verbose(mut self, verbose: bool) -> Self {
38 self.verbose = verbose;
39 self
40 }
41
42 pub fn build(&self, config: &BuildConfig) -> Result<BuildResult, BenchError> {
55 println!("Building Rust libraries for Android...");
57 self.build_rust_libraries(config)?;
58
59 println!("Generating UniFFI Kotlin bindings...");
61 self.generate_uniffi_bindings()?;
62
63 println!("Copying native libraries to jniLibs...");
65 self.copy_native_libraries(config)?;
66
67 println!("Building Android APK with Gradle...");
69 let apk_path = self.build_apk(config)?;
70
71 Ok(BuildResult {
72 platform: Target::Android,
73 app_path: apk_path,
74 test_suite_path: None, })
76 }
77
78 fn build_rust_libraries(&self, config: &BuildConfig) -> Result<(), BenchError> {
80 let bench_mobile_dir = self.project_root.join("bench-mobile");
81
82 if !bench_mobile_dir.exists() {
83 return Err(BenchError::Build(format!(
84 "bench-mobile crate not found at {:?}",
85 bench_mobile_dir
86 )));
87 }
88
89 self.check_cargo_ndk()?;
91
92 let abis = vec!["arm64-v8a", "armeabi-v7a", "x86_64"];
94
95 for abi in abis {
96 if self.verbose {
97 println!(" Building for {}", abi);
98 }
99
100 let mut cmd = Command::new("cargo");
101 cmd.arg("ndk")
102 .arg("--target")
103 .arg(abi)
104 .arg("--platform")
105 .arg("24") .arg("build");
107
108 if matches!(config.profile, BuildProfile::Release) {
110 cmd.arg("--release");
111 }
112
113 cmd.current_dir(&bench_mobile_dir);
115
116 let output = cmd
118 .output()
119 .map_err(|e| BenchError::Build(format!("Failed to run cargo-ndk: {}", e)))?;
120
121 if !output.status.success() {
122 let stderr = String::from_utf8_lossy(&output.stderr);
123 return Err(BenchError::Build(format!(
124 "cargo-ndk build failed for {}: {}",
125 abi, stderr
126 )));
127 }
128 }
129
130 Ok(())
131 }
132
133 fn check_cargo_ndk(&self) -> Result<(), BenchError> {
135 let output = Command::new("cargo").arg("ndk").arg("--version").output();
136
137 match output {
138 Ok(output) if output.status.success() => Ok(()),
139 _ => Err(BenchError::Build(
140 "cargo-ndk is not installed. Install it with: cargo install cargo-ndk".to_string(),
141 )),
142 }
143 }
144
145 fn generate_uniffi_bindings(&self) -> Result<(), BenchError> {
147 let bench_mobile_dir = self.project_root.join("bench-mobile");
148 if !bench_mobile_dir.exists() {
149 return Err(BenchError::Build(format!(
150 "bench-mobile crate not found at {:?}",
151 bench_mobile_dir
152 )));
153 }
154
155 let mut build_cmd = Command::new("cargo");
157 build_cmd.arg("build");
158 build_cmd.current_dir(&bench_mobile_dir);
159 run_command(build_cmd, "cargo build (host)")?;
160
161 let lib_path = host_lib_path(&bench_mobile_dir, &self.crate_name)?;
162 let out_dir = self
163 .project_root
164 .join("android")
165 .join("app")
166 .join("src")
167 .join("main")
168 .join("java");
169
170 let mut cmd = Command::new("uniffi-bindgen");
171 cmd.arg("generate")
172 .arg("--library")
173 .arg(&lib_path)
174 .arg("--language")
175 .arg("kotlin")
176 .arg("--out-dir")
177 .arg(&out_dir);
178 run_command(cmd, "uniffi-bindgen kotlin")?;
179
180 if self.verbose {
181 println!(" Generated UniFFI Kotlin bindings at {:?}", out_dir);
182 }
183 Ok(())
184 }
185
186 fn copy_native_libraries(&self, config: &BuildConfig) -> Result<(), BenchError> {
188 let profile_dir = match config.profile {
189 BuildProfile::Debug => "debug",
190 BuildProfile::Release => "release",
191 };
192
193 let target_dir = self.project_root.join("target");
194 let jni_libs_dir = self.project_root.join("android/app/src/main/jniLibs");
195
196 std::fs::create_dir_all(&jni_libs_dir)
198 .map_err(|e| BenchError::Build(format!("Failed to create jniLibs directory: {}", e)))?;
199
200 let abi_mappings = vec![
202 ("aarch64-linux-android", "arm64-v8a"),
203 ("armv7-linux-androideabi", "armeabi-v7a"),
204 ("x86_64-linux-android", "x86_64"),
205 ];
206
207 for (rust_target, android_abi) in abi_mappings {
208 let src = target_dir
209 .join(rust_target)
210 .join(profile_dir)
211 .join(format!("lib{}.so", self.crate_name.replace("-", "_")));
212
213 let dest_dir = jni_libs_dir.join(android_abi);
214 std::fs::create_dir_all(&dest_dir).map_err(|e| {
215 BenchError::Build(format!("Failed to create {} directory: {}", android_abi, e))
216 })?;
217
218 let dest = dest_dir.join(format!(
219 "lib{}.so",
220 self.crate_name.replace("-", "_")
221 ));
222
223 if src.exists() {
224 std::fs::copy(&src, &dest).map_err(|e| {
225 BenchError::Build(format!("Failed to copy {} library: {}", android_abi, e))
226 })?;
227
228 if self.verbose {
229 println!(" Copied {} -> {}", src.display(), dest.display());
230 }
231 } else if self.verbose {
232 println!(" Warning: {} not found, skipping", src.display());
233 }
234 }
235
236 Ok(())
237 }
238
239 fn build_apk(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
241 let android_dir = self.project_root.join("android");
242
243 if !android_dir.exists() {
244 return Err(BenchError::Build(format!(
245 "Android project not found at {:?}",
246 android_dir
247 )));
248 }
249
250 let gradle_task = match config.profile {
252 BuildProfile::Debug => "assembleDebug",
253 BuildProfile::Release => "assembleRelease",
254 };
255
256 let mut cmd = Command::new("./gradlew");
258 cmd.arg(gradle_task).current_dir(&android_dir);
259
260 if self.verbose {
261 cmd.arg("--info");
262 }
263
264 let output = cmd
265 .output()
266 .map_err(|e| BenchError::Build(format!("Failed to run Gradle: {}", e)))?;
267
268 if !output.status.success() {
269 let stderr = String::from_utf8_lossy(&output.stderr);
270 return Err(BenchError::Build(format!(
271 "Gradle build failed: {}",
272 stderr
273 )));
274 }
275
276 let profile_name = match config.profile {
278 BuildProfile::Debug => "debug",
279 BuildProfile::Release => "release",
280 };
281
282 let apk_path = android_dir
283 .join("app/build/outputs/apk")
284 .join(profile_name)
285 .join(format!("app-{}.apk", profile_name));
286
287 if !apk_path.exists() {
288 return Err(BenchError::Build(format!(
289 "APK not found at expected location: {:?}",
290 apk_path
291 )));
292 }
293
294 Ok(apk_path)
295 }
296}
297
298fn host_lib_path(project_dir: &PathBuf, crate_name: &str) -> Result<PathBuf, BenchError> {
300 let lib_prefix = if cfg!(target_os = "windows") {
301 ""
302 } else {
303 "lib"
304 };
305 let lib_ext = match env::consts::OS {
306 "macos" => "dylib",
307 "linux" => "so",
308 other => {
309 return Err(BenchError::Build(format!(
310 "unsupported host OS for binding generation: {}",
311 other
312 )));
313 }
314 };
315 let path = project_dir.join("target").join("debug").join(format!(
316 "{}{}.{}",
317 lib_prefix,
318 crate_name.replace('-', "_"),
319 lib_ext
320 ));
321 if !path.exists() {
322 return Err(BenchError::Build(format!(
323 "host library for UniFFI not found at {:?}",
324 path
325 )));
326 }
327 Ok(path)
328}
329
330fn run_command(mut cmd: Command, description: &str) -> Result<(), BenchError> {
331 let output = cmd
332 .output()
333 .map_err(|e| BenchError::Build(format!("Failed to run {}: {}", description, e)))?;
334 if !output.status.success() {
335 let stderr = String::from_utf8_lossy(&output.stderr);
336 return Err(BenchError::Build(format!(
337 "{} failed: {}",
338 description, stderr
339 )));
340 }
341 Ok(())
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_android_builder_creation() {
350 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
351 assert!(!builder.verbose);
352 }
353
354 #[test]
355 fn test_android_builder_verbose() {
356 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile").verbose(true);
357 assert!(builder.verbose);
358 }
359}