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