>
>
>
Relocation: QMake -> CMake

Guest
Articles: 23

Relocation: QMake -> CMake

On our way there, we'll walk along the Cross Compilers Street, sit in the Build System Square, and have a drink at the Dependency Management Bar. We'll also visit those who use Qt in embedded Linux.

Figure 1. We had.. A picture to attract attention

The root of the evil

We published and translated this article with the copyright holder's permission. The author is Xadok, LinkedIn - http://www.linkedin.com/in/alimoff-anton. The article was originally published on Habr [RU]

We didn't possess any controlled substances (although you may have doubts by the end of this article). We had something more interesting if you know what I mean. Yes, this is the legacy Win-only Qt project, started before time under Qt 4.

Our company develops tools for monitoring and evaluating power equipment. This industry has a lot of old projects. Here, they don't scare anyone — especially with computerized appliances. But sometimes you have to deal with these old projects, and this time I had to do it. I had to deal with some kind of service software for our hardware, which would work with the hardware via different protocols. I wanted to simplify the dependency management and throw out some bicycles. Then I wanted Linux to become the target platform, and the architecture now had ARM. All of it made us consider CMake. Besides, CMake is supported by the most progressive IDEs — CLion and MSVS, while QMake is supported by QtCreator (KDevelop? No thanks). Of course, there are still other build tools — make, autotools, and MSBuild — but I wanted a single project for all.

A bit about build systems

Over time, projects become bigger and bigger, it becomes more and more difficult to build them. One can build a very small project from main.cpp. But if a project has hundreds of files, they won't have enough patience to type commands all the time. Build systems help simplify this process. The developer describes a set of commands in advance, and this set builds a project every time.

In fact, a build system is a set of scripts. The set contains commands to the compiler on how to build our targets. It removes the burden from the developer. Thus, we have to write short scripts that the build system converts into full commands to the compiler. The most famous build systems are make, autotools, and ninja, but there are many others.

If you think that make is a compiler — no, it's a kind of a wrapper over the compiler.

Even though build systems simplify developers' life, they're still platform dependent. So, we have two ways:

  • make build systems platform independent — hard and difficult (almost like making a binary that runs on *nix and Windows);
  • add an abstraction level — easier.

Some went the first way. The second way — appearance of meta build systems.

Now developers write scripts for meta build systems, and these, in turn, generate scripts for build systems. Another wrapper, but this time we have one front-end (meta build systems) and a lot of back-ends (build systems). For example, we use CMake as front-end. For Windows, we'll use MSBuild as back-end. It will be a wrapper over MSVC. For *nix, make is back-end, and it's a wrapper over GCC.

It's no secret that The Qt Company, starting with QT 6, abandons QMake in favor of CMake to build Qt. At the same time, Qbs was deprecated. It is still developed, though. Kudos to the community. But what does Qbs have to do with it? Qbs initially was a replacement for QMake.

[SPOILER BLOCK BEGINS]

We wanted the best; you know the rest...

[SPOILER BLOCK ENDS]

We had the main build system — QMake — and everything seemed fine with it. But let's look at the activity in the repository. Unfortunately, it is impossible to look at statistics by year. But we can do it locally and get the following:

In 2020, there were fewer commits than in any year before. It's going to be even fewer in 2021. Such high activity in 2019 is associated with the Qt 6 release and has almost nothing to do with QMake. If you look at the commits, you can notice that it's mostly fixes and not some new features. Thus, we can assume that QMake is maintained on a residual basis, and there's no rapid development planned.

Is QMake good?

Just the fact that QMake is a meta build system already makes it more user-friendly than make or autotools. But there are other important features. It's not hard to write "Hello world" in any build system. But it just gets better... Here's the benefit of popularity — it's easy to find an answer to any question on Stack Overflow or in Google. Let's look at the results of the 2021 Annual C++ Developer Survey "Lite". We need only one question: What build tools do you use? (Check all that apply).

Figure 2. Answered: 1,853 Skipped: 20

We can safely say that QMake is among the three most popular meta build systems in 2021 (ninja and make are not meta). Which means it won't be so difficult to find answers to many questions, even though many points are omitted in the documentation.

