#!/bin/sh # ============================================================================= # xerj installer · curl -fsSL https://xerj.org/get | sh # ============================================================================= # Detects your OS + CPU, downloads the matching release binary from # github.com/xerj-org/xerj/releases, verifies its SHA-256, and installs a # single `xerj` binary. No JVM, no dependencies — one static binary. # # Environment overrides: # XERJ_VERSION=v0.1.0 install a specific tag (default: latest release) # XERJ_INSTALL_DIR=/path install location (default: ~/.local/bin, else ./ ) # XERJ_REPO=owner/name source repo (default: xerj-org/xerj) # XERJ_LIBC=gnu|musl Linux C library (default: musl — see below) # XERJ_INSECURE_SKIP_CHECKSUM=1 # install without verifying SHA-256. Only useful on a # box with no hashing tool at all; you are choosing to # run an unverified binary. # ============================================================================= set -eu REPO="${XERJ_REPO:-xerj-org/xerj}" BOLD=''; DIM=''; RED=''; GRN=''; YEL=''; RST='' if [ -t 1 ]; then BOLD=$(printf '\033[1m'); DIM=$(printf '\033[2m'); RED=$(printf '\033[31m') GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m'); RST=$(printf '\033[0m') fi say() { printf '%s\n' "$*"; } info() { printf '%s==>%s %s\n' "$YEL" "$RST" "$*"; } ok() { printf '%s ok %s %s\n' "$GRN" "$RST" "$*"; } die() { printf '%serror:%s %s\n' "$RED" "$RST" "$*" >&2; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || die "'$1' is required but not found in PATH"; } need uname; need tar # --- pick a downloader ------------------------------------------------------ if command -v curl >/dev/null 2>&1; then DL() { curl -fsSL "$1" -o "$2"; } DLS() { curl -fsSL "$1"; } elif command -v wget >/dev/null 2>&1; then DL() { wget -qO "$2" "$1"; } DLS() { wget -qO - "$1"; } else die "need curl or wget" fi # --- detect target triple --------------------------------------------------- # Supported matrix (8 release targets): # Linux x86_64 / aarch64 (static musl build - any distro incl. Alpine) # macOS x86_64 / arm64 (Apple Silicon) # Windows x64 / ARM64 (via Git Bash / MSYS here; native PowerShell: # powershell -ExecutionPolicy Bypass -c "irm https://xerj.org/get.ps1 | iex") os=$(uname -s); arch=$(uname -m) # Linux defaults to the fully-static musl build. That is a deliberate choice, # not a fallback: # * it runs on any distro including Alpine, with no glibc-version floor — # a gnu binary linked against a newer glibc dies at exec on an older host # ("GLIBC_2.xx not found"), which is an install failure we cannot see; # * the usual objection to musl, its slow malloc, does not apply: xerj links # jemalloc as the global allocator on every non-MSVC target # (engine/crates/xerj-server/Cargo.toml). # The gnu artifacts are still published and still supported — ask for them # with XERJ_LIBC=gnu. (Before 2026-08-08 an unconditional override below made # gnu unreachable from this script while the case arm still claimed gnu; the # comment and the behaviour now agree.) case "$os" in Linux) plat="unknown-linux-musl"; ext="tar.gz"; bin="xerj" ;; Darwin) plat="apple-darwin"; ext="tar.gz"; bin="xerj" ;; MINGW*|MSYS*|CYGWIN*) plat="pc-windows-msvc"; ext="zip"; bin="xerj.exe" ;; *) die "unsupported OS: $os (xerj ships Linux, macOS and Windows builds)" ;; esac case "$arch" in x86_64|amd64) cpu="x86_64" ;; aarch64|arm64) cpu="aarch64" ;; *) die "unsupported CPU: $arch (xerj ships x86_64/amd64 and aarch64/arm64 builds for Linux, macOS and Windows)" ;; esac if [ "$os" = "Linux" ]; then case "${XERJ_LIBC:-musl}" in musl) ;; # the default set above gnu) plat="unknown-linux-gnu" ;; *) die "XERJ_LIBC must be 'gnu' or 'musl' (got '${XERJ_LIBC}')" ;; esac elif [ -n "${XERJ_LIBC:-}" ]; then say "${YEL}note:${RST} XERJ_LIBC only applies to Linux — ignoring it on ${os}" fi target="${cpu}-${plat}" # --- resolve version -------------------------------------------------------- if [ "${XERJ_VERSION:-}" = "" ]; then info "resolving latest release of ${REPO}…" # Follow the /releases/latest redirect to read the tag without jq. tag=$(DLS "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null \ | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1) [ -n "$tag" ] || die "could not resolve latest release — set XERJ_VERSION=vX.Y.Z (see https://github.com/${REPO}/releases)" else tag="$XERJ_VERSION" # Release tags are v-prefixed; accept XERJ_VERSION=1.0.0 too. case "$tag" in v*) ;; *) tag="v$tag" ;; esac fi ver="${tag#v}" info "installing xerj ${BOLD}${tag}${RST} for ${BOLD}${target}${RST}" stage="xerj-${ver}-${target}" asset="${stage}.${ext}" base="https://github.com/${REPO}/releases/download/${tag}" # The X-template form works on GNU, BSD/macOS, and busybox mktemp alike. tmp=$(mktemp -d 2>/dev/null || mktemp -d "${TMPDIR:-/tmp}/xerj.XXXXXX") trap 'rm -rf "$tmp"' EXIT INT TERM info "downloading ${asset}…" DL "${base}/${asset}" "${tmp}/${asset}" \ || die "download failed for ${target}. Available assets: https://github.com/${REPO}/releases/tag/${tag}" # --- verify checksum (fail closed — every release publishes the .sha256) ---- # # DO NOT REORDER THE NEXT LINE BELOW THE TOOL SEARCH. The checksum is fetched # unconditionally, before we look for a hashing tool, so every installer run # downloads exactly one binary and exactly one matching .sha256. That 1:1 # pairing is the ONLY fingerprint that lets GitHub's per-asset download counts # be read as installer runs (see metrics/README.md); checking for the tool # first would skip the fetch on some machines and silently destroy the only # install metric this project has. DL "${base}/${asset}.sha256" "${tmp}/${asset}.sha256" 2>/dev/null \ || die "could not download checksum ${asset}.sha256 — refusing to install an unverified binary" want=$(awk '{print $1}' "${tmp}/${asset}.sha256" | tr 'A-F' 'a-f') [ "${#want}" -eq 64 ] || die "malformed ${asset}.sha256 (expected 64 hex chars) — refusing to install an unverified binary" case "$want" in *[!0-9a-f]*) die "malformed ${asset}.sha256 (not hex) — refusing to install an unverified binary" ;; esac # Try hard to find a SHA-256 implementation before giving up: coreutils, # perl-shasum, openssl, busybox, and the BSD/macOS spellings. got='' if command -v sha256sum >/dev/null 2>&1; then got=$(sha256sum "${tmp}/${asset}" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then got=$(shasum -a 256 "${tmp}/${asset}" | awk '{print $1}') elif command -v openssl >/dev/null 2>&1; then got=$(openssl dgst -sha256 "${tmp}/${asset}" | awk '{print $NF}') elif command -v sha256 >/dev/null 2>&1; then got=$(sha256 -q "${tmp}/${asset}" | awk '{print $NF}') elif command -v busybox >/dev/null 2>&1; then got=$(busybox sha256sum "${tmp}/${asset}" | awk '{print $1}') elif command -v cksum >/dev/null 2>&1; then got=$(cksum -a sha256 "${tmp}/${asset}" 2>/dev/null | awk '{print $NF}') fi got=$(printf '%s' "$got" | tr 'A-F' 'a-f') # Fail CLOSED. Until 2026-08-08 this branch printed a warning and installed # anyway, which is not what "refusing to install an unverified binary" three # lines up promises. If a machine genuinely has no hashing tool, the user can # still opt in explicitly — but it has to be their decision, not ours. if [ -z "$got" ]; then if [ "${XERJ_INSECURE_SKIP_CHECKSUM:-}" = "1" ]; then say "${YEL}warning:${RST} no SHA-256 tool found and XERJ_INSECURE_SKIP_CHECKSUM=1 —" say "${YEL} ${RST} installing an ${BOLD}unverified${RST} binary at your request (expected ${want})" else die "no SHA-256 tool found (tried sha256sum, shasum, openssl, sha256, busybox, cksum) — refusing to install an unverified binary. Install coreutils or openssl and re-run, or verify by hand: expected sha256: ${want} source: ${base}/${asset} To install anyway (not recommended): XERJ_INSECURE_SKIP_CHECKSUM=1" fi elif [ "$got" = "$want" ]; then ok "sha256 verified" else die "sha256 mismatch (want $want, got $got)" fi # --- extract ---------------------------------------------------------------- info "extracting…" if [ "$ext" = "zip" ]; then # Git Bash / MSYS often lack `unzip` — fall back to the PowerShell that # ships with every Windows box. Paths travel via environment variables # (never interpolated into the PS command) and -LiteralPath, so spaces, # apostrophes and [brackets] in %TEMP% can't break quoting or glob. if command -v unzip >/dev/null 2>&1; then unzip -q "${tmp}/${asset}" -d "$tmp" elif command -v powershell.exe >/dev/null 2>&1; then zip_w=$(cygpath -w "${tmp}/${asset}" 2>/dev/null || printf '%s' "${tmp}/${asset}") dst_w=$(cygpath -w "$tmp" 2>/dev/null || printf '%s' "$tmp") XERJ_ZIP="$zip_w" XERJ_DST="$dst_w" powershell.exe -NoProfile -NonInteractive -Command \ 'Expand-Archive -LiteralPath $env:XERJ_ZIP -DestinationPath $env:XERJ_DST -Force' \ || die "failed to extract ${asset} (install 'unzip', or use the native installer: powershell -ExecutionPolicy Bypass -c \"irm https://xerj.org/get.ps1 | iex\")" else die "need 'unzip' to extract ${asset} — or use the native installer: powershell -ExecutionPolicy Bypass -c \"irm https://xerj.org/get.ps1 | iex\"" fi else tar -xzf "${tmp}/${asset}" -C "$tmp" fi src="${tmp}/${stage}/${bin}" [ -f "$src" ] || src=$(find "$tmp" -name "$bin" -type f | head -n1) [ -f "$src" ] || die "binary '$bin' not found in archive" chmod +x "$src" 2>/dev/null || true # --- choose install dir ----------------------------------------------------- # Re-testing -w AFTER the mkdir matters: mkdir -p on an EXISTING but # root-owned ~/.local/bin returns 0, and we must fall through to $(pwd) # instead of failing later at the copy. if [ "${XERJ_INSTALL_DIR:-}" != "" ]; then dir="$XERJ_INSTALL_DIR" elif [ -n "${HOME:-}" ] \ && { [ -d "$HOME/.local/bin" ] || mkdir -p "$HOME/.local/bin" 2>/dev/null; } \ && [ -w "$HOME/.local/bin" ]; then dir="$HOME/.local/bin" else dir="$(pwd)"; fi mkdir -p "$dir" 2>/dev/null || die "cannot create install dir: $dir" # Stage inside the destination directory, then rename: same-directory mv is # an atomic rename(2), so $dest is always either the old or the new binary — # never a truncated copy (mv from tmpfs /tmp is a non-atomic cross-device # copy that Ctrl-C or ENOSPC could otherwise tear mid-write). dest="${dir}/${bin}" tmpdest="${dir}/.${bin}.tmp.$$" if cp "$src" "$tmpdest" && mv -f "$tmpdest" "$dest"; then ok "installed ${BOLD}${dest}${RST}" else rm -f "$tmpdest" die "failed to install to $dest" fi # --- next steps ------------------------------------------------------------- say "" say "${BOLD}xerj ${tag}${RST} is ready." case ":${PATH}:" in *":${dir}:"*) say " ${DIM}start it:${RST} xerj --insecure --data-dir ./data" ;; *) say " ${DIM}${dir} is not on your PATH — either add it:${RST}" say " export PATH=\"${dir}:\$PATH\"" say " ${DIM}or run it directly:${RST} ${dest} --insecure --data-dir ./data" ;; esac say "" say " ${DIM}then open the console:${RST} http://localhost:9200/_xerj-console/" say " ${DIM}docs:${RST} https://xerj.org/docs/ ${DIM}·${RST} ${DIM}source:${RST} https://github.com/${REPO}"