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 println!(
214 " Step 1: Build Rust libraries for Android ABIs ({})",
215 android_abis.join(", ")
216 );
217 println!(
218 " Command: cargo ndk --target <abi> --platform 24 build {}",
219 if matches!(config.profile, BuildProfile::Release) {
220 "--release"
221 } else {
222 ""
223 }
224 );
225 if self.ffi_backend.uses_uniffi() {
226 println!(" Step 2: Generate UniFFI Kotlin bindings");
227 println!(
228 " Output: {:?}",
229 android_dir.join("app/src/main/java/uniffi")
230 );
231 } else {
232 println!(
233 " Step 2: Skip UniFFI Kotlin bindings (backend: {})",
234 self.ffi_backend
235 );
236 }
237 println!(" Step 3: Copy .so files to jniLibs directories");
238 println!(
239 " Destination: {:?}",
240 android_dir.join("app/src/main/jniLibs")
241 );
242 println!(" Step 4: Build Android APK with Gradle");
243 println!(
244 " Command: ./gradlew assemble{}",
245 if profile_name == "release" {
246 "Release"
247 } else {
248 "Debug"
249 }
250 );
251 println!(
252 " Output: {:?}",
253 android_dir.join(format!(
254 "app/build/outputs/apk/{}/app-{}.apk",
255 profile_name, profile_name
256 ))
257 );
258 println!(" Step 5: Build Android test APK");
259 println!(
260 " Command: ./gradlew assemble{}AndroidTest",
261 if profile_name == "release" {
262 "Release"
263 } else {
264 "Debug"
265 }
266 );
267
268 return Ok(BuildResult {
270 platform: Target::Android,
271 app_path: android_dir.join(format!(
272 "app/build/outputs/apk/{}/app-{}.apk",
273 profile_name, profile_name
274 )),
275 test_suite_path: Some(android_dir.join(format!(
276 "app/build/outputs/apk/androidTest/{}/app-{}-androidTest.apk",
277 profile_name, profile_name
278 ))),
279 native_libraries: Vec::new(),
280 });
281 }
282
283 crate::codegen::ensure_android_project_with_backend_options(
286 &self.output_dir,
287 &self.crate_name,
288 Some(&self.project_root),
289 self.crate_dir.as_deref(),
290 self.ffi_backend,
291 )?;
292
293 self.ensure_gradle_wrapper(&android_dir)?;
295
296 println!("Building Rust libraries for Android...");
298 self.build_rust_libraries(config)?;
299
300 if self.ffi_backend.uses_uniffi() {
302 println!("Generating UniFFI Kotlin bindings...");
303 self.generate_uniffi_bindings()?;
304 } else {
305 println!(
306 "Skipping UniFFI Kotlin bindings for {} backend",
307 self.ffi_backend
308 );
309 }
310
311 println!("Copying native libraries to jniLibs...");
313 let native_libraries = self.copy_native_libraries(config)?;
314
315 println!("Building Android APK with Gradle...");
317 let apk_path = self.build_apk(config)?;
318
319 println!("Building Android test APK...");
321 let test_suite_path = self.build_test_apk(config)?;
322
323 let result = BuildResult {
325 platform: Target::Android,
326 app_path: apk_path,
327 test_suite_path: Some(test_suite_path),
328 native_libraries,
329 };
330 self.validate_build_artifacts(&result, config)?;
331
332 Ok(result)
333 }
334
335 fn validate_build_artifacts(
337 &self,
338 result: &BuildResult,
339 config: &BuildConfig,
340 ) -> Result<(), BenchError> {
341 let mut missing = Vec::new();
342 let profile_dir = match config.profile {
343 BuildProfile::Debug => "debug",
344 BuildProfile::Release => "release",
345 };
346
347 if !result.app_path.exists() {
349 missing.push(format!("Main APK: {}", result.app_path.display()));
350 }
351
352 if let Some(ref test_path) = result.test_suite_path
354 && !test_path.exists()
355 {
356 missing.push(format!("Test APK: {}", test_path.display()));
357 }
358
359 let jni_libs_dir = self.output_dir.join("android/app/src/main/jniLibs");
361 let lib_name = format!("lib{}.so", self.crate_name.replace("-", "_"));
362 let required_abis = self.resolve_android_abis(config)?;
363 let mut found_libs = 0;
364 for abi in &required_abis {
365 let lib_path = jni_libs_dir.join(abi).join(&lib_name);
366 if lib_path.exists() {
367 found_libs += 1;
368 } else {
369 missing.push(format!(
370 "Native library ({} {}): {}",
371 abi,
372 profile_dir,
373 lib_path.display()
374 ));
375 }
376 }
377
378 if found_libs == 0 {
379 return Err(BenchError::Build(format!(
380 "Build validation failed: No native libraries found.\n\n\
381 Expected at least one .so file in jniLibs directories.\n\
382 Missing artifacts:\n{}\n\n\
383 This usually means the Rust build step failed. Check the cargo-ndk output above.",
384 missing
385 .iter()
386 .map(|s| format!(" - {}", s))
387 .collect::<Vec<_>>()
388 .join("\n")
389 )));
390 }
391
392 if !missing.is_empty() {
393 eprintln!(
394 "Warning: Some build artifacts are missing:\n{}\n\
395 The build may still work but some features might be unavailable.",
396 missing
397 .iter()
398 .map(|s| format!(" - {}", s))
399 .collect::<Vec<_>>()
400 .join("\n")
401 );
402 }
403
404 Ok(())
405 }
406
407 fn resolve_android_abis(&self, config: &BuildConfig) -> Result<Vec<String>, BenchError> {
408 let requested = config
409 .android_abis
410 .as_ref()
411 .filter(|abis| !abis.is_empty())
412 .cloned()
413 .unwrap_or_else(|| {
414 DEFAULT_ANDROID_ABIS
415 .iter()
416 .map(|abi| (*abi).to_string())
417 .collect()
418 });
419
420 let mut resolved = Vec::new();
421 for abi in requested {
422 if android_abi_to_rust_target(&abi).is_none() {
423 return Err(BenchError::Build(format!(
424 "Unsupported Android ABI '{abi}'. Supported values: arm64-v8a, armeabi-v7a, x86_64"
425 )));
426 }
427 if !resolved.contains(&abi) {
428 resolved.push(abi);
429 }
430 }
431
432 Ok(resolved)
433 }
434
435 fn find_crate_dir(&self) -> Result<PathBuf, BenchError> {
443 if let Some(ref dir) = self.crate_dir {
445 if dir.exists() {
446 return Ok(dir.clone());
447 }
448 return Err(BenchError::Build(format!(
449 "Specified crate path does not exist: {:?}.\n\n\
450 Tip: pass --crate-path pointing at a directory containing Cargo.toml.",
451 dir
452 )));
453 }
454
455 let root_cargo_toml = self.project_root.join("Cargo.toml");
458 if root_cargo_toml.exists()
459 && let Some(pkg_name) = super::common::read_package_name(&root_cargo_toml)
460 && pkg_name == self.crate_name
461 {
462 return Ok(self.project_root.clone());
463 }
464
465 let bench_mobile_dir = self.project_root.join("bench-mobile");
467 if bench_mobile_dir.exists() {
468 return Ok(bench_mobile_dir);
469 }
470
471 let crates_dir = self.project_root.join("crates").join(&self.crate_name);
473 if crates_dir.exists() {
474 return Ok(crates_dir);
475 }
476
477 let named_dir = self.project_root.join(&self.crate_name);
479 if named_dir.exists() {
480 return Ok(named_dir);
481 }
482
483 let root_manifest = root_cargo_toml;
484 let bench_mobile_manifest = bench_mobile_dir.join("Cargo.toml");
485 let crates_manifest = crates_dir.join("Cargo.toml");
486 let named_manifest = named_dir.join("Cargo.toml");
487 Err(BenchError::Build(format!(
488 "Benchmark crate '{}' not found.\n\n\
489 Searched locations:\n\
490 - {} (checked [package] name)\n\
491 - {}\n\
492 - {}\n\
493 - {}\n\n\
494 To fix this:\n\
495 1. Run from the crate directory (where Cargo.toml has name = \"{}\")\n\
496 2. Create a bench-mobile/ directory with your benchmark crate, or\n\
497 3. Use --crate-path to specify the benchmark crate location:\n\
498 cargo mobench build --target android --crate-path ./my-benchmarks\n\n\
499 Common issues:\n\
500 - Typo in crate name (check Cargo.toml [package] name)\n\
501 - Wrong working directory (run from project root)\n\
502 - Missing Cargo.toml in the crate directory\n\n\
503 Run 'cargo mobench init --help' to generate a new benchmark project.",
504 self.crate_name,
505 root_manifest.display(),
506 bench_mobile_manifest.display(),
507 crates_manifest.display(),
508 named_manifest.display(),
509 self.crate_name,
510 )))
511 }
512
513 fn build_rust_libraries(&self, config: &BuildConfig) -> Result<(), BenchError> {
515 let crate_dir = self.find_crate_dir()?;
516
517 self.check_cargo_ndk()?;
519
520 let abis = self.resolve_android_abis(config)?;
521 let release_flag = if matches!(config.profile, BuildProfile::Release) {
522 "--release"
523 } else {
524 ""
525 };
526
527 for abi in abis {
528 if self.verbose {
529 println!(" Building for {}", abi);
530 }
531
532 let mut cmd = Command::new("cargo");
533 cmd.arg("ndk")
534 .arg("--target")
535 .arg(&abi)
536 .arg("--platform")
537 .arg("24") .arg("build");
539
540 if !release_flag.is_empty() {
542 cmd.arg(release_flag);
543 }
544
545 cmd.current_dir(&crate_dir);
547
548 let command_hint = if release_flag.is_empty() {
550 format!("cargo ndk --target {} --platform 24 build", abi)
551 } else {
552 format!(
553 "cargo ndk --target {} --platform 24 build {}",
554 abi, release_flag
555 )
556 };
557 let output = cmd.output().map_err(|e| {
558 BenchError::Build(format!(
559 "Failed to start cargo-ndk for {}.\n\n\
560 Command: {}\n\
561 Crate directory: {}\n\
562 System error: {}\n\n\
563 Tips:\n\
564 - Install cargo-ndk: cargo install cargo-ndk\n\
565 - Ensure cargo is on PATH",
566 abi,
567 command_hint,
568 crate_dir.display(),
569 e
570 ))
571 })?;
572
573 if !output.status.success() {
574 let stdout = String::from_utf8_lossy(&output.stdout);
575 let stderr = String::from_utf8_lossy(&output.stderr);
576 let profile = if matches!(config.profile, BuildProfile::Release) {
577 "release"
578 } else {
579 "debug"
580 };
581 let rust_target = android_abi_to_rust_target(&abi).unwrap_or(abi.as_str());
582 return Err(BenchError::Build(format!(
583 "cargo-ndk build failed for {} ({} profile).\n\n\
584 Command: {}\n\
585 Crate directory: {}\n\
586 Exit status: {}\n\n\
587 Stdout:\n{}\n\n\
588 Stderr:\n{}\n\n\
589 Common causes:\n\
590 - Missing Rust target: rustup target add {}\n\
591 - NDK not found: set ANDROID_NDK_HOME\n\
592 - Compilation error in Rust code (see output above)\n\
593 - Incompatible native dependencies (some C libraries do not support Android)",
594 abi,
595 profile,
596 command_hint,
597 crate_dir.display(),
598 output.status,
599 stdout,
600 stderr,
601 rust_target,
602 )));
603 }
604 }
605
606 Ok(())
607 }
608
609 fn check_cargo_ndk(&self) -> Result<(), BenchError> {
611 let output = Command::new("cargo").arg("ndk").arg("--version").output();
612
613 match output {
614 Ok(output) if output.status.success() => Ok(()),
615 _ => Err(BenchError::Build(
616 "cargo-ndk is not installed or not in PATH.\n\n\
617 cargo-ndk is required to cross-compile Rust for Android.\n\n\
618 To install:\n\
619 cargo install cargo-ndk\n\
620 Verify with:\n\
621 cargo ndk --version\n\n\
622 You also need the Android NDK. Set ANDROID_NDK_HOME or install via Android Studio.\n\
623 See: https://github.com/nickelc/cargo-ndk"
624 .to_string(),
625 )),
626 }
627 }
628
629 fn generate_uniffi_bindings(&self) -> Result<(), BenchError> {
631 let crate_dir = self.find_crate_dir()?;
632 let crate_name_underscored = self.crate_name.replace("-", "_");
633
634 let bindings_path = self
636 .output_dir
637 .join("android")
638 .join("app")
639 .join("src")
640 .join("main")
641 .join("java")
642 .join("uniffi")
643 .join(&crate_name_underscored)
644 .join(format!("{}.kt", crate_name_underscored));
645
646 if bindings_path.exists() {
647 if self.verbose {
648 println!(" Using existing Kotlin bindings at {:?}", bindings_path);
649 }
650 return Ok(());
651 }
652
653 let mut build_cmd = Command::new("cargo");
655 build_cmd.arg("build");
656 build_cmd.current_dir(&crate_dir);
657 run_command(build_cmd, "cargo build (host)")?;
658
659 let lib_path = host_lib_path(&crate_dir, &self.crate_name)?;
660 let out_dir = self
661 .output_dir
662 .join("android")
663 .join("app")
664 .join("src")
665 .join("main")
666 .join("java");
667
668 let cargo_run_result = Command::new("cargo")
670 .args([
671 "run",
672 "-p",
673 &self.crate_name,
674 "--bin",
675 "uniffi-bindgen",
676 "--",
677 ])
678 .arg("generate")
679 .arg("--library")
680 .arg(&lib_path)
681 .arg("--language")
682 .arg("kotlin")
683 .arg("--out-dir")
684 .arg(&out_dir)
685 .current_dir(&crate_dir)
686 .output();
687
688 let use_cargo_run = cargo_run_result
689 .as_ref()
690 .map(|o| o.status.success())
691 .unwrap_or(false);
692
693 if use_cargo_run {
694 if self.verbose {
695 println!(" Generated bindings using cargo run uniffi-bindgen");
696 }
697 } else {
698 let uniffi_available = Command::new("uniffi-bindgen")
700 .arg("--version")
701 .output()
702 .map(|o| o.status.success())
703 .unwrap_or(false);
704
705 if !uniffi_available {
706 return Err(BenchError::Build(
707 "uniffi-bindgen not found and no pre-generated bindings exist.\n\n\
708 To fix this, either:\n\
709 1. Add a uniffi-bindgen binary to your crate:\n\
710 [[bin]]\n\
711 name = \"uniffi-bindgen\"\n\
712 path = \"src/bin/uniffi-bindgen.rs\"\n\n\
713 2. Or install uniffi-bindgen globally:\n\
714 cargo install uniffi-bindgen\n\n\
715 3. Or pre-generate bindings and commit them."
716 .to_string(),
717 ));
718 }
719
720 let mut cmd = Command::new("uniffi-bindgen");
721 cmd.arg("generate")
722 .arg("--library")
723 .arg(&lib_path)
724 .arg("--language")
725 .arg("kotlin")
726 .arg("--out-dir")
727 .arg(&out_dir);
728 run_command(cmd, "uniffi-bindgen kotlin")?;
729 }
730
731 if self.verbose {
732 println!(" Generated UniFFI Kotlin bindings at {:?}", out_dir);
733 }
734 Ok(())
735 }
736
737 fn copy_native_libraries(
739 &self,
740 config: &BuildConfig,
741 ) -> Result<Vec<NativeLibraryArtifact>, BenchError> {
742 let crate_dir = self.find_crate_dir()?;
743 let profile_dir = match config.profile {
744 BuildProfile::Debug => "debug",
745 BuildProfile::Release => "release",
746 };
747
748 let target_dir = get_cargo_target_dir(&crate_dir)?;
750 let jni_libs_dir = self.output_dir.join("android/app/src/main/jniLibs");
751
752 std::fs::create_dir_all(&jni_libs_dir).map_err(|e| {
754 BenchError::Build(format!(
755 "Failed to create jniLibs directory at {}: {}. Check output directory permissions.",
756 jni_libs_dir.display(),
757 e
758 ))
759 })?;
760
761 let mut native_libraries = Vec::new();
762
763 for android_abi in self.resolve_android_abis(config)? {
764 let rust_target = android_abi_to_rust_target(&android_abi).ok_or_else(|| {
765 BenchError::Build(format!(
766 "Unsupported Android ABI '{android_abi}'. Supported values: arm64-v8a, armeabi-v7a, x86_64"
767 ))
768 })?;
769 let library_name = format!("lib{}.so", self.crate_name.replace("-", "_"));
770 let src = target_dir
771 .join(rust_target)
772 .join(profile_dir)
773 .join(&library_name);
774
775 let dest_dir = jni_libs_dir.join(&android_abi);
776 std::fs::create_dir_all(&dest_dir).map_err(|e| {
777 BenchError::Build(format!(
778 "Failed to create ABI directory {} at {}: {}. Check output directory permissions.",
779 android_abi,
780 dest_dir.display(),
781 e
782 ))
783 })?;
784
785 let dest = dest_dir.join(&library_name);
786
787 if src.exists() {
788 std::fs::copy(&src, &dest).map_err(|e| {
789 BenchError::Build(format!(
790 "Failed to copy {} library from {} to {}: {}. Ensure cargo-ndk completed successfully.",
791 android_abi,
792 src.display(),
793 dest.display(),
794 e
795 ))
796 })?;
797
798 if self.verbose {
799 println!(" Copied {} -> {}", src.display(), dest.display());
800 }
801
802 native_libraries.push(NativeLibraryArtifact {
803 abi: android_abi.clone(),
804 library_name: library_name.clone(),
805 unstripped_path: src,
806 packaged_path: dest,
807 });
808 } else {
809 eprintln!(
811 "Warning: Native library for {} not found at {}.\n\
812 This will cause a runtime crash when the app tries to load the library.\n\
813 Ensure cargo-ndk build completed successfully for this ABI.",
814 android_abi,
815 src.display()
816 );
817 }
818 }
819
820 Ok(native_libraries)
821 }
822
823 fn ensure_local_properties(&self, android_dir: &Path) -> Result<(), BenchError> {
834 let local_props = android_dir.join("local.properties");
835
836 if local_props.exists() {
838 return Ok(());
839 }
840
841 let sdk_dir = self.find_android_sdk_from_env();
844
845 match sdk_dir {
846 Some(path) => {
847 let content = format!("sdk.dir={}\n", path.display());
849 fs::write(&local_props, content).map_err(|e| {
850 BenchError::Build(format!(
851 "Failed to write local.properties at {:?}: {}. Check output directory permissions.",
852 local_props, e
853 ))
854 })?;
855
856 if self.verbose {
857 println!(
858 " Generated local.properties with sdk.dir={}",
859 path.display()
860 );
861 }
862 }
863 None => {
864 if self.verbose {
867 println!(
868 " Skipping local.properties generation (ANDROID_HOME/ANDROID_SDK_ROOT not set)"
869 );
870 println!(
871 " Gradle will auto-detect SDK or you can create local.properties manually"
872 );
873 }
874 }
875 }
876
877 Ok(())
878 }
879
880 fn find_android_sdk_from_env(&self) -> Option<PathBuf> {
888 if let Ok(path) = env::var("ANDROID_HOME") {
890 let sdk_path = PathBuf::from(&path);
891 if sdk_path.exists() {
892 return Some(sdk_path);
893 }
894 }
895
896 if let Ok(path) = env::var("ANDROID_SDK_ROOT") {
898 let sdk_path = PathBuf::from(&path);
899 if sdk_path.exists() {
900 return Some(sdk_path);
901 }
902 }
903
904 None
905 }
906
907 fn ensure_gradle_wrapper(&self, android_dir: &Path) -> Result<(), BenchError> {
912 let gradlew = android_dir.join("gradlew");
913
914 if gradlew.exists() {
916 return Ok(());
917 }
918
919 println!("Gradle wrapper not found, generating...");
920
921 let gradle_available = Command::new("gradle")
923 .arg("--version")
924 .output()
925 .map(|o| o.status.success())
926 .unwrap_or(false);
927
928 if !gradle_available {
929 return Err(BenchError::Build(
930 "Gradle wrapper (gradlew) not found and 'gradle' command is not available.\n\n\
931 The Android project requires Gradle to build. You have two options:\n\n\
932 1. Install Gradle globally and run the build again (it will auto-generate the wrapper):\n\
933 - macOS: brew install gradle\n\
934 - Linux: sudo apt install gradle\n\
935 - Or download from https://gradle.org/install/\n\n\
936 2. Or generate the wrapper manually in the Android project directory:\n\
937 cd target/mobench/android && gradle wrapper --gradle-version 8.5"
938 .to_string(),
939 ));
940 }
941
942 let mut cmd = Command::new("gradle");
944 cmd.arg("wrapper")
945 .arg("--gradle-version")
946 .arg("8.5")
947 .current_dir(android_dir);
948
949 let output = cmd.output().map_err(|e| {
950 BenchError::Build(format!(
951 "Failed to run 'gradle wrapper' command: {}\n\n\
952 Ensure Gradle is installed and on your PATH.",
953 e
954 ))
955 })?;
956
957 if !output.status.success() {
958 let stderr = String::from_utf8_lossy(&output.stderr);
959 return Err(BenchError::Build(format!(
960 "Failed to generate Gradle wrapper.\n\n\
961 Command: gradle wrapper --gradle-version 8.5\n\
962 Working directory: {}\n\
963 Exit status: {}\n\
964 Stderr: {}\n\n\
965 Try running this command manually in the Android project directory.",
966 android_dir.display(),
967 output.status,
968 stderr
969 )));
970 }
971
972 #[cfg(unix)]
974 {
975 use std::os::unix::fs::PermissionsExt;
976 if let Ok(metadata) = fs::metadata(&gradlew) {
977 let mut perms = metadata.permissions();
978 perms.set_mode(0o755);
979 let _ = fs::set_permissions(&gradlew, perms);
980 }
981 }
982
983 if self.verbose {
984 println!(" Generated Gradle wrapper at {:?}", gradlew);
985 }
986
987 Ok(())
988 }
989
990 fn build_apk(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
992 let android_dir = self.output_dir.join("android");
993
994 if !android_dir.exists() {
995 return Err(BenchError::Build(format!(
996 "Android project not found at {}.\n\n\
997 Expected a Gradle project under the output directory.\n\
998 Run `cargo mobench init --target android` or `cargo mobench build --target android` from the project root to generate it.",
999 android_dir.display()
1000 )));
1001 }
1002
1003 self.ensure_local_properties(&android_dir)?;
1005
1006 let gradle_task = match config.profile {
1008 BuildProfile::Debug => "assembleDebug",
1009 BuildProfile::Release => "assembleRelease",
1010 };
1011
1012 let mut cmd = Command::new("./gradlew");
1014 cmd.arg(gradle_task).current_dir(&android_dir);
1015
1016 if self.verbose {
1017 cmd.arg("--info");
1018 }
1019
1020 let output = cmd.output().map_err(|e| {
1021 BenchError::Build(format!(
1022 "Failed to run Gradle wrapper.\n\n\
1023 Command: ./gradlew {}\n\
1024 Working directory: {}\n\
1025 Error: {}\n\n\
1026 Tips:\n\
1027 - Ensure ./gradlew is executable (chmod +x ./gradlew)\n\
1028 - Run ./gradlew --version in that directory to verify the wrapper",
1029 gradle_task,
1030 android_dir.display(),
1031 e
1032 ))
1033 })?;
1034
1035 if !output.status.success() {
1036 let stdout = String::from_utf8_lossy(&output.stdout);
1037 let stderr = String::from_utf8_lossy(&output.stderr);
1038 return Err(BenchError::Build(format!(
1039 "Gradle build failed.\n\n\
1040 Command: ./gradlew {}\n\
1041 Working directory: {}\n\
1042 Exit status: {}\n\n\
1043 Stdout:\n{}\n\n\
1044 Stderr:\n{}\n\n\
1045 Tips:\n\
1046 - Re-run with verbose mode to pass --info to Gradle\n\
1047 - Run ./gradlew {} --stacktrace for a full stack trace",
1048 gradle_task,
1049 android_dir.display(),
1050 output.status,
1051 stdout,
1052 stderr,
1053 gradle_task,
1054 )));
1055 }
1056
1057 let profile_name = match config.profile {
1059 BuildProfile::Debug => "debug",
1060 BuildProfile::Release => "release",
1061 };
1062
1063 let apk_dir = android_dir.join("app/build/outputs/apk").join(profile_name);
1064
1065 let apk_path = self.find_apk(&apk_dir, profile_name, gradle_task)?;
1071
1072 Ok(apk_path)
1073 }
1074
1075 fn find_apk(
1085 &self,
1086 apk_dir: &Path,
1087 profile_name: &str,
1088 gradle_task: &str,
1089 ) -> Result<PathBuf, BenchError> {
1090 let metadata_path = apk_dir.join("output-metadata.json");
1092 if metadata_path.exists()
1093 && let Ok(metadata_content) = fs::read_to_string(&metadata_path)
1094 {
1095 if let Some(apk_name) = self.parse_output_metadata(&metadata_content) {
1098 let apk_path = apk_dir.join(&apk_name);
1099 if apk_path.exists() {
1100 if self.verbose {
1101 println!(
1102 " Found APK from output-metadata.json: {}",
1103 apk_path.display()
1104 );
1105 }
1106 return Ok(apk_path);
1107 }
1108 }
1109 }
1110
1111 let candidates = if profile_name == "release" {
1113 vec![
1114 format!("app-{}.apk", profile_name), format!("app-{}-unsigned.apk", profile_name), ]
1117 } else {
1118 vec![
1119 format!("app-{}.apk", profile_name), ]
1121 };
1122
1123 for candidate in &candidates {
1125 let apk_path = apk_dir.join(candidate);
1126 if apk_path.exists() {
1127 if self.verbose {
1128 println!(" Found APK: {}", apk_path.display());
1129 }
1130 return Ok(apk_path);
1131 }
1132 }
1133
1134 Err(BenchError::Build(format!(
1136 "APK not found in {}.\n\n\
1137 Gradle task {} reported success but no APK was produced.\n\
1138 Searched for:\n{}\n\n\
1139 Check the build output directory and rerun ./gradlew {} if needed.",
1140 apk_dir.display(),
1141 gradle_task,
1142 candidates
1143 .iter()
1144 .map(|c| format!(" - {}", c))
1145 .collect::<Vec<_>>()
1146 .join("\n"),
1147 gradle_task
1148 )))
1149 }
1150
1151 fn parse_output_metadata(&self, content: &str) -> Option<String> {
1165 let pattern = "\"outputFile\"";
1168 if let Some(pos) = content.find(pattern) {
1169 let after_key = &content[pos + pattern.len()..];
1170 let after_colon = after_key.trim_start().strip_prefix(':')?;
1172 let after_ws = after_colon.trim_start();
1173 if let Some(value_start) = after_ws.strip_prefix('"')
1175 && let Some(end_quote) = value_start.find('"')
1176 {
1177 let filename = &value_start[..end_quote];
1178 if filename.ends_with(".apk") {
1179 return Some(filename.to_string());
1180 }
1181 }
1182 }
1183 None
1184 }
1185
1186 fn build_test_apk(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
1188 let android_dir = self.output_dir.join("android");
1189
1190 if !android_dir.exists() {
1191 return Err(BenchError::Build(format!(
1192 "Android project not found at {}.\n\n\
1193 Expected a Gradle project under the output directory.\n\
1194 Run `cargo mobench init --target android` or `cargo mobench build --target android` from the project root to generate it.",
1195 android_dir.display()
1196 )));
1197 }
1198
1199 let gradle_task = match config.profile {
1200 BuildProfile::Debug => "assembleDebugAndroidTest",
1201 BuildProfile::Release => "assembleReleaseAndroidTest",
1202 };
1203 let profile_name = match config.profile {
1204 BuildProfile::Debug => "debug",
1205 BuildProfile::Release => "release",
1206 };
1207
1208 let mut cmd = Command::new("./gradlew");
1209 cmd.arg(format!("-PmobenchTestBuildType={profile_name}"))
1210 .arg(gradle_task)
1211 .current_dir(&android_dir);
1212
1213 if self.verbose {
1214 cmd.arg("--info");
1215 }
1216
1217 let output = cmd.output().map_err(|e| {
1218 BenchError::Build(format!(
1219 "Failed to run Gradle wrapper.\n\n\
1220 Command: ./gradlew {}\n\
1221 Working directory: {}\n\
1222 Error: {}\n\n\
1223 Tips:\n\
1224 - Ensure ./gradlew is executable (chmod +x ./gradlew)\n\
1225 - Run ./gradlew --version in that directory to verify the wrapper",
1226 gradle_task,
1227 android_dir.display(),
1228 e
1229 ))
1230 })?;
1231
1232 if !output.status.success() {
1233 let stdout = String::from_utf8_lossy(&output.stdout);
1234 let stderr = String::from_utf8_lossy(&output.stderr);
1235 return Err(BenchError::Build(format!(
1236 "Gradle test APK build failed.\n\n\
1237 Command: ./gradlew {}\n\
1238 Working directory: {}\n\
1239 Exit status: {}\n\n\
1240 Stdout:\n{}\n\n\
1241 Stderr:\n{}\n\n\
1242 Tips:\n\
1243 - Re-run with verbose mode to pass --info to Gradle\n\
1244 - Run ./gradlew {} --stacktrace for a full stack trace",
1245 gradle_task,
1246 android_dir.display(),
1247 output.status,
1248 stdout,
1249 stderr,
1250 gradle_task,
1251 )));
1252 }
1253
1254 let test_apk_dir = android_dir
1255 .join("app/build/outputs/apk/androidTest")
1256 .join(profile_name);
1257
1258 let apk_path = self.find_test_apk(&test_apk_dir, profile_name, gradle_task)?;
1260
1261 Ok(apk_path)
1262 }
1263
1264 fn find_test_apk(
1270 &self,
1271 apk_dir: &Path,
1272 profile_name: &str,
1273 gradle_task: &str,
1274 ) -> Result<PathBuf, BenchError> {
1275 let metadata_path = apk_dir.join("output-metadata.json");
1277 if metadata_path.exists()
1278 && let Ok(metadata_content) = fs::read_to_string(&metadata_path)
1279 && let Some(apk_name) = self.parse_output_metadata(&metadata_content)
1280 {
1281 let apk_path = apk_dir.join(&apk_name);
1282 if apk_path.exists() {
1283 if self.verbose {
1284 println!(
1285 " Found test APK from output-metadata.json: {}",
1286 apk_path.display()
1287 );
1288 }
1289 return Ok(apk_path);
1290 }
1291 }
1292
1293 let apk_path = apk_dir.join(format!("app-{}-androidTest.apk", profile_name));
1295 if apk_path.exists() {
1296 if self.verbose {
1297 println!(" Found test APK: {}", apk_path.display());
1298 }
1299 return Ok(apk_path);
1300 }
1301
1302 Err(BenchError::Build(format!(
1304 "Android test APK not found in {}.\n\n\
1305 Gradle task {} reported success but no test APK was produced.\n\
1306 Expected: app-{}-androidTest.apk\n\n\
1307 Check app/build/outputs/apk/androidTest/{} and rerun ./gradlew {} if needed.",
1308 apk_dir.display(),
1309 gradle_task,
1310 profile_name,
1311 profile_name,
1312 gradle_task
1313 )))
1314 }
1315}
1316
1317fn android_abi_to_rust_target(abi: &str) -> Option<&'static str> {
1318 match abi {
1319 "arm64-v8a" => Some("aarch64-linux-android"),
1320 "armeabi-v7a" => Some("armv7-linux-androideabi"),
1321 "x86_64" => Some("x86_64-linux-android"),
1322 _ => None,
1323 }
1324}
1325
1326#[derive(Debug, Clone, PartialEq, Eq)]
1327pub struct AndroidStackSymbolization {
1328 pub line: String,
1329 pub resolved_frames: u64,
1330 pub unresolved_frames: u64,
1331}
1332
1333pub fn symbolize_android_native_stack_line_with_resolver<F>(
1334 line: &str,
1335 mut resolve: F,
1336) -> AndroidStackSymbolization
1337where
1338 F: FnMut(&str, u64) -> Option<String>,
1339{
1340 let (stack, sample_count) = split_folded_stack_line(line);
1341 let mut resolved_frames = 0;
1342 let mut unresolved_frames = 0;
1343 let rewritten = stack
1344 .split(';')
1345 .map(|frame| {
1346 if let Some((library_name, offset)) = parse_android_native_offset_frame(frame) {
1347 if let Some(symbol) = resolve(library_name, offset) {
1348 resolved_frames += 1;
1349 return symbol;
1350 }
1351 unresolved_frames += 1;
1352 }
1353 frame.to_string()
1354 })
1355 .collect::<Vec<_>>()
1356 .join(";");
1357
1358 let line = match sample_count {
1359 Some(count) => format!("{rewritten} {count}"),
1360 None => rewritten,
1361 };
1362
1363 AndroidStackSymbolization {
1364 line,
1365 resolved_frames,
1366 unresolved_frames,
1367 }
1368}
1369
1370pub fn resolve_android_native_symbol_with_addr2line(
1371 library_path: &Path,
1372 offset: u64,
1373) -> Option<String> {
1374 let tool_path = locate_android_addr2line_tool_path()?;
1375 resolve_android_native_symbol_with_tool(&tool_path, library_path, offset)
1376}
1377
1378pub fn resolve_android_native_symbol_with_tool(
1379 tool_path: &Path,
1380 library_path: &Path,
1381 offset: u64,
1382) -> Option<String> {
1383 let output = Command::new(tool_path)
1384 .args(["-Cfpe"])
1385 .arg(library_path)
1386 .arg(format!("0x{offset:x}"))
1387 .output()
1388 .ok()?;
1389 if !output.status.success() {
1390 return None;
1391 }
1392
1393 parse_android_addr2line_stdout(&String::from_utf8_lossy(&output.stdout))
1394}
1395
1396fn parse_android_addr2line_stdout(stdout: &str) -> Option<String> {
1397 stdout.lines().find_map(|line| {
1398 let symbol = line.trim();
1399 if symbol.is_empty() || symbol == "??" || symbol.starts_with("?? ") {
1400 None
1401 } else {
1402 Some(
1403 symbol
1404 .split(" at ")
1405 .next()
1406 .unwrap_or(symbol)
1407 .trim()
1408 .to_owned(),
1409 )
1410 }
1411 })
1412}
1413
1414fn locate_android_addr2line_tool_path() -> Option<PathBuf> {
1415 let override_path = std::env::var_os("MOBENCH_ANDROID_LLVM_ADDR2LINE")
1416 .or_else(|| std::env::var_os("LLVM_ADDR2LINE"))
1417 .map(PathBuf::from);
1418 if let Some(path) = override_path {
1419 return path.exists().then_some(path);
1420 }
1421
1422 let sdk_root = std::env::var_os("ANDROID_HOME")
1423 .map(PathBuf::from)
1424 .or_else(|| std::env::var_os("ANDROID_SDK_ROOT").map(PathBuf::from))
1425 .or_else(|| {
1426 std::env::var_os("ANDROID_NDK_HOME")
1427 .map(PathBuf::from)
1428 .and_then(|ndk_home| ndk_home.parent().and_then(Path::parent).map(PathBuf::from))
1429 })?;
1430 let ndk_root = std::env::var_os("ANDROID_NDK_HOME")
1431 .map(PathBuf::from)
1432 .or_else(|| {
1433 let ndk_dir = sdk_root.join("ndk");
1434 std::fs::read_dir(&ndk_dir).ok().and_then(|entries| {
1435 entries
1436 .filter_map(|entry| entry.ok())
1437 .map(|entry| entry.path())
1438 .filter(|path| path.is_dir())
1439 .max()
1440 })
1441 })?;
1442
1443 let tool_name = if cfg!(windows) {
1444 "llvm-addr2line.exe"
1445 } else {
1446 "llvm-addr2line"
1447 };
1448 let prebuilt_root = ndk_root.join("toolchains").join("llvm").join("prebuilt");
1449 let mut candidates = Vec::new();
1450 if let Ok(entries) = std::fs::read_dir(&prebuilt_root) {
1451 for entry in entries.flatten() {
1452 let candidate = entry.path().join("bin").join(tool_name);
1453 if candidate.exists() {
1454 candidates.push(candidate);
1455 }
1456 }
1457 }
1458 candidates.sort();
1459 candidates.into_iter().next()
1460}
1461
1462fn split_folded_stack_line(line: &str) -> (&str, Option<&str>) {
1463 match line.rsplit_once(' ') {
1464 Some((stack, count))
1465 if !stack.is_empty() && count.chars().all(|ch| ch.is_ascii_digit()) =>
1466 {
1467 (stack, Some(count))
1468 }
1469 _ => (line, None),
1470 }
1471}
1472
1473fn parse_android_native_offset_frame(frame: &str) -> Option<(&str, u64)> {
1474 let marker = ".so[+";
1475 let marker_index = frame.find(marker)?;
1476 let library_end = marker_index + 3;
1477 let library_name = frame[..library_end].rsplit('/').next()?;
1478 let offset_start = marker_index + marker.len();
1479 let offset_end = frame[offset_start..].find(']')? + offset_start;
1480 let offset_raw = &frame[offset_start..offset_end];
1481 let offset = if let Some(hex) = offset_raw.strip_prefix("0x") {
1482 u64::from_str_radix(hex, 16).ok()?
1483 } else {
1484 offset_raw.parse().ok()?
1485 };
1486 Some((library_name, offset))
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use super::*;
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 test_parse_output_metadata_unsigned() {
1518 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1519 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"}"#;
1520 let result = builder.parse_output_metadata(metadata);
1521 assert_eq!(result, Some("app-release-unsigned.apk".to_string()));
1522 }
1523
1524 #[test]
1525 fn test_parse_output_metadata_signed() {
1526 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1527 let metadata = r#"{"version":3,"elements":[{"outputFile":"app-release.apk"}]}"#;
1528 let result = builder.parse_output_metadata(metadata);
1529 assert_eq!(result, Some("app-release.apk".to_string()));
1530 }
1531
1532 #[test]
1533 fn test_parse_output_metadata_no_apk() {
1534 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1535 let metadata = r#"{"version":3,"elements":[]}"#;
1536 let result = builder.parse_output_metadata(metadata);
1537 assert_eq!(result, None);
1538 }
1539
1540 #[test]
1541 fn test_parse_output_metadata_invalid_json() {
1542 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1543 let metadata = "not valid json";
1544 let result = builder.parse_output_metadata(metadata);
1545 assert_eq!(result, None);
1546 }
1547
1548 #[test]
1549 fn test_android_builder_defaults_to_arm64_only() {
1550 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1551 let config = BuildConfig {
1552 target: Target::Android,
1553 profile: BuildProfile::Debug,
1554 incremental: true,
1555 android_abis: None,
1556 };
1557
1558 let abis = builder
1559 .resolve_android_abis(&config)
1560 .expect("resolve default ABIs");
1561 assert_eq!(abis, vec!["arm64-v8a".to_string()]);
1562 }
1563
1564 #[test]
1565 fn test_android_builder_uses_explicit_abis_when_configured() {
1566 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1567 let config = BuildConfig {
1568 target: Target::Android,
1569 profile: BuildProfile::Release,
1570 incremental: true,
1571 android_abis: Some(vec!["arm64-v8a".to_string(), "x86_64".to_string()]),
1572 };
1573
1574 let abis = builder
1575 .resolve_android_abis(&config)
1576 .expect("resolve configured ABIs");
1577 assert_eq!(abis, vec!["arm64-v8a".to_string(), "x86_64".to_string()]);
1578 }
1579
1580 #[test]
1581 fn android_native_offsets_are_symbolized_into_rust_frames() {
1582 let input = "dev.world.samplefns;uniffi.sample_fns.Sample_fnsKt.runBenchmark;libsample_fns.so[+94138] 1";
1583 let output =
1584 symbolize_android_native_stack_line_with_resolver(input, |library_name, offset| {
1585 if library_name == "libsample_fns.so" && offset == 94_138 {
1586 Some("sample_fns::fibonacci".into())
1587 } else {
1588 None
1589 }
1590 });
1591
1592 assert!(
1593 output.line.contains("sample_fns::fibonacci"),
1594 "expected unresolved native offsets to be rewritten into Rust symbols, got: {}",
1595 output.line
1596 );
1597 assert_eq!(output.resolved_frames, 1);
1598 assert_eq!(output.unresolved_frames, 0);
1599 }
1600
1601 #[test]
1602 fn resolve_android_native_symbol_with_tool_invokes_addr2line() {
1603 let temp_dir = std::env::temp_dir().join(format!(
1604 "mobench-addr2line-{}-{}",
1605 std::process::id(),
1606 std::time::SystemTime::now()
1607 .duration_since(std::time::UNIX_EPOCH)
1608 .expect("system time")
1609 .as_nanos()
1610 ));
1611 std::fs::create_dir_all(&temp_dir).expect("create temp dir");
1612 let tool_path = temp_dir.join("llvm-addr2line.sh");
1613 let args_path = temp_dir.join("args.txt");
1614 let script = format!(
1615 "#!/bin/sh\nprintf '%s\\n' \"$@\" > '{}'\nprintf '%s\\n' 'sample_fns::fibonacci at /tmp/src/lib.rs:131'\n",
1616 args_path.display()
1617 );
1618 std::fs::write(&tool_path, script).expect("write shim");
1619
1620 #[cfg(unix)]
1621 {
1622 use std::os::unix::fs::PermissionsExt;
1623 let mut perms = std::fs::metadata(&tool_path)
1624 .expect("metadata")
1625 .permissions();
1626 perms.set_mode(0o755);
1627 std::fs::set_permissions(&tool_path, perms).expect("chmod");
1628 }
1629
1630 let symbol = resolve_android_native_symbol_with_tool(
1631 &tool_path,
1632 Path::new("/cargo/target/aarch64-linux-android/release/libsample_fns.so"),
1633 94_138,
1634 );
1635
1636 assert_eq!(symbol.as_deref(), Some("sample_fns::fibonacci"));
1637
1638 let args = std::fs::read_to_string(&args_path).expect("read args");
1639 let expected_offset = format!("0x{:x}", 94_138);
1640 assert!(
1641 args.lines().any(|line| line == "-Cfpe"),
1642 "expected llvm-addr2line to be called with -Cfpe, got:\n{args}"
1643 );
1644 assert!(
1645 args.lines().any(|line| {
1646 line == "/cargo/target/aarch64-linux-android/release/libsample_fns.so"
1647 }),
1648 "expected llvm-addr2line to use the unstripped library path, got:\n{args}"
1649 );
1650 assert!(
1651 args.lines().any(|line| line == expected_offset),
1652 "expected llvm-addr2line to receive the resolved offset, got:\n{args}"
1653 );
1654 }
1655
1656 #[test]
1657 fn android_native_offsets_preserve_unresolved_frames() {
1658 let input = "dev.world.samplefns;libsample_fns.so[+94138];libother.so[+17] 1";
1659 let output =
1660 symbolize_android_native_stack_line_with_resolver(input, |library_name, offset| {
1661 if library_name == "libsample_fns.so" && offset == 94_138 {
1662 Some("sample_fns::fibonacci".into())
1663 } else {
1664 None
1665 }
1666 });
1667
1668 assert!(output.line.contains("sample_fns::fibonacci"));
1669 assert!(output.line.contains("libother.so[+17]"));
1670 assert_eq!(output.resolved_frames, 1);
1671 assert_eq!(output.unresolved_frames, 1);
1672 }
1673
1674 #[test]
1675 fn test_find_crate_dir_current_directory_is_crate() {
1676 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-current");
1678 let _ = std::fs::remove_dir_all(&temp_dir);
1679 std::fs::create_dir_all(&temp_dir).unwrap();
1680
1681 std::fs::write(
1683 temp_dir.join("Cargo.toml"),
1684 r#"[package]
1685name = "bench-mobile"
1686version = "0.1.0"
1687"#,
1688 )
1689 .unwrap();
1690
1691 let builder = AndroidBuilder::new(&temp_dir, "bench-mobile");
1692 let result = builder.find_crate_dir();
1693 assert!(result.is_ok(), "Should find crate in current directory");
1694 assert_eq!(result.unwrap(), temp_dir);
1695
1696 std::fs::remove_dir_all(&temp_dir).unwrap();
1697 }
1698
1699 #[test]
1700 fn test_find_crate_dir_nested_bench_mobile() {
1701 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-nested");
1703 let _ = std::fs::remove_dir_all(&temp_dir);
1704 std::fs::create_dir_all(temp_dir.join("bench-mobile")).unwrap();
1705
1706 std::fs::write(
1708 temp_dir.join("Cargo.toml"),
1709 r#"[workspace]
1710members = ["bench-mobile"]
1711"#,
1712 )
1713 .unwrap();
1714
1715 std::fs::write(
1717 temp_dir.join("bench-mobile/Cargo.toml"),
1718 r#"[package]
1719name = "bench-mobile"
1720version = "0.1.0"
1721"#,
1722 )
1723 .unwrap();
1724
1725 let builder = AndroidBuilder::new(&temp_dir, "bench-mobile");
1726 let result = builder.find_crate_dir();
1727 assert!(
1728 result.is_ok(),
1729 "Should find crate in bench-mobile/ directory"
1730 );
1731 assert_eq!(result.unwrap(), temp_dir.join("bench-mobile"));
1732
1733 std::fs::remove_dir_all(&temp_dir).unwrap();
1734 }
1735
1736 #[test]
1737 fn test_find_crate_dir_crates_subdir() {
1738 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-crates");
1740 let _ = std::fs::remove_dir_all(&temp_dir);
1741 std::fs::create_dir_all(temp_dir.join("crates/my-bench")).unwrap();
1742
1743 std::fs::write(
1745 temp_dir.join("Cargo.toml"),
1746 r#"[workspace]
1747members = ["crates/*"]
1748"#,
1749 )
1750 .unwrap();
1751
1752 std::fs::write(
1754 temp_dir.join("crates/my-bench/Cargo.toml"),
1755 r#"[package]
1756name = "my-bench"
1757version = "0.1.0"
1758"#,
1759 )
1760 .unwrap();
1761
1762 let builder = AndroidBuilder::new(&temp_dir, "my-bench");
1763 let result = builder.find_crate_dir();
1764 assert!(result.is_ok(), "Should find crate in crates/ directory");
1765 assert_eq!(result.unwrap(), temp_dir.join("crates/my-bench"));
1766
1767 std::fs::remove_dir_all(&temp_dir).unwrap();
1768 }
1769
1770 #[test]
1771 fn test_find_crate_dir_not_found() {
1772 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-notfound");
1774 let _ = std::fs::remove_dir_all(&temp_dir);
1775 std::fs::create_dir_all(&temp_dir).unwrap();
1776
1777 std::fs::write(
1779 temp_dir.join("Cargo.toml"),
1780 r#"[package]
1781name = "some-other-crate"
1782version = "0.1.0"
1783"#,
1784 )
1785 .unwrap();
1786
1787 let builder = AndroidBuilder::new(&temp_dir, "nonexistent-crate");
1788 let result = builder.find_crate_dir();
1789 assert!(result.is_err(), "Should fail to find nonexistent crate");
1790 let err_msg = result.unwrap_err().to_string();
1791 assert!(err_msg.contains("Benchmark crate 'nonexistent-crate' not found"));
1792 assert!(err_msg.contains("Searched locations"));
1793
1794 std::fs::remove_dir_all(&temp_dir).unwrap();
1795 }
1796
1797 #[test]
1798 fn test_find_crate_dir_explicit_crate_path() {
1799 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-explicit");
1801 let _ = std::fs::remove_dir_all(&temp_dir);
1802 std::fs::create_dir_all(temp_dir.join("custom-location")).unwrap();
1803
1804 let builder =
1805 AndroidBuilder::new(&temp_dir, "any-name").crate_dir(temp_dir.join("custom-location"));
1806 let result = builder.find_crate_dir();
1807 assert!(result.is_ok(), "Should use explicit crate_dir");
1808 assert_eq!(result.unwrap(), temp_dir.join("custom-location"));
1809
1810 std::fs::remove_dir_all(&temp_dir).unwrap();
1811 }
1812}