cargo_e/e_command_builder.rs
1use regex::Regex;
2use std::collections::{HashMap, HashSet};
3use std::env;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::mpsc::{channel, Sender};
9use std::time::SystemTime;
10use which::which;
11
12use crate::e_cargocommand_ext::CargoProcessResult;
13use crate::e_cargocommand_ext::{CargoCommandExt, CargoDiagnostic, CargoProcessHandle};
14use crate::e_eventdispatcher::{
15 CallbackResponse, CallbackType, CargoDiagnosticLevel, EventDispatcher,
16};
17use crate::e_runner::GLOBAL_CHILDREN;
18use crate::e_target::{CargoTarget, TargetKind, TargetOrigin};
19use std::sync::{Arc, Mutex};
20
21#[derive(Debug, Clone, PartialEq, Copy)]
22pub enum TerminalError {
23 NotConnected,
24 NoTerminal,
25 NoError,
26}
27
28impl Default for TerminalError {
29 fn default() -> Self {
30 TerminalError::NoError
31 }
32}
33
34/// A builder that constructs a Cargo command for a given target.
35#[derive(Clone, Debug)]
36pub struct CargoCommandBuilder {
37 pub target_name: String,
38 pub manifest_path: PathBuf,
39 pub args: Vec<String>,
40 pub subcommand: String,
41 pub pid: Option<u32>,
42 pub alternate_cmd: Option<String>,
43 pub execution_dir: Option<PathBuf>,
44 pub suppressed_flags: HashSet<String>,
45 pub stdout_dispatcher: Option<Arc<EventDispatcher>>,
46 pub stderr_dispatcher: Option<Arc<EventDispatcher>>,
47 pub progress_dispatcher: Option<Arc<EventDispatcher>>,
48 pub stage_dispatcher: Option<Arc<EventDispatcher>>,
49 pub terminal_error_flag: Arc<Mutex<bool>>,
50 pub sender: Option<Arc<Mutex<Sender<TerminalError>>>>,
51 pub diagnostics: Arc<Mutex<Vec<CargoDiagnostic>>>,
52 pub is_filter: bool,
53}
54
55impl std::fmt::Display for CargoCommandBuilder {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(
58 f,
59 "CargoCommandBuilder {{\n target_name: {:?},\n manifest_path: {:?},\n args: {:?},\n subcommand: {:?},\n pid: {:?},\n alternate_cmd: {:?},\n execution_dir: {:?},\n suppressed_flags: {:?},\n is_filter: {:?}\n}}",
60 self.target_name,
61 self.manifest_path,
62 self.args,
63 self.subcommand,
64 self.pid,
65 self.alternate_cmd,
66 self.execution_dir,
67 self.suppressed_flags,
68 self.is_filter
69 )
70 }
71}
72impl Default for CargoCommandBuilder {
73 fn default() -> Self {
74 Self::new(
75 &String::new(),
76 &PathBuf::from("Cargo.toml"),
77 "run".into(),
78 false,
79 )
80 }
81}
82impl CargoCommandBuilder {
83 /// Creates a new, empty builder.
84 pub fn new(target_name: &str, manifest: &PathBuf, subcommand: &str, is_filter: bool) -> Self {
85 let (sender, _receiver) = channel::<TerminalError>();
86 let sender = Arc::new(Mutex::new(sender));
87 let mut builder = CargoCommandBuilder {
88 target_name: target_name.to_owned(),
89 manifest_path: manifest.clone(),
90 args: Vec::new(),
91 subcommand: subcommand.to_string(),
92 pid: None,
93 alternate_cmd: None,
94 execution_dir: None,
95 suppressed_flags: HashSet::new(),
96 stdout_dispatcher: None,
97 stderr_dispatcher: None,
98 progress_dispatcher: None,
99 stage_dispatcher: None,
100 terminal_error_flag: Arc::new(Mutex::new(false)),
101 sender: Some(sender),
102 diagnostics: Arc::new(Mutex::new(Vec::<CargoDiagnostic>::new())),
103 is_filter,
104 };
105 builder.set_default_dispatchers();
106
107 builder
108 }
109
110 // Switch to passthrough mode when the terminal error is detected
111 fn switch_to_passthrough_mode<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
112 where
113 F: FnOnce(u32, CargoProcessHandle),
114 {
115 let mut command = self.build_command();
116
117 // Now, spawn the cargo process in passthrough mode
118 let cargo_process_handle = command.spawn_cargo_passthrough(Arc::clone(&self));
119 let pid = cargo_process_handle.pid;
120 // Notify observer
121 on_spawn(pid, cargo_process_handle);
122
123 Ok(pid)
124 }
125
126 // Set up the default dispatchers, which includes error detection
127 fn set_default_dispatchers(&mut self) {
128 if !self.is_filter {
129 // If this is a filter, we don't need to set up dispatchers
130 return;
131 }
132 let sender = self.sender.clone().unwrap();
133
134 let mut stdout_dispatcher = EventDispatcher::new();
135 stdout_dispatcher.add_callback(
136 r"listening on",
137 Box::new(|line, _captures, _state, stats| {
138 println!("(STDOUT) Dispatcher caught: {}", line);
139 // Use a regex to capture a URL from the line.
140 if let Ok(url_regex) = Regex::new(r"(http://[^\s]+)") {
141 if let Some(url_caps) = url_regex.captures(line) {
142 if let Some(url_match) = url_caps.get(1) {
143 let url = url_match.as_str();
144 // Call open::that on the captured URL.
145 if let Err(e) = open::that_detached(url) {
146 eprintln!("Failed to open URL: {}. Error: {}", url, e);
147 } else {
148 println!("Opened URL: {}", url);
149 }
150 }
151 }
152 } else {
153 eprintln!("Failed to create URL regex");
154 }
155 let mut stats = stats.lock().unwrap();
156 if stats.build_finished_time.is_none() {
157 let now = SystemTime::now();
158 stats.build_finished_time = Some(now);
159 }
160 None
161 }),
162 );
163
164 stdout_dispatcher.add_callback(
165 r"BuildFinished",
166 Box::new(|line, _captures, _state, stats| {
167 println!("******* {}", line);
168 let mut stats = stats.lock().unwrap();
169 if stats.build_finished_time.is_none() {
170 let now = SystemTime::now();
171 stats.build_finished_time = Some(now);
172 }
173 None
174 }),
175 );
176 stdout_dispatcher.add_callback(
177 r"server listening at:",
178 Box::new(|line, _captures, state, stats| {
179 // If we're not already in multiline mode, this is the initial match.
180 if !state.load(Ordering::Relaxed) {
181 println!("Matched 'server listening at:' in: {}", line);
182 state.store(true, Ordering::Relaxed);
183 Some(CallbackResponse {
184 callback_type: CallbackType::Note, // Choose as appropriate
185 message: Some(format!("Started multiline mode after: {}", line)),
186 file: None,
187 line: None,
188 column: None,
189 suggestion: None,
190 terminal_status: None,
191 })
192 } else {
193 // We are in multiline mode; process subsequent lines.
194 println!("Multiline callback received: {}", line);
195 // Use a regex to capture a URL from the line.
196 let url_regex = match Regex::new(r"(http://[^\s]+)") {
197 Ok(regex) => regex,
198 Err(e) => {
199 eprintln!("Failed to create URL regex: {}", e);
200 return None;
201 }
202 };
203 if let Some(url_caps) = url_regex.captures(line) {
204 let url = url_caps.get(1).unwrap().as_str();
205 // Call open::that on the captured URL.
206 match open::that_detached(url) {
207 Ok(_) => println!("Opened URL: {}", url),
208 Err(e) => eprintln!("Failed to open URL: {}. Error: {}", url, e),
209 }
210 let mut stats = stats.lock().unwrap();
211 if stats.build_finished_time.is_none() {
212 let now = SystemTime::now();
213 stats.build_finished_time = Some(now);
214 }
215 // End multiline mode.
216 state.store(false, Ordering::Relaxed);
217 Some(CallbackResponse {
218 callback_type: CallbackType::Note, // Choose as appropriate
219 message: Some(format!("Captured and opened URL: {}", url)),
220 file: None,
221 line: None,
222 column: None,
223 suggestion: None,
224 terminal_status: None,
225 })
226 } else {
227 None
228 }
229 }
230 }),
231 );
232
233 let mut stderr_dispatcher = EventDispatcher::new();
234
235 let suggestion_mode = Arc::new(AtomicBool::new(false));
236 let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
237 let warning_location: Arc<Mutex<Option<CallbackResponse>>> = Arc::new(Mutex::new(None));
238 let pending_diag: Arc<Mutex<Option<CargoDiagnostic>>> = Arc::new(Mutex::new(None));
239 let diagnostic_counts: Arc<Mutex<HashMap<CargoDiagnosticLevel, usize>>> =
240 Arc::new(Mutex::new(HashMap::new()));
241
242 let pending_d = Arc::clone(&pending_diag);
243 let counts = Arc::clone(&diagnostic_counts);
244
245 let diagnostics_arc = Arc::clone(&self.diagnostics);
246
247 // Callback for Rust panic messages (e.g., "thread 'main' panicked at ...")
248 stderr_dispatcher.add_callback(
249 r"^thread '([^']+)' panicked at (.+):([^\s:]+):(\d+):(\d+)",
250 Box::new(|line, captures, _state, _stats| {
251 if let Some(caps) = captures {
252 let thread = caps.get(1).map(|m| m.as_str()).unwrap_or("unknown");
253 let message = caps.get(2).map(|m| m.as_str()).unwrap_or("unknown panic");
254 let file = caps.get(3).map(|m| m.as_str()).unwrap_or("unknown file");
255 let line_num = caps
256 .get(4)
257 .map(|m| m.as_str())
258 .unwrap_or("0")
259 .parse()
260 .unwrap_or(0);
261 let col_num = caps
262 .get(5)
263 .map(|m| m.as_str())
264 .unwrap_or("0")
265 .parse()
266 .unwrap_or(0);
267 println!("\n\n\n");
268 println!("{}", line);
269 println!(
270 "Panic detected: thread='{}', message='{}', file='{}:{}:{}'",
271 thread, message, file, line_num, col_num
272 );
273 println!("\n\n\n");
274 Some(CallbackResponse {
275 callback_type: CallbackType::Error,
276 message: Some(format!(
277 "thread '{}' panicked at {} ({}:{}:{})",
278 thread, message, file, line_num, col_num
279 )),
280 file: Some(file.to_string()),
281 line: Some(line_num),
282 column: Some(col_num),
283 suggestion: None,
284 terminal_status: None,
285 })
286 } else {
287 None
288 }
289 }),
290 );
291
292 // Add a callback to detect "could not compile" errors
293 stderr_dispatcher.add_callback(
294 r"error: could not compile `(?P<crate_name>.+)` \((?P<due_to>.+)\) due to (?P<error_count>\d+) previous errors; (?P<warning_count>\d+) warnings emitted",
295 Box::new(|line, captures, _state, stats| {
296 println!("{}", line);
297 if let Some(caps) = captures {
298 // Extract dynamic fields from the error message
299 let crate_name = caps.name("crate_name").map(|m| m.as_str()).unwrap_or("unknown");
300 let due_to = caps.name("due_to").map(|m| m.as_str()).unwrap_or("unknown");
301 let error_count: usize = caps
302 .name("error_count")
303 .map(|m| m.as_str().parse().unwrap_or(0))
304 .unwrap_or(0);
305 let warning_count: usize = caps
306 .name("warning_count")
307 .map(|m| m.as_str().parse().unwrap_or(0))
308 .unwrap_or(0);
309
310 // Log the captured information (optional)
311 println!(
312 "Detected compilation failure: crate=`{}`, due_to=`{}`, errors={}, warnings={}",
313 crate_name, due_to, error_count, warning_count
314 );
315
316 // Set `is_could_not_compile` to true in the stats
317 let mut stats = stats.lock().unwrap();
318 stats.is_could_not_compile = true;
319 }
320 None // No callback response needed
321 }),
322 );
323
324 // Clone diagnostics_arc for this closure to avoid move
325 let diagnostics_arc_for_diag = Arc::clone(&diagnostics_arc);
326 stderr_dispatcher.add_callback(
327 r"^(?P<level>\w+)(\[(?P<error_code>E\d+)\])?:\s+(?P<msg>.+)$", // Regex for diagnostic line
328 Box::new(move |_line, caps, _multiline_flag, _stats| {
329 if let Some(caps) = caps {
330 let mut counts = counts.lock().unwrap();
331 // Create a PendingDiag and save the message
332 let mut pending_diag = pending_d.lock().unwrap();
333 let mut last_lineref = String::new();
334 if let Some(existing_diag) = pending_diag.take() {
335 let mut diags = diagnostics_arc_for_diag.lock().unwrap();
336 last_lineref = existing_diag.lineref.clone();
337 diags.push(existing_diag.clone());
338 }
339 log::trace!("Diagnostic line: {}", _line);
340 let level = caps["level"].to_string(); // e.g., "warning", "error"
341 let message = caps["msg"].to_string();
342 // If the message contains "generated" followed by one or more digits,
343 // then ignore this diagnostic by returning None.
344 let re_generated = regex::Regex::new(r"generated\s+\d+").unwrap();
345 if re_generated.is_match(&message) {
346 log::trace!("Skipping generated diagnostic: {}", _line);
347 return None;
348 }
349
350 let error_code = caps.name("error_code").map(|m| m.as_str().to_string());
351 let diag_level = match level.as_str() {
352 "error" => CargoDiagnosticLevel::Error,
353 "warning" => CargoDiagnosticLevel::Warning,
354 "help" => CargoDiagnosticLevel::Help,
355 "note" => CargoDiagnosticLevel::Note,
356 _ => {
357 println!("Unknown diagnostic level: {}", level);
358 return None; // Ignore unknown levels
359 }
360 };
361 // Increment the count for this level
362 *counts.entry(diag_level).or_insert(0) += 1;
363
364 let current_count = counts.get(&diag_level).unwrap_or(&0);
365 let diag = CargoDiagnostic {
366 error_code: error_code.clone(),
367 lineref: last_lineref.clone(),
368 level: level.clone(),
369 message,
370 suggestion: None,
371 help: None,
372 note: None,
373 uses_color: true,
374 diag_num_padding: Some(2),
375 diag_number: Some(*current_count),
376 };
377
378 // Save the new diagnostic
379 *pending_diag = Some(diag);
380
381 // Track the count of diagnostics for each level
382 return Some(CallbackResponse {
383 callback_type: CallbackType::LevelMessage, // Treat subsequent lines as warnings
384 message: None,
385 file: None,
386 line: None,
387 column: None,
388 suggestion: None, // This is the suggestion part
389 terminal_status: None,
390 });
391 } else {
392 println!("No captures found in line: {}", _line);
393 None
394 }
395 }),
396 );
397 // Look-behind buffer for last 6 lines before backtrace
398 let look_behind = Arc::new(Mutex::new(Vec::<String>::new()));
399 {
400 let look_behind = Arc::clone(&look_behind);
401 // This callback runs for every stderr line to update the look-behind buffer
402 stderr_dispatcher.add_callback(
403 r"^(?P<msg>.*)$",
404 Box::new(move |line, _captures, _state, _stats| {
405 let mut buf = look_behind.lock().unwrap();
406 if line.trim().is_empty() {
407 return None;
408 }
409 buf.push(line.to_string());
410 if buf.len() > 6 {
411 buf.remove(0);
412 }
413 None
414 }),
415 );
416 }
417
418 // --- Patch: Use look_behind before backtrace_lines in the note ---
419 {
420 let pending_diag = Arc::clone(&pending_diag);
421 let diagnostics_arc = Arc::clone(&diagnostics_arc);
422 let backtrace_mode = Arc::new(AtomicBool::new(false));
423 let backtrace_lines = Arc::new(Mutex::new(Vec::<String>::new()));
424 let look_behind = Arc::clone(&look_behind);
425 let stored_lines_behind = Arc::new(Mutex::new(Vec::<String>::new()));
426
427 // Enable backtrace mode when we see "stack backtrace:"
428 {
429 let backtrace_mode = Arc::clone(&backtrace_mode);
430 let backtrace_lines = Arc::clone(&backtrace_lines);
431 let stored_lines_behind = Arc::clone(&stored_lines_behind);
432 let look_behind = Arc::clone(&look_behind);
433 stderr_dispatcher.add_callback(
434 r"stack backtrace:",
435 Box::new(move |_line, _captures, _state, _stats| {
436 backtrace_mode.store(true, Ordering::Relaxed);
437 backtrace_lines.lock().unwrap().clear();
438 // Save the current look_behind buffer into a shared stored_lines_behind for later use
439 {
440 let look_behind_buf = look_behind.lock().unwrap();
441 let mut stored = stored_lines_behind.lock().unwrap();
442 *stored = look_behind_buf.clone();
443 }
444 None
445 }),
446 );
447 }
448
449 // Process backtrace lines, filter and summarize
450 {
451 let backtrace_mode = Arc::clone(&backtrace_mode);
452 let backtrace_lines = Arc::clone(&backtrace_lines);
453 let pending_diag = Arc::clone(&pending_diag);
454 let diagnostics_arc = Arc::clone(&diagnostics_arc);
455 let look_behind = Arc::clone(&look_behind);
456
457 // Regex for numbered backtrace line: " 0: type::path"
458 let re_number_type = Regex::new(r"^\s*(\d+):\s+(.*)$").unwrap();
459 // Regex for "at path:line"
460 let re_at_path = Regex::new(r"^\s*at\s+([^\s:]+):(\d+)").unwrap();
461
462 stderr_dispatcher.add_callback(
463 r"^(?P<msg>.*)$",
464 Box::new(move |mut line, _captures, _state, _stats| {
465 if backtrace_mode.load(Ordering::Relaxed) {
466 line = line.trim();
467 // End of backtrace if empty line or new diagnostic/note
468 if line.trim().is_empty()
469 || line.starts_with("note:")
470 || line.starts_with("error:")
471 {
472 let mut bt_lines = Vec::new();
473 let mut skip_next = false;
474 let mut last_number_type: Option<(String, String)> = None;
475 for l in backtrace_lines.lock().unwrap().iter() {
476 if let Some(caps) = re_number_type.captures(l) {
477 // Save the (number, type) line, but don't push yet
478 last_number_type =
479 Some((caps[1].to_string(), caps[2].to_string()));
480 skip_next = true;
481 } else if skip_next && re_at_path.is_match(l) {
482 let path_caps = re_at_path.captures(l).unwrap();
483 let path = path_caps.get(1).unwrap().as_str();
484 let line_num = path_caps.get(2).unwrap().as_str();
485 if path.starts_with("/rustc")
486 || path.contains(".cargo")
487 || path.contains(".rustup")
488 {
489 // Skip both the number: type and the at line
490 // (do not push either)
491 } else {
492 // Push both the number: type and the at line, on the same line
493 if let Some((num, typ)) = last_number_type.take() {
494 // Canonicalize the path if possible for better readability
495 let path = match std::fs::canonicalize(path) {
496 Ok(canon) => canon.display().to_string(),
497 Err(_) => path.to_string(),
498 };
499
500 bt_lines.push(format!(
501 "{}: {} @ {}:{}",
502 num, typ, path, line_num
503 ));
504 }
505 }
506 skip_next = false;
507 } else if let Some((num, typ)) = last_number_type.take() {
508 // If the previous number: type was not followed by an at line, push it
509 bt_lines.push(format!("{}: {}", num, typ));
510 if !l.trim().is_empty() {
511 bt_lines.push(l.clone());
512 }
513 skip_next = false;
514 } else if !l.trim().is_empty() {
515 bt_lines.push(l.clone());
516 skip_next = false;
517 }
518 }
519 if !bt_lines.is_empty() {
520 let mut pending_diag = pending_diag.lock().unwrap();
521 if let Some(ref mut diag) = *pending_diag {
522 // --- Insert stored_lines_behind lines before backtrace_lines ---
523 let stored_lines = {
524 let buf = stored_lines_behind.lock().unwrap();
525 buf.clone()
526 };
527 let note = diag.note.get_or_insert_with(String::new);
528 if !stored_lines.is_empty() {
529 note.push_str(&stored_lines.join("\n"));
530 note.push('\n');
531 }
532 note.push_str(&bt_lines.join("\n"));
533 let mut diags = diagnostics_arc.lock().unwrap();
534 diags.push(diag.clone());
535 }
536 }
537 backtrace_mode.store(false, Ordering::Relaxed);
538 backtrace_lines.lock().unwrap().clear();
539 return None;
540 }
541
542 // Only keep lines that are part of the backtrace
543 if re_number_type.is_match(line) || re_at_path.is_match(line) {
544 backtrace_lines.lock().unwrap().push(line.to_string());
545 }
546 // Ignore further lines
547 return None;
548 }
549 None
550 }),
551 );
552 }
553 }
554
555 // suggestion callback
556 {
557 let location_lock_clone = Arc::clone(&warning_location);
558 let suggestion_m = Arc::clone(&suggestion_mode);
559
560 // Suggestion callback that adds subsequent lines as suggestions
561 stderr_dispatcher.add_callback(
562 r"^(?P<msg>.*)$", // Capture all lines following the location
563 Box::new(move |line, _captures, _multiline_flag, _stats| {
564 if suggestion_m.load(Ordering::Relaxed) {
565 // Only process lines that match the suggestion format
566 if let Some(caps) = suggestion_regex.captures(line.trim()) {
567 // Capture the line number and code from the suggestion line
568 // let line_num = caps[1].parse::<usize>().unwrap_or(0);
569 let code = caps[2].to_string();
570
571 // Lock the pending_diag to add the suggestion
572 if let Ok(mut lock) = location_lock_clone.lock() {
573 if let Some(mut loc) = lock.take() {
574 // let file = loc.file.clone().unwrap_or_default();
575 // let col = loc.column.unwrap_or(0);
576
577 // Concatenate the suggestion line to the message
578 let mut msg = loc.message.unwrap_or_default();
579 msg.push_str(&format!("\n{}", code));
580
581 // Print the concatenated suggestion for debugging
582 // println!("daveSuggestion for {}:{}:{} - {}", file, line_num, col, msg);
583
584 // Update the location with the new concatenated message
585 loc.message = Some(msg.clone());
586 // println!("Updating location lock with new suggestion: {}", msg);
587 // Save the updated location back to shared state
588 // if let Ok(mut lock) = location_lock_clone.lock() {
589 // println!("Updating location lock with new suggestion: {}", msg);
590 lock.replace(loc);
591 // } else {
592 // eprintln!("Failed to acquire lock for location_lock_clone");
593 // }
594 }
595 // return Some(CallbackResponse {
596 // callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
597 // message: Some(msg.clone()),
598 // file: Some(file),
599 // line: Some(line_num),
600 // column: Some(col),
601 // suggestion: Some(msg), // This is the suggestion part
602 // terminal_status: None,
603 // });
604 }
605 }
606 } else {
607 // println!("Suggestion mode is not active. Ignoring line: {}", line);
608 }
609
610 None
611 }),
612 );
613 }
614 {
615 let suggestion_m = Arc::clone(&suggestion_mode);
616 let pending_diag_clone = Arc::clone(&pending_diag);
617 let diagnostics_arc = Arc::clone(&self.diagnostics);
618 // Callback for handling when an empty line or new diagnostic is received
619 stderr_dispatcher.add_callback(
620 r"^\s*$", // Regex to capture empty line
621 Box::new(move |_line, _captures, _multiline_flag, _stats| {
622 // println!("Empty line detected: {}", line);
623 suggestion_m.store(false, Ordering::Relaxed);
624 // End of current diagnostic: take and process it.
625 if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
626 //println!("{:?}", pending_diag);
627 // Use diagnostics_arc instead of self.diagnostices
628 let mut diags = diagnostics_arc.lock().unwrap();
629 diags.push(pending_diag.clone());
630 } else {
631 // println!("No pending diagnostic to process.");
632 }
633 // Handle empty line scenario to end the current diagnostic processing
634 // if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
635 // println!("{:?}", pending_diag);
636 // let mut diags = self.diagnostics.lock().unwrap();
637 // diags.push(pending_diag.clone());
638 // // let diag = crate::e_eventdispatcher::convert_message_to_diagnostic(msg, &msg_str);
639 // // diags.push(diag.clone());
640 // // if let Some(ref sd) = stage_disp_clone {
641 // // sd.dispatch(&format!("Stage: Diagnostic occurred at {:?}", now));
642 // // }
643 // // Handle the saved PendingDiag and its CallbackResponse
644 // // if let Some(callback_response) = pending_diag.callback_response {
645 // // println!("End of Diagnostic: {:?}", callback_response);
646 // // }
647 // } else {
648 // println!("No pending diagnostic to process.");
649 // }
650
651 None
652 }),
653 );
654 }
655
656 // {
657 // let pending_diag = Arc::clone(&pending_diag);
658 // let location_lock = Arc::clone(&warning_location);
659 // let suggestion_m = Arc::clone(&suggestion_mode);
660
661 // let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
662
663 // stderr_dispatcher.add_callback(
664 // r"^\s*(\d+)\s*\|\s*(.*)$", // Match suggestion line format
665 // Box::new(move |line, _captures, _multiline_flag| {
666 // if suggestion_m.load(Ordering::Relaxed) {
667 // // Only process lines that match the suggestion format
668 // if let Some(caps) = suggestion_regex.captures(line.trim()) {
669 // // Capture the line number and code from the suggestion line
670 // let line_num = caps[1].parse::<usize>().unwrap_or(0);
671 // let code = caps[2].to_string();
672
673 // // Lock the pending_diag to add the suggestion
674 // if let Some(mut loc) = location_lock.lock().unwrap().take() {
675 // println!("Suggestion line: {}", line);
676 // let file = loc.file.clone().unwrap_or_default();
677 // let col = loc.column.unwrap_or(0);
678
679 // // Concatenate the suggestion line to the message
680 // let mut msg = loc.message.unwrap_or_default();
681 // msg.push_str(&format!("\n{} | {}", line_num, code)); // Append the suggestion properly
682
683 // // Print the concatenated suggestion for debugging
684 // println!("Suggestion for {}:{}:{} - {}", file, line_num, col, msg);
685
686 // // Update the location with the new concatenated message
687 // loc.message = Some(msg.clone());
688
689 // // Save the updated location back to shared state
690 // location_lock.lock().unwrap().replace(loc);
691
692 // // return Some(CallbackResponse {
693 // // callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
694 // // message: Some(msg.clone()),
695 // // file: Some(file),
696 // // line: Some(line_num),
697 // // column: Some(col),
698 // // suggestion: Some(msg), // This is the suggestion part
699 // // terminal_status: None,
700 // // });
701 // } else {
702 // println!("No location information available for suggestion line: {}", line);
703 // }
704 // } else {
705 // println!("Suggestion line does not match expected format: {}", line);
706 // }
707 // } else {
708 // println!("Suggestion mode is not active. Ignoring line: {}", line);
709 // }
710
711 // None
712 // }),
713 // );
714
715 // }
716
717 {
718 let location_lock = Arc::clone(&warning_location);
719 let pending_diag = Arc::clone(&pending_diag);
720 let suggestion_mode = Arc::clone(&suggestion_mode);
721 stderr_dispatcher.add_callback(
722 r"^(?P<msg>.*)$", // Capture all lines following the location
723 Box::new(move |line, _captures, _multiline_flag, _stats| {
724 // Lock the location to fetch the original diagnostic info
725 if let Ok(location_guard) = location_lock.lock() {
726 if let Some(loc) = location_guard.as_ref() {
727 let file = loc.file.clone().unwrap_or_default();
728 let line_num = loc.line.unwrap_or(0);
729 let col = loc.column.unwrap_or(0);
730 // println!("SUGGESTION: Suggestion for {}:{}:{} {}", file, line_num, col, line);
731
732 // Only treat lines starting with | or numbers as suggestion lines
733 if line.trim().starts_with('|')
734 || line.trim().starts_with(char::is_numeric)
735 {
736 // Get the existing suggestion and append the new line
737 let suggestion = line.trim();
738
739 // Print the suggestion for debugging
740 // println!("Suggestion for {}:{}:{} - {}", file, line_num, col, suggestion);
741
742 // Lock the pending_diag and update its callback_response field
743 let mut pending_diag = match pending_diag.lock() {
744 Ok(lock) => lock,
745 Err(e) => {
746 eprintln!("Failed to acquire lock: {}", e);
747 return None; // Handle the error appropriately
748 }
749 };
750 if let Some(diag) = pending_diag.take() {
751 // If a PendingDiag already exists, update the existing callback response with the new suggestion
752 let mut diag = diag;
753
754 // Append the new suggestion to the existing one
755 if let Some(ref mut existing) = diag.suggestion {
756 diag.suggestion =
757 Some(format!("{}\n{}", existing, suggestion));
758 } else {
759 diag.suggestion = Some(suggestion.to_string());
760 }
761
762 // Update the shared state with the new PendingDiag
763 *pending_diag = Some(diag.clone());
764 return Some(CallbackResponse {
765 callback_type: CallbackType::Suggestion, // Treat subsequent lines as warnings
766 message: Some(
767 diag.clone().suggestion.clone().unwrap().clone(),
768 ),
769 file: Some(file),
770 line: Some(line_num),
771 column: Some(col),
772 suggestion: diag.clone().suggestion.clone(), // This is the suggestion part
773 terminal_status: None,
774 });
775 } else {
776 // println!("No pending diagnostic to process for suggestion line: {}", line);
777 }
778 } else {
779 // If the line doesn't match the suggestion format, just return it as is
780 if line.trim().is_empty() {
781 // Ignore empty lines
782 suggestion_mode.store(false, Ordering::Relaxed);
783 return None;
784 }
785 }
786 } else {
787 // println!("No location information available for suggestion line: {}", line);
788 }
789 }
790 None
791 }),
792 );
793 }
794
795 // 2) Location callback stores its response into that shared state
796 {
797 let pending_diag = Arc::clone(&pending_diag);
798 let warning_location = Arc::clone(&warning_location);
799 let location_lock = Arc::clone(&warning_location);
800 let suggestion_mode = Arc::clone(&suggestion_mode);
801 let manifest_path = self.manifest_path.clone();
802 stderr_dispatcher.add_callback(
803 // r"^\s*-->\s+(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+)$",
804 r"^\s*-->\s+(?P<file>.+?)(?::(?P<line>\d+))?(?::(?P<col>\d+))?\s*$",
805 Box::new(move |_line, caps, _multiline_flag, _stats| {
806 log::trace!("Location line: {}", _line);
807 // if multiline_flag.load(Ordering::Relaxed) {
808 if let Some(caps) = caps {
809 let file = caps["file"].to_string();
810 let resolved_path = resolve_file_path(&manifest_path, &file);
811 let file = resolved_path.to_str().unwrap_or_default().to_string();
812 let line = caps["line"].parse::<usize>().unwrap_or(0);
813 let column = caps["col"].parse::<usize>().unwrap_or(0);
814 let resp = CallbackResponse {
815 callback_type: CallbackType::Location,
816 message: format!("{}:{}:{}", file, line, column).into(),
817 file: Some(file.clone()),
818 line: Some(line),
819 column: Some(column),
820 suggestion: None,
821 terminal_status: None,
822 };
823 // Lock the pending_diag and update its callback_response field
824 let mut pending_diag = pending_diag.lock().unwrap();
825 if let Some(diag) = pending_diag.take() {
826 // If a PendingDiag already exists, save the new callback response in the existing PendingDiag
827 let mut diag = diag;
828 diag.lineref = format!("{}:{}:{}", file, line, column); // Update the lineref
829 // diag.save_callback_response(resp.clone()); // Save the callback response
830 // Update the shared state with the new PendingDiag
831 *pending_diag = Some(diag);
832 }
833 // Save it for the generic callback to see
834 *warning_location.lock().unwrap() = Some(resp.clone());
835 *location_lock.lock().unwrap() = Some(resp.clone());
836 // Set suggestion mode to true as we've encountered a location line
837 suggestion_mode.store(true, Ordering::Relaxed);
838 return Some(resp.clone());
839 } else {
840 println!("No captures found in line: {}", _line);
841 }
842 // }
843 None
844 }),
845 );
846 }
847
848 // // 3) Note callback — attach note to pending_diag
849 {
850 let pending_diag = Arc::clone(&pending_diag);
851 stderr_dispatcher.add_callback(
852 r"^\s*=\s*note:\s*(?P<msg>.+)$",
853 Box::new(move |_line, caps, _state, _stats| {
854 if let Some(caps) = caps {
855 let mut pending_diag = pending_diag.lock().unwrap();
856 if let Some(ref mut resp) = *pending_diag {
857 // Prepare the new note with the blue prefix
858 let new_note = format!("note: {}", caps["msg"].to_string());
859
860 // Append or set the note
861 if let Some(existing_note) = &resp.note {
862 // If there's already a note, append with newline and the new note
863 resp.note = Some(format!("{}\n{}", existing_note, new_note));
864 } else {
865 // If no existing note, just set the new note
866 resp.note = Some(new_note);
867 }
868 }
869 }
870 None
871 }),
872 );
873 }
874
875 // 4) Help callback — attach help to pending_diag
876 {
877 let pending_diag = Arc::clone(&pending_diag);
878 stderr_dispatcher.add_callback(
879 r"^\s*(?:\=|\|)\s*help:\s*(?P<msg>.+)$", // Regex to match both '=' and '|' before help:
880 Box::new(move |_line, caps, _state, _stats| {
881 if let Some(caps) = caps {
882 let mut pending_diag = pending_diag.lock().unwrap();
883 if let Some(ref mut resp) = *pending_diag {
884 // Create the new help message with the orange "h:" prefix
885 let new_help =
886 format!("\x1b[38;5;214mhelp: {}\x1b[0m", caps["msg"].to_string());
887
888 // Append or set the help message
889 if let Some(existing_help) = &resp.help {
890 // If there's already a help message, append with newline
891 resp.help = Some(format!("{}\n{}", existing_help, new_help));
892 } else {
893 // If no existing help message, just set the new one
894 resp.help = Some(new_help);
895 }
896 }
897 }
898 None
899 }),
900 );
901 }
902
903 stderr_dispatcher.add_callback(
904 r"(?:\x1b\[[0-9;]*[A-Za-z])*\s*Serving(?:\x1b\[[0-9;]*[A-Za-z])*\s+at\s+(http://[^\s]+)",
905 Box::new(|line, captures, _state, stats | {
906 if let Some(caps) = captures {
907 let url = caps.get(1).unwrap().as_str();
908 let url = url.replace("0.0.0.0", "127.0.0.1");
909 println!("(STDERR) Captured URL: {}", url);
910 match open::that_detached(&url) {
911 Ok(_) => println!("(STDERR) Opened URL: {}",&url),
912 Err(e) => eprintln!("(STDERR) Failed to open URL: {}. Error: {:?}", url, e),
913 }
914 let mut stats = stats.lock().unwrap();
915 if stats.build_finished_time.is_none() {
916 let now = SystemTime::now();
917 stats.build_finished_time = Some(now);
918 }
919 Some(CallbackResponse {
920 callback_type: CallbackType::OpenedUrl, // Choose as appropriate
921 message: Some(format!("Captured and opened URL: {}", url)),
922 file: None,
923 line: None,
924 column: None,
925 suggestion: None,
926 terminal_status: None,
927 })
928 } else {
929 println!("(STDERR) No URL captured in line: {}", line);
930 None
931 }
932 }),
933);
934
935 let finished_flag = Arc::new(AtomicBool::new(false));
936
937 // 0) Finished‐profile summary callback
938 {
939 let finished_flag = Arc::clone(&finished_flag);
940 stderr_dispatcher.add_callback(
941 r"^Finished\s+`(?P<profile>[^`]+)`\s+profile\s+\[(?P<opts>[^\]]+)\]\s+target\(s\)\s+in\s+(?P<dur>[0-9.]+s)$",
942 Box::new(move |_line, caps, _multiline_flag, stats | {
943 if let Some(caps) = caps {
944 finished_flag.store(true, Ordering::Relaxed);
945 let profile = &caps["profile"];
946 let opts = &caps["opts"];
947 let dur = &caps["dur"];
948 let mut stats = stats.lock().unwrap();
949 if stats.build_finished_time.is_none() {
950 let now = SystemTime::now();
951 stats.build_finished_time = Some(now);
952 }
953 Some(CallbackResponse {
954 callback_type: CallbackType::Note,
955 message: Some(format!("Finished `{}` [{}] in {}", profile, opts, dur)),
956 file: None, line: None, column: None, suggestion: None, terminal_status: None,
957 })
958 } else {
959 None
960 }
961 }),
962 );
963 }
964
965 let summary_flag = Arc::new(AtomicBool::new(false));
966 {
967 let summary_flag = Arc::clone(&summary_flag);
968 stderr_dispatcher.add_callback(
969 r"^(?P<level>warning|error):\s+`(?P<name>[^`]+)`\s+\((?P<otype>lib|bin)\)\s+generated\s+(?P<count>\d+)\s+(?P<kind>warnings|errors).*run\s+`(?P<cmd>[^`]+)`\s+to apply\s+(?P<fixes>\d+)\s+suggestions",
970 Box::new(move |_line, caps, multiline_flag, _stats | {
971 let summary_flag = Arc::clone(&summary_flag);
972 if let Some(caps) = caps {
973 summary_flag.store(true, Ordering::Relaxed);
974 // Always start fresh
975 multiline_flag.store(false, Ordering::Relaxed);
976
977 let level = &caps["level"];
978 let name = &caps["name"];
979 let otype = &caps["otype"];
980 let count: usize = caps["count"].parse().unwrap_or(0);
981 let kind = &caps["kind"]; // "warnings" or "errors"
982 let cmd = caps["cmd"].to_string();
983 let fixes: usize = caps["fixes"].parse().unwrap_or(0);
984
985 println!("SUMMARIZATION CALLBACK {}",
986 &format!("{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
987 level, name, otype, count, kind, cmd, fixes));
988 Some(CallbackResponse {
989 callback_type: CallbackType::Note, // treat as informational
990 message: Some(format!(
991 "{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
992 level, name, otype, count, kind, cmd, fixes
993 )),
994 file: None,
995 line: None,
996 column: None,
997 suggestion: Some(cmd),
998 terminal_status: None,
999 })
1000 } else {
1001 None
1002 }
1003 }),
1004 );
1005 }
1006
1007 // {
1008 // let summary_flag = Arc::clone(&summary_flag);
1009 // let finished_flag = Arc::clone(&finished_flag);
1010 // let warning_location = Arc::clone(&warning_location);
1011 // // Warning callback for stdout.
1012 // stderr_dispatcher.add_callback(
1013 // r"^warning:\s+(?P<msg>.+)$",
1014 // Box::new(
1015 // move |line: &str, captures: Option<regex::Captures>, multiline_flag: Arc<AtomicBool>| {
1016 // // If summary or finished just matched, skip
1017 // if summary_flag.swap(false, Ordering::Relaxed)
1018 // || finished_flag.swap(false, Ordering::Relaxed)
1019 // {
1020 // return None;
1021 // }
1022
1023 // // 2) If this line *matches* the warning regex, handle as a new warning
1024 // if let Some(caps) = captures {
1025 // let msg = caps.name("msg").unwrap().as_str().to_string();
1026 // // 1) If a location was saved, print file:line:col – msg
1027 // // println!("*WARNING detected: {:?}", msg);
1028 // multiline_flag.store(true, Ordering::Relaxed);
1029 // if let Some(loc) = warning_location.lock().unwrap().take() {
1030 // let file = loc.file.unwrap_or_default();
1031 // let line_num = loc.line.unwrap_or(0);
1032 // let col = loc.column.unwrap_or(0);
1033 // println!("{}:{}:{} - {}", file, line_num, col, msg);
1034 // return Some(CallbackResponse {
1035 // callback_type: CallbackType::Warning,
1036 // message: Some(msg.to_string()),
1037 // file: None, line: None, column: None, suggestion: None, terminal_status: None,
1038 // });
1039 // }
1040 // return Some(CallbackResponse {
1041 // callback_type: CallbackType::Warning,
1042 // message: Some(msg),
1043 // file: None,
1044 // line: None,
1045 // column: None,
1046 // suggestion: None,
1047 // terminal_status: None,
1048 // });
1049 // }
1050
1051 // // 3) Otherwise, if we’re in multiline mode, treat as continuation
1052 // if multiline_flag.load(Ordering::Relaxed) {
1053 // let text = line.trim();
1054 // if text.is_empty() {
1055 // multiline_flag.store(false, Ordering::Relaxed);
1056 // return None;
1057 // }
1058 // // println!(" - {:?}", text);
1059 // return Some(CallbackResponse {
1060 // callback_type: CallbackType::Warning,
1061 // message: Some(text.to_string()),
1062 // file: None,
1063 // line: None,
1064 // column: None,
1065 // suggestion: None,
1066 // terminal_status: None,
1067 // });
1068 // }
1069 // None
1070 // },
1071 // ),
1072 // );
1073 // }
1074
1075 stderr_dispatcher.add_callback(
1076 r"IO\(Custom \{ kind: NotConnected",
1077 Box::new(move |line, _captures, _state, _stats| {
1078 println!("(STDERR) Terminal error detected: {:?}", &line);
1079 let result = if line.contains("NotConnected") {
1080 TerminalError::NoTerminal
1081 } else {
1082 TerminalError::NoError
1083 };
1084 let sender = sender.lock().unwrap();
1085 sender.send(result).ok();
1086 Some(CallbackResponse {
1087 callback_type: CallbackType::Warning, // Choose as appropriate
1088 message: Some(format!("Terminal Error: {}", line)),
1089 file: None,
1090 line: None,
1091 column: None,
1092 suggestion: None,
1093 terminal_status: None,
1094 })
1095 }),
1096 );
1097 stderr_dispatcher.add_callback(
1098 r".*",
1099 Box::new(|line, _captures, _state, _stats| {
1100 log::trace!("stdraw[{:?}]", line);
1101 None // We're just printing, so no callback response is needed.
1102 }),
1103 );
1104 // need to implement autosense/filtering for tool installers; TBD
1105 // stderr_dispatcher.add_callback(
1106 // r"Command 'perl' not found\. Is perl installed\?",
1107 // Box::new(|line, _captures, _state, stats| {
1108 // println!("cargo e sees a perl issue; maybe a prompt in the future or auto-resolution.");
1109 // crate::e_autosense::auto_sense_perl();
1110 // None
1111 // }),
1112 // );
1113 // need to implement autosense/filtering for tool installers; TBD
1114 // stderr_dispatcher.add_callback(
1115 // r"Error configuring OpenSSL build:\s+Command 'perl' not found\. Is perl installed\?",
1116 // Box::new(|line, _captures, _state, stats| {
1117 // println!("Detected OpenSSL build error due to missing 'perl'. Attempting auto-resolution.");
1118 // crate::e_autosense::auto_sense_perl();
1119 // None
1120 // }),
1121 // );
1122 self.stderr_dispatcher = Some(Arc::new(stderr_dispatcher));
1123
1124 // let mut progress_dispatcher = EventDispatcher::new();
1125 // progress_dispatcher.add_callback(r"Progress", Box::new(|line, _captures,_state| {
1126 // println!("(Progress) {}", line);
1127 // None
1128 // }));
1129 // self.progress_dispatcher = Some(Arc::new(progress_dispatcher));
1130
1131 // let mut stage_dispatcher = EventDispatcher::new();
1132 // stage_dispatcher.add_callback(r"Stage:", Box::new(|line, _captures, _state| {
1133 // println!("(Stage) {}", line);
1134 // None
1135 // }));
1136 // self.stage_dispatcher = Some(Arc::new(stage_dispatcher));
1137 }
1138
1139 pub fn run<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
1140 where
1141 F: FnOnce(u32, CargoProcessHandle),
1142 {
1143 if !self.is_filter {
1144 return self.switch_to_passthrough_mode(on_spawn);
1145 }
1146 let mut command = self.build_command();
1147
1148 let mut cargo_process_handle = command.spawn_cargo_capture(
1149 self.clone(),
1150 self.stdout_dispatcher.clone(),
1151 self.stderr_dispatcher.clone(),
1152 self.progress_dispatcher.clone(),
1153 self.stage_dispatcher.clone(),
1154 None,
1155 );
1156 cargo_process_handle.diagnostics = Arc::clone(&self.diagnostics);
1157 let pid = cargo_process_handle.pid;
1158
1159 // Notify observer
1160 on_spawn(pid, cargo_process_handle);
1161
1162 Ok(pid)
1163 }
1164
1165 // pub fn run(self: Arc<Self>) -> anyhow::Result<u32> {
1166 // // Build the command using the builder's configuration
1167 // let mut command = self.build_command();
1168
1169 // // Spawn the cargo process handle
1170 // let cargo_process_handle = command.spawn_cargo_capture(
1171 // self.stdout_dispatcher.clone(),
1172 // self.stderr_dispatcher.clone(),
1173 // self.progress_dispatcher.clone(),
1174 // self.stage_dispatcher.clone(),
1175 // None,
1176 // );
1177 // let pid = cargo_process_handle.pid;
1178 // let mut global = GLOBAL_CHILDREN.lock().unwrap();
1179 // global.insert(pid, Arc::new(Mutex::new(cargo_process_handle)));
1180 // Ok(pid)
1181 // }
1182
1183 pub fn wait(self: Arc<Self>, pid: Option<u32>) -> anyhow::Result<CargoProcessResult> {
1184 let mut global = GLOBAL_CHILDREN.lock().unwrap();
1185 if let Some(pid) = pid {
1186 // Lock the global list of processes and attempt to find the cargo process handle directly by pid
1187 if let Some(cargo_process_handle) = global.get_mut(&pid) {
1188 let mut cargo_process_handle = cargo_process_handle.lock().unwrap();
1189
1190 // Wait for the process to finish and retrieve the result
1191 // println!("Waiting for process with PID: {}", pid);
1192 // let result = cargo_process_handle.wait();
1193 // println!("Process with PID {} finished", pid);
1194 loop {
1195 println!("Waiting for process with PID: {}", pid);
1196
1197 // Attempt to wait for the process, but don't block indefinitely
1198 let status = cargo_process_handle.child.try_wait()?;
1199
1200 // If the status is `Some(status)`, the process has finished
1201 if let Some(status) = status {
1202 if status.code() == Some(101) {
1203 println!("Process with PID {} finished with cargo error", pid);
1204 }
1205
1206 // Check the terminal error flag and update the result if there is an error
1207 if *cargo_process_handle.terminal_error_flag.lock().unwrap()
1208 != TerminalError::NoError
1209 {
1210 let terminal_error =
1211 *cargo_process_handle.terminal_error_flag.lock().unwrap();
1212 cargo_process_handle.result.terminal_error = Some(terminal_error);
1213 }
1214
1215 let final_diagnostics = {
1216 let diag_lock = self.diagnostics.lock().unwrap();
1217 diag_lock.clone()
1218 };
1219 cargo_process_handle.result.diagnostics = final_diagnostics.clone();
1220 cargo_process_handle.result.exit_status = Some(status);
1221 cargo_process_handle.result.end_time = Some(SystemTime::now());
1222 cargo_process_handle.result.elapsed_time = Some(
1223 cargo_process_handle
1224 .result
1225 .end_time
1226 .unwrap()
1227 .duration_since(cargo_process_handle.result.start_time.unwrap())
1228 .unwrap(),
1229 );
1230 println!(
1231 "Process with PID {} finished {:?} {}",
1232 pid,
1233 status,
1234 final_diagnostics.len()
1235 );
1236 return Ok(cargo_process_handle.result.clone());
1237 // return Ok(CargoProcessResult { exit_status: status, ..Default::default() });
1238 }
1239
1240 // Sleep briefly to yield control back to the system and avoid blocking
1241 std::thread::sleep(std::time::Duration::from_secs(1));
1242 }
1243
1244 // Return the result
1245 // match result {
1246 // Ok(res) => Ok(res),
1247 // Err(e) => Err(anyhow::anyhow!("Failed to wait for cargo process: {}", e).into()),
1248 // }
1249 } else {
1250 Err(anyhow::anyhow!(
1251 "Process handle with PID {} not found in GLOBAL_CHILDREN",
1252 pid
1253 )
1254 .into())
1255 }
1256 } else {
1257 Err(anyhow::anyhow!("No PID provided for waiting on cargo process").into())
1258 }
1259 }
1260
1261 // pub fn run_wait(self: Arc<Self>) -> anyhow::Result<CargoProcessResult> {
1262 // // Run the cargo command and get the process handle (non-blocking)
1263 // let pid = self.clone().run()?; // adds to global list of processes
1264 // let result = self.wait(Some(pid)); // Wait for the process to finish
1265 // // Remove the completed process from GLOBAL_CHILDREN
1266 // let mut global = GLOBAL_CHILDREN.lock().unwrap();
1267 // global.remove(&pid);
1268
1269 // result
1270 // }
1271
1272 // Runs the cargo command using the builder's configuration.
1273 // pub fn run(&self) -> anyhow::Result<CargoProcessResult> {
1274 // // Build the command using the builder's configuration
1275 // let mut command = self.build_command();
1276
1277 // // Now use the `spawn_cargo_capture` extension to run the command
1278 // let mut cargo_process_handle = command.spawn_cargo_capture(
1279 // self.stdout_dispatcher.clone(),
1280 // self.stderr_dispatcher.clone(),
1281 // self.progress_dispatcher.clone(),
1282 // self.stage_dispatcher.clone(),
1283 // None,
1284 // );
1285
1286 // // Wait for the process to finish and retrieve the results
1287 // cargo_process_handle.wait().context("Failed to execute cargo process")
1288 // }
1289
1290 /// Configure the command based on the target kind.
1291 pub fn with_target(mut self, target: &CargoTarget) -> Self {
1292 if let Some(origin) = target.origin.clone() {
1293 println!("Target origin: {:?}", origin);
1294 } else {
1295 println!("Target origin is not set");
1296 }
1297 match target.kind {
1298 TargetKind::Unknown | TargetKind::Plugin => {
1299 return self;
1300 }
1301 TargetKind::Bench => {
1302 // // To run benchmarks, use the "bench" command.
1303 // let exe_path = match which("bench") {
1304 // Ok(path) => path,
1305 // Err(err) => {
1306 // eprintln!("Error: 'trunk' not found in PATH: {}", err);
1307 // return self;
1308 // }
1309 // };
1310 // self.alternate_cmd = Some("bench".to_string())
1311 self.args.push("bench".into());
1312 self.args.push(target.name.clone());
1313 }
1314 TargetKind::Test => {
1315 self.args.push("test".into());
1316 // Pass the target's name as a filter to run specific tests.
1317 self.args.push(target.name.clone());
1318 }
1319 TargetKind::UnknownExample
1320 | TargetKind::UnknownExtendedExample
1321 | TargetKind::Example
1322 | TargetKind::ExtendedExample => {
1323 self.args.push(self.subcommand.clone());
1324 //self.args.push("--message-format=json".into());
1325 self.args.push("--example".into());
1326 self.args.push(target.name.clone());
1327 self.args.push("--manifest-path".into());
1328 self.args.push(
1329 target
1330 .manifest_path
1331 .clone()
1332 .to_str()
1333 .unwrap_or_default()
1334 .to_owned(),
1335 );
1336 self = self.with_required_features(&target.manifest_path, target);
1337 }
1338 TargetKind::UnknownBinary
1339 | TargetKind::UnknownExtendedBinary
1340 | TargetKind::Binary
1341 | TargetKind::ExtendedBinary => {
1342 self.args.push(self.subcommand.clone());
1343 self.args.push("--bin".into());
1344 self.args.push(target.name.clone());
1345 self.args.push("--manifest-path".into());
1346 self.args.push(
1347 target
1348 .manifest_path
1349 .clone()
1350 .to_str()
1351 .unwrap_or_default()
1352 .to_owned(),
1353 );
1354 self = self.with_required_features(&target.manifest_path, target);
1355 }
1356 TargetKind::Manifest => {
1357 self.suppressed_flags.insert("quiet".to_string());
1358 self.args.push(self.subcommand.clone());
1359 self.args.push("--manifest-path".into());
1360 self.args.push(
1361 target
1362 .manifest_path
1363 .clone()
1364 .to_str()
1365 .unwrap_or_default()
1366 .to_owned(),
1367 );
1368 }
1369 TargetKind::ManifestTauriExample => {
1370 self.suppressed_flags.insert("quiet".to_string());
1371 self.args.push(self.subcommand.clone());
1372 self.args.push("--example".into());
1373 self.args.push(target.name.clone());
1374 self.args.push("--manifest-path".into());
1375 self.args.push(
1376 target
1377 .manifest_path
1378 .clone()
1379 .to_str()
1380 .unwrap_or_default()
1381 .to_owned(),
1382 );
1383 self = self.with_required_features(&target.manifest_path, target);
1384 }
1385 TargetKind::ScriptScriptisto => {
1386 let exe_path = match which("scriptisto") {
1387 Ok(path) => path,
1388 Err(err) => {
1389 eprintln!("Error: 'scriptisto' not found in PATH: {}", err);
1390 return self;
1391 }
1392 };
1393 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1394 let candidate_opt = match &target.origin {
1395 Some(TargetOrigin::SingleFile(path))
1396 | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1397 _ => None,
1398 };
1399 if let Some(candidate) = candidate_opt {
1400 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1401 self.args.push(candidate.to_string_lossy().to_string());
1402 } else {
1403 println!("No scriptisto origin found for: {:?}", target);
1404 }
1405 }
1406 TargetKind::ScriptRustScript => {
1407 let exe_path = match crate::e_installer::ensure_rust_script() {
1408 Ok(p) => p,
1409 Err(e) => {
1410 eprintln!("{}", e);
1411 return self;
1412 }
1413 };
1414 let candidate_opt = match &target.origin {
1415 Some(TargetOrigin::SingleFile(path))
1416 | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1417 _ => None,
1418 };
1419 if let Some(candidate) = candidate_opt {
1420 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1421 if self.is_filter {
1422 self.args.push("-c".into()); // ask for cargo output
1423 }
1424 self.args.push(candidate.to_string_lossy().to_string());
1425 } else {
1426 println!("No rust-script origin found for: {:?}", target);
1427 }
1428 }
1429 TargetKind::ManifestTauri => {
1430 // First, locate the Cargo.toml using the existing function
1431 let manifest_path = crate::locate_manifest(true).unwrap_or_else(|_| {
1432 eprintln!("Error: Unable to locate Cargo.toml file.");
1433 std::process::exit(1);
1434 });
1435
1436 // Now, get the workspace parent from the manifest directory
1437 let manifest_dir = Path::new(&manifest_path)
1438 .parent()
1439 .unwrap_or(Path::new(".."));
1440
1441 // Ensure npm dependencies are handled at the workspace parent level
1442 let pnpm =
1443 crate::e_installer::check_pnpm_and_install(manifest_dir).unwrap_or_else(|_| {
1444 eprintln!("Error: Unable to check pnpm dependencies.");
1445 PathBuf::new()
1446 });
1447 if pnpm == PathBuf::new() {
1448 crate::e_installer::check_npm_and_install(manifest_dir).unwrap_or_else(|_| {
1449 eprintln!("Error: Unable to check npm dependencies.");
1450 });
1451 }
1452
1453 self.suppressed_flags.insert("quiet".to_string());
1454 // Helper closure to check for tauri.conf.json in a directory.
1455 let has_tauri_conf = |dir: &Path| -> bool { dir.join("tauri.conf.json").exists() };
1456
1457 // Helper closure to check for tauri.conf.json and package.json in a directory.
1458 let has_file = |dir: &Path, filename: &str| -> bool { dir.join(filename).exists() };
1459 // Try candidate's parent (if origin is SingleFile or DefaultBinary).
1460 let candidate_dir_opt = match &target.origin {
1461 Some(TargetOrigin::SingleFile(path))
1462 | Some(TargetOrigin::DefaultBinary(path)) => path.parent(),
1463 _ => None,
1464 };
1465
1466 if let Some(candidate_dir) = candidate_dir_opt {
1467 if has_tauri_conf(candidate_dir) {
1468 println!("Using candidate directory: {}", candidate_dir.display());
1469 self.execution_dir = Some(candidate_dir.to_path_buf());
1470 } else if let Some(manifest_parent) = target.manifest_path.parent() {
1471 if has_tauri_conf(manifest_parent) {
1472 println!("Using manifest parent: {}", manifest_parent.display());
1473 self.execution_dir = Some(manifest_parent.to_path_buf());
1474 } else if let Some(grandparent) = manifest_parent.parent() {
1475 if has_tauri_conf(grandparent) {
1476 println!("Using manifest grandparent: {}", grandparent.display());
1477 self.execution_dir = Some(grandparent.to_path_buf());
1478 } else {
1479 println!("No tauri.conf.json found in candidate, manifest parent, or grandparent; defaulting to manifest parent: {}", manifest_parent.display());
1480 self.execution_dir = Some(manifest_parent.to_path_buf());
1481 }
1482 } else {
1483 println!("No grandparent for manifest; defaulting to candidate directory: {}", candidate_dir.display());
1484 self.execution_dir = Some(candidate_dir.to_path_buf());
1485 }
1486 } else {
1487 println!(
1488 "No manifest parent found for: {}",
1489 target.manifest_path.display()
1490 );
1491 }
1492 // Check for package.json and run npm ls if found.
1493 println!("Checking for package.json in: {}", candidate_dir.display());
1494 if has_file(candidate_dir, "package.json") {
1495 crate::e_installer::check_npm_and_install(candidate_dir).ok();
1496 }
1497 } else if let Some(manifest_parent) = target.manifest_path.parent() {
1498 if has_tauri_conf(manifest_parent) {
1499 println!("Using manifest parent: {}", manifest_parent.display());
1500 self.execution_dir = Some(manifest_parent.to_path_buf());
1501 } else if let Some(grandparent) = manifest_parent.parent() {
1502 if has_tauri_conf(grandparent) {
1503 println!("Using manifest grandparent: {}", grandparent.display());
1504 self.execution_dir = Some(grandparent.to_path_buf());
1505 } else {
1506 println!(
1507 "No tauri.conf.json found; defaulting to manifest parent: {}",
1508 manifest_parent.display()
1509 );
1510 self.execution_dir = Some(manifest_parent.to_path_buf());
1511 }
1512 }
1513 // Check for package.json and run npm ls if found.
1514 println!(
1515 "Checking for package.json in: {}",
1516 manifest_parent.display()
1517 );
1518 if has_file(manifest_parent, "package.json") {
1519 crate::e_installer::check_npm_and_install(manifest_parent).ok();
1520 }
1521 if has_file(Path::new("."), "package.json") {
1522 crate::e_installer::check_npm_and_install(manifest_parent).ok();
1523 }
1524 } else {
1525 println!(
1526 "No manifest parent found for: {}",
1527 target.manifest_path.display()
1528 );
1529 }
1530 self.args.push("tauri".into());
1531 self.args.push("dev".into());
1532 }
1533 TargetKind::ManifestLeptos => {
1534 let readme_path = target
1535 .manifest_path
1536 .parent()
1537 .map(|p| p.join("README.md"))
1538 .filter(|p| p.exists())
1539 .or_else(|| {
1540 target
1541 .manifest_path
1542 .parent()
1543 .map(|p| p.join("readme.md"))
1544 .filter(|p| p.exists())
1545 });
1546
1547 if let Some(readme) = readme_path {
1548 if let Ok(mut file) = std::fs::File::open(&readme) {
1549 let mut contents = String::new();
1550 if file.read_to_string(&mut contents).is_ok()
1551 && contents.contains("cargo leptos watch")
1552 {
1553 // Use cargo leptos watch
1554 println!("Detected 'cargo leptos watch' in {}", readme.display());
1555 crate::e_installer::ensure_leptos().unwrap_or_else(|_| {
1556 eprintln!("Error: Unable to ensure leptos installation.");
1557 PathBuf::new() // Return an empty PathBuf as a fallback
1558 });
1559 self.execution_dir =
1560 target.manifest_path.parent().map(|p| p.to_path_buf());
1561
1562 self.alternate_cmd = Some("cargo".to_string());
1563 self.args.push("leptos".into());
1564 self.args.push("watch".into());
1565 self = self.with_required_features(&target.manifest_path, target);
1566 if let Some(exec_dir) = &self.execution_dir {
1567 if exec_dir.join("package.json").exists() {
1568 println!(
1569 "Found package.json in execution directory: {}",
1570 exec_dir.display()
1571 );
1572 crate::e_installer::check_npm_and_install(exec_dir).ok();
1573 }
1574 }
1575 return self;
1576 }
1577 }
1578 }
1579
1580 // fallback to trunk
1581 let exe_path = match crate::e_installer::ensure_trunk() {
1582 Ok(p) => p,
1583 Err(e) => {
1584 eprintln!("{}", e);
1585 return self;
1586 }
1587 };
1588
1589 if let Some(manifest_parent) = target.manifest_path.parent() {
1590 println!("Manifest path: {}", target.manifest_path.display());
1591 println!(
1592 "Execution directory (same as manifest folder): {}",
1593 manifest_parent.display()
1594 );
1595 self.execution_dir = Some(manifest_parent.to_path_buf());
1596 } else {
1597 println!(
1598 "No manifest parent found for: {}",
1599 target.manifest_path.display()
1600 );
1601 }
1602 if let Some(exec_dir) = &self.execution_dir {
1603 if exec_dir.join("package.json").exists() {
1604 println!(
1605 "Found package.json in execution directory: {}",
1606 exec_dir.display()
1607 );
1608 crate::e_installer::check_npm_and_install(exec_dir).ok();
1609 }
1610 }
1611 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1612 self.args.push("serve".into());
1613 self.args.push("--open".into());
1614 self.args.push("--color".into());
1615 self.args.push("always".into());
1616 self = self.with_required_features(&target.manifest_path, target);
1617 }
1618 TargetKind::ManifestDioxus => {
1619 // For Dioxus targets, print the manifest path and set the execution directory
1620 let exe_path = match crate::e_installer::ensure_dx() {
1621 Ok(path) => path,
1622 Err(e) => {
1623 eprintln!("Error locating `dx`: {}", e);
1624 return self;
1625 }
1626 };
1627 // to be the same directory as the manifest.
1628 if let Some(manifest_parent) = target.manifest_path.parent() {
1629 println!("Manifest path: {}", target.manifest_path.display());
1630 println!(
1631 "Execution directory (same as manifest folder): {}",
1632 manifest_parent.display()
1633 );
1634 self.execution_dir = Some(manifest_parent.to_path_buf());
1635 } else {
1636 println!(
1637 "No manifest parent found for: {}",
1638 target.manifest_path.display()
1639 );
1640 }
1641 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1642 self.args.push("serve".into());
1643 self = self.with_required_features(&target.manifest_path, target);
1644 }
1645 TargetKind::ManifestDioxusExample => {
1646 let exe_path = match crate::e_installer::ensure_dx() {
1647 Ok(path) => path,
1648 Err(e) => {
1649 eprintln!("Error locating `dx`: {}", e);
1650 return self;
1651 }
1652 };
1653 // For Dioxus targets, print the manifest path and set the execution directory
1654 // to be the same directory as the manifest.
1655 if let Some(manifest_parent) = target.manifest_path.parent() {
1656 println!("Manifest path: {}", target.manifest_path.display());
1657 println!(
1658 "Execution directory (same as manifest folder): {}",
1659 manifest_parent.display()
1660 );
1661 self.execution_dir = Some(manifest_parent.to_path_buf());
1662 } else {
1663 println!(
1664 "No manifest parent found for: {}",
1665 target.manifest_path.display()
1666 );
1667 }
1668 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1669 self.args.push("serve".into());
1670 self.args.push("--example".into());
1671 self.args.push(target.name.clone());
1672 self = self.with_required_features(&target.manifest_path, target);
1673 }
1674 }
1675 self
1676 }
1677
1678 /// Configure the command using CLI options.
1679 pub fn with_cli(mut self, cli: &crate::Cli) -> Self {
1680 if cli.quiet && !self.suppressed_flags.contains("quiet") {
1681 // Insert --quiet right after "run" if present.
1682 if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1683 self.args.insert(pos + 1, "--quiet".into());
1684 } else {
1685 self.args.push("--quiet".into());
1686 }
1687 }
1688 if cli.release {
1689 // Insert --release right after the initial "run" command if applicable.
1690 // For example, if the command already contains "run", insert "--release" after it.
1691 if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1692 self.args.insert(pos + 1, "--release".into());
1693 } else {
1694 // If not running a "run" command (like in the Tauri case), simply push it.
1695 self.args.push("--release".into());
1696 }
1697 }
1698 // Append extra arguments (if any) after a "--" separator.
1699 if !cli.extra.is_empty() {
1700 self.args.push("--".into());
1701 self.args.extend(cli.extra.iter().cloned());
1702 }
1703 self
1704 }
1705 /// Append required features based on the manifest, target kind, and name.
1706 /// This method queries your manifest helper function and, if features are found,
1707 /// appends "--features" and the feature list.
1708 pub fn with_required_features(mut self, manifest: &PathBuf, target: &CargoTarget) -> Self {
1709 if !self.args.contains(&"--features".to_string()) {
1710 if let Some(features) = crate::e_manifest::get_required_features_from_manifest(
1711 manifest,
1712 &target.kind,
1713 &target.name,
1714 ) {
1715 self.args.push("--features".to_string());
1716 self.args.push(features);
1717 }
1718 }
1719 self
1720 }
1721
1722 /// Appends extra arguments to the command.
1723 pub fn with_extra_args(mut self, extra: &[String]) -> Self {
1724 if !extra.is_empty() {
1725 // Use "--" to separate Cargo arguments from target-specific arguments.
1726 self.args.push("--".into());
1727 self.args.extend(extra.iter().cloned());
1728 }
1729 self
1730 }
1731
1732 /// Builds the final vector of command-line arguments.
1733 pub fn build(self) -> Vec<String> {
1734 self.args
1735 }
1736
1737 pub fn is_compiler_target(&self) -> bool {
1738 let supported_subcommands = ["run", "build", "check", "leptos", "tauri"];
1739 if let Some(alternate) = &self.alternate_cmd {
1740 if alternate == "trunk" {
1741 return true;
1742 }
1743 if alternate != "cargo" {
1744 return false;
1745 }
1746 }
1747 if let Some(_) = self
1748 .args
1749 .iter()
1750 .position(|arg| supported_subcommands.contains(&arg.as_str()))
1751 {
1752 return true;
1753 }
1754 false
1755 }
1756
1757 pub fn injected_args(&self) -> (String, Vec<String>) {
1758 let mut new_args = self.args.clone();
1759 let supported_subcommands = [
1760 "run", "build", "test", "bench", "clean", "doc", "publish", "update",
1761 ];
1762
1763 if self.is_filter {
1764 if let Some(pos) = new_args
1765 .iter()
1766 .position(|arg| supported_subcommands.contains(&arg.as_str()))
1767 {
1768 // If the command is a supported subcommand like "cargo run", insert the JSON output format and color options.
1769 new_args.insert(pos + 1, "--message-format=json".into());
1770 new_args.insert(pos + 2, "--color".into());
1771 new_args.insert(pos + 3, "always".into());
1772 }
1773 }
1774
1775 let program = self.alternate_cmd.as_deref().unwrap_or("cargo").to_string();
1776 (program, new_args)
1777 }
1778
1779 pub fn print_command(&self) {
1780 let (program, new_args) = self.injected_args();
1781 println!("{} {}", program, new_args.join(" "));
1782 }
1783
1784 /// builds a std::process::Command.
1785 pub fn build_command(&self) -> Command {
1786 let (program, new_args) = self.injected_args();
1787
1788 let mut cmd = Command::new(program);
1789 cmd.args(new_args);
1790
1791 if let Some(dir) = &self.execution_dir {
1792 cmd.current_dir(dir);
1793 }
1794
1795 cmd
1796 }
1797 /// Runs the command and returns everything it printed (stdout + stderr),
1798 /// regardless of exit status.
1799 pub fn capture_output(&self) -> anyhow::Result<String> {
1800 // Build and run
1801 let mut cmd = self.build_command();
1802 let output = cmd
1803 .output()
1804 .map_err(|e| anyhow::anyhow!("Failed to spawn cargo process: {}", e))?;
1805
1806 // Decode both stdout and stderr lossily
1807 let mut all = String::new();
1808 all.push_str(&String::from_utf8_lossy(&output.stdout));
1809 all.push_str(&String::from_utf8_lossy(&output.stderr));
1810
1811 // Return the combined string, even if exit was !success
1812 Ok(all)
1813 }
1814}
1815/// Resolves a file path by:
1816/// 1. If the path is relative, try to resolve it relative to the current working directory.
1817/// 2. If that file does not exist, try to resolve it relative to the parent directory of the manifest path.
1818/// 3. Otherwise, return the original relative path.
1819pub(crate) fn resolve_file_path(manifest_path: &PathBuf, file_str: &str) -> PathBuf {
1820 let file_path = Path::new(file_str);
1821 if file_path.is_relative() {
1822 // 1. Try resolving relative to the current working directory.
1823 if let Ok(cwd) = env::current_dir() {
1824 let cwd_path = cwd.join(file_path);
1825 if cwd_path.exists() {
1826 return cwd_path;
1827 }
1828 }
1829 // 2. Try resolving relative to the parent of the manifest path.
1830 if let Some(manifest_parent) = manifest_path.parent() {
1831 let parent_path = manifest_parent.join(file_path);
1832 if parent_path.exists() {
1833 return parent_path;
1834 }
1835 }
1836 // 3. Neither existed; return the relative path as-is.
1837 return file_path.to_path_buf();
1838 }
1839 file_path.to_path_buf()
1840}
1841
1842// --- Example usage ---
1843#[cfg(test)]
1844mod tests {
1845 use crate::e_target::TargetOrigin;
1846
1847 use super::*;
1848
1849 #[test]
1850 fn test_command_builder_example() {
1851 let target_name = "my_example".to_string();
1852 let target = CargoTarget {
1853 name: "my_example".to_string(),
1854 display_name: "My Example".to_string(),
1855 manifest_path: "Cargo.toml".into(),
1856 kind: TargetKind::Example,
1857 extended: true,
1858 toml_specified: false,
1859 origin: Some(TargetOrigin::SingleFile(PathBuf::from(
1860 "examples/my_example.rs",
1861 ))),
1862 };
1863
1864 let extra_args = vec!["--flag".to_string(), "value".to_string()];
1865
1866 let manifest_path = PathBuf::from("Cargo.toml");
1867 let args = CargoCommandBuilder::new(&target_name, &manifest_path, "run", false)
1868 .with_target(&target)
1869 .with_extra_args(&extra_args)
1870 .build();
1871
1872 // For an example target, we expect something like:
1873 // cargo run --example my_example --manifest-path Cargo.toml -- --flag value
1874 assert!(args.contains(&"--example".to_string()));
1875 assert!(args.contains(&"my_example".to_string()));
1876 assert!(args.contains(&"--manifest-path".to_string()));
1877 assert!(args.contains(&"Cargo.toml".to_string()));
1878 assert!(args.contains(&"--".to_string()));
1879 assert!(args.contains(&"--flag".to_string()));
1880 assert!(args.contains(&"value".to_string()));
1881 }
1882}