Files
ledgrab/android/build-scripts/build-pydantic-core.sh
T
alexei.dolgolyov 151cea3ecb
Build Android APK / build-android (push) Failing after 2m31s
Lint & Test / test (push) Successful in 6m15s
ci: Android multi-ABI APK pipeline + pydantic-core wheel rebuild
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).
2026-04-14 12:36:13 +03:00

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