#!/bin/sh # llamay installer for macOS and Linux. # # curl -fsSL https://llamay.com/install.sh | sh # # POSIX sh, not bash: this is piped into whatever /bin/sh the machine has, and # on Debian and Ubuntu that is dash. Every construct here works in both. # # What it does, in order, and nothing else: # # 1. works out the platform and picks the right artifact # 2. downloads it and the release's SHA256SUMS, and checks the sum # 3. installs the binary to $LLAMAY_INSTALL_DIR (default /usr/local/bin) # 4. on Linux with systemd, installs and starts a service # 5. on macOS, installs and loads a LaunchAgent # # Overridable before it runs: # # LLAMAY_VERSION a specific version, e.g. 1.2.3 (default: latest) # LLAMAY_INSTALL_DIR where the binary goes (default /usr/local/bin) # LLAMAY_NO_SERVICE set to 1 to install the CLI and no service # LLAMAY_ACCEL cpu|gpu — gpu takes the CUDA+Vulkan build on Linux # amd64, and the Metal build on Apple Silicon; the # default picks gpu where one exists for the platform set -eu # The artifacts live in a public repository of their own. The engine's source # is private, and a private repository answers 404 to an unauthenticated # download -- which is every user of this script. Same arrangement as # kosa8-releases and anz33-releases. REPO="AzmxAI/llamay-releases" BASE="https://github.com/${REPO}/releases" say() { printf '%s\n' "$*"; } warn() { printf '%s\n' "$*" >&2; } die() { printf 'error: %s\n' "$*" >&2; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || die "$1 is required and was not found" } # sudo only where it is actually needed. A user who has write access to the # install directory should not be asked for a password, and a machine with no # sudo at all should still be able to install into a directory it owns. SUDO="" maybe_sudo() { if [ -w "$1" ]; then SUDO="" elif command -v sudo >/dev/null 2>&1; then SUDO="sudo" elif [ "$(id -u)" = 0 ]; then SUDO="" else die "no write access to $1 and no sudo; set LLAMAY_INSTALL_DIR to a directory you own" fi } need curl need tar OS=$(uname -s) ARCH=$(uname -m) case "$ARCH" in x86_64 | amd64) ARCH=amd64 ;; aarch64 | arm64) ARCH=arm64 ;; *) die "unsupported architecture: $ARCH" ;; esac case "$OS" in Linux) OS=linux ;; Darwin) OS=darwin ;; *) die "unsupported operating system: $OS — for Windows see https://llamay.com/download/windows" ;; esac # Apple Silicon gets the Metal build and Intel Macs do not: the Metal shaders # need Apple Silicon, and there is no darwin/amd64 GPU artifact to fall back to. ACCEL="${LLAMAY_ACCEL:-auto}" if [ "$ACCEL" = auto ]; then ACCEL=cpu if [ "$OS" = linux ] && [ "$ARCH" = amd64 ]; then ACCEL=gpu; fi if [ "$OS" = darwin ] && [ "$ARCH" = arm64 ]; then ACCEL=gpu; fi fi VERSION="${LLAMAY_VERSION:-}" if [ -z "$VERSION" ]; then # The redirect on /releases/latest names the tag, which avoids depending on # the API and its rate limit for what is one lookup. VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' "${BASE}/latest" | sed 's|.*/tag/v||') [ -n "$VERSION" ] || die "could not determine the latest version; set LLAMAY_VERSION" fi case "$ACCEL" in gpu) if [ "$OS" = linux ]; then ASSET="llamay_${VERSION}_linux_amd64_gpu.tar.gz" else ASSET="llamay_${VERSION}_darwin_arm64_metal.tar.gz" fi ;; cpu) ASSET="llamay_${VERSION}_${OS}_${ARCH}.tar.gz" ;; *) die "LLAMAY_ACCEL must be cpu, gpu or auto" ;; esac INSTALL_DIR="${LLAMAY_INSTALL_DIR:-/usr/local/bin}" URL="${BASE}/download/v${VERSION}/${ASSET}" say "llamay ${VERSION} — ${OS}/${ARCH} (${ACCEL})" TMP=$(mktemp -d) # The trap is the only cleanup: every path out of here goes through it, # including the die() calls above this line, which is why it is set as early # as the directory exists. trap 'rm -rf "$TMP"' EXIT INT TERM say "downloading ${ASSET}" curl -fsSL --retry 3 -o "${TMP}/${ASSET}" "$URL" || die "download failed: $URL" # The checksum is not optional and not best-effort. A release publishes # SHA256SUMS over the artifacts as they were uploaded, and an installer that # skips the check on a machine with no sha256 tool is an installer that runs # whatever it was handed. if curl -fsSL --retry 3 -o "${TMP}/SHA256SUMS" "${BASE}/download/v${VERSION}/SHA256SUMS"; then if command -v sha256sum >/dev/null 2>&1; then SUM=$(sha256sum "${TMP}/${ASSET}" | cut -d' ' -f1) elif command -v shasum >/dev/null 2>&1; then SUM=$(shasum -a 256 "${TMP}/${ASSET}" | cut -d' ' -f1) else die "neither sha256sum nor shasum is available to verify the download" fi WANT=$(grep " ${ASSET}\$" "${TMP}/SHA256SUMS" | cut -d' ' -f1) [ -n "$WANT" ] || die "${ASSET} is not listed in SHA256SUMS" [ "$SUM" = "$WANT" ] || die "checksum mismatch for ${ASSET}: got ${SUM}, expected ${WANT}" say "checksum ok" else die "could not fetch SHA256SUMS for v${VERSION}" fi tar -xzf "${TMP}/${ASSET}" -C "$TMP" [ -f "${TMP}/llamay" ] || die "the archive does not contain a llamay binary" maybe_sudo "$(dirname "$INSTALL_DIR")" $SUDO mkdir -p "$INSTALL_DIR" maybe_sudo "$INSTALL_DIR" $SUDO install -m 0755 "${TMP}/llamay" "${INSTALL_DIR}/llamay" say "installed ${INSTALL_DIR}/llamay" # The binary answers to a second name, and the name deployment tooling looks # for. It is a symlink rather than a copy so an upgrade moves both at once. $SUDO ln -sf "${INSTALL_DIR}/llamay" "${INSTALL_DIR}/llamay-server" 2>/dev/null || true if [ "${LLAMAY_NO_SERVICE:-0}" = 1 ]; then say "" say "run: llamay serve" exit 0 fi install_systemd() { # A system service needs an account that is not a person's. It gets a home # because the model store lives under it, and no shell because nothing # should be able to log in as it. if ! id llamay >/dev/null 2>&1; then $SUDO useradd -r -m -d /var/lib/llamay -s /usr/sbin/nologin llamay 2>/dev/null || $SUDO useradd -r -m -d /var/lib/llamay -s /bin/false llamay 2>/dev/null || true fi $SUDO mkdir -p /var/lib/llamay $SUDO chown -R llamay:llamay /var/lib/llamay 2>/dev/null || true $SUDO tee /etc/systemd/system/llamay.service >/dev/null <<-UNIT [Unit] Description=llamay inference server After=network-online.target Wants=network-online.target [Service] ExecStart=${INSTALL_DIR}/llamay serve User=llamay Group=llamay Restart=always RestartSec=3 Environment=LLAMAY_MODELS=/var/lib/llamay/models # It listens on loopback by default and reads model files. Nothing here # needs to write outside its own state directory or see another user's # processes, so it is told it may not. NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/lib/llamay [Install] WantedBy=multi-user.target UNIT $SUDO systemctl daemon-reload $SUDO systemctl enable --now llamay >/dev/null 2>&1 || { warn "the service was installed but did not start; run: systemctl status llamay" return } say "service running — systemctl status llamay" } install_launchd() { # A LaunchAgent rather than a LaunchDaemon: it runs as the user who # installed it, so the model store is the one their CLI already uses, and # it needs no root to load. AGENTS="${HOME}/Library/LaunchAgents" PLIST="${AGENTS}/com.llamay.server.plist" mkdir -p "$AGENTS" cat >"$PLIST" <<-PLIST Labelcom.llamay.server ProgramArguments ${INSTALL_DIR}/llamay serve RunAtLoad KeepAlive StandardOutPath${HOME}/Library/Logs/llamay.log StandardErrorPath${HOME}/Library/Logs/llamay.log PLIST # bootout before bootstrap, so re-running the installer replaces the agent # rather than failing on one that is already loaded. launchctl bootout "gui/$(id -u)/com.llamay.server" 2>/dev/null || true if launchctl bootstrap "gui/$(id -u)" "$PLIST" 2>/dev/null; then say "service running — launchctl print gui/$(id -u)/com.llamay.server" else warn "the agent was written to ${PLIST} but did not load; run: launchctl bootstrap gui/$(id -u) ${PLIST}" fi } if [ "$OS" = linux ]; then if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then install_systemd else warn "no systemd here, so no service was installed" say "run: llamay serve" fi else install_launchd fi say "" say "llamay $(${INSTALL_DIR}/llamay version -short 2>/dev/null || echo "$VERSION") is installed" say " llamay pull hf:Qwen/Qwen2.5-0.5B-Instruct-GGUF/qwen2.5-0.5b-instruct-q4_k_m.gguf" say " llamay run -m qwen2.5-0.5b-instruct:q4_k_m -p 'hello'" say " the server is on http://127.0.0.1:11435"