Private/Download-WingetApp.ps1
|
function Download-WingetApp { <# .SYNOPSIS Downloads a winget package's installer into the project's Files\ folder. .DESCRIPTION Fixes two real defects in the original implementation: 1. IT REPORTED SUCCESS WHEN WINGET FAILED. The old version returned $true unless PowerShell itself threw — and a native command failing does NOT throw. A non-zero winget exit (package not found, no installer for the requested architecture, network failure) therefore produced an EMPTY Files\ folder that every downstream step treated as a valid download: the project got scaffolded, the sandbox "tested" nothing, and detection rules were generated from an install that never happened. Now: `& winget @wingetArgs`, check $LASTEXITCODE, and verify an installer actually landed. 2. INVOKE-EXPRESSION ON AN UNTRUSTED VALUE. The command was assembled as a STRING containing $AppId — a winget-supplied identifier — and run through Invoke-Expression, so an id carrying shell metacharacters was a code-execution surface on the packaging host. The arguments are now passed as an ARRAY: no shell parsing, nothing to escape. Mirrors Download-OldVersionInstaller, which already did both correctly. .PARAMETER AppId winget PackageIdentifier (e.g. 'Git.Git'). Passed as an argument, never spliced into a command line. .PARAMETER AppName Display name, for progress output only. .PARAMETER DownloadPath Destination folder (the project's Files\). .PARAMETER Architecture x64 | x86 | arm64. 'all' or empty lets winget choose. .OUTPUTS [bool] $true ONLY if winget exited 0 AND an installer file is actually present. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$AppId, [string]$AppName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$DownloadPath, [string]$Architecture ) $label = if ($AppName) { "$AppName ($AppId)" } else { $AppId } Write-Verbose "Downloading $label [$Architecture]..." if (-not (Test-Path -LiteralPath $DownloadPath)) { New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null } # Argument ARRAY — no shell, no Invoke-Expression, nothing to escape. $wingetArgs = @( 'download' '--id', $AppId '--download-directory', $DownloadPath '--accept-source-agreements' '--accept-package-agreements' ) if ($Architecture -and $Architecture -ne 'all') { $wingetArgs += '--architecture' $wingetArgs += $Architecture } try { & winget @wingetArgs } catch { Write-Error "Failed to run winget for $label : $($_.Exception.Message)" return $false } # A native command failing does NOT throw — this check is the entire point of the fix. if ($LASTEXITCODE -ne 0) { Write-Error "winget download exited with code $LASTEXITCODE for $label — no usable installer was downloaded." return $false } # Belt and braces: winget can exit 0 having written only a manifest (a zip/portable/store package), # which would leave the project with nothing to install. # Bundles count as installers: .msixbundle/.appxbundle install and uninstall exactly like a plain # package (see Get-Win32ToolkitInstallerExtension). Note winget names the file after the manifest's # InstallerType, NOT the URL — Microsoft.PowerShell's manifest says 'msix' while serving a # .msixbundle, so a bundle routinely arrives here already called '.msix'. Extension-based bundle # handling is therefore useless; identity is content-detected downstream. $produced = @(Get-ChildItem -LiteralPath $DownloadPath -File -ErrorAction SilentlyContinue) $installer = @($produced | Where-Object { $_.Extension -in (Get-Win32ToolkitInstallerExtension) }) if ($installer.Count -eq 0) { Write-Error "winget reported success for $label but wrote no installer (.exe/.msi/.msix/.appx/.msixbundle/.appxbundle) to '$DownloadPath'. This is usually a zip/portable/store package with no silent installer." return $false } Write-Host "✓ Downloaded: $($installer[0].Name)" -ForegroundColor Green return $true } |