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