Qt's CMake deployment API

Say you have just started with Qt. You installed a recent version with the Qt online installer, found your way through the C++ and QML API, and now have a nice desktop application that works for you ™. Now you want to share your precious marvel with others, for eternal fame and/or profit! But you don’t want to share just your code but a binary that also end-users can install and run without installing a compiler, Qt ... How do you do this?

A popular approach is to create a folder that contains both your application executable, but also the Qt libs and plugins, in the right structure, so that the executable finds everything it needs in the folder. Qt does help you in setting up such a folder with several command line tools, named windeployqt, and macdeployqt, and androiddeployqt. But how do you use them from  CMake? Enter Qt’s CMake deployment API!

The usual deployment process consists of the following steps:

  1. installing the application into a staging area. The staging area is determined by CMAKE_INSTALL_PREFIX (or more generally, CMAKE_STAGING_PREFIX in cross builds).
  2. Calling windeployqt or macdeployqt to copy Qt libraries, plugins and assets into the staging area.
  3. Custom adjustments that are not covered by our *deployqt tools.

After these steps, the staging area is ready to be archived and shipped.

Usually, it's up to the user to create a separate script that handles the full deployment.

Since version 6.3, Qt offers a way to define additional deployment steps that are executed on installation. Our goal for this article is to show how to deploy a Qt application using Qt's CMake deployment API.

We start off with a simple Qt application.

cmake_minimum_required(VERSION 3.22)
project(MyApp)

find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_standard_project_setup()

qt_add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE Qt::Widgets)

We add statements to install the application target. On macOS, we install directly into ${CMAKE_INSTALL_PREFIX}, on other platforms into the "bin" directory underneath.

