Private/Set-WingetPackageVersion.ps1
|
function Set-WingetPackageVersion { <# .SYNOPSIS Move an installed package to an exact version (up or down) and verify it. .DESCRIPTION Installing an older version on top of a newer one is unreliable: MSI packages usually refuse while WinGet still reports 'Ok', and a package can switch installer technology between versions (VLC: MSI 3.0.23 vs EXE 3.0.21), which leaves two registered copies sharing one folder. So downgrades remove the package first and then install the pinned version. Every path ends by checking the version WinGet actually sees. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Id, [Parameter(Mandatory)][string]$Version, [string]$Source ) $getInstalled = { @(Microsoft.WinGet.Client\Get-WinGetPackage -Id $Id -MatchOption EqualsCaseInsensitive -ErrorAction SilentlyContinue) | ForEach-Object { [string]$_.InstalledVersion } } $isTarget = { param($v) $v -and (Compare-WingetVersion -ReferenceVersion $v -DifferenceVersion $Version) -eq 0 } $fail = { param($status, $message) [PSCustomObject]@{ Id = $Id; Action = 'Install'; Succeeded = $false; Status = $status; RebootRequired = $false; Message = $message } } # Remove every installed copy. The COM API cannot target one of several installed # versions (it returns MULTIPLE_APPLICATIONS_FOUND), so fall back to the CLI. $removeAll = { $winget = Get-Command winget -ErrorAction SilentlyContinue # Uninstalling one copy can re-expose another registration, so re-check a few times for ($attempt = 1; $attempt -le 3; $attempt++) { if (@(& $getInstalled).Count -eq 0) { return $true } $r = Invoke-WingetPackageAction -Action Uninstall -Id $Id -Source $Source -Options @{ Mode = 'Silent' } if (-not $r.Succeeded -and $winget) { & $winget.Source uninstall --id $Id --exact --all-versions --silent --disable-interactivity --accept-source-agreements 2>&1 | Out-Null } } return (@(& $getInstalled).Count -eq 0) } $current = @(& $getInstalled) if ($current.Count -eq 1 -and (& $isTarget $current[0])) { return [PSCustomObject]@{ Id = $Id; Action = 'Install'; Succeeded = $true; Status = 'Ok'; RebootRequired = $false; Message = 'Already at target version.' } } $isDowngrade = @($current | Where-Object { (Compare-WingetVersion -ReferenceVersion $_ -DifferenceVersion $Version) -gt 0 }).Count -gt 0 if ($isDowngrade -or $current.Count -gt 1) { Write-Verbose "Removing $Id ($($current -join ', ')) before installing $Version" if (-not (& $removeAll)) { return (& $fail 'UninstallFailed' "Could not remove the installed version(s) $($current -join ', ') before installing $Version.") } $result = Invoke-WingetPackageAction -Action Install -Id $Id -Version $Version -Source $Source -Options @{ Mode = 'Silent' } } else { $result = Invoke-WingetPackageAction -Action Install -Id $Id -Version $Version -Source $Source -Options @{ Mode = 'Silent'; Force = $true } } if ($result.Succeeded) { $after = @(& $getInstalled) if ($after.Count -ne 1 -or -not (& $isTarget $after[0])) { $result.Succeeded = $false $result.Status = 'VersionMismatch' $result.Message = "WinGet reported success but installed version(s) are: $(if ($after) { $after -join ', ' } else { 'none' }), expected $Version." } } return $result } |