#!/bin/sh
# shellcheck shell=dash
#
# Licensed under the MIT license
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.

if [ "$KSH_VERSION" = 'Version JM 93t+ 2010-03-05' ]; then
    # The version of ksh93 that ships with many illumos systems does not
    # support the "local" extension.  Print a message rather than fail in
    # subtle ways later on:
    echo 'this installer does not work with this ksh93 version; please try bash!' >&2
    exit 1
fi

set -u

APP_NAME="{{ app_name }}"
APP_VERSION="{{ app_version }}"
# Look for GitHub Enterprise-style base URL first
if [ -n "{{ '${' }}{{ env_vars.ghe_base_url_env_var }}:-}" ]; then
    INSTALLER_BASE_URL="${{ env_vars.ghe_base_url_env_var }}"
else
    INSTALLER_BASE_URL="{{ '${' }}{{ env_vars.github_base_url_env_var }}:-{{ hosting.github.artifact_base_url }}}"
fi
if [ -n "${INSTALLER_DOWNLOAD_URL:-}" ]; then
    ARTIFACT_DOWNLOAD_URL="$INSTALLER_DOWNLOAD_URL"
else
    ARTIFACT_DOWNLOAD_URL="${INSTALLER_BASE_URL}{{ hosting.github.artifact_download_path }}"
fi
PRINT_VERBOSE=${INSTALLER_PRINT_VERBOSE:-0}
PRINT_QUIET=${INSTALLER_PRINT_QUIET:-0}
if [ -n "{{ '${' }}{{ env_vars.no_modify_path_env_var }}:-}" ]; then
    NO_MODIFY_PATH="${{ env_vars.no_modify_path_env_var }}"
else
    NO_MODIFY_PATH=${INSTALLER_NO_MODIFY_PATH:-0}
fi
if [ "{{ '${' }}{{ env_vars.disable_update_env_var }}:-0}" = "1" ]; then
    INSTALL_UPDATER=0
else
    INSTALL_UPDATER=1
fi
UNMANAGED_INSTALL="{{ '${' }}{{ env_vars.unmanaged_dir_env_var }}:-}"
if [ -n "${UNMANAGED_INSTALL}" ]; then
    NO_MODIFY_PATH=1
    INSTALL_UPDATER=0
fi

read -r RECEIPT <<EORECEIPT
{{ receipt | tojson }}
EORECEIPT
RECEIPT_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/{{ app_name }}"

usage() {
    # print help (this cat/EOF stuff is a "heredoc" string)
    cat <<EOF
{{ app_name }}-installer.sh

The installer for {{ app_name }} {{ app_version }}

This script detects what platform you're on and fetches an appropriate archive from
{{ base_url }}
then unpacks the binaries and installs them to{% if install_paths|length > 1 %} the first of the following locations{% endif %}
{% for install_path in install_paths %}
{%- if install_path.kind == "CargoHome" %}
    \$CARGO_HOME/bin (or \$HOME/.cargo/bin)
{%- elif install_path.kind == "HomeSubdir" %}
    \$HOME/{{ install_path.subdir }}
{%- elif install_path.kind == "EnvSubdir" %}
    \${{ install_path.env_key }}{% if install_path.subdir | length %}/{% endif %}{{ install_path.subdir }}
{%- else %}
    {{ error("unimplemented install_path format: " ~ install_path.kind) }}
{%- endif %}
{%- endfor %}

It will then add that dir to PATH by adding the appropriate line to your shell profiles.

USAGE:
    {{ app_name }}-installer.sh [OPTIONS]

OPTIONS:
    -v, --verbose
            Enable verbose output

    -q, --quiet
            Disable progress output

        --no-modify-path
            Don't configure the PATH environment variable

    -h, --help
            Print help information
EOF
}

