diff --git a/.github/actions/build-library-and-upload/action.yml b/.github/actions/build-library-and-upload/action.yml index c59cf95aa..5a7bb4fbc 100644 --- a/.github/actions/build-library-and-upload/action.yml +++ b/.github/actions/build-library-and-upload/action.yml @@ -16,7 +16,7 @@ runs: - id: build run: | rm -rf out - cmake --preset ${{ inputs.preset }} -DBUILD_SHARED_LIBS=ON + cmake --preset ${{ inputs.preset }} cmake --build ./build/${{ inputs.preset }} --config ${{ inputs.build-type }} --verbose cmake --install ./build/${{ inputs.preset }} --config ${{ inputs.build-type }} working-directory: ${{ inputs.nakama-cpp-path }} diff --git a/.github/actions/setup-android/action.yml b/.github/actions/setup-android/action.yml new file mode 100644 index 000000000..9d5210fb6 --- /dev/null +++ b/.github/actions/setup-android/action.yml @@ -0,0 +1,85 @@ +name: 'Setup Android' +description: 'Sets up Java and Android SDK components' +inputs: + java-version: + description: 'Java version' + required: false + default: '17' + ndk-version: + description: 'Android NDK version' + required: true + cmake-version: + description: 'Android CMake version' + required: true + install-emulator: + description: 'Whether to install emulator packages' + required: false + default: 'false' + system-image: + description: 'Android system image package' + required: false + default: '' + +runs: + using: "composite" + steps: + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ inputs.java-version }} + + - name: Install Android SDK packages + shell: bash + run: | + set -euo pipefail + + # Initialize ANDROID_HOME if not set (standard on x86, missing on ARM) + if [ -z "${ANDROID_HOME:-}" ]; then + export ANDROID_HOME="$HOME/android-sdk" + mkdir -p "$ANDROID_HOME" + fi + echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV" + + # Install command-line tools if missing + if [ ! -d "$ANDROID_HOME/cmdline-tools/latest" ]; then + echo "Downloading Android Command Line Tools..." + mkdir -p "$ANDROID_HOME/cmdline-tools" + curl -LSs https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o cmdline-tools.zip + unzip -q cmdline-tools.zip -d "$ANDROID_HOME/cmdline-tools" + mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest" + rm cmdline-tools.zip + fi + + SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" + + packages=( + "ndk;${{ inputs.ndk-version }}" + "cmake;${{ inputs.cmake-version }}" + ) + + #use emulator for test-android + if [ "${{ inputs.install-emulator }}" = "true" ]; then + packages+=("emulator") + packages+=("platform-tools") + if [ -n "${{ inputs.system-image }}" ]; then + packages+=("${{ inputs.system-image }}") + fi + fi + + # Accept licenses + yes | "$SDKMANAGER" --sdk_root="$ANDROID_HOME" --licenses || true + + "$SDKMANAGER" --sdk_root="$ANDROID_HOME" "${packages[@]}" + + - name: Set Android environment variables + shell: bash + run: | + export NDK_PATH="$ANDROID_HOME/ndk/${{ inputs.ndk-version }}" + echo "ANDROID_NDK=$NDK_PATH" >> "$GITHUB_ENV" + echo "ANDROID_NDK_HOME=$NDK_PATH" >> "$GITHUB_ENV" + echo "ANDROID_NDK_ROOT=$NDK_PATH" >> "$GITHUB_ENV" + echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV" + echo "$ANDROID_HOME/cmake/${{ inputs.cmake-version }}/bin" >> "$GITHUB_PATH" + echo "$ANDROID_HOME/platform-tools" >> "$GITHUB_PATH" + echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> "$GITHUB_PATH" \ No newline at end of file diff --git a/.github/actions/setup-vcpkg-android/action.yml b/.github/actions/setup-vcpkg-android/action.yml new file mode 100644 index 000000000..47e838655 --- /dev/null +++ b/.github/actions/setup-vcpkg-android/action.yml @@ -0,0 +1,28 @@ +name: 'Setup vcpkg for Android' +description: 'Setup vcpkg for Android workflow' +inputs: + vcpkg-path: + description: 'Path to vcpkg' + required: true + +runs: + using: "composite" + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Set VCPKG_ROOT and Binary Sources + shell: bash + run: | + echo "VCPKG_ROOT=${{ github.workspace }}/${{ inputs.vcpkg-path }}" >> $GITHUB_ENV + echo "VCPKG_BINARY_SOURCES=clear;x-gha,readwrite" >> $GITHUB_ENV + + - name: Bootstrap vcpkg + shell: bash + run: ./bootstrap-vcpkg.sh -disableMetrics + working-directory: ${{ inputs.vcpkg-path }} + diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index eed9994bc..e058d6934 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -1,29 +1,38 @@ name: Build Android on: [workflow_call, workflow_dispatch] + jobs: build_android: timeout-minutes: 30 - strategy: - matrix: - preset: ["android-arm64-v8a-host_linux-x64", "android-x64-host_linux-x64", "android-armeabi-v7a-host_linux-x64"] - build-type: [MinSizeRel] runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - name: Checkout nakama-cpp + uses: actions/checkout@v4 with: path: nakama-cpp - - uses: ./nakama-cpp/.github/actions/setup-vcpkg + submodules: false + + - name: Checkout vcpkg submodule + run: | + git submodule update --init --filter=blob:none -- submodules/vcpkg + working-directory: nakama-cpp + + - uses: ./nakama-cpp/.github/actions/setup-android with: - github_token: ${{ secrets.github_token }} - vcpkg-path: vcpkg - - uses: ./nakama-cpp/.github/actions/setup-ubuntu - - uses: ./nakama-cpp/.github/actions/build-library-and-upload + ndk-version: 27.2.12479018 + cmake-version: 4.0.2 + + - uses: ./nakama-cpp/.github/actions/setup-vcpkg-android with: - nakama-cpp-path: nakama-cpp - preset: ${{ matrix.preset }} - build-type: ${{ matrix.build-type }} - - if: failure() - uses: ./nakama-cpp/.github/actions/handle-failure + vcpkg-path: nakama-cpp/submodules/vcpkg + + - name : Build with Gradle + run : ./gradlew build --no-daemon + working-directory: nakama-cpp/android + + - name: Upload artifacts + uses: actions/upload-artifact@v4 with: - nakama-cpp-path: nakama-cpp - vcpkg-path: vcpkg \ No newline at end of file + name: nakama-sdk-android-aar + path: nakama-cpp/android/nakama-sdk/build/outputs/aar/*.aar + if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/test_android.yml b/.github/workflows/test_android.yml new file mode 100644 index 000000000..0fb47bc38 --- /dev/null +++ b/.github/workflows/test_android.yml @@ -0,0 +1,63 @@ +name: Test Android +on: [workflow_call, workflow_dispatch, pull_request] +jobs: + test_android: + timeout-minutes: 120 + runs-on: ubuntu-22.04 + env: + ABI: x86_64 + CMAKE_PRESET: android-x86_64 + + steps: + - name: Checkout nakama-cpp + uses: actions/checkout@v4 + with: + path: nakama-cpp + submodules: false + + - name: Checkout vcpkg submodule + run: | + git submodule update --init --filter=blob:none -- submodules/vcpkg + working-directory: nakama-cpp + + - uses: ./nakama-cpp/.github/actions/setup-android + with: + ndk-version: 27.2.12479018 + cmake-version: 4.0.2 + install-emulator: true + system-image: "system-images;android-29;google_apis;x86_64" + + - uses: ./nakama-cpp/.github/actions/setup-vcpkg-android + with: + vcpkg-path: nakama-cpp/submodules/vcpkg + + - name: Start Nakama server + run: docker compose -f integrationtests/server/docker-compose.yml up -d --build --wait + working-directory: nakama-cpp + + - name: Install Task + run: npm install -g @go-task/cli + + - name: Build Release AAR + run: ./gradlew :nakama-sdk:assembleRelease -Pabi=$ABI + working-directory: nakama-cpp/android + + - name: Upload Release AAR + uses: actions/upload-artifact@v4 + with: + name: nakama-sdk-android-aar-${{ env.ABI }} + path: nakama-cpp/android/nakama-sdk/build/outputs/aar/*-release.aar + if-no-files-found: error + + - name: Run tests on emulator + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a + with: + api-level: 29 + target: google_apis + arch: x86_64 + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 -cores 2 + disable-animations: true + timeout: 50 + script: | + set -xe && for port in 7349 7350 7351; do adb reverse tcp:$port tcp:$port; done && cd nakama-cpp && sleep 30 && task test-android ABI=$ABI BUILD_TYPE=MinSizeRel diff --git a/CMakePresets.json b/CMakePresets.json index 34a2042dc..26e940ea7 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -114,6 +114,14 @@ "ANDROID_ABI": "armeabi-v7a" } }, + { + "name": "android-x86_64", + "inherits": ["android-default"], + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x86-64-android-heroic", + "ANDROID_ABI": "x86_64" + } + }, { "name": "linux-amd64", "inherits": ["default"], diff --git a/Taskfile.yml b/Taskfile.yml index 3aeba4162..6e402c075 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -64,259 +64,11 @@ tasks: test-android: desc: Build and run Android integration tests (optional DEVICE=, ABI=arm64-v8a|armeabi-v7a) - vars: - ABI: '{{default "arm64-v8a" .ABI}}' - DEVICE: '{{default "" .DEVICE}}' - CMAKE_PRESET: "android-{{.ABI}}" + env: + ABI: '{{.ABI}}' + CMAKE_PRESET: '{{.CMAKE_PRESET}}' + VCPKG_ROOT: '{{.VCPKG_ROOT}}' + ANDROID_NDK_HOME: '{{.ANDROID_NDK_HOME}}' + DEVICE: '{{.DEVICE}}' cmds: - - docker compose -f integrationtests/server/docker-compose.yml up -d --build --wait - - | - set -euo pipefail - - HOST_OS="{{OS}}" - - # Portable sleep (Taskfile's Windows shell lacks sleep/read -t) - _sleep() { - {{if eq OS "windows"}}powershell.exe -Command "Start-Sleep -Seconds $1"{{else}}sleep "$1"{{end}} - } - - # --- Resolve ANDROID_HOME --- - if [ -z "${ANDROID_HOME:-}" ]; then - case "$HOST_OS" in - windows) - _localappdata="${LOCALAPPDATA:-}" - if [ -n "$_localappdata" ] && [ -d "$_localappdata/Android/Sdk" ]; then - export ANDROID_HOME="$_localappdata/Android/Sdk" - fi;; - darwin) - if [ -d "$HOME/Library/Android/sdk" ]; then - export ANDROID_HOME="$HOME/Library/Android/sdk" - fi;; - esac - fi - if [ -z "${ANDROID_HOME:-}" ]; then - echo "Error: ANDROID_HOME not set and could not be auto-detected." - exit 1 - fi - ANDROID_HOME="${ANDROID_HOME//\\//}" - - # --- Resolve adb --- - if command -v adb &>/dev/null; then - ADB="adb" - else - ADB="$ANDROID_HOME/platform-tools/adb" - if [ ! -x "$ADB" ] && [ -x "${ADB}.exe" ]; then - ADB="${ADB}.exe" - fi - fi - - # --- Resolve ANDROID_NDK_HOME --- - if [ -z "${ANDROID_NDK_HOME:-}" ]; then - if [ -d "$ANDROID_HOME/ndk" ]; then - for _d in "$ANDROID_HOME/ndk"/*/; do [ -d "$_d" ] && ANDROID_NDK_HOME="${_d%/}"; done - fi - if [ -z "${ANDROID_NDK_HOME:-}" ] || [ ! -d "${ANDROID_NDK_HOME}" ]; then - echo "Error: Could not find Android NDK. Set ANDROID_NDK_HOME." - exit 1 - fi - echo "Auto-detected NDK: $ANDROID_NDK_HOME" - export ANDROID_NDK_HOME - fi - ANDROID_NDK_HOME="${ANDROID_NDK_HOME//\\//}" - - # --- JDK 17 detection (Gradle 7.x requires it) --- - _need_jdk17=false - if [ -n "${JAVA_HOME:-}" ]; then - _jv=$("$JAVA_HOME/bin/java" -version 2>&1); _jv=${_jv#*\"}; _jv=${_jv%%.*} - [ "${_jv:-0}" -gt 17 ] 2>/dev/null && _need_jdk17=true || true - elif command -v java &>/dev/null; then - _jv=$(java -version 2>&1); _jv=${_jv#*\"}; _jv=${_jv%%.*} - [ "${_jv:-0}" -gt 17 ] 2>/dev/null && _need_jdk17=true || true - fi - if [ "$_need_jdk17" = true ]; then - _found="" - case "$HOST_OS" in - windows) - for _d in "/c/Program Files/Eclipse Adoptium"/jdk-17.*; do - [ -d "$_d" ] && _found="$_d" && break - done;; - darwin) - for _d in /Library/Java/JavaVirtualMachines/temurin-17.*/Contents/Home; do - [ -d "$_d" ] && _found="$_d" && break - done;; - linux) - for _d in /usr/lib/jvm/temurin-17-* /usr/lib/jvm/java-17-*; do - [ -d "$_d" ] && _found="$_d" && break - done;; - esac - if [ -n "$_found" ]; then - echo "Auto-detected JDK 17: $_found" - export JAVA_HOME="$_found" - else - echo "Warning: Java ${_jv} detected but Gradle 7.x needs JDK 17. Set JAVA_HOME to JDK 17." - fi - fi - - # --- Build native libraries --- - echo "=== Building native libraries (preset: {{.CMAKE_PRESET}}) ===" - - cmake --preset "{{.CMAKE_PRESET}}" \ - -DBUILD_TESTING=ON \ - -DCMAKE_ANDROID_NDK="$ANDROID_NDK_HOME" \ - -DCMAKE_ANDROID_ARCH_ABI="{{.ABI}}" \ - -DCMAKE_MAKE_PROGRAM="$(command -v ninja)" - - cmake --build "build/{{.CMAKE_PRESET}}" --config Debug \ - --target nakama-sdk nakama-test - - # --- Stage native libraries for Gradle --- - echo "=== Staging native libraries ===" - - jni_dir="integrationtests/android/jniLibs/{{.ABI}}" - mkdir -p "$jni_dir" - cp "build/{{.CMAKE_PRESET}}/Debug/libnakama-sdk.so" "$jni_dir/" - cp "build/{{.CMAKE_PRESET}}/integrationtests/Debug/libnakama-test.so" "$jni_dir/" - - case "{{.ABI}}" in - arm64-v8a) triple="aarch64-linux-android";; - armeabi-v7a) triple="arm-linux-androideabi";; - esac - stl_lib="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/${HOST_OS}-x86_64/sysroot/usr/lib/${triple}/libc++_shared.so" - if [ -f "$stl_lib" ]; then - cp "$stl_lib" "$jni_dir/" - else - echo "Warning: libc++_shared.so not found at $stl_lib" - fi - - # --- Build APK --- - echo "=== Building APK ===" - cd integrationtests/android - {{if eq OS "windows"}}cmd.exe /c gradlew.bat{{else}}./gradlew{{end}} assembleCustomDebugType -Pabi="{{.ABI}}" - - # --- Resolve device serial --- - PACKAGE="com.heroiclabs.nakamatest" - ACTIVITY="${PACKAGE}/.MainActivity" - TIMEOUT=300 - LOG_TAG="nakama" - APK_PATH="build/outputs/apk/customDebugType/nakamatest-customDebugType.apk" - - parse_devices() { - devices=() - local output - output=$("$ADB" devices) - while IFS= read -r line; do - if [[ "$line" == *$'\t'device ]]; then - devices+=("${line%%$'\t'*}") - fi - done <<< "$output" - } - - serial="{{.DEVICE}}" - if [ -z "$serial" ]; then - parse_devices - - if [ ${#devices[@]} -eq 0 ]; then - echo "No connected devices/emulators found. Starting emulator..." - EMULATOR="$ANDROID_HOME/emulator/emulator" - if [ ! -x "$EMULATOR" ] && [ -x "${EMULATOR}.exe" ]; then - EMULATOR="${EMULATOR}.exe" - fi - avd=$("$EMULATOR" -list-avds) - avd="${avd%%$'\n'*}" - avd="${avd//$'\r'/}" - if [ -z "$avd" ]; then - echo "Error: No AVDs found. Create one in Android Studio first." - exit 1 - fi - echo "Starting AVD: $avd" - {{if eq OS "windows"}}powershell.exe -Command "Start-Process -FilePath '${EMULATOR}' -ArgumentList '-avd','${avd}' -WindowStyle Hidden"{{else}}"$EMULATOR" -avd "$avd" &>/dev/null &{{end}} - echo "Waiting for device to come online..." - "$ADB" wait-for-device - while [[ "$("$ADB" shell getprop sys.boot_completed 2>/dev/null)" != *"1"* ]]; do - _sleep 2 - done - echo "Emulator booted." - parse_devices - fi - - if [ ${#devices[@]} -gt 1 ]; then - echo "Error: Multiple devices connected. Specify DEVICE=:" - "$ADB" devices - exit 1 - fi - serial="${devices[0]}" - echo "Auto-detected device: $serial" - fi - - adb_cmd() { - "$ADB" -s "$serial" "$@" - } - - # --- Port forwarding --- - echo "Setting up port forwarding..." - for port in 7349 7350 7351; do - adb_cmd reverse tcp:$port tcp:$port - done - - # --- Install APK --- - if [ ! -f "$APK_PATH" ]; then - echo "Error: APK not found at $APK_PATH" - exit 1 - fi - - echo "Installing APK..." - adb_cmd uninstall "$PACKAGE" 2>/dev/null || true - adb_cmd install -r "$APK_PATH" - - # --- Clear logcat and launch --- - adb_cmd logcat -c - echo "Launching $ACTIVITY..." - adb_cmd shell am start -n "$ACTIVITY" - - # --- Monitor logcat for test results --- - echo "Waiting for test results (timeout: ${TIMEOUT}s)..." - echo "---" - - logfile="logcat-output.tmp" - > "$logfile" - "$ADB" -s "$serial" logcat -s "${LOG_TAG}:V" -v raw > "$logfile" 2>/dev/null & - logcat_pid=$! - trap 'rm -f "$logfile"; kill $logcat_pid 2>/dev/null || true' EXIT - - result="" - SECONDS=0 - last_line=0 - - while [ "$SECONDS" -lt "$TIMEOUT" ]; do - line_num=0 - while IFS= read -r line; do - line_num=$((line_num + 1)) - if [ "$line_num" -le "$last_line" ]; then - continue - fi - echo "$line" - if [[ "$line" == *"Tests failed: 0"* ]]; then - result="passed" - elif [[ "$line" == *"Tests failed:"* ]]; then - result="failed" - fi - done < "$logfile" - last_line=$line_num - [ -n "$result" ] && break - _sleep 1 - done - - kill $logcat_pid 2>/dev/null || true - - echo "---" - - if [ -z "$result" ]; then - echo "TIMEOUT: Tests did not complete within ${TIMEOUT}s." - exit 1 - elif [ "$result" = "passed" ]; then - echo "ALL TESTS PASSED" - exit 0 - else - echo "TESTS FAILED" - exit 1 - fi + - bash integrationtests/android/test_android.sh \ No newline at end of file diff --git a/android/nakama-sdk/build.gradle.kts b/android/nakama-sdk/build.gradle.kts index ed59be4fb..b7a3f862b 100644 --- a/android/nakama-sdk/build.gradle.kts +++ b/android/nakama-sdk/build.gradle.kts @@ -94,7 +94,7 @@ android { // This is a prefab-only AAR, remove all jniLibs , which are just stripped version // of what we already include in prefab packaging { - jniLibs.excludes += ("**/*.so") + // jniLibs.excludes += ("**/*.so") } buildToolsVersion = "36.0.0" diff --git a/integrationtests/android/build.gradle b/integrationtests/android/build.gradle index bb51530aa..a452e00a5 100644 --- a/integrationtests/android/build.gradle +++ b/integrationtests/android/build.gradle @@ -16,35 +16,38 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' + classpath 'com.android.tools.build:gradle:8.11.0' } } plugins { - id('com.android.application') version '7.1.1' + id('com.android.application') version '8.11.0' } def abi = (project.findProperty("abi") ?: "arm64-v8a").toString() -def supportedAbis = ["arm64-v8a", "armeabi-v7a"] +def supportedAbis = ["arm64-v8a", "armeabi-v7a", "x86_64"] if (!supportedAbis.contains(abi)) { throw new GradleException("Unsupported abi '${abi}'. Supported values: ${supportedAbis}") } dependencies { implementation 'androidx.annotation:annotation:1.7.1' -} - -repositories { - google() - mavenCentral() - gradlePluginPortal() + implementation project(':nakama-sdk') } android { - compileSdkVersion 30 + namespace 'com.heroiclabs.nakamatest' + compileSdk 34 + ndkVersion "27.2.12479018" + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + defaultConfig { applicationId "com.heroiclabs.nakamatest" - minSdkVersion 21 - targetSdkVersion 31 + minSdk 28 + targetSdk 34 versionCode 1 versionName '1.0' ndk { @@ -56,13 +59,14 @@ android { // and the project's vcpkg-based toolchain. sourceSets { main { - java.srcDirs += '../../android/nakama-sdk/src/main/java' jniLibs.srcDirs = ['jniLibs'] } } buildTypes { customDebugType { debuggable true + signingConfig signingConfigs.debug + matchingFallbacks = ['debug', 'release'] } release { minifyEnabled false diff --git a/integrationtests/android/gradle/wrapper/gradle-wrapper.properties b/integrationtests/android/gradle/wrapper/gradle-wrapper.properties index bac04d6ef..57cb6703f 100644 --- a/integrationtests/android/gradle/wrapper/gradle-wrapper.properties +++ b/integrationtests/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists android.enablePrefab=true diff --git a/integrationtests/android/settings.gradle b/integrationtests/android/settings.gradle index d5244b5f5..a3a5a6227 100644 --- a/integrationtests/android/settings.gradle +++ b/integrationtests/android/settings.gradle @@ -6,4 +6,14 @@ pluginManagement { } } -rootProject.name = 'nakamatest' \ No newline at end of file +rootProject.name = 'nakamatest' +include ':nakama-sdk' +project(':nakama-sdk').projectDir = new File(settingsDir, '../../android/nakama-sdk') + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + repositories { + google() + mavenCentral() + } +} \ No newline at end of file diff --git a/integrationtests/android/src/main/java/com/heroiclabs/nakamatest/MainActivity.java b/integrationtests/android/src/main/java/com/heroiclabs/nakamatest/MainActivity.java index 1ee7de863..87497ba46 100644 --- a/integrationtests/android/src/main/java/com/heroiclabs/nakamatest/MainActivity.java +++ b/integrationtests/android/src/main/java/com/heroiclabs/nakamatest/MainActivity.java @@ -7,10 +7,22 @@ public class MainActivity extends Activity { @Override protected void onCreate(final Bundle savedInstanceState) { - // load sdk first so that it may initialize before the test application - Log.i("libnakama-test MainActivity", "Loading libnakama..."); - System.loadLibrary("nakama-sdk"); - Log.i("libnakama-test MainActivity", "Loading libnakama-test..."); - System.loadLibrary("nakama-test"); + super.onCreate(savedInstanceState); + + Log.i("libnakama-test MainActivity", "Starting tests in background thread..."); + + new Thread(new Runnable() { + @Override + public void run() { + try { + Log.i("libnakama-test MainActivity", "Loading libnakama..."); + System.loadLibrary("nakama-sdk"); + Log.i("libnakama-test MainActivity", "Loading libnakama-test..."); + System.loadLibrary("nakama-test"); + } catch (Exception e) { + Log.e("libnakama-test MainActivity", "Failed to load libraries: " + e.getMessage()); + } + } + }).start(); } } \ No newline at end of file diff --git a/integrationtests/android/test_android.sh b/integrationtests/android/test_android.sh new file mode 100644 index 000000000..e0ab79a0b --- /dev/null +++ b/integrationtests/android/test_android.sh @@ -0,0 +1,179 @@ +#!/bin/bash +set -eo pipefail + +: "${ABI:?Set ABI}" +: "${CMAKE_PRESET:?Set CMAKE_PRESET}" +: "${VCPKG_ROOT:?Set VCPKG_ROOT}" +: "${ANDROID_NDK_HOME:?Set ANDROID_NDK_HOME}" + +echo "=== Building C++ Test Library ===" +BUILD_TYPE="${BUILD_TYPE:-MinSizeRel}" +time cmake --preset "$CMAKE_PRESET" \ + -DBUILD_TESTING=ON \ + -DCMAKE_ANDROID_NDK="$ANDROID_NDK_HOME" \ + -DCMAKE_ANDROID_ARCH_ABI="$ABI" \ + -DCMAKE_MAKE_PROGRAM="$(command -v ninja)" + +cmake --build "build/$CMAKE_PRESET" --config "$BUILD_TYPE" --target nakama-test + +jni_dir="integrationtests/android/jniLibs/$ABI" +mkdir -p "$jni_dir" + +# ONLY stage the test and SDK libraries. +cp "build/$CMAKE_PRESET/integrationtests/$BUILD_TYPE/libnakama-test.so" "$jni_dir/" +cp "build/$CMAKE_PRESET/$BUILD_TYPE/libnakama-sdk.so" "$jni_dir/" + +case "$ABI" in + arm64-v8a) triple="aarch64-linux-android";; + armeabi-v7a) triple="arm-linux-androideabi";; + x86_64) triple="x86_64-linux-android";; +esac + +HOST_OS=$(uname -s | tr '[:upper:]' '[:lower:]') +stl_lib="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/${HOST_OS}-x86_64/sysroot/usr/lib/$triple/libc++_shared.so" +if [ -f "$stl_lib" ]; then + cp "$stl_lib" "$jni_dir/" +fi + +echo "=== Building Official Android SDK (Release) ===" +cd android +time ./gradlew assembleRelease -Pabi="$ABI" --no-daemon +cd .. + +echo "=== Building Integration Tests ===" +cd integrationtests/android +time ./gradlew assembleCustomDebugType -Pabi="$ABI" --no-daemon +cd ../../ + + + # --- Resolve device serial --- +ADB="${ADB:-adb}" +PACKAGE="com.heroiclabs.nakamatest" + ACTIVITY="${PACKAGE}/.MainActivity" + TIMEOUT=900 + LOG_TAG="nakama" + APK_PATH=$(ls integrationtests/android/build/outputs/apk/customDebugType/*.apk 2>/dev/null | head -n 1) + + parse_devices() { + devices=() + local output + output=$("$ADB" devices) + while IFS= read -r line; do + if [[ "$line" == *$'\t'device ]]; then + devices+=("${line%%$'\t'*}") + fi + done <<< "$output" + } + + serial="${DEVICE}" + if [ -z "$serial" ]; then + parse_devices + + if [ ${#devices[@]} -eq 0 ]; then + echo "No connected devices/emulators found. Starting emulator..." + EMULATOR="$ANDROID_HOME/emulator/emulator" + if [ ! -x "$EMULATOR" ] && [ -x "${EMULATOR}.exe" ]; then + EMULATOR="${EMULATOR}.exe" + fi + avd=$("$EMULATOR" -list-avds) + avd="${avd%%$'\n'*}" + avd="${avd//$'\r'/}" + if [ -z "$avd" ]; then + echo "Error: No AVDs found. Create one in Android Studio first." + exit 1 + fi + echo "Starting AVD: $avd" + {{if eq OS "windows"}}powershell.exe -Command "Start-Process -FilePath '${EMULATOR}' -ArgumentList '-avd','${avd}' -WindowStyle Hidden"{{else}}"$EMULATOR" -avd "$avd" &>/dev/null &{{end}} + echo "Waiting for device to come online..." + "$ADB" wait-for-device + while [[ "$("$ADB" shell getprop sys.boot_completed 2>/dev/null)" != *"1"* ]]; do + sleep 2 + done + echo "Emulator booted." + parse_devices + fi + + if [ ${#devices[@]} -gt 1 ]; then + echo "Error: Multiple devices connected. Specify DEVICE=:" + "$ADB" devices + exit 1 + fi + serial="${devices[0]}" + echo "Auto-detected device: $serial" + fi + + adb_cmd() { + "$ADB" -s "$serial" "$@" + } + + # --- Port forwarding --- + echo "Setting up port forwarding..." + for port in 7349 7350 7351; do + adb_cmd reverse tcp:$port tcp:$port + done + + # --- Install APK --- + if [ ! -f "$APK_PATH" ]; then + echo "Error: APK not found at $APK_PATH" + exit 1 + fi + + echo "Installing APK..." + adb_cmd uninstall "$PACKAGE" 2>/dev/null || true + adb_cmd install -r "$APK_PATH" + + # --- Clear logcat and launch --- + adb_cmd logcat -c + echo "Launching $PACKAGE via monkey..." + adb_cmd shell monkey -p "$PACKAGE" -c android.intent.category.LAUNCHER 1 + + # --- Monitor logcat for test results --- + echo "Waiting for test results (timeout: ${TIMEOUT}s)..." + echo "---" + + logfile="logcat-output.tmp" + > "$logfile" + "$ADB" -s "$serial" logcat "libnakama-test:V" "nakama:V" "AndroidRuntime:E" "ActivityManager:E" "*:S" -v raw > "$logfile" 2>/dev/null & + logcat_pid=$! + trap 'rm -f "$logfile"; kill $logcat_pid 2>/dev/null || true' EXIT + + result="" + SECONDS=0 + last_line=0 + + while [ "$SECONDS" -lt "$TIMEOUT" ]; do + line_num=0 + while IFS= read -r line; do + line_num=$((line_num + 1)) + if [ "$line_num" -le "$last_line" ]; then + continue + fi + echo "$line" + if [[ "$line" == *"Tests failed: 0"* ]]; then + result="passed" + elif [[ "$line" == *"Tests failed:"* ]]; then + result="failed" + fi + done < "$logfile" + last_line=$line_num + [ -n "$result" ] && break + sleep 1 + done + + kill $logcat_pid 2>/dev/null || true + + echo "---" + + if [ -z "$result" ]; then + echo "TIMEOUT: Tests did not complete within ${TIMEOUT}s." + exit 1 + elif [ "$result" = "passed" ]; then + echo "ALL TESTS PASSED" + exit 0 + else + echo "TESTS FAILED" + exit 1 + fi + + + diff --git a/integrationtests/src/NTestLib.cpp b/integrationtests/src/NTestLib.cpp index 1651d6f9f..2e17bf1b4 100644 --- a/integrationtests/src/NTestLib.cpp +++ b/integrationtests/src/NTestLib.cpp @@ -91,11 +91,18 @@ int runAllTests( // Run internals first (pure unit tests, no server) test_internals(); +#ifdef ANDROID + // Launch all test groups sequentially to avoid resource contention on emulator + auto startSuite = [](const char* suiteName, void (*suite)()) { + runSuiteSafely(suiteName, suite); + }; +#else // Launch all test groups in parallel threads std::vector threads; auto startSuite = [&threads](const char* suiteName, void (*suite)()) { threads.emplace_back([suiteName, suite]() { runSuiteSafely(suiteName, suite); }); }; +#endif startSuite("test_authentication", test_authentication); startSuite("test_session", test_session); @@ -122,9 +129,12 @@ int runAllTests( startSuite("test_throughput", test_throughput); startSuite("test_cancellation", test_cancellation); +#ifndef ANDROID for (auto& t : threads) { t.join(); } +#endif + // total stats uint32_t total = g_runTestsCount.load(); diff --git a/integrationtests/src/test_throughput.cpp b/integrationtests/src/test_throughput.cpp index a7ffc6d91..b47a2547d 100644 --- a/integrationtests/src/test_throughput.cpp +++ b/integrationtests/src/test_throughput.cpp @@ -219,7 +219,11 @@ void test_throughput_rpc100MB() { string payload(payloadSize, 'w'); payload = "{\"d\":\"" + payload + "\"}"; +#if defined(__ANDROID__) + const size_t targetBytes = 20ULL * 1024 * 1024; +#else const size_t targetBytes = 100ULL * 1024 * 1024; +#endif const int messageCount = static_cast(targetBytes / payload.size()); const size_t totalBytesSent = payload.size() * messageCount; const size_t totalBytesRoundTrip = totalBytesSent * 2;