2019-08-22 22:29:31 +03:00
use std ::env ;
2018-06-17 14:17:36 +03:00
use std ::ffi ::OsStr ;
2019-12-29 23:18:59 +02:00
use std ::fmt ::Debug ;
use std ::path ::{ Path , PathBuf } ;
2023-06-23 17:02:58 +08:00
use std ::process ::Command ;
2023-05-25 12:22:11 +02:00
use color_eyre ::eyre ::Result ;
2024-10-03 12:47:35 +02:00
use rust_i18n ::t ;
2023-10-17 11:19:47 +08:00
2022-11-16 13:43:57 -05:00
use tracing ::{ debug , error } ;
2023-10-17 11:19:47 +08:00
use tracing_subscriber ::layer ::SubscriberExt ;
use tracing_subscriber ::reload ::{ Handle , Layer } ;
use tracing_subscriber ::util ::SubscriberInitExt ;
use tracing_subscriber ::{ fmt , Registry } ;
use tracing_subscriber ::{ registry , EnvFilter } ;
2020-12-01 19:56:12 -08:00
2023-06-23 17:02:58 +08:00
use crate ::command ::CommandExt ;
2023-10-17 11:19:47 +08:00
use crate ::config ::DEFAULT_LOG_LEVEL ;
2023-05-25 12:22:11 +02:00
use crate ::error ::SkipStep ;
2018-07-07 09:18:53 +03:00
pub trait PathExt
where
Self : Sized ,
{
fn if_exists ( self ) -> Option < Self > ;
2018-08-22 10:43:32 +03:00
fn is_descendant_of ( & self , ancestor : & Path ) -> bool ;
2019-01-13 23:20:32 +02:00
/// Returns the path if it exists or ErrorKind::SkipStep otherwise
2019-12-11 23:05:38 +02:00
fn require ( self ) -> Result < Self > ;
2018-07-07 09:18:53 +03:00
}
2019-09-28 20:26:03 +03:00
impl < T > PathExt for T
where
T : AsRef < Path > ,
{
2018-07-07 09:18:53 +03:00
fn if_exists ( self ) -> Option < Self > {
2019-09-28 20:26:03 +03:00
if self . as_ref ( ) . exists ( ) {
2020-07-21 09:09:37 +03:00
debug! ( " Path {:?} exists " , self . as_ref ( ) ) ;
2018-07-07 09:18:53 +03:00
Some ( self )
} else {
2020-07-21 09:09:37 +03:00
debug! ( " Path {:?} doesn't exist " , self . as_ref ( ) ) ;
2018-07-07 09:18:53 +03:00
None
}
2018-06-17 11:43:25 +03:00
}
2018-07-07 09:18:53 +03:00
fn is_descendant_of ( & self , ancestor : & Path ) -> bool {
2019-09-28 20:26:03 +03:00
self . as_ref ( ) . iter ( ) . zip ( ancestor . iter ( ) ) . all ( | ( a , b ) | a = = b )
2018-07-07 09:18:53 +03:00
}
2019-01-13 23:20:32 +02:00
2019-12-11 23:05:38 +02:00
fn require ( self ) -> Result < Self > {
2019-09-28 20:26:03 +03:00
if self . as_ref ( ) . exists ( ) {
2019-12-08 20:56:03 +02:00
debug! ( " Path {:?} exists " , self . as_ref ( ) ) ;
2019-01-13 23:20:32 +02:00
Ok ( self )
} else {
2024-10-03 12:47:35 +02:00
Err ( SkipStep ( format! (
" {} " ,
t! ( " Path {path} doesn't exist " , path = format! ( " {:?} " , self . as_ref ( ) ) )
) )
. into ( ) )
2019-01-13 23:20:32 +02:00
}
}
2018-06-17 11:43:25 +03:00
}
2018-06-17 14:17:36 +03:00
pub fn which < T : AsRef < OsStr > + Debug > ( binary_name : T ) -> Option < PathBuf > {
2018-12-09 10:30:41 +02:00
match which_crate ::which ( & binary_name ) {
2018-06-17 14:17:36 +03:00
Ok ( path ) = > {
debug! ( " Detected {:?} as {:?} " , & path , & binary_name ) ;
Some ( path )
}
Err ( e ) = > {
2020-06-18 22:25:27 +03:00
match e {
which_crate ::Error ::CannotFindBinaryPath = > {
2018-06-17 14:17:36 +03:00
debug! ( " Cannot find {:?} " , & binary_name ) ;
}
_ = > {
error! ( " Detecting {:?} failed: {} " , & binary_name , e ) ;
}
}
None
}
}
}
2019-01-30 16:00:10 +02:00
2020-06-24 08:59:06 +03:00
pub fn editor ( ) -> Vec < String > {
env ::var ( " EDITOR " )
. unwrap_or_else ( | _ | String ::from ( if cfg! ( windows ) { " notepad " } else { " vi " } ) )
. split_whitespace ( )
2025-02-02 19:24:57 -08:00
. map ( std ::borrow ::ToOwned ::to_owned )
2020-06-24 08:59:06 +03:00
. collect ( )
2019-08-22 22:29:31 +03:00
}
2019-12-11 23:05:38 +02:00
pub fn require < T : AsRef < OsStr > + Debug > ( binary_name : T ) -> Result < PathBuf > {
2019-01-13 23:20:32 +02:00
match which_crate ::which ( & binary_name ) {
Ok ( path ) = > {
debug! ( " Detected {:?} as {:?} " , & path , & binary_name ) ;
Ok ( path )
}
2020-06-18 22:25:27 +03:00
Err ( e ) = > match e {
2024-10-03 12:47:35 +02:00
which_crate ::Error ::CannotFindBinaryPath = > Err ( SkipStep ( format! (
" {} " ,
t! (
" Cannot find {binary_name} in PATH " ,
binary_name = format! ( " {:?} " , & binary_name )
)
) )
. into ( ) ) ,
2019-01-13 23:20:32 +02:00
_ = > {
panic! ( " Detecting {:?} failed: {} " , & binary_name , e ) ;
}
} ,
}
}
2019-02-11 14:10:06 +02:00
2025-06-17 04:52:58 +01:00
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 ( ) )
}
2019-02-11 20:38:51 +02:00
#[ allow(dead_code) ]
2020-08-21 23:04:36 +03:00
pub fn require_option < T > ( option : Option < T > , cause : String ) -> Result < T > {
2021-10-28 22:05:35 +03:00
if let Some ( value ) = option {
Ok ( value )
} else {
Err ( SkipStep ( cause ) . into ( ) )
}
2019-02-11 14:10:06 +02:00
}
2022-11-16 20:05:20 -03:00
2023-05-25 12:22:11 +02:00
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 ;
}
2025-11-08 11:04:52 +01:00
#[ cfg(unix) ]
2022-11-16 20:05:20 -03:00
pub fn hostname ( ) -> Result < String > {
2023-11-24 07:50:41 +08:00
match nix ::unistd ::gethostname ( ) {
Ok ( os_str ) = > Ok ( os_str
. into_string ( )
2024-10-03 12:47:35 +02:00
. map_err ( | _ | SkipStep ( t! ( " Failed to get a UTF-8 encoded hostname " ) . into ( ) ) ) ? ) ,
2023-11-24 07:50:41 +08:00
Err ( e ) = > Err ( e . into ( ) ) ,
2022-11-16 20:05:20 -03:00
}
}
2025-11-08 11:04:52 +01:00
#[ cfg(windows) ]
2022-11-16 20:05:20 -03:00
pub fn hostname ( ) -> Result < String > {
Command ::new ( " hostname " )
. output_checked_utf8 ( )
2024-10-03 12:47:35 +02:00
. map_err ( | err | SkipStep ( t! ( " Failed to get hostname: {err} " , err = err ) . to_string ( ) ) . into ( ) )
2022-11-16 20:05:20 -03:00
. map ( | output | output . stdout . trim ( ) . to_owned ( ) )
}
2023-05-25 12:22:11 +02:00
2025-09-26 14:49:12 +02:00
#[ 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
}
2023-05-25 12:22:11 +02:00
pub mod merge_strategies {
use merge ::Merge ;
use crate ::config ::Commands ;
/// Prepends right to left (both Option<Vec<T>>)
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 ) ;
2025-08-13 11:01:25 -04:00
let _ = left . replace ( right_vec ) ;
2023-05-25 12:22:11 +02:00
}
} else {
* left = right ;
}
}
/// Appends an Option<String> to another Option<String>
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 ( ref mut 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 ( ref mut left_inner ) = left {
if let Some ( right_inner ) = right {
left_inner . extend ( right_inner ) ;
}
} else {
* left = right ;
}
}
}
2023-06-13 22:15:57 +08:00
2023-06-23 17:02:58 +08:00
/// Return `Err(SkipStep)` if `python` is a Python 2 or shim.
///
/// # Shim
/// On Windows, if you install `python` through `winget`, an actual `python`
2025-08-12 00:15:21 +08:00
/// is installed as well as a `python3` shim. Shim is invocable, but when you
2023-06-23 17:02:58 +08:00
/// execute it, the Microsoft App Store will be launched instead of a Python
/// shell.
///
/// We do this check through `python -V`, a shim will just give `Python` with
/// no version number.
pub fn check_is_python_2_or_shim ( python : PathBuf ) -> Result < PathBuf > {
let output = Command ::new ( & python ) . arg ( " -V " ) . output_checked_utf8 ( ) ? ;
// "Python x.x.x\n"
let stdout = output . stdout ;
// ["Python"] or ["Python", "x.x.x"], the newline char is trimmed.
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 {
2024-10-03 12:47:35 +02:00
return Err ( SkipStep ( t! ( " {python} is a Python 2, skip. " , python = python . display ( ) ) . to_string ( ) ) . into ( ) ) ;
2023-06-23 17:02:58 +08:00
}
} else {
// No version number, is a shim
2024-10-03 12:47:35 +02:00
return Err ( SkipStep ( t! ( " {python} is a Python shim, skip. " , python = python . display ( ) ) . to_string ( ) ) . into ( ) ) ;
2023-06-23 17:02:58 +08:00
}
Ok ( python )
}
2023-10-17 11:19:47 +08:00
/// Set up the tracing logger
///
/// # Return value
/// A reload handle will be returned so that we can change the log level at
/// runtime.
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 ) ) ? ;
2024-03-16 14:17:19 +08:00
let fmt_layer = fmt ::layer ( ) . with_target ( false ) . without_time ( ) ;
2023-10-17 11:19:47 +08:00
let ( filter , reload_handle ) = Layer ::new ( env_filter ) ;
registry ( ) . with ( filter ) . with ( fmt_layer ) . init ( ) ;
Ok ( reload_handle )
}
/// Update the tracing logger with new `filter_directives`.
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 ( ( ) )
}
/// Set up the error handler crate
pub fn install_color_eyre ( ) -> Result < ( ) > {
color_eyre ::config ::HookBuilder ::new ( )
// Don't display the backtrace reminder by default:
// Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
// Run with RUST_BACKTRACE=full to include source snippets.
. display_env_section ( false )
// Display location information by default:
// Location:
// src/steps.rs:92
. display_location_section ( true )
. install ( )
}
2025-04-13 10:43:08 +02:00
/// Macro to construct an error message for when the output of a command is unexpected.
#[ 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 ,
)
} ;
}