Building a CMake-based Qt app for the Desktop, iOS, Android, and WebAssembly

Introduction

In Qt6, CMake became Qt's default build system, replacing qmake. Unfortunately, the transition was not smooth. In the first Qt6 releases, Android support was missing. iOS requires native Xcode projects, and even on Qt 6.3, WebAssembly is still in technology preview.

The slowly-paced adoption of CMake, when targeting iOS, Android, and WebAssembly, forced many developers to implement their own custom CMake toolchains to build Qt-based apps on all the required target platforms for their projects.

That's my case too. I'm the developer of the Cute server, which extends the Qt's signals and slots mechanism, enabling their use over a network. With Cute, clients interact with servers by creating remote objects and interacting with their signals and slots as if they were local, instead of relying on the traditional request/response model. The Cute project provides Linux-based servers and client SDKs for Linux, macOS, Windows, iOS, Android, and WebAssembly, and thus requires building multiple assets for diverse platforms.

To create the Cute server project's assets on all the required platforms, I developed a CMake file to build for the Desktop and a CMake toolchain that allows cross-compilation for iOS, Android, and WebAssembly.

I wanted something independent from IDEs. I also wanted a general CMake solution that I could use in an IDE or directly on the command line.

The CMake assets are available on GitHub, where a simple Qt Quick app that prints a hello message on the screen uses the Cute's CMake assets to target many platforms.

Here we will show how to build on the command line the example app for the Desktop, iOS, Android, and WebAssembly. And although an intel-based mac was used to build the app, Linux and Windows are also valid build hosts.

To use macOS as a build host, you should install Xcode and Qt from the online installer. The commands below show how to build the project for all the supported platforms.

Desktop

The app can be built for the Desktop using Qt5 as follows:

# configure project
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release \
-DQT_SDK_DIR=/Users/glauco/Programming/Qt/SDK/5.15.2/clang_64 \
/Users/glauco/Programming/MyProjects/HelloWorld
# build
make -j8
# run app
open App/MyApp.app

Using Qt6 is just a matter of changing the QT_SDK_DIR CMake variable, as shown below:

# configure project
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release \
-DQT_SDK_DIR=/Users/glauco/Programming/Qt/SDK/6.2.3/macos \
/Users/glauco/Programming/MyProjects/HelloWorld
# build
make -j8
# run app
open App/MyApp.app

iOS

The command below builds the app for iOS Simulator using Qt5 (note that cross-compilation requires the CMake toolchain):

# configure project
cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DAPPLE_IOS_TARGET=True \
-DAPPLE_IOS_SIMULATOR=True \
-DMIN_IOS_SDK_TARGET=12 \
-DQT_SDK_DIR=/Users/glauco/Programming/Qt/SDK/5.15.2/ios \
-DCMAKE_TOOLCHAIN_FILE=CMake/Cute.toolchain.cmake \
/Users/glauco/Programming/MyProjects/HelloWorld
# build
make -j8
# App/MyApp.app can be run on simulator

And below, we build the app for iOS devices using Qt6 (QT_HOST_PATH is required):

# configure project
cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DAPPLE_IOS_TARGET=True \
-DAPPLE_IOS_DEVICE=True \
-DMIN_IOS_SDK_TARGET=13 \
-DQT_HOST_PATH=/Users/glauco/Programming/Qt/SDK/6.2.3/macos \
-DQT_SDK_DIR=/Users/glauco/Programming/Qt/SDK/6.2.3/ios \
-DCMAKE_TOOLCHAIN_FILE=CMake/Cute.toolchain.cmake \
/Users/glauco/Programming/MyProjects/HelloWorld
# build
make -j8
# App/MyApp.app can be run on devices

The app can be signed using the code signing identity specified in the CMake variable APPLE_CODE_SIGN_IDENTITY.

Android

The Android build used Android build tools 31.0.0 and NDK 22.1.7171670. You can install them through Android Studio. If you do not have java installed, you can define the JAVA_HOME environment variable to use the java runtime environment provided by Android Studio.

