# SWAT+ Toolbox installer / updater for Windows (per user, no administrator rights). # # irm https://install.swat.tamu.edu/spt.ps1 | iex # # Downloads the SWAT+ Toolbox setup for this computer (x64, or ARM64 for Windows # on Arm) from the download mirror, checks its SHA-256 and runs it silently. The # Toolbox goes to %USERPROFILE%\SWATPlus\SWATPlusToolbox, where SWAT+ Editor looks # for it, and keeps itself up to date from then on. Re-running this updates the # Toolbox when the mirror has a newer version and leaves it alone otherwise; # -Force installs the mirror's version even when it is the one installed. # # With options: # # & ([scriptblock]::Create((irm https://install.swat.tamu.edu/spt.ps1))) -Check # # -Check only check the requirements; install nothing. Throws when one is # not met, returns normally when all are. # -Force install even when the installed version matches the mirror's, # to replace a build with a rebuild of the same version # -Uninstall remove the Toolbox # # Windows PowerShell 5.1 or PowerShell 7. Errors are thrown, never `exit`, which # would close the window the command was pasted into. param([switch]$Check, [switch]$Force, [switch]$Uninstall) & { $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Invoke-WebRequest is many times slower with it [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 # Download mirrors, tried in order. $Bases = @( 'https://install.swat.tamu.edu/downloads' 'https://install.swatplus.link/downloads' ) $Feed = 'sptoolbox/windows' $UninstallRoot = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall' # the uninstall entry Velopack writes for the Toolbox $OwnKey = "$UninstallRoot\celray.SWATPlusToolbox" function Say([string]$m) { Write-Host '==> ' -ForegroundColor Cyan -NoNewline; Write-Host $m } function Warn([string]$m) { Write-Host '!! ' -ForegroundColor Yellow -NoNewline; Write-Host $m } # A mirror can answer a missing file with a 200 and an HTML page, so every # download is checked for what it should be before it is used. function Test-Download([string]$kind, [string]$file) { $head = [byte[]]::new(16) $fs = [IO.File]::OpenRead($file) try { $n = $fs.Read($head, 0, 16) } finally { $fs.Close() } switch ($kind) { 'exe' { return $n -ge 2 -and $head[0] -eq 0x4D -and $head[1] -eq 0x5A } 'sha256' { return (Get-Content -Raw $file) -match '^[0-9a-fA-F]{64}' } 'version' { return (Get-Content -Raw $file) -match '^v?\d+\.\d+' } } return $false } function Get-FromMirror([string]$rel, [string]$out, [string]$kind) { foreach ($base in $Bases) { try { Invoke-WebRequest -UseBasicParsing -Uri "$base/$rel" -OutFile $out -TimeoutSec 900 } catch { continue } if (Test-Download $kind $out) { return $true } } return $false } # The Toolbox installed by Velopack, with the version of its current\ folder. # The Toolbox updates itself, and current\sq.version follows those updates; the # registry entry is the fallback. function Get-InstalledToolbox { $e = Get-ItemProperty $OwnKey -ErrorAction SilentlyContinue if (-not $e) { return } $version = $e.DisplayVersion $channel = '' $sq = Join-Path "$($e.InstallLocation)" 'current\sq.version' if ($e.InstallLocation -and (Test-Path $sq)) { try { $meta = ([xml](Get-Content -Raw $sq)).package.metadata if ($meta.version) { $version = $meta.version } $channel = "$($meta.channel)" } catch { } } [pscustomobject]@{ Version = $version; Channel = $channel; Location = $e.InstallLocation; Entry = $e } } # Toolbox uninstall entries from other installers (an Inno Setup "SWAT+ Toolbox # 4.0.1"). When one points at the same folder, its uninstaller deletes files the # Velopack install uses, swatplusToolboX.exe among them. function Get-OtherInstall { if (-not (Test-Path $UninstallRoot)) { return } Get-ChildItem $UninstallRoot | Where-Object { $_.PSChildName -ne 'celray.SWATPlusToolbox' } | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { "$($_.DisplayName)" -like 'SWAT+ Toolbox*' } } if ($Uninstall) { $t = Get-InstalledToolbox if (-not $t) { Warn 'The SWAT+ Toolbox is not installed.'; return } if (Get-Process -Name 'swatplusToolboX' -ErrorAction SilentlyContinue) { throw 'The SWAT+ Toolbox is running. Close it, then run this again.' } $update = Join-Path $t.Location 'Update.exe' if (-not (Test-Path $update)) { throw "The Toolbox's uninstaller is missing: $update" } Say "Removing the SWAT+ Toolbox $($t.Version)" $p = Start-Process -FilePath $update -ArgumentList '--uninstall', '--silent' -Wait -PassThru if ($p.ExitCode -ne 0) { throw "The Toolbox's uninstaller failed (exit code $($p.ExitCode))." } if (Test-Path $OwnKey) { throw 'The uninstaller finished, but Windows still lists the SWAT+ Toolbox as installed.' } Say 'Done.' return } $tmp = Join-Path ([IO.Path]::GetTempPath()) ('swatplus-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $tmp | Out-Null try { # The machine's architecture from the system environment: a PowerShell running # under emulation reports the emulated one in its environment. $native = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment').PROCESSOR_ARCHITECTURE $rid = switch ($native) { 'AMD64' { 'win-x64' } 'ARM64' { 'win-arm64' } default { '' } } Say "SWAT+ Toolbox installer (Windows, $native)" # --- requirements: Windows 10 or newer on x64 or ARM64, and a setup on a mirror if ([Environment]::OSVersion.Version.Major -lt 10) { throw 'The SWAT+ Toolbox needs Windows 10 or newer.' } if (-not $rid) { throw "The SWAT+ Toolbox is built for x64 and ARM64 Windows; this computer is $native." } $SetupName = "SWATPlusToolbox-$rid-Setup.exe" $shaFile = Join-Path $tmp 'setup.sha256' if (-not (Get-FromMirror "$Feed/$SetupName.sha256" $shaFile 'sha256')) { throw "Could not find $SetupName on any mirror." } $installed = Get-InstalledToolbox foreach ($o in @(Get-OtherInstall)) { $same = $installed -and $o.InstallLocation -and ($o.InstallLocation.TrimEnd('\') -eq "$($installed.Location)".TrimEnd('\')) $where = if ($same) { 'the same folder' } else { $o.InstallLocation } Warn "$($o.DisplayName) from another installer is registered in $where. Its uninstaller deletes files the Toolbox installed here uses; remove '$($o.DisplayName)' in Settings > Apps before installing to avoid this." } if ($Check) { Say 'SWAT+ Toolbox requirements met.' return } # --- install or update $verFile = Join-Path $tmp 'version.txt' if (-not (Get-FromMirror "$Feed/version.txt" $verFile 'version')) { throw 'Could not read the Toolbox version from any mirror.' } $version = (Get-Content -Raw $verFile).Trim() if ($installed -and $installed.Version -eq $version -and -not $Force) { $ch = if ($installed.Channel) { ", channel $($installed.Channel)" } else { '' } Say "The SWAT+ Toolbox $version is up to date$ch. Run with -Force to reinstall it." return } if (Get-Process -Name 'swatplusToolboX' -ErrorAction SilentlyContinue) { throw 'The SWAT+ Toolbox is running. Close it, then run this again.' } $want = ((Get-Content -Raw $shaFile) -split '\s+')[0].ToLower() $from = if ($installed -and $installed.Version -ne $version) { "$($installed.Version) -> " } else { '' } $again = if ($installed -and $installed.Version -eq $version) { ' to reinstall it' } else { '' } Say "Downloading the SWAT+ Toolbox $from$version ($rid)$again" $setup = Join-Path $tmp $SetupName if (-not (Get-FromMirror "$Feed/$SetupName" $setup 'exe')) { throw 'Could not download the Toolbox setup from any mirror.' } if ((Get-FileHash $setup -Algorithm SHA256).Hash.ToLower() -ne $want) { throw "Checksum mismatch for $SetupName." } Say 'Checksum OK; installing (this unpacks a self-contained .NET app and may take a minute)' $p = Start-Process -FilePath $setup -ArgumentList '/S' -Wait -PassThru if ($p.ExitCode -ne 0) { throw "The Toolbox setup failed (exit code $($p.ExitCode)). Its log is %TEMP%\spt-velopack-setup.log." } $now = Get-InstalledToolbox if (-not $now -or $now.Version -ne $version) { throw "The Toolbox setup finished, but Windows does not list the SWAT+ Toolbox $version as installed." } if (-not (Test-Path (Join-Path $now.Location 'swatplusToolboX.exe'))) { throw "The Toolbox setup finished, but $($now.Location)\swatplusToolboX.exe is missing." } Say "Installed the SWAT+ Toolbox $version -> $($now.Location)" } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } }