# SPDX-FileCopyrightText: Copyright Hewlett Packard Enterprise Development LP # SPDX-License-Identifier: MIT # # PiG installer for Windows. # # irm https://pi-in-go.dev/install.ps1 | iex # # Downloads the PiG release archive for this machine from GitHub Releases, # verifies it against the release's SHA256SUMS, installs pig.exe, and adds the # install directory to the user PATH. It fails closed: no published release, a # missing checksum, or a checksum mismatch stops the install before anything # is written to the install directory. It mirrors install.sh. # # Environment: # PIG_VERSION install this version (for example 0.2.0) instead of the latest # PIG_INSTALL_DIR install directory (default: %LOCALAPPDATA%\Programs\pig) # PIG_NO_MODIFY_PATH set to 1 to leave the user PATH unchanged # PIG_API_BASE API that names the latest release (default: https://pi-in-go.dev/api) # PIG_DOWNLOAD_BASE release download root (default: https://github.com/MichaelKinsy/PiG/releases/download) # PIG_ARCH override CPU detection (amd64 or arm64) # # Works in Windows PowerShell 5.1 and PowerShell 7. The whole script is one # function called on its last line, so a truncated download runs nothing. function Install-Pig { [CmdletBinding()] param() $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' Set-StrictMode -Version 2.0 # Tests run this script on macOS and Linux (PIG_INSTALL_TEST=1): the PATH # update then goes to PIG_INSTALL_TEST_PATH_FILE instead of the registry. $testMode = $env:PIG_INSTALL_TEST -eq '1' $onWindows = ($PSVersionTable.PSEdition -eq 'Desktop') -or ((Test-Path variable:IsWindows) -and $IsWindows) if (-not $onWindows -and -not $testMode) { throw 'this installer is for Windows; on macOS and Linux run: curl -fsSL https://pi-in-go.dev/install.sh | sh' } $apiBase = if ($env:PIG_API_BASE) { $env:PIG_API_BASE } else { 'https://pi-in-go.dev/api' } $downloadBase = if ($env:PIG_DOWNLOAD_BASE) { $env:PIG_DOWNLOAD_BASE } else { 'https://github.com/MichaelKinsy/PiG/releases/download' } if ($env:PIG_INSTALL_DIR) { $installDir = $env:PIG_INSTALL_DIR } else { if (-not $env:LOCALAPPDATA) { throw 'LOCALAPPDATA is not set; set PIG_INSTALL_DIR' } $installDir = Join-Path (Join-Path $env:LOCALAPPDATA 'Programs') 'pig' } # Windows PowerShell 5.1 may not offer TLS 1.2 by default. if ($PSVersionTable.PSEdition -eq 'Desktop') { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } $arch = Get-PigArch $platform = "windows-$arch" $version = $env:PIG_VERSION if (-not $version) { $version = Get-PigLatestVersion $apiBase } if ($version.StartsWith('v')) { $version = $version.Substring(1) } if ($version -notmatch '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$') { throw "not a release version: $version" } $name = "pig-$version-$platform" $archive = "$name.zip" $releaseUrl = "$($downloadBase.TrimEnd('/'))/v$version" $work = Join-Path ([IO.Path]::GetTempPath()) ("pig-install-" + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $work | Out-Null $staged = $null try { Write-PigMessage "Downloading PiG $version for $platform" $sums = Join-Path $work 'SHA256SUMS' $zip = Join-Path $work $archive try { Get-PigFile "$releaseUrl/SHA256SUMS" $sums } catch { throw "PiG $version has no SHA256SUMS at $releaseUrl; refusing to install an unverified archive" } try { Get-PigFile "$releaseUrl/$archive" $zip } catch { throw "could not download $releaseUrl/$archive" } $expected = Get-PigExpectedSha256 $sums $archive if (-not $expected) { throw "SHA256SUMS has no single valid entry for $archive" } $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $zip).Hash.ToLowerInvariant() if ($actual -ne $expected) { throw "checksum mismatch for ${archive}: expected $expected, got $actual" } Write-PigMessage "Verified SHA-256 $actual" $extract = Join-Path $work 'extract' Expand-Archive -LiteralPath $zip -DestinationPath $extract $binary = Join-Path (Join-Path $extract $name) 'pig.exe' $item = Get-Item -LiteralPath $binary -Force -ErrorAction SilentlyContinue if (-not $item -or $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw "$archive does not contain $name/pig.exe" } # Stage beside the destination, smoke-test it, then swap it in. A running # pig.exe cannot be overwritten on Windows but can be renamed. New-Item -ItemType Directory -Force -Path $installDir | Out-Null $target = Join-Path $installDir 'pig.exe' $staged = Join-Path $installDir ".pig.install.$PID.exe" Copy-Item -LiteralPath $binary -Destination $staged -Force if (-not $onWindows) { & chmod 0755 $staged } try { $null = & $staged --version 2>&1 } catch { $global:LASTEXITCODE = 1 } if ($LASTEXITCODE -ne 0) { throw 'the downloaded pig.exe does not run on this machine' } if (Test-Path -LiteralPath $target) { $old = Join-Path $installDir 'pig.old.exe' Remove-Item -LiteralPath $old -Force -ErrorAction SilentlyContinue Move-Item -LiteralPath $target -Destination $old -Force } Move-Item -LiteralPath $staged -Destination $target -Force $staged = $null Remove-Item -LiteralPath (Join-Path $installDir 'pig.old.exe') -Force -ErrorAction SilentlyContinue $installed = "pig $version" try { $out = & $target --version 2>$null; if ($LASTEXITCODE -eq 0 -and $out) { $installed = ($out | Select-Object -First 1) } } catch { } Write-PigMessage "Installed $installed to $target" if ($env:PIG_NO_MODIFY_PATH -eq '1') { Write-PigMessage "Left PATH unchanged; add $installDir to PATH to run pig" } elseif (Add-PigToUserPath $installDir) { Write-PigMessage "Added $installDir to your user PATH; open a new terminal to run pig" } $sep = [IO.Path]::PathSeparator if (-not (Test-PigPathContains $env:PATH $installDir)) { $env:PATH = "$installDir$sep$env:PATH" } } finally { if ($staged) { Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue } Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue } } function Write-PigMessage([string]$Message) { Write-Host "pig-install: $Message" } function Get-PigArch { $raw = $env:PIG_ARCH if (-not $raw) { try { $raw = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() } catch { # .NET Framework before 4.7.1 has no RuntimeInformation. $raw = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } } } switch -Regex ($raw) { '^(?i)(x64|amd64|x86_64)$' { return 'amd64' } '^(?i)(arm64|aarch64)$' { return 'arm64' } default { throw "unsupported CPU architecture $raw; PiG publishes Windows builds for amd64 and arm64" } } } # Get-PigFile downloads over HTTPS only. Plain HTTP is allowed for loopback # hosts, where the tests serve a fake release. function Get-PigFile([string]$Url, [string]$OutFile) { Assert-PigUrl $Url Invoke-WebRequest -UseBasicParsing -Uri $Url -OutFile $OutFile -MaximumRedirection 10 } function Assert-PigUrl([string]$Url) { $uri = [Uri]$Url if ($uri.Scheme -eq 'https') { return } if ($uri.Scheme -eq 'http' -and $uri.IsLoopback) { return } throw "refusing a non-HTTPS URL: $Url" } function Get-PigLatestVersion([string]$ApiBase) { $url = "$($ApiBase.TrimEnd('/'))/latest-version" Assert-PigUrl $url try { $response = Invoke-RestMethod -UseBasicParsing -Uri $url } catch { throw "no PiG release is published yet ($url did not answer); build from source or set PIG_VERSION" } $latest = $null if ($response -and ($response.PSObject.Properties.Name -contains 'version')) { $latest = [string]$response.version } if (-not $latest) { throw "could not read the latest PiG version from $url" } return $latest } # Get-PigExpectedSha256 returns the digest of Name (listed as Name, ./Name, # *Name, or *./Name) only when exactly one well-formed line names it. function Get-PigExpectedSha256([string]$SumsPath, [string]$Name) { $found = @() foreach ($line in [IO.File]::ReadAllLines($SumsPath)) { $fields = @($line.Trim() -split '\s+' | Where-Object { $_ -ne '' }) if ($fields.Count -ne 2) { continue } $file = $fields[1] if ($file -eq $Name -or $file -eq "./$Name" -or $file -eq "*$Name" -or $file -eq "*./$Name") { $found += $fields[0].ToLowerInvariant() } } if ($found.Count -eq 1 -and $found[0] -match '^[0-9a-f]{64}$') { return $found[0] } return $null } function Test-PigPathContains([string]$PathValue, [string]$Dir) { if (-not $PathValue) { return $false } $want = $Dir.TrimEnd('\', '/') foreach ($entry in $PathValue.Split([IO.Path]::PathSeparator)) { if ($entry.TrimEnd('\', '/') -ieq $want) { return $true } } return $false } # Add-PigToUserPath appends Dir to the user PATH and reports whether it # changed it. On Windows it edits HKCU\Environment\Path, keeping %VAR% entries # unexpanded. In test mode it edits PIG_INSTALL_TEST_PATH_FILE instead. function Add-PigToUserPath([string]$Dir) { if ($env:PIG_INSTALL_TEST -eq '1') { $file = $env:PIG_INSTALL_TEST_PATH_FILE if (-not $file) { throw 'PIG_INSTALL_TEST_PATH_FILE is not set' } $current = if (Test-Path -LiteralPath $file) { [IO.File]::ReadAllText($file).Trim() } else { '' } if (Test-PigPathContains ($current -replace ';', [IO.Path]::PathSeparator) $Dir) { return $false } $next = if ($current) { "$current;$Dir" } else { $Dir } [IO.File]::WriteAllText($file, $next) return $true } $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment') try { $current = [string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) $expanded = [Environment]::ExpandEnvironmentVariables($current) if (Test-PigPathContains $expanded $Dir) { return $false } $next = if ($current) { "$($current.TrimEnd(';'));$Dir" } else { $Dir } # SetEnvironmentVariable('Path', ..., 'User') would store REG_SZ and stop # entries such as %USERPROFILE%\... from expanding, so write REG_EXPAND_SZ. $key.SetValue('Path', $next, [Microsoft.Win32.RegistryValueKind]::ExpandString) } finally { $key.Close() } # SetEnvironmentVariable broadcasts WM_SETTINGCHANGE, so Explorer and new # terminals see the new PATH. Removing a variable that does not exist # changes nothing else. [Environment]::SetEnvironmentVariable('PIG_INSTALL_PATH_NOTIFY', $null, 'User') return $true } try { Install-Pig } catch { [Console]::Error.WriteLine("pig-install: error: $($_.Exception.Message)") $global:LASTEXITCODE = 1 # Run as a file (-File), exit non-zero; piped into iex, keep the shell open. if ($PSCommandPath) { exit 1 } }