From 31415b25b80245565246d08558618fde9e8b15fe Mon Sep 17 00:00:00 2001 From: z-Fng <54583083+z-Fng@users.noreply.github.com> Date: Mon, 18 Aug 2025 03:55:24 +0800 Subject: [PATCH] fix(uninstall|scoop-uninstall): Fix incomplete self-uninstallation, unify uninstall flow for self and packages --- bin/uninstall.ps1 | 153 ++++++++++++++++++++++-------------- lib/uninstall.ps1 | 113 ++++++++++++++++++++++++++ libexec/scoop-uninstall.ps1 | 109 +++---------------------- 3 files changed, 220 insertions(+), 155 deletions(-) create mode 100644 lib/uninstall.ps1 diff --git a/bin/uninstall.ps1 b/bin/uninstall.ps1 index 2e2dc36ea8..ce247e50ef 100644 --- a/bin/uninstall.ps1 +++ b/bin/uninstall.ps1 @@ -14,6 +14,7 @@ param( . "$PSScriptRoot\..\lib\core.ps1" . "$PSScriptRoot\..\lib\system.ps1" . "$PSScriptRoot\..\lib\install.ps1" +. "$PSScriptRoot\..\lib\uninstall.ps1" # 'uninstall_app' . "$PSScriptRoot\..\lib\shortcuts.ps1" . "$PSScriptRoot\..\lib\versions.ps1" . "$PSScriptRoot\..\lib\manifest.ps1" @@ -31,77 +32,115 @@ if ($purge) { $yn = Read-Host 'Are you sure? (yN)' if ($yn -notlike 'y*') { exit } -$errors = $false - -# Uninstall given app -function do_uninstall($app, $global) { - $version = Select-CurrentVersion -AppName $app -Global:$global - $dir = versiondir $app $version $global - $manifest = installed_manifest $app $version $global - $install = install_info $app $version $global - $architecture = $install.architecture - - Write-Output "Uninstalling '$app'" - Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Uninstall - rm_shims $app $manifest $global $architecture - - # If a junction was used during install, that will have been used - # as the reference directory. Othewise it will just be the version - # directory. - $refdir = unlink_current (appdir $app $global) - - env_rm_path $manifest $refdir $global $architecture - env_rm $manifest $global $architecture - - $appdir = appdir $app $global - try { - Remove-Item $appdir -Recurse -Force -ErrorAction Stop - } catch { - $errors = $true - warn "Couldn't remove $(friendly_path $appdir): $_.Exception" + +# Run uninstallation for each app if necessary, continuing if there's +# a problem deleting a directory (which is quite likely) +function UninstallApps { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [bool]$global, + [Parameter(Mandatory = $true)] + [bool]$purge + ) + + $allDone = $true + + foreach ($app in installed_apps $global) { + try { + $succ = uninstall_app -app $app -global $global -purge $purge + $errMsg = "Failed to uninstall $app." + } catch { + $succ = $false + $errMsg = "Failed to uninstall $app : $($_.Exception.Message)." + } + + if (-not $succ) { + $allDone = $false + error $errMsg + } } -} -function rm_dir($dir) { - try { - Remove-Item $dir -Recurse -Force -ErrorAction Stop - } catch { - abort "Couldn't remove $(friendly_path $dir): $_" + if (-not $allDone) { + abort 'Not all apps were uninstalled. Please try again or restart.' } } -# Remove all folders (except persist) inside given scoop directory. -function keep_onlypersist($directory) { - Get-ChildItem $directory -Exclude 'persist' | ForEach-Object { rm_dir $_ } -} +function RemoveDirs { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [bool]$global, + [Parameter(Mandatory = $true)] + [bool]$purge + ) -# Run uninstallation for each app if necessary, continuing if there's -# a problem deleting a directory (which is quite likely) -if ($global) { - installed_apps $true | ForEach-Object { # global apps - do_uninstall $_ $true + $dirs = @() + + # Remove the shortcut directory + $dirs += (shortcut_folder $global) + + # Remove the scoop root directory + $scoopDir = basedir $global + $persistDir = "$scoopDir\persist" + + $dirs += (Get-ChildItem $scoopDir -Exclude @('persist', 'apps', 'shims') | Select-Object -ExpandProperty FullName) + + # Remove the persist directory if purge is specified or if it's empty + $rmPersist = $purge -or + ((Get-ChildItem -Path $persistDir -Force -ErrorAction SilentlyContinue).Count -eq 0) + + # Ensure shims dir and apps dir are removed last + if ($rmPersist) { + $dirs += @($persistDir, "$scoopDir\shims", "$scoopDir\apps", $scoopDir) + } else { + $dirs += @("$scoopDir\shims", "$scoopDir\apps") } -} -installed_apps $false | ForEach-Object { # local apps - do_uninstall $_ $false + foreach ($dir in $dirs) { + try { + if (($null -ne $dir) -and (Test-Path -Path $dir)) { + Write-Host "Removing $(friendly_path $dir)..." + + Remove-Item -Path "$dir" -Recurse -Force -ErrorAction Stop + } + } catch { + abort "Couldn't remove $(friendly_path $dir): $($_.Exception.Message)" + } + } } -if ($errors) { - abort 'Not all apps could be deleted. Try again or restart.' +function RemoveEnvVars { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [bool]$global + ) + + # Remove environment variable "$scoopPathEnvVar" + if (get_config USE_ISOLATED_PATH) { + Remove-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global -PassThru:$false + } } -if ($purge) { - rm_dir $scoopdir - if ($global) { rm_dir $globaldir } -} else { - keep_onlypersist $scoopdir - if ($global) { keep_onlypersist $globaldir } +function Invoke-SelfUninstall{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [bool]$global, + [Parameter(Mandatory = $true)] + [bool]$purge + ) + + UninstallApps -global $global -purge $purge + RemoveDirs -global $global -purge $purge + RemoveEnvVars -global $global } -Remove-Path -Path (shimdir $global) -Global:$global -if (get_config USE_ISOLATED_PATH) { - Remove-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global +if($global) { + Invoke-SelfUninstall -global $true -purge $purge } +Invoke-SelfUninstall -global $false -purge $purge + success 'Scoop has been uninstalled.' diff --git a/lib/uninstall.ps1 b/lib/uninstall.ps1 new file mode 100644 index 0000000000..038a417355 --- /dev/null +++ b/lib/uninstall.ps1 @@ -0,0 +1,113 @@ +function uninstall_app { + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string]$app, + [Parameter(Mandatory = $true)] + [bool]$global, + [Parameter(Mandatory = $true)] + [bool]$purge + ) + + $version = Select-CurrentVersion -AppName $app -Global:$global + $appDir = appdir $app $global + if ($version) { + Write-Host "Uninstalling '$app' ($version)." + + $dir = versiondir $app $version $global + $persist_dir = persistdir $app $global + + $manifest = installed_manifest $app $version $global + $install = install_info $app $version $global + $architecture = $install.architecture + + Invoke-HookScript -HookType 'pre_uninstall' -Manifest $manifest -Arch $architecture + + # region Workaround for #2952 + # https://github.com/ScoopInstaller/Scoop/issues/2952 + if (test_running_process $app $global) { + return $false + } + # endregion Workaround for #2952 + + try { + Test-Path $dir -ErrorAction Stop | Out-Null + } catch [UnauthorizedAccessException] { + error "Access denied: $dir. You might need to restart." + return $false + } + + Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Global:$global -Uninstall + rm_shims $app $manifest $global $architecture + rm_startmenu_shortcuts $manifest $global $architecture + + # If a junction was used during install, that will have been used + # as the reference directory. Otherwise it will just be the version + # directory. + $refdir = unlink_current $dir + + uninstall_psmodule $manifest $refdir $global + + env_rm_path $manifest $refdir $global $architecture + env_rm $manifest $global $architecture + + try { + # unlink all potential old link before doing recursive Remove-Item + unlink_persist_data $manifest $dir + Remove-Item $dir -Recurse -Force -ErrorAction Stop + } catch { + if (Test-Path $dir) { + error "Couldn't remove '$(friendly_path $dir)'; it may be in use." + return $false + } + } + + Invoke-HookScript -HookType 'post_uninstall' -Manifest $manifest -Arch $architecture + } + # remove older versions + $oldVersions = @(Get-ChildItem $appDir -Name -Exclude 'current') + foreach ($version in $oldVersions) { + Write-Host "Removing older version ($version)." + $dir = versiondir $app $version $global + try { + # unlink all potential old link before doing recursive Remove-Item + unlink_persist_data $manifest $dir + Remove-Item $dir -Recurse -Force -ErrorAction Stop + } catch { + error "Couldn't remove '$(friendly_path $dir)'; it may be in use." + return $false + } + } + if (Test-Path ($currentDir = Join-Path $appDir 'current')) { + attrib $currentDir -R /L + Remove-Item $currentDir -ErrorAction Stop -Force + } + if (!(Get-ChildItem $appDir)) { + try { + # if last install failed, the directory seems to be locked and this + # will throw an error about the directory not existing + Remove-Item $appDir -Recurse -Force -ErrorAction Stop + } catch { + if ((Test-Path $appDir)) { throw } # only throw if the dir still exists + } + } + + # purge persistant data + if ($purge) { + Write-Host 'Removing persisted data.' + $persist_dir = persistdir $app $global + + if (Test-Path $persist_dir) { + try { + Remove-Item $persist_dir -Recurse -Force -ErrorAction Stop + } catch { + error "Couldn't remove '$(friendly_path $persist_dir)'; it may be in use." + return $false + } + } + } + + success "'$app' was uninstalled." + return $true +} diff --git a/libexec/scoop-uninstall.ps1 b/libexec/scoop-uninstall.ps1 index 5bdd57e5d1..9a8fcc9f41 100644 --- a/libexec/scoop-uninstall.ps1 +++ b/libexec/scoop-uninstall.ps1 @@ -10,6 +10,7 @@ . "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' 'Select-CurrentVersion' (indirectly) . "$PSScriptRoot\..\lib\system.ps1" . "$PSScriptRoot\..\lib\install.ps1" +. "$PSScriptRoot\..\lib\uninstall.ps1" # 'uninstall_app' . "$PSScriptRoot\..\lib\shortcuts.ps1" . "$PSScriptRoot\..\lib\psmodules.ps1" . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' @@ -38,113 +39,25 @@ if ($global -and !(is_admin)) { if ($apps -eq 'scoop') { & "$PSScriptRoot\..\bin\uninstall.ps1" $global $purge - exit + exit 0 } $apps = Confirm-InstallationStatus $apps -Global:$global -if (!$apps) { exit 0 } -:app_loop foreach ($_ in $apps) { +$apps | ForEach-Object { ($app, $global) = $_ - $version = Select-CurrentVersion -AppName $app -Global:$global - $appDir = appdir $app $global - if ($version) { - Write-Host "Uninstalling '$app' ($version)." - - $dir = versiondir $app $version $global - $persist_dir = persistdir $app $global - - $manifest = installed_manifest $app $version $global - $install = install_info $app $version $global - $architecture = $install.architecture - - Invoke-HookScript -HookType 'pre_uninstall' -Manifest $manifest -Arch $architecture - - #region Workaround for #2952 - if (test_running_process $app $global) { - continue - } - #endregion Workaround for #2952 - - try { - Test-Path $dir -ErrorAction Stop | Out-Null - } catch [UnauthorizedAccessException] { - error "Access denied: $dir. You might need to restart." - continue - } - - Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Global $global -Uninstall - rm_shims $app $manifest $global $architecture - rm_startmenu_shortcuts $manifest $global $architecture - - # If a junction was used during install, that will have been used - # as the reference directory. Otherwise it will just be the version - # directory. - $refdir = unlink_current $dir - - uninstall_psmodule $manifest $refdir $global - - env_rm_path $manifest $refdir $global $architecture - env_rm $manifest $global $architecture - - try { - # unlink all potential old link before doing recursive Remove-Item - unlink_persist_data $manifest $dir - Remove-Item $dir -Recurse -Force -ErrorAction Stop - } catch { - if (Test-Path $dir) { - error "Couldn't remove '$(friendly_path $dir)'; it may be in use." - continue - } - } - - Invoke-HookScript -HookType 'post_uninstall' -Manifest $manifest -Arch $architecture - } - # remove older versions - $oldVersions = @(Get-ChildItem $appDir -Name -Exclude 'current') - foreach ($version in $oldVersions) { - Write-Host "Removing older version ($version)." - $dir = versiondir $app $version $global - try { - # unlink all potential old link before doing recursive Remove-Item - unlink_persist_data $manifest $dir - Remove-Item $dir -Recurse -Force -ErrorAction Stop - } catch { - error "Couldn't remove '$(friendly_path $dir)'; it may be in use." - continue app_loop - } - } - if (Test-Path ($currentDir = Join-Path $appDir 'current')) { - attrib $currentDir -R /L - Remove-Item $currentDir -ErrorAction Stop -Force - } - if (!(Get-ChildItem $appDir)) { - try { - # if last install failed, the directory seems to be locked and this - # will throw an error about the directory not existing - Remove-Item $appdir -Recurse -Force -ErrorAction Stop - } catch { - if ((Test-Path $appdir)) { throw } # only throw if the dir still exists - } + try { + $succ = uninstall_app -app $app -global $global -purge $purge + $errMsg = "Failed to uninstall $app." + } catch { + $succ = $false + $errMsg = "Failed to uninstall $app : $($_.Exception.Message)." } - # purge persistant data - if ($purge) { - Write-Host 'Removing persisted data.' - $persist_dir = persistdir $app $global - - if (Test-Path $persist_dir) { - try { - Remove-Item $persist_dir -Recurse -Force -ErrorAction Stop - } catch { - error "Couldn't remove '$(friendly_path $persist_dir)'; it may be in use." - continue - } - } + if (-not $succ) { + error $errMsg } - - success "'$app' was uninstalled." } exit 0