Qt and CMake: The Past, the Present and the Future

We made a big decision to start using CMake to build Qt 6 one and a half years ago. The main reason for the decision was user feedback. Most Qt users wanted to have easier integration of their Qt projects with the rest of their software. According to the studies at that time, CMake was clearly the most commonly used build tool among Qt users - qmake aside. In addition, migrating to CMake gave us an opportunity to get rid of the maintenance burden of our internal build tools. 


Even bigger than the decision was the effort required to migrate to CMake.  Now the essential migration work has been completed and it's time to share our findings.

Although well established and used by many projects, CMake was missing some key features that were supported in the previous Qt build tools. That's why we collaborated with Kitware to get the obstacles out of the way and improve CMake for the benefit of the Qt Project and the larger CMake community.

Building Qt is complex, due to various contributing points:

  • Qt supports many, many platforms.
  • Qt is split into modules that can be built stand-alone or all-in-on.
  • Qt supports different ways of building it (prefix, non-prefix, different feature sets)

Let's have a look of the CMake improvements that Qt's build tool switch caused or influenced.

Precompiled Headers

In a C++ project, the same header files are potentially included over and over again. That is especially true for library headers. If the toolchain supports it, one can speed up compilation by precompiling header files.

For the longest time, CMake did not offer out-of-the-box support for that. But the days of searching the web for snippets to enable precompiled headers in CMake are over. Since CMake 3.16, there's the target_precompile_headers command that lets you add a list of header files to precompile.

See the original announcement for details.

Since: 3.16
Documentation: https://cmake.org/cmake/help/latest/command/target_precompile_headers.html

Unity Builds

Another way to speed up compilation is unity builds. This is the Technique of the Many Names - it is also known as jumbo build, amalgamated build and single compilation unit.

This technique creates one source file that includes all other source files, and only that one source file is compiled:

#include "source_file1.cpp"
#include "source_file2.cpp"
/*...*/
#include "source_file8.cpp"

The includes of all source files are processed only once, everything ends up in one translation unit, and the optimizer has a global view on the project.

However, not every C++ project can utilize unity builds without modification.  Follow the links in the description of the original CMake issue #19526 to learn more.

Since: 3.16
Documentation: https://cmake.org/cmake/help/latest/prop_tgt/UNITY_BUILD.html

depfile support for AUTOMOC + Ninja

There were longstanding complaints about CMake's AUTOMOC, that it would run needlessly or that it wouldn't detect properly when to call moc again. One reason for this was that the exact dependencies of moc's output were invisible to AUTOMOC.

Since Qt 5.15, moc learned to write out the exact files that form the dependencies of moc's output. And CMake 3.17 learned to read moc's depfiles and use it for the Ninja generator.

To sum it up: with Qt >= 5.15 and CMake >= 3.17 with the Ninja generator, AUTOMOC knows the right dependencies to re-run moc at the right time.

Since: 3.17
See CMake issue #19058. Led to fixing #17750 and #18749 too.

Ninja Multi-Config

On Windows and macOS, Qt was traditionally built in two configurations, debug and release, but in one build directory.

CMake did not offer a way to do this until version 3.17 introduced a new generator called "Ninja Multi-Config", which can build multiple configurations at once.

Since: 3.17
Documentation: https://cmake.org/cmake/help/latest/generator/Ninja%20Multi-Config.html#generator:Ninja%20Multi-Config

iOS multi-architecture builds

Qt for iOS offers simulator-and-device builds that combine a Qt built for the actual target with a Qt built for the iOS simulator. While CMake 3.17 introduced Ninja Multi-Config, its iOS support was not quite ready yet for iOS multi-architecture builds.

CMake 3.18 fixed this, and we can happily build for the simulator and the device.

Since: 3.18
See the issues #19534, #20534 and #20588 for details.

file(CONFIGURE)

In the Qt build there are quite some files generated at configure time with dynamically created content. The configure_file command however, only consumes input files, no strings.

