use std::{
borrow::Cow,
env,
ffi::{OsStr, OsString},
fmt::Write as _,
path::PathBuf,
process::{Command, ExitStatus},
sync::{LazyLock, OnceLock},
time::Duration,
};
use anyhow::{Context as _, Result, bail};
use indicatif::{ProgressBar, ProgressStyle};
use log::LevelFilter;
use crate::utils::{self, platform};
#[cfg(windows)]
const DEFAULT_SHELL: &str = "C:\\Program Files\\Git\\bin\\bash.EXE";
#[cfg(not(windows))]
const DEFAULT_SHELL: &str = "/bin/sh";
pub static SHELL: LazyLock<OsString> =
LazyLock::new(|| env::var_os("SHELL").unwrap_or_else(|| DEFAULT_SHELL.into()));
static VERBOSITY: OnceLock<LevelFilter> = OnceLock::new();
static PATH: OnceLock<String> = OnceLock::new();
pub fn verbosity() -> &'static LevelFilter {
VERBOSITY.get().expect("verbosity is not initialized")
}
pub fn path() -> &'static String {
PATH.get().expect("path is not initialized")
}
pub fn set_repo_dir() -> Result<()> {
env::set_current_dir(path()).context("Could not change directory")
}
pub fn version() -> Result<String> {
let mut version = utils::cargo::get_version()?;
let channel = utils::git::get_channel();
if channel == "release" {
let head = utils::git::git_head()?;
if !head.status.success() {
let error = String::from_utf8_lossy(&head.stderr);
bail!("Error running `git describe`:\n{error}");
}
let tag = String::from_utf8_lossy(&head.stdout).trim().to_string();
if tag != format!("v{version}") {
bail!(
"On latest release channel and tag {tag:?} is different from Cargo.toml {version:?}. Aborting"
);
}
} else if channel == "custom" && !version.contains("custom") {
let sha = utils::git::get_git_sha()?;
version = format!("{version}.custom.{sha}");
}
Ok(version)
}
pub trait CommandExt {
fn script(script: &str) -> Self;
fn in_repo(&mut self) -> &mut Self;
fn check_output(&mut self) -> Result<String>;
fn check_run(&mut self) -> Result<()>;
fn run(&mut self) -> Result<ExitStatus>;
fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()>;
fn pre_exec(&self);
fn features(&mut self, features: &[String]) -> &mut Self;
}
impl CommandExt for Command {
fn script(script: &str) -> Self {
let path: PathBuf = [path(), "scripts", script].into_iter().collect();
if cfg!(windows) {
let mut command = Command::new(&*SHELL);
command.arg(path);
command
} else {
Command::new(path)
}
}
fn in_repo(&mut self) -> &mut Self {
self.current_dir(path())
}
fn check_output(&mut self) -> Result<String> {
self.pre_exec();
let output = self.output()?;
if output.status.success() {
Ok(String::from_utf8(output.stdout)?)
} else {
bail!(
"{}",
format_command_error(&output, Some(&format!("Command: {self:?}")))
)
}
}
fn run(&mut self) -> Result<ExitStatus> {
self.pre_exec();
self.status().map_err(Into::into)
}
fn check_run(&mut self) -> Result<()> {
let status = self.run()?;
if status.success() {
Ok(())
} else {
let exit = status.code().unwrap();
bail!("command: {self:?}\n failed with exit code: {exit}")
}
}
fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()> {
self.pre_exec();
let progress_bar = get_progress_bar()?;
progress_bar.set_message(message);
let result = self.output();
progress_bar.finish_and_clear();
let Ok(output) = result else {
bail!("could not run command")
};
if output.status.success() {
Ok(())
} else {
bail!("{}", format_command_error(&output, None))
}
}
fn pre_exec(&self) {
debug!("Executing: {self:?}");
if let Some(cwd) = self.get_current_dir() {
debug!(" in working directory {cwd:?}");
}
for (key, value) in self.get_envs() {
let key = key.to_string_lossy();
if let Some(value) = value {
debug!(" ${key}={:?}", value.to_string_lossy());
} else {
debug!(" unset ${key}");
}
}
}
fn features(&mut self, features: &[String]) -> &mut Self {
self.arg("--no-default-features");
self.arg("--features");
if features.is_empty() {
self.arg(platform::default_features());
} else {
self.arg(features.join(","));
}
self
}
}
fn format_command_error(
output: &std::process::Output,
command_description: Option<&str>,
) -> String {
let mut error_msg = String::new();
if !output.stdout.is_empty() {
error_msg.push_str(&String::from_utf8_lossy(&output.stdout));
error_msg.push('\n');
}
if !output.stderr.is_empty() {
error_msg.push_str(&String::from_utf8_lossy(&output.stderr));
error_msg.push('\n');
}
if let Some(description) = command_description {
let _ = writeln!(error_msg, "{description}");
}
let _ = write!(
error_msg,
"failed with exit code: {}",
output.status.code().unwrap()
);
error_msg
}
pub fn exec<T: AsRef<OsStr>>(
program: &str,
args: impl IntoIterator<Item = T>,
in_repo: bool,
) -> Result<()> {
let mut command = match program.strip_prefix("scripts/") {
Some(script) => Command::script(script),
None => Command::new(program),
};
command.args(args);
if in_repo {
command.in_repo();
}
command.check_run()
}
fn get_progress_bar() -> Result<ProgressBar> {
let progress_bar = ProgressBar::new_spinner();
progress_bar.enable_steady_tick(Duration::from_millis(125));
progress_bar.set_style(
ProgressStyle::with_template("{spinner} {msg:.magenta.bold}")?
.tick_strings(&["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]),
);
Ok(progress_bar)
}
pub fn set_global_verbosity(verbosity: LevelFilter) {
VERBOSITY.set(verbosity).expect("could not set verbosity");
}
pub fn set_global_path(path: String) {
PATH.set(path).expect("could not set path");
}