#!/bin/bash # # Biopb Tensor Server Installer # Usage: curl -fsSL https://biopb.org/install.sh | bash # # Idempotent: rerun to upgrade to latest version # # By default this installs prebuilt wheels from the latest GitHub release. # Set BIOPB_INSTALL_FROM_SOURCE=1 to instead build HEAD from a git checkout # (the fast path for development); that mode additionally needs git + a compiler. # # Requirements: curl, tar (+ git for BIOPB_INSTALL_FROM_SOURCE=1) # # ANSI colors — suppressed when stdout is not a terminal if [ -t 1 ]; then RED=$'\033[0;31m'; YELLOW=$'\033[0;33m'; GREEN=$'\033[0;32m' CYAN=$'\033[0;36m'; BOLD=$'\033[1m'; DIM=$'\033[2m'; RESET=$'\033[0m' else RED=''; YELLOW=''; GREEN=''; CYAN=''; BOLD=''; DIM=''; RESET='' fi _step() { printf "\n${BOLD}%s${RESET}\n" "$*"; } _ok() { printf " ${GREEN}%s${RESET}\n" "$*"; } _info() { printf " %s\n" "$*"; } _warn() { printf " ${YELLOW}WARNING:${RESET} %s\n" "$*"; } _err() { printf "${RED}ERROR:${RESET} %s\n" "$*" >&2; } _note() { printf " ${DIM}NOTE: %s${RESET}\n" "$*"; } _cmd() { printf " ${CYAN}%s${RESET}\n" "$*"; } # Yes/No prompt (default Yes). Usage: if _confirm "Question?"; then ...; fi # Reads from /dev/tty so it works when the script is piped in from curl. _confirm() { local reply printf " ${BOLD}%s${RESET} [Y/n]: " "$1" >/dev/tty read -r reply /dev/tty first=0 printf "\n %sOptional components:%s\n\n" "$BOLD" "$RESET" >/dev/tty for ((i=0; i/dev/tty done printf "\n ${DIM}Toggle [1-%d] or Enter to confirm:${RESET} " "$n" >/dev/tty local choice; read -r choice [keep] — writes result into caller's variable # (no subshell). All prompts go to /dev/tty. Requires PLATFORM to be set first. # # When the second arg is non-empty ("keep" mode), an extra "0) Keep my current # config file" option is shown as the *default*; choosing it (or Enter, or any # invalid input) returns the empty string as a sentinel meaning "don't touch the # existing config." Callers pass this when a biopb.toml already exists. _pick_data_dir() { # Caller passes the name of a variable to receive the result. We assign into # it with `printf -v` rather than a `local -n` nameref, because namerefs need # bash >= 4.3 and macOS ships bash 3.2. local _retvar_name=$1 local keep_mode="${2:-}" local candidates=() seen=() for dir in \ "$HOME" \ "$HOME/data" "$HOME/Data" \ "$HOME/microscopy" "$HOME/Microscopy" \ "$HOME/Documents" \ /mnt/data /data; do [ -d "$dir" ] || continue local real; real=$(realpath "$dir" 2>/dev/null || echo "$dir") local dup=0 for s in "${seen[@]+"${seen[@]}"}"; do [ "$s" = "$real" ] && dup=1 && break; done [ "$dup" = "0" ] && candidates+=("$dir") && seen+=("$real") done if [ "$PLATFORM" = "WSL" ]; then # On WSL, offer dedicated data subfolders under the Windows profile, but # NEVER the profile root itself. Recursively scanning the profile walks # AppData and, fatally, OneDrive "Files On-Demand" placeholders, which # hydrate-on-read through drvfs and hang discovery before the server can # bind. Data/Microscopy folders aren't OneDrive-redirected # by default the way Documents/Desktop/Pictures are; users who keep data # elsewhere can still type a /mnt/c/... path manually. local win_user; win_user=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r') if [ -n "$win_user" ]; then for dir in \ "/mnt/c/Users/$win_user/Microscopy" \ "/mnt/c/Users/$win_user/Data" \ "/mnt/c/Users/$win_user/data"; do [ -d "$dir" ] || continue local real; real=$(realpath "$dir" 2>/dev/null || echo "$dir") local dup=0 for s in "${seen[@]+"${seen[@]}"}"; do [ "$s" = "$real" ] && dup=1 && break; done [ "$dup" = "0" ] && candidates+=("$dir") && seen+=("$real") done fi fi local n=${#candidates[@]} local manual_opt=$((n + 1)) local default_dir="${candidates[0]:-$HOME}" printf "\n %sSelect your microscopy data directory:%s\n\n" "$BOLD" "$RESET" >/dev/tty if [ -n "$keep_mode" ]; then printf " ${CYAN}0)${RESET} Keep my current config file ${DIM}(default)${RESET}\n" >/dev/tty fi local i=1 for dir in "${candidates[@]+"${candidates[@]}"}"; do printf " ${CYAN}%d)${RESET} %s\n" "$i" "$dir" >/dev/tty i=$((i + 1)) done printf " ${CYAN}%d)${RESET} Enter path manually\n\n" "$manual_opt" >/dev/tty # Default differs by mode: keep current config (0) vs first candidate (1). local default_choice=1 [ -n "$keep_mode" ] && default_choice=0 printf " %sChoice [%s]:%s " "$DIM" "$default_choice" "$RESET" >/dev/tty local choice; read -r choice /dev/tty read -r manual /dev/tty printf -v "$_retvar_name" '%s' "" else printf " Invalid choice, using default\n" >/dev/tty printf -v "$_retvar_name" '%s' "$default_dir" fi } # Run a short inline Python script (reads the program from stdin, forwards args). # Python is guaranteed at this point — the installer is a Python toolchain bootstrapped # by uv — so we prefer a Python interpreter on PATH and fall back to uv's managed one # (`--no-project` so a stray pyproject.toml in the cwd doesn't trigger a project sync). _py() { if command -v python3 &>/dev/null; then python3 "$@" elif command -v python &>/dev/null; then python "$@" else uv run --no-project python "$@" fi } # Merge a biopb stdio MCP entry into JSON under top-level key # (e.g. "mcpServers" or "mcp").