install(TARGETS MyApp
BUNDLE DESTINATION .
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

Note that qt_standard_project_setup pulls in GNUInstallDirs.cmake. This is what defines the CMAKE_INSTALL_BINDIR variable.

A call to cmake --install ... would now only install the application's executable (or bundle) to the install prefix.

Up to this point, there's nothing new. But now we are going to instruct CMake to call macdeployqt or windeployqt on installation. In order to do that, we use CMake's ability to run a CMake script on installation.

qt_generate_deploy_app_script(
TARGET MyApp
OUTPUT_SCRIPT deploy_script
NO_UNSUPPORTED_PLATFORM_ERROR
)
install(SCRIPT ${deploy_script})

This generates a CMake script file that calls macdeployqt or windeployqt on our application target. The file name of the generated script file is stored in deploy_script. With the install(SCRIPT) call we instruct CMake to run the script on installation.

We can now configure, build and install the project. Here's what it looks like on Windows:

D:\projects\myapp\build>D:\Qt\bin\qt-cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=D:/TEMP/myapp ..
...
D:\projects\myapp\build>ninja
...
D:\projects\myapp\build>ninja install
-- Install configuration: "Release"
-- Installing: D:/TEMP/myapp/bin/myapp.exe
-- Writing D:/TEMP/myapp/bin/qt.conf
-- Running Qt deploy tool for bin/myapp.exe in working directory 'D:/TEMP/myapp'
'D:/Qt/bin/windeployqt.exe' 'bin/myapp.exe' '--dir' '.' '--libdir' 'bin' '--plugindir' 'plugins' '--force'
D:\TEMP\myapp\dist\bin\myapp.exe 64 bit, release executable
Direct dependencies: Qt6Core Qt6Gui Qt6Widgets
All dependencies : Qt6Core Qt6Gui Qt6Widgets
To be deployed : Qt6Core Qt6Gui Qt6Widgets
Updating Qt6Core.dll.
Updating Qt6Gui.dll.
Updating Qt6Widgets.dll.
Updating d3dcompiler_47.dll.
Updating vc_redist.x64.exe.
Creating directory D:/TEMP/myapp/plugins/imageformats.
Updating qgif.dll.
Updating qico.dll.
Updating qjpeg.dll.
Creating directory D:/TEMP/myapp/plugins/platforms.
Updating qwindows.dll.
Creating directory D:/TEMP/myapp/plugins/styles.
Updating qwindowsvistastyle.dll.

The installation produced a self-contained directory, ready to be packaged - for example by cpack.


Under the hood

The call to qt_generate_deploy_app_script generates a CMake script file in the build directory that does two things:

  • include the QtDeploySupport.cmake file
  • call qt6_deploy_runtime_dependencies on the installed executable

The QtDeploySupport.cmake file provides the CMake commands that aid us with deployment. Also, it sets variables like QT_DEPLOY_PREFIX.

The qt_deploy_runtime_dependencies command is the actual driver of the deployment process. It calls mac/windeployqt and generates a qt.conf file, if necessary.

For setups that need more customization, we have to do the script generation ourselves.

set(deploy_script "${CMAKE_CURRENT_BINARY_DIR}/deploy_MyApp.cmake")
file(GENERATE OUTPUT ${deploy_script} CONTENT "
include(\"${QT_DEPLOY_SUPPORT}\")

qt_deploy_runtime_dependencies(
EXECUTABLE \"\${QT_DEPLOY_BIN_DIR}/$<TARGET_FILE_NAME:MyApp>\"
GENERATE_QT_CONF
)")

For the initial include statement we need to expand the CMake variable QT_DEPLOY_SUPPORT that is available at configure time.

In the remainder of the script we access the QT_DEPLOY_BIN_DIR variable that is available at installation time. That's why it must be escaped: we don't want to evaluate it at configuration time.

Lastly, we use a generator expression to extract the target file name of the MyApp target.

Suppose our project provides its own qt.conf, we don't want qt_deploy_runtime_dependencies to generate one. To do that, we can remove the GENERATE_QT_CONF.

 

The layout of the staging area

Qt's CMake deployment API uses the CMAKE_INSTALL_BINDIR and CMAKE_INSTALL_LIBDIR variables defined by GNUInstallDirs.cmake to determine the layout of the staging area.

On installation time, these directory variables are available as QT_DEPLOY_BIN_DIR and QT_DEPLOY_LIB_DIR.

Make sure to use CMAKE_INSTALL_BINDIR as DESTINATION in the install command for executables and CMAKE_INSTALL_LIBDIR for libraries.

To change the values of CMAKE_INSTALL_BINDIR or CMAKE_INSTALL_LIBDIR, set the variables before the Qt6Core package is found.

Qt plugins are deployed into QT_DEPLOY_PLUGINS_DIR, which defaults to "plugin". QML modules are deployed into QT_DEPLOY_QML_DIR, which defaults to "qml".

 

Deployment of QtQuick projects

Deploying QtQuick projects is a tad more involved - fortunately not for the user. The details are hidden by the command qt_generate_deploy_qml_app_script.

Let's have a look how to prepare the CMakeLists.txt of a QtQuick application.

First, we create the QtQuick application.

qt_add_executable(MyApp main.cpp)
qt_add_qml_module(MyApp
URI Application
VERSION 1.0
QML_FILES main.qml MyThing.qml
)

We need to add the regular install command:

install(TARGETS MyApp
BUNDLE DESTINATION .
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

And call qt_generate_deploy_qml_app_script:

qt_generate_deploy_qml_app_script(
TARGET MyApp
   OUTPUT_SCRIPT deploy_script
)
install(SCRIPT ${deploy_script})

On installation, the application binary will be deployed, including the QML files and the bits of Qt that are used by the project.

qt_generate_deploy_qml_app_script calls qt_deploy_qml_imports in addition to qt_deploy_runtime_dependencies. Refer to the documentation of these commands for details.

 

Outlook

At the moment, Qt's CMake deployment API is supported on Windows and macOS only. Android is handled via build targets.

We strive to add more platforms in the future, including Linux (QTBUG-74940).

The CMake deployment API is in technical preview for the time being. Please try it on your favorite project and provide feedback on https://bugreports.qt.io/.


Blog Topics:

Comments