install.ps1 RawHistoryFolder
1# zut installer for Windows (PowerShell).
2#
3# Usage (in PowerShell):
4# iwr -useb https://git.bnema.dev/zut/raw/main/install.ps1 | iex
5#
6# Or with arguments:
7# $env:ZUT_VERSION = "v0.1.0"
8# $env:ZUT_PREFIX = "$HOME\bin"
9# iwr -useb https://git.bnema.dev/zut/raw/main/install.ps1 | iex
10#
11# Detects architecture, downloads the matching .zip from the forge
12# release, verifies the sha256 against checksums.txt, extracts zut.exe,
13# and moves it into $ZUT_PREFIX (defaults to $HOME\bin, added to PATH
14# via the User environment if missing).
15#
16
17[CmdletBinding()]
18param(
19 [string]$Version = $env:ZUT_VERSION,
20 [string]$Prefix = $env:ZUT_PREFIX
21)
22
23$ErrorActionPreference = "Stop"
24
25$owner = "bnema"
26$repo = "zut"
27$binary = "zut"
28
29if (-not $Version) { $Version = "latest" }
30if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }
31
32function Msg($m) { Write-Host "==> $m" -ForegroundColor Cyan }
33function Warn($m) { Write-Warning $m }
34function Die($m) { Write-Error $m; exit 1 }
35
36# ---- detect architecture ----
37
38switch -wildcard ($env:PROCESSOR_ARCHITECTURE) {
39 "AMD64" { $arch = "amd64" }
40 "ARM64" { $arch = "arm64" }
41 default { Die "unsupported arch: $($env:PROCESSOR_ARCHITECTURE)" }
42}
43
44# ARM64 Windows isn't shipped (see .goreleaser.yaml ignore rule) — fall
45# back to amd64 which runs fine under ARM64 emulation.
46if ($arch -eq "arm64") {
47 Warn "windows/arm64 is not published; falling back to amd64"
48 $arch = "amd64"
49}
50
51# ---- resolve version ----
52#
53# Resolve "latest" through the forge's GitHub-compatible releases API. This works the same
54# on Windows PowerShell 5.1 and PowerShell 7+, unlike scraping the
55# /releases/latest redirect target: on PS7 the final URL lives at
56# $resp.BaseResponse.RequestMessage.RequestUri while on PS5.1 it is
57# $resp.BaseResponse.ResponseUri, and relying on either breaks on the
58# other runtime. The API returns the tag directly, so there is nothing
59# to scrape.
60
61if ($Version -eq "latest") {
62 $apiUrl = "https://git.bnema.dev/-/github/api/repos/$owner/$repo/releases/latest"
63 $apiHeaders = @{ "User-Agent" = "zut-installer"; "Accept" = "application/json" }
64
65 try {
66 $api = Invoke-RestMethod -UseBasicParsing -Headers $apiHeaders -Uri $apiUrl
67 } catch {
68 $status = $null
69 try { $status = [int]$_.Exception.Response.StatusCode } catch {}
70 if ($status -eq 404) {
71 Die "no published release found for $owner/$repo (the repo may have no releases yet)"
72 } elseif ($status -eq 401 -or $status -eq 403) {
73 Die "forge API request was rejected ($status); try again later"
74 } else {
75 Die "could not resolve latest version: $($_.Exception.Message)"
76 }
77 }
78
79 $Version = $api.tag_name
80 if (-not $Version) {
81 Die "could not resolve latest version: forge API returned no tag_name for $owner/$repo"
82 }
83}
84
85if (-not $Version.StartsWith("v")) { $Version = "v$Version" }
86$verNum = $Version.TrimStart("v")
87
88# ---- download + verify + extract ----
89
90$archive = "${binary}_${verNum}_windows_${arch}.zip"
91$baseUrl = "https://git.bnema.dev/$repo/releases/download/$Version"
92$archiveUrl = "$baseUrl/$archive"
93$checksumUrl = "$baseUrl/checksums.txt"
94
95$tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zut-install-" + [System.Guid]::NewGuid().ToString("N").Substring(0,8)))
96
97try {
98 Msg "downloading $archive"
99 Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
100
101 Msg "verifying checksum"
102 $checksumFile = Join-Path $tmp "checksums.txt"
103 Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl -OutFile $checksumFile
104 $expected = Get-Content -LiteralPath $checksumFile | ForEach-Object {
105 $line = $_.Trim()
106 if ($line) {
107 $parts = $line -split "\s+"
108 if ($parts.Count -ge 2 -and $parts[($parts.Count - 1)] -eq $archive) { $line }
109 }
110 } | Select-Object -First 1
111 if (-not $expected) { Die "no checksum for $archive in checksums.txt" }
112 $expectedHash = ($expected -split "\s+")[0]
113
114 $actualHash = (Get-FileHash -Path (Join-Path $tmp $archive) -Algorithm SHA256).Hash.ToLower()
115 if ($expectedHash.ToLower() -ne $actualHash) {
116 Die "checksum mismatch: expected $expectedHash, got $actualHash"
117 }
118
119 Msg "extracting"
120 Expand-Archive -Path (Join-Path $tmp $archive) -DestinationPath $tmp -Force
121
122 $exe = Join-Path $tmp "$binary.exe"
123 if (-not (Test-Path $exe)) { Die "archive did not contain $binary.exe" }
124
125 Msg "installing to $Prefix\$binary.exe"
126 New-Item -ItemType Directory -Path $Prefix -Force | Out-Null
127 Copy-Item $exe (Join-Path $Prefix "$binary.exe") -Force
128
129 # ---- PATH hint ----
130
131 $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
132 $parts = $userPath -split ";" | Where-Object { $_ }
133 if (-not ($parts -contains $Prefix)) {
134 Warn "$Prefix is not on your user PATH"
135 Warn "adding it for future sessions..."
136 [Environment]::SetEnvironmentVariable("Path", ($userPath.TrimEnd(";") + ";" + $Prefix), "User")
137 Warn "open a new terminal to pick up the change, or run:"
138 Warn " `$env:Path = `"$Prefix;`$env:Path`""
139 }
140
141 Msg "installed. run: zut --help"
142}
143finally {
144 Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
145}