#!/bin/bash # >>> shared main-head: same in cli/ and engine/, see scripts/check.sh # Everything is in main, called on the last line: a download cut short by # `curl | sh` defines a function and runs nothing. main() { set -e # No globbing: $ALIASES and ls's fields are split into words, never expanded. set -f # writable_by_others prints the first of dir and the dirs above it that a # user other than root can write to: not owned by root, writable by its # group (other than root's), or by everyone. Nothing when there is none. writable_by_others() { _d=$(cd "$1" 2>/dev/null && pwd -P) || return 0 while :; do # shellcheck disable=SC2046 set -- $(ls -ldn "$_d") case "$1" in ????????w*) echo "$_d"; return 0 ;; esac case "$1" in ?????w*) [ "$4" = 0 ] || { echo "$_d"; return 0; } ;; esac [ "$3" = 0 ] || { echo "$_d"; return 0; } [ "$_d" = / ] && return 0 _d=$(dirname "$_d") done } VERSION=${VERSION:-latest} case "$VERSION" in latest) ;; ""|.*|*[!A-Za-z0-9._-]*) echo "error: VERSION '$VERSION' is not a version (want latest or e.g. v1.2.3)"; exit 1 ;; esac # <<< shared main-head OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) # Git Bash / MSYS / Cygwin report e.g. "mingw64_nt-10.0-26200". case "$OS" in mingw*|msys*|cygwin*) OS="windows" ;; esac case "$ARCH" in x86_64) ARCH="amd64" ;; arm64|aarch64) ARCH="arm64" ;; *) echo "unsupported architecture: $ARCH" && exit 1 ;; esac # No native windows-arm64 build yet; the amd64 build runs under emulation. if [ "$OS" = "windows" ] && [ "$ARCH" = "arm64" ]; then ARCH="amd64" fi # set install dir if [ "$(id -u)" -ne 0 ] && [ -z "$INSTALL_DIR" ]; then INSTALL_DIR="$HOME/.local/bin" mkdir -p "$INSTALL_DIR" PATH_LINE='export PATH="$HOME/.local/bin:$PATH"' # A read-only or missing rc file must not abort the install. add_path() { { [ -f "$1" ] && grep -q '\.local/bin' "$1"; } && return 0 { echo "$PATH_LINE" >> "$1"; } 2>/dev/null || PATH_WARN=1 } case ":$PATH:" in *":$INSTALL_DIR:"*) ;; *) PATH_MISSING=1 ;; esac if [ "$OS" = "darwin" ]; then add_path "$HOME/.zshrc" add_path "$HOME/.bash_profile" else add_path "$HOME/.bashrc" fi export PATH="$INSTALL_DIR:$PATH" else INSTALL_DIR=${INSTALL_DIR:-/usr/local/bin} mkdir -p "$INSTALL_DIR" fi # >>> shared root-warning: same in cli/ and engine/, see scripts/check.sh # As root, what is installed may be run by a root service: whoever can # write to the install dir, or to a dir above it, can replace it. if [ "$(id -u)" -eq 0 ] && [ "$OS" != "windows" ]; then UNSAFE_DIR=$(writable_by_others "$INSTALL_DIR") || true if [ -n "$UNSAFE_DIR" ]; then echo "warning: $UNSAFE_DIR can be written by a user other than root: that user can replace" echo " $INSTALL_DIR/inferencesh-engine with their own program, and anything that runs it as root" echo " runs theirs. Install into a dir only root can write to, such as /usr/local/bin." fi fi # <<< shared root-warning # resolve download url and aliases from manifest if [ "$VERSION" = "latest" ]; then MANIFEST_URL="https://dist.inference.sh/engine/manifest.json" else MANIFEST_URL="https://dist.inference.sh/engine/${VERSION}/manifest.json" fi MANIFEST=$(curl -fsSL "$MANIFEST_URL") || { echo "error: version $VERSION not found"; exit 1; } URL=$(echo "$MANIFEST" | grep -o "\"$OS-$ARCH\" *: *{[^}]*}" | grep -o 'https[^"]*' | head -n 1) || true if [ -z "$URL" ]; then echo "error: no build for $OS-$ARCH in $MANIFEST_URL" exit 1 fi # parse aliases from manifest. defaults to "engine" when not specified or # when installing a pinned older version that predates the field. if [ -n "$MANIFEST" ]; then ALIASES=$(echo "$MANIFEST" | grep -o '"aliases" *: *\[[^]]*\]' | grep -o '"[^"]*"' | grep -v '^"aliases"$' | tr -d '"' | tr '\n' ' ') fi if [ -z "$ALIASES" ]; then ALIASES="engine" fi # >>> shared alias-names: same in cli/ and engine/, see scripts/check.sh # An alias is a file name in the install dir, nothing more. SAFE_ALIASES="" for alias in $ALIASES; do case "$alias" in .*|*/*|*[!A-Za-z0-9._-]*) echo "warning: skipping alias '$alias': not a plain file name" ;; *) SAFE_ALIASES="$SAFE_ALIASES $alias" ;; esac done ALIASES=${SAFE_ALIASES# } # <<< shared alias-names case "$URL" in *.zip) EXT=".zip" ;; *) EXT=".tar.gz" ;; esac NAME=$(basename "$URL" "$EXT") TARBALL_NAME=$(basename "$URL") TMP=$(mktemp -d) # >>> shared stage-trap: same in cli/ and engine/, see scripts/check.sh STAGE="" trap 'rm -rf "$TMP"; [ -z "$STAGE" ] || rm -f "$STAGE"' EXIT # <<< shared stage-trap echo "downloading engine $VERSION for $OS-$ARCH..." curl -fsSL "$URL" -o "$TMP/$TARBALL_NAME" # verify checksum echo "verifying checksum..." CHECKSUMS_URL=$(echo "$MANIFEST_URL" | sed 's/manifest\.json/checksums.txt/') curl -fsSL "$CHECKSUMS_URL" -o "$TMP/checksums.txt" EXPECTED=$(grep "$TARBALL_NAME" "$TMP/checksums.txt" | awk '{print $1}') if [ -z "$EXPECTED" ]; then echo "warning: no checksum found for $TARBALL_NAME, skipping verification" else if command -v sha256sum >/dev/null 2>&1; then ACTUAL=$(sha256sum "$TMP/$TARBALL_NAME" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then ACTUAL=$(shasum -a 256 "$TMP/$TARBALL_NAME" | awk '{print $1}') else echo "warning: no sha256sum or shasum found, skipping verification" ACTUAL="$EXPECTED" fi if [ "$ACTUAL" != "$EXPECTED" ]; then echo "error: checksum mismatch!" echo " expected: $EXPECTED" echo " got: $ACTUAL" echo "the download may be corrupted or tampered with." exit 1 fi echo "checksum verified." fi # verify cosign signature on checksums (optional) DIST_BASE=$(echo "$MANIFEST_URL" | sed 's/\/manifest\.json//') if command -v cosign >/dev/null 2>&1; then echo "verifying signature..." BUNDLE="$TMP/checksums.txt.bundle" curl -fsSL "$DIST_BASE/checksums.txt.bundle" -o "$BUNDLE" 2>/dev/null && \ cosign verify-blob "$TMP/checksums.txt" \ --bundle "$BUNDLE" \ --key "$DIST_BASE/cosign.pub" && \ echo "signature verified." || \ echo "warning: signature verification failed, continuing with checksum-only." fi if [ "$EXT" = ".zip" ]; then if command -v unzip >/dev/null 2>&1; then unzip -q "$TMP/$TARBALL_NAME" -d "$TMP" elif command -v powershell.exe >/dev/null 2>&1; then powershell.exe -NoProfile -Command "Expand-Archive -Force -Path '$(cygpath -w "$TMP/$TARBALL_NAME")' -DestinationPath '$(cygpath -w "$TMP")'" else echo "error: need unzip or powershell.exe to extract $TARBALL_NAME" exit 1 fi else tar -xzf "$TMP/$TARBALL_NAME" -C "$TMP" fi if [ "$OS" = "windows" ]; then # No symlinks without elevation on Windows: install one exe per name. BIN="$TMP/$NAME.exe" [ -f "$BIN" ] || BIN=$(find "$TMP" -maxdepth 1 -name '*.exe' | head -n 1) for name in inferencesh-engine $ALIASES; do cp -f "$BIN" "$INSTALL_DIR/$name.exe" done else BIN="$TMP/$NAME" # >>> shared install-binary: same in cli/ and engine/, see scripts/check.sh # A new file beside the target, renamed over it: on one filesystem the # swap is atomic, so the binary is never missing mid-install, even while # a running copy updates itself. It is made here, owned by whoever runs # this, never extracted in place: tar run as root keeps the archive's # owner (uid 1000), and a root service must not run a file another user # can replace. An existing file's owner does not carry over: the rename # replaces it. STAGE=$(mktemp "$INSTALL_DIR/.inferencesh-engine.XXXXXX") cat "$BIN" > "$STAGE" if [ "$(id -u)" -eq 0 ]; then chown 0:0 "$STAGE" fi chmod 0755 "$STAGE" mv -f "$STAGE" "$INSTALL_DIR/inferencesh-engine" STAGE="" # Recreate aliases as symlinks (relative target so the link survives moves). for alias in $ALIASES; do ln -sfn inferencesh-engine "$INSTALL_DIR/$alias" done # <<< shared install-binary fi # ensure config directory exists CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/inferencesh" mkdir -p "$CONFIG_DIR" echo "installed to $INSTALL_DIR/inferencesh-engine" ALIAS_LIST=$(echo "$ALIASES" | tr ' ' ',' | sed 's/,$//') echo "aliases: $ALIAS_LIST" # Give the user the right next step based on what's already set up if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then echo "" echo "run 'engine serve' to start the worker daemon" else echo "run 'inferencesh-engine setup docker' to set up dependencies" fi if [ -n "$PATH_MISSING" ]; then if [ -n "$PATH_WARN" ]; then echo "note: could not update your shell profile. add this line to it yourself:" else echo "note: $INSTALL_DIR was added to your shell profile. restart your shell, or run:" fi echo " $PATH_LINE" fi } main "$@"