-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathInstall-GithubRelease.ps1
381 lines (320 loc) · 14.3 KB
/
Install-GithubRelease.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
<#
.DESCRIPTION
A cross-platform script to download, check the file hash, and make sure the binary is on your PATH.
.SYNOPSIS
Install a binary from a github release.
.EXAMPLE
Install-GithubRelease FluxCD Flux2
Install `Flux` from the https://github.com/FluxCD/Flux2 repository
.EXAMPLE
Install-GithubRelease earthly earthly
Install `earthly` from the https://github.com/earthly/earthly repository
.EXAMPLE
Install-GithubRelease junegunn fzf
Install `fzf` from the https://github.com/junegunn/fzf repository
.EXAMPLE
Install-GithubRelease BurntSushi ripgrep
Install `rg` from the https://github.com/BurntSushi/ripgrep repository
.EXAMPLE
Install-GithubRelease opentofu opentofu
Install `opentofu` from the https://github.com/opentofu/opentofu repository
.EXAMPLE
Install-GithubRelease twpayne chezmoi
Install `chezmoi` from the https://github.com/twpayne/chezmoi repository
.EXAMPLE
Install-GitHubRelease mikefarah yq
Install `yq` from the https://github.com/mikefarah/yq repository
.EXAMPLE
Install-GithubRelease sharkdp bat
Install-GithubRelease sharkdp fd
Install `bat` and `fd` from their repositories
.NOTES
All these examples are (only) tested on Windows and WSL Ubuntu
#>
<#PSScriptInfo
.VERSION 1.3.2
.GUID 802367c6-654a-450b-94db-87e1d52e020a
.AUTHOR Joel Bennett
.COMPANYNAME HuddledMasses.org
.COPYRIGHT Copyright (c) 2019-2023, Joel Bennett
.TAGS Install Github Releases Binaries Linux Windows
.LICENSEURI https://github.com/Jaykul/BoxStarter-Boxes/blob/master/LICENSE
.PROJECTURI https://github.com/Jaykul/BoxStarter-Boxes
.RELEASENOTES
- **1.3.2** Allow pasting user/repo as a single string
- **1.3.1** Fixed the -BinDir parameter (and installing to hidden folders)
- **1.3.0** Added support for mikefarah/yq, by supporting checksum files with multiple hashes (for different hash algorithms)
- **1.2.0** Added support for .zip files on Linux
Also for checksum files based on the name "SHA256SUMS" instead of "checksums"
- **1.1.0** Added support for directly downloading binaries (.exe on Windows, or no extension) to support earthly/earthly
- **1.0.0** Broke this out from my BoxStarter Boxes, so I could share it more easily.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
# The user or organization that owns the repository
[Parameter(Mandatory)]
[Alias("User")]
[string]$Org,
# The name of the repository or project to download from
[Parameter()]
[string]$Repo,
# The version (tag) of the release to download. Defaults to 'latest' which is always the latest release.
[string]$Version = 'latest',
# The location to install to. Defaults to $Env:LocalAppData\Programs on Windows, /usr/local/bin on Linux/MacOS
[string]$BinDir
)
function Get-OSPlatform {
[CmdletBinding()]
param(
[switch]$Pattern
)
$ri = [System.Runtime.InteropServices.RuntimeInformation]
$platform = [System.Runtime.InteropServices.OSPlatform]
# if $ri isn't defined, then we must be running in Powershell 5.1, which only works on Windows.
$OS = if (-not $ri -or $ri::IsOSPlatform($platform::Windows)) {
"windows"
} elseif ($ri::IsOSPlatform($platform::Linux)) {
"linux"
} elseif ($ri::IsOSPlatform($platform::OSX)) {
"darwin"
} elseif ($ri::IsOSPlatform($platform::FreeBSD)) {
"freebsd"
} else {
throw "unsupported platform"
}
if ($Pattern) {
Write-Information $OS
switch ($OS) {
"windows" { "windows|(?<!dar)win" }
"linux" { "linux|unix" }
"darwin" { "darwin|osx" }
"freebsd" { "freebsd" }
}
} else {
$OS
}
}
function Get-OSArchitecture {
[CmdletBinding()]
param(
[switch]$Pattern
)
# PowerShell Core
$Architecture = if (($arch = "$([Runtime.InteropServices.RuntimeInformation]::OSArchitecture)")) {
$arch
# Legacy Windows PowerShell
} elseif ([Environment]::Is64BitOperatingSystem) {
"X64";
} else {
"X86";
}
# Optionally, turn this into a regex pattern that usually works
if ($Pattern) {
Write-Information $arch
switch ($arch) {
"Arm" { "arm(?!64)" }
"Arm64" { "arm64" }
"X86" { "x86|386" }
"X64" { "amd64|x64|x86_64" }
}
} else {
$arch
}
}
function Get-GitHubRelease {
[CmdletBinding()]
param(
[Parameter(Mandatory, Position = 0)]
[Alias("User")]
[string]$Org,
[Parameter(Mandatory, Position = 1)]
[string]$Repo,
[Parameter(Position = 2)]
[Alias("Version")]
[string]$Tag = 'latest'
)
Write-Debug "Checking GitHub for tag '$tag'"
$result = if ($tag -eq 'latest') {
Invoke-RestMethod "https://api.github.com/repos/$org/$repo/releases/$tag" -Headers @{Accept = 'application/json' } -Verbose:$false
} else {
Invoke-RestMethod "https://api.github.com/repos/$org/$repo/releases/tags/$tag" -Headers @{Accept = 'application/json' } -Verbose:$false
}
Write-Debug "Found tag '$($result.tag_name)' for $tag"
$result
}
function Test-FileHash {
<#
.SYNOPSIS
Test the hash of a file against one or more checksum files or strings
.DESCRIPTION
Checksum files are assumed to have one line per file name, with the hash (or multiple hashes) on the line following the file name.
In order to support installing yq (which has a checksum file with multiple hashes), this function handles checksum files with an ARRAY of valid checksums for each file name by searching the array for any matching hash.
This isn't great, but an accidental pass is almost inconceivable, and determining the hash order is too complicated (given only one weird project does this so far).
#>
[OutputType([bool])]
[CmdletBinding()]
param(
# The path to the file to check the hash of
[string]$Target,
# The hash(es) or checksum(s) to compare to (can be one or more files, or one or more hash strings)
[string[]]$Checksum
)
# If Checksum is a file, get the checksum from the file
if (Test-Path $Checksum) {
$basename = [Regex]::Escape([IO.Path]::GetFileName($Target))
Write-Debug "Checksum is a file, getting checksum for $basename from $checksum"
$Checksum = (Select-String -LiteralPath $Checksum -Pattern $basename).Line -split "\s+|=" -notmatch $basename
}
$Actual = (Get-FileHash -LiteralPath $Target -Algorithm SHA256).Hash
# Supports checksum files with an ARRAY of valid checksums (for different hash algorithms)
# ... by searching the array for any matching hash (an accidental pass is almost inconceivable).
[bool]($Checksum -eq $Actual)
if ($Checksum -eq $Actual) {
Write-Verbose "Checksum matches $Actual"
} else {
Write-Error "Checksum mismatch!`nValid: $Checksum`nActual: $Actual"
}
}
function Install-GitHubRelease {
<#
.SYNOPSIS
Install a binary from a github release.
.DESCRIPTION
Cross-platform script to download, check file hash, and make sure the binary is on your PATH.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
# The user or organization that owns the repository
[Parameter(Mandatory)]
[Alias("User")]
[string]$Org,
# The name of the repository or project to download from
[Parameter()]
[string]$Repo,
# The version of the release to download. Defaults to 'latest'
[string]$Version = 'latest',
# The operating system (will be detected, if not specified)
$OS = (Get-OSPlatform -Pattern),
# The architecture (will be detected, if not specified)
$Architecture = (Get-OSArchitecture -Pattern),
# The location to install to. Defaults to $Env:LocalAppData\Programs on Windows, /usr/local/bin on Linux/MacOS
[string]$BinDir = $(if ($OS -notmatch "windows") { '/usr/local/bin' } elseif ($Env:LocalAppData) { "$Env:LocalAppData\Programs\Tools" } else { "$HOME/.tools" })
)
# A list of extensions in order of preference
$extension = ".zip", ".tgz", ".tar.gz", ".exe"
if (!$Repo) {
$Org, $Repo = $Org.Split('/')
}
$release = Get-GitHubRelease -Org $Org -Repo $Repo -Tag $Version
Write-Verbose "found release $($release.tag_name) for $org/$repo"
$assets = $release.assets.where{ $_.name -match $OS -and $_.name -match $Architecture } |
Select-Object *, @{ Name = "Extension"; Expr = { $_.name -replace '^[^.]+$', '' -replace ".*?((?:\.tar)?\.[^.]+$)", '$1' } } |
Select-Object *, @{ Name = "Priority"; Expr = { if (($index = [array]::IndexOf($extension, $_.Extension)) -lt 0) { $index * -10 } else { $index } } } |
Sort-Object Priority, Name
if ($assets.Count -gt 1) {
if ($asset = $assets.where({ $_.Extension -in $extension }, "First")) {
Write-Warning "Found multiple available downloands for $OS/$Architecture`n $($assets| Format-Table name, Extension, b*url | Out-String)`nUsing $($asset.name)"
# If it's not on windows, executables don't need an extesion
} elseif ($os -notmatch "windows" -and ($asset = $assets.Where({ !$_.Extension }, "First", 0))) {
Write-Warning "Found multiple available downloands for $OS/$Architecture`n $($assets| Format-Table name, Extension, b*url | Out-String)`nUsing $($asset.name)"
} else {
throw "Found multiple available downloands for $OS/$Architecture`n $($assets| Format-Table name, Extension, b*url | Out-String)`nUnable to detect usable release."
}
} elseif ($assets.Count -eq 0) {
throw "No asset found for $OS/$Architecture`n $($release.assets.name -join "`n")"
} else {
$asset = $assets[0]
}
# Make a folder to unpack in
$tempdir = Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName())
New-Item -Type Directory -Path $tempdir | Out-Null
Push-Location $tempdir
$ProgressPreference = "SilentlyContinue"
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $asset.name -Verbose:$false
# There might be a checksum file
$checksum = $release.assets.where{ $_.name -match "checksum|sha256sums" }[0]
if ($checksum.Count -gt 0) {
Write-Verbose "Found checksum file $($checksum.name)"
Invoke-WebRequest -Uri $checksum.browser_download_url -OutFile $checksum.name -Verbose:$false
if (!(Test-FileHash -Target $asset.name -Checksum $checksum.name)) {
throw "Checksum mismatch for $($asset.name)"
}
} else {
Write-Warning "No checksum file found for $($asset.name)"
}
# If it's an archive, expand it
if ($asset.Extension -and $asset.Extension -ne ".exe") {
$File = Get-Item $asset.name
New-Item -Type Directory -Path $Repo | Convert-Path -OutVariable PackagePath | Set-Location
Write-Verbose "Extracting $File to $PackagePath"
if ($asset.Extension -eq ".zip") {
Microsoft.PowerShell.Archive\Expand-Archive $File.FullName
} else {
if ($VerbosePreference -eq "Continue") {
tar -xzvf $File.FullName
} else {
tar -xzf $File.FullName
}
}
Set-Location $tempdir
} else {
Remove-Item $checksum.name
$PackagePath = $tempdir
}
$Filter = @{ }
if ($OS -match "windows") {
$Filter.Include = @($ENV:PATHEXT -replace '\.', '*.' -split ';') + '*.exe'
}
if (!(Test-Path $BinDir)) {
# First time use of $BinDir
if ($Force -or $PSCmdlet.ShouldContinue("Create $BinDir and add to Path?", "$BinDir does not exist")) {
New-Item -Type Directory -Path $BinDir | Out-Null
if ($Env:PATH -split [IO.Path]::PathSeparator -notcontains $BinDir) {
$Env:PATH += [IO.Path]::PathSeparator + $BinDir
# If it's *not* Windows, $BinDir should be /usr/local/bin or something already in your PATH
# Make the change permanent
if ($OS -match "windows") {
$PATH = [Environment]::GetEnvironmentVariable("PATH", [EnvironmentVariableTarget]::User)
$PATH += [IO.Path]::PathSeparator + $BinDir
[Environment]::SetEnvironmentVariable("PATH", $PATH, [EnvironmentVariableTarget]::User)
}
}
} else {
throw "Cannot install $Repo to $BinDir"
}
}
Write-Verbose "Moving files from $PackagePath"
foreach ($File in Get-ChildItem $PackagePath -File -Recurse @Filter) {
# Some teams (e.g. earthly/earthly), name the actual binary with the platform name, which is annoying
if ($File.BaseName -match $OS -and $File.BaseName -match $Architecture ) {
# $File = Rename-Item $File.FullName -NewName "$Repo$($_.Extension)" -PassThru
if (!($NewName = ($File.BaseName -replace "[-_\.]*$OS" -replace "[-_\.]*$Architecture"))) {
$NewName = $Repo
}
$NewName += $File.Extension
Write-Warning "Renaming $File to $NewName"
$File = Rename-Item $File.FullName -NewName $NewName -PassThru
}
# Some few teams include the docs with their package (e.g. opentofu)
if ($File.BaseName -match "README|LICENSE|CHANGELOG" -or $File.Extension -in ".md", ".rst", ".txt", ".asc", ".doc" ) {
Write-Verbose "Skipping doc $File"
continue
}
Write-Verbose "Moving $File to $BinDir"
if ($OS -notmatch "windows" -and (Get-Item $BinDir -Force).Attributes -eq "ReadOnly,Directory") {
sudo mv -f $File.FullName $BinDir
sudo chmod +x "$BinDir/$($File.Name)"
} else {
if (Test-Path $BinDir/$($File.Name)) {
Remove-Item $BinDir/$($File.Name) -Recurse -Force
}
$Executable = Move-Item $File.FullName -Destination $BinDir -Force -ErrorAction Stop -PassThru
if ($OS -notmatch "windows") {
chmod +x $Executable.FullName
}
}
}
Pop-Location
Remove-Item $tempdir -Recurse
}
Install-GitHubRelease @PSBoundParameters