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).
193 lines
7.1 KiB
YAML
193 lines
7.1 KiB
YAML
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"
|