download_binary_and_run_installer() {
    downloader --check
    need_cmd uname
    need_cmd mktemp
    need_cmd chmod
    need_cmd mkdir
    need_cmd rm
    need_cmd tar
    need_cmd grep
    need_cmd cat

    for arg in "$@"; do
        case "$arg" in
            --help)
                usage
                exit 0
                ;;
            --quiet)
                PRINT_QUIET=1
                ;;
            --verbose)
                PRINT_VERBOSE=1
                ;;
            --no-modify-path)
                say "--no-modify-path has been deprecated; please set {{ env_vars.no_modify_path_env_var }}=1 in the environment"
                NO_MODIFY_PATH=1
                ;;
            *)
                OPTIND=1
                if [ "${arg%%--*}" = "" ]; then
                    err "unknown option $arg"
                fi
                while getopts :hvq sub_arg "$arg"; do
                    case "$sub_arg" in
                        h)
                            usage
                            exit 0
                            ;;
                        v)
                            # user wants to skip the prompt --
                            # we don't need /dev/tty
                            PRINT_VERBOSE=1
                            ;;
                        q)
                            # user wants to skip the prompt --
                            # we don't need /dev/tty
                            PRINT_QUIET=1
                            ;;
                        *)
                            err "unknown option -$OPTARG"
                            ;;
                        esac
                done
                ;;
        esac
    done

    get_architecture || return 1
    local _true_arch="$RETVAL"
    assert_nz "$_true_arch" "arch"
    local _cur_arch="$_true_arch"


    # look up what archives support this platform
    local _artifact_name
    _artifact_name="$(select_archive_for_arch "$_true_arch")" || return 1
    local _bins
    local _zip_ext
    local _arch
    local _checksum_style
    local _checksum_value

    # destructure selected archive info into locals
    case "$_artifact_name" in {% for archive in platform_support.archives %}
        "{{ archive.id }}")
            _arch="{{ archive.target_triple }}"
            _zip_ext="{{ archive.zip_style }}"
            {%- if archive.checksum %}
            _checksum_style="{{ archive.checksum.style }}"
            _checksum_value="{{ archive.checksum.value }}"
            {%- endif %}
            _bins="{% for bin in archive.executables %}{{ bin }}{{ " " if not loop.last else "" }}{% endfor %}"
            _bins_js_array='{% for bin in archive.executables %}"{{ bin }}"{{ "," if not loop.last else ""}}{% endfor %}'
            {%- if "cdylib" in install_libraries %}
            _libs="{% for lib in archive.cdylibs %}{{ lib }}{{ " " if not loop.last else "" }}{% endfor %}"
            _libs_js_array='{% for lib in archive.cdylibs %}"{{ lib }}"{{ "," if not loop.last else ""}}{% endfor %}'
            {%- else %}
            _libs=""
            _libs_js_array=""
            {%- endif %}
            {%- if "cstaticlib" in install_libraries %}
            _staticlibs="{% for lib in archive.cstaticlibs %}{{ lib }}{{ " " if not loop.last else "" }}{% endfor %}"
            _staticlibs_js_array='{% for lib in archive.cstaticlibs %}"{{ lib }}"{{ "," if not loop.last else ""}}{% endfor %}'
            {%- else %}
            _staticlibs=""
            _staticlibs_js_array=""
            {%- endif %}
            {%- if archive.updater != None %}
            _updater_name="{{ platform_support.updaters[archive.updater].id }}"
            _updater_bin="{{ platform_support.updaters[archive.updater].binary }}"
            {%- else %}
            _updater_name=""
            _updater_bin=""
            {%- endif%}
            ;;{% endfor %}
        *)
            err "internal installer error: selected download $_artifact_name doesn't exist!?"
            ;;
    esac


    # Replace the placeholder binaries with the calculated array from above
    RECEIPT="$(echo "$RECEIPT" | sed s/'"CARGO_DIST_BINS"'/"$_bins_js_array"/)"
    RECEIPT="$(echo "$RECEIPT" | sed s/'"CARGO_DIST_DYLIBS"'/"$_libs_js_array"/)"
    RECEIPT="$(echo "$RECEIPT" | sed s/'"CARGO_DIST_STATICLIBS"'/"$_staticlibs_js_array"/)"

    # download the archive
    local _url="$ARTIFACT_DOWNLOAD_URL/$_artifact_name"
    local _dir
    _dir="$(ensure mktemp -d)" || return 1
    local _file="$_dir/input$_zip_ext"

    say "downloading $APP_NAME $APP_VERSION ${_arch}" 1>&2
    say_verbose "  from $_url" 1>&2
    say_verbose "  to $_file" 1>&2

    ensure mkdir -p "$_dir"

    if ! downloader "$_url" "$_file"; then
      say "failed to download $_url"
      say "this may be a standard network error, but it may also indicate"
      say "that $APP_NAME's release process is not working. When in doubt"
      say "please feel free to open an issue!"
      exit 1
    fi

    if [ -n "${_checksum_style:-}" ]; then
        verify_checksum "$_file" "$_checksum_style" "$_checksum_value"
    else
        say "no checksums to verify"
    fi

    # ...and then the updater, if it exists
    if [ -n "$_updater_name" ] && [ "$INSTALL_UPDATER" = "1" ]; then
        local _updater_url="$ARTIFACT_DOWNLOAD_URL/$_updater_name"
        # This renames the artifact while doing the download, removing the
        # target triple and leaving just the appname-update format
        local _updater_file="$_dir/$APP_NAME-update"

        if ! downloader "$_updater_url" "$_updater_file"; then
          say "failed to download $_updater_url"
          say "this may be a standard network error, but it may also indicate"
          say "that $APP_NAME's release process is not working. When in doubt"
          say "please feel free to open an issue!"
          exit 1
        fi

        # Add the updater to the list of binaries to install
        _bins="$_bins $APP_NAME-update"
    fi

    # unpack the archive
    case "$_zip_ext" in
        ".zip")
            ensure unzip -q "$_file" -d "$_dir"
            ;;

        ".tar."*)
            ensure tar xf "$_file" --strip-components 1 -C "$_dir"
            ;;
        *)
            err "unknown archive format: $_zip_ext"
            ;;
    esac

    install "$_dir" "$_bins" "$_libs" "$_staticlibs" "$_arch" "$@"
    local _retval=$?
    if [ "$_retval" != 0 ]; then
        return "$_retval"
    fi

    ignore rm -rf "$_dir"

    # Install the install receipt
    if [ "$INSTALL_UPDATER" = "1" ]; then
        if ! mkdir -p "$RECEIPT_HOME"; then
            err "unable to create receipt directory at $RECEIPT_HOME"
        else
            echo "$RECEIPT" > "$RECEIPT_HOME/$APP_NAME-receipt.json"
            # shellcheck disable=SC2320
            local _retval=$?
        fi
    else
        local _retval=0
    fi

    return "$_retval"
}

# Replaces $HOME with the variable name for display to the user,
# only if $HOME is defined.
replace_home() {
    local _str="$1"

    if [ -n "${HOME:-}" ]; then
        echo "$_str" | sed "s,$HOME,\$HOME,"
    else
        echo "$_str"
    fi
}

json_binary_aliases() {
    local _arch="$1"

    case "$_arch" in {% for artifact in artifacts %}
    "{{ artifact.target_triple }}")
        echo '{{ bin_aliases[artifact.target_triple] | tojson }}'
        ;;{% endfor %}
    *)
        echo '{}'
        ;;
    esac
}

