Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
200 changes: 200 additions & 0 deletions Engine/cmake/TempestAndroid.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
include_guard(GLOBAL)

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 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(NOT ANDROID)
message(FATAL_ERROR "add_android_apk requires an Android NDK build")
endif()
cmake_parse_arguments(APP "SHRINK_RELEASE"
"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 add_android_apk: ${APP_UNPARSED_ARGUMENTS};${APP_KEYWORDS_MISSING_VALUES}")
endif()
foreach(required PACKAGE_NAME CODE MANIFEST)
if(NOT APP_${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()
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")
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()

# 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")
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()

# 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_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 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(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(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()
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)
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}")
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)
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()
# 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 "${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()
_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(SHRINK_RELEASE false)
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" NO_CMAKE_FIND_ROOT_PATH)
if(TEMPEST_ANDROID_GRADLE_EXECUTABLE)
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}
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}")
endfunction()
103 changes: 103 additions & 0 deletions Engine/cmake/android/build.gradle.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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 }
}

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@

android {
namespace @APPLICATION_ID@
compileSdk @TEMPEST_ANDROID_COMPILE_SDK@
buildToolsVersion '@TEMPEST_ANDROID_BUILD_TOOLS@'
@NDK_CONFIGURATION@

defaultConfig {
applicationId @APPLICATION_ID@
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()
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
externalNativeBuild.cmake.arguments '-DCMAKE_BUILD_TYPE=Debug'
}
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 @NATIVE_SYMBOLS@
externalNativeBuild.cmake.arguments @RELEASE_TYPE@
}
}
externalNativeBuild {
cmake {
path file(@NATIVE_CMAKE@)
version '@TEMPEST_ANDROID_CMAKE@'
}
}
sourceSets {
main {
manifest.srcFile file(@MANIFEST@)
java.srcDirs = @JAVA_DIRS@
res.srcDirs = @RESOURCE_DIRS@
assets.srcDirs = @ASSET_DIRS@
}
}
androidResources {
noCompress.addAll(@NO_COMPRESS@)
}
lint {
disable 'ChromeOsAbiSupport'
}
}
14 changes: 14 additions & 0 deletions Examples/Android/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:label="Tempest example" android:hasCode="false" android:allowBackup="false"
android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen">
<activity android:name="android.app.NativeActivity" android:exported="true"
android:configChanges="keyboardHidden|orientation|screenSize">
<meta-data android:name="android.app.lib_name" android:value="tempest-example" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
19 changes: 19 additions & 0 deletions Examples/Android/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.22)
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)

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)
29 changes: 29 additions & 0 deletions Examples/Android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Android packaging
Comment thread
Try marked this conversation as resolved.

A small NativeActivity packaging example. It will move into `Examples/Empty` when the Android backend is available upstream.

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 -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-apk/build/outputs/apk/release/TempestExample-apk-release.apk
adb shell am start -n org.tempest.example/android.app.NativeActivity
```

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.

In an application's existing CMakeLists.txt, after defining its shared-library target and adding Tempest:

```cmake
if(ANDROID)
add_android_apk(MyGame-apk
CODE MyGame
PACKAGE_NAME org.example.mygame
MANIFEST AndroidManifest.xml)
endif()
```

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.

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.
Loading
Loading