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::{config::Config, git, platform, util};
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 CONFIG: OnceLock<Config> = OnceLock::new();
34static PATH: OnceLock<String> = OnceLock::new();
35
36pub fn verbosity() -> &'static LevelFilter {
37 VERBOSITY.get().expect("verbosity is not initialized")
38}
39
40pub fn config() -> &'static Config {
41 CONFIG.get().expect("config is not initialized")
42}
43
44pub fn path() -> &'static String {
45 PATH.get().expect("path is not initialized")
46}
47
48pub fn set_repo_dir() -> Result<()> {
49 env::set_current_dir(path()).context("Could not change directory")
50}
51
52pub fn version() -> Result<String> {
53 let mut version = util::get_version()?;
54
55 let channel = util::get_channel();
56
57 if channel == "release" {
58 let head = util::git_head()?;
59 if !head.status.success() {
60 let error = String::from_utf8_lossy(&head.stderr);
61 bail!("Error running `git describe`:\n{error}");
62 }
63 let tag = String::from_utf8_lossy(&head.stdout).trim().to_string();
64 if tag != format!("v{version}") {
65 bail!(
66 "On latest release channel and tag {tag:?} is different from Cargo.toml {version:?}. Aborting"
67 );
68 }
69
70 } else if channel == "custom" && !version.contains("custom") {
72 let sha = git::get_git_sha()?;
73
74 version = format!("{version}.custom.{sha}");
77 }
78
79 Ok(version)
80}
81
82pub trait CommandExt {
84 fn script(script: &str) -> Self;
85 fn in_repo(&mut self) -> &mut Self;
86 fn check_output(&mut self) -> Result<String>;
87 fn check_run(&mut self) -> Result<()>;
88 fn run(&mut self) -> Result<ExitStatus>;
89 fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()>;
90 fn pre_exec(&self);
91 fn features(&mut self, features: &[String]) -> &mut Self;
92}
93
94impl CommandExt for Command {
95 fn script(script: &str) -> Self {
97 let path: PathBuf = [path(), "scripts", script].into_iter().collect();
98 if cfg!(windows) {
99 let mut command = Command::new(&*SHELL);
101 command.arg(path);
102 command
103 } else {
104 Command::new(path)
106 }
107 }
108
109 fn in_repo(&mut self) -> &mut Self {
111 self.current_dir(path())
112 }
113
114 fn check_output(&mut self) -> Result<String> {
116 self.pre_exec();
117
118 let output = self.output()?;
119
120 if output.status.success() {
121 Ok(String::from_utf8(output.stdout)?)
123 } else {
124 bail!(
125 "{}",
126 format_command_error(&output, Some(&format!("Command: {self:?}")))
127 )
128 }
129 }
130
131 fn run(&mut self) -> Result<ExitStatus> {
133 self.pre_exec();
134 self.status().map_err(Into::into)
135 }
136
137 fn check_run(&mut self) -> Result<()> {
138 let status = self.run()?;
139 if status.success() {
140 Ok(())
141 } else {
142 let exit = status.code().unwrap();
143 bail!("command: {self:?}\n failed with exit code: {exit}")
144 }
145 }
146
147 fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()> {
150 self.pre_exec();
151
152 let progress_bar = get_progress_bar()?;
153 progress_bar.set_message(message);
154
155 let result = self.output();
156 progress_bar.finish_and_clear();
157
158 let Ok(output) = result else {
159 bail!("could not run command")
160 };
161
162 if output.status.success() {
163 Ok(())
164 } else {
165 bail!("{}", format_command_error(&output, None))
166 }
167 }
168
169 fn pre_exec(&self) {
171 debug!("Executing: {self:?}");
172 if let Some(cwd) = self.get_current_dir() {
173 debug!(" in working directory {cwd:?}");
174 }
175 for (key, value) in self.get_envs() {
176 let key = key.to_string_lossy();
177 if let Some(value) = value {
178 debug!(" ${key}={:?}", value.to_string_lossy());
179 } else {
180 debug!(" unset ${key}");
181 }
182 }
183 }
184
185 fn features(&mut self, features: &[String]) -> &mut Self {
186 self.arg("--no-default-features");
187 self.arg("--features");
188 if features.is_empty() {
189 self.arg(platform::default_features());
190 } else {
191 self.arg(features.join(","));
192 }
193 self
194 }
195}
196
197fn format_command_error(
199 output: &std::process::Output,
200 command_description: Option<&str>,
201) -> String {
202 let mut error_msg = String::new();
203
204 if !output.stdout.is_empty() {
205 error_msg.push_str(&String::from_utf8_lossy(&output.stdout));
206 error_msg.push('\n');
207 }
208
209 if !output.stderr.is_empty() {
210 error_msg.push_str(&String::from_utf8_lossy(&output.stderr));
211 error_msg.push('\n');
212 }
213
214 if let Some(description) = command_description {
215 let _ = writeln!(error_msg, "{description}");
216 }
217
218 let _ = write!(
219 error_msg,
220 "failed with exit code: {}",
221 output.status.code().unwrap()
222 );
223
224 error_msg
225}
226
227pub fn exec<T: AsRef<OsStr>>(
230 program: &str,
231 args: impl IntoIterator<Item = T>,
232 in_repo: bool,
233) -> Result<()> {
234 let mut command = match program.strip_prefix("scripts/") {
235 Some(script) => Command::script(script),
236 None => Command::new(program),
237 };
238 command.args(args);
239 if in_repo {
240 command.in_repo();
241 }
242 command.check_run()
243}
244
245fn get_progress_bar() -> Result<ProgressBar> {
246 let progress_bar = ProgressBar::new_spinner();
247 progress_bar.enable_steady_tick(Duration::from_millis(125));
248 progress_bar.set_style(
249 ProgressStyle::with_template("{spinner} {msg:.magenta.bold}")?
250 .tick_strings(&["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]),
252 );
253
254 Ok(progress_bar)
255}
256
257pub fn set_global_verbosity(verbosity: LevelFilter) {
258 VERBOSITY.set(verbosity).expect("could not set verbosity");
259}
260
261pub fn set_global_config(config: Config) {
262 CONFIG.set(config).expect("could not set config");
263}
264
265pub fn set_global_path(path: String) {
266 PATH.set(path).expect("could not set path");
267}