aliases_for_binary() {
    local _bin="$1"
    local _arch="$2"

    case "$_arch" in {% for artifact in artifacts %}
    "{{ artifact.target_triple }}")
        case "$_bin" in
        {%- for source, dests in bin_aliases[artifact.target_triple] | items %}
        "{{ source }}")
            echo "{{ dests | join(" ") }}"
            ;;{% endfor %}
        *)
            echo ""
            ;;
        esac
        ;;{% endfor %}
    *)
        echo ""
        ;;
    esac
}

select_archive_for_arch() {
    local _true_arch="$1"
    local _archive

    # try each archive, checking runtime conditions like libc versions
    # accepting the first one that matches, as it's the best match
    case "$_true_arch" in {% for target in platform_support.platforms %}
        "{{ target }}")
            {%- for option in platform_support.platforms[target] %}
            _archive="{{platform_support.archives[option.archive_idx].id}}"
            {%- if option.runtime_conditions.min_glibc_version is defined %}
            if ! check_glibc "{{option.runtime_conditions.min_glibc_version.major}}" "{{option.runtime_conditions.min_glibc_version.series}}"; then
                _archive=""
            fi
            {%- endif %}
            if [ -n "$_archive" ]; then
                echo "$_archive"
                return 0
            fi
            {%- endfor %}
            ;;{% endfor %}
        *)
            err "there isn't a download for your platform $_true_arch"
            ;;
    esac
    err "no compatible downloads were found for your platform $_true_arch"
}

check_glibc() {
    local _min_glibc_major="$1"
    local _min_glibc_series="$2"

    # Parsing version out from line 1 like:
    # ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35
    _local_glibc="$(ldd --version | awk -F' ' '{ if (FNR<=1) print $NF }')"

    if [ "$(echo "${_local_glibc}" | awk -F. '{ print $1 }')" = "$_min_glibc_major" ] && [ "$(echo "${_local_glibc}" | awk -F. '{ print $2 }')" -ge "$_min_glibc_series" ]; then
        return 0
    else
        say "System glibc version (\`${_local_glibc}') is too old; checking alternatives" >&2
        return 1
    fi
}

