Files
ledgrab/android/build-scripts/build-pydantic-core.sh
T
alexei.dolgolyov 8574424fb7
Lint & Test / test (push) Successful in 2m10s
feat: Android TV app embedding Python server via Chaquopy
Adds a native Android TV application that runs the full LedGrab Python
server in-process via Chaquopy. Captures the TV box screen using the
MediaProjection API and exposes the existing web UI on the device's
local network — users configure via phone/tablet browser.

Android (new /android/ module):
- Kotlin shell: MainActivity, CaptureService (foreground service),
  ScreenCapture (MediaProjection + ImageReader), PythonBridge (Chaquopy).
- Polished Leanback-themed UI with QR code for easy web UI access.
- AGP 8.9 + Chaquopy 17 + Gradle 8.11 (avoids the AGP 8.7 thread-lock bug).
- Pre-built pydantic-core wheels for arm64-v8a, x86_64, x86 cross-compiled
  with maturin + Android NDK, linked against Chaquopy's libpython3.11.so.

Python server platform guards:
- New utils/platform.py with is_android()/is_windows()/is_linux() helpers.
- Guard every top-level import of desktop-only packages (mss, psutil,
  sounddevice, pyserial, PyAudioWPatch, etc.) with try/except ImportError.
- Android-incompatible calls gated with None-checks so the server runs on
  reduced capabilities on Android (no CPU/RAM metrics, no mss displays).
- utils/image_codec.py gains a Pillow fallback for resize + JPEG encode
  when cv2 is unavailable; all internal cv2.resize callers migrated.
- New android_entry.py start_server/stop_server invoked from Kotlin.
- get_displays API falls back to best available engine when mss fails.

New capture engines:
- MediaProjectionEngine: receives RGBA frames pushed from Kotlin through
  a thread-safe queue; caches last frame for static-screen previews.
- ScrcpyClientEngine: optional H.264 streaming via scrcpy-client library
  (priority 10, overrides the ADB-screencap engine when installed).

Frontend:
- Tab loaders previously required an apiKey; now correctly treat
  "auth disabled" as authenticated (Android has no auth by default).
- Re-trigger the active tab's loader after loadServerInfo resolves
  authRequired, since initTabs runs earlier.
- Add i18n keys for the demo / mediaprojection / scrcpy_client engines.

Docs:
- TODO.md: follow-ups for multi-ABI wheel rebuilds, CI pipeline, USB
  serial LED controllers, root-only capture, perf metrics abstraction.
- CLAUDE.md: Android dependency sync policy (pip --exclude doesn't exist).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 03:11:43 +03:00

99 lines
3.5 KiB
Bash

#!/usr/bin/env bash
#
# Cross-compile pydantic-core for Android ARM64.
#
# Prerequisites:
# - Rust toolchain (rustup)
# - Android NDK (set ANDROID_NDK_HOME or let this script find it)
# - maturin (pip install maturin)
# - Python 3.11 (matching Chaquopy's embedded version)
#
# Output: ../wheels/pydantic_core-*.whl
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WHEELS_DIR="$SCRIPT_DIR/../wheels"
BUILD_DIR="$SCRIPT_DIR/../.build-cache"
# ── Pydantic-core version (must match pydantic>=2.9.2 requirement) ──
PYDANTIC_CORE_VERSION="2.27.2"
# ── Find Android NDK ────────────────────────────────────────────────
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
# Try common locations
for candidate in \
"$HOME/Library/Android/sdk/ndk"/* \
"$HOME/Android/Sdk/ndk"/* \
"$LOCALAPPDATA/Android/Sdk/ndk"/* \
"/usr/local/lib/android/sdk/ndk"/*; do
if [ -d "$candidate" ]; then
ANDROID_NDK_HOME="$candidate"
break
fi
done
fi
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
echo "ERROR: Android NDK not found. Set ANDROID_NDK_HOME or install via Android Studio."
exit 1
fi
echo "Using Android NDK: $ANDROID_NDK_HOME"
# ── Determine NDK API level and toolchain ───────────────────────────
API_LEVEL=24
HOST_TAG=""
case "$(uname -s)" in
Linux*) HOST_TAG="linux-x86_64" ;;
Darwin*) HOST_TAG="darwin-x86_64" ;;
MINGW*|MSYS*|CYGWIN*) HOST_TAG="windows-x86_64" ;;
*) echo "Unsupported host OS"; exit 1 ;;
esac
TOOLCHAIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$HOST_TAG"
CC="$TOOLCHAIN/bin/aarch64-linux-android${API_LEVEL}-clang"
AR="$TOOLCHAIN/bin/llvm-ar"
if [ ! -f "$CC" ] && [ ! -f "${CC}.cmd" ]; then
echo "ERROR: NDK compiler not found at $CC"
echo "Check your NDK installation."
exit 1
fi
# ── Install Rust Android target ─────────────────────────────────────
echo "Adding Rust target aarch64-linux-android..."
rustup target add aarch64-linux-android
# ── Configure Cargo for cross-compilation ───────────────────────────
mkdir -p "$BUILD_DIR"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$CC"
export CC_aarch64_linux_android="$CC"
export AR_aarch64_linux_android="$AR"
# ── Clone pydantic-core ─────────────────────────────────────────────
REPO_DIR="$BUILD_DIR/pydantic-core"
if [ -d "$REPO_DIR" ]; then
echo "Updating existing pydantic-core checkout..."
cd "$REPO_DIR"
git fetch --tags
git checkout "v$PYDANTIC_CORE_VERSION"
else
echo "Cloning pydantic-core v$PYDANTIC_CORE_VERSION..."
git clone --depth 1 --branch "v$PYDANTIC_CORE_VERSION" \
https://github.com/pydantic/pydantic-core.git "$REPO_DIR"
cd "$REPO_DIR"
fi
# ── Build with maturin ──────────────────────────────────────────────
echo "Building pydantic-core for aarch64-linux-android..."
maturin build \
--release \
--target aarch64-linux-android \
--interpreter python3.11 \
--out "$WHEELS_DIR"
echo ""
echo "=== Build complete ==="
echo "Wheels:"
ls -la "$WHEELS_DIR"/pydantic_core-*.whl 2>/dev/null || echo "WARNING: No wheel found!"