1use std::ffi::{OsStr, OsString};
2use std::{
3 borrow::Cow, env, io::Read, path::PathBuf, process::Command, process::ExitStatus,
4 process::Stdio, sync::LazyLock, sync::OnceLock, time::Duration,
5};
6
7use anyhow::{bail, Context as _, Result};
8use indicatif::{ProgressBar, ProgressStyle};
9use log::LevelFilter;
10
11use crate::{config::Config, git, platform, util};
12
13#[cfg(windows)]
16const DEFAULT_SHELL: &str = "C:\\Program Files\\Git\\bin\\bash.EXE";
17
18#[cfg(not(windows))]
20const DEFAULT_SHELL: &str = "/bin/sh";
21
22pub static SHELL: LazyLock<OsString> =
25 LazyLock::new(|| (env::var_os("SHELL").unwrap_or_else(|| DEFAULT_SHELL.into())));
26
27static VERBOSITY: OnceLock<LevelFilter> = OnceLock::new();
28static CONFIG: OnceLock<Config> = OnceLock::new();
29static PATH: OnceLock<String> = OnceLock::new();
30
31pub fn verbosity() -> &'static LevelFilter {
32 VERBOSITY.get().expect("verbosity is not initialized")
33}
34
35pub fn config() -> &'static Config {
36 CONFIG.get().expect("config 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 = util::get_version()?;
49
50 let channel = util::get_channel();
51
52 if channel == "release" {
53 let head = util::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!("On latest release channel and tag {tag:?} is different from Cargo.toml {version:?}. Aborting");
61 }
62
63 } else if channel == "custom" && !version.contains("custom") {
65 let sha = git::get_git_sha()?;
66
67 version = format!("{version}.custom.{sha}");
70 }
71
72 Ok(version)
73}
74
75pub trait CommandExt {
77 fn script(script: &str) -> Self;
78 fn in_repo(&mut self) -> &mut Self;
79 fn check_output(&mut self) -> Result<String>;
80 fn check_run(&mut self) -> Result<()>;
81 fn run(&mut self) -> Result<ExitStatus>;
82 fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()>;
83 fn pre_exec(&self);
84 fn features(&mut self, features: &[String]) -> &mut Self;
85}
86
87impl CommandExt for Command {
88 fn script(script: &str) -> Self {
90 let path: PathBuf = [path(), "scripts", script].into_iter().collect();
91 if cfg!(windows) {
92 let mut command = Command::new(&*SHELL);
94 command.arg(path);
95 command
96 } else {
97 Command::new(path)
99 }
100 }
101
102 fn in_repo(&mut self) -> &mut Self {
104 self.current_dir(path())
105 }
106
107 fn check_output(&mut self) -> Result<String> {
109 self.pre_exec();
111 self.stdout(Stdio::piped());
112
113 let mut child = self.spawn()?;
115
116 let mut buffer = Vec::new();
118 child.stdout.take().unwrap().read_to_end(&mut buffer)?;
119
120 let status = child.wait()?;
122 if !status.success() {
126 let stdout = String::from_utf8_lossy(&buffer);
127 bail!(
128 "Command: {:?}\nfailed with exit code: {}\n\noutput:\n{}",
129 self,
130 status.code().unwrap(),
131 stdout
132 );
133 }
134
135 Ok(String::from_utf8(buffer)?)
137 }
138
139 fn run(&mut self) -> Result<ExitStatus> {
141 self.pre_exec();
142 self.status().map_err(Into::into)
143 }
144
145 fn check_run(&mut self) -> Result<()> {
146 let status = self.run()?;
147 if status.success() {
148 Ok(())
149 } else {
150 let exit = status.code().unwrap();
151 bail!("command: {self:?}\n failed with exit code: {exit}")
152 }
153 }
154
155 fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()> {
158 self.pre_exec();
159
160 let progress_bar = get_progress_bar()?;
161 progress_bar.set_message(message);
162
163 let result = self.output();
164 progress_bar.finish_and_clear();
165
166 let Ok(output) = result else {
167 bail!("could not run command")
168 };
169
170 if output.status.success() {
171 Ok(())
172 } else {
173 bail!(
174 "{}\nfailed with exit code: {}",
175 String::from_utf8(output.stdout)?,
176 output.status.code().unwrap()
177 )
178 }
179 }
180
181 fn pre_exec(&self) {
183 debug!("Executing: {self:?}");
184 if let Some(cwd) = self.get_current_dir() {
185 debug!(" in working directory {cwd:?}");
186 }
187 for (key, value) in self.get_envs() {
188 let key = key.to_string_lossy();
189 if let Some(value) = value {
190 debug!(" ${key}={:?}", value.to_string_lossy());
191 } else {
192 debug!(" unset ${key}");
193 }
194 }
195 }
196
197 fn features(&mut self, features: &[String]) -> &mut Self {
198 self.arg("--no-default-features");
199 self.arg("--features");
200 if features.is_empty() {
201 self.arg(platform::default_features());
202 } else {
203 self.arg(features.join(","));
204 }
205 self
206 }
207}
208
209pub fn exec<T: AsRef<OsStr>>(
212 program: &str,
213 args: impl IntoIterator<Item = T>,
214 in_repo: bool,
215) -> Result<()> {
216 let mut command = match program.strip_prefix("scripts/") {
217 Some(script) => Command::script(script),
218 None => Command::new(program),
219 };
220 command.args(args);
221 if in_repo {
222 command.in_repo();
223 }
224 command.check_run()
225}
226
227fn get_progress_bar() -> Result<ProgressBar> {
228 let progress_bar = ProgressBar::new_spinner();
229 progress_bar.enable_steady_tick(Duration::from_millis(125));
230 progress_bar.set_style(
231 ProgressStyle::with_template("{spinner} {msg:.magenta.bold}")?
232 .tick_strings(&["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]),
234 );
235
236 Ok(progress_bar)
237}
238
239pub fn set_global_verbosity(verbosity: LevelFilter) {
240 VERBOSITY.set(verbosity).expect("could not set verbosity");
241}
242
243pub fn set_global_config(config: Config) {
244 CONFIG.set(config).expect("could not set config");
245}
246
247pub fn set_global_path(path: String) {
248 PATH.set(path).expect("could not set path");
249}