# See discussion of late-bound vs early-bound for why we use single-quotes with env vars
# shellcheck disable=SC2016
install() {
    # This code needs to both compute certain paths for itself to write to, and
    # also write them to shell/rc files so that they can look them up to e.g.
    # add them to PATH. This requires an active distinction between paths
    # and expressions that can compute them.
    #
    # The distinction lies in when we want env-vars to be evaluated. For instance
    # if we determine that we want to install to $HOME/.myapp, which do we add
    # to e.g. $HOME/.profile:
    #
    # * early-bound: export PATH="/home/myuser/.myapp:$PATH"
    # * late-bound:  export PATH="$HOME/.myapp:$PATH"
    #
    # In this case most people would prefer the late-bound version, but in other
    # cases the early-bound version might be a better idea. In particular when using
    # other env-vars than $HOME, they are more likely to be only set temporarily
    # for the duration of this install script, so it's more advisable to erase their
    # existence with early-bounding.
    #
    # This distinction is handled by "double-quotes" (early) vs 'single-quotes' (late).
    #
    # However if we detect that "$SOME_VAR/..." is a subdir of $HOME, we try to rewrite
    # it to be '$HOME/...' to get the best of both worlds.
    #
    # This script has a few different variants, the most complex one being the
    # CARGO_HOME version which attempts to install things to Cargo's bin dir,
    # potentially setting up a minimal version if the user hasn't ever installed Cargo.
    #
    # In this case we need to:
    #
    # * Install to $HOME/.cargo/bin/
    # * Create a shell script at $HOME/.cargo/env that:
    #   * Checks if $HOME/.cargo/bin/ is on PATH
    #   * and if not prepends it to PATH
    # * Edits $HOME/.profile to run $HOME/.cargo/env (if the line doesn't exist)
    #
    # To do this we need these 4 values:

    # The actual path we're going to install to
    local _install_dir
    # The directory C dynamic/static libraries install to
    local _lib_install_dir
    # The install prefix we write to the receipt.
    # For organized install methods like CargoHome, which have
    # subdirectories, this is the root without `/bin`. For other
    # methods, this is the same as `_install_dir`.
    local _receipt_install_dir
    # Path to the an shell script that adds install_dir to PATH
    local _env_script_path
    # Potentially-late-bound version of install_dir to write env_script
    local _install_dir_expr
    # Potentially-late-bound version of env_script_path to write to rcfiles like $HOME/.profile
    local _env_script_path_expr
    # Forces the install to occur at this path, not the default
    local _force_install_dir
    # Which install layout to use - "flat" or "hierarchical"
    local _install_layout="unspecified"
    # A list of binaries which are shadowed in the PATH
    local _shadowed_bins=""

    # Check the newer app-specific variable before falling back
    # to the older generic one
    if [ -n "{{ '${' }}{{ env_vars.install_dir_env_var }}:-}" ]; then
        _force_install_dir="${{ env_vars.install_dir_env_var }}"
        _install_layout={%- if install_paths | selectattr("kind", "equalto", "CargoHome") -%} "cargo-home" {%- else -%} "flat" {%- endif %}
    elif [ -n "${CARGO_DIST_FORCE_INSTALL_DIR:-}" ]; then
        _force_install_dir="$CARGO_DIST_FORCE_INSTALL_DIR"
        _install_layout={%- if install_paths | selectattr("kind", "equalto", "CargoHome") -%} "cargo-home" {%- else -%} "flat" {%- endif %}
    elif [ -n "$UNMANAGED_INSTALL" ]; then
        _force_install_dir="$UNMANAGED_INSTALL"
        _install_layout="flat"
    fi

    # Check if the install layout should be changed from `flat` to `cargo-home`
    # for backwards compatible updates of applications that switched layouts.
    if [ -n "${_force_install_dir:-}" ]; then
        if [ "$_install_layout" = "flat" ]; then
            # If the install directory is targeting the Cargo home directory, then
            # we assume this application was previously installed that layout
            if [ "$_force_install_dir" = "${CARGO_HOME:-${HOME:-}/.cargo}" ]; then
                _install_layout="cargo-home"
            fi
        fi
     fi

    # Before actually consulting the configured install strategy, see
    # if we're overriding it.
    if [ -n "${_force_install_dir:-}" ]; then
        case "$_install_layout" in
            "hierarchical")
                _install_dir="$_force_install_dir/bin"
                _lib_install_dir="$_force_install_dir/lib"
                _receipt_install_dir="$_force_install_dir"
                _env_script_path="$_force_install_dir/env"
                _install_dir_expr="$(replace_home "$_force_install_dir/bin")"
                _env_script_path_expr="$(replace_home "$_force_install_dir/env")"
                ;;
            "cargo-home")
                _install_dir="$_force_install_dir/bin"
                _lib_install_dir="$_force_install_dir/bin"
                _receipt_install_dir="$_force_install_dir"
                _env_script_path="$_force_install_dir/env"
                _install_dir_expr="$(replace_home "$_force_install_dir/bin")"
                _env_script_path_expr="$(replace_home "$_force_install_dir/env")"
                ;;
            "flat")
                _install_dir="$_force_install_dir"
                _lib_install_dir="$_force_install_dir"
                _receipt_install_dir="$_install_dir"
                _env_script_path="$_force_install_dir/env"
                _install_dir_expr="$(replace_home "$_force_install_dir")"
                _env_script_path_expr="$(replace_home "$_force_install_dir/env")"
                ;;
            *)
                err "Unrecognized install layout: $_install_layout"
                ;;
        esac
    fi

    {%- for install_path in install_paths %}
    if [ -z "${_install_dir:-}" ]; then
    {%- if install_path.kind == "CargoHome" %}
        _install_layout="cargo-home"
        # first try $CARGO_HOME, then fallback to $HOME/.cargo
        if [ -n "${CARGO_HOME:-}" ]; then
            _receipt_install_dir="$CARGO_HOME"
            _install_dir="$CARGO_HOME/bin"
            _lib_install_dir="$CARGO_HOME/bin"
            _env_script_path="$CARGO_HOME/env"
            # Initially make this early-bound to erase the potentially-temporary env-var
            _install_dir_expr="$_install_dir"
            _env_script_path_expr="$_env_script_path"
            # If CARGO_HOME was set but it ended up being the default $HOME-based path,
            # then keep things late-bound. Otherwise bake the value for safety.
            # This is what rustup does, and accurately reproducing it is useful.
            if [ -n "${HOME:-}" ]; then
                if [ "$HOME/.cargo/bin" = "$_install_dir" ]; then
                    _install_dir_expr='$HOME/.cargo/bin'
                    _env_script_path_expr='$HOME/.cargo/env'
                fi
            fi
        elif [ -n "${HOME:-}" ]; then
            _receipt_install_dir="$HOME/.cargo"
            _install_dir="$HOME/.cargo/bin"
            _lib_install_dir="$HOME/.cargo/bin"
            _env_script_path="$HOME/.cargo/env"
            _install_dir_expr='$HOME/.cargo/bin'
            _env_script_path_expr='$HOME/.cargo/env'
        fi
    {%- elif install_path.kind == "HomeSubdir" %}
        _install_layout="flat"
        # Install to $HOME/{{ install_path.subdir }}
        if [ -n "${HOME:-}" ]; then
            _install_dir="$HOME/{{ install_path.subdir }}"
            _lib_install_dir="$HOME/{{ install_path.subdir }}"
            _receipt_install_dir="$_install_dir"
            _env_script_path="$HOME/{{ install_path.subdir }}/env"
            _install_dir_expr='$HOME/{{ install_path.subdir }}'
            _env_script_path_expr='$HOME/{{ install_path.subdir }}/env'
        fi
    {%- elif install_path.kind == "EnvSubdir" %}
        _install_layout="flat"
        # Install to ${{ install_path.env_key }}{% if install_path.subdir | length %}/{% endif %}{{ install_path.subdir }}
        if [ -n "{{ "${" }}{{ install_path.env_key }}:-}" ]; then
            _install_dir="${{ install_path.env_key }}{% if install_path.subdir | length %}/{% endif %}{{ install_path.subdir }}"
            _lib_install_dir="$_install_dir"
            _receipt_install_dir="$_install_dir"
            _env_script_path="${{ install_path.env_key }}{% if install_path.subdir | length %}/{% endif %}{{ install_path.subdir }}/env"
            _install_dir_expr="$(replace_home "$_install_dir")"
            _env_script_path_expr="$(replace_home "$_env_script_path")"
        fi
    {%- else %}
        {{ error("unimplemented install_path format: " ~ install_path.kind) }}
    {%- endif %}
    fi
    {%- endfor %}

    if [ -z "$_install_dir_expr" ]; then
        err "could not find a valid path to install to!"
    fi

    # Identical to the sh version, just with a .fish file extension
    # We place it down here to wait until it's been assigned in every
    # path.
    _fish_env_script_path="${_env_script_path}.fish"
    _fish_env_script_path_expr="${_env_script_path_expr}.fish"

    # Replace the temporary cargo home with the calculated one
    RECEIPT=$(echo "$RECEIPT" | sed "s,AXO_INSTALL_PREFIX,$_receipt_install_dir,")
    # Also replace the aliases with the arch-specific one
    RECEIPT=$(echo "$RECEIPT" | sed "s'\"binary_aliases\":{}'\"binary_aliases\":$(json_binary_aliases "$_arch")'")
    # And replace the install layout
    RECEIPT=$(echo "$RECEIPT" | sed "s'\"install_layout\":\"unspecified\"'\"install_layout\":\"$_install_layout\"'")
    if [ "$NO_MODIFY_PATH" = "1" ]; then
        RECEIPT=$(echo "$RECEIPT" | sed "s'\"modify_path\":true'\"modify_path\":false'")
    fi

    say "installing to $_install_dir"
    ensure mkdir -p "$_install_dir"
    ensure mkdir -p "$_lib_install_dir"

    # copy all the binaries to the install dir
    local _src_dir="$1"
    local _bins="$2"
    local _libs="$3"
    local _staticlibs="$4"
    local _arch="$5"
    for _bin_name in $_bins; do
        local _bin="$_src_dir/$_bin_name"
        ensure mv "$_bin" "$_install_dir"
        # unzip seems to need this chmod
        ensure chmod +x "$_install_dir/$_bin_name"
        for _dest in $(aliases_for_binary "$_bin_name" "$_arch"); do
            ln -sf "$_install_dir/$_bin_name" "$_install_dir/$_dest"
        done
        say "  $_bin_name"
    done
    # Like the above, but no aliases
    for _lib_name in $_libs; do
        local _lib="$_src_dir/$_lib_name"
        ensure mv "$_lib" "$_lib_install_dir"
        # unzip seems to need this chmod
        ensure chmod +x "$_lib_install_dir/$_lib_name"
        say "  $_lib_name"
    done
    for _lib_name in $_staticlibs; do
        local _lib="$_src_dir/$_lib_name"
        ensure mv "$_lib" "$_lib_install_dir"
        # unzip seems to need this chmod
        ensure chmod +x "$_lib_install_dir/$_lib_name"
        say "  $_lib_name"
    done

    say "{{ install_success_msg }}"

    # Avoid modifying the users PATH if they are managing their PATH manually
    case :$PATH:
      in *:$_install_dir:*) NO_MODIFY_PATH=1 ;;
         *) ;;
    esac

    if [ "0" = "$NO_MODIFY_PATH" ]; then
        add_install_dir_to_ci_path "$_install_dir"
        add_install_dir_to_path "$_install_dir_expr" "$_env_script_path" "$_env_script_path_expr" ".profile" "sh"
        exit1=$?
        shotgun_install_dir_to_path "$_install_dir_expr" "$_env_script_path" "$_env_script_path_expr" ".profile .bashrc .bash_profile .bash_login" "sh"
        exit2=$?
        add_install_dir_to_path "$_install_dir_expr" "$_env_script_path" "$_env_script_path_expr" ".zshrc .zshenv" "sh"
        exit3=$?
        # This path may not exist by default
        ensure mkdir -p "$HOME/.config/fish/conf.d"
        exit4=$?
        add_install_dir_to_path "$_install_dir_expr" "$_fish_env_script_path" "$_fish_env_script_path_expr" ".config/fish/conf.d/$APP_NAME.env.fish" "fish"
        exit5=$?

        if [ "${exit1:-0}" = 1 ] || [ "${exit2:-0}" = 1 ] || [ "${exit3:-0}" = 1 ] || [ "${exit4:-0}" = 1 ] || [ "${exit5:-0}" = 1 ]; then
            say ""
            say "To add $_install_dir_expr to your PATH, either restart your shell or run:"
            say ""
            say "    source $_env_script_path_expr (sh, bash, zsh)"
            say "    source $_fish_env_script_path_expr (fish)"
        fi
    fi

    _shadowed_bins="$(check_for_shadowed_bins "$_install_dir" "$_bins")"
    if [ -n "$_shadowed_bins" ]; then
        say "WARNING: The following commands are shadowed by other commands in your PATH:$_shadowed_bins"
    fi
}