One way to solve this is to have an input file with only one variable

 --conf-file-content.txt.in--
@my-conf-file-content@

and call configure_file like this

set(my-conf-file-content "This is the generated content!")
configure_file("my-conf-file-content.txt.in" "output.txt" @ONLY)

In CMake 3.18, we can achieve the same much easier:

file(CONFIGURE OUTPUT "output.txt" CONTENT "This is the generated content!")

Since: 3.18
Documentation: https://cmake.org/cmake/help/latest/command/file.html?highlight=file#configure
See CMake Issue #20388

Eval! In League with Functions!

In the Qt build we exhaust the CMake language's ability to perform as an actual programming language. There's not only the CMake functions that are supposed to be called by the user, but a much more complicated machinery under the hood.

One thing that bothered us quite a bit is that it was impossible to call functions dynamically. In qmake, calling a function that is defined in a variable is possible, and the Qt5 build uses this ability extensively

f = message
$${f}("Hello World!")

There are ways to make this work in CMake, involving generating a file that's then included, but especially on Windows this would slow down the project configuration step immensely.

CMake 3.18 comes with cmake_language(EVAL) to evaluate CMake code and cmake_language(CALL) to call macros or functions.

set(f "message")
cmake_language(CALL ${f} STATUS "Hello World!")
cmake_language(EVAL CODE "${f}(\"Hello World!\")")

Since: 3.18
Documentation: https://cmake.org/cmake/help/latest/command/cmake_language.html
See CMake issue #18392

Call me later - deferred code

Again, a qmake feature inspired a CMake command.

In a QMake project, one can write CONFIG += foo, which in turn loads mkspecs/features/foo.prf after the actual project file is processed.

This is particularly useful for user-facing API. Assume, you're creating a project for Android.

qt_add_executable(MyApp)
set_property(MyApp TARGET PROPERTY QT_ANDROID_EXTRA_LIBS SuperDuperLib)
qt_finalize_executable(MyApp)

The qt_finalize_executable call generates proper deployment settings for the target, depending on the target's properties. If the user forgets to call qt_finalize_executable, the deployment settings are not generated, and there's no error nor warning.

Note: That's an example. The real API isn't that error prone. See the commit message of b94b7687b0635ee74a3ccd83a234ead0600fd47f how we approach this.

But wouldn't it be great if the user was liberated from the burden of calling qt_finalize_executable themselves? With CMake 3.19, the Qt build utilizes the new command cmake_language(DEFER CALL). With that, one can call functions at defined times, for example, after the project files of the current directory are evaluated.

Since: 3.19
Documentation: https://cmake.org/cmake/help/latest/command/cmake_language.html#defer
See CMake issue #19575

Properties on source files for different directory scopes

In certain places, we create a target for a module, and add source files to that target in a subdirectory.  This keeps the names of source files close to the CMakeLists.txt file for them.

However, one could not specify per-source file properties in those locations - we have to go up to the top level of the hierarchy to set those.  We do not do this often, but it sometimes comes up during development to allow compile-time selection of experimental features based on option settings.

In CMake 3.18, set_source_files_properties learned to set properties on different directory scopes:

The DIRECTORY option takes a list of paths pointing to processed source directories, and TARGET_DIRECTORY takes a list of targets, whose source directories will be used as the list of scopes where to set the source file properties.

get_property() and get_source_file_property() also got the same new arguments, except only one value can be specified instead of a list.

Since: 3.18
Documentation: https://cmake.org/cmake/help/latest/command/set_source_files_properties.html
See CMake issue #20128

add_custom_command DEPFILE support for Makefiles

CMake 3.20 will provide depfile support for the Unix Makefile generator. This is a premise for using moc-generated depfiles for Makefiles too.

Since: 3.20
See CMake issue #20286

Closing Remarks

The work isn't done yet. CMake is constantly improved, and the porting effort of more and more Qt modules to CMake will most probably spawn some more ideas.

