use std::ffi::OsStr;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use color_eyre::eyre::Result;
use rust_i18n::t;
use tracing::{debug, error};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::reload::{Handle, Layer};
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, registry};
use tracing_subscriber::{Registry, fmt};
use crate::command::CommandExt;
use crate::config::DEFAULT_LOG_LEVEL;
use crate::error::SkipStep;
use crate::execution_context::ExecutionContext;
use crate::executor::Executor;
pub trait PathExt
where
Self: Sized,
{
fn if_exists(self) -> Option<Self>;
fn is_descendant_of(&self, ancestor: &Path) -> bool;
fn require(self) -> Result<Self>;
}
impl<T> PathExt for T
where
T: AsRef<Path>,
{
fn if_exists(self) -> Option<Self> {
if self.as_ref().exists() {
debug!("Path {:?} exists", self.as_ref());
Some(self)
} else {
debug!("Path {:?} doesn't exist", self.as_ref());
None
}
}
fn is_descendant_of(&self, ancestor: &Path) -> bool {
self.as_ref().iter().zip(ancestor.iter()).all(|(a, b)| a == b)
}
fn require(self) -> Result<Self> {
if self.as_ref().exists() {
debug!("Path {:?} exists", self.as_ref());
Ok(self)
} else {
Err(SkipStep(format!(
"{}",
t!("Path {path} doesn't exist", path = format!("{:?}", self.as_ref()))
))
.into())
}
}
}
pub fn which<T: AsRef<OsStr> + Debug>(binary_name: T) -> Option<PathBuf> {
match which_crate::which(&binary_name) {
Ok(path) => {
debug!("Detected {:?} as {:?}", &path, &binary_name);
Some(path)
}
Err(e) => {
match e {
which_crate::Error::CannotFindBinaryPath => {
debug!("Cannot find {:?}", &binary_name);
}
_ => {
error!("Detecting {:?} failed: {}", &binary_name, e);
}
}
None
}
}
}
pub fn require<T: AsRef<OsStr> + Debug>(binary_name: T) -> Result<PathBuf> {
match which_crate::which(&binary_name) {
Ok(path) => {
debug!("Detected {:?} as {:?}", &path, &binary_name);
Ok(path)
}
Err(e) => match e {
which_crate::Error::CannotFindBinaryPath => Err(SkipStep(format!(
"{}",
t!(
"Cannot find {binary_name} in PATH",
binary_name = format!("{:?}", &binary_name)
)
))
.into()),
_ => {
panic!("Detecting {:?} failed: {}", &binary_name, e);
}
},
}
}
#[allow(unused)]
pub fn require_flatpak(ctx: &ExecutionContext, name: &str) -> Result<Executor> {
let flatpak = require("flatpak")?;
let result = ctx.execute(&flatpak).always().args(["info", name]).output_checked();
match result {
Ok(_) => {
debug!("Flatpak {name:?} is installed");
let mut cmd = ctx.execute(&flatpak);
cmd.args(["run", name]);
Ok(cmd)
}
_ => Err(SkipStep(t!("Flatpak {name} is not installed", name = name).to_string()).into()),
}
}
pub fn require_one<T: AsRef<OsStr> + Debug>(binary_names: impl IntoIterator<Item = T>) -> Result<PathBuf> {
let mut failed_bins = Vec::new();
for bin in binary_names {
match require(&bin) {
Ok(path) => return Ok(path),
Err(_) => failed_bins.push(bin),
}
}
Err(SkipStep(format!(
"{}",
t!(
"Cannot find any of {binary_names} in PATH",
binary_names = failed_bins
.iter()
.map(|bin| format!("{:?}", bin))
.collect::<Vec<_>>()
.join(", ")
)
))
.into())
}
#[allow(dead_code)]
pub fn require_option<T>(option: Option<T>, cause: String) -> Result<T> {
if let Some(value) = option {
Ok(value)
} else {
Err(SkipStep(cause).into())
}
}
pub fn string_prepend_str(string: &mut String, s: &str) {
let mut new_string = String::with_capacity(string.len() + s.len());
new_string.push_str(s);
new_string.push_str(string);
*string = new_string;
}
#[cfg(unix)]
pub fn hostname() -> Result<String> {
match nix::unistd::gethostname() {
Ok(os_str) => Ok(os_str
.into_string()
.map_err(|_| SkipStep(t!("Failed to get a UTF-8 encoded hostname").into()))?),
Err(e) => Err(e.into()),
}
}
#[cfg(windows)]
pub fn hostname() -> Result<String> {
std::env::var("COMPUTERNAME")
.map_err(|err| SkipStep(t!("Failed to get hostname: {err}", err = err).to_string()).into())
}
#[cfg(unix)]
pub fn is_elevated() -> bool {
let euid = nix::unistd::Uid::effective();
debug!("Running with euid: {euid}");
euid.is_root()
}
#[cfg(windows)]
pub fn is_elevated() -> bool {
let elevated = is_elevated::is_elevated();
if elevated {
debug!("Detected elevated process");
}
elevated
}
pub mod merge_strategies {
use merge::Merge;
use crate::config::Commands;
pub fn vec_prepend_opt<T>(left: &mut Option<Vec<T>>, right: Option<Vec<T>>) {
if let Some(left_vec) = left {
if let Some(mut right_vec) = right {
right_vec.append(left_vec);
let _ = left.replace(right_vec);
}
} else {
*left = right;
}
}
pub fn string_append_opt(left: &mut Option<String>, right: Option<String>) {
if let Some(left_str) = left {
if let Some(right_str) = right {
left_str.push(' ');
left_str.push_str(&right_str);
}
} else {
*left = right;
}
}
pub fn inner_merge_opt<T>(left: &mut Option<T>, right: Option<T>)
where
T: Merge,
{
if let Some(left_inner) = left {
if let Some(right_inner) = right {
left_inner.merge(right_inner);
}
} else {
*left = right;
}
}
pub fn commands_merge_opt(left: &mut Option<Commands>, right: Option<Commands>) {
if let Some(left_inner) = left {
if let Some(right_inner) = right {
left_inner.extend(right_inner);
}
} else {
*left = right;
}
}
}
pub fn check_is_python_2_or_shim(ctx: &ExecutionContext, python: PathBuf) -> Result<PathBuf> {
let output = ctx.execute(&python).always().arg("-V").output_checked_utf8()?;
let stdout = output.stdout;
let mut split = stdout.split_whitespace();
if let Some(version) = split.nth(1) {
let major_version = version
.split('.')
.next()
.expect("Should have a major version number")
.parse::<u32>()
.expect("Major version should be a valid number");
if major_version == 2 {
return Err(SkipStep(t!("{python} is a Python 2, skip.", python = python.display()).to_string()).into());
}
} else {
return Err(SkipStep(t!("{python} is a Python shim, skip.", python = python.display()).to_string()).into());
}
Ok(python)
}
pub fn install_tracing(filter_directives: &str) -> Result<Handle<EnvFilter, Registry>> {
let env_filter = EnvFilter::try_new(filter_directives)
.or_else(|_| EnvFilter::try_from_default_env())
.or_else(|_| EnvFilter::try_new(DEFAULT_LOG_LEVEL))?;
let fmt_layer = fmt::layer().with_target(false).without_time();
let (filter, reload_handle) = Layer::new(env_filter);
registry().with(filter).with(fmt_layer).init();
Ok(reload_handle)
}
pub fn update_tracing(reload_handle: &Handle<EnvFilter, Registry>, filter_directives: &str) -> Result<()> {
let new = EnvFilter::try_new(filter_directives)
.or_else(|_| EnvFilter::try_from_default_env())
.or_else(|_| EnvFilter::try_new(DEFAULT_LOG_LEVEL))?;
reload_handle.modify(|old| *old = new)?;
Ok(())
}
pub fn install_color_eyre() -> Result<()> {
color_eyre::config::HookBuilder::new()
.display_env_section(false)
.display_location_section(true)
.install()
}
#[macro_export]
macro_rules! output_changed_message {
($command:expr, $message:expr) => {
format!(
"The output of `{}` changed: {}. This is not your fault, this is an issue in Topgrade. Please open an issue at: https://github.com/topgrade-rs/topgrade/issues/new?template=bug_report.md",
$command,
$message,
)
};
}