From 8b4d59a838b688528f3cb9f9258314ccd38a0dca Mon Sep 17 00:00:00 2001 From: Nick Hale <4175918+njhale@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:40:28 -0500 Subject: [PATCH] chore: add install script Add a cross-platform install script that installs the latest release of `gptscript`. Also add a github action that publishes that script to R2, making it publicly available at `https://get.gptscript.ai/install.sh`. Signed-off-by: Nick Hale <4175918+njhale@users.noreply.github.com> --- .github/workflows/push-install.yaml | 26 +++ README.md | 24 ++- scripts/install.sh | 305 ++++++++++++++++++++++++++++ 3 files changed, 351 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/push-install.yaml create mode 100755 scripts/install.sh diff --git a/.github/workflows/push-install.yaml b/.github/workflows/push-install.yaml new file mode 100644 index 00000000..0c739c9f --- /dev/null +++ b/.github/workflows/push-install.yaml @@ -0,0 +1,26 @@ +name: push-install +on: + push: + branches: + - main + paths: + - "scripts/install.sh" + +jobs: + push-install: + name: push-install + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + with: + fetch-depth: 1 + - name: Copy files to S3 + env: + AWS_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }} + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_KEY }} + AWS_DEFAULT_REGION: auto + run: | + ls -la + aws s3 cp scripts/install.sh s3://${{ secrets.R2_BUCKET }}/install.sh diff --git a/README.md b/README.md index 3e2bcb70..14152334 100644 --- a/README.md +++ b/README.md @@ -31,18 +31,34 @@ OUTPUT: The artist with the most number of albums in the database is Iron Maiden, with a total of 21 albums. ``` - ## Quick Start -1. Install with brew (or from [releases](https://github.com/gptscript-ai/gptscript/releases/latest)) +### 1. Install the latest release + +#### Homebrew (macOS and Linux) + ```shell brew install gptscript-ai/tap/gptscript ``` -2. Get an API key from [OpenAI](https://platform.openai.com/api-keys). + +#### Install Script (macOS and Linux): + +```shell +curl https://get.gptscript.ai/install.sh | sh +``` + +#### Manually + +Download and install the archive for your platform and architecture from the [release page](https://github.com/gptscript-ai/gptscript/releases). + +### 2. Get an API key from [OpenAI](https://platform.openai.com/api-keys). + ```shell export OPENAI_API_KEY="your-api-key" ``` -3. Run Hello World: + +### 3. Run Hello World + ```shell gptscript https://gptscript.ai/echo.gpt --input "Hello, World!" ``` diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 00000000..875fe256 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,305 @@ +#!/bin/sh +set -e +set -o noglob + +# Usage: +# curl ... | ENV_VAR=... sh - +# or +# ENV_VAR=... ./install.sh +# +# Example: +# Installing the most recent release: +# curl ... | sh - +# +# Environment variables: +# - INSTALL_GPTSCRIPT_SKIP_DOWNLOAD +# If set to true will not download gptscript hash or binary. +# +# - INSTALL_GPTSCRIPT_SYMLINK +# If set to 'skip' will not create symlinks, 'force' will overwrite, +# default will symlink if command does not exist in path. +# +# - INSTALL_GPTSCRIPT_BIN_DIR +# Directory to install gptscript binary, links, and uninstall script to, or use +# /usr/local/bin as the default +# +# - INSTALL_GPTSCRIPT_BIN_DIR_READ_ONLY +# If set to true will not write files to INSTALL_GPTSCRIPT_BIN_DIR, forces +# setting INSTALL_GPTSCRIPT_SKIP_DOWNLOAD=true + +GITHUB_URL=https://github.com/gptscript-ai/gptscript/releases +DOWNLOADER= +SHA= +ARCH= +SUFFIX= +EXT= +SUDO=sudo + +# --- helper functions for logs --- +info() { + echo '[INFO] ' "$@" +} + +warn() { + echo '[WARN] ' "$@" >&2 +} + +fatal() { + echo '[ERROR] ' "$@" >&2 + exit 1 +} + +# --- define needed environment variables --- +setup_env() { + # --- don't use sudo if we are already root --- + if [ $(id -u) -eq 0 ]; then + SUDO= + fi + + # --- use binary install directory if defined or create default --- + if [ -n "${INSTALL_GPTSCRIPT_BIN_DIR}" ]; then + BIN_DIR=${INSTALL_GPTSCRIPT_BIN_DIR} + else + # --- use /usr/local/bin if root can write to it, otherwise use /opt/bin if it exists + BIN_DIR=/usr/local/bin + if ! $SUDO sh -c "touch ${BIN_DIR}/gptscript-ro-test && rm -rf ${BIN_DIR}/gptscript-ro-test"; then + if [ -d /opt/bin ]; then + BIN_DIR=/opt/bin + fi + fi + fi + + # --- if bin directory is read only skip download --- + if [ "${INSTALL_GPTSCRIPT_BIN_DIR_READ_ONLY}" = true ]; then + INSTALL_GPTSCRIPT_SKIP_DOWNLOAD=true + fi +} + +# --- check if skip download environment variable set --- +can_skip_download() { + if [ "${INSTALL_GPTSCRIPT_SKIP_DOWNLOAD}" != true ]; then + return 1 + fi +} + +# --- verify an executable gptscript binary is installed --- +verify_gptscript_is_executable() { + if [ ! -x ${BIN_DIR}/gptscript ]; then + fatal "Executable gptscript binary not found at ${BIN_DIR}/gptscript" + fi +} + +# --- set arch and suffix, fatal if architecture not supported --- +setup_verify_arch() { + if [ -z "$ARCH" ]; then + PLATFORM=$(uname) + EXT=".tar.gz" + + case $PLATFORM in + Linux) + PLATFORM="linux" + ;; + Darwin) + PLATFORM="macOS" + ARCH=universal + ;; + Windows) + PLATFORM="windows" + EXT=".zip" + ;; + *) + fatal "Unsupported platform $PLATFORM" + esac + fi + + if [ -z "$ARCH" ]; then + ARCH=$(uname -m) + + case $ARCH in + amd64) + ARCH=amd64 + ;; + x86_64) + ARCH=amd64 + ;; + arm64) + ARCH=arm64 + ;; + aarch64) + ARCH=arm64 + ;; + *) + fatal "Unsupported architecture $ARCH" + esac + fi + + SUFFIX=-${PLATFORM}-${ARCH} +} + +# --- verify existence of network downloader executable --- +verify_downloader() { + # Return failure if it doesn't exist or is no executable + [ -x "$(command -v $1)" ] || return 1 + + # Set verified executable as our downloader program and return success + DOWNLOADER=$1 + return 0 +} + +verify_sha() { + # Return failure if it doesn't exist or is no executable + [ -x "$(command -v $1)" ] || return 1 + + # Set verified executable as our sha program and return success + SHA=$1 + return 0 +} + +get_sha() { + if [ "${SHA}" = "shasum" ]; then + $SHA -a 256 $1 + else + $SHA $1 + fi +} + +# --- create temporary directory and cleanup when done --- +setup_tmp() { + TMP_DIR=$(mktemp -d -t gptscript-install.XXXXXXXXXX) + TMP_HASH=${TMP_DIR}/gptscript.hash + TMP_ARCHIVE=${TMP_DIR}/gptscript${EXT} + cleanup() { + code=$? + set +e + trap - EXIT + rm -rf ${TMP_DIR} + exit $code + } + trap cleanup INT EXIT +} + +# --- use desired latest gptscript version if defined or find version from channel --- +get_release_version() { + info "Finding latest release" + version_url="${GITHUB_URL}/latest" + case $DOWNLOADER in + curl) + VERSION_GPTSCRIPT=$(curl -w '%{url_effective}' -L -s -S ${version_url} -o /dev/null | sed -e 's|.*/||') + ;; + wget) + VERSION_GPTSCRIPT=$(wget -SqO /dev/null ${version_url} 2>&1 | grep -i Location | sed -e 's|.*/||') + ;; + *) + fatal "Incorrect downloader executable '$DOWNLOADER'" + ;; + esac + info "Using ${VERSION_GPTSCRIPT} as release" +} + +# --- download from github url --- +download() { + [ $# -eq 2 ] || fatal 'download needs exactly 2 arguments' + + case $DOWNLOADER in + curl) + curl -o $1 -sfL $2 + ;; + wget) + wget -qO $1 $2 + ;; + *) + fatal "Incorrect executable '$DOWNLOADER'" + ;; + esac + + # Abort if download command failed + [ $? -eq 0 ] || fatal 'Download failed' +} + +# --- download hash from github url --- +download_hash() { + HASH_URL=${GITHUB_URL}/download/${VERSION_GPTSCRIPT}/checksums.txt + info "Downloading hash ${HASH_URL}" + download ${TMP_HASH} ${HASH_URL} + HASH_EXPECTED=$(grep " gptscript-${VERSION_GPTSCRIPT}${SUFFIX}${EXT}" ${TMP_HASH}) + HASH_EXPECTED=${HASH_EXPECTED%%[[:blank:]]*} +} + +# --- check hash against installed version --- +installed_hash_matches() { + if [ -x ${BIN_DIR}/gptscript ]; then + HASH_INSTALLED=$(get_sha "${BIN_DIR}/gptscript") + HASH_INSTALLED=${HASH_INSTALLED%%[[:blank:]]*} + if [ "${HASH_EXPECTED}" = "${HASH_INSTALLED}" ]; then + return + fi + fi + return 1 +} + +# --- download archive from github url --- +download_archive() { + ARCHIVE_URL=${GITHUB_URL}/download/${VERSION_GPTSCRIPT}/gptscript-${VERSION_GPTSCRIPT}${SUFFIX}${EXT} + info "Downloading archive ${ARCHIVE_URL}" + download ${TMP_ARCHIVE} ${ARCHIVE_URL} +} + +# --- verify downloaded archive hash --- +verify_archive() { + info "Verifying binary download" + HASH_BIN=$(get_sha $TMP_ARCHIVE) + HASH_BIN=${HASH_BIN%%[[:blank:]]*} + if [ "${HASH_EXPECTED}" != "${HASH_BIN}" ]; then + fatal "Download sha256 does not match ${HASH_EXPECTED}, got ${HASH_BIN}" + fi +} + +expand_archive() { + if [ "${EXT}" = ".zip" ]; then + unzip ${TMP_ARCHIVE} -d ${TMP_DIR} + else + tar xzf ${TMP_ARCHIVE} -C ${TMP_DIR} + fi + + TMP_BIN=${TMP_DIR}/gptscript +} + +# --- setup permissions and move binary to system directory --- +setup_binary() { + chmod 755 ${TMP_BIN} + info "Installing gptscript to ${BIN_DIR}/gptscript" + $SUDO chown root ${TMP_BIN} + $SUDO mv -f ${TMP_BIN} ${BIN_DIR}/gptscript +} + +# --- download and verify gptscript --- +download_and_verify() { + if can_skip_download; then + info 'Skipping gptscript download and verify' + verify_gptscript_is_executable + return + fi + + setup_verify_arch + verify_downloader curl || verify_downloader wget || fatal 'Can not find curl or wget for downloading files' + verify_sha sha256sum || verify_sha shasum || fatal 'Can not find sha256sum or shasum for verifying files' + setup_tmp + get_release_version + download_hash + + if installed_hash_matches; then + info 'Skipping binary download, installed gptscript matches hash' + return + fi + + download_archive + verify_archive + expand_archive + setup_binary +} + +# --- run the install process -- +{ + setup_env "$@" + download_and_verify +}