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