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