#!/usr/bin/env bash
#
# 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.sh --help` for the available options.
#
set -e

# ---------------------------------------------------------------------------
# Configuration (generated by eish <{ eish_version }>)
# ---------------------------------------------------------------------------
EI_OWNER='<{ owner }>'
EI_REPO='<{ repo }>'
EI_BINARY_NAME='<{ binary }>'

EI_PROXY="${EI_PROXY:-<{ proxy }>}"
EI_TAG="${EI_TAG:-<{ tag }>}"
EI_DIR="${EI_DIR:-<{ install_dir }>}"
EI_TYPE="${EI_TYPE:-<{ resource_type }>}"
EI_REF="${EI_REF:-<{ resource_ref }>}"
EI_TARGET="${EI_TARGET:-<{ default_target }>}"
EI_MIN_DISK_SPACE="${EI_MIN_DISK_SPACE:-<{ min_disk_space_mb }>}"

# Path to an already-downloaded asset, for an offline install. When set, the
# file name decides which target is installed and nothing is downloaded.
EI_FILE="${EI_FILE:-}"

# Target triples bundled in this installer, space separated.
EI_SUPPORTED_TARGETS='<{ targets | join(" ") }>'

# Asset file names bundled in this installer, space separated.
EI_SUPPORTED_ASSETS='<{ filenames | join(" ") }>'

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
log() {
    printf '%s\n' "$*" >&2
}

die() {
    printf 'error: %s\n' "$*" >&2
    exit 1
}

command_exists() {
    command -v "$1" >/dev/null 2>&1
}

# 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.
powershell_bin() {
    if command_exists powershell; then
        printf 'powershell\n'
    elif command_exists pwsh; then
        printf 'pwsh\n'
    fi
}

usage() {
    cat <<EI_USAGE
Install $EI_BINARY_NAME from GitHub ($EI_OWNER/$EI_REPO).

Usage: $0 [options]

Options:
  --proxy <type>     download through a mirror: 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:
$(printf '  %s\n' $EI_SUPPORTED_TARGETS)
EI_USAGE
}

# ---------------------------------------------------------------------------
# Platform table: Rust target triple -> release asset file name
# ---------------------------------------------------------------------------
platform_filename() {
    case "$1" in<% for asset in assets %>
        '<{ asset.target }>') echo '<{ asset.filename }>' ;;
<%- endfor %>
        *) echo '' ;;
    esac
}

# Compatible triples to try, in order, when no exact build exists.
platform_fallbacks() {
    case "$1" in<% for fallback in fallbacks %>
        '<{ fallback.target }>') echo '<{ fallback.alternatives | join(" ") }>' ;;
<%- endfor %>
        *) echo '' ;;
    esac
}

# Targets that can be installed from a given asset file name.
#
# Several targets may share one file — a release that ships a single
# `tool-windows-x64.zip` serves both the msvc and the gnu target — so this
# returns them all and the caller picks.
platform_targets_for_asset() {
    case "$1" in<% for group in asset_groups %>
        '<{ group.filename }>') echo '<{ group.targets | join(" ") }>' ;;
<%- endfor %>
        *) echo '' ;;
    esac
}

# Detect the libc flavour of a GNU/Linux system: prints "musl" or "gnu".
detect_libc() {
    for candidate in /bin/sh /usr/bin/ldd /lib/libc.so.6 /lib64/libc.so.6; do
        [ -f "$candidate" ] || continue
        if command_exists strings && strings "$candidate" 2>/dev/null | grep -q musl; then
            echo musl
            return 0
        fi
        if grep -qa musl "$candidate" 2>/dev/null; then
            echo musl
            return 0
        fi
    done

    for ld in /lib/ld-musl-x86_64.so.1 /lib/ld-musl-aarch64.so.1 /lib/ld-musl-armhf.so.1; do
        if [ -f "$ld" ]; then
            echo musl
            return 0
        fi
    done

    if [ -f /etc/os-release ] && grep -qi 'alpine\|void' /etc/os-release 2>/dev/null; then
        echo musl
        return 0
    fi

    if command_exists ldd; then
        if ldd --version 2>&1 | head -n 1 | grep -qi musl; then
            echo musl
            return 0
        fi
    fi

    echo gnu
}

