2020-12-01 08:59:59 +02:00
#![ allow(clippy::cognitive_complexity) ]
2021-12-18 20:39:57 +02:00
use std ::env ;
use std ::io ;
2023-05-01 00:02:13 +05:30
use std ::path ::PathBuf ;
2021-12-18 20:39:57 +02:00
use std ::process ::exit ;
2023-01-29 19:19:27 +00:00
use std ::time ::Duration ;
2021-12-18 20:39:57 +02:00
2024-01-23 14:50:35 +08:00
use crate ::breaking_changes ::{ first_run_of_major_release , print_breaking_changes , should_skip , write_keep_file } ;
2022-11-26 20:42:35 +01:00
use clap ::CommandFactory ;
2022-04-23 12:35:06 +02:00
use clap ::{ crate_version , Parser } ;
2022-11-11 09:39:29 -05:00
use color_eyre ::eyre ::Context ;
2023-05-01 00:02:13 +05:30
use color_eyre ::eyre ::Result ;
2021-12-18 20:39:57 +02:00
use console ::Key ;
2023-12-03 09:52:35 +08:00
use etcetera ::base_strategy ::BaseStrategy ;
2023-05-01 00:02:13 +05:30
#[ cfg(windows) ]
use etcetera ::base_strategy ::Windows ;
2023-12-03 09:52:35 +08:00
#[ cfg(unix) ]
use etcetera ::base_strategy ::Xdg ;
2024-10-03 12:47:35 +02:00
use rust_i18n ::{ i18n , t } ;
2025-08-11 09:57:32 +02:00
use std ::sync ::LazyLock ;
2022-11-16 13:43:57 -05:00
use tracing ::debug ;
2021-12-18 20:39:57 +02:00
2025-07-16 10:16:27 +01:00
use self ::config ::{ CommandLineArgs , Config } ;
2021-12-18 20:39:57 +02:00
use self ::error ::StepFailed ;
#[ cfg(all(windows, feature = " self-update " )) ]
use self ::error ::Upgraded ;
2025-09-26 15:41:38 +02:00
use self ::runner ::StepResult ;
2025-02-02 19:24:57 -08:00
#[ allow(clippy::wildcard_imports) ]
2021-12-18 20:39:57 +02:00
use self ::steps ::{ remote ::* , * } ;
2025-09-26 16:08:27 +02:00
use self ::sudo ::{ Sudo , SudoCreateError , SudoKind } ;
2025-02-02 19:24:57 -08:00
#[ allow(clippy::wildcard_imports) ]
2021-12-18 20:39:57 +02:00
use self ::terminal ::* ;
2025-09-26 14:49:12 +02:00
use self ::utils ::{ install_color_eyre , install_tracing , is_elevated , update_tracing } ;
2023-10-17 11:19:47 +08:00
2023-12-03 09:52:35 +08:00
mod breaking_changes ;
2022-11-08 05:54:35 -05:00
mod command ;
2018-06-28 12:16:54 +03:00
mod config ;
2018-10-17 14:07:58 +03:00
mod ctrlc ;
2018-12-11 16:43:26 +02:00
mod error ;
2020-02-08 22:13:56 +02:00
mod execution_context ;
2018-08-26 16:12:59 +03:00
mod executor ;
2020-02-09 13:41:55 +02:00
mod runner ;
2020-07-01 21:03:19 +03:00
#[ cfg(windows) ]
mod self_renamer ;
2018-11-26 14:27:19 +02:00
#[ cfg(feature = " self-update " ) ]
mod self_update ;
2025-07-16 10:16:27 +01:00
mod step ;
2018-12-15 21:52:21 +02:00
mod steps ;
2022-11-24 14:15:43 -05:00
mod sudo ;
2018-05-31 16:00:01 +03:00
mod terminal ;
2018-06-17 11:43:25 +03:00
mod utils ;
2018-05-30 07:53:19 +03:00
2025-08-11 09:57:32 +02:00
pub ( crate ) static HOME_DIR : LazyLock < PathBuf > = LazyLock ::new ( | | home ::home_dir ( ) . expect ( " No home directory " ) ) ;
2023-12-03 09:52:35 +08:00
#[ cfg(unix) ]
2025-08-11 09:57:32 +02:00
pub ( crate ) static XDG_DIRS : LazyLock < Xdg > = LazyLock ::new ( | | Xdg ::new ( ) . expect ( " No home directory " ) ) ;
2024-03-09 17:57:33 +08:00
2023-05-01 00:02:13 +05:30
#[ cfg(windows) ]
2025-08-11 09:57:32 +02:00
pub ( crate ) static WINDOWS_DIRS : LazyLock < Windows > = LazyLock ::new ( | | Windows ::new ( ) . expect ( " No home directory " ) ) ;
2023-05-01 00:02:13 +05:30
2024-10-03 12:47:35 +02:00
// Init and load the i18n files
i18n! ( " locales " , fallback = " en " ) ;
2025-02-02 19:24:57 -08:00
#[ allow(clippy::too_many_lines) ]
2019-12-11 23:05:38 +02:00
fn run ( ) -> Result < ( ) > {
2023-09-18 21:09:58 -04:00
install_color_eyre ( ) ? ;
2018-10-17 14:07:58 +03:00
ctrlc ::set_handler ( ) ;
2022-04-23 12:35:06 +02:00
let opt = CommandLineArgs ::parse ( ) ;
2023-10-17 11:19:47 +08:00
// Set up the logger with the filter directives from:
// 1. CLI option `--log-filter`
// 2. `debug` if the `--verbose` option is present
// We do this because we need our logger to work while loading the
// configuration file.
//
// When the configuration file is loaded, update the logger with the full
// filter directives.
//
// For more info, see the comments in `CommandLineArgs::tracing_filter_directives()`
// and `Config::tracing_filter_directives()`.
let reload_handle = install_tracing ( & opt . tracing_filter_directives ( ) ) ? ;
2022-05-07 08:11:53 +03:00
2024-10-03 12:47:35 +02:00
// Get current system locale and set it as the default locale
let system_locale = sys_locale ::get_locale ( ) . unwrap_or ( " en " . to_string ( ) ) ;
rust_i18n ::set_locale ( & system_locale ) ;
debug! ( " Current system locale is {system_locale} " ) ;
2022-11-26 20:42:35 +01:00
if let Some ( shell ) = opt . gen_completion {
let cmd = & mut CommandLineArgs ::command ( ) ;
2023-05-27 17:41:42 +08:00
clap_complete ::generate ( shell , cmd , clap ::crate_name! ( ) , & mut io ::stdout ( ) ) ;
2022-11-26 20:42:35 +01:00
return Ok ( ( ) ) ;
}
if opt . gen_manpage {
let man = clap_mangen ::Man ::new ( CommandLineArgs ::command ( ) ) ;
2023-05-27 17:41:42 +08:00
man . render ( & mut io ::stdout ( ) ) ? ;
2022-11-26 20:42:35 +01:00
return Ok ( ( ) ) ;
}
2022-05-07 08:11:53 +03:00
for env in opt . env_variables ( ) {
2025-09-13 17:12:39 +02:00
let mut parts = env . split ( '=' ) ;
let var = parts . next ( ) . unwrap ( ) ;
let value = parts . next ( ) . unwrap ( ) ;
2022-05-07 08:11:53 +03:00
env ::set_var ( var , value ) ;
}
2019-11-04 22:55:06 +02:00
if opt . edit_config ( ) {
2023-05-01 00:02:13 +05:30
Config ::edit ( ) ? ;
2019-08-22 22:29:31 +03:00
return Ok ( ( ) ) ;
} ;
2020-06-30 10:01:22 +03:00
if opt . show_config_reference ( ) {
2023-05-27 17:41:42 +08:00
print! ( " {} " , config ::EXAMPLE_CONFIG ) ;
2020-06-30 10:01:22 +03:00
return Ok ( ( ) ) ;
}
2023-05-01 00:02:13 +05:30
let config = Config ::load ( opt ) ? ;
2023-10-17 11:19:47 +08:00
// Update the logger with the full filter directives.
update_tracing ( & reload_handle , & config . tracing_filter_directives ( ) ) ? ;
2023-05-27 17:41:42 +08:00
set_title ( config . set_title ( ) ) ;
display_time ( config . display_time ( ) ) ;
set_desktop_notifications ( config . notify_each_step ( ) ) ;
2019-11-04 22:55:06 +02:00
2020-02-26 12:27:58 +02:00
debug! ( " Version: {} " , crate_version! ( ) ) ;
debug! ( " OS: {} " , env! ( " TARGET " ) ) ;
2025-07-16 10:16:27 +01:00
debug! ( " {:?} " , env ::args ( ) ) ;
2020-02-26 12:27:58 +02:00
debug! ( " Binary path: {:?} " , std ::env ::current_exe ( ) ) ;
2023-10-18 12:19:53 +08:00
debug! ( " self-update Feature Enabled: {:?} " , cfg! ( feature = " self-update " ) ) ;
2023-10-17 11:19:47 +08:00
debug! ( " Configuration: {:?} " , config ) ;
2020-02-26 12:27:58 +02:00
2019-06-13 09:21:39 +03:00
if config . run_in_tmux ( ) & & env ::var ( " TOPGRADE_INSIDE_TMUX " ) . is_err ( ) {
2018-06-27 23:04:39 +03:00
#[ cfg(unix) ]
{
2024-09-17 20:06:39 +07:00
tmux ::run_in_tmux ( config . tmux_config ( ) ? ) ? ;
2022-11-15 10:30:26 -05:00
return Ok ( ( ) ) ;
2018-06-20 21:05:49 +03:00
}
}
2025-09-26 14:49:12 +02:00
let elevated = is_elevated ( ) ;
#[ cfg(unix) ]
if ! config . allow_root ( ) & & elevated {
print_warning ( t! (
" Topgrade should not be run as root, it will run commands with sudo or equivalent where needed. "
) ) ;
if ! prompt_yesno ( & t! ( " Continue? " ) ) ? {
exit ( 1 )
}
}
2025-09-26 14:58:35 +02:00
let sudo = match config . sudo_command ( ) {
Some ( kind ) = > Sudo ::new ( kind ) ,
None if elevated = > Sudo ::new ( SudoKind ::Null ) ,
None = > Sudo ::detect ( ) ,
} ;
debug! ( " Sudo: {:?} " , sudo ) ;
2025-09-26 16:08:27 +02:00
let ( sudo , sudo_err ) = match sudo {
Ok ( sudo ) = > ( Some ( sudo ) , None ) ,
Err ( e ) = > ( None , Some ( e ) ) ,
} ;
2023-06-03 04:20:42 +08:00
#[ cfg(target_os = " linux " ) ]
let distribution = linux ::Distribution ::detect ( ) ;
2018-11-07 14:31:44 +02:00
2025-06-24 15:20:29 +02:00
let run_type = execution_context ::RunType ::new ( config . dry_run ( ) ) ;
2025-07-16 10:16:27 +01:00
let ctx = execution_context ::ExecutionContext ::new (
run_type ,
sudo ,
& config ,
#[ cfg(target_os = " linux " ) ]
& distribution ,
) ;
2020-02-09 13:41:55 +02:00
let mut runner = runner ::Runner ::new ( & ctx ) ;
2024-01-23 14:50:35 +08:00
// If
//
2025-08-12 00:15:21 +08:00
// 1. the breaking changes notification shouldn't be skipped
2024-01-23 14:50:35 +08:00
// 2. this is the first execution of a major release
//
// inform user of breaking changes
if ! should_skip ( ) & & first_run_of_major_release ( ) ? {
2023-12-03 09:52:35 +08:00
print_breaking_changes ( ) ;
2025-09-26 14:49:12 +02:00
if prompt_yesno ( & t! ( " Continue? " ) ) ? {
2023-12-03 09:52:35 +08:00
write_keep_file ( ) ? ;
} else {
exit ( 1 ) ;
}
}
2023-10-18 12:19:53 +08:00
// Self-Update step, this will execute only if:
// 1. the `self-update` feature is enabled
// 2. it is not disabled from configuration (env var/CLI opt/file)
2018-11-12 21:27:49 +02:00
#[ cfg(feature = " self-update " ) ]
{
2023-10-18 12:19:53 +08:00
let should_self_update = env ::var ( " TOPGRADE_NO_SELF_UPGRADE " ) . is_err ( ) & & ! config . no_self_update ( ) ;
if should_self_update {
2025-07-16 10:16:27 +01:00
runner . execute ( step ::Step ::SelfUpdate , " Self Update " , | | self_update ::self_update ( & ctx ) ) ? ;
2018-11-12 21:27:49 +02:00
}
}
2018-11-07 14:31:44 +02:00
2020-07-01 21:03:19 +03:00
#[ cfg(windows) ]
let _self_rename = if config . self_rename ( ) {
Some ( crate ::self_renamer ::SelfRenamer ::create ( ) ? )
} else {
None
} ;
2018-06-20 20:26:08 +03:00
if let Some ( commands ) = config . pre_commands ( ) {
for ( name , command ) in commands {
2021-09-02 06:18:01 +03:00
generic ::run_custom_command ( name , command , & ctx ) ? ;
2018-06-20 20:26:08 +03:00
}
}
2022-11-24 14:15:43 -05:00
if config . pre_sudo ( ) {
2022-11-25 17:19:32 -05:00
if let Some ( sudo ) = ctx . sudo ( ) {
sudo . elevate ( & ctx ) ? ;
}
2022-11-24 14:15:43 -05:00
}
2025-07-16 10:16:27 +01:00
for step in step ::default_steps ( ) {
step . run ( & mut runner , & ctx ) ?
2020-06-10 11:51:52 +03:00
}
2025-09-28 08:25:10 +02:00
let mut failed = false ;
let report = runner . report ( ) ;
if ! report . is_empty ( ) {
2024-10-03 12:47:35 +02:00
print_separator ( t! ( " Summary " ) ) ;
2018-06-03 18:04:58 +03:00
2025-09-26 15:41:38 +02:00
let mut skipped_missing_sudo = false ;
2025-09-28 08:25:10 +02:00
for ( key , result ) in report {
if ! failed & & result . failed ( ) {
failed = true ;
}
2025-09-26 15:41:38 +02:00
if let StepResult ::SkippedMissingSudo = result {
skipped_missing_sudo = true ;
}
2020-08-21 23:04:36 +03:00
print_result ( key , result ) ;
2018-06-03 18:04:58 +03:00
}
2025-09-26 15:41:38 +02:00
if skipped_missing_sudo {
print_warning ( t! (
" \n Some steps were skipped as sudo or equivalent could not be found. "
) ) ;
2025-09-26 16:08:27 +02:00
// Steps can only fail with SkippedMissingSudo if sudo is None,
// therefore we must have a sudo_err
match sudo_err . unwrap ( ) {
SudoCreateError ::CannotFindBinary = > {
#[ cfg(unix) ]
print_warning ( t! (
" Install one of `sudo`, `doas`, `pkexec`, `run0` or `please` to run these steps. "
) ) ;
// if this windows version supported Windows Sudo, the error would have been WinSudoDisabled
#[ cfg(windows) ]
print_warning ( t! ( " Install gsudo to run these steps. " ) ) ;
}
#[ cfg(windows) ]
SudoCreateError ::WinSudoDisabled = > {
print_warning ( t! (
" Install gsudo or enable Windows Sudo to run these steps. \n For Windows Sudo, the default 'In a new window' mode is not supported as it prevents Topgrade from waiting for commands to finish. Please configure it to use 'Inline' mode instead. \n Go to https://go.microsoft.com/fwlink/?linkid=2257346 to learn more. "
) ) ;
}
#[ cfg(windows) ]
SudoCreateError ::WinSudoNewWindowMode = > {
print_warning ( t! (
" Windows Sudo was found, but it is set to 'In a new window' mode, which prevents Topgrade from waiting for commands to finish. Please configure it to use 'Inline' mode instead. \n Go to https://go.microsoft.com/fwlink/?linkid=2257346 to learn more. "
) ) ;
}
}
2025-09-26 15:41:38 +02:00
}
2025-09-28 08:25:10 +02:00
}
2018-10-02 11:36:10 +03:00
2025-09-28 08:25:10 +02:00
#[ cfg(target_os = " linux " ) ]
{
if let Ok ( distribution ) = & distribution {
distribution . show_summary ( ) ;
2018-10-02 11:36:10 +03:00
}
2018-05-29 23:48:30 +03:00
}
2020-08-30 07:40:06 +03:00
if let Some ( commands ) = config . post_commands ( ) {
for ( name , command ) in commands {
2025-09-28 08:25:10 +02:00
let result = generic ::run_custom_command ( name , command , & ctx ) ;
if ! failed & & result . is_err ( ) {
failed = true ;
2020-08-30 07:40:06 +03:00
}
}
}
2019-06-16 09:09:05 +03:00
if config . keep_at_end ( ) {
2024-10-03 12:47:35 +02:00
print_info ( t! ( " \n (R)eboot \n (S)hell \n (Q)uit " ) ) ;
2019-08-04 09:25:35 +03:00
loop {
2021-02-13 06:26:50 +02:00
match get_key ( ) {
2025-02-02 19:24:57 -08:00
Ok ( Key ::Char ( 's' | 'S' ) ) = > {
2022-11-08 05:54:35 -05:00
run_shell ( ) . context ( " Failed to execute shell " ) ? ;
2021-10-28 22:05:35 +03:00
}
2025-02-02 19:24:57 -08:00
Ok ( Key ::Char ( 'r' | 'R' ) ) = > {
2025-06-25 15:59:02 +02:00
println! ( " {} " , t! ( " Rebooting... " ) ) ;
reboot ( & ctx ) . context ( " Failed to reboot " ) ? ;
2021-10-28 22:05:35 +03:00
}
2025-02-02 19:24:57 -08:00
Ok ( Key ::Char ( 'q' | 'Q' ) ) = > ( ) ,
2021-10-28 22:05:35 +03:00
_ = > {
continue ;
}
2019-06-13 22:05:18 +03:00
}
2019-08-04 09:25:35 +03:00
break ;
2019-06-13 22:05:18 +03:00
}
2019-06-13 09:21:39 +03:00
}
2022-10-10 20:21:20 +00:00
if ! config . skip_notify ( ) {
2023-05-27 17:41:42 +08:00
notify_desktop (
2024-10-03 12:47:35 +02:00
if failed {
t! ( " Topgrade finished with errors " )
} else {
t! ( " Topgrade finished successfully " )
} ,
2023-01-29 19:19:27 +00:00
Some ( Duration ::from_secs ( 10 ) ) ,
2025-02-02 19:24:57 -08:00
) ;
2022-10-10 20:21:20 +00:00
}
2020-11-04 11:31:09 +02:00
if failed {
2019-12-11 23:05:38 +02:00
Err ( StepFailed . into ( ) )
2020-07-02 11:15:56 +03:00
} else {
Ok ( ( ) )
2018-06-11 08:57:55 +03:00
}
}
fn main ( ) {
match run ( ) {
Ok ( ( ) ) = > {
exit ( 0 ) ;
}
Err ( error ) = > {
2019-06-03 09:41:25 +03:00
#[ cfg(all(windows, feature = " self-update " )) ]
{
2019-12-11 23:05:38 +02:00
if let Some ( Upgraded ( status ) ) = error . downcast_ref ::< Upgraded > ( ) {
2019-06-03 09:41:25 +03:00
exit ( status . code ( ) . unwrap ( ) ) ;
}
}
2019-12-11 23:05:38 +02:00
let skip_print = ( error . downcast_ref ::< StepFailed > ( ) . is_some ( ) )
| | ( error
. downcast_ref ::< io ::Error > ( )
2018-12-11 16:43:26 +02:00
. filter ( | io_error | io_error . kind ( ) = = io ::ErrorKind ::Interrupted )
2019-12-11 23:05:38 +02:00
. is_some ( ) ) ;
2018-12-11 16:43:26 +02:00
2019-12-11 23:05:38 +02:00
if ! skip_print {
2022-11-11 09:39:29 -05:00
// The `Debug` implementation of `eyre::Result` prints a multi-line
2022-11-03 12:46:43 -04:00
// error message that includes all the 'causes' added with
// `.with_context(...)` calls.
2024-10-03 12:47:35 +02:00
println! ( " {} " , t! ( " Error: {error} " , error = format! ( " {:?} " , error ) ) ) ;
2018-10-17 14:07:58 +03:00
}
2018-06-11 08:57:55 +03:00
exit ( 1 ) ;
}
}
2018-05-29 23:48:30 +03:00
}