# oppi installer (Windows). # # irm https://oppi-lang.eu/install.ps1 | iex # # Installs into $env:LOCALAPPDATA\Programs\oppi by default: # ...\oppi\bin\oppi.exe # ...\oppi\share\oppi\... # so the binary finds its data via \..\share\oppi (no env vars needed), # then adds ...\bin to the USER PATH if missing (registry + current session). # # NOTE: this file is deliberately pure ASCII — it is parsed by Windows # PowerShell 5.1 via `irm | iex`, which mangles BOM-less UTF-8. # # Environment overrides (same contracts as install.sh): # OPPI_PREFIX= install elsewhere # OPPI_RELEASE_BASE= download base (defaults to the public oppi-lang site) # OPPI_ZIP= install from a local zip instead of downloading # (checksum verified only if .sha256 exists - # a local file has no separate trust channel) # OPPI_NO_VERIFY=1 skip checksum verification (downloads verify by # default and FAIL otherwise - this is the explicit # opt-out) # OPPI_BASIC_AUTH=user:pass credentials if the site is still basic-auth gated # for pre-launch testing (equivalent of curl's # --netrc-optional: optional, anonymous otherwise - # the installer works both gated and public) $ErrorActionPreference = 'Stop' $Prefix = if ($env:OPPI_PREFIX) { $env:OPPI_PREFIX } else { Join-Path $env:LOCALAPPDATA 'Programs\oppi' } $ReleaseBase = if ($env:OPPI_RELEASE_BASE) { $env:OPPI_RELEASE_BASE } else { 'https://oppi-lang.eu' } $Zip = 'oppi-windows-x86_64.zip' function Say([string]$m) { Write-Host $m } function Fail([string]$m) { Write-Error "error: $m"; exit 1 } # --- platform check (x86_64 only for now) ------------------------------------ if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64') { Fail "this installer supports x86_64 only for now (got $env:PROCESSOR_ARCHITECTURE); build from source instead." } # modern TLS even under Windows PowerShell 5.1 try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {} function Fetch([string]$Url, [string]$OutFile) { $params = @{ Uri = $Url; OutFile = $OutFile; UseBasicParsing = $true } if ($env:OPPI_BASIC_AUTH) { # OPTIONAL credentials (pre-launch gated site); anonymous otherwise $u, $p = $env:OPPI_BASIC_AUTH.Split(':', 2) $sec = ConvertTo-SecureString $p -AsPlainText -Force $params.Credential = New-Object System.Management.Automation.PSCredential($u, $sec) } Invoke-WebRequest @params } $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ('oppi-install-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $tmp | Out-Null try { # --- obtain the zip (+ checksum) ------------------------------------------- if ($env:OPPI_ZIP) { Say "Using local zip: $env:OPPI_ZIP" Copy-Item $env:OPPI_ZIP (Join-Path $tmp $Zip) if (Test-Path "$env:OPPI_ZIP.sha256") { Copy-Item "$env:OPPI_ZIP.sha256" (Join-Path $tmp "$Zip.sha256") } } else { Say 'Downloading oppi...' Fetch "$ReleaseBase/$Zip" (Join-Path $tmp $Zip) if ($env:OPPI_NO_VERIFY -ne '1') { try { Fetch "$ReleaseBase/$Zip.sha256" (Join-Path $tmp "$Zip.sha256") } catch { Fail "could not fetch checksum $Zip.sha256 - refusing to install unverified (set OPPI_NO_VERIFY=1 to override)" } } } # --- verify checksum -------------------------------------------------------- # Verification is the DEFAULT for downloads and any failure is fatal; the # only soft paths are a local OPPI_ZIP without a .sha256 next to it (same # origin: a checksum would add no trust) or the explicit OPPI_NO_VERIFY=1. if ($env:OPPI_NO_VERIFY -eq '1') { Say 'warning: OPPI_NO_VERIFY=1 - checksum verification SKIPPED' } elseif (Test-Path (Join-Path $tmp "$Zip.sha256")) { $expected = ((Get-Content (Join-Path $tmp "$Zip.sha256") -Raw).Trim() -split '\s+')[0].ToLower() $actual = (Get-FileHash -Algorithm SHA256 (Join-Path $tmp $Zip)).Hash.ToLower() if ($expected -ne $actual) { Fail "checksum mismatch (expected $expected, got $actual)" } Say 'Checksum OK.' } # --- verify the SIGNATURE --------------------------------------------------- # WHY, AND WHY THE CHECKSUM IS NOT ENOUGH (2026-09-04): the sha256 sum is # served by the SAME host as the archive. Whoever controls the server # replaces both, and the check still agrees. It protects against a damaged # download, NOT a substituted archive. This script installs what it # downloads, on the machine of whoever runs it. # # NAMED DIFFERENCE FROM LINUX (do not "harmonize" it away): install.sh # verifies Ed25519; this installer verifies RSA-3072/SHA-256 INSTEAD, # because the runtime available on the user's machine (Windows PowerShell # 5.1 / .NET Framework 4.8) cannot verify Ed25519 natively, and requiring # PowerShell 7 from a student would amount to verifying nothing at all. # Same class of guarantee, same flow, distinct key. The public modulus is # PINNED HERE so it travels with the installer instead of being fetched # from the very host it is meant to check; the private key never leaves # the publishing machine (not on the server, not in the repo, not in CI). # # LIMIT, stated plainly: an attacker who also replaces THIS script removes # this block. The signature protects whoever keeps a copy of install.ps1, # whoever checks the zip by hand, and against substitution of the zip # ALONE. Strictly better than a co-hosted checksum; not a full trust chain. # --- BEGIN OPPI VERIFY FUNCTION (extracted verbatim by the CI witnesses) --- $OppiPubModulusB64 = 'uN0JDZ39WXhT9FbfYldYABNgtqtJ0OXsUW6O4WOxSQzYDXl+wQIlwhAzVELwuySXhnfh73nfpw0Zm/Hy3rdDF2gHaPUn4IWq8HjjnFaTSJeDLZ2rmmv3ARoJ0SbXZzIE65T5VWG6Og0/HaGa1CW+ceKdC3gE4G1tAmUufMUgGdBkT9sI93VytWQADit7LashY9WfqFkoNNhRv3SmXY5y6MD3FXhNReA8ERs3QVOV+x1KuuLrfvVzB+LsPIpA+AtgLsacy4EwSRxkHy+YkUHGSI47V2kxiLaUqzz8SyAK84H0RXn0h3OGNVimZ1Xk4e/WbgCQwIIm5Y6BkPrM2Jl4qpKpazeJkP/IHqXd4FX8mOslmrQaDm/5zMsH5ETONSVaQqy5YPEgvpdurMyXn+Jit6CXFHszMA/pQof3FUX3ub2sUtGfajLNBTjTaJcl4vBqa49TXXQoA3PVOk/pwwwb/5t1piAnAbiBp1PDASHQT0cTRfcqLreik4Gxt0f1SaT5' function Test-OppiSignature([string]$DataPath, [string]$SigPath, [string]$ModulusB64) { $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider try { $params = New-Object System.Security.Cryptography.RSAParameters $params.Modulus = [Convert]::FromBase64String($ModulusB64) $params.Exponent = [byte[]](1, 0, 1) # 65537 $rsa.ImportParameters($params) $data = [System.IO.File]::ReadAllBytes($DataPath) $sig = [System.IO.File]::ReadAllBytes($SigPath) return $rsa.VerifyData($data, 'SHA256', $sig) } finally { $rsa.Dispose() } } # --- END OPPI VERIFY FUNCTION --- if ($env:OPPI_NO_VERIFY -eq '1') { # explicit opt-out - already warned above } elseif (-not $env:OPPI_ZIP) { try { Fetch "$ReleaseBase/$Zip.sig" (Join-Path $tmp "$Zip.sig") } catch { Fail "could not fetch signature $Zip.sig - refusing to install unverified (set OPPI_NO_VERIFY=1 to override)" } if (-not (Test-OppiSignature (Join-Path $tmp $Zip) (Join-Path $tmp "$Zip.sig") $OppiPubModulusB64)) { Fail "SIGNATURE MISMATCH: the downloaded archive is not the one published for oppi-lang.eu. Refusing to install." } Say 'Signature OK.' } # --- install ---------------------------------------------------------------- Say "Installing to $Prefix..." New-Item -ItemType Directory -Force -Path $Prefix | Out-Null Expand-Archive -Path (Join-Path $tmp $Zip) -DestinationPath $Prefix -Force $bin = Join-Path $Prefix 'bin\oppi.exe' if (-not (Test-Path $bin)) { Fail "install failed: $bin not found in zip" } # --- user PATH -------------------------------------------------------------- $binDir = Join-Path $Prefix 'bin' $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') $onPath = ($userPath -split ';' | Where-Object { $_ -eq $binDir }).Count -gt 0 if (-not $onPath) { $newPath = if ($userPath) { "$userPath;$binDir" } else { $binDir } [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') Say "Added $binDir to your user PATH (new terminals will pick it up)." } if (($env:Path -split ';' | Where-Object { $_ -eq $binDir }).Count -eq 0) { $env:Path = "$env:Path;$binDir" # current session } Say '' Say "Installed: $(& $bin --version)" Say 'Get started: oppi --help' } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue }