ci(android): replace cargo-apk with cargo-ndk + manual APK assembly
cargo-apk 0.10 and its fork cargo-apk2 both failed to discover the installed Android platform in this Gitea runner, despite ANDROID_HOME, platforms;android-34, build-tools, and NDK all being present, readable, and pointed at correctly. We never isolated whether the bug is in the shared ndk-build crate's discovery logic or in the runner's env-var propagation through cargo subcommand exec, so this commit stops fighting either tool and assembles the APK from explicit toolchain steps instead: cargo ndk -> per-ABI .so files aapt2 compile/link -> manifest + resources -> base APK zip -> bundle native libs into lib/<abi>/ zipalign -> 4-byte alignment apksigner -> v2/v3 signing (debug keystore for CI, real for release) The pipeline lives in scripts/build_android_apk.sh so it's reproducible locally (same env vars, same commands). AndroidManifest.xml is now checked in under solitaire_app/android/ and mirrors what cargo-apk would have generated from [package.metadata.android] — keep them in sync if either is changed. Local `cargo apk build` still works on developer machines where cargo-apk is happy; CI just stops depending on it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }}" \
|
||||
|
||||
Executable
+135
@@ -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/<abi>/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"
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Mirrors what cargo-apk would generate from [package.metadata.android]
|
||||
in solitaire_app/Cargo.toml. Kept in-tree so the CI workflow can drive
|
||||
aapt2 directly without going through cargo-apk's brittle SDK discovery.
|
||||
|
||||
Keep in sync with:
|
||||
* Cargo.toml: package, min_sdk_version, target_sdk_version,
|
||||
uses_feature, uses_permission, application label/icon,
|
||||
activity orientation
|
||||
* [lib].name (currently "solitaire_app") — matches the
|
||||
`android.app.lib_name` meta-data value below, which is the
|
||||
shared object name without the `lib` prefix or `.so` suffix.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.solitairequest.app"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="26"
|
||||
android:targetSdkVersion="34" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:label="Ferrous Solitaire"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:hasCode="false">
|
||||
|
||||
<activity
|
||||
android:name="android.app.NativeActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|navigation|screenLayout">
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="solitaire_app" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
Reference in New Issue
Block a user