ci: Android multi-ABI APK pipeline + pydantic-core wheel rebuild
Build Android APK / build-android (push) Failing after 2m31s
Lint & Test / test (push) Successful in 6m15s

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).
This commit is contained in:
2026-04-14 12:36:13 +03:00
parent 8574424fb7
commit 151cea3ecb
11 changed files with 750 additions and 78 deletions
+192
View File
@@ -0,0 +1,192 @@
name: Build Android APK
on:
push:
branches: [master]
tags: ['v*']
paths:
- 'android/**'
- 'server/src/ledgrab/**'
- '.gitea/workflows/build-android.yml'
workflow_dispatch:
inputs:
version:
description: 'Version label (e.g. dev, 0.3.0-test)'
required: false
default: 'dev'
jobs:
build-android:
runs-on: ubuntu-latest
env:
JAVA_VERSION: '17'
PYTHON_VERSION: '3.11'
ANDROID_CMDLINE_TOOLS_VERSION: '11076708'
ANDROID_SDK_PLATFORM: 'android-34'
ANDROID_BUILD_TOOLS: '34.0.0'
ANDROID_NDK_VERSION: '26.1.10909125'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve build label
id: label
run: |
REF="${{ gitea.ref_name }}"
if echo "$REF" | grep -qE '^v[0-9]'; then
LABEL="${REF#v}"
IS_RELEASE="true"
elif [ -n "${{ inputs.version }}" ]; then
LABEL="${{ inputs.version }}"
IS_RELEASE="false"
else
LABEL="dev-${{ gitea.sha }}"
IS_RELEASE="false"
fi
LABEL="${LABEL:0:40}"
echo "label=$LABEL" >> "$GITHUB_OUTPUT"
echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
echo "Build label: $LABEL (release=$IS_RELEASE)"
- name: Setup JDK ${{ env.JAVA_VERSION }}
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ env.JAVA_VERSION }}
- name: Setup Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Android SDK + NDK
run: |
set -euo pipefail
SDK_ROOT="$HOME/android-sdk"
mkdir -p "$SDK_ROOT/cmdline-tools"
cd "$SDK_ROOT/cmdline-tools"
curl -sSL --retry 3 \
-o cmdline-tools.zip \
"https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip"
unzip -q cmdline-tools.zip
rm cmdline-tools.zip
mv cmdline-tools latest
export ANDROID_SDK_ROOT="$SDK_ROOT"
export ANDROID_HOME="$SDK_ROOT"
SDKMANAGER="$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager"
yes | "$SDKMANAGER" --licenses > /dev/null 2>&1 || true
"$SDKMANAGER" --install \
"platform-tools" \
"platforms;${ANDROID_SDK_PLATFORM}" \
"build-tools;${ANDROID_BUILD_TOOLS}" \
"ndk;${ANDROID_NDK_VERSION}" > /dev/null
echo "ANDROID_SDK_ROOT=$SDK_ROOT" >> "$GITHUB_ENV"
echo "ANDROID_HOME=$SDK_ROOT" >> "$GITHUB_ENV"
echo "ANDROID_NDK_HOME=$SDK_ROOT/ndk/${ANDROID_NDK_VERSION}" >> "$GITHUB_ENV"
echo "$SDK_ROOT/platform-tools" >> "$GITHUB_PATH"
echo "$SDK_ROOT/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
- name: Create local.properties
run: |
echo "sdk.dir=$ANDROID_SDK_ROOT" > android/local.properties
- name: Link Python source (junction equivalent)
run: |
# Chaquopy reads Python modules from android/app/src/main/python/
# On Windows dev machines this is a directory junction; on Linux CI use a symlink.
ln -sfn "$(pwd)/server/src/ledgrab" android/app/src/main/python/ledgrab
ls -la android/app/src/main/python/
- name: Decode signing keystore
id: keystore
if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }}
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
mkdir -p android/keystore
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/keystore/release.jks
echo "path=$(pwd)/android/keystore/release.jks" >> "$GITHUB_OUTPUT"
echo "present=true" >> "$GITHUB_OUTPUT"
- name: Build APK
working-directory: android
env:
ANDROID_KEYSTORE_PATH: ${{ steps.keystore.outputs.path }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
chmod +x gradlew
if [ "${{ steps.keystore.outputs.present }}" = "true" ] && [ "${{ steps.label.outputs.is_release }}" = "true" ]; then
echo "Building signed release APK"
./gradlew --no-daemon assembleRelease
else
echo "Building debug APK (no signing keystore available or not a release tag)"
./gradlew --no-daemon assembleDebug
fi
- name: Locate and rename APK
id: apk
run: |
set -euo pipefail
SRC=$(ls android/app/build/outputs/apk/release/*.apk 2>/dev/null | head -1 || true)
if [ -z "$SRC" ]; then
SRC=$(ls android/app/build/outputs/apk/debug/*.apk | head -1)
VARIANT="debug"
else
VARIANT="release"
fi
DEST="build/LedGrab-${{ steps.label.outputs.label }}-android-${VARIANT}.apk"
mkdir -p build
cp "$SRC" "$DEST"
echo "path=$DEST" >> "$GITHUB_OUTPUT"
echo "name=$(basename "$DEST")" >> "$GITHUB_OUTPUT"
ls -lh "$DEST"
- name: Upload APK artifact
uses: actions/upload-artifact@v3
with:
name: LedGrab-${{ steps.label.outputs.label }}-android
path: ${{ steps.apk.outputs.path }}
retention-days: 90
- name: Attach APK to Gitea release
if: ${{ steps.label.outputs.is_release == 'true' }}
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
set -euo pipefail
TAG="${{ gitea.ref_name }}"
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
APK_PATH="${{ steps.apk.outputs.path }}"
APK_NAME="${{ steps.apk.outputs.name }}"
# Fetch release by tag (created by release.yml `create-release` job)
RELEASE_ID=$(curl -s "$BASE_URL/releases/tags/$TAG" \
-H "Authorization: token $GITEA_TOKEN" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
if [ -z "$RELEASE_ID" ]; then
echo "::warning::No release found for tag $TAG — skipping asset upload"
exit 0
fi
# Replace existing asset if present (re-run safety)
EXISTING_ID=$(curl -s "$BASE_URL/releases/$RELEASE_ID/assets" \
-H "Authorization: token $GITEA_TOKEN" \
| python3 -c "import sys,json; assets=json.load(sys.stdin); print(next((str(a['id']) for a in assets if a['name']=='$APK_NAME'),''))" 2>/dev/null)
if [ -n "$EXISTING_ID" ]; then
curl -s -X DELETE "$BASE_URL/releases/$RELEASE_ID/assets/$EXISTING_ID" \
-H "Authorization: token $GITEA_TOKEN"
fi
curl -s -X POST \
"$BASE_URL/releases/$RELEASE_ID/assets?name=$APK_NAME" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$APK_PATH"
echo "Uploaded: $APK_NAME"
+18 -15
View File
@@ -4,13 +4,10 @@
During emulator testing, we switched the build to **x86 only** (see `android/app/build.gradle.kts` `abiFilters`) to avoid having to keep the arm64-v8a / x86_64 pydantic-core wheels current. Before shipping, restore all three ABIs:
- [ ] Rebuild `pydantic-core` wheels for all three ABIs with the current SOABI + libpython linking settings:
- `arm64-v8a` (real TV boxes — the primary target)
- `x86_64` (modern emulators)
- `x86` (legacy emulators)
- [ ] Verify wheels with `readelf -d`: SONAME must be `libpython3.11.so` in NEEDED
- [ ] Restore `abiFilters += listOf("arm64-v8a", "x86_64", "x86")` in `build.gradle.kts`
- [ ] Re-test on real ARM64 Android TV hardware
- [x] Rebuild `pydantic-core` wheels for all three ABIs with the current SOABI + libpython linking settings (`android/build-scripts/build-pydantic-core.sh` — now supports `arm64`, `x86_64`, `x86` args; defaults to all three).
- [x] Verify wheels: all three now list `libpython3.11.so` in `NEEDED` (`llvm-readelf -d`), automated in the build script.
- [x] Restored `abiFilters += listOf("arm64-v8a", "x86_64", "x86")` in `build.gradle.kts`. Multi-ABI debug APK builds cleanly (~99 MB).
- [ ] Re-test on real ARM64 Android TV hardware (still pending — only emulator-verified build).
Build cache + scripts live in `android/build-scripts/` and `android/.build-cache/` (junction host + sysconfigdata for each ABI).
@@ -18,16 +15,22 @@ Build cache + scripts live in `android/build-scripts/` and `android/.build-cache
Build the Android APK automatically on push/tag.
- [ ] Generate Gradle wrapper (`gradlew`) and commit it
- [ ] Create CI workflow (`.gitea/workflows/build-android.yaml` or `.github/workflows/`)
- [x] Generate Gradle wrapper (`gradlew`) and commit it
- [x] Create CI workflow (`.gitea/workflows/build-android.yml`)
- JDK 17 + Android SDK + NDK setup
- Python 3.11 for Chaquopy build
- Recreate the directory junction (`ln -s` on Linux, `mklink /J` on Windows)
- `./gradlew assembleRelease`
- Upload APK as artifact
- [ ] Commit pre-built pydantic-core wheels to `android/wheels/` (arm64, x86, x86_64)
- [ ] APK signing for release builds (keystore setup)
- [ ] Consider: publish APK to GitHub/Gitea releases on tag push
- Recreate the directory junction via `ln -s` on Linux CI
- `./gradlew assembleDebug` on master push, `assembleRelease` on `v*` tags (if signing secrets set)
- Uploads APK as CI artifact; attaches to Gitea release on tag push
- [x] Commit pre-built pydantic-core wheels to `android/wheels/` (arm64, x86, x86_64)
- [x] APK signing for release builds — conditional signing config reads keystore from env vars (`ANDROID_KEYSTORE_PATH/_PASSWORD/_ALIAS/_KEY_PASSWORD`), falls back to debug signing locally
- [ ] Provision a real keystore and add the four CI secrets:
- `ANDROID_KEYSTORE_BASE64` (base64-encoded .jks)
- `ANDROID_KEYSTORE_PASSWORD`
- `ANDROID_KEY_ALIAS`
- `ANDROID_KEY_PASSWORD`
- [ ] Add `LedGrab-{tag}-android-release.apk` row to the release description table in `.gitea/workflows/release.yml``create-release` job
- [ ] Verify the CI workflow passes end-to-end with the now-restored multi-ABI build (larger APK, longer Android build step)
## Android Root Capture (No Permission Dialog, No System Indicator)
+3
View File
@@ -15,3 +15,6 @@ local.properties
# Python source junction (points at ../server/src/ledgrab — do not commit)
/app/src/main/python/ledgrab
# Signing keystore decoded from CI secrets
/keystore/
+29 -2
View File
@@ -16,14 +16,41 @@ android {
versionName = "0.3.0"
ndk {
// Temporarily x86 only for emulator testing with new wheel
abiFilters += listOf("x86")
// All three ABIs: arm64-v8a (real TV hardware), x86_64 (modern
// emulators), x86 (legacy emulators). Wheels in android/wheels/
// must be kept in sync — see build-scripts/build-pydantic-core.sh.
abiFilters += listOf("arm64-v8a", "x86_64", "x86")
}
}
// Signing config from env vars (CI) — only registered when all four are set.
// Local release builds fall back to the debug signing config.
val ciKeystorePath = System.getenv("ANDROID_KEYSTORE_PATH")
val ciKeystorePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD")
val ciKeyAlias = System.getenv("ANDROID_KEY_ALIAS")
val ciKeyPassword = System.getenv("ANDROID_KEY_PASSWORD")
val hasCiSigning = listOf(ciKeystorePath, ciKeystorePassword, ciKeyAlias, ciKeyPassword)
.all { !it.isNullOrBlank() } && file(ciKeystorePath!!).exists()
signingConfigs {
if (hasCiSigning) {
create("release") {
storeFile = file(ciKeystorePath!!)
storePassword = ciKeystorePassword
keyAlias = ciKeyAlias
keyPassword = ciKeyPassword
}
}
}
buildTypes {
release {
isMinifyEnabled = false
signingConfig = if (hasCiSigning) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
}
}
+160 -60
View File
@@ -1,98 +1,198 @@
#!/usr/bin/env bash
#
# Cross-compile pydantic-core for Android ARM64.
# Cross-compile pydantic-core for Android across all three ABIs:
# arm64-v8a (primary — real TV hardware)
# x86_64 (modern emulators)
# x86 (legacy emulators)
#
# Prerequisites:
# - Rust toolchain (rustup)
# - Android NDK (set ANDROID_NDK_HOME or let this script find it)
# 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)
# - Python 3.11 (matching Chaquopy's embedded version)
#
# Output: ../wheels/pydantic_core-*.whl
# 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)"
WHEELS_DIR="$SCRIPT_DIR/../wheels"
BUILD_DIR="$SCRIPT_DIR/../.build-cache"
ANDROID_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
WHEELS_DIR="$ANDROID_DIR/wheels"
BUILD_DIR="$ANDROID_DIR/.build-cache"
# ── Pydantic-core version (must match pydantic>=2.9.2 requirement) ──
PYDANTIC_CORE_VERSION="2.27.2"
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
# 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
"${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"
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" ;;
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"
READELF="$TOOLCHAIN/bin/llvm-readelf"
if [ ! -f "$CC" ] && [ ! -f "${CC}.cmd" ]; then
echo "ERROR: NDK compiler not found at $CC"
echo "Check your NDK installation."
exit 1
# ── 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
# ── Install Rust Android target ─────────────────────────────────────
echo "Adding Rust target aarch64-linux-android..."
rustup target add aarch64-linux-android
# ── 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"
)
# ── 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"
declare -A ABI_TAG_MAP=(
[arm64]="arm64_v8a"
[x86_64]="x86_64"
[x86]="x86"
)
# ── 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"
# ── Select which ABIs to build ──────────────────────────────────────
SELECTED=("$@")
if [ ${#SELECTED[@]} -eq 0 ]; then
SELECTED=(arm64 x86_64 x86)
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"
# ── 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 ==="
echo "Wheels:"
ls -la "$WHEELS_DIR"/pydantic_core-*.whl 2>/dev/null || echo "WARNING: No wheel found!"
ls -lh "$WHEELS_DIR"/pydantic_core-*.whl
Binary file not shown.
+252
View File
@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+2 -1
View File
@@ -7,7 +7,8 @@
| File | Trigger | Purpose |
|------|---------|---------|
| `.gitea/workflows/test.yml` | Push/PR to master | Lint (ruff) + pytest |
| `.gitea/workflows/release.yml` | Tag `v*` | Build artifacts + create Gitea release |
| `.gitea/workflows/release.yml` | Tag `v*` | Build Windows/Linux/Docker artifacts + create Gitea release |
| `.gitea/workflows/build-android.yml` | Push master (android/server paths), tag `v*`, manual | Build Android APK; attach to release on tag |
## Release Pipeline (`release.yml`)