04 Aug 2018

Building Android Projects With Qt Creator And CMake

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:

  • Build your Qt based Android app with Qt Creator.
  • Do the same from the command line, e.g. for CI/CD.
  • Deploy your app to a connected Android device and start it from with Qt Creator.

Let’s start.

Prerequisites

I will assume the following:

  • You have a working and recent Qt installation (e.g. 5.11.1, which is at the time of writing the most recent one).
  • You have a working Android SDK.
  • You have a working Android NDK, ideally version r10e.
  • You have a recent version of cmake, at least v3.7.
  • Qt Creator is readily set up, i.e. it finds the Qt installation as well as 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.

Step 1: Setting up Qt Kits in Qt Creator

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:

kits-auto-configured

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:

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:

cmake-configuration-variables

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.
  • Finally, via 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:

  1. Load the CMakeLists.txt file in Qt Creator by using File and Open File or Project (unless you generated the project it right from Qt Creator - then, of course, it already is open).
  2. Configure and compile the project at least for your host OS plus Android ARM and x86.

Step 2: Adjusting a Project’s CMakeFiles to Build For Android

When you build the sample app for your host OS, you should get the following:

sample-app-desktop

So far, so good. However, without further ado we won’t be able to deploy out app to Android. This has two reasons:

  1. In the 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.
  2. The app needs to be packaged as APK.

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:

  1. Via include(), we pull in the utility file from above into the build system.
  2. Via 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.
  3. At the very end, we use our 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.

Step 3: Building And Running The APK From Within Qt Creator

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:

disable-default-deployment

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:

custom-deployment-step

Finally, we tweak the run settings:

custom-run-step

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 😉

Step 4: Building Your APK From The Command Line

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
Thank You For Reading
Martin Hoeher

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.

Comments
Your comment has been filed

We'll review it and it will appear here as soon as we're done.

Sorry, something went wrong...

Your comment could not be posted.

Regarding your personal information...
  • The name you enter will be shown next to your comment. You may enter your real name or whatever you like.
  • Your e-mail will be used to show an image representing you next to your comment. We do not store nor show your address somewhere. Instead, an ID will be calculated from it and stored. This ID is then used to retrieve an image from Gravatar.