check_for_shadowed_bins() {
    local _install_dir="$1"
    local _bins="$2"

    for _bin_name in $_bins; do
        if [ "$(command -v "$_bin_name")" != "$_install_dir/$_bin_name" ]; then
            _shadowed_bins="$_shadowed_bins $_bin_name"
        fi
    done

    echo "$_shadowed_bins"
}

print_home_for_script() {
    local script="$1"

    local _home
    case "$script" in
        # zsh has a special ZDOTDIR directory, which if set
        # should be considered instead of $HOME
        .zsh*)
            if [ -n "${ZDOTDIR:-}" ]; then
                _home="$ZDOTDIR"
            else
                _home="$HOME"
            fi
            ;;
        *)
            _home="$HOME"
            ;;
    esac

    echo "$_home"
}

add_install_dir_to_ci_path() {
    # Attempt to do CI-specific rituals to get the install-dir on PATH faster
    local _install_dir="$1"

    # If GITHUB_PATH is present, then write install_dir to the file it refs.
    # After each GitHub Action, the contents will be added to PATH.
    # So if you put a curl | sh for this script in its own "run" step,
    # the next step will have this dir on PATH.
    #
    # Note that GITHUB_PATH will not resolve any variables, so we in fact
    # want to write install_dir and not install_dir_expr
    if [ -n "${GITHUB_PATH:-}" ]; then
        ensure echo "$_install_dir" >> "$GITHUB_PATH"
    fi
}

