1use std::{
2 borrow::Cow,
3 env,
4 ffi::{OsStr, OsString},
5 fmt::Write as _,
6 path::PathBuf,
7 process::{Command, ExitStatus},
8 sync::{LazyLock, OnceLock},
9 time::Duration,
10};
11
12use anyhow::{Context as _, Result, bail};
13use indicatif::{ProgressBar, ProgressStyle};
14use log::LevelFilter;
15
16use crate::utils::{self, platform};
17
18#[cfg(windows)]
21const DEFAULT_SHELL: &str = "C:\\Program Files\\Git\\bin\\bash.EXE";
22
23#[cfg(not(windows))]
25const DEFAULT_SHELL: &str = "/bin/sh";
26
27pub static SHELL: LazyLock<OsString> =
30 LazyLock::new(|| env::var_os("SHELL").unwrap_or_else(|| DEFAULT_SHELL.into()));
31
32static VERBOSITY: OnceLock<LevelFilter> = OnceLock::new();
33static PATH: OnceLock<String> = OnceLock::new();
34
35pub fn verbosity() -> &'static LevelFilter {
36 VERBOSITY.get().expect("verbosity is not initialized")
37}
38
39pub fn path() -> &'static String {
40 PATH.get().expect("path is not initialized")
41}
42
43pub fn set_repo_dir() -> Result<()> {
44 env::set_current_dir(path()).context("Could not change directory")
45}
46
47pub fn version() -> Result<String> {
48 let mut version = utils::cargo::get_version()?;
49
50 let channel = utils::git::get_channel();
51
52 if channel == "release" {
53 let head = utils::git::git_head()?;
54 if !head.status.success() {
55 let error = String::from_utf8_lossy(&head.stderr);
56 bail!("Error running `git describe`:\n{error}");
57 }
58 let tag = String::from_utf8_lossy(&head.stdout).trim().to_string();
59 if tag != format!("v{version}") {
60 bail!(
61 "On latest release channel and tag {tag:?} is different from Cargo.toml {version:?}. Aborting"
62 );
63 }
64
65 } else if channel == "custom" && !version.contains("custom") {
67 let sha = utils::git::get_git_sha()?;
68
69 version = format!("{version}.custom.{sha}");
72 }
73
74 Ok(version)
75}
76
77pub trait CommandExt {
79 fn script(script: &str) -> Self;
80 fn in_repo(&mut self) -> &mut Self;
81 fn check_output(&mut self) -> Result<String>;
82 fn check_run(&mut self) -> Result<()>;
83 fn run(&mut self) -> Result<ExitStatus>;
84 fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()>;
85 fn pre_exec(&self);
86 fn features(&mut self, features: &[String]) -> &mut Self;
87}
88
89impl CommandExt for Command {
90 fn script(script: &str) -> Self {
92 let path: PathBuf = [path(), "scripts", script].into_iter().collect();
93 if cfg!(windows) {
94 let mut command = Command::new(&*SHELL);
96 command.arg(path);
97 command
98 } else {
99 Command::new(path)
101 }
102 }
103
104 fn in_repo(&mut self) -> &mut Self {
106 self.current_dir(path())
107 }
108
109 fn check_output(&mut self) -> Result<String> {
111 self.pre_exec();
112
113 let output = self.output()?;
114
115 if output.status.success() {
116 Ok(String::from_utf8(output.stdout)?)
118 } else {
119 bail!(
120 "{}",
121 format_command_error(&output, Some(&format!("Command: {self:?}")))
122 )
123 }
124 }
125
126 fn run(&mut self) -> Result<ExitStatus> {
128 self.pre_exec();
129 self.status().map_err(Into::into)
130 }
131
132 fn check_run(&mut self) -> Result<()> {
133 let status = self.run()?;
134 if status.success() {
135 Ok(())
136 } else {
137 let exit = status.code().unwrap();
138 bail!("command: {self:?}\n failed with exit code: {exit}")
139 }
140 }
141
142 fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()> {
145 self.pre_exec();
146
147 let progress_bar = get_progress_bar()?;
148 progress_bar.set_message(message);
149
150 let result = self.output();
151 progress_bar.finish_and_clear();
152
153 let Ok(output) = result else {
154 bail!("could not run command")
155 };
156
157 if output.status.success() {
158 Ok(())
159 } else {
160 bail!("{}", format_command_error(&output, None))
161 }
162 }
163
164 fn pre_exec(&self) {
166 debug!("Executing: {self:?}");
167 if let Some(cwd) = self.get_current_dir() {
168 debug!(" in working directory {cwd:?}");
169 }
170 for (key, value) in self.get_envs() {
171 let key = key.to_string_lossy();
172 if let Some(value) = value {
173 debug!(" ${key}={:?}", value.to_string_lossy());
174 } else {
175 debug!(" unset ${key}");
176 }
177 }
178 }
179
180 fn features(&mut self, features: &[String]) -> &mut Self {
181 self.arg("--no-default-features");
182 self.arg("--features");
183 if features.is_empty() {
184 self.arg(platform::default_features());
185 } else {
186 self.arg(features.join(","));
187 }
188 self
189 }
190}
191
192fn format_command_error(
194 output: &std::process::Output,
195 command_description: Option<&str>,
196) -> String {
197 let mut error_msg = String::new();
198
199 if !output.stdout.is_empty() {
200 error_msg.push_str(&String::from_utf8_lossy(&output.stdout));
201 error_msg.push('\n');
202 }
203
204 if !output.stderr.is_empty() {
205 error_msg.push_str(&String::from_utf8_lossy(&output.stderr));
206 error_msg.push('\n');
207 }
208
209 if let Some(description) = command_description {
210 let _ = writeln!(error_msg, "{description}");
211 }
212
213 let _ = write!(
214 error_msg,
215 "failed with exit code: {}",
216 output.status.code().unwrap()
217 );
218
219 error_msg
220}
221
222pub fn exec<T: AsRef<OsStr>>(
225 program: &str,
226 args: impl IntoIterator<Item = T>,
227 in_repo: bool,
228) -> Result<()> {
229 let mut command = match program.strip_prefix("scripts/") {
230 Some(script) => Command::script(script),
231 None => Command::new(program),
232 };
233 command.args(args);
234 if in_repo {
235 command.in_repo();
236 }
237 command.check_run()
238}
239
240fn get_progress_bar() -> Result<ProgressBar> {
241 let progress_bar = ProgressBar::new_spinner();
242 progress_bar.enable_steady_tick(Duration::from_millis(125));
243 progress_bar.set_style(
244 ProgressStyle::with_template("{spinner} {msg:.magenta.bold}")?
245 .tick_strings(&["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]),
247 );
248
249 Ok(progress_bar)
250}
251
252pub fn set_global_verbosity(verbosity: LevelFilter) {
253 VERBOSITY.set(verbosity).expect("could not set verbosity");
254}
255
256pub fn set_global_path(path: String) {
257 PATH.set(path).expect("could not set path");
258}