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