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