2021-12-06 14:44:20 +02:00
use std ::path ::Path ;
use std ::{ ffi ::OsStr , process ::Command } ;
2022-11-11 09:39:29 -05:00
use color_eyre ::eyre ::Result ;
2023-05-01 00:02:13 +05:30
use etcetera ::base_strategy ::BaseStrategy ;
2025-09-27 19:55:56 +02:00
use rust_i18n ::t ;
2022-11-16 13:43:57 -05:00
use tracing ::debug ;
2021-12-06 14:44:20 +02:00
2022-11-08 05:54:35 -05:00
use crate ::command ::CommandExt ;
2025-09-27 19:55:56 +02:00
use crate ::config ::UpdatesAutoReboot ;
2021-04-23 07:01:06 +03:00
use crate ::execution_context ::ExecutionContext ;
2025-07-16 10:16:27 +01:00
use crate ::step ::Step ;
2024-05-06 13:24:57 +01:00
use crate ::terminal ::{ print_separator , print_warning } ;
2023-07-25 14:02:13 +08:00
use crate ::utils ::{ require , which } ;
2024-03-09 17:57:33 +08:00
use crate ::{ error ::SkipStep , steps ::git ::RepoStep } ;
2018-06-28 12:16:54 +03:00
2020-06-30 11:55:39 +03:00
pub fn run_chocolatey ( ctx : & ExecutionContext ) -> Result < ( ) > {
2019-02-11 20:38:51 +02:00
let choco = require ( " choco " ) ? ;
2021-12-06 14:44:20 +02:00
let yes = ctx . config ( ) . yes ( Step ::Chocolatey ) ;
2018-06-28 12:16:54 +03:00
2019-02-11 20:38:51 +02:00
print_separator ( " Chocolatey " ) ;
2020-06-30 11:55:39 +03:00
2022-11-25 17:19:32 -05:00
let mut command = match ctx . sudo ( ) {
2025-06-25 15:59:02 +02:00
Some ( sudo ) = > sudo . execute ( ctx , & choco ) ? ,
2025-06-24 15:20:29 +02:00
None = > ctx . execute ( choco ) ,
2022-11-25 17:19:32 -05:00
} ;
command . args ( [ " upgrade " , " all " ] ) ;
2020-06-30 11:55:39 +03:00
if yes {
command . arg ( " --yes " ) ;
}
2022-11-08 05:54:35 -05:00
command . status_checked ( )
2018-06-28 12:16:54 +03:00
}
2018-08-22 22:01:06 +03:00
2021-06-03 13:08:17 +03:00
pub fn run_winget ( ctx : & ExecutionContext ) -> Result < ( ) > {
let winget = require ( " winget " ) ? ;
print_separator ( " winget " ) ;
2025-06-24 15:20:29 +02:00
ctx . execute ( & winget ) . args ( [ " source " , " update " ] ) . status_checked ( ) ? ;
2025-04-10 13:50:35 +02:00
2025-07-15 09:12:55 +02:00
let mut command = if ctx . config ( ) . winget_use_sudo ( ) {
match ctx . sudo ( ) {
2025-06-25 15:59:02 +02:00
Some ( sudo ) = > sudo . execute ( ctx , & winget ) ? ,
2025-06-24 15:20:29 +02:00
None = > ctx . execute ( winget ) ,
2025-07-15 09:12:55 +02:00
}
} else {
2025-06-24 15:20:29 +02:00
ctx . execute ( winget )
2025-07-15 09:12:55 +02:00
} ;
2025-03-30 09:11:04 -04:00
let mut args = vec! [ " upgrade " , " --all " ] ;
if ctx . config ( ) . winget_silent_install ( ) {
args . push ( " --silent " ) ;
}
2025-07-15 09:12:55 +02:00
command . args ( args ) . status_checked ( ) ? ;
2025-04-10 13:50:35 +02:00
Ok ( ( ) )
2021-06-03 13:08:17 +03:00
}
2023-06-03 04:20:42 +08:00
pub fn run_scoop ( ctx : & ExecutionContext ) -> Result < ( ) > {
2019-02-11 20:38:51 +02:00
let scoop = require ( " scoop " ) ? ;
2018-10-17 13:57:30 +03:00
2019-02-11 20:38:51 +02:00
print_separator ( " Scoop " ) ;
2018-10-17 13:57:30 +03:00
2025-06-24 15:20:29 +02:00
ctx . execute ( & scoop ) . args ( [ " update " ] ) . status_checked ( ) ? ;
ctx . execute ( & scoop ) . args ( [ " update " , " * " ] ) . status_checked ( ) ? ;
2020-01-30 20:32:37 +02:00
2023-06-03 04:20:42 +08:00
if ctx . config ( ) . cleanup ( ) {
2025-06-24 15:20:29 +02:00
ctx . execute ( & scoop ) . args ( [ " cleanup " , " * " ] ) . status_checked ( ) ? ;
ctx . execute ( & scoop ) . args ( [ " cache " , " rm " , " -a " ] ) . status_checked ( ) ?
2020-01-30 20:32:37 +02:00
}
Ok ( ( ) )
2018-10-17 13:57:30 +03:00
}
2023-01-29 19:19:27 +00:00
pub fn update_wsl ( ctx : & ExecutionContext ) -> Result < ( ) > {
2023-07-25 14:02:13 +08:00
if ! is_wsl_installed ( ) ? {
2024-10-03 12:47:35 +02:00
return Err ( SkipStep ( t! ( " WSL not installed " ) . to_string ( ) ) . into ( ) ) ;
2023-07-25 14:02:13 +08:00
}
2023-01-29 19:19:27 +00:00
let wsl = require ( " wsl " ) ? ;
2024-10-03 12:47:35 +02:00
print_separator ( t! ( " Update WSL " ) ) ;
2023-01-29 19:19:27 +00:00
2025-06-24 15:20:29 +02:00
let mut wsl_command = ctx . execute ( wsl ) ;
2023-01-29 19:19:27 +00:00
wsl_command . args ( [ " --update " ] ) ;
if ctx . config ( ) . wsl_update_pre_release ( ) {
wsl_command . args ( [ " --pre-release " ] ) ;
}
if ctx . config ( ) . wsl_update_use_web_download ( ) {
wsl_command . args ( [ " --web-download " ] ) ;
}
wsl_command . status_checked ( ) ? ;
Ok ( ( ) )
}
2023-07-25 14:02:13 +08:00
/// Detect if WSL is installed or not.
///
/// For WSL, we cannot simply check if command `wsl` is installed as on newer
2025-08-12 00:15:21 +08:00
/// versions of Windows (since windows 10 version 2004), this command is
2023-07-25 14:02:13 +08:00
/// installed by default.
///
/// If the command is installed and the user hasn't installed any Linux distros
/// on it, command `wsl -l` would print a help message and exit with failure, we
/// use this to check whether WSL is install or not.
fn is_wsl_installed ( ) -> Result < bool > {
if let Some ( wsl ) = which ( " wsl " ) {
// Don't use `output_checked` as an execution failure log is not wanted
#[ allow(clippy::disallowed_methods) ]
let output = Command ::new ( wsl ) . arg ( " -l " ) . output ( ) ? ;
let status = output . status ;
if status . success ( ) {
return Ok ( true ) ;
}
}
Ok ( false )
}
2022-11-03 21:43:45 +00:00
fn get_wsl_distributions ( wsl : & Path ) -> Result < Vec < String > > {
2022-11-08 05:54:35 -05:00
let output = Command ::new ( wsl ) . args ( [ " --list " , " -q " ] ) . output_checked_utf8 ( ) ? . stdout ;
2022-02-13 08:08:18 +02:00
Ok ( output
. lines ( )
. filter ( | s | ! s . is_empty ( ) )
2022-11-03 18:54:40 +00:00
. map ( | x | x . replace ( [ '\u{0}' , '\r' ] , " " ) )
2022-02-13 08:08:18 +02:00
. collect ( ) )
}
fn upgrade_wsl_distribution ( wsl : & Path , dist : & str , ctx : & ExecutionContext ) -> Result < ( ) > {
2022-11-03 18:54:40 +00:00
let topgrade = Command ::new ( wsl )
. args ( [ " -d " , dist , " bash " , " -lc " , " which topgrade " ] )
2022-11-08 05:54:35 -05:00
. output_checked_utf8 ( )
2024-10-03 12:47:35 +02:00
. map_err ( | _ | SkipStep ( t! ( " Could not find Topgrade installed in WSL " ) . to_string ( ) ) ) ?
2023-09-25 20:11:19 -07:00
. stdout // The normal output from `which topgrade` appends a newline, so we trim it here.
. trim_end ( )
. to_owned ( ) ;
2021-05-13 21:53:13 +03:00
2025-06-24 15:20:29 +02:00
let mut command = ctx . execute ( wsl ) ;
2023-09-25 20:11:19 -07:00
// The `arg` method automatically quotes its arguments.
// This means we can't append additional arguments to `topgrade` in WSL
// by calling `arg` successively.
//
// For example:
//
// ```rust
// command
2025-09-18 17:06:59 +02:00
// .args(["-d", dist, "bash", "-lc"])
2023-09-25 20:11:19 -07:00
// .arg(format!("TOPGRADE_PREFIX={dist} exec {topgrade}"));
// ```
//
// creates a command string like:
2025-09-18 17:06:59 +02:00
// > `C:\WINDOWS\system32\wsl.EXE -d Ubuntu bash -lc 'TOPGRADE_PREFIX=Ubuntu exec /bin/topgrade'`
2023-09-25 20:11:19 -07:00
//
// Adding the following:
//
// ```rust
// command.arg("-v");
// ```
//
// appends the next argument like so:
2025-09-18 17:06:59 +02:00
// > `C:\WINDOWS\system32\wsl.EXE -d Ubuntu bash -lc 'TOPGRADE_PREFIX=Ubuntu exec /bin/topgrade' -v`
2023-09-25 20:11:19 -07:00
// which means `-v` isn't passed to `topgrade`.
let mut args = String ::new ( ) ;
if ctx . config ( ) . verbose ( ) {
args . push_str ( " -v " ) ;
}
2021-05-13 21:53:13 +03:00
command
2025-09-18 17:06:59 +02:00
. args ( [ " -d " , dist , " bash " , " -lc " ] )
2023-09-25 20:11:19 -07:00
. arg ( format! ( " TOPGRADE_PREFIX= {dist} exec {topgrade} {args} " ) ) ;
2020-08-21 21:10:54 +03:00
2021-12-06 14:44:20 +02:00
if ctx . config ( ) . yes ( Step ::Wsl ) {
2020-08-21 21:10:54 +03:00
command . arg ( " -y " ) ;
}
2022-11-08 05:54:35 -05:00
command . status_checked ( )
2019-06-04 09:35:29 +03:00
}
2019-06-13 22:05:18 +03:00
2022-02-13 08:08:18 +02:00
pub fn run_wsl_topgrade ( ctx : & ExecutionContext ) -> Result < ( ) > {
2023-07-25 14:02:13 +08:00
if ! is_wsl_installed ( ) ? {
2024-10-03 12:47:35 +02:00
return Err ( SkipStep ( t! ( " WSL not installed " ) . to_string ( ) ) . into ( ) ) ;
2023-07-25 14:02:13 +08:00
}
2022-02-13 08:08:18 +02:00
let wsl = require ( " wsl " ) ? ;
2022-11-03 21:43:45 +00:00
let wsl_distributions = get_wsl_distributions ( & wsl ) ? ;
2022-02-13 08:08:18 +02:00
let mut ran = false ;
debug! ( " WSL distributions: {:?} " , wsl_distributions ) ;
for distribution in wsl_distributions {
let result = upgrade_wsl_distribution ( & wsl , & distribution , ctx ) ;
debug! ( " Upgrading {:?}: {:?} " , distribution , result ) ;
if let Err ( e ) = result {
if e . is ::< SkipStep > ( ) {
continue ;
}
}
ran = true
}
if ran {
Ok ( ( ) )
} else {
2025-08-12 00:15:21 +08:00
Err ( SkipStep ( t! ( " Could not find Topgrade in any WSL distribution " ) . to_string ( ) ) . into ( ) )
2022-02-13 08:08:18 +02:00
}
}
2020-02-27 22:06:14 +02:00
pub fn windows_update ( ctx : & ExecutionContext ) -> Result < ( ) > {
2025-07-17 15:13:29 +02:00
let powershell = ctx . require_powershell ( ) ? ;
2020-02-27 22:06:14 +02:00
2024-10-03 12:47:35 +02:00
print_separator ( t! ( " Windows Update " ) ) ;
2024-05-06 13:24:57 +01:00
2025-09-27 19:55:56 +02:00
if ! powershell . has_module ( " PSWindowsUpdate " ) {
2024-10-03 12:47:35 +02:00
print_warning ( t! (
2025-06-23 01:08:37 +02:00
" The PSWindowsUpdate PowerShell module isn't installed so Topgrade can't run Windows Update. \n Install PSWindowsUpdate by running `Install-Module PSWindowsUpdate` in PowerShell. "
2024-10-03 12:47:35 +02:00
) ) ;
2024-05-06 13:24:57 +01:00
2025-09-27 19:55:56 +02:00
return Err ( SkipStep ( t! ( " PSWindowsUpdate is not installed " ) . to_string ( ) ) . into ( ) ) ;
}
let mut cmd = " Import-Module PSWindowsUpdate; Install-WindowsUpdate -Verbose " . to_string ( ) ;
if ctx . config ( ) . accept_all_windows_updates ( ) {
cmd . push_str ( " -AcceptAll " ) ;
}
match ctx . config ( ) . windows_updates_auto_reboot ( ) {
UpdatesAutoReboot ::Yes = > cmd . push_str ( " -AutoReboot " ) ,
UpdatesAutoReboot ::No = > cmd . push_str ( " -IgnoreReboot " ) ,
UpdatesAutoReboot ::Ask = > ( ) , // Prompting is the default for Install-WindowsUpdate
2020-02-27 22:06:14 +02:00
}
2025-09-27 19:55:56 +02:00
powershell . build_command ( ctx , & cmd , true ) ? . status_checked ( )
2020-02-27 22:06:14 +02:00
}
2024-10-23 02:15:46 +02:00
pub fn microsoft_store ( ctx : & ExecutionContext ) -> Result < ( ) > {
2025-07-17 15:13:29 +02:00
let powershell = ctx . require_powershell ( ) ? ;
2024-10-23 02:15:46 +02:00
print_separator ( t! ( " Microsoft Store " ) ) ;
2025-09-27 19:55:56 +02:00
println! ( " {} " , t! ( " Scanning for updates... " ) ) ;
// Scan for updates using the MDM UpdateScanMethod
// This method is also available for non-MDM devices
let cmd = r # "(Get-CimInstance -Namespace "Root\cimv2\mdm\dmmap" -ClassName "MDM_EnterpriseModernAppManagement_AppManagement01" | Invoke-CimMethod -MethodName UpdateScanMethod).ReturnValue"# ;
powershell
. build_command ( ctx , cmd , true ) ?
. output_checked_with_utf8 ( | output | {
if ! output . status . success ( ) {
return Err ( ( ) ) ;
}
let ret_val = output . stdout . trim ( ) ;
debug! ( " Command return value: {} " , ret_val ) ;
if ret_val = = " 0 " {
Ok ( ( ) )
} else {
Err ( ( ) )
}
} ) ? ;
println! (
" {} " ,
t! ( " Success, Microsoft Store apps are being updated in the background " )
) ;
Ok ( ( ) )
2024-10-23 02:15:46 +02:00
}
2025-06-25 15:59:02 +02:00
pub fn reboot ( ctx : & ExecutionContext ) -> Result < ( ) > {
2022-11-08 05:54:35 -05:00
// If this works, it won't return, but if it doesn't work, it may return a useful error
// message.
2025-06-25 15:59:02 +02:00
ctx . execute ( " shutdown.exe " ) . args ( [ " /R " , " /T " , " 0 " ] ) . status_checked ( )
2019-06-13 22:05:18 +03:00
}
2021-02-02 22:28:22 +02:00
2024-03-09 17:57:33 +08:00
pub fn insert_startup_scripts ( git_repos : & mut RepoStep ) -> Result < ( ) > {
2023-05-01 00:02:13 +05:30
let startup_dir = crate ::WINDOWS_DIRS
2021-02-02 22:28:22 +02:00
. data_dir ( )
. join ( " Microsoft \\ Windows \\ Start Menu \\ Programs \\ Startup " ) ;
2021-05-08 22:50:42 +03:00
for entry in std ::fs ::read_dir ( & startup_dir ) ? . flatten ( ) {
let path = entry . path ( ) ;
if path . extension ( ) . and_then ( OsStr ::to_str ) = = Some ( " lnk " ) {
if let Ok ( lnk ) = parselnk ::Lnk ::try_from ( Path ::new ( & path ) ) {
debug! ( " Startup link: {:?} " , lnk ) ;
if let Some ( path ) = lnk . relative_path ( ) {
2024-07-29 09:01:04 +08:00
git_repos . insert_if_repo ( startup_dir . join ( path ) ) ;
2021-02-02 22:28:22 +02:00
}
}
}
}
Ok ( ( ) )
}