1use std::env;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23
24use crate::types::BenchError;
25
26pub fn validate_project_root(project_root: &Path, crate_name: &str) -> Result<(), BenchError> {
40 if !project_root.exists() {
42 return Err(BenchError::Build(format!(
43 "Project root does not exist: {}\n\n\
44 Ensure you are running from the correct directory or specify --project-root.",
45 project_root.display()
46 )));
47 }
48
49 if !project_root.is_dir() {
51 return Err(BenchError::Build(format!(
52 "Project root is not a directory: {}\n\n\
53 Expected a directory containing your Rust project.",
54 project_root.display()
55 )));
56 }
57
58 let root_cargo = project_root.join("Cargo.toml");
60 let bench_mobile_cargo = project_root.join("bench-mobile/Cargo.toml");
61 let crates_cargo = project_root.join(format!("crates/{}/Cargo.toml", crate_name));
62
63 if !root_cargo.exists() && !bench_mobile_cargo.exists() && !crates_cargo.exists() {
64 return Err(BenchError::Build(format!(
65 "No Cargo.toml found in project root or expected crate locations.\n\n\
66 Searched:\n\
67 - {}\n\
68 - {}\n\
69 - {}\n\n\
70 Ensure you are in a Rust project directory or use --crate-path to specify the crate location.",
71 root_cargo.display(),
72 bench_mobile_cargo.display(),
73 crates_cargo.display()
74 )));
75 }
76
77 Ok(())
78}
79
80pub fn get_cargo_target_dir(crate_dir: &Path) -> Result<PathBuf, BenchError> {
95 let output = Command::new("cargo")
96 .args(["metadata", "--format-version", "1", "--no-deps"])
97 .current_dir(crate_dir)
98 .output()
99 .map_err(|e| {
100 BenchError::Build(format!(
101 "Failed to run cargo metadata.\n\n\
102 Working directory: {}\n\
103 Error: {}\n\n\
104 Ensure cargo is installed and on PATH.",
105 crate_dir.display(),
106 e
107 ))
108 })?;
109
110 if !output.status.success() {
111 let fallback = crate_dir.join("target");
113 let stderr = String::from_utf8_lossy(&output.stderr);
114 eprintln!(
115 "Warning: cargo metadata failed (exit {}), falling back to {}.\n\
116 Stderr: {}\n\
117 This may cause build issues if you are in a Cargo workspace.",
118 output.status,
119 fallback.display(),
120 stderr.lines().take(3).collect::<Vec<_>>().join("\n")
121 );
122 return Ok(fallback);
123 }
124
125 let stdout = String::from_utf8_lossy(&output.stdout);
126
127 if let Some(start) = stdout.find("\"target_directory\":\"") {
130 let rest = &stdout[start + 20..];
131 if let Some(end) = rest.find('"') {
132 let target_dir = &rest[..end];
133 let target_dir = target_dir.replace("\\\\", "\\");
135 return Ok(PathBuf::from(target_dir));
136 }
137 }
138
139 let fallback = crate_dir.join("target");
141 eprintln!(
142 "Warning: Failed to parse target_directory from cargo metadata output, \
143 falling back to {}.\n\
144 This may cause build issues if you are in a Cargo workspace.",
145 fallback.display()
146 );
147 Ok(fallback)
148}
149
150pub fn host_lib_path(crate_dir: &Path, crate_name: &str) -> Result<PathBuf, BenchError> {
162 let lib_prefix = if cfg!(target_os = "windows") {
163 ""
164 } else {
165 "lib"
166 };
167 let lib_ext = match env::consts::OS {
168 "macos" => "dylib",
169 "linux" => "so",
170 other => {
171 return Err(BenchError::Build(format!(
172 "Unsupported host OS for binding generation: {}\n\n\
173 Supported platforms:\n\
174 - macOS (generates .dylib)\n\
175 - Linux (generates .so)\n\n\
176 Windows is not currently supported for binding generation.",
177 other
178 )));
179 }
180 };
181
182 let target_dir = get_cargo_target_dir(crate_dir)?;
184
185 let lib_name = format!(
186 "{}{}.{}",
187 lib_prefix,
188 crate_name.replace('-', "_"),
189 lib_ext
190 );
191 let path = target_dir.join("debug").join(&lib_name);
192
193 if !path.exists() {
194 return Err(BenchError::Build(format!(
195 "Host library for UniFFI not found.\n\n\
196 Expected: {}\n\
197 Target directory: {}\n\n\
198 To fix this:\n\
199 1. Build the host library first:\n\
200 cargo build -p {}\n\n\
201 2. Ensure your crate produces a cdylib:\n\
202 [lib]\n\
203 crate-type = [\"cdylib\"]\n\n\
204 3. Check that the library name matches: {}",
205 path.display(),
206 target_dir.display(),
207 crate_name,
208 lib_name
209 )));
210 }
211 Ok(path)
212}
213
214pub fn run_command(mut cmd: Command, description: &str) -> Result<(), BenchError> {
226 let output = cmd.output().map_err(|e| {
227 BenchError::Build(format!(
228 "Failed to start {}.\n\n\
229 Error: {}\n\n\
230 Ensure the tool is installed and available on PATH.",
231 description, e
232 ))
233 })?;
234
235 if !output.status.success() {
236 let stdout = String::from_utf8_lossy(&output.stdout);
237 let stderr = String::from_utf8_lossy(&output.stderr);
238 return Err(BenchError::Build(format!(
239 "{} failed.\n\n\
240 Exit status: {}\n\n\
241 Stdout:\n{}\n\n\
242 Stderr:\n{}",
243 description, output.status, stdout, stderr
244 )));
245 }
246 Ok(())
247}
248
249pub fn read_package_name(cargo_toml_path: &Path) -> Option<String> {
269 let content = std::fs::read_to_string(cargo_toml_path).ok()?;
270
271 let package_start = content.find("[package]")?;
273 let package_section = &content[package_start..];
274
275 let section_end = package_section[1..]
277 .find("\n[")
278 .map(|i| i + 1)
279 .unwrap_or(package_section.len());
280 let package_section = &package_section[..section_end];
281
282 for line in package_section.lines() {
284 let trimmed = line.trim();
285 if trimmed.starts_with("name") {
286 if let Some(eq_pos) = trimmed.find('=') {
288 let value_part = trimmed[eq_pos + 1..].trim();
289 let (quote_char, start) = if value_part.starts_with('"') {
291 ('"', 1)
292 } else if value_part.starts_with('\'') {
293 ('\'', 1)
294 } else {
295 continue;
296 };
297 if let Some(end) = value_part[start..].find(quote_char) {
298 return Some(value_part[start..start + end].to_string());
299 }
300 }
301 }
302 }
303
304 None
305}
306
307pub fn embed_bench_spec<S: serde::Serialize>(output_dir: &Path, spec: &S) -> Result<(), BenchError> {
331 let spec_json = serde_json::to_string_pretty(spec).map_err(|e| {
332 BenchError::Build(format!("Failed to serialize bench spec: {}", e))
333 })?;
334
335 let android_assets_dir = output_dir.join("android/app/src/main/assets");
337 if output_dir.join("android").exists() {
338 std::fs::create_dir_all(&android_assets_dir).map_err(|e| {
339 BenchError::Build(format!(
340 "Failed to create Android assets directory at {}: {}",
341 android_assets_dir.display(),
342 e
343 ))
344 })?;
345 let android_spec_path = android_assets_dir.join("bench_spec.json");
346 std::fs::write(&android_spec_path, &spec_json).map_err(|e| {
347 BenchError::Build(format!(
348 "Failed to write Android bench spec to {}: {}",
349 android_spec_path.display(),
350 e
351 ))
352 })?;
353 }
354
355 let ios_resources_dir = output_dir.join("ios/BenchRunner/BenchRunner/Resources");
357 if output_dir.join("ios/BenchRunner").exists() {
358 std::fs::create_dir_all(&ios_resources_dir).map_err(|e| {
359 BenchError::Build(format!(
360 "Failed to create iOS Resources directory at {}: {}",
361 ios_resources_dir.display(),
362 e
363 ))
364 })?;
365 let ios_spec_path = ios_resources_dir.join("bench_spec.json");
366 std::fs::write(&ios_spec_path, &spec_json).map_err(|e| {
367 BenchError::Build(format!(
368 "Failed to write iOS bench spec to {}: {}",
369 ios_spec_path.display(),
370 e
371 ))
372 })?;
373 }
374
375 Ok(())
376}
377
378#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
383pub struct EmbeddedBenchSpec {
384 pub function: String,
386 pub iterations: u32,
388 pub warmup: u32,
390}
391
392#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
397pub struct BenchMeta {
398 pub spec: EmbeddedBenchSpec,
400 #[serde(skip_serializing_if = "Option::is_none")]
402 pub commit_hash: Option<String>,
403 #[serde(skip_serializing_if = "Option::is_none")]
405 pub branch: Option<String>,
406 #[serde(skip_serializing_if = "Option::is_none")]
408 pub dirty: Option<bool>,
409 pub build_time: String,
411 pub build_time_unix: u64,
413 pub target: String,
415 pub profile: String,
417 pub mobench_version: String,
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub rust_version: Option<String>,
422 pub host_os: String,
424}
425
426pub fn get_git_commit() -> Option<String> {
428 let output = Command::new("git")
429 .args(["rev-parse", "--short", "HEAD"])
430 .output()
431 .ok()?;
432
433 if output.status.success() {
434 let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
435 if !hash.is_empty() {
436 return Some(hash);
437 }
438 }
439 None
440}
441
442pub fn get_git_branch() -> Option<String> {
444 let output = Command::new("git")
445 .args(["rev-parse", "--abbrev-ref", "HEAD"])
446 .output()
447 .ok()?;
448
449 if output.status.success() {
450 let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
451 if !branch.is_empty() && branch != "HEAD" {
452 return Some(branch);
453 }
454 }
455 None
456}
457
458pub fn is_git_dirty() -> Option<bool> {
460 let output = Command::new("git")
461 .args(["status", "--porcelain"])
462 .output()
463 .ok()?;
464
465 if output.status.success() {
466 let status = String::from_utf8_lossy(&output.stdout);
467 Some(!status.trim().is_empty())
468 } else {
469 None
470 }
471}
472
473pub fn get_rust_version() -> Option<String> {
475 let output = Command::new("rustc")
476 .args(["--version"])
477 .output()
478 .ok()?;
479
480 if output.status.success() {
481 let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
482 if !version.is_empty() {
483 return Some(version);
484 }
485 }
486 None
487}
488
489pub fn create_bench_meta(spec: &EmbeddedBenchSpec, target: &str, profile: &str) -> BenchMeta {
491 use std::time::{SystemTime, UNIX_EPOCH};
492
493 let now = SystemTime::now()
494 .duration_since(UNIX_EPOCH)
495 .unwrap_or_default();
496
497 let build_time = {
499 let secs = now.as_secs();
500 let days_since_epoch = secs / 86400;
502 let remaining_secs = secs % 86400;
503 let hours = remaining_secs / 3600;
504 let minutes = (remaining_secs % 3600) / 60;
505 let seconds = remaining_secs % 60;
506
507 let (year, month, day) = days_to_ymd(days_since_epoch);
510
511 format!(
512 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
513 year, month, day, hours, minutes, seconds
514 )
515 };
516
517 BenchMeta {
518 spec: spec.clone(),
519 commit_hash: get_git_commit(),
520 branch: get_git_branch(),
521 dirty: is_git_dirty(),
522 build_time,
523 build_time_unix: now.as_secs(),
524 target: target.to_string(),
525 profile: profile.to_string(),
526 mobench_version: env!("CARGO_PKG_VERSION").to_string(),
527 rust_version: get_rust_version(),
528 host_os: env::consts::OS.to_string(),
529 }
530}
531
532fn days_to_ymd(days: u64) -> (i32, u32, u32) {
535 let mut remaining_days = days as i64;
536 let mut year = 1970i32;
537
538 loop {
540 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
541 if remaining_days < days_in_year {
542 break;
543 }
544 remaining_days -= days_in_year;
545 year += 1;
546 }
547
548 let days_in_months: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
550
551 let mut month = 1u32;
552 for (i, &days_in_month) in days_in_months.iter().enumerate() {
553 let mut dim = days_in_month;
554 if i == 1 && is_leap_year(year) {
555 dim = 29;
556 }
557 if remaining_days < dim {
558 break;
559 }
560 remaining_days -= dim;
561 month += 1;
562 }
563
564 (year, month, remaining_days as u32 + 1)
565}
566
567fn is_leap_year(year: i32) -> bool {
568 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
569}
570
571pub fn embed_bench_meta(
586 output_dir: &Path,
587 spec: &EmbeddedBenchSpec,
588 target: &str,
589 profile: &str,
590) -> Result<(), BenchError> {
591 let meta = create_bench_meta(spec, target, profile);
592 let meta_json = serde_json::to_string_pretty(&meta).map_err(|e| {
593 BenchError::Build(format!("Failed to serialize bench meta: {}", e))
594 })?;
595
596 let android_assets_dir = output_dir.join("android/app/src/main/assets");
598 if output_dir.join("android").exists() {
599 std::fs::create_dir_all(&android_assets_dir).map_err(|e| {
600 BenchError::Build(format!(
601 "Failed to create Android assets directory at {}: {}",
602 android_assets_dir.display(),
603 e
604 ))
605 })?;
606 let android_meta_path = android_assets_dir.join("bench_meta.json");
607 std::fs::write(&android_meta_path, &meta_json).map_err(|e| {
608 BenchError::Build(format!(
609 "Failed to write Android bench meta to {}: {}",
610 android_meta_path.display(),
611 e
612 ))
613 })?;
614 }
615
616 let ios_resources_dir = output_dir.join("ios/BenchRunner/BenchRunner/Resources");
618 if output_dir.join("ios/BenchRunner").exists() {
619 std::fs::create_dir_all(&ios_resources_dir).map_err(|e| {
620 BenchError::Build(format!(
621 "Failed to create iOS Resources directory at {}: {}",
622 ios_resources_dir.display(),
623 e
624 ))
625 })?;
626 let ios_meta_path = ios_resources_dir.join("bench_meta.json");
627 std::fs::write(&ios_meta_path, &meta_json).map_err(|e| {
628 BenchError::Build(format!(
629 "Failed to write iOS bench meta to {}: {}",
630 ios_meta_path.display(),
631 e
632 ))
633 })?;
634 }
635
636 Ok(())
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 #[test]
644 fn test_get_cargo_target_dir_fallback() {
645 let result = get_cargo_target_dir(Path::new("/nonexistent/path"));
647 assert!(result.is_ok() || result.is_err());
649 }
650
651 #[test]
652 fn test_host_lib_path_not_found() {
653 let result = host_lib_path(Path::new("/tmp"), "nonexistent-crate");
654 assert!(result.is_err());
655 let err = result.unwrap_err();
656 let msg = format!("{}", err);
657 assert!(msg.contains("Host library for UniFFI not found"));
658 assert!(msg.contains("cargo build"));
659 }
660
661 #[test]
662 fn test_run_command_not_found() {
663 let cmd = Command::new("nonexistent-command-12345");
664 let result = run_command(cmd, "test command");
665 assert!(result.is_err());
666 let err = result.unwrap_err();
667 let msg = format!("{}", err);
668 assert!(msg.contains("Failed to start"));
669 }
670
671 #[test]
672 fn test_read_package_name_standard() {
673 let temp_dir = std::env::temp_dir().join("mobench-test-read-package");
674 let _ = std::fs::remove_dir_all(&temp_dir);
675 std::fs::create_dir_all(&temp_dir).unwrap();
676
677 let cargo_toml = temp_dir.join("Cargo.toml");
678 std::fs::write(
679 &cargo_toml,
680 r#"[package]
681name = "my-awesome-crate"
682version = "0.1.0"
683edition = "2021"
684
685[dependencies]
686"#,
687 )
688 .unwrap();
689
690 let result = read_package_name(&cargo_toml);
691 assert_eq!(result, Some("my-awesome-crate".to_string()));
692
693 std::fs::remove_dir_all(&temp_dir).unwrap();
694 }
695
696 #[test]
697 fn test_read_package_name_with_single_quotes() {
698 let temp_dir = std::env::temp_dir().join("mobench-test-read-package-sq");
699 let _ = std::fs::remove_dir_all(&temp_dir);
700 std::fs::create_dir_all(&temp_dir).unwrap();
701
702 let cargo_toml = temp_dir.join("Cargo.toml");
703 std::fs::write(
704 &cargo_toml,
705 r#"[package]
706name = 'single-quoted-crate'
707version = "0.1.0"
708"#,
709 )
710 .unwrap();
711
712 let result = read_package_name(&cargo_toml);
713 assert_eq!(result, Some("single-quoted-crate".to_string()));
714
715 std::fs::remove_dir_all(&temp_dir).unwrap();
716 }
717
718 #[test]
719 fn test_read_package_name_not_found() {
720 let result = read_package_name(Path::new("/nonexistent/Cargo.toml"));
721 assert_eq!(result, None);
722 }
723
724 #[test]
725 fn test_read_package_name_no_package_section() {
726 let temp_dir = std::env::temp_dir().join("mobench-test-read-package-no-pkg");
727 let _ = std::fs::remove_dir_all(&temp_dir);
728 std::fs::create_dir_all(&temp_dir).unwrap();
729
730 let cargo_toml = temp_dir.join("Cargo.toml");
731 std::fs::write(
732 &cargo_toml,
733 r#"[workspace]
734members = ["crates/*"]
735"#,
736 )
737 .unwrap();
738
739 let result = read_package_name(&cargo_toml);
740 assert_eq!(result, None);
741
742 std::fs::remove_dir_all(&temp_dir).unwrap();
743 }
744
745 #[test]
746 fn test_create_bench_meta() {
747 let spec = EmbeddedBenchSpec {
748 function: "test_crate::my_benchmark".to_string(),
749 iterations: 100,
750 warmup: 10,
751 };
752
753 let meta = create_bench_meta(&spec, "android", "release");
754
755 assert_eq!(meta.spec.function, "test_crate::my_benchmark");
756 assert_eq!(meta.spec.iterations, 100);
757 assert_eq!(meta.spec.warmup, 10);
758 assert_eq!(meta.target, "android");
759 assert_eq!(meta.profile, "release");
760 assert!(!meta.mobench_version.is_empty());
761 assert!(!meta.host_os.is_empty());
762 assert!(!meta.build_time.is_empty());
763 assert!(meta.build_time_unix > 0);
764 assert!(meta.build_time.contains('T'));
766 assert!(meta.build_time.ends_with('Z'));
767 }
768
769 #[test]
770 fn test_days_to_ymd_epoch() {
771 let (year, month, day) = days_to_ymd(0);
773 assert_eq!(year, 1970);
774 assert_eq!(month, 1);
775 assert_eq!(day, 1);
776 }
777
778 #[test]
779 fn test_days_to_ymd_known_date() {
780 let (year, month, day) = days_to_ymd(365);
784 assert_eq!(year, 1971);
785 assert_eq!(month, 1);
786 assert_eq!(day, 1);
787 }
788
789 #[test]
790 fn test_is_leap_year() {
791 assert!(!is_leap_year(1970)); assert!(is_leap_year(2000)); assert!(!is_leap_year(1900)); assert!(is_leap_year(2024)); }
796
797 #[test]
798 fn test_bench_meta_serialization() {
799 let spec = EmbeddedBenchSpec {
800 function: "my_func".to_string(),
801 iterations: 50,
802 warmup: 5,
803 };
804
805 let meta = create_bench_meta(&spec, "ios", "debug");
806 let json = serde_json::to_string(&meta).expect("serialization should work");
807
808 assert!(json.contains("my_func"));
810 assert!(json.contains("ios"));
811 assert!(json.contains("debug"));
812 assert!(json.contains("build_time"));
813 assert!(json.contains("mobench_version"));
814 }
815}