#!/bin/sh # # get-synapcores-ce.sh — one-line remote installer for SynapCores # Community Edition. # # Hosted at: https://get.synapcores.com # # Usage: # curl -fsSL https://get.synapcores.com/install.sh | sh # # Or, to pin a specific release: # curl -fsSL https://get.synapcores.com/install.sh | SYNAPCORES_VERSION=v1.2.0 sh # # Written in POSIX sh on purpose: when invoked via `curl ... | sh`, # Debian/Ubuntu's /bin/sh is dash, not bash. Bash-only constructs like # `[[ ... ]]`, `set -o pipefail`, `local`, `$EUID`, and arrays MUST NOT # appear in this file. If you need them, branch out into a helper that # bash can run after the binary is on disk. # # What it does: # 1. Detects the OS and architecture # 2. Resolves the latest GitHub release tag (or honors $SYNAPCORES_VERSION) # 3. Downloads the matching binary tarball + checksum # 4. Verifies the checksum # 5. Drops the binary at /usr/local/bin/synapcores (or asks if non-root) # 6. Hands off to install-ce.sh for system setup (user, paths, # systemd unit, default config), unless SYNAPCORES_BINARY_ONLY is set # # Requirements: # - curl # - tar # - sha256sum (or shasum -a 256) set -eu # --------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------- GITHUB_REPO="${SYNAPCORES_REPO:-SynapCores/synapcores-releases}" RELEASE_BASE="https://github.com/${GITHUB_REPO}/releases" PINNED_VERSION="${SYNAPCORES_VERSION:-}" INSTALL_PREFIX="${SYNAPCORES_PREFIX:-/usr/local/bin}" BINARY_ONLY="${SYNAPCORES_BINARY_ONLY:-}" # --------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------- log() { printf '\033[1;34m[get-synapcores]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[get-synapcores]\033[0m %s\n' "$*" >&2; } fail() { printf '\033[1;31m[get-synapcores]\033[0m %s\n' "$*" >&2; exit 1; } require() { command -v "$1" >/dev/null 2>&1 || fail "missing required tool: $1" } require curl require tar if command -v sha256sum >/dev/null 2>&1; then SHA256_TOOL="sha256sum" elif command -v shasum >/dev/null 2>&1; then SHA256_TOOL="shasum -a 256" else fail "missing required tool: sha256sum (or shasum)" fi # --------------------------------------------------------------------- # Platform detection # --------------------------------------------------------------------- # detect_platform: prints "-" to stdout. Exits non-zero if # the platform is unsupported. POSIX sh has no `local`, so we use # uppercase variable names to make it obvious these survive the call. detect_platform() { DETECTED_OS=$(uname -s) DETECTED_ARCH=$(uname -m) case "$DETECTED_OS" in Linux) DETECTED_OS=linux ;; Darwin) DETECTED_OS=darwin ;; *) fail "unsupported OS: $DETECTED_OS (CE binaries are published for Linux and macOS)" ;; esac case "$DETECTED_ARCH" in x86_64|amd64) DETECTED_ARCH=x86_64 ;; aarch64|arm64) DETECTED_ARCH=aarch64 ;; *) fail "unsupported architecture: $DETECTED_ARCH" ;; esac # Intel Mac native binary is not published as of v1.3.0-ce. The # source migration to ffmpeg-next 7 unblocked the build, but # GitHub-hosted macos-13 runner availability has been unreliable # on personal-account repos (multi-hour queue waits with no # allocation). Apple Silicon (macos-14) builds reliably, so we # ship Apple Silicon native + recommend Docker for Intel Macs. if [ "$DETECTED_OS" = "darwin" ] && [ "$DETECTED_ARCH" = "x86_64" ]; then cat >&2 <&2 <&2 <&2 <&2 <&2 <) to extract the tag. PINNED_VERSION=$( curl -fsS -o /dev/null -w '%{redirect_url}' "${RELEASE_BASE}/latest" \ | sed 's@^.*/tag/@@' ) [ -n "$PINNED_VERSION" ] || fail "could not resolve latest release" fi log "Version: $PINNED_VERSION" # --------------------------------------------------------------------- # Download # --------------------------------------------------------------------- # DISTRO_TAG is "" on Ubuntu 22.04 / Debian 12 / macOS / unknown distros # (FFmpeg 4 / native Mac tarball) and "-ubuntu24" on Ubuntu 24.04 / Debian 13 # (FFmpeg 6 tarball variant). Set by check_distro() above. TARBALL="synapcores-ce-${PINNED_VERSION}-${PLATFORM}${DISTRO_TAG}.tar.gz" TARBALL_URL="${RELEASE_BASE}/download/${PINNED_VERSION}/${TARBALL}" CHECKSUM_URL="${TARBALL_URL}.sha256" # "alias to latest available": GitHub's release `latest` is global, not # per-platform. If the resolved latest release has no binary for THIS platform # (e.g. a Linux-only release), fall back to the newest release that does — so # Mac/ARM users always get the latest *available* build instead of a 404. # Only when the version was auto-resolved; a pinned $SYNAPCORES_VERSION fails loud. asset_exists() { curl -fsSL -r 0-0 -o /dev/null "$1" 2>/dev/null; } if [ "$AUTO_RESOLVED" = "1" ] && ! asset_exists "$TARBALL_URL"; then warn "No ${PLATFORM}${DISTRO_TAG} binary in ${PINNED_VERSION}; finding the latest release that has one..." _found=0 _tags=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=20" 2>/dev/null \ | grep '"tag_name":' | sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/') for _t in $_tags; do _cand="synapcores-ce-${_t}-${PLATFORM}${DISTRO_TAG}.tar.gz" _url="${RELEASE_BASE}/download/${_t}/${_cand}" if asset_exists "$_url"; then log "Using ${_t} — latest release with a ${PLATFORM}${DISTRO_TAG} binary." PINNED_VERSION="$_t" TARBALL="$_cand" TARBALL_URL="$_url" CHECKSUM_URL="${_url}.sha256" _found=1 break fi done [ "$_found" = "1" ] || fail "no ${PLATFORM}${DISTRO_TAG} binary in recent releases — run via Docker instead: docker run -d -p 8080:8080 synapcores/community:latest" fi # --------------------------------------------------------------------- # Anonymous install counter ping (no PII, opt-out via COUNTER_OPT_OUT=1) # --------------------------------------------------------------------- # Fires a single GET to the install-counter endpoint right after we've # resolved the platform + version and committed to an install — but # BEFORE the download starts. This is how we count installs by OS/arch/ # version. Until this existed, Linux + Windows installs were invisible # (curl-pipe-sh leaves no trace on GitHub Pages); the binary download # itself is counted on GitHub Releases for Linux/Windows users via this # path. Total bytes added to the network footprint: < 200 bytes. # # No personal data is sent — just the platform string and version tag, # both already public (they're literally in the tarball URL above). # 5-second timeout. `|| true` means: if the counter is down, the # install still proceeds. We never block on telemetry. # # Set COUNTER_OPT_OUT=1 in your environment to skip the ping entirely. if [ -z "${COUNTER_OPT_OUT:-}" ]; then _counter_url="https://synapcores.com/api/install-counter?v=${PINNED_VERSION}&os=${DETECTED_OS}&arch=${DETECTED_ARCH}&channel=cli" curl -fsSL --max-time 5 -o /dev/null "$_counter_url" 2>/dev/null || true fi WORK_DIR=$(mktemp -d) trap 'rm -rf "$WORK_DIR"' EXIT INT TERM log "Downloading ${TARBALL}..." curl -fsSL "$TARBALL_URL" -o "${WORK_DIR}/${TARBALL}" \ || fail "download failed: $TARBALL_URL" log "Downloading checksum..." curl -fsSL "$CHECKSUM_URL" -o "${WORK_DIR}/${TARBALL}.sha256" \ || fail "checksum download failed: $CHECKSUM_URL" # --------------------------------------------------------------------- # Verify # --------------------------------------------------------------------- log "Verifying checksum..." ( cd "$WORK_DIR" EXPECTED=$(awk '{print $1}' "${TARBALL}.sha256") ACTUAL=$($SHA256_TOOL "${TARBALL}" | awk '{print $1}') if [ "$EXPECTED" != "$ACTUAL" ]; then fail "checksum mismatch: expected $EXPECTED got $ACTUAL" fi ) log "Checksum OK." # --------------------------------------------------------------------- # Extract # --------------------------------------------------------------------- log "Extracting..." tar -xzf "${WORK_DIR}/${TARBALL}" -C "$WORK_DIR" # The workflow packages the binary inside a single top-level dir: # synapcores-ce--/synapcores # Locate it without hardcoding the dir name so the script keeps # working if the packaging convention changes. BINARY_SRC=$(find "$WORK_DIR" -maxdepth 3 -type f -name synapcores -perm -u+x 2>/dev/null | head -1) if [ -z "$BINARY_SRC" ] || [ ! -x "$BINARY_SRC" ]; then fail "extracted archive does not contain a 'synapcores' executable" fi # --------------------------------------------------------------------- # Install binary # --------------------------------------------------------------------- # v1.6.4.1 fix: on Apple Silicon Macs /usr/local/bin doesn't exist by # default (Homebrew lives at /opt/homebrew/bin there). When the user # hasn't pinned SYNAPCORES_PREFIX, prefer the Homebrew bin dir if # it's present so the binary lands inside the user's existing $PATH. # Falls back to /usr/local/bin only if Homebrew isn't there. Intel # Macs and Linux keep the original /usr/local/bin default since # that's where their Homebrew lives too (Intel) or always exists # (most Linux distros). if [ -z "${SYNAPCORES_PREFIX:-}" ] \ && [ "$DETECTED_OS" = "darwin" ] \ && [ "$DETECTED_ARCH" = "aarch64" ] \ && [ -d /opt/homebrew/bin ]; then INSTALL_PREFIX="/opt/homebrew/bin" log "Apple Silicon detected with Homebrew; using ${INSTALL_PREFIX}" fi # POSIX sh has no $EUID — use `id -u`. Also POSIX has no arrays, so we # use SUDO_PREFIX as a (possibly empty) string that is word-split into # argv when the install command runs. if [ "$(id -u)" -eq 0 ]; then SUDO_PREFIX="" else log "Installing to ${INSTALL_PREFIX} requires sudo..." SUDO_PREFIX="sudo" fi # v1.6.4.1 fix: ensure the install directory exists before invoking # install(1). On a fresh Apple Silicon Mac /usr/local/bin doesn't exist # yet, which made BSD install fail with the cryptic # "install: /usr/local/bin/INS@: No such file or directory" # error — INS@* is the BSD-install atomic-rename tempfile, and it # can't be created in a directory that doesn't exist. # shellcheck disable=SC2086 $SUDO_PREFIX mkdir -p "$INSTALL_PREFIX" \ || fail "could not create ${INSTALL_PREFIX} (need write or sudo)" # shellcheck disable=SC2086 $SUDO_PREFIX install -m 0755 "$BINARY_SRC" "${INSTALL_PREFIX}/synapcores" \ || fail "binary install failed" log "Installed: ${INSTALL_PREFIX}/synapcores" # v1.6.4.1: surface the PATH hint when we installed somewhere that # isn't already on the user's PATH. Saves the next 60 seconds of # "synapcores: command not found" frustration. case ":${PATH}:" in *":${INSTALL_PREFIX}:"*) ;; # already on PATH *) cat >&2 </dev/null 2>&1; then warn "Homebrew is required for macOS runtime deps (ffmpeg/tesseract/leptonica)." warn "Install Homebrew first: https://brew.sh" warn "Then re-run: curl -fsSL https://get.synapcores.com/install.sh | sh" exit 1 fi # Pick the right ffmpeg formula. We try ffmpeg@7 first because # that's what the GitHub Actions release built against; if it's # not in the user's Homebrew tap we fall back to plain ffmpeg # (ffmpeg-next 7 supports both 7.x and 8.x at runtime). if brew info ffmpeg@7 >/dev/null 2>&1; then FFMPEG_FORMULA="ffmpeg@7" else FFMPEG_FORMULA="ffmpeg" fi REQUIRED_DEPS="$FFMPEG_FORMULA tesseract leptonica" MISSING_DEPS="" for dep in $REQUIRED_DEPS; do if ! brew list --formula "$dep" >/dev/null 2>&1; then MISSING_DEPS="$MISSING_DEPS $dep" fi done # Strip leading whitespace MISSING_DEPS=$(printf '%s' "$MISSING_DEPS" | sed 's/^ *//') if [ -n "$MISSING_DEPS" ]; then log "Missing Homebrew deps: $MISSING_DEPS" if [ -n "${SYNAPCORES_NONINTERACTIVE:-}" ]; then log "Non-interactive mode (SYNAPCORES_NONINTERACTIVE set); installing..." ANSWER="y" elif [ ! -t 0 ] && [ ! -e /dev/tty ]; then warn "stdin is not a terminal and /dev/tty is unavailable; cannot prompt." warn "Install manually: brew install $MISSING_DEPS" warn "Or set SYNAPCORES_NONINTERACTIVE=1 to install automatically." exit 1 else printf '\033[1;34m[get-synapcores]\033[0m Install them now? (brew install %s) [Y/n] ' "$MISSING_DEPS" # Read from /dev/tty so the prompt works under # `curl ... | sh` where stdin is the script itself. read -r ANSWER < /dev/tty || ANSWER="n" fi case "$ANSWER" in ""|[Yy]*) log "Running: brew install $MISSING_DEPS" # shellcheck disable=SC2086 brew install $MISSING_DEPS || { warn "brew install failed. Install manually: brew install $MISSING_DEPS" exit 1 } ;; *) log "Skipping. Install manually: brew install $MISSING_DEPS" log "Then run: ${INSTALL_PREFIX}/synapcores --version" exit 0 ;; esac else log "All Homebrew runtime deps present." fi # With deps installed, the binary should now run. if "${INSTALL_PREFIX}/synapcores" --version 2>/dev/null | grep -q "Community"; then log "Edition check: $("${INSTALL_PREFIX}/synapcores" --version)" else warn "Binary still won't run cleanly. Diagnose with:" warn " otool -L ${INSTALL_PREFIX}/synapcores | head -20" warn "Look for any 'not found' lines pointing at missing dylibs." fi # ----- Drop default config + data dir ----- SC_HOME="${HOME}/.synapcores" SC_CONFIG="${SC_HOME}/gateway.toml" SC_DATA_DIR="${SC_HOME}/data" SC_MODELS_DIR="${SC_HOME}/models/text" mkdir -p "$SC_HOME" "$SC_DATA_DIR" "$SC_MODELS_DIR" # The bundled template (v1.3.1+) lives next to the binary in the # extracted tarball. Older tarballs don't have it, so fall through # to the inline minimal config in that case. BUNDLED_TEMPLATE="$(dirname "$BINARY_SRC")/community.toml.template" if [ -f "$SC_CONFIG" ]; then log "Config already exists at ${SC_CONFIG} — leaving as-is." elif [ -f "$BUNDLED_TEMPLATE" ]; then # Adapt the template for macOS: rewrite the data_dir to live # under the user's home (the template defaults to # /opt/synapcores/aidb_data, which would need root and isn't # the macOS convention). sed "s|^data_dir = .*|data_dir = \"${SC_DATA_DIR}\"|" \ "$BUNDLED_TEMPLATE" \ > "$SC_CONFIG" log "Wrote default config to ${SC_CONFIG}" else warn "No config template bundled (older tarball?). Generating minimal config." cat >"$SC_CONFIG" </dev/null; then chmod +x "$INSTALLER" if [ "$(id -u)" -eq 0 ]; then "$INSTALLER" --binary "${INSTALL_PREFIX}/synapcores" else log "System setup requires sudo..." sudo "$INSTALLER" --binary "${INSTALL_PREFIX}/synapcores" fi else warn "system installer not found at $INSTALLER_URL" warn "binary is installed; complete setup manually per the docs" fi log "Done. Try: synapcores --version"