1use std::path::Path;
2use std::time::Duration;
3
4use crate::diagnostic::{Diagnostic, DiagnosticParser};
5use crate::error::{ForgeError, Result};
6#[derive(Debug, Clone)]
7pub struct BuildOutput {
8 pub success: bool,
9 pub diagnostics: Vec<Diagnostic>,
10 pub duration: Duration,
11 pub stdout: String,
12 pub stderr: String,
13}
14
15impl BuildOutput {
16 pub fn ok(stdout: String, stderr: String, duration: Duration) -> Self {
17 Self {
18 success: true,
19 diagnostics: Vec::new(),
20 duration,
21 stdout,
22 stderr,
23 }
24 }
25
26 pub fn fail(
27 diagnostics: Vec<Diagnostic>,
28 stdout: String,
29 stderr: String,
30 duration: Duration,
31 ) -> Self {
32 Self {
33 success: false,
34 diagnostics,
35 duration,
36 stdout,
37 stderr,
38 }
39 }
40
41 pub fn errors(&self) -> Vec<&Diagnostic> {
42 self.diagnostics
43 .iter()
44 .filter(|d| matches!(d.severity, crate::diagnostic::DiagnosticSeverity::Error))
45 .collect()
46 }
47
48 pub fn warnings(&self) -> Vec<&Diagnostic> {
49 self.diagnostics
50 .iter()
51 .filter(|d| matches!(d.severity, crate::diagnostic::DiagnosticSeverity::Warning))
52 .collect()
53 }
54}
55
56pub struct BuildModule {
57 system: Box<dyn BuildSystem>,
58}
59
60impl BuildModule {
61 pub fn new(system: Box<dyn BuildSystem>) -> Self {
62 Self { system }
63 }
64
65 pub fn detect(project_root: &Path) -> Option<Self> {
66 detect_build_system(project_root).map(Self::new)
67 }
68
69 pub fn system_name(&self) -> &str {
70 self.system.name()
71 }
72
73 pub async fn check(&self, project_root: &Path) -> Result<BuildOutput> {
74 self.system.check(project_root).await
75 }
76
77 pub async fn build(&self, project_root: &Path) -> Result<BuildOutput> {
78 self.system.build(project_root).await
79 }
80
81 pub async fn test(&self, project_root: &Path) -> Result<BuildOutput> {
82 self.system.test(project_root).await
83 }
84
85 pub async fn clean(&self, project_root: &Path) -> Result<BuildOutput> {
86 self.system.clean(project_root).await
87 }
88}
89
90pub fn detect_build_system(project_root: &Path) -> Option<Box<dyn BuildSystem>> {
91 let systems: Vec<Box<dyn BuildSystem>> = vec![
92 Box::new(CargoBuildSystem),
93 Box::new(GoBuildSystem),
94 Box::new(NpmBuildSystem),
95 Box::new(MakeBuildSystem),
96 ];
97
98 systems.into_iter().find(|sys| sys.detect(project_root))
99}
100
101#[async_trait::async_trait]
102pub trait BuildSystem: Send + Sync {
103 fn name(&self) -> &str;
104 fn detect(&self, project_root: &Path) -> bool;
105 async fn check(&self, project_root: &Path) -> Result<BuildOutput>;
106 async fn build(&self, project_root: &Path) -> Result<BuildOutput>;
107 async fn test(&self, project_root: &Path) -> Result<BuildOutput>;
108 async fn clean(&self, project_root: &Path) -> Result<BuildOutput>;
109}
110
111async fn run_command(
112 program: &str,
113 args: &[&str],
114 working_dir: &Path,
115 parser: &dyn DiagnosticParser,
116) -> Result<BuildOutput> {
117 let start = std::time::Instant::now();
118 let output = tokio::process::Command::new(program)
119 .args(args)
120 .current_dir(working_dir)
121 .output()
122 .await
123 .map_err(|e| {
124 ForgeError::ToolError(format!(
125 "Failed to run {} {}: {}",
126 program,
127 args.join(" "),
128 e
129 ))
130 })?;
131
132 let duration = start.elapsed();
133 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
134 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
135 let diagnostics = parser.parse(&stdout, &stderr);
136
137 Ok(BuildOutput {
138 success: output.status.success(),
139 diagnostics,
140 duration,
141 stdout,
142 stderr,
143 })
144}
145
146pub struct CargoBuildSystem;
147
148#[async_trait::async_trait]
149impl BuildSystem for CargoBuildSystem {
150 fn name(&self) -> &str {
151 "cargo"
152 }
153
154 fn detect(&self, project_root: &Path) -> bool {
155 project_root.join("Cargo.toml").exists()
156 }
157
158 async fn check(&self, project_root: &Path) -> Result<BuildOutput> {
159 let parser = crate::diagnostic::CargoDiagnosticParser;
160 run_command(
161 "cargo",
162 &["check", "--message-format=json"],
163 project_root,
164 &parser,
165 )
166 .await
167 }
168
169 async fn build(&self, project_root: &Path) -> Result<BuildOutput> {
170 let parser = crate::diagnostic::CargoDiagnosticParser;
171 run_command("cargo", &["build"], project_root, &parser).await
172 }
173
174 async fn test(&self, project_root: &Path) -> Result<BuildOutput> {
175 let parser = crate::diagnostic::CargoDiagnosticParser;
176 run_command("cargo", &["test"], project_root, &parser).await
177 }
178
179 async fn clean(&self, project_root: &Path) -> Result<BuildOutput> {
180 let parser = crate::diagnostic::GenericDiagnosticParser {
181 tool_name: "cargo".to_string(),
182 };
183 run_command("cargo", &["clean"], project_root, &parser).await
184 }
185}
186
187pub struct GoBuildSystem;
188
189#[async_trait::async_trait]
190impl BuildSystem for GoBuildSystem {
191 fn name(&self) -> &str {
192 "go"
193 }
194
195 fn detect(&self, project_root: &Path) -> bool {
196 project_root.join("go.mod").exists()
197 }
198
199 async fn check(&self, project_root: &Path) -> Result<BuildOutput> {
200 let parser = crate::diagnostic::GoDiagnosticParser;
201 run_command("go", &["vet", "./..."], project_root, &parser).await
202 }
203
204 async fn build(&self, project_root: &Path) -> Result<BuildOutput> {
205 let parser = crate::diagnostic::GoDiagnosticParser;
206 run_command("go", &["build", "./..."], project_root, &parser).await
207 }
208
209 async fn test(&self, project_root: &Path) -> Result<BuildOutput> {
210 let parser = crate::diagnostic::GoDiagnosticParser;
211 run_command("go", &["test", "./..."], project_root, &parser).await
212 }
213
214 async fn clean(&self, project_root: &Path) -> Result<BuildOutput> {
215 let parser = crate::diagnostic::GenericDiagnosticParser {
216 tool_name: "go".to_string(),
217 };
218 run_command("go", &["clean", "-cache"], project_root, &parser).await
219 }
220}
221
222pub struct NpmBuildSystem;
223
224#[async_trait::async_trait]
225impl BuildSystem for NpmBuildSystem {
226 fn name(&self) -> &str {
227 "npm"
228 }
229
230 fn detect(&self, project_root: &Path) -> bool {
231 project_root.join("package.json").exists()
232 }
233
234 async fn check(&self, project_root: &Path) -> Result<BuildOutput> {
235 let parser = crate::diagnostic::GenericDiagnosticParser {
236 tool_name: "npm".to_string(),
237 };
238 run_command("npm", &["run", "check"], project_root, &parser).await
239 }
240
241 async fn build(&self, project_root: &Path) -> Result<BuildOutput> {
242 let parser = crate::diagnostic::GenericDiagnosticParser {
243 tool_name: "npm".to_string(),
244 };
245 run_command("npm", &["run", "build"], project_root, &parser).await
246 }
247
248 async fn test(&self, project_root: &Path) -> Result<BuildOutput> {
249 let parser = crate::diagnostic::GenericDiagnosticParser {
250 tool_name: "npm".to_string(),
251 };
252 run_command("npm", &["test"], project_root, &parser).await
253 }
254
255 async fn clean(&self, project_root: &Path) -> Result<BuildOutput> {
256 let parser = crate::diagnostic::GenericDiagnosticParser {
257 tool_name: "npm".to_string(),
258 };
259 run_command("npm", &["run", "clean"], project_root, &parser).await
260 }
261}
262
263pub struct MakeBuildSystem;
264
265#[async_trait::async_trait]
266impl BuildSystem for MakeBuildSystem {
267 fn name(&self) -> &str {
268 "make"
269 }
270
271 fn detect(&self, project_root: &Path) -> bool {
272 project_root.join("Makefile").exists() || project_root.join("makefile").exists()
273 }
274
275 async fn check(&self, project_root: &Path) -> Result<BuildOutput> {
276 let parser = crate::diagnostic::GenericDiagnosticParser {
277 tool_name: "make".to_string(),
278 };
279 run_command("make", &["check"], project_root, &parser).await
280 }
281
282 async fn build(&self, project_root: &Path) -> Result<BuildOutput> {
283 let parser = crate::diagnostic::GenericDiagnosticParser {
284 tool_name: "make".to_string(),
285 };
286 run_command("make", &[], project_root, &parser).await
287 }
288
289 async fn test(&self, project_root: &Path) -> Result<BuildOutput> {
290 let parser = crate::diagnostic::GenericDiagnosticParser {
291 tool_name: "make".to_string(),
292 };
293 run_command("make", &["test"], project_root, &parser).await
294 }
295
296 async fn clean(&self, project_root: &Path) -> Result<BuildOutput> {
297 let parser = crate::diagnostic::GenericDiagnosticParser {
298 tool_name: "make".to_string(),
299 };
300 run_command("make", &["clean"], project_root, &parser).await
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_detect_cargo() {
310 let temp = tempfile::tempdir().unwrap();
311 std::fs::write(
312 temp.path().join("Cargo.toml"),
313 "[package]\nname = \"test\"\n",
314 )
315 .unwrap();
316 let sys = detect_build_system(temp.path()).unwrap();
317 assert_eq!(sys.name(), "cargo");
318 }
319
320 #[test]
321 fn test_detect_go() {
322 let temp = tempfile::tempdir().unwrap();
323 std::fs::write(temp.path().join("go.mod"), "module test\n").unwrap();
324 let sys = detect_build_system(temp.path()).unwrap();
325 assert_eq!(sys.name(), "go");
326 }
327
328 #[test]
329 fn test_detect_npm() {
330 let temp = tempfile::tempdir().unwrap();
331 std::fs::write(temp.path().join("package.json"), "{}").unwrap();
332 let sys = detect_build_system(temp.path()).unwrap();
333 assert_eq!(sys.name(), "npm");
334 }
335
336 #[test]
337 fn test_detect_make() {
338 let temp = tempfile::tempdir().unwrap();
339 std::fs::write(temp.path().join("Makefile"), "all:\n\techo hello\n").unwrap();
340 let sys = detect_build_system(temp.path()).unwrap();
341 assert_eq!(sys.name(), "make");
342 }
343
344 #[test]
345 fn test_detect_makefile_lowercase() {
346 let temp = tempfile::tempdir().unwrap();
347 std::fs::write(temp.path().join("makefile"), "all:\n\techo hello\n").unwrap();
348 let sys = detect_build_system(temp.path()).unwrap();
349 assert_eq!(sys.name(), "make");
350 }
351
352 #[test]
353 fn test_detect_nothing() {
354 let temp = tempfile::tempdir().unwrap();
355 assert!(detect_build_system(temp.path()).is_none());
356 }
357
358 #[test]
359 fn test_detect_cargo_priority_over_make() {
360 let temp = tempfile::tempdir().unwrap();
361 std::fs::write(
362 temp.path().join("Cargo.toml"),
363 "[package]\nname = \"test\"\n",
364 )
365 .unwrap();
366 std::fs::write(temp.path().join("Makefile"), "all:\n\techo hello\n").unwrap();
367 let sys = detect_build_system(temp.path()).unwrap();
368 assert_eq!(sys.name(), "cargo");
369 }
370
371 #[test]
372 fn test_build_output_ok() {
373 let out = BuildOutput::ok(
374 "done".to_string(),
375 String::new(),
376 Duration::from_millis(100),
377 );
378 assert!(out.success);
379 assert!(out.diagnostics.is_empty());
380 }
381
382 #[test]
383 fn test_build_output_fail() {
384 let out = BuildOutput::fail(
385 vec![Diagnostic::error("broken")],
386 String::new(),
387 "error".to_string(),
388 Duration::from_millis(50),
389 );
390 assert!(!out.success);
391 assert_eq!(out.errors().len(), 1);
392 }
393
394 #[test]
395 fn test_build_output_errors_warnings() {
396 let diags = vec![
397 Diagnostic::error("e1"),
398 Diagnostic::warning("w1"),
399 Diagnostic::error("e2"),
400 Diagnostic::warning("w2"),
401 ];
402 let out = BuildOutput::fail(diags, String::new(), String::new(), Duration::ZERO);
403 assert_eq!(out.errors().len(), 2);
404 assert_eq!(out.warnings().len(), 2);
405 }
406
407 #[tokio::test]
408 async fn test_cargo_check_on_forge() {
409 let forge_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
410 let sys = CargoBuildSystem;
411 if !sys.detect(&forge_root) {
412 return;
413 }
414 let out = sys.check(&forge_root).await.unwrap();
415 assert!(out.success, "forge should pass cargo check: {}", out.stderr);
416 }
417
418 #[test]
419 fn test_build_module_detect() {
420 let temp = tempfile::tempdir().unwrap();
421 std::fs::write(
422 temp.path().join("Cargo.toml"),
423 "[package]\nname = \"test\"\n",
424 )
425 .unwrap();
426 let module = BuildModule::detect(temp.path()).unwrap();
427 assert_eq!(module.system_name(), "cargo");
428 }
429}