1use crate::types::{BenchError, BuildConfig, BuildProfile, BuildResult, Target};
7use std::env;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11pub struct AndroidBuilder {
13 project_root: PathBuf,
15 output_dir: PathBuf,
17 crate_name: String,
19 verbose: bool,
21}
22
23impl AndroidBuilder {
24 pub fn new(project_root: impl Into<PathBuf>, crate_name: impl Into<String>) -> Self {
31 let root = project_root.into();
32 Self {
33 output_dir: root.join("target/mobench"),
34 project_root: root,
35 crate_name: crate_name.into(),
36 verbose: false,
37 }
38 }
39
40 pub fn output_dir(mut self, dir: impl Into<PathBuf>) -> Self {
45 self.output_dir = dir.into();
46 self
47 }
48
49 pub fn verbose(mut self, verbose: bool) -> Self {
51 self.verbose = verbose;
52 self
53 }
54
55 pub fn build(&self, config: &BuildConfig) -> Result<BuildResult, BenchError> {
68 println!("Building Rust libraries for Android...");
70 self.build_rust_libraries(config)?;
71
72 println!("Generating UniFFI Kotlin bindings...");
74 self.generate_uniffi_bindings()?;
75
76 println!("Copying native libraries to jniLibs...");
78 self.copy_native_libraries(config)?;
79
80 println!("Building Android APK with Gradle...");
82 let apk_path = self.build_apk(config)?;
83
84 println!("Building Android test APK...");
86 let test_suite_path = self.build_test_apk(config)?;
87
88 Ok(BuildResult {
89 platform: Target::Android,
90 app_path: apk_path,
91 test_suite_path: Some(test_suite_path),
92 })
93 }
94
95 fn find_crate_dir(&self) -> Result<PathBuf, BenchError> {
97 let bench_mobile_dir = self.project_root.join("bench-mobile");
99 if bench_mobile_dir.exists() {
100 return Ok(bench_mobile_dir);
101 }
102
103 let crates_dir = self.project_root.join("crates").join(&self.crate_name);
105 if crates_dir.exists() {
106 return Ok(crates_dir);
107 }
108
109 Err(BenchError::Build(format!(
110 "Benchmark crate '{}' not found. Tried:\n - {:?}\n - {:?}",
111 self.crate_name, bench_mobile_dir, crates_dir
112 )))
113 }
114
115 fn build_rust_libraries(&self, config: &BuildConfig) -> Result<(), BenchError> {
117 let crate_dir = self.find_crate_dir()?;
118
119 self.check_cargo_ndk()?;
121
122 let abis = vec!["arm64-v8a", "armeabi-v7a", "x86_64"];
124
125 for abi in abis {
126 if self.verbose {
127 println!(" Building for {}", abi);
128 }
129
130 let mut cmd = Command::new("cargo");
131 cmd.arg("ndk")
132 .arg("--target")
133 .arg(abi)
134 .arg("--platform")
135 .arg("24") .arg("build");
137
138 if matches!(config.profile, BuildProfile::Release) {
140 cmd.arg("--release");
141 }
142
143 cmd.current_dir(&crate_dir);
145
146 let output = cmd
148 .output()
149 .map_err(|e| BenchError::Build(format!("Failed to run cargo-ndk: {}", e)))?;
150
151 if !output.status.success() {
152 let stderr = String::from_utf8_lossy(&output.stderr);
153 return Err(BenchError::Build(format!(
154 "cargo-ndk build failed for {}: {}",
155 abi, stderr
156 )));
157 }
158 }
159
160 Ok(())
161 }
162
163 fn check_cargo_ndk(&self) -> Result<(), BenchError> {
165 let output = Command::new("cargo").arg("ndk").arg("--version").output();
166
167 match output {
168 Ok(output) if output.status.success() => Ok(()),
169 _ => Err(BenchError::Build(
170 "cargo-ndk is not installed. Install it with: cargo install cargo-ndk".to_string(),
171 )),
172 }
173 }
174
175 fn generate_uniffi_bindings(&self) -> Result<(), BenchError> {
177 let crate_dir = self.find_crate_dir()?;
178 let crate_name_underscored = self.crate_name.replace("-", "_");
179
180 let bindings_path = self
182 .output_dir
183 .join("android")
184 .join("app")
185 .join("src")
186 .join("main")
187 .join("java")
188 .join("uniffi")
189 .join(&crate_name_underscored)
190 .join(format!("{}.kt", crate_name_underscored));
191
192 if bindings_path.exists() {
193 if self.verbose {
194 println!(" Using existing Kotlin bindings at {:?}", bindings_path);
195 }
196 return Ok(());
197 }
198
199 let uniffi_available = Command::new("uniffi-bindgen")
201 .arg("--version")
202 .output()
203 .map(|o| o.status.success())
204 .unwrap_or(false);
205
206 if !uniffi_available {
207 return Err(BenchError::Build(
208 "uniffi-bindgen not found and no pre-generated bindings exist.\n\
209 Install it with: cargo install uniffi-bindgen\n\
210 Or use pre-generated bindings by copying them to the expected location."
211 .to_string(),
212 ));
213 }
214
215 let mut build_cmd = Command::new("cargo");
217 build_cmd.arg("build");
218 build_cmd.current_dir(&crate_dir);
219 run_command(build_cmd, "cargo build (host)")?;
220
221 let lib_path = host_lib_path(&crate_dir, &self.crate_name)?;
222 let out_dir = self
223 .output_dir
224 .join("android")
225 .join("app")
226 .join("src")
227 .join("main")
228 .join("java");
229
230 let mut cmd = Command::new("uniffi-bindgen");
231 cmd.arg("generate")
232 .arg("--library")
233 .arg(&lib_path)
234 .arg("--language")
235 .arg("kotlin")
236 .arg("--out-dir")
237 .arg(&out_dir);
238 run_command(cmd, "uniffi-bindgen kotlin")?;
239
240 if self.verbose {
241 println!(" Generated UniFFI Kotlin bindings at {:?}", out_dir);
242 }
243 Ok(())
244 }
245
246 fn copy_native_libraries(&self, config: &BuildConfig) -> Result<(), BenchError> {
248 let profile_dir = match config.profile {
249 BuildProfile::Debug => "debug",
250 BuildProfile::Release => "release",
251 };
252
253 let target_dir = self.project_root.join("target");
254 let jni_libs_dir = self.output_dir.join("android/app/src/main/jniLibs");
255
256 std::fs::create_dir_all(&jni_libs_dir)
258 .map_err(|e| BenchError::Build(format!("Failed to create jniLibs directory: {}", e)))?;
259
260 let abi_mappings = vec![
262 ("aarch64-linux-android", "arm64-v8a"),
263 ("armv7-linux-androideabi", "armeabi-v7a"),
264 ("x86_64-linux-android", "x86_64"),
265 ];
266
267 for (rust_target, android_abi) in abi_mappings {
268 let src = target_dir
269 .join(rust_target)
270 .join(profile_dir)
271 .join(format!("lib{}.so", self.crate_name.replace("-", "_")));
272
273 let dest_dir = jni_libs_dir.join(android_abi);
274 std::fs::create_dir_all(&dest_dir).map_err(|e| {
275 BenchError::Build(format!("Failed to create {} directory: {}", android_abi, e))
276 })?;
277
278 let dest = dest_dir.join(format!("lib{}.so", self.crate_name.replace("-", "_")));
279
280 if src.exists() {
281 std::fs::copy(&src, &dest).map_err(|e| {
282 BenchError::Build(format!("Failed to copy {} library: {}", android_abi, e))
283 })?;
284
285 if self.verbose {
286 println!(" Copied {} -> {}", src.display(), dest.display());
287 }
288 } else if self.verbose {
289 println!(" Warning: {} not found, skipping", src.display());
290 }
291 }
292
293 Ok(())
294 }
295
296 fn build_apk(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
298 let android_dir = self.output_dir.join("android");
299
300 if !android_dir.exists() {
301 return Err(BenchError::Build(format!(
302 "Android project not found at {:?}",
303 android_dir
304 )));
305 }
306
307 let gradle_task = match config.profile {
309 BuildProfile::Debug => "assembleDebug",
310 BuildProfile::Release => "assembleRelease",
311 };
312
313 let mut cmd = Command::new("./gradlew");
315 cmd.arg(gradle_task).current_dir(&android_dir);
316
317 if self.verbose {
318 cmd.arg("--info");
319 }
320
321 let output = cmd
322 .output()
323 .map_err(|e| BenchError::Build(format!("Failed to run Gradle: {}", e)))?;
324
325 if !output.status.success() {
326 let stderr = String::from_utf8_lossy(&output.stderr);
327 return Err(BenchError::Build(format!(
328 "Gradle build failed: {}",
329 stderr
330 )));
331 }
332
333 let profile_name = match config.profile {
335 BuildProfile::Debug => "debug",
336 BuildProfile::Release => "release",
337 };
338
339 let apk_path = android_dir
340 .join("app/build/outputs/apk")
341 .join(profile_name)
342 .join(format!("app-{}.apk", profile_name));
343
344 if !apk_path.exists() {
345 return Err(BenchError::Build(format!(
346 "APK not found at expected location: {:?}",
347 apk_path
348 )));
349 }
350
351 Ok(apk_path)
352 }
353
354 fn build_test_apk(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
356 let android_dir = self.output_dir.join("android");
357
358 if !android_dir.exists() {
359 return Err(BenchError::Build(format!(
360 "Android project not found at {:?}",
361 android_dir
362 )));
363 }
364
365 let gradle_task = match config.profile {
366 BuildProfile::Debug => "assembleDebugAndroidTest",
367 BuildProfile::Release => "assembleReleaseAndroidTest",
368 };
369
370 let mut cmd = Command::new("./gradlew");
371 cmd.arg(gradle_task).current_dir(&android_dir);
372
373 if self.verbose {
374 cmd.arg("--info");
375 }
376
377 let output = cmd
378 .output()
379 .map_err(|e| BenchError::Build(format!("Failed to run Gradle: {}", e)))?;
380
381 if !output.status.success() {
382 let stderr = String::from_utf8_lossy(&output.stderr);
383 return Err(BenchError::Build(format!(
384 "Gradle test APK build failed: {}",
385 stderr
386 )));
387 }
388
389 let profile_name = match config.profile {
390 BuildProfile::Debug => "debug",
391 BuildProfile::Release => "release",
392 };
393
394 let apk_path = android_dir
395 .join("app/build/outputs/apk/androidTest")
396 .join(profile_name)
397 .join(format!("app-{}-androidTest.apk", profile_name));
398
399 if !apk_path.exists() {
400 return Err(BenchError::Build(format!(
401 "Android test APK not found at expected location: {:?}",
402 apk_path
403 )));
404 }
405
406 Ok(apk_path)
407 }
408}
409
410fn host_lib_path(project_dir: &Path, crate_name: &str) -> Result<PathBuf, BenchError> {
412 let lib_prefix = if cfg!(target_os = "windows") {
413 ""
414 } else {
415 "lib"
416 };
417 let lib_ext = match env::consts::OS {
418 "macos" => "dylib",
419 "linux" => "so",
420 other => {
421 return Err(BenchError::Build(format!(
422 "unsupported host OS for binding generation: {}",
423 other
424 )));
425 }
426 };
427 let path = project_dir.join("target").join("debug").join(format!(
428 "{}{}.{}",
429 lib_prefix,
430 crate_name.replace('-', "_"),
431 lib_ext
432 ));
433 if !path.exists() {
434 return Err(BenchError::Build(format!(
435 "host library for UniFFI not found at {:?}",
436 path
437 )));
438 }
439 Ok(path)
440}
441
442fn run_command(mut cmd: Command, description: &str) -> Result<(), BenchError> {
443 let output = cmd
444 .output()
445 .map_err(|e| BenchError::Build(format!("Failed to run {}: {}", description, e)))?;
446 if !output.status.success() {
447 let stderr = String::from_utf8_lossy(&output.stderr);
448 return Err(BenchError::Build(format!(
449 "{} failed: {}",
450 description, stderr
451 )));
452 }
453 Ok(())
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn test_android_builder_creation() {
462 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
463 assert!(!builder.verbose);
464 assert_eq!(
465 builder.output_dir,
466 PathBuf::from("/tmp/test-project/target/mobench")
467 );
468 }
469
470 #[test]
471 fn test_android_builder_verbose() {
472 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile").verbose(true);
473 assert!(builder.verbose);
474 }
475
476 #[test]
477 fn test_android_builder_custom_output_dir() {
478 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile")
479 .output_dir("/custom/output");
480 assert_eq!(builder.output_dir, PathBuf::from("/custom/output"));
481 }
482}