#!/bin/sh # OwnLLM installer for macOS and Linux. # # Easiest: sh install.sh (from an unpacked OwnLLM download) # Or: curl -fsSL /install.sh | sh (downloads, checks, then installs) # # What it does, and only this: # * checks the download against its SHA-256 fingerprints # * copies OwnLLM into ~/.local/share/ownllm and links ~/.local/bin/ownllm # * offers to fetch a model that fits your computer (you can say no) # What it never does: use sudo, change shell settings, start anything at login, # open a network port, send any information anywhere, or touch files outside the two # folders above. # # Options: --dry-run show what would happen, change nothing # --yes do not ask questions (also downloads the recommended model) # --no-model do not offer a model set -eu umask 022 VERSION="1.1.1" BASE_URL="${OWNLLM_RELEASE_BASE:-https://github.com/ownllm-org/ownllm/releases/download/v$VERSION}" ROOT="${OWNLLM_ROOT:-${XDG_DATA_HOME:-$HOME/.local/share}/ownllm}" BINDIR="${OWNLLM_BIN:-$HOME/.local/bin}" DRY=0; YES=0; MODEL=1 for a in "$@"; do case "$a" in --dry-run) DRY=1 ;; --yes|-y) YES=1 ;; --no-model) MODEL=0 ;; -h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "install: unknown option $a" >&2; exit 2 ;; esac done say() { printf '%s\n' "$*"; } die() { printf 'install: %s\n' "$*" >&2; exit 1; } run() { if [ "$DRY" -eq 1 ]; then say " (dry run) $*"; else "$@"; fi; } [ "$(id -u)" -ne 0 ] || die "please run this as your normal user, not as root. OwnLLM never needs administrator rights." case "$ROOT" in */ownllm) : ;; *) die "the install folder must end in /ownllm (got: $ROOT)." ;; esac sha_check() { # verify "$1" (a SHA256SUMS file) from inside its own folder if command -v shasum >/dev/null 2>&1; then shasum -a 256 -c "$1"; else sha256sum -c "$1"; fi } sha_of() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'; else sha256sum "$1" | awk '{print $1}'; fi } command -v tar >/dev/null 2>&1 || die "tar is required." command -v shasum >/dev/null 2>&1 || command -v sha256sum >/dev/null 2>&1 || die "shasum or sha256sum is required." case "$(uname -s)-$(uname -m)" in Darwin-arm64) PLAT="macos-arm64" ;; Darwin-x86_64) PLAT="macos-x64" ;; Linux-x86_64) PLAT="linux-x64" ;; Linux-aarch64|Linux-arm64) PLAT="linux-arm64" ;; *) die "this computer ($(uname -s) $(uname -m)) is not supported yet. On Windows, use WSL2 (see docs/INSTALL.md)." ;; esac # Fingerprints of the release packages, written into this file when the release is built. This copy # of install.sh is served from ownllm.org, so the check below does not depend on GitHub alone. PKG_HASHES="linux-arm64 d5b1a872635ec1e9bcee4d183a18f57fc81ab86d54019fd50c311433045eb14c linux-x64 98d74ee9eae7fed374f6411d6ae98278acb7583250e71b27490f9b103c3674ef macos-arm64 3ed42ccab42aeaa1188a7e6a75e80683a93d8ac1ab7c5292a3ab5bd5c1e82145 macos-x64 0e2925f3d4fe26822d7d19ed6c0c11c3dedbfb4228c24d2243b0b266ccac8e70" HERE="" [ -f "$0" ] && HERE=$(cd "$(dirname "$0")" && pwd) # empty when piped from curl: never trust the current folder WORK="" cleanup() { [ -z "$WORK" ] || rm -rf "$WORK"; } trap cleanup EXIT INT TERM # ---- find the package: next to this script, or download it --------------------------- if [ -n "$HERE" ] && [ -d "$HERE/engine" ] && [ -f "$HERE/SHA256SUMS" ]; then SRC="$HERE" else [ "$VERSION" != "@VER""SION@" ] || die "this is a development copy; use a release download (see README)." command -v curl >/dev/null 2>&1 || die "curl is required to download OwnLLM." WORK=$(mktemp -d "${TMPDIR:-/tmp}/ownllm-install.XXXXXX") PKG="ownllm-$VERSION-$PLAT.tar.xz" say "Downloading $PKG (small: the engine only, no model yet) ..." if [ "$DRY" -eq 1 ]; then say " (dry run) would download $BASE_URL/$PKG and $BASE_URL/SHA256SUMS"; SRC="" else curl --proto '=https' --tlsv1.2 -fL --progress-bar -o "$WORK/$PKG" "$BASE_URL/$PKG" || die "download failed." curl --proto '=https' --tlsv1.2 -fsSL -o "$WORK/SHA256SUMS" "$BASE_URL/SHA256SUMS" || die "could not download SHA256SUMS." want=$(awk -v f="$PKG" '$2==f || $2=="*"f {print $1}' "$WORK/SHA256SUMS") [ -n "$want" ] || die "$PKG is not listed in SHA256SUMS." got=$(sha_of "$WORK/$PKG") [ "$got" = "$want" ] || die "the download does not match its fingerprint; nothing was installed." pinned=$(printf '%s\n' "$PKG_HASHES" | awk -v p="$PLAT" '$1==p {print $2}') [ -n "$pinned" ] || die "this installer does not list a fingerprint for $PLAT; use the one from ownllm.org/install.sh." [ "$got" = "$pinned" ] || die "the download does not match the fingerprint built into this installer; nothing was installed." say "Fingerprint checked (twice: release page and this installer)." mkdir "$WORK/pkg"; tar -xJf "$WORK/$PKG" -C "$WORK/pkg" --strip-components=1 SRC="$WORK/pkg" fi fi say "" say "OwnLLM $VERSION for $PLAT" say " install folder : $ROOT" say " command : $BINDIR/ownllm" say "" if [ -n "$SRC" ]; then say "Checking every file in the package ..." ( cd "$SRC" && sha_check SHA256SUMS ) >/dev/null 2>&1 || die "a file in the package does not match SHA256SUMS; nothing was installed." # Nothing may be in the package that the fingerprint list does not name. listed=$(awk '{print $2}' "$SRC/SHA256SUMS" | sort) found=$(cd "$SRC" && find . -type f ! -name SHA256SUMS ! -path ./install.sh | sed 's#^\./##' | sort) [ "$listed" = "$found" ] || die "the package holds files that SHA256SUMS does not list; nothing was installed." # Links are only allowed inside engine/ and only to a neighbouring file. ( cd "$SRC" && find . -type l | sed 's#^\./##' ) | while read -r ln; do tgt=$(readlink "$SRC/$ln") case "$ln" in engine/*) : ;; *) echo "link outside engine/: $ln" >&2; exit 1 ;; esac case "$tgt" in /*|*..*|*/*) echo "unsafe link: $ln -> $tgt" >&2; exit 1 ;; esac done || die "the package contains an unsafe link; nothing was installed." say "All files match." fi # ---- install (programs are replaced; your models are never touched) ------------------ if [ "$DRY" -eq 1 ] && [ -z "$SRC" ]; then say "(dry run) nothing else to show."; exit 0; fi run mkdir -p "$ROOT" "$BINDIR" for part in engine ui search ctx-builtin bin manifest docs; do [ -d "$SRC/$part" ] || continue run rm -rf "$ROOT/$part.new" run cp -R "$SRC/$part" "$ROOT/$part.new" run rm -rf "$ROOT/$part" run mv "$ROOT/$part.new" "$ROOT/$part" done for f in VERSION SHA256SUMS LICENSE LICENSE-Apache-2.0 LICENSING.md COMMERCIAL.md NOTICE THIRD_PARTY.md PRIVACY.md; do [ -f "$SRC/$f" ] && run cp "$SRC/$f" "$ROOT/$f" done run mkdir -p "$ROOT/models" "$ROOT/run" if [ -d "$SRC/models" ]; then for m in "$SRC"/models/*.gguf; do [ -f "$m" ] && [ ! -f "$ROOT/models/$(basename "$m")" ] && run cp "$m" "$ROOT/models/"; done fi run chmod 755 "$ROOT/bin/ownllm" run ln -sf "$ROOT/bin/ownllm" "$BINDIR/ownllm" # Programs from a browser download carry a macOS "quarantine" flag that blocks unsigned # programs (these are not code-signed). They were just verified against their fingerprints, so the # flag is cleared for the engine and the command only - not for models or anything else. if [ "$(uname -s)" = "Darwin" ] && [ "$DRY" -ne 1 ]; then xattr -dr com.apple.quarantine "$ROOT/engine" "$ROOT/bin" 2>/dev/null || true; fi say "Installed." case ":$PATH:" in *":$BINDIR:"*) : ;; *) say "" say "One more step: add $BINDIR to your PATH, or run OwnLLM with its full path:" say " $BINDIR/ownllm" ;; esac # ---- offer a model --------------------------------------------------------------------- if [ "$MODEL" -eq 1 ] && [ "$DRY" -ne 1 ]; then OWN="$ROOT/bin/ownllm" have=$("$OWN" models 2>/dev/null | grep -c ' - installed' || true) if [ "$have" -eq 0 ]; then ram=$("$OWN" doctor 2>/dev/null | sed -n 's/.*memory: about \([0-9]*\) GB.*/\1/p' | head -n 1) ram=${ram:-8} if [ "$ram" -ge 12 ]; then rec=standard elif [ "$ram" -ge 8 ]; then rec=medium elif [ "$ram" -ge 4 ]; then rec=small else rec=tiny; fi say "" say "OwnLLM needs a model to talk to. For about ${ram} GB of memory, the best fit is: $rec." if [ "$YES" -eq 1 ]; then ans=y elif [ -t 0 ]; then printf 'Download it now? [Y/n] '; read -r ans || ans=n else ans=n; fi case "$ans" in ''|y|Y|yes|YES) "$OWN" get "$rec" ;; *) say "Okay. Later, run: ownllm get $rec" ;; esac fi fi say "" say "Try it: ownllm chat" say "Check it: ownllm doctor"