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}
109
110const DEFAULT_ANDROID_ABIS: &[&str] = &["arm64-v8a"];
111
112impl AndroidBuilder {
113 pub fn new(project_root: impl Into<PathBuf>, crate_name: impl Into<String>) -> Self {
120 let root = project_root.into();
121 Self {
122 output_dir: root.join("target/mobench"),
123 project_root: root,
124 crate_name: crate_name.into(),
125 verbose: false,
126 crate_dir: None,
127 dry_run: false,
128 }
129 }
130
131 pub fn output_dir(mut self, dir: impl Into<PathBuf>) -> Self {
136 self.output_dir = dir.into();
137 self
138 }
139
140 pub fn crate_dir(mut self, dir: impl Into<PathBuf>) -> Self {
150 self.crate_dir = Some(dir.into());
151 self
152 }
153
154 pub fn verbose(mut self, verbose: bool) -> Self {
156 self.verbose = verbose;
157 self
158 }
159
160 pub fn dry_run(mut self, dry_run: bool) -> Self {
165 self.dry_run = dry_run;
166 self
167 }
168
169 pub fn build(&self, config: &BuildConfig) -> Result<BuildResult, BenchError> {
183 if self.crate_dir.is_none() {
185 validate_project_root(&self.project_root, &self.crate_name)?;
186 }
187
188 let android_dir = self.output_dir.join("android");
189 let profile_name = match config.profile {
190 BuildProfile::Debug => "debug",
191 BuildProfile::Release => "release",
192 };
193 let android_abis = self.resolve_android_abis(config)?;
194
195 if self.dry_run {
196 println!("\n[dry-run] Android build plan:");
197 println!(
198 " Step 0: Check/generate Android project scaffolding at {:?}",
199 android_dir
200 );
201 println!(" Step 0.5: Ensure Gradle wrapper exists (run 'gradle wrapper' if needed)");
202 println!(
203 " Step 1: Build Rust libraries for Android ABIs ({})",
204 android_abis.join(", ")
205 );
206 println!(
207 " Command: cargo ndk --target <abi> --platform 24 build {}",
208 if matches!(config.profile, BuildProfile::Release) {
209 "--release"
210 } else {
211 ""
212 }
213 );
214 println!(" Step 2: Generate UniFFI Kotlin bindings");
215 println!(
216 " Output: {:?}",
217 android_dir.join("app/src/main/java/uniffi")
218 );
219 println!(" Step 3: Copy .so files to jniLibs directories");
220 println!(
221 " Destination: {:?}",
222 android_dir.join("app/src/main/jniLibs")
223 );
224 println!(" Step 4: Build Android APK with Gradle");
225 println!(
226 " Command: ./gradlew assemble{}",
227 if profile_name == "release" {
228 "Release"
229 } else {
230 "Debug"
231 }
232 );
233 println!(
234 " Output: {:?}",
235 android_dir.join(format!(
236 "app/build/outputs/apk/{}/app-{}.apk",
237 profile_name, profile_name
238 ))
239 );
240 println!(" Step 5: Build Android test APK");
241 println!(
242 " Command: ./gradlew assemble{}AndroidTest",
243 if profile_name == "release" {
244 "Release"
245 } else {
246 "Debug"
247 }
248 );
249
250 return Ok(BuildResult {
252 platform: Target::Android,
253 app_path: android_dir.join(format!(
254 "app/build/outputs/apk/{}/app-{}.apk",
255 profile_name, profile_name
256 )),
257 test_suite_path: Some(android_dir.join(format!(
258 "app/build/outputs/apk/androidTest/{}/app-{}-androidTest.apk",
259 profile_name, profile_name
260 ))),
261 native_libraries: Vec::new(),
262 });
263 }
264
265 crate::codegen::ensure_android_project_with_options(
268 &self.output_dir,
269 &self.crate_name,
270 Some(&self.project_root),
271 self.crate_dir.as_deref(),
272 )?;
273
274 self.ensure_gradle_wrapper(&android_dir)?;
276
277 println!("Building Rust libraries for Android...");
279 self.build_rust_libraries(config)?;
280
281 println!("Generating UniFFI Kotlin bindings...");
283 self.generate_uniffi_bindings()?;
284
285 println!("Copying native libraries to jniLibs...");
287 let native_libraries = self.copy_native_libraries(config)?;
288
289 println!("Building Android APK with Gradle...");
291 let apk_path = self.build_apk(config)?;
292
293 println!("Building Android test APK...");
295 let test_suite_path = self.build_test_apk(config)?;
296
297 let result = BuildResult {
299 platform: Target::Android,
300 app_path: apk_path,
301 test_suite_path: Some(test_suite_path),
302 native_libraries,
303 };
304 self.validate_build_artifacts(&result, config)?;
305
306 Ok(result)
307 }
308
309 fn validate_build_artifacts(
311 &self,
312 result: &BuildResult,
313 config: &BuildConfig,
314 ) -> Result<(), BenchError> {
315 let mut missing = Vec::new();
316 let profile_dir = match config.profile {
317 BuildProfile::Debug => "debug",
318 BuildProfile::Release => "release",
319 };
320
321 if !result.app_path.exists() {
323 missing.push(format!("Main APK: {}", result.app_path.display()));
324 }
325
326 if let Some(ref test_path) = result.test_suite_path {
328 if !test_path.exists() {
329 missing.push(format!("Test APK: {}", test_path.display()));
330 }
331 }
332
333 let jni_libs_dir = self.output_dir.join("android/app/src/main/jniLibs");
335 let lib_name = format!("lib{}.so", self.crate_name.replace("-", "_"));
336 let required_abis = self.resolve_android_abis(config)?;
337 let mut found_libs = 0;
338 for abi in &required_abis {
339 let lib_path = jni_libs_dir.join(abi).join(&lib_name);
340 if lib_path.exists() {
341 found_libs += 1;
342 } else {
343 missing.push(format!(
344 "Native library ({} {}): {}",
345 abi,
346 profile_dir,
347 lib_path.display()
348 ));
349 }
350 }
351
352 if found_libs == 0 {
353 return Err(BenchError::Build(format!(
354 "Build validation failed: No native libraries found.\n\n\
355 Expected at least one .so file in jniLibs directories.\n\
356 Missing artifacts:\n{}\n\n\
357 This usually means the Rust build step failed. Check the cargo-ndk output above.",
358 missing
359 .iter()
360 .map(|s| format!(" - {}", s))
361 .collect::<Vec<_>>()
362 .join("\n")
363 )));
364 }
365
366 if !missing.is_empty() {
367 eprintln!(
368 "Warning: Some build artifacts are missing:\n{}\n\
369 The build may still work but some features might be unavailable.",
370 missing
371 .iter()
372 .map(|s| format!(" - {}", s))
373 .collect::<Vec<_>>()
374 .join("\n")
375 );
376 }
377
378 Ok(())
379 }
380
381 fn resolve_android_abis(&self, config: &BuildConfig) -> Result<Vec<String>, BenchError> {
382 let requested = config
383 .android_abis
384 .as_ref()
385 .filter(|abis| !abis.is_empty())
386 .cloned()
387 .unwrap_or_else(|| {
388 DEFAULT_ANDROID_ABIS
389 .iter()
390 .map(|abi| (*abi).to_string())
391 .collect()
392 });
393
394 let mut resolved = Vec::new();
395 for abi in requested {
396 if android_abi_to_rust_target(&abi).is_none() {
397 return Err(BenchError::Build(format!(
398 "Unsupported Android ABI '{abi}'. Supported values: arm64-v8a, armeabi-v7a, x86_64"
399 )));
400 }
401 if !resolved.contains(&abi) {
402 resolved.push(abi);
403 }
404 }
405
406 Ok(resolved)
407 }
408
409 fn find_crate_dir(&self) -> Result<PathBuf, BenchError> {
417 if let Some(ref dir) = self.crate_dir {
419 if dir.exists() {
420 return Ok(dir.clone());
421 }
422 return Err(BenchError::Build(format!(
423 "Specified crate path does not exist: {:?}.\n\n\
424 Tip: pass --crate-path pointing at a directory containing Cargo.toml.",
425 dir
426 )));
427 }
428
429 let root_cargo_toml = self.project_root.join("Cargo.toml");
432 if root_cargo_toml.exists() {
433 if let Some(pkg_name) = super::common::read_package_name(&root_cargo_toml) {
434 if pkg_name == self.crate_name {
435 return Ok(self.project_root.clone());
436 }
437 }
438 }
439
440 let bench_mobile_dir = self.project_root.join("bench-mobile");
442 if bench_mobile_dir.exists() {
443 return Ok(bench_mobile_dir);
444 }
445
446 let crates_dir = self.project_root.join("crates").join(&self.crate_name);
448 if crates_dir.exists() {
449 return Ok(crates_dir);
450 }
451
452 let named_dir = self.project_root.join(&self.crate_name);
454 if named_dir.exists() {
455 return Ok(named_dir);
456 }
457
458 let root_manifest = root_cargo_toml;
459 let bench_mobile_manifest = bench_mobile_dir.join("Cargo.toml");
460 let crates_manifest = crates_dir.join("Cargo.toml");
461 let named_manifest = named_dir.join("Cargo.toml");
462 Err(BenchError::Build(format!(
463 "Benchmark crate '{}' not found.\n\n\
464 Searched locations:\n\
465 - {} (checked [package] name)\n\
466 - {}\n\
467 - {}\n\
468 - {}\n\n\
469 To fix this:\n\
470 1. Run from the crate directory (where Cargo.toml has name = \"{}\")\n\
471 2. Create a bench-mobile/ directory with your benchmark crate, or\n\
472 3. Use --crate-path to specify the benchmark crate location:\n\
473 cargo mobench build --target android --crate-path ./my-benchmarks\n\n\
474 Common issues:\n\
475 - Typo in crate name (check Cargo.toml [package] name)\n\
476 - Wrong working directory (run from project root)\n\
477 - Missing Cargo.toml in the crate directory\n\n\
478 Run 'cargo mobench init --help' to generate a new benchmark project.",
479 self.crate_name,
480 root_manifest.display(),
481 bench_mobile_manifest.display(),
482 crates_manifest.display(),
483 named_manifest.display(),
484 self.crate_name,
485 )))
486 }
487
488 fn build_rust_libraries(&self, config: &BuildConfig) -> Result<(), BenchError> {
490 let crate_dir = self.find_crate_dir()?;
491
492 self.check_cargo_ndk()?;
494
495 let abis = self.resolve_android_abis(config)?;
496 let release_flag = if matches!(config.profile, BuildProfile::Release) {
497 "--release"
498 } else {
499 ""
500 };
501
502 for abi in abis {
503 if self.verbose {
504 println!(" Building for {}", abi);
505 }
506
507 let mut cmd = Command::new("cargo");
508 cmd.arg("ndk")
509 .arg("--target")
510 .arg(&abi)
511 .arg("--platform")
512 .arg("24") .arg("build");
514
515 if !release_flag.is_empty() {
517 cmd.arg(release_flag);
518 }
519
520 cmd.current_dir(&crate_dir);
522
523 let command_hint = if release_flag.is_empty() {
525 format!("cargo ndk --target {} --platform 24 build", abi)
526 } else {
527 format!(
528 "cargo ndk --target {} --platform 24 build {}",
529 abi, release_flag
530 )
531 };
532 let output = cmd.output().map_err(|e| {
533 BenchError::Build(format!(
534 "Failed to start cargo-ndk for {}.\n\n\
535 Command: {}\n\
536 Crate directory: {}\n\
537 System error: {}\n\n\
538 Tips:\n\
539 - Install cargo-ndk: cargo install cargo-ndk\n\
540 - Ensure cargo is on PATH",
541 abi,
542 command_hint,
543 crate_dir.display(),
544 e
545 ))
546 })?;
547
548 if !output.status.success() {
549 let stdout = String::from_utf8_lossy(&output.stdout);
550 let stderr = String::from_utf8_lossy(&output.stderr);
551 let profile = if matches!(config.profile, BuildProfile::Release) {
552 "release"
553 } else {
554 "debug"
555 };
556 let rust_target = android_abi_to_rust_target(&abi).unwrap_or(abi.as_str());
557 return Err(BenchError::Build(format!(
558 "cargo-ndk build failed for {} ({} profile).\n\n\
559 Command: {}\n\
560 Crate directory: {}\n\
561 Exit status: {}\n\n\
562 Stdout:\n{}\n\n\
563 Stderr:\n{}\n\n\
564 Common causes:\n\
565 - Missing Rust target: rustup target add {}\n\
566 - NDK not found: set ANDROID_NDK_HOME\n\
567 - Compilation error in Rust code (see output above)\n\
568 - Incompatible native dependencies (some C libraries do not support Android)",
569 abi,
570 profile,
571 command_hint,
572 crate_dir.display(),
573 output.status,
574 stdout,
575 stderr,
576 rust_target,
577 )));
578 }
579 }
580
581 Ok(())
582 }
583
584 fn check_cargo_ndk(&self) -> Result<(), BenchError> {
586 let output = Command::new("cargo").arg("ndk").arg("--version").output();
587
588 match output {
589 Ok(output) if output.status.success() => Ok(()),
590 _ => Err(BenchError::Build(
591 "cargo-ndk is not installed or not in PATH.\n\n\
592 cargo-ndk is required to cross-compile Rust for Android.\n\n\
593 To install:\n\
594 cargo install cargo-ndk\n\
595 Verify with:\n\
596 cargo ndk --version\n\n\
597 You also need the Android NDK. Set ANDROID_NDK_HOME or install via Android Studio.\n\
598 See: https://github.com/nickelc/cargo-ndk"
599 .to_string(),
600 )),
601 }
602 }
603
604 fn generate_uniffi_bindings(&self) -> Result<(), BenchError> {
606 let crate_dir = self.find_crate_dir()?;
607 let crate_name_underscored = self.crate_name.replace("-", "_");
608
609 let bindings_path = self
611 .output_dir
612 .join("android")
613 .join("app")
614 .join("src")
615 .join("main")
616 .join("java")
617 .join("uniffi")
618 .join(&crate_name_underscored)
619 .join(format!("{}.kt", crate_name_underscored));
620
621 if bindings_path.exists() {
622 if self.verbose {
623 println!(" Using existing Kotlin bindings at {:?}", bindings_path);
624 }
625 return Ok(());
626 }
627
628 let mut build_cmd = Command::new("cargo");
630 build_cmd.arg("build");
631 build_cmd.current_dir(&crate_dir);
632 run_command(build_cmd, "cargo build (host)")?;
633
634 let lib_path = host_lib_path(&crate_dir, &self.crate_name)?;
635 let out_dir = self
636 .output_dir
637 .join("android")
638 .join("app")
639 .join("src")
640 .join("main")
641 .join("java");
642
643 let cargo_run_result = Command::new("cargo")
645 .args([
646 "run",
647 "-p",
648 &self.crate_name,
649 "--bin",
650 "uniffi-bindgen",
651 "--",
652 ])
653 .arg("generate")
654 .arg("--library")
655 .arg(&lib_path)
656 .arg("--language")
657 .arg("kotlin")
658 .arg("--out-dir")
659 .arg(&out_dir)
660 .current_dir(&crate_dir)
661 .output();
662
663 let use_cargo_run = cargo_run_result
664 .as_ref()
665 .map(|o| o.status.success())
666 .unwrap_or(false);
667
668 if use_cargo_run {
669 if self.verbose {
670 println!(" Generated bindings using cargo run uniffi-bindgen");
671 }
672 } else {
673 let uniffi_available = Command::new("uniffi-bindgen")
675 .arg("--version")
676 .output()
677 .map(|o| o.status.success())
678 .unwrap_or(false);
679
680 if !uniffi_available {
681 return Err(BenchError::Build(
682 "uniffi-bindgen not found and no pre-generated bindings exist.\n\n\
683 To fix this, either:\n\
684 1. Add a uniffi-bindgen binary to your crate:\n\
685 [[bin]]\n\
686 name = \"uniffi-bindgen\"\n\
687 path = \"src/bin/uniffi-bindgen.rs\"\n\n\
688 2. Or install uniffi-bindgen globally:\n\
689 cargo install uniffi-bindgen\n\n\
690 3. Or pre-generate bindings and commit them."
691 .to_string(),
692 ));
693 }
694
695 let mut cmd = Command::new("uniffi-bindgen");
696 cmd.arg("generate")
697 .arg("--library")
698 .arg(&lib_path)
699 .arg("--language")
700 .arg("kotlin")
701 .arg("--out-dir")
702 .arg(&out_dir);
703 run_command(cmd, "uniffi-bindgen kotlin")?;
704 }
705
706 if self.verbose {
707 println!(" Generated UniFFI Kotlin bindings at {:?}", out_dir);
708 }
709 Ok(())
710 }
711
712 fn copy_native_libraries(
714 &self,
715 config: &BuildConfig,
716 ) -> Result<Vec<NativeLibraryArtifact>, BenchError> {
717 let crate_dir = self.find_crate_dir()?;
718 let profile_dir = match config.profile {
719 BuildProfile::Debug => "debug",
720 BuildProfile::Release => "release",
721 };
722
723 let target_dir = get_cargo_target_dir(&crate_dir)?;
725 let jni_libs_dir = self.output_dir.join("android/app/src/main/jniLibs");
726
727 std::fs::create_dir_all(&jni_libs_dir).map_err(|e| {
729 BenchError::Build(format!(
730 "Failed to create jniLibs directory at {}: {}. Check output directory permissions.",
731 jni_libs_dir.display(),
732 e
733 ))
734 })?;
735
736 let mut native_libraries = Vec::new();
737
738 for android_abi in self.resolve_android_abis(config)? {
739 let rust_target = android_abi_to_rust_target(&android_abi).ok_or_else(|| {
740 BenchError::Build(format!(
741 "Unsupported Android ABI '{android_abi}'. Supported values: arm64-v8a, armeabi-v7a, x86_64"
742 ))
743 })?;
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.clone(),
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
1293fn android_abi_to_rust_target(abi: &str) -> Option<&'static str> {
1294 match abi {
1295 "arm64-v8a" => Some("aarch64-linux-android"),
1296 "armeabi-v7a" => Some("armv7-linux-androideabi"),
1297 "x86_64" => Some("x86_64-linux-android"),
1298 _ => None,
1299 }
1300}
1301
1302#[derive(Debug, Clone, PartialEq, Eq)]
1303pub struct AndroidStackSymbolization {
1304 pub line: String,
1305 pub resolved_frames: u64,
1306 pub unresolved_frames: u64,
1307}
1308
1309pub fn symbolize_android_native_stack_line_with_resolver<F>(
1310 line: &str,
1311 mut resolve: F,
1312) -> AndroidStackSymbolization
1313where
1314 F: FnMut(&str, u64) -> Option<String>,
1315{
1316 let (stack, sample_count) = split_folded_stack_line(line);
1317 let mut resolved_frames = 0;
1318 let mut unresolved_frames = 0;
1319 let rewritten = stack
1320 .split(';')
1321 .map(|frame| {
1322 if let Some((library_name, offset)) = parse_android_native_offset_frame(frame) {
1323 if let Some(symbol) = resolve(library_name, offset) {
1324 resolved_frames += 1;
1325 return symbol;
1326 }
1327 unresolved_frames += 1;
1328 }
1329 frame.to_string()
1330 })
1331 .collect::<Vec<_>>()
1332 .join(";");
1333
1334 let line = match sample_count {
1335 Some(count) => format!("{rewritten} {count}"),
1336 None => rewritten,
1337 };
1338
1339 AndroidStackSymbolization {
1340 line,
1341 resolved_frames,
1342 unresolved_frames,
1343 }
1344}
1345
1346pub fn resolve_android_native_symbol_with_addr2line(
1347 library_path: &Path,
1348 offset: u64,
1349) -> Option<String> {
1350 let tool_path = locate_android_addr2line_tool_path()?;
1351 resolve_android_native_symbol_with_tool(&tool_path, library_path, offset)
1352}
1353
1354pub fn resolve_android_native_symbol_with_tool(
1355 tool_path: &Path,
1356 library_path: &Path,
1357 offset: u64,
1358) -> Option<String> {
1359 let output = Command::new(tool_path)
1360 .args(["-Cfpe"])
1361 .arg(library_path)
1362 .arg(format!("0x{offset:x}"))
1363 .output()
1364 .ok()?;
1365 if !output.status.success() {
1366 return None;
1367 }
1368
1369 parse_android_addr2line_stdout(&String::from_utf8_lossy(&output.stdout))
1370}
1371
1372fn parse_android_addr2line_stdout(stdout: &str) -> Option<String> {
1373 stdout.lines().find_map(|line| {
1374 let symbol = line.trim();
1375 if symbol.is_empty() || symbol == "??" || symbol.starts_with("?? ") {
1376 None
1377 } else {
1378 Some(
1379 symbol
1380 .split(" at ")
1381 .next()
1382 .unwrap_or(symbol)
1383 .trim()
1384 .to_owned(),
1385 )
1386 }
1387 })
1388}
1389
1390fn locate_android_addr2line_tool_path() -> Option<PathBuf> {
1391 let override_path = std::env::var_os("MOBENCH_ANDROID_LLVM_ADDR2LINE")
1392 .or_else(|| std::env::var_os("LLVM_ADDR2LINE"))
1393 .map(PathBuf::from);
1394 if let Some(path) = override_path {
1395 return path.exists().then_some(path);
1396 }
1397
1398 let sdk_root = std::env::var_os("ANDROID_HOME")
1399 .map(PathBuf::from)
1400 .or_else(|| std::env::var_os("ANDROID_SDK_ROOT").map(PathBuf::from))
1401 .or_else(|| {
1402 std::env::var_os("ANDROID_NDK_HOME")
1403 .map(PathBuf::from)
1404 .and_then(|ndk_home| ndk_home.parent().and_then(Path::parent).map(PathBuf::from))
1405 })?;
1406 let ndk_root = std::env::var_os("ANDROID_NDK_HOME")
1407 .map(PathBuf::from)
1408 .or_else(|| {
1409 let ndk_dir = sdk_root.join("ndk");
1410 std::fs::read_dir(&ndk_dir).ok().and_then(|entries| {
1411 entries
1412 .filter_map(|entry| entry.ok())
1413 .map(|entry| entry.path())
1414 .filter(|path| path.is_dir())
1415 .max()
1416 })
1417 })?;
1418
1419 let tool_name = if cfg!(windows) {
1420 "llvm-addr2line.exe"
1421 } else {
1422 "llvm-addr2line"
1423 };
1424 let prebuilt_root = ndk_root.join("toolchains").join("llvm").join("prebuilt");
1425 let mut candidates = Vec::new();
1426 if let Ok(entries) = std::fs::read_dir(&prebuilt_root) {
1427 for entry in entries.flatten() {
1428 let candidate = entry.path().join("bin").join(tool_name);
1429 if candidate.exists() {
1430 candidates.push(candidate);
1431 }
1432 }
1433 }
1434 candidates.sort();
1435 candidates.into_iter().next()
1436}
1437
1438fn split_folded_stack_line(line: &str) -> (&str, Option<&str>) {
1439 match line.rsplit_once(' ') {
1440 Some((stack, count))
1441 if !stack.is_empty() && count.chars().all(|ch| ch.is_ascii_digit()) =>
1442 {
1443 (stack, Some(count))
1444 }
1445 _ => (line, None),
1446 }
1447}
1448
1449fn parse_android_native_offset_frame(frame: &str) -> Option<(&str, u64)> {
1450 let marker = ".so[+";
1451 let marker_index = frame.find(marker)?;
1452 let library_end = marker_index + 3;
1453 let library_name = frame[..library_end].rsplit('/').next()?;
1454 let offset_start = marker_index + marker.len();
1455 let offset_end = frame[offset_start..].find(']')? + offset_start;
1456 let offset_raw = &frame[offset_start..offset_end];
1457 let offset = if let Some(hex) = offset_raw.strip_prefix("0x") {
1458 u64::from_str_radix(hex, 16).ok()?
1459 } else {
1460 offset_raw.parse().ok()?
1461 };
1462 Some((library_name, offset))
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467 use super::*;
1468
1469 #[test]
1470 fn test_android_builder_creation() {
1471 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1472 assert!(!builder.verbose);
1473 assert_eq!(
1474 builder.output_dir,
1475 PathBuf::from("/tmp/test-project/target/mobench")
1476 );
1477 }
1478
1479 #[test]
1480 fn test_android_builder_verbose() {
1481 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile").verbose(true);
1482 assert!(builder.verbose);
1483 }
1484
1485 #[test]
1486 fn test_android_builder_custom_output_dir() {
1487 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile")
1488 .output_dir("/custom/output");
1489 assert_eq!(builder.output_dir, PathBuf::from("/custom/output"));
1490 }
1491
1492 #[test]
1493 fn test_parse_output_metadata_unsigned() {
1494 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1495 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"}"#;
1496 let result = builder.parse_output_metadata(metadata);
1497 assert_eq!(result, Some("app-release-unsigned.apk".to_string()));
1498 }
1499
1500 #[test]
1501 fn test_parse_output_metadata_signed() {
1502 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1503 let metadata = r#"{"version":3,"elements":[{"outputFile":"app-release.apk"}]}"#;
1504 let result = builder.parse_output_metadata(metadata);
1505 assert_eq!(result, Some("app-release.apk".to_string()));
1506 }
1507
1508 #[test]
1509 fn test_parse_output_metadata_no_apk() {
1510 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1511 let metadata = r#"{"version":3,"elements":[]}"#;
1512 let result = builder.parse_output_metadata(metadata);
1513 assert_eq!(result, None);
1514 }
1515
1516 #[test]
1517 fn test_parse_output_metadata_invalid_json() {
1518 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1519 let metadata = "not valid json";
1520 let result = builder.parse_output_metadata(metadata);
1521 assert_eq!(result, None);
1522 }
1523
1524 #[test]
1525 fn test_android_builder_defaults_to_arm64_only() {
1526 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1527 let config = BuildConfig {
1528 target: Target::Android,
1529 profile: BuildProfile::Debug,
1530 incremental: true,
1531 android_abis: None,
1532 };
1533
1534 let abis = builder
1535 .resolve_android_abis(&config)
1536 .expect("resolve default ABIs");
1537 assert_eq!(abis, vec!["arm64-v8a".to_string()]);
1538 }
1539
1540 #[test]
1541 fn test_android_builder_uses_explicit_abis_when_configured() {
1542 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1543 let config = BuildConfig {
1544 target: Target::Android,
1545 profile: BuildProfile::Release,
1546 incremental: true,
1547 android_abis: Some(vec!["arm64-v8a".to_string(), "x86_64".to_string()]),
1548 };
1549
1550 let abis = builder
1551 .resolve_android_abis(&config)
1552 .expect("resolve configured ABIs");
1553 assert_eq!(abis, vec!["arm64-v8a".to_string(), "x86_64".to_string()]);
1554 }
1555
1556 #[test]
1557 fn android_native_offsets_are_symbolized_into_rust_frames() {
1558 let input = "dev.world.samplefns;uniffi.sample_fns.Sample_fnsKt.runBenchmark;libsample_fns.so[+94138] 1";
1559 let output =
1560 symbolize_android_native_stack_line_with_resolver(input, |library_name, offset| {
1561 if library_name == "libsample_fns.so" && offset == 94_138 {
1562 Some("sample_fns::fibonacci".into())
1563 } else {
1564 None
1565 }
1566 });
1567
1568 assert!(
1569 output.line.contains("sample_fns::fibonacci"),
1570 "expected unresolved native offsets to be rewritten into Rust symbols, got: {}",
1571 output.line
1572 );
1573 assert_eq!(output.resolved_frames, 1);
1574 assert_eq!(output.unresolved_frames, 0);
1575 }
1576
1577 #[test]
1578 fn resolve_android_native_symbol_with_tool_invokes_addr2line() {
1579 let temp_dir = std::env::temp_dir().join(format!(
1580 "mobench-addr2line-{}-{}",
1581 std::process::id(),
1582 std::time::SystemTime::now()
1583 .duration_since(std::time::UNIX_EPOCH)
1584 .expect("system time")
1585 .as_nanos()
1586 ));
1587 std::fs::create_dir_all(&temp_dir).expect("create temp dir");
1588 let tool_path = temp_dir.join("llvm-addr2line.sh");
1589 let args_path = temp_dir.join("args.txt");
1590 let script = format!(
1591 "#!/bin/sh\nprintf '%s\\n' \"$@\" > '{}'\nprintf '%s\\n' 'sample_fns::fibonacci at /tmp/src/lib.rs:131'\n",
1592 args_path.display()
1593 );
1594 std::fs::write(&tool_path, script).expect("write shim");
1595
1596 #[cfg(unix)]
1597 {
1598 use std::os::unix::fs::PermissionsExt;
1599 let mut perms = std::fs::metadata(&tool_path)
1600 .expect("metadata")
1601 .permissions();
1602 perms.set_mode(0o755);
1603 std::fs::set_permissions(&tool_path, perms).expect("chmod");
1604 }
1605
1606 let symbol = resolve_android_native_symbol_with_tool(
1607 &tool_path,
1608 Path::new("/cargo/target/aarch64-linux-android/release/libsample_fns.so"),
1609 94_138,
1610 );
1611
1612 assert_eq!(symbol.as_deref(), Some("sample_fns::fibonacci"));
1613
1614 let args = std::fs::read_to_string(&args_path).expect("read args");
1615 let expected_offset = format!("0x{:x}", 94_138);
1616 assert!(
1617 args.lines().any(|line| line == "-Cfpe"),
1618 "expected llvm-addr2line to be called with -Cfpe, got:\n{args}"
1619 );
1620 assert!(
1621 args.lines().any(|line| {
1622 line == "/cargo/target/aarch64-linux-android/release/libsample_fns.so"
1623 }),
1624 "expected llvm-addr2line to use the unstripped library path, got:\n{args}"
1625 );
1626 assert!(
1627 args.lines().any(|line| line == expected_offset),
1628 "expected llvm-addr2line to receive the resolved offset, got:\n{args}"
1629 );
1630 }
1631
1632 #[test]
1633 fn android_native_offsets_preserve_unresolved_frames() {
1634 let input = "dev.world.samplefns;libsample_fns.so[+94138];libother.so[+17] 1";
1635 let output =
1636 symbolize_android_native_stack_line_with_resolver(input, |library_name, offset| {
1637 if library_name == "libsample_fns.so" && offset == 94_138 {
1638 Some("sample_fns::fibonacci".into())
1639 } else {
1640 None
1641 }
1642 });
1643
1644 assert!(output.line.contains("sample_fns::fibonacci"));
1645 assert!(output.line.contains("libother.so[+17]"));
1646 assert_eq!(output.resolved_frames, 1);
1647 assert_eq!(output.unresolved_frames, 1);
1648 }
1649
1650 #[test]
1651 fn test_find_crate_dir_current_directory_is_crate() {
1652 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-current");
1654 let _ = std::fs::remove_dir_all(&temp_dir);
1655 std::fs::create_dir_all(&temp_dir).unwrap();
1656
1657 std::fs::write(
1659 temp_dir.join("Cargo.toml"),
1660 r#"[package]
1661name = "bench-mobile"
1662version = "0.1.0"
1663"#,
1664 )
1665 .unwrap();
1666
1667 let builder = AndroidBuilder::new(&temp_dir, "bench-mobile");
1668 let result = builder.find_crate_dir();
1669 assert!(result.is_ok(), "Should find crate in current directory");
1670 assert_eq!(result.unwrap(), temp_dir);
1671
1672 std::fs::remove_dir_all(&temp_dir).unwrap();
1673 }
1674
1675 #[test]
1676 fn test_find_crate_dir_nested_bench_mobile() {
1677 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-nested");
1679 let _ = std::fs::remove_dir_all(&temp_dir);
1680 std::fs::create_dir_all(temp_dir.join("bench-mobile")).unwrap();
1681
1682 std::fs::write(
1684 temp_dir.join("Cargo.toml"),
1685 r#"[workspace]
1686members = ["bench-mobile"]
1687"#,
1688 )
1689 .unwrap();
1690
1691 std::fs::write(
1693 temp_dir.join("bench-mobile/Cargo.toml"),
1694 r#"[package]
1695name = "bench-mobile"
1696version = "0.1.0"
1697"#,
1698 )
1699 .unwrap();
1700
1701 let builder = AndroidBuilder::new(&temp_dir, "bench-mobile");
1702 let result = builder.find_crate_dir();
1703 assert!(
1704 result.is_ok(),
1705 "Should find crate in bench-mobile/ directory"
1706 );
1707 assert_eq!(result.unwrap(), temp_dir.join("bench-mobile"));
1708
1709 std::fs::remove_dir_all(&temp_dir).unwrap();
1710 }
1711
1712 #[test]
1713 fn test_find_crate_dir_crates_subdir() {
1714 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-crates");
1716 let _ = std::fs::remove_dir_all(&temp_dir);
1717 std::fs::create_dir_all(temp_dir.join("crates/my-bench")).unwrap();
1718
1719 std::fs::write(
1721 temp_dir.join("Cargo.toml"),
1722 r#"[workspace]
1723members = ["crates/*"]
1724"#,
1725 )
1726 .unwrap();
1727
1728 std::fs::write(
1730 temp_dir.join("crates/my-bench/Cargo.toml"),
1731 r#"[package]
1732name = "my-bench"
1733version = "0.1.0"
1734"#,
1735 )
1736 .unwrap();
1737
1738 let builder = AndroidBuilder::new(&temp_dir, "my-bench");
1739 let result = builder.find_crate_dir();
1740 assert!(result.is_ok(), "Should find crate in crates/ directory");
1741 assert_eq!(result.unwrap(), temp_dir.join("crates/my-bench"));
1742
1743 std::fs::remove_dir_all(&temp_dir).unwrap();
1744 }
1745
1746 #[test]
1747 fn test_find_crate_dir_not_found() {
1748 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-notfound");
1750 let _ = std::fs::remove_dir_all(&temp_dir);
1751 std::fs::create_dir_all(&temp_dir).unwrap();
1752
1753 std::fs::write(
1755 temp_dir.join("Cargo.toml"),
1756 r#"[package]
1757name = "some-other-crate"
1758version = "0.1.0"
1759"#,
1760 )
1761 .unwrap();
1762
1763 let builder = AndroidBuilder::new(&temp_dir, "nonexistent-crate");
1764 let result = builder.find_crate_dir();
1765 assert!(result.is_err(), "Should fail to find nonexistent crate");
1766 let err_msg = result.unwrap_err().to_string();
1767 assert!(err_msg.contains("Benchmark crate 'nonexistent-crate' not found"));
1768 assert!(err_msg.contains("Searched locations"));
1769
1770 std::fs::remove_dir_all(&temp_dir).unwrap();
1771 }
1772
1773 #[test]
1774 fn test_find_crate_dir_explicit_crate_path() {
1775 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-explicit");
1777 let _ = std::fs::remove_dir_all(&temp_dir);
1778 std::fs::create_dir_all(temp_dir.join("custom-location")).unwrap();
1779
1780 let builder =
1781 AndroidBuilder::new(&temp_dir, "any-name").crate_dir(temp_dir.join("custom-location"));
1782 let result = builder.find_crate_dir();
1783 assert!(result.is_ok(), "Should use explicit crate_dir");
1784 assert_eq!(result.unwrap(), temp_dir.join("custom-location"));
1785
1786 std::fs::remove_dir_all(&temp_dir).unwrap();
1787 }
1788}