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