Why do many still choose QMake?

  • simplicity — it's way simpler than Cmake;
  • documentation — a strong side of all Qt projects (there are some exceptions though);
  • large knowledge base — undocumented aspects of QMake can at least be googled;
  • ease of connecting Qt libraries — for many years everything revolved around QMake, so in some moments QMake still wins over CMake (static build and plugins).

Perfect for a small Qt project, isn't it? That's why QMake is still a working solution and it's too early to throw it into the dust heap of history.

In short.

I'm not urging you to move to CMake immediately. QMake is a simpler and more user-friendly system for beginners (IMHO), and its capabilities can be enough for most projects.

What's wrong?

Ideologically, QMake is more suitable for projects where one .pro file is per one target i.e., TEMPLATE = lib or app. If this is not enough for us and we want to use TEMPLATE = subdirs, we'll have to be ready to jump on rakes laid for us. We'll talk about the rakes later. Of course, you can make this all work, but at what cost...

We have quite good cross-platform implemented via mkspecs (similar to CMake-toolchains). It gets much worse with cross-compilation. I never managed to implement it properly. Maybe I wasn't skilled enough. Although implementing CMake was easy.

Let's add to this a very hazy future (or a clear one, given all of the above). Is it still not enough to go left? Then CMake is for you.

According to the Annual C++ Developer Survey mentioned above, the most painful topic in C++ development is dependency management. So, this can't be ignored.

Figure 3. green — pain, blue — problem, yellow — doesn't matter; you can view the full version in the source.

We'll go back to it later. Now let's just say that QMake isn't really good at it. And if the third-party library doesn't have a pro file — Qmake is really bad.

To sum things up:

  • difficulty of managing divided large projects;
  • no future;
  • difficulties with cross-compilation;
  • managing non-Qt dependencies.

Figure 4. Into the bright future?

Is CMake better?

Or is it the same old soup, just reheated? I'll try to find an answer to this question.

Let's start from the first thing we don't like in QMake —difficulty of managing large projects divided into separate modules. CMake is designed differently. This is a plus for large projects, but it has a steep learning curve — so much that it can scare children. There is no explicit division into app, lib, and subdirs. There is always one root project. All other projects may or may not be its subprojects (add_subdirectory). Which means subdirs are active by default, but they may not be used.

Our project is interesting and complicated because we have different target OS and architectures. Let's assume that we need to build the project for 4 different versions: Windows x86, Windows x86_64, Linux Debian amd64, and Linux Debian armhf. As a result, we have three architectures and two OS. In addition to shot feet and lots of bruises (invaluable experience).

To answer your question, yes, we are dragging Qt into embedded. In my defense, it saved us a lot of development time. We don't need to rewrite the Qt parts in C++, we just take as is.

We don't use MinGW under Windows, only MSVC. We cross-compile with Clang, also use Clang to build under amd64 with CI, and so we can use GCC, but a compiler bug sometimes forces you to switch to another one. In the case of CMake, we must mention generators - Ninja is used everywhere, but Visual Studio also supports the generator as a backup option. This is important because what works for one, sometimes does not work for another, it's not even about a multi-config feature.

[SPOILER BLOCK BEGINS]

CMakeLists initially didn't look good.

[SPOILER BLOCK ENDS]

Does it sound too bad? However, QMake doesn't let us choose a generator (a build system). That's why we suffer — use JOM under Windows and make under *nix. Great opportunities make us pay a great price— CMake in one phrase.

What is the future of CMake? This is de facto a standard build system in C++, I don't think I need to say something else.

Cross-compilation in CMake works via cmake-toolchains, we just need to build the environment correctly, and write a toolchain file. All this will be completely transparent to the project file. Which means we don't need to separately specify any conditions and flags for cross-compilation. Really skilled developers cross-compile under embedded using CMake and non-widespread compilers. Here everything is limited by your imagination (and sometimes by the missing generator).

Managing dependencies is the hardest of all. CMake provides many ways to do this. So many that you can meet discussion on what exactly is better to use and why. CMake here completely follows the ideology of the language: one task can be solved in many ways.

Let's compare it in detail

Difficulty of managing divided large projects

Let's take a simple example. We have App1, App2, and lib1, lib2. Each App depends on each lib. If we simplify this a bit, we get the following files. Compare yourself:

qmake, src/root.pro:

TEMPLATE = subdirs

