cargo_run/commands/
script.rs

1//! This module provides the functionality to run scripts defined in `Scripts.toml`.
2
3use std::{collections::HashMap, env, process::{Command, Stdio}, sync::{Arc, Mutex}, time::{Duration, Instant}};
4use serde::Deserialize;
5use emoji::symbols;
6use colored::*;
7
8/// Enum representing a script, which can be either a default command or a detailed script with additional metadata.
9#[derive(Deserialize, Debug)]
10#[serde(untagged)]
11pub enum Script {
12    Default(String),
13    Inline {
14        command: Option<String>,
15        requires: Option<Vec<String>>,
16        toolchain: Option<String>,
17        info: Option<String>,
18        env: Option<HashMap<String, String>>,
19        include: Option<Vec<String>>,
20        interpreter: Option<String>,
21    },
22    CILike {
23        script: String,
24        command: Option<String>,
25        requires: Option<Vec<String>>,
26        toolchain: Option<String>,
27        info: Option<String>,
28        env: Option<HashMap<String, String>>,
29        include: Option<Vec<String>>,
30        interpreter: Option<String>,
31    }
32}
33
34/// Struct representing the collection of scripts defined in Scripts.toml.
35#[derive(Deserialize)]
36pub struct Scripts {
37    pub global_env: Option<HashMap<String, String>>,
38    pub scripts: HashMap<String, Script>
39}
40
41/// Run a script by name, executing any included scripts in sequence.
42///
43/// This function runs a script and any scripts it includes, measuring the execution time
44/// for each script and printing performance metrics.
45///
46/// # Arguments
47///
48/// * `scripts` - A reference to the collection of scripts.
49/// * `script_name` - The name of the script to run.
50/// * `env_overrides` - A vector of command line environment variable overrides.
51///
52/// # Panics
53///
54/// This function will panic if it fails to execute the script commands.
55pub fn run_script(scripts: &Scripts, script_name: &str, env_overrides: Vec<String>) {
56    let script_durations = Arc::new(Mutex::new(HashMap::new()));
57
58    fn run_script_with_level(
59        scripts: &Scripts,
60        script_name: &str,
61        env_overrides: Vec<String>,
62        level: usize,
63        script_durations: Arc<Mutex<HashMap<String, Duration>>>,
64    ) {
65        let mut env_vars = scripts.global_env.clone().unwrap_or_default();
66        let indent = "  ".repeat(level);
67
68        let script_start_time = Instant::now();
69
70        if let Some(script) = scripts.scripts.get(script_name) {
71            match script {
72                Script::Default(cmd) => {
73                    let msg = format!(
74                        "{}{}  {}: [ {} ]",
75                        indent,
76                        symbols::other_symbol::CHECK_MARK.glyph,
77                        "Running script".green(),
78                        script_name
79                    );
80                    println!("{}\n", msg);
81                    apply_env_vars(&env_vars, &env_overrides);
82                    execute_command(None, cmd, None);
83                }
84                Script::Inline {
85                    command,
86                    info,
87                    env,
88                    include,
89                    interpreter,
90                    requires,
91                    toolchain,
92                    ..
93                } | Script::CILike {
94                    command,
95                    info,
96                    env,
97                    include,
98                    interpreter,
99                    requires,
100                    toolchain,
101                    ..
102                } => {
103                    if let Err(e) = check_requirements(requires.as_deref().unwrap_or(&[]), toolchain.as_ref()) {
104                        eprintln!("{} {}: {}", symbols::other_symbol::CROSS_MARK.glyph, "Requirement check failed".red(), e);
105                        return;
106                    }
107
108                    let description = format!(
109                        "{}  {}: {}",
110                        emoji::objects::book_paper::BOOKMARK_TABS.glyph,
111                        "Description".green(),
112                        info.as_deref().unwrap_or("No description provided")
113                    );
114
115                    if let Some(include_scripts) = include {
116                        let msg = format!(
117                            "{}{}  {}: [ {} ]  {}",
118                            indent,
119                            symbols::other_symbol::CHECK_MARK.glyph,
120                            "Running include script".green(),
121                            script_name,
122                            description
123                        );
124                        println!("{}\n", msg);
125                        for include_script in include_scripts {
126                            run_script_with_level(
127                                scripts,
128                                include_script,
129                                env_overrides.clone(),
130                                level + 1,
131                                script_durations.clone(),
132                            );
133                        }
134                    }
135
136                    if let Some(cmd) = command {
137                        let msg = format!(
138                            "{}{}  {}: [ {} ]  {}",
139                            indent,
140                            symbols::other_symbol::CHECK_MARK.glyph,
141                            "Running script".green(),
142                            script_name,
143                            description
144                        );
145                        println!("{}\n", msg);
146
147                        if let Some(script_env) = env {
148                            env_vars.extend(script_env.clone());
149                        }
150                        apply_env_vars(&env_vars, &env_overrides);
151                        execute_command(interpreter.as_deref(), cmd, toolchain.as_deref());
152                    }
153                }
154            }
155
156            let script_duration = script_start_time.elapsed();
157            if level > 0 || scripts.scripts.get(script_name).map_or(false, |s| matches!(s, Script::Default(_) | Script::Inline { command: Some(_), .. } | Script::CILike { command: Some(_), .. })) {
158                script_durations
159                    .lock()
160                    .unwrap()
161                    .insert(script_name.to_string(), script_duration);
162            }
163        } else {
164            println!(
165                "{}{} {}: [ {} ]",
166                indent,
167                symbols::other_symbol::CROSS_MARK.glyph,
168                "Script not found".red(),
169                script_name
170            );
171        }
172    }
173
174    run_script_with_level(scripts, script_name, env_overrides, 0, script_durations.clone());
175
176    let durations = script_durations.lock().unwrap();
177    if !durations.is_empty() {
178        let total_duration: Duration = durations.values().cloned().sum();
179        
180        println!("\n");
181        println!("{}", "Scripts Performance".bold().yellow());
182        println!("{}", "-".repeat(80).yellow());
183        for (script, duration) in durations.iter() {
184            println!("āœ”ļø  Script: {:<25}  šŸ•’ Running time: {:.2?}", script.green(), duration);
185        }
186        if !durations.is_empty() {
187            println!("\nšŸ•’ Total running time: {:.2?}", total_duration);
188        }
189    }
190}
191
192
193/// Apply environment variables from global, script-specific, and command line overrides.
194///
195/// This function sets the environment variables for the script execution, giving precedence
196/// to command line overrides over script-specific variables, and script-specific variables over global variables.
197///
198/// # Arguments
199///
200/// * `env_vars` - A reference to the global environment variables.
201/// * `env_overrides` - A vector of command line environment variable overrides.
202fn apply_env_vars(env_vars: &HashMap<String, String>, env_overrides: &[String]) {
203    let mut final_env = env_vars.clone();
204
205    for override_str in env_overrides {
206        if let Some((key, value)) = override_str.split_once('=') {
207            final_env.insert(key.to_string(), value.to_string());
208        }
209    }
210
211    for (key, value) in &final_env {
212        env::set_var(key, value);
213    }
214}
215
216/// Execute a command using the specified interpreter, or the default shell if none is specified.
217///
218/// This function runs the command with the appropriate interpreter, depending on the operating system
219/// and the specified interpreter.
220///
221/// # Arguments
222///
223/// * `interpreter` - An optional string representing the interpreter to use.
224/// * `command` - The command to execute.
225/// * `toolchain` - An optional string representing the toolchain to use.
226///
227/// # Panics
228///
229/// This function will panic if it fails to execute the command.
230fn execute_command(interpreter: Option<&str>, command: &str, toolchain: Option<&str>) {
231    let mut cmd = if let Some(tc) = toolchain {
232        let mut command_with_toolchain = format!("cargo +{} ", tc);
233        command_with_toolchain.push_str(command);
234        Command::new("sh")
235            .arg("-c")
236            .arg(command_with_toolchain)
237            .stdout(Stdio::inherit())
238            .stderr(Stdio::inherit())
239            .spawn()
240            .expect("Failed to execute command")
241    } else {
242        match interpreter {
243            Some("bash") => Command::new("bash")
244                .arg("-c")
245                .arg(command)
246                .stdout(Stdio::inherit())
247                .stderr(Stdio::inherit())
248                .spawn()
249                .expect("Failed to execute script using bash"),
250            Some("zsh") => Command::new("zsh")
251                .arg("-c")
252                .arg(command)
253                .stdout(Stdio::inherit())
254                .stderr(Stdio::inherit())
255                .spawn()
256                .expect("Failed to execute script using zsh"),
257            Some("powershell") => Command::new("powershell")
258                .args(&["-Command", command])
259                .stdout(Stdio::inherit())
260                .stderr(Stdio::inherit())
261                .spawn()
262                .expect("Failed to execute script using PowerShell"),
263            Some("cmd") => Command::new("cmd")
264                .args(&["/C", command])
265                .stdout(Stdio::inherit())
266                .stderr(Stdio::inherit())
267                .spawn()
268                .expect("Failed to execute script using cmd"),
269            Some(other) => Command::new(other)
270                .arg("-c")
271                .arg(command)
272                .stdout(Stdio::inherit())
273                .stderr(Stdio::inherit())
274                .spawn()
275                .expect(&format!("Failed to execute script using {}", other)),
276            None => {
277                if cfg!(target_os = "windows") {
278                    Command::new("cmd")
279                        .args(&["/C", command])
280                        .stdout(Stdio::inherit())
281                        .stderr(Stdio::inherit())
282                        .spawn()
283                        .expect("Failed to execute script using cmd")
284                } else {
285                    Command::new("sh")
286                        .arg("-c")
287                        .arg(command)
288                        .stdout(Stdio::inherit())
289                        .stderr(Stdio::inherit())
290                        .spawn()
291                        .expect("Failed to execute script using sh")
292                }
293            }
294        }
295    };
296
297    cmd.wait().expect("Command wasn't running");
298}
299
300/// Check if the required tools and toolchain are installed.
301/// 
302/// This function checks if the required tools and toolchain are installed on the system.
303/// If any of the requirements are not met, an error message is returned.
304/// 
305/// # Arguments
306/// 
307/// * `requires` - A slice of strings representing the required tools.
308/// * `toolchain` - An optional string representing the required toolchain.
309/// 
310/// # Returns
311/// 
312/// An empty result if all requirements are met, otherwise an error message.
313/// 
314/// # Errors
315/// 
316/// This function will return an error message if any of the requirements are not met.
317fn check_requirements(requires: &[String], toolchain: Option<&String>) -> Result<(), String> {
318    for req in requires {
319        if let Some((tool, version)) = req.split_once(' ') {
320            let output = Command::new(tool)
321                .arg("--version")
322                .output()
323                .map_err(|e| format!("Failed to execute {}: {}", tool, e))?;
324            let output_str = String::from_utf8_lossy(&output.stdout);
325
326            if !output_str.contains(version) {
327                return Err(format!(
328                    "Required version for {} is {}, but found {}",
329                    tool, version, output_str
330                ));
331            }
332        } else {
333            // Just check if the tool is installed
334            Command::new(req)
335                .output()
336                .map_err(|e| format!("Failed to execute {}: {}", req, e))?;
337        }
338    }
339
340    if let Some(toolchain) = toolchain {
341        let output = Command::new("rustup")
342            .arg("toolchain")
343            .arg("list")
344            .output()
345            .map_err(|e| format!("Failed to execute rustup: {}", e))?;
346        let output_str = String::from_utf8_lossy(&output.stdout);
347
348        if !output_str.contains(toolchain) {
349            return Err(format!("Required toolchain {} is not installed", toolchain));
350        }
351    }
352
353    Ok(())
354}