From 3f892493c015e63f2e98cc1a36ecdb3fd8c7f558 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:33:09 -0700 Subject: [PATCH] Improves Winget installer selection logic Refines the post-download cleanup process for applications with multiple installers. When multiple installers are downloaded for an application, this change introduces logic to select the most appropriate one based on the target Windows architecture. It prioritizes universal installers first, then architecture-specific installers, before falling back to the previous method of selecting the newest file by signature date. This ensures a more accurate installer is chosen, especially when Winget provides multiple architecture options for a single package. --- .../FFU.Common/FFU.Common.Parallel.psm1 | 1 + .../FFU.Common/FFU.Common.Winget.psm1 | 95 +++++++++++++++---- .../FFUUI.Core/FFUUI.Core.Winget.psm1 | 16 ++-- 3 files changed, 85 insertions(+), 27 deletions(-) diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1 index fde80cb..308d61e 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1 @@ -163,6 +163,7 @@ function Invoke-ParallelProcessing { AppsPath = $localJobArgs['AppsPath'] OrchestrationPath = $localJobArgs['OrchestrationPath'] ProgressQueue = $localProgressQueue + SelectedWindowsArch = $localJobArgs['SelectedWindowsArch'] } $taskResult = Start-WingetAppDownloadTask @wingetTaskArgs if ($null -ne $taskResult) { diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 index 624623d..9d7f036 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 @@ -23,7 +23,8 @@ function Get-Application { [string]$WindowsArch, [Parameter(Mandatory = $true)] [string]$OrchestrationPath, - [switch]$SkipWin32Json + [switch]$SkipWin32Json, + [string]$SelectedWindowsArch ) # Block Company Portal from winget source @@ -252,26 +253,84 @@ function Get-Application { } } - # Clean up multiple versions (keep only the latest) - WriteLog "$AppName has completed downloading. Identifying the latest version of $AppName." + # Clean up multiple versions honoring SelectedWindowsArch (keep only one installer) + WriteLog "$AppName has completed downloading. Evaluating installer set for pruning." $packages = Get-ChildItem -Path "$appFolderPath\*" -Exclude "Dependencies\*", "*.xml", "*.yaml" -File -ErrorAction Stop - - # Find latest version based on signature date - $latestPackage = $packages | Sort-Object { (Get-AuthenticodeSignature $_.FullName).SignerCertificate.NotBefore } -Descending | Select-Object -First 1 - - # Remove older versions - WriteLog "Latest version of $AppName has been identified as $latestPackage. Removing old versions of $AppName that may have downloaded." - foreach ($package in $packages) { - if ($package.FullName -ne $latestPackage.FullName) { - try { - WriteLog "Removing $($package.FullName)" - Remove-Item -Path $package.FullName -Force + if ($packages.Count -gt 1 -and $SelectedWindowsArch) { + WriteLog "SelectedWindowsArch provided for pruning: $SelectedWindowsArch" + # Detect universal bundles (contain x86,x64,arm64 in name) + $universalCandidates = $packages | Where-Object { + $base = $_.BaseName + # Split base name into tokens to avoid partial matches (e.g. arm inside arm64) + $tokens = ($base -split '[\.\-_]') | ForEach-Object { $_.ToLower() } + # Architecture tokens we recognize + $archTokens = @('x86', 'x64', 'arm', 'arm64') + # Distinct matched architecture tokens + $matched = $tokens | Where-Object { $_ -in $archTokens } | Select-Object -Unique + if ($matched.Count -ge 2) { + WriteLog "Multi-architecture bundle detected: $base (tokens: $($matched -join ', '))" + $true } - catch { - WriteLog "Failed to delete: $($package.FullName) - $_" - throw $_ + else { + $false } } + if ($universalCandidates) { + WriteLog "Universal bundle candidate(s) detected: $($universalCandidates.Name -join ', ')" + $candidateSet = $universalCandidates + } + else { + $archToken = switch -Regex ($SelectedWindowsArch.ToLower()) { + '^x64$' { 'x64' ; break } + '^x86$' { 'x86' ; break } + '^arm64$' { 'arm64' ; break } + default { $SelectedWindowsArch.ToLower() } + } + $archMatches = $packages | Where-Object { $_.BaseName -match "(?i)$archToken" } + if ($archMatches) { + WriteLog "Architecture-specific candidates matching '$archToken': $($archMatches.Name -join ', ')" + $candidateSet = $archMatches + } + else { + WriteLog "No installer filename matched '$archToken'. Falling back to all installers." + $candidateSet = $packages + } + } + # From candidate set, choose latest by signature date + $latestPackage = $candidateSet | Sort-Object { (Get-AuthenticodeSignature $_.FullName).SignerCertificate.NotBefore } -Descending | Select-Object -First 1 + WriteLog "Retaining installer: $($latestPackage.Name)" + foreach ($package in $packages) { + if ($package.FullName -ne $latestPackage.FullName) { + try { + WriteLog "Removing $($package.FullName)" + Remove-Item -Path $package.FullName -Force + } + catch { + WriteLog "Failed to delete: $($package.FullName) - $_" + throw $_ + } + } + } + } + elseif ($packages.Count -gt 1) { + WriteLog "Multiple installers present but no SelectedWindowsArch supplied. Using original latest-version logic." + $latestPackage = $packages | Sort-Object { (Get-AuthenticodeSignature $_.FullName).SignerCertificate.NotBefore } -Descending | Select-Object -First 1 + WriteLog "Retaining latest by signature date: $($latestPackage.Name)" + foreach ($package in $packages) { + if ($package.FullName -ne $latestPackage.FullName) { + try { + WriteLog "Removing $($package.FullName)" + Remove-Item -Path $package.FullName -Force + } + catch { + WriteLog "Failed to delete: $($package.FullName) - $_" + throw $_ + } + } + } + } + else { + WriteLog "Single installer present; no pruning required." } } } # End foreach ($arch in $architecturesToDownload) @@ -348,7 +407,7 @@ function Get-Apps { foreach ($storeApp in $StoreApps) { try { $appArch = if ($storeApp.PSObject.Properties['architecture']) { $storeApp.architecture } else { $WindowsArch } - Get-Application -AppName $storeApp.Name -AppId $storeApp.Id -Source 'msstore' -AppsPath $AppsPath -WindowsArch $appArch -OrchestrationPath $OrchestrationPath + Get-Application -AppName $storeApp.Name -AppId $storeApp.Id -Source 'msstore' -AppsPath $AppsPath -WindowsArch $appArch -OrchestrationPath $OrchestrationPath -SelectedWindowsArch $WindowsArch } catch { WriteLog "Error occurred while processing $($storeApp.Name): $_" diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 index 58777bb..0ccb010 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 @@ -385,7 +385,8 @@ function Start-WingetAppDownloadTask { [Parameter(Mandatory = $true)] [string]$OrchestrationPath, [Parameter(Mandatory = $true)] - [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue # Add queue parameter + [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue, # Add queue parameter + [string]$SelectedWindowsArch ) $appName = $ApplicationItemData.Name @@ -398,10 +399,6 @@ function Start-WingetAppDownloadTask { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)." - # WriteLog "Apps Path: $($AppsPath)" - # WriteLog "AppList JSON Path: $($AppListJsonPath)" - # WriteLog "Windows Architecture: $($ApplicationItemData.Architecture)" - # WriteLog "Orchestration Path: $($OrchestrationPath)" try { # Define paths @@ -599,7 +596,7 @@ function Start-WingetAppDownloadTask { try { # Call Get-Application - $resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -AppsPath $AppsPath -WindowsArch $ApplicationItemData.Architecture -OrchestrationPath $OrchestrationPath -SkipWin32Json -ErrorAction Stop + $resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -AppsPath $AppsPath -WindowsArch $ApplicationItemData.Architecture -OrchestrationPath $OrchestrationPath -SkipWin32Json -SelectedWindowsArch $SelectedWindowsArch -ErrorAction Stop # Determine status based on result code switch ($resultCode) { @@ -718,9 +715,10 @@ function Invoke-WingetDownload { # Create hashtable for task-specific arguments to pass to Invoke-ParallelProcessing $taskArguments = @{ - AppsPath = $localAppsPath - AppListJsonPath = $localAppListJsonPath - OrchestrationPath = $localOrchestrationPath + AppsPath = $localAppsPath + AppListJsonPath = $localAppListJsonPath + OrchestrationPath = $localOrchestrationPath + SelectedWindowsArch = $localWindowsArch } # Select only necessary properties before passing to Invoke-ParallelProcessing