151cea3ecb
Adds .gitea/workflows/build-android.yml — Linux runner installs JDK 17, Python 3.11, Android SDK/NDK, symlinks server/src/ledgrab into the Chaquopy python source dir, and runs assembleDebug on master pushes / assembleRelease on v* tags. APK is uploaded as an artifact and attached to the Gitea release on tag push. Conditional signing config in build.gradle.kts reads keystore from env vars (CI secrets) and falls back to debug signing locally. Gradle wrapper (gradlew/gradlew.bat/ gradle-wrapper.jar) committed so CI can drive the build. Rebuilds pydantic-core wheels for arm64-v8a and x86_64 — both were missing libpython3.11.so in NEEDED, which would have crashed at import on real devices. build-pydantic-core.sh rewritten as a multi-ABI builder: selects targets via args, sets RUSTFLAGS=-C link-arg=-Wl,--no-as-needed -C link-arg=-lpython3.11 to force the symbol-resolution dependency, uses the per-ABI sysconfigdata + libpython staged in android/.build-cache/, prefers `py -3.11` on Windows (Git Bash's python3.11 is an MSStore stub), uses the .cmd clang wrapper on Windows (fixes os error 193), and verifies NEEDED via llvm-readelf after each build. abiFilters restored to the full triple in build.gradle.kts; multi-ABI debug APK builds cleanly (~99 MB).
199 lines
8.2 KiB
Bash
199 lines
8.2 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# Cross-compile pydantic-core for Android across all three ABIs:
|
|
# arm64-v8a (primary — real TV hardware)
|
|
# x86_64 (modern emulators)
|
|
# x86 (legacy emulators)
|
|
#
|
|
# Outputs wheels into android/wheels/. Wheels are linked against the real
|
|
# libpython3.11.so shipped by Chaquopy (stub .so does NOT work — see
|
|
# memory/project_android_app.md for the incident notes).
|
|
#
|
|
# Prerequisites (on host):
|
|
# - Rust + cargo (rustup) with targets: aarch64/x86_64/i686-linux-android
|
|
# - Android NDK (ANDROID_NDK_HOME, or installed at Sdk/ndk/*)
|
|
# - Python 3.11 (matches Chaquopy's embedded version)
|
|
# - maturin (pip install maturin)
|
|
#
|
|
# Rebuild cadence: whenever PYDANTIC_CORE_VERSION changes, or pydantic's
|
|
# core dependency version changes.
|
|
#
|
|
# Usage:
|
|
# ./build-pydantic-core.sh # build all three ABIs
|
|
# ./build-pydantic-core.sh arm64 # build a single ABI
|
|
# ./build-pydantic-core.sh arm64 x86_64 # build a subset
|
|
#
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
ANDROID_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
WHEELS_DIR="$ANDROID_DIR/wheels"
|
|
BUILD_DIR="$ANDROID_DIR/.build-cache"
|
|
|
|
PYDANTIC_CORE_VERSION="2.46.0"
|
|
API_LEVEL=24
|
|
PY_VERSION="3.11"
|
|
|
|
# Resolve a concrete python3.11 interpreter path (maturin needs it callable).
|
|
# Windows Git Bash doesn't ship `python3.11` on PATH; fall back to `py -3.11`.
|
|
if [ -z "${PYTHON_BIN:-}" ]; then
|
|
# Prefer `py -3.11` on Windows (Git Bash's `python3.11` resolves to a
|
|
# non-functional MSStore stub). Fall back to `python3.11` on Linux/macOS.
|
|
if command -v py >/dev/null 2>&1 && py -3.11 -c '' 2>/dev/null; then
|
|
PYTHON_BIN="$(py -3.11 -c 'import sys; print(sys.executable)' 2>/dev/null | tr -d '\r')"
|
|
elif command -v python3.11 >/dev/null 2>&1 && python3.11 -c '' 2>/dev/null; then
|
|
PYTHON_BIN="$(command -v python3.11)"
|
|
fi
|
|
fi
|
|
[ -n "${PYTHON_BIN:-}" ] || { echo "ERROR: python3.11 not found; set PYTHON_BIN"; exit 1; }
|
|
echo "Python: $PYTHON_BIN"
|
|
|
|
mkdir -p "$WHEELS_DIR"
|
|
|
|
# ── Find Android NDK ────────────────────────────────────────────────
|
|
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
|
|
for candidate in \
|
|
"$HOME/Library/Android/sdk/ndk"/* \
|
|
"$HOME/Android/Sdk/ndk"/* \
|
|
"${LOCALAPPDATA:-}/Android/Sdk/ndk"/* \
|
|
"/c/Users/$(whoami)/AppData/Local/Android/Sdk/ndk"/* \
|
|
"/usr/local/lib/android/sdk/ndk"/* \
|
|
"${ANDROID_SDK_ROOT:-}/ndk"/*; do
|
|
if [ -d "$candidate" ]; then
|
|
ANDROID_NDK_HOME="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
[ -n "${ANDROID_NDK_HOME:-}" ] || { echo "ERROR: NDK not found; set ANDROID_NDK_HOME"; exit 1; }
|
|
echo "NDK: $ANDROID_NDK_HOME"
|
|
|
|
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"
|
|
READELF="$TOOLCHAIN/bin/llvm-readelf"
|
|
|
|
# ── Prepare source tree ─────────────────────────────────────────────
|
|
SRC_DIR="$BUILD_DIR/pydantic_core-$PYDANTIC_CORE_VERSION"
|
|
if [ ! -d "$SRC_DIR" ]; then
|
|
echo "Downloading pydantic_core $PYDANTIC_CORE_VERSION sdist..."
|
|
mkdir -p "$BUILD_DIR"
|
|
SDIST="$BUILD_DIR/pydantic_core-$PYDANTIC_CORE_VERSION.tar.gz"
|
|
[ -f "$SDIST" ] || curl -sSL --retry 3 -o "$SDIST" \
|
|
"https://files.pythonhosted.org/packages/source/p/pydantic_core/pydantic_core-$PYDANTIC_CORE_VERSION.tar.gz"
|
|
tar -xzf "$SDIST" -C "$BUILD_DIR"
|
|
fi
|
|
|
|
# ── ABI table ───────────────────────────────────────────────────────
|
|
# Columns: short_name rust_target clang_prefix sysconfig_dir
|
|
ABI_TABLE=(
|
|
"arm64 aarch64-linux-android aarch64-linux-android${API_LEVEL} cross-sysconfig"
|
|
"x86_64 x86_64-linux-android x86_64-linux-android${API_LEVEL} cross-sysconfig-x86_64"
|
|
"x86 i686-linux-android i686-linux-android${API_LEVEL} cross-sysconfig-x86"
|
|
)
|
|
|
|
declare -A ABI_TAG_MAP=(
|
|
[arm64]="arm64_v8a"
|
|
[x86_64]="x86_64"
|
|
[x86]="x86"
|
|
)
|
|
|
|
# ── Select which ABIs to build ──────────────────────────────────────
|
|
SELECTED=("$@")
|
|
if [ ${#SELECTED[@]} -eq 0 ]; then
|
|
SELECTED=(arm64 x86_64 x86)
|
|
fi
|
|
|
|
# ── Ensure rust targets are installed ───────────────────────────────
|
|
for name in "${SELECTED[@]}"; do
|
|
for row in "${ABI_TABLE[@]}"; do
|
|
read -r sname rtarget _ _ <<<"$row"
|
|
if [ "$sname" = "$name" ]; then
|
|
rustup target add "$rtarget" >/dev/null 2>&1 || true
|
|
fi
|
|
done
|
|
done
|
|
|
|
# ── Build loop ──────────────────────────────────────────────────────
|
|
cd "$SRC_DIR"
|
|
|
|
for name in "${SELECTED[@]}"; do
|
|
MATCHED=""
|
|
for row in "${ABI_TABLE[@]}"; do
|
|
read -r sname rtarget cprefix sysdir <<<"$row"
|
|
if [ "$sname" = "$name" ]; then
|
|
MATCHED="1"
|
|
break
|
|
fi
|
|
done
|
|
[ -n "$MATCHED" ] || { echo "Unknown ABI: $name"; exit 1; }
|
|
|
|
CROSS_LIB_DIR="$BUILD_DIR/$sysdir"
|
|
[ -f "$CROSS_LIB_DIR/libpython3.11.so" ] || {
|
|
echo "ERROR: missing $CROSS_LIB_DIR/libpython3.11.so (extract from a Chaquopy APK)"
|
|
exit 1
|
|
}
|
|
|
|
# On Windows hosts the clang wrapper is a .cmd batch file; cargo/rustc
|
|
# can't exec it as a linker directly (os error 193). Use the .cmd path
|
|
# explicitly so CreateProcess picks up cmd.exe as interpreter.
|
|
CC_BIN="$TOOLCHAIN/bin/${cprefix}-clang"
|
|
if [ "$HOST_TAG" = "windows-x86_64" ] && [ -f "${CC_BIN}.cmd" ]; then
|
|
CC_BIN="${CC_BIN}.cmd"
|
|
fi
|
|
AR_BIN="$TOOLCHAIN/bin/llvm-ar"
|
|
[ "$HOST_TAG" = "windows-x86_64" ] && [ -f "${AR_BIN}.exe" ] && AR_BIN="${AR_BIN}.exe"
|
|
[ -f "$CC_BIN" ] || { echo "ERROR: $CC_BIN not found"; exit 1; }
|
|
|
|
# Normalize rust target name for env var (cargo wants UPPER_WITH_UNDERSCORES)
|
|
TARGET_ENV=$(echo "$rtarget" | tr 'a-z-' 'A-Z_')
|
|
|
|
echo ""
|
|
echo "════════════════════════════════════════════════════════════"
|
|
echo " Building pydantic_core $PYDANTIC_CORE_VERSION for $name ($rtarget)"
|
|
echo "════════════════════════════════════════════════════════════"
|
|
|
|
env \
|
|
CARGO_TARGET_DIR="$SRC_DIR/target" \
|
|
"CC_${rtarget//-/_}=$CC_BIN" \
|
|
"AR_${rtarget//-/_}=$AR_BIN" \
|
|
"CARGO_TARGET_${TARGET_ENV}_LINKER=$CC_BIN" \
|
|
PYO3_CROSS=1 \
|
|
PYO3_CROSS_LIB_DIR="$CROSS_LIB_DIR" \
|
|
PYO3_CROSS_PYTHON_VERSION="$PY_VERSION" \
|
|
RUSTFLAGS="-C link-arg=-Wl,--no-as-needed -C link-arg=-lpython3.11 -C link-arg=-L$CROSS_LIB_DIR" \
|
|
maturin build \
|
|
--release \
|
|
--target "$rtarget" \
|
|
--interpreter "$PYTHON_BIN" \
|
|
--out "$WHEELS_DIR" \
|
|
--compatibility linux
|
|
|
|
# maturin writes the wheel with the correct android_<api>_<abi> platform
|
|
# tag directly (the sysconfigdata provides MULTIARCH). Find + verify.
|
|
ABI_TAG="${ABI_TAG_MAP[$name]}"
|
|
OUT_WHL="$WHEELS_DIR/pydantic_core-$PYDANTIC_CORE_VERSION-cp311-cp311-android_${API_LEVEL}_${ABI_TAG}.whl"
|
|
[ -f "$OUT_WHL" ] || { echo "ERROR: expected wheel not found: $OUT_WHL"; exit 1; }
|
|
|
|
TMP=$(mktemp -d)
|
|
unzip -qo "$OUT_WHL" -d "$TMP" "pydantic_core/*.so"
|
|
SO=$(find "$TMP" -name "*.so" | head -1)
|
|
if "$READELF" -d "$SO" | grep -q "libpython3.11.so"; then
|
|
echo " NEEDED OK: libpython3.11.so present in $(basename "$OUT_WHL")"
|
|
else
|
|
echo " ERROR: libpython3.11.so NOT in NEEDED — wheel will crash at import"
|
|
"$READELF" -d "$SO" | grep NEEDED
|
|
rm -rf "$TMP"
|
|
exit 1
|
|
fi
|
|
rm -rf "$TMP"
|
|
done
|
|
|
|
echo ""
|
|
echo "=== Build complete ==="
|
|
ls -lh "$WHEELS_DIR"/pydantic_core-*.whl
|