Files
featherChat/warzone/scripts/build-linux.sh
Siavash Sameni b9e7b3e05c fix: arch linux uses rustup for wasm target support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 09:12:09 +04:00

609 lines
18 KiB
Bash
Executable File

#!/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 <user@host> [--federation <config.json>]"
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 <host>: Stop service, upload binaries, restart
# ---------------------------------------------------------------------------
do_update() {
local host="${1:-}"
if [ -z "$host" ]; then
echo "Usage: $0 --update <user@host>"
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 <host>: 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 <command> [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 <user@host> Stop service, upload binaries, restart"
echo " --update-all Update mequ + kh3rad3ree"
echo " --deploy <user@host> 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