#!/usr/bin/env bash set -euo pipefail # Build featherChat Linux x86_64 release binaries using a Hetzner Cloud VPS. # Prerequisites: hcloud CLI authenticated, SSH key "wz" registered. # # Usage: # ./scripts/build-linux.sh --prepare Create VM, install deps, upload source # ./scripts/build-linux.sh --build Build release binaries on the VM # ./scripts/build-linux.sh --transfer Download binaries from VM to local # ./scripts/build-linux.sh --destroy Delete the VM # ./scripts/build-linux.sh --all Run prepare + build + transfer (no destroy) # ./scripts/build-linux.sh --upload Re-upload source to existing VM # # The VM persists between steps so you can iterate on build errors. # Reuses the same WZP builder VM if it already exists. VM_NAME="fc-builder" SSH_KEY_NAME="wz" SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp" SERVER_TYPE="cx33" IMAGE="debian-12" REMOTE_USER="root" OUTPUT_DIR="target/linux-x86_64" PROJECT_DIR="/Users/manwe/CascadeProjects/featherChat/warzone" SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10" # Binaries to build BINS="warzone-server warzone-client" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- get_vm_ip() { local ip ip=$(hcloud server list -o columns=name,ipv4 -o noheader 2>/dev/null | grep "$VM_NAME" | awk '{print $2}' | tr -d ' ') if [ -z "$ip" ]; then echo "ERROR: No VM '$VM_NAME' found. Run --prepare first." >&2 exit 1 fi echo "$ip" } ssh_cmd() { local ip ip=$(get_vm_ip) ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "$@" } scp_to() { local ip ip=$(get_vm_ip) scp $SSH_OPTS -i "$SSH_KEY_PATH" "$@" "$REMOTE_USER@$ip:/root/" 2>/dev/null } scp_from() { local ip ip=$(get_vm_ip) # args: remote_path local_path scp $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip:$1" "$2" 2>/dev/null } # --------------------------------------------------------------------------- # --prepare: Create VM, install deps, upload source # --------------------------------------------------------------------------- do_prepare() { # Check if VM already exists local existing existing=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$VM_NAME" || true) if [ -n "$existing" ]; then echo "VM already exists: $existing" echo "Reusing it. Uploading fresh source..." do_upload return fi echo "[1/5] Creating Hetzner VM: $VM_NAME ($SERVER_TYPE, $IMAGE)..." hcloud server create \ --name "$VM_NAME" \ --type "$SERVER_TYPE" \ --image "$IMAGE" \ --ssh-key "$SSH_KEY_NAME" \ --location fsn1 \ --quiet local ip ip=$(get_vm_ip) echo " VM: $VM_NAME @ $ip" # Wait for SSH echo "[2/5] Waiting for SSH..." for i in $(seq 1 30); do if ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "echo ok" &>/dev/null; then break fi sleep 2 done # Install build dependencies echo "[3/5] Installing build dependencies..." ssh_cmd "apt-get update -qq && apt-get install -y -qq \ build-essential \ pkg-config \ libssl-dev \ curl \ git \ > /dev/null 2>&1" # Install Rust + wasm-pack echo "[4/5] Installing Rust + wasm-pack..." ssh_cmd "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable > /dev/null 2>&1" ssh_cmd "source ~/.cargo/env && rustup target add wasm32-unknown-unknown > /dev/null 2>&1" ssh_cmd "source ~/.cargo/env && cargo install wasm-pack > /dev/null 2>&1 || true" # Upload source echo "[5/5] Uploading source code..." do_upload echo "" echo "=== VM Ready ===" echo "IP: $ip" echo "SSH: ssh -i $SSH_KEY_PATH root@$ip" echo "" echo "Next: ./scripts/build-linux.sh --build" } do_upload() { echo " Creating source tarball..." # Create tarball excluding build artifacts and non-essential files tar czf /tmp/fc-src.tar.gz \ --exclude='target' \ --exclude='.git' \ --exclude='.claude' \ --exclude='warzone-phone' \ --exclude='notes' \ -C "$PROJECT_DIR" . 2>/dev/null local ip ip=$(get_vm_ip) local size size=$(du -h /tmp/fc-src.tar.gz | cut -f1) echo " Uploading $size to VM..." scp $SSH_OPTS -i "$SSH_KEY_PATH" /tmp/fc-src.tar.gz "$REMOTE_USER@$ip:/root/fc-src.tar.gz" 2>/dev/null ssh_cmd "rm -rf /root/featherChat && mkdir -p /root/featherChat && tar xzf /root/fc-src.tar.gz -C /root/featherChat" 2>/dev/null rm -f /tmp/fc-src.tar.gz echo " Source uploaded." } # --------------------------------------------------------------------------- # --build: Build release binaries on the VM # --------------------------------------------------------------------------- do_build() { local ip ip=$(get_vm_ip) echo "=== Building on $ip ===" local bin_args="" for bin in $BINS; do bin_args="$bin_args --bin $bin" done echo "[1/3] Building WASM (warzone-wasm)..." ssh_cmd "source ~/.cargo/env && cd /root/featherChat && wasm-pack build crates/warzone-wasm --target web --out-dir ../../wasm-pkg 2>&1" | tail -5 echo "" echo "[2/3] Building: $BINS" ssh_cmd "source ~/.cargo/env && cd /root/featherChat && cargo build --release $bin_args 2>&1" echo "" echo "[3/3] Verifying binaries..." for bin in $BINS; do ssh_cmd "ls -lh /root/featherChat/target/release/$bin" 2>/dev/null done echo "" echo "=== Build Complete ===" echo "Next: ./scripts/build-linux.sh --transfer" } # --------------------------------------------------------------------------- # --transfer: Download binaries from VM to local # --------------------------------------------------------------------------- do_transfer() { local ip ip=$(get_vm_ip) echo "=== Downloading binaries from $ip ===" mkdir -p "$OUTPUT_DIR" for bin in $BINS; do echo " $bin..." scp_from "/root/featherChat/target/release/$bin" "$OUTPUT_DIR/$bin" done # Also grab the embedded web client HTML if it exists if ssh_cmd "test -f /root/featherChat/target/release/warzone-server" 2>/dev/null; then echo " federation.example.json..." scp $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip:/root/featherChat/federation.example.json" "$OUTPUT_DIR/federation.example.json" 2>/dev/null || true fi echo "" echo "=== Transfer Complete ===" ls -lh "$OUTPUT_DIR"/warzone-* echo "" echo "Deploy with:" echo " scp $OUTPUT_DIR/warzone-server $OUTPUT_DIR/warzone-client user@mequ:~/warzone/" echo "" echo "Run on server:" echo " ./warzone-server --bind 0.0.0.0:7700" echo " ./warzone-server --bind 0.0.0.0:7700 --federation federation.json" } # --------------------------------------------------------------------------- # --destroy: Delete the VM # --------------------------------------------------------------------------- do_destroy() { local existing existing=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$VM_NAME" || true) if [ -z "$existing" ]; then echo "No VM '$VM_NAME' to destroy." return fi echo "Deleting VM: $VM_NAME" hcloud server delete "$VM_NAME" echo "Done." } # --------------------------------------------------------------------------- # --deploy: Transfer + deploy to production server # --------------------------------------------------------------------------- do_deploy() { local deploy_host="${2:-}" if [ -z "$deploy_host" ]; then echo "Usage: $0 --deploy [--federation ]" echo "" echo "Example:" echo " $0 --deploy root@mequ.example.com" echo " $0 --deploy root@mequ.example.com --federation federation.json" exit 1 fi echo "=== Deploying to $deploy_host ===" # Ensure binaries exist locally if [ ! -f "$OUTPUT_DIR/warzone-server" ]; then echo "ERROR: No binaries in $OUTPUT_DIR. Run --build and --transfer first." exit 1 fi echo "[1/3] Uploading binaries..." scp "$OUTPUT_DIR/warzone-server" "$OUTPUT_DIR/warzone-client" "$deploy_host:~/warzone/" # Upload federation config if specified local fed_arg="" if [ "${3:-}" = "--federation" ] && [ -n "${4:-}" ]; then echo "[2/3] Uploading federation config..." scp "$4" "$deploy_host:~/warzone/federation.json" fed_arg="--federation ~/warzone/federation.json" else echo "[2/3] No federation config (standalone mode)" fi echo "[3/3] Restarting server..." ssh "$deploy_host" "pkill warzone-server || true; sleep 1; cd ~/warzone && nohup ./warzone-server --bind 0.0.0.0:7700 $fed_arg > server.log 2>&1 &" echo "" echo "=== Deployed ===" echo "Server running at $deploy_host:7700" echo "Logs: ssh $deploy_host 'tail -f ~/warzone/server.log'" } # --------------------------------------------------------------------------- # Production servers # --------------------------------------------------------------------------- PROD_SERVERS=( "root@mequ" "root@kh3rad3ree" ) PROD_SERVICE="warzone-server" PROD_BIN_DIR="/home/warzone" # --------------------------------------------------------------------------- # --update : Stop service, upload binaries, restart # --------------------------------------------------------------------------- do_update() { local host="${1:-}" if [ -z "$host" ]; then echo "Usage: $0 --update " echo " or: $0 --update-all" exit 1 fi if [ ! -f "$OUTPUT_DIR/warzone-server" ]; then echo "ERROR: No binaries in $OUTPUT_DIR. Run --all first." exit 1 fi echo "=== Updating $host ===" echo "[1/4] Stopping service..." ssh "$host" "systemctl stop $PROD_SERVICE 2>/dev/null || true" echo "[2/4] Uploading binaries..." scp "$OUTPUT_DIR/warzone-server" "$OUTPUT_DIR/warzone-client" "$host:$PROD_BIN_DIR/" ssh "$host" "chmod +x $PROD_BIN_DIR/warzone-server $PROD_BIN_DIR/warzone-client" echo "[3/4] Starting service..." ssh "$host" "systemctl start $PROD_SERVICE" echo "[4/4] Verifying..." sleep 1 local status status=$(ssh "$host" "systemctl is-active $PROD_SERVICE 2>/dev/null" || true) if [ "$status" = "active" ]; then echo " $host: $PROD_SERVICE is running" else echo " WARNING: $host: $PROD_SERVICE status = $status" echo " Check logs: ssh $host 'journalctl -u $PROD_SERVICE -n 20'" fi echo "" } # --------------------------------------------------------------------------- # --update-all: Update all production servers # --------------------------------------------------------------------------- do_update_all() { if [ ! -f "$OUTPUT_DIR/warzone-server" ]; then echo "ERROR: No binaries in $OUTPUT_DIR. Run --all first." exit 1 fi echo "=== Updating all production servers ===" echo "" for host in "${PROD_SERVERS[@]}"; do do_update "$host" done echo "=== All servers updated ===" } # --------------------------------------------------------------------------- # --status: Check service status on all production servers # --------------------------------------------------------------------------- do_status() { echo "=== Production server status ===" for host in "${PROD_SERVERS[@]}"; do local status status=$(ssh "$host" "systemctl is-active $PROD_SERVICE 2>/dev/null" || echo "unreachable") local uptime uptime=$(ssh "$host" "systemctl show $PROD_SERVICE --property=ActiveEnterTimestamp --value 2>/dev/null" || echo "?") printf " %-20s %s (since %s)\n" "$host" "$status" "$uptime" done echo "" # Check federation for host in "${PROD_SERVERS[@]}"; do local addr addr=$(echo "$host" | cut -d@ -f2) echo " Federation ($addr):" curl -s "http://$addr:7700/v1/federation/status" 2>/dev/null | python3 -m json.tool 2>/dev/null || echo " (unreachable)" echo "" done } # --------------------------------------------------------------------------- # --logs : Tail logs # --------------------------------------------------------------------------- do_logs() { local host="${1:-${PROD_SERVERS[0]}}" echo "=== Logs from $host ===" ssh "$host" "journalctl -u $PROD_SERVICE -f --no-pager" } # --------------------------------------------------------------------------- # --local: Build locally on this machine (auto-detect package manager) # --------------------------------------------------------------------------- detect_pkg_manager() { if command -v apt-get &>/dev/null; then echo "apt" elif command -v dnf &>/dev/null; then echo "dnf" elif command -v pacman &>/dev/null; then echo "pacman" elif command -v brew &>/dev/null; then echo "brew" else echo "unknown"; fi } do_local_deps() { local pm pm=$(detect_pkg_manager) echo "[1/4] Installing dependencies ($pm)..." case "$pm" in apt) sudo apt-get update -qq sudo apt-get install -y -qq build-essential pkg-config libssl-dev curl >/dev/null 2>&1 ;; dnf) sudo dnf install -y gcc gcc-c++ make pkg-config openssl-devel curl >/dev/null 2>&1 ;; pacman) sudo pacman -Sy --noconfirm base-devel pkg-config openssl curl rustup >/dev/null 2>&1 # Arch: ensure rustup manages the toolchain (pacman rust conflicts with rustup) if ! rustup show active-toolchain &>/dev/null; then rustup default stable 2>/dev/null || true fi ;; brew) brew install openssl pkg-config 2>/dev/null || true ;; *) echo "WARNING: Unknown package manager. Ensure build-essential, pkg-config, libssl-dev are installed." ;; esac # Ensure Rust is installed if ! command -v cargo &>/dev/null; then echo " Installing Rust..." curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable source "$HOME/.cargo/env" fi # Ensure wasm-pack is installed if ! command -v wasm-pack &>/dev/null; then echo " Installing wasm-pack..." cargo install wasm-pack 2>/dev/null || true fi # Ensure wasm target rustup target add wasm32-unknown-unknown 2>/dev/null || true } do_local_build() { # cd to project root (script may be run from scripts/ or project root) local script_dir script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local project_root="$(dirname "$script_dir")" cd "$project_root" echo " Project root: $(pwd)" local arch arch=$(uname -m) local os os=$(uname -s | tr '[:upper:]' '[:lower:]') local out_dir="target/${os}-${arch}" echo "=== Local Build (${os}-${arch}) ===" do_local_deps echo "[2/4] Building WASM..." wasm-pack build crates/warzone-wasm --target web --out-dir ../../wasm-pkg 2>&1 | tail -3 echo "[3/4] Building release binaries..." cargo build --release --bin warzone-server --bin warzone-client 2>&1 echo "[4/4] Copying to ${out_dir}..." mkdir -p "$out_dir" cp target/release/warzone-server target/release/warzone-client "$out_dir/" cp federation.example.json "$out_dir/" 2>/dev/null || true # Clean cargo cache if requested if [ "${CLEAN_CACHE:-}" = "1" ]; then echo " Cleaning build cache..." cargo clean 2>/dev/null || true fi echo "" echo "=== Local Build Complete ===" ls -lh "$out_dir"/warzone-* echo "" echo "Run:" echo " $out_dir/warzone-server --bind 0.0.0.0:7700" echo " $out_dir/warzone-client tui --server http://localhost:7700" } do_local_ship() { do_local_build echo "" do_update_all echo "" do_status echo "" echo "========================================" echo " LOCAL SHIP COMPLETE" echo "========================================" } # --------------------------------------------------------------------------- # --ship: Build + deploy to all servers + destroy VM (full pipeline) # --------------------------------------------------------------------------- do_ship() { echo "========================================" echo " SHIPPING featherChat to production" echo "========================================" echo "" do_prepare echo "" do_build echo "" do_transfer echo "" do_update_all echo "" do_destroy echo "" do_status echo "" echo "========================================" echo " SHIP COMPLETE" echo "========================================" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- case "${1:-}" in --prepare) do_prepare ;; --build) do_build ;; --transfer) do_transfer ;; --destroy) do_destroy ;; --deploy) do_deploy "$@" ;; --update) do_update "${2:-}" ;; --update-all) do_update_all ;; --status) do_status ;; --logs) do_logs "${2:-}" ;; --all) do_prepare do_build do_transfer echo "" echo "VM is still running. Destroy with: ./scripts/build-linux.sh --destroy" ;; --ship) do_ship ;; --local) do_local_build ;; --local-ship) do_local_ship ;; --local-clean) CLEAN_CACHE=1 do_local_build ;; --upload) do_upload ;; *) echo "Usage: $0 [args]" echo "" echo "Local build:" echo " --local Build locally (auto-detect OS, install deps)" echo " --local-ship Build locally + deploy to all servers" echo " --local-clean Build locally + clean cargo cache after" echo "" echo "Remote build (Hetzner VM):" echo " --ship Build on VM + deploy + destroy VM" echo " --prepare Create VM, install deps, upload source" echo " --build Build release binaries on VM" echo " --transfer Download binaries to $OUTPUT_DIR" echo " --destroy Delete the build VM" echo " --all prepare + build + transfer (VM persists)" echo " --upload Re-upload source to existing VM" echo "" echo "Deploy:" echo " --update Stop service, upload binaries, restart" echo " --update-all Update mequ + kh3rad3ree" echo " --deploy First-time deploy (upload + start)" echo "" echo "Monitor:" echo " --status Check service status on all servers" echo " --logs [user@host] Tail server logs (default: mequ)" exit 1 ;; esac