add_install_dir_to_path() {
    # Edit rcfiles ($HOME/.profile) to add install_dir to $PATH
    #
    # We do this slightly indirectly by creating an "env" shell script which checks if install_dir
    # is on $PATH already, and prepends it if not. The actual line we then add to rcfiles
    # is to just source that script. This allows us to blast it into lots of different rcfiles and
    # have it run multiple times without causing problems. It's also specifically compatible
    # with the system rustup uses, so that we don't conflict with it.
    local _install_dir_expr="$1"
    local _env_script_path="$2"
    local _env_script_path_expr="$3"
    local _rcfiles="$4"
    local _shell="$5"

    if [ -n "${HOME:-}" ]; then
        local _target
        local _home

        # Find the first file in the array that exists and choose
        # that as our target to write to
        for _rcfile_relative in $_rcfiles; do
            _home="$(print_home_for_script "$_rcfile_relative")"
            local _rcfile="$_home/$_rcfile_relative"

            if [ -f "$_rcfile" ]; then
                _target="$_rcfile"
                break
            fi
        done

        # If we didn't find anything, pick the first entry in the
        # list as the default to create and write to
        if [ -z "${_target:-}" ]; then
            local _rcfile_relative
            _rcfile_relative="$(echo "$_rcfiles" | awk '{ print $1 }')"
            _home="$(print_home_for_script "$_rcfile_relative")"
            _target="$_home/$_rcfile_relative"
        fi

        # `source x` is an alias for `. x`, and the latter is more portable/actually-posix.
        # This apparently comes up a lot on freebsd. It's easy enough to always add
        # the more robust line to rcfiles, but when telling the user to apply the change
        # to their current shell ". x" is pretty easy to misread/miscopy, so we use the
        # prettier "source x" line there. Hopefully people with Weird Shells are aware
        # this is a thing and know to tweak it (or just restart their shell).
        local _robust_line=". \"$_env_script_path_expr\""
        local _pretty_line="source \"$_env_script_path_expr\""

        # Add the env script if it doesn't already exist
        if [ ! -f "$_env_script_path" ]; then
            say_verbose "creating $_env_script_path"
            if [ "$_shell" = "sh" ]; then
                write_env_script_sh "$_install_dir_expr" "$_env_script_path"
            else
                write_env_script_fish "$_install_dir_expr" "$_env_script_path"
            fi
        else
            say_verbose "$_env_script_path already exists"
        fi

        # Check if the line is already in the rcfile
        # grep: 0 if matched, 1 if no match, and 2 if an error occurred
        #
        # Ideally we could use quiet grep (-q), but that makes "match" and "error"
        # have the same behaviour, when we want "no match" and "error" to be the same
        # (on error we want to create the file, which >> conveniently does)
        #
        # We search for both kinds of line here just to do the right thing in more cases.
        if ! grep -F "$_robust_line" "$_target" > /dev/null 2>/dev/null && \
           ! grep -F "$_pretty_line" "$_target" > /dev/null 2>/dev/null
        then
            # If the script now exists, add the line to source it to the rcfile
            # (This will also create the rcfile if it doesn't exist)
            if [ -f "$_env_script_path" ]; then
                local _line
                # Fish has deprecated `.` as an alias for `source` and
                # it will be removed in a later version.
                # https://fishshell.com/docs/current/cmds/source.html
                # By contrast, `.` is the traditional syntax in sh and
                # `source` isn't always supported in all circumstances.
                if [ "$_shell" = "fish" ]; then
                    _line="$_pretty_line"
                else
                    _line="$_robust_line"
                fi
                say_verbose "adding $_line to $_target"
                # prepend an extra newline in case the user's file is missing a trailing one
                ensure echo "" >> "$_target"
                ensure echo "$_line" >> "$_target"
                return 1
            fi
        else
            say_verbose "$_install_dir already on PATH"
        fi
    fi
}

shotgun_install_dir_to_path() {
    # Edit rcfiles ($HOME/.profile) to add install_dir to $PATH
    # (Shotgun edition - write to all provided files that exist rather than just the first)
    local _install_dir_expr="$1"
    local _env_script_path="$2"
    local _env_script_path_expr="$3"
    local _rcfiles="$4"
    local _shell="$5"

    if [ -n "${HOME:-}" ]; then
        local _found=false
        local _home

        for _rcfile_relative in $_rcfiles; do
            _home="$(print_home_for_script "$_rcfile_relative")"
            local _rcfile_abs="$_home/$_rcfile_relative"

            if [ -f "$_rcfile_abs" ]; then
                _found=true
                add_install_dir_to_path "$_install_dir_expr" "$_env_script_path" "$_env_script_path_expr" "$_rcfile_relative" "$_shell"
            fi
        done

        # Fall through to previous "create + write to first file in list" behavior
	    if [ "$_found" = false ]; then
            add_install_dir_to_path "$_install_dir_expr" "$_env_script_path" "$_env_script_path_expr" "$_rcfiles" "$_shell"
        fi
    fi
}

write_env_script_sh() {
    # write this env script to the given path (this cat/EOF stuff is a "heredoc" string)
    local _install_dir_expr="$1"
    local _env_script_path="$2"
    ensure cat <<EOF > "$_env_script_path"
#!/bin/sh
# add binaries to PATH if they aren't added yet
# affix colons on either side of \$PATH to simplify matching
case ":\${PATH}:" in
    *:"$_install_dir_expr":*)
        ;;
    *)
        # Prepending path in case a system-installed binary needs to be overridden
        export PATH="$_install_dir_expr:\$PATH"
        ;;
esac
EOF
}

write_env_script_fish() {
    # write this env script to the given path (this cat/EOF stuff is a "heredoc" string)
    local _install_dir_expr="$1"
    local _env_script_path="$2"
    ensure cat <<EOF > "$_env_script_path"
if not contains "$_install_dir_expr" \$PATH
    # Prepending path in case a system-installed binary needs to be overridden
    set -x PATH "$_install_dir_expr" \$PATH
end
EOF
}

check_proc() {
    # Check for /proc by looking for the /proc/self/exe link
    # This is only run on Linux
    if ! test -L /proc/self/exe ; then
        err "fatal: Unable to find /proc/self/exe.  Is /proc mounted?  Installation cannot proceed without /proc."
    fi
}

