diff --git a/CHANGELOG.md b/CHANGELOG.md index 68165f684c..aff194921f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - **install:** Add separator at the end of notes, highlight suggestions ([#6418](https://github.com/ScoopInstaller/Scoop/issues/6418)) - **download|scoop-download:** Add GitHub issue prompt when the default downloader fails ([#6539](https://github.com/ScoopInstaller/Scoop/issues/6539)) - **download|scoop-config:** Allow disabling automatic fallback to the default downloader when Aria2c download fails ([#6538](https://github.com/ScoopInstaller/Scoop/issues/6538)) +- **scoop-install|update:** Update extraction tools ahead of installs/updates ([#6572](https://github.com/ScoopInstaller/Scoop/issues/6572)) ### Bug Fixes diff --git a/lib/core.ps1 b/lib/core.ps1 index 63be1a5c4f..99e281fd1c 100644 --- a/lib/core.ps1 +++ b/lib/core.ps1 @@ -474,12 +474,14 @@ function Get-HelperPath { '7zip' { $HelperPath = Get-AppFilePath '7zip' '7z.exe' } 'Lessmsi' { $HelperPath = Get-AppFilePath 'lessmsi' 'lessmsi.exe' } 'Innounp' { + # Changes to the extraction tool priority should be synced with the Get-OutdatedHelper function as well $HelperPath = Get-AppFilePath 'innounp-unicode' 'innounp.exe' if ([String]::IsNullOrEmpty($HelperPath)) { $HelperPath = Get-AppFilePath 'innounp' 'innounp.exe' } } 'Dark' { + # Changes to the extraction tool priority should be synced with the Get-OutdatedHelper function as well $HelperPath = Get-AppFilePath 'dark' 'dark.exe' if ([String]::IsNullOrEmpty($HelperPath)) { $HelperPath = Get-AppFilePath 'wixtoolset' 'wix.exe' diff --git a/lib/depends.ps1 b/lib/depends.ps1 index 3a38ca2b23..9ddee13b99 100644 --- a/lib/depends.ps1 +++ b/lib/depends.ps1 @@ -156,3 +156,92 @@ function Test-LessmsiRequirement { ) return ($Uri | Where-Object { $_ -match '\.msi$' }).Count -gt 0 } + +function Get-OutdatedHelper { + <# + .SYNOPSIS + Get outdated installation helpers + .PARAMETER Manifest + App's Manifest + .PARAMETER Architecture + Architecture of the app + .OUTPUTS + [Object[]] + A list of concrete outdated helper apps, each represented as a PSCustomObject with 'App' and 'Global' properties + .NOTES + helper | concrete helper name + 7zip | 7zip + lessmsi | lessmsi + innounp | innounp-unicode/innounp + dark | dark/wixtoolset + #> + [CmdletBinding()] + [OutputType([Object[]])] + param ( + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [PSObject] + $Manifest, + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [String] + $Architecture + ) + + begin { + $helpers = @() + } + + process { + $helpers += Get-InstallationHelper -Manifest $Manifest -Architecture $Architecture -All | Where-Object { + (Test-HelperInstalled -Helper $_) -and ($helpers -notcontains $_) + } + } + + end { + $outdated = @() + + foreach ($helper in $helpers) { + # Get the concrete app name + $app = switch ($helper) { + '7zip' { '7zip' } + 'lessmsi' { 'lessmsi' } + 'innounp' { if (installed 'innounp-unicode') { 'innounp-unicode' } else { 'innounp' } } + 'dark' { if (installed 'dark') { 'dark' } else { 'wixtoolset' } } + default { $null } + } + + if (-not $app) { + continue + } + + $global = installed $app $true + $status = app_status $app $global + + if (-not ($status.installed -and $status.outdated)) { + continue + } + + warn ("Outdated extraction tool '$app' detected: $($status.version) -> $($status.latest_version){0}." -f ('', ' (global)')[$global]) + + # Filter out outdated helpers that are held + if ($status.hold) { + warn "Skipping update of '$app' because it is held at version $($status.version)." + warn ("Outdated extraction tool may cause decompression errors. Please run 'scoop unhold $app{0}' to unhold it." -f ('', ' -g')[$global]) + continue + } + + # Filter out outdated helpers that are blocked by permission issues + if ((-not (is_admin)) -and $global) { + warn "Skipping update of '$app' because it is globally installed and requires admin rights to update." + warn "Outdated extraction tool may cause decompression errors. Please run 'scoop update $app -g' to update it." + continue + } + + $outdated += [PSCustomObject]@{ + App = $app + Global = $global + } + } + + return , $outdated + } +} diff --git a/lib/install.ps1 b/lib/install.ps1 index 78acdd252b..152ec69752 100644 --- a/lib/install.ps1 +++ b/lib/install.ps1 @@ -110,7 +110,7 @@ function Invoke-Installer { if ($installer.file -or $installer.args) { # Installer filename is either explicit defined ('installer.file') or file name in the first URL if (!$Name) { - $Name = url_filename @(url $manifest $architecture) + $Name = url_filename @(url $manifest $ProcessorArchitecture) } $progName = "$Path\$(coalesce $installer.file $Name[0])" if (!(is_in_dir $Path $progName)) { diff --git a/lib/update.ps1 b/lib/update.ps1 new file mode 100644 index 0000000000..3aa3f0e866 --- /dev/null +++ b/lib/update.ps1 @@ -0,0 +1,126 @@ +function update($app, $global, $force, $quiet = $false, $independent, $suggested, $use_cache = $true, $check_hash = $true) { + $old_version = Select-CurrentVersion -AppName $app -Global:$global + $old_manifest = installed_manifest $app $old_version $global + $install = install_info $app $old_version $global + + # re-use architecture, bucket and url from first install + $architecture = Format-ArchitectureString $install.architecture + $bucket = $install.bucket + if ($null -eq $bucket) { + $bucket = 'main' + } + $url = $install.url + + $manifest = manifest $app $bucket $url + $version = $manifest.version + $is_nightly = $version -eq 'nightly' + if ($is_nightly) { + $version = nightly_version $quiet + $check_hash = $false + } + + if (!$force -and ($old_version -eq $version)) { + if (!$quiet) { + warn "The latest version of '$app' ($version) is already installed." + } + return + } + if (!$version) { + # installed from a custom bucket/no longer supported + error "No manifest available for '$app'." + return + } + + Write-Host "Updating '$app' ($old_version -> $version)" + + #region Workaround for #2952 + if (test_running_process $app $global) { + Write-Host 'Running process detected, skip updating.' + return + } + #endregion Workaround for #2952 + + # region Workaround + # Workaround for https://github.com/ScoopInstaller/Scoop/issues/2220 until install is refactored + # Remove and replace whole region after proper fix + Write-Host 'Downloading new version' + if (Test-Aria2Enabled) { + Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $true $check_hash + } else { + $urls = script:url $manifest $architecture + + foreach ($url in $urls) { + Invoke-CachedDownload $app $version $url $null $manifest.cookie $true + + if ($check_hash) { + $manifest_hash = hash_for_url $manifest $url $architecture + $source = cache_path $app $version $url + $ok, $err = check_hash $source $manifest_hash $(show_app $app $bucket) + + if (!$ok) { + error $err + if (Test-Path $source) { + # rm cached file + Remove-Item -Force $source + } + if ($url.Contains('sourceforge.net')) { + Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.' + } + abort $(new_issue_msg $app $bucket 'hash check failed') + } + } + } + } + # There is no need to check hash again while installing + $check_hash = $false + # endregion Workaround + + $dir = versiondir $app $old_version $global + $persist_dir = persistdir $app $global + + Invoke-HookScript -HookType 'pre_uninstall' -Manifest $old_manifest -Arch $architecture + + Write-Host "Uninstalling '$app' ($old_version)" + Invoke-Installer -Path $dir -Manifest $old_manifest -ProcessorArchitecture $architecture -Global:$global -Uninstall + rm_shims $app $old_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 $old_manifest $refdir $global + env_rm_path $old_manifest $refdir $global $architecture + env_rm $old_manifest $global $architecture + + if ($force -and ($old_version -eq $version)) { + if (!(Test-Path "$dir/../_$version.old")) { + Move-Item "$dir" "$dir/../_$version.old" + } else { + $i = 1 + while (Test-Path "$dir/../_$version.old($i)") { + $i++ + } + Move-Item "$dir" "$dir/../_$version.old($i)" + } + } + + Invoke-HookScript -HookType 'post_uninstall' -Manifest $old_manifest -Arch $architecture + + if ($bucket) { + # add bucket name it was installed from + $app = "$bucket/$app" + } + if ($install.url) { + # use the url of the install json if the application was installed through url + $app = $install.url + } + + if ($independent) { + install_app $app $architecture $global $suggested $use_cache $check_hash + } else { + # Also add missing dependencies + $apps = @(Get-Dependency $app $architecture) -ne $app + ensure_none_failed $apps + $apps.Where({ !(installed $_) }) + $app | ForEach-Object { install_app $_ $architecture $global $suggested $use_cache $check_hash } + } +} diff --git a/libexec/scoop-install.ps1 b/libexec/scoop-install.ps1 index 6af199a8b8..09174b8959 100644 --- a/libexec/scoop-install.ps1 +++ b/libexec/scoop-install.ps1 @@ -33,6 +33,7 @@ . "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly) . "$PSScriptRoot\..\lib\system.ps1" . "$PSScriptRoot\..\lib\install.ps1" +. "$PSScriptRoot\..\lib\update.ps1" . "$PSScriptRoot\..\lib\download.ps1" . "$PSScriptRoot\..\lib\decompress.ps1" . "$PSScriptRoot\..\lib\shortcuts.ps1" @@ -65,7 +66,7 @@ if ($global -and !(is_admin)) { if (is_scoop_outdated) { if ($opt.u -or $opt.'no-update-scoop') { - warn "Scoop is out of date." + warn 'Scoop is out of date.' } else { & "$PSScriptRoot\scoop-update.ps1" } @@ -126,12 +127,24 @@ $skip | Where-Object { $explicit_apps -contains $_ } | ForEach-Object { warn "'$app' ($version) is already installed. Skipping." } +$outdated_helpers = $apps | ForEach-Object { + $null, $manifest, $null, $null = Get-Manifest $_ + [PSCustomObject]@{ + Manifest = $manifest + Architecture = $architecture + } +} | Get-OutdatedHelper + $suggested = @{ }; if ((Test-Aria2Enabled) -and (get_config 'aria2-warning-enabled' $true)) { warn "Scoop uses 'aria2c' for multi-connection downloads." warn "Should it cause issues, run 'scoop config aria2-enabled false' to disable it." warn "To disable this warning, run 'scoop config aria2-warning-enabled false'." } + +# Update extraction tools ahead of installs/updates +$outdated_helpers | ForEach-Object { update $_.App $_.Global $false $false $independent $suggested $use_cache $check_hash } + $apps | ForEach-Object { install_app $_ $architecture $global $suggested $use_cache $check_hash } show_suggestions $suggested diff --git a/libexec/scoop-update.ps1 b/libexec/scoop-update.ps1 index bc590a13f6..1c72b412be 100644 --- a/libexec/scoop-update.ps1 +++ b/libexec/scoop-update.ps1 @@ -24,6 +24,7 @@ . "$PSScriptRoot\..\lib\versions.ps1" . "$PSScriptRoot\..\lib\depends.ps1" . "$PSScriptRoot\..\lib\install.ps1" +. "$PSScriptRoot\..\lib\update.ps1" . "$PSScriptRoot\..\lib\download.ps1" if (get_config USE_SQLITE_CACHE) { . "$PSScriptRoot\..\lib\database.ps1" @@ -63,7 +64,7 @@ $show_update_log = get_config SHOW_UPDATE_LOG $true function Sync-Scoop { [CmdletBinding()] - Param ( + param ( [Switch]$Log ) # Test if Scoop Core is hold @@ -153,7 +154,7 @@ function Sync-Scoop { } function Sync-Bucket { - Param ( + param ( [Switch]$Log ) Write-Host 'Updating Buckets...' @@ -258,133 +259,6 @@ function Sync-Bucket { } } -function update($app, $global, $quiet = $false, $independent, $suggested, $use_cache = $true, $check_hash = $true) { - $old_version = Select-CurrentVersion -AppName $app -Global:$global - $old_manifest = installed_manifest $app $old_version $global - $install = install_info $app $old_version $global - - # re-use architecture, bucket and url from first install - $architecture = Format-ArchitectureString $install.architecture - $bucket = $install.bucket - if ($null -eq $bucket) { - $bucket = 'main' - } - $url = $install.url - - $manifest = manifest $app $bucket $url - $version = $manifest.version - $is_nightly = $version -eq 'nightly' - if ($is_nightly) { - $version = nightly_version $quiet - $check_hash = $false - } - - if (!$force -and ($old_version -eq $version)) { - if (!$quiet) { - warn "The latest version of '$app' ($version) is already installed." - } - return - } - if (!$version) { - # installed from a custom bucket/no longer supported - error "No manifest available for '$app'." - return - } - - Write-Host "Updating '$app' ($old_version -> $version)" - - #region Workaround for #2952 - if (test_running_process $app $global) { - Write-Host 'Running process detected, skip updating.' - return - } - #endregion Workaround for #2952 - - # region Workaround - # Workaround for https://github.com/ScoopInstaller/Scoop/issues/2220 until install is refactored - # Remove and replace whole region after proper fix - Write-Host 'Downloading new version' - if (Test-Aria2Enabled) { - Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $true $check_hash - } else { - $urls = script:url $manifest $architecture - - foreach ($url in $urls) { - Invoke-CachedDownload $app $version $url $null $manifest.cookie $true - - if ($check_hash) { - $manifest_hash = hash_for_url $manifest $url $architecture - $source = cache_path $app $version $url - $ok, $err = check_hash $source $manifest_hash $(show_app $app $bucket) - - if (!$ok) { - error $err - if (Test-Path $source) { - # rm cached file - Remove-Item -Force $source - } - if ($url.Contains('sourceforge.net')) { - Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.' - } - abort $(new_issue_msg $app $bucket 'hash check failed') - } - } - } - } - # There is no need to check hash again while installing - $check_hash = $false - # endregion Workaround - - $dir = versiondir $app $old_version $global - $persist_dir = persistdir $app $global - - Invoke-HookScript -HookType 'pre_uninstall' -Manifest $old_manifest -Arch $architecture - - Write-Host "Uninstalling '$app' ($old_version)" - Invoke-Installer -Path $dir -Manifest $old_manifest -ProcessorArchitecture $architecture -Global:$global -Uninstall - rm_shims $app $old_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 $old_manifest $refdir $global - env_rm_path $old_manifest $refdir $global $architecture - env_rm $old_manifest $global $architecture - - if ($force -and ($old_version -eq $version)) { - if (!(Test-Path "$dir/../_$version.old")) { - Move-Item "$dir" "$dir/../_$version.old" - } else { - $i = 1 - While (Test-Path "$dir/../_$version.old($i)") { - $i++ - } - Move-Item "$dir" "$dir/../_$version.old($i)" - } - } - - Invoke-HookScript -HookType 'post_uninstall' -Manifest $old_manifest -Arch $architecture - - if ($bucket) { - # add bucket name it was installed from - $app = "$bucket/$app" - } - if ($install.url) { - # use the url of the install json if the application was installed through url - $app = $install.url - } - - if ($independent) { - install_app $app $architecture $global $suggested $use_cache $check_hash - } else { - # Also add missing dependencies - $apps = @(Get-Dependency $app $architecture) -ne $app - ensure_none_failed $apps - $apps.Where({ !(installed $_) }) + $app | ForEach-Object { install_app $_ $architecture $global $suggested $use_cache $check_hash } - } -} - if (-not ($apps -or $all)) { if ($global) { error 'scoop update: --global is invalid when is not specified.' @@ -431,8 +305,10 @@ if (-not ($apps -or $all)) { $status = app_status $app $global if ($status.installed -and ($force -or $status.outdated)) { if (!$status.hold) { - $outdated += applist $app $global - Write-Host -f yellow ("$app`: $($status.version) -> $($status.latest_version){0}" -f ('', ' (global)')[$global]) + $outdated += [PSCustomObject]@{ + App = $app + Global = $global + } } else { warn "'$app' is held to version $($status.version)" } @@ -446,6 +322,24 @@ if (-not ($apps -or $all)) { } } + # Update extraction tools ahead of installs/updates + $outdated_helpers = $outdated | ForEach-Object { + $version = Select-CurrentVersion -AppName $_.App -Global:$($_.Global) + $install = install_info $_.App $version $_.Global + + [PSCustomObject]@{ + Manifest = manifest $_.App $install.bucket $install.url + Architecture = Format-ArchitectureString -Architecture $install.architecture + } + } | Get-OutdatedHelper + $outdated = $outdated | Where-Object { $_.App -notin $outdated_helpers.App } + $outdated = $outdated_helpers + $outdated + + $outdated | ForEach-Object { + $status = app_status $_.App $_.Global + Write-Host -f yellow ("$($_.App)`: $($status.version) -> $($status.latest_version){0}" -f ('', ' (global)')[$_.Global]) + } + if ($outdated -and ((Test-Aria2Enabled) -and (get_config 'aria2-warning-enabled' $true))) { warn "Scoop uses 'aria2c' for multi-connection downloads." warn "Should it cause issues, run 'scoop config aria2-enabled false' to disable it." @@ -461,8 +355,7 @@ if (-not ($apps -or $all)) { } $suggested = @{} - # $outdated is a list of ($app, $global) tuples - $outdated | ForEach-Object { update @_ $quiet $independent $suggested $use_cache $check_hash } + $outdated | ForEach-Object { update $_.App $_.Global $force $quiet $independent $suggested $use_cache $check_hash } } exit 0