From fe44ac7c0fbc538ded9967b7328066d76e23b4e8 Mon Sep 17 00:00:00 2001
From: Solessfir <34831419+Solessfir@users.noreply.github.com>
Date: Tue, 8 Sep 2026 23:29:05 +0500
Subject: [PATCH 1/3] Generated Android APK packaging from a single Gradle
build
---
Engine/cmake/TempestAndroid.cmake | 169 ++++++++++++++++++++
Engine/cmake/android/AndroidManifest.xml.in | 14 ++
Engine/cmake/android/build.gradle.in | 122 ++++++++++++++
Examples/Android/CMakeLists.txt | 10 ++
Examples/Android/README.md | 80 +++++++++
Examples/Android/native/CMakeLists.txt | 12 ++
Examples/Android/native/main.cpp | 27 ++++
README.md | 4 +
8 files changed, 438 insertions(+)
create mode 100644 Engine/cmake/TempestAndroid.cmake
create mode 100644 Engine/cmake/android/AndroidManifest.xml.in
create mode 100644 Engine/cmake/android/build.gradle.in
create mode 100644 Examples/Android/CMakeLists.txt
create mode 100644 Examples/Android/README.md
create mode 100644 Examples/Android/native/CMakeLists.txt
create mode 100644 Examples/Android/native/main.cpp
diff --git a/Engine/cmake/TempestAndroid.cmake b/Engine/cmake/TempestAndroid.cmake
new file mode 100644
index 00000000..3e70ceed
--- /dev/null
+++ b/Engine/cmake/TempestAndroid.cmake
@@ -0,0 +1,169 @@
+include_guard(GLOBAL)
+
+# Apply to the application's shared-library target.
+# Merely including this module never searches for Android or Java tools.
+function(tempest_android_native_target target)
+ if(NOT ANDROID)
+ return()
+ endif()
+ get_target_property(kind ${target} TYPE)
+ if(NOT kind STREQUAL "SHARED_LIBRARY")
+ message(FATAL_ERROR "${target} must be a shared library on Android")
+ endif()
+ target_link_options(${target} PRIVATE
+ "-Wl,-u,ANativeActivity_onCreate"
+ "-Wl,-z,max-page-size=16384")
+endfunction()
+
+function(_tempest_android_quote output value)
+ string(REPLACE "\\" "\\\\" value "${value}")
+ string(REPLACE "'" "\\'" value "${value}")
+ string(REPLACE "\n" "\\n" value "${value}")
+ string(REPLACE "\r" "\\r" value "${value}")
+ set(${output} "'${value}'" PARENT_SCOPE)
+endfunction()
+
+function(_tempest_android_list output)
+ set(result "")
+ foreach(value IN LISTS ARGN)
+ _tempest_android_quote(quoted "${value}")
+ string(APPEND result "${quoted}, ")
+ endforeach()
+ set(${output} "[${result}]" PARENT_SCOPE)
+endfunction()
+
+# Call from a separate project(... LANGUAGES NONE), never the native project.
+# Gradle invokes NATIVE_SOURCE_DIR in its own NDK build, without invoking this project.
+function(tempest_android_application name)
+ if(NOT name MATCHES "^[A-Za-z][A-Za-z0-9_-]*$")
+ message(FATAL_ERROR "Use letters, digits, underscores and hyphens for the packaging target name")
+ endif()
+ if(CMAKE_VERSION VERSION_LESS 3.22)
+ message(FATAL_ERROR "Android project generation requires CMake 3.22 or newer")
+ endif()
+ if(ANDROID)
+ message(FATAL_ERROR "Generate Android packaging in a separate host LANGUAGES NONE project")
+ endif()
+ cmake_parse_arguments(APP "SHRINK_RELEASE;REPACKAGE"
+ "APPLICATION_ID;LABEL;NATIVE_SOURCE_DIR;NATIVE_TARGET;LIBRARY_NAME;MANIFEST;VERSION_CODE;VERSION_NAME;PROPERTY_PREFIX;SIGNING_ENV_PREFIX;ASSET_PROPERTY"
+ "JAVA_DIRS;RESOURCE_DIRS;ASSET_DIRS;DEPENDENCIES;CMAKE_ARGUMENTS;CPP_FLAGS;PROGUARD_FILES;NO_COMPRESS" ${ARGN})
+ if(APP_UNPARSED_ARGUMENTS OR APP_KEYWORDS_MISSING_VALUES)
+ message(FATAL_ERROR "Invalid arguments to tempest_android_application: ${APP_UNPARSED_ARGUMENTS};${APP_KEYWORDS_MISSING_VALUES}")
+ endif()
+ foreach(required APPLICATION_ID NATIVE_SOURCE_DIR NATIVE_TARGET LIBRARY_NAME)
+ if(NOT APP_${required})
+ message(FATAL_ERROR "tempest_android_application requires ${required}")
+ endif()
+ endforeach()
+ if(NOT APP_APPLICATION_ID MATCHES "^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$")
+ message(FATAL_ERROR "Invalid Android application ID: ${APP_APPLICATION_ID}")
+ endif()
+ if(NOT APP_LIBRARY_NAME MATCHES "^[A-Za-z0-9_-]+$")
+ message(FATAL_ERROR "Use a plain library name without lib prefix or .so suffix")
+ endif()
+ get_filename_component(APP_NATIVE_SOURCE_DIR "${APP_NATIVE_SOURCE_DIR}" REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+ if(APP_NATIVE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR OR NOT EXISTS "${APP_NATIVE_SOURCE_DIR}/CMakeLists.txt")
+ message(FATAL_ERROR "NATIVE_SOURCE_DIR must name a separate native CMake project")
+ endif()
+ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
+ message(FATAL_ERROR "Android packaging requires an out-of-source build")
+ endif()
+
+ set(TEMPEST_ANDROID_COMPILE_SDK 35 CACHE STRING "Android compile and target SDK")
+ set(TEMPEST_ANDROID_MIN_SDK 24 CACHE STRING "Minimum Android SDK")
+ set(TEMPEST_ANDROID_BUILD_TOOLS "35.0.0" CACHE STRING "Android build tools version")
+ set(TEMPEST_ANDROID_NDK "27.0.12077973" CACHE STRING "Android NDK version")
+ set(TEMPEST_ANDROID_CMAKE "3.22.1" CACHE STRING "Android native CMake version")
+ set(TEMPEST_ANDROID_AGP "8.7.3" CACHE STRING "Android Gradle plugin version")
+ set(TEMPEST_ANDROID_ABIS "arm64-v8a" CACHE STRING "Android ABIs")
+ set(TEMPEST_ANDROID_BUILD_TYPE "Release" CACHE STRING "APK build variant")
+ set_property(CACHE TEMPEST_ANDROID_BUILD_TYPE PROPERTY STRINGS Debug Release)
+ if(NOT TEMPEST_ANDROID_BUILD_TYPE MATCHES "^(Debug|Release)$")
+ message(FATAL_ERROR "TEMPEST_ANDROID_BUILD_TYPE must be Debug or Release")
+ endif()
+ if(NOT APP_LABEL)
+ set(APP_LABEL "${name}")
+ endif()
+ if(NOT APP_VERSION_CODE)
+ set(APP_VERSION_CODE 1)
+ endif()
+ if(NOT APP_VERSION_CODE MATCHES "^[1-9][0-9]*$")
+ message(FATAL_ERROR "VERSION_CODE must be a positive integer")
+ endif()
+ if(NOT APP_VERSION_NAME)
+ set(APP_VERSION_NAME "1.0")
+ endif()
+ if(NOT APP_PROPERTY_PREFIX)
+ set(APP_PROPERTY_PREFIX "tempest")
+ endif()
+ if(NOT APP_SIGNING_ENV_PREFIX)
+ set(APP_SIGNING_ENV_PREFIX "TEMPEST")
+ endif()
+
+ set(templates "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/android")
+ set(output "${CMAKE_CURRENT_BINARY_DIR}/${name}")
+ file(MAKE_DIRECTORY "${output}")
+ foreach(kind JAVA RESOURCE ASSET PROGUARD)
+ if(kind STREQUAL "PROGUARD")
+ set(list_name PROGUARD_FILES)
+ else()
+ set(list_name "${kind}_DIRS")
+ endif()
+ set(paths "")
+ foreach(path IN LISTS APP_${list_name})
+ get_filename_component(path "${path}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+ list(APPEND paths "${path}")
+ endforeach()
+ set(APP_${list_name} "${paths}")
+ endforeach()
+ list(PREPEND APP_CMAKE_ARGUMENTS "-DANDROID_STL=c++_static")
+ foreach(value APPLICATION_ID VERSION_NAME PROPERTY_PREFIX SIGNING_ENV_PREFIX ASSET_PROPERTY NATIVE_TARGET)
+ _tempest_android_quote(${value} "${APP_${value}}")
+ endforeach()
+ _tempest_android_quote(NATIVE_CMAKE "${APP_NATIVE_SOURCE_DIR}/CMakeLists.txt")
+ foreach(value JAVA_DIRS RESOURCE_DIRS ASSET_DIRS DEPENDENCIES CMAKE_ARGUMENTS CPP_FLAGS PROGUARD_FILES NO_COMPRESS)
+ _tempest_android_list(${value} ${APP_${value}})
+ endforeach()
+ _tempest_android_list(ABIS ${TEMPEST_ANDROID_ABIS})
+ foreach(value SHRINK_RELEASE REPACKAGE)
+ if(APP_${value})
+ set(${value} true)
+ else()
+ set(${value} false)
+ endif()
+ endforeach()
+ if(APP_MANIFEST)
+ get_filename_component(manifest "${APP_MANIFEST}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+ # Keep the application's manifest next to its resources; do not rewrite it.
+ else()
+ set(manifest "${output}/AndroidManifest.xml")
+ if(APP_JAVA_DIRS OR APP_DEPENDENCIES)
+ set(HAS_CODE true)
+ else()
+ set(HAS_CODE false)
+ endif()
+ string(REPLACE "&" "&" APP_LABEL "${APP_LABEL}")
+ string(REPLACE "<" "<" APP_LABEL "${APP_LABEL}")
+ string(REPLACE "\"" """ APP_LABEL "${APP_LABEL}")
+ configure_file("${templates}/AndroidManifest.xml.in" "${manifest}" @ONLY)
+ endif()
+ _tempest_android_quote(MANIFEST "${manifest}")
+ configure_file("${templates}/build.gradle.in" "${output}/build.gradle" @ONLY NEWLINE_STYLE LF)
+ find_program(TEMPEST_ANDROID_GRADLE_EXECUTABLE NAMES gradle gradle.bat HINTS "$ENV{GRADLE_HOME}/bin"
+ DOC "Gradle executable used by the APK build target")
+ if(TEMPEST_ANDROID_GRADLE_EXECUTABLE)
+ add_custom_target(${name}-apk
+ COMMAND "${TEMPEST_ANDROID_GRADLE_EXECUTABLE}" -p "${output}" --no-daemon --max-workers=2
+ "-Dorg.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8"
+ "assemble${TEMPEST_ANDROID_BUILD_TYPE}" "lint${TEMPEST_ANDROID_BUILD_TYPE}"
+ USES_TERMINAL VERBATIM)
+ else()
+ # Generation still works for IDE users without a Gradle command on PATH.
+ add_custom_target(${name}-apk
+ COMMAND "${CMAKE_COMMAND}" -E echo "Install Gradle 8.9 and configure TEMPEST_ANDROID_GRADLE_EXECUTABLE, then rerun CMake."
+ COMMAND "${CMAKE_COMMAND}" -E false
+ VERBATIM)
+ endif()
+ message(STATUS "Generated Android project: ${output}")
+ message(STATUS "Build APK: cmake --build ${CMAKE_BINARY_DIR} --target ${name}-apk")
+endfunction()
diff --git a/Engine/cmake/android/AndroidManifest.xml.in b/Engine/cmake/android/AndroidManifest.xml.in
new file mode 100644
index 00000000..09d0a604
--- /dev/null
+++ b/Engine/cmake/android/AndroidManifest.xml.in
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Engine/cmake/android/build.gradle.in b/Engine/cmake/android/build.gradle.in
new file mode 100644
index 00000000..5e36b456
--- /dev/null
+++ b/Engine/cmake/android/build.gradle.in
@@ -0,0 +1,122 @@
+// Generated by Tempest. Change the application's CMake packaging configuration.
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:@TEMPEST_ANDROID_AGP@'
+ }
+}
+apply plugin: 'com.android.application'
+
+repositories {
+ google()
+ mavenCentral()
+}
+
+dependencies {
+ @DEPENDENCIES@.each { implementation it }
+}
+
+// Multi-gigabyte asset packages need full repackaging to avoid stale ZIP offsets.
+if (@REPACKAGE@) {
+ tasks.configureEach {
+ if (name == 'packageDebug' || name == 'packageRelease') {
+ outputs.upToDateWhen { false }
+ }
+ }
+}
+
+def signingPrefix = @SIGNING_ENV_PREFIX@
+def signingValues = ['KEYSTORE', 'KEY_ALIAS', 'STORE_PASSWORD', 'KEY_PASSWORD'].collect {
+ providers.environmentVariable(signingPrefix + '_' + it).orNull
+}
+if (signingValues.any { it != null } && signingValues.any { !it }) {
+ throw new GradleException('Set all four ' + signingPrefix + ' signing variables, or leave all unset for local signing.')
+}
+def propertyPrefix = @PROPERTY_PREFIX@
+def assetProperty = @ASSET_PROPERTY@
+
+android {
+ namespace @APPLICATION_ID@
+ compileSdk @TEMPEST_ANDROID_COMPILE_SDK@
+ buildToolsVersion '@TEMPEST_ANDROID_BUILD_TOOLS@'
+ ndkVersion '@TEMPEST_ANDROID_NDK@'
+
+ defaultConfig {
+ applicationId @APPLICATION_ID@
+ minSdk @TEMPEST_ANDROID_MIN_SDK@
+ targetSdk @TEMPEST_ANDROID_COMPILE_SDK@
+ versionCode providers.gradleProperty(propertyPrefix + 'VersionCode').orElse('@APP_VERSION_CODE@').get().toInteger()
+ versionName providers.gradleProperty(propertyPrefix + 'VersionName').orElse(@VERSION_NAME@).get()
+ ndk {
+ abiFilters.addAll(@ABIS@)
+ }
+ externalNativeBuild {
+ cmake {
+ arguments.addAll(@CMAKE_ARGUMENTS@)
+ cppFlags.addAll(@CPP_FLAGS@)
+ targets.add(@NATIVE_TARGET@)
+ }
+ }
+ }
+ signingConfigs {
+ if (signingValues[0] != null) {
+ distribution {
+ storeFile file(signingValues[0])
+ keyAlias signingValues[1]
+ storePassword signingValues[2]
+ keyPassword signingValues[3]
+ }
+ }
+ }
+ buildTypes {
+ debug {
+ debuggable true
+ jniDebuggable true
+ ndk.debugSymbolLevel 'FULL'
+ externalNativeBuild.cmake.arguments '-DCMAKE_BUILD_TYPE=RelWithDebInfo'
+ }
+ release {
+ debuggable false
+ jniDebuggable false
+ minifyEnabled @SHRINK_RELEASE@
+ shrinkResources @SHRINK_RELEASE@
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
+ proguardFiles.addAll(@PROGUARD_FILES@.collect { file(it) })
+ // Reuse the local key for testing unless distribution signing is configured.
+ signingConfig signingValues[0] != null ? signingConfigs.distribution : signingConfigs.debug
+ ndk.debugSymbolLevel 'FULL'
+ externalNativeBuild.cmake.arguments '-DCMAKE_BUILD_TYPE=Release'
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path file(@NATIVE_CMAKE@)
+ version '@TEMPEST_ANDROID_CMAKE@'
+ }
+ }
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging true
+ }
+ }
+ sourceSets {
+ main {
+ manifest.srcFile file(@MANIFEST@)
+ java.srcDirs = @JAVA_DIRS@
+ res.srcDirs = @RESOURCE_DIRS@
+ assets.srcDirs = @ASSET_DIRS@
+ if (assetProperty && providers.gradleProperty(assetProperty).isPresent()) {
+ assets.srcDir file(providers.gradleProperty(assetProperty).get())
+ }
+ }
+ }
+ androidResources {
+ noCompress.addAll(@NO_COMPRESS@)
+ }
+ lint {
+ disable 'ChromeOsAbiSupport'
+ }
+}
diff --git a/Examples/Android/CMakeLists.txt b/Examples/Android/CMakeLists.txt
new file mode 100644
index 00000000..9891674f
--- /dev/null
+++ b/Examples/Android/CMakeLists.txt
@@ -0,0 +1,10 @@
+cmake_minimum_required(VERSION 3.22)
+project(TempestAndroidPackaging LANGUAGES NONE)
+
+include(../../Engine/cmake/TempestAndroid.cmake)
+tempest_android_application(TempestExample
+ APPLICATION_ID org.tempest.example
+ LABEL "Tempest example"
+ NATIVE_SOURCE_DIR native
+ NATIVE_TARGET TempestExample
+ LIBRARY_NAME tempest-example)
diff --git a/Examples/Android/README.md b/Examples/Android/README.md
new file mode 100644
index 00000000..02d4b4f8
--- /dev/null
+++ b/Examples/Android/README.md
@@ -0,0 +1,80 @@
+# Android packaging
+
+This example packages a native CMake target into an APK. It draws a gold rectangle using Android's built-in NativeActivity, without Java sources, game assets or the Tempest Android backend. Events, Vulkan swapchains and controllers are separate work.
+
+## Build
+
+Install JDK 17, Gradle 8.9, CMake 3.22 or newer, and Ninja. Install Android SDK packages `platforms;android-35`, `build-tools;35.0.0`, `ndk;27.0.12077973` and `cmake;3.22.1`. Set `JAVA_HOME` and `ANDROID_HOME`, and put Gradle on `PATH` or set `GRADLE_HOME` to its installation directory.
+
+From the repository root:
+
+```sh
+cmake -S Examples/Android -B build/android-example -G Ninja
+cmake --build build/android-example --target TempestExample-apk
+adb install -r build/android-example/TempestExample/build/outputs/apk/release/TempestExample-release.apk
+adb shell am start -n org.tempest.example/android.app.NativeActivity
+```
+
+Release is the default; select debug with `-DTEMPEST_ANDROID_BUILD_TYPE=Debug` when configuring. Release APKs use the local debug signing key unless distribution signing is configured below.
+
+Generation needs only CMake and its build tool. It creates one `build.gradle` and a manifest in the build directory. There is no root/app split, `settings.gradle`, `gradle.properties`, wrapper JAR or wrapper script to maintain. Command-line builds use the installed Gradle; set `TEMPEST_ANDROID_GRADLE_EXECUTABLE` to its executable if discovery fails.
+
+In Android Studio, import `build/android-example/TempestExample` and select the local Gradle 8.9 installation if prompted. If you prefer a wrapper, generate it in that build directory with `gradle -p build/android-example/TempestExample wrapper --gradle-version 8.9`. Generated files stay out of the source repository. See the [Gradle wrapper documentation](https://docs.gradle.org/current/userguide/gradle_wrapper.html).
+
+## Use in another application
+
+Create a separate packaging project with `project(... LANGUAGES NONE)`, include `Engine/cmake/TempestAndroid.cmake`, and call `tempest_android_application` as in this example. `NATIVE_SOURCE_DIR` points to the existing native CMake project, not the packaging project. Gradle configures that native project with the NDK, avoiding recursive packaging generation.
+
+The native project builds a shared library and calls `tempest_android_native_target` to retain `ANativeActivity_onCreate` and enable 16 KiB page alignment. Desktop builds do not invoke the packaging function and need no Android tools.
+
+Required arguments: `APPLICATION_ID`, `NATIVE_SOURCE_DIR`, `NATIVE_TARGET`, `LIBRARY_NAME`. The library name must match the target's `OUTPUT_NAME`, without `lib` or `.so`.
+
+Optional configuration:
+
+- `LABEL`, `VERSION_CODE`, `VERSION_NAME`: app metadata.
+- `MANIFEST`: an application-owned manifest for a custom activity, permissions or device requirements.
+- `JAVA_DIRS`, `RESOURCE_DIRS`, `ASSET_DIRS`: source directories.
+- `DEPENDENCIES`, `CMAKE_ARGUMENTS`, `CPP_FLAGS`, `PROGUARD_FILES`, `NO_COMPRESS`: lists.
+- `SHRINK_RELEASE`, `REPACKAGE`: enable shrinking or force ZIP repackaging for large asset bundles.
+- `ASSET_PROPERTY`: a Gradle property naming an additional asset directory.
+
+Paths are relative to the packaging CMakeLists.txt. The default activity is `android.app.NativeActivity`; Java sources and JNI keep rules are not injected automatically. Apps using a custom backend must supply its manifest, Java sources and keep rules explicitly. AndroidX apps can pass `-Pandroid.useAndroidX=true` to Gradle or configure it in their user-level Gradle properties.
+
+Tool versions and ABIs are `TEMPEST_ANDROID_*` CMake cache settings. Gradle properties `tempestVersionCode` and `tempestVersionName` override versions; `PROPERTY_PREFIX` changes the prefix.
+
+## Distribution signing
+
+Create a signing key once and reuse it for every update. Keep it outside the repository and back it up securely with its password. With JDK 17's `bin` on `PATH`, this command prompts for the password and certificate details:
+
+```sh
+keytool -genkeypair -v -storetype PKCS12 -keystore /path/to/app-release.p12 -alias release -keyalg RSA -keysize 2048 -validity 10000
+```
+
+Set all four environment variables before running the APK build target: `TEMPEST_KEYSTORE` (absolute keystore path), `TEMPEST_KEY_ALIAS`, `TEMPEST_STORE_PASSWORD` and `TEMPEST_KEY_PASSWORD`. For PKCS12, use the same password for both. Gradle reads them at build time; secrets are not written to generated files or the CMake cache. `SIGNING_ENV_PREFIX` changes the `TEMPEST` prefix.
+
+PowerShell, after creating the key:
+
+```powershell
+$env:TEMPEST_KEYSTORE = 'C:/Keys/app-release.p12'
+$env:TEMPEST_KEY_ALIAS = 'release'
+$env:TEMPEST_STORE_PASSWORD = [System.Net.NetworkCredential]::new('', (Read-Host 'Keystore password' -AsSecureString)).Password
+$env:TEMPEST_KEY_PASSWORD = $env:TEMPEST_STORE_PASSWORD
+cmake --build build/android-example --target TempestExample-apk
+$env:TEMPEST_STORE_PASSWORD = $null
+$env:TEMPEST_KEY_PASSWORD = $null
+```
+
+Bash, after creating the key:
+
+```sh
+export TEMPEST_KEYSTORE='/path/to/app-release.p12'
+export TEMPEST_KEY_ALIAS='release'
+read -r -s -p 'Keystore password: ' TEMPEST_STORE_PASSWORD
+echo
+export TEMPEST_STORE_PASSWORD
+export TEMPEST_KEY_PASSWORD="$TEMPEST_STORE_PASSWORD"
+cmake --build build/android-example --target TempestExample-apk
+unset TEMPEST_STORE_PASSWORD TEMPEST_KEY_PASSWORD
+```
+
+Never commit keystores or passwords. A differently signed APK cannot update an existing installation. See Android's [signing guide](https://developer.android.com/studio/publish/app-signing).
diff --git a/Examples/Android/native/CMakeLists.txt b/Examples/Android/native/CMakeLists.txt
new file mode 100644
index 00000000..ded3dada
--- /dev/null
+++ b/Examples/Android/native/CMakeLists.txt
@@ -0,0 +1,12 @@
+cmake_minimum_required(VERSION 3.16)
+project(TempestExample LANGUAGES CXX)
+
+if(NOT ANDROID)
+ message(FATAL_ERROR "Build this packaging example through Examples/Android")
+endif()
+
+add_library(TempestExample SHARED main.cpp)
+set_target_properties(TempestExample PROPERTIES OUTPUT_NAME tempest-example)
+target_link_libraries(TempestExample PRIVATE android log)
+include(../../../Engine/cmake/TempestAndroid.cmake)
+tempest_android_native_target(TempestExample)
diff --git a/Examples/Android/native/main.cpp b/Examples/Android/native/main.cpp
new file mode 100644
index 00000000..6a2743bd
--- /dev/null
+++ b/Examples/Android/native/main.cpp
@@ -0,0 +1,27 @@
+#include
+#include
+#include
+#include
+
+// A packaging smoke test using the platform activity, independent of Tempest's Android backend.
+static void draw(ANativeActivity*, ANativeWindow* window) {
+ ANativeWindow_setBuffersGeometry(window,0,0,WINDOW_FORMAT_RGBA_8888);
+ ANativeWindow_Buffer buffer = {};
+ if(ANativeWindow_lock(window,&buffer,nullptr)!=0)
+ return;
+ auto pixels = static_cast(buffer.bits);
+ for(int y=0; ybuffer.width/3 && xbuffer.height/3 && ycallbacks->onNativeWindowCreated = draw;
+ activity->callbacks->onNativeWindowResized = draw;
+ activity->callbacks->onNativeWindowRedrawNeeded = draw;
+ __android_log_print(ANDROID_LOG_INFO,"TempestExample","Native packaging example started");
+ }
diff --git a/README.md b/README.md
index 38ed6f9a..aca586b6 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,10 @@ auto pm = device.readPixels(tex);
pm.save(outImg);
```
+### Android builds
+
+The [Android packaging example](Examples/Android/README.md) generates a single Gradle build from CMake while keeping the application's native CMake project. It uses Android's built-in NativeActivity; the Tempest Android backend is separate work. Desktop builds do not require Java, the Android SDK or Gradle.
+
### Ecosystem
During development various issues of Vulkan stack been found, reported and some were fixed.
From d3591ce292f33fd01a6443777e50745267097663 Mon Sep 17 00:00:00 2001
From: Solessfir <34831419+Solessfir@users.noreply.github.com>
Date: Wed, 9 Sep 2026 11:56:24 +0500
Subject: [PATCH 2/3] Used standard Android settings and left manifest and
packaging choices to the application
---
Engine/cmake/TempestAndroid.cmake | 116 ++++++++++++------
Engine/cmake/android/build.gradle.in | 31 +----
.../Android/AndroidManifest.xml | 4 +-
Examples/Android/CMakeLists.txt | 3 +-
Examples/Android/README.md | 27 ++--
5 files changed, 103 insertions(+), 78 deletions(-)
rename Engine/cmake/android/AndroidManifest.xml.in => Examples/Android/AndroidManifest.xml (83%)
diff --git a/Engine/cmake/TempestAndroid.cmake b/Engine/cmake/TempestAndroid.cmake
index 3e70ceed..de48bdb2 100644
--- a/Engine/cmake/TempestAndroid.cmake
+++ b/Engine/cmake/TempestAndroid.cmake
@@ -44,13 +44,13 @@ function(tempest_android_application name)
if(ANDROID)
message(FATAL_ERROR "Generate Android packaging in a separate host LANGUAGES NONE project")
endif()
- cmake_parse_arguments(APP "SHRINK_RELEASE;REPACKAGE"
- "APPLICATION_ID;LABEL;NATIVE_SOURCE_DIR;NATIVE_TARGET;LIBRARY_NAME;MANIFEST;VERSION_CODE;VERSION_NAME;PROPERTY_PREFIX;SIGNING_ENV_PREFIX;ASSET_PROPERTY"
+ cmake_parse_arguments(APP "SHRINK_RELEASE"
+ "APPLICATION_ID;NATIVE_SOURCE_DIR;NATIVE_TARGET;MANIFEST;VERSION_CODE;VERSION_NAME;PROPERTY_PREFIX;SIGNING_ENV_PREFIX;NATIVE_SYMBOLS"
"JAVA_DIRS;RESOURCE_DIRS;ASSET_DIRS;DEPENDENCIES;CMAKE_ARGUMENTS;CPP_FLAGS;PROGUARD_FILES;NO_COMPRESS" ${ARGN})
if(APP_UNPARSED_ARGUMENTS OR APP_KEYWORDS_MISSING_VALUES)
message(FATAL_ERROR "Invalid arguments to tempest_android_application: ${APP_UNPARSED_ARGUMENTS};${APP_KEYWORDS_MISSING_VALUES}")
endif()
- foreach(required APPLICATION_ID NATIVE_SOURCE_DIR NATIVE_TARGET LIBRARY_NAME)
+ foreach(required APPLICATION_ID NATIVE_SOURCE_DIR NATIVE_TARGET MANIFEST)
if(NOT APP_${required})
message(FATAL_ERROR "tempest_android_application requires ${required}")
endif()
@@ -58,9 +58,6 @@ function(tempest_android_application name)
if(NOT APP_APPLICATION_ID MATCHES "^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$")
message(FATAL_ERROR "Invalid Android application ID: ${APP_APPLICATION_ID}")
endif()
- if(NOT APP_LIBRARY_NAME MATCHES "^[A-Za-z0-9_-]+$")
- message(FATAL_ERROR "Use a plain library name without lib prefix or .so suffix")
- endif()
get_filename_component(APP_NATIVE_SOURCE_DIR "${APP_NATIVE_SOURCE_DIR}" REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
if(APP_NATIVE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR OR NOT EXISTS "${APP_NATIVE_SOURCE_DIR}/CMakeLists.txt")
message(FATAL_ERROR "NATIVE_SOURCE_DIR must name a separate native CMake project")
@@ -68,21 +65,74 @@ function(tempest_android_application name)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
message(FATAL_ERROR "Android packaging requires an out-of-source build")
endif()
+ get_filename_component(manifest "${APP_MANIFEST}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+ if(NOT EXISTS "${manifest}" OR IS_DIRECTORY "${manifest}")
+ message(FATAL_ERROR "MANIFEST must name an application-owned AndroidManifest.xml")
+ endif()
+
+ # These configure the native NDK build as well as its Gradle packaging.
+ # Do not use the host's CMAKE_SYSTEM_VERSION as the Android API level.
+ if(NOT DEFINED CMAKE_ANDROID_API)
+ set(CMAKE_ANDROID_API 24 CACHE STRING "Minimum Android API")
+ endif()
+ if(NOT DEFINED CMAKE_ANDROID_ARCH_ABI)
+ set(CMAKE_ANDROID_ARCH_ABI "arm64-v8a" CACHE STRING "Android ABI")
+ endif()
+ if(NOT DEFINED CMAKE_ANDROID_NDK)
+ set(CMAKE_ANDROID_NDK "" CACHE PATH "Android NDK directory; empty uses the SDK default below")
+ endif()
+ if(NOT DEFINED CMAKE_ANDROID_STL_TYPE)
+ set(CMAKE_ANDROID_STL_TYPE "c++_static" CACHE STRING "Android C++ runtime")
+ endif()
+ if(NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE Release CACHE STRING "Native build type" FORCE)
+ endif()
+ if(NOT CMAKE_BUILD_TYPE MATCHES "^(Debug|Release|RelWithDebInfo|MinSizeRel)$")
+ message(FATAL_ERROR "Unsupported Android native build type: ${CMAKE_BUILD_TYPE}")
+ endif()
+ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+ set(variant Debug)
+ set(release_type Release)
+ else()
+ set(variant Release)
+ set(release_type "${CMAKE_BUILD_TYPE}")
+ endif()
+ if(NOT CMAKE_ANDROID_ARCH_ABI MATCHES "^(arm64-v8a|armeabi-v7a|x86|x86_64)$")
+ message(FATAL_ERROR "CMAKE_ANDROID_ARCH_ABI must name one Android ABI")
+ endif()
+ if(NOT CMAKE_ANDROID_STL_TYPE MATCHES "^(c\\+\\+_static|c\\+\\+_shared|none|system)$")
+ message(FATAL_ERROR "Unsupported Android STL: ${CMAKE_ANDROID_STL_TYPE}")
+ endif()
- set(TEMPEST_ANDROID_COMPILE_SDK 35 CACHE STRING "Android compile and target SDK")
- set(TEMPEST_ANDROID_MIN_SDK 24 CACHE STRING "Minimum Android SDK")
+ # Gradle's compile/target SDKs are separate from the native minimum API.
+ set(TEMPEST_ANDROID_COMPILE_SDK 35 CACHE STRING "Android compile SDK")
+ set(TEMPEST_ANDROID_TARGET_SDK "${TEMPEST_ANDROID_COMPILE_SDK}" CACHE STRING "Android target SDK")
set(TEMPEST_ANDROID_BUILD_TOOLS "35.0.0" CACHE STRING "Android build tools version")
- set(TEMPEST_ANDROID_NDK "27.0.12077973" CACHE STRING "Android NDK version")
set(TEMPEST_ANDROID_CMAKE "3.22.1" CACHE STRING "Android native CMake version")
set(TEMPEST_ANDROID_AGP "8.7.3" CACHE STRING "Android Gradle plugin version")
- set(TEMPEST_ANDROID_ABIS "arm64-v8a" CACHE STRING "Android ABIs")
- set(TEMPEST_ANDROID_BUILD_TYPE "Release" CACHE STRING "APK build variant")
- set_property(CACHE TEMPEST_ANDROID_BUILD_TYPE PROPERTY STRINGS Debug Release)
- if(NOT TEMPEST_ANDROID_BUILD_TYPE MATCHES "^(Debug|Release)$")
- message(FATAL_ERROR "TEMPEST_ANDROID_BUILD_TYPE must be Debug or Release")
+ foreach(api CMAKE_ANDROID_API TEMPEST_ANDROID_COMPILE_SDK TEMPEST_ANDROID_TARGET_SDK)
+ if(NOT "${${api}}" MATCHES "^[1-9][0-9]*$")
+ message(FATAL_ERROR "${api} must be a numeric Android API level")
+ endif()
+ endforeach()
+ if(CMAKE_ANDROID_API GREATER TEMPEST_ANDROID_TARGET_SDK OR TEMPEST_ANDROID_TARGET_SDK GREATER TEMPEST_ANDROID_COMPILE_SDK)
+ message(FATAL_ERROR "Android APIs must satisfy minimum <= target <= compile")
+ endif()
+ if(CMAKE_ANDROID_NDK)
+ get_filename_component(ndk "${CMAKE_ANDROID_NDK}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+ if(NOT EXISTS "${ndk}/build/cmake/android.toolchain.cmake")
+ message(FATAL_ERROR "CMAKE_ANDROID_NDK must point to an NDK installation")
+ endif()
+ _tempest_android_quote(ndk "${ndk}")
+ set(NDK_CONFIGURATION "ndkPath ${ndk}")
+ else()
+ set(NDK_CONFIGURATION "ndkVersion '27.0.12077973'")
endif()
- if(NOT APP_LABEL)
- set(APP_LABEL "${name}")
+ if(NOT APP_NATIVE_SYMBOLS)
+ set(APP_NATIVE_SYMBOLS NONE)
+ endif()
+ if(NOT APP_NATIVE_SYMBOLS MATCHES "^(NONE|SYMBOL_TABLE|FULL)$")
+ message(FATAL_ERROR "NATIVE_SYMBOLS must be NONE, SYMBOL_TABLE or FULL")
endif()
if(NOT APP_VERSION_CODE)
set(APP_VERSION_CODE 1)
@@ -116,36 +166,22 @@ function(tempest_android_application name)
endforeach()
set(APP_${list_name} "${paths}")
endforeach()
- list(PREPEND APP_CMAKE_ARGUMENTS "-DANDROID_STL=c++_static")
- foreach(value APPLICATION_ID VERSION_NAME PROPERTY_PREFIX SIGNING_ENV_PREFIX ASSET_PROPERTY NATIVE_TARGET)
+ # AGP also reads the NDK alias when deciding whether to package libc++_shared.so.
+ list(PREPEND APP_CMAKE_ARGUMENTS "-DCMAKE_ANDROID_STL_TYPE=${CMAKE_ANDROID_STL_TYPE}"
+ "-DANDROID_STL=${CMAKE_ANDROID_STL_TYPE}")
+ foreach(value APPLICATION_ID VERSION_NAME PROPERTY_PREFIX SIGNING_ENV_PREFIX NATIVE_TARGET NATIVE_SYMBOLS)
_tempest_android_quote(${value} "${APP_${value}}")
endforeach()
_tempest_android_quote(NATIVE_CMAKE "${APP_NATIVE_SOURCE_DIR}/CMakeLists.txt")
foreach(value JAVA_DIRS RESOURCE_DIRS ASSET_DIRS DEPENDENCIES CMAKE_ARGUMENTS CPP_FLAGS PROGUARD_FILES NO_COMPRESS)
_tempest_android_list(${value} ${APP_${value}})
endforeach()
- _tempest_android_list(ABIS ${TEMPEST_ANDROID_ABIS})
- foreach(value SHRINK_RELEASE REPACKAGE)
- if(APP_${value})
- set(${value} true)
- else()
- set(${value} false)
- endif()
- endforeach()
- if(APP_MANIFEST)
- get_filename_component(manifest "${APP_MANIFEST}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
- # Keep the application's manifest next to its resources; do not rewrite it.
+ _tempest_android_list(ABIS "${CMAKE_ANDROID_ARCH_ABI}")
+ _tempest_android_quote(RELEASE_TYPE "-DCMAKE_BUILD_TYPE=${release_type}")
+ if(APP_SHRINK_RELEASE)
+ set(SHRINK_RELEASE true)
else()
- set(manifest "${output}/AndroidManifest.xml")
- if(APP_JAVA_DIRS OR APP_DEPENDENCIES)
- set(HAS_CODE true)
- else()
- set(HAS_CODE false)
- endif()
- string(REPLACE "&" "&" APP_LABEL "${APP_LABEL}")
- string(REPLACE "<" "<" APP_LABEL "${APP_LABEL}")
- string(REPLACE "\"" """ APP_LABEL "${APP_LABEL}")
- configure_file("${templates}/AndroidManifest.xml.in" "${manifest}" @ONLY)
+ set(SHRINK_RELEASE false)
endif()
_tempest_android_quote(MANIFEST "${manifest}")
configure_file("${templates}/build.gradle.in" "${output}/build.gradle" @ONLY NEWLINE_STYLE LF)
@@ -155,7 +191,7 @@ function(tempest_android_application name)
add_custom_target(${name}-apk
COMMAND "${TEMPEST_ANDROID_GRADLE_EXECUTABLE}" -p "${output}" --no-daemon --max-workers=2
"-Dorg.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8"
- "assemble${TEMPEST_ANDROID_BUILD_TYPE}" "lint${TEMPEST_ANDROID_BUILD_TYPE}"
+ "assemble${variant}" "lint${variant}"
USES_TERMINAL VERBATIM)
else()
# Generation still works for IDE users without a Gradle command on PATH.
diff --git a/Engine/cmake/android/build.gradle.in b/Engine/cmake/android/build.gradle.in
index 5e36b456..6ecf0d2f 100644
--- a/Engine/cmake/android/build.gradle.in
+++ b/Engine/cmake/android/build.gradle.in
@@ -19,15 +19,6 @@ dependencies {
@DEPENDENCIES@.each { implementation it }
}
-// Multi-gigabyte asset packages need full repackaging to avoid stale ZIP offsets.
-if (@REPACKAGE@) {
- tasks.configureEach {
- if (name == 'packageDebug' || name == 'packageRelease') {
- outputs.upToDateWhen { false }
- }
- }
-}
-
def signingPrefix = @SIGNING_ENV_PREFIX@
def signingValues = ['KEYSTORE', 'KEY_ALIAS', 'STORE_PASSWORD', 'KEY_PASSWORD'].collect {
providers.environmentVariable(signingPrefix + '_' + it).orNull
@@ -36,18 +27,17 @@ if (signingValues.any { it != null } && signingValues.any { !it }) {
throw new GradleException('Set all four ' + signingPrefix + ' signing variables, or leave all unset for local signing.')
}
def propertyPrefix = @PROPERTY_PREFIX@
-def assetProperty = @ASSET_PROPERTY@
android {
namespace @APPLICATION_ID@
compileSdk @TEMPEST_ANDROID_COMPILE_SDK@
buildToolsVersion '@TEMPEST_ANDROID_BUILD_TOOLS@'
- ndkVersion '@TEMPEST_ANDROID_NDK@'
+ @NDK_CONFIGURATION@
defaultConfig {
applicationId @APPLICATION_ID@
- minSdk @TEMPEST_ANDROID_MIN_SDK@
- targetSdk @TEMPEST_ANDROID_COMPILE_SDK@
+ minSdk @CMAKE_ANDROID_API@
+ targetSdk @TEMPEST_ANDROID_TARGET_SDK@
versionCode providers.gradleProperty(propertyPrefix + 'VersionCode').orElse('@APP_VERSION_CODE@').get().toInteger()
versionName providers.gradleProperty(propertyPrefix + 'VersionName').orElse(@VERSION_NAME@).get()
ndk {
@@ -75,8 +65,7 @@ android {
debug {
debuggable true
jniDebuggable true
- ndk.debugSymbolLevel 'FULL'
- externalNativeBuild.cmake.arguments '-DCMAKE_BUILD_TYPE=RelWithDebInfo'
+ externalNativeBuild.cmake.arguments '-DCMAKE_BUILD_TYPE=Debug'
}
release {
debuggable false
@@ -87,8 +76,8 @@ android {
proguardFiles.addAll(@PROGUARD_FILES@.collect { file(it) })
// Reuse the local key for testing unless distribution signing is configured.
signingConfig signingValues[0] != null ? signingConfigs.distribution : signingConfigs.debug
- ndk.debugSymbolLevel 'FULL'
- externalNativeBuild.cmake.arguments '-DCMAKE_BUILD_TYPE=Release'
+ ndk.debugSymbolLevel @NATIVE_SYMBOLS@
+ externalNativeBuild.cmake.arguments @RELEASE_TYPE@
}
}
externalNativeBuild {
@@ -97,20 +86,12 @@ android {
version '@TEMPEST_ANDROID_CMAKE@'
}
}
- packagingOptions {
- jniLibs {
- useLegacyPackaging true
- }
- }
sourceSets {
main {
manifest.srcFile file(@MANIFEST@)
java.srcDirs = @JAVA_DIRS@
res.srcDirs = @RESOURCE_DIRS@
assets.srcDirs = @ASSET_DIRS@
- if (assetProperty && providers.gradleProperty(assetProperty).isPresent()) {
- assets.srcDir file(providers.gradleProperty(assetProperty).get())
- }
}
}
androidResources {
diff --git a/Engine/cmake/android/AndroidManifest.xml.in b/Examples/Android/AndroidManifest.xml
similarity index 83%
rename from Engine/cmake/android/AndroidManifest.xml.in
rename to Examples/Android/AndroidManifest.xml
index 09d0a604..1915b1bd 100644
--- a/Engine/cmake/android/AndroidManifest.xml.in
+++ b/Examples/Android/AndroidManifest.xml
@@ -1,10 +1,10 @@
-
-
+
diff --git a/Examples/Android/CMakeLists.txt b/Examples/Android/CMakeLists.txt
index 9891674f..6ff000c1 100644
--- a/Examples/Android/CMakeLists.txt
+++ b/Examples/Android/CMakeLists.txt
@@ -4,7 +4,6 @@ project(TempestAndroidPackaging LANGUAGES NONE)
include(../../Engine/cmake/TempestAndroid.cmake)
tempest_android_application(TempestExample
APPLICATION_ID org.tempest.example
- LABEL "Tempest example"
NATIVE_SOURCE_DIR native
NATIVE_TARGET TempestExample
- LIBRARY_NAME tempest-example)
+ MANIFEST AndroidManifest.xml)
diff --git a/Examples/Android/README.md b/Examples/Android/README.md
index 02d4b4f8..d2279ebe 100644
--- a/Examples/Android/README.md
+++ b/Examples/Android/README.md
@@ -15,9 +15,9 @@ adb install -r build/android-example/TempestExample/build/outputs/apk/release/Te
adb shell am start -n org.tempest.example/android.app.NativeActivity
```
-Release is the default; select debug with `-DTEMPEST_ANDROID_BUILD_TYPE=Debug` when configuring. Release APKs use the local debug signing key unless distribution signing is configured below.
+Release is the default; select debug with `-DCMAKE_BUILD_TYPE=Debug` when configuring. Release APKs use the local debug signing key unless distribution signing is configured below.
-Generation needs only CMake and its build tool. It creates one `build.gradle` and a manifest in the build directory. There is no root/app split, `settings.gradle`, `gradle.properties`, wrapper JAR or wrapper script to maintain. Command-line builds use the installed Gradle; set `TEMPEST_ANDROID_GRADLE_EXECUTABLE` to its executable if discovery fails.
+Generation needs only CMake and its build tool. It creates one `build.gradle` in the build directory and references the application's manifest without copying or rewriting it. There is no root/app split, `settings.gradle`, `gradle.properties`, wrapper JAR or wrapper script to maintain. Command-line builds use the installed Gradle; set `TEMPEST_ANDROID_GRADLE_EXECUTABLE` to its executable if discovery fails.
In Android Studio, import `build/android-example/TempestExample` and select the local Gradle 8.9 installation if prompted. If you prefer a wrapper, generate it in that build directory with `gradle -p build/android-example/TempestExample wrapper --gradle-version 8.9`. Generated files stay out of the source repository. See the [Gradle wrapper documentation](https://docs.gradle.org/current/userguide/gradle_wrapper.html).
@@ -27,20 +27,29 @@ Create a separate packaging project with `project(... LANGUAGES NONE)`, include
The native project builds a shared library and calls `tempest_android_native_target` to retain `ANativeActivity_onCreate` and enable 16 KiB page alignment. Desktop builds do not invoke the packaging function and need no Android tools.
-Required arguments: `APPLICATION_ID`, `NATIVE_SOURCE_DIR`, `NATIVE_TARGET`, `LIBRARY_NAME`. The library name must match the target's `OUTPUT_NAME`, without `lib` or `.so`.
+Required arguments: `APPLICATION_ID`, `NATIVE_SOURCE_DIR`, `NATIVE_TARGET`, `MANIFEST`. The application owns `AndroidManifest.xml`, including its label, activity, permissions and device requirements. For NativeActivity, its `android.app.lib_name` metadata must match the native target's `OUTPUT_NAME`, without `lib` or `.so`. Applications can use their own `configure_file` call when they need a manifest template.
Optional configuration:
-- `LABEL`, `VERSION_CODE`, `VERSION_NAME`: app metadata.
-- `MANIFEST`: an application-owned manifest for a custom activity, permissions or device requirements.
+- `VERSION_CODE`, `VERSION_NAME`: app version metadata.
- `JAVA_DIRS`, `RESOURCE_DIRS`, `ASSET_DIRS`: source directories.
- `DEPENDENCIES`, `CMAKE_ARGUMENTS`, `CPP_FLAGS`, `PROGUARD_FILES`, `NO_COMPRESS`: lists.
-- `SHRINK_RELEASE`, `REPACKAGE`: enable shrinking or force ZIP repackaging for large asset bundles.
-- `ASSET_PROPERTY`: a Gradle property naming an additional asset directory.
+- `SHRINK_RELEASE`: enable Java/resource shrinking.
+- `NATIVE_SYMBOLS`: `NONE` (default), `SYMBOL_TABLE` or `FULL` for a separate release crash-symbol archive. The APK's native libraries remain stripped; this does not select a debug build.
-Paths are relative to the packaging CMakeLists.txt. The default activity is `android.app.NativeActivity`; Java sources and JNI keep rules are not injected automatically. Apps using a custom backend must supply its manifest, Java sources and keep rules explicitly. AndroidX apps can pass `-Pandroid.useAndroidX=true` to Gradle or configure it in their user-level Gradle properties.
+Paths are relative to the packaging CMakeLists.txt. Java sources and JNI keep rules are not injected automatically. Apps using a custom backend must supply its manifest, Java sources and keep rules explicitly. AndroidX apps can pass `-Pandroid.useAndroidX=true` to Gradle or configure it in their user-level Gradle properties.
-Tool versions and ABIs are `TEMPEST_ANDROID_*` CMake cache settings. Gradle properties `tempestVersionCode` and `tempestVersionName` override versions; `PROPERTY_PREFIX` changes the prefix.
+The helper respects existing CMake Android variables and sets defaults only when needed:
+
+- `CMAKE_ANDROID_API`: native minimum API and Gradle `minSdk`, default 24.
+- `CMAKE_ANDROID_ARCH_ABI`: one ABI per packaging build, default `arm64-v8a`.
+- `CMAKE_ANDROID_NDK`: an explicit NDK directory, passed to Gradle's `ndkPath`. If unset, Gradle uses SDK NDK version `27.0.12077973`.
+- `CMAKE_ANDROID_STL_TYPE`: native C++ runtime, default `c++_static`.
+- `CMAKE_BUILD_TYPE`: `Debug`, `Release`, `RelWithDebInfo` or `MinSizeRel`, default `Release`. Only `Debug` produces a debuggable APK; the other configurations use the release packaging variant.
+
+The packaging project runs on the host; do not set an Android toolchain or `CMAKE_SYSTEM_NAME` there. Gradle selects the NDK toolchain for the separate native project. Its compile and target SDKs are independent of the native minimum API: `TEMPEST_ANDROID_COMPILE_SDK` defaults to 35 and `TEMPEST_ANDROID_TARGET_SDK` defaults to the compile SDK. Gradle-specific AGP, native CMake and build-tools versions remain `TEMPEST_ANDROID_*` settings.
+
+Gradle properties `tempestVersionCode` and `tempestVersionName` override app versions; `PROPERTY_PREFIX` changes the prefix. Assets use normal Gradle packaging without forced repackaging. Native libraries use AGP's default uncompressed packaging, with 16 KiB ELF alignment supplied by the native helper.
## Distribution signing
From 261ad7f6c5fb717c9cf7adfaa287a5bd0a11fb30 Mon Sep 17 00:00:00 2001
From: Solessfir <34831419+Solessfir@users.noreply.github.com>
Date: Thu, 10 Sep 2026 09:50:04 +0500
Subject: [PATCH 3/3] Built Android APKs from the application's existing CMake
project
---
Engine/CMakeLists.txt | 2 +
Engine/cmake/TempestAndroid.cmake | 101 ++++++++++++-------------
Engine/cmake/android/build.gradle.in | 2 +-
Examples/Android/CMakeLists.txt | 20 +++--
Examples/Android/README.md | 90 ++++------------------
Examples/Android/{native => }/main.cpp | 1 +
Examples/Android/native/CMakeLists.txt | 12 ---
README.md | 2 +-
8 files changed, 83 insertions(+), 147 deletions(-)
rename Examples/Android/{native => }/main.cpp (94%)
delete mode 100644 Examples/Android/native/CMakeLists.txt
diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt
index 1387dee2..1936129d 100644
--- a/Engine/CMakeLists.txt
+++ b/Engine/CMakeLists.txt
@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.16)
project(Tempest)
+include(cmake/TempestAndroid.cmake)
+
set(CMAKE_CXX_STANDARD 20)
option(TEMPEST_BUILD_SHARED "Build shared Tempest." ON)
diff --git a/Engine/cmake/TempestAndroid.cmake b/Engine/cmake/TempestAndroid.cmake
index de48bdb2..df41abba 100644
--- a/Engine/cmake/TempestAndroid.cmake
+++ b/Engine/cmake/TempestAndroid.cmake
@@ -1,20 +1,5 @@
include_guard(GLOBAL)
-# Apply to the application's shared-library target.
-# Merely including this module never searches for Android or Java tools.
-function(tempest_android_native_target target)
- if(NOT ANDROID)
- return()
- endif()
- get_target_property(kind ${target} TYPE)
- if(NOT kind STREQUAL "SHARED_LIBRARY")
- message(FATAL_ERROR "${target} must be a shared library on Android")
- endif()
- target_link_options(${target} PRIVATE
- "-Wl,-u,ANativeActivity_onCreate"
- "-Wl,-z,max-page-size=16384")
-endfunction()
-
function(_tempest_android_quote output value)
string(REPLACE "\\" "\\\\" value "${value}")
string(REPLACE "'" "\\'" value "${value}")
@@ -32,35 +17,49 @@ function(_tempest_android_list output)
set(${output} "[${result}]" PARENT_SCOPE)
endfunction()
-# Call from a separate project(... LANGUAGES NONE), never the native project.
-# Gradle invokes NATIVE_SOURCE_DIR in its own NDK build, without invoking this project.
-function(tempest_android_application name)
+# Call after defining the application's native shared-library target.
+# Including this module alone never searches for Android or Java tools.
+function(add_android_apk name)
if(NOT name MATCHES "^[A-Za-z][A-Za-z0-9_-]*$")
message(FATAL_ERROR "Use letters, digits, underscores and hyphens for the packaging target name")
endif()
if(CMAKE_VERSION VERSION_LESS 3.22)
message(FATAL_ERROR "Android project generation requires CMake 3.22 or newer")
endif()
- if(ANDROID)
- message(FATAL_ERROR "Generate Android packaging in a separate host LANGUAGES NONE project")
+ if(NOT ANDROID)
+ message(FATAL_ERROR "add_android_apk requires an Android NDK build")
endif()
cmake_parse_arguments(APP "SHRINK_RELEASE"
- "APPLICATION_ID;NATIVE_SOURCE_DIR;NATIVE_TARGET;MANIFEST;VERSION_CODE;VERSION_NAME;PROPERTY_PREFIX;SIGNING_ENV_PREFIX;NATIVE_SYMBOLS"
+ "PACKAGE_NAME;CODE;MANIFEST;VERSION_CODE;VERSION_NAME;PROPERTY_PREFIX;SIGNING_ENV_PREFIX;NATIVE_SYMBOLS"
"JAVA_DIRS;RESOURCE_DIRS;ASSET_DIRS;DEPENDENCIES;CMAKE_ARGUMENTS;CPP_FLAGS;PROGUARD_FILES;NO_COMPRESS" ${ARGN})
if(APP_UNPARSED_ARGUMENTS OR APP_KEYWORDS_MISSING_VALUES)
- message(FATAL_ERROR "Invalid arguments to tempest_android_application: ${APP_UNPARSED_ARGUMENTS};${APP_KEYWORDS_MISSING_VALUES}")
+ message(FATAL_ERROR "Invalid arguments to add_android_apk: ${APP_UNPARSED_ARGUMENTS};${APP_KEYWORDS_MISSING_VALUES}")
endif()
- foreach(required APPLICATION_ID NATIVE_SOURCE_DIR NATIVE_TARGET MANIFEST)
+ foreach(required PACKAGE_NAME CODE MANIFEST)
if(NOT APP_${required})
- message(FATAL_ERROR "tempest_android_application requires ${required}")
+ message(FATAL_ERROR "add_android_apk requires ${required}")
endif()
endforeach()
+ set(APP_APPLICATION_ID "${APP_PACKAGE_NAME}")
+ set(APP_NATIVE_TARGET "${APP_CODE}")
if(NOT APP_APPLICATION_ID MATCHES "^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$")
message(FATAL_ERROR "Invalid Android application ID: ${APP_APPLICATION_ID}")
endif()
- get_filename_component(APP_NATIVE_SOURCE_DIR "${APP_NATIVE_SOURCE_DIR}" REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
- if(APP_NATIVE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR OR NOT EXISTS "${APP_NATIVE_SOURCE_DIR}/CMakeLists.txt")
- message(FATAL_ERROR "NATIVE_SOURCE_DIR must name a separate native CMake project")
+ if(NOT TARGET "${APP_CODE}")
+ message(FATAL_ERROR "CODE must name an existing shared-library target")
+ endif()
+ get_target_property(kind "${APP_CODE}" TYPE)
+ get_target_property(imported "${APP_CODE}" IMPORTED)
+ if(NOT kind STREQUAL "SHARED_LIBRARY" OR imported)
+ message(FATAL_ERROR "CODE must name a shared library built by this project")
+ endif()
+ target_link_options(${APP_CODE} PRIVATE
+ "-Wl,-u,ANativeActivity_onCreate"
+ "-Wl,-z,max-page-size=16384")
+
+ # Gradle builds the same CMake project, but must not regenerate its own build.gradle.
+ if(TEMPEST_ANDROID_GRADLE_BUILD)
+ return()
endif()
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
message(FATAL_ERROR "Android packaging requires an out-of-source build")
@@ -70,16 +69,12 @@ function(tempest_android_application name)
message(FATAL_ERROR "MANIFEST must name an application-owned AndroidManifest.xml")
endif()
- # These configure the native NDK build as well as its Gradle packaging.
- # Do not use the host's CMAKE_SYSTEM_VERSION as the Android API level.
- if(NOT DEFINED CMAKE_ANDROID_API)
- set(CMAKE_ANDROID_API 24 CACHE STRING "Minimum Android API")
- endif()
- if(NOT DEFINED CMAKE_ANDROID_ARCH_ABI)
- set(CMAKE_ANDROID_ARCH_ABI "arm64-v8a" CACHE STRING "Android ABI")
- endif()
- if(NOT DEFINED CMAKE_ANDROID_NDK)
- set(CMAKE_ANDROID_NDK "" CACHE PATH "Android NDK directory; empty uses the SDK default below")
+ # Use the API, ABI and NDK already selected by the native toolchain.
+ if(DEFINED ANDROID_PLATFORM_LEVEL)
+ # The NDK's default toolchain sets CMAKE_SYSTEM_VERSION to 1, not the API level.
+ set(MIN_SDK "${ANDROID_PLATFORM_LEVEL}")
+ else()
+ set(MIN_SDK "${CMAKE_SYSTEM_VERSION}")
endif()
if(NOT DEFINED CMAKE_ANDROID_STL_TYPE)
set(CMAKE_ANDROID_STL_TYPE "c++_static" CACHE STRING "Android C++ runtime")
@@ -110,24 +105,19 @@ function(tempest_android_application name)
set(TEMPEST_ANDROID_BUILD_TOOLS "35.0.0" CACHE STRING "Android build tools version")
set(TEMPEST_ANDROID_CMAKE "3.22.1" CACHE STRING "Android native CMake version")
set(TEMPEST_ANDROID_AGP "8.7.3" CACHE STRING "Android Gradle plugin version")
- foreach(api CMAKE_ANDROID_API TEMPEST_ANDROID_COMPILE_SDK TEMPEST_ANDROID_TARGET_SDK)
+ foreach(api MIN_SDK TEMPEST_ANDROID_COMPILE_SDK TEMPEST_ANDROID_TARGET_SDK)
if(NOT "${${api}}" MATCHES "^[1-9][0-9]*$")
message(FATAL_ERROR "${api} must be a numeric Android API level")
endif()
endforeach()
- if(CMAKE_ANDROID_API GREATER TEMPEST_ANDROID_TARGET_SDK OR TEMPEST_ANDROID_TARGET_SDK GREATER TEMPEST_ANDROID_COMPILE_SDK)
+ if(MIN_SDK GREATER TEMPEST_ANDROID_TARGET_SDK OR TEMPEST_ANDROID_TARGET_SDK GREATER TEMPEST_ANDROID_COMPILE_SDK)
message(FATAL_ERROR "Android APIs must satisfy minimum <= target <= compile")
endif()
- if(CMAKE_ANDROID_NDK)
- get_filename_component(ndk "${CMAKE_ANDROID_NDK}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
- if(NOT EXISTS "${ndk}/build/cmake/android.toolchain.cmake")
- message(FATAL_ERROR "CMAKE_ANDROID_NDK must point to an NDK installation")
- endif()
- _tempest_android_quote(ndk "${ndk}")
- set(NDK_CONFIGURATION "ndkPath ${ndk}")
- else()
- set(NDK_CONFIGURATION "ndkVersion '27.0.12077973'")
+ if(NOT EXISTS "${CMAKE_ANDROID_NDK}/build/cmake/android.toolchain.cmake")
+ message(FATAL_ERROR "CMAKE_ANDROID_NDK must point to an NDK installation")
endif()
+ _tempest_android_quote(ndk "${CMAKE_ANDROID_NDK}")
+ set(NDK_CONFIGURATION "ndkPath ${ndk}")
if(NOT APP_NATIVE_SYMBOLS)
set(APP_NATIVE_SYMBOLS NONE)
endif()
@@ -153,6 +143,10 @@ function(tempest_android_application name)
set(templates "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/android")
set(output "${CMAKE_CURRENT_BINARY_DIR}/${name}")
file(MAKE_DIRECTORY "${output}")
+ if(manifest MATCHES "\\.in$")
+ configure_file("${manifest}" "${output}/AndroidManifest.xml" @ONLY)
+ set(manifest "${output}/AndroidManifest.xml")
+ endif()
foreach(kind JAVA RESOURCE ASSET PROGUARD)
if(kind STREQUAL "PROGUARD")
set(list_name PROGUARD_FILES)
@@ -169,10 +163,11 @@ function(tempest_android_application name)
# AGP also reads the NDK alias when deciding whether to package libc++_shared.so.
list(PREPEND APP_CMAKE_ARGUMENTS "-DCMAKE_ANDROID_STL_TYPE=${CMAKE_ANDROID_STL_TYPE}"
"-DANDROID_STL=${CMAKE_ANDROID_STL_TYPE}")
+ list(APPEND APP_CMAKE_ARGUMENTS "-DTEMPEST_ANDROID_GRADLE_BUILD=ON")
foreach(value APPLICATION_ID VERSION_NAME PROPERTY_PREFIX SIGNING_ENV_PREFIX NATIVE_TARGET NATIVE_SYMBOLS)
_tempest_android_quote(${value} "${APP_${value}}")
endforeach()
- _tempest_android_quote(NATIVE_CMAKE "${APP_NATIVE_SOURCE_DIR}/CMakeLists.txt")
+ _tempest_android_quote(NATIVE_CMAKE "${CMAKE_SOURCE_DIR}/CMakeLists.txt")
foreach(value JAVA_DIRS RESOURCE_DIRS ASSET_DIRS DEPENDENCIES CMAKE_ARGUMENTS CPP_FLAGS PROGUARD_FILES NO_COMPRESS)
_tempest_android_list(${value} ${APP_${value}})
endforeach()
@@ -186,20 +181,20 @@ function(tempest_android_application name)
_tempest_android_quote(MANIFEST "${manifest}")
configure_file("${templates}/build.gradle.in" "${output}/build.gradle" @ONLY NEWLINE_STYLE LF)
find_program(TEMPEST_ANDROID_GRADLE_EXECUTABLE NAMES gradle gradle.bat HINTS "$ENV{GRADLE_HOME}/bin"
- DOC "Gradle executable used by the APK build target")
+ DOC "Gradle executable used by the APK build target" NO_CMAKE_FIND_ROOT_PATH)
if(TEMPEST_ANDROID_GRADLE_EXECUTABLE)
- add_custom_target(${name}-apk
+ add_custom_target(${name}
COMMAND "${TEMPEST_ANDROID_GRADLE_EXECUTABLE}" -p "${output}" --no-daemon --max-workers=2
"-Dorg.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8"
"assemble${variant}" "lint${variant}"
USES_TERMINAL VERBATIM)
else()
# Generation still works for IDE users without a Gradle command on PATH.
- add_custom_target(${name}-apk
+ add_custom_target(${name}
COMMAND "${CMAKE_COMMAND}" -E echo "Install Gradle 8.9 and configure TEMPEST_ANDROID_GRADLE_EXECUTABLE, then rerun CMake."
COMMAND "${CMAKE_COMMAND}" -E false
VERBATIM)
endif()
message(STATUS "Generated Android project: ${output}")
- message(STATUS "Build APK: cmake --build ${CMAKE_BINARY_DIR} --target ${name}-apk")
+ message(STATUS "Build APK: cmake --build ${CMAKE_BINARY_DIR} --target ${name}")
endfunction()
diff --git a/Engine/cmake/android/build.gradle.in b/Engine/cmake/android/build.gradle.in
index 6ecf0d2f..2a75ab62 100644
--- a/Engine/cmake/android/build.gradle.in
+++ b/Engine/cmake/android/build.gradle.in
@@ -36,7 +36,7 @@ android {
defaultConfig {
applicationId @APPLICATION_ID@
- minSdk @CMAKE_ANDROID_API@
+ minSdk @MIN_SDK@
targetSdk @TEMPEST_ANDROID_TARGET_SDK@
versionCode providers.gradleProperty(propertyPrefix + 'VersionCode').orElse('@APP_VERSION_CODE@').get().toInteger()
versionName providers.gradleProperty(propertyPrefix + 'VersionName').orElse(@VERSION_NAME@).get()
diff --git a/Examples/Android/CMakeLists.txt b/Examples/Android/CMakeLists.txt
index 6ff000c1..7dc73837 100644
--- a/Examples/Android/CMakeLists.txt
+++ b/Examples/Android/CMakeLists.txt
@@ -1,9 +1,19 @@
cmake_minimum_required(VERSION 3.22)
-project(TempestAndroidPackaging LANGUAGES NONE)
+project(TempestExample LANGUAGES CXX)
+if(NOT ANDROID)
+ message(FATAL_ERROR "Configure this example with the Android NDK toolchain")
+endif()
+
+# Tempest applications get this helper through add_subdirectory(Engine).
+# This packaging sample does not depend on the Android backend yet.
include(../../Engine/cmake/TempestAndroid.cmake)
-tempest_android_application(TempestExample
- APPLICATION_ID org.tempest.example
- NATIVE_SOURCE_DIR native
- NATIVE_TARGET TempestExample
+
+add_library(TempestExample SHARED main.cpp)
+set_target_properties(TempestExample PROPERTIES OUTPUT_NAME tempest-example)
+target_link_libraries(TempestExample PRIVATE android log)
+
+add_android_apk(TempestExample-apk
+ PACKAGE_NAME org.tempest.example
+ CODE TempestExample
MANIFEST AndroidManifest.xml)
diff --git a/Examples/Android/README.md b/Examples/Android/README.md
index d2279ebe..6cb013d6 100644
--- a/Examples/Android/README.md
+++ b/Examples/Android/README.md
@@ -1,89 +1,29 @@
# Android packaging
-This example packages a native CMake target into an APK. It draws a gold rectangle using Android's built-in NativeActivity, without Java sources, game assets or the Tempest Android backend. Events, Vulkan swapchains and controllers are separate work.
+A small NativeActivity packaging example. It will move into `Examples/Empty` when the Android backend is available upstream.
-## Build
-
-Install JDK 17, Gradle 8.9, CMake 3.22 or newer, and Ninja. Install Android SDK packages `platforms;android-35`, `build-tools;35.0.0`, `ndk;27.0.12077973` and `cmake;3.22.1`. Set `JAVA_HOME` and `ANDROID_HOME`, and put Gradle on `PATH` or set `GRADLE_HOME` to its installation directory.
-
-From the repository root:
+With JDK 17, Gradle 8.9, Ninja and the Android SDK configured (`ANDROID_HOME`), install SDK 35, build-tools 35.0.0, NDK 27.0.12077973 and CMake 3.22.1. Replace `/path/to/ndk` below with the NDK installation directory.
```sh
-cmake -S Examples/Android -B build/android-example -G Ninja
+cmake -S Examples/Android -B build/android-example -G Ninja -DCMAKE_TOOLCHAIN_FILE=/path/to/ndk/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-24 -DCMAKE_BUILD_TYPE=Release
cmake --build build/android-example --target TempestExample-apk
-adb install -r build/android-example/TempestExample/build/outputs/apk/release/TempestExample-release.apk
+adb install -r build/android-example/TempestExample-apk/build/outputs/apk/release/TempestExample-apk-release.apk
adb shell am start -n org.tempest.example/android.app.NativeActivity
```
-Release is the default; select debug with `-DCMAKE_BUILD_TYPE=Debug` when configuring. Release APKs use the local debug signing key unless distribution signing is configured below.
-
-Generation needs only CMake and its build tool. It creates one `build.gradle` in the build directory and references the application's manifest without copying or rewriting it. There is no root/app split, `settings.gradle`, `gradle.properties`, wrapper JAR or wrapper script to maintain. Command-line builds use the installed Gradle; set `TEMPEST_ANDROID_GRADLE_EXECUTABLE` to its executable if discovery fails.
-
-In Android Studio, import `build/android-example/TempestExample` and select the local Gradle 8.9 installation if prompted. If you prefer a wrapper, generate it in that build directory with `gradle -p build/android-example/TempestExample wrapper --gradle-version 8.9`. Generated files stay out of the source repository. See the [Gradle wrapper documentation](https://docs.gradle.org/current/userguide/gradle_wrapper.html).
-
-## Use in another application
-
-Create a separate packaging project with `project(... LANGUAGES NONE)`, include `Engine/cmake/TempestAndroid.cmake`, and call `tempest_android_application` as in this example. `NATIVE_SOURCE_DIR` points to the existing native CMake project, not the packaging project. Gradle configures that native project with the NDK, avoiding recursive packaging generation.
-
-The native project builds a shared library and calls `tempest_android_native_target` to retain `ANativeActivity_onCreate` and enable 16 KiB page alignment. Desktop builds do not invoke the packaging function and need no Android tools.
-
-Required arguments: `APPLICATION_ID`, `NATIVE_SOURCE_DIR`, `NATIVE_TARGET`, `MANIFEST`. The application owns `AndroidManifest.xml`, including its label, activity, permissions and device requirements. For NativeActivity, its `android.app.lib_name` metadata must match the native target's `OUTPUT_NAME`, without `lib` or `.so`. Applications can use their own `configure_file` call when they need a manifest template.
-
-Optional configuration:
-
-- `VERSION_CODE`, `VERSION_NAME`: app version metadata.
-- `JAVA_DIRS`, `RESOURCE_DIRS`, `ASSET_DIRS`: source directories.
-- `DEPENDENCIES`, `CMAKE_ARGUMENTS`, `CPP_FLAGS`, `PROGUARD_FILES`, `NO_COMPRESS`: lists.
-- `SHRINK_RELEASE`: enable Java/resource shrinking.
-- `NATIVE_SYMBOLS`: `NONE` (default), `SYMBOL_TABLE` or `FULL` for a separate release crash-symbol archive. The APK's native libraries remain stripped; this does not select a debug build.
-
-Paths are relative to the packaging CMakeLists.txt. Java sources and JNI keep rules are not injected automatically. Apps using a custom backend must supply its manifest, Java sources and keep rules explicitly. AndroidX apps can pass `-Pandroid.useAndroidX=true` to Gradle or configure it in their user-level Gradle properties.
-
-The helper respects existing CMake Android variables and sets defaults only when needed:
-
-- `CMAKE_ANDROID_API`: native minimum API and Gradle `minSdk`, default 24.
-- `CMAKE_ANDROID_ARCH_ABI`: one ABI per packaging build, default `arm64-v8a`.
-- `CMAKE_ANDROID_NDK`: an explicit NDK directory, passed to Gradle's `ndkPath`. If unset, Gradle uses SDK NDK version `27.0.12077973`.
-- `CMAKE_ANDROID_STL_TYPE`: native C++ runtime, default `c++_static`.
-- `CMAKE_BUILD_TYPE`: `Debug`, `Release`, `RelWithDebInfo` or `MinSizeRel`, default `Release`. Only `Debug` produces a debuggable APK; the other configurations use the release packaging variant.
-
-The packaging project runs on the host; do not set an Android toolchain or `CMAKE_SYSTEM_NAME` there. Gradle selects the NDK toolchain for the separate native project. Its compile and target SDKs are independent of the native minimum API: `TEMPEST_ANDROID_COMPILE_SDK` defaults to 35 and `TEMPEST_ANDROID_TARGET_SDK` defaults to the compile SDK. Gradle-specific AGP, native CMake and build-tools versions remain `TEMPEST_ANDROID_*` settings.
-
-Gradle properties `tempestVersionCode` and `tempestVersionName` override app versions; `PROPERTY_PREFIX` changes the prefix. Assets use normal Gradle packaging without forced repackaging. Native libraries use AGP's default uncompressed packaging, with 16 KiB ELF alignment supplied by the native helper.
-
-## Distribution signing
-
-Create a signing key once and reuse it for every update. Keep it outside the repository and back it up securely with its password. With JDK 17's `bin` on `PATH`, this command prompts for the password and certificate details:
-
-```sh
-keytool -genkeypair -v -storetype PKCS12 -keystore /path/to/app-release.p12 -alias release -keyalg RSA -keysize 2048 -validity 10000
-```
-
-Set all four environment variables before running the APK build target: `TEMPEST_KEYSTORE` (absolute keystore path), `TEMPEST_KEY_ALIAS`, `TEMPEST_STORE_PASSWORD` and `TEMPEST_KEY_PASSWORD`. For PKCS12, use the same password for both. Gradle reads them at build time; secrets are not written to generated files or the CMake cache. `SIGNING_ENV_PREFIX` changes the `TEMPEST` prefix.
+Use `-DCMAKE_BUILD_TYPE=Debug` for a debug APK. Gradle must be on `PATH`, under `GRADLE_HOME`, or selected with `TEMPEST_ANDROID_GRADLE_EXECUTABLE`. Release APKs use the local debug key unless the `TEMPEST_KEYSTORE`, `TEMPEST_KEY_ALIAS`, `TEMPEST_STORE_PASSWORD` and `TEMPEST_KEY_PASSWORD` environment variables are set.
-PowerShell, after creating the key:
+In an application's existing CMakeLists.txt, after defining its shared-library target and adding Tempest:
-```powershell
-$env:TEMPEST_KEYSTORE = 'C:/Keys/app-release.p12'
-$env:TEMPEST_KEY_ALIAS = 'release'
-$env:TEMPEST_STORE_PASSWORD = [System.Net.NetworkCredential]::new('', (Read-Host 'Keystore password' -AsSecureString)).Password
-$env:TEMPEST_KEY_PASSWORD = $env:TEMPEST_STORE_PASSWORD
-cmake --build build/android-example --target TempestExample-apk
-$env:TEMPEST_STORE_PASSWORD = $null
-$env:TEMPEST_KEY_PASSWORD = $null
+```cmake
+if(ANDROID)
+ add_android_apk(MyGame-apk
+ CODE MyGame
+ PACKAGE_NAME org.example.mygame
+ MANIFEST AndroidManifest.xml)
+endif()
```
-Bash, after creating the key:
-
-```sh
-export TEMPEST_KEYSTORE='/path/to/app-release.p12'
-export TEMPEST_KEY_ALIAS='release'
-read -r -s -p 'Keystore password: ' TEMPEST_STORE_PASSWORD
-echo
-export TEMPEST_STORE_PASSWORD
-export TEMPEST_KEY_PASSWORD="$TEMPEST_STORE_PASSWORD"
-cmake --build build/android-example --target TempestExample-apk
-unset TEMPEST_STORE_PASSWORD TEMPEST_KEY_PASSWORD
-```
+The application owns the manifest; `.in` templates are also supported. NativeActivity's `android.app.lib_name` must match the library's `OUTPUT_NAME` without `lib` or `.so`. Use `CMAKE_ARGUMENTS` to pass project-specific CMake options into Gradle's native build.
-Never commit keystores or passwords. A differently signed APK cannot update an existing installation. See Android's [signing guide](https://developer.android.com/studio/publish/app-signing).
+The helper generates one `build.gradle` and points Gradle at this same CMake project. Its inner native build skips packaging generation. The generated directory can also be imported into Android Studio. No separate packaging CMake project or checked-in Gradle wrapper is needed, and desktop builds do not look for Android tools.
diff --git a/Examples/Android/native/main.cpp b/Examples/Android/main.cpp
similarity index 94%
rename from Examples/Android/native/main.cpp
rename to Examples/Android/main.cpp
index 6a2743bd..30a01449 100644
--- a/Examples/Android/native/main.cpp
+++ b/Examples/Android/main.cpp
@@ -4,6 +4,7 @@
#include
// A packaging smoke test using the platform activity, independent of Tempest's Android backend.
+// Fold this into Examples/Empty once that backend is available upstream.
static void draw(ANativeActivity*, ANativeWindow* window) {
ANativeWindow_setBuffersGeometry(window,0,0,WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer buffer = {};
diff --git a/Examples/Android/native/CMakeLists.txt b/Examples/Android/native/CMakeLists.txt
deleted file mode 100644
index ded3dada..00000000
--- a/Examples/Android/native/CMakeLists.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-cmake_minimum_required(VERSION 3.16)
-project(TempestExample LANGUAGES CXX)
-
-if(NOT ANDROID)
- message(FATAL_ERROR "Build this packaging example through Examples/Android")
-endif()
-
-add_library(TempestExample SHARED main.cpp)
-set_target_properties(TempestExample PROPERTIES OUTPUT_NAME tempest-example)
-target_link_libraries(TempestExample PRIVATE android log)
-include(../../../Engine/cmake/TempestAndroid.cmake)
-tempest_android_native_target(TempestExample)
diff --git a/README.md b/README.md
index aca586b6..8331d4b0 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ pm.save(outImg);
### Android builds
-The [Android packaging example](Examples/Android/README.md) generates a single Gradle build from CMake while keeping the application's native CMake project. It uses Android's built-in NativeActivity; the Tempest Android backend is separate work. Desktop builds do not require Java, the Android SDK or Gradle.
+The [Android packaging example](Examples/Android/README.md) uses `add_android_apk` in the application's existing CMake project to generate a single Gradle build. It uses Android's built-in NativeActivity; the Tempest Android backend is separate work. Desktop builds do not require Java, the Android SDK or Gradle.
### Ecosystem
During development various issues of Vulkan stack been found, reported and some were fixed.