#!/usr/bin/env bash # curl -fsSL https://downloads.querybench.app/install.sh | bash # Optional QUERYBENCH_INSTALL_DIR changes the application parent directory. # A license key is entered in QueryBench after installation, never in this command. set -euo pipefail # Bash can unwind main's local variables before invoking EXIT after errexit. # Keep cleanup state at script scope so rollback works on both Bash 3 and 5. install_work='' install_staging='' install_backup='' install_mounted='' install_destination='' fail() { printf 'QueryBench: %s\n' "$*" >&2; exit 1; } log() { printf 'QueryBench: %s\n' "$*"; } download() { local status status=$(curl --fail --silent --show-error --proto '=https' --tlsv1.2 \ --connect-timeout 15 --max-time 600 --retry 2 --output "$2" --write-out '%{http_code}' "$1") || fail "Download failed: $1" [[ "$status" == 200 ]] || fail "Expected HTTP 200 from $1; received $status" } json_value() { if [[ "$os" == Darwin ]]; then /usr/bin/plutil -extract "$1" raw -o - "$install_work/latest.json" else python3 - "$install_work/latest.json" "$1" <<'PY' import json, sys try: value = json.load(open(sys.argv[1])) for key in sys.argv[2].split('.'): value = value[key] if isinstance(value, (dict, list)) or value is None: raise ValueError('Missing value') print(value) except (KeyError, ValueError, TypeError): sys.exit('QueryBench: no published installer is available for this platform yet.') PY fi } cleanup() { local result=$? if [[ -n "${install_mounted:-}" ]]; then hdiutil detach "$install_mounted" -quiet >/dev/null 2>&1 || true; fi # A failed replacement restores the previous installation. if [[ -n "${install_backup:-}" && -e "$install_backup" && ! -e "$install_destination" ]]; then if ! mv "$install_backup" "$install_destination"; then printf 'QueryBench: previous installation preserved at %s; restore it manually.\n' "$install_backup" >&2 install_staging='' fi fi [[ -z "${install_staging:-}" ]] || rm -rf "$install_staging" [[ -z "${install_work:-}" ]] || rm -rf "$install_work" return "$result" } main() { local os arch target url expected size version actual parent os=$(uname -s) case "$os" in MINGW*|MSYS*|CYGWIN*) command -v powershell.exe >/dev/null || fail 'Run the Windows installer from PowerShell: irm https://downloads.querybench.app/install.ps1 | iex' powershell.exe -NoProfile -Command 'Invoke-RestMethod https://downloads.querybench.app/install.ps1 | Invoke-Expression' return ;; Darwin|Linux) ;; *) fail "Unsupported operating system: $os" ;; esac [[ $(id -u) != 0 ]] || fail 'Run this as your regular desktop user, without sudo.' command -v curl >/dev/null || fail 'curl is required.' command -v pgrep >/dev/null || fail 'pgrep is required (procps on Linux).' if pgrep -x querybench >/dev/null; then fail 'Save your work and quit QueryBench, then rerun this command.'; fi arch=$(uname -m) case "$arch" in x86_64|amd64) arch=amd64 ;; arm64|aarch64) arch=arm64 ;; *) fail "Unsupported architecture: $arch" ;; esac if [[ "$os" == Linux ]]; then [[ "$arch" == amd64 ]] || fail 'The Linux release currently supports x86_64 only.' command -v python3 >/dev/null || fail 'Install python3 and rerun this command.' command -v ldd >/dev/null || fail 'This build requires glibc 2.39+, GTK 4.14+, and WebKitGTK 6.' python3 - <<'PY' import platform, sys libc, version = platform.libc_ver() if libc != 'glibc' or tuple(map(int, version.split('.')[:2])) < (2, 39): sys.exit('QueryBench requires glibc 2.39 or newer (Ubuntu 24.04+).') PY target="linux/$arch" parent="${QUERYBENCH_INSTALL_DIR:-$HOME/.local/share/querybench}" install_destination="$parent/QueryBench" else [[ $(sw_vers -productVersion | cut -d . -f 1) -ge 12 ]] || fail 'macOS 12 or newer is required.' target="darwin/$arch" parent="${QUERYBENCH_INSTALL_DIR:-$HOME/Applications}" install_destination="$parent/QueryBench.app" fi [[ "$parent" == /* && "$parent" != *$'\n'* && "$parent" != *$'\r'* ]] || fail 'QUERYBENCH_INSTALL_DIR must be an absolute path without line breaks.' [[ ! -L "$install_destination" ]] || fail "Installation target is a symlink: $install_destination" if [[ "$os" == Linux && -d "$HOME/.local/bin/querybench" ]]; then fail 'The launcher path ~/.local/bin/querybench is an existing directory.' fi install_work=$(mktemp -d) install_staging='' install_backup='' install_mounted='' trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM download 'https://downloads.querybench.app/latest.json' "$install_work/latest.json" [[ $(json_value schema) == 1 ]] || fail 'Unsupported release manifest.' [[ $(json_value publishedAt) =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T ]] || fail 'Release has not been published.' url=$(json_value "assets.$target.url") || fail "No published release for $target." expected=$(json_value "assets.$target.sha256") size=$(json_value "assets.$target.size") version=$(json_value "assets.$target.version") [[ "$url" =~ ^https://downloads\.querybench\.app/[0-9]+\.[0-9]+\.[0-9]+-[0-9]+/desktop/[A-Za-z0-9._-]+$ ]] || fail 'Invalid installer URL.' [[ "$expected" =~ ^[a-f0-9]{64}$ && "$size" =~ ^[1-9][0-9]*$ && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail 'Invalid release metadata.' log "Downloading $version for $target" download "$url" "$install_work/package" [[ $(wc -c < "$install_work/package" | tr -d '[:space:]') == "$size" ]] || fail 'Installer size mismatch; nothing installed.' if command -v sha256sum >/dev/null; then actual=$(sha256sum "$install_work/package" | cut -d ' ' -f 1) else actual=$(shasum -a 256 "$install_work/package" | cut -d ' ' -f 1) fi [[ "$actual" == "$expected" ]] || fail 'Installer checksum mismatch; nothing installed.' mkdir -p "$parent" install_staging=$(mktemp -d "$parent/.querybench-install.XXXXXX") if [[ "$os" == Darwin ]]; then hdiutil attach "$install_work/package" -readonly -nobrowse -mountpoint "$install_work/mount" -quiet install_mounted="$install_work/mount" [[ -d "$install_mounted/QueryBench.app" && ! -L "$install_mounted/QueryBench.app" ]] || fail 'The disk image does not contain QueryBench.app.' codesign --verify --deep --strict "$install_mounted/QueryBench.app" spctl --assess --type execute "$install_mounted/QueryBench.app" ditto "$install_mounted/QueryBench.app" "$install_staging/QueryBench.app" hdiutil detach "$install_mounted" -quiet install_mounted='' else python3 - "$install_work/package" "$install_staging" <<'PY' import pathlib, shutil, sys, tarfile expected = {'QueryBench/querybench', 'QueryBench/querybench.png', 'QueryBench/querybench.desktop', 'QueryBench/README.txt'} with tarfile.open(sys.argv[1], 'r:gz') as archive: members = archive.getmembers() if len(members) != len(expected) or {m.name for m in members} != expected or any(not m.isfile() for m in members): sys.exit('QueryBench: unexpected archive contents; nothing installed.') root = pathlib.Path(sys.argv[2]) (root/'QueryBench').mkdir() for member in members: path = root/member.name with archive.extractfile(member) as source, path.open('xb') as destination: shutil.copyfileobj(source, destination) path.chmod(0o755 if member.name.endswith('/querybench') else 0o644) PY local dependencies dependencies=$(ldd "$install_staging/QueryBench/querybench") || fail 'System libraries are incompatible with this Linux build.' if [[ "$dependencies" == *'not found'* ]]; then fail 'Missing system libraries. On Ubuntu 24.04+: sudo apt install libgtk-4-1 libwebkitgtk-6.0-4' fi fi install_backup="$install_staging/previous" if [[ -e "$install_destination" ]]; then mv "$install_destination" "$install_backup"; fi mv "$install_staging/$(basename "$install_destination")" "$install_destination" # The app is installed. Desktop registration only touches user-owned files. if [[ "$os" == Linux ]]; then mkdir -p "$HOME/.local/bin" "$HOME/.local/share/applications" ln -sfn "$install_destination/querybench" "$HOME/.local/bin/querybench" python3 - "$install_destination" "$HOME/.local/share/applications/app.querybench.desktop.desktop" <<'PY' import pathlib, sys root = pathlib.Path(sys.argv[1]) binary = str(root/'querybench').replace('%', '%%') for char in ('\\', '"', '`', '$'): binary = binary.replace(char, '\\'+char) pathlib.Path(sys.argv[2]).write_text('[Desktop Entry]\nType=Application\nName=QueryBench\nExec="'+binary+'"\nIcon='+str(root/'querybench.png')+'\nTerminal=false\nCategories=Development;Database;\n') PY log "Installed $version. Open QueryBench from your app menu or run $HOME/.local/bin/querybench." else log "Installed $version at $install_destination. Open QueryBench from Finder." fi log 'Activate your license inside the app, or start using it for free.' cleanup trap - EXIT } main "$@"