Android
Building and deploying Android projects.
The following guide explains how to build Android projects using minimal command-line tools, without Android Studio involved.
At the very least you need to have Android SDK and Android NDK installed, the guide assumes NDK r19 and newer. Running console utilities and tests on the device don't need much more, in case you want to develop actual applications, you need also the SDK and a platform + SDK platform build tools for version of your choice.
For APK building it's possible to use either an experimental support in CMake or the official way with Gradle. Gradle is able to download all the dependencies on its own, however it's also possible to install system packages for a cleaner setup.
Gradle requires Android SDK version of CMake, which is currently at version 3.10. See below for an experimental way to use the system CMake instead. On the other hand, while it's possible to use the CMake from Android SDK to build Magnum itself, the following guide is written with CMake 3.20 and newer in mind, which has Android NDK r22+ support built-in. CMake 3.16 to 3.19 supports only NDK until r21, in even older versions you'd need to supply a toolchain file instead and the configuration values are different, see the related troubleshooting section for more information.
Building and running console applications
Android allows to run arbitrary console utilities and tests via ADB. Assuming you have Magnum installed in the NDK path as described in Cross-compiling for Android, build your project as below. Adapt paths to your system, Android ABI and version and NDK location as needed.
mkdir build-android-arm64 && cd build-android-arm64 cmake .. \ -DCMAKE_SYSTEM_NAME=Android \ -DCMAKE_SYSTEM_VERSION=29 \ -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a \ -DCMAKE_ANDROID_STL_TYPE=c++_static \ -DCMAKE_BUILD_TYPE=Release cmake --build .
After that you can use ADB to upload your executable to the device and run it there. The global temporary directory is /data/local/tmp
and while the parent directories often don't have permissions, it's possible to cd
into it and create arbitrary files there. Assuming you built an executable in build-android-arm64/Release/bin/my-application
, the workflow would be like this:
adb push build-android-arm64/Release/bin/my-application /data/local/tmp adb shell /data/local/tmp/my-application
You can also use adb shell
to enter the device shell directly and continue from there. Besides plain command-line apps it's also possible to create an EGL context without any extra setup using Platform::
Building and installing graphics apps
Magnum provides Android application wrapper in Platform::
If you plan to use Platform::
if(CORRADE_TARGET_ANDROID) find_package(Magnum REQUIRED AndroidApplication) else() find_package(Magnum REQUIRED Sdl2Application) endif()
Compared to building an app for other platforms, you need to create a shared library instead of an executable:
if(NOT CORRADE_TARGET_ANDROID) add_executable(my-application MyApplication.cpp) else() add_library(my-application SHARED MyApplication.cpp) endif()
Android manifest file
For Android you additionally need an AndroidManifest.xml
file, which describes various properties of the Android package. A minimal stripped-down version is:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="{{ package }}" android:versionCode="1" android:versionName="1.0"> <uses-feature android:glEsVersion="0x00020000" /> <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="11" /> <application android:label="{{ app_name }}" android:hasCode="false"> <activity android:name="android.app.NativeActivity" android:label="{{ app_name }}" android:configChanges="orientation|screenSize"> <meta-data android:name="android.app.lib_name" android:value="{{ lib_name }}" /> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Replace {{ package }}
with Java-like package name for your app (in this case it could be e.g. "cz.mosra.magnum.my_application"
, for example), {{ app_name }}
with human-readable app name that's displayed in the system (so e.g. "My Application"
) and finally the {{ lib_name }}
is name of the library that you compiled with CMake, which in this case would be "my-application"
.
The <uses-feature android:glEsVersion="0x00020000" />
line says that the minimal OpenGL ES version is 2.0, change it in case you require a different version.
The <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="11" />
line requests the minimal and target Android SDK version. Setting the version lower than this (or accidentally omitting this element) will enable the Screen Compatibility Mode, which doesn't report events in coordinates that match underlying framebuffer pixel size. That is not supported by Platform::
The <activity … android:configChanges="orientation|screenSize">
attribute is needed in order to make the application properly receive a viewport event. By default it's being outright killed and recreated when device orientation changes for questionable "performance reasons". If you really want to handle orientation changes that way, remove screenSize
from the set. Among other options is restricting the app to only portrait or landscape screen orientation.
Consult the Android developer documentation for further information about the manifest file.
With this set up, you have two options how to build the final APK, either using plain CMake or using Gradle.
Using CMake
The toolchains
repository contains an UseAndroid.cmake
module that allows you to create an APK in a significantly faster and simpler way than when using Gradle.
Download contents of the toolchains repository from https:/UseAndroid
module from there. Wrapping it in a check for presence of CMAKE_ANDROID_NDK
will make it possible to have the pure CMake and a Gradle build coexist — because Gradle internally uses CMake 3.6 which doesn't know about CMAKE_ANDROID_NDK
yet.
if(CORRADE_TARGET_ANDROID AND CMAKE_ANDROID_NDK) include(${PROJECT_SOURCE_DIR}/toolchains/modules/UseAndroid.cmake) endif()
On the first run, the macro will attempt to detect SDK location, Android Build Tools version and Android Platform version and it prints them to the output like this:
$ cmake . … -- ANDROID_SDK not set, detected /opt/android-sdk -- ANDROID_BUILD_TOOLS_VERSION not set, detected 27.0.3 -- ANDROID_PLATFORM_VERSION not set, detected 27 …
If you don't like what it detected (or the detection failed), edit these values in CMake cache. After that, pass your library target name and location of the AndroidManifest.xml
file to android_create_apk()
. Continuing from the above:
add_library(my-application SHARED MyApplication.cpp) android_create_apk(my-application AndroidManifest.xml)
Note that even thought it doesn't make any sense, the aapt
tool demands the manifest file to be called exactly AndroidManifest.xml
. It can be, however, in any location. This will create an APK named my-application.apk
and additionally also provide a new target, my-application-deploy
, that will use adb install -r
to install the built APK on the device. Building and uploading the application can be then done in a single step:
$ ninja my-application-deploy [5/5] Installing my-application.apk Success
(Or e.g. cmake --build . --target my-application-deploy
if you don't use Ninja.)
For full compatibility with the Gradle build wrap the call again in a check for CMAKE_ANDROID_NDK
and put the manifest in the src/main
subdirectory, where Gradle expects it:
add_library(my-application SHARED MyApplication.cpp) if(CMAKE_ANDROID_NDK) android_create_apk(my-application src/main/AndroidManifest.xml) endif()
Besides packing the main library, the function can also copy resources and assets:
android_create_apk(<target> <manifest> [RESOURCE_DIRECTORY <res>] [ASSET_DIRECTORY <assets>])
The optional RESOURCE_DIRECTORY
and ASSET_DIRECTORY
arguments take relative or absolute paths to directories containing resources (usually named res/
) and assets (usually named assets/
). Both options track file dependencies, so the APK gets repacked when any files in the directories change contents, are added or removed. CMake before 3.12 checks the directories only during CMake runs, on newer versions at every incremental build.
Using Gradle
Besides plain CMake and Gradle it's also possible to use the classic ndk-build
. but that's the least recommended way and it might not be supported in newer NDK builds. The following guide assumes you have Gradle installed in a system-wide location available in $PATH
. See the Gradle installation docs for more information, see below if you want to use the gradlew
wrappers instead.
Create a build.gradle
file that references your root CMakeLists.txt
. Assuming it's saved right next to your root CMakeLists.txt
, the most minimal version might look like this:
buildscript { repositories { jcenter() google() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' } } apply plugin: 'com.android.application' android { compileSdkVersion 25 defaultConfig { minSdkVersion 22 externalNativeBuild { cmake { arguments '-DANDROID_STL=c++_static' } } ndk { abiFilters 'arm64-v8a' } } externalNativeBuild { cmake { path 'CMakeLists.txt' } } }
Important things are compileSdkVersion
and minSdkVersion
, which set SDK version that will be used to compile the project and minimal SDK version that the app can run on. You can add further CMake parameters in the arguments
line (here it's just requesting to use static libc++) and the abiFilters
allow you to restrict which ABIs will the project be built for — Gradle by default builds for both 32 and 64-bit ARM, MIPS and x86, which might be quite annoying to wait for (during development at least). The path
then references your CMakeLists.txt
file. Gradle by default bundles all shared library targets defined in the CMake project, so there's no need to specify a particular library name.
Gradle by default chooses Android SDK Build Tools version that corresponds to the compileSdkVersion
. If you want to have control (for example to make it use Build Tools that you already have installed), specify it using buildToolsVersion
:
android { compileSdkVersion 27 buildToolsVersion '27.0.3' ...
The official documentation contains a more complete overview of all possibilities.
For the AndroidManifest.xml
file, Gradle expects it to be placed inside the src/main
subdirectory, not straight besides the build.gradle
file.
With everything set up, you are now ready to build the project by simply executing the following from the directory with your build.gradle
. During the first run, Gradle will download a huge amount of random stuff when building even the simplest thing. Close your eyes and ignore that it happened.
gradle build
You can also use gradle assembleDebug
which is slightly faster as it doesn't build both Debug and Release and omits some unneeded checks. Installing on a connected device or emulator is then a matter of
gradle installDebug
after which you can launch the app from your home screen. See the Troubleshooting section below if you ran into problems.
Building for multiple ABIs and system versions
The above guide simplifies things a bit and builds for just a single ARM64 ABI. In order to support multiple platforms, you need to separately build and install the dependencies for all ABIs of choice — create separate build directories and run CMake with different CMAKE_ANDROID_ARCH_ABI
and corresponding CMAKE_INSTALL_PREFIX
. Similarly with SDK versions, adapt CMAKE_SYSTEM_VERSION
and CMAKE_INSTALL_PREFIX
to a desired version. The headers are shared and should be always installed into <ndk>/sysroot/usr
regardless of ABI or SDK version. The supported ABI values are:
ABI | Corresponding install prefix |
---|---|
armeabi-v7a | <nk>/platforms/android-<version>/arch-arm/usr |
arm64-v8a | <nk>/platforms/android-<version>/arch-arm64/usr |
x86 | <nk>/platforms/android-<version>/arch-x86/usr |
x86_64 | <nk>/platforms/android-<version>/arch-x86_64/usr |
After that, you can add the additional ABIs to the abiFilters
list in your build.gradle
.
For example, building Magnum for 32-bit and 64-bit ARM with SDK version 29 could look like this, assuming CMake 3.20+ being used.
mkdir build-android-arm && cd build-android-arm cmake .. \ -DCMAKE_SYSTEM_NAME=Android \ -DCMAKE_SYSTEM_VERSION=29 \ -DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a \ -DCMAKE_ANDROID_STL_TYPE=c++_static \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=<ndk>/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr cmake --build . --target install cd .. mkdir build-android-arm64 && cd build-android-arm64 cmake .. \ -DCMAKE_SYSTEM_NAME=Android \ -DCMAKE_SYSTEM_VERSION=29 \ -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a \ -DCMAKE_ANDROID_STL_TYPE=c++_static \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=<ndk>/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr cmake --build . --target install
The build.gradle
for your app then looks like below.
buildscript { repositories { jcenter() google() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' } } apply plugin: 'com.android.application' android { compileSdkVersion 25 defaultConfig { minSdkVersion 24 externalNativeBuild { cmake { arguments '-DANDROID_STL=c++_static' } } ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' } } externalNativeBuild { cmake { path 'CMakeLists.txt' } } }
See the official documentation about ABIs for more information.
Redirecting output to Android log buffer
While printing to standard output and standard error output "just works" with command-line apps, you might want to redirect your Debug, Warning and Error output to Android log buffer. so it can be accessed through the adb logcat
utility. See Corrade::
The Platform::magnum
tag, you can then filter it out like this, for example. Omit -v raw
to show also a date/time prefix and other info for each printed line.
$ adb logcat *:S magnum -v raw ... Renderer: Mali-G71 by ARM OpenGL version: OpenGL ES 3.2 v1.r2p0 Using optional features: GL_EXT_robustness
Using system-wide CMake installation
According to the official documentation, it's possible to use system CMake installation without needing to install Android SDK version of CMake 3.10. Simply update the externalNativeBuild
in your build.gradle
file to specify CMake version that you have installed in your system, for example:
android { ... externalNativeBuild { cmake { path 'CMakeLists.txt' ... version '3.17' } } }
However, be aware that this is an experimental feature and may be broken.
Using gradlew wrappers instead of a system installation
It's possible to bundle Gradle in the project itself as opposed to requiring a pre-existing system installation. It has the downside of having a bit more boilerplate files in your project, though.
First, add the following to your build.gradle
file:
task wrapper(type: Wrapper) { gradleVersion = '4.0' }
Then run this on a system that has Gradle installed:
gradle wrapper
This will generate the following files that you can then add to version control:
gradlew
shell script for Unix-like systemsgradle.bat
batch script for Windowsgradle/
directory with wrapper binaries
With this in place, you can just use gradlew
instead of gradle
.
Setting up Android build on Travis CI
For simple compilation tests, add the following to your .travis.yml
matrix builds. According to the Travis Android documentation, build-tools-22.0.1
and android-22
are always present, so your builds shouldn't get any extra delay when requesting them. The $TARGET
environment variable is used here only to disambiguate later, you might or might not need it.
matrix: include: # ... - language: android os: linux dist: trusty env: - TARGET=android android: components: - build-tools-22.0.1 - android-22
At the time of writing (March 2018), while the generic Ubuntu 14.04 images already have CMake 3.9.2, for some reason the Android Ubuntu 14.04 images have just CMake 3.2. Android support is builtin since version 3.7, but an important fix for the LLVM toolchain was merged as late as in 3.9.2, so you may want to grab that version. Example .travis.yml
setup that downloads the binary and extracts it to $HOME/cmake
, with $PATH
setup and caching:
cache: directories: - $HOME/cmake install: - > if [ "$TARGET" == "android" ] && [ ! -e "$HOME/cmake/bin" ]; then cd $HOME ; wget https://cmake.org/files/v3.9/cmake-3.9.2-Linux-x86_64.tar.gz && mkdir -p cmake && cd cmake && tar --strip-components=1 -xzf ../cmake-3.9.2-Linux-x86_64.tar.gz && cd $TRAVIS_BUILD_DIR ; fi - > if [ "$TARGET" == "android" ]; then export PATH=$HOME/cmake/bin:$PATH && cmake --version ; fi
The NDK can be fetched as a simple *.zip
file. However, version r16b has over 800 MB, so you might want to explore creation of a Standalone Toolchain with only the things you need to speed up the build. Again, downlading it into $HOME/android-ndk-r16b
is a matter of adding this into your install:
section:
- > if [ "$TARGET" == "android" ]; then cd $HOME ; wget https://dl.google.com/android/repository/android-ndk-r16b-linux-x86_64.zip && unzip -q android-*.zip && cd $TRAVIS_BUILD_DIR ; fi
Travis CI discourages caching the NDK, as downloading the cache will take roughly the same amount of time as downloading it from upstream.
Building your actual code is just a matter of setting up a correct NDK path. You can install the dependencies to any location as long as you specify the same location in CMAKE_PREFIX_PATH
and CMAKE_FIND_ROOT_PATH
in depending projects. Using armeabi-v7a
instead of arm64-v8a
ensures that you can run the code in a preinstalled emulator later, see below.
mkdir build-android-arm && cd build-android-arm cmake .. \ -DCMAKE_ANDROID_NDK=$HOME/android-ndk-r16b \ -DCMAKE_SYSTEM_NAME=Android \ -DCMAKE_SYSTEM_VERSION=22 \ -DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a \ -DCMAKE_ANDROID_NDK_TOOLCHAIN_VERSION=clang \ -DCMAKE_ANDROID_STL_TYPE=c++_static \ -DCMAKE_PREFIX_PATH=$HOME/deps \ -DCMAKE_FIND_ROOT_PATH=$HOME/deps \ ...
Running tests on the emulator
In order to run your tests on the emulator, you need to request some system image. Again, sys-img-armeabi-v7a-android-22
is part of the default installation, so it shouldn't add any extra time to your build:
matrix: include: - language: android # ... android: components: # ... - sys-img-armeabi-v7a-android-22
As described in the Travis documentation, create a system image and wait for the emulator to start (be prepared, it can easily take up minutes). Assuming you use the Corrade::ctest
and optionally enable colored output for extra clarity:
echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a emulator -avd test -no-audio -no-window & android-wait-for-emulator CORRADE_TEST_COLOR=ON ctest -V
APK bundle creation
At the time of writing (March 2018), Travis Ubuntu 14.04 has Gradle 4.0, however the Android build plugin 3.0 requires at least Gradle 4.1, so you need to backport gradle.build
to plugin version 2.3.3 compared to the template above. In particular, the classpath
needs to be updated, compileSdkVersion
and minSdkVersion
adapted to versions defined in components:
in your travis.yml
file and the buildToolsVersion
explicitly specified, because that's needed in plugin versions before 3.0:
buildscript { // ... dependencies { classpath 'com.android.tools.build:gradle:2.3.3' } } // ... android { compileSdkVersion 22 buildToolsVersion '26.0.2' defaultConfig { minSdkVersion 22 // ...
Gradle bundles its own CMake 3.6, downloading it on-demand and then failing because SDK licenses are not signed. Solution is to install CMake and sign its license explicitly beforehand. Add the following to your .travis.yml
:
before_install: - if [ "$TARGET" == "android" ]; then yes | sdkmanager "cmake;3.6.4111459"; fi
Unlike above, and especially if you build for multiple ABIs, it's better to install all dependencies where Gradle expects them. In particular, in case of Corrade and ARM64 ABI and NDK being in $HOME/android-ndk-r16b
, the install prefixes look like this:
cmake .. \ -DCMAKE_INSTALL_PREFIX=$HOME/android-ndk-r16b/platforms/android-22/arch-arm64/usr \ -DCORRADE_INCLUDE_INSTALL_PREFIX=$HOME/android-ndk-r16b/sysroot/usr \ ...
Finally, you need to tell Gradle where the NDK is located and where to look for native binaries (for example the corrade-rc
executable) using environment variables. At last, execute gradle build
in the directory where build.gradle
is:
export ANDROID_NDK_HOME=$HOME/android-ndk-r16b export CMAKE_PREFIX_PATH=$HOME/deps-native/ gradle build
Troubleshooting
CMake or NDK is too old
While the minimal CMake version that's required for building Magnum for Android is 3.7, NDK r22 and newer need at least CMake 3.20, and attempting to use CMake 3.19 or older with NDK r22+ will greet you with an error similar to the following, as NDK r22 removed certain deprecated internal paths:
CMake Error at /usr/share/cmake-3.17/Modules/Platform/Android-Determine.cmake:176 (message): Android: The API specified by CMAKE_SYSTEM_VERSION='29' does not exist in the NDK. The directory: /opt/android/sdk/ndk/22.1.7171670/platforms/android-29 does not exist. Call Stack (most recent call first): /usr/share/cmake-3.17/Modules/CMakeDetermineSystem.cmake:129 (include) CMakeLists.txt:70 (project)
NDK r19 to r21 need at least CMake 3.16 to work, and CMake before version 3.20 needs extra help with CMAKE_FIND_ROOT_PATH
and CMAKE_FIND_LIBRARY_CUSTOM_LIB_SUFFIX
to correctly find Android libraries, otherwise it falls back to looking in native system locations. An example is shown below, adapt them to your system, Android ABI and version NDK location as needed.
cmake .. \ -DCMAKE_SYSTEM_NAME=Android \ -DCMAKE_SYSTEM_VERSION=29 \ -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a \ -DCMAKE_ANDROID_STL_TYPE=c++_static \ -DCMAKE_FIND_ROOT_PATH=/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot \ -DCMAKE_FIND_LIBRARY_CUSTOM_LIB_SUFFIX=/aarch64-linux-android/29 \ -DCMAKE_BUILD_TYPE=Release
For NDKs before r19 you might want to grab at least CMake 3.9.2, as it fixes an issue with the Clang toolchain. With NDK r18 and older you'll need to additionally supply -DCMAKE_ANDROID_NDK_TOOLCHAIN_VERSION=clang
, on the other hand the -DCMAKE_FIND_ROOT_PATH
isn't needed — CMake is able to figure out the paths on its own in that case.
Compiler suddenly fails at life
If you're suddenly greeted with an overwhelming amount of strange errors coming from standard library C++ headers including things like
/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/c++/v1/cmath:319:9: error: no member named 'isnormal' in the global namespace using ::isnormal; ~~^ /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/c++/v1/cmath:320:9: error: no member named 'isgreater' in the global namespace using ::isgreater; ~~^ /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/c++/v1/cmath:321:9: error: no member named 'isgreaterequal' in the global namespace using ::isgreaterequal; ~~^ /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/c++/v1/cmath:322:9: error: no member named 'isless' in the global namespace using ::isless; ~~^ /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/c++/v1/cmath:323:9: error: no member named 'islessequal' in the global namespace using ::islessequal; ~~^ /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/c++/v1/cmath:324:9: error: no member named 'islessgreater' in the global namespace using ::islessgreater; ~~^
it might be due to <ndk>/sysroot/usr/include
being present in your include path. For some reason the NDK has two copies of system includes, one in <ndk>/toolchains/llvm/prebuilt/<host>/sysroot/usr
and one here, and the one in <ndk>/sysroot
causes the above errors. Find the offending include dir and point it to the other location to fix the error.
ADB fails when more devices are connected
If any adb
command fails with
error: more than one device/emulator
you can pass the device ID via the -s
parameter to each command. That is, however, verbose and doesn't work for example in case of running Corrade::ctest
. Instead, it's possible to supply the device ID via an environment variable, for example:
export ANDROID_SERIAL="bla"
Signing the APK using CMake fails
At the moment the location and passphrase for the Android signing keystore is implicitly set to $HOME/.android/debug.keystore
with android
as a password, since that's the location (and password) that Gradle uses. If you see output similar to this:
[1/2] Signing my-application.apk FAILED: my-application.apk … Failed to load signer "signer #1" java.io.FileNotFoundException: <home>/.android/debug.keystore
then you either don't have the keystore generated yet or it's in some other location. Similar error can happen if the password is incrrect, in which case it will say the following instead:
Failed to load signer "signer #1" java.io.IOException: Keystore was tampered with, or password was incorrect
Generating a debug keystore can be done by running through the Gradle build at least once. It's also possible to create a keystore using Android Studio or using the keytool
utility that's bundled with Java SDK. The following creates a key equivalent to the Gradle-generated one, adapt the location as necessary:
mkdir -p $HOME/.android # The destination directory has to exist keytool -genkeypair -keystore $HOME/.android/debug.keystore \ -storepass android -alias androiddebugkey -keypass android \ -keyalg RSA -validity 10000 -dname CN=,OU=,O=,L=,S=,C=
The location and password is controlled via the ANDROID_APKSIGNER_KEY
variable, edit your CMake cache to point it to a different location or password. Note that the command parameters have to be delimited by ;
instead of a space. For example, setting the location to /etc/android.keystore
with a password secret
can be done like this:
cmake . -DANDROID_APKSIGNER_KEY="--ks;/etc/android.keystore;--ks-pass;pass:secret"
You can also add further arguments to apksigner
here. See the official documentaton for apksigner for details.
Signing fails because of some cryptic x509 error
If you managed past the above but get an error like
Exception in thread "main" java.lang.IllegalAccessError: class com.android.apksig.internal.apk.v1.V1SchemeSigner (in unnamed module @0x1684264) cannot access class sun.security.x509.AlgorithmId (in module java.base) because module java.base does not export sun.security.x509 to unnamed module @0x1684264
it's probably due to the Java version used being too new. Signing is known to work with Java 8, and Java 8 is also known to work for Gradle and everything else. See Too new Java for more information.
Warning about ANDROID_NDK_HOME being deprecated
If you see the following warning when building with Android Gradle Plugin 3.6.0 and newer, it's because an $ANDROID_NDK_HOME
environment variable is defined.
$ gradle build > Configure project : WARNING: Support for ANDROID_NDK_HOME is deprecated and will be removed in the future. Use android.ndkVersion in build.gradle instead. Support for ANDROID_NDK_HOME is deprecated and will be removed in the future. Use android.ndkVersion in build.gradle instead.
Ignore it, it's the best way. If you really want to get rid of the warning you can unset ANDROID_NDK_HOME
or edit your default system environment and remove this entry, but at that point Gradle will most probably suddenly want to download the NDK again somewhere inside the SDK installation directory, which may fail due to readonly directory permissions. At that point you're on your own, sorry. This path hasn't been investigated yet.
Specifying NDK version in gradle.build
With Android Gradle plugin 3.6.0 and newer it's possible to specify which NDK to use via android.ndkVersion
. However it requires you to specify the full version and doing something like
android { ndkVersion "19" // ...
will result in the following error, or worse, a cryptic error about some closure.
> Specified android.ndkVersion '19' does not have enough precision. Use major.minor.micro in version.
The complete NDK version isn't listed in any of the online Android docs, you have to look into the source.properties
file in the NDK root, which might say for example the following. It may be also available through the GUI SDK manager.
Pkg.Desc = Android NDK Pkg.Revision = 19.0.5232133
Debugging Gradle CMake issues
Gradle by default doesn't show any useful output for the CMake run, only the error output when something goes wrong. For general debugging, you have to run gradle build -i
, which will spit out immense heaps of even more useless content, and the actual CMake output messages are interleaved with some JSON dumps in a way that makes them practically invisible. To find a start of the CMake log, it might help to redirect the output and then search for Check for working CXX compiler
.
To make things worse, the CMake build isn't stored in the build/
directory, but rather in (hidden) .externalNativeBuild/
or .cxx/
directories, depending on which version you're at. Be sure to remove those when retrying a CMake configure step, otherwise CMake may not even get run.
Gradle CMake can't find dependencies
Gradle by default searches only in the NDK install path. If you have your dependencies installed somewhere else (this goes especially for the native corrade-rc
executable), you might want to point the CMAKE_PREFIX_PATH
environment variable to your install location:
export CMAKE_PREFIX_PATH=<path-where-your-dependencies-are-installed> gradle build
You can also put that directly into the gradle.build
file, although that's not recommended because it hardcodes your local system setup in the project:
android { ... defaultConfig { ... externalNativeBuild { cmake { // Append to the existing arguments that are there arguments '-DANDROID_STL=c++_static', '-DCMAKE_PREFIX_PATH=<path-where-your-dependencies-are-installed>' } }
If you have the dependencies installed in the NDK path, but it still fails, check that you installed for the same SDK version as in minSdkVersion
and all ABIs mentioned in abiFilters
inside your build.gradle
file — Gradle runs CMake once for each entry in the list so it might happen that it finds them for all but one ABI. See Building for multiple ABIs and system versions above for more information.
App can't launch
If your application can't launch (or it just blinks and then disappears), you can inspect adb logcat
output to see what went wrong, but be quick, the log is spitting out a lot of info all the time. Possible causes:
- Mismatch between actual library name and library referenced from
AndroidManifest.xml
, causing Java to fail loading it - The device has an ABI for which the app was not compiled (check the
abiFilters
option inbuild.gradle
) or the app was compiled with SDK version that's not supported by the device yet. See the official API level documentation for more information. - The device doesn't support OpenGL ES 3.0 yet. Rebuild Magnum and its dependencies with the
MAGNUM_TARGET_GLES2
option enabled. See Enabling or disabling features for more information. - Loading fails with
ANativeActivity_onCreate
symbol not being found. If you are using Platform::AndroidApplication, this issue should be prevented, otherwise you need to add -u ANativeActivity_onCreate
to your linker flags or reference the symbol some other way. See android-ndk/ndk#381 for details. - Additional
*.so
libraries are referenced by the main*.so
but not bundled in the*.apk
. One option is to switch to static libraries, another is explicitly specifying them in thebuild.gradle
file. See the official documentation for details.
Gradle aborting due to "failed to notify project evaluation listener"
All these errors look the same to an untrained eye, starting with the following, however there's a broad variety of reasons and of course it won't tell you what is actually wrong:
$ gradle build FAILURE: Build failed with an exception. What went wrong: … > Failed to notify project evaluation listener.
The line right after is the key clue to know the root cause — see the following sections
Too new Java
Older versions of Android build tools didn't support Java 9 or 10, which could manifest as any of the following:
> Failed to notify project evaluation listener. > javax/xml/bind/annotation/XmlSchema
> Failed to notify project evaluation listener. > Could not initialize class com.android.sdklib.repository.AndroidSdkHandler
Solution is to point $JAVA_HOME
to a Java 8 installation, for example. This affects not just gradle
, but also other tools like sdkmanager
(see below), so you may want to export it for the whole session:
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk/ gradle build
On ArchLinux, Java 8 is provided by the jdk8-openjdk package. Unfortunately the android-sdk
and gradle
packages depend on a java-runtime
alias, which allows you to pick any other version as well.
Too new Gradle (or maybe NDK or...)
Older version of Android build tools don't seem to work with new Gradle (in this case 6.6.1) anymore. Or that's the most plausible explanation, it might also be that these don't work with new NDK versions either (r19 in this case) but why is that reported by a throwing exception 1000 stack frames deep in some totally random ungoogleable API is just beyond me. Anyway — the following may happen with Android Gradle plugin 2.3.3:
> Failed to notify project evaluation listener. > org.gradle.api.tasks.compile.CompileOptions.setBootClasspath(Ljava/lang/String;)V
While this with 3.0.0:
> Failed to notify project evaluation listener. > org.gradle.api.internal.TaskInputsInternal.property(Ljava/lang/String;Ljava/lang/Object;)Lorg/gradle/api/tasks/TaskInputs;
This for 3.3.0:
> Failed to notify project evaluation listener. > org.gradle.api.file.ProjectLayout.directoryProperty(Lorg/gradle/api/provider/Provider;)Lorg/gradle/api/file/DirectoryProperty;
And probably a dozen other variants, but I am not willing to waste my time any further by enumerating all possible embarrassing crashes of cursed tools. The oldest Android Gradle plugin version that worked with Gradle 6.6.1 and NDK r19 was 3.6.0, and to change it update the following entry in your build.gradle
:
buildscript { // ... dependencies { classpath 'com.android.tools.build:gradle:3.6.0' } }
Gradle fails because no repositories are defined
Android Gradle plugin 3.6 and newer may greet you with the following when doing a fresh build:
$ gradle build > Task :mergeDebugResources FAILED FAILURE: Build failed with an exception. What went wrong: Execution failed for task ':mergeDebugResources'. > Could not resolve all files for configuration ':_internal_aapt2_binary'. > Cannot resolve external dependency com.android.tools.build:aapt2:4.0.0-6051327 because no repositories are defined. Required by: project :
Solution is to add the following to your build.gradle
:
allprojects { repositories { google() } }
in addition to the existing repositories defined inside buildscript
:
buildscript { repositories { jcenter() google() } // ...
Both the two existing repositories inside buildscript
are still needed, it will fail with different but no less cryptic errors if you attempt to remove either of them. When you add the allprojects
entry, the problem may seemingly look like it disappeared, since removing allprojects
again will still work. This is only true until you recreate the build directory from scratch so better keep it there.
Accepting SDK licenses for Gradle
Gradle might refuse to build a project if SDK licenses are not accepted, with an error that might look like this:
$ gradle build FAILURE: Build failed with an exception. A problem occurred configuring root project 'MyApplication'. > You have not accepted the license agreements of the following SDK components: [Android SDK Platform 22]. Before building your project, you need to accept the license agreements and complete the installation of the missing components using the Android Studio SDK Manager. Alternatively, to learn how to transfer the license agreements from one workstation to another, go to http://d.android.com/r/studio-ui/export-licenses.html
Depending on where your SDKs are installed, you might need to execute the following (assuming you have SDK version 26 at least):
sdkmanager --licenses # and then manually accept all of them
If the tool blows up with the following error, it's again because the Java version is too new, see above for a solution:
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156) …
Gradle complains about an exception while marshalling some XML file that doesn't exist
$ gradle build > Configure project : Exception while marshalling /opt/android-sdk/build-tools/29.0.3/package.xml. Probably the SDK is read-only …
This message doesn't seem to result in a build error, however it might be annoying. The files it complains about don't exist on the filesystem and it's related to the Accepting SDK licenses for Gradle problem — the package.xml
files get created during the (sudo
ed) run of sdkmanager
, even if there are no licenses left to sign.
Gradle fails because no signature of method is applicable for argument types
$ gradle build Where: Build file '…/build.gradle' line: … What went wrong: … > No signature of method: build_3w19jaw3zqx2h2gvdj91syekc.android() is applicable for argument types: (build_3w19jaw3zqx2h2gvdj91syekc$_run_closure1) values: [build_3w19jaw3zqx2h2gvdj91syekc$_run_closure1@393eb206]
The particular cryptic value may differ, but this means you have some error in your Gradle file. Of course it won't tell you what the error is, so good luck commenting out parts of the file until it works or you reach a different error.
Android SDK directory permissions
Gradle is able to work with system-installed Android SDK. If it complains about directory permissions such as
> Failed to install the following SDK components: [Android SDK Build-Tools 26.0.2, Android SDK Platform 25] The SDK directory (/opt/android-sdk) is not writeable, please update the directory permissions.
it's often enough to just install such packages. In case of ArchLinux, all relevant packages are available in AUR. For the above error in particular, the matching packages are android-sdk-build-tools-26.0.2 and android-platform-25.
Obsolete troubleshooting
These issues should no longer appear in practice with reasonably recent versions of Android tools, however are still kept to remind us everything used to be even worse than it is now in case your project keeps using the older versions (which is recommended for sanity, there's not many things worse than upgrading Android NDK).
Gradle aborting due to termcap
If you see the following output, Gradle is crashing because $TERM
is set to xterm-256color
or xterm-24
:
$ gradle build FAILURE: Build failed with an exception. What went wrong: Could not open terminal for stdout: could not get termcap entry
Solution is to set TERM=xterm
. See gradle/
TERM=xterm gradle build
CMake complaining about some NVIDIA Nsight Tegra Visual Studio Edition on Windows
On Windows, with CMake versions earlier than 3.19, it's possible that you get the following CMake error when configuring an Android project:
CMAKE_SYSTEM_NAME is 'Android' but 'NVIDIA Nsight Tegra Visual Studio Edition' is not installed.
This is because Visual Studio Project Files as the default generator on Windows was not able to build for any other system than Windows itself. To fix it, either update to CMake 3.19 or newer, or use a different generator — for example Ninja. Download the binary, put it somewhere on your PATH
and pass -G Ninja
to CMake. Alternatively, pass the path to it using -DCMAKE_MAKE_PROGRAM=C:/path/to/ninja.exe
.