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