1use std::process::Command;
2
3use wowlab_fs::{
4 directory::{self, EntryKind},
5 path::{Path, PathBuf},
6};
7
8use super::targets::ProfileTarget;
9
10pub(super) fn build_target(
11 target: &ProfileTarget,
12 crates_dir: &Path,
13 quiet: bool,
14) -> Option<PathBuf> {
15 let mut cmd = Command::new("cargo");
16
17 cmd.arg("build")
18 .arg("--profile")
19 .arg("bench-profile")
20 .arg("-p")
21 .arg(&target.package);
22
23 if !target.features.is_empty() {
24 cmd.arg("--features").arg(target.features.join(","));
25 }
26
27 cmd.current_dir(crates_dir);
28
29 if quiet {
30 cmd.stdout(std::process::Stdio::null())
31 .stderr(std::process::Stdio::null());
32 }
33
34 let status = cmd.status().ok()?;
35
36 if !status.success() {
37 return None;
38 }
39
40 let binary = crates_dir
41 .join("target")
42 .join("bench-profile")
43 .join(&target.bin_name);
44
45 directory::inspect(&binary)
46 .ok()
47 .flatten()
48 .is_some_and(|entry| entry.kind() == EntryKind::File)
49 .then_some(binary)
50}
51
52pub(super) fn build_bench(
53 target: &ProfileTarget,
54 crates_dir: &Path,
55 quiet: bool,
56) -> Option<PathBuf> {
57 let mut cmd = Command::new("cargo");
58
59 cmd.arg("bench")
60 .arg("--profile")
61 .arg("bench-profile")
62 .arg("-p")
63 .arg(&target.package)
64 .arg("--bench")
65 .arg(&target.bin_name)
66 .arg("--no-run");
67
68 cmd.current_dir(crates_dir);
69
70 if quiet {
71 cmd.stdout(std::process::Stdio::null())
72 .stderr(std::process::Stdio::null());
73 }
74
75 let status = cmd.status().ok()?;
76
77 if !status.success() {
78 return None;
79 }
80
81 let deps_dir = crates_dir.join("target").join("bench-profile").join("deps");
84
85 find_newest_binary(&deps_dir, &target.bin_name)
86}
87
88fn find_newest_binary(dir: &Path, prefix: &str) -> Option<PathBuf> {
89 let dashed = format!("{}-", prefix.replace('-', "_"));
90
91 let mut best: Option<(PathBuf, std::time::SystemTime)> = None;
92 let Ok(entries) = directory::entries(dir) else {
93 return None;
94 };
95
96 for entry in entries {
97 if entry.kind() != EntryKind::File {
98 continue;
99 }
100
101 let path = entry.path();
102 let name = path
103 .file_name()
104 .unwrap_or_default()
105 .to_string_lossy()
106 .to_string();
107
108 if name.contains('.') {
109 continue;
110 }
111
112 if !name.starts_with(&dashed) {
113 continue;
114 }
115
116 let Some(modified) = entry.modified() else {
117 continue;
118 };
119
120 if best.as_ref().is_none_or(|(_, t)| modified > *t) {
121 best = Some((path.to_path_buf(), modified));
122 }
123 }
124
125 best.map(|(p, _)| p)
126}
127
128#[cfg(test)]
129mod tests {
130 use googletest::prelude::*;
131 use wowlab_fs::{file, temporary::Directory};
132
133 use super::find_newest_binary;
134
135 #[gtest]
136 fn benchmark_discovery_ignores_metadata_and_other_targets() -> Result<()> {
137 let directory = Directory::new().or_fail()?;
138 let artifact = directory.path().join("engine_bench-1234");
139
140 file::write_text(&artifact, "").or_fail()?;
141 file::write_text(&directory.path().join("engine_bench-1234.d"), "").or_fail()?;
142 file::write_text(&directory.path().join("other_bench-1234"), "").or_fail()?;
143
144 verify_that!(
145 find_newest_binary(directory.path(), "engine-bench").as_deref(),
146 some(eq(&*artifact))
147 )
148 }
149}