Skip to main content

ferrous_forge/safety/checks/
build.rs

1//! Build checking with cargo build
2
3use crate::Result;
4use std::path::Path;
5use std::time::Instant;
6
7use super::SafetyCheck;
8use crate::safety::{CheckType, report::CheckResult};
9
10/// Build check implementation
11pub struct BuildCheck;
12
13impl SafetyCheck for BuildCheck {
14    async fn run(project_path: &Path) -> Result<CheckResult> {
15        run(project_path).await
16    }
17
18    fn name() -> &'static str {
19        "build"
20    }
21
22    fn description() -> &'static str {
23        "Ensures project builds successfully in release mode"
24    }
25}
26
27/// Run cargo build --release
28///
29/// # Errors
30///
31/// Returns an error if the `cargo build --release` command fails to execute.
32pub async fn run(project_path: &Path) -> Result<CheckResult> {
33    let start = Instant::now();
34    let mut result = CheckResult::new(CheckType::Build);
35
36    // Run cargo build --release
37    // Disable the Ferrous Forge cargo wrapper to avoid recursive validation
38    let output = tokio::process::Command::new("cargo")
39        .current_dir(project_path)
40        .env("FERROUS_FORGE_ENABLED", "0")
41        .args(["build", "--release"])
42        .output()
43        .await?;
44
45    result.set_duration(start.elapsed());
46
47    if !output.status.success() {
48        handle_build_failure(&mut result, &output);
49    } else {
50        handle_build_success(&mut result, &output);
51    }
52
53    Ok(result)
54}
55
56/// Handle build failure output
57fn handle_build_failure(result: &mut CheckResult, output: &std::process::Output) {
58    result.add_error("Build failed");
59    result.add_suggestion("Fix compilation errors before proceeding");
60
61    let error_count = parse_build_errors(result, &output.stderr);
62
63    if error_count >= 3 {
64        result.add_error("... and more build errors (showing first 3)");
65    }
66
67    result.add_suggestion("Run 'cargo build' to see detailed error messages");
68    result.add_suggestion("Check for missing dependencies or syntax errors");
69}
70
71/// Parse build errors from stderr
72fn parse_build_errors(result: &mut CheckResult, stderr: &[u8]) -> usize {
73    let stderr = String::from_utf8_lossy(stderr);
74    let mut error_count = 0;
75
76    for line in stderr.lines() {
77        if line.starts_with("error") && error_count < 3 {
78            result.add_error(format!("Build: {}", line.trim()));
79            error_count += 1;
80        } else if line.trim().starts_with("-->") && error_count <= 3 {
81            result.add_context(format!("Location: {}", line.trim()));
82        }
83    }
84
85    error_count
86}
87
88/// Handle successful build output
89fn handle_build_success(result: &mut CheckResult, output: &std::process::Output) {
90    result.add_context("Project builds successfully in release mode");
91
92    // Check for warnings
93    let stderr = String::from_utf8_lossy(&output.stderr);
94    let warning_count = stderr
95        .lines()
96        .filter(|line| line.starts_with("warning:"))
97        .count();
98
99    if warning_count > 0 {
100        result.add_context(format!("Build completed with {} warnings", warning_count));
101    }
102}
103
104#[cfg(test)]
105#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
106mod tests {
107    use super::*;
108    use tempfile::TempDir;
109    use tokio::fs;
110
111    #[tokio::test]
112    async fn test_build_check_on_valid_project() {
113        let temp_dir = TempDir::new().unwrap();
114
115        // Create a basic Cargo.toml
116        let cargo_toml = r#"
117[package]
118name = "test"
119version = "0.1.0"
120edition = "2021"
121"#;
122        fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
123            .await
124            .unwrap();
125
126        // Create src directory
127        fs::create_dir_all(temp_dir.path().join("src"))
128            .await
129            .unwrap();
130
131        // Create a valid main.rs
132        let main_rs = r#"fn main() {
133    println!("Hello, world!");
134}
135"#;
136        fs::write(temp_dir.path().join("src/main.rs"), main_rs)
137            .await
138            .unwrap();
139
140        let result = run(temp_dir.path()).await.unwrap();
141
142        // Should pass for valid code
143        assert!(result.passed);
144    }
145
146    #[test]
147    fn test_build_check_struct() {
148        assert_eq!(BuildCheck::name(), "build");
149        assert!(!BuildCheck::description().is_empty());
150    }
151}