Revisiting the Qt Installer Framework with Qt 6

Revisiting the Qt Installer Framework with CMake

By Christopher Probst

As The Qt Company has shifted its focus from qmake to cmake, it is time to revisit a previous blog post on the Qt Installer Framework. The post presented a small qmake example in which the generation of an application's installer was part of its build.

Today, I'll illustrate how to do the same thing with cmake. It turns out that the process is simpler.

CMake's cpack module

To generate a binary installer or distributable source packages, cmake has the cpack module. As is illustrated in this short video, in its simplest form on Linux,  running in the build directory, the set of commands 

cmake -- build . --target all 

cmake  --build  . --target install

cmake --build . --target package 

with the few cmake lines below, generates a rudimentary install-script along with a tar.gz file that contains the installable payload. The payload contains all the files installed through cmake’s install() directive.

include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE  ${CMAKE_CURRENT_SOURCE_DIR}/licence.txt)
set(CPACK_PACKAGE_VERSION_MAJOR "1")
set(CPACK_PACKAGE_VERSION_MINOR "0")
include(CPACK)

The IFW generator

The cpack module in addition features a variety of cpack generators to create more sophisticated installers. The one that integrates with the Qt Installer Framework, is the cpack IFW generator. We illustrate its usage on Windows with a simple example hosted in the following git repository.

To start, the CPACK_GENERATOR variable is set to “IFW” and the CPACK_IFW_ROOT variable is set to the location of the Qt Installer framework. For purposes of simplicity, we hard-code it in. Our CMakelist.txt now has the following lines:

include(InstallRequiredSystemLibraries)
set(CPACK_GENERATOR "IFW")
set(CPACK_IFW_ROOT "C:/Qt6/Tools/QtInstallerFramework/4.1")
set(CPACK_PACKAGE_VERSION_MAJOR "1")
set(CPACK_PACKAGE_VERSION_MINOR "0")
include(CPACK)
include(CPackIFW)

If in the CMakelist.txt, the project's components are installed using cmake's install command like this:

install(TARGETS awesomeWorld DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${COMPONENT_NAME_MAIN})

and if cpack is made aware of them like this:

CPACK_ADD_COMPONENT(${COMPONENT_NAME_MAIN})

the build then produces the desired sophisticated graphical installer. Our CMakeLists.txt now has the following lines:

install(TARGETS awesomeWorld DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${COMPONENT_NAME_MAIN})

set(CPACK_GENERATOR "IFW")
set(CPACK_IFW_ROOT "C:/Qt6/Tools/QtInstallerFramework/4.1")
set(CPACK_PACKAGE_VERSION_MAJOR "1")
set(CPACK_PACKAGE_VERSION_MINOR "0")
include(CPack)
include(CPackIFW)

CPACK_ADD_COMPONENT(${COMPONENT_NAME_MAIN})

The Problem with Deployment

The payload installed however does not contain the necessary Qt dependencies. It is necessary to involve the platform-specific deploy utilities, windeployqt that comes with Qt on Windows and linuxdeploy offered by a third party with an MIT license.

To integrate with these utilities, we specify the CPACK_PRE_BUILD_SCRIPTS variable to a cmake script that runs the deployment tool. On Windows, our CMakeLists.txt would have the following lines:

if(WIN32)
find_program(WINDEPLOYQT windeployqt HINTS "${_qt_bin_dir}")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/deploy-qt-windows.cmake.in" "${CMAKE_CURRENT_SOURCE_DIR}/deploy-qt-windows.cmake" @ONLY)

set(CPACK_PRE_BUILD_SCRIPTS ${CMAKE_CURRENT_SOURCE_DIR}/deploy-qt-windows.cmake)
endif()

Where an additional deploy-qt-windows.cmake.in file would have the following:

set(WINDEPLOYQT "@WINDEPLOYQT@")
set(COMPONENT_NAME_MAIN "@COMPONENT_NAME_MAIN@")
set(CMAKE_CURRENT_SOURCE_DIR "@CMAKE_CURRENT_SOURCE_DIR@")

execute_process(COMMAND ${WINDEPLOYQT} --qmldir ${CMAKE_CURRENT_SOURCE_DIR} ${COMPONENT_NAME_MAIN}/data/bin WORKING_DIRECTORY ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages)

Customizing the Installer

Further customization of the look and feel of the installer is now easier. Unlike in the previous blog with qmake, there is no need now for the set of xml files (i.e., config.xml, packages.xml). All of the meta values in these xml files have a corresponding cmake variable. The original config.xml with the following metadata can be deleted.

<Installer>
    <Name>AwesomeWorld</Name>
    <Version>1.0.0</Version>
    <Title>AwesomeWorld</Title>
    <Publisher>Me</Publisher>
    <WizardStyle>Aero</WizardStyle>
<\Installer>

And replaced with the following lines in the CMakelists.txt:

set (CPACK_IFW_PACKAGE_NAME "Awesome World")
set (CPACK_IFW_PACKAGE_TITLE "AwesomeWorld")
set (CPACK_IFW_PACKAGE_PUBLISHER "Me")
set (CPACK_IFW_PACKAGE_WIZARD_STYLE "Aero")

And all metadata in the set of packages.xml files can now be specified with the cpack_ifw_configure_component cmake function. This function's SCRIPT parameter set to a .qs script file allows it to achieve component scripting. Control scripting can be achieved by setting the CPACK_IFW_PACKAGE_CONTROL_SCRIPT to a .qs script file.

The Result

We have, for fun, incorporated these ideas in a small project hosted in this git repository try. Running from the build directory on Windows

cmake -- build . --target all 

cmake  --build  . --target install- install and 

cmake --build . --target package, 

builds a small application, installs it in the appropriate platform-specific install directory and creates a customized graphical installer able to deploy the application. In the repository, there is a branch that will additionally create a packages directory that can be uploaded to an http-server where the installer grabs its payload. Please have fun with it!

Conclusion

Pertaining to the Qt installer framework, the IFW cpack generator makes the transition from qmake to cmake visibly simpler. There is now more reason than even to include the generation of an installer to a build. For more on this topic, please check our talk at the 2021 Qt World Summit.