SUBDIRS = \
            lib1 \   # relative paths
            lib2 \
...
            App1 \
            App2

App1.depends = lib1 lib2 ...
App2.depends = lib1 lib2 ...

cmake, src/CMakeLists.txt:

add_subdirectory(lib1)
add_subdirectory(lib2)
add_subdirectory(App1)
add_subdirectory(App2)

In both cases, we list the subdirectories to include. But then in QMake, we need to explicitly specify that the final executable file depends on the library built. Otherwise, files of libraries will be built simultaneously, and we may encounter linking errors on a clean build (almost UB). In CMake, they made it differently and subtle. We'll talk about it later.

Library

Let's go further and describe our libraries first. For QMake, we have a bicycle, which obliges us to create a library with the same name and file name in the lib1 directory. It simplifies our work later - reduces the amount of boilerplate code. Actually, it's strange that we need a bicycle for a small project, isn't it? If you have the same question, maybe you should move to CMake too.

What's interesting — I couldn't get this hack to work under *nix. In the end I just threw out QMake.

qmake, src/lib1/lib1.pro

QT += core network xml 
## we specify the necessary Qt components
TARGET = lib1$${LIB_SUFFIX} 
## we specify the target
TEMPLATE = lib 
## tell it that we build a library
DEFINES += LIB1_LIBRARY
## add define, it may come in handy
include(lib1.pri) 
## specify .pri file that consists of enumeration of sources
QMake, src/lib1/lib1.pri
SOURCES += \
    src.cpp \
    ...

HEADERS += \
    hdr.h \
    ...

The division into pri and pro is used on purpose — one file would have all the directories, and another would list the sources and headers. It has no real meaning, but it was easier for me to navigate.

cmake, src/lib1/CMakeLists.txt

project(gen LANGUAGES CXX) 
## specify the project and languages used
find_package(
  QT NAMES Qt6 Qt5
  COMPONENTS Core Network Xml
  REQUIRED) 
## specify that we want to find a Qt6 or Qt5 package
find_package(
  Qt${QT_VERSION_MAJOR}
  COMPONENTS Core Network Xml
  REQUIRED) 
## specify that we need these components from the package found
add_library(
  lib1 STATIC
  hdr.h
  ...
  src.cpp
  ...) 
## specify that we want to build a static library
target_link_libraries(
  lib1
  PRIVATE Qt${QT_VERSION_MAJOR}::Core
  PRIVATE Qt${QT_VERSION_MAJOR}::Xml
  PRIVATE Qt${QT_VERSION_MAJOR}::Network) 
## link it with these libraries
target_compile_definitions(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_LIBRARY)
## also add a macro

Here it may seem that CMake is wordy and overloaded. But the target_link_libraries directory allows us to specify which linking type we want. In QMake, we'll get PUBLIC by default and then only linker/compiler flags. The find_package command at first seems bulky but turns out to be a very flexible and user-friendly tool. Let's omit lib2 and others for now.

The QT_VERSION_MAJOR variable is not set in older versions, be careful. Then you can get it the following way:

if (NOT QT_VERSION_MAJOR)
    set(QT_VERSION ${Qt5Core_VERSION})
    string(SUBSTRING ${QT_VERSION} 0 1 QT_VERSION_MAJOR)
endif()

Application

Let's look at App1.

qmake, src/App1/App1.pro

QT       += core gui network widgets xml 
TARGET = App1
VERSION = 1.0.0 
## specify the version
QMAKE_TARGET_COMPANY = Company
QMAKE_TARGET_COPYRIGHT = Company
QMAKE_TARGET_PRODUCT = Product
## specify information about our executable file
TEMPLATE = app 
## now we are building the executable file
RC_ICONS = ../../logo.ico 
## it's easier to specify the icon here, but it's still win-only
QMAKE_SUBSTITUTES += config.h.in 
## templates for generated files
## the ready config.h file is next to the template
include(App1.pri)
LIBRARIES += lib1 \
    ...
    lib2 
## and this is a hack listing what our App1 depends on

I omitted the insides of App1.pri. We don't need them, since there's only an enumeration of sources and headers.

qmake, src/App1/config.h.in — add a bit of useful information