Please post issues you find or suggestions you have in our bug tracker.


Blog Topics:

Comments

Commenting for this post has ended.

Benoît Gradit
0 points
51 months ago

That's really great, and I personally play with those new feature they same me many life !But what does that means for Qt users ? Are you finally going to give an official way (and real life example) of doing the following in cmake/Qt ?

  • Build for iOS/Android out of the box

  • QML modules that works on every platforms

J
Jörg Bornemann
0 points
51 months ago

We have an open issue about building iOS projects which is being worked on. I'm not sure what you're referring to when mentioning "Build for Android out of the box". Also, maybe you could elaborate on "QML modules that work on every platform".

Benoît Gradit
1 point
51 months ago

For the cli something like:

qt create my-project

cd my-project

qt add qml-application my-application --ios --android

qt add qml-module my.module

Benoît Gradit
0 points
51 months ago

Either for Android or for iOS, the journey is painful. The documentation is really poor for those platforms and only presents the minimal cases. It’s the same with QML module. It’s painful to create one, especially when you target multiple platforms.

There is a lack of an official way to do a more realistic application. Containing multiple libraries and QML modules (real ones with both QML and C++), which build on all major platforms (Windows, Linux, macOS, iOS and Android) and ne can be launch in one click from QtCreator.

The exemples and Qt modules are too tight to you own build system to be really useful in those case. Which make Qt really not friendly for medium to high level application out of the box. This requires a long time to go trough this and have something that should be updated every time Qt or Google or Apple have decided to change it (which happens too often).

Qt is really an interesting concept, but it’s really too abstract for now, and I think it loose some market of being such not as friendly as it should be.

If you want to have a point of comparaison, I suggest you to look at VueJS or React, as starting project here, or scaling projects is made really simple.

Benoît Gradit
0 points
51 months ago

The point here, I think is mainly that the documentation lack of some developers guides that instruct how you should achieve such a multiplatform scale using cmake. For now the cmake documentation is nearly reduced to a list of available functions.

Also, it would make sense to me, that you provide cmake function to create complete projects easily. Function like add_qt_application, add_qml_module function that handle all the job. Having this out of the Qt source in a separate open-source repository may be a plus.

In addition to this, having a cli set of tools to generate a project structure and allow for quick edit of some tasks (like quicly adding a new qml module) doesn't appear to be an impossible task, and really add great value to the product. Again, look at what vuejs and react has in this direction.

Luca Carlon
0 points
51 months ago

What about build times? Any improvement?

J
Jörg Bornemann
0 points
51 months ago

What are your major pain points here? For the initial build not much changed. Incremental builds gained speed up from using ninja alone.

Benoît Gradit
-1 points
51 months ago

As my personal experience (I'm not working at Qt, I'm just a Qt enthusiastic), so far so good, improvements (like those made to AUTOMOC) greatly improved the build time. I have no metrics here, but simply that recompiling after a small change was really fast compared to before.

Tom Deblauwe
0 points
51 months ago

Great that you did this! Thanks

M
Michał
0 points
50 months ago

Hey, lots of good stuff! By the way, how about https://gitlab.kitware.com/cmake/cmake/-/issues/16776? Was it also fixed along the way?

J
Jörg Bornemann
0 points
50 months ago

No, that issue is unfortunately still open. But it's on our radar.

Silver Zachara
0 points
50 months ago

qmake is an amazing build tool, simple, readable, very user-friendly, you need minimal code to write in qmake to make things work, even very complex things. If you compare the same project written in qmake and cmake, ehm, but cmake is modern and new fame.

Another thing is, that if you will not write in qmake one year and you come back to the project, you are ready to go, but after one year of inactivity on a complex cmake project you will be lost.

But I'm not judging, it is only my personal view, I love cmake too, cmake is absolutely amazing piece of software, it is only a current situation, I agree with porting to cmake. And it is primarily the way how cmake manages dependencies that is simply the winner, among the other things.