<# LXS - Update Windows Description: Install pending Windows updates and upgrade every winget package. Mirror of linux/tools/update-server.sh. Repo: https://git.hyko.cx/hykocx/lxs Usage: update-windows.ps1 [-Yes] [-NoWinget] [-NoWindowsUpdate] [-Help] #> # Load LXS common library (colors, UI helpers, spinner, loggers, guards). # Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and # only hits the network when this script is run standalone. $LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' } if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" } $LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null } if ($LxsLibPath -and (Test-Path $LxsLibPath)) { . $LxsLibPath } else { try { $LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop } catch { Write-Error 'Failed to fetch lib/common.ps1' exit 1 } . ([scriptblock]::Create($LxsLibSource)) } $env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_update_windows.log' $LxsOpts = Read-LxsFlags -Arguments $args -Known @( '-Yes', '-y', '-NoWinget', '-NoWindowsUpdate', '-Help', '-h' ) if (Show-LxsUnknownFlags $LxsOpts) { exit 1 } $AssumeYes = Test-LxsFlag $LxsOpts @('-Yes', '-y') $NoWinget = Test-LxsFlag $LxsOpts @('-NoWinget') $NoWindowsUpdate = Test-LxsFlag $LxsOpts @('-NoWindowsUpdate') if (Test-LxsFlag $LxsOpts @('-Help', '-h')) { Write-Host @' Usage: update-windows.ps1 [options] Options: -Yes, -y Skip the confirmation prompt -NoWinget Skip `winget upgrade --all` -NoWindowsUpdate Skip Windows Update -Help, -h Show this help '@ exit 0 } if (-not (Assert-LxsWindows)) { exit 1 } if (-not $NoWindowsUpdate) { Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args } # ═══════════════════════════════════════════════════════════════════════════ # Windows Update via the built-in COM API. Deliberately not PSWindowsUpdate: # that module has to be pulled from the PowerShell Gallery first, which fails # on locked-down or offline machines. The COM surface ships with Windows. # ═══════════════════════════════════════════════════════════════════════════ function Invoke-LxsWindowsUpdate { Write-Host '' Show-LxsBoxMid 'WINDOWS UPDATE' Write-Host '' try { $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() } catch { Write-LxsErr "Windows Update API unavailable: $($_.Exception.Message)" return $false } Write-LxsInfo 'Searching for updates (this can take several minutes)...' try { $result = $searcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0") } catch { Write-LxsErr "Search failed: $($_.Exception.Message)" return $false } $updates = @($result.Updates) if ($updates.Count -eq 0) { Write-LxsOk 'No pending updates' return $true } Write-LxsOk "$($updates.Count) update(s) available:" Write-Host '' foreach ($u in $updates) { $sizeMB = [math]::Round($u.MaxDownloadSize / 1MB, 1) Write-Host " $($script:Gray)-$($script:NC) $($u.Title) $($script:Gray)(${sizeMB} MB)$($script:NC)" } Write-Host '' if (-not (Confirm-LxsAction -Question 'Download and install these updates?' -DefaultYes -AssumeYes:$AssumeYes)) { Write-LxsInfo 'Skipped.' return $true } # EULAs must be accepted per-update before an unattended install. $toInstall = New-Object -ComObject Microsoft.Update.UpdateColl foreach ($u in $updates) { if (-not $u.EulaAccepted) { try { $u.AcceptEula() } catch { Write-LxsWarn "Could not accept the EULA for: $($u.Title)" } } [void]$toInstall.Add($u) } Write-Host '' Write-LxsInfo 'Downloading...' try { $downloader = $session.CreateUpdateDownloader() $downloader.Updates = $toInstall $dlResult = $downloader.Download() if ($dlResult.ResultCode -ne 2) { Write-LxsWarn "Download finished with result code $($dlResult.ResultCode) (2 = success)." } else { Write-LxsOk 'Download complete' } } catch { Write-LxsErr "Download failed: $($_.Exception.Message)" return $false } Write-Host '' Write-LxsInfo 'Installing...' try { $installer = $session.CreateUpdateInstaller() $installer.Updates = $toInstall $instResult = $installer.Install() } catch { Write-LxsErr "Install failed: $($_.Exception.Message)" return $false } for ($i = 0; $i -lt $toInstall.Count; $i++) { $code = $instResult.GetUpdateResult($i).ResultCode $title = $toInstall.Item($i).Title if ($code -eq 2) { Write-LxsOk $title } else { Write-LxsWarn "$title (result code $code)" } } Write-Host '' if ($instResult.RebootRequired) { Write-LxsWarn 'A reboot is required to finish installing these updates.' } else { Write-LxsOk 'Windows Update finished' } return $true } # ═══════════════════════════════════════════════════════════════════════════ # winget # ═══════════════════════════════════════════════════════════════════════════ function Invoke-LxsWingetUpgrade { Write-Host '' Show-LxsBoxMid 'WINGET' Write-Host '' if (-not (Assert-LxsWinget)) { return $false } Write-LxsInfo 'Packages with an available upgrade:' Write-Host '' & winget upgrade --include-unknown --accept-source-agreements | Out-String | Write-Host if (-not (Confirm-LxsAction -Question 'Upgrade all of them?' -DefaultYes -AssumeYes:$AssumeYes)) { Write-LxsInfo 'Skipped.' return $true } Write-Host '' & winget upgrade --all --include-unknown --silent ` --accept-package-agreements --accept-source-agreements --disable-interactivity $code = $LASTEXITCODE Write-Host '' if ($code -eq 0) { Write-LxsOk 'winget upgrade completed' return $true } # -1978335189 = no applicable upgrade found; nothing actually went wrong. if ($code -eq -1978335189) { Write-LxsOk 'Everything is already up to date' return $true } Write-LxsWarn "winget exited with code $code" return $false } # ═══════════════════════════════════════════════════════════════════════════ # Run # ═══════════════════════════════════════════════════════════════════════════ Clear-Host Show-LxsBoxTop -Title 'UPDATE WINDOWS' Write-Host '' Write-Host 'The following will run on this machine:' if (-not $NoWindowsUpdate) { Write-Host ' - Windows Update: search, download and install pending updates' } if (-not $NoWinget) { Write-Host ' - winget: upgrade every package with a newer version' } Write-Host '' Show-LxsSeparator if (-not (Confirm-LxsAction -Question 'Continue?' -DefaultYes -AssumeYes:$AssumeYes)) { Write-LxsInfo 'Cancelled.' exit 0 } $failed = $false if (-not $NoWindowsUpdate) { if (-not (Invoke-LxsWindowsUpdate)) { $failed = $true } } if (-not $NoWinget) { if (-not (Invoke-LxsWingetUpgrade)) { $failed = $true } } Write-Host '' Show-LxsSeparator Write-Host '' if ($failed) { Write-LxsWarn 'Update pass finished with warnings.' exit 1 } Write-LxsOk 'Update pass finished' exit 0