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 && !test_path.exists()
329 {
330 missing.push(format!("Test APK: {}", test_path.display()));
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 && let Some(pkg_name) = super::common::read_package_name(&root_cargo_toml)
434 && pkg_name == self.crate_name
435 {
436 return Ok(self.project_root.clone());
437 }
438
439 let bench_mobile_dir = self.project_root.join("bench-mobile");
441 if bench_mobile_dir.exists() {
442 return Ok(bench_mobile_dir);
443 }
444
445 let crates_dir = self.project_root.join("crates").join(&self.crate_name);
447 if crates_dir.exists() {
448 return Ok(crates_dir);
449 }
450
451 let named_dir = self.project_root.join(&self.crate_name);
453 if named_dir.exists() {
454 return Ok(named_dir);
455 }
456
457 let root_manifest = root_cargo_toml;
458 let bench_mobile_manifest = bench_mobile_dir.join("Cargo.toml");
459 let crates_manifest = crates_dir.join("Cargo.toml");
460 let named_manifest = named_dir.join("Cargo.toml");
461 Err(BenchError::Build(format!(
462 "Benchmark crate '{}' not found.\n\n\
463 Searched locations:\n\
464 - {} (checked [package] name)\n\
465 - {}\n\
466 - {}\n\
467 - {}\n\n\
468 To fix this:\n\
469 1. Run from the crate directory (where Cargo.toml has name = \"{}\")\n\
470 2. Create a bench-mobile/ directory with your benchmark crate, or\n\
471 3. Use --crate-path to specify the benchmark crate location:\n\
472 cargo mobench build --target android --crate-path ./my-benchmarks\n\n\
473 Common issues:\n\
474 - Typo in crate name (check Cargo.toml [package] name)\n\
475 - Wrong working directory (run from project root)\n\
476 - Missing Cargo.toml in the crate directory\n\n\
477 Run 'cargo mobench init --help' to generate a new benchmark project.",
478 self.crate_name,
479 root_manifest.display(),
480 bench_mobile_manifest.display(),
481 crates_manifest.display(),
482 named_manifest.display(),
483 self.crate_name,
484 )))
485 }
486
487 fn build_rust_libraries(&self, config: &BuildConfig) -> Result<(), BenchError> {
489 let crate_dir = self.find_crate_dir()?;
490
491 self.check_cargo_ndk()?;
493
494 let abis = self.resolve_android_abis(config)?;
495 let release_flag = if matches!(config.profile, BuildProfile::Release) {
496 "--release"
497 } else {
498 ""
499 };
500
501 for abi in abis {
502 if self.verbose {
503 println!(" Building for {}", abi);
504 }
505
506 let mut cmd = Command::new("cargo");
507 cmd.arg("ndk")
508 .arg("--target")
509 .arg(&abi)
510 .arg("--platform")
511 .arg("24") .arg("build");
513
514 if !release_flag.is_empty() {
516 cmd.arg(release_flag);
517 }
518
519 cmd.current_dir(&crate_dir);
521
522 let command_hint = if release_flag.is_empty() {
524 format!("cargo ndk --target {} --platform 24 build", abi)
525 } else {
526 format!(
527 "cargo ndk --target {} --platform 24 build {}",
528 abi, release_flag
529 )
530 };
531 let output = cmd.output().map_err(|e| {
532 BenchError::Build(format!(
533 "Failed to start cargo-ndk for {}.\n\n\
534 Command: {}\n\
535 Crate directory: {}\n\
536 System error: {}\n\n\
537 Tips:\n\
538 - Install cargo-ndk: cargo install cargo-ndk\n\
539 - Ensure cargo is on PATH",
540 abi,
541 command_hint,
542 crate_dir.display(),
543 e
544 ))
545 })?;
546
547 if !output.status.success() {
548 let stdout = String::from_utf8_lossy(&output.stdout);
549 let stderr = String::from_utf8_lossy(&output.stderr);
550 let profile = if matches!(config.profile, BuildProfile::Release) {
551 "release"
552 } else {
553 "debug"
554 };
555 let rust_target = android_abi_to_rust_target(&abi).unwrap_or(abi.as_str());
556 return Err(BenchError::Build(format!(
557 "cargo-ndk build failed for {} ({} profile).\n\n\
558 Command: {}\n\
559 Crate directory: {}\n\
560 Exit status: {}\n\n\
561 Stdout:\n{}\n\n\
562 Stderr:\n{}\n\n\
563 Common causes:\n\
564 - Missing Rust target: rustup target add {}\n\
565 - NDK not found: set ANDROID_NDK_HOME\n\
566 - Compilation error in Rust code (see output above)\n\
567 - Incompatible native dependencies (some C libraries do not support Android)",
568 abi,
569 profile,
570 command_hint,
571 crate_dir.display(),
572 output.status,
573 stdout,
574 stderr,
575 rust_target,
576 )));
577 }
578 }
579
580 Ok(())
581 }
582
583 fn check_cargo_ndk(&self) -> Result<(), BenchError> {
585 let output = Command::new("cargo").arg("ndk").arg("--version").output();
586
587 match output {
588 Ok(output) if output.status.success() => Ok(()),
589 _ => Err(BenchError::Build(
590 "cargo-ndk is not installed or not in PATH.\n\n\
591 cargo-ndk is required to cross-compile Rust for Android.\n\n\
592 To install:\n\
593 cargo install cargo-ndk\n\
594 Verify with:\n\
595 cargo ndk --version\n\n\
596 You also need the Android NDK. Set ANDROID_NDK_HOME or install via Android Studio.\n\
597 See: https://github.com/nickelc/cargo-ndk"
598 .to_string(),
599 )),
600 }
601 }
602
603 fn generate_uniffi_bindings(&self) -> Result<(), BenchError> {
605 let crate_dir = self.find_crate_dir()?;
606 let crate_name_underscored = self.crate_name.replace("-", "_");
607
608 let bindings_path = self
610 .output_dir
611 .join("android")
612 .join("app")
613 .join("src")
614 .join("main")
615 .join("java")
616 .join("uniffi")
617 .join(&crate_name_underscored)
618 .join(format!("{}.kt", crate_name_underscored));
619
620 if bindings_path.exists() {
621 if self.verbose {
622 println!(" Using existing Kotlin bindings at {:?}", bindings_path);
623 }
624 return Ok(());
625 }
626
627 let mut build_cmd = Command::new("cargo");
629 build_cmd.arg("build");
630 build_cmd.current_dir(&crate_dir);
631 run_command(build_cmd, "cargo build (host)")?;
632
633 let lib_path = host_lib_path(&crate_dir, &self.crate_name)?;
634 let out_dir = self
635 .output_dir
636 .join("android")
637 .join("app")
638 .join("src")
639 .join("main")
640 .join("java");
641
642 let cargo_run_result = Command::new("cargo")
644 .args([
645 "run",
646 "-p",
647 &self.crate_name,
648 "--bin",
649 "uniffi-bindgen",
650 "--",
651 ])
652 .arg("generate")
653 .arg("--library")
654 .arg(&lib_path)
655 .arg("--language")
656 .arg("kotlin")
657 .arg("--out-dir")
658 .arg(&out_dir)
659 .current_dir(&crate_dir)
660 .output();
661
662 let use_cargo_run = cargo_run_result
663 .as_ref()
664 .map(|o| o.status.success())
665 .unwrap_or(false);
666
667 if use_cargo_run {
668 if self.verbose {
669 println!(" Generated bindings using cargo run uniffi-bindgen");
670 }
671 } else {
672 let uniffi_available = Command::new("uniffi-bindgen")
674 .arg("--version")
675 .output()
676 .map(|o| o.status.success())
677 .unwrap_or(false);
678
679 if !uniffi_available {
680 return Err(BenchError::Build(
681 "uniffi-bindgen not found and no pre-generated bindings exist.\n\n\
682 To fix this, either:\n\
683 1. Add a uniffi-bindgen binary to your crate:\n\
684 [[bin]]\n\
685 name = \"uniffi-bindgen\"\n\
686 path = \"src/bin/uniffi-bindgen.rs\"\n\n\
687 2. Or install uniffi-bindgen globally:\n\
688 cargo install uniffi-bindgen\n\n\
689 3. Or pre-generate bindings and commit them."
690 .to_string(),
691 ));
692 }
693
694 let mut cmd = Command::new("uniffi-bindgen");
695 cmd.arg("generate")
696 .arg("--library")
697 .arg(&lib_path)
698 .arg("--language")
699 .arg("kotlin")
700 .arg("--out-dir")
701 .arg(&out_dir);
702 run_command(cmd, "uniffi-bindgen kotlin")?;
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 mut native_libraries = Vec::new();
736
737 for android_abi in self.resolve_android_abis(config)? {
738 let rust_target = android_abi_to_rust_target(&android_abi).ok_or_else(|| {
739 BenchError::Build(format!(
740 "Unsupported Android ABI '{android_abi}'. Supported values: arm64-v8a, armeabi-v7a, x86_64"
741 ))
742 })?;
743 let library_name = format!("lib{}.so", self.crate_name.replace("-", "_"));
744 let src = target_dir
745 .join(rust_target)
746 .join(profile_dir)
747 .join(&library_name);
748
749 let dest_dir = jni_libs_dir.join(&android_abi);
750 std::fs::create_dir_all(&dest_dir).map_err(|e| {
751 BenchError::Build(format!(
752 "Failed to create ABI directory {} at {}: {}. Check output directory permissions.",
753 android_abi,
754 dest_dir.display(),
755 e
756 ))
757 })?;
758
759 let dest = dest_dir.join(&library_name);
760
761 if src.exists() {
762 std::fs::copy(&src, &dest).map_err(|e| {
763 BenchError::Build(format!(
764 "Failed to copy {} library from {} to {}: {}. Ensure cargo-ndk completed successfully.",
765 android_abi,
766 src.display(),
767 dest.display(),
768 e
769 ))
770 })?;
771
772 if self.verbose {
773 println!(" Copied {} -> {}", src.display(), dest.display());
774 }
775
776 native_libraries.push(NativeLibraryArtifact {
777 abi: android_abi.clone(),
778 library_name: library_name.clone(),
779 unstripped_path: src,
780 packaged_path: dest,
781 });
782 } else {
783 eprintln!(
785 "Warning: Native library for {} not found at {}.\n\
786 This will cause a runtime crash when the app tries to load the library.\n\
787 Ensure cargo-ndk build completed successfully for this ABI.",
788 android_abi,
789 src.display()
790 );
791 }
792 }
793
794 Ok(native_libraries)
795 }
796
797 fn ensure_local_properties(&self, android_dir: &Path) -> Result<(), BenchError> {
808 let local_props = android_dir.join("local.properties");
809
810 if local_props.exists() {
812 return Ok(());
813 }
814
815 let sdk_dir = self.find_android_sdk_from_env();
818
819 match sdk_dir {
820 Some(path) => {
821 let content = format!("sdk.dir={}\n", path.display());
823 fs::write(&local_props, content).map_err(|e| {
824 BenchError::Build(format!(
825 "Failed to write local.properties at {:?}: {}. Check output directory permissions.",
826 local_props, e
827 ))
828 })?;
829
830 if self.verbose {
831 println!(
832 " Generated local.properties with sdk.dir={}",
833 path.display()
834 );
835 }
836 }
837 None => {
838 if self.verbose {
841 println!(
842 " Skipping local.properties generation (ANDROID_HOME/ANDROID_SDK_ROOT not set)"
843 );
844 println!(
845 " Gradle will auto-detect SDK or you can create local.properties manually"
846 );
847 }
848 }
849 }
850
851 Ok(())
852 }
853
854 fn find_android_sdk_from_env(&self) -> Option<PathBuf> {
862 if let Ok(path) = env::var("ANDROID_HOME") {
864 let sdk_path = PathBuf::from(&path);
865 if sdk_path.exists() {
866 return Some(sdk_path);
867 }
868 }
869
870 if let Ok(path) = env::var("ANDROID_SDK_ROOT") {
872 let sdk_path = PathBuf::from(&path);
873 if sdk_path.exists() {
874 return Some(sdk_path);
875 }
876 }
877
878 None
879 }
880
881 fn ensure_gradle_wrapper(&self, android_dir: &Path) -> Result<(), BenchError> {
886 let gradlew = android_dir.join("gradlew");
887
888 if gradlew.exists() {
890 return Ok(());
891 }
892
893 println!("Gradle wrapper not found, generating...");
894
895 let gradle_available = Command::new("gradle")
897 .arg("--version")
898 .output()
899 .map(|o| o.status.success())
900 .unwrap_or(false);
901
902 if !gradle_available {
903 return Err(BenchError::Build(
904 "Gradle wrapper (gradlew) not found and 'gradle' command is not available.\n\n\
905 The Android project requires Gradle to build. You have two options:\n\n\
906 1. Install Gradle globally and run the build again (it will auto-generate the wrapper):\n\
907 - macOS: brew install gradle\n\
908 - Linux: sudo apt install gradle\n\
909 - Or download from https://gradle.org/install/\n\n\
910 2. Or generate the wrapper manually in the Android project directory:\n\
911 cd target/mobench/android && gradle wrapper --gradle-version 8.5"
912 .to_string(),
913 ));
914 }
915
916 let mut cmd = Command::new("gradle");
918 cmd.arg("wrapper")
919 .arg("--gradle-version")
920 .arg("8.5")
921 .current_dir(android_dir);
922
923 let output = cmd.output().map_err(|e| {
924 BenchError::Build(format!(
925 "Failed to run 'gradle wrapper' command: {}\n\n\
926 Ensure Gradle is installed and on your PATH.",
927 e
928 ))
929 })?;
930
931 if !output.status.success() {
932 let stderr = String::from_utf8_lossy(&output.stderr);
933 return Err(BenchError::Build(format!(
934 "Failed to generate Gradle wrapper.\n\n\
935 Command: gradle wrapper --gradle-version 8.5\n\
936 Working directory: {}\n\
937 Exit status: {}\n\
938 Stderr: {}\n\n\
939 Try running this command manually in the Android project directory.",
940 android_dir.display(),
941 output.status,
942 stderr
943 )));
944 }
945
946 #[cfg(unix)]
948 {
949 use std::os::unix::fs::PermissionsExt;
950 if let Ok(metadata) = fs::metadata(&gradlew) {
951 let mut perms = metadata.permissions();
952 perms.set_mode(0o755);
953 let _ = fs::set_permissions(&gradlew, perms);
954 }
955 }
956
957 if self.verbose {
958 println!(" Generated Gradle wrapper at {:?}", gradlew);
959 }
960
961 Ok(())
962 }
963
964 fn build_apk(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
966 let android_dir = self.output_dir.join("android");
967
968 if !android_dir.exists() {
969 return Err(BenchError::Build(format!(
970 "Android project not found at {}.\n\n\
971 Expected a Gradle project under the output directory.\n\
972 Run `cargo mobench init --target android` or `cargo mobench build --target android` from the project root to generate it.",
973 android_dir.display()
974 )));
975 }
976
977 self.ensure_local_properties(&android_dir)?;
979
980 let gradle_task = match config.profile {
982 BuildProfile::Debug => "assembleDebug",
983 BuildProfile::Release => "assembleRelease",
984 };
985
986 let mut cmd = Command::new("./gradlew");
988 cmd.arg(gradle_task).current_dir(&android_dir);
989
990 if self.verbose {
991 cmd.arg("--info");
992 }
993
994 let output = cmd.output().map_err(|e| {
995 BenchError::Build(format!(
996 "Failed to run Gradle wrapper.\n\n\
997 Command: ./gradlew {}\n\
998 Working directory: {}\n\
999 Error: {}\n\n\
1000 Tips:\n\
1001 - Ensure ./gradlew is executable (chmod +x ./gradlew)\n\
1002 - Run ./gradlew --version in that directory to verify the wrapper",
1003 gradle_task,
1004 android_dir.display(),
1005 e
1006 ))
1007 })?;
1008
1009 if !output.status.success() {
1010 let stdout = String::from_utf8_lossy(&output.stdout);
1011 let stderr = String::from_utf8_lossy(&output.stderr);
1012 return Err(BenchError::Build(format!(
1013 "Gradle build failed.\n\n\
1014 Command: ./gradlew {}\n\
1015 Working directory: {}\n\
1016 Exit status: {}\n\n\
1017 Stdout:\n{}\n\n\
1018 Stderr:\n{}\n\n\
1019 Tips:\n\
1020 - Re-run with verbose mode to pass --info to Gradle\n\
1021 - Run ./gradlew {} --stacktrace for a full stack trace",
1022 gradle_task,
1023 android_dir.display(),
1024 output.status,
1025 stdout,
1026 stderr,
1027 gradle_task,
1028 )));
1029 }
1030
1031 let profile_name = match config.profile {
1033 BuildProfile::Debug => "debug",
1034 BuildProfile::Release => "release",
1035 };
1036
1037 let apk_dir = android_dir.join("app/build/outputs/apk").join(profile_name);
1038
1039 let apk_path = self.find_apk(&apk_dir, profile_name, gradle_task)?;
1045
1046 Ok(apk_path)
1047 }
1048
1049 fn find_apk(
1059 &self,
1060 apk_dir: &Path,
1061 profile_name: &str,
1062 gradle_task: &str,
1063 ) -> Result<PathBuf, BenchError> {
1064 let metadata_path = apk_dir.join("output-metadata.json");
1066 if metadata_path.exists()
1067 && let Ok(metadata_content) = fs::read_to_string(&metadata_path)
1068 {
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 let candidates = if profile_name == "release" {
1087 vec![
1088 format!("app-{}.apk", profile_name), format!("app-{}-unsigned.apk", profile_name), ]
1091 } else {
1092 vec![
1093 format!("app-{}.apk", profile_name), ]
1095 };
1096
1097 for candidate in &candidates {
1099 let apk_path = apk_dir.join(candidate);
1100 if apk_path.exists() {
1101 if self.verbose {
1102 println!(" Found APK: {}", apk_path.display());
1103 }
1104 return Ok(apk_path);
1105 }
1106 }
1107
1108 Err(BenchError::Build(format!(
1110 "APK not found in {}.\n\n\
1111 Gradle task {} reported success but no APK was produced.\n\
1112 Searched for:\n{}\n\n\
1113 Check the build output directory and rerun ./gradlew {} if needed.",
1114 apk_dir.display(),
1115 gradle_task,
1116 candidates
1117 .iter()
1118 .map(|c| format!(" - {}", c))
1119 .collect::<Vec<_>>()
1120 .join("\n"),
1121 gradle_task
1122 )))
1123 }
1124
1125 fn parse_output_metadata(&self, content: &str) -> Option<String> {
1139 let pattern = "\"outputFile\"";
1142 if let Some(pos) = content.find(pattern) {
1143 let after_key = &content[pos + pattern.len()..];
1144 let after_colon = after_key.trim_start().strip_prefix(':')?;
1146 let after_ws = after_colon.trim_start();
1147 if let Some(value_start) = after_ws.strip_prefix('"')
1149 && let Some(end_quote) = value_start.find('"')
1150 {
1151 let filename = &value_start[..end_quote];
1152 if filename.ends_with(".apk") {
1153 return Some(filename.to_string());
1154 }
1155 }
1156 }
1157 None
1158 }
1159
1160 fn build_test_apk(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
1162 let android_dir = self.output_dir.join("android");
1163
1164 if !android_dir.exists() {
1165 return Err(BenchError::Build(format!(
1166 "Android project not found at {}.\n\n\
1167 Expected a Gradle project under the output directory.\n\
1168 Run `cargo mobench init --target android` or `cargo mobench build --target android` from the project root to generate it.",
1169 android_dir.display()
1170 )));
1171 }
1172
1173 let gradle_task = match config.profile {
1174 BuildProfile::Debug => "assembleDebugAndroidTest",
1175 BuildProfile::Release => "assembleReleaseAndroidTest",
1176 };
1177 let profile_name = match config.profile {
1178 BuildProfile::Debug => "debug",
1179 BuildProfile::Release => "release",
1180 };
1181
1182 let mut cmd = Command::new("./gradlew");
1183 cmd.arg(format!("-PmobenchTestBuildType={profile_name}"))
1184 .arg(gradle_task)
1185 .current_dir(&android_dir);
1186
1187 if self.verbose {
1188 cmd.arg("--info");
1189 }
1190
1191 let output = cmd.output().map_err(|e| {
1192 BenchError::Build(format!(
1193 "Failed to run Gradle wrapper.\n\n\
1194 Command: ./gradlew {}\n\
1195 Working directory: {}\n\
1196 Error: {}\n\n\
1197 Tips:\n\
1198 - Ensure ./gradlew is executable (chmod +x ./gradlew)\n\
1199 - Run ./gradlew --version in that directory to verify the wrapper",
1200 gradle_task,
1201 android_dir.display(),
1202 e
1203 ))
1204 })?;
1205
1206 if !output.status.success() {
1207 let stdout = String::from_utf8_lossy(&output.stdout);
1208 let stderr = String::from_utf8_lossy(&output.stderr);
1209 return Err(BenchError::Build(format!(
1210 "Gradle test APK build failed.\n\n\
1211 Command: ./gradlew {}\n\
1212 Working directory: {}\n\
1213 Exit status: {}\n\n\
1214 Stdout:\n{}\n\n\
1215 Stderr:\n{}\n\n\
1216 Tips:\n\
1217 - Re-run with verbose mode to pass --info to Gradle\n\
1218 - Run ./gradlew {} --stacktrace for a full stack trace",
1219 gradle_task,
1220 android_dir.display(),
1221 output.status,
1222 stdout,
1223 stderr,
1224 gradle_task,
1225 )));
1226 }
1227
1228 let test_apk_dir = android_dir
1229 .join("app/build/outputs/apk/androidTest")
1230 .join(profile_name);
1231
1232 let apk_path = self.find_test_apk(&test_apk_dir, profile_name, gradle_task)?;
1234
1235 Ok(apk_path)
1236 }
1237
1238 fn find_test_apk(
1244 &self,
1245 apk_dir: &Path,
1246 profile_name: &str,
1247 gradle_task: &str,
1248 ) -> Result<PathBuf, BenchError> {
1249 let metadata_path = apk_dir.join("output-metadata.json");
1251 if metadata_path.exists()
1252 && let Ok(metadata_content) = fs::read_to_string(&metadata_path)
1253 && let Some(apk_name) = self.parse_output_metadata(&metadata_content)
1254 {
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 let apk_path = apk_dir.join(format!("app-{}-androidTest.apk", profile_name));
1269 if apk_path.exists() {
1270 if self.verbose {
1271 println!(" Found test APK: {}", apk_path.display());
1272 }
1273 return Ok(apk_path);
1274 }
1275
1276 Err(BenchError::Build(format!(
1278 "Android test APK not found in {}.\n\n\
1279 Gradle task {} reported success but no test APK was produced.\n\
1280 Expected: app-{}-androidTest.apk\n\n\
1281 Check app/build/outputs/apk/androidTest/{} and rerun ./gradlew {} if needed.",
1282 apk_dir.display(),
1283 gradle_task,
1284 profile_name,
1285 profile_name,
1286 gradle_task
1287 )))
1288 }
1289}
1290
1291fn android_abi_to_rust_target(abi: &str) -> Option<&'static str> {
1292 match abi {
1293 "arm64-v8a" => Some("aarch64-linux-android"),
1294 "armeabi-v7a" => Some("armv7-linux-androideabi"),
1295 "x86_64" => Some("x86_64-linux-android"),
1296 _ => None,
1297 }
1298}
1299
1300#[derive(Debug, Clone, PartialEq, Eq)]
1301pub struct AndroidStackSymbolization {
1302 pub line: String,
1303 pub resolved_frames: u64,
1304 pub unresolved_frames: u64,
1305}
1306
1307pub fn symbolize_android_native_stack_line_with_resolver<F>(
1308 line: &str,
1309 mut resolve: F,
1310) -> AndroidStackSymbolization
1311where
1312 F: FnMut(&str, u64) -> Option<String>,
1313{
1314 let (stack, sample_count) = split_folded_stack_line(line);
1315 let mut resolved_frames = 0;
1316 let mut unresolved_frames = 0;
1317 let rewritten = stack
1318 .split(';')
1319 .map(|frame| {
1320 if let Some((library_name, offset)) = parse_android_native_offset_frame(frame) {
1321 if let Some(symbol) = resolve(library_name, offset) {
1322 resolved_frames += 1;
1323 return symbol;
1324 }
1325 unresolved_frames += 1;
1326 }
1327 frame.to_string()
1328 })
1329 .collect::<Vec<_>>()
1330 .join(";");
1331
1332 let line = match sample_count {
1333 Some(count) => format!("{rewritten} {count}"),
1334 None => rewritten,
1335 };
1336
1337 AndroidStackSymbolization {
1338 line,
1339 resolved_frames,
1340 unresolved_frames,
1341 }
1342}
1343
1344pub fn resolve_android_native_symbol_with_addr2line(
1345 library_path: &Path,
1346 offset: u64,
1347) -> Option<String> {
1348 let tool_path = locate_android_addr2line_tool_path()?;
1349 resolve_android_native_symbol_with_tool(&tool_path, library_path, offset)
1350}
1351
1352pub fn resolve_android_native_symbol_with_tool(
1353 tool_path: &Path,
1354 library_path: &Path,
1355 offset: u64,
1356) -> Option<String> {
1357 let output = Command::new(tool_path)
1358 .args(["-Cfpe"])
1359 .arg(library_path)
1360 .arg(format!("0x{offset:x}"))
1361 .output()
1362 .ok()?;
1363 if !output.status.success() {
1364 return None;
1365 }
1366
1367 parse_android_addr2line_stdout(&String::from_utf8_lossy(&output.stdout))
1368}
1369
1370fn parse_android_addr2line_stdout(stdout: &str) -> Option<String> {
1371 stdout.lines().find_map(|line| {
1372 let symbol = line.trim();
1373 if symbol.is_empty() || symbol == "??" || symbol.starts_with("?? ") {
1374 None
1375 } else {
1376 Some(
1377 symbol
1378 .split(" at ")
1379 .next()
1380 .unwrap_or(symbol)
1381 .trim()
1382 .to_owned(),
1383 )
1384 }
1385 })
1386}
1387
1388fn locate_android_addr2line_tool_path() -> Option<PathBuf> {
1389 let override_path = std::env::var_os("MOBENCH_ANDROID_LLVM_ADDR2LINE")
1390 .or_else(|| std::env::var_os("LLVM_ADDR2LINE"))
1391 .map(PathBuf::from);
1392 if let Some(path) = override_path {
1393 return path.exists().then_some(path);
1394 }
1395
1396 let sdk_root = std::env::var_os("ANDROID_HOME")
1397 .map(PathBuf::from)
1398 .or_else(|| std::env::var_os("ANDROID_SDK_ROOT").map(PathBuf::from))
1399 .or_else(|| {
1400 std::env::var_os("ANDROID_NDK_HOME")
1401 .map(PathBuf::from)
1402 .and_then(|ndk_home| ndk_home.parent().and_then(Path::parent).map(PathBuf::from))
1403 })?;
1404 let ndk_root = std::env::var_os("ANDROID_NDK_HOME")
1405 .map(PathBuf::from)
1406 .or_else(|| {
1407 let ndk_dir = sdk_root.join("ndk");
1408 std::fs::read_dir(&ndk_dir).ok().and_then(|entries| {
1409 entries
1410 .filter_map(|entry| entry.ok())
1411 .map(|entry| entry.path())
1412 .filter(|path| path.is_dir())
1413 .max()
1414 })
1415 })?;
1416
1417 let tool_name = if cfg!(windows) {
1418 "llvm-addr2line.exe"
1419 } else {
1420 "llvm-addr2line"
1421 };
1422 let prebuilt_root = ndk_root.join("toolchains").join("llvm").join("prebuilt");
1423 let mut candidates = Vec::new();
1424 if let Ok(entries) = std::fs::read_dir(&prebuilt_root) {
1425 for entry in entries.flatten() {
1426 let candidate = entry.path().join("bin").join(tool_name);
1427 if candidate.exists() {
1428 candidates.push(candidate);
1429 }
1430 }
1431 }
1432 candidates.sort();
1433 candidates.into_iter().next()
1434}
1435
1436fn split_folded_stack_line(line: &str) -> (&str, Option<&str>) {
1437 match line.rsplit_once(' ') {
1438 Some((stack, count))
1439 if !stack.is_empty() && count.chars().all(|ch| ch.is_ascii_digit()) =>
1440 {
1441 (stack, Some(count))
1442 }
1443 _ => (line, None),
1444 }
1445}
1446
1447fn parse_android_native_offset_frame(frame: &str) -> Option<(&str, u64)> {
1448 let marker = ".so[+";
1449 let marker_index = frame.find(marker)?;
1450 let library_end = marker_index + 3;
1451 let library_name = frame[..library_end].rsplit('/').next()?;
1452 let offset_start = marker_index + marker.len();
1453 let offset_end = frame[offset_start..].find(']')? + offset_start;
1454 let offset_raw = &frame[offset_start..offset_end];
1455 let offset = if let Some(hex) = offset_raw.strip_prefix("0x") {
1456 u64::from_str_radix(hex, 16).ok()?
1457 } else {
1458 offset_raw.parse().ok()?
1459 };
1460 Some((library_name, offset))
1461}
1462
1463#[cfg(test)]
1464mod tests {
1465 use super::*;
1466
1467 #[test]
1468 fn test_android_builder_creation() {
1469 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1470 assert!(!builder.verbose);
1471 assert_eq!(
1472 builder.output_dir,
1473 PathBuf::from("/tmp/test-project/target/mobench")
1474 );
1475 }
1476
1477 #[test]
1478 fn test_android_builder_verbose() {
1479 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile").verbose(true);
1480 assert!(builder.verbose);
1481 }
1482
1483 #[test]
1484 fn test_android_builder_custom_output_dir() {
1485 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile")
1486 .output_dir("/custom/output");
1487 assert_eq!(builder.output_dir, PathBuf::from("/custom/output"));
1488 }
1489
1490 #[test]
1491 fn test_parse_output_metadata_unsigned() {
1492 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1493 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"}"#;
1494 let result = builder.parse_output_metadata(metadata);
1495 assert_eq!(result, Some("app-release-unsigned.apk".to_string()));
1496 }
1497
1498 #[test]
1499 fn test_parse_output_metadata_signed() {
1500 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1501 let metadata = r#"{"version":3,"elements":[{"outputFile":"app-release.apk"}]}"#;
1502 let result = builder.parse_output_metadata(metadata);
1503 assert_eq!(result, Some("app-release.apk".to_string()));
1504 }
1505
1506 #[test]
1507 fn test_parse_output_metadata_no_apk() {
1508 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1509 let metadata = r#"{"version":3,"elements":[]}"#;
1510 let result = builder.parse_output_metadata(metadata);
1511 assert_eq!(result, None);
1512 }
1513
1514 #[test]
1515 fn test_parse_output_metadata_invalid_json() {
1516 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1517 let metadata = "not valid json";
1518 let result = builder.parse_output_metadata(metadata);
1519 assert_eq!(result, None);
1520 }
1521
1522 #[test]
1523 fn test_android_builder_defaults_to_arm64_only() {
1524 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1525 let config = BuildConfig {
1526 target: Target::Android,
1527 profile: BuildProfile::Debug,
1528 incremental: true,
1529 android_abis: None,
1530 };
1531
1532 let abis = builder
1533 .resolve_android_abis(&config)
1534 .expect("resolve default ABIs");
1535 assert_eq!(abis, vec!["arm64-v8a".to_string()]);
1536 }
1537
1538 #[test]
1539 fn test_android_builder_uses_explicit_abis_when_configured() {
1540 let builder = AndroidBuilder::new("/tmp/test-project", "test-bench-mobile");
1541 let config = BuildConfig {
1542 target: Target::Android,
1543 profile: BuildProfile::Release,
1544 incremental: true,
1545 android_abis: Some(vec!["arm64-v8a".to_string(), "x86_64".to_string()]),
1546 };
1547
1548 let abis = builder
1549 .resolve_android_abis(&config)
1550 .expect("resolve configured ABIs");
1551 assert_eq!(abis, vec!["arm64-v8a".to_string(), "x86_64".to_string()]);
1552 }
1553
1554 #[test]
1555 fn android_native_offsets_are_symbolized_into_rust_frames() {
1556 let input = "dev.world.samplefns;uniffi.sample_fns.Sample_fnsKt.runBenchmark;libsample_fns.so[+94138] 1";
1557 let output =
1558 symbolize_android_native_stack_line_with_resolver(input, |library_name, offset| {
1559 if library_name == "libsample_fns.so" && offset == 94_138 {
1560 Some("sample_fns::fibonacci".into())
1561 } else {
1562 None
1563 }
1564 });
1565
1566 assert!(
1567 output.line.contains("sample_fns::fibonacci"),
1568 "expected unresolved native offsets to be rewritten into Rust symbols, got: {}",
1569 output.line
1570 );
1571 assert_eq!(output.resolved_frames, 1);
1572 assert_eq!(output.unresolved_frames, 0);
1573 }
1574
1575 #[test]
1576 fn resolve_android_native_symbol_with_tool_invokes_addr2line() {
1577 let temp_dir = std::env::temp_dir().join(format!(
1578 "mobench-addr2line-{}-{}",
1579 std::process::id(),
1580 std::time::SystemTime::now()
1581 .duration_since(std::time::UNIX_EPOCH)
1582 .expect("system time")
1583 .as_nanos()
1584 ));
1585 std::fs::create_dir_all(&temp_dir).expect("create temp dir");
1586 let tool_path = temp_dir.join("llvm-addr2line.sh");
1587 let args_path = temp_dir.join("args.txt");
1588 let script = format!(
1589 "#!/bin/sh\nprintf '%s\\n' \"$@\" > '{}'\nprintf '%s\\n' 'sample_fns::fibonacci at /tmp/src/lib.rs:131'\n",
1590 args_path.display()
1591 );
1592 std::fs::write(&tool_path, script).expect("write shim");
1593
1594 #[cfg(unix)]
1595 {
1596 use std::os::unix::fs::PermissionsExt;
1597 let mut perms = std::fs::metadata(&tool_path)
1598 .expect("metadata")
1599 .permissions();
1600 perms.set_mode(0o755);
1601 std::fs::set_permissions(&tool_path, perms).expect("chmod");
1602 }
1603
1604 let symbol = resolve_android_native_symbol_with_tool(
1605 &tool_path,
1606 Path::new("/cargo/target/aarch64-linux-android/release/libsample_fns.so"),
1607 94_138,
1608 );
1609
1610 assert_eq!(symbol.as_deref(), Some("sample_fns::fibonacci"));
1611
1612 let args = std::fs::read_to_string(&args_path).expect("read args");
1613 let expected_offset = format!("0x{:x}", 94_138);
1614 assert!(
1615 args.lines().any(|line| line == "-Cfpe"),
1616 "expected llvm-addr2line to be called with -Cfpe, got:\n{args}"
1617 );
1618 assert!(
1619 args.lines().any(|line| {
1620 line == "/cargo/target/aarch64-linux-android/release/libsample_fns.so"
1621 }),
1622 "expected llvm-addr2line to use the unstripped library path, got:\n{args}"
1623 );
1624 assert!(
1625 args.lines().any(|line| line == expected_offset),
1626 "expected llvm-addr2line to receive the resolved offset, got:\n{args}"
1627 );
1628 }
1629
1630 #[test]
1631 fn android_native_offsets_preserve_unresolved_frames() {
1632 let input = "dev.world.samplefns;libsample_fns.so[+94138];libother.so[+17] 1";
1633 let output =
1634 symbolize_android_native_stack_line_with_resolver(input, |library_name, offset| {
1635 if library_name == "libsample_fns.so" && offset == 94_138 {
1636 Some("sample_fns::fibonacci".into())
1637 } else {
1638 None
1639 }
1640 });
1641
1642 assert!(output.line.contains("sample_fns::fibonacci"));
1643 assert!(output.line.contains("libother.so[+17]"));
1644 assert_eq!(output.resolved_frames, 1);
1645 assert_eq!(output.unresolved_frames, 1);
1646 }
1647
1648 #[test]
1649 fn test_find_crate_dir_current_directory_is_crate() {
1650 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-current");
1652 let _ = std::fs::remove_dir_all(&temp_dir);
1653 std::fs::create_dir_all(&temp_dir).unwrap();
1654
1655 std::fs::write(
1657 temp_dir.join("Cargo.toml"),
1658 r#"[package]
1659name = "bench-mobile"
1660version = "0.1.0"
1661"#,
1662 )
1663 .unwrap();
1664
1665 let builder = AndroidBuilder::new(&temp_dir, "bench-mobile");
1666 let result = builder.find_crate_dir();
1667 assert!(result.is_ok(), "Should find crate in current directory");
1668 assert_eq!(result.unwrap(), temp_dir);
1669
1670 std::fs::remove_dir_all(&temp_dir).unwrap();
1671 }
1672
1673 #[test]
1674 fn test_find_crate_dir_nested_bench_mobile() {
1675 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-nested");
1677 let _ = std::fs::remove_dir_all(&temp_dir);
1678 std::fs::create_dir_all(temp_dir.join("bench-mobile")).unwrap();
1679
1680 std::fs::write(
1682 temp_dir.join("Cargo.toml"),
1683 r#"[workspace]
1684members = ["bench-mobile"]
1685"#,
1686 )
1687 .unwrap();
1688
1689 std::fs::write(
1691 temp_dir.join("bench-mobile/Cargo.toml"),
1692 r#"[package]
1693name = "bench-mobile"
1694version = "0.1.0"
1695"#,
1696 )
1697 .unwrap();
1698
1699 let builder = AndroidBuilder::new(&temp_dir, "bench-mobile");
1700 let result = builder.find_crate_dir();
1701 assert!(
1702 result.is_ok(),
1703 "Should find crate in bench-mobile/ directory"
1704 );
1705 assert_eq!(result.unwrap(), temp_dir.join("bench-mobile"));
1706
1707 std::fs::remove_dir_all(&temp_dir).unwrap();
1708 }
1709
1710 #[test]
1711 fn test_find_crate_dir_crates_subdir() {
1712 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-crates");
1714 let _ = std::fs::remove_dir_all(&temp_dir);
1715 std::fs::create_dir_all(temp_dir.join("crates/my-bench")).unwrap();
1716
1717 std::fs::write(
1719 temp_dir.join("Cargo.toml"),
1720 r#"[workspace]
1721members = ["crates/*"]
1722"#,
1723 )
1724 .unwrap();
1725
1726 std::fs::write(
1728 temp_dir.join("crates/my-bench/Cargo.toml"),
1729 r#"[package]
1730name = "my-bench"
1731version = "0.1.0"
1732"#,
1733 )
1734 .unwrap();
1735
1736 let builder = AndroidBuilder::new(&temp_dir, "my-bench");
1737 let result = builder.find_crate_dir();
1738 assert!(result.is_ok(), "Should find crate in crates/ directory");
1739 assert_eq!(result.unwrap(), temp_dir.join("crates/my-bench"));
1740
1741 std::fs::remove_dir_all(&temp_dir).unwrap();
1742 }
1743
1744 #[test]
1745 fn test_find_crate_dir_not_found() {
1746 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-notfound");
1748 let _ = std::fs::remove_dir_all(&temp_dir);
1749 std::fs::create_dir_all(&temp_dir).unwrap();
1750
1751 std::fs::write(
1753 temp_dir.join("Cargo.toml"),
1754 r#"[package]
1755name = "some-other-crate"
1756version = "0.1.0"
1757"#,
1758 )
1759 .unwrap();
1760
1761 let builder = AndroidBuilder::new(&temp_dir, "nonexistent-crate");
1762 let result = builder.find_crate_dir();
1763 assert!(result.is_err(), "Should fail to find nonexistent crate");
1764 let err_msg = result.unwrap_err().to_string();
1765 assert!(err_msg.contains("Benchmark crate 'nonexistent-crate' not found"));
1766 assert!(err_msg.contains("Searched locations"));
1767
1768 std::fs::remove_dir_all(&temp_dir).unwrap();
1769 }
1770
1771 #[test]
1772 fn test_find_crate_dir_explicit_crate_path() {
1773 let temp_dir = std::env::temp_dir().join("mobench-test-find-crate-explicit");
1775 let _ = std::fs::remove_dir_all(&temp_dir);
1776 std::fs::create_dir_all(temp_dir.join("custom-location")).unwrap();
1777
1778 let builder =
1779 AndroidBuilder::new(&temp_dir, "any-name").crate_dir(temp_dir.join("custom-location"));
1780 let result = builder.find_crate_dir();
1781 assert!(result.is_ok(), "Should use explicit crate_dir");
1782 assert_eq!(result.unwrap(), temp_dir.join("custom-location"));
1783
1784 std::fs::remove_dir_all(&temp_dir).unwrap();
1785 }
1786}