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