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