t3-code-android-nightly/scripts/install.ps1

224 lines
10 KiB
PowerShell

# Installs the T3 Code CLI from a GitHub Release archive on Windows. Needs
# only PowerShell 5.1+; no Node, npm, or compiler.
#
# irm https://t3.codes/install.ps1 | iex
#
# Environment:
# T3CODE_CHANNEL release train to follow: stable, nightly, or preview
# (default: stable; preview is a maintainers' test train)
# T3CODE_VERSION exact version to install (overrides T3CODE_CHANNEL)
# T3CODE_HOME T3 home directory (default: ~\.t3)
# T3CODE_INSTALL_BIN_DIR where t3.exe is linked (default: ~\.local\bin)
# T3CODE_RELEASE_BASE_URL mirror for releases/download (default: GitHub)
#
# The archive is unpacked into $T3CODE_HOME\runtime\versions\<version>, the
# same layout `t3 service install` uses, so the service reuses this download.
$ErrorActionPreference = "Stop"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$repo = "pingdotgg/t3code"
$baseUrl = if ($env:T3CODE_RELEASE_BASE_URL) { $env:T3CODE_RELEASE_BASE_URL.TrimEnd("/") } else { "https://github.com/$repo/releases/download" }
$t3Home = if ($env:T3CODE_HOME) { $env:T3CODE_HOME } else { Join-Path $HOME ".t3" }
$binDir = if ($env:T3CODE_INSTALL_BIN_DIR) { $env:T3CODE_INSTALL_BIN_DIR } else { Join-Path $HOME ".local\bin" }
function Fail([string] $message) {
Write-Error "t3 install: $message"
exit 1
}
$terminal = -not [Console]::IsErrorRedirected -and $env:TERM -ne "dumb"
$interactive = $terminal -and $Host.UI.SupportsVirtualTerminal
if ($interactive) {
# Legacy code pages may support ANSI escapes but not the logo or bar glyphs.
$glyphs = -join [char[]](0x2588, 0x2580, 0x2584, 0x25A0, 0x00B7)
$encoding = [Console]::Error.Encoding
$interactive = $encoding.GetString($encoding.GetBytes($glyphs)) -eq $glyphs
}
$esc = [char]27
$reset = $bold = $muted = $accent = $green = ""
if ($interactive -and -not $env:NO_COLOR) {
$reset = "$esc[0m"; $bold = "$esc[1m"; $muted = "$esc[2m"
$accent = "$esc[94m"; $green = "$esc[32m"
}
function Step([string] $message) {
if ($interactive) { [Console]::Error.Write("`r$esc[2K $muted$message$reset") }
else { [Console]::Error.WriteLine(" $message") }
}
function Draw-Download([long] $bytes, [long] $total) {
if (-not $interactive) { return }
$columns = [Console]::WindowWidth
if ($columns -le 0) { $columns = 80 }
if ($total -gt 0 -and $columns -ge 9) {
$percent = [Math]::Min(100, [Math]::Floor($bytes * 100.0 / $total))
$width = [Math]::Min(32, $columns - 8)
$filled = [int][Math]::Floor($percent * $width / 100)
$bar = ([string][char]0x25A0) * $filled
$rest = ([string][char]0x00B7) * ($width - $filled)
$sizes = (" {0:F1} / {1:F1} MB" -f ($bytes / 1MB), ($total / 1MB))
if ($width + 7 + $sizes.Length -ge $columns) { $sizes = "" }
[Console]::Error.Write(("`r$esc[2K $accent$bar$reset$muted$rest$reset {0,3}%" -f $percent) + "$muted$sizes$reset")
} else {
$line = if ($columns -ge 32) { " Downloading {0:F1} MB" -f ($bytes / 1MB) } else { " {0:F1} MB" -f ($bytes / 1MB) }
[Console]::Error.Write("`r$esc[2K" + $line.Substring(0, [Math]::Min($line.Length, $columns - 1)))
}
}
function Fetch([string] $uri, [string] $destination, [switch] $progress) {
if (-not $progress -or -not $interactive) {
$ProgressPreference = if ($progress -and $terminal) { "Continue" } else { "SilentlyContinue" }
Invoke-WebRequest -Uri $uri -OutFile $destination -UseBasicParsing
return
}
# Read the response once, displaying actual bytes received at most ten times a second.
Add-Type -AssemblyName System.Net.Http
$client = New-Object System.Net.Http.HttpClient
$response = $source = $file = $null
try {
$response = $client.GetAsync($uri, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
$response.EnsureSuccessStatusCode() | Out-Null
$source = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
$file = [IO.File]::Create($destination)
$buffer = New-Object byte[] 65536
$bytes = 0L
$clock = [Diagnostics.Stopwatch]::StartNew()
Draw-Download 0 $response.Content.Headers.ContentLength
while (($count = $source.Read($buffer, 0, $buffer.Length)) -gt 0) {
$file.Write($buffer, 0, $count)
$bytes += $count
if ($clock.ElapsedMilliseconds -ge 100) {
Draw-Download $bytes $response.Content.Headers.ContentLength
$clock.Restart()
}
}
Draw-Download $bytes $bytes
[Console]::Error.WriteLine()
} finally {
if ($file) { $file.Dispose() }
if ($source) { $source.Dispose() }
if ($response) { $response.Dispose() }
$client.Dispose()
}
}
if ($interactive) {
# Block characters are constructed so this file also loads correctly in PowerShell 5.1.
$mark = @(
"########## ######## ",
" ### _##^ ",
" ### ####_ ",
" ### _ ###",
" ### #######^ "
)
[Console]::Error.WriteLine()
for ($i = 0; $i -lt $mark.Length; $i++) {
$row = $mark[$i].Replace('#', [char]0x2588).Replace('^', [char]0x2580).Replace('_', [char]0x2584)
$label = if ($i -eq 1) { " ${bold}T3 Code$reset" } elseif ($i -eq 2) { " ${muted}CLI installer$reset" } else { "" }
[Console]::Error.WriteLine(" $bold$row$reset$label")
}
[Console]::Error.WriteLine()
}
Step "Finding your release..."
# PROCESSOR_ARCHITEW6432 reports the real machine when a 32-bit PowerShell
# runs under WOW64; RuntimeInformation needs .NET 4.7.1+, which 5.1 hosts
# may lack.
$rawArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
$arch = switch ($rawArch) {
"AMD64" { "x64" }
"ARM64" { "arm64" }
default { Fail "unsupported architecture $rawArch" }
}
$channel = if ($env:T3CODE_CHANNEL) { $env:T3CODE_CHANNEL } else { "stable" }
$version = $env:T3CODE_VERSION
if (-not $version) {
# Tags are v<semver>; the channel is the prerelease identifier, or none for
# stable. Only tags of the requested train are considered, so a stable
# install can never pick up a nightly or preview build by accident.
$tagPattern = switch ($channel) {
"stable" { '^v\d+\.\d+\.\d+$' }
"nightly" { '^v\d+\.\d+\.\d+-nightly\.\d+\.\d+$' }
"preview" { '^v\d+\.\d+\.\d+-preview\.\d+\.\d+$' }
default { Fail "T3CODE_CHANNEL must be stable, nightly, or preview" }
}
$releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases?per_page=100" -Headers @{ "User-Agent" = "t3-install" }
$tag = ($releases | Where-Object { -not $_.draft -and $_.tag_name -match $tagPattern } | Select-Object -First 1).tag_name
if (-not $tag) { Fail "could not find a $channel release; set T3CODE_VERSION" }
$version = $tag.Substring(1)
}
if ($version -match '-preview\.') {
Write-Warning "t3 $version is a preview build. Preview builds are cut by maintainers from unreleased branches to exercise the release pipeline. They can be broken, receive no fixes, and are never offered as updates. Set T3CODE_CHANNEL=stable (the default) for a supported build."
if ($channel -ne "preview" -and -not $env:T3CODE_VERSION) {
Fail "refusing a preview build that was not explicitly requested"
}
}
$stem = "t3-$version-win32-$arch"
$archive = "$stem.zip"
$versionsDir = Join-Path $t3Home "runtime\versions"
$targetDir = Join-Path $versionsDir $version
$marker = Join-Path $targetDir ".install-complete"
if ((Test-Path $marker) -and ((Get-Content $marker -Raw).Trim() -eq $version)) {
Step "Version $version is already downloaded."
} else {
New-Item -ItemType Directory -Force -Path $versionsDir | Out-Null
$staging = Join-Path $versionsDir (".staging-" + [System.IO.Path]::GetRandomFileName())
New-Item -ItemType Directory -Path $staging | Out-Null
try {
if ($interactive) { [Console]::Error.Write("`r$esc[2K") }
[Console]::Error.WriteLine(" ${muted}Installing$reset T3 Code $bold$version$reset`n")
Step "Downloading..."
try {
Fetch "$baseUrl/v$version/SHA256SUMS" (Join-Path $staging "SHA256SUMS")
} catch {
$status = $_.Exception.Response.StatusCode.value__
if ($status -eq 404) {
Fail "t3 $version has no release archive for win32-$arch; releases before the self-contained CLI can only be installed with 'npm install -g t3@$version'"
}
throw
}
Fetch "$baseUrl/v$version/$archive" (Join-Path $staging $archive) -progress
Step "Verifying the download..."
$expected = (Get-Content (Join-Path $staging "SHA256SUMS") | Where-Object { $_ -match "\s\*?$([regex]::Escape($archive))$" } | Select-Object -First 1)
if (-not $expected) { Fail "$archive is not listed in SHA256SUMS" }
$expected = ($expected -split "\s+")[0].ToLowerInvariant()
$actual = (Get-FileHash -Algorithm SHA256 (Join-Path $staging $archive)).Hash.ToLowerInvariant()
if ($actual -ne $expected) { Fail "checksum mismatch for $archive" }
Step "Extracting T3 Code..."
# The archive module reads the global preference, not the caller's local scope.
$savedProgress = $global:ProgressPreference
try {
$global:ProgressPreference = "SilentlyContinue"
Expand-Archive -Path (Join-Path $staging $archive) -DestinationPath $staging -Force
} finally { $global:ProgressPreference = $savedProgress }
# The archive wraps everything in one directory named after its stem.
Get-ChildItem (Join-Path $staging $stem) | Move-Item -Destination $staging
Remove-Item (Join-Path $staging $stem), (Join-Path $staging $archive), (Join-Path $staging "SHA256SUMS") -Recurse -Force
& (Join-Path $staging "t3.exe") --version | Out-Null
if ($LASTEXITCODE -ne 0) { Fail "the downloaded executable does not run" }
Set-Content -Path (Join-Path $staging ".install-complete") -Value $version -NoNewline
if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force }
Move-Item $staging $targetDir
} catch {
if (Test-Path $staging) { Remove-Item $staging -Recurse -Force }
throw
}
}
Step "Setting up the t3 command..."
New-Item -ItemType Directory -Force -Path $binDir | Out-Null
$shim = Join-Path $binDir "t3.cmd"
# UTF-8 without a BOM: cmd.exe reads the shim as-is, and ASCII would corrupt
# non-ASCII characters in the user's home path.
[System.IO.File]::WriteAllText($shim, "@echo off`r`n`"$(Join-Path $targetDir 't3.exe')`" %*", (New-Object System.Text.UTF8Encoding $false))
if ($interactive) { [Console]::Error.Write("`r$esc[2K") }
[Console]::Error.WriteLine(" ${green}Installed T3 Code $version$reset`n")
if (($env:PATH -split ";") -notcontains $binDir) {
Write-Host " Add $binDir to your PATH, then run ${bold}t3$reset.`n"
} else {
Write-Host " Run ${bold}t3$reset to get started.`n"
}