1use std::{collections::HashMap, env, process::{Command, Stdio}, sync::{Arc, Mutex}, time::{Duration, Instant}};
4use serde::Deserialize;
5use emoji::symbols;
6use colored::*;
7use dialoguer::FuzzySelect;
8
9#[derive(Deserialize, Debug)]
11#[serde(untagged)]
12pub enum Script {
13 Default(String),
14 Inline {
15 command: Option<String>,
16 requires: Option<Vec<String>>,
17 toolchain: Option<String>,
18 info: Option<String>,
19 env: Option<HashMap<String, String>>,
20 include: Option<Vec<String>>,
21 interpreter: Option<String>,
22 },
23 CILike {
24 script: String,
25 command: Option<String>,
26 requires: Option<Vec<String>>,
27 toolchain: Option<String>,
28 info: Option<String>,
29 env: Option<HashMap<String, String>>,
30 include: Option<Vec<String>>,
31 interpreter: Option<String>,
32 }
33}
34
35#[derive(Deserialize)]
37pub struct Scripts {
38 pub global_env: Option<HashMap<String, String>>,
39 pub scripts: HashMap<String, Script>
40}
41
42use crate::error::{CargoScriptError, create_tool_not_found_error, create_toolchain_not_found_error};
43
44pub fn run_script(scripts: &Scripts, script_name: &str, env_overrides: Vec<String>, dry_run: bool, quiet: bool, verbose: bool, show_metrics: bool) -> Result<(), CargoScriptError> {
63 if dry_run {
64 if !quiet {
65 println!("{}", "DRY-RUN MODE: Preview of what would be executed".bold().yellow());
66 println!("{}\n", "=".repeat(80).yellow());
67 }
68 dry_run_script(scripts, script_name, env_overrides, 0, quiet, verbose)?;
69 if !quiet {
70 println!("\n{}", "No commands were actually executed.".italic().green());
71 }
72 return Ok(());
73 }
74
75 let script_durations = Arc::new(Mutex::new(HashMap::new()));
76
77 fn run_script_with_level(
78 scripts: &Scripts,
79 script_name: &str,
80 env_overrides: Vec<String>,
81 level: usize,
82 script_durations: Arc<Mutex<HashMap<String, Duration>>>,
83 quiet: bool,
84 verbose: bool,
85 ) -> Result<(), CargoScriptError> {
86 let mut env_vars = scripts.global_env.clone().unwrap_or_default();
87 let indent = " ".repeat(level);
88
89 let script_start_time = Instant::now();
90
91 if let Some(script) = scripts.scripts.get(script_name) {
92 match script {
93 Script::Default(cmd) => {
94 if !quiet {
95 let msg = format!(
96 "{}{} {}: [ {} ]",
97 indent,
98 symbols::other_symbol::CHECK_MARK.glyph,
99 "Running script".green(),
100 script_name
101 );
102 println!("{}\n", msg);
103 }
104 let final_env = get_final_env(&env_vars, &env_overrides);
105 apply_env_vars(&env_vars, &env_overrides);
106 execute_command(script_name, None, cmd, None, &final_env)?;
107 }
108 Script::Inline {
109 command,
110 info,
111 env,
112 include,
113 interpreter,
114 requires,
115 toolchain,
116 ..
117 } | Script::CILike {
118 command,
119 info,
120 env,
121 include,
122 interpreter,
123 requires,
124 toolchain,
125 ..
126 } => {
127 if let Err(e) = check_requirements(requires.as_deref().unwrap_or(&[]), toolchain.as_ref()) {
128 return Err(e);
129 }
130
131 let description = info.as_deref().map(|desc| {
133 format!(
134 "{} {}: {}",
135 emoji::objects::book_paper::BOOKMARK_TABS.glyph,
136 "Description".green(),
137 desc
138 )
139 });
140
141 if let Some(include_scripts) = include {
142 if !quiet {
143 let desc_str = description.as_deref().unwrap_or("");
144 let msg = format!(
145 "{}{} {}: [ {} ]{}",
146 indent,
147 symbols::other_symbol::CHECK_MARK.glyph,
148 "Running include script".green(),
149 script_name,
150 if desc_str.is_empty() { String::new() } else { format!(" {}", desc_str) }
151 );
152 println!("{}\n", msg);
153 }
154 for include_script in include_scripts {
155 run_script_with_level(
156 scripts,
157 include_script,
158 env_overrides.clone(),
159 level + 1,
160 script_durations.clone(),
161 quiet,
162 verbose,
163 )?;
164 }
165 }
166
167 if let Some(cmd) = command {
168 if !quiet {
169 let desc_str = description.as_deref().unwrap_or("");
170 let msg = format!(
171 "{}{} {}: [ {} ]{}",
172 indent,
173 symbols::other_symbol::CHECK_MARK.glyph,
174 "Running script".green(),
175 script_name,
176 if desc_str.is_empty() { String::new() } else { format!(" {}", desc_str) }
177 );
178 println!("{}\n", msg);
179 }
180
181 if let Some(script_env) = env {
182 env_vars.extend(script_env.clone());
183 }
184 let final_env = get_final_env(&env_vars, &env_overrides);
185 apply_env_vars(&env_vars, &env_overrides);
186 execute_command(script_name, interpreter.as_deref(), cmd, toolchain.as_deref(), &final_env)?;
187 }
188 }
189 }
190
191 let script_duration = script_start_time.elapsed();
192 if level > 0 || scripts.scripts.get(script_name).map_or(false, |s| matches!(s, Script::Default(_) | Script::Inline { command: Some(_), .. } | Script::CILike { command: Some(_), .. })) {
193 script_durations
194 .lock()
195 .unwrap()
196 .insert(script_name.to_string(), script_duration);
197 }
198 Ok(())
199 } else {
200 let available_scripts: Vec<String> = scripts.scripts.keys().cloned().collect();
201 return Err(CargoScriptError::ScriptNotFound {
202 script_name: script_name.to_string(),
203 available_scripts,
204 });
205 }
206 }
207
208 run_script_with_level(scripts, script_name, env_overrides, 0, script_durations.clone(), quiet, verbose)?;
209
210 if show_metrics && !quiet {
212 let durations = script_durations.lock().unwrap();
213 if !durations.is_empty() {
214 let total_duration: Duration = durations.values().cloned().sum();
215
216 println!("\n");
217 println!("{}", "Scripts Performance".bold().yellow());
218 println!("{}", "-".repeat(80).yellow());
219 for (script, duration) in durations.iter() {
220 println!("āļø Script: {:<25} š Running time: {:.2?}", script.green(), duration);
221 }
222 println!("\nš Total running time: {:.2?}", total_duration);
223 }
224 }
225
226 Ok(())
227}
228
229
230fn get_final_env(env_vars: &HashMap<String, String>, env_overrides: &[String]) -> HashMap<String, String> {
244 let mut final_env = env_vars.clone();
245
246 for override_str in env_overrides {
247 if let Some((key, value)) = override_str.split_once('=') {
248 final_env.insert(key.to_string(), value.to_string());
249 }
250 }
251
252 final_env
253}
254
255fn apply_env_vars(env_vars: &HashMap<String, String>, env_overrides: &[String]) {
265 let final_env = get_final_env(env_vars, env_overrides);
266
267 for (key, value) in &final_env {
268 unsafe {
272 env::set_var(key, value);
273 }
274 }
275}
276
277fn execute_command(script_name: &str, interpreter: Option<&str>, command: &str, toolchain: Option<&str>, env_vars: &HashMap<String, String>) -> Result<(), CargoScriptError> {
294 let mut cmd = if let Some(tc) = toolchain {
295 let mut command_with_toolchain = format!("cargo +{} ", tc);
296 command_with_toolchain.push_str(command);
297 let mut cmd = Command::new("sh");
298 cmd.arg("-c")
299 .arg(command_with_toolchain)
300 .stdout(Stdio::inherit())
301 .stderr(Stdio::inherit());
302 for (key, value) in env_vars {
303 cmd.env(key, value);
304 }
305 cmd.spawn()
306 .map_err(|e| CargoScriptError::ExecutionError {
307 script: script_name.to_string(),
308 command: command.to_string(),
309 source: e,
310 })?
311 } else {
312 match interpreter {
313 Some("bash") => {
314 let mut cmd = Command::new("bash");
315 cmd.arg("-c")
316 .arg(command)
317 .stdout(Stdio::inherit())
318 .stderr(Stdio::inherit());
319 for (key, value) in env_vars {
320 cmd.env(key, value);
321 }
322 cmd.spawn()
323 .map_err(|e| CargoScriptError::ExecutionError {
324 script: "unknown".to_string(),
325 command: command.to_string(),
326 source: e,
327 })?
328 },
329 Some("zsh") => {
330 let mut cmd = Command::new("zsh");
331 cmd.arg("-c")
332 .arg(command)
333 .stdout(Stdio::inherit())
334 .stderr(Stdio::inherit());
335 for (key, value) in env_vars {
336 cmd.env(key, value);
337 }
338 cmd.spawn()
339 .map_err(|e| CargoScriptError::ExecutionError {
340 script: "unknown".to_string(),
341 command: command.to_string(),
342 source: e,
343 })?
344 },
345 Some("powershell") => {
346 let mut cmd = Command::new("powershell");
347 cmd.args(&["-NoProfile", "-Command", command])
348 .stdout(Stdio::inherit())
349 .stderr(Stdio::inherit());
350 for (key, value) in env_vars {
351 cmd.env(key, value);
352 }
353 cmd.spawn()
354 .map_err(|e| CargoScriptError::ExecutionError {
355 script: "unknown".to_string(),
356 command: command.to_string(),
357 source: e,
358 })?
359 },
360 Some("cmd") => {
361 let mut cmd = Command::new("cmd");
362 cmd.args(&["/C", command])
363 .stdout(Stdio::inherit())
364 .stderr(Stdio::inherit());
365 for (key, value) in env_vars {
366 cmd.env(key, value);
367 }
368 cmd.spawn()
369 .map_err(|e| CargoScriptError::ExecutionError {
370 script: "unknown".to_string(),
371 command: command.to_string(),
372 source: e,
373 })?
374 },
375 Some(other) => {
376 let mut cmd = Command::new(other);
377 cmd.arg("-c")
378 .arg(command)
379 .stdout(Stdio::inherit())
380 .stderr(Stdio::inherit());
381 for (key, value) in env_vars {
382 cmd.env(key, value);
383 }
384 cmd.spawn()
385 .map_err(|e| CargoScriptError::ExecutionError {
386 script: "unknown".to_string(),
387 command: command.to_string(),
388 source: e,
389 })?
390 },
391 None => {
392 if cfg!(target_os = "windows") {
393 let mut cmd = Command::new("cmd");
394 cmd.args(&["/C", command])
395 .stdout(Stdio::inherit())
396 .stderr(Stdio::inherit());
397 for (key, value) in env_vars {
398 cmd.env(key, value);
399 }
400 cmd.spawn()
401 .map_err(|e| CargoScriptError::ExecutionError {
402 script: "unknown".to_string(),
403 command: command.to_string(),
404 source: e,
405 })?
406 } else {
407 let mut cmd = Command::new("sh");
408 cmd.arg("-c")
409 .arg(command)
410 .stdout(Stdio::inherit())
411 .stderr(Stdio::inherit());
412 for (key, value) in env_vars {
413 cmd.env(key, value);
414 }
415 cmd.spawn()
416 .map_err(|e| CargoScriptError::ExecutionError {
417 script: "unknown".to_string(),
418 command: command.to_string(),
419 source: e,
420 })?
421 }
422 }
423 }
424 };
425
426 let exit_status = cmd.wait().map_err(|e| CargoScriptError::ExecutionError {
427 script: script_name.to_string(),
428 command: command.to_string(),
429 source: e,
430 })?;
431
432 if !exit_status.success() {
434 let is_self_replace_attempt = cfg!(target_os = "windows")
435 && (command.contains("cargo install --path .")
436 || command.contains("cargo install --path")
437 || (command.contains("cargo install") && command.contains("--path")));
438
439 if is_self_replace_attempt {
440 return Err(CargoScriptError::WindowsSelfReplacementError {
441 script: script_name.to_string(),
442 command: command.to_string(),
443 });
444 }
445 }
446
447 Ok(())
448}
449
450fn dry_run_script(
461 scripts: &Scripts,
462 script_name: &str,
463 env_overrides: Vec<String>,
464 level: usize,
465 quiet: bool,
466 verbose: bool,
467) -> Result<(), CargoScriptError> {
468 let indent = " ".repeat(level);
469 let mut env_vars = scripts.global_env.clone().unwrap_or_default();
470
471 if let Some(script) = scripts.scripts.get(script_name) {
472 match script {
473 Script::Default(cmd) => {
474 if !quiet {
475 println!(
476 "{}{} {}: [ {} ]",
477 indent,
478 "š".yellow(),
479 "Would run script".cyan(),
480 script_name.bold()
481 );
482 println!("{} Command: {}", indent, cmd.green());
483 let final_env = get_final_env(&env_vars, &env_overrides);
484 if !final_env.is_empty() {
486 println!("{} Environment variables:", indent);
487 for (key, value) in &final_env {
488 println!("{} {} = {}", indent, key.cyan(), value.green());
489 }
490 }
491 if level == 0 {
492 println!(); }
494 }
495 }
496 Script::Inline {
497 command,
498 info,
499 env,
500 include,
501 interpreter,
502 requires,
503 toolchain,
504 ..
505 } | Script::CILike {
506 command,
507 info,
508 env,
509 include,
510 interpreter,
511 requires,
512 toolchain,
513 ..
514 } => {
515 if !quiet {
516 if verbose {
518 if let Some(reqs) = requires {
519 if !reqs.is_empty() {
520 println!(
521 "{}{} {}: [ {} ]",
522 indent,
523 "š".yellow(),
524 "Would check requirements".cyan(),
525 script_name.bold()
526 );
527 for req in reqs {
528 println!("{} - {}", indent, req.green());
529 }
530 println!();
531 }
532 }
533 }
534
535 if verbose {
536 if let Some(tc) = toolchain {
537 println!(
538 "{}{} {}: {}",
539 indent,
540 "š§".yellow(),
541 "Would use toolchain".cyan(),
542 tc.bold().green()
543 );
544 println!();
545 }
546 }
547
548 if verbose {
549 if let Some(desc) = info {
550 println!(
551 "{}{} {}: {}",
552 indent,
553 "š".yellow(),
554 "Description".cyan(),
555 desc.green()
556 );
557 println!();
558 }
559 }
560
561 if let Some(include_scripts) = include {
562 println!(
563 "{}{} {}: [ {} ]",
564 indent,
565 "š".yellow(),
566 "Would run include scripts".cyan(),
567 script_name.bold()
568 );
569 if verbose {
570 if let Some(desc) = info {
571 println!("{} Description: {}", indent, desc.green());
572 }
573 }
574 println!();
575 for include_script in include_scripts {
576 dry_run_script(scripts, include_script, env_overrides.clone(), level + 1, quiet, verbose)?;
577 }
578 }
579
580 if let Some(cmd) = command {
581 println!(
582 "{}{} {}: [ {} ]",
583 indent,
584 "š".yellow(),
585 "Would run script".cyan(),
586 script_name.bold()
587 );
588
589 if let Some(interp) = interpreter {
591 println!("{} Interpreter: {}", indent, interp.green());
592 }
593
594 if let Some(tc) = toolchain {
595 println!("{} Toolchain: {}", indent, tc.green());
596 }
597
598 println!("{} Command: {}", indent, cmd.green());
599
600 if let Some(script_env) = env {
601 env_vars.extend(script_env.clone());
602 }
603
604 let final_env = get_final_env(&env_vars, &env_overrides);
605 if !final_env.is_empty() {
607 println!("{} Environment variables:", indent);
608 for (key, value) in &final_env {
609 println!("{} {} = {}", indent, key.cyan(), value.green());
610 }
611 }
612 if level == 0 {
613 println!(); }
615 }
616 } else {
617 if let Some(include_scripts) = include {
619 for include_script in include_scripts {
620 dry_run_script(scripts, include_script, env_overrides.clone(), level + 1, quiet, verbose)?;
621 }
622 }
623 }
624 }
625 }
626 } else {
627 let available_scripts: Vec<String> = scripts.scripts.keys().cloned().collect();
628 return Err(CargoScriptError::ScriptNotFound {
629 script_name: script_name.to_string(),
630 available_scripts,
631 });
632 }
633
634 Ok(())
635}
636
637pub fn interactive_select_script(scripts: &Scripts, quiet: bool) -> Result<String, CargoScriptError> {
654 if scripts.scripts.is_empty() {
655 return Err(CargoScriptError::ScriptNotFound {
656 script_name: "".to_string(),
657 available_scripts: vec![],
658 });
659 }
660
661 let mut items: Vec<(String, String)> = scripts.scripts
663 .iter()
664 .map(|(name, script)| {
665 let description = match script {
666 Script::Default(_) => "".to_string(),
667 Script::Inline { info, .. } | Script::CILike { info, .. } => {
668 info.clone().unwrap_or_else(|| "".to_string())
669 }
670 };
671 (name.clone(), description)
672 })
673 .collect();
674
675 items.sort_by(|a, b| a.0.cmp(&b.0));
677
678 let display_items: Vec<String> = items
680 .iter()
681 .map(|(name, desc)| {
682 if desc.is_empty() {
683 name.clone()
684 } else {
685 format!("{} - {}", name, desc)
686 }
687 })
688 .collect();
689
690 if !quiet {
691 println!("{}", "Select a script to run:".cyan().bold());
692 println!();
693 }
694
695 let selection = FuzzySelect::new()
696 .with_prompt("Script")
697 .items(&display_items)
698 .default(0)
699 .interact()
700 .map_err(|e| CargoScriptError::ExecutionError {
701 script: "interactive".to_string(),
702 command: "fuzzy_select".to_string(),
703 source: std::io::Error::new(std::io::ErrorKind::Other, format!("Interactive selection failed: {}", e)),
704 })?;
705
706 Ok(items[selection].0.clone())
707}
708
709fn check_requirements(requires: &[String], toolchain: Option<&String>) -> Result<(), CargoScriptError> {
727 for req in requires {
728 if let Some((tool, version)) = req.split_once(' ') {
729 let output = Command::new(tool)
730 .arg("--version")
731 .output()
732 .map_err(|_| create_tool_not_found_error(tool, Some(version)))?;
733 let output_str = String::from_utf8_lossy(&output.stdout);
734
735 if !output_str.contains(version) {
736 return Err(create_tool_not_found_error(tool, Some(version)));
737 }
738 } else {
739 Command::new(req)
741 .output()
742 .map_err(|_| create_tool_not_found_error(req, None))?;
743 }
744 }
745
746 if let Some(tc) = toolchain {
747 let output = Command::new("rustup")
748 .arg("toolchain")
749 .arg("list")
750 .output()
751 .map_err(|_| create_tool_not_found_error("rustup", None))?;
752 let output_str = String::from_utf8_lossy(&output.stdout);
753
754 if !output_str.contains(tc) {
755 return Err(create_toolchain_not_found_error(tc));
756 }
757 }
758
759 Ok(())
760}