Private/Install-Prerequisite.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Installs missing tools via winget and re-detects them afterwards. .DESCRIPTION The re-detection is the entire point. Before 1.4.0 the Phase 4b handler ran winget and then returned without touching the prerequisites hashtable, so the readiness check that followed still saw Found=$false and refused to install - even though the tool had just been installed successfully. A second, working copy of this logic sat unreachable inside Start-Installation. There is now exactly one implementation, and "did it work" is answered by detection rather than by winget's exit code, which reports failure for harmless cases such as the package already being present. #> function Invoke-WingetInstall { <# .SYNOPSIS Installs one winget package, preferring the per-user scope. .DESCRIPTION User scope avoids the UAC prompt and keeps the install inside the profile, which is what this module needs - it never requires admin rights. Not every package supports --scope user, so a scoped failure retries unscoped. .OUTPUTS Hashtable with ExitCode and Scope. Exit codes are advisory only. #> [OutputType([hashtable])] param( [Parameter(Mandatory = $true)][string]$PackageId, [string]$LogPath ) $commonArgs = @( 'install', '--id', $PackageId, '--exact', '--silent', '--accept-package-agreements', '--accept-source-agreements' ) Write-InstallLog "winget install $PackageId --scope user" -LogPath $LogPath $output = & winget.exe @commonArgs --scope user 2>&1 | ForEach-Object { "$_" } $exitCode = $LASTEXITCODE Write-InstallLog "winget (user scope) exit code $exitCode" -LogPath $LogPath if ($exitCode -ne 0) { # Log sanitised: winget echoes URLs and occasionally environment data. foreach ($line in ($output | Select-Object -Last 5)) { Write-InstallLog "winget: $line" -LogPath $LogPath -Level WARN } Write-InstallLog "Retrying $PackageId without an explicit scope" -LogPath $LogPath $output = & winget.exe @commonArgs 2>&1 | ForEach-Object { "$_" } $exitCode = $LASTEXITCODE Write-InstallLog "winget (default scope) exit code $exitCode" -LogPath $LogPath return @{ ExitCode = $exitCode; Scope = 'default' } } return @{ ExitCode = $exitCode; Scope = 'user' } } function Install-MissingPrerequisite { <# .SYNOPSIS Installs Python and/or FFmpeg if missing, then re-detects both. .PARAMETER Prerequisites The detection results. Returned updated - callers must use the return value rather than relying on the argument being mutated. .OUTPUTS The updated prerequisites hashtable. #> [OutputType([hashtable])] param( [Parameter(Mandatory = $true)][hashtable]$Prerequisites, [string]$LogPath ) if (-not (Test-WingetPresent)) { Write-Host " winget is not available on this system." -ForegroundColor Red Write-Host " Install 'App Installer' from the Microsoft Store, or install the" -ForegroundColor Yellow Write-Host " missing tools manually and run the installer again." -ForegroundColor Yellow Write-InstallLog "winget not available - cannot auto-install" -LogPath $LogPath -Level ERROR return $Prerequisites } $targets = @() if (-not $Prerequisites.Python.Found) { $targets += @{ Name = 'Python 3.12'; PackageId = 'Python.Python.3.12'; Key = 'Python' } } if (-not $Prerequisites.FFmpeg.Found) { $targets += @{ Name = 'FFmpeg'; PackageId = 'Gyan.FFmpeg'; Key = 'FFmpeg' } } foreach ($target in $targets) { Write-Host " Installing $($target.Name) via winget..." -ForegroundColor Gray $result = Invoke-WingetInstall -PackageId $target.PackageId -LogPath $LogPath if ($result.ExitCode -ne 0) { # Not fatal yet: winget reports non-zero when the package is # already installed, so let detection have the final word. Write-Host " winget reported exit code $($result.ExitCode) - verifying anyway..." -ForegroundColor Yellow } } if ($targets.Count -eq 0) { return $Prerequisites } # winget updates the persisted PATH, not the one this process inherited. Update-ProcessPath Write-Host " Re-checking..." -ForegroundColor Gray $updated = @{} foreach ($key in $Prerequisites.Keys) { $updated[$key] = $Prerequisites[$key] } foreach ($target in $targets) { $detected = switch ($target.Key) { 'Python' { Find-Python } 'FFmpeg' { Find-FFmpeg } } $updated[$target.Key] = $detected if ($detected.Found) { Write-Host " OK $($target.Name): $($detected.Path)" -ForegroundColor Green Write-InstallLog "$($target.Name) installed and verified at $($detected.Path)" -LogPath $LogPath } else { Write-Host " -- $($target.Name) still not detected: $($detected.Reason)" -ForegroundColor Red Write-InstallLog "$($target.Name) still not detected after install: $($detected.Reason)" -LogPath $LogPath -Level ERROR } } Write-Host "" return $updated } function Update-ProcessPath { <# .SYNOPSIS Rebuilds $env:PATH from the machine and user environment. .DESCRIPTION A freshly installed tool is only on the PATH of processes started after the install, so without this the current session keeps missing it. #> param() try { $machine = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') $user = [System.Environment]::GetEnvironmentVariable('PATH', 'User') $env:PATH = (@($machine, $user) | Where-Object { $_ }) -join ';' } catch { # Keep the inherited PATH; detection has filesystem fallbacks anyway. } } Export-ModuleMember -Function @( 'Install-MissingPrerequisite', 'Invoke-WingetInstall', 'Update-ProcessPath' ) |