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)]
36pub struct CargoCommandBuilder {
37 pub manifest_path: PathBuf,
38 pub args: Vec<String>,
39 pub subcommand: String,
40 pub pid: Option<u32>,
41 pub alternate_cmd: Option<String>,
42 pub execution_dir: Option<PathBuf>,
43 pub suppressed_flags: HashSet<String>,
44 pub stdout_dispatcher: Option<Arc<EventDispatcher>>,
45 pub stderr_dispatcher: Option<Arc<EventDispatcher>>,
46 pub progress_dispatcher: Option<Arc<EventDispatcher>>,
47 pub stage_dispatcher: Option<Arc<EventDispatcher>>,
48 pub terminal_error_flag: Arc<Mutex<bool>>,
49 pub sender: Option<Arc<Mutex<Sender<TerminalError>>>>,
50 pub diagnostics: Arc<Mutex<Vec<CargoDiagnostic>>>,
51 pub is_filter: bool,
52}
53impl Default for CargoCommandBuilder {
54 fn default() -> Self {
55 Self::new(&PathBuf::from("Cargo.toml"), "run".into(), false)
56 }
57}
58impl CargoCommandBuilder {
59 /// Creates a new, empty builder.
60 pub fn new(manifest: &PathBuf, subcommand: &str, is_filter: bool) -> Self {
61 let (sender, _receiver) = channel::<TerminalError>();
62 let sender = Arc::new(Mutex::new(sender));
63 let mut builder = CargoCommandBuilder {
64 manifest_path: manifest.clone(),
65 args: Vec::new(),
66 subcommand: subcommand.to_string(),
67 pid: None,
68 alternate_cmd: None,
69 execution_dir: None,
70 suppressed_flags: HashSet::new(),
71 stdout_dispatcher: None,
72 stderr_dispatcher: None,
73 progress_dispatcher: None,
74 stage_dispatcher: None,
75 terminal_error_flag: Arc::new(Mutex::new(false)),
76 sender: Some(sender),
77 diagnostics: Arc::new(Mutex::new(Vec::<CargoDiagnostic>::new())),
78 is_filter: is_filter,
79 };
80 builder.set_default_dispatchers();
81
82 builder
83 }
84
85 // Switch to passthrough mode when the terminal error is detected
86 fn switch_to_passthrough_mode<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
87 where
88 F: FnOnce(u32, CargoProcessHandle),
89 {
90 let mut command = self.build_command();
91
92 // Now, spawn the cargo process in passthrough mode
93 let cargo_process_handle = command.spawn_cargo_passthrough(Arc::clone(&self));
94 let pid = cargo_process_handle.pid;
95 // Notify observer
96 on_spawn(pid, cargo_process_handle);
97
98 Ok(pid)
99 }
100
101 // Set up the default dispatchers, which includes error detection
102 fn set_default_dispatchers(&mut self) {
103 if !self.is_filter {
104 // If this is a filter, we don't need to set up dispatchers
105 return;
106 }
107 let sender = self.sender.clone().unwrap();
108
109 let mut stdout_dispatcher = EventDispatcher::new();
110 stdout_dispatcher.add_callback(
111 r"listening on",
112 Box::new(|line, _captures, _state| {
113 println!("(STDOUT) Dispatcher caught: {}", line);
114 // Use a regex to capture a URL from the line.
115 if let Ok(url_regex) = Regex::new(r"(http://[^\s]+)") {
116 if let Some(url_caps) = url_regex.captures(line) {
117 if let Some(url_match) = url_caps.get(1) {
118 let url = url_match.as_str();
119 // Call open::that on the captured URL.
120 if let Err(e) = open::that(url) {
121 eprintln!("Failed to open URL: {}. Error: {}", url, e);
122 } else {
123 println!("Opened URL: {}", url);
124 }
125 }
126 }
127 } else {
128 eprintln!("Failed to create URL regex");
129 }
130 None
131 }),
132 );
133 stdout_dispatcher.add_callback(
134 r"BuildFinished",
135 Box::new(|line, _captures, _state| {
136 println!("******* {}", line);
137 None
138 }),
139 );
140 stdout_dispatcher.add_callback(
141 r"server listening at:",
142 Box::new(|line, _captures, state| {
143 // If we're not already in multiline mode, this is the initial match.
144 if !state.load(Ordering::Relaxed) {
145 println!("Matched 'server listening at:' in: {}", line);
146 state.store(true, Ordering::Relaxed);
147 Some(CallbackResponse {
148 callback_type: CallbackType::Note, // Choose as appropriate
149 message: Some(format!("Started multiline mode after: {}", line)),
150 file: None,
151 line: None,
152 column: None,
153 suggestion: None,
154 terminal_status: None,
155 })
156 } else {
157 // We are in multiline mode; process subsequent lines.
158 println!("Multiline callback received: {}", line);
159 // Use a regex to capture a URL from the line.
160 let url_regex = match Regex::new(r"(http://[^\s]+)") {
161 Ok(regex) => regex,
162 Err(e) => {
163 eprintln!("Failed to create URL regex: {}", e);
164 return None;
165 }
166 };
167 if let Some(url_caps) = url_regex.captures(line) {
168 let url = url_caps.get(1).unwrap().as_str();
169 // Call open::that on the captured URL.
170 match open::that(url) {
171 Ok(_) => println!("Opened URL: {}", url),
172 Err(e) => eprintln!("Failed to open URL: {}. Error: {}", url, e),
173 }
174 // End multiline mode.
175 state.store(false, Ordering::Relaxed);
176 Some(CallbackResponse {
177 callback_type: CallbackType::Note, // Choose as appropriate
178 message: Some(format!("Captured and opened URL: {}", url)),
179 file: None,
180 line: None,
181 column: None,
182 suggestion: None,
183 terminal_status: None,
184 })
185 } else {
186 None
187 }
188 }
189 }),
190 );
191
192 let mut stderr_dispatcher = EventDispatcher::new();
193
194 let suggestion_mode = Arc::new(AtomicBool::new(false));
195 let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
196 let warning_location: Arc<Mutex<Option<CallbackResponse>>> = Arc::new(Mutex::new(None));
197 let pending_diag: Arc<Mutex<Option<CargoDiagnostic>>> = Arc::new(Mutex::new(None));
198 let diagnostic_counts: Arc<Mutex<HashMap<CargoDiagnosticLevel, usize>>> =
199 Arc::new(Mutex::new(HashMap::new()));
200
201 let pending_d = Arc::clone(&pending_diag);
202 let counts = Arc::clone(&diagnostic_counts);
203
204 let diagnostics_arc = Arc::clone(&self.diagnostics);
205 stderr_dispatcher.add_callback(
206 r"^(?P<level>\w+)(\[(?P<error_code>E\d+)\])?:\s+(?P<msg>.+)$", // Regex for diagnostic line
207 Box::new(move |_line, caps, _multiline_flag| {
208 if let Some(caps) = caps {
209 let mut counts = counts.lock().unwrap();
210 // Create a PendingDiag and save the message
211 let mut pending_diag = pending_d.lock().unwrap();
212 let mut last_lineref = String::new();
213 if let Some(existing_diag) = pending_diag.take() {
214 let mut diags = diagnostics_arc.lock().unwrap();
215 last_lineref = existing_diag.lineref.clone();
216 // let diag_level = match existing_diag.level.as_str() {
217 // "error" => CargoDiagnosticLevel::Error,
218 // "warning" => CargoDiagnosticLevel::Warning,
219 // "help" => CargoDiagnosticLevel::Help,
220 // "note" => CargoDiagnosticLevel::Note,
221 // _ => {
222 // println!("Unknown diagnostic level: {}", existing_diag.level);
223 // return None; // Ignore unknown levels
224 // }
225 // };
226 // *counts.entry(diag_level).or_insert(0) += 1;
227 // let current_count = counts.get(&diag_level).unwrap_or(&0);
228 // existing_diag.diag_number = Some(*current_count);
229 println!("{:?}", existing_diag);
230 diags.push(existing_diag.clone());
231 }
232 log::trace!("Diagnostic line: {}", _line);
233 let level = caps["level"].to_string(); // e.g., "warning", "error"
234 // If the diagnostic level is "note" or "help", skip processing.
235 // if level == "note" || level == "help" {
236 // println!("Skipping note/help diagnostic: {}", _line);
237 // return None;
238 // }
239 let message = caps["msg"].to_string();
240 // If the message contains "generated" followed by one or more digits,
241 // then ignore this diagnostic by returning None.
242 //
243 // Using a regex to check for "generated\s+\d+"
244 let re_generated = regex::Regex::new(r"generated\s+\d+").unwrap();
245 if re_generated.is_match(&message) {
246 log::trace!("Skipping generated diagnostic: {}", _line);
247 return None;
248 }
249
250 let error_code = caps.name("error_code").map(|m| m.as_str().to_string());
251 let diag_level = match level.as_str() {
252 "error" => CargoDiagnosticLevel::Error,
253 "warning" => CargoDiagnosticLevel::Warning,
254 "help" => CargoDiagnosticLevel::Help,
255 "note" => CargoDiagnosticLevel::Note,
256 _ => {
257 println!("Unknown diagnostic level: {}", level);
258 return None; // Ignore unknown levels
259 }
260 };
261 // Increment the count for this level
262 *counts.entry(diag_level).or_insert(0) += 1;
263 // // Check if the previous diagnostic
264
265 let current_count = counts.get(&diag_level).unwrap_or(&0);
266 let diag = CargoDiagnostic {
267 error_code: error_code.clone(),
268 lineref: last_lineref.clone(),
269 level: level.clone(),
270 message,
271 suggestion: None,
272 help: None,
273 note: None,
274 uses_color: true,
275 diag_num_padding: Some(2),
276 diag_number: Some(*current_count),
277 };
278
279 // // Check if the previous diagnostic has a callback response
280 // if let Some(existing_diag) = pending_diag.take() {
281 // // Save the current diagnostic's callback response
282 // if let Some(response) = existing_diag.callback_response {
283 // // Save it into the new diagnostic
284 // diag.save_callback_response(response);
285 // }
286 // }
287
288 // Save the new diagnostic
289 *pending_diag = Some(diag);
290
291 // Track the count of diagnostics for each level
292 return Some(CallbackResponse {
293 callback_type: CallbackType::LevelMessage, // Treat subsequent lines as warnings
294 message: None,
295 file: None,
296 line: None,
297 column: None,
298 suggestion: None, // This is the suggestion part
299 terminal_status: None,
300 });
301 } else {
302 println!("No captures found in line: {}", _line);
303 None
304 }
305 }),
306 );
307
308 {
309 let location_lock_clone = Arc::clone(&warning_location);
310 let suggestion_m = Arc::clone(&suggestion_mode);
311
312 // Suggestion callback that adds subsequent lines as suggestions
313 stderr_dispatcher.add_callback(
314 r"^(?P<msg>.*)$", // Capture all lines following the location
315 Box::new(move |line, _captures, _multiline_flag| {
316 if suggestion_m.load(Ordering::Relaxed) {
317 // Only process lines that match the suggestion format
318 if let Some(caps) = suggestion_regex.captures(line.trim()) {
319 // Capture the line number and code from the suggestion line
320 // let line_num = caps[1].parse::<usize>().unwrap_or(0);
321 let code = caps[2].to_string();
322
323 // Lock the pending_diag to add the suggestion
324 if let Ok(mut lock) = location_lock_clone.lock() {
325 if let Some(mut loc) = lock.take() {
326 // let file = loc.file.clone().unwrap_or_default();
327 // let col = loc.column.unwrap_or(0);
328
329 // Concatenate the suggestion line to the message
330 let mut msg = loc.message.unwrap_or_default();
331 msg.push_str(&format!("\n{}", code));
332
333 // Print the concatenated suggestion for debugging
334 // println!("daveSuggestion for {}:{}:{} - {}", file, line_num, col, msg);
335
336 // Update the location with the new concatenated message
337 loc.message = Some(msg.clone());
338 // println!("Updating location lock with new suggestion: {}", msg);
339 // Save the updated location back to shared state
340 // if let Ok(mut lock) = location_lock_clone.lock() {
341 // println!("Updating location lock with new suggestion: {}", msg);
342 lock.replace(loc);
343 // } else {
344 // eprintln!("Failed to acquire lock for location_lock_clone");
345 // }
346 }
347 // return Some(CallbackResponse {
348 // callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
349 // message: Some(msg.clone()),
350 // file: Some(file),
351 // line: Some(line_num),
352 // column: Some(col),
353 // suggestion: Some(msg), // This is the suggestion part
354 // terminal_status: None,
355 // });
356 }
357 }
358 } else {
359 // println!("Suggestion mode is not active. Ignoring line: {}", line);
360 }
361
362 None
363 }),
364 );
365 }
366 {
367 let suggestion_m = Arc::clone(&suggestion_mode);
368 let pending_diag_clone = Arc::clone(&pending_diag);
369 let diagnostics_arc = Arc::clone(&self.diagnostics);
370 // Callback for handling when an empty line or new diagnostic is received
371 stderr_dispatcher.add_callback(
372 r"^\s*$", // Regex to capture empty line
373 Box::new(move |_line, _captures, _multiline_flag| {
374 // println!("Empty line detected: {}", line);
375 suggestion_m.store(false, Ordering::Relaxed);
376 // End of current diagnostic: take and process it.
377 if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
378 println!("{:?}", pending_diag);
379 // Use diagnostics_arc instead of self.diagnostices
380 let mut diags = diagnostics_arc.lock().unwrap();
381 diags.push(pending_diag.clone());
382 } else {
383 // println!("No pending diagnostic to process.");
384 }
385 // Handle empty line scenario to end the current diagnostic processing
386 // if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
387 // println!("{:?}", pending_diag);
388 // let mut diags = self.diagnostics.lock().unwrap();
389 // diags.push(pending_diag.clone());
390 // // let diag = crate::e_eventdispatcher::convert_message_to_diagnostic(msg, &msg_str);
391 // // diags.push(diag.clone());
392 // // if let Some(ref sd) = stage_disp_clone {
393 // // sd.dispatch(&format!("Stage: Diagnostic occurred at {:?}", now));
394 // // }
395 // // Handle the saved PendingDiag and its CallbackResponse
396 // // if let Some(callback_response) = pending_diag.callback_response {
397 // // println!("End of Diagnostic: {:?}", callback_response);
398 // // }
399 // } else {
400 // println!("No pending diagnostic to process.");
401 // }
402
403 None
404 }),
405 );
406 }
407
408 // {
409 // let pending_diag = Arc::clone(&pending_diag);
410 // let location_lock = Arc::clone(&warning_location);
411 // let suggestion_m = Arc::clone(&suggestion_mode);
412
413 // let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
414
415 // stderr_dispatcher.add_callback(
416 // r"^\s*(\d+)\s*\|\s*(.*)$", // Match suggestion line format
417 // Box::new(move |line, _captures, _multiline_flag| {
418 // if suggestion_m.load(Ordering::Relaxed) {
419 // // Only process lines that match the suggestion format
420 // if let Some(caps) = suggestion_regex.captures(line.trim()) {
421 // // Capture the line number and code from the suggestion line
422 // let line_num = caps[1].parse::<usize>().unwrap_or(0);
423 // let code = caps[2].to_string();
424
425 // // Lock the pending_diag to add the suggestion
426 // if let Some(mut loc) = location_lock.lock().unwrap().take() {
427 // println!("Suggestion line: {}", line);
428 // let file = loc.file.clone().unwrap_or_default();
429 // let col = loc.column.unwrap_or(0);
430
431 // // Concatenate the suggestion line to the message
432 // let mut msg = loc.message.unwrap_or_default();
433 // msg.push_str(&format!("\n{} | {}", line_num, code)); // Append the suggestion properly
434
435 // // Print the concatenated suggestion for debugging
436 // println!("Suggestion for {}:{}:{} - {}", file, line_num, col, msg);
437
438 // // Update the location with the new concatenated message
439 // loc.message = Some(msg.clone());
440
441 // // Save the updated location back to shared state
442 // location_lock.lock().unwrap().replace(loc);
443
444 // // return Some(CallbackResponse {
445 // // callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
446 // // message: Some(msg.clone()),
447 // // file: Some(file),
448 // // line: Some(line_num),
449 // // column: Some(col),
450 // // suggestion: Some(msg), // This is the suggestion part
451 // // terminal_status: None,
452 // // });
453 // } else {
454 // println!("No location information available for suggestion line: {}", line);
455 // }
456 // } else {
457 // println!("Suggestion line does not match expected format: {}", line);
458 // }
459 // } else {
460 // println!("Suggestion mode is not active. Ignoring line: {}", line);
461 // }
462
463 // None
464 // }),
465 // );
466
467 // }
468
469 {
470 let location_lock = Arc::clone(&warning_location);
471 let pending_diag = Arc::clone(&pending_diag);
472 let suggestion_mode = Arc::clone(&suggestion_mode);
473 stderr_dispatcher.add_callback(
474 r"^(?P<msg>.*)$", // Capture all lines following the location
475 Box::new(move |line, _captures, _multiline_flag| {
476 // Lock the location to fetch the original diagnostic info
477 if let Ok(location_guard) = location_lock.lock() {
478 if let Some(loc) = location_guard.as_ref() {
479 let file = loc.file.clone().unwrap_or_default();
480 let line_num = loc.line.unwrap_or(0);
481 let col = loc.column.unwrap_or(0);
482 // println!("SUGGESTION: Suggestion for {}:{}:{} {}", file, line_num, col, line);
483
484 // Only treat lines starting with | or numbers as suggestion lines
485 if line.trim().starts_with('|')
486 || line.trim().starts_with(char::is_numeric)
487 {
488 // Get the existing suggestion and append the new line
489 let suggestion = line.trim();
490
491 // Print the suggestion for debugging
492 // println!("Suggestion for {}:{}:{} - {}", file, line_num, col, suggestion);
493
494 // Lock the pending_diag and update its callback_response field
495 let mut pending_diag = match pending_diag.lock() {
496 Ok(lock) => lock,
497 Err(e) => {
498 eprintln!("Failed to acquire lock: {}", e);
499 return None; // Handle the error appropriately
500 }
501 };
502 if let Some(diag) = pending_diag.take() {
503 // If a PendingDiag already exists, update the existing callback response with the new suggestion
504 let mut diag = diag;
505
506 // Append the new suggestion to the existing one
507 if let Some(ref mut existing) = diag.suggestion {
508 diag.suggestion =
509 Some(format!("{}\n{}", existing, suggestion));
510 } else {
511 diag.suggestion = Some(suggestion.to_string());
512 }
513
514 // Update the shared state with the new PendingDiag
515 *pending_diag = Some(diag.clone());
516 return Some(CallbackResponse {
517 callback_type: CallbackType::Suggestion, // Treat subsequent lines as warnings
518 message: Some(
519 diag.clone().suggestion.clone().unwrap().clone(),
520 ),
521 file: Some(file),
522 line: Some(line_num),
523 column: Some(col),
524 suggestion: diag.clone().suggestion.clone(), // This is the suggestion part
525 terminal_status: None,
526 });
527 } else {
528 // println!("No pending diagnostic to process for suggestion line: {}", line);
529 }
530 } else {
531 // If the line doesn't match the suggestion format, just return it as is
532 if line.trim().is_empty() {
533 // Ignore empty lines
534 suggestion_mode.store(false, Ordering::Relaxed);
535 return None;
536 }
537 }
538 } else {
539 // println!("No location information available for suggestion line: {}", line);
540 }
541 }
542 None
543 }),
544 );
545 }
546
547 // 2) Location callback stores its response into that shared state
548 {
549 let pending_diag = Arc::clone(&pending_diag);
550 let warning_location = Arc::clone(&warning_location);
551 let location_lock = Arc::clone(&warning_location);
552 let suggestion_mode = Arc::clone(&suggestion_mode);
553 let manifest_path = self.manifest_path.clone();
554 stderr_dispatcher.add_callback(
555 // r"^\s*-->\s+(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+)$",
556 r"^\s*-->\s+(?P<file>.+?)(?::(?P<line>\d+))?(?::(?P<col>\d+))?\s*$",
557 Box::new(move |_line, caps, _multiline_flag| {
558 log::trace!("Location line: {}", _line);
559 // if multiline_flag.load(Ordering::Relaxed) {
560 if let Some(caps) = caps {
561 let file = caps["file"].to_string();
562 let resolved_path = resolve_file_path(&manifest_path, &file);
563 let file = resolved_path.to_str().unwrap_or_default().to_string();
564 let line = caps["line"].parse::<usize>().unwrap_or(0);
565 let column = caps["col"].parse::<usize>().unwrap_or(0);
566 let resp = CallbackResponse {
567 callback_type: CallbackType::Location,
568 message: format!("{}:{}:{}", file, line, column).into(),
569 file: Some(file.clone()),
570 line: Some(line),
571 column: Some(column),
572 suggestion: None,
573 terminal_status: None,
574 };
575 // Lock the pending_diag and update its callback_response field
576 let mut pending_diag = pending_diag.lock().unwrap();
577 if let Some(diag) = pending_diag.take() {
578 // If a PendingDiag already exists, save the new callback response in the existing PendingDiag
579 let mut diag = diag;
580 diag.lineref = format!("{}:{}:{}", file, line, column); // Update the lineref
581 // diag.save_callback_response(resp.clone()); // Save the callback response
582 // Update the shared state with the new PendingDiag
583 *pending_diag = Some(diag);
584 }
585 // Save it for the generic callback to see
586 *warning_location.lock().unwrap() = Some(resp.clone());
587 *location_lock.lock().unwrap() = Some(resp.clone());
588 // Set suggestion mode to true as we've encountered a location line
589 suggestion_mode.store(true, Ordering::Relaxed);
590 return Some(resp.clone());
591 } else {
592 println!("No captures found in line: {}", _line);
593 }
594 // }
595 None
596 }),
597 );
598 }
599
600 // // 3) Note callback — attach note to pending_diag
601 {
602 let pending_diag = Arc::clone(&pending_diag);
603 stderr_dispatcher.add_callback(
604 r"^\s*=\s*note:\s*(?P<msg>.+)$",
605 Box::new(move |_line, caps, _state| {
606 if let Some(caps) = caps {
607 let mut pending_diag = pending_diag.lock().unwrap();
608 if let Some(ref mut resp) = *pending_diag {
609 // Prepare the new note with the blue prefix
610 let new_note =
611 format!("\x1b[34m{}\x1b[0m: {}", "note", caps["msg"].to_string());
612
613 // Append or set the note
614 if let Some(existing_note) = &resp.note {
615 // If there's already a note, append with newline and the new note
616 resp.note = Some(format!("{}\n{}", existing_note, new_note));
617 } else {
618 // If no existing note, just set the new note
619 resp.note = Some(new_note);
620 }
621 }
622 }
623 None
624 }),
625 );
626 }
627
628 // 4) Help callback — attach help to pending_diag
629 {
630 let pending_diag = Arc::clone(&pending_diag);
631 stderr_dispatcher.add_callback(
632 r"^\s*(?:\=|\|)\s*help:\s*(?P<msg>.+)$", // Regex to match both '=' and '|' before help:
633 Box::new(move |_line, caps, _state| {
634 if let Some(caps) = caps {
635 let mut pending_diag = pending_diag.lock().unwrap();
636 if let Some(ref mut resp) = *pending_diag {
637 // Create the new help message with the orange "h:" prefix
638 let new_help =
639 format!("\x1b[38;5;214mhelp: {}\x1b[0m", caps["msg"].to_string());
640
641 // Append or set the help message
642 if let Some(existing_help) = &resp.help {
643 // If there's already a help message, append with newline
644 resp.help = Some(format!("{}\n{}", existing_help, new_help));
645 } else {
646 // If no existing help message, just set the new one
647 resp.help = Some(new_help);
648 }
649 }
650 }
651 None
652 }),
653 );
654 }
655
656 stderr_dispatcher.add_callback(
657 r"(?:\x1b\[[0-9;]*[A-Za-z])*\s*Serving(?:\x1b\[[0-9;]*[A-Za-z])*\s+at\s+(http://[^\s]+)",
658 Box::new(|line, captures, _state| {
659 if let Some(caps) = captures {
660 let url = caps.get(1).unwrap().as_str();
661 println!("(STDERR) Captured URL: {}", url);
662 match open::that(url) {
663 Ok(_) => println!("(STDERR) Opened URL: {}", url),
664 Err(e) => eprintln!("(STDERR) Failed to open URL: {}. Error: {:?}", url, e),
665 }
666 Some(CallbackResponse {
667 callback_type: CallbackType::OpenedUrl, // Choose as appropriate
668 message: Some(format!("Captured and opened URL: {}", url)),
669 file: None,
670 line: None,
671 column: None,
672 suggestion: None,
673 terminal_status: None,
674 })
675 } else {
676 println!("(STDERR) No URL captured in line: {}", line);
677 None
678 }
679 }),
680);
681
682 let finished_flag = Arc::new(AtomicBool::new(false));
683
684 // 0) Finished‐profile summary callback
685 {
686 let finished_flag = Arc::clone(&finished_flag);
687 stderr_dispatcher.add_callback(
688 r"^Finished\s+`(?P<profile>[^`]+)`\s+profile\s+\[(?P<opts>[^\]]+)\]\s+target\(s\)\s+in\s+(?P<dur>[0-9.]+s)$",
689 Box::new(move |_line, caps, _multiline_flag| {
690 if let Some(caps) = caps {
691 finished_flag.store(true, Ordering::Relaxed);
692 let profile = &caps["profile"];
693 let opts = &caps["opts"];
694 let dur = &caps["dur"];
695 Some(CallbackResponse {
696 callback_type: CallbackType::Note,
697 message: Some(format!("Finished `{}` [{}] in {}", profile, opts, dur)),
698 file: None, line: None, column: None, suggestion: None, terminal_status: None,
699 })
700 } else {
701 None
702 }
703 }),
704 );
705 }
706
707 let summary_flag = Arc::new(AtomicBool::new(false));
708 {
709 let summary_flag = Arc::clone(&summary_flag);
710 stderr_dispatcher.add_callback(
711 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",
712 Box::new(move |_line, caps, multiline_flag| {
713 let summary_flag = Arc::clone(&summary_flag);
714 if let Some(caps) = caps {
715 summary_flag.store(true, Ordering::Relaxed);
716 // Always start fresh
717 multiline_flag.store(false, Ordering::Relaxed);
718
719 let level = &caps["level"];
720 let name = &caps["name"];
721 let otype = &caps["otype"];
722 let count: usize = caps["count"].parse().unwrap_or(0);
723 let kind = &caps["kind"]; // "warnings" or "errors"
724 let cmd = caps["cmd"].to_string();
725 let fixes: usize = caps["fixes"].parse().unwrap_or(0);
726
727 println!("SUMMARIZATION CALLBACK {}",
728 &format!("{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
729 level, name, otype, count, kind, cmd, fixes));
730 Some(CallbackResponse {
731 callback_type: CallbackType::Note, // treat as informational
732 message: Some(format!(
733 "{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
734 level, name, otype, count, kind, cmd, fixes
735 )),
736 file: None,
737 line: None,
738 column: None,
739 suggestion: Some(cmd),
740 terminal_status: None,
741 })
742 } else {
743 None
744 }
745 }),
746 );
747 }
748
749 // {
750 // let summary_flag = Arc::clone(&summary_flag);
751 // let finished_flag = Arc::clone(&finished_flag);
752 // let warning_location = Arc::clone(&warning_location);
753 // // Warning callback for stdout.
754 // stderr_dispatcher.add_callback(
755 // r"^warning:\s+(?P<msg>.+)$",
756 // Box::new(
757 // move |line: &str, captures: Option<regex::Captures>, multiline_flag: Arc<AtomicBool>| {
758 // // If summary or finished just matched, skip
759 // if summary_flag.swap(false, Ordering::Relaxed)
760 // || finished_flag.swap(false, Ordering::Relaxed)
761 // {
762 // return None;
763 // }
764
765 // // 2) If this line *matches* the warning regex, handle as a new warning
766 // if let Some(caps) = captures {
767 // let msg = caps.name("msg").unwrap().as_str().to_string();
768 // // 1) If a location was saved, print file:line:col – msg
769 // // println!("*WARNING detected: {:?}", msg);
770 // multiline_flag.store(true, Ordering::Relaxed);
771 // if let Some(loc) = warning_location.lock().unwrap().take() {
772 // let file = loc.file.unwrap_or_default();
773 // let line_num = loc.line.unwrap_or(0);
774 // let col = loc.column.unwrap_or(0);
775 // println!("{}:{}:{} - {}", file, line_num, col, msg);
776 // return Some(CallbackResponse {
777 // callback_type: CallbackType::Warning,
778 // message: Some(msg.to_string()),
779 // file: None, line: None, column: None, suggestion: None, terminal_status: None,
780 // });
781 // }
782 // return Some(CallbackResponse {
783 // callback_type: CallbackType::Warning,
784 // message: Some(msg),
785 // file: None,
786 // line: None,
787 // column: None,
788 // suggestion: None,
789 // terminal_status: None,
790 // });
791 // }
792
793 // // 3) Otherwise, if we’re in multiline mode, treat as continuation
794 // if multiline_flag.load(Ordering::Relaxed) {
795 // let text = line.trim();
796 // if text.is_empty() {
797 // multiline_flag.store(false, Ordering::Relaxed);
798 // return None;
799 // }
800 // // println!(" - {:?}", text);
801 // return Some(CallbackResponse {
802 // callback_type: CallbackType::Warning,
803 // message: Some(text.to_string()),
804 // file: None,
805 // line: None,
806 // column: None,
807 // suggestion: None,
808 // terminal_status: None,
809 // });
810 // }
811 // None
812 // },
813 // ),
814 // );
815 // }
816
817 stderr_dispatcher.add_callback(
818 r"IO\(Custom \{ kind: NotConnected",
819 Box::new(move |line, _captures, _state| {
820 println!("(STDERR) Terminal error detected: {:?}", &line);
821 let result = if line.contains("NotConnected") {
822 TerminalError::NoTerminal
823 } else {
824 TerminalError::NoError
825 };
826 let sender = sender.lock().unwrap();
827 sender.send(result).ok();
828 Some(CallbackResponse {
829 callback_type: CallbackType::Warning, // Choose as appropriate
830 message: Some(format!("Terminal Error: {}", line)),
831 file: None,
832 line: None,
833 column: None,
834 suggestion: None,
835 terminal_status: None,
836 })
837 }),
838 );
839 stderr_dispatcher.add_callback(
840 r".*",
841 Box::new(|line, _captures, _state| {
842 log::trace!("stdraw[{:?}]", line);
843 None // We're just printing, so no callback response is needed.
844 }),
845 );
846 self.stderr_dispatcher = Some(Arc::new(stderr_dispatcher));
847
848 // let mut progress_dispatcher = EventDispatcher::new();
849 // progress_dispatcher.add_callback(r"Progress", Box::new(|line, _captures,_state| {
850 // println!("(Progress) {}", line);
851 // None
852 // }));
853 // self.progress_dispatcher = Some(Arc::new(progress_dispatcher));
854
855 // let mut stage_dispatcher = EventDispatcher::new();
856 // stage_dispatcher.add_callback(r"Stage:", Box::new(|line, _captures, _state| {
857 // println!("(Stage) {}", line);
858 // None
859 // }));
860 // self.stage_dispatcher = Some(Arc::new(stage_dispatcher));
861 }
862
863 pub fn run<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
864 where
865 F: FnOnce(u32, CargoProcessHandle),
866 {
867 if !self.is_filter {
868 return self.switch_to_passthrough_mode(on_spawn);
869 }
870 let mut command = self.build_command();
871
872 let mut cargo_process_handle = command.spawn_cargo_capture(
873 self.clone(),
874 self.stdout_dispatcher.clone(),
875 self.stderr_dispatcher.clone(),
876 self.progress_dispatcher.clone(),
877 self.stage_dispatcher.clone(),
878 None,
879 );
880 cargo_process_handle.diagnostics = Arc::clone(&self.diagnostics);
881 let pid = cargo_process_handle.pid;
882
883 // Notify observer
884 on_spawn(pid, cargo_process_handle);
885
886 Ok(pid)
887 }
888
889 // pub fn run(self: Arc<Self>) -> anyhow::Result<u32> {
890 // // Build the command using the builder's configuration
891 // let mut command = self.build_command();
892
893 // // Spawn the cargo process handle
894 // let cargo_process_handle = command.spawn_cargo_capture(
895 // self.stdout_dispatcher.clone(),
896 // self.stderr_dispatcher.clone(),
897 // self.progress_dispatcher.clone(),
898 // self.stage_dispatcher.clone(),
899 // None,
900 // );
901 // let pid = cargo_process_handle.pid;
902 // let mut global = GLOBAL_CHILDREN.lock().unwrap();
903 // global.insert(pid, Arc::new(Mutex::new(cargo_process_handle)));
904 // Ok(pid)
905 // }
906
907 pub fn wait(self: Arc<Self>, pid: Option<u32>) -> anyhow::Result<CargoProcessResult> {
908 let mut global = GLOBAL_CHILDREN.lock().unwrap();
909 if let Some(pid) = pid {
910 // Lock the global list of processes and attempt to find the cargo process handle directly by pid
911 if let Some(cargo_process_handle) = global.get_mut(&pid) {
912 let mut cargo_process_handle = cargo_process_handle.lock().unwrap();
913
914 // Wait for the process to finish and retrieve the result
915 // println!("Waiting for process with PID: {}", pid);
916 // let result = cargo_process_handle.wait();
917 // println!("Process with PID {} finished", pid);
918 loop {
919 println!("Waiting for process with PID: {}", pid);
920
921 // Attempt to wait for the process, but don't block indefinitely
922 let status = cargo_process_handle.child.try_wait()?;
923
924 // If the status is `Some(status)`, the process has finished
925 if let Some(status) = status {
926 // Check the terminal error flag and update the result if there is an error
927 if *cargo_process_handle.terminal_error_flag.lock().unwrap()
928 != TerminalError::NoError
929 {
930 let terminal_error =
931 *cargo_process_handle.terminal_error_flag.lock().unwrap();
932 cargo_process_handle.result.terminal_error = Some(terminal_error);
933 }
934
935 let final_diagnostics = {
936 let diag_lock = self.diagnostics.lock().unwrap();
937 diag_lock.clone()
938 };
939 cargo_process_handle.result.diagnostics = final_diagnostics.clone();
940 cargo_process_handle.result.exit_status = Some(status);
941 cargo_process_handle.result.end_time = Some(SystemTime::now());
942 cargo_process_handle.result.elapsed_time = Some(
943 cargo_process_handle
944 .result
945 .end_time
946 .unwrap()
947 .duration_since(cargo_process_handle.result.start_time.unwrap())
948 .unwrap(),
949 );
950 println!(
951 "Process with PID {} finished {:?} {}",
952 pid,
953 status,
954 final_diagnostics.len()
955 );
956 return Ok(cargo_process_handle.result.clone());
957 // return Ok(CargoProcessResult { exit_status: status, ..Default::default() });
958 }
959
960 // Sleep briefly to yield control back to the system and avoid blocking
961 std::thread::sleep(std::time::Duration::from_secs(1));
962 }
963
964 // Return the result
965 // match result {
966 // Ok(res) => Ok(res),
967 // Err(e) => Err(anyhow::anyhow!("Failed to wait for cargo process: {}", e).into()),
968 // }
969 } else {
970 Err(anyhow::anyhow!(
971 "Process handle with PID {} not found in GLOBAL_CHILDREN",
972 pid
973 )
974 .into())
975 }
976 } else {
977 Err(anyhow::anyhow!("No PID provided for waiting on cargo process").into())
978 }
979 }
980
981 // pub fn run_wait(self: Arc<Self>) -> anyhow::Result<CargoProcessResult> {
982 // // Run the cargo command and get the process handle (non-blocking)
983 // let pid = self.clone().run()?; // adds to global list of processes
984 // let result = self.wait(Some(pid)); // Wait for the process to finish
985 // // Remove the completed process from GLOBAL_CHILDREN
986 // let mut global = GLOBAL_CHILDREN.lock().unwrap();
987 // global.remove(&pid);
988
989 // result
990 // }
991
992 /// Runs the cargo command using the builder's configuration.
993 // pub fn run(&self) -> anyhow::Result<CargoProcessResult> {
994 // // Build the command using the builder's configuration
995 // let mut command = self.build_command();
996
997 // // Now use the `spawn_cargo_capture` extension to run the command
998 // let mut cargo_process_handle = command.spawn_cargo_capture(
999 // self.stdout_dispatcher.clone(),
1000 // self.stderr_dispatcher.clone(),
1001 // self.progress_dispatcher.clone(),
1002 // self.stage_dispatcher.clone(),
1003 // None,
1004 // );
1005
1006 // // Wait for the process to finish and retrieve the results
1007 // cargo_process_handle.wait().context("Failed to execute cargo process")
1008 // }
1009
1010 /// Configure the command based on the target kind.
1011 pub fn with_target(mut self, target: &CargoTarget) -> Self {
1012 if let Some(origin) = target.origin.clone() {
1013 println!("Target origin: {:?}", origin);
1014 } else {
1015 println!("Target origin is not set");
1016 }
1017 match target.kind {
1018 TargetKind::Unknown | TargetKind::Plugin => {
1019 return self;
1020 }
1021 TargetKind::Bench => {
1022 // // To run benchmarks, use the "bench" command.
1023 // let exe_path = match which("bench") {
1024 // Ok(path) => path,
1025 // Err(err) => {
1026 // eprintln!("Error: 'trunk' not found in PATH: {}", err);
1027 // return self;
1028 // }
1029 // };
1030 // self.alternate_cmd = Some("bench".to_string())
1031 self.args.push("bench".into());
1032 self.args.push(target.name.clone());
1033 }
1034 TargetKind::Test => {
1035 self.args.push("test".into());
1036 // Pass the target's name as a filter to run specific tests.
1037 self.args.push(target.name.clone());
1038 }
1039 TargetKind::UnknownExample
1040 | TargetKind::UnknownExtendedExample
1041 | TargetKind::Example
1042 | TargetKind::ExtendedExample => {
1043 self.args.push(self.subcommand.clone());
1044 //self.args.push("--message-format=json".into());
1045 self.args.push("--example".into());
1046 self.args.push(target.name.clone());
1047 self.args.push("--manifest-path".into());
1048 self.args.push(
1049 target
1050 .manifest_path
1051 .clone()
1052 .to_str()
1053 .unwrap_or_default()
1054 .to_owned(),
1055 );
1056 }
1057 TargetKind::UnknownBinary
1058 | TargetKind::UnknownExtendedBinary
1059 | TargetKind::Binary
1060 | TargetKind::ExtendedBinary => {
1061 self.args.push(self.subcommand.clone());
1062 self.args.push("--bin".into());
1063 self.args.push(target.name.clone());
1064 self.args.push("--manifest-path".into());
1065 self.args.push(
1066 target
1067 .manifest_path
1068 .clone()
1069 .to_str()
1070 .unwrap_or_default()
1071 .to_owned(),
1072 );
1073 }
1074 TargetKind::Manifest => {
1075 self.suppressed_flags.insert("quiet".to_string());
1076 self.args.push(self.subcommand.clone());
1077 self.args.push("--manifest-path".into());
1078 self.args.push(
1079 target
1080 .manifest_path
1081 .clone()
1082 .to_str()
1083 .unwrap_or_default()
1084 .to_owned(),
1085 );
1086 }
1087 TargetKind::ManifestTauriExample => {
1088 self.suppressed_flags.insert("quiet".to_string());
1089 self.args.push(self.subcommand.clone());
1090 self.args.push("--example".into());
1091 self.args.push(target.name.clone());
1092 self.args.push("--manifest-path".into());
1093 self.args.push(
1094 target
1095 .manifest_path
1096 .clone()
1097 .to_str()
1098 .unwrap_or_default()
1099 .to_owned(),
1100 );
1101 }
1102 TargetKind::ScriptScriptisto => {
1103 let exe_path = match which("scriptisto") {
1104 Ok(path) => path,
1105 Err(err) => {
1106 eprintln!("Error: 'scriptisto' not found in PATH: {}", err);
1107 return self;
1108 }
1109 };
1110 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1111 let candidate_opt = match &target.origin {
1112 Some(TargetOrigin::SingleFile(path))
1113 | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1114 _ => None,
1115 };
1116 if let Some(candidate) = candidate_opt {
1117 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1118 self.args.push(candidate.to_string_lossy().to_string());
1119 } else {
1120 println!("No scriptisto origin found for: {:?}", target);
1121 }
1122 }
1123 TargetKind::ScriptRustScript => {
1124 let exe_path = match which("rust-script") {
1125 Ok(path) => path,
1126 Err(err) => {
1127 eprintln!("Error: 'rust-script' not found in PATH: {}", err);
1128 return self;
1129 }
1130 };
1131 let candidate_opt = match &target.origin {
1132 Some(TargetOrigin::SingleFile(path))
1133 | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1134 _ => None,
1135 };
1136 if let Some(candidate) = candidate_opt {
1137 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1138 if self.is_filter {
1139 self.args.push("-c".into()); // ask for cargo output
1140 }
1141 self.args.push(candidate.to_string_lossy().to_string());
1142 } else {
1143 println!("No rust-script origin found for: {:?}", target);
1144 }
1145 }
1146 TargetKind::ManifestTauri => {
1147 self.suppressed_flags.insert("quiet".to_string());
1148 // Helper closure to check for tauri.conf.json in a directory.
1149 let has_tauri_conf = |dir: &Path| -> bool { dir.join("tauri.conf.json").exists() };
1150
1151 // Try candidate's parent (if origin is SingleFile or DefaultBinary).
1152 let candidate_dir_opt = match &target.origin {
1153 Some(TargetOrigin::SingleFile(path))
1154 | Some(TargetOrigin::DefaultBinary(path)) => path.parent(),
1155 _ => None,
1156 };
1157
1158 if let Some(candidate_dir) = candidate_dir_opt {
1159 if has_tauri_conf(candidate_dir) {
1160 println!("Using candidate directory: {}", candidate_dir.display());
1161 self.execution_dir = Some(candidate_dir.to_path_buf());
1162 } else if let Some(manifest_parent) = target.manifest_path.parent() {
1163 if has_tauri_conf(manifest_parent) {
1164 println!("Using manifest parent: {}", manifest_parent.display());
1165 self.execution_dir = Some(manifest_parent.to_path_buf());
1166 } else if let Some(grandparent) = manifest_parent.parent() {
1167 if has_tauri_conf(grandparent) {
1168 println!("Using manifest grandparent: {}", grandparent.display());
1169 self.execution_dir = Some(grandparent.to_path_buf());
1170 } else {
1171 println!("No tauri.conf.json found in candidate, manifest parent, or grandparent; defaulting to manifest parent: {}", manifest_parent.display());
1172 self.execution_dir = Some(manifest_parent.to_path_buf());
1173 }
1174 } else {
1175 println!("No grandparent for manifest; defaulting to candidate directory: {}", candidate_dir.display());
1176 self.execution_dir = Some(candidate_dir.to_path_buf());
1177 }
1178 } else {
1179 println!(
1180 "No manifest parent found for: {}",
1181 target.manifest_path.display()
1182 );
1183 }
1184 } else if let Some(manifest_parent) = target.manifest_path.parent() {
1185 if has_tauri_conf(manifest_parent) {
1186 println!("Using manifest parent: {}", manifest_parent.display());
1187 self.execution_dir = Some(manifest_parent.to_path_buf());
1188 } else if let Some(grandparent) = manifest_parent.parent() {
1189 if has_tauri_conf(grandparent) {
1190 println!("Using manifest grandparent: {}", grandparent.display());
1191 self.execution_dir = Some(grandparent.to_path_buf());
1192 } else {
1193 println!(
1194 "No tauri.conf.json found; defaulting to manifest parent: {}",
1195 manifest_parent.display()
1196 );
1197 self.execution_dir = Some(manifest_parent.to_path_buf());
1198 }
1199 }
1200 } else {
1201 println!(
1202 "No manifest parent found for: {}",
1203 target.manifest_path.display()
1204 );
1205 }
1206 self.args.push("tauri".into());
1207 self.args.push("dev".into());
1208 }
1209 TargetKind::ManifestLeptos => {
1210 let readme_path = target
1211 .manifest_path
1212 .parent()
1213 .map(|p| p.join("README.md"))
1214 .filter(|p| p.exists())
1215 .or_else(|| {
1216 target
1217 .manifest_path
1218 .parent()
1219 .map(|p| p.join("readme.md"))
1220 .filter(|p| p.exists())
1221 });
1222
1223 if let Some(readme) = readme_path {
1224 if let Ok(mut file) = std::fs::File::open(&readme) {
1225 let mut contents = String::new();
1226 if file.read_to_string(&mut contents).is_ok()
1227 && contents.contains("cargo leptos watch")
1228 {
1229 // Use cargo leptos watch
1230 println!("Detected 'cargo leptos watch' in {}", readme.display());
1231 self.execution_dir =
1232 target.manifest_path.parent().map(|p| p.to_path_buf());
1233 self.execution_dir =
1234 Some(target.manifest_path.parent().unwrap().to_path_buf());
1235 self.alternate_cmd = Some("cargo".to_string());
1236 self.args.push("leptos".into());
1237 self.args.push("watch".into());
1238 self = self.with_required_features(&target.manifest_path, target);
1239 return self;
1240 }
1241 }
1242 }
1243
1244 // fallback to trunk
1245 let exe_path = match which("trunk") {
1246 Ok(path) => path,
1247 Err(err) => {
1248 eprintln!("Error: 'trunk' not found in PATH: {}", err);
1249 return self;
1250 }
1251 };
1252
1253 if let Some(manifest_parent) = target.manifest_path.parent() {
1254 println!("Manifest path: {}", target.manifest_path.display());
1255 println!(
1256 "Execution directory (same as manifest folder): {}",
1257 manifest_parent.display()
1258 );
1259 self.execution_dir = Some(manifest_parent.to_path_buf());
1260 } else {
1261 println!(
1262 "No manifest parent found for: {}",
1263 target.manifest_path.display()
1264 );
1265 }
1266
1267 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1268 self.args.push("serve".into());
1269 self.args.push("--open".into());
1270 self.args.push("--color".into());
1271 self.args.push("always".into());
1272 self = self.with_required_features(&target.manifest_path, target);
1273 }
1274 TargetKind::ManifestDioxus => {
1275 let exe_path = match which("dx") {
1276 Ok(path) => path,
1277 Err(err) => {
1278 eprintln!("Error: 'dx' not found in PATH: {}", err);
1279 return self;
1280 }
1281 };
1282 // For Dioxus targets, print the manifest path and set the execution directory
1283 // to be the same directory as the manifest.
1284 if let Some(manifest_parent) = target.manifest_path.parent() {
1285 println!("Manifest path: {}", target.manifest_path.display());
1286 println!(
1287 "Execution directory (same as manifest folder): {}",
1288 manifest_parent.display()
1289 );
1290 self.execution_dir = Some(manifest_parent.to_path_buf());
1291 } else {
1292 println!(
1293 "No manifest parent found for: {}",
1294 target.manifest_path.display()
1295 );
1296 }
1297 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1298 self.args.push("serve".into());
1299 self = self.with_required_features(&target.manifest_path, target);
1300 }
1301 TargetKind::ManifestDioxusExample => {
1302 let exe_path = match which("dx") {
1303 Ok(path) => path,
1304 Err(err) => {
1305 eprintln!("Error: 'dx' not found in PATH: {}", err);
1306 return self;
1307 }
1308 };
1309 // For Dioxus targets, print the manifest path and set the execution directory
1310 // to be the same directory as the manifest.
1311 if let Some(manifest_parent) = target.manifest_path.parent() {
1312 println!("Manifest path: {}", target.manifest_path.display());
1313 println!(
1314 "Execution directory (same as manifest folder): {}",
1315 manifest_parent.display()
1316 );
1317 self.execution_dir = Some(manifest_parent.to_path_buf());
1318 } else {
1319 println!(
1320 "No manifest parent found for: {}",
1321 target.manifest_path.display()
1322 );
1323 }
1324 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1325 self.args.push("serve".into());
1326 self.args.push("--example".into());
1327 self.args.push(target.name.clone());
1328 self = self.with_required_features(&target.manifest_path, target);
1329 }
1330 }
1331 self
1332 }
1333
1334 /// Configure the command using CLI options.
1335 pub fn with_cli(mut self, cli: &crate::Cli) -> Self {
1336 if cli.quiet && !self.suppressed_flags.contains("quiet") {
1337 // Insert --quiet right after "run" if present.
1338 if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1339 self.args.insert(pos + 1, "--quiet".into());
1340 } else {
1341 self.args.push("--quiet".into());
1342 }
1343 }
1344 if cli.release {
1345 // Insert --release right after the initial "run" command if applicable.
1346 // For example, if the command already contains "run", insert "--release" after it.
1347 if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1348 self.args.insert(pos + 1, "--release".into());
1349 } else {
1350 // If not running a "run" command (like in the Tauri case), simply push it.
1351 self.args.push("--release".into());
1352 }
1353 }
1354 // Append extra arguments (if any) after a "--" separator.
1355 if !cli.extra.is_empty() {
1356 self.args.push("--".into());
1357 self.args.extend(cli.extra.iter().cloned());
1358 }
1359 self
1360 }
1361 /// Append required features based on the manifest, target kind, and name.
1362 /// This method queries your manifest helper function and, if features are found,
1363 /// appends "--features" and the feature list.
1364 pub fn with_required_features(mut self, manifest: &PathBuf, target: &CargoTarget) -> Self {
1365 if let Some(features) = crate::e_manifest::get_required_features_from_manifest(
1366 manifest,
1367 &target.kind,
1368 &target.name,
1369 ) {
1370 self.args.push("--features".to_string());
1371 self.args.push(features);
1372 }
1373 self
1374 }
1375
1376 /// Appends extra arguments to the command.
1377 pub fn with_extra_args(mut self, extra: &[String]) -> Self {
1378 if !extra.is_empty() {
1379 // Use "--" to separate Cargo arguments from target-specific arguments.
1380 self.args.push("--".into());
1381 self.args.extend(extra.iter().cloned());
1382 }
1383 self
1384 }
1385
1386 /// Builds the final vector of command-line arguments.
1387 pub fn build(self) -> Vec<String> {
1388 self.args
1389 }
1390
1391 /// Optionally, builds a std::process::Command.
1392 pub fn build_command(&self) -> Command {
1393 let mut is_cargo = false;
1394 let mut new_args = self.args.clone();
1395 let supported_subcommands = [
1396 "run", "build", "test", "bench", "clean", "doc", "publish", "update",
1397 ];
1398
1399 let mut cmd = if let Some(alternate) = &self.alternate_cmd {
1400 Command::new(alternate)
1401 } else {
1402 is_cargo = true;
1403 Command::new("cargo")
1404 };
1405 if is_cargo && self.is_filter {
1406 if let Some(pos) = new_args
1407 .iter()
1408 .position(|arg| supported_subcommands.contains(&arg.as_str()))
1409 {
1410 // If the command is "cargo run", insert the JSON output format and color options.
1411 new_args.insert(pos + 1, "--message-format=json".into());
1412 new_args.insert(pos + 2, "--color".into());
1413 new_args.insert(pos + 3, "always".into());
1414 }
1415 }
1416 cmd.args(new_args);
1417 if let Some(dir) = &self.execution_dir {
1418 cmd.current_dir(dir);
1419 }
1420 cmd
1421 }
1422}
1423/// Resolves a file path by:
1424/// 1. If the path is relative, try to resolve it relative to the current working directory.
1425/// 2. If that file does not exist, try to resolve it relative to the parent directory of the manifest path.
1426/// 3. Otherwise, return the original relative path.
1427fn resolve_file_path(manifest_path: &PathBuf, file_str: &str) -> PathBuf {
1428 let file_path = Path::new(file_str);
1429 if file_path.is_relative() {
1430 // 1. Try resolving relative to the current working directory.
1431 if let Ok(cwd) = env::current_dir() {
1432 let cwd_path = cwd.join(file_path);
1433 if cwd_path.exists() {
1434 return cwd_path;
1435 }
1436 }
1437 // 2. Try resolving relative to the parent of the manifest path.
1438 if let Some(manifest_parent) = manifest_path.parent() {
1439 let parent_path = manifest_parent.join(file_path);
1440 if parent_path.exists() {
1441 return parent_path;
1442 }
1443 }
1444 // 3. Neither existed; return the relative path as-is.
1445 return file_path.to_path_buf();
1446 }
1447 file_path.to_path_buf()
1448}
1449
1450// --- Example usage ---
1451#[cfg(test)]
1452mod tests {
1453 use crate::e_target::TargetOrigin;
1454
1455 use super::*;
1456
1457 #[test]
1458 fn test_command_builder_example() {
1459 let target = CargoTarget {
1460 name: "my_example".to_string(),
1461 display_name: "My Example".to_string(),
1462 manifest_path: "Cargo.toml".into(),
1463 kind: TargetKind::Example,
1464 extended: true,
1465 toml_specified: false,
1466 origin: Some(TargetOrigin::SingleFile(PathBuf::from(
1467 "examples/my_example.rs",
1468 ))),
1469 };
1470
1471 let extra_args = vec!["--flag".to_string(), "value".to_string()];
1472
1473 let manifest_path = PathBuf::from("Cargo.toml");
1474 let args = CargoCommandBuilder::new(&manifest_path, &"run".to_string(), false)
1475 .with_target(&target)
1476 .with_extra_args(&extra_args)
1477 .build();
1478
1479 // For an example target, we expect something like:
1480 // cargo run --example my_example --manifest-path Cargo.toml -- --flag value
1481 assert!(args.contains(&"--example".to_string()));
1482 assert!(args.contains(&"my_example".to_string()));
1483 assert!(args.contains(&"--manifest-path".to_string()));
1484 assert!(args.contains(&"Cargo.toml".to_string()));
1485 assert!(args.contains(&"--".to_string()));
1486 assert!(args.contains(&"--flag".to_string()));
1487 assert!(args.contains(&"value".to_string()));
1488 }
1489}