#!/usr/bin/env fish
#
# 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.fish --help` for the available options.
#
# ---------------------------------------------------------------------------
# Configuration (generated by eish <{ eish_version }>)
# ---------------------------------------------------------------------------
set -g EI_OWNER '<{ owner }>'
set -g EI_REPO '<{ repo }>'
set -g EI_BINARY_NAME '<{ binary }>'
set -q EI_PROXY; or set -g EI_PROXY '<{ proxy }>'
set -q EI_TAG; or set -g EI_TAG '<{ tag }>'
set -q EI_DIR; or set -g EI_DIR (string replace -r '^~' "$HOME" '<{ install_dir }>')
set -q EI_TYPE; or set -g EI_TYPE '<{ resource_type }>'
set -q EI_REF; or set -g EI_REF '<{ resource_ref }>'
set -q EI_TARGET; or set -g EI_TARGET '<{ default_target }>'
set -q EI_MIN_DISK_SPACE; or set -g EI_MIN_DISK_SPACE '<{ min_disk_space_mb }>'
set -q EI_FILE; or set -g EI_FILE ''
# Target triples bundled in this installer.
set -g EI_SUPPORTED_TARGETS <{ targets | join(" ") }>
# Asset file names bundled in this installer.
set -g EI_SUPPORTED_ASSETS <{ filenames | join(" ") }>
set -g EI_TMP ''
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function ei_log
printf '%s\n' $argv >&2
end
function ei_cleanup
if test -n "$EI_TMP"; and test -d "$EI_TMP"
rm -rf "$EI_TMP"
set -g EI_TMP ''
end
end
function ei_die
printf 'error: %s\n' $argv >&2
ei_cleanup
exit 1
end
function ei_command_exists
type -q $argv[1]
end
function ei_usage
printf '%s\n' \
"Install $EI_BINARY_NAME from GitHub ($EI_OWNER/$EI_REPO)." \
'' \
'Usage: install.fish [options]' \
'' \
'Options:' \
' --proxy <type> mirror to download through: github, gh-proxy, xget,' \
" jsdelivr, statically (default: $EI_PROXY)" \
" --tag <tag> release tag to install (default: $EI_TAG)" \
" --dir <path> installation directory (default: $EI_DIR)" \
' --target <triple> force a Rust target triple instead of auto-detection' \
" --type <type> resource type: release|file (default: $EI_TYPE)" \
" --ref <reference> branch/tag/commit used by --type file (default: $EI_REF)" \
' --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' \
' -h, --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:'
for target in $EI_SUPPORTED_TARGETS
printf ' %s\n' $target
end
end
# ---------------------------------------------------------------------------
# Platform table: Rust target triple -> release asset file name
# ---------------------------------------------------------------------------
function ei_platform_filename
switch $argv[1]<% for asset in assets %>
case '<{ asset.target }>'
echo '<{ asset.filename }>'
<%- endfor %>
case '*'
return 1
end
end
# Compatible triples to try, in order, when no exact build exists.
function ei_platform_fallbacks
switch $argv[1]<% for fallback in fallbacks %>
case '<{ fallback.target }>'
echo <{ fallback.alternatives | join(" ") }>
<%- endfor %>
case '*'
return 1
end
return 0
end
# Targets that can be installed from a given asset file name.
#
# Several targets may share one file, so this returns them all and the caller
# picks.
function ei_platform_targets_for_asset
switch $argv[1]<% for group in asset_groups %>
case '<{ group.filename }>'
echo <{ group.targets | join(" ") }>
<%- endfor %>
case '*'
return 1
end
return 0
end
# Detect the libc flavour of a GNU/Linux system: prints "musl" or "gnu".
function ei_detect_libc
for candidate in /bin/sh /usr/bin/ldd /lib/libc.so.6 /lib64/libc.so.6
test -f $candidate; or continue
if ei_command_exists strings
if strings $candidate 2>/dev/null | grep -q musl
echo musl
return 0
end
end
if grep -qa musl $candidate 2>/dev/null
echo musl
return 0
end
end
for ld in /lib/ld-musl-x86_64.so.1 /lib/ld-musl-aarch64.so.1 /lib/ld-musl-armhf.so.1
if test -f $ld
echo musl
return 0
end
end
if test -f /etc/os-release
if grep -qi 'alpine\|void' /etc/os-release 2>/dev/null
echo musl
return 0
end
end
if ei_command_exists ldd
if ldd --version 2>&1 | head -n 1 | grep -qi musl
echo musl
return 0
end
end
echo gnu
end
# Print the Rust target triple this machine needs.
function ei_detect_platform
set -l os (uname -s 2>/dev/null; or echo unknown)
set -l arch (uname -m 2>/dev/null; or echo unknown)
if test "$os" = Linux
set -l kernel (uname -o 2>/dev/null; or echo '')
if test "$kernel" = Android
switch $arch
case aarch64 arm64
echo aarch64-linux-android
case 'armv7*' armv8l
echo armv7-linux-androideabi
case i686 x86
echo i686-linux-android
case x86_64 amd64
echo x86_64-linux-android
end
return 0
end
end
switch $os
case 'MINGW*' 'MSYS*' 'CYGWIN*' 'Windows*' 'UWIN*'
switch $arch
case x86_64 amd64
echo x86_64-pc-windows-gnu
case i686 i386
echo i686-pc-windows-gnu
case aarch64 arm64
echo aarch64-pc-windows-msvc
end
return 0
case Darwin
switch $arch
case arm64 aarch64
echo aarch64-apple-darwin
case '*'
echo x86_64-apple-darwin
end
return 0
case FreeBSD
if test $arch = x86_64; or test $arch = amd64
echo x86_64-unknown-freebsd
end
return 0
case NetBSD
echo x86_64-unknown-netbsd
return 0
end
set -l libc (ei_detect_libc)
switch $arch
case x86_64 amd64
echo "x86_64-unknown-linux-$libc"
case aarch64 arm64
echo "aarch64-unknown-linux-$libc"
case 'armv7*' armv8l
printf 'armv7-unknown-linux-%seabihf\n' $libc
case 'arm*'
printf 'arm-unknown-linux-%seabihf\n' $libc
case i686 i386
echo "i686-unknown-linux-$libc"
case 'riscv64*'
echo "riscv64gc-unknown-linux-$libc"
case 'loongarch64*'
echo "loongarch64-unknown-linux-$libc"
case ppc64le powerpc64le
echo powerpc64le-unknown-linux-gnu
case s390x
echo s390x-unknown-linux-gnu
end
end
# Resolve the target triple to install, honouring EI_FILE and EI_TARGET, and
# falling back to a compatible build when the exact one was not published.
function ei_resolve_target
if test -n "$EI_FILE"
ei_resolve_target_from_file
return $status
end
set -l primary $EI_TARGET
if test -z "$primary"
set primary (ei_detect_platform)
end
if test -z "$primary"
return 1
end
if ei_platform_filename $primary >/dev/null 2>&1
echo $primary
return 0
end
for alternative in (ei_platform_fallbacks $primary 2>/dev/null)
if ei_platform_filename $alternative >/dev/null 2>&1
ei_log "no build for $primary, using $alternative instead"
echo $alternative
return 0
end
end
return 1
end
# 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 ei_resolve_target_from_file
set -l name (basename $EI_FILE)
set -l matches (ei_platform_targets_for_asset $name 2>/dev/null)
if test -z "$matches"
ei_log "known assets: $EI_SUPPORTED_ASSETS"
ei_die "$name is not an asset of $EI_OWNER/$EI_REPO (see the list above)"
end
if test -n "$EI_TARGET"
if contains -- $EI_TARGET $matches
echo $EI_TARGET
return 0
end
ei_die "$name is for $matches, not $EI_TARGET"
end
set -l detected (ei_detect_platform)
if contains -- $detected $matches
echo $detected
return 0
end
ei_log "installing $name for $matches[1] (detected $detected)"
echo $matches[1]
return 0
end
function ei_is_windows
set -l os (uname -s 2>/dev/null; or echo '')
string match -qr '^(MINGW|MSYS|CYGWIN|Windows|UWIN)' -- $os
end
# Name of a PowerShell interpreter, or nothing if there is none.
#
# 5.1 (`powershell`) and 7+ (`pwsh`) both do everything this script asks of
# them, so whichever is present will do.
function ei_powershell_bin
if ei_command_exists powershell
echo powershell
else if ei_command_exists pwsh
echo pwsh
end
end
# The path as a Windows program spells it: backslashes, drive letter.
#
# This is for values a Windows program will consume — the registry, GITHUB_PATH
# — not for anything a person reads; `ei_display_path` covers those. Without
# `cygpath` the path is passed through rather than guessed: `GetFullPath` would
# read `/c/Users/x` as a path on the current drive and produce `C:\c\Users\x`.
function ei_windows_path --argument-names path
if ei_command_exists cygpath
cygpath -w $path
return 0
end
echo $path
end
# Expand `~` and make the path absolute.
#
# An absolute path matters because the directory ends up in a shell profile as
# `export PATH="..."`; a relative one would break the next time the shell
# started somewhere else.
#
# This deliberately avoids `cd`: fish runs command substitutions in the current
# shell rather than a subshell, so a `cd` inside one leaks out and would leave
# the caller sitting in the install directory.
function ei_resolve_dir --argument-names path
set -l raw (string replace -r '^~' "$HOME" -- $path)
if test -z "$raw"
return 0
end
if type -q path
set -l resolved (path resolve $raw 2>/dev/null)
if test -n "$resolved"
echo $resolved
return 0
end
end
# GNU `realpath -m` tolerates a directory that does not exist yet; the
# version without it does not, so it is only used when `path` is missing.
if command -q realpath
set -l resolved (realpath -m $raw 2>/dev/null)
if test -n "$resolved"
echo $resolved
return 0
end
end
# Lower on features than `path resolve`, which handles missing directories
# too; this only catches the remaining case, a relative path with nothing
# on disk to anchor to.
if string match -qv '^/' -- $raw; and test -n "$PWD"
echo "$PWD/$raw"
return 0
end
echo $raw
end
# Render a path the way the platform spells it.
#
# Under MSYS2 `$HOME` is `/c/Users/you`, which no Windows program can use.
# `cygpath -m` gives the mixed form `C:/Users/you`: Windows accepts forward
# slashes, and it is still valid when pasted back into a POSIX shell. Only
# messages go through this — the paths handed to `cp`, `install` and `mkdir`
# keep the shell's own spelling, which is what those tools expect.
#
# Without `cygpath` the path is printed as-is rather than rewritten by hand: a
# `/x/...` path only maps to a drive when `/x` is a mount point, and guessing
# mangles things like `/etc/hosts` into `E://etc/hosts`.
function ei_display_path --argument-names path
if ei_is_windows; and ei_command_exists cygpath
cygpath -m $path
return 0
end
echo $path
end
# ---------------------------------------------------------------------------
# Downloading and unpacking
# ---------------------------------------------------------------------------
function ei_download_url --argument-names file
set -l gh "https://github.com/$EI_OWNER/$EI_REPO"
set -l xget "https://xget.xi-xu.me/gh/$EI_OWNER/$EI_REPO"
if test "$EI_TYPE" = file
switch $EI_PROXY
case github
echo "$gh/raw/$EI_REF/$file"
case gh-proxy
echo "https://gh-proxy.com/$gh/raw/$EI_REF/$file"
case xget
echo "$xget/raw/$EI_REF/$file"
case jsdelivr
echo "https://cdn.jsdelivr.net/gh/$EI_OWNER/$EI_REPO@$EI_REF/$file"
case statically
echo "https://cdn.statically.io/gh/$EI_OWNER/$EI_REPO/$EI_REF/$file"
end
return 0
end
if test "$EI_TAG" = latest
switch $EI_PROXY
case github
echo "$gh/releases/latest/download/$file"
case gh-proxy
echo "https://gh-proxy.com/$gh/releases/latest/download/$file"
case xget
echo "$xget/releases/latest/download/$file"
end
else
switch $EI_PROXY
case github
echo "$gh/releases/download/$EI_TAG/$file"
case gh-proxy
echo "https://gh-proxy.com/$gh/releases/download/$EI_TAG/$file"
case xget
echo "$xget/releases/download/$EI_TAG/$file"
end
end
return 0
end
function ei_download --argument-names url out
ei_log "downloading $url"
if ei_command_exists curl
set -l opts --fail --location --retry 3 --max-time 300
if test -t 2
set -a opts --progress-bar
else
set -a opts --silent --show-error
end
curl $opts --output $out $url
return $status
end
if ei_command_exists wget
wget --tries=3 --timeout=300 --output-document=$out $url
return $status
end
ei_die 'neither curl nor wget is available; install one of them and retry'
end
function ei_extract --argument-names archive dest name
switch $name
case '*.tar.gz' '*.tgz'
ei_command_exists tar; or ei_die 'tar is required to extract .tar.gz archives'
tar -xzf $archive -C $dest
case '*.tar.xz' '*.txz'
ei_command_exists tar; or ei_die 'tar is required to extract .tar.xz archives'
tar -xJf $archive -C $dest
case '*.tar.bz2' '*.tbz2'
ei_command_exists tar; or ei_die 'tar is required to extract .tar.bz2 archives'
tar -xjf $archive -C $dest
case '*.zip'
if ei_command_exists unzip
unzip -q -o $archive -d $dest
else if test -n (ei_powershell_bin)
# Bound to a variable: fish will not accept a command
# substitution in command position.
set -l ps_bin (ei_powershell_bin)
set -l ps_archive $archive
set -l ps_dest $dest
if ei_command_exists cygpath
set ps_archive (cygpath -w $archive)
set ps_dest (cygpath -w $dest)
end
$ps_bin -NoProfile -Command "Expand-Archive -LiteralPath '$ps_archive' -DestinationPath '$ps_dest' -Force"
else
ei_die 'unzip (or powershell) is required to extract .zip archives'
end
case '*.exe'
cp $archive "$dest/$EI_BINARY_NAME.exe"
case '*.gz'
ei_command_exists gzip; or ei_die 'gzip is required to extract .gz archives'
gzip -dc $archive > "$dest/$EI_BINARY_NAME"
case '*'
cp $archive "$dest/$EI_BINARY_NAME"
end
end
function ei_find_binary --argument-names dir
for path in "$dir/$EI_BINARY_NAME" "$dir/$EI_BINARY_NAME.exe" "$dir/bin/$EI_BINARY_NAME" "$dir/bin/$EI_BINARY_NAME.exe"
if test -f "$path"
echo "$path"
return 0
end
end
set -l found (find $dir -maxdepth 3 -type f \( -name "$EI_BINARY_NAME" -o -name "$EI_BINARY_NAME.exe" \) 2>/dev/null | head -n 1)
if test -n "$found"
echo "$found"
return 0
end
return 1
end
# ---------------------------------------------------------------------------
# Installation
# ---------------------------------------------------------------------------
function ei_check_disk_space --argument-names dir
if not string match -qr '^[0-9]+$' -- "$EI_MIN_DISK_SPACE"
return 0
end
if test $EI_MIN_DISK_SPACE -le 0
return 0
end
set -l probe $dir
while test -n "$probe"; and not test -d "$probe"
set -l parent (dirname $probe)
if test "$parent" = "$probe"
break
end
set probe $parent
end
test -d "$probe"; or return 0
set -l free (df -Pk $probe 2>/dev/null | awk 'NR==2 {printf "%d", $4 / 1024}')
if not string match -qr '^[0-9]+$' -- "$free"
return 0
end
if test $free -lt $EI_MIN_DISK_SPACE
ei_die (string join '' 'not enough disk space in ' (ei_display_path $dir) ': ' $free 'MB available, ' $EI_MIN_DISK_SPACE 'MB required')
end
end
function ei_update_path_fish --argument-names dir
# A PATH entry for a directory that is not there is dead weight: nothing can
# ever be found through it, and the config only grows.
if not test -d "$dir"
ei_log (ei_display_path $dir)" does not exist; leaving PATH alone"
return 0
end
# Case-insensitively: on Windows `Path` and `path` are the same directory,
# so an exact match would add a second entry that does nothing. The odd
# false skip on a case-sensitive filesystem needs two directories differing
# only in case, which is not worth the duplicate it prevents.
#
# Both sides are lowered rather than using a flag: for fish's `contains`,
# `-i` means `--index`, not case-insensitive.
if contains -- (string lower -- $dir) (string lower -- $PATH)
ei_log (ei_display_path $dir)" is already on PATH"
return 0
end
set -l cfg "$HOME/.config/fish/config.fish"
mkdir -p (dirname $cfg) 2>/dev/null
if test -f $cfg; and grep -Fiq -- $dir $cfg 2>/dev/null
ei_log "$cfg already mentions "(ei_display_path $dir)
ei_log "restart your shell (or run: source $cfg) to pick up the change"
return 0
end
# A read-only home directory is unusual but not fatal: the binary is
# installed either way, so say what to add rather than aborting here.
if not printf '\n# Added by the %s installer\nfish_add_path %s\n' $EI_BINARY_NAME $dir >> $cfg 2>/dev/null
ei_log "could not write to $cfg"
ei_log 'add this to your fish config by hand:'
ei_log " fish_add_path $dir"
return 0
end
set -gx PATH $dir $PATH
ei_log "added "(ei_display_path $dir)" to $cfg"
ei_log "restart your shell (or run: source $cfg) to pick up the change"
end
function ei_update_path_windows --argument-names dir
# A PATH entry for a directory that is not there is dead weight: nothing can
# ever be found through it, and the registry only grows.
if not test -d "$dir"
ei_log (ei_display_path $dir)" does not exist; leaving PATH alone"
return 0
end
# Writing a POSIX path into the registry would be worse than doing nothing:
# nothing on Windows could use it. Without `cygpath` there is no trustworthy
# way to spell it, so leave the environment alone and say so.
if not ei_command_exists cygpath
ei_log "add "(ei_display_path $dir)" to your PATH manually"
return 0
end
# The PATH value keeps the native `C:\...` spelling. Slashes and letter case
# are normalised on both sides before comparing, because the registry may
# already hold `C:/x/y` or `c:\X\Y` for the very same directory.
set -l win_dir (cygpath -w $dir)
set -l ps (ei_powershell_bin)
if test -z "$ps"
ei_log "add "(ei_display_path $dir)" to your PATH manually"
return 0
end
# Prints `present` when the directory is already there, so the message can
# say which of the two things happened.
set -l result ($ps -NoProfile -Command "\$native = '$win_dir'.TrimEnd('\'); \$p = [Environment]::GetEnvironmentVariable('Path', 'User'); \$found = \$false; foreach (\$e in (\$p -split ';')) { if (\$e -and (\$e -replace '/','\').TrimEnd('\') -ieq \$native) { \$found = \$true; break } }; if (\$found) { 'present' } else { [Environment]::SetEnvironmentVariable('Path', \"\$p;\$native\", 'User') }" 2>/dev/null)
if test $status -ne 0
ei_log 'could not update the Windows PATH automatically'
ei_log "add "(ei_display_path $dir)" to your PATH manually"
return 0
end
if test "$result" = present
ei_log (ei_display_path $dir)" is already on the user PATH"
return 0
end
ei_log "added "(ei_display_path $dir)" to the user PATH"
ei_log 'restart your terminal to pick up the change'
end
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
function ei_validate_config
switch $EI_PROXY
case github gh-proxy xget jsdelivr statically
case '*'
ei_die "unknown proxy: $EI_PROXY"
end
switch $EI_TYPE
case release file
case '*'
ei_die "unknown resource type: $EI_TYPE (expected release or file)"
end
if test "$EI_TYPE" = release
switch $EI_PROXY
case jsdelivr statically
# Only worth complaining about when something will actually be
# downloaded: an offline install never builds a URL.
if test -z "$EI_FILE"
ei_die "the $EI_PROXY proxy cannot serve release assets; use --proxy github or --type file"
end
end
end
if test -n "$EI_FILE"; and not test -f "$EI_FILE"
ei_die "no such file: "(ei_display_path $EI_FILE)
end
end
function ei_main
argparse -n install.fish 'p/proxy=' 't/tag=' 'd/dir=' 'T/target=' 'type=' 'ref=' 'file=' 'list' 'h/help' -- $argv
or return 1
if set -q _flag_help
ei_usage
return 0
end
if set -q _flag_proxy
set -g EI_PROXY $_flag_proxy
end
if set -q _flag_tag
set -g EI_TAG $_flag_tag
end
if set -q _flag_dir
set -g EI_DIR (ei_resolve_dir $_flag_dir)
end
if set -q _flag_target
set -g EI_TARGET $_flag_target
end
if set -q _flag_type
set -g EI_TYPE $_flag_type
end
if set -q _flag_ref
set -g EI_REF $_flag_ref
end
if set -q _flag_file
set -g EI_FILE $_flag_file
end
# `~` in the default and any relative `--dir` both become absolute here, so
# that whatever lands in a shell profile is usable from anywhere.
set -g EI_DIR (ei_resolve_dir $EI_DIR)
if set -q _flag_list
for target in $EI_SUPPORTED_TARGETS
echo $target
end
return 0
end
# After the list check: `--list` is a question about this installer, not
# about a release, so it answers even with an unusable proxy or a missing
# --file.
ei_validate_config
set -l target (ei_resolve_target)
if test $status -ne 0; or test -z "$target"
ei_log "supported targets: $EI_SUPPORTED_TARGETS"
if test -n "$EI_TARGET"
ei_die "no build for target $EI_TARGET (check --target, or drop it to auto-detect)"
end
ei_die "no prebuilt binary for "(uname -s)"/"(uname -m)
end
set -l filename (ei_platform_filename $target)
if test -n "$EI_FILE"
ei_log "installing $EI_BINARY_NAME ($target) from "(ei_display_path $EI_FILE)
else
ei_log "installing $EI_BINARY_NAME ($target) into "(ei_display_path $EI_DIR)
end
ei_check_disk_space $EI_DIR
set -g EI_TMP (mktemp -d 2>/dev/null; or echo "$TMPDIR/eish-$fish_pid")
mkdir -p $EI_TMP
# A local file is used where it lies, so it is never copied or deleted.
set -l archive "$EI_TMP/$filename"
if test -n "$EI_FILE"
set archive $EI_FILE
else
ei_download (ei_download_url $filename) $archive
or ei_die "download failed"
end
ei_extract $archive "$EI_TMP" "$filename"
or ei_die "could not extract $filename"
set -l binary (ei_find_binary $EI_TMP)
if test $status -ne 0; or test -z "$binary"
ei_die "could not find $EI_BINARY_NAME inside $filename"
end
mkdir -p $EI_DIR
set -l stem $EI_BINARY_NAME
if ei_is_windows
set stem "$EI_BINARY_NAME.exe"
end
if ei_command_exists install
install -m 0755 $binary "$EI_DIR/$stem"
else
cp $binary "$EI_DIR/$stem"
chmod 0755 "$EI_DIR/$stem"
end
or ei_die "could not install to "(ei_display_path "$EI_DIR/$stem")
ei_log "installed "(ei_display_path "$EI_DIR/$stem")
if ei_is_windows
ei_update_path_windows $EI_DIR
else
ei_update_path_fish $EI_DIR
end
if set -q GITHUB_PATH
# The file is read by the CI runner, not by a person, so it gets the
# platform's own spelling: a POSIX path in a Windows runner's PATH is
# useless to everything but MSYS itself.
if ei_is_windows
echo (ei_windows_path $EI_DIR) >> $GITHUB_PATH
else
echo $EI_DIR >> $GITHUB_PATH
end
ei_log "added "(ei_display_path $EI_DIR)" to GITHUB_PATH"
end
ei_cleanup
return 0
end
ei_main $argv