# Print the Rust target triple this machine needs.
detect_platform() {
    ei_os="$(uname -s 2>/dev/null || echo unknown)"
    ei_arch="$(uname -m 2>/dev/null || echo unknown)"

    if [ "$ei_os" = Linux ] && [ "$(uname -o 2>/dev/null || echo '')" = Android ]; then
        case "$ei_arch" in
            aarch64|arm64) echo 'aarch64-linux-android' ;;
            armv7*|armv8l) echo 'armv7-linux-androideabi' ;;
            i686|x86)      echo 'i686-linux-android' ;;
            x86_64|amd64)  echo 'x86_64-linux-android' ;;
            *)             echo '' ;;
        esac
        return 0
    fi

    case "$ei_os" in
        MINGW*|MSYS*|CYGWIN*|Windows*|UWIN*)
            case "$ei_arch" in
                x86_64|amd64)  echo 'x86_64-pc-windows-gnu' ;;
                i686|i386)     echo 'i686-pc-windows-gnu' ;;
                aarch64|arm64) echo 'aarch64-pc-windows-msvc' ;;
                *)             echo '' ;;
            esac
            return 0
            ;;
        Darwin)
            case "$ei_arch" in
                arm64|aarch64) echo 'aarch64-apple-darwin' ;;
                *)             echo 'x86_64-apple-darwin' ;;
            esac
            return 0
            ;;
        FreeBSD)
            if [ "$ei_arch" = x86_64 ] || [ "$ei_arch" = amd64 ]; then
                echo 'x86_64-unknown-freebsd'
            fi
            return 0
            ;;
        NetBSD)
            echo 'x86_64-unknown-netbsd'
            return 0
            ;;
    esac

    ei_libc="$(detect_libc)"
    case "$ei_arch" in
        x86_64|amd64)        echo "x86_64-unknown-linux-$ei_libc" ;;
        aarch64|arm64)       echo "aarch64-unknown-linux-$ei_libc" ;;
        armv7*|armv8l)       echo "armv7-unknown-linux-${ei_libc}eabihf" ;;
        arm*)                echo "arm-unknown-linux-${ei_libc}eabihf" ;;
        i686|i386)           echo "i686-unknown-linux-$ei_libc" ;;
        riscv64*)            echo "riscv64gc-unknown-linux-$ei_libc" ;;
        loongarch64*)        echo "loongarch64-unknown-linux-$ei_libc" ;;
        ppc64le|powerpc64le) echo 'powerpc64le-unknown-linux-gnu' ;;
        s390x)               echo 's390x-unknown-linux-gnu' ;;
        *)                   echo '' ;;
    esac
}

# 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.
resolve_target() {
    if [ -n "$EI_FILE" ]; then
        resolve_target_from_file
        return 0
    fi

    ei_primary="$EI_TARGET"
    if [ -z "$ei_primary" ]; then
        ei_primary="$(detect_platform)"
    fi
    if [ -z "$ei_primary" ]; then
        echo ''
        return 0
    fi
    if [ -n "$(platform_filename "$ei_primary")" ]; then
        echo "$ei_primary"
        return 0
    fi
    for ei_alt in $(platform_fallbacks "$ei_primary"); do
        if [ -n "$(platform_filename "$ei_alt")" ]; then
            log "no build for $ei_primary, using $ei_alt instead"
            echo "$ei_alt"
            return 0
        fi
    done
    echo ''
}

# 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; anything else is a typo or the wrong file
# entirely. 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.
resolve_target_from_file() {
    ei_name="$(basename "$EI_FILE")"
    ei_matches="$(platform_targets_for_asset "$ei_name")"

    if [ -z "$ei_matches" ]; then
        log "known assets: $EI_SUPPORTED_ASSETS"
        die "$ei_name is not an asset of $EI_OWNER/$EI_REPO (see the list above)"
    fi

    if [ -n "$EI_TARGET" ]; then
        case " $ei_matches " in
            *" $EI_TARGET "*) echo "$EI_TARGET"; return 0 ;;
            *) die "$ei_name is for $ei_matches, not $EI_TARGET" ;;
        esac
    fi

    ei_detected="$(detect_platform)"
    case " $ei_matches " in
        *" $ei_detected "*) echo "$ei_detected"; return 0 ;;
    esac

    for ei_candidate in $ei_matches; do
        log "installing $ei_name for $ei_candidate (detected $ei_detected)"
        echo "$ei_candidate"
        return 0
    done
}

