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