diff --git a/.gitea/workflows/android-build.yml b/.gitea/workflows/android-build.yml index fbed122..1b0fb3d 100644 --- a/.gitea/workflows/android-build.yml +++ b/.gitea/workflows/android-build.yml @@ -15,6 +15,7 @@ env: ANDROID_SDK: /opt/android-sdk NDK_VERSION: "25.2.9519653" BUILD_TOOLS_VERSION: "34.0.0" + PLATFORM: "android-34" jobs: build-apk: @@ -32,7 +33,7 @@ jobs: - name: Install system dependencies run: | sudo apt-get update -y - sudo apt-get install -y openjdk-17-jdk-headless unzip + sudo apt-get install -y openjdk-17-jdk-headless unzip zip # ── Android SDK (shared cache key with release workflow) ────────── - name: Cache Android SDK @@ -56,7 +57,7 @@ jobs: sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \ --sdk_root=${{ env.ANDROID_SDK }} \ "build-tools;${{ env.BUILD_TOOLS_VERSION }}" \ - "platforms;android-34" \ + "platforms;${{ env.PLATFORM }}" \ "ndk;${{ env.NDK_VERSION }}" # ── Rust toolchain ───────────────────────────────────────────────── @@ -84,15 +85,12 @@ jobs: key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} restore-keys: cargo-registry- - # cargo-apk2 is the maintained fork of cargo-apk; reads the same - # `[package.metadata.android]` block. Cache key prefix `apk2-` - # so we don't restore an old cargo-apk binary from the previous key. - - name: Cache cargo-apk2 binary + - name: Cache cargo-ndk binary uses: actions/cache@v4 - id: apk-tool-cache + id: ndk-tool-cache with: - path: ~/.cargo/bin/cargo-apk2 - key: apk2-${{ runner.os }}-stable + path: ~/.cargo/bin/cargo-ndk + key: cargo-ndk-${{ runner.os }}-stable - name: Cache build artifacts uses: actions/cache@v4 @@ -101,16 +99,19 @@ jobs: key: android-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }} restore-keys: android-target-${{ hashFiles('**/Cargo.lock') }}- - # ── Build ────────────────────────────────────────────────────────── - - name: Install cargo-apk2 - if: steps.apk-tool-cache.outputs.cache-hit != 'true' - run: cargo install cargo-apk2 --locked + - name: Install cargo-ndk + if: steps.ndk-tool-cache.outputs.cache-hit != 'true' + run: cargo install cargo-ndk --locked + # ── Build APK ────────────────────────────────────────────────────── - name: Build debug APK - run: | - export ANDROID_HOME=${{ env.ANDROID_SDK }} - export ANDROID_NDK_HOME=${{ env.ANDROID_SDK }}/ndk/${{ env.NDK_VERSION }} - cargo apk2 build --package solitaire_app --lib + env: + ANDROID_HOME: ${{ env.ANDROID_SDK }} + ANDROID_NDK_HOME: ${{ env.ANDROID_SDK }}/ndk/${{ env.NDK_VERSION }} + BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOLS_VERSION }} + PLATFORM: ${{ env.PLATFORM }} + PROFILE: debug + run: ./scripts/build_android_apk.sh # ── Artifact ─────────────────────────────────────────────────────── - name: Upload APK diff --git a/.gitea/workflows/android-release.yml b/.gitea/workflows/android-release.yml index 2a79000..f93f49d 100644 --- a/.gitea/workflows/android-release.yml +++ b/.gitea/workflows/android-release.yml @@ -9,6 +9,7 @@ env: ANDROID_SDK: /opt/android-sdk NDK_VERSION: "25.2.9519653" BUILD_TOOLS_VERSION: "34.0.0" + PLATFORM: "android-34" GITEA_API: https://git.aleshym.co/api/v1 REPO: funman300/Rusty_Solitare @@ -24,9 +25,13 @@ jobs: id: meta run: echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" - # ── Android SDK + NDK ────────────────────────────────────────────── - # Shared cache key with the debug workflow so a warm debug run - # saves the ~2 GB SDK download for the release run too. + # ── System dependencies ──────────────────────────────────────────── + - name: Install system dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y openjdk-17-jdk-headless unzip zip jq + + # ── Android SDK (shared cache key with debug workflow) ───────────── - name: Cache Android SDK uses: actions/cache@v4 id: sdk-cache @@ -34,12 +39,6 @@ jobs: path: ${{ env.ANDROID_SDK }} key: v2-android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }} - # Java and jq are always needed (apksigner requires Java even on cache hits). - - name: Install system dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y openjdk-17-jdk-headless unzip jq - - name: Install Android SDK + NDK if: steps.sdk-cache.outputs.cache-hit != 'true' run: | @@ -54,10 +53,10 @@ jobs: sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \ --sdk_root=${{ env.ANDROID_SDK }} \ "build-tools;${{ env.BUILD_TOOLS_VERSION }}" \ - "platforms;android-34" \ + "platforms;${{ env.PLATFORM }}" \ "ndk;${{ env.NDK_VERSION }}" -# ── Rust toolchain ───────────────────────────────────────────────── + # ── Rust toolchain ───────────────────────────────────────────────── - name: Install Rust stable run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ @@ -82,12 +81,12 @@ jobs: key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} restore-keys: cargo-registry- - - name: Cache cargo-apk2 binary + - name: Cache cargo-ndk binary uses: actions/cache@v4 - id: apk-tool-cache + id: ndk-tool-cache with: - path: ~/.cargo/bin/cargo-apk2 - key: apk2-${{ runner.os }}-stable + path: ~/.cargo/bin/cargo-ndk + key: cargo-ndk-${{ runner.os }}-stable - name: Cache build artifacts uses: actions/cache@v4 @@ -96,51 +95,33 @@ jobs: key: android-release-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }} restore-keys: android-release-target-${{ hashFiles('**/Cargo.lock') }}- - # ── Build ────────────────────────────────────────────────────────── - - name: Install cargo-apk2 - if: steps.apk-tool-cache.outputs.cache-hit != 'true' - run: cargo install cargo-apk2 --locked + - name: Install cargo-ndk + if: steps.ndk-tool-cache.outputs.cache-hit != 'true' + run: cargo install cargo-ndk --locked - - name: Build release APK - run: | - export ANDROID_HOME=${{ env.ANDROID_SDK }} - export ANDROID_NDK_HOME=${{ env.ANDROID_SDK }}/ndk/${{ env.NDK_VERSION }} - cargo apk2 build --release --package solitaire_app --lib - - # ── Sign ─────────────────────────────────────────────────────────── + # ── Build & sign with release keystore ───────────────────────────── - name: Decode keystore run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > /tmp/solitaire-release.jks - - name: Align and sign APK - run: | - TAG="${{ steps.meta.outputs.tag }}" - UNSIGNED="target/release/apk/solitaire-quest.apk" - ALIGNED="/tmp/solitaire-quest-aligned.apk" - SIGNED="ferrous-solitaire-${TAG}.apk" + - name: Build signed release APK + env: + ANDROID_HOME: ${{ env.ANDROID_SDK }} + ANDROID_NDK_HOME: ${{ env.ANDROID_SDK }}/ndk/${{ env.NDK_VERSION }} + BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOLS_VERSION }} + PLATFORM: ${{ env.PLATFORM }} + PROFILE: release + KEYSTORE: /tmp/solitaire-release.jks + KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASS: ${{ secrets.KEY_PASS }} + APK_OUT: ferrous-solitaire-${{ steps.meta.outputs.tag }}.apk + run: ./scripts/build_android_apk.sh - ${{ env.ANDROID_SDK }}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/zipalign -v 4 \ - "$UNSIGNED" "$ALIGNED" - - ${{ env.ANDROID_SDK }}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner sign \ - --ks /tmp/solitaire-release.jks \ - --ks-pass "pass:${{ secrets.KEYSTORE_PASS }}" \ - --ks-key-alias "${{ secrets.KEY_ALIAS }}" \ - --key-pass "pass:${{ secrets.KEY_PASS }}" \ - --out "$SIGNED" \ - "$ALIGNED" - - - name: Verify APK signature - run: | - TAG="${{ steps.meta.outputs.tag }}" - ${{ env.ANDROID_SDK }}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify \ - --verbose "ferrous-solitaire-${TAG}.apk" - - # ── Publish ──────────────────────────────────────────────────────── + # ── Publish to Gitea release ─────────────────────────────────────── - name: Create Gitea release id: release run: | TAG="${{ steps.meta.outputs.tag }}" - # Try to create; fall back to fetching the existing release on 409. RESPONSE=$(curl -s -o /tmp/release.json -w "%{http_code}" \ -X POST "$GITEA_API/repos/$REPO/releases" \ -H "Authorization: token ${{ secrets.CI_TOKEN }}" \ diff --git a/scripts/build_android_apk.sh b/scripts/build_android_apk.sh new file mode 100755 index 0000000..3948929 --- /dev/null +++ b/scripts/build_android_apk.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# Build a self-signed Android APK from solitaire_app's cdylib targets. +# +# Replaces the cargo-apk pipeline with explicit cargo-ndk + aapt2 + apksigner +# steps. The CI runner was hitting an SDK-discovery bug inside cargo-apk's +# ndk-build crate that we couldn't isolate; running each Android toolchain +# step explicitly gives us a debuggable pipeline. +# +# Required environment: +# ANDROID_HOME Path to Android SDK root +# ANDROID_NDK_HOME Path to the specific NDK version +# BUILD_TOOLS_VERSION e.g. "34.0.0" +# PLATFORM e.g. "android-34" +# +# Optional environment: +# PROFILE "debug" (default) | "release" +# APK_OUT Output APK path (default: target/$PROFILE/apk/solitaire-quest.apk) +# KEYSTORE Path to keystore for signing (default: generates a debug keystore) +# KEYSTORE_PASS Keystore password (default: "android" for the generated debug keystore) +# KEY_ALIAS Key alias (default: "androiddebugkey") +# KEY_PASS Key password (default: same as KEYSTORE_PASS) +# +# Outputs: +# $APK_OUT Signed, zipaligned APK +set -euo pipefail + +: "${ANDROID_HOME:?ANDROID_HOME must be set}" +: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}" +: "${BUILD_TOOLS_VERSION:?BUILD_TOOLS_VERSION must be set}" +: "${PLATFORM:?PLATFORM must be set (e.g. android-34)}" + +PROFILE="${PROFILE:-debug}" +APK_OUT="${APK_OUT:-target/${PROFILE}/apk/solitaire-quest.apk}" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +BT="$ANDROID_HOME/build-tools/$BUILD_TOOLS_VERSION" +PLATFORM_JAR="$ANDROID_HOME/platforms/$PLATFORM/android.jar" +MANIFEST="solitaire_app/android/AndroidManifest.xml" +RES_DIR="solitaire_app/res" +ASSETS_DIR="assets" + +# --- sanity ---------------------------------------------------------------- +for f in "$BT/aapt2" "$BT/zipalign" "$BT/apksigner" "$PLATFORM_JAR" "$MANIFEST"; do + [ -e "$f" ] || { echo "missing: $f"; exit 1; } +done + +STAGING="$(mktemp -d)" +trap 'rm -rf "$STAGING"' EXIT +mkdir -p "$STAGING/lib" "$STAGING/compiled-res" + +# --- 1. native libraries via cargo-ndk ------------------------------------- +# `-o $STAGING/lib` lays out files as $STAGING/lib//libsolitaire_app.so +# which is the directory structure the APK expects under lib/. +CARGO_NDK_ARGS=( + -t arm64-v8a + -t armeabi-v7a + -t x86_64 + --platform 26 + -o "$STAGING/lib" + build --package solitaire_app --lib +) +if [ "$PROFILE" = "release" ]; then + CARGO_NDK_ARGS+=( --release ) +fi +echo ">>> cargo ndk ${CARGO_NDK_ARGS[*]}" +cargo ndk "${CARGO_NDK_ARGS[@]}" + +# --- 2. compile + link resources and manifest ------------------------------ +if [ -d "$RES_DIR" ]; then + echo ">>> aapt2 compile resources" + "$BT/aapt2" compile --dir "$RES_DIR" -o "$STAGING/compiled-res" +fi + +LINK_ARGS=( + link + -o "$STAGING/app-unsigned.apk" + -I "$PLATFORM_JAR" + --manifest "$MANIFEST" +) +[ -d "$ASSETS_DIR" ] && LINK_ARGS+=( -A "$ASSETS_DIR" ) +# Add compiled resources if any +shopt -s nullglob +RES_FLATS=( "$STAGING/compiled-res"/*.flat ) +shopt -u nullglob +if [ ${#RES_FLATS[@]} -gt 0 ]; then + LINK_ARGS+=( "${RES_FLATS[@]}" ) +fi +echo ">>> aapt2 link" +"$BT/aapt2" "${LINK_ARGS[@]}" + +# --- 3. add native libraries to the APK ------------------------------------ +echo ">>> bundle native libraries" +( cd "$STAGING" && zip -r -q app-unsigned.apk lib/ ) + +# --- 4. zipalign ----------------------------------------------------------- +echo ">>> zipalign" +"$BT/zipalign" -p -f 4 "$STAGING/app-unsigned.apk" "$STAGING/app-aligned.apk" + +# --- 5. sign --------------------------------------------------------------- +if [ -z "${KEYSTORE:-}" ]; then + # Generate a deterministic debug keystore on the fly. + KEYSTORE="$STAGING/debug.keystore" + KEYSTORE_PASS="${KEYSTORE_PASS:-android}" + KEY_ALIAS="${KEY_ALIAS:-androiddebugkey}" + KEY_PASS="${KEY_PASS:-$KEYSTORE_PASS}" + echo ">>> generating debug keystore at $KEYSTORE" + keytool -genkeypair -v \ + -keystore "$KEYSTORE" \ + -storepass "$KEYSTORE_PASS" \ + -alias "$KEY_ALIAS" \ + -keypass "$KEY_PASS" \ + -keyalg RSA -keysize 2048 -validity 10000 \ + -dname "CN=Android Debug,O=Android,C=US" > /dev/null +fi + +KEYSTORE_PASS="${KEYSTORE_PASS:-android}" +KEY_ALIAS="${KEY_ALIAS:-androiddebugkey}" +KEY_PASS="${KEY_PASS:-$KEYSTORE_PASS}" + +mkdir -p "$(dirname "$APK_OUT")" +echo ">>> apksigner sign -> $APK_OUT" +"$BT/apksigner" sign \ + --ks "$KEYSTORE" \ + --ks-pass "pass:$KEYSTORE_PASS" \ + --ks-key-alias "$KEY_ALIAS" \ + --key-pass "pass:$KEY_PASS" \ + --out "$APK_OUT" \ + "$STAGING/app-aligned.apk" + +echo ">>> verify" +"$BT/apksigner" verify --verbose "$APK_OUT" + +echo ">>> done: $APK_OUT" diff --git a/solitaire_app/android/AndroidManifest.xml b/solitaire_app/android/AndroidManifest.xml new file mode 100644 index 0000000..ceb25bc --- /dev/null +++ b/solitaire_app/android/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + +