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