Skip to main content

cvkg_cli/
build_pipeline.rs

1//! Build Pipeline Hook
2//! Hooks into the build process for incremental rebuilds
3
4use std::path::Path;
5use std::time::{Duration, Instant};
6
7use super::patch_engine::CompiledArtifact;
8
9/// Build pipeline hook
10pub struct BuildPipeline;
11
12impl Default for BuildPipeline {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18impl BuildPipeline {
19    /// Create a new BuildPipeline
20    pub fn new() -> Self {
21        Self
22    }
23
24    /// Compile the project using cargo build with support for targets and features.
25    pub fn compile_project<P: AsRef<Path>>(
26        project_path: P,
27        target: Option<&str>,
28        release: bool,
29        features: &[String],
30    ) -> CompiledArtifact {
31        use console::style;
32        use indicatif::{ProgressBar, ProgressStyle};
33
34        // Clear terminal for a fresh build output
35        print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
36        println!("{} CVKG Forge: Rebuilding project...", style("🚀").cyan());
37
38        let pb = ProgressBar::new_spinner();
39        pb.set_style(
40            ProgressStyle::default_spinner()
41                .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")
42                .template("{spinner:.green} [{elapsed_precise}] {msg}")
43                .unwrap(),
44        );
45        pb.set_message("Compiling target...".to_string());
46        pb.enable_steady_tick(std::time::Duration::from_millis(100));
47
48        let start_time = Instant::now();
49        let mut cmd = std::process::Command::new("cargo");
50        cmd.arg("build");
51
52        if let Some(t) = target {
53            cmd.arg("--target").arg(t);
54        }
55
56        if release {
57            cmd.arg("--release");
58        }
59
60        if !features.is_empty() {
61            cmd.arg("--features").arg(features.join(","));
62        }
63
64        // We want to see colored output from cargo
65        cmd.env("CARGO_TERM_COLOR", "always");
66
67        let output = cmd.current_dir(project_path.as_ref()).output();
68
69        match output {
70            Ok(out) if out.status.success() => {
71                let duration = start_time.elapsed();
72                pb.finish_with_message(format!(
73                    "{} Build Success in {:.2?}",
74                    style("✅").green(),
75                    duration
76                ));
77                CompiledArtifact {
78                    root_id: 1,
79                    view: super::patch_engine::SerializedView {
80                        view_type: "App".to_string(),
81                        props: serde_json::json!({ "status": "built", "target": target, "release": release }),
82                        children: Vec::new(),
83                    },
84                }
85            }
86            Ok(out) => {
87                let stderr = String::from_utf8_lossy(&out.stderr);
88                pb.finish_with_message(format!("{} Build Failed", style("❌").red()));
89                println!("\n{}", style(stderr).red());
90                CompiledArtifact {
91                    root_id: 0,
92                    view: super::patch_engine::SerializedView {
93                        view_type: "Error".to_string(),
94                        props: serde_json::json!({ "message": String::from_utf8_lossy(&out.stderr).into_owned() }),
95                        children: Vec::new(),
96                    },
97                }
98            }
99            Err(e) => {
100                pb.finish_with_message(format!(
101                    "{} Failed to execute cargo: {}",
102                    style("💥").red(),
103                    e
104                ));
105                CompiledArtifact {
106                    root_id: 0,
107                    view: super::patch_engine::SerializedView {
108                        view_type: "FatalError".to_string(),
109                        props: serde_json::json!({ "message": e.to_string() }),
110                        children: Vec::new(),
111                    },
112                }
113            }
114        }
115    }
116
117    /// Watch for file changes and trigger incremental rebuilds with debouncing
118    pub fn watch_changes<P: AsRef<Path>, F>(project_path: P, callback: F)
119    where
120        F: FnMut(CompiledArtifact) + Send + 'static,
121    {
122        use notify::{Config, RecursiveMode, Watcher};
123        use std::sync::mpsc::RecvTimeoutError;
124        use std::sync::{Arc, Mutex};
125
126        let path = project_path.as_ref().to_path_buf();
127        let (tx, rx) = std::sync::mpsc::channel();
128        let callback = Arc::new(Mutex::new(callback));
129
130        let mut watcher = match notify::RecommendedWatcher::new(tx, Config::default()) {
131            Ok(w) => w,
132            Err(e) => {
133                log::error!("Failed to create watcher: {}", e);
134                return;
135            }
136        };
137
138        if let Err(e) = watcher.watch(&path, RecursiveMode::Recursive) {
139            log::error!("Failed to watch path {:?}: {}", path, e);
140            return;
141        }
142
143        std::thread::spawn(move || {
144            let _watcher = watcher;
145            println!(
146                "{} CVKG Hot-Reload Engine watching for changes...",
147                console::style("👀").cyan()
148            );
149
150            let debounce_duration = Duration::from_millis(300);
151            let mut pending_build = false;
152
153            loop {
154                let event_result = if pending_build {
155                    rx.recv_timeout(debounce_duration)
156                } else {
157                    rx.recv().map_err(|_| RecvTimeoutError::Disconnected)
158                };
159
160                match event_result {
161                    Ok(Ok(event)) => {
162                        // Filter events
163                        let is_relevant = event.paths.iter().any(|p| {
164                            // Ignore target directory and git
165                            if p.components()
166                                .any(|c| c.as_os_str() == "target" || c.as_os_str() == ".git")
167                            {
168                                return false;
169                            }
170
171                            let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("");
172                            matches!(ext, "rs" | "wgsl" | "toml" | "json")
173                        });
174
175                        if event.kind.is_modify() && is_relevant {
176                            pending_build = true;
177                        }
178                    }
179                    Ok(Err(e)) => log::error!("Watch error: {:?}", e),
180                    Err(RecvTimeoutError::Timeout) => {
181                        if pending_build {
182                            pending_build = false;
183                            let artifact = Self::compile_project(&path, None, false, &[]);
184                            let mut cb = callback.lock().unwrap();
185                            (cb)(artifact);
186                        }
187                    }
188                    Err(RecvTimeoutError::Disconnected) => {
189                        log::info!("Watcher disconnected, stopping hot-reload engine.");
190                        break;
191                    }
192                }
193            }
194        });
195    }
196}