get_bitness() {
    need_cmd head
    # Architecture detection without dependencies beyond coreutils.
    # ELF files start out "\x7fELF", and the following byte is
    #   0x01 for 32-bit and
    #   0x02 for 64-bit.
    # The printf builtin on some shells like dash only supports octal
    # escape sequences, so we use those.
    local _current_exe_head
    _current_exe_head=$(head -c 5 /proc/self/exe )
    if [ "$_current_exe_head" = "$(printf '\177ELF\001')" ]; then
        echo 32
    elif [ "$_current_exe_head" = "$(printf '\177ELF\002')" ]; then
        echo 64
    else
        err "unknown platform bitness"
    fi
}

is_host_amd64_elf() {
    need_cmd head
    need_cmd tail
    # ELF e_machine detection without dependencies beyond coreutils.
    # Two-byte field at offset 0x12 indicates the CPU,
    # but we're interested in it being 0x3E to indicate amd64, or not that.
    local _current_exe_machine
    _current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1)
    [ "$_current_exe_machine" = "$(printf '\076')" ]
}

get_endianness() {
    local cputype=$1
    local suffix_eb=$2
    local suffix_el=$3

    # detect endianness without od/hexdump, like get_bitness() does.
    need_cmd head
    need_cmd tail

    local _current_exe_endianness
    _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)"
    if [ "$_current_exe_endianness" = "$(printf '\001')" ]; then
        echo "${cputype}${suffix_el}"
    elif [ "$_current_exe_endianness" = "$(printf '\002')" ]; then
        echo "${cputype}${suffix_eb}"
    else
        err "unknown platform endianness"
    fi
}

get_architecture() {
    local _ostype
    local _cputype
    _ostype="$(uname -s)"
    _cputype="$(uname -m)"
    local _clibtype="gnu"
    local _local_glibc

    if [ "$_ostype" = Linux ]; then
        if [ "$(uname -o)" = Android ]; then
            _ostype=Android
        fi
        if ldd --version 2>&1 | grep -q 'musl'; then
            _clibtype="musl-dynamic"
        else
            # Assume all other linuxes are glibc (even if wrong, static libc fallback will apply)
            _clibtype="gnu"
        fi
    fi

    if [ "$_ostype" = Darwin ] && [ "$_cputype" = i386 ]; then
        # Darwin `uname -m` lies
        if sysctl hw.optional.x86_64 | grep -q ': 1'; then
            _cputype=x86_64
        fi
    fi

    if [ "$_ostype" = Darwin ] && [ "$_cputype" = x86_64 ]; then
        # Rosetta on aarch64
        if [ "$(sysctl -n hw.optional.arm64 2>/dev/null)" = "1" ]; then
            _cputype=aarch64
        fi
    fi

    if [ "$_ostype" = SunOS ]; then
        # Both Solaris and illumos presently announce as "SunOS" in "uname -s"
        # so use "uname -o" to disambiguate.  We use the full path to the
        # system uname in case the user has coreutils uname first in PATH,
        # which has historically sometimes printed the wrong value here.
        if [ "$(/usr/bin/uname -o)" = illumos ]; then
            _ostype=illumos
        fi

        # illumos systems have multi-arch userlands, and "uname -m" reports the
        # machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86
        # systems.  Check for the native (widest) instruction set on the
        # running kernel:
        if [ "$_cputype" = i86pc ]; then
            _cputype="$(isainfo -n)"
        fi
    fi

    case "$_ostype" in

        Android)
            _ostype=linux-android
            ;;

        Linux)
            check_proc
            _ostype=unknown-linux-$_clibtype
            _bitness=$(get_bitness)
            ;;

        FreeBSD)
            _ostype=unknown-freebsd
            ;;

        NetBSD)
            _ostype=unknown-netbsd
            ;;

        DragonFly)
            _ostype=unknown-dragonfly
            ;;

        Darwin)
            _ostype=apple-darwin
            ;;

        illumos)
            _ostype=unknown-illumos
            ;;

        MINGW* | MSYS* | CYGWIN* | Windows_NT)
            _ostype=pc-windows-gnu
            ;;

        *)
            err "unrecognized OS type: $_ostype"
            ;;

    esac

    case "$_cputype" in

        i386 | i486 | i686 | i786 | x86)
            _cputype=i686
            ;;

        xscale | arm)
            _cputype=arm
            if [ "$_ostype" = "linux-android" ]; then
                _ostype=linux-androideabi
            fi
            ;;

        armv6l)
            _cputype=arm
            if [ "$_ostype" = "linux-android" ]; then
                _ostype=linux-androideabi
            else
                _ostype="${_ostype}eabihf"
            fi
            ;;

        armv7l | armv8l)
            _cputype=armv7
            if [ "$_ostype" = "linux-android" ]; then
                _ostype=linux-androideabi
            else
                _ostype="${_ostype}eabihf"
            fi
            ;;

        aarch64 | arm64)
            _cputype=aarch64
            ;;

        x86_64 | x86-64 | x64 | amd64)
            _cputype=x86_64
            ;;

        mips)
            _cputype=$(get_endianness mips '' el)
            ;;

        mips64)
            if [ "$_bitness" -eq 64 ]; then
                # only n64 ABI is supported for now
                _ostype="${_ostype}abi64"
                _cputype=$(get_endianness mips64 '' el)
            fi
            ;;

        ppc)
            _cputype=powerpc
            ;;

        ppc64)
            _cputype=powerpc64
            ;;

        ppc64le)
            _cputype=powerpc64le
            ;;

        s390x)
            _cputype=s390x
            ;;
        riscv64)
            _cputype=riscv64gc
            ;;
        loongarch64)
            _cputype=loongarch64
            ;;
        *)
            err "unknown CPU type: $_cputype"

    esac

    # Detect 64-bit linux with 32-bit userland
    if [ "${_ostype}" = unknown-linux-gnu ] && [ "${_bitness}" -eq 32 ]; then
        case $_cputype in
            x86_64)
                # 32-bit executable for amd64 = x32
                if is_host_amd64_elf; then {
                    err "x32 linux unsupported"
                }; else
                    _cputype=i686
                fi
                ;;
            mips64)
                _cputype=$(get_endianness mips '' el)
                ;;
            powerpc64)
                _cputype=powerpc
                ;;
            aarch64)
                _cputype=armv7
                if [ "$_ostype" = "linux-android" ]; then
                    _ostype=linux-androideabi
                else
                    _ostype="${_ostype}eabihf"
                fi
                ;;
            riscv64gc)
                err "riscv64 with 32-bit userland unsupported"
                ;;
        esac
    fi

    # treat armv7 systems without neon as plain arm
    if [ "$_ostype" = "unknown-linux-gnueabihf" ] && [ "$_cputype" = armv7 ]; then
        if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then
            # At least one processor does not have NEON.
            _cputype=arm
        fi
    fi

    _arch="${_cputype}-${_ostype}"

    RETVAL="$_arch"
}

