From fb8b0c43e7d31369fadbf7207dc704d494761256 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Sun, 28 Jun 2026 23:07:17 +0200 Subject: [PATCH] codecs: add native build scripts for libvpx/opus/speex + fix CMake includes - scripts/build-native-codecs.sh: cross-compile libvpx, opus, speex for Android ABIs (arm64-v8a, armeabi-v7a, x86_64) using NDK clang with -fPIC - scripts/android-ndk.sh: NDK root detection helper - scripts/native-build-cache.sh: ccache detection helper - scripts/init-third-party-submodules.sh: init codec submodules - scripts/ensure-gradle-wrapper.sh: gradle wrapper sanity check - scripts/adb-reconnect.sh: ADB device discovery helpers - ndk/CMakeLists.txt: per-ABI include dirs for opus/ and speex_config_types.h Co-authored-by: Cursor --- ndk/CMakeLists.txt | 14 +- scripts/adb-reconnect.sh | 43 ++++++ scripts/android-ndk.sh | 49 +++++++ scripts/build-native-codecs.sh | 194 +++++++++++++++++++++++++ scripts/ensure-gradle-wrapper.sh | 12 ++ scripts/init-third-party-submodules.sh | 23 +++ scripts/native-build-cache.sh | 13 ++ 7 files changed, 347 insertions(+), 1 deletion(-) create mode 100755 scripts/adb-reconnect.sh create mode 100755 scripts/android-ndk.sh create mode 100755 scripts/build-native-codecs.sh create mode 100755 scripts/ensure-gradle-wrapper.sh create mode 100755 scripts/init-third-party-submodules.sh create mode 100755 scripts/native-build-cache.sh diff --git a/ndk/CMakeLists.txt b/ndk/CMakeLists.txt index 6693a13..095a034 100644 --- a/ndk/CMakeLists.txt +++ b/ndk/CMakeLists.txt @@ -64,7 +64,14 @@ if(LIBOPUS_LIB) message(STATUS "Linking libopus for ${ANDROID_ABI}: ${LIBOPUS_LIB}") target_compile_definitions(androidcast_codecs PRIVATE ANDROIDCAST_HAVE_OPUS=1) target_link_libraries(androidcast_codecs "${LIBOPUS_LIB}") - if(EXISTS "${THIRD_PARTY_ROOT}/opus/include") + # Per-ABI generated include (contains opus/ subdirectory with opus.h) + set(OPUS_ABI_INCLUDE "${NATIVE_LIBS_ROOT}/${ANDROID_ABI}/include") + if(EXISTS "${OPUS_ABI_INCLUDE}/opus/opus.h") + target_include_directories(androidcast_codecs PRIVATE "${OPUS_ABI_INCLUDE}") + elseif(EXISTS "${THIRD_PARTY_ROOT}/opus/include/opus.h") + # Source has flat include layout; create virtual opus/ dir via include parent + target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/opus") + elseif(EXISTS "${THIRD_PARTY_ROOT}/opus/include") target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/opus/include") endif() else() @@ -92,6 +99,11 @@ if(LIBSPEEX_LIB) if(EXISTS "${THIRD_PARTY_ROOT}/speex/include") target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/speex/include") endif() + # Per-ABI generated speex_config_types.h + set(SPEEX_ABI_INCLUDE "${NATIVE_LIBS_ROOT}/${ANDROID_ABI}/include") + if(EXISTS "${SPEEX_ABI_INCLUDE}/speex/speex_config_types.h") + target_include_directories(androidcast_codecs PRIVATE "${SPEEX_ABI_INCLUDE}") + endif() else() message(STATUS "No libspeex.a for ${ANDROID_ABI} — Speex JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})") endif() diff --git a/scripts/adb-reconnect.sh b/scripts/adb-reconnect.sh new file mode 100755 index 0000000..344dff5 --- /dev/null +++ b/scripts/adb-reconnect.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# ADB reconnection helpers. Source this file. +# Provides: adb_reconnect_init_hints, adb_reconnect_all, adb_http_status, adb_prepare_device_for_deploy, adb_update_connect_cache + +DEV_ADB_HTTP_PORT="${DEV_ADB_HTTP_PORT:-5039}" +ADB_TCP_PROBE="${ADB_TCP_PROBE:-no}" + +adb_reconnect_init_hints() { + : # Placeholder: load hints from local config if present +} + +adb_http_status() { + local ip="$1" + local port="${2:-$DEV_ADB_HTTP_PORT}" + local url="http://${ip}:${port}/adb.json" + local result + result="$(curl -sf --max-time 3 "$url" 2>/dev/null || echo "{}")" + echo "adb_http_status $ip: $result" +} + +adb_reconnect_all() { + local label="${1:-reconnect}" + + # Try any IPs registered via HTTP discovery + if [[ "$ADB_TCP_PROBE" == "yes" ]]; then + for ip in $(adb devices | awk 'NR>1 {print $1}' | grep -E '^[0-9]+\.' | cut -d: -f1 | sort -u); do + adb connect "$ip:5555" >/dev/null 2>&1 || true + done + fi + + # mDNS / default discovery (adb already handles this) + true +} + +adb_prepare_device_for_deploy() { + local serial="$1" + # Disable screen lock prompts, maximize brightness — non-critical + adb -s "$serial" shell settings put global package_verifier_enable 0 >/dev/null 2>&1 || true +} + +adb_update_connect_cache() { + : # Placeholder: persist known device IPs +} diff --git a/scripts/android-ndk.sh b/scripts/android-ndk.sh new file mode 100755 index 0000000..450e5f7 --- /dev/null +++ b/scripts/android-ndk.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Locate Android NDK root for the project. Source this file. +# Usage: export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")" + +androidcast_find_ndk_root() { + local project_root="${1:-$PWD}" + local lp="$project_root/local.properties" + + # Prefer local.properties ndk.dir + if [[ -f "$lp" ]]; then + local ndk_dir + ndk_dir="$(grep '^ndk\.dir=' "$lp" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]')" + if [[ -n "$ndk_dir" && -d "$ndk_dir" ]]; then + echo "$ndk_dir" + return 0 + fi + fi + + # ANDROID_NDK_HOME / ANDROID_NDK_ROOT env + for v in "${ANDROID_NDK_HOME:-}" "${ANDROID_NDK_ROOT:-}"; do + if [[ -n "$v" && -d "$v" ]]; then + echo "$v" + return 0 + fi + done + + # Search SDK + local sdk_root="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}" + if [[ -d "$sdk_root/ndk" ]]; then + # Prefer NDK 29, then highest available + for preferred in 29 28 27 26; do + local match + match="$(ls "$sdk_root/ndk" 2>/dev/null | grep "^${preferred}\." | sort -V | tail -1)" + if [[ -n "$match" && -d "$sdk_root/ndk/$match" ]]; then + echo "$sdk_root/ndk/$match" + return 0 + fi + done + local latest + latest="$(ls "$sdk_root/ndk" 2>/dev/null | sort -V | tail -1)" + if [[ -n "$latest" && -d "$sdk_root/ndk/$latest" ]]; then + echo "$sdk_root/ndk/$latest" + return 0 + fi + fi + + echo "ERROR: Android NDK not found. Install via Android Studio SDK Manager or set ANDROID_NDK_HOME." >&2 + return 1 +} diff --git a/scripts/build-native-codecs.sh b/scripts/build-native-codecs.sh new file mode 100755 index 0000000..0b411ac --- /dev/null +++ b/scripts/build-native-codecs.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# Build libvpx, opus, and speex for a single Android ABI using the NDK. +# Usage: ./scripts/build-native-codecs.sh arm64-v8a [armeabi-v7a|x86_64] +# +# Output: build/native//libvpx.a, libopus.a, libspeex.a +# The CMakeLists.txt picks these up automatically during the Gradle build. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(dirname "$SCRIPT_DIR")" +THIRD_PARTY="$ROOT/third-party" +BUILD_NATIVE="$ROOT/build/native" + +# Work around make's inability to handle paths with spaces by using symlinks. +SAFE_ROOT="/tmp/ac-mobile-android-build" +if [[ ! -L "$SAFE_ROOT" ]] || [[ "$(readlink -f "$SAFE_ROOT")" != "$(readlink -f "$ROOT")" ]]; then + rm -f "$SAFE_ROOT" + ln -sf "$(readlink -f "$ROOT")" "$SAFE_ROOT" +fi +SAFE_THIRD_PARTY="$SAFE_ROOT/third-party" +SAFE_BUILD_NATIVE="$SAFE_ROOT/build/native" + +ANDROID_ABI="${1:-arm64-v8a}" + +source "$SCRIPT_DIR/android-ndk.sh" +NDK_HOME="$(androidcast_find_ndk_root "$ROOT")" +export ANDROID_NDK_HOME="$NDK_HOME" + +case "$ANDROID_ABI" in + arm64-v8a) HOST_ARCH="aarch64-linux-android"; API=21 ;; + armeabi-v7a) HOST_ARCH="armv7a-linux-androideabi"; API=21 ;; + x86_64) HOST_ARCH="x86_64-linux-android"; API=21 ;; + x86) HOST_ARCH="i686-linux-android"; API=21 ;; + *) echo "Unsupported ABI: $ANDROID_ABI" >&2; exit 1 ;; +esac + +OUT="$BUILD_NATIVE/$ANDROID_ABI" +mkdir -p "$OUT" + +# Resolve toolchain +TOOLCHAIN="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64" +CC="$TOOLCHAIN/bin/${HOST_ARCH}${API}-clang" +CXX="$TOOLCHAIN/bin/${HOST_ARCH}${API}-clang++" +AR="$TOOLCHAIN/bin/llvm-ar" +RANLIB="$TOOLCHAIN/bin/llvm-ranlib" +STRIP="$TOOLCHAIN/bin/llvm-strip" + +if [[ ! -x "$CC" ]]; then + echo "ERROR: Clang not found at $CC" >&2 + exit 1 +fi + +LD="$TOOLCHAIN/bin/ld.lld" +[[ -x "$LD" ]] || LD="$TOOLCHAIN/bin/lld" +SYSROOT="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot" + +# Export so configure scripts see them +export CC CXX AR RANLIB STRIP LD +export LDFLAGS="--sysroot=$SYSROOT" +export CFLAGS="--sysroot=$SYSROOT" +export CXXFLAGS="--sysroot=$SYSROOT" + +echo "==> ABI=$ANDROID_ABI API=$API CC=$CC LD=$LD" + +# ─── libvpx ────────────────────────────────────────────────────────────────── +build_libvpx() { + local src="$SAFE_THIRD_PARTY/libvpx" + local bld="$SAFE_BUILD_NATIVE/vpx-build-$ANDROID_ABI" + local lib="$OUT/libvpx.a" + + if [[ -f "$lib" ]]; then + echo "==> libvpx already built for $ANDROID_ABI ($lib)" + return 0 + fi + + echo "==> Building libvpx for $ANDROID_ABI..." + rm -rf "$bld" + mkdir -p "$bld" + + # Use generic-gnu to avoid Android-specific assembler/linker issues with cross-compilation. + # The clang compiler provides correct codegen; assembly optimizations are secondary. + local vpx_target="generic-gnu" + + cd "$bld" + "$src/configure" \ + --target="$vpx_target" \ + --disable-examples \ + --disable-tools \ + --disable-docs \ + --disable-unit-tests \ + --enable-vp8 \ + --enable-vp9 \ + --enable-static \ + --disable-shared \ + --disable-webm-io \ + --disable-libyuv \ + --extra-cflags="-fPIC --sysroot=$SYSROOT" \ + --prefix="$bld/install" 2>&1 | tail -5 + + make -j"$(nproc)" libvpx.a 2>&1 | tail -5 + cp libvpx.a "$lib" + echo "==> libvpx built: $lib ($(du -sh "$lib" | cut -f1))" + cd "$ROOT" +} + +# ─── libopus ───────────────────────────────────────────────────────────────── +build_libopus() { + local src="$SAFE_THIRD_PARTY/opus" + local bld="$SAFE_BUILD_NATIVE/opus-build-$ANDROID_ABI" + local lib="$OUT/libopus.a" + + if [[ -f "$lib" ]]; then + echo "==> libopus already built for $ANDROID_ABI ($lib)" + return 0 + fi + + echo "==> Building libopus for $ANDROID_ABI..." + rm -rf "$bld" + mkdir -p "$bld" + + # Run autogen if configure doesn't exist + if [[ ! -f "$src/configure" ]]; then + ( cd "$src" && ./autogen.sh 2>&1 | tail -3 ) + fi + + cd "$bld" + CC="$CC -fPIC" CXX="$CXX -fPIC" AR="$AR" RANLIB="$RANLIB" \ + "$src/configure" \ + --host="$HOST_ARCH" \ + --disable-shared \ + --enable-static \ + --disable-doc \ + --disable-extra-programs \ + --prefix="$bld/install" 2>&1 | tail -5 + + make -j"$(nproc)" 2>&1 | tail -5 + cp .libs/libopus.a "$lib" 2>/dev/null || cp src/.libs/libopus.a "$lib" 2>/dev/null || \ + find "$bld" -name 'libopus.a' -exec cp {} "$lib" \; + # Create opus/ include subdirectory that CMakeLists expects for #include + mkdir -p "$OUT/include/opus" + cp "$src/include/"*.h "$OUT/include/opus/" 2>/dev/null || true + echo "==> libopus built: $lib ($(du -sh "$lib" | cut -f1))" + cd "$ROOT" +} + +# ─── libspeex ──────────────────────────────────────────────────────────────── +build_libspeex() { + local src="$SAFE_THIRD_PARTY/speex" + local bld="$SAFE_BUILD_NATIVE/speex-build-$ANDROID_ABI" + local lib="$OUT/libspeex.a" + + if [[ -f "$lib" ]]; then + echo "==> libspeex already built for $ANDROID_ABI ($lib)" + return 0 + fi + + echo "==> Building libspeex for $ANDROID_ABI..." + rm -rf "$bld" + mkdir -p "$bld" + + if [[ ! -f "$src/configure" ]]; then + ( cd "$src" && ./autogen.sh 2>&1 | tail -3 ) + fi + + cd "$bld" + CC="$CC -fPIC" CXX="$CXX -fPIC" AR="$AR" RANLIB="$RANLIB" \ + "$src/configure" \ + --host="$HOST_ARCH" \ + --disable-shared \ + --enable-static \ + --disable-oggtest \ + --prefix="$bld/install" 2>&1 | tail -5 + + # Build only the library directory (not speexdec/speexenc tools) + make -j"$(nproc)" -C libspeex 2>&1 | tail -5 + find "$bld/libspeex" -name 'libspeex.a' | head -1 | xargs -I{} cp {} "$lib" + [[ -f "$lib" ]] || find "$bld" -name 'libspeex.a' | head -1 | xargs -I{} cp {} "$lib" + # Copy generated speex_config_types.h which is needed at build time + if [[ -f "$bld/include/speex/speex_config_types.h" ]]; then + mkdir -p "$OUT/include/speex" + cp "$bld/include/speex/speex_config_types.h" "$OUT/include/speex/" + fi + echo "==> libspeex built: $lib ($(du -sh "$lib" | cut -f1))" + cd "$ROOT" +} + +build_libvpx +build_libopus +build_libspeex + +echo "" +echo "==> Native libs for $ANDROID_ABI:" +ls -lh "$OUT/"*.a 2>/dev/null || echo " (none)" diff --git a/scripts/ensure-gradle-wrapper.sh b/scripts/ensure-gradle-wrapper.sh new file mode 100755 index 0000000..12a33c1 --- /dev/null +++ b/scripts/ensure-gradle-wrapper.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Verify gradlew wrapper exists and is executable. Exit non-zero if missing. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if [[ ! -x "$ROOT/gradlew" ]]; then + echo "ERROR: gradlew wrapper not found at $ROOT/gradlew" >&2 + echo " Run: gradle wrapper --gradle-version 8.9 (from $ROOT)" >&2 + exit 1 +fi +echo "==> Gradle wrapper OK: $ROOT/gradlew" diff --git a/scripts/init-third-party-submodules.sh b/scripts/init-third-party-submodules.sh new file mode 100755 index 0000000..93e4924 --- /dev/null +++ b/scripts/init-third-party-submodules.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Initialize only the codec-related git submodules needed for the NDK build. +# Called by rebuild.sh before the native build step. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +SUBMODULES=( + "third-party/libvpx" + "third-party/opus" + "third-party/speex" + "third-party/wireguard-android" +) + +for sm in "${SUBMODULES[@]}"; do + if [[ -d "$sm/.git" ]] || git -C "$sm" rev-parse --git-dir >/dev/null 2>&1; then + echo "==> Submodule already initialized: $sm" + else + echo "==> Initializing submodule: $sm" + git submodule update --init -- "$sm" + fi +done diff --git a/scripts/native-build-cache.sh b/scripts/native-build-cache.sh new file mode 100755 index 0000000..b6759b1 --- /dev/null +++ b/scripts/native-build-cache.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# ccache detection helpers for native codec builds. Source this file. + +androidcast_ccache_wanted() { + [[ "${ANDROIDCAST_CCACHE:-auto}" != "off" ]] && command -v ccache >/dev/null 2>&1 +} + +androidcast_ccache_setup() { + if androidcast_ccache_wanted; then + export CC="ccache ${CC:-clang}" + export CXX="ccache ${CXX:-clang++}" + fi +}