diff --git a/.github/workflows/install-script.yml b/.github/workflows/install-script.yml new file mode 100644 index 00000000..257cc505 --- /dev/null +++ b/.github/workflows/install-script.yml @@ -0,0 +1,88 @@ +name: Install Script + +on: + push: + branches: [main] + paths: + - install.sh + - .github/workflows/install-script.yml + pull_request: + paths: + - install.sh + - .github/workflows/install-script.yml + +permissions: + contents: read + +concurrency: + group: install-script-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Shellcheck + run: shellcheck --shell=sh install.sh + + install: + name: Install (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + defaults: + run: + shell: bash + env: + PUP_INSTALL_DIR: ${{ github.workspace }}/pup-bin + PUP_NO_BREW: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install latest via pipe + run: sh < install.sh + - name: Verify latest version + run: | + latest="$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/DataDog/pup/releases/latest)" + latest="${latest##*/tag/v}" + "$PUP_INSTALL_DIR/pup" --version | tee version.txt + grep -F "$latest" version.txt + - name: Install pinned version with v prefix + run: PUP_VERSION=v1.27.0 sh install.sh + - name: Verify pinned version + run: | + "$PUP_INSTALL_DIR/pup" --version | tee version.txt + grep -F "1.27.0" version.txt + - name: Nonexistent version fails + run: | + if PUP_VERSION=0.0.0 sh install.sh; then + echo "expected install of a nonexistent version to fail" + exit 1 + fi + + install-brew: + name: Install via Homebrew (macOS) + runs-on: macos-latest + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + HOMEBREW_NO_INSTALL_CLEANUP: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install via pipe + run: sh < install.sh 2>&1 | tee install.log + - name: Verify Homebrew was used + run: | + grep -F "Homebrew detected" install.log + brew list --formula pup + pup --version + - name: Overrides skip Homebrew + run: | + PUP_VERSION=1.27.0 PUP_INSTALL_DIR="$RUNNER_TEMP/pup-bin" sh install.sh 2>&1 | tee override.log + if grep -F "Homebrew detected" override.log; then + echo "expected PUP_VERSION/PUP_INSTALL_DIR to bypass Homebrew" + exit 1 + fi + "$RUNNER_TEMP/pup-bin/pup" --version | grep -F "1.27.0" diff --git a/README.md b/README.md index 59064ba8..d579d79e 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,24 @@ list of commands as built. ## Installation +### Install Script (macOS/Linux/Windows) + +```bash +curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | sh +``` + +On macOS with Homebrew installed, the script runs `brew install datadog-labs/pack/pup`, unless `PUP_VERSION` or `PUP_INSTALL_DIR` is set. Otherwise (Linux, macOS without Homebrew, and Windows via Git Bash, MSYS2, or Cygwin) it downloads the release archive, verifies its SHA-256 checksum, and installs the binary to `~/.local/bin`. Environment variables override the defaults: + +| Variable | Default | Description | +|----------|---------|-------------| +| `PUP_VERSION` | latest release | Version to install (e.g. `1.27.0`) | +| `PUP_INSTALL_DIR` | `~/.local/bin` | Directory to install the binary into | +| `PUP_NO_BREW` | unset | Set to `1` on macOS to skip Homebrew and download directly | + +```bash +curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | PUP_VERSION=1.27.0 PUP_INSTALL_DIR=/usr/local/bin sh +``` + ### Homebrew (macOS/Linux) ```bash diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..75c8bb6d --- /dev/null +++ b/install.sh @@ -0,0 +1,172 @@ +#!/bin/sh +# Install the pup CLI (https://github.com/DataDog/pup). +# +# curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | sh +# +# On macOS with Homebrew available, installs via `brew install datadog-labs/pack/pup` +# unless PUP_VERSION or PUP_INSTALL_DIR is set, since Homebrew cannot honor them. +# Everywhere else (Linux, macOS without Homebrew, Windows via Git Bash/MSYS2/Cygwin), +# downloads the release archive, verifies its SHA-256 checksum, and installs the binary. +# +# Environment variables: +# PUP_VERSION Version to install, e.g. 1.27.0 (default: latest release) +# PUP_INSTALL_DIR Directory for the binary (default: $HOME/.local/bin) +# PUP_NO_BREW Set to 1 to skip Homebrew on macOS and download directly + +set -eu + +REPO="DataDog/pup" +BREW_FORMULA="datadog-labs/pack/pup" + +say() { printf 'pup-install: %s\n' "$*" >&2; } +die() { + say "error: $*" + exit 1 +} + +need() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +detect_os() { + case "$(uname -s)" in + Darwin) echo Darwin ;; + Linux) echo Linux ;; + MINGW* | MSYS* | CYGWIN*) echo Windows ;; + *) die "unsupported operating system: $(uname -s)" ;; + esac +} + +detect_arch() { + os="$1" + arch="$(uname -m)" + case "$arch" in + x86_64 | amd64) arch=x86_64 ;; + arm64 | aarch64) arch=arm64 ;; + *) die "unsupported architecture: $arch" ;; + esac + # A shell running under Rosetta reports x86_64 on Apple Silicon. + if [ "$os" = Darwin ] && [ "$arch" = x86_64 ] && + [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then + arch=arm64 + fi + # Only an x86_64 Windows build is published; Windows on ARM runs it under emulation. + if [ "$os" = Windows ]; then + arch=x86_64 + fi + echo "$arch" +} + +latest_version() { + # Follow the /releases/latest redirect instead of the rate-limited GitHub API. + url="$(curl -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest")" || + die "could not resolve the latest release" + case "$url" in + */tag/v*) echo "${url##*/tag/v}" ;; + *) die "unexpected latest release URL: $url" ;; + esac +} + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + die "sha256sum or shasum is required to verify the download" + fi +} + +verify_checksum() { + file="$1" + checksums="$2" + name="$(basename "$file")" + expected="$(awk -v f="$name" '$2 == f {print $1}' "$checksums")" + [ -n "$expected" ] || die "no checksum listed for $name" + actual="$(sha256_of "$file")" + [ "$expected" = "$actual" ] || die "checksum mismatch for $name (expected $expected, got $actual)" +} + +extract() { + archive="$1" + dest="$2" + case "$archive" in + *.tar.gz) tar -xzf "$archive" -C "$dest" ;; + *.zip) + if command -v unzip >/dev/null 2>&1; then + unzip -q "$archive" -d "$dest" + else + powershell.exe -NoProfile -Command \ + "Expand-Archive -LiteralPath '$(cygpath -w "$archive")' -DestinationPath '$(cygpath -w "$dest")'" + fi + ;; + esac +} + +install_with_brew() { + say "Homebrew detected; running: brew install $BREW_FORMULA" + brew install "$BREW_FORMULA" /dev/null 2>&1; then + install_with_brew + return + fi + need curl + need tar + need awk + install_direct "$os" +} + +# POSIX sh: no `local`, so function variables are script-global. +# Everything runs from main so a truncated `curl | sh` download executes nothing. +main