Qt is an excellent toolkit for developing cross-platform applications in C++ and CMake/CPack is an excellent build system for C++ applications. The two work very well together and I have used them in a number of projects. Deploying the projects on Windows with CPack, however, requires some extra effort. This article will take a look at my approach to this problem.
A Simple Example
When preparing an installer or archive for installation, CPack builds the default target and bundles the files indicated by the install() commands. Consider the following executable target, for example:
add_executable(myapp WIN32 main.cpp)
install(TARGETS myapp DESTINATION bin)
On Unix-based platforms, this will produce an executable named myapp
and place it in /usr/local/bin
. On Windows, this will produce an executable named myapp.exe
and place it in the directory pointed to by %ProgramFiles%
.
If we add CPack, it becomes possible to run the following command to generate a self-contained installer (assuming NSIS is installed):
cpack -G NSIS
Understanding the Problem
There is one small problem, however. The installer will only include the executable and will not include any libraries required by the application at runtime. For example, if you are building with Visual C++, the MSVC runtime libraries will not be included in the installer. If your application uses Qt, the Qt runtime libraries and platform plugins will not be included in the installer.
CMake offers its own solution for the MSVC runtime libraries in the form of InstallRequiredSystemLibraries
:
include(InstallRequiredSystemLibraries)
If your application is compiled with Visual Studio 2015 or newer, you’ll want to add the following line before including InstallRequiredSystemLibraries
:
set(CMAKE_INSTALL_UCRT_LIBRARIES TRUE)
This does not take care of the Qt libraries, however.
windeployqt
Qt provides a tool for simplifying the process of installation: windeployqt. Simply pass the path of an executable to the application and windeployqt will copy the required libraries and plugins to the directory:
windeployqt path/to/myapp.exe
Integrating this with CMake is a bit of work but still possible. The first step is to find the absolute path to the windeployqt
executable. This executable should exist in the same directory as qmake
, so we can use that path as a starting point:
get_target_property(_qmake_executable Qt5::qmake IMPORTED_LOCATION)
get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY)
find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${_qt_bin_dir}")
If successful, ${WINDEPLOYQT_EXECUTABLE}
will contain the absolute path to the executable. Invoking it is simply a matter of adding a custom command:
add_custom_command(TARGET myapp POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E
env PATH="${_qt_bin_dir}" "${WINDEPLOYQT_EXECUTABLE}"
--verbose 0
--no-compiler-runtime
\"$<TARGET_FILE:myapp>\"
COMMENT "Deploying Qt..."
)
There are a couple of important things to note here:
env
is required in order to ensure thatwindeployqt
can find the librarieswindeployqt
has a number of issues deploying the compiler runtime, so it is disabled in favor ofInstallRequiredSystemLibraries
(described above)
More Problems
This, however, does not solve the original problem since these files will not be included by CPack. Only files with an explicit install()
command will be included. This might seem like an insurmountable problem at first — how can an install()
command be created for the files if they don’t exist until after building the application?
The first step towards a solution involves passing a couple special parameters to windeployqt
:
windeployqt --dry-run --list mapping path/to/myapp.exe
This changes the behavior of the tool. Instead of copying the files, it prints a sequence of lines that indicate which files would have been copied and their relative location in the destination directory:
"C:\Qt\Qt5.8.0\5.9.3\msvc2015_64\bin\Qt5Cored.dll" "Qt5Cored.dll"
"C:\Qt\Qt5.8.0\5.9.3\msvc2015_64\bin\Qt5Guid.dll" "Qt5Guid.dll"
...
This seems promising. There is a CMake command that enables the output of a command to be captured — execute_process()
. An implementation of this might resemble the following:
execute_process(
COMMAND "${CMAKE_COMMAND}" -E
env PATH="${_qt_bin_dir}" "${WINDEPLOYQT_EXECUTABLE}"
--dry-run
--no-compiler-runtime
--list mapping
\"$<TARGET_FILE:myapp>\"
OUTPUT_VARIABLE _output
OUTPUT_STRIP_TRAILING_WHITESPACE
)
The command would need to be run after building the executable since windeployqt
needs to examine the executable itself to determine which files to deploy. install(CODE)
would seem like the logical place to run the command:
install(CODE "execute_process( ... )")
Even More Problems
There is, however, yet another problem. A generator expression is required to obtain the path to a compiled executable and only a small subset of commands can use generator expressions. Sadly, execute_process()
is not one of them.
But, there is a way around this. file(GENERATE)
supports generator expressions. Using this command, the path to the executable can be written to disk for later retrieval:
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/myapp_path"
CONTENT "$<TARGET_FILE:myapp>"
)
Now when invoking install(CODE)
, the path can easily be retrieved:
install(CODE
"
file(READ \"${CMAKE_CURRENT_BINARY_DIR}/myapp_path\" _file)
execute_process(
COMMAND \"${CMAKE_COMMAND}\" -E
env PATH=\"${_qt_bin_dir}\" \"${WINDEPLOYQT_EXECUTABLE}\"
--dry-run
--no-compiler-runtime
--list mapping
\${_file}
OUTPUT_VARIABLE _output
OUTPUT_STRIP_TRAILING_WHITESPACE
)
"
)
Progress is being made. The install(CODE)
command above reads the path to the executable from disk and then passes it to windeployqt
. Note that all of the quotes in the execute_process()
command must be escaped.
Processing the List
Now that the output is captured and stored, a set of commands are needed to parse the mapping and copy the files to the installation directory. The first step is to parse the output as a list. separate_arguments()
does this quite well:
separate_arguments(_files WINDOWS_COMMAND ${_output})
Next, for each pair of items in the list, execute a command that will copy them to the correct location:
while(_files)
list(GET _files 0 _src)
list(GET _files 1 _dest)
execute_process(
COMMAND "${CMAKE_COMMAND}" -E
copy_if_different ${_src} "${CMAKE_INSTALL_PREFIX}/bin/${_dest}"
)
list(REMOVE_AT _files 0 1)
endwhile()
Summary
Although difficult, integrating windeployqt with CPack is possible. The solution described above will ensure that all of the appropriate files are included when CPack creates an installer or achive.
The nitroshare-desktop
repository contains a file that can easily be added to your own projects: Windeployqt.cmake
. The file is released under the MIT license.