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