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