#!/bin/bash # # Bash program to download the latest Nextstrain CLI standalone installation # archive for Linux or macOS, extract it into the current user's app data # directory, and check if PATH includes the installation destination. # # It maintains rough parity with the PowerShell program for Windows, # standalone-installer-windows. # # Set DESTINATION to change the installation location. # # Set VERSION to change the version downloaded and installed, or pass the # desired version as the first argument to this program. # set -euo pipefail shopt -s failglob : "${NEXTSTRAIN_DOT_ORG:=https://nextstrain.org}" # Globals declared here to make them more obvious, but set by main() to avoid # source ordering requirements and delay execution until the full file is # parsed. declare VERSION KERNEL TARGET DESTINATION # Wrap everything in a function which we call at the end to avoid execution of # a partially-downloaded program. main() { VERSION="${1:-${VERSION:-latest}}" KERNEL="${KERNEL:-$(uname -s)}" TARGET="${TARGET:-$(target-triple)}" DESTINATION="${DESTINATION:-${NEXTSTRAIN_HOME:-$HOME/.nextstrain}/cli-standalone}" local archive archive_url tmp archive="standalone-${TARGET}.tar.gz" archive_url="${NEXTSTRAIN_DOT_ORG}/cli/download/${VERSION}/${archive}" # Move into a temporary working dir tmp="$(mktemp -d)" if ! debug; then # shellcheck disable=SC2064 trap "rm -rf $tmp" EXIT fi pushd "$tmp" >/dev/null log "Temporary working directory: $tmp" log "Downloading $archive_url" curl "$archive_url" \ --fail --show-error --location --proto "=https" \ > "$archive" log "Extracting $archive" mkdir standalone tar xz"$(if-debug v)"f "$archive" -C standalone if [[ -d "$DESTINATION" ]]; then log "Removing existing $DESTINATION" rm -rf "$DESTINATION" fi log "Installing to $DESTINATION" mkdir -p "$(dirname "$DESTINATION")" mv standalone "$DESTINATION" popd >/dev/null if ! debug; then log "Cleaning up" rm -rf "$tmp" fi log "Checking installation" local installed_version if ! installed_version="$("$DESTINATION"/nextstrain --version)"; then die "installation check failed: unable to run \`\"$DESTINATION\"/nextstrain --version\`; look for error message above." fi cat <<~~ ______________________________________________________________________________ Nextstrain CLI ($installed_version) installed to $DESTINATION. ~~ if [[ "$(type -p nextstrain 2>/dev/null)" != "$DESTINATION/nextstrain" ]]; then local shell rc shell="$(default-shell)" rc="$(shell-rc "$shell")" cat <<~~ To make the "nextstrain" command available in your default shell ($shell) without using the full path, please run these two commands now: printf '\n%s\n' 'eval "\$("$DESTINATION/nextstrain" init-shell $shell)"' >> $rc eval "\$("$DESTINATION/nextstrain" init-shell $shell)" The first adds a line to your shell initialization file ($rc) for future sessions. The second sets up your current shell session. You only need to run these once. ~~ fi } target-triple() { local machine vendor os machine="$(uname -m)" case "$KERNEL" in Linux) [[ "$machine" == x86_64 ]] || die "unsupported architecture: $machine" vendor=unknown os=linux-gnu ;; Darwin) # Rosetta 2 will not be needed for the standalone version of # Nextstrain CLI itself as of 8.2.0. The Conda runtime still # requires it and the Docker runtime can benefit from it, but we # now check for it in their setup instead. # -trs, 1 Feb 2024 version-gte 8.2.0 \ || [[ "$machine" != arm64 ]] \ || pgrep -qU _oahd 2>/dev/null \ || die "Rosetta 2 not enabled. Please run:"$'\n\n'" softwareupdate --install-rosetta"$'\n\n'"and then retry this installation of Nextstrain CLI." # aarch64 builds will only be available starting with 8.2.0. Older # versions only provide x86_64, which will run under Rosetta. case "$machine" in x86_64) ;; arm64) version-gte 8.2.0 && machine=aarch64 || machine=x86_64;; *) die "unsupported architecture: $machine";; esac vendor=apple os=darwin ;; *) die "unknown kernel: $KERNEL" esac echo "$machine-$vendor-$os" } version-gte() { local minimum="$1" # If $minimum sorts before $VERSION, then $VERSION is at least ≥$minimum. # Note that non-numeric values sort by byte order, so "latest" and # "pr-build/123" and so on end up after (greater than) a numeric value like # 8.1.0. Both macOS/BSD sort and GNU sort support -V. [[ $(printf '%s\n%s\n' "$minimum" "$VERSION" | sort -V | head -n1) == "$minimum" ]] } default-shell() { local shell case "$KERNEL" in Linux) shell=$(getent passwd "$(id -u)" | awk -F: '{print $NF}');; Darwin) shell=$(dscl . -read ~/ UserShell | sed 's/UserShell: //');; *) die "unknown kernel: $KERNEL" esac shell="$(basename "$shell")" # Remove any -x.y version from the name echo "${shell%%-*}" } shell-rc() { local shell="$1" # Paths below are presumed to be meta-char safe and returned with # unexpanded ~/… prefixes for a nicer appearance in suggested commands, # i.e. expansion of ~ is expected to happen after the user pastes and runs # the commands, rather than here and now. case "$shell" in bash) case "$KERNEL" in Darwin) # macOS Terminal.app (and iTerm2.app) always launches login # shells, i.e. for all windows and tabs, so use the login # initialization file instead of the interactive one # because Bash (unlike zsh) only reads one or the other. # Although it's common practice for users to source their # bashrc from their bash_profile, we can't rely on this. # # The order and conditions on files here follows bash(1) § # INVOCATION: # # After reading [/etc/profile], [bash] looks for # ~/.bash_profile, ~/.bash_login, and ~/.profile, in that # order, and reads and executes commands from the first one # that exists and is readable. # if [[ -r ~/.bash_profile ]]; then # shellcheck disable=SC2088 echo "~/.bash_profile" elif [[ -r ~/.bash_login ]]; then # shellcheck disable=SC2088 echo "~/.bash_login" elif [[ -r ~/.profile ]]; then # shellcheck disable=SC2088 echo "~/.profile" else # If none exist, then we fallback to ~/.bash_profile # and expect it to be created by our suggested shell # init commands. # # shellcheck disable=SC2088 echo "~/.bash_profile" fi;; *) # shellcheck disable=SC2088 echo "~/.bashrc";; esac;; zsh) echo "${ZDOTDIR:-~}/.zshrc";; sh) # sh, whether a historic version or bash in sh-compat mode, doesn't # use any default startup file for interactive shells. It only # uses a default startup file for login shells. We'll have to # assume here that anyone using sh as their default shell will also # be using login shells appropriately. On macOS, at least, common # terminal emulators spawn login shells (see commentary above). # -trs, 26 September 2023 # shellcheck disable=SC2088 echo "~/.profile";; *) # A decent guess? # # shellcheck disable=SC2088 echo "~/.${shell}rc";; esac } debug() { [[ -n "${DEBUG:-}" ]] } if-debug() { if debug; then echo "$@" fi } log() { echo "--> $*" } die() { echo "ERROR:" "$@" >&2 exit 1 } main "$@"