1use std::{collections::HashMap, env, process::{Command, Stdio}, sync::{Arc, Mutex}, time::{Duration, Instant}};
4use serde::Deserialize;
5use emoji::symbols;
6use colored::*;
7
8#[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#[derive(Deserialize)]
36pub struct Scripts {
37 pub global_env: Option<HashMap<String, String>>,
38 pub scripts: HashMap<String, Script>
39}
40
41pub 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
193fn 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
216fn 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
300fn 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 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}