From 6c279a8325e063deed6c1e74e6d4fb4245184423 Mon Sep 17 00:00:00 2001 From: ThioJoe <12518330+ThioJoe@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:07:37 -0700 Subject: [PATCH] VC Redist Installer - Add cached installer support and version checks - Add parameters to support using a local cache (-ExistingInstallerFilesPath) and control behavior (-ForceCachedFilesOnly, -NoCheckLatestVersion). - Change download target to the user's Downloads folder under "VCRedist Install" - Detect remote installer version and choose between cached installers or downloading updated files. --- Installer Scripts/Install VC Redist.ps1 | 181 +++++++++++++++++++++--- 1 file changed, 164 insertions(+), 17 deletions(-) diff --git a/Installer Scripts/Install VC Redist.ps1 b/Installer Scripts/Install VC Redist.ps1 index 9739994..3b0b860 100644 --- a/Installer Scripts/Install VC Redist.ps1 +++ b/Installer Scripts/Install VC Redist.ps1 @@ -1,3 +1,43 @@ +# Downloads the latest VC Redistributables from Microsoft + +param( + # Optional path to a local directory containing the installation files. If provided, the download steps will be skipped. + # - Just copy the entire "VCRedist Install" folder with the files the script normally downloads, and put it in your mounted folder to avoid having to re-download it. + # - Make sure to use the mounted path from the perspective of within the sandbox. + [string]$ExistingInstallerFilesPath, + + # If set, forces using the installer files from ExistingInstallerFilesPath even if a new version exists. Still warns about there being a new version. + [switch]$ForceCachedFilesOnly, + + # If set, implies ForceCachedFilesOnly and uses cached installer files only, but does not even bother checking for a latest version. + [switch]$NoCheckLatestVersion +) + +# --- Parameter Usage Examples --- +# Standard run (Download & Install): +# .\Install-VC-Redist.ps1 +# +# Install from existing files instead of downloading: +# .\Install-VC-Redist.ps1 -ExistingInstallerFilesPath "C:\Users\WDAGUtilityAccount\Desktop\HostShared\VCRedist Install" +# +# Use cached files only, even if outdated (still warns about new versions): +# .\Install-VC-Redist.ps1 -ExistingInstallerFilesPath "C:\Users\WDAGUtilityAccount\Desktop\HostShared\VCRedist Install" -ForceCachedFilesOnly +# +# Use cached files only, skip all version checks: +# .\Install-VC-Redist.ps1 -ExistingInstallerFilesPath "C:\Users\WDAGUtilityAccount\Desktop\HostShared\VCRedist Install" -NoCheckLatestVersion + +# ======================================================= + +# Validate parameter combinations +if ($NoCheckLatestVersion) { + $ForceCachedFilesOnly = [switch]::new($true) +} +if ($ForceCachedFilesOnly -and [string]::IsNullOrWhiteSpace($ExistingInstallerFilesPath)) { + Write-Host "Error: -ForceCachedFilesOnly requires -ExistingInstallerFilesPath to be specified." -ForegroundColor Red + Read-Host "Press Enter to exit" + exit 1 +} + # URLs for the latest Visual C++ Redistributables $urls = @( "https://aka.ms/vs/17/release/vc_redist.x86.exe", @@ -7,33 +47,140 @@ if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { $urls += "https://aka.ms/vs/17/release/vc_redist.arm64.exe" } -# Directory to save the downloads -$downloadPath = "$env:TEMP" +# Directory to save the downloads. This will save it into the user "Downloads" folder. +$folderName = "VCRedist Install" +$userDownloadsFolder = (New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path +$downloadPath = Join-Path -Path $userDownloadsFolder -ChildPath $folderName -# To improve download performance, the progress bar is suppressed. [2, 6] +# To improve download performance, the progress bar is suppressed. $ProgressPreference = 'SilentlyContinue' +# Load stored version hashes from the pre-existing installer cache (read-only mount) +$storedHashes = @{} +if (-not [string]::IsNullOrWhiteSpace($ExistingInstallerFilesPath)) { + $hashFilePath = Join-Path $ExistingInstallerFilesPath "versions.txt" + if (Test-Path $hashFilePath) { + Get-Content $hashFilePath | ForEach-Object { + $parts = $_ -split '=', 2 + if ($parts.Count -eq 2) { $storedHashes[$parts[0]] = $parts[1] } + } + } +} + +# Track current remote hashes to save at the end +$currentHashes = @{} +$pauseAtEnd = $false + foreach ($url in $urls) { $fileName = $url.Split('/')[-1] - $filePath = Join-Path $downloadPath $fileName + $downloadFilePath = Join-Path $downloadPath $fileName + $installerToRun = $null + $updateDetected = $false + $remoteHash = $null - Write-Host "Downloading $fileName..." - # Download the file without a progress bar [1, 4] - Invoke-WebRequest -Uri $url -OutFile $filePath + Write-Host "`nChecking $fileName..." - if (Test-Path $filePath) { - Write-Host "Installing $fileName..." - # Silently install the redistributable and wait for it to complete [3, 5, 9] - Start-Process -FilePath $filePath -ArgumentList "/install /quiet /norestart" -Wait - Write-Host "$fileName has been installed." - # Optional: Remove the installer after installation - # Remove-Item -Path $filePath - } else { - Write-Host "Error: Failed to download $fileName." + if (-not $NoCheckLatestVersion) { + try { + # Get the final redirected URL to extract the version hash + $headResponse = Invoke-WebRequest -Uri $url -Method Head -UseBasicParsing -ErrorAction Stop + if ($headResponse.BaseResponse.ResponseUri) { + # PowerShell 5.1 + $finalUrl = $headResponse.BaseResponse.ResponseUri.AbsoluteUri + } else { + # PowerShell 7+ + $finalUrl = $headResponse.BaseResponse.RequestMessage.RequestUri.AbsoluteUri + } + $remoteHash = $finalUrl.Split('/')[-2] + $currentHashes[$fileName] = $remoteHash + Write-Host "Remote hash: $remoteHash" + } + catch { + Write-Host "Warning: Could not retrieve remote info for $fileName." -ForegroundColor Yellow + Write-Host "Error Info: $_" -ForegroundColor Yellow + } } + + # Check if a pre-existing cached installer can be used + if (-not [string]::IsNullOrWhiteSpace($ExistingInstallerFilesPath)) { + $cachedFilePath = Join-Path $ExistingInstallerFilesPath $fileName + if (Test-Path $cachedFilePath) { + if ($null -ne $remoteHash -and $storedHashes.ContainsKey($fileName) -and $storedHashes[$fileName] -eq $remoteHash) { + Write-Host "Cached version is up to date. Using $cachedFilePath" + $installerToRun = $cachedFilePath + } elseif ($ForceCachedFilesOnly) { + if ($null -ne $remoteHash) { + Write-Host "WARNING: Cached installer is out of date, but using it anyway (-ForceCachedFilesOnly)." -ForegroundColor Yellow + Write-Host "Please update your cache at: $ExistingInstallerFilesPath" -ForegroundColor Yellow + $pauseAtEnd = $true + } else { + Write-Host "Using cached file (version check skipped). Using $cachedFilePath" + } + $installerToRun = $cachedFilePath + } else { + Write-Host "Newer version detected. Cached installer is out of date." -ForegroundColor Yellow + $updateDetected = $true + $pauseAtEnd = $true + } + } elseif ($ForceCachedFilesOnly) { + Write-Host "Error: Cached file not found at $cachedFilePath and -ForceCachedFilesOnly is set." -ForegroundColor Red + continue + } + } + + # Download if no valid cache was found or an update is needed + if ($null -eq $installerToRun) { + if ($ForceCachedFilesOnly) { + Write-Host "Error: No cached installer available for $fileName and -ForceCachedFilesOnly is set. Skipping." -ForegroundColor Red + continue + } + + Write-Host "Downloading $fileName..." + + # Create the directory if it doesn't exist + if (-not (Test-Path -Path $downloadPath)) { + New-Item -Path $downloadPath -ItemType Directory -Force | Out-Null + } + + Invoke-WebRequest -Uri $url -OutFile $downloadFilePath -UseBasicParsing + $installerToRun = $downloadFilePath + + if ($updateDetected) { + Write-Host "ACTION REQUIRED: A new version of $fileName was downloaded." -ForegroundColor Yellow + Write-Host "Please update your cache at: $ExistingInstallerFilesPath" -ForegroundColor Yellow + Write-Host "Then update versions.txt with: $fileName=$remoteHash" -ForegroundColor Yellow + $pauseAtEnd = $true + } + } + + if (Test-Path $installerToRun) { + Write-Host "Installing $fileName from $installerToRun..." + # Silently install the redistributable and wait for it to complete + Start-Process -FilePath $installerToRun -ArgumentList "/install /quiet /norestart" -Wait + Write-Host "$fileName has been installed." + + # Optional: Remove the downloaded installer if it was downloaded to TEMP + if ($installerToRun -eq $downloadFilePath) { + # Remove-Item -Path $downloadFilePath + } + } else { + Write-Host "Error: Failed to locate installer for $fileName." + } +} + +# Save versions.txt to the download folder so it can be copied alongside the installers +if ($currentHashes.Count -gt 0) { + if (-not (Test-Path -Path $downloadPath)) { + New-Item -Path $downloadPath -ItemType Directory -Force | Out-Null + } + $versionsFilePath = Join-Path $downloadPath "versions.txt" + $currentHashes.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" } | Set-Content $versionsFilePath } # Restore the default progress preference $ProgressPreference = 'Continue' -Write-Host "Script execution finished." +Write-Host "`nScript execution finished." +if ($pauseAtEnd) { + Read-Host "`nPress Enter to exit" +}