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