1use crate::types::{BenchError, BuildConfig, BuildProfile, BuildResult, Target};
74use super::common::{get_cargo_target_dir, host_lib_path, run_command, validate_project_root};
75use std::env;
76use std::fs;
77use std::path::{Path, PathBuf};
78use std::process::Command;
79
80pub struct IosBuilder {
109 project_root: PathBuf,
111 output_dir: PathBuf,
113 crate_name: String,
115 verbose: bool,
117 crate_dir: Option<PathBuf>,
119 dry_run: bool,
121}
122
123impl IosBuilder {
124 pub fn new(project_root: impl Into<PathBuf>, crate_name: impl Into<String>) -> Self {
133 let root_input = project_root.into();
134 let root = root_input.canonicalize().unwrap_or(root_input);
136 Self {
137 output_dir: root.join("target/mobench"),
138 project_root: root,
139 crate_name: crate_name.into(),
140 verbose: false,
141 crate_dir: None,
142 dry_run: false,
143 }
144 }
145
146 pub fn output_dir(mut self, dir: impl Into<PathBuf>) -> Self {
151 self.output_dir = dir.into();
152 self
153 }
154
155 pub fn crate_dir(mut self, dir: impl Into<PathBuf>) -> Self {
165 self.crate_dir = Some(dir.into());
166 self
167 }
168
169 pub fn verbose(mut self, verbose: bool) -> Self {
171 self.verbose = verbose;
172 self
173 }
174
175 pub fn dry_run(mut self, dry_run: bool) -> Self {
180 self.dry_run = dry_run;
181 self
182 }
183
184 pub fn build(&self, config: &BuildConfig) -> Result<BuildResult, BenchError> {
199 if self.crate_dir.is_none() {
201 validate_project_root(&self.project_root, &self.crate_name)?;
202 }
203
204 let framework_name = self.crate_name.replace("-", "_");
205 let ios_dir = self.output_dir.join("ios");
206 let xcframework_path = ios_dir.join(format!("{}.xcframework", framework_name));
207
208 if self.dry_run {
209 println!("\n[dry-run] iOS build plan:");
210 println!(" Step 0: Check/generate iOS project scaffolding at {:?}", ios_dir.join("BenchRunner"));
211 println!(" Step 1: Build Rust libraries for iOS targets");
212 println!(" Command: cargo build --target aarch64-apple-ios --lib {}",
213 if matches!(config.profile, BuildProfile::Release) { "--release" } else { "" });
214 println!(" Command: cargo build --target aarch64-apple-ios-sim --lib {}",
215 if matches!(config.profile, BuildProfile::Release) { "--release" } else { "" });
216 println!(" Command: cargo build --target x86_64-apple-ios --lib {}",
217 if matches!(config.profile, BuildProfile::Release) { "--release" } else { "" });
218 println!(" Step 2: Generate UniFFI Swift bindings");
219 println!(" Output: {:?}", ios_dir.join("BenchRunner/BenchRunner/Generated"));
220 println!(" Step 3: Create xcframework at {:?}", xcframework_path);
221 println!(" - ios-arm64/{}.framework (device)", framework_name);
222 println!(" - ios-arm64_x86_64-simulator/{}.framework (simulator - arm64 + x86_64 lipo)", framework_name);
223 println!(" Step 4: Code-sign xcframework");
224 println!(" Command: codesign --force --deep --sign - {:?}", xcframework_path);
225 println!(" Step 5: Generate Xcode project with xcodegen (if project.yml exists)");
226 println!(" Command: xcodegen generate");
227
228 return Ok(BuildResult {
230 platform: Target::Ios,
231 app_path: xcframework_path,
232 test_suite_path: None,
233 });
234 }
235
236 crate::codegen::ensure_ios_project_with_options(
239 &self.output_dir,
240 &self.crate_name,
241 Some(&self.project_root),
242 self.crate_dir.as_deref(),
243 )?;
244
245 println!("Building Rust libraries for iOS...");
247 self.build_rust_libraries(config)?;
248
249 println!("Generating UniFFI Swift bindings...");
251 self.generate_uniffi_bindings()?;
252
253 println!("Creating xcframework...");
255 let xcframework_path = self.create_xcframework(config)?;
256
257 println!("Code-signing xcframework...");
259 self.codesign_xcframework(&xcframework_path)?;
260
261 let header_src = self
263 .find_uniffi_header(&format!("{}FFI.h", framework_name))
264 .ok_or_else(|| {
265 BenchError::Build(format!(
266 "UniFFI header {}FFI.h not found after generation",
267 framework_name
268 ))
269 })?;
270 let include_dir = self.output_dir.join("ios/include");
271 fs::create_dir_all(&include_dir).map_err(|e| {
272 BenchError::Build(format!(
273 "Failed to create include dir at {}: {}. Check output directory permissions.",
274 include_dir.display(),
275 e
276 ))
277 })?;
278 let header_dest = include_dir.join(format!("{}.h", framework_name));
279 fs::copy(&header_src, &header_dest).map_err(|e| {
280 BenchError::Build(format!(
281 "Failed to copy UniFFI header to {:?}: {}. Check output directory permissions.",
282 header_dest, e
283 ))
284 })?;
285
286 self.generate_xcode_project()?;
288
289 let result = BuildResult {
291 platform: Target::Ios,
292 app_path: xcframework_path,
293 test_suite_path: None,
294 };
295 self.validate_build_artifacts(&result, config)?;
296
297 Ok(result)
298 }
299
300 fn validate_build_artifacts(&self, result: &BuildResult, config: &BuildConfig) -> Result<(), BenchError> {
302 let mut missing = Vec::new();
303 let framework_name = self.crate_name.replace("-", "_");
304 let profile_dir = match config.profile {
305 BuildProfile::Debug => "debug",
306 BuildProfile::Release => "release",
307 };
308
309 if !result.app_path.exists() {
311 missing.push(format!("XCFramework: {}", result.app_path.display()));
312 }
313
314 let xcframework_path = &result.app_path;
316 let device_slice = xcframework_path.join(format!("ios-arm64/{}.framework", framework_name));
317 let sim_slice = xcframework_path.join(format!("ios-arm64_x86_64-simulator/{}.framework", framework_name));
319
320 if xcframework_path.exists() {
321 if !device_slice.exists() {
322 missing.push(format!("Device framework slice: {}", device_slice.display()));
323 }
324 if !sim_slice.exists() {
325 missing.push(format!("Simulator framework slice (arm64+x86_64): {}", sim_slice.display()));
326 }
327 }
328
329 let crate_dir = self.find_crate_dir()?;
331 let target_dir = get_cargo_target_dir(&crate_dir)?;
332 let lib_name = format!("lib{}.a", framework_name);
333
334 let device_lib = target_dir.join("aarch64-apple-ios").join(profile_dir).join(&lib_name);
335 let sim_arm64_lib = target_dir.join("aarch64-apple-ios-sim").join(profile_dir).join(&lib_name);
336 let sim_x86_64_lib = target_dir.join("x86_64-apple-ios").join(profile_dir).join(&lib_name);
337
338 if !device_lib.exists() {
339 missing.push(format!("Device static library: {}", device_lib.display()));
340 }
341 if !sim_arm64_lib.exists() {
342 missing.push(format!("Simulator (arm64) static library: {}", sim_arm64_lib.display()));
343 }
344 if !sim_x86_64_lib.exists() {
345 missing.push(format!("Simulator (x86_64) static library: {}", sim_x86_64_lib.display()));
346 }
347
348 let swift_bindings = self.output_dir
350 .join("ios/BenchRunner/BenchRunner/Generated")
351 .join(format!("{}.swift", framework_name));
352 if !swift_bindings.exists() {
353 missing.push(format!("Swift bindings: {}", swift_bindings.display()));
354 }
355
356 if !missing.is_empty() {
357 let critical = missing.iter().any(|m| m.contains("XCFramework") || m.contains("static library"));
358 if critical {
359 return Err(BenchError::Build(format!(
360 "Build validation failed: Critical artifacts are missing.\n\n\
361 Missing artifacts:\n{}\n\n\
362 This usually means the Rust build step failed. Check the cargo build output above.",
363 missing.iter().map(|s| format!(" - {}", s)).collect::<Vec<_>>().join("\n")
364 )));
365 } else {
366 eprintln!(
367 "Warning: Some build artifacts are missing:\n{}\n\
368 The build may still work but some features might be unavailable.",
369 missing.iter().map(|s| format!(" - {}", s)).collect::<Vec<_>>().join("\n")
370 );
371 }
372 }
373
374 Ok(())
375 }
376
377 fn find_crate_dir(&self) -> Result<PathBuf, BenchError> {
385 if let Some(ref dir) = self.crate_dir {
387 if dir.exists() {
388 return Ok(dir.clone());
389 }
390 return Err(BenchError::Build(format!(
391 "Specified crate path does not exist: {:?}.\n\n\
392 Tip: pass --crate-path pointing at a directory containing Cargo.toml.",
393 dir
394 )));
395 }
396
397 let root_cargo_toml = self.project_root.join("Cargo.toml");
400 if root_cargo_toml.exists() {
401 if let Some(pkg_name) = super::common::read_package_name(&root_cargo_toml) {
402 if pkg_name == self.crate_name {
403 return Ok(self.project_root.clone());
404 }
405 }
406 }
407
408 let bench_mobile_dir = self.project_root.join("bench-mobile");
410 if bench_mobile_dir.exists() {
411 return Ok(bench_mobile_dir);
412 }
413
414 let crates_dir = self.project_root.join("crates").join(&self.crate_name);
416 if crates_dir.exists() {
417 return Ok(crates_dir);
418 }
419
420 let named_dir = self.project_root.join(&self.crate_name);
422 if named_dir.exists() {
423 return Ok(named_dir);
424 }
425
426 let root_manifest = root_cargo_toml;
427 let bench_mobile_manifest = bench_mobile_dir.join("Cargo.toml");
428 let crates_manifest = crates_dir.join("Cargo.toml");
429 let named_manifest = named_dir.join("Cargo.toml");
430 Err(BenchError::Build(format!(
431 "Benchmark crate '{}' not found.\n\n\
432 Searched locations:\n\
433 - {} (checked [package] name)\n\
434 - {}\n\
435 - {}\n\
436 - {}\n\n\
437 To fix this:\n\
438 1. Run from the crate directory (where Cargo.toml has name = \"{}\")\n\
439 2. Create a bench-mobile/ directory with your benchmark crate, or\n\
440 3. Use --crate-path to specify the benchmark crate location:\n\
441 cargo mobench build --target ios --crate-path ./my-benchmarks\n\n\
442 Common issues:\n\
443 - Typo in crate name (check Cargo.toml [package] name)\n\
444 - Wrong working directory (run from project root)\n\
445 - Missing Cargo.toml in the crate directory\n\n\
446 Run 'cargo mobench init --help' to generate a new benchmark project.",
447 self.crate_name,
448 root_manifest.display(),
449 bench_mobile_manifest.display(),
450 crates_manifest.display(),
451 named_manifest.display(),
452 self.crate_name,
453 )))
454 }
455
456 fn build_rust_libraries(&self, config: &BuildConfig) -> Result<(), BenchError> {
458 let crate_dir = self.find_crate_dir()?;
459
460 let targets = vec![
462 "aarch64-apple-ios", "aarch64-apple-ios-sim", "x86_64-apple-ios", ];
466
467 self.check_rust_targets(&targets)?;
469 let release_flag = if matches!(config.profile, BuildProfile::Release) {
470 "--release"
471 } else {
472 ""
473 };
474
475 for target in targets {
476 if self.verbose {
477 println!(" Building for {}", target);
478 }
479
480 let mut cmd = Command::new("cargo");
481 cmd.arg("build").arg("--target").arg(target).arg("--lib");
482
483 if !release_flag.is_empty() {
485 cmd.arg(release_flag);
486 }
487
488 cmd.current_dir(&crate_dir);
490
491 let command_hint = if release_flag.is_empty() {
493 format!("cargo build --target {} --lib", target)
494 } else {
495 format!("cargo build --target {} --lib {}", target, release_flag)
496 };
497 let output = cmd
498 .output()
499 .map_err(|e| BenchError::Build(format!(
500 "Failed to run cargo for {}.\n\n\
501 Command: {}\n\
502 Crate directory: {}\n\
503 Error: {}\n\n\
504 Tip: ensure cargo is installed and on PATH.",
505 target,
506 command_hint,
507 crate_dir.display(),
508 e
509 )))?;
510
511 if !output.status.success() {
512 let stdout = String::from_utf8_lossy(&output.stdout);
513 let stderr = String::from_utf8_lossy(&output.stderr);
514 return Err(BenchError::Build(format!(
515 "cargo build failed for {}.\n\n\
516 Command: {}\n\
517 Crate directory: {}\n\
518 Exit status: {}\n\n\
519 Stdout:\n{}\n\n\
520 Stderr:\n{}\n\n\
521 Tips:\n\
522 - Ensure Xcode command line tools are installed (xcode-select --install)\n\
523 - Confirm Rust targets are installed (rustup target add {})",
524 target,
525 command_hint,
526 crate_dir.display(),
527 output.status,
528 stdout,
529 stderr,
530 target
531 )));
532 }
533 }
534
535 Ok(())
536 }
537
538 fn check_rust_targets(&self, targets: &[&str]) -> Result<(), BenchError> {
540 let output = Command::new("rustup")
541 .arg("target")
542 .arg("list")
543 .arg("--installed")
544 .output()
545 .map_err(|e| {
546 BenchError::Build(format!(
547 "Failed to check rustup targets: {}. Ensure rustup is installed and on PATH.",
548 e
549 ))
550 })?;
551
552 let installed = String::from_utf8_lossy(&output.stdout);
553
554 for target in targets {
555 if !installed.contains(target) {
556 return Err(BenchError::Build(format!(
557 "Rust target '{}' is not installed.\n\n\
558 This target is required to compile for iOS.\n\n\
559 To install:\n\
560 rustup target add {}\n\n\
561 For a complete iOS setup, you need all three:\n\
562 rustup target add aarch64-apple-ios # Device\n\
563 rustup target add aarch64-apple-ios-sim # Simulator (Apple Silicon)\n\
564 rustup target add x86_64-apple-ios # Simulator (Intel Macs)",
565 target, target
566 )));
567 }
568 }
569
570 Ok(())
571 }
572
573 fn generate_uniffi_bindings(&self) -> Result<(), BenchError> {
575 let crate_dir = self.find_crate_dir()?;
576 let crate_name_underscored = self.crate_name.replace("-", "_");
577
578 let bindings_path = self
580 .output_dir
581 .join("ios")
582 .join("BenchRunner")
583 .join("BenchRunner")
584 .join("Generated")
585 .join(format!("{}.swift", crate_name_underscored));
586
587 if bindings_path.exists() {
588 if self.verbose {
589 println!(" Using existing Swift bindings at {:?}", bindings_path);
590 }
591 return Ok(());
592 }
593
594 let mut build_cmd = Command::new("cargo");
596 build_cmd.arg("build");
597 build_cmd.current_dir(&crate_dir);
598 run_command(build_cmd, "cargo build (host)")?;
599
600 let lib_path = host_lib_path(&crate_dir, &self.crate_name)?;
601 let out_dir = self
602 .output_dir
603 .join("ios")
604 .join("BenchRunner")
605 .join("BenchRunner")
606 .join("Generated");
607 fs::create_dir_all(&out_dir).map_err(|e| {
608 BenchError::Build(format!(
609 "Failed to create Swift bindings dir at {}: {}. Check output directory permissions.",
610 out_dir.display(),
611 e
612 ))
613 })?;
614
615 let cargo_run_result = Command::new("cargo")
617 .args(["run", "-p", &self.crate_name, "--bin", "uniffi-bindgen", "--"])
618 .arg("generate")
619 .arg("--library")
620 .arg(&lib_path)
621 .arg("--language")
622 .arg("swift")
623 .arg("--out-dir")
624 .arg(&out_dir)
625 .current_dir(&crate_dir)
626 .output();
627
628 let use_cargo_run = cargo_run_result
629 .as_ref()
630 .map(|o| o.status.success())
631 .unwrap_or(false);
632
633 if use_cargo_run {
634 if self.verbose {
635 println!(" Generated bindings using cargo run uniffi-bindgen");
636 }
637 } else {
638 let uniffi_available = Command::new("uniffi-bindgen")
640 .arg("--version")
641 .output()
642 .map(|o| o.status.success())
643 .unwrap_or(false);
644
645 if !uniffi_available {
646 return Err(BenchError::Build(
647 "uniffi-bindgen not found and no pre-generated bindings exist.\n\n\
648 To fix this, either:\n\
649 1. Add a uniffi-bindgen binary to your crate:\n\
650 [[bin]]\n\
651 name = \"uniffi-bindgen\"\n\
652 path = \"src/bin/uniffi-bindgen.rs\"\n\n\
653 2. Or install uniffi-bindgen globally:\n\
654 cargo install uniffi-bindgen\n\n\
655 3. Or pre-generate bindings and commit them."
656 .to_string(),
657 ));
658 }
659
660 let mut cmd = Command::new("uniffi-bindgen");
661 cmd.arg("generate")
662 .arg("--library")
663 .arg(&lib_path)
664 .arg("--language")
665 .arg("swift")
666 .arg("--out-dir")
667 .arg(&out_dir);
668 run_command(cmd, "uniffi-bindgen swift")?;
669 }
670
671 if self.verbose {
672 println!(" Generated UniFFI Swift bindings at {:?}", out_dir);
673 }
674
675 Ok(())
676 }
677
678 fn create_xcframework(&self, config: &BuildConfig) -> Result<PathBuf, BenchError> {
680 let profile_dir = match config.profile {
681 BuildProfile::Debug => "debug",
682 BuildProfile::Release => "release",
683 };
684
685 let crate_dir = self.find_crate_dir()?;
686 let target_dir = get_cargo_target_dir(&crate_dir)?;
687 let xcframework_dir = self.output_dir.join("ios");
688 let framework_name = &self.crate_name.replace("-", "_");
689 let xcframework_path = xcframework_dir.join(format!("{}.xcframework", framework_name));
690
691 if xcframework_path.exists() {
693 fs::remove_dir_all(&xcframework_path).map_err(|e| {
694 BenchError::Build(format!(
695 "Failed to remove old xcframework at {}: {}. Close any tools using it and retry.",
696 xcframework_path.display(),
697 e
698 ))
699 })?;
700 }
701
702 fs::create_dir_all(&xcframework_dir).map_err(|e| {
704 BenchError::Build(format!(
705 "Failed to create xcframework directory at {}: {}. Check output directory permissions.",
706 xcframework_dir.display(),
707 e
708 ))
709 })?;
710
711 self.create_framework_slice(
714 &target_dir.join("aarch64-apple-ios").join(profile_dir),
715 &xcframework_path.join("ios-arm64"),
716 framework_name,
717 "ios",
718 )?;
719
720 self.create_simulator_framework_slice(
722 &target_dir,
723 profile_dir,
724 &xcframework_path.join("ios-arm64_x86_64-simulator"),
725 framework_name,
726 )?;
727
728 self.create_xcframework_plist(&xcframework_path, framework_name)?;
730
731 Ok(xcframework_path)
732 }
733
734 fn create_framework_slice(
736 &self,
737 lib_path: &Path,
738 output_dir: &Path,
739 framework_name: &str,
740 platform: &str,
741 ) -> Result<(), BenchError> {
742 let framework_dir = output_dir.join(format!("{}.framework", framework_name));
743 let headers_dir = framework_dir.join("Headers");
744
745 fs::create_dir_all(&headers_dir).map_err(|e| {
747 BenchError::Build(format!(
748 "Failed to create framework directories at {}: {}. Check output directory permissions.",
749 headers_dir.display(),
750 e
751 ))
752 })?;
753
754 let src_lib = lib_path.join(format!("lib{}.a", framework_name));
756 let dest_lib = framework_dir.join(framework_name);
757
758 if !src_lib.exists() {
759 return Err(BenchError::Build(format!(
760 "Static library not found at {}.\n\n\
761 Expected output from cargo build --target <target> --lib.\n\
762 Ensure your crate has [lib] crate-type = [\"staticlib\"].",
763 src_lib.display()
764 )));
765 }
766
767 fs::copy(&src_lib, &dest_lib).map_err(|e| {
768 BenchError::Build(format!(
769 "Failed to copy static library from {} to {}: {}. Check output directory permissions.",
770 src_lib.display(),
771 dest_lib.display(),
772 e
773 ))
774 })?;
775
776 let header_name = format!("{}FFI.h", framework_name);
778 let header_path = self.find_uniffi_header(&header_name).ok_or_else(|| {
779 BenchError::Build(format!(
780 "UniFFI header {} not found; run binding generation before building",
781 header_name
782 ))
783 })?;
784 let dest_header = headers_dir.join(&header_name);
785 fs::copy(&header_path, &dest_header).map_err(|e| {
786 BenchError::Build(format!(
787 "Failed to copy UniFFI header from {} to {}: {}. Check output directory permissions.",
788 header_path.display(),
789 dest_header.display(),
790 e
791 ))
792 })?;
793
794 let modulemap_content = format!(
796 "framework module {} {{\n umbrella header \"{}FFI.h\"\n export *\n module * {{ export * }}\n}}",
797 framework_name, framework_name
798 );
799 let modulemap_path = headers_dir.join("module.modulemap");
800 fs::write(&modulemap_path, modulemap_content).map_err(|e| {
801 BenchError::Build(format!(
802 "Failed to write module.modulemap at {}: {}. Check output directory permissions.",
803 modulemap_path.display(),
804 e
805 ))
806 })?;
807
808 self.create_framework_plist(&framework_dir, framework_name, platform)?;
810
811 Ok(())
812 }
813
814 fn create_simulator_framework_slice(
816 &self,
817 target_dir: &Path,
818 profile_dir: &str,
819 output_dir: &Path,
820 framework_name: &str,
821 ) -> Result<(), BenchError> {
822 let framework_dir = output_dir.join(format!("{}.framework", framework_name));
823 let headers_dir = framework_dir.join("Headers");
824
825 fs::create_dir_all(&headers_dir).map_err(|e| {
827 BenchError::Build(format!(
828 "Failed to create framework directories at {}: {}. Check output directory permissions.",
829 headers_dir.display(),
830 e
831 ))
832 })?;
833
834 let arm64_lib = target_dir
836 .join("aarch64-apple-ios-sim")
837 .join(profile_dir)
838 .join(format!("lib{}.a", framework_name));
839 let x86_64_lib = target_dir
840 .join("x86_64-apple-ios")
841 .join(profile_dir)
842 .join(format!("lib{}.a", framework_name));
843
844 if !arm64_lib.exists() {
846 return Err(BenchError::Build(format!(
847 "Simulator library (arm64) not found at {}.\n\n\
848 Expected output from cargo build --target aarch64-apple-ios-sim --lib.\n\
849 Ensure your crate has [lib] crate-type = [\"staticlib\"].",
850 arm64_lib.display()
851 )));
852 }
853 if !x86_64_lib.exists() {
854 return Err(BenchError::Build(format!(
855 "Simulator library (x86_64) not found at {}.\n\n\
856 Expected output from cargo build --target x86_64-apple-ios --lib.\n\
857 Ensure your crate has [lib] crate-type = [\"staticlib\"].",
858 x86_64_lib.display()
859 )));
860 }
861
862 let dest_lib = framework_dir.join(framework_name);
864 let output = Command::new("lipo")
865 .arg("-create")
866 .arg(&arm64_lib)
867 .arg(&x86_64_lib)
868 .arg("-output")
869 .arg(&dest_lib)
870 .output()
871 .map_err(|e| {
872 BenchError::Build(format!(
873 "Failed to run lipo to create universal simulator binary.\n\n\
874 Command: lipo -create {} {} -output {}\n\
875 Error: {}\n\n\
876 Ensure Xcode command line tools are installed: xcode-select --install",
877 arm64_lib.display(),
878 x86_64_lib.display(),
879 dest_lib.display(),
880 e
881 ))
882 })?;
883
884 if !output.status.success() {
885 let stderr = String::from_utf8_lossy(&output.stderr);
886 return Err(BenchError::Build(format!(
887 "lipo failed to create universal simulator binary.\n\n\
888 Command: lipo -create {} {} -output {}\n\
889 Exit status: {}\n\
890 Stderr: {}\n\n\
891 Ensure both libraries are valid static libraries.",
892 arm64_lib.display(),
893 x86_64_lib.display(),
894 dest_lib.display(),
895 output.status,
896 stderr
897 )));
898 }
899
900 if self.verbose {
901 println!(
902 " Created universal simulator binary (arm64 + x86_64) at {:?}",
903 dest_lib
904 );
905 }
906
907 let header_name = format!("{}FFI.h", framework_name);
909 let header_path = self.find_uniffi_header(&header_name).ok_or_else(|| {
910 BenchError::Build(format!(
911 "UniFFI header {} not found; run binding generation before building",
912 header_name
913 ))
914 })?;
915 let dest_header = headers_dir.join(&header_name);
916 fs::copy(&header_path, &dest_header).map_err(|e| {
917 BenchError::Build(format!(
918 "Failed to copy UniFFI header from {} to {}: {}. Check output directory permissions.",
919 header_path.display(),
920 dest_header.display(),
921 e
922 ))
923 })?;
924
925 let modulemap_content = format!(
927 "framework module {} {{\n umbrella header \"{}FFI.h\"\n export *\n module * {{ export * }}\n}}",
928 framework_name, framework_name
929 );
930 let modulemap_path = headers_dir.join("module.modulemap");
931 fs::write(&modulemap_path, modulemap_content).map_err(|e| {
932 BenchError::Build(format!(
933 "Failed to write module.modulemap at {}: {}. Check output directory permissions.",
934 modulemap_path.display(),
935 e
936 ))
937 })?;
938
939 self.create_framework_plist(&framework_dir, framework_name, "ios-simulator")?;
941
942 Ok(())
943 }
944
945 fn create_framework_plist(
947 &self,
948 framework_dir: &Path,
949 framework_name: &str,
950 platform: &str,
951 ) -> Result<(), BenchError> {
952 let bundle_id: String = framework_name
955 .chars()
956 .filter(|c| c.is_ascii_alphanumeric())
957 .collect::<String>()
958 .to_lowercase();
959 let plist_content = format!(
960 r#"<?xml version="1.0" encoding="UTF-8"?>
961<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
962<plist version="1.0">
963<dict>
964 <key>CFBundleExecutable</key>
965 <string>{}</string>
966 <key>CFBundleIdentifier</key>
967 <string>dev.world.{}</string>
968 <key>CFBundleInfoDictionaryVersion</key>
969 <string>6.0</string>
970 <key>CFBundleName</key>
971 <string>{}</string>
972 <key>CFBundlePackageType</key>
973 <string>FMWK</string>
974 <key>CFBundleShortVersionString</key>
975 <string>0.1.0</string>
976 <key>CFBundleVersion</key>
977 <string>1</string>
978 <key>CFBundleSupportedPlatforms</key>
979 <array>
980 <string>{}</string>
981 </array>
982</dict>
983</plist>"#,
984 framework_name,
985 bundle_id,
986 framework_name,
987 if platform == "ios" {
988 "iPhoneOS"
989 } else {
990 "iPhoneSimulator"
991 }
992 );
993
994 let plist_path = framework_dir.join("Info.plist");
995 fs::write(&plist_path, plist_content).map_err(|e| {
996 BenchError::Build(format!(
997 "Failed to write framework Info.plist at {}: {}. Check output directory permissions.",
998 plist_path.display(),
999 e
1000 ))
1001 })?;
1002
1003 Ok(())
1004 }
1005
1006 fn create_xcframework_plist(
1008 &self,
1009 xcframework_path: &Path,
1010 framework_name: &str,
1011 ) -> Result<(), BenchError> {
1012 let plist_content = format!(
1013 r#"<?xml version="1.0" encoding="UTF-8"?>
1014<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1015<plist version="1.0">
1016<dict>
1017 <key>AvailableLibraries</key>
1018 <array>
1019 <dict>
1020 <key>LibraryIdentifier</key>
1021 <string>ios-arm64</string>
1022 <key>LibraryPath</key>
1023 <string>{}.framework</string>
1024 <key>SupportedArchitectures</key>
1025 <array>
1026 <string>arm64</string>
1027 </array>
1028 <key>SupportedPlatform</key>
1029 <string>ios</string>
1030 </dict>
1031 <dict>
1032 <key>LibraryIdentifier</key>
1033 <string>ios-arm64_x86_64-simulator</string>
1034 <key>LibraryPath</key>
1035 <string>{}.framework</string>
1036 <key>SupportedArchitectures</key>
1037 <array>
1038 <string>arm64</string>
1039 <string>x86_64</string>
1040 </array>
1041 <key>SupportedPlatform</key>
1042 <string>ios</string>
1043 <key>SupportedPlatformVariant</key>
1044 <string>simulator</string>
1045 </dict>
1046 </array>
1047 <key>CFBundlePackageType</key>
1048 <string>XFWK</string>
1049 <key>XCFrameworkFormatVersion</key>
1050 <string>1.0</string>
1051</dict>
1052</plist>"#,
1053 framework_name, framework_name
1054 );
1055
1056 let plist_path = xcframework_path.join("Info.plist");
1057 fs::write(&plist_path, plist_content).map_err(|e| {
1058 BenchError::Build(format!(
1059 "Failed to write xcframework Info.plist at {}: {}. Check output directory permissions.",
1060 plist_path.display(),
1061 e
1062 ))
1063 })?;
1064
1065 Ok(())
1066 }
1067
1068 fn codesign_xcframework(&self, xcframework_path: &Path) -> Result<(), BenchError> {
1075 let output = Command::new("codesign")
1076 .arg("--force")
1077 .arg("--deep")
1078 .arg("--sign")
1079 .arg("-")
1080 .arg(xcframework_path)
1081 .output()
1082 .map_err(|e| {
1083 BenchError::Build(format!(
1084 "Failed to run codesign.\n\n\
1085 XCFramework: {}\n\
1086 Error: {}\n\n\
1087 Ensure Xcode command line tools are installed:\n\
1088 xcode-select --install\n\n\
1089 The xcframework must be signed for Xcode to accept it.",
1090 xcframework_path.display(),
1091 e
1092 ))
1093 })?;
1094
1095 if output.status.success() {
1096 if self.verbose {
1097 println!(" Successfully code-signed xcframework");
1098 }
1099 Ok(())
1100 } else {
1101 let stderr = String::from_utf8_lossy(&output.stderr);
1102 Err(BenchError::Build(format!(
1103 "codesign failed to sign xcframework.\n\n\
1104 XCFramework: {}\n\
1105 Exit status: {}\n\
1106 Stderr: {}\n\n\
1107 Ensure you have valid signing credentials:\n\
1108 security find-identity -v -p codesigning\n\n\
1109 For ad-hoc signing (most common), the '-' identity should work.\n\
1110 If signing continues to fail, check that the xcframework structure is valid.",
1111 xcframework_path.display(),
1112 output.status,
1113 stderr
1114 )))
1115 }
1116 }
1117
1118 fn generate_xcode_project(&self) -> Result<(), BenchError> {
1128 let ios_dir = self.output_dir.join("ios");
1129 let project_yml = ios_dir.join("BenchRunner/project.yml");
1130
1131 if !project_yml.exists() {
1132 if self.verbose {
1133 println!(" No project.yml found, skipping xcodegen");
1134 }
1135 return Ok(());
1136 }
1137
1138 if self.verbose {
1139 println!(" Generating Xcode project with xcodegen");
1140 }
1141
1142 let project_dir = ios_dir.join("BenchRunner");
1143 let output = Command::new("xcodegen")
1144 .arg("generate")
1145 .current_dir(&project_dir)
1146 .output()
1147 .map_err(|e| {
1148 BenchError::Build(format!(
1149 "Failed to run xcodegen.\n\n\
1150 project.yml found at: {}\n\
1151 Working directory: {}\n\
1152 Error: {}\n\n\
1153 xcodegen is required to generate the Xcode project.\n\
1154 Install it with:\n\
1155 brew install xcodegen\n\n\
1156 After installation, re-run the build.",
1157 project_yml.display(),
1158 project_dir.display(),
1159 e
1160 ))
1161 })?;
1162
1163 if output.status.success() {
1164 if self.verbose {
1165 println!(" Successfully generated Xcode project");
1166 }
1167 Ok(())
1168 } else {
1169 let stdout = String::from_utf8_lossy(&output.stdout);
1170 let stderr = String::from_utf8_lossy(&output.stderr);
1171 Err(BenchError::Build(format!(
1172 "xcodegen failed.\n\n\
1173 Command: xcodegen generate\n\
1174 Working directory: {}\n\
1175 Exit status: {}\n\n\
1176 Stdout:\n{}\n\n\
1177 Stderr:\n{}\n\n\
1178 Check that project.yml is valid YAML and has correct xcodegen syntax.\n\
1179 Try running 'xcodegen generate' manually in {} for more details.",
1180 project_dir.display(),
1181 output.status,
1182 stdout,
1183 stderr,
1184 project_dir.display()
1185 )))
1186 }
1187 }
1188
1189 fn find_uniffi_header(&self, header_name: &str) -> Option<PathBuf> {
1191 let swift_dir = self
1193 .output_dir
1194 .join("ios/BenchRunner/BenchRunner/Generated");
1195 let candidate_swift = swift_dir.join(header_name);
1196 if candidate_swift.exists() {
1197 return Some(candidate_swift);
1198 }
1199
1200 let crate_dir = self.find_crate_dir().ok()?;
1202 let target_dir = get_cargo_target_dir(&crate_dir).ok()?;
1203 let candidate = target_dir.join("uniffi").join(header_name);
1205 if candidate.exists() {
1206 return Some(candidate);
1207 }
1208
1209 let mut stack = vec![target_dir];
1211 while let Some(dir) = stack.pop() {
1212 if let Ok(entries) = fs::read_dir(&dir) {
1213 for entry in entries.flatten() {
1214 let path = entry.path();
1215 if path.is_dir() {
1216 if let Some(name) = path.file_name().and_then(|n| n.to_str())
1218 && name == "incremental"
1219 {
1220 continue;
1221 }
1222 stack.push(path);
1223 } else if let Some(name) = path.file_name().and_then(|n| n.to_str())
1224 && name == header_name
1225 {
1226 return Some(path);
1227 }
1228 }
1229 }
1230 }
1231
1232 None
1233 }
1234}
1235
1236#[allow(clippy::collapsible_if)]
1237fn find_codesign_identity() -> Option<String> {
1238 let output = Command::new("security")
1239 .args(["find-identity", "-v", "-p", "codesigning"])
1240 .output()
1241 .ok()?;
1242 if !output.status.success() {
1243 return None;
1244 }
1245 let stdout = String::from_utf8_lossy(&output.stdout);
1246 let mut identities = Vec::new();
1247 for line in stdout.lines() {
1248 if let Some(start) = line.find('"') {
1249 if let Some(end) = line[start + 1..].find('"') {
1250 identities.push(line[start + 1..start + 1 + end].to_string());
1251 }
1252 }
1253 }
1254 let preferred = [
1255 "Apple Distribution",
1256 "iPhone Distribution",
1257 "Apple Development",
1258 "iPhone Developer",
1259 ];
1260 for label in preferred {
1261 if let Some(identity) = identities.iter().find(|i| i.contains(label)) {
1262 return Some(identity.clone());
1263 }
1264 }
1265 identities.first().cloned()
1266}
1267
1268#[allow(clippy::collapsible_if)]
1269fn find_provisioning_profile() -> Option<PathBuf> {
1270 if let Ok(path) = env::var("MOBENCH_IOS_PROFILE") {
1271 let profile = PathBuf::from(path);
1272 if profile.exists() {
1273 return Some(profile);
1274 }
1275 }
1276 let home = env::var("HOME").ok()?;
1277 let profiles_dir = PathBuf::from(home).join("Library/MobileDevice/Provisioning Profiles");
1278 let entries = fs::read_dir(&profiles_dir).ok()?;
1279 let mut newest: Option<(std::time::SystemTime, PathBuf)> = None;
1280 for entry in entries.flatten() {
1281 let path = entry.path();
1282 if path.extension().and_then(|e| e.to_str()) != Some("mobileprovision") {
1283 continue;
1284 }
1285 if let Ok(metadata) = entry.metadata()
1286 && let Ok(modified) = metadata.modified()
1287 {
1288 match &newest {
1289 Some((current, _)) if *current >= modified => {}
1290 _ => newest = Some((modified, path)),
1291 }
1292 }
1293 }
1294 newest.map(|(_, path)| path)
1295}
1296
1297fn embed_provisioning_profile(app_path: &Path, profile: &Path) -> Result<(), BenchError> {
1298 let dest = app_path.join("embedded.mobileprovision");
1299 fs::copy(profile, &dest).map_err(|e| {
1300 BenchError::Build(format!(
1301 "Failed to embed provisioning profile at {:?}: {}. Check the profile path and file permissions.",
1302 dest, e
1303 ))
1304 })?;
1305 Ok(())
1306}
1307
1308fn codesign_bundle(app_path: &Path, identity: &str) -> Result<(), BenchError> {
1309 let output = Command::new("codesign")
1310 .args(["--force", "--deep", "--sign", identity])
1311 .arg(app_path)
1312 .output()
1313 .map_err(|e| {
1314 BenchError::Build(format!(
1315 "Failed to run codesign: {}. Ensure Xcode command line tools are installed.",
1316 e
1317 ))
1318 })?;
1319 if !output.status.success() {
1320 let stderr = String::from_utf8_lossy(&output.stderr);
1321 return Err(BenchError::Build(format!(
1322 "codesign failed: {}. Verify you have a valid signing identity.",
1323 stderr
1324 )));
1325 }
1326 Ok(())
1327}
1328
1329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1331pub enum SigningMethod {
1332 AdHoc,
1334 Development,
1336}
1337
1338impl IosBuilder {
1339 pub fn package_ipa(&self, scheme: &str, method: SigningMethod) -> Result<PathBuf, BenchError> {
1367 let ios_dir = self.output_dir.join("ios").join(scheme);
1370 let project_path = ios_dir.join(format!("{}.xcodeproj", scheme));
1371
1372 if !project_path.exists() {
1374 return Err(BenchError::Build(format!(
1375 "Xcode project not found at {}.\n\n\
1376 Run `cargo mobench build --target ios` first or check --output-dir.",
1377 project_path.display()
1378 )));
1379 }
1380
1381 let export_path = self.output_dir.join("ios");
1382 let ipa_path = export_path.join(format!("{}.ipa", scheme));
1383
1384 fs::create_dir_all(&export_path).map_err(|e| {
1386 BenchError::Build(format!(
1387 "Failed to create export directory at {}: {}. Check output directory permissions.",
1388 export_path.display(),
1389 e
1390 ))
1391 })?;
1392
1393 println!("Building {} for device...", scheme);
1394
1395 let build_dir = self.output_dir.join("ios/build");
1397 let build_configuration = "Debug";
1398 let mut cmd = Command::new("xcodebuild");
1399 cmd.args([
1400 "-project",
1401 project_path.to_str().unwrap(),
1402 "-scheme",
1403 scheme,
1404 "-destination",
1405 "generic/platform=iOS",
1406 "-configuration",
1407 build_configuration,
1408 "-derivedDataPath",
1409 build_dir.to_str().unwrap(),
1410 "build",
1411 ]);
1412
1413 match method {
1415 SigningMethod::AdHoc => {
1416 cmd.args(["CODE_SIGNING_REQUIRED=NO", "CODE_SIGNING_ALLOWED=NO"]);
1419 }
1420 SigningMethod::Development => {
1421 cmd.args([
1423 "CODE_SIGN_STYLE=Automatic",
1424 "CODE_SIGN_IDENTITY=iPhone Developer",
1425 ]);
1426 }
1427 }
1428
1429 if self.verbose {
1430 println!(" Running: {:?}", cmd);
1431 }
1432
1433 let build_result = cmd.output();
1435
1436 let app_path = build_dir
1438 .join(format!("Build/Products/{}-iphoneos", build_configuration))
1439 .join(format!("{}.app", scheme));
1440
1441 if !app_path.exists() {
1442 match build_result {
1443 Ok(output) => {
1444 let stdout = String::from_utf8_lossy(&output.stdout);
1445 let stderr = String::from_utf8_lossy(&output.stderr);
1446 return Err(BenchError::Build(format!(
1447 "xcodebuild build failed and app bundle was not created.\n\n\
1448 Project: {}\n\
1449 Scheme: {}\n\
1450 Configuration: {}\n\
1451 Derived data: {}\n\
1452 Exit status: {}\n\n\
1453 Stdout:\n{}\n\n\
1454 Stderr:\n{}\n\n\
1455 Tip: run xcodebuild manually to inspect the failure.",
1456 project_path.display(),
1457 scheme,
1458 build_configuration,
1459 build_dir.display(),
1460 output.status,
1461 stdout,
1462 stderr
1463 )));
1464 }
1465 Err(err) => {
1466 return Err(BenchError::Build(format!(
1467 "Failed to run xcodebuild: {}.\n\n\
1468 App bundle not found at {}.\n\
1469 Check that Xcode command line tools are installed.",
1470 err,
1471 app_path.display()
1472 )));
1473 }
1474 }
1475 }
1476
1477 if self.verbose {
1478 println!(" App bundle created successfully at {:?}", app_path);
1479 }
1480
1481 if matches!(method, SigningMethod::AdHoc) {
1482 let profile = find_provisioning_profile();
1483 let identity = find_codesign_identity();
1484 match (profile.as_ref(), identity.as_ref()) {
1485 (Some(profile), Some(identity)) => {
1486 embed_provisioning_profile(&app_path, profile)?;
1487 codesign_bundle(&app_path, identity)?;
1488 if self.verbose {
1489 println!(" Signed app bundle with identity {}", identity);
1490 }
1491 }
1492 _ => {
1493 let output = Command::new("codesign")
1494 .arg("--force")
1495 .arg("--deep")
1496 .arg("--sign")
1497 .arg("-")
1498 .arg(&app_path)
1499 .output();
1500 match output {
1501 Ok(output) if output.status.success() => {
1502 println!(
1503 "Warning: Signed app bundle without provisioning profile; BrowserStack install may fail."
1504 );
1505 }
1506 Ok(output) => {
1507 let stderr = String::from_utf8_lossy(&output.stderr);
1508 println!("Warning: Ad-hoc signing failed: {}", stderr);
1509 }
1510 Err(err) => {
1511 println!("Warning: Could not run codesign: {}", err);
1512 }
1513 }
1514 }
1515 }
1516 }
1517
1518 println!("Creating IPA from app bundle...");
1519
1520 let payload_dir = export_path.join("Payload");
1522 if payload_dir.exists() {
1523 fs::remove_dir_all(&payload_dir).map_err(|e| {
1524 BenchError::Build(format!(
1525 "Failed to remove old Payload dir at {}: {}. Close any tools using it and retry.",
1526 payload_dir.display(),
1527 e
1528 ))
1529 })?;
1530 }
1531 fs::create_dir_all(&payload_dir).map_err(|e| {
1532 BenchError::Build(format!(
1533 "Failed to create Payload dir at {}: {}. Check output directory permissions.",
1534 payload_dir.display(),
1535 e
1536 ))
1537 })?;
1538
1539 let dest_app = payload_dir.join(format!("{}.app", scheme));
1541 self.copy_dir_recursive(&app_path, &dest_app)?;
1542
1543 if ipa_path.exists() {
1545 fs::remove_file(&ipa_path).map_err(|e| {
1546 BenchError::Build(format!(
1547 "Failed to remove old IPA at {}: {}. Check file permissions.",
1548 ipa_path.display(),
1549 e
1550 ))
1551 })?;
1552 }
1553
1554 let mut cmd = Command::new("zip");
1555 cmd.args(["-qr", ipa_path.to_str().unwrap(), "Payload"])
1556 .current_dir(&export_path);
1557
1558 if self.verbose {
1559 println!(" Running: {:?}", cmd);
1560 }
1561
1562 run_command(cmd, "zip IPA")?;
1563
1564 fs::remove_dir_all(&payload_dir).map_err(|e| {
1566 BenchError::Build(format!(
1567 "Failed to clean up Payload dir at {}: {}. Check file permissions.",
1568 payload_dir.display(),
1569 e
1570 ))
1571 })?;
1572
1573 println!("✓ IPA created: {:?}", ipa_path);
1574 Ok(ipa_path)
1575 }
1576
1577 pub fn package_xcuitest(&self, scheme: &str) -> Result<PathBuf, BenchError> {
1582 let ios_dir = self.output_dir.join("ios").join(scheme);
1583 let project_path = ios_dir.join(format!("{}.xcodeproj", scheme));
1584
1585 if !project_path.exists() {
1586 return Err(BenchError::Build(format!(
1587 "Xcode project not found at {}.\n\n\
1588 Run `cargo mobench build --target ios` first or check --output-dir.",
1589 project_path.display()
1590 )));
1591 }
1592
1593 let export_path = self.output_dir.join("ios");
1594 fs::create_dir_all(&export_path).map_err(|e| {
1595 BenchError::Build(format!(
1596 "Failed to create export directory at {}: {}. Check output directory permissions.",
1597 export_path.display(),
1598 e
1599 ))
1600 })?;
1601
1602 let build_dir = self.output_dir.join("ios/build");
1603 println!("Building XCUITest runner for {}...", scheme);
1604
1605 let mut cmd = Command::new("xcodebuild");
1606 cmd.args([
1607 "build-for-testing",
1608 "-project",
1609 project_path.to_str().unwrap(),
1610 "-scheme",
1611 scheme,
1612 "-destination",
1613 "generic/platform=iOS",
1614 "-sdk",
1615 "iphoneos",
1616 "-configuration",
1617 "Release",
1618 "-derivedDataPath",
1619 build_dir.to_str().unwrap(),
1620 "VALIDATE_PRODUCT=NO",
1621 "CODE_SIGN_STYLE=Manual",
1622 "CODE_SIGN_IDENTITY=",
1623 "CODE_SIGNING_ALLOWED=NO",
1624 "CODE_SIGNING_REQUIRED=NO",
1625 "DEVELOPMENT_TEAM=",
1626 "PROVISIONING_PROFILE_SPECIFIER=",
1627 "ENABLE_BITCODE=NO",
1628 "BITCODE_GENERATION_MODE=none",
1629 "STRIP_BITCODE_FROM_COPIED_FILES=NO",
1630 ]);
1631
1632 if self.verbose {
1633 println!(" Running: {:?}", cmd);
1634 }
1635
1636 let runner_name = format!("{}UITests-Runner.app", scheme);
1637 let runner_path = build_dir
1638 .join("Build/Products/Release-iphoneos")
1639 .join(&runner_name);
1640
1641 let build_result = cmd.output();
1642 let log_path = export_path.join("xcuitest-build.log");
1643 if let Ok(output) = &build_result
1644 && !output.status.success()
1645 {
1646 let mut log = String::new();
1647 let stdout = String::from_utf8_lossy(&output.stdout);
1648 let stderr = String::from_utf8_lossy(&output.stderr);
1649 log.push_str("STDOUT:\n");
1650 log.push_str(&stdout);
1651 log.push_str("\n\nSTDERR:\n");
1652 log.push_str(&stderr);
1653 let _ = fs::write(&log_path, log);
1654 println!("xcodebuild log written to {:?}", log_path);
1655 if runner_path.exists() {
1656 println!(
1657 "Warning: xcodebuild build-for-testing failed, but runner exists: {}",
1658 stderr
1659 );
1660 }
1661 }
1662
1663 if !runner_path.exists() {
1664 match build_result {
1665 Ok(output) => {
1666 let stdout = String::from_utf8_lossy(&output.stdout);
1667 let stderr = String::from_utf8_lossy(&output.stderr);
1668 return Err(BenchError::Build(format!(
1669 "xcodebuild build-for-testing failed and runner was not created.\n\n\
1670 Project: {}\n\
1671 Scheme: {}\n\
1672 Derived data: {}\n\
1673 Exit status: {}\n\
1674 Log: {}\n\n\
1675 Stdout:\n{}\n\n\
1676 Stderr:\n{}\n\n\
1677 Tip: open the log file above for more context.",
1678 project_path.display(),
1679 scheme,
1680 build_dir.display(),
1681 output.status,
1682 log_path.display(),
1683 stdout,
1684 stderr
1685 )));
1686 }
1687 Err(err) => {
1688 return Err(BenchError::Build(format!(
1689 "Failed to run xcodebuild: {}.\n\n\
1690 XCUITest runner not found at {}.\n\
1691 Check that Xcode command line tools are installed.",
1692 err,
1693 runner_path.display()
1694 )));
1695 }
1696 }
1697 }
1698
1699 let profile = find_provisioning_profile();
1700 let identity = find_codesign_identity();
1701 if let (Some(profile), Some(identity)) = (profile.as_ref(), identity.as_ref()) {
1702 embed_provisioning_profile(&runner_path, profile)?;
1703 codesign_bundle(&runner_path, identity)?;
1704 if self.verbose {
1705 println!(" Signed XCUITest runner with identity {}", identity);
1706 }
1707 } else {
1708 println!(
1709 "Warning: No provisioning profile/identity found; XCUITest runner may not install."
1710 );
1711 }
1712
1713 let zip_path = export_path.join(format!("{}UITests.zip", scheme));
1714 if zip_path.exists() {
1715 fs::remove_file(&zip_path).map_err(|e| {
1716 BenchError::Build(format!(
1717 "Failed to remove old zip at {}: {}. Check file permissions.",
1718 zip_path.display(),
1719 e
1720 ))
1721 })?;
1722 }
1723
1724 let mut zip_cmd = Command::new("zip");
1725 zip_cmd
1726 .args(["-qr", zip_path.to_str().unwrap(), runner_name.as_str()])
1727 .current_dir(runner_path.parent().unwrap());
1728
1729 if self.verbose {
1730 println!(" Running: {:?}", zip_cmd);
1731 }
1732
1733 run_command(zip_cmd, "zip XCUITest runner")?;
1734 println!("✓ XCUITest runner packaged: {:?}", zip_path);
1735
1736 Ok(zip_path)
1737 }
1738
1739 fn copy_dir_recursive(&self, src: &Path, dest: &Path) -> Result<(), BenchError> {
1741 fs::create_dir_all(dest).map_err(|e| {
1742 BenchError::Build(format!("Failed to create directory {:?}: {}", dest, e))
1743 })?;
1744
1745 for entry in fs::read_dir(src)
1746 .map_err(|e| BenchError::Build(format!("Failed to read directory {:?}: {}", src, e)))?
1747 {
1748 let entry =
1749 entry.map_err(|e| BenchError::Build(format!("Failed to read entry: {}", e)))?;
1750 let path = entry.path();
1751 let file_name = path
1752 .file_name()
1753 .ok_or_else(|| BenchError::Build(format!("Invalid file name in {:?}", path)))?;
1754 let dest_path = dest.join(file_name);
1755
1756 if path.is_dir() {
1757 self.copy_dir_recursive(&path, &dest_path)?;
1758 } else {
1759 fs::copy(&path, &dest_path).map_err(|e| {
1760 BenchError::Build(format!(
1761 "Failed to copy {:?} to {:?}: {}",
1762 path, dest_path, e
1763 ))
1764 })?;
1765 }
1766 }
1767
1768 Ok(())
1769 }
1770}
1771
1772#[cfg(test)]
1773mod tests {
1774 use super::*;
1775
1776 #[test]
1777 fn test_ios_builder_creation() {
1778 let builder = IosBuilder::new("/tmp/test-project", "test-bench-mobile");
1779 assert!(!builder.verbose);
1780 assert_eq!(
1781 builder.output_dir,
1782 PathBuf::from("/tmp/test-project/target/mobench")
1783 );
1784 }
1785
1786 #[test]
1787 fn test_ios_builder_verbose() {
1788 let builder = IosBuilder::new("/tmp/test-project", "test-bench-mobile").verbose(true);
1789 assert!(builder.verbose);
1790 }
1791
1792 #[test]
1793 fn test_ios_builder_custom_output_dir() {
1794 let builder =
1795 IosBuilder::new("/tmp/test-project", "test-bench-mobile").output_dir("/custom/output");
1796 assert_eq!(builder.output_dir, PathBuf::from("/custom/output"));
1797 }
1798
1799 #[test]
1800 fn test_find_crate_dir_current_directory_is_crate() {
1801 let temp_dir = std::env::temp_dir().join("mobench-ios-test-find-crate-current");
1803 let _ = std::fs::remove_dir_all(&temp_dir);
1804 std::fs::create_dir_all(&temp_dir).unwrap();
1805
1806 std::fs::write(
1808 temp_dir.join("Cargo.toml"),
1809 r#"[package]
1810name = "bench-mobile"
1811version = "0.1.0"
1812"#,
1813 )
1814 .unwrap();
1815
1816 let builder = IosBuilder::new(&temp_dir, "bench-mobile");
1817 let result = builder.find_crate_dir();
1818 assert!(result.is_ok(), "Should find crate in current directory");
1819 let expected = temp_dir.canonicalize().unwrap_or(temp_dir.clone());
1821 assert_eq!(result.unwrap(), expected);
1822
1823 std::fs::remove_dir_all(&temp_dir).unwrap();
1824 }
1825
1826 #[test]
1827 fn test_find_crate_dir_nested_bench_mobile() {
1828 let temp_dir = std::env::temp_dir().join("mobench-ios-test-find-crate-nested");
1830 let _ = std::fs::remove_dir_all(&temp_dir);
1831 std::fs::create_dir_all(temp_dir.join("bench-mobile")).unwrap();
1832
1833 std::fs::write(
1835 temp_dir.join("Cargo.toml"),
1836 r#"[workspace]
1837members = ["bench-mobile"]
1838"#,
1839 )
1840 .unwrap();
1841
1842 std::fs::write(
1844 temp_dir.join("bench-mobile/Cargo.toml"),
1845 r#"[package]
1846name = "bench-mobile"
1847version = "0.1.0"
1848"#,
1849 )
1850 .unwrap();
1851
1852 let builder = IosBuilder::new(&temp_dir, "bench-mobile");
1853 let result = builder.find_crate_dir();
1854 assert!(result.is_ok(), "Should find crate in bench-mobile/ directory");
1855 let expected = temp_dir.canonicalize().unwrap_or(temp_dir.clone()).join("bench-mobile");
1856 assert_eq!(result.unwrap(), expected);
1857
1858 std::fs::remove_dir_all(&temp_dir).unwrap();
1859 }
1860
1861 #[test]
1862 fn test_find_crate_dir_crates_subdir() {
1863 let temp_dir = std::env::temp_dir().join("mobench-ios-test-find-crate-crates");
1865 let _ = std::fs::remove_dir_all(&temp_dir);
1866 std::fs::create_dir_all(temp_dir.join("crates/my-bench")).unwrap();
1867
1868 std::fs::write(
1870 temp_dir.join("Cargo.toml"),
1871 r#"[workspace]
1872members = ["crates/*"]
1873"#,
1874 )
1875 .unwrap();
1876
1877 std::fs::write(
1879 temp_dir.join("crates/my-bench/Cargo.toml"),
1880 r#"[package]
1881name = "my-bench"
1882version = "0.1.0"
1883"#,
1884 )
1885 .unwrap();
1886
1887 let builder = IosBuilder::new(&temp_dir, "my-bench");
1888 let result = builder.find_crate_dir();
1889 assert!(result.is_ok(), "Should find crate in crates/ directory");
1890 let expected = temp_dir.canonicalize().unwrap_or(temp_dir.clone()).join("crates/my-bench");
1891 assert_eq!(result.unwrap(), expected);
1892
1893 std::fs::remove_dir_all(&temp_dir).unwrap();
1894 }
1895
1896 #[test]
1897 fn test_find_crate_dir_not_found() {
1898 let temp_dir = std::env::temp_dir().join("mobench-ios-test-find-crate-notfound");
1900 let _ = std::fs::remove_dir_all(&temp_dir);
1901 std::fs::create_dir_all(&temp_dir).unwrap();
1902
1903 std::fs::write(
1905 temp_dir.join("Cargo.toml"),
1906 r#"[package]
1907name = "some-other-crate"
1908version = "0.1.0"
1909"#,
1910 )
1911 .unwrap();
1912
1913 let builder = IosBuilder::new(&temp_dir, "nonexistent-crate");
1914 let result = builder.find_crate_dir();
1915 assert!(result.is_err(), "Should fail to find nonexistent crate");
1916 let err_msg = result.unwrap_err().to_string();
1917 assert!(err_msg.contains("Benchmark crate 'nonexistent-crate' not found"));
1918 assert!(err_msg.contains("Searched locations"));
1919
1920 std::fs::remove_dir_all(&temp_dir).unwrap();
1921 }
1922
1923 #[test]
1924 fn test_find_crate_dir_explicit_crate_path() {
1925 let temp_dir = std::env::temp_dir().join("mobench-ios-test-find-crate-explicit");
1927 let _ = std::fs::remove_dir_all(&temp_dir);
1928 std::fs::create_dir_all(temp_dir.join("custom-location")).unwrap();
1929
1930 let builder = IosBuilder::new(&temp_dir, "any-name")
1931 .crate_dir(temp_dir.join("custom-location"));
1932 let result = builder.find_crate_dir();
1933 assert!(result.is_ok(), "Should use explicit crate_dir");
1934 assert_eq!(result.unwrap(), temp_dir.join("custom-location"));
1935
1936 std::fs::remove_dir_all(&temp_dir).unwrap();
1937 }
1938}