# SWAT+ Editor installer / updater for Windows (per user, no administrator rights). # # irm https://install.swat.tamu.edu/spe.ps1 | iex # # Downloads the SWAT+ Editor setup from the Editor's latest GitHub release, checks # its SHA-512 against the release's latest.yml and runs it silently. That is the # release the Editor's own update check follows, so the install updates itself from # then on. The Editor goes to %USERPROFILE%\SWATPlus\SWATPlusEditor and the reference # databases, from the download mirror, to %USERPROFILE%\SWATPlus\Databases. # Re-running it updates the Editor when GitHub has a newer release and leaves it # alone otherwise. # # With options: # # & ([scriptblock]::Create((irm https://install.swat.tamu.edu/spe.ps1))) -WithWgn # # -WithWgn also install the WGN database (~180 MB), no prompt # -NoWgn skip the WGN database, no prompt # -Uninstall remove the Editor and its datasets database (the WGN database stays) # # Windows PowerShell 5.1 or PowerShell 7. Errors are thrown, never `exit`, which # would close the window the command was pasted into. param([switch]$WithWgn, [switch]$NoWgn, [switch]$Uninstall) & { $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Invoke-WebRequest is many times slower with it [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 # The Editor's GitHub release; its latest.yml is what the Editor's update check reads. $Release = 'https://github.com/swat-model/swatplus-editor/releases/latest/download' # Download mirrors for the databases, tried in order. $Bases = @( 'https://install.swat.tamu.edu/downloads' 'https://install.swatplus.link/downloads' ) $DatasetsRel = '4.0/swatplus_datasets.sqlite' $WgnZip = 'swatplus_wgn.zip' $DbDir = Join-Path $env:USERPROFILE 'SWATPlus\Databases' 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 } 'zip' { return $n -ge 2 -and $head[0] -eq 0x50 -and $head[1] -eq 0x4B } 'sqlite' { return $n -eq 16 -and [Text.Encoding]::ASCII.GetString($head, 0, 15) -eq 'SQLite format 3' } } 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 Editor's uninstall entry, written by its setup. A per-user install is in # HKCU; an install for all users (HKLM) is reported, since this script cannot update it. function Get-InstalledEditor { foreach ($root in 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall') { if (-not (Test-Path $root)) { continue } $hit = Get-ChildItem $root | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { $_.DisplayName -like 'SWATPlusEditor*' } | Select-Object -First 1 if ($hit) { $hit | Add-Member -NotePropertyName Scope -NotePropertyValue $root.Substring(0, 4) -PassThru; return } } } function Split-Command([string]$cmd) { if ($cmd -match '^\s*"([^"]+)"\s*(.*)$') { return $Matches[1], $Matches[2] } $exe, $rest = $cmd -split '\s+', 2 return $exe, $rest } if ($Uninstall) { $e = Get-InstalledEditor if (-not $e) { Warn 'The SWAT+ Editor is not installed.'; return } if ($e.Scope -eq 'HKLM') { throw 'The SWAT+ Editor is installed for all users; remove it from Settings > Apps as an administrator.' } $exe = (Split-Command $e.UninstallString)[0] $dir = Split-Path $exe Say "Removing the SWAT+ Editor $($e.DisplayVersion)" # Without _?= an NSIS uninstaller copies itself to %TEMP% and returns at once, # so -Wait would not wait. With it, the uninstaller stays behind and goes here. $p = Start-Process -FilePath $exe -ArgumentList '/S', '/currentuser', "_?=$dir" -Wait -PassThru if ($p.ExitCode -ne 0) { throw "The Editor's uninstaller failed (exit code $($p.ExitCode))." } Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue if (Get-InstalledEditor) { throw 'The uninstaller finished, but Windows still lists the SWAT+ Editor.' } Say 'Done. The WGN database in %USERPROFILE%\SWATPlus\Databases was left in place.' return } $tmp = Join-Path ([IO.Path]::GetTempPath()) ('swatplus-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $tmp | Out-Null try { Say "SWAT+ Editor installer (Windows, $env:PROCESSOR_ARCHITECTURE)" if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { Say 'The Editor is built for x64; Windows on ARM runs it under emulation.' } # latest.yml names the setup and gives its SHA-512 (base64), as electron-builder writes it. $yml = Join-Path $tmp 'latest.yml' try { Invoke-WebRequest -UseBasicParsing -Uri "$Release/latest.yml" -OutFile $yml -TimeoutSec 120 } catch { throw "Could not read the Editor's latest release from GitHub ($Release/latest.yml)." } $text = Get-Content -Raw $yml $field = { param($k) $m = [regex]::Match($text, "(?m)^${k}:\s*'?([^'\r\n]+?)'?\s*$"); if ($m.Success) { $m.Groups[1].Value } } $version = & $field 'version'; $setupName = & $field 'path'; $want = & $field 'sha512' if (-not ($version -and $setupName -and $want)) { throw "The Editor's latest.yml on GitHub has no version, path or sha512." } $installed = Get-InstalledEditor if ($installed -and $installed.Scope -eq 'HKLM') { Warn "The SWAT+ Editor $($installed.DisplayVersion) is installed for all users. This installs a separate copy for you only." $installed = $null } # GitHub marks the most recently created release as latest, so a 3.2.x fix # released after 4.x would be offered here; never go back to an older version. $older = $false if ($installed) { try { $older = [version]$version -lt [version]$installed.DisplayVersion } catch { } } if ($installed -and $installed.DisplayVersion -eq $version) { Say "The SWAT+ Editor $version is up to date." } elseif ($older) { Warn "The SWAT+ Editor $($installed.DisplayVersion) is installed; the latest GitHub release is the older $version, so it was left alone." } else { if (Get-Process -Name 'SWATPlusEditor' -ErrorAction SilentlyContinue) { throw 'The SWAT+ Editor is running. Close it, then run this again.' } $from = if ($installed) { "$($installed.DisplayVersion) -> " } else { '' } Say "Downloading the SWAT+ Editor $from$version from GitHub" $setup = Join-Path $tmp $setupName try { Invoke-WebRequest -UseBasicParsing -Uri "$Release/$setupName" -OutFile $setup -TimeoutSec 900 } catch { throw "Could not download $setupName from GitHub." } $hex = (Get-FileHash $setup -Algorithm SHA512).Hash $bytes = [byte[]]::new($hex.Length / 2) for ($i = 0; $i -lt $bytes.Length; $i++) { $bytes[$i] = [Convert]::ToByte($hex.Substring(2 * $i, 2), 16) } # A release published between the two downloads also gives a mismatch; running again fixes that. if ([Convert]::ToBase64String($bytes) -ne $want) { throw "Checksum mismatch for $setupName; run this again." } Say 'Checksum OK; installing' $p = Start-Process -FilePath $setup -ArgumentList '/S', '/currentuser' -Wait -PassThru if ($p.ExitCode -ne 0) { throw "The Editor setup failed (exit code $($p.ExitCode))." } $now = Get-InstalledEditor if (-not $now -or $now.DisplayVersion -ne $version) { throw "The Editor setup finished, but Windows does not list the SWAT+ Editor $version as installed." } Say "Installed the SWAT+ Editor $version" } New-Item -ItemType Directory -Force -Path $DbDir | Out-Null # The datasets database is refreshed every time: projects are created from it, # so an old schema breaks New Project. $ds = Join-Path $tmp 'datasets.sqlite' if (Get-FromMirror $DatasetsRel $ds 'sqlite') { Move-Item -Force $ds (Join-Path $DbDir 'swatplus_datasets.sqlite') Say "Datasets database -> $DbDir\swatplus_datasets.sqlite" } else { Warn 'Could not download the datasets database from any mirror; kept the copy from the setup.' } $wgnDb = Join-Path $DbDir 'swatplus_wgn.sqlite' if (Test-Path $wgnDb) { Say "WGN database already present: $wgnDb" } else { $wantWgn = if ($WithWgn) { $true } elseif ($NoWgn -or -not [Environment]::UserInteractive) { $false } else { (Read-Host 'Also install the WGN weather generator database (~180 MB)? [y/N]') -match '^(y|yes)$' } if ($wantWgn) { Say 'Downloading the WGN database (~180 MB)' $zip = Join-Path $tmp 'wgn.zip' if (-not (Get-FromMirror $WgnZip $zip 'zip')) { throw "Could not download $WgnZip from any mirror." } Expand-Archive -Path $zip -DestinationPath (Join-Path $tmp 'wgn') -Force $src = Get-ChildItem (Join-Path $tmp 'wgn') -Recurse -Filter '*.sqlite' | Select-Object -First 1 if (-not $src) { throw 'The WGN archive has no .sqlite file.' } Move-Item -Force $src.FullName $wgnDb Say "WGN database -> $wgnDb" } else { Say 'Skipping the WGN database (add it later with -WithWgn).' } } Say 'Start the SWAT+ Editor from the Start menu.' } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } }