Skip to content

Commit

Permalink
Merge branch 'trs/standalone-installer'
Browse files Browse the repository at this point in the history
  • Loading branch information
tsibley committed Sep 19, 2022
2 parents 2c07a58 + b475587 commit 2216314
Show file tree
Hide file tree
Showing 8 changed files with 587 additions and 2 deletions.
59 changes: 59 additions & 0 deletions .github/workflows/standalone-installers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Standalone installers

on:
push:
branches:
- master
paths:
- bin/standalone-installer-unix
- bin/standalone-installer-windows

pull_request:
paths:
- bin/standalone-installer-unix
- bin/standalone-installer-windows

# Routinely check that the external resources the installers rely on keep
# functioning as expected.
schedule:
# Every day at 17:42 UTC / 9:42 Seattle (winter) / 10:42 Seattle (summer)
- cron: "42 17 * * *"

workflow_dispatch:

jobs:
# The goal here is to make sure the installers run successfully on a variety
# of OS versions. We're _not_ testing unreleased standalone builds here—the
# installation archives are downloaded from the latest release on GitHub via
# nextstrain.org endpoints—which is why this isn't part of CI. That is, this
# is akin to testing `pip install nextstrain-cli` if we wanted to make sure
# `pip` worked.
# -trs, 29 August 2022
test:
name: test (os=${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os:
- ubuntu-18.04
- ubuntu-20.04
- ubuntu-22.04
- macos-11
- macos-12
- windows-2019
- windows-2022

runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v3

# Use pipes like are used in the real instructions when the installer is
# fetched from nextstrain.org. This tests that the shell programs work
# when they're specifically executed this way.
- if: runner.os != 'Windows'
run: cat ./bin/standalone-installer-unix | bash
shell: bash

- if: runner.os == 'Windows'
run: Get-Content -Raw ./bin/standalone-installer-windows | Invoke-Expression
shell: pwsh
19 changes: 19 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@ This release contains **a potentially-breaking change** for existing usages of
option continues to be applicable only to the AWS Batch runtime (e.g. the
`--aws-batch` option).

## Development

* We now provide standalone installers (i.e. shell programs) to download and
unpack the standalone installation archives into standard locations,
potentially upgrading/overwriting a prior standalone install. These
installers will be served from GitHub directly out of this project's
repository via convenience redirects on nextstrain.org.

These will eventually form the basis for Nextstrain install instructions that
don't suffer from Python bootstrapping issues. As a preview for now, you can
play around with the following platform-specific commands:

curl -fsSL --proto '=https' https://nextstrain.org/cli/installer/linux | bash
curl -fsSL --proto '=https' https://nextstrain.org/cli/installer/mac | bash
Invoke-RestMethod https://nextstrain.org/cli/installer/windows | Invoke-Expression

A new companion command, `init-shell`, exists to simplify shell configuration
(i.e. `PATH` modification) for such installations.


# 4.2.0 (29 July 2022)

Expand Down
237 changes: 237 additions & 0 deletions bin/standalone-installer-unix
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#!/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 KERNEL TARGET DESTINATION VERSION

# Wrap everything in a function which we call at the end to avoid execution of
# a partially-downloaded program.
main() {
KERNEL="${KERNEL:-$(uname -s)}"
TARGET="${TARGET:-$(target-triple)}"
DESTINATION="${DESTINATION:-$(destination)}"

VERSION="${1:-${VERSION:-latest}}"

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
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

cat <<~~
______________________________________________________________________________
Nextstrain CLI ($("$DESTINATION"/nextstrain --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:
echo 'source <("$DESTINATION/nextstrain" init-shell $shell)' >> $rc
source <("$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)
[[ "$machine" == x86_64 || "$machine" == arm64 ]] || die "unsupported architecture: $machine"
machine=x86_64
vendor=apple
os=darwin
;;

*)
die "unknown kernel: $KERNEL"
esac

echo "$machine-$vendor-$os"
}

destination() {
local user_data dest

# Paths from appdirs <https://github.com/ActiveState/appdirs/blob/master/appdirs.py>
case "$KERNEL" in
Linux)
user_data="${XDG_DATA_HOME:-$HOME/.local/share}";;

Darwin)
user_data="$HOME/Library/Application Support";;

*)
die "unknown kernel: $KERNEL"
esac

if [[ ! -d "$user_data" ]]; then
log "Current user's app data dir ($user_data) does not exist. Will create it." >&2
fi

echo "$user_data/nextstrain/cli/standalone"
}

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
echo "~/.bash_profile"

elif [[ -r ~/.bash_login ]]; then
echo "~/.bash_login"

elif [[ -r ~/.profile ]]; then
echo "~/.profile"

else
# If none exist, then we fallback to ~/.bash_profile
# and expect it to be created by our suggested shell
# init commands.
echo "~/.bash_profile"
fi;;
*)
echo "~/.bashrc";;
esac;;
zsh)
echo "${ZDOTDIR:-~}/.zshrc";;
*)
# A decent guess?
echo "~/.${shell}rc";;
esac
}

debug() {
[[ -n "${DEBUG:-}" ]]
}

if-debug() {
if debug; then
echo "$@"
fi
}

log() {
echo "--> $@"
}

die() {
echo "ERROR:" "$@" >&2
exit 1
}

main "$@"

0 comments on commit 2216314

Please sign in to comment.