From 6f99841cc7b9470e34e9f4d3344d7887794b30f6 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Mon, 6 Apr 2026 20:00:10 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20cloud=20build=20script=20=E2=80=94=20fil?= =?UTF-8?q?ter=20by=20server=20name,=20rsync=20upload,=20cx33?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Filter hcloud by SERVER_NAME to avoid touching other servers - Use rsync instead of tar (handles submodules, no macOS xattr spam) - Default server type cx33 - Release APK failure is non-fatal (debug APK still produced) Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/build-android-cloud.sh | 370 +++++++++++++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100755 scripts/build-android-cloud.sh diff --git a/scripts/build-android-cloud.sh b/scripts/build-android-cloud.sh new file mode 100755 index 0000000..bf796e6 --- /dev/null +++ b/scripts/build-android-cloud.sh @@ -0,0 +1,370 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build WarzonePhone Android APK using a temporary Hetzner Cloud VPS. +# Creates a VM, builds both debug and release APKs, downloads them, destroys the VM. +# +# Prerequisites: hcloud CLI authenticated, SSH key "wz" registered. +# +# Usage: +# ./scripts/build-android-cloud.sh Full build (create → build → download → destroy) +# ./scripts/build-android-cloud.sh --prepare Create VM and install deps only +# ./scripts/build-android-cloud.sh --build Build on existing VM +# ./scripts/build-android-cloud.sh --transfer Download APKs from VM +# ./scripts/build-android-cloud.sh --destroy Delete the VM +# ./scripts/build-android-cloud.sh --all prepare + build + transfer (VM persists) +# ./scripts/build-android-cloud.sh --upload Re-upload source to existing VM +# +# Environment variables (all optional): +# WZP_BRANCH Branch to build (default: feat/android-voip-client) +# WZP_SERVER_TYPE Hetzner server type (default: cx32 — 4 vCPU, 8GB RAM) +# WZP_KEEP_VM Set to 1 to skip destroy on full build + +SSH_KEY_NAME="wz" +SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp" +SERVER_TYPE="${WZP_SERVER_TYPE:-cx33}" +IMAGE="ubuntu-24.04" +SERVER_NAME="wzp-android-builder" +REMOTE_USER="root" +OUTPUT_DIR="target/android-apk" +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +BRANCH="${WZP_BRANCH:-feat/android-voip-client}" +KEEP_VM="${WZP_KEEP_VM:-0}" + +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o LogLevel=ERROR" + +# NDK 26.1 — NDK 27 crashes scudo on Android 16 MTE devices +NDK_VERSION="26.1.10909125" +ANDROID_API="34" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +log() { echo -e "\n\033[1;36m>>> $*\033[0m"; } +err() { echo -e "\033[1;31mERROR: $*\033[0m" >&2; } +die() { err "$@"; do_destroy_quiet; exit 1; } + +get_vm_ip() { + hcloud server list -o columns=name,ipv4 -o noheader 2>/dev/null | grep "$SERVER_NAME" | awk '{print $2}' | tr -d ' ' +} + +ssh_cmd() { + local ip + ip=$(get_vm_ip) + [ -n "$ip" ] || die "No VM found. Run --prepare first." + ssh $SSH_OPTS -A -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "$@" +} + +scp_down() { + local ip + ip=$(get_vm_ip) + [ -n "$ip" ] || die "No VM found." + scp $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip:$1" "$2" +} + +do_destroy_quiet() { + local name + name=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$SERVER_NAME" | tr -d ' ' || true) + if [ -n "$name" ]; then + echo "" + err "Cleaning up — destroying VM $name" + hcloud server delete "$name" 2>/dev/null || true + fi +} + +# --------------------------------------------------------------------------- +# --prepare: Create VM, install all build dependencies +# --------------------------------------------------------------------------- + +do_prepare() { + # Check if VM already exists + local existing + existing=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$SERVER_NAME" | tr -d ' ' || true) + if [ -n "$existing" ]; then + log "VM already exists: $existing — reusing" + do_upload + return + fi + + log "Creating Hetzner VM ($SERVER_TYPE, $IMAGE)..." + hcloud server create \ + --name "$SERVER_NAME" \ + --type "$SERVER_TYPE" \ + --image "$IMAGE" \ + --ssh-key "$SSH_KEY_NAME" \ + --location fsn1 \ + --quiet \ + || die "Failed to create VM" + + local ip + ip=$(get_vm_ip) + [ -n "$ip" ] || die "VM created but no IP found" + echo " VM: $SERVER_NAME @ $ip" + + # Wait for SSH + log "Waiting for SSH..." + local ok=0 + for i in $(seq 1 30); do + if ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "echo ok" &>/dev/null; then + ok=1 + break + fi + sleep 2 + done + [ "$ok" -eq 1 ] || die "SSH timeout after 60s" + + # System packages + log "Installing system packages (cmake, JDK 17, build tools)..." + ssh_cmd "export DEBIAN_FRONTEND=noninteractive && \ + apt-get update -qq && \ + apt-get install -y -qq \ + build-essential cmake curl git libssl-dev pkg-config \ + unzip wget zip openjdk-17-jdk-headless \ + > /dev/null 2>&1" \ + || die "Failed to install system packages" + + # Verify cmake version (must be <= 3.30) + local cmake_ver + cmake_ver=$(ssh_cmd "cmake --version | head -1") + echo " cmake: $cmake_ver" + echo " java: $(ssh_cmd "java -version 2>&1 | head -1")" + + # Rust + log "Installing Rust toolchain..." + ssh_cmd "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable > /dev/null 2>&1" \ + || die "Failed to install Rust" + ssh_cmd "source \$HOME/.cargo/env && rustup target add aarch64-linux-android > /dev/null 2>&1" + ssh_cmd "source \$HOME/.cargo/env && cargo install cargo-ndk > /dev/null 2>&1" \ + || die "Failed to install cargo-ndk" + echo " rust: $(ssh_cmd "source \$HOME/.cargo/env && rustc --version")" + + # Android SDK + NDK + log "Installing Android SDK + NDK $NDK_VERSION..." + ssh_cmd "export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 && \ + mkdir -p \$HOME/android-sdk/cmdline-tools && \ + cd /tmp && \ + wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdtools.zip && \ + unzip -qo cmdtools.zip -d \$HOME/android-sdk/cmdline-tools && \ + mv \$HOME/android-sdk/cmdline-tools/cmdline-tools \$HOME/android-sdk/cmdline-tools/latest 2>/dev/null; \ + yes | \$HOME/android-sdk/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null 2>&1; \ + \$HOME/android-sdk/cmdline-tools/latest/bin/sdkmanager --install \ + 'platforms;android-${ANDROID_API}' \ + 'build-tools;${ANDROID_API}.0.0' \ + 'ndk;${NDK_VERSION}' \ + 'platform-tools' \ + 2>&1 | grep -v '^\[' > /dev/null" \ + || die "Failed to install Android SDK/NDK" + + ssh_cmd "[ -d \$HOME/android-sdk/ndk/$NDK_VERSION ]" \ + || die "NDK not found after install" + echo " NDK: $NDK_VERSION" + + # Upload source + do_upload + + log "VM ready!" + echo " IP: $ip" + echo " SSH: ssh -A -i $SSH_KEY_PATH root@$ip" +} + +# --------------------------------------------------------------------------- +# --upload: Upload source code to VM +# --------------------------------------------------------------------------- + +do_upload() { + log "Uploading source code (rsync)..." + local ip + ip=$(get_vm_ip) + [ -n "$ip" ] || die "No VM found." + rsync -az --delete \ + --exclude='target' \ + --exclude='.git' \ + --exclude='.claude' \ + --exclude='node_modules' \ + --exclude='dist' \ + --exclude='desktop/src-tauri/gen' \ + -e "ssh $SSH_OPTS -i $SSH_KEY_PATH" \ + "$PROJECT_DIR/" "$REMOTE_USER@$ip:/root/wzp-build/" + echo " Source uploaded." +} + +# --------------------------------------------------------------------------- +# --build: Build native .so + debug & release APKs +# --------------------------------------------------------------------------- + +do_build() { + log "Building Rust native library (arm64-v8a, release)..." + + # ANDROID_NDK must be set (not just ANDROID_NDK_HOME) — cmake checks it + ssh_cmd "source \$HOME/.cargo/env && \ + export ANDROID_HOME=\$HOME/android-sdk && \ + export ANDROID_NDK_HOME=\$ANDROID_HOME/ndk/$NDK_VERSION && \ + export ANDROID_NDK=\$ANDROID_NDK_HOME && \ + cd /root/wzp-build && \ + cargo ndk -t arm64-v8a \ + -o android/app/src/main/jniLibs \ + build --release -p wzp-android 2>&1" | tail -5 \ + || die "Rust native build failed" + + ssh_cmd "[ -f /root/wzp-build/android/app/src/main/jniLibs/arm64-v8a/libwzp_android.so ]" \ + || die "libwzp_android.so not found after build" + + local so_size + so_size=$(ssh_cmd "du -h /root/wzp-build/android/app/src/main/jniLibs/arm64-v8a/libwzp_android.so | cut -f1") + echo " .so: $so_size" + + # Generate debug keystore if missing + ssh_cmd "[ -f /root/wzp-build/android/keystore/wzp-debug.jks ] || \ + (mkdir -p /root/wzp-build/android/keystore && \ + keytool -genkey -v \ + -keystore /root/wzp-build/android/keystore/wzp-debug.jks \ + -keyalg RSA -keysize 2048 -validity 10000 \ + -alias wzp-debug -storepass android -keypass android \ + -dname 'CN=WZP Debug' > /dev/null 2>&1)" + + # Build debug APK + log "Building debug APK..." + ssh_cmd "export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 && \ + export ANDROID_HOME=\$HOME/android-sdk && \ + cd /root/wzp-build/android && \ + chmod +x ./gradlew && \ + ./gradlew assembleDebug --no-daemon --warning-mode=none 2>&1" | tail -3 \ + || die "Debug APK build failed" + + # Build release APK (uses debug keystore for now) + log "Building release APK..." + # Copy debug keystore as release keystore (same password in build.gradle) + ssh_cmd "cp /root/wzp-build/android/keystore/wzp-debug.jks /root/wzp-build/android/keystore/wzp-release.jks 2>/dev/null; true" + ssh_cmd "export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 && \ + export ANDROID_HOME=\$HOME/android-sdk && \ + cd /root/wzp-build/android && \ + ./gradlew assembleRelease --no-daemon --warning-mode=none 2>&1" | tail -3 \ + || echo " (release APK failed — debug APK still available)" + + log "Build complete!" + ssh_cmd "find /root/wzp-build/android -name '*.apk' -path '*/outputs/apk/*' -exec ls -lh {} \;" +} + +# --------------------------------------------------------------------------- +# --transfer: Download APKs to local machine +# --------------------------------------------------------------------------- + +do_transfer() { + log "Downloading APKs..." + mkdir -p "$OUTPUT_DIR" + + local ip + ip=$(get_vm_ip) + + # Debug APK + local debug_apk + debug_apk=$(ssh_cmd "find /root/wzp-build/android -name 'app-debug*.apk' -path '*/outputs/apk/*' | head -1") + if [ -n "$debug_apk" ]; then + scp_down "$debug_apk" "$OUTPUT_DIR/wzp-debug.apk" + echo " debug: $OUTPUT_DIR/wzp-debug.apk ($(du -h "$OUTPUT_DIR/wzp-debug.apk" | cut -f1))" + fi + + # Release APK + local release_apk + release_apk=$(ssh_cmd "find /root/wzp-build/android -name 'app-release*.apk' -path '*/outputs/apk/*' | head -1" || true) + if [ -n "$release_apk" ]; then + scp_down "$release_apk" "$OUTPUT_DIR/wzp-release.apk" + echo " release: $OUTPUT_DIR/wzp-release.apk ($(du -h "$OUTPUT_DIR/wzp-release.apk" | cut -f1))" + fi + + # Also copy the .so for inspection + scp_down "/root/wzp-build/android/app/src/main/jniLibs/arm64-v8a/libwzp_android.so" "$OUTPUT_DIR/libwzp_android.so" + echo " .so: $OUTPUT_DIR/libwzp_android.so" + + log "Transfer complete!" + echo "" + echo " Install debug: adb install -r $OUTPUT_DIR/wzp-debug.apk" + [ -f "$OUTPUT_DIR/wzp-release.apk" ] && echo " Install release: adb install -r $OUTPUT_DIR/wzp-release.apk" +} + +# --------------------------------------------------------------------------- +# --destroy: Delete the VM +# --------------------------------------------------------------------------- + +do_destroy() { + local name + name=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$SERVER_NAME" | tr -d ' ' || true) + if [ -z "$name" ]; then + echo "No VM to destroy." + return + fi + log "Deleting VM: $name" + hcloud server delete "$name" + echo " Done." +} + +# --------------------------------------------------------------------------- +# Full build: create → build → transfer → destroy +# --------------------------------------------------------------------------- + +do_full() { + trap 'err "Build failed!"; do_destroy_quiet; exit 1' ERR + + do_prepare + + # Disable trap during build — release APK failure is non-fatal + trap - ERR + do_build + do_transfer + trap 'err "Build failed!"; do_destroy_quiet; exit 1' ERR + + if [ "$KEEP_VM" = "1" ]; then + log "VM kept alive (WZP_KEEP_VM=1). Destroy with: $0 --destroy" + else + do_destroy + fi + + log "All done!" + echo "" + echo " ┌──────────────────────────────────────────────────┐" + echo " │ Debug APK: $OUTPUT_DIR/wzp-debug.apk" + [ -f "$OUTPUT_DIR/wzp-release.apk" ] && \ + echo " │ Release APK: $OUTPUT_DIR/wzp-release.apk" + echo " │" + echo " │ Install: adb install -r $OUTPUT_DIR/wzp-debug.apk" + echo " └──────────────────────────────────────────────────┘" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +case "${1:-}" in + --prepare) do_prepare ;; + --build) do_build ;; + --transfer) do_transfer ;; + --destroy) do_destroy ;; + --upload) do_upload ;; + --all) + do_prepare + do_build + do_transfer + log "VM still running. Destroy with: $0 --destroy" + ;; + "") + do_full + ;; + *) + echo "Usage: $0 [--prepare|--build|--transfer|--destroy|--all|--upload]" + echo "" + echo " (no args) Full build: create VM → build → download → destroy VM" + echo " --prepare Create VM and install deps" + echo " --build Build on existing VM" + echo " --transfer Download APKs from VM" + echo " --destroy Delete the VM" + echo " --all prepare + build + transfer (VM persists)" + echo " --upload Re-upload source to existing VM" + echo "" + echo "Environment:" + echo " WZP_BRANCH=$BRANCH" + echo " WZP_SERVER_TYPE=$SERVER_TYPE" + echo " WZP_KEEP_VM=$KEEP_VM (set to 1 to skip auto-destroy)" + exit 1 + ;; +esac