#pragma once
#define PROGNAME '"$$TARGET"'
#define PROGVERSION '"$$VERSION"'
#define PROGCAPTION '"$$TARGET v"'
#define SOFTDEVELOPER '"$$QMAKE_TARGET_COMPANY"'

cmake, src/App1/CMakeLists.txt

project(App1)

set(PROJECT_VERSION_MAJOR 1)
set(PROJECT_VERSION_MINOR 0)
set(PROJECT_VERSION_PATCH 0)
## here the version can be specified in different ways
## we will specify it like this
 
configure_file(
  ${CMAKE_SOURCE_DIR}/config.h.in 
  ## take this file as a template
  ${CMAKE_CURRENT_BINARY_DIR}/config.h 
  ## generate a new one from it along a path
  @ONLY)
configure_file(
  ${CMAKE_SOURCE_DIR}/versioninfo.rc.in
  ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc
  ## similar generation, but rc files here
  @ONLY)
## generated files

find_package(
  QT NAMES Qt6 Qt5
  COMPONENTS Core Xml Widgets Network
  REQUIRED)
find_package(
  Qt${QT_VERSION_MAJOR}
  COMPONENTS Core Xml Widgets Network
  REQUIRED)

add_executable(${PROJECT_NAME}
    main.cpp
    ...
    ../../icon.rc # also an icon, but windows only
    ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc # windows-only
    )

target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
## lets add our directory to include directories, where the generated files
## will be

if(CMAKE_BUILD_TYPE STREQUAL "Release")
    set_property(TARGET ${PROJECT_NAME} PROPERTY WIN32_EXECUTABLE true)
endif() 
## of course crutches, we say that it is necessary to run gui without a console

target_link_libraries(
  ${PROJECT_NAME}
  lib1
  ...
  lib2
  Qt${QT_VERSION_MAJOR}::Core
  Qt${QT_VERSION_MAJOR}::Xml
  Qt${QT_VERSION_MAJOR}::Widgets
  Qt${QT_VERSION_MAJOR}::Network
  )

Almost two times more lines in CMake, what the...

cmake, src/config.h.in

#define PROGNAME "@PROJECT_NAME@"
#define PROGVERSION "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.
@PROJECT_VERSION_PATCH@"
#define PROGCAPTION "@PROJECT_NAME@ v"
#define SOFTDEVELOPER "@SOFTDEVELOPER@"

cmake, src/versioninfo.rc.in

1 TYPELIB "versioninfo.rc"

1 VERSIONINFO
 FILEVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@,
@PROJECT_VERSION_PATCH@, 0
 PRODUCTVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@,
@PROJECT_VERSION_PATCH@, 0
 FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
 FILEFLAGS 0x1L
#else
 FILEFLAGS 0x0L
#endif
 FILEOS 0x4L
 FILETYPE 0x2L
 FILESUBTYPE 0x0L
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904e4"
        BEGIN
            VALUE "CompanyName", "@SOFTDEVELOPER@"
            VALUE "FileDescription", "@PROJECT_NAME@"
            VALUE "FileVersion", 
            "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.
@PROJECT_VERSION_PATCH@.0"
            VALUE "InternalName", "@PROJECT_NAME@"
            VALUE "LegalCopyright", "Copyright (c) 2021 @SOFTDEVELOPER@"
            VALUE "OriginalFilename", "@PROJECT_NAME@.exe"
            VALUE "ProductName", "@PROJECT_NAME@"
            VALUE "ProductVersion",
            "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.
@PROJECT_VERSION_PATCH@.0"
        ## here we also provide information about our 
        ## executable file
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x409, 1252

The title is about the build system, and we have .rc files. Why? Easy. CMake does not provide an opportunity to specify an icon or information about an executable file via variables (unlike QMake), so we need the .rc file.

But still .rc files are windows-only, like QMAKE_TARGET_*RC_ICONS. Actually, you can also use the generated .rc file in QMake. But would you do this if there are enough built-in variables, and QMake do everything itself? So, the magic and .rc files are hidden from us in QMake.

The configure_file directive is similar to QMAKE_SUBSTITUTES, but with one important difference. You can specify the path where the file will be generated. In QMake, it will be next to the original file. It doesn't matter if you only need to use it once. But what if we need to generate multiple files using the same template? For example, what if we need to pull out the version with the information of the current commit? We'll have to suffer. In the case of QMake, each target should have a file copy in another directory. Otherwise, they will be overwritten. CMake provides more ways to work with paths.

