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