# ---------------------------------------------------------------------------
# Downloading and unpacking
# ---------------------------------------------------------------------------
download_url() {
    ei_file="$1"
    ei_gh="https://github.com/$EI_OWNER/$EI_REPO"
    ei_xget="https://xget.xi-xu.me/gh/$EI_OWNER/$EI_REPO"

    if [ "$EI_TYPE" = file ]; then
        case "$EI_PROXY" in
            github)     echo "$ei_gh/raw/$EI_REF/$ei_file" ;;
            gh-proxy)   echo "https://gh-proxy.com/$ei_gh/raw/$EI_REF/$ei_file" ;;
            xget)       echo "$ei_xget/raw/$EI_REF/$ei_file" ;;
            jsdelivr)   echo "https://cdn.jsdelivr.net/gh/$EI_OWNER/$EI_REPO@$EI_REF/$ei_file" ;;
            statically) echo "https://cdn.statically.io/gh/$EI_OWNER/$EI_REPO/$EI_REF/$ei_file" ;;
        esac
        return 0
    fi

    if [ "$EI_TAG" = latest ]; then
        case "$EI_PROXY" in
            github)   echo "$ei_gh/releases/latest/download/$ei_file" ;;
            gh-proxy) echo "https://gh-proxy.com/$ei_gh/releases/latest/download/$ei_file" ;;
            xget)     echo "$ei_xget/releases/latest/download/$ei_file" ;;
        esac
    else
        case "$EI_PROXY" in
            github)   echo "$ei_gh/releases/download/$EI_TAG/$ei_file" ;;
            gh-proxy) echo "https://gh-proxy.com/$ei_gh/releases/download/$EI_TAG/$ei_file" ;;
            xget)     echo "$ei_xget/releases/download/$EI_TAG/$ei_file" ;;
        esac
    fi
    return 0
}

download() {
    ei_url="$1"
    ei_out="$2"

    log "downloading $ei_url"

    if command_exists curl; then
        ei_opts='--fail --location --retry 3 --max-time 300'
        if [ -t 2 ]; then
            ei_opts="$ei_opts --progress-bar"
        else
            ei_opts="$ei_opts --silent --show-error"
        fi
        # shellcheck disable=SC2086
        curl $ei_opts --output "$ei_out" "$ei_url"
        return 0
    fi

    if command_exists wget; then
        wget --tries=3 --timeout=300 --output-document="$ei_out" "$ei_url"
        return 0
    fi

    die 'neither curl nor wget is available; install one of them and retry'
}

extract() {
    ei_archive="$1"
    ei_dest="$2"
    ei_name="$3"

    case "$ei_name" in
        *.tar.gz|*.tgz)
            command_exists tar || die 'tar is required to extract .tar.gz archives'
            tar -xzf "$ei_archive" -C "$ei_dest"
            ;;
        *.tar.xz|*.txz)
            command_exists tar || die 'tar is required to extract .tar.xz archives'
            tar -xJf "$ei_archive" -C "$ei_dest"
            ;;
        *.tar.bz2|*.tbz2)
            command_exists tar || die 'tar is required to extract .tar.bz2 archives'
            tar -xjf "$ei_archive" -C "$ei_dest"
            ;;
        *.zip)
            if command_exists unzip; then
                unzip -q -o "$ei_archive" -d "$ei_dest"
            elif [ -n "$(powershell_bin)" ]; then
                ei_ps_archive="$ei_archive"
                ei_ps_dest="$ei_dest"
                if command_exists cygpath; then
                    ei_ps_archive="$(cygpath -w "$ei_archive")"
                    ei_ps_dest="$(cygpath -w "$ei_dest")"
                fi
                "$(powershell_bin)" -NoProfile -Command \
                    "Expand-Archive -LiteralPath '$ei_ps_archive' -DestinationPath '$ei_ps_dest' -Force"
            else
                die 'unzip (or powershell) is required to extract .zip archives'
            fi
            ;;
        *.exe)
            cp "$ei_archive" "$ei_dest/$EI_BINARY_NAME.exe"
            ;;
        *.gz)
            command_exists gzip || die 'gzip is required to extract .gz archives'
            gzip -dc "$ei_archive" > "$ei_dest/$EI_BINARY_NAME"
            ;;
        *)
            # A bare, unarchived binary.
            cp "$ei_archive" "$ei_dest/$EI_BINARY_NAME"
            ;;
    esac

    chmod -R u+rwX "$ei_dest" 2>/dev/null || true
}

