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