Android requires building for multiple architectures. CMake supports multiple architectures through external projects. Thus, the toolchain defines many targets to create the assets for all architectures. The target MyApp-all builds for all architectures and creates the Bundle/APK using the androiddeployqt tool. The command shown below creates the Android app using Qt5:

# configure project
cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DANDROID_TARGET=True \
-DANDROID_TARGET_SDK_VERSION=28 \
-DANDROID_SDK=/Users/glauco/Library/Android/sdk \
-DANDROID_BUILD_TOOLS_REVISION=31.0.0 \
-DANDROID_NDK=/Users/glauco/Library/Android/sdk/ndk/22.1.7171670 \
-DANDROID_ABIS='arm64-v8a;armeabi-v7a;x86;x86_64' \
-DKS_URL=/Users/glauco/Programming/AndroidKeystore/android.keystore \
-DKS_KEY_ALIAS=AndroidDeveloperKeystore \
-DKS_PASS_FILE=/Users/glauco/Programming/AndroidKeystore/keystore.pass \
-DQT_SDK_DIR=/Users/glauco/Programming/Qt/SDK/5.15.2 \
-DCMAKE_TOOLCHAIN_FILE=CMake/Cute.toolchain.cmake \
/Users/glauco/Programming/MyProjects/HelloWorld
# build
make MyApp-all -j8
# App/MyApp/android-build/build/outputs has apk and bundle folders
# containing debug apk for testing and bundle with release build

The following command builds the Android app using Qt6 (QT_HOST_PATH is required):

# configure project
cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DANDROID_TARGET=True \
-DANDROID_TARGET_SDK_VERSION=31 \
-DANDROID_SDK=/Users/glauco/Library/Android/sdk \
-DANDROID_BUILD_TOOLS_REVISION=31.0.0 \
-DANDROID_NDK=/Users/glauco/Library/Android/sdk/ndk/22.1.7171670 \
-DANDROID_ABIS='arm64-v8a;armeabi-v7a;x86;x86_64' \
-DKS_URL=/Users/glauco/Programming/AndroidKeystore/android.keystore \
-DKS_KEY_ALIAS=AndroidDeveloperKeystore \
-DKS_PASS_FILE=/Users/glauco/Programming/AndroidKeystore/keystore.pass \
-DQT_HOST_PATH=/Users/glauco/Programming/Qt/SDK/6.3.0/macos \
-DQT_SDK_DIR=/Users/glauco/Programming/Qt/SDK/6.3.0 \
-DCMAKE_TOOLCHAIN_FILE=CMake/Cute.toolchain.cmake \
/Users/glauco/Programming/MyProjects/HelloWorld
# build
make MyApp-all -j8
# App/MyApp/android-build/build/outputs has apk and bundle folders
# containing debug apk for testing and bundle with release build

WebAssembly

Qt for WebAssembly requires Emscripten. The commands below set up Emscripten on the host machine:

# we are at /Users/glauco/Programming/Wasm
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install 3.1.6
./emsdk activate 3.1.6
echo 'source "/Users/glauco/Programming/Wasm/emsdk/emsdk_env.sh"' >> ~/.zprofile

And the commands below use Qt 6.3 to build the app for WebAssembly:

# configure project
cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DWASM_TARGET=True \
-DWASM_SDK=/Users/glauco/Programming/Wasm/emsdk/upstream \
-DQT_SDK_DIR=/Users/glauco/Programming/Qt/SDK/6.3.0/wasm_32 \
-DQT_HOST_PATH=/Users/glauco/Programming/Qt/SDK/6.3.0/macos \
-DCMAKE_TOOLCHAIN_FILE=CMake/Cute.toolchain.cmake \
/Users/glauco/Programming/MyProjects/HelloWorld
# build project
make -j8
# load page on browser
emrun --browser firefox App/MyApp.html

Conclusion

Programmers solve problems with code. The best way to view CMake is as a programming language used to code the build process of your projects.

By viewing CMake as a programming language, you can code a solution whenever you stumble on a build problem instead of halting and expecting someone to fix it. With this view in mind, everything is possible.