Compare commits
29 Commits
01d6b27e61
...
v0.25.7
| Author | SHA1 | Date | |
|---|---|---|---|
| 27eed98922 | |||
| f304917d62 | |||
| d49c478efa | |||
| 29f9b9358e | |||
| 9ef5759f40 | |||
| 9c9c0c76d3 | |||
| d4fb9e36a8 | |||
| ab35fcf906 | |||
| 32991301dd | |||
| c5fd928dcb | |||
| f6907671be | |||
| a54fff7257 | |||
| 533bcec2d8 | |||
| ba786f5a09 | |||
| 7ee7cb6d93 | |||
| 14324b09ef | |||
| 124f1f5cf5 | |||
| a6a73b5f36 | |||
| b84fe79806 | |||
| 3248f00d66 | |||
| c680a043ae | |||
| d0ab7ed97b | |||
| 1144a96757 | |||
| ac6668cee7 | |||
| eba1f66b45 | |||
| 90959728b1 | |||
| 8b30f8778b | |||
| d6a7924f14 | |||
| 4db43fb3fb |
@@ -12,8 +12,10 @@ on:
|
|||||||
- '**.md'
|
- '**.md'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
ANDROID_SDK: /opt/android-sdk
|
||||||
NDK_VERSION: "25.2.9519653"
|
NDK_VERSION: "25.2.9519653"
|
||||||
BUILD_TOOLS_VERSION: "34.0.0"
|
BUILD_TOOLS_VERSION: "34.0.0"
|
||||||
|
PLATFORM: "android-34"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-apk:
|
build-apk:
|
||||||
@@ -27,59 +29,36 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT"
|
run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# ── Probe the container's existing Android SDK ─────────────────────
|
# ── System dependencies ────────────────────────────────────────────
|
||||||
- name: Detect Android SDK
|
|
||||||
id: sdk
|
|
||||||
run: |
|
|
||||||
# Gitea/GitHub ubuntu-latest runners may already have an Android
|
|
||||||
# SDK at /usr/local/lib/android/sdk. If it has the platform and
|
|
||||||
# NDK we need we use it directly; otherwise we install our own.
|
|
||||||
DEFAULT="${ANDROID_HOME:-/usr/local/lib/android/sdk}"
|
|
||||||
echo "container default ANDROID_HOME: $DEFAULT"
|
|
||||||
echo "sdk_path=$DEFAULT" >> "$GITHUB_OUTPUT"
|
|
||||||
ls "$DEFAULT/platforms/" 2>/dev/null || echo "(no platforms/ in default)"
|
|
||||||
ls "$DEFAULT/ndk/" 2>/dev/null || echo "(no ndk/ in default)"
|
|
||||||
|
|
||||||
# ── System dependencies (always needed for build tools + signing) ──
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y openjdk-17-jdk-headless unzip jq
|
sudo apt-get install -y openjdk-17-jdk-headless unzip zip
|
||||||
|
|
||||||
# ── Install missing SDK components if needed ───────────────────────
|
# ── Android SDK (shared cache key with release workflow) ──────────
|
||||||
- name: Install SDK platform and NDK
|
- name: Cache Android SDK
|
||||||
env:
|
uses: actions/cache@v4
|
||||||
ANDROID_SDK: ${{ steps.sdk.outputs.sdk_path }}
|
id: sdk-cache
|
||||||
|
with:
|
||||||
|
path: ${{ env.ANDROID_SDK }}
|
||||||
|
key: v2-android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Android SDK + NDK
|
||||||
|
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
# Ensure sdkmanager is on PATH
|
sudo mkdir -p ${{ env.ANDROID_SDK }}/cmdline-tools
|
||||||
SDKMAN="$ANDROID_SDK/cmdline-tools/latest/bin/sdkmanager"
|
curl -sL \
|
||||||
if [ ! -f "$SDKMAN" ]; then
|
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
||||||
echo "sdkmanager not found — installing cmdline-tools"
|
-o /tmp/cmdtools.zip
|
||||||
mkdir -p "$ANDROID_SDK/cmdline-tools"
|
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
||||||
curl -sL \
|
sudo mv /tmp/cmdtools/cmdline-tools ${{ env.ANDROID_SDK }}/cmdline-tools/latest
|
||||||
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
yes | sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||||
-o /tmp/cmdtools.zip
|
--sdk_root=${{ env.ANDROID_SDK }} --licenses > /dev/null 2>&1 || true
|
||||||
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||||
sudo mv /tmp/cmdtools/cmdline-tools "$ANDROID_SDK/cmdline-tools/latest"
|
--sdk_root=${{ env.ANDROID_SDK }} \
|
||||||
fi
|
"build-tools;${{ env.BUILD_TOOLS_VERSION }}" \
|
||||||
|
"platforms;${{ env.PLATFORM }}" \
|
||||||
yes | sudo "$SDKMAN" --sdk_root="$ANDROID_SDK" --licenses \
|
"ndk;${{ env.NDK_VERSION }}"
|
||||||
> /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
NEEDS=""
|
|
||||||
[ ! -f "$ANDROID_SDK/platforms/android-34/android.jar" ] && NEEDS="$NEEDS platforms;android-34"
|
|
||||||
[ ! -d "$ANDROID_SDK/ndk/$NDK_VERSION" ] && NEEDS="$NEEDS ndk;$NDK_VERSION"
|
|
||||||
[ ! -d "$ANDROID_SDK/build-tools/$BUILD_TOOLS_VERSION" ] && NEEDS="$NEEDS build-tools;$BUILD_TOOLS_VERSION"
|
|
||||||
|
|
||||||
if [ -n "$NEEDS" ]; then
|
|
||||||
echo "Installing:$NEEDS"
|
|
||||||
sudo "$SDKMAN" --sdk_root="$ANDROID_SDK" $NEEDS
|
|
||||||
else
|
|
||||||
echo "All SDK components already present"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "ANDROID_HOME=$ANDROID_SDK" >> "$GITHUB_ENV"
|
|
||||||
echo "ANDROID_NDK_HOME=$ANDROID_SDK/ndk/$NDK_VERSION" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
# ── Rust toolchain ─────────────────────────────────────────────────
|
# ── Rust toolchain ─────────────────────────────────────────────────
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
@@ -106,12 +85,12 @@ jobs:
|
|||||||
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
restore-keys: cargo-registry-
|
restore-keys: cargo-registry-
|
||||||
|
|
||||||
- name: Cache cargo-apk binary
|
- name: Cache cargo-ndk binary
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
id: apk-tool-cache
|
id: ndk-tool-cache
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/bin/cargo-apk
|
path: ~/.cargo/bin/cargo-ndk
|
||||||
key: cargo-apk-${{ runner.os }}-stable
|
key: cargo-ndk-${{ runner.os }}-stable
|
||||||
|
|
||||||
- name: Cache build artifacts
|
- name: Cache build artifacts
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -120,17 +99,32 @@ jobs:
|
|||||||
key: android-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }}
|
key: android-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }}
|
||||||
restore-keys: android-target-${{ hashFiles('**/Cargo.lock') }}-
|
restore-keys: android-target-${{ hashFiles('**/Cargo.lock') }}-
|
||||||
|
|
||||||
# ── Build ──────────────────────────────────────────────────────────
|
- name: Install cargo-ndk
|
||||||
- name: Install cargo-apk
|
if: steps.ndk-tool-cache.outputs.cache-hit != 'true'
|
||||||
if: steps.apk-tool-cache.outputs.cache-hit != 'true'
|
run: cargo install cargo-ndk --locked
|
||||||
run: cargo install cargo-apk --locked
|
|
||||||
|
|
||||||
|
# ── Build APK ──────────────────────────────────────────────────────
|
||||||
|
# Debug CI only builds arm64-v8a — full three-ABI debug builds blow
|
||||||
|
# past the runner's disk budget (~25 GB of target/ + intermediate
|
||||||
|
# APKs caused apksigner to OOM-on-disk in the previous run). Release
|
||||||
|
# CI still ships all three ABIs from android-release.yml.
|
||||||
- name: Build debug APK
|
- name: Build debug APK
|
||||||
run: cargo apk 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
|
||||||
|
ABIS: arm64-v8a
|
||||||
|
run: ./scripts/build_android_apk.sh
|
||||||
|
|
||||||
# ── Artifact ───────────────────────────────────────────────────────
|
# ── Artifact ───────────────────────────────────────────────────────
|
||||||
|
# Pinned to v3 because Gitea Actions doesn't implement the github.com
|
||||||
|
# artifact service that upload-artifact@v4+ requires; v3 uses the
|
||||||
|
# older chunked HTTP API that Gitea's GHES-compatibility layer
|
||||||
|
# supports.
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: solitaire-quest-debug-${{ steps.meta.outputs.sha }}
|
name: solitaire-quest-debug-${{ steps.meta.outputs.sha }}
|
||||||
path: target/debug/apk/solitaire-quest.apk
|
path: target/debug/apk/solitaire-quest.apk
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ on:
|
|||||||
- 'v*.*.*'
|
- 'v*.*.*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ANDROID_HOME: /opt/android-sdk
|
ANDROID_SDK: /opt/android-sdk
|
||||||
ANDROID_NDK_HOME: /opt/android-sdk/ndk/25.2.9519653
|
|
||||||
NDK_VERSION: "25.2.9519653"
|
NDK_VERSION: "25.2.9519653"
|
||||||
BUILD_TOOLS_VERSION: "34.0.0"
|
BUILD_TOOLS_VERSION: "34.0.0"
|
||||||
|
PLATFORM: "android-34"
|
||||||
GITEA_API: https://git.aleshym.co/api/v1
|
GITEA_API: https://git.aleshym.co/api/v1
|
||||||
REPO: funman300/Rusty_Solitare
|
REPO: funman300/Rusty_Solitare
|
||||||
|
|
||||||
@@ -25,39 +25,38 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
run: echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
run: echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# ── Android SDK + NDK ──────────────────────────────────────────────
|
# ── System dependencies ────────────────────────────────────────────
|
||||||
# Shared cache key with the debug workflow so a warm debug run
|
- name: Install system dependencies
|
||||||
# saves the ~2 GB SDK download for the release run too.
|
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
|
- name: Cache Android SDK
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
id: sdk-cache
|
id: sdk-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ env.ANDROID_HOME }}
|
path: ${{ env.ANDROID_SDK }}
|
||||||
key: v2-android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }}
|
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
|
- name: Install Android SDK + NDK
|
||||||
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
sudo mkdir -p ${{ env.ANDROID_SDK }}/cmdline-tools
|
||||||
curl -sL \
|
curl -sL \
|
||||||
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
||||||
-o /tmp/cmdtools.zip
|
-o /tmp/cmdtools.zip
|
||||||
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
||||||
mv /tmp/cmdtools/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest"
|
sudo mv /tmp/cmdtools/cmdline-tools ${{ env.ANDROID_SDK }}/cmdline-tools/latest
|
||||||
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" --licenses \
|
yes | sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||||
> /dev/null 2>&1 || true
|
--sdk_root=${{ env.ANDROID_SDK }} --licenses > /dev/null 2>&1 || true
|
||||||
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" \
|
sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||||
"build-tools;$BUILD_TOOLS_VERSION" \
|
--sdk_root=${{ env.ANDROID_SDK }} \
|
||||||
"platforms;android-34" \
|
"build-tools;${{ env.BUILD_TOOLS_VERSION }}" \
|
||||||
"ndk;$NDK_VERSION"
|
"platforms;${{ env.PLATFORM }}" \
|
||||||
|
"ndk;${{ env.NDK_VERSION }}"
|
||||||
|
|
||||||
# ── Rust toolchain ─────────────────────────────────────────────────
|
# ── Rust toolchain ─────────────────────────────────────────────────
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
run: |
|
run: |
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||||
@@ -82,12 +81,12 @@ jobs:
|
|||||||
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
restore-keys: cargo-registry-
|
restore-keys: cargo-registry-
|
||||||
|
|
||||||
- name: Cache cargo-apk binary
|
- name: Cache cargo-ndk binary
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
id: apk-tool-cache
|
id: ndk-tool-cache
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/bin/cargo-apk
|
path: ~/.cargo/bin/cargo-ndk
|
||||||
key: cargo-apk-${{ runner.os }}-stable
|
key: cargo-ndk-${{ runner.os }}-stable
|
||||||
|
|
||||||
- name: Cache build artifacts
|
- name: Cache build artifacts
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -96,48 +95,43 @@ jobs:
|
|||||||
key: android-release-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }}
|
key: android-release-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }}
|
||||||
restore-keys: android-release-target-${{ hashFiles('**/Cargo.lock') }}-
|
restore-keys: android-release-target-${{ hashFiles('**/Cargo.lock') }}-
|
||||||
|
|
||||||
# ── Build ──────────────────────────────────────────────────────────
|
- name: Install cargo-ndk
|
||||||
- name: Install cargo-apk
|
if: steps.ndk-tool-cache.outputs.cache-hit != 'true'
|
||||||
if: steps.apk-tool-cache.outputs.cache-hit != 'true'
|
run: cargo install cargo-ndk --locked
|
||||||
run: cargo install cargo-apk --locked
|
|
||||||
|
|
||||||
- name: Build release APK
|
# ── Build & sign with release keystore ─────────────────────────────
|
||||||
run: cargo apk build --release --package solitaire_app --lib
|
|
||||||
|
|
||||||
# ── Sign ───────────────────────────────────────────────────────────
|
|
||||||
- name: Decode keystore
|
- name: Decode keystore
|
||||||
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > /tmp/solitaire-release.jks
|
|
||||||
|
|
||||||
- name: Align and sign APK
|
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
secret_len=$(echo -n "${{ secrets.KEYSTORE_BASE64 }}" | wc -c)
|
||||||
UNSIGNED="target/release/apk/solitaire-quest.apk"
|
echo "KEYSTORE_BASE64 secret length: ${secret_len} chars"
|
||||||
ALIGNED="/tmp/solitaire-quest-aligned.apk"
|
set +e
|
||||||
SIGNED="ferrous-solitaire-${TAG}.apk"
|
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > /tmp/solitaire-release.jks 2>/tmp/b64_err.txt
|
||||||
|
b64_exit=$?
|
||||||
|
set -e
|
||||||
|
size=$(wc -c < /tmp/solitaire-release.jks)
|
||||||
|
echo "base64 exit code: ${b64_exit}, keystore size: ${size} bytes"
|
||||||
|
[ -s /tmp/b64_err.txt ] && echo "base64 error: $(cat /tmp/b64_err.txt)" || true
|
||||||
|
[ "$size" -gt 0 ] || { echo "ERROR: KEYSTORE_BASE64 is empty or invalid base64"; exit 1; }
|
||||||
|
|
||||||
"$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS_VERSION/zipalign" -v 4 \
|
- name: Build signed release APK
|
||||||
"$UNSIGNED" "$ALIGNED"
|
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
|
||||||
|
|
||||||
"$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS_VERSION/apksigner" sign \
|
# ── Publish to Gitea release ───────────────────────────────────────
|
||||||
--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 }}"
|
|
||||||
"$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS_VERSION/apksigner" verify \
|
|
||||||
--verbose "ferrous-solitaire-${TAG}.apk"
|
|
||||||
|
|
||||||
# ── Publish ────────────────────────────────────────────────────────
|
|
||||||
- name: Create Gitea release
|
- name: Create Gitea release
|
||||||
id: release
|
id: release
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
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}" \
|
RESPONSE=$(curl -s -o /tmp/release.json -w "%{http_code}" \
|
||||||
-X POST "$GITEA_API/repos/$REPO/releases" \
|
-X POST "$GITEA_API/repos/$REPO/releases" \
|
||||||
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ resources:
|
|||||||
images:
|
images:
|
||||||
- name: solitaire-server
|
- name: solitaire-server
|
||||||
newName: git.aleshym.co/funman300/solitaire-server
|
newName: git.aleshym.co/funman300/solitaire-server
|
||||||
newTag: bb670d6c
|
newTag: 9ef5759f
|
||||||
|
|||||||
Executable
+140
@@ -0,0 +1,140 @@
|
|||||||
|
#!/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"
|
||||||
|
# ABIS Space-separated Android ABIs to build (default:
|
||||||
|
# "arm64-v8a armeabi-v7a x86_64"). Reduce in CI to
|
||||||
|
# fit the runner's disk budget — a full three-ABI
|
||||||
|
# debug build can exceed 25 GB of target/ output.
|
||||||
|
# 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}"
|
||||||
|
ABIS="${ABIS:-arm64-v8a armeabi-v7a x86_64}"
|
||||||
|
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=( --platform 26 -o "$STAGING/lib" )
|
||||||
|
for abi in $ABIS; do
|
||||||
|
CARGO_NDK_ARGS+=( -t "$abi" )
|
||||||
|
done
|
||||||
|
CARGO_NDK_ARGS+=( 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"
|
||||||
|
# Free the unsigned intermediate now — apksigner reads $app-aligned.apk and
|
||||||
|
# writes $APK_OUT, and the runner's disk is tight after a multi-ABI build.
|
||||||
|
rm -f "$STAGING/app-unsigned.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>
|
||||||
@@ -143,10 +143,10 @@ pub struct Settings {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub window_geometry: Option<WindowGeometry>,
|
pub window_geometry: Option<WindowGeometry>,
|
||||||
/// Identifier of the active card-art theme. Matches `meta.id` from
|
/// Identifier of the active card-art theme. Matches `meta.id` from
|
||||||
/// the theme's `theme.ron` manifest. `"classic"` and `"dark"` are
|
/// the theme's `theme.ron` manifest. `"dark"` and `"classic"` are
|
||||||
/// always present; user-supplied themes register under their own ids.
|
/// always present; user-supplied themes register under their own ids.
|
||||||
/// Older `settings.json` files that stored `"default"` will fall
|
/// Older `settings.json` files that stored `"default"` or `"classic"`
|
||||||
/// back to the dark embedded theme at runtime.
|
/// are migrated to `"dark"` by [`Settings::sanitized`].
|
||||||
#[serde(default = "default_theme_id")]
|
#[serde(default = "default_theme_id")]
|
||||||
pub selected_theme_id: String,
|
pub selected_theme_id: String,
|
||||||
/// Set to `true` once the achievement-onboarding info-toast has been
|
/// Set to `true` once the achievement-onboarding info-toast has been
|
||||||
@@ -272,7 +272,7 @@ fn default_music_volume() -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_theme_id() -> String {
|
fn default_theme_id() -> String {
|
||||||
"classic".to_string()
|
"dark".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default tooltip-hover dwell delay in seconds. Mirrors
|
/// Default tooltip-hover dwell delay in seconds. Mirrors
|
||||||
@@ -395,6 +395,13 @@ impl Settings {
|
|||||||
/// their respective ranges after deserialization or hand-editing of
|
/// their respective ranges after deserialization or hand-editing of
|
||||||
/// `settings.json`.
|
/// `settings.json`.
|
||||||
pub fn sanitized(self) -> Self {
|
pub fn sanitized(self) -> Self {
|
||||||
|
// Migrate stale theme IDs: "default" was removed when the theme was
|
||||||
|
// renamed to "dark"; "classic" was briefly the default before "dark"
|
||||||
|
// was restored as the shipped default.
|
||||||
|
let selected_theme_id = match self.selected_theme_id.as_str() {
|
||||||
|
"default" | "classic" => "dark".to_string(),
|
||||||
|
_ => self.selected_theme_id,
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
sfx_volume: self.sfx_volume.clamp(0.0, 1.0),
|
sfx_volume: self.sfx_volume.clamp(0.0, 1.0),
|
||||||
music_volume: self.music_volume.clamp(0.0, 1.0),
|
music_volume: self.music_volume.clamp(0.0, 1.0),
|
||||||
@@ -407,6 +414,7 @@ impl Settings {
|
|||||||
replay_move_interval_secs: self
|
replay_move_interval_secs: self
|
||||||
.replay_move_interval_secs
|
.replay_move_interval_secs
|
||||||
.clamp(REPLAY_MOVE_INTERVAL_MIN_SECS, REPLAY_MOVE_INTERVAL_MAX_SECS),
|
.clamp(REPLAY_MOVE_INTERVAL_MIN_SECS, REPLAY_MOVE_INTERVAL_MAX_SECS),
|
||||||
|
selected_theme_id,
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ fn build_registry_on_startup(mut registry: bevy::ecs::system::ResMut<ThemeRegist
|
|||||||
/// [`user_theme_dir`].
|
/// [`user_theme_dir`].
|
||||||
pub fn build_registry(user_dir: &Path) -> ThemeRegistry {
|
pub fn build_registry(user_dir: &Path) -> ThemeRegistry {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
entries.push(classic_entry());
|
|
||||||
entries.push(dark_entry());
|
entries.push(dark_entry());
|
||||||
|
entries.push(classic_entry());
|
||||||
entries.extend(discover_user_themes(user_dir));
|
entries.extend(discover_user_themes(user_dir));
|
||||||
ThemeRegistry { entries }
|
ThemeRegistry { entries }
|
||||||
}
|
}
|
||||||
@@ -264,8 +264,8 @@ mod tests {
|
|||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let registry = build_registry(tmp.path());
|
let registry = build_registry(tmp.path());
|
||||||
assert_eq!(registry.len(), BUNDLED_COUNT);
|
assert_eq!(registry.len(), BUNDLED_COUNT);
|
||||||
assert_eq!(registry.entries[0].id, "classic");
|
assert_eq!(registry.entries[0].id, "dark");
|
||||||
assert_eq!(registry.entries[1].id, "dark");
|
assert_eq!(registry.entries[1].id, "classic");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user