From 69ee3115b6869a8c987c3ef556c928fa71f65062 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Thu, 9 Apr 2026 11:25:54 +0400 Subject: [PATCH] build: tauri-android docker pipeline + ntfy notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dockerfile.android-builder: install Android API 36 platform + build-tools 35.0.0 alongside the existing API 34 set. Tauri 2.x mobile defaults to compileSdk 36 / build-tools 35; without these the gradle build fails with "SDK directory is not writable" because the read-only /opt/android-sdk volume can't grow at build time. Adding Node.js 20, all four Rust android targets, and tauri-cli 2.x was already in place. scripts/build-tauri-android.sh: new build wrapper for the desktop/ Tauri project (parallel to scripts/build-and-notify.sh which targets the legacy android/ Kotlin app). Pulls the branch on remote, runs cargo tauri android build inside the docker image, and sends three ntfy.sh/wzp notifications that all carry the short git hash: - STARTED [hash] — - OK [hash] (size) — - FAILED [hash] (line N) — On failure the full /tmp/wzp-tauri-build.log is uploaded to rustypaste so the URL in the failure ntfy is directly downloadable, same place as the APK. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/Dockerfile.android-builder | 39 +++++- scripts/build-tauri-android.sh | 217 +++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 4 deletions(-) create mode 100755 scripts/build-tauri-android.sh diff --git a/scripts/Dockerfile.android-builder b/scripts/Dockerfile.android-builder index 647c9f9..b5ea21f 100644 --- a/scripts/Dockerfile.android-builder +++ b/scripts/Dockerfile.android-builder @@ -1,11 +1,16 @@ # ============================================================================= # WZ Phone — Android build environment (Debian 12 / Bookworm) # -# Matches the bare-metal build-android.sh environment: +# Supports both: +# 1. Legacy Kotlin+JNI Android app (via cargo-ndk + gradle) +# 2. Tauri 2.x Mobile Android app (via tauri-cli + Node/npm) +# +# Toolchain: # - Debian 12 (cmake 3.25, no Android cross-compilation bugs) # - JDK 17 (Gradle 8.5 + AGP 8.2.0 compatible) # - NDK 26.1 (last stable before scudo/MTE crash on NDK 27+) -# - Rust stable with aarch64-linux-android target + cargo-ndk +# - Node.js 20 LTS (for Tauri frontend build) +# - Rust stable with all 4 Android targets + cargo-ndk + tauri-cli 2.x # # Build: docker build -t wzp-android-builder -f Dockerfile.android-builder . # ============================================================================= @@ -13,6 +18,11 @@ FROM debian:bookworm ARG NDK_VERSION=26.1.10909125 ARG ANDROID_API=34 +# Tauri 2.x mobile targets compileSdk 36 + build-tools 35 by default. Install +# both 34 (legacy Kotlin app) and 35/36 (Tauri mobile) so the same image works +# for both pipelines. +ARG ANDROID_API_TAURI=36 +ARG BUILD_TOOLS_TAURI=35.0.0 ENV DEBIAN_FRONTEND=noninteractive \ ANDROID_HOME=/opt/android-sdk \ @@ -35,8 +45,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ openjdk-17-jdk-headless \ ca-certificates \ libasound2-dev \ + file \ + xz-utils \ && rm -rf /var/lib/apt/lists/* +# ── Node.js 20 LTS (required by Tauri for frontend build) ──────────────────── +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* \ + && node --version \ + && npm --version + # ── Android SDK + NDK 26.1 ────────────────────────────────────────────────── RUN mkdir -p $ANDROID_HOME/cmdline-tools \ && cd /tmp \ @@ -49,6 +68,8 @@ RUN yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/nu && $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install \ "platforms;android-${ANDROID_API}" \ "build-tools;${ANDROID_API}.0.0" \ + "platforms;android-${ANDROID_API_TAURI}" \ + "build-tools;${BUILD_TOOLS_TAURI}" \ "ndk;${NDK_VERSION}" \ "platform-tools" \ 2>&1 | grep -v '^\[' > /dev/null @@ -64,12 +85,22 @@ USER builder WORKDIR /home/builder # ── Rust toolchain ─────────────────────────────────────────────────────────── +# Install all 4 Android targets (Tauri Mobile builds for all ABIs by default; +# cargo-ndk legacy path only needs arm64-v8a — both workflows supported). RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --default-toolchain stable \ && . $HOME/.cargo/env \ - && rustup target add aarch64-linux-android \ - && cargo install cargo-ndk + && rustup target add \ + aarch64-linux-android \ + armv7-linux-androideabi \ + i686-linux-android \ + x86_64-linux-android \ + && cargo install cargo-ndk \ + && cargo install tauri-cli --version "^2.0" --locked ENV PATH="/home/builder/.cargo/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$JAVA_HOME/bin:$PATH" +# NDK_HOME is the env var tauri-cli checks (in addition to ANDROID_NDK_HOME) +ENV NDK_HOME=$ANDROID_NDK_HOME + WORKDIR /build/source diff --git a/scripts/build-tauri-android.sh b/scripts/build-tauri-android.sh new file mode 100755 index 0000000..362e021 --- /dev/null +++ b/scripts/build-tauri-android.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# WZ Phone — Tauri 2.x Mobile Android APK build +# +# Builds the desktop/ Tauri app as an Android APK via cargo-tauri inside the +# wzp-android-builder Docker image on SepehrHomeserverdk. Uploads the APK to +# rustypaste, fires ntfy.sh/wzp notifications at start + finish, and SCPs the +# APK back locally. +# +# Same pattern as build-and-notify.sh but for the Tauri mobile pipeline: +# - Source: desktop/src-tauri/ (not android/) +# - Build: cargo tauri android build (not gradlew assembleDebug) +# - Output: desktop/src-tauri/gen/android/.../*.apk +# +# Usage: +# ./scripts/build-tauri-android.sh # full pipeline (debug) +# ./scripts/build-tauri-android.sh --release # release APK +# ./scripts/build-tauri-android.sh --no-pull # skip git fetch +# ./scripts/build-tauri-android.sh --rust # force-clean rust target +# ./scripts/build-tauri-android.sh --init # also run `cargo tauri android init` +# +# Environment: +# WZP_BRANCH Branch to build (default: feat/desktop-audio-rewrite) +# ============================================================================= + +REMOTE_HOST="SepehrHomeserverdk" +BASE_DIR="/mnt/storage/manBuilder" +NTFY_TOPIC="https://ntfy.sh/wzp" +LOCAL_OUTPUT="target/tauri-android-apk" +BRANCH="${WZP_BRANCH:-feat/desktop-audio-rewrite}" +SSH_OPTS="-o ConnectTimeout=15 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o LogLevel=ERROR" + +REBUILD_RUST=0 +DO_PULL=1 +DO_INIT=0 +BUILD_RELEASE=0 +for arg in "$@"; do + case "$arg" in + --rust) REBUILD_RUST=1 ;; + --pull) DO_PULL=1 ;; + --no-pull) DO_PULL=0 ;; + --init) DO_INIT=1 ;; + --release) BUILD_RELEASE=1 ;; + -h|--help) + sed -n '3,30p' "$0" + exit 0 + ;; + esac +done + +log() { echo -e "\033[1;36m>>> $*\033[0m"; } +ssh_cmd() { ssh -A $SSH_OPTS "$REMOTE_HOST" "$@"; } + +notify_local() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; } + +mkdir -p "$LOCAL_OUTPUT" + +log "Uploading remote build script..." +ssh_cmd "cat > /tmp/wzp-tauri-build.sh" <<'REMOTE_SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +BASE_DIR="/mnt/storage/manBuilder" +NTFY_TOPIC="https://ntfy.sh/wzp" +BRANCH="${1:-feat/desktop-audio-rewrite}" +DO_PULL="${2:-1}" +REBUILD_RUST="${3:-0}" +DO_INIT="${4:-0}" +BUILD_RELEASE="${5:-0}" + +LOG_FILE=/tmp/wzp-tauri-build.log +GIT_HASH="unknown" # populated after fetch +ENV_FILE="$BASE_DIR/.env" + +notify() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; } + +# Upload a file to rustypaste; print URL on stdout (or empty on failure). +upload_to_rustypaste() { + local file="$1" + [ ! -f "$ENV_FILE" ] && { echo ""; return; } + # shellcheck disable=SC1090 + source "$ENV_FILE" + if [ -n "${rusty_address:-}" ] && [ -n "${rusty_auth_token:-}" ]; then + curl -s -F "file=@$file" -H "Authorization: $rusty_auth_token" "$rusty_address" || echo "" + else + echo "" + fi +} + +# On failure: upload the build log to rustypaste, then notify with hash + url. +on_error() { + local line="$1" + local log_url + log_url=$(upload_to_rustypaste "$LOG_FILE" || echo "") + if [ -n "$log_url" ]; then + notify "WZP Tauri Android build FAILED [$GIT_HASH] (line $line) +log: $log_url" + else + notify "WZP Tauri Android build FAILED [$GIT_HASH] (line $line) — log upload failed, see $LOG_FILE on remote" + fi +} +trap 'on_error $LINENO' ERR + +exec > >(tee "$LOG_FILE") 2>&1 + +if [ "$DO_PULL" = "1" ]; then + echo ">>> git fetch + reset $BRANCH" + cd "$BASE_DIR/data/source" + git reset --hard HEAD 2>/dev/null || true + git clean -fd 2>/dev/null || true + git gc --prune=now 2>/dev/null || true + git fetch origin "$BRANCH" 2>&1 | tail -3 + git checkout "$BRANCH" 2>/dev/null || git checkout -b "$BRANCH" "origin/$BRANCH" + git reset --hard "origin/$BRANCH" + git submodule update --init || true +fi + +GIT_HASH=$(cd "$BASE_DIR/data/source" && git rev-parse --short HEAD 2>/dev/null || echo unknown) +GIT_MSG=$(cd "$BASE_DIR/data/source" && git log -1 --pretty=%s 2>/dev/null | head -c 60 || echo "?") +notify "WZP Tauri Android build STARTED [$GIT_HASH] — $GIT_MSG" + +# Fix perms so uid 1000 can write +find "$BASE_DIR/data/source" "$BASE_DIR/data/cache" \ + ! -user 1000 -o ! -group 1000 2>/dev/null | \ + xargs -r chown 1000:1000 2>/dev/null || true + +# Optionally clean rust target for android triples +if [ "$REBUILD_RUST" = "1" ]; then + echo ">>> Cleaning Rust android target dirs..." + rm -rf "$BASE_DIR/data/cache/target/aarch64-linux-android" \ + "$BASE_DIR/data/cache/target/armv7-linux-androideabi" \ + "$BASE_DIR/data/cache/target/i686-linux-android" \ + "$BASE_DIR/data/cache/target/x86_64-linux-android" +fi + +# Profile flag +PROFILE_FLAG="--debug" +[ "$BUILD_RELEASE" = "1" ] && PROFILE_FLAG="" + +docker run --rm \ + --user 1000:1000 \ + -e DO_INIT="$DO_INIT" \ + -e PROFILE_FLAG="$PROFILE_FLAG" \ + -v "$BASE_DIR/data/source:/build/source" \ + -v "$BASE_DIR/data/cache/cargo-registry:/home/builder/.cargo/registry" \ + -v "$BASE_DIR/data/cache/cargo-git:/home/builder/.cargo/git" \ + -v "$BASE_DIR/data/cache/target:/build/source/target" \ + -v "$BASE_DIR/data/cache/gradle:/home/builder/.gradle" \ + wzp-android-builder \ + bash -c ' +set -euo pipefail +cd /build/source/desktop + +echo ">>> npm install" +npm install --silent 2>&1 | tail -5 || npm install 2>&1 | tail -20 + +cd src-tauri + +if [ "${DO_INIT}" = "1" ] || [ ! -d gen/android ]; then + echo ">>> cargo tauri android init" + cargo tauri android init 2>&1 | tail -20 +fi + +echo ">>> cargo tauri android build ${PROFILE_FLAG} --target aarch64 --apk" +cargo tauri android build ${PROFILE_FLAG} --target aarch64 --apk + +echo "" +echo ">>> Build artifacts:" +find gen/android -name "*.apk" -exec ls -lh {} \; 2>/dev/null +' + +# Locate the produced APK +APK=$(find "$BASE_DIR/data/source/desktop/src-tauri/gen/android" -name "*.apk" -type f 2>/dev/null | head -1) +if [ -z "$APK" ] || [ ! -f "$APK" ]; then + LOG_URL=$(upload_to_rustypaste "$LOG_FILE" || echo "") + if [ -n "$LOG_URL" ]; then + notify "WZP Tauri Android build [$GIT_HASH]: no APK produced +log: $LOG_URL" + else + notify "WZP Tauri Android build [$GIT_HASH]: no APK produced — log upload failed" + fi + exit 1 +fi +APK_SIZE=$(du -h "$APK" | cut -f1) + +RUSTY_URL=$(upload_to_rustypaste "$APK" || echo "") +if [ -n "$RUSTY_URL" ]; then + notify "WZP Tauri Android build OK [$GIT_HASH] ($APK_SIZE) +$RUSTY_URL" +else + notify "WZP Tauri Android build OK [$GIT_HASH] ($APK_SIZE) — rustypaste upload skipped" +fi + +# Print path so the local script can grab it +echo "APK_REMOTE_PATH=$APK" +REMOTE_SCRIPT + +ssh_cmd "chmod +x /tmp/wzp-tauri-build.sh" + +notify_local "WZP Tauri Android build dispatched (branch=$BRANCH, release=$BUILD_RELEASE)" +log "Triggering remote build (branch=$BRANCH)..." + +# Run; capture full output, last line is APK_REMOTE_PATH=... +REMOTE_OUTPUT=$(ssh_cmd "/tmp/wzp-tauri-build.sh '$BRANCH' '$DO_PULL' '$REBUILD_RUST' '$DO_INIT' '$BUILD_RELEASE'" || true) +echo "$REMOTE_OUTPUT" | tail -60 + +APK_REMOTE=$(echo "$REMOTE_OUTPUT" | grep '^APK_REMOTE_PATH=' | tail -1 | cut -d= -f2-) +if [ -n "$APK_REMOTE" ]; then + log "Downloading APK to $LOCAL_OUTPUT/wzp-tauri.apk..." + scp $SSH_OPTS "$REMOTE_HOST:$APK_REMOTE" "$LOCAL_OUTPUT/wzp-tauri.apk" + echo " $LOCAL_OUTPUT/wzp-tauri.apk ($(du -h "$LOCAL_OUTPUT/wzp-tauri.apk" | cut -f1))" +else + log "No APK produced — see ntfy / remote log /tmp/wzp-tauri-build.log" + exit 1 +fi