#!/bin/sh # Install or update blacktop/ipsw from its latest GitHub release. # POSIX sh is intentional: this works with `curl -fsSL URL | sh`. set -eu REPO="blacktop/ipsw" RELEASES_URL="https://github.com/$REPO/releases/latest" DOWNLOAD_BASE="https://github.com/$REPO/releases/download" die() { echo "ipsw installer: $*" >&2 exit 1 } need_command() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" } need_command curl need_command uname need_command tar need_command mktemp need_command chmod need_command mkdir OS_NAME=$(uname -s 2>/dev/null || die "cannot determine operating system") MACHINE=$(uname -m 2>/dev/null || die "cannot determine CPU architecture") case "$OS_NAME" in Linux) RELEASE_OS="linux" ;; Darwin) RELEASE_OS="macOS" ;; *) die "unsupported operating system '$OS_NAME'; upstream releases provide Linux and macOS binaries" ;; esac case "$MACHINE" in x86_64|amd64|x86-64) RELEASE_ARCH="x86_64" ;; arm64|aarch64|arm64e) RELEASE_ARCH="arm64" ;; *) die "unsupported CPU architecture '$MACHINE'; upstream releases provide x86_64 and arm64 binaries" ;; esac # The Linux artifact is the same for glibc and musl. Report libc for useful # diagnostics without making it a release-selection requirement. LIBC="unknown" if [ "$OS_NAME" = Linux ] && command -v ldd >/dev/null 2>&1; then if ldd --version 2>&1 | grep -qi musl; then LIBC="musl" elif ldd --version 2>&1 | grep -qi glibc; then LIBC="glibc" fi fi echo "Detected platform: $OS_NAME/$MACHINE ($LIBC)" echo "Checking latest ipsw release..." # GitHub redirects this URL to /releases/tag/vX.Y.Z. This avoids jq, Python, # and fragile grep-based JSON parsing. LATEST_URL=$(curl -fsSL -o /dev/null -w '%{url_effective}' "$RELEASES_URL") || \ die "could not query the latest GitHub release" TAG=${LATEST_URL##*/} case "$TAG" in v[0-9]*|[0-9]*) ;; *) die "GitHub returned an unexpected release URL: $LATEST_URL" ;; esac VERSION=${TAG#v} ASSET="ipsw_${VERSION}_${RELEASE_OS}_${RELEASE_ARCH}.tar.gz" DOWNLOAD_URL="$DOWNLOAD_BASE/$TAG/$ASSET" echo "Latest release: $TAG" echo "Release asset: $ASSET" # Prefer the existing command, then any writable directory already in PATH. # That makes updates immediately available to the current shell. INSTALL_DIR="" CURRENT_COMMAND=$(command -v ipsw 2>/dev/null || true) if [ -n "$CURRENT_COMMAND" ] && [ -f "$CURRENT_COMMAND" ]; then CURRENT_DIR=$(dirname "$CURRENT_COMMAND") if [ -w "$CURRENT_DIR" ]; then INSTALL_DIR=$CURRENT_DIR; fi fi if [ -z "$INSTALL_DIR" ]; then OLD_IFS=$IFS IFS=: for PATH_DIR in ${PATH:-}; do [ -n "$PATH_DIR" ] || continue [ -d "$PATH_DIR" ] || continue if [ -w "$PATH_DIR" ]; then INSTALL_DIR=$PATH_DIR break fi done IFS=$OLD_IFS fi # Standard user-local fallback. It avoids requiring root when no PATH entry is # writable; the installer prints a PATH export if this directory is new. HOME_DIR=${HOME:-} if [ -z "$INSTALL_DIR" ] && [ -n "$HOME_DIR" ]; then INSTALL_DIR="$HOME_DIR/.local/bin" mkdir -p "$INSTALL_DIR" || die "cannot create $INSTALL_DIR" [ -w "$INSTALL_DIR" ] || die "cannot write to $INSTALL_DIR" fi # If there is no user-local option, use /usr/local/bin through sudo when needed. USE_SUDO="" if [ -z "$INSTALL_DIR" ] && [ -d /usr/local/bin ]; then INSTALL_DIR=/usr/local/bin if [ ! -w "$INSTALL_DIR" ]; then command -v sudo >/dev/null 2>&1 || die "no writable install directory; add ~/.local/bin to PATH or run with sudo" USE_SUDO=sudo fi fi [ -n "$INSTALL_DIR" ] || die "could not find an install directory" # Check before downloading the archive so repeated runs are inexpensive. CURRENT_VERSION="" if [ -n "$CURRENT_COMMAND" ] && [ -x "$CURRENT_COMMAND" ]; then CURRENT_VERSION=$( "$CURRENT_COMMAND" version 2>/dev/null \ | sed -n 's/.*Version:[[:space:]]*\([^,[:space:]]*\).*/\1/p' \ | head -n 1 ) || true fi if [ "$CURRENT_VERSION" = "$VERSION" ]; then echo "ipsw $TAG is already installed at $CURRENT_COMMAND" exit 0 fi TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ipsw-install.XXXXXX") || die "cannot create a temporary directory" cleanup() { rm -rf "$TMP_DIR"; } trap cleanup EXIT HUP INT TERM ARCHIVE="$TMP_DIR/$ASSET" echo "Downloading $DOWNLOAD_URL" curl -fL --retry 3 --connect-timeout 15 --max-time 300 "$DOWNLOAD_URL" -o "$ARCHIVE" || \ die "could not download the release asset" # Verify the release checksum when a standard checksum utility is available. CHECKSUMS="$TMP_DIR/checksums.txt" if curl -fL --retry 3 --connect-timeout 15 --max-time 60 \ "$DOWNLOAD_BASE/$TAG/checksums.txt" -o "$CHECKSUMS" >/dev/null 2>&1; then EXPECTED=$(awk -v name="$ASSET" '$2 == name { print $1; exit }' "$CHECKSUMS") [ -n "$EXPECTED" ] || die "release checksum does not contain $ASSET" ACTUAL="" if command -v sha256sum >/dev/null 2>&1; then ACTUAL=$(sha256sum "$ARCHIVE" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then ACTUAL=$(shasum -a 256 "$ARCHIVE" | awk '{print $1}') fi if [ -n "$ACTUAL" ] && [ "$EXPECTED" != "$ACTUAL" ]; then die "checksum verification failed for $ASSET" fi fi tar -xzf "$ARCHIVE" -C "$TMP_DIR" || die "could not extract the release archive" NEW_BINARY="$TMP_DIR/ipsw" [ -f "$NEW_BINARY" ] || die "release archive did not contain an ipsw binary" chmod 755 "$NEW_BINARY" DESTINATION="$INSTALL_DIR/ipsw" echo "Installing ipsw $TAG to $DESTINATION" if [ -n "$USE_SUDO" ]; then "$USE_SUDO" install -m 0755 "$NEW_BINARY" "$DESTINATION" elif command -v install >/dev/null 2>&1; then install -m 0755 "$NEW_BINARY" "$DESTINATION" else cp "$NEW_BINARY" "$DESTINATION" chmod 755 "$DESTINATION" fi echo "Installed: $DESTINATION" if ! command -v ipsw >/dev/null 2>&1 || [ "$(command -v ipsw)" != "$DESTINATION" ]; then case ":${PATH:-}:" in *:"$INSTALL_DIR":*) ;; *) echo "Add this directory to your PATH:" echo " export PATH=\"$INSTALL_DIR:\$PATH\"" ;; esac fi "$DESTINATION" version