1use super::common::{
61 ToolCommand, get_cargo_target_dir, host_lib_path, run_tool_command, validate_project_root,
62};
63use crate::types::{
64 BenchError, BuildConfig, BuildProfile, BuildResult, NativeLibraryArtifact, Target,
65};
66use std::env;
67use std::fs;
68use std::path::{Path, PathBuf};
69
70pub struct AndroidBuilder {
97 project_root: PathBuf,
99 output_dir: PathBuf,
101 crate_name: String,
103 verbose: bool,
105 crate_dir: Option<PathBuf>,
107 dry_run: bool,
109 ffi_backend: crate::FfiBackend,
111}
112
113const DEFAULT_ANDROID_ABIS: &[&str] = &["arm64-v8a"];
114
115impl AndroidBuilder {
116 pub fn new(project_root: impl Into<PathBuf>, crate_name: impl Into<String>) -> Self {
123 let root = project_root.into();
124 Self {
125 output_dir: root.join("target/mobench"),
126 project_root: root,
127 crate_name: crate_name.into(),
128 verbose: false,
129 crate_dir: None,
130 dry_run: false,
131 ffi_backend: crate::FfiBackend::Uniffi,
132 }
133 }
134
135 pub fn output_dir(mut self, dir: impl Into<PathBuf>) -> Self {
140 self.output_dir = dir.into();
141 self
142 }
143
144 pub fn crate_dir(mut self, dir: impl Into<PathBuf>) -> Self {
154 self.crate_dir = Some(dir.into());
155 self
156 }
157
158 pub fn verbose(mut self, verbose: bool) -> Self {
160 self.verbose = verbose;
161 self
162 }
163
164 pub fn dry_run(mut self, dry_run: bool) -> Self {
169 self.dry_run = dry_run;
170 self
171 }
172
173 pub fn ffi_backend(mut self, ffi_backend: crate::FfiBackend) -> Self {
177 self.ffi_backend = ffi_backend;
178 self
179 }
180
181 pub fn build(&self, config: &BuildConfig) -> Result<BuildResult, BenchError> {
195 if self.crate_dir.is_none() {
197 validate_project_root(&self.project_root, &self.crate_name)?;
198 }
199
200 let android_dir = self.output_dir.join("android");
201 let profile_name = match config.profile {
202 BuildProfile::Debug => "debug",
203 BuildProfile::Release => "release",
204 };
205 let android_abis = self.resolve_android_abis(config)?;
206
207 if self.dry_run {
208 println!("\n[dry-run] Android build plan:");
209 println!(
210 " Step 0: Check/generate Android project scaffolding at {:?}",
211 android_dir
212 );
213 println!(" Step 0.5: Ensure Gradle wrapper exists (run 'gradle wrapper' if needed)");
214 if self.ffi_backend.uses_boltffi() {
215 let crate_dir = self.find_crate_dir()?;
216 println!(
217 " Step 1: Write BoltFFI config at {:?}",
218 crate_dir.join("boltffi.toml")
219 );
220 println!(" Step 2: Generate and package BoltFFI Android bindings");
221 println!(
222 " Command: boltffi pack android --regenerate {}",
223 if matches!(config.profile, BuildProfile::Release) {
224 "--release"
225 } else {
226 ""
227 }
228 );
229 println!(
230 " Kotlin output: {:?}",
231 android_dir.join("app/src/main/java")
232 );
233 println!(
234 " Native output: {:?}",
235 android_dir.join("app/src/main/jniLibs")
236 );
237 println!(" Step 3: Build Android APK with Gradle");
238 println!(
239 " Command: ./gradlew assemble{}",
240 if profile_name == "release" {
241 "Release"
242 } else {
243 "Debug"
244 }
245 );
246 println!(
247 " Output: {:?}",
248 android_dir.join(format!(
249 "app/build/outputs/apk/{}/app-{}.apk",
250 profile_name, profile_name
251 ))
252 );
253 println!(" Step 4: Build Android test APK");
254 println!(
255 " Command: ./gradlew assemble{}AndroidTest",
256 if profile_name == "release" {
257 "Release"
258 } else {
259 "Debug"
260 }
261 );
262
263 return Ok(BuildResult {
264 platform: Target::Android,
265 app_path: android_dir.join(format!(
266 "app/build/outputs/apk/{}/app-{}.apk",
267 profile_name, profile_name
268 )),
269 test_suite_path: Some(android_dir.join(format!(
270 "app/build/outputs/apk/androidTest/{}/app-{}-androidTest.apk",
271 profile_name, profile_name
272 ))),
273 native_libraries: Vec::new(),
274 });
275 }
276
277 println!(
278 " Step 1: Build Rust libraries for Android ABIs ({})",
279 android_abis.join(", ")
280 );
281 println!(
282 " Command: cargo ndk --target <abi> --platform 24 build {}",
283 if matches!(config.profile, BuildProfile::Release) {
284 "--release"
285 } else {
286 ""
287 }
288 );
289 if self.ffi_backend.uses_uniffi() {
290 println!(" Step 2: Generate UniFFI Kotlin bindings");
291 println!(
292 " Output: {:?}",
293 android_dir.join("app/src/main/java/uniffi")
294 );
295 } else {
296 println!(
297 " Step 2: Skip UniFFI Kotlin bindings (backend: {})",
298 self.ffi_backend
299 );
300 }
301 println!(" Step 3: Copy .so files to jniLibs directories");
302 println!(
303 " Destination: {:?}",
304 android_dir.join("app/src/main/jniLibs")
305 );
306 println!(" Step 4: Build Android APK with Gradle");
307 println!(
308 " Command: ./gradlew assemble{}",
309 if profile_name == "release" {
310 "Release"
311 } else {
312 "Debug"
313 }
314 );
315 println!(
316 " Output: {:?}",
317 android_dir.join(format!(
318 "app/build/outputs/apk/{}/app-{}.apk",
319 profile_name, profile_name
320 ))
321 );
322 println!(" Step 5: Build Android test APK");
323 println!(
324 " Command: ./gradlew assemble{}AndroidTest",
325 if profile_name == "release" {
326 "Release"
327 } else {
328 "Debug"
329 }
330 );
331
332 return Ok(BuildResult {
334 platform: Target::Android,
335 app_path: android_dir.join(format!(
336 "app/build/outputs/apk/{}/app-{}.apk",
337 profile_name, profile_name
338 )),
339 test_suite_path: Some(android_dir.join(format!(
340 "app/build/outputs/apk/androidTest/{}/app-{}-androidTest.apk",
341 profile_name, profile_name
342 ))),
343 native_libraries: Vec::new(),
344 });
345 }
346
347 crate::codegen::ensure_android_project_with_backend_options(
350 &self.output_dir,
351 &self.crate_name,
352 Some(&self.project_root),
353 self.crate_dir.as_deref(),
354 self.ffi_backend,
355 )?;
356
357 self.ensure_gradle_wrapper(&android_dir)?;
359
360 if self.ffi_backend.uses_boltffi() {
361 println!("Generating and packaging BoltFFI Android bindings...");
362 self.write_boltffi_config(config)?;
363 self.run_boltffi_pack_android(config)?;
364
365 println!("Building Android APK with Gradle...");
366 let apk_path = self.build_apk(config)?;
367
368 println!("Building Android test APK...");
369 let test_suite_path = self.build_test_apk(config)?;
370
371 let result = BuildResult {
372 platform: Target::Android,
373 app_path: apk_path,
374 test_suite_path: Some(test_suite_path),
375 native_libraries: self.collect_packaged_native_libraries(config)?,
376 };
377 self.validate_build_artifacts(&result, config)?;
378 return Ok(result);
379 }
380
381 println!("Building Rust libraries for Android...");
383 self.build_rust_libraries(config)?;
384
385 if self.ffi_backend.uses_uniffi() {
387 println!("Generating UniFFI Kotlin bindings...");
388 self.generate_uniffi_bindings()?;
389 } else {
390 println!(
391 "Skipping UniFFI Kotlin bindings for {} backend",
392 self.ffi_backend
393 );
394 }
395
396 println!("Copying native libraries to jniLibs...");
398 let native_libraries = self.copy_native_libraries(config)?;
399
400 println!("Building Android APK with Gradle...");
402 let apk_path = self.build_apk(config)?;
403
404 println!("Building Android test APK...");
406 let test_suite_path = self.build_test_apk(config)?;
407
408 let result = BuildResult {
410 platform: Target::Android,
411 app_path: apk_path,
412 test_suite_path: Some(test_suite_path),
413 native_libraries,
414 };
415 self.validate_build_artifacts(&result, config)?;
416
417 Ok(result)
418 }
419
420 fn write_boltffi_config(&self, config: &BuildConfig) -> Result<(), BenchError> {
421 let crate_dir = self.find_crate_dir()?;
422 let library_name = self.crate_name.replace('-', "_");
423 let package_component = crate::codegen::sanitize_bundle_id_component(&library_name);
424 let kotlin_package = format!("com.mobench.{package_component}");
425 let architectures = self
426 .resolve_android_abis(config)?
427 .iter()
428 .map(|abi| android_abi_to_boltffi_architecture(abi))
429 .collect::<Result<Vec<_>, _>>()?;
430
431 crate::codegen::write_boltffi_config_with_output_dir(
432 &crate_dir,
433 &library_name,
434 &self.crate_name,
435 "BenchRunner",
436 &kotlin_package,
437 &architectures,
438 &self.output_dir,
439 )
440 }
441
442 fn run_boltffi_pack_android(&self, config: &BuildConfig) -> Result<(), BenchError> {
443 let crate_dir = self.find_crate_dir()?;
444 let mut cmd = ToolCommand::path_search("boltffi");
445 cmd.arg("pack").arg("android").arg("--regenerate");
446 if matches!(config.profile, BuildProfile::Release) {
447 cmd.arg("--release");
448 }
449 cmd.current_dir(&crate_dir);
450 run_tool_command(cmd, "boltffi pack android")
451 }
452
453 fn collect_packaged_native_libraries(
454 &self,
455 config: &BuildConfig,
456 ) -> Result<Vec<NativeLibraryArtifact>, BenchError> {
457 let jni_libs_dir = self.output_dir.join("android/app/src/main/jniLibs");
458 let library_name = format!("lib{}.so", self.crate_name.replace("-", "_"));
459 let mut native_libraries = Vec::new();
460
461 for abi in self.resolve_android_abis(config)? {
462 let packaged_path = jni_libs_dir.join(&abi).join(&library_name);
463 if packaged_path.exists() {
464 native_libraries.push(NativeLibraryArtifact {
465 abi,
466 library_name: library_name.clone(),
467 unstripped_path: packaged_path.clone(),
468 packaged_path,
469 });
470 }
471 }
472
473 Ok(native_libraries)
474 }
475
476 fn validate_build_artifacts(
478 &self,
479 result: &BuildResult,
480 config: &BuildConfig,
481 ) -> Result<(), BenchError> {
482 let mut missing = Vec::new();
483 let profile_dir = match config.profile {
484 BuildProfile::Debug => "debug",
485 BuildProfile::Release => "release",
486 };
487
488 if !result.app_path.exists() {
490 missing.push(format!("Main APK: {}", result.app_path.display()));
491 }
492
493 if let Some(ref test_path) = result.test_suite_path
495 && !test_path.exists()
496 {
497 missing.push(format!("Test APK: {}", test_path.display()));
498 }
499
500 let jni_libs_dir = self.output_dir.join("android/app/src/main/jniLibs");
502 let lib_name = format!("lib{}.so", self.crate_name.replace("-", "_"));
503 let required_abis = self.resolve_android_abis(config)?;
504 let mut found_libs = 0;
505 for abi in &required_abis {
506 let lib_path = jni_libs_dir.join(abi).join(&lib_name);
507 if lib_path.exists() {
508 found_libs += 1;
509 } else {
510 missing.push(format!(
511 "Native library ({} {}): {}",
512 abi,
513 profile_dir,
514 lib_path.display()
515 ));
516 }
517 }
518
519 if found_libs == 0 {
520 return Err(BenchError::Build(format!(
521 "Build validation failed: No native libraries found.\n\n\
522 Expected at least one .so file in jniLibs directories.\n\
523 Missing artifacts:\n{}\n\n\
524 This usually means the Rust build step failed. Check the cargo-ndk output above.",
525 missing
526 .iter()
527 .map(|s| format!(" - {}", s))
528 .collect::<Vec<_>>()
529 .join("\n")
530 )));
531 }
532
533 if !missing.is_empty() {
534 eprintln!(
535 "Warning: Some build artifacts are missing:\n{}\n\
536 The build may still work but some features might be unavailable.",
537 missing
538 .iter()
539 .map(|s| format!(" - {}", s))
540 .collect::<Vec<_>>()
541 .join("\n")
542 );
543 }
544
545 Ok(())
546 }
547
548 fn resolve_android_abis(&self, config: &BuildConfig) -> Result<Vec<String>, BenchError> {
549 let requested = config
550 .android_abis
551 .as_ref()
552 .filter(|abis| !abis.is_empty())
553 .cloned()
554 .unwrap_or_else(|| {
555 DEFAULT_ANDROID_ABIS
556 .iter()
557 .map(|abi| (*abi).to_string())
558 .collect()
559 });
560
561 let mut resolved = Vec::new();
562 for abi in requested {
563 if android_abi_to_rust_target(&abi).is_none() {
564 return Err(BenchError::Build(format!(
565 "Unsupported Android ABI '{abi}'. Supported values: arm64-v8a, armeabi-v7a, x86_64"
566 )));
567 }
568 if !resolved.contains(&abi) {
569 resolved.push(abi);
570 }
571 }
572
573 Ok(resolved)
574 }
575
576 fn find_crate_dir(&self) -> Result<PathBuf, BenchError> {
584 if let Some(ref dir) = self.crate_dir {
586 if dir.exists() {
587 return Ok(dir.clone());
588 }
589 return Err(BenchError::Build(format!(
590 "Specified crate path does not exist: {:?}.\n\n\
591 Tip: pass --crate-path pointing at a directory containing Cargo.toml.",
592 dir
593 )));
594 }
595
596 let root_cargo_toml = self.project_root.join("Cargo.toml");
599 if root_cargo_toml.exists()
600 && let Some(pkg_name) = super::common::read_package_name(&root_cargo_toml)
601 && pkg_name == self.crate_name
602 {
603 return Ok(self.project_root.clone());
604 }
605
606 let bench_mobile_dir = self.project_root.join("bench-mobile");
608 if bench_mobile_dir.exists() {
609 return Ok(bench_mobile_dir);
610 }
611
612 let crates_dir = self.project_root.join("crates").join(&self.crate_name);
614 if crates_dir.exists() {
615 return Ok(crates_dir);
616 }
617
618 let named_dir = self.project_root.join(&self.crate_name);
620 if named_dir.exists() {
621 return Ok(named_dir);
622 }
623
624 let root_manifest = root_cargo_toml;
625 let bench_mobile_manifest = bench_mobile_dir.join("Cargo.toml");
626 let crates_manifest = crates_dir.join("Cargo.toml");
627 let named_manifest = named_dir.join("Cargo.toml");
628 Err(BenchError::Build(format!(
629 "Benchmark crate '{}' not found.\n\n\
630 Searched locations:\n\
631 - {} (checked [package] name)\n\
632 - {}\n\
633 - {}\n\
634 - {}\n\n\
635 To fix this:\n\
636 1. Run from the crate directory (where Cargo.toml has name = \"{}\")\n\
637 2. Create a bench-mobile/ directory with your benchmark crate, or\n\
638 3. Use --crate-path to specify the benchmark crate location:\n\
639 cargo mobench build --target android --crate-path ./my-benchmarks\n\n\
640 Common issues:\n\
641 - Typo in crate name (check Cargo.toml [package] name)\n\
642 - Wrong working directory (run from project root)\n\
643 - Missing Cargo.toml in the crate directory\n\n\
644 Run 'cargo mobench init --help' to generate a new benchmark project.",
645 self.crate_name,
646 root_manifest.display(),
647 bench_mobile_manifest.display(),
648 crates_manifest.display(),
649 named_manifest.display(),
650 self.crate_name,
651 )))
652 }
653
654 fn build_rust_libraries(&self, config: &BuildConfig) -> Result<(), BenchError> {
656 let crate_dir = self.find_crate_dir()?;
657
658 self.check_cargo_ndk()?;
660
661 let abis = self.resolve_android_abis(config)?;
662 let release_flag = if matches!(config.profile, BuildProfile::Release) {
663 "--release"
664 } else {
665 ""
666 };
667
668 for abi in abis {
669 if self.verbose {
670 println!(" Building for {}", abi);
671 }
672
673 let mut cmd = ToolCommand::path_search("cargo");
674 cmd.arg("ndk")
675 .arg("--target")
676 .arg(&abi)
677 .arg("--platform")
678 .arg("24") .arg("build");
680
681 if !release_flag.is_empty() {
683 cmd.arg(release_flag);
684 }
685
686 cmd.current_dir(&crate_dir);
688
689 let command_hint = if release_flag.is_empty() {
691 format!("cargo ndk --target {} --platform 24 build", abi)
692 } else {
693 format!(
694 "cargo ndk --target {} --platform 24 build {}",
695 abi, release_flag
696 )
697 };
698 let output = cmd.output().map_err(|e| {
699 BenchError::Build(format!(
700 "Failed to start cargo-ndk for {}.\n\n\
701 Command: {}\n\
702 Crate directory: {}\n\
703 System error: {}\n\n\
704 Tips:\n\
705 - Install cargo-ndk: cargo install cargo-ndk\n\
706 - Ensure cargo is on PATH",
707 abi,
708 command_hint,
709 crate_dir.display(),
710 e
711 ))
712 })?;
713
714 if !output.status.success() {
715 let stdout = String::from_utf8_lossy(&output.stdout);
716 let stderr = String::from_utf8_lossy(&output.stderr);
717 let profile = if matches!(config.profile, BuildProfile::Release) {
718 "release"
719 } else {
720 "debug"
721 };
722 let rust_target = android_abi_to_rust_target(&abi).unwrap_or(abi.as_str());
723 return Err(BenchError::Build(format!(
724 "cargo-ndk build failed for {} ({} profile).\n\n\
725 Command: {}\n\
726 Crate directory: {}\n\
727 Exit status: {}\n\n\
728 Stdout:\n{}\n\n\
729 Stderr:\n{}\n\n\
730 Common causes:\n\
731 - Missing Rust target: rustup target add {}\n\
732 - NDK not found: set ANDROID_NDK_HOME\n\
733 - Compilation error in Rust code (see output above)\n\
734 - Incompatible native dependencies (some C libraries do not support Android)",
735 abi,
736 profile,
737 command_hint,
738 crate_dir.display(),
739 output.status,
740 stdout,
741 stderr,
742 rust_target,
743 )));
744 }
745 }
746
747 Ok(())
748 }
749
750 fn check_cargo_ndk(&self) -> Result<(), BenchError> {
752 let output = ToolCommand::path_search("cargo")
753 .arg("ndk")
754 .arg("--version")
755 .output();
756
757 match output {
758 Ok(output) if output.status.success() => Ok(()),
759 _ => Err(BenchError::Build(
760 "cargo-ndk is not installed or not in PATH.\n\n\
761 cargo-ndk is required to cross-compile Rust for Android.\n\n\
762 To install:\n\
763 cargo install cargo-ndk\n\
764 Verify with:\n\
765 cargo ndk --version\n\n\
766 You also need the Android NDK. Set ANDROID_NDK_HOME or install via Android Studio.\n\
767 See: https://github.com/nickelc/cargo-ndk"
768 .to_string(),
769 )),
770 }
771 }
772
773 fn generate_uniffi_bindings(&self) -> Result<(), BenchError> {
775 let crate_dir = self.find_crate_dir()?;
776 let crate_name_underscored = self.crate_name.replace("-", "_");
777
778 let bindings_path = self
780 .output_dir
781 .join("android")
782 .join("app")
783 .join("src")
784 .join("main")
785 .join("java")
786 .join("uniffi")
787 .join(&crate_name_underscored)
788 .join(format!("{}.kt", crate_name_underscored));
789
790 if bindings_path.exists() {
791 if self.verbose {
792 println!(" Using existing Kotlin bindings at {:?}", bindings_path);
793 }
794 return Ok(());
795 }
796
797 let mut build_cmd = ToolCommand::path_search("cargo");
799 build_cmd.arg("build");
800 build_cmd.current_dir(&crate_dir);
801 run_tool_command(build_cmd, "cargo build (host)")?;
802
803 let lib_path = host_lib_path(&crate_dir, &self.crate_name)?;
804 let out_dir = self
805 .output_dir
806 .join("android")
807 .join("app")
808 .join("src")
809 .join("main")
810 .join("java");
811
812 let cargo_run_result = ToolCommand::path_search("cargo")
814 .args([
815 "run",
816 "-p",
817 &self.crate_name,
818 "--bin",
819 "uniffi-bindgen",
820 "--",
821 ])
822 .arg("generate")
823 .arg("--library")
824 .arg(&lib_path)
825 .arg("--language")
826 .arg("kotlin")
827 .arg("--out-dir")
828 .arg(&out_dir)
829 .current_dir(&crate_dir)
830 .output();
831
832 let use_cargo_run = cargo_run_result
833 .as_ref()
834 .map(|o| o.status.success())
835 .unwrap_or(false);
836
837 if use_cargo_run {
838 if self.verbose {
839 println!(" Generated bindings using cargo run uniffi-bindgen");
840 }
841 } else {
842 let uniffi_available = ToolCommand::path_search("uniffi-bindgen")
844 .arg("--version")
845 .output()
846 .map(|o| o.status.success())
847 .unwrap_or(false);
848
849 if !uniffi_available {
850 return Err(BenchError::Build(
851 "uniffi-bindgen not found and no pre-generated bindings exist.\n\n\
852 To fix this, either:\n\
853 1. Add a uniffi-bindgen binary to your crate:\n\
854 [[bin]]\n\
855 name = \"uniffi-bindgen\"\n\
856 path = \"src/bin/uniffi-bindgen.rs\"\n\n\
857 2. Or install uniffi-bindgen globally:\n\
858 cargo install uniffi-bindgen\n\n\
859 3. Or pre-generate bindings and commit them."
860 .to_string(),
861 ));
862 }
863
864 let mut cmd = ToolCommand::path_search("uniffi-bindgen");
865 cmd.arg("generate")
866 .arg("--library")
867 .arg(&lib_path)
868 .arg("--language")
869 .arg("kotlin")
870 .arg("--out-dir")
871 .arg(&out_dir);
872 run_tool_command(cmd, "uniffi-bindgen kotlin")?;
873 }
874
875 if self.verbose {
876 println!(" Generated UniFFI Kotlin bindings at {:?}", out_dir);
877 }
878 Ok(())
879 }
880
881 fn copy_native_libraries(
883 &self,
884 config: &BuildConfig,
885 ) -> Result<Vec<NativeLibraryArtifact>, BenchError> {
886 let crate_dir = self.find_crate_dir()?;
887 let profile_dir = match config.profile {
888 BuildProfile::Debug => "debug",
889 BuildProfile::Release => "release",
890 };
891
892 let target_dir = get_cargo_target_dir(&crate_dir)?;
894 let jni_libs_dir = self.output_dir.join("android/app/src/main/jniLibs");
895
896 std::fs::create_dir_all(&jni_libs_dir).map_err(|e| {
898 BenchError::Build(format!(
899 "Failed to create jniLibs directory at {}: {}. Check output directory permissions.",
900 jni_libs_dir.display(),
901 e
902 ))
903 })?;
904
905 let mut native_libraries = Vec::new();
906
907 for android_abi in self.resolve_android_abis(config)? {
908 let rust_target = android_abi_to_rust_target(&android_abi).ok_or_else(|| {
909 BenchError::Build(format!(
910 "Unsupported Android ABI '{android_abi}'. Supported values: arm64-v8a, armeabi-v7a, x86_64"
911 ))
912 })?;
913 let library_name = format!("lib{}.so", self.crate_name.replace("-", "_"));
914 let src = target_dir
915 .join(rust_target)
916 .join(profile_dir)
917 .join(&library_name);
918
919 let dest_dir = jni_libs_dir.join(&android_abi);
920 std::fs::create_dir_all(&dest_dir).map_err(|e| {
921 BenchError::Build(format!(
922 "Failed to create ABI directory {} at {}: {}. Check output directory permissions.",
923 android_abi,
924 dest_dir.display(),
925 e
926 ))
927 })?;
928
929 let dest = dest_dir.join(&library_name);
930
931 if src.exists() {
932 std::fs::copy(&src, &dest).map_err(|e| {
933 BenchError::Build(format!(
934 "Failed to copy {} library from {} to {}: {}. Ensure cargo-ndk completed successfully.",
935 android_abi,
936 src.display(),
937 dest.display(),
938 e
939 ))
940 })?;
941
942 if self.verbose {
943 println!(" Copied {} -> {}", src.display(), dest.display());
944 }
945
946 native_libraries.push(NativeLibraryArtifact {
947 abi: android_abi.clone(),
948 library_name: library_name.clone(),
949 unstripped_path: src,
950 packaged_path: dest,
951 });
952 } else {
953 eprintln!(
955 "Warning: Native library for {} not found at {}.\n\
956 This will cause a runtime crash when the app tries to load the library.\n\
957 Ensure cargo-ndk build completed successfully for this ABI.",
958 android_abi,
959 src.display()
960 );
961 }
962 }
963
964 Ok(native_libraries)
965 }
966
967 fn ensure_local_properties(&self, android_dir: &Path) -> Result<(), BenchError> {
978 let local_props = android_dir.join("local.properties");
979
980 if local_props.exists() {
982 return Ok(());
983 }
984
985 let sdk_dir = self.find_android_sdk_from_env();
988
989 match sdk_dir {
990 Some(path) => {
991 let content = format!("sdk.dir={}\n", path.display());
993 fs::write(&local_props, content).map_err(|e| {
994 BenchError::Build(format!(
995 "Failed to write local.properties at {:?}: {}. Check output directory permissions.",
996 local_props, e
997 ))
998 })?;
999
1000 if self.verbose {
1001 println!(
1002 " Generated local.properties with sdk.dir={}",
1003 path.display()
1004 );
1005 }
1006 }
1007 None => {
1008 if self.verbose {
1011 println!(
1012 " Skipping local.properties generation (ANDROID_HOME/ANDROID_SDK_ROOT not set)"
1013 );
1014 println!(
1015 " Gradle will auto-detect SDK or you can create local.properties manually"
1016 );
1017 }
1018 }
1019 }
1020
1021 Ok(())
1022 }
1023
1024 fn find_android_sdk_from_env(&self) -> Option<PathBuf> {
1032 if let Ok(path) = env::var("ANDROID_HOME") {
1034 let sdk_path = PathBuf::from(&path);
1035 if sdk_path.exists() {
1036 return Some(sdk_path);
1037 }
1038 }
1039
1040 if let Ok(path) = env::var("ANDROID_SDK_ROOT") {
1042 let sdk_path = PathBuf::from(&path);
1043 if sdk_path.exists() {
1044 return Some(sdk_path);
1045 }
1046 }
1047
1048 None
1049 }
1050
1051 fn ensure_gradle_wrapper(&self, android_dir: &Path) -> Result<(), BenchError> {
1056 let gradlew = android_dir.join("gradlew");
1057
1058 if gradlew.exists() {
1060 return Ok(());
1061 }
1062
1063 println!("Gradle wrapper not found, generating...");
1064
1065 let gradle_available = ToolCommand::path_search("gradle")
1067 .arg("--version")
1068 .output()
1069 .map(|o| o.status.success())
1070 .unwrap_or(false);
1071
1072 if !gradle_available {
1073 return Err(BenchError::Build(
1074 "Gradle wrapper (gradlew) not found and 'gradle' command is not available.\n\n\
1075 The Android project requires Gradle to build. You have two options:\n\n\
1076 1. Install Gradle globally and run the build again (it will auto-generate the wrapper):\n\
1077 - macOS: brew install gradle\n\
1078 - Linux: sudo apt install gradle\n\
1079 - Or download from https://gradle.org/install/\n\n\
1080 2. Or generate the wrapper manually in the Android project directory:\n\
1081 cd target/mobench/android && gradle wrapper --gradle-version 8.5"
1082 .to_string(),
1083 ));
1084 }
1085
1086 let mut cmd = ToolCommand::path_search("gradle");
1088 cmd.arg("wrapper")
1089 .arg("--gradle-version")
1090 .arg("8.5")
1091 .current_dir(android_dir);
1092
1093 let output = cmd.output().map_err(|e| {
1094 BenchError::Build(format!(
1095 "Failed to run 'gradle wrapper' command: {}\n\n\
1096 Ensure Gradle is installed and on your PATH.",
1097 e
1098 ))
1099 })?;
1100
1101 if !output.status.success() {
1102 let stderr = String::from_utf8_lossy(&output.stderr);
1103 return Err(BenchError::Build(format!(
1104 "Failed to generate Gradle wrapper.\n\n\
1105 Command: gradle wrapper --gradle-version 8.5\n\
1106 Working directory: {}\n\
1107 Exit status: {}\n\
1108 Stderr: {}\n\n\
1109 Try running this command manually in the Android project directory.",
1110 android_dir.display(),
1111 output.status,
1112 stderr
1113 )));
1114 }
1115
1116 #[cfg(unix)]
1118 {
1119 use std::os::unix::fs::PermissionsExt;
1120 if let Ok(metadata) = fs::metadata(&gradlew) {
1121 let mut perms = metadata.permissions();
1122 perms.set_mode(0o755);
1123 let _ = fs::set_permissions(&gradlew, perms);
1124 }
1125 }
1126
1127 if self.verbose {
1128 println!(" Generated Gradle wrapper at {:?}", gradlew);
1129 }
1130
1131 Ok(())
1132 }
1133
1134 fn build_apk(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
1136 let android_dir = self.output_dir.join("android");
1137
1138 if !android_dir.exists() {
1139 return Err(BenchError::Build(format!(
1140 "Android project not found at {}.\n\n\
1141 Expected a Gradle project under the output directory.\n\
1142 Run `cargo mobench init --target android` or `cargo mobench build --target android` from the project root to generate it.",
1143 android_dir.display()
1144 )));
1145 }
1146
1147 self.ensure_local_properties(&android_dir)?;
1149
1150 let gradle_task = match config.profile {
1152 BuildProfile::Debug => "assembleDebug",
1153 BuildProfile::Release => "assembleRelease",
1154 };
1155
1156 let mut cmd = ToolCommand::explicit(android_dir.join("gradlew"))?;
1158 cmd.arg(gradle_task).current_dir(&android_dir);
1159
1160 if self.verbose {
1161 cmd.arg("--info");
1162 }
1163
1164 let output = cmd.output().map_err(|e| {
1165 BenchError::Build(format!(
1166 "Failed to run Gradle wrapper.\n\n\
1167 Command: ./gradlew {}\n\
1168 Working directory: {}\n\
1169 Error: {}\n\n\
1170 Tips:\n\
1171 - Ensure ./gradlew is executable (chmod +x ./gradlew)\n\
1172 - Run ./gradlew --version in that directory to verify the wrapper",
1173 gradle_task,
1174 android_dir.display(),
1175 e
1176 ))
1177 })?;
1178
1179 if !output.status.success() {
1180 let stdout = String::from_utf8_lossy(&output.stdout);
1181 let stderr = String::from_utf8_lossy(&output.stderr);
1182 return Err(BenchError::Build(format!(
1183 "Gradle build failed.\n\n\
1184 Command: ./gradlew {}\n\
1185 Working directory: {}\n\
1186 Exit status: {}\n\n\
1187 Stdout:\n{}\n\n\
1188 Stderr:\n{}\n\n\
1189 Tips:\n\
1190 - Re-run with verbose mode to pass --info to Gradle\n\
1191 - Run ./gradlew {} --stacktrace for a full stack trace",
1192 gradle_task,
1193 android_dir.display(),
1194 output.status,
1195 stdout,
1196 stderr,
1197 gradle_task,
1198 )));
1199 }
1200
1201 let profile_name = match config.profile {
1203 BuildProfile::Debug => "debug",
1204 BuildProfile::Release => "release",
1205 };
1206
1207 let apk_dir = android_dir.join("app/build/outputs/apk").join(profile_name);
1208
1209 let apk_path = self.find_apk(&apk_dir, profile_name, gradle_task)?;
1215
1216 Ok(apk_path)
1217 }
1218
1219 fn find_apk(
1229 &self,
1230 apk_dir: &Path,
1231 profile_name: &str,
1232 gradle_task: &str,
1233 ) -> Result<PathBuf, BenchError> {
1234 let metadata_path = apk_dir.join("output-metadata.json");
1236 if metadata_path.exists()
1237 && let Ok(metadata_content) = fs::read_to_string(&metadata_path)
1238 {
1239 if let Some(apk_name) = self.parse_output_metadata(&metadata_content) {
1242 let apk_path = apk_dir.join(&apk_name);
1243 if apk_path.exists() {
1244 if self.verbose {
1245 println!(
1246 " Found APK from output-metadata.json: {}",
1247 apk_path.display()
1248 );
1249 }
1250 return Ok(apk_path);
1251 }
1252 }
1253 }
1254
1255 let candidates = if profile_name == "release" {
1257 vec![
1258 format!("app-{}.apk", profile_name), format!("app-{}-unsigned.apk", profile_name), ]
1261 } else {
1262 vec![
1263 format!("app-{}.apk", profile_name), ]
1265 };
1266
1267 for candidate in &candidates {
1269 let apk_path = apk_dir.join(candidate);
1270 if apk_path.exists() {
1271 if self.verbose {
1272 println!(" Found APK: {}", apk_path.display());
1273 }
1274 return Ok(apk_path);
1275 }
1276 }
1277
1278 Err(BenchError::Build(format!(
1280 "APK not found in {}.\n\n\
1281 Gradle task {} reported success but no APK was produced.\n\
1282 Searched for:\n{}\n\n\
1283 Check the build output directory and rerun ./gradlew {} if needed.",
1284 apk_dir.display(),
1285 gradle_task,
1286 candidates
1287 .iter()
1288 .map(|c| format!(" - {}", c))
1289 .collect::<Vec<_>>()
1290 .join("\n"),
1291 gradle_task
1292 )))
1293 }
1294
1295 fn parse_output_metadata(&self, content: &str) -> Option<String> {
1309 let pattern = "\"outputFile\"";
1312 if let Some(pos) = content.find(pattern) {
1313 let after_key = &content[pos + pattern.len()..];
1314 let after_colon = after_key.trim_start().strip_prefix(':')?;
1316 let after_ws = after_colon.trim_start();
1317 if let Some(value_start) = after_ws.strip_prefix('"')
1319 && let Some(end_quote) = value_start.find('"')
1320 {
1321 let filename = &value_start[..end_quote];
1322 if filename.ends_with(".apk") {
1323 return Some(filename.to_string());
1324 }
1325 }
1326 }
1327 None
1328 }
1329
1330 fn build_test_apk(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
1332 let android_dir = self.output_dir.join("android");
1333
1334 if !android_dir.exists() {
1335 return Err(BenchError::Build(format!(
1336 "Android project not found at {}.\n\n\
1337 Expected a Gradle project under the output directory.\n\
1338 Run `cargo mobench init --target android` or `cargo mobench build --target android` from the project root to generate it.",
1339 android_dir.display()
1340 )));
1341 }
1342
1343 let gradle_task = match config.profile {
1344 BuildProfile::Debug => "assembleDebugAndroidTest",
1345 BuildProfile::Release => "assembleReleaseAndroidTest",
1346 };
1347 let profile_name = match config.profile {
1348 BuildProfile::Debug => "debug",
1349 BuildProfile::Release => "release",
1350 };
1351
1352 let mut cmd = ToolCommand::explicit(android_dir.join("gradlew"))?;
1353 cmd.arg(format!("-PmobenchTestBuildType={profile_name}"))
1354 .arg(gradle_task)
1355 .current_dir(&android_dir);
1356
1357 if self.verbose {
1358 cmd.arg("--info");
1359 }
1360
1361 let output = cmd.output().map_err(|e| {
1362 BenchError::Build(format!(
1363 "Failed to run Gradle wrapper.\n\n\
1364 Command: ./gradlew {}\n\
1365 Working directory: {}\n\
1366 Error: {}\n\n\
1367 Tips:\n\
1368 - Ensure ./gradlew is executable (chmod +x ./gradlew)\n\
1369 - Run ./gradlew --version in that directory to verify the wrapper",
1370 gradle_task,
1371 android_dir.display(),
1372 e
1373 ))
1374 })?;
1375
1376 if !output.status.success() {
1377 let stdout = String::from_utf8_lossy(&output.stdout);
1378 let stderr = String::from_utf8_lossy(&output.stderr);
1379 return Err(BenchError::Build(format!(
1380 "Gradle test APK build failed.\n\n\
1381 Command: ./gradlew {}\n\
1382 Working directory: {}\n\
1383 Exit status: {}\n\n\
1384 Stdout:\n{}\n\n\
1385 Stderr:\n{}\n\n\
1386 Tips:\n\
1387 - Re-run with verbose mode to pass --info to Gradle\n\
1388 - Run ./gradlew {} --stacktrace for a full stack trace",
1389 gradle_task,
1390 android_dir.display(),
1391 output.status,
1392 stdout,
1393 stderr,
1394 gradle_task,
1395 )));
1396 }
1397
1398 let test_apk_dir = android_dir
1399 .join("app/build/outputs/apk/androidTest")
1400 .join(profile_name);
1401
1402 let apk_path = self.find_test_apk(&test_apk_dir, profile_name, gradle_task)?;
1404
1405 Ok(apk_path)
1406 }
1407
1408 fn find_test_apk(
1414 &self,
1415 apk_dir: &Path,
1416 profile_name: &str,
1417 gradle_task: &str,
1418 ) -> Result<PathBuf, BenchError> {
1419 let metadata_path = apk_dir.join("output-metadata.json");
1421 if metadata_path.exists()
1422 && let Ok(metadata_content) = fs::read_to_string(&metadata_path)
1423 && let Some(apk_name) = self.parse_output_metadata(&metadata_content)
1424 {
1425 let apk_path = apk_dir.join(&apk_name);
1426 if apk_path.exists() {
1427 if self.verbose {
1428 println!(
1429 " Found test APK from output-metadata.json: {}",
1430 apk_path.display()
1431 );
1432 }
1433 return Ok(apk_path);
1434 }
1435 }
1436
1437 let apk_path = apk_dir.join(format!("app-{}-androidTest.apk", profile_name));
1439 if apk_path.exists() {
1440 if self.verbose {
1441 println!(" Found test APK: {}", apk_path.display());
1442 }
1443 return Ok(apk_path);
1444 }
1445
1446 Err(BenchError::Build(format!(
1448 "Android test APK not found in {}.\n\n\
1449 Gradle task {} reported success but no test APK was produced.\n\
1450 Expected: app-{}-androidTest.apk\n\n\
1451 Check app/build/outputs/apk/androidTest/{} and rerun ./gradlew {} if needed.",
1452 apk_dir.display(),
1453 gradle_task,
1454 profile_name,
1455 profile_name,
1456 gradle_task
1457 )))
1458 }
1459}
1460
1461fn android_abi_to_rust_target(abi: &str) -> Option<&'static str> {
1462 match abi {
1463 "arm64-v8a" => Some("aarch64-linux-android"),
1464 "armeabi-v7a" => Some("armv7-linux-androideabi"),
1465 "x86_64" => Some("x86_64-linux-android"),
1466 _ => None,
1467 }
1468}
1469
1470fn android_abi_to_boltffi_architecture(abi: &str) -> Result<String, BenchError> {
1471 match abi {
1472 "arm64-v8a" => Ok("arm64".to_string()),
1473 "armeabi-v7a" => Ok("armv7".to_string()),
1474 "x86_64" => Ok("x86_64".to_string()),
1475 _ => Err(BenchError::Build(format!(
1476 "Unsupported Android ABI '{abi}' for BoltFFI. Supported values: arm64-v8a, armeabi-v7a, x86_64"
1477 ))),
1478 }
1479}
1480
1481#[derive(Debug, Clone, PartialEq, Eq)]
1482pub struct AndroidStackSymbolization {
1483 pub line: String,
1484 pub resolved_frames: u64,
1485 pub unresolved_frames: u64,
1486}
1487
1488pub fn symbolize_android_native_stack_line_with_resolver<F>(
1489 line: &str,
1490 mut resolve: F,
1491) -> AndroidStackSymbolization
1492where
1493 F: FnMut(&str, u64) -> Option<String>,
1494{
1495 let (stack, sample_count) = split_folded_stack_line(line);
1496 let mut resolved_frames = 0;
1497 let mut unresolved_frames = 0;
1498 let rewritten = stack
1499 .split(';')
1500 .map(|frame| {
1501 if let Some((library_name, offset)) = parse_android_native_offset_frame(frame) {
1502 if let Some(symbol) = resolve(library_name, offset) {
1503 resolved_frames += 1;
1504 return symbol;
1505 }
1506 unresolved_frames += 1;
1507 }
1508 frame.to_string()
1509 })
1510 .collect::<Vec<_>>()
1511 .join(";");
1512
1513 let line = match sample_count {
1514 Some(count) => format!("{rewritten} {count}"),
1515 None => rewritten,
1516 };
1517
1518 AndroidStackSymbolization {
1519 line,
1520 resolved_frames,
1521 unresolved_frames,
1522 }
1523}
1524
1525pub fn resolve_android_native_symbol_with_addr2line(
1526 library_path: &Path,
1527 offset: u64,
1528) -> Option<String> {
1529 let tool_path = locate_android_addr2line_tool_path()?;
1530 resolve_android_native_symbol_with_tool(&tool_path, library_path, offset)
1531}
1532
1533pub fn resolve_android_native_symbol_with_tool(
1534 tool_path: &Path,
1535 library_path: &Path,
1536 offset: u64,
1537) -> Option<String> {
1538 let output = ToolCommand::explicit(tool_path)
1539 .ok()?
1540 .args(["-Cfpe"])
1541 .arg(library_path)
1542 .arg(format!("0x{offset:x}"))
1543 .output()
1544 .ok()?;
1545 if !output.status.success() {
1546 return None;
1547 }
1548
1549 parse_android_addr2line_stdout(&String::from_utf8_lossy(&output.stdout))
1550}
1551
1552fn parse_android_addr2line_stdout(stdout: &str) -> Option<String> {
1553 stdout.lines().find_map(|line| {
1554 let symbol = line.trim();
1555 if symbol.is_empty() || symbol == "??" || symbol.starts_with("?? ") {
1556 None
1557 } else {
1558 Some(
1559 symbol
1560 .split(" at ")
1561 .next()
1562 .unwrap_or(symbol)
1563 .trim()
1564 .to_owned(),
1565 )
1566 }
1567 })
1568}
1569
1570fn locate_android_addr2line_tool_path() -> Option<PathBuf> {
1571 let override_path = std::env::var_os("MOBENCH_ANDROID_LLVM_ADDR2LINE")
1572 .or_else(|| std::env::var_os("LLVM_ADDR2LINE"))
1573 .map(PathBuf::from);
1574 if let Some(path) = override_path {
1575 return path.exists().then_some(path);
1576 }
1577
1578 let sdk_root = std::env::var_os("ANDROID_HOME")
1579 .map(PathBuf::from)
1580 .or_else(|| std::env::var_os("ANDROID_SDK_ROOT").map(PathBuf::from))
1581 .or_else(|| {
1582 std::env::var_os("ANDROID_NDK_HOME")
1583 .map(PathBuf::from)
1584 .and_then(|ndk_home| ndk_home.parent().and_then(Path::parent).map(PathBuf::from))
1585 })?;
1586 let ndk_root = std::env::var_os("ANDROID_NDK_HOME")
1587 .map(PathBuf::from)
1588 .or_else(|| {
1589 let ndk_dir = sdk_root.join("ndk");
1590 std::fs::read_dir(&ndk_dir).ok().and_then(|entries| {
1591 entries
1592 .filter_map(|entry| entry.ok())
1593 .map(|entry| entry.path())
1594 .filter(|path| path.is_dir())
1595 .max()
1596 })
1597 })?;
1598
1599 let tool_name = if cfg!(windows) {
1600 "llvm-addr2line.exe"
1601 } else {
1602 "llvm-addr2line"
1603 };
1604 let prebuilt_root = ndk_root.join("toolchains").join("llvm").join("prebuilt");
1605 let mut candidates = Vec::new();
1606 if let Ok(entries) = std::fs::read_dir(&prebuilt_root) {
1607 for entry in entries.flatten() {
1608 let candidate = entry.path().join("bin").join(tool_name);
1609 if candidate.exists() {
1610 candidates.push(candidate);
1611 }
1612 }
1613 }
1614 candidates.sort();
1615 candidates.into_iter().next()
1616}
1617
1618fn split_folded_stack_line(line: &str) -> (&str, Option<&str>) {
1619 match line.rsplit_once(' ') {
1620 Some((stack, count))
1621 if !stack.is_empty() && count.chars().all(|ch| ch.is_ascii_digit()) =>
1622 {
1623 (stack, Some(count))
1624 }
1625 _ => (line, None),
1626 }
1627}
1628
1629fn parse_android_native_offset_frame(frame: &str) -> Option<(&str, u64)> {
1630 let marker = ".so[+";
1631 let marker_index = frame.find(marker)?;
1632 let library_end = marker_index + 3;
1633 let library_name = frame[..library_end].rsplit('/').next()?;
1634 let offset_start = marker_index + marker.len();
1635 let offset_end = frame[offset_start..].find(']')? + offset_start;
1636 let offset_raw = &frame[offset_start..offset_end];
1637 let offset = if let Some(hex) = offset_raw.strip_prefix("0x") {
1638 u64::from_str_radix(hex, 16).ok()?
1639 } else {
1640 offset_raw.parse().ok()?
1641 };
1642 Some((library_name, offset))
1643}
1644
1645#[cfg(test)]
1646mod tests {
1647 use super::*;
1648
1649 #[test]
1650 fn test_android_builder_creation() {
1651 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1652 assert!(!builder.verbose);
1653 assert_eq!(
1654 builder.output_dir,
1655 PathBuf::from("/tmp/test-project/target/mobench")
1656 );
1657 }
1658
1659 #[test]
1660 fn test_android_builder_verbose() {
1661 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile").verbose(true);
1662 assert!(builder.verbose);
1663 }
1664
1665 #[test]
1666 fn test_android_builder_custom_output_dir() {
1667 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile")
1668 .output_dir("/custom/output");
1669 assert_eq!(builder.output_dir, PathBuf::from("/custom/output"));
1670 }
1671
1672 #[test]
1673 fn test_parse_output_metadata_unsigned() {
1674 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1675 let metadata = r#"{"version":3,"artifactType":{"type":"APK","kind":"Directory"},"applicationId":"dev.world.bench","variantName":"release","elements":[{"type":"SINGLE","filters":[],"attributes":[],"versionCode":1,"versionName":"0.1","outputFile":"app-release-unsigned.apk"}],"elementType":"File"}"#;
1676 let result = builder.parse_output_metadata(metadata);
1677 assert_eq!(result, Some("app-release-unsigned.apk".to_string()));
1678 }
1679
1680 #[test]
1681 fn test_parse_output_metadata_signed() {
1682 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1683 let metadata = r#"{"version":3,"elements":[{"outputFile":"app-release.apk"}]}"#;
1684 let result = builder.parse_output_metadata(metadata);
1685 assert_eq!(result, Some("app-release.apk".to_string()));
1686 }
1687
1688 #[test]
1689 fn test_parse_output_metadata_no_apk() {
1690 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1691 let metadata = r#"{"version":3,"elements":[]}"#;
1692 let result = builder.parse_output_metadata(metadata);
1693 assert_eq!(result, None);
1694 }
1695
1696 #[test]
1697 fn test_parse_output_metadata_invalid_json() {
1698 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1699 let metadata = "not valid json";
1700 let result = builder.parse_output_metadata(metadata);
1701 assert_eq!(result, None);
1702 }
1703
1704 #[test]
1705 fn test_android_builder_defaults_to_arm64_only() {
1706 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1707 let config = BuildConfig {
1708 target: Target::Android,
1709 profile: BuildProfile::Debug,
1710 incremental: true,
1711 android_abis: None,
1712 };
1713
1714 let abis = builder
1715 .resolve_android_abis(&config)
1716 .expect("resolve default ABIs");
1717 assert_eq!(abis, vec!["arm64-v8a".to_string()]);
1718 }
1719
1720 #[test]
1721 fn test_android_builder_uses_explicit_abis_when_configured() {
1722 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1723 let config = BuildConfig {
1724 target: Target::Android,
1725 profile: BuildProfile::Release,
1726 incremental: true,
1727 android_abis: Some(vec!["arm64-v8a".to_string(), "x86_64".to_string()]),
1728 };
1729
1730 let abis = builder
1731 .resolve_android_abis(&config)
1732 .expect("resolve configured ABIs");
1733 assert_eq!(abis, vec!["arm64-v8a".to_string(), "x86_64".to_string()]);
1734 }
1735
1736 #[test]
1737 fn android_native_offsets_are_symbolized_into_rust_frames() {
1738 let input = "dev.world.samplefns;uniffi.sample_fns.Sample_fnsKt.runBenchmark;libsample_fns.so[+94138] 1";
1739 let output =
1740 symbolize_android_native_stack_line_with_resolver(input, |library_name, offset| {
1741 if library_name == "libsample_fns.so" && offset == 94_138 {
1742 Some("sample_fns::fibonacci".into())
1743 } else {
1744 None
1745 }
1746 });
1747
1748 assert!(
1749 output.line.contains("sample_fns::fibonacci"),
1750 "expected unresolved native offsets to be rewritten into Rust symbols, got: {}",
1751 output.line
1752 );
1753 assert_eq!(output.resolved_frames, 1);
1754 assert_eq!(output.unresolved_frames, 0);
1755 }
1756
1757 #[test]
1758 fn resolve_android_native_symbol_with_tool_invokes_addr2line() {
1759 let temp_dir = std::env::temp_dir().join(format!(
1760 "mobench-addr2line-{}-{}",
1761 std::process::id(),
1762 std::time::SystemTime::now()
1763 .duration_since(std::time::UNIX_EPOCH)
1764 .expect("system time")
1765 .as_nanos()
1766 ));
1767 std::fs::create_dir_all(&temp_dir).expect("create temp dir");
1768 let tool_path = temp_dir.join("llvm-addr2line.sh");
1769 let args_path = temp_dir.join("args.txt");
1770 let script = format!(
1771 "#!/bin/sh\nprintf '%s\\n' \"$@\" > '{}'\nprintf '%s\\n' 'sample_fns::fibonacci at /tmp/src/lib.rs:131'\n",
1772 args_path.display()
1773 );
1774 std::fs::write(&tool_path, script).expect("write shim");
1775
1776 #[cfg(unix)]
1777 {
1778 use std::os::unix::fs::PermissionsExt;
1779 let mut perms = std::fs::metadata(&tool_path)
1780 .expect("metadata")
1781 .permissions();
1782 perms.set_mode(0o755);
1783 std::fs::set_permissions(&tool_path, perms).expect("chmod");
1784 }
1785
1786 let symbol = resolve_android_native_symbol_with_tool(
1787 &tool_path,
1788 Path::new("/cargo/target/aarch64-linux-android/release/libsample_fns.so"),
1789 94_138,
1790 );
1791
1792 assert_eq!(symbol.as_deref(), Some("sample_fns::fibonacci"));
1793
1794 let args = std::fs::read_to_string(&args_path).expect("read args");
1795 let expected_offset = format!("0x{:x}", 94_138);
1796 assert!(
1797 args.lines().any(|line| line == "-Cfpe"),
1798 "expected llvm-addr2line to be called with -Cfpe, got:\n{args}"
1799 );
1800 assert!(
1801 args.lines().any(|line| {
1802 line == "/cargo/target/aarch64-linux-android/release/libsample_fns.so"
1803 }),
1804 "expected llvm-addr2line to use the unstripped library path, got:\n{args}"
1805 );
1806 assert!(
1807 args.lines().any(|line| line == expected_offset),
1808 "expected llvm-addr2line to receive the resolved offset, got:\n{args}"
1809 );
1810 }
1811
1812 #[test]
1813 fn android_native_offsets_preserve_unresolved_frames() {
1814 let input = "dev.world.samplefns;libsample_fns.so[+94138];libother.so[+17] 1";
1815 let output =
1816 symbolize_android_native_stack_line_with_resolver(input, |library_name, offset| {
1817 if library_name == "libsample_fns.so" && offset == 94_138 {
1818 Some("sample_fns::fibonacci".into())
1819 } else {
1820 None
1821 }
1822 });
1823
1824 assert!(output.line.contains("sample_fns::fibonacci"));
1825 assert!(output.line.contains("libother.so[+17]"));
1826 assert_eq!(output.resolved_frames, 1);
1827 assert_eq!(output.unresolved_frames, 1);
1828 }
1829
1830 #[test]
1831 fn test_find_crate_dir_current_directory_is_crate() {
1832 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-current");
1834 let _ = std::fs::remove_dir_all(&temp_dir);
1835 std::fs::create_dir_all(&temp_dir).unwrap();
1836
1837 std::fs::write(
1839 temp_dir.join("Cargo.toml"),
1840 r#"[package]
1841name = "bench-mobile"
1842version = "0.1.0"
1843"#,
1844 )
1845 .unwrap();
1846
1847 let builder = AndroidBuilder::new(&temp_dir, "bench-mobile");
1848 let result = builder.find_crate_dir();
1849 assert!(result.is_ok(), "Should find crate in current directory");
1850 assert_eq!(result.unwrap(), temp_dir);
1851
1852 std::fs::remove_dir_all(&temp_dir).unwrap();
1853 }
1854
1855 #[test]
1856 fn test_find_crate_dir_nested_bench_mobile() {
1857 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-nested");
1859 let _ = std::fs::remove_dir_all(&temp_dir);
1860 std::fs::create_dir_all(temp_dir.join("bench-mobile")).unwrap();
1861
1862 std::fs::write(
1864 temp_dir.join("Cargo.toml"),
1865 r#"[workspace]
1866members = ["bench-mobile"]
1867"#,
1868 )
1869 .unwrap();
1870
1871 std::fs::write(
1873 temp_dir.join("bench-mobile/Cargo.toml"),
1874 r#"[package]
1875name = "bench-mobile"
1876version = "0.1.0"
1877"#,
1878 )
1879 .unwrap();
1880
1881 let builder = AndroidBuilder::new(&temp_dir, "bench-mobile");
1882 let result = builder.find_crate_dir();
1883 assert!(
1884 result.is_ok(),
1885 "Should find crate in bench-mobile/ directory"
1886 );
1887 assert_eq!(result.unwrap(), temp_dir.join("bench-mobile"));
1888
1889 std::fs::remove_dir_all(&temp_dir).unwrap();
1890 }
1891
1892 #[test]
1893 fn test_find_crate_dir_crates_subdir() {
1894 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-crates");
1896 let _ = std::fs::remove_dir_all(&temp_dir);
1897 std::fs::create_dir_all(temp_dir.join("crates/my-bench")).unwrap();
1898
1899 std::fs::write(
1901 temp_dir.join("Cargo.toml"),
1902 r#"[workspace]
1903members = ["crates/*"]
1904"#,
1905 )
1906 .unwrap();
1907
1908 std::fs::write(
1910 temp_dir.join("crates/my-bench/Cargo.toml"),
1911 r#"[package]
1912name = "my-bench"
1913version = "0.1.0"
1914"#,
1915 )
1916 .unwrap();
1917
1918 let builder = AndroidBuilder::new(&temp_dir, "my-bench");
1919 let result = builder.find_crate_dir();
1920 assert!(result.is_ok(), "Should find crate in crates/ directory");
1921 assert_eq!(result.unwrap(), temp_dir.join("crates/my-bench"));
1922
1923 std::fs::remove_dir_all(&temp_dir).unwrap();
1924 }
1925
1926 #[test]
1927 fn test_find_crate_dir_not_found() {
1928 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-notfound");
1930 let _ = std::fs::remove_dir_all(&temp_dir);
1931 std::fs::create_dir_all(&temp_dir).unwrap();
1932
1933 std::fs::write(
1935 temp_dir.join("Cargo.toml"),
1936 r#"[package]
1937name = "some-other-crate"
1938version = "0.1.0"
1939"#,
1940 )
1941 .unwrap();
1942
1943 let builder = AndroidBuilder::new(&temp_dir, "nonexistent-crate");
1944 let result = builder.find_crate_dir();
1945 assert!(result.is_err(), "Should fail to find nonexistent crate");
1946 let err_msg = result.unwrap_err().to_string();
1947 assert!(err_msg.contains("Benchmark crate 'nonexistent-crate' not found"));
1948 assert!(err_msg.contains("Searched locations"));
1949
1950 std::fs::remove_dir_all(&temp_dir).unwrap();
1951 }
1952
1953 #[test]
1954 fn test_find_crate_dir_explicit_crate_path() {
1955 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-explicit");
1957 let _ = std::fs::remove_dir_all(&temp_dir);
1958 std::fs::create_dir_all(temp_dir.join("custom-location")).unwrap();
1959
1960 let builder =
1961 AndroidBuilder::new(&temp_dir, "any-name").crate_dir(temp_dir.join("custom-location"));
1962 let result = builder.find_crate_dir();
1963 assert!(result.is_ok(), "Should use explicit crate_dir");
1964 assert_eq!(result.unwrap(), temp_dir.join("custom-location"));
1965
1966 std::fs::remove_dir_all(&temp_dir).unwrap();
1967 }
1968}