say() {
    if [ "0" = "$PRINT_QUIET" ]; then
        echo "$1"
    fi
}

say_verbose() {
    if [ "1" = "$PRINT_VERBOSE" ]; then
        echo "$1"
    fi
}

err() {
    if [ "0" = "$PRINT_QUIET" ]; then
        local red
        local reset
        red=$(tput setaf 1 2>/dev/null || echo '')
        reset=$(tput sgr0 2>/dev/null || echo '')
        say "${red}ERROR${reset}: $1" >&2
    fi
    exit 1
}

need_cmd() {
    if ! check_cmd "$1"
    then err "need '$1' (command not found)"
    fi
}

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

assert_nz() {
    if [ -z "$1" ]; then err "assert_nz $2"; fi
}

# Run a command that should never fail. If the command fails execution
# will immediately terminate with an error showing the failing
# command.
ensure() {
    if ! "$@"; then err "command failed: $*"; fi
}

# This is just for indicating that commands' results are being
# intentionally ignored. Usually, because it's being executed
# as part of error handling.
ignore() {
    "$@"
}

# This wraps curl or wget. Try curl first, if not installed,
# use wget instead.
downloader() {
    if check_cmd curl
    then _dld=curl
    elif check_cmd wget
    then _dld=wget
    else _dld='curl or wget' # to be used in error message of need_cmd
    fi

    if [ "$1" = --check ]
    then need_cmd "$_dld"
    elif [ "$_dld" = curl ]
    then curl -sSfL "$1" -o "$2"
    elif [ "$_dld" = wget ]
    then wget "$1" -O "$2"
    else err "Unknown downloader"   # should not reach here
    fi
}

verify_checksum() {
    local _file="$1"
    local _checksum_style="$2"
    local _checksum_value="$3"
    local _calculated_checksum

    if [ -z "$_checksum_value" ]; then
        return 0
    fi
    case "$_checksum_style" in
        sha256)
            if ! check_cmd sha256sum; then
                say "skipping sha256 checksum verification (it requires the 'sha256sum' command)"
                return 0
            fi
            _calculated_checksum="$(sha256sum -b "$_file" | awk '{printf $1}')"
            ;;
        sha512)
            if ! check_cmd sha512sum; then
                say "skipping sha512 checksum verification (it requires the 'sha512sum' command)"
                return 0
            fi
            _calculated_checksum="$(sha512sum -b "$_file" | awk '{printf $1}')"
            ;;
        sha3-256)
            if ! check_cmd openssl; then
                say "skipping sha3-256 checksum verification (it requires the 'openssl' command)"
                return 0
            fi
            _calculated_checksum="$(openssl dgst -sha3-256 "$_file" | awk '{printf $NF}')"
            ;;
        sha3-512)
            if ! check_cmd openssl; then
                say "skipping sha3-512 checksum verification (it requires the 'openssl' command)"
                return 0
            fi
            _calculated_checksum="$(openssl dgst -sha3-512 "$_file" | awk '{printf $NF}')"
            ;;
        blake2s)
            if ! check_cmd b2sum; then
                say "skipping blake2s checksum verification (it requires the 'b2sum' command)"
                return 0
            fi
            # Test if we have official b2sum with blake2s support
            local _well_known_blake2s_checksum="93314a61f470985a40f8da62df10ba0546dc5216e1d45847bf1dbaa42a0e97af"
            local _test_blake2s
            _test_blake2s="$(printf "can do blake2s" | b2sum -a blake2s | awk '{printf $1}')" || _test_blake2s=""

            if [ "X$_test_blake2s" = "X$_well_known_blake2s_checksum" ]; then
                _calculated_checksum="$(b2sum -a blake2s "$_file" | awk '{printf $1}')" || _calculated_checksum=""
            else
                say "skipping blake2s checksum verification (installed b2sum doesn't support blake2s)"
                return 0
            fi
            ;;
        blake2b)
            if ! check_cmd b2sum; then
                say "skipping blake2b checksum verification (it requires the 'b2sum' command)"
                return 0
            fi
            _calculated_checksum="$(b2sum "$_file" | awk '{printf $1}')"
            ;;
        false)
            ;;
        *)
            say "skipping unknown checksum style: $_checksum_style"
            return 0
            ;;
    esac

    if [ "$_calculated_checksum" != "$_checksum_value" ]; then
        err "checksum mismatch
            want: $_checksum_value
            got:  $_calculated_checksum"
    fi
}

download_binary_and_run_installer "$@" || exit 1