Let's go back and remember lines in the first .pro file — App1.depends = lib1 lib2 ... CMake has a similar tool under the hood. However, it looks much more user-friendly. All this works through the target_link_libraries(<target> ... <item>... ...) directory. Here target depends on itemitem should be built before linking with target. If you use the recommended syntax, i.e. item is a library target name (item must be created by the add_library() directive or be the IMPORTED library), then everything will be built and link itself perfectly. When the library is rebuilt, it will be linked again. I must say this is more user-friendly than the implementation in QMake. Why is this not in QMake?

We can say that CMake provides more features, but you also have to write more with your hands. CMake starts to look like a well-known programming language...

Managing dependencies

Here we have solutions common to both build systems and specific to each. Let's start with common.

Package managers (specifically Conan) provide user-friendly ways of integration with both build system. But there is a small nuance - the main way of integration into QMake. It's not transparent. Now we'll completely depend on Conan and cannot build project without using it. Great? Other languages also depend on package systems, but they're part of the language itself.

The things with CMake are different now. There are three generators: cmake, cmake_find_package, cmake_find_package_multi. The first is similar to the one for QMake and gets us hooked on a package manager. The last two provide transparent integration, which is a big plus. On Windows, for example, we can link with the library from Conan. On Linux — with libraries from packages without any problem. Here are lots of buts and ifs, which partly relate to weird receipts in Conan. But the opportunity still exists and covers most cases. So, a bit of magic is here. A small example:

find_package(hidapi REQUIRED) # finds the system dev package
                              # and the package from conan

if (UNIX)
# debian package
    target_link_libraries(${PROJECT_NAME} PRIVATE hidapi-hidraw)
endif()
if (WIN32)
# conan
    target_link_libraries(${PROJECT_NAME} PRIVATE hidapi::hidapi)
endif()

I specially pulled out such an example. hidapi under *nix and hidapi under Windows are different libraries with same API. That is, under *nix it is done either with libusb or hidraw, but Windows has only one option.

But what should we do if our library is not in the package manager (or our distribution repacks)? And this happens often. I hope someday in our terrible C++ world there will be a package manager with libraries for anything (hello npm).

With QMake, we don't have this opportunity. If the desired library provides integration capabilities (for example, it contains a .pro file), then everything is cool. For example, here: https://github.com/QtExcel/QXlsx/blob/master/HowToSetProject.md , 4 lines and everything is fine. But if the desired library doesn't support QMake...you can't do anything except first collect and sort it all.

With CMake, the situation is completely different, it provides an interface for catching and building third-party libs out of the box, even if they do not support CMake - ExternalProject. Of course, if the desired lib contains perfect CMakeLists, then you need to write about 4 lines too (there is an example: https://github.com/QtExcel/QXlsx/issues/49#issuecomment-907870633 ). Or you can even go through add_subdirectory and then limit yourself to 1 line, manage versions via git submodule. But the world of crutches is big. Let's imagine that the desired library supports only QMake and nothing more (postpone the option with patches and contribution to Open Source). For example, LimeReport — I intentionally specified the old commit, because later I corrected CMakeLists. We can build a really interesting bicycle. If the library supports something else but we want to patch and build in our own way, then use QXlsx. CMake provides lots of features even here, we just need to learn how to use them.

Conclusion

QMake is good as a build system, easy to learn and user-friendly. If you are writing a small Qt-only project or a project strictly for one platform with one compiler, then everything is fine in your world, but as soon as you need to go beyond what is allowed...

CMake is complicated. One good person said that it should be considered as a separate programming language. I've got to agree with him, because you have to write a lot. CMake allows to do many things, so many that sometimes such a thing is born.

If you have a complex project, you want to play with dependencies, use the same codebase on a variety of operating systems and architectures (or just be on the same wavelength with others) then your choice is CMake.

If to make a parallel, then QMake is js/python, and CMake is C++.

P.S. The article omits generator expressions because there are simply no similar things in QMake.

Special thanks to the fellows from this channel [RU], this channel [RU], and the author of this article, because this project wouldn't have been ported without them.

The old state of the project can be viewed here, and the new one is available without binding to the commit, if you suddenly want to look.