cvkg_cli/
build_pipeline.rs1use std::path::Path;
5use std::time::{Duration, Instant};
6
7use super::patch_engine::CompiledArtifact;
8
9pub struct BuildPipeline;
11
12impl Default for BuildPipeline {
13 fn default() -> Self {
14 Self::new()
15 }
16}
17
18impl BuildPipeline {
19 pub fn new() -> Self {
21 Self
22 }
23
24 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 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 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 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 let is_relevant = event.paths.iter().any(|p| {
164 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}