find_binary() {
    ei_dir="$1"

    for ei_path in \
        "$ei_dir/$EI_BINARY_NAME" \
        "$ei_dir/$EI_BINARY_NAME.exe" \
        "$ei_dir/bin/$EI_BINARY_NAME" \
        "$ei_dir/bin/$EI_BINARY_NAME.exe"; do
        if [ -f "$ei_path" ]; then
            echo "$ei_path"
            return 0
        fi
    done

    ei_found="$(find "$ei_dir" -maxdepth 3 -type f \
        \( -name "$EI_BINARY_NAME" -o -name "$EI_BINARY_NAME.exe" \) 2>/dev/null | head -n 1)"
    if [ -n "$ei_found" ]; then
        echo "$ei_found"
        return 0
    fi

    return 1
}

# ---------------------------------------------------------------------------
# Installation
# ---------------------------------------------------------------------------
resolve_install_dir() {
    ei_raw="$1"

    case "$ei_raw" in
        "~/"*) ei_raw="$HOME/${ei_raw#\~/}" ;;
        "~")   ei_raw="$HOME" ;;
    esac
    [ -n "$ei_raw" ] || return 0

    # An absolute path, so that `--dir local/bin` cannot end up in a shell
    # profile as `export PATH="local/bin:$PATH"`, which would break the next
    # time the shell started somewhere else.
    if [ -d "$ei_raw" ]; then
        ei_resolved="$(cd "$ei_raw" 2>/dev/null && pwd -P)"
        [ -n "$ei_resolved" ] && { printf '%s\n' "$ei_resolved"; return 0; }
        printf '%s\n' "$ei_raw"
        return 0
    fi

    # Not created yet: resolve the parent, which is what `pwd -P` needs.
    ei_parent="$(dirname "$ei_raw")"
    ei_base="$(basename "$ei_raw")"
    if [ -d "$ei_parent" ]; then
        ei_resolved="$(cd "$ei_parent" 2>/dev/null && pwd -P)"
        if [ -n "$ei_resolved" ]; then
            printf '%s/%s\n' "$ei_resolved" "$ei_base"
            return 0
        fi
    fi

    # Neither the directory nor its parent exists, so there is nothing to
    # anchor to — fall back on the working directory, which is always known.
    # Not symlink-corrected like `pwd -P`, but absolute, which is what matters.
    case "$ei_raw" in
        /*) ;;
        *)  printf '%s/%s\n' "$PWD" "$ei_raw"; return 0 ;;
    esac

    printf '%s\n' "$ei_raw"
}

is_windows_shell() {
    case "$(uname -s 2>/dev/null || echo '')" in
        MINGW*|MSYS*|CYGWIN*|Windows*|UWIN*) return 0 ;;
        *) return 1 ;;
    esac
}

# 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; `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`.
windows_path() {
    if command_exists cygpath; then
        cygpath -w "$1"
        return 0
    fi

    printf '%s\n' "$1"
}

# Render a path the way the platform spells it.
#
# Under MSYS2/Git Bash `$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`.
display_path() {
    ei_display="$1"

    if is_windows_shell && command_exists cygpath; then
        cygpath -m "$ei_display"
        return 0
    fi

    printf '%s\n' "$ei_display"
}

check_disk_space() {
    ei_dir="$1"

    case "$EI_MIN_DISK_SPACE" in
        ''|*[!0-9]*) return 0 ;;
    esac
    [ "$EI_MIN_DISK_SPACE" -gt 0 ] || return 0

    ei_probe="$ei_dir"
    while [ -n "$ei_probe" ] && [ ! -d "$ei_probe" ]; do
        ei_parent="$(dirname "$ei_probe")"
        [ "$ei_parent" = "$ei_probe" ] && break
        ei_probe="$ei_parent"
    done
    [ -d "$ei_probe" ] || return 0

    ei_free="$(df -Pk "$ei_probe" 2>/dev/null | awk 'NR==2 {printf "%d", $4 / 1024}')"
    case "$ei_free" in
        ''|*[!0-9]*) return 0 ;;
    esac

    if [ "$ei_free" -lt "$EI_MIN_DISK_SPACE" ]; then
        die "not enough disk space in $(display_path "$ei_dir"): ${ei_free}MB available, ${EI_MIN_DISK_SPACE}MB required"
    fi
}

update_path_unix() {
    ei_dir="$1"

    # A PATH entry for a directory that is not there is dead weight: nothing can
    # ever be found through it, and the profile only grows.
    if [ ! -d "$ei_dir" ]; then
        log "$(display_path "$ei_dir") does not exist; leaving PATH alone"
        return 0
    fi

    # Compared 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.
    ei_lower_dir="$(printf '%s' "$ei_dir" | tr '[:upper:]' '[:lower:]')"
    ei_lower_path="$(printf '%s' "$PATH" | tr '[:upper:]' '[:lower:]')"
    case ":$ei_lower_path:" in
        *":$ei_lower_dir:"*)
            log "$(display_path "$ei_dir") is already on PATH"
            return 0
            ;;
    esac

    ei_shell_name="$(basename "${SHELL:-sh}")"
    case "$ei_shell_name" in
        fish) ei_profile="$HOME/.config/fish/config.fish" ;;
        zsh)  ei_profile="$HOME/.zshrc" ;;
        bash) ei_profile="$HOME/.bashrc" ;;
        *)    ei_profile="$HOME/.profile" ;;
    esac

    if [ "$ei_shell_name" = fish ]; then
        ei_add_line="fish_add_path $ei_dir"
        mkdir -p "$HOME/.config/fish" 2>/dev/null || true
    else
        ei_add_line="export PATH=\"$ei_dir:\$PATH\""
    fi

    if [ -f "$ei_profile" ] && grep -Fiq "$ei_dir" "$ei_profile" 2>/dev/null; then
        log "$ei_profile already mentions $(display_path "$ei_dir")"
        log "restart your shell (or run: source $ei_profile) to pick up the change"
        return 0
    fi

    # 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 ! printf '\n# Added by the %s installer\n%s\n' \
        "$EI_BINARY_NAME" "$ei_add_line" >> "$ei_profile" 2>/dev/null; then
        log "could not write to $ei_profile"
        log "add this to your shell profile by hand:"
        log "  $ei_add_line"
        return 0
    fi

    log "added $(display_path "$ei_dir") to $ei_profile"
    log "restart your shell (or run: source $ei_profile) to pick up the change"
}

update_path_windows() {
    ei_dir="$1"

    # 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 [ ! -d "$ei_dir" ]; then
        log "$(display_path "$ei_dir") does not exist; leaving PATH alone"
        return 0
    fi

    # 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 ! command_exists cygpath; then
        log "add $(display_path "$ei_dir") to your PATH manually"
        return 0
    fi

    # 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.
    ei_win_dir="$(cygpath -w "$ei_dir")"

    ei_ps="$(powershell_bin)"
    if [ -z "$ei_ps" ]; then
        log "add $(display_path "$ei_dir") to your PATH manually"
        return 0
    fi

    # Prints `present` when the directory is already there, so the message can
    # say which of the two things happened.
    if ! ei_ps_result="$("$ei_ps" -NoProfile -Command \
        "\$native = '$ei_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)"; then
        log "could not update the Windows PATH automatically"
        log "add $(display_path "$ei_dir") to your PATH manually"
        return 0
    fi

    if [ "$ei_ps_result" = present ]; then
        log "$(display_path "$ei_dir") is already on the user PATH"
        return 0
    fi

    log "added $(display_path "$ei_dir") to the user PATH"
    log 'restart your terminal to pick up the change'
}

# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
parse_args() {
    while [ $# -gt 0 ]; do
        case "$1" in
            --proxy)  [ $# -ge 2 ] || die '--proxy requires a value';  EI_PROXY="$2";  shift 2 ;;
            --tag)    [ $# -ge 2 ] || die '--tag requires a value';    EI_TAG="$2";    shift 2 ;;
            --dir)    [ $# -ge 2 ] || die '--dir requires a value';    EI_DIR="$2";    shift 2 ;;
            --target) [ $# -ge 2 ] || die '--target requires a value'; EI_TARGET="$2"; shift 2 ;;
            --type)   [ $# -ge 2 ] || die '--type requires a value';   EI_TYPE="$2";   shift 2 ;;
            --ref)    [ $# -ge 2 ] || die '--ref requires a value';    EI_REF="$2";    shift 2 ;;
            --file)   [ $# -ge 2 ] || die '--file requires a value';   EI_FILE="$2";   shift 2 ;;
            --list)   EI_LIST=1; shift ;;
            -h|--help) usage; exit 0 ;;
            *) die "unknown argument: $1 (try --help)" ;;
        esac
    done
}

validate_config() {
    case "$EI_PROXY" in
        github|gh-proxy|xget|jsdelivr|statically) ;;
        *) die "unknown proxy: $EI_PROXY" ;;
    esac

    case "$EI_TYPE" in
        release|file) ;;
        *) die "unknown resource type: $EI_TYPE (expected release or file)" ;;
    esac

    if [ "$EI_TYPE" = release ]; then
        case "$EI_PROXY" in
            jsdelivr|statically)
                # Only worth complaining about when something will actually be
                # downloaded: an offline install never builds a URL.
                if [ -z "$EI_FILE" ]; then
                    die "the $EI_PROXY proxy cannot serve release assets; use --proxy github or --type file"
                fi
                ;;
        esac
    fi

    if [ -n "$EI_FILE" ] && [ ! -f "$EI_FILE" ]; then
        die "no such file: $(display_path "$EI_FILE")"
    fi
}

main() {
    EI_LIST=0
    parse_args "$@"

    # Before validate_config: `--list` is a question about this installer, not
    # about a release, so it answers even with an unusable proxy or a missing
    # --file.
    if [ "$EI_LIST" = 1 ]; then
        printf '%s\n' $EI_SUPPORTED_TARGETS
        return 0
    fi

    validate_config

    ei_target="$(resolve_target)"
    if [ -z "$ei_target" ]; then
        log "supported targets: $EI_SUPPORTED_TARGETS"
        if [ -n "$EI_TARGET" ]; then
            die "no build for target $EI_TARGET (check --target, or drop it to auto-detect)"
        fi
        die "no prebuilt binary for $(uname -s)/$(uname -m)"
    fi

    ei_filename="$(platform_filename "$ei_target")"
    ei_install_dir="$(resolve_install_dir "$EI_DIR")"

    if [ -n "$EI_FILE" ]; then
        log "installing $EI_BINARY_NAME ($ei_target) from $(display_path "$EI_FILE")"
    else
        log "installing $EI_BINARY_NAME ($ei_target) into $(display_path "$ei_install_dir")"
    fi

    check_disk_space "$ei_install_dir"

    ei_tmp="$(mktemp -d 2>/dev/null || echo "${TMPDIR:-/tmp}/eish-$$")"
    mkdir -p "$ei_tmp"
    trap 'rm -rf "$ei_tmp"' EXIT INT TERM

    if [ -n "$EI_FILE" ]; then
        # The file is used where it lies, so it is never copied or deleted.
        ei_archive="$EI_FILE"
    else
        ei_archive="$ei_tmp/$ei_filename"
        download "$(download_url "$ei_filename")" "$ei_archive"
    fi

    extract "$ei_archive" "$ei_tmp" "$ei_filename"

    ei_binary="$(find_binary "$ei_tmp")" || die "could not find $EI_BINARY_NAME inside $ei_filename"

    mkdir -p "$ei_install_dir"
    ei_stem="$EI_BINARY_NAME"
    if is_windows_shell; then
        ei_stem="$EI_BINARY_NAME.exe"
    fi

    if command_exists install; then
        install -m 0755 "$ei_binary" "$ei_install_dir/$ei_stem"
    else
        cp "$ei_binary" "$ei_install_dir/$ei_stem"
        chmod 0755 "$ei_install_dir/$ei_stem"
    fi

    log "installed $(display_path "$ei_install_dir/$ei_stem")"

    if is_windows_shell; then
        update_path_windows "$ei_install_dir"
    else
        update_path_unix "$ei_install_dir"
    fi

    if [ -n "${GITHUB_PATH:-}" ]; then
        # 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 is_windows_shell; then
            printf '%s\n' "$(windows_path "$ei_install_dir")" >> "$GITHUB_PATH"
        else
            printf '%s\n' "$ei_install_dir" >> "$GITHUB_PATH"
        fi
        log "added $(display_path "$ei_install_dir") to GITHUB_PATH"
    fi
}

main "$@"
