#!/usr/bin/env pwsh
#
# Installer for <{ slug }>
#
# This file was generated by eish <{ eish_version }> and is not meant to be
# edited: the next time it is regenerated, any change made here is lost. To
# change the installer, change what it is generated from and regenerate.
#
# The complete command that produced this file:
#
# <{ command }>
#
# Run `./install.ps1 -Help` for the available options.
#
# Works on Windows PowerShell 5.1 and PowerShell 7+ (Windows, Linux and macOS).
#
[CmdletBinding()]
param(
# Mirror to download through: github, gh-proxy, xget, jsdelivr, statically.
[ValidateSet('github', 'gh-proxy', 'xget', 'jsdelivr', 'statically')]
[string]$Proxy = '<{ proxy }>',
# Release tag to install.
[string]$Tag = '<{ tag }>',
# Installation directory.
[string]$Dir = '<{ install_dir }>',
# Force a Rust target triple instead of auto-detecting one.
[string]$Target = '',
# Resource type: release or file.
[ValidateSet('release', 'file')]
[string]$Type = '<{ resource_type }>',
# Branch/tag/commit used by -Type file.
[string]$Ref = '<{ resource_ref }>',
# Minimum free disk space, in megabytes (0 disables the check).
[int]$MinDiskSpace = <{ min_disk_space_mb }>,
# Install from a local file instead of downloading.
#
# The file name must match a release asset, which is also what decides the
# target triple.
[string]$File = '',
# List the target triples this installer knows and exit.
[switch]$List,
# Show help.
[switch]$Help
)
$ErrorActionPreference = 'Stop'
# ---------------------------------------------------------------------------
# Configuration (generated by eish <{ eish_version }>)
# ---------------------------------------------------------------------------
$EiOwner = '<{ owner }>'
$EiRepo = '<{ repo }>'
$EiBinary = '<{ binary }>'
# Environment variables take precedence over the generated defaults.
if ($env:EI_PROXY) { $Proxy = $env:EI_PROXY }
if ($env:EI_TAG) { $Tag = $env:EI_TAG }
if ($env:EI_DIR) { $Dir = $env:EI_DIR }
if ($env:EI_TARGET) { $Target = $env:EI_TARGET }
if ($env:EI_TYPE) { $Type = $env:EI_TYPE }
if ($env:EI_REF) { $Ref = $env:EI_REF }
if ($env:EI_MIN_DISK_SPACE) { $MinDiskSpace = [int]$env:EI_MIN_DISK_SPACE }
if ($env:EI_FILE) { $File = $env:EI_FILE }
# Target triple -> release asset file name.
$EiAssets = [ordered]@{<% for asset in assets %>
'<{ asset.target }>' = '<{ asset.filename }>'
<%- endfor %>
}
# Asset file name -> the target triples it can be installed as.
#
# Several triples may share one file, so the value is a list.
$EiAssetsByName = @{<% for group in asset_groups %>
'<{ group.filename }>' = @(<% for target in group.targets %>, '<{ target }>'<% endfor %>)
<%- endfor %>
}
# Every distinct asset file name, for error messages.
$EiAssetNames = @(<% for filename in filenames %>, '<{ filename }>'<% endfor %>)
$EiTemp = $null
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function Write-EiLog {
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Message)
Write-Host ($Message -join ' ') -ForegroundColor DarkGray
}
function Write-EiError {
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Message)
Write-Host ('error: ' + ($Message -join ' ')) -ForegroundColor Red
}
# Render a path the way the platform spells it.
#
# Windows writes `C:\Users\you\.ei`, but `/` is accepted everywhere Windows
# takes a path, and it is the form the other installers print on Unix — so every
# message uses it. Only messages: the paths handed to `Copy-Item`, `New-Item`
# and `SetEnvironmentVariable` keep their native spelling, and are normalised
# before being compared.
function ConvertTo-EiDisplayPath {
param([string]$Path)
if (-not $Path) { return $Path }
return $Path -replace '\\', '/'
}
function Get-EiPlatformFilename {
param([string]$Triple)
if ($EiAssets.Contains($Triple)) { return $EiAssets[$Triple] }
return $null
}
# Compatible triples to try, in order, when no exact build exists.
$EiFallbacks = @{<% for fallback in fallbacks %>
'<{ fallback.target }>' = @(<% for alternative in fallback.alternatives %>, '<{ alternative }>'<% endfor %>)
<%- endfor %>
}
function Get-EiFallbackTargets {
param([string]$Triple)
if ($EiFallbacks.ContainsKey($Triple)) { return $EiFallbacks[$Triple] }
return @()
}
function Test-EiWindows {
if ($PSVersionTable.PSEdition -eq 'Desktop') { return $true }
if (Test-Path variable:global:IsWindows) { return [bool]$IsWindows }
return $true
}
function Get-EiArch {
$value = $null
try { $value = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() } catch { $value = $null }
if (-not $value) { $value = $env:PROCESSOR_ARCHITECTURE }
switch ($value.ToLowerInvariant()) {
'x64' { return 'x86_64' }
'amd64' { return 'x86_64' }
'arm64' { return 'aarch64' }
'x86' { return 'i686' }
'i386' { return 'i686' }
default { return 'x86_64' }
}
}
function Get-EiLinuxLibc {
foreach ($loader in '/lib/ld-musl-x86_64.so.1', '/lib/ld-musl-aarch64.so.1', '/lib/ld-musl-armhf.so.1') {
if (Test-Path -LiteralPath $loader) { return 'musl' }
}
if (Test-Path -LiteralPath '/etc/os-release') {
$release = Get-Content -LiteralPath '/etc/os-release' -Raw -ErrorAction SilentlyContinue
if ($release -match '(?i)alpine|void') { return 'musl' }
}
return 'gnu'
}
function Get-EiPlatform {
$arch = Get-EiArch
if (Test-EiWindows) {
if ($env:MSYSTEM -match '^(MINGW|MSYS|UCRT)') { return "$arch-pc-windows-gnu" }
switch ($arch) {
'aarch64' { return 'aarch64-pc-windows-msvc' }
'i686' { return 'i686-pc-windows-msvc' }
default { return 'x86_64-pc-windows-msvc' }
}
}
if ($PSVersionTable.PSEdition -ne 'Desktop' -and $IsMacOS) {
if ($arch -eq 'aarch64') { return 'aarch64-apple-darwin' }
return 'x86_64-apple-darwin'
}
if ($arch -eq 'aarch64') { return "aarch64-unknown-linux-$(Get-EiLinuxLibc)" }
if ($arch -eq 'i686') { return "i686-unknown-linux-$(Get-EiLinuxLibc)" }
return "x86_64-unknown-linux-$(Get-EiLinuxLibc)"
}
function Resolve-EiTarget {
if ($File) { return Resolve-EiTargetFromFile }
$primary = $Target
if (-not $primary) { $primary = Get-EiPlatform }
if (-not $primary) { return $null }
if (Get-EiPlatformFilename $primary) { return $primary }
foreach ($alternative in (Get-EiFallbackTargets $primary)) {
if (Get-EiPlatformFilename $alternative) {
Write-EiLog "no build for $primary, using $alternative instead"
return $alternative
}
}
return $null
}
# Pick the target to install from the name of a local file.
#
# The file name is the only thing an offline install has to go on, so it has to
# be one of the release's assets. When several targets share that file the
# machine's own platform decides, and only if it is not among them does the
# first one win.
function Resolve-EiTargetFromFile {
$name = Split-Path -Path $File -Leaf
$matches = @()
if ($EiAssetsByName.ContainsKey($name)) { $matches = @($EiAssetsByName[$name]) }
if ($matches.Count -eq 0) {
Write-EiLog "known assets: $($EiAssetNames -join ' ')"
throw "$name is not an asset of $EiOwner/$EiRepo (see the list above)"
}
if ($Target) {
if ($matches -contains $Target) { return $Target }
throw "$name is for $($matches -join ' '), not $Target"
}
$detected = Get-EiPlatform
if ($matches -contains $detected) { return $detected }
Write-EiLog "installing $name for $($matches[0]) (detected $detected)"
return $matches[0]
}
function Expand-EiPath {
param([string]$Path)
if (-not $Path) { return $Path }
if ($Path -eq '~') { $Path = $HOME }
elseif ($Path.StartsWith('~/') -or $Path.StartsWith('~\')) { $Path = Join-Path $HOME $Path.Substring(2) }
# Absolute, so a relative -Dir cannot end up in a shell profile as
# `export PATH="relative/dir:..."`. `GetFullPath` also settles `/` against
# `\` and strips any trailing separator.
try { return [System.IO.Path]::GetFullPath($Path) } catch { return $Path }
}
function Get-EiDownloadUrl {
param([string]$Asset)
$github = "https://github.com/$EiOwner/$EiRepo"
$xget = "https://xget.xi-xu.me/gh/$EiOwner/$EiRepo"
if ($Type -eq 'file') {
switch ($Proxy) {
'github' { return "$github/raw/$Ref/$Asset" }
'gh-proxy' { return "https://gh-proxy.com/$github/raw/$Ref/$Asset" }
'xget' { return "$xget/raw/$Ref/$Asset" }
'jsdelivr' { return "https://cdn.jsdelivr.net/gh/$EiOwner/$EiRepo@$Ref/$Asset" }
'statically' { return "https://cdn.statically.io/gh/$EiOwner/$EiRepo/$Ref/$Asset" }
}
return $null
}
if ($Tag -eq 'latest') {
switch ($Proxy) {
'github' { return "$github/releases/latest/download/$Asset" }
'gh-proxy' { return "https://gh-proxy.com/$github/releases/latest/download/$Asset" }
'xget' { return "$xget/releases/latest/download/$Asset" }
}
} else {
switch ($Proxy) {
'github' { return "$github/releases/download/$Tag/$Asset" }
'gh-proxy' { return "https://gh-proxy.com/$github/releases/download/$Tag/$Asset" }
'xget' { return "$xget/releases/download/$Tag/$Asset" }
}
}
return $null
}
# ---------------------------------------------------------------------------
# Downloading and unpacking
# ---------------------------------------------------------------------------
function Save-EiDownload {
param([string]$Url, [string]$OutFile)
Write-EiLog "downloading $Url"
if ($PSVersionTable.PSEdition -eq 'Desktop') {
# Windows PowerShell 5.1 still defaults to TLS 1.0 on some machines.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}
$parameters = @{ Uri = $Url; OutFile = $OutFile; ErrorAction = 'Stop' }
if ($PSVersionTable.PSEdition -eq 'Desktop') { $parameters['UseBasicParsing'] = $true }
try {
Invoke-WebRequest @parameters
return
} catch {
if (Get-Command 'curl.exe' -ErrorAction SilentlyContinue) {
Write-EiLog "download failed, retrying with curl.exe"
& curl.exe --fail --location --retry 3 --silent --show-error --output $OutFile $Url
if ($LASTEXITCODE -eq 0) { return }
}
throw "could not download $Url"
}
}
function Expand-EiArchive {
param([string]$Archive, [string]$Destination, [string]$Name)
switch -Regex ($Name) {
'\.tar\.gz$|\.tgz$|\.tar\.xz$|\.txz$|\.tar\.bz2$|\.tbz2$' {
if (-not (Get-Command 'tar' -ErrorAction SilentlyContinue)) {
throw "tar is required to extract $Name"
}
& tar -xf $Archive -C $Destination
if ($LASTEXITCODE -ne 0) { throw "could not extract $Name" }
return
}
'\.zip$' {
Expand-Archive -LiteralPath $Archive -DestinationPath $Destination -Force
return
}
'\.gz$' {
# A single gzip-compressed binary. `GzipStream` is always available,
# unlike the `gzip` command on Windows.
$source = [System.IO.File]::OpenRead($Archive)
try {
$gzip = [System.IO.Compression.GzipStream]::new(
$source, [System.IO.Compression.CompressionMode]::Decompress)
try {
$target = [System.IO.File]::Create((Join-Path $Destination $EiBinary))
try { $gzip.CopyTo($target) } finally { $target.Dispose() }
} finally { $gzip.Dispose() }
} finally { $source.Dispose() }
return
}
}
if ($Name -match '\.exe$') {
Copy-Item -LiteralPath $Archive -Destination (Join-Path $Destination "$EiBinary.exe") -Force
return
}
# A bare, unarchived binary.
Copy-Item -LiteralPath $Archive -Destination (Join-Path $Destination $EiBinary) -Force
}
function Find-EiBinary {
param([string]$Directory)
foreach ($candidate in @(
(Join-Path $Directory $EiBinary),
(Join-Path $Directory "$EiBinary.exe"),
(Join-Path (Join-Path $Directory 'bin') $EiBinary),
(Join-Path (Join-Path $Directory 'bin') "$EiBinary.exe")
)) {
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
}
$found = Get-ChildItem -LiteralPath $Directory -Recurse -Depth 3 -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -eq $EiBinary -or $_.Name -eq "$EiBinary.exe" } |
Select-Object -First 1
if ($found) { return $found.FullName }
return $null
}
# ---------------------------------------------------------------------------
# Installation
# ---------------------------------------------------------------------------
function Test-EiDiskSpace {
param([string]$Directory)
if ($MinDiskSpace -le 0) { return }
$probe = $Directory
while (-not (Test-Path -LiteralPath $probe)) {
$parent = Split-Path -Path $probe -Parent
if (-not $parent -or $parent -eq $probe) { break }
$probe = $parent
}
if (-not (Test-Path -LiteralPath $probe)) { return }
$drive = (Get-Item -LiteralPath $probe).PSDrive
if ($drive -and $drive.Free -lt ($MinDiskSpace * 1MB)) {
throw "not enough disk space in $(ConvertTo-EiDisplayPath $Directory)`": $([int]($drive.Free / 1MB))MB available, ${MinDiskSpace}MB required"
}
}
function Add-EiToPath {
param([string]$Directory)
# A PATH entry for a directory that is not there is dead weight: nothing can
# ever be found through it, and the environment or profile only grows.
if (-not (Test-Path -LiteralPath $Directory -PathType Container)) {
Write-EiLog "$(ConvertTo-EiDisplayPath $Directory) does not exist; leaving PATH alone"
return
}
if (Test-EiWindows) {
# Normalise to a native path so that `C:/x/y` and `C:\x\y` dedupe.
$native = try { [System.IO.Path]::GetFullPath($Directory) } catch { $Directory }
$current = [Environment]::GetEnvironmentVariable('Path', 'User')
$existing = @($current -split ';' | Where-Object { $_ } | ForEach-Object {
try { [System.IO.Path]::GetFullPath($_) } catch { $_ }
})
# `-icontains`, not `-contains`: the default happens to be case
# insensitive too, but spelling it out keeps the intent visible.
if ($existing -icontains $native) {
Write-EiLog "$(ConvertTo-EiDisplayPath $native) is already on the user PATH"
$env:Path = "$env:Path;$native"
return
}
$updated = (@($existing) + $native) -join ';'
[Environment]::SetEnvironmentVariable('Path', $updated, 'User')
Write-EiLog "added $(ConvertTo-EiDisplayPath $native) to the user PATH"
Write-EiLog 'restart your terminal to pick up the change'
$env:Path = "$env:Path;$native"
return
}
# This branch only runs on a genuine Unix shell, so `:` is the separator.
# The comparison is case-insensitive for the same reason as the Windows
# branch above: a duplicate entry is worse than a case-only miss.
$onPath = @($env:Path -split ':' | Where-Object { $_ })
if ($onPath -icontains $Directory) {
Write-EiLog "$(ConvertTo-EiDisplayPath $Directory) is already on PATH"
return
}
$shell = if ($env:SHELL) { Split-Path -Path $env:SHELL -Leaf } else { 'sh' }
$config = switch ($shell) {
'fish' { Join-Path $HOME '.config/fish/config.fish' }
'zsh' { Join-Path $HOME '.zshrc' }
'bash' { Join-Path $HOME '.bashrc' }
default { Join-Path $HOME '.profile' }
}
# A POSIX shell reads this file, so it gets the forward-slash spelling even
# when the path itself is a Windows one.
$shPath = ConvertTo-EiDisplayPath $Directory
$line = if ($shell -eq 'fish') { "fish_add_path $shPath" } else { "export PATH=`"$shPath`:`$PATH`"" }
# Only a regular file can be read and appended to. A directory sitting at
# the profile path, or one that cannot be read, has to be treated as "no
# profile yet" rather than allowed to throw: `$ErrorActionPreference` is
# `Stop`, so an unguarded `Select-String` would abort the whole install
# after the binary was already in place.
$exists = Test-Path -LiteralPath $config -PathType Leaf
$mentioned = $false
if ($exists) {
try { $mentioned = [bool](Select-String -LiteralPath $config -SimpleMatch $shPath -Quiet -ErrorAction Stop) }
catch { $mentioned = $false }
}
if ($mentioned) {
Write-EiLog "$(ConvertTo-EiDisplayPath $config) already mentions $shPath"
} else {
if ($shell -eq 'fish') { New-Item -ItemType Directory -Path (Split-Path $config -Parent) -Force | Out-Null }
try {
Add-Content -LiteralPath $config -Value "`n# Added by the $EiBinary installer`n$line" -ErrorAction Stop
Write-EiLog "added $shPath to $(ConvertTo-EiDisplayPath $config)"
} catch {
# A read-only home directory is unusual but not fatal: the binary is
# installed either way, so say what to add rather than aborting.
Write-EiLog "could not write to $(ConvertTo-EiDisplayPath $config)"
Write-EiLog 'add this to your shell profile by hand:'
Write-EiLog " $line"
return
}
}
Write-EiLog "restart your shell (or run: . $(ConvertTo-EiDisplayPath $config)) to pick up the change"
}
function Show-EiUsage {
@"
Install $EiBinary from GitHub ($EiOwner/$EiRepo).
Usage: install.ps1 [options]
Options:
-Proxy <type> mirror to download through: github, gh-proxy, xget,
jsdelivr, statically (default: $Proxy)
-Tag <tag> release tag to install (default: $Tag)
-Dir <path> installation directory (default: $Dir)
-Target <triple> force a Rust target triple instead of auto-detection
-Type <type> resource type: release|file (default: $Type)
-Ref <reference> branch/tag/commit used by -Type file (default: $Ref)
-MinDiskSpace <mb> minimum free disk space (default: $MinDiskSpace)
-File <path> install from a local file instead of downloading;
its name must match a release asset
-List list the target triples this installer knows
-Help show this help
Environment variables mirror the options above:
EI_PROXY EI_TAG EI_DIR EI_TARGET EI_TYPE EI_REF EI_MIN_DISK_SPACE EI_FILE
Supported targets:
$((($EiAssets.Keys | ForEach-Object { " $_" }) -join "`n"))
"@ | Write-Host
}
function Install-EiBinary {
# `-List` is a question about this installer, not about a release, so it is
# answered before the rest of the configuration is validated.
if ($List) {
$EiAssets.Keys | ForEach-Object { Write-Host $_ }
return
}
if ($Type -eq 'release' -and $Proxy -in @('jsdelivr', 'statically')) {
# Only worth complaining about when something will actually be
# downloaded: an offline install never builds a URL.
if (-not $File) {
throw "the $Proxy proxy cannot serve release assets; use -Proxy github or -Type file"
}
}
if ($File -and -not (Test-Path -LiteralPath $File -PathType Leaf)) {
throw "no such file: $(ConvertTo-EiDisplayPath $File)"
}
$triple = Resolve-EiTarget
if (-not $triple) {
Write-EiLog "supported targets: $((($EiAssets.Keys) -join ' '))"
if ($Target) {
throw "no build for target $Target (check -Target, or drop it to auto-detect)"
}
throw "no prebuilt binary for $(Get-EiArch)"
}
# PowerShell variable names are case-insensitive, so this must not be called
# `$file`: that would silently overwrite the `$File` parameter above and
# turn a local install into a download of a bare file name.
$asset = Get-EiPlatformFilename $triple
$installDir = Expand-EiPath $Dir
if ($File) {
Write-EiLog "installing $EiBinary ($triple) from $(ConvertTo-EiDisplayPath $File)"
} else {
Write-EiLog "installing $EiBinary ($triple) into $(ConvertTo-EiDisplayPath $installDir)"
}
Test-EiDiskSpace $installDir
$script:EiTemp = Join-Path ([System.IO.Path]::GetTempPath()) ("eish-" + [System.Guid]::NewGuid().ToString('n'))
New-Item -ItemType Directory -Path $EiTemp -Force | Out-Null
try {
# A local file is used where it lies, so it is never copied or deleted.
$archive = Join-Path $EiTemp $asset
if ($File) {
$archive = $File
} else {
Save-EiDownload (Get-EiDownloadUrl $asset) $archive
}
Expand-EiArchive $archive $EiTemp $asset
$binary = Find-EiBinary $EiTemp
if (-not $binary) { throw "could not find $EiBinary inside $asset" }
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
$stem = if (Test-EiWindows) { "$EiBinary.exe" } else { $EiBinary }
$destination = Join-Path $installDir $stem
Copy-Item -LiteralPath $binary -Destination $destination -Force
if (-not (Test-EiWindows)) {
if (Get-Command 'chmod' -ErrorAction SilentlyContinue) { & chmod 0755 $destination }
}
Write-EiLog "installed $(ConvertTo-EiDisplayPath $destination)"
Add-EiToPath $installDir
if ($env:GITHUB_PATH) {
Add-Content -LiteralPath $env:GITHUB_PATH -Value $installDir
Write-EiLog "added $(ConvertTo-EiDisplayPath $installDir) to GITHUB_PATH"
}
} finally {
if ($EiTemp -and (Test-Path -LiteralPath $EiTemp)) {
Remove-Item -LiteralPath $EiTemp -Recurse -Force -ErrorAction SilentlyContinue
$script:EiTemp = $null
}
}
}
if ($Help) {
Show-EiUsage
exit 0
}
try {
Install-EiBinary
} catch {
Write-EiError $_.Exception.Message
exit 1
}