Ashok
Very nice and informative.
As a Qt developer, you most likely are using qmake
as a build system for your apps. It comes bundles with Qt, is easy to use and usually gets the job done.
However, there are other alternatives out there, most notably cmake
. What makes it interesting is, that there are a lot of projects (and in particular libraries) which use it, so if you go with cmake
in your own app, you can easily pull in such libraries directly into your own build.
Now, cmake
and Qt vastly play well together. However, if you target Android and you are using Qt Creator as your development vehicle, you have to tweak it a little bit. In this post, I show the steps you need to take to configure your Qt Creator IDE and your projects to be able to:
Let’s start.
I will assume the following:
r10e
.cmake
, at least v3.7
.cmake
, and the Android SDK and NDK.Some words on the Android NDK requirement: r10e
is a bit dated, yes. However, Qt itself currently is still compiled using it. Newer ones tend to cause issues, so you better stick with that specific release at least for building Qt apps. Currently, Qt is considering moving to clang
as a compiler (which is by now the default one in the NDK). This switch will probably be released with Qt 5.12 (see this issue in the Qt bug tracker) - which probably also will mean you can use a more recent NDK release afterwards.
If your environment is correctly set up, you should find Kit definitions for at least the installed Qt Android versions in the Kits section in the configuration dialog of Qt Creator:
These definitions work well when building apps using qmake
, however, we have to fine-tune them to be really usable with cmake
. Alternatively, you could provide the configurations needed in each app you build.
Start by selecting one of the Android kits, e.g the ARM one, and press the Clone button. We need to do this, because Qt Creator does not let us change the auto-detected kits. The cloned one should appear in the list of manually defined kits. Select it and scroll to the very bottom. There, you’ll find the cmake
configuration:
Hit the Change button. This will bring up another dialog where you can provide variables that Qt Creator will pass down to cmake
when configuring:
You will find some pre-defined variables here, most notably the C and C++ compilers to use (CMAKE_C_COMPILER
and CMAKE_CXX_COMPILER
), the path to the Qt installation (CMAKE_PREFIX_PATH
) and the full path to the qmake
executable (QT_QMAKE_EXECUTABLE
).
We need to add some more variables to this list. Here’s the complete list of compiling to Android on ARM:
CMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx}
CMAKE_C_COMPILER:STRING=%{Compiler:Executable:C}
CMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX}
QT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable}
CMAKE_SYSTEM_NAME=Android
CMAKE_SYSTEM_VERSION=16
CMAKE_ANDROID_ARCH_ABI=armeabi-v7a
CMAKE_ANDROID_STL_TYPE=gnustl_shared
ANDROID_SDK_ROOT=/opt/Android/android-sdk-linux/
For Android on x86, the list would look like this:
CMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx}
CMAKE_C_COMPILER:STRING=%{Compiler:Executable:C}
CMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX}
QT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable}
CMAKE_SYSTEM_NAME=Android
CMAKE_SYSTEM_VERSION=16
CMAKE_ANDROID_ARCH_ABI=x86
CMAKE_ANDROID_STL_TYPE=gnustl_shared
ANDROID_SDK_ROOT=/opt/Android/android-sdk-linux/
What does the magic mean?
CMAKE_SYSTEM_NAME
causes cmake
to go to cross compiling mode, trying to configure and compile for Android.CMAKE_SYSTEM_VERSION
is the version of the target system to compile for. In case of Android, the version corresponds to the (minimum) Android SDK level we target.CMAKE_ANDROID_ARCH_ABI
tells cmake
the Android ABI we want to compile to.CMAKE_ANDROID_STL_TYPE
specifies the variant of the STL to use.ANDROID_SDK_ROOT
we point to the root of the Android SDK. This is not a standard cmake
variable, We’ll use it later in our cmake
files, as we need to pass this location to some Qt tools plus we need it ourselves for e.g. deploying our apps.That’s it. To verify the configuration works as expected, create a minimal example like the below (which is basically generated via Qt Creator, selecting cmake
as build system):
// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
// main.qml
import QtQuick 2.11
import QtQuick.Controls 2.2
import QtQuick.Window 2.11
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Label {
text: qsTr("Android CMake Test")
anchors.centerIn: parent
}
}
<!-- qml.qrc -->
<RCC>
<qresource prefix="/">
<file>main.qml</file>
</qresource>
</RCC>
# CMakeLists.txt
cmake_minimum_required(VERSION 3.1)
project(android-cmake-demo LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt5 COMPONENTS Core Quick REQUIRED)
add_executable(${PROJECT_NAME} "main.cpp" "qml.qrc")
target_compile_definitions(${PROJECT_NAME} PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Core Qt5::Quick)
Put these files together in one folder. You should be able to:
When you build the sample app for your host OS, you should get the following:
So far, so good. However, without further ado we won’t be able to deploy out app to Android. This has two reasons:
CMakeLists.txt
file above, we compile our app as executable. However, on Android, every app must have a Java entry point. Hence, we must provide such one as well. To better understand this, check out this Qt blog post.To manage this, we first need to add two more files to the project:
# cmake/qt-android-mk-apk.cmake
include(CMakeParseArguments)
set(QT_ANDROID_MK_APK_DIR ${CMAKE_CURRENT_LIST_DIR})
function(qt_android_build_apk)
set(options)
set(oneValueArgs
TARGET PACKAGE_NAME ANDROID_EXTRA_FILES QML_ROOT_PATH
SDK_BUILD_TOOLS_VERSION EXTRA_LIBS)
set(multiValueArgs)
cmake_parse_arguments(
APK "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
# Gather required variables to create the configuration file.
find_package(Qt5 COMPONENTS Core REQUIRED)
# Qt5Core_DIR now points to $Qt5InstallPrefix/lib/cmake/Qt5Core, so
# we get the parent directory three times:
get_filename_component(QT5_INSTALL_PREFIX "${Qt5Core_DIR}/../../.." ABSOLUTE)
message("Qt5 installed in ${QT5_INSTALL_PREFIX}")
# Adjust QML root path if not set:
if(NOT APK_QML_ROOT_PATH)
set(APK_QML_ROOT_PATH $<TARGET_FILE_DIR:${APK_TARGET}>)
endif()
# Get he toolchain prefix, i.e. the folder name within the
# toolchains/ folder without the compiler version
# APK_NDK_TOOLCHAIN_PREFIX
file(RELATIVE_PATH APK_NDK_TOOLCHAIN_PREFIX ${CMAKE_ANDROID_NDK} ${CMAKE_CXX_COMPILER})
string(REPLACE "/" ";" APK_NDK_TOOLCHAIN_PREFIX ${APK_NDK_TOOLCHAIN_PREFIX})
list(GET APK_NDK_TOOLCHAIN_PREFIX 1 APK_NDK_TOOLCHAIN_PREFIX)
string(LENGTH "-${CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION}" VERSION_LENGTH)
string(LENGTH "${APK_NDK_TOOLCHAIN_PREFIX}" FOLDER_LENGTH)
math(EXPR PREFIX_LENGTH ${FOLDER_LENGTH}-${VERSION_LENGTH})
string(SUBSTRING "${APK_NDK_TOOLCHAIN_PREFIX}" 0 ${PREFIX_LENGTH} APK_NDK_TOOLCHAIN_PREFIX)
# Get path to the target:
set(APK_TARGET_OUTPUT_FILENAME $<TARGET_FILE:${APK_TARGET}>)
# Get Android SDK build tools version:
if(NOT APK_SDK_BUILD_TOOLS_VERSION)
file(GLOB sdk_versions RELATIVE ${ANDROID_SDK_ROOT}/build-tools
${ANDROID_SDK_ROOT}/build-tools/*)
list(GET sdk_versions -1 APK_SDK_BUILD_TOOLS_VERSION)
endif()
# Step 1: Create an intermediate config file. At this point,
# the generator expressions will we use are not yet resolved.
configure_file(
${QT_ANDROID_MK_APK_DIR}/qt-android-deployment.json.in
${CMAKE_CURRENT_BINARY_DIR}/${APK_TARGET}-config.json.pre)
# Step 2: Run file(CONFIGURE ...) to create the final config JSON
# with generator expressions resolved:
file(
GENERATE
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${APK_TARGET}-config.json
INPUT ${CMAKE_CURRENT_BINARY_DIR}/${APK_TARGET}-config.json.pre)
# Step 3: Create a custom target which will build our APK:
set(APK_DIR ${CMAKE_CURRENT_BINARY_DIR}/${APK_TARGET}-apk-build)
if(NOT APK_ANDROID_EXTRA_FILES)
set(
APK_ANDROID_EXTRA_FILES
${QT5_INSTALL_PREFIX}/src/android/templates/)
endif()
if(JAVA_HOME)
set(ANDROIDDEPLOYQT_EXTRA_ARGS
${ANDROIDDEPLOYQT_EXTRA_ARGS} --jdk '${JAVA_HOME}')
endif()
if(${CMAKE_BUILD_TYPE} STREQUAL Release)
set(ANDROIDDEPLOYQT_EXTRA_ARGS
${ANDROIDDEPLOYQT_EXTRA_ARGS} --release)
set(APK_FILENAME ${APK_TARGET}-apk-build-release-unsigned.apk)
else()
set(APK_FILENAME ${APK_TARGET}-apk-build-debug.apk)
endif()
add_custom_target(
${APK_TARGET}-apk
COMMAND ${CMAKE_COMMAND} -E remove_directory ${APK_DIR}
COMMAND ${CMAKE_COMMAND} -E copy_directory
${QT5_INSTALL_PREFIX}/src/android/templates/
${APK_DIR}
COMMAND ${CMAKE_COMMAND} -E copy_directory
${APK_ANDROID_EXTRA_FILES}/
${APK_DIR}
COMMAND ${CMAKE_COMMAND} -E make_directory
${APK_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI}
COMMAND ${CMAKE_COMMAND} -E copy
${APK_TARGET_OUTPUT_FILENAME}
${APK_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI}
COMMAND ${QT5_INSTALL_PREFIX}/bin/androiddeployqt
--verbose
--output ${APK_DIR}
--input ${CMAKE_CURRENT_BINARY_DIR}/${APK_TARGET}-config.json
--deployment bundled
--gradle
${ANDROIDDEPLOYQT_EXTRA_ARGS}
)
# Step 4: Create a custom target which pushes the created APK onto
# the device.
add_custom_target(
${APK_TARGET}-apk-install
COMMAND ${ANDROID_SDK_ROOT}/platform-tools/adb install -r
${APK_DIR}/build/outputs/apk/${APK_FILENAME}
DEPENDS
${APK_TARGET}-apk
)
endfunction()
… and …
// cmake/qt-android-deployment.json.in
{
"description": "This file is read by androiddeployqt",
"qt": "@QT5_INSTALL_PREFIX@",
"sdk": "@ANDROID_SDK_ROOT@",
"ndk": "@CMAKE_ANDROID_NDK@",
"toolchain-prefix": "@APK_NDK_TOOLCHAIN_PREFIX@",
"tool-prefix": "@CMAKE_CXX_ANDROID_TOOLCHAIN_MACHINE@",
"toolchain-version": "@CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION@",
"ndk-host": "@CMAKE_ANDROID_NDK_TOOLCHAIN_HOST_TAG@",
"target-architecture": "@CMAKE_ANDROID_ARCH_ABI@",
"application-binary": "@APK_TARGET_OUTPUT_FILENAME@",
"android-package": "@APK_PACKAGE_NAME@",
"qml-root-path": "@APK_QML_ROOT_PATH@",
"sdkBuildToolsRevision": "@APK_SDK_BUILD_TOOLS_VERSION@",
"android-package-source-directory": "@APK_ANDROID_EXTRA_FILES@",
"android-extra-libs": "@APK_EXTRA_LIBS@"
}
We will pull in the first file into our CMakeLists.txt
. It provides a function - qt_android_build_apk
- which we can use to wrap our app and create an APK file. This function also will create a target to deploy the app to a connected Android device. The second file is a template for a JSON deployment configuration. This configuration will later be passed to the androiddeployqt
tool, which copies the dependencies of our app into the APK build folder.
With these two, we can adjust our CMakeLists.txt
file as follows:
cmake_minimum_required(VERSION 3.1)
project(android-cmake-demo LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt5 COMPONENTS Core Quick REQUIRED)
include(cmake/qt-android-mk-apk.cmake)
if(ANDROID)
add_library(${PROJECT_NAME} SHARED "main.cpp" "qml.qrc")
else()
add_executable(${PROJECT_NAME} "main.cpp" "qml.qrc")
endif()
target_compile_definitions(${PROJECT_NAME} PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Core Qt5::Quick)
qt_android_build_apk(
TARGET ${PROJECT_NAME}
PACKAGE_NAME org.example.QmlCmakeDemo
QML_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}
ANDROID_EXTRA_FILES ${CMAKE_CURRENT_SOURCE_DIR}/android
)
The notable changes:
include()
, we pull in the utility file from above into the build system.if(ANDROID)
, we check if we are building for Android. If this is the case, we use add_library
instead of add_executable
to build a shared library instead of an executable file.qt_android_build_apk
function.The last point is the most important one. It will cause the build to create the deployment JSON file required to use androiddeployqt
. In addition, it will add two new targets to the build: android-cmake-demo-apk
will cause our app to be packaged into an APK file. If we are doing a debug build, the resulting APK will be signed with a debug key, so you can immediately deploy it to a device. For release builds, the APK will not be signed. You can, however, adjust the function to do exactly this, if you need. The second target - android-cmake-demo-akp-install
- causes the APK to get deployed to an Android device connected to our host (or a running Android Virtual Device).
The qt_android_build_apk
takes several options to customize its behavior:
TARGET
is the application we want to generate an APK for. Hence, if you have a larger project with several apps, you can call the function several times, once for each app you want to package.PACKAGE_NAME
is the name of the package (e.g. org.example.AndroidCmakeDemo
) used to identify the app on Android. This usually should be the same as mentioned in the Android Manifest file used for the build (see below).ANDROID_EXTRA_FILES
is the path to a directory containing extra files to use for the Android APK build. This usually should be at least the file AndroidManifest.xml
. However, you can also add other files. A template for a complete folder including Java sources for the main activity class can be found in the Qt installation (e.g. $HOME/Qt/5.11.1/android_armv7/src/android/templates/
). In the example above, we use the android
subdirectory as source for these files.QML_ROOT_PATH
is the path to the app’s QML files. This is required to include all dependencies of the app.EXTRA_LIBS
is a (comma separated) list of additional library files which shall be included in the APK
and loaded as soon as the app starts. This is useful if your app links against additional libraries
or needs them at runtime (e.g. OpenSSL - in case you need HTTPS support).Note: If you encounter any issues during the APK build, make sure you include a custom AndroidManifest.xml
and set the target properties right, e.g. minimal and target SDK versions, the application name and so on.
With the above changes, we add the basic tools into our app’s build to create an APK. We can make use of these additional build steps to comfortably build and run our APKs from within Qt Creator.
First, head over to the Projects tab, where you can manage the enabled kits for the open project. For each kit, you can configure settings for building and running. Switch to the Run area for the Android kit you wish to build and run an APK for. The first thing here is to disable the default deployment step by clicking once on the disable button at the top of the Deploy Configurations box:
If you don’t do this, deployment will fail, as this step only works properly when using qmake
. Next, we need to add add a custom build step to the deployment. In this step, we select the app-name-apk-install
target, which will cause the APK to be build and deployed to a connected Android (virtual) device:
Finally, we tweak the run settings:
We need to select Custom Executable. As executable, we select the adb
tool located in our Android SDK installation, i.e. in my case this is /home/martin/Android/android-sdk-linux/platform-tools/adb
. In the field Command line arguments, we use something like:
shell am start -n org.example.QmlCmakeDemo/org.qtproject.qt5.android.bindings.QtActivity
The shell am start
part tells adb
to run an installed app on the connected device. Via -n $PACKAGE_NAME/$ACTIVITY_NAME
, we tell it which one. You find both strings in the Android Manifest file.
When everything is set up correctly, you should not be able to create and run your app right from Qt Creator 😉
Being able to build and run your APK from within Qt Creator is nice. However, you also want to be able to do the same from the command line (e.g. to enable CI/CD for your project). This is quite easy, we basically just need to take the variables we set in Qt Creator and also pass them to cmake
when configuring our project. Here’s a minimal shell script which does so for ARM targets:
# Set up some variables to shorten stuff later on:
export ANDROID_SDK=$HOME/Android/android-sdk-linux
export ANDROID_NDK=$HOME/Android/android-ndk-r10e
export QT_ROOT=$HOME/Qt/5.11.1/android_armv7
cd path/to/your/project
mkdir build
cd build
cmake \
-DANDROID_SDK_ROOT=$ANDROID_SDK \
-DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a \
-DCMAKE_ANDROID_STL_TYPE=gnustl_shared \
-DCMAKE_CXX_COMPILER=$NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-g++ \
-DCMAKE_C_COMPILER:STRING=$NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc \
-DCMAKE_PREFIX_PATH=$QT_ROOT \
-DCMAKE_SYSTEM_NAME=Android \
-DCMAKE_SYSTEM_VERSION=16 \
-DQT_QMAKE_EXECUTABLE=$QT_ROOT/bin/qmake \
..
cmake --build .
# Build APK for your app, e.g. to package our demo:
cmake --build . android-cmake-demo-apk
# And to run it, just use the install target:
cmake --build . android-cmake-demo-apk-install
I am a software/firmware developer, working in Dresden, Germany. In my free time, I spent some time on developing apps, presenting some interesting findings here in my blog.
Very nice and informative.
Thanks for the discription, it is very usefull!
When i tried to configure this project there was a problem in qt_android_build_apk: /home/ifolbort/tasks/t64317-ioau-to-android/test-cmake-for-android-proj/cmake/qt-android-mk-apk.cmake:37: error: string end index: -2 should be -1 or greater CMakeLists.txt:27 (qt_android_build_apk)
I fixed it by removing “-“ in front of definition of “VERSION_LENGTH” inside the qt-android-mk-apk.cmake:
Is this “-“ was intended there, or it is a bug
Thanks again for the article!
Good question… I assume it was by intention, however, the blog post is a bit older now and if you are using a recent version of Qt+QtCreator and the Android SDK+NDK, chances are that the presented code no longer works without modifications.
In fact, you could check if building against recent NDK versions with an up to date Qt works now without additional cmake code (I remember there were quite some changes recently w.r.t. building with cmake for Android, however, I did not try yet).