illustrations

Porting ScreenPlay from QMake to CMake: A story on why CMake is actually pretty nice in 2020.

Published on Sep 01, 2020 by Elias Steurer | Kelteseth

post-thumb


ScreenPlay is an Open-Source Live Wallpaper and Widgets app for Windows (and soon Linux & MacOSX) written in modern C++/Qt/QML and is in active development since early 2017 on Gitlab.





1. Code sharing with qmake is a mess

When developing apps that are a bit more complex, the best solution is often try to split them into little more managable pieces. For example if you want to test your app as a separate executable, a subdirs project (a tree of projects) is the only choice here.

A subdirs projects is just a simple MyApp.pro that only contains a template and subdirs:

1
2
3
4
5
6
7
8
9
  

  TEMPLATE = subdirs
 
  SUBDIRS = \
            src/app \   # relative paths
            src/lib \
            src/lib2
            

We now have multiple subprojects needing to share code between each other. To tell the compiler where to find the header and source files from the other project, we need to tell the linker what library to link and where to find the compiled files. The way to go in qmake is to make giant .pri files that is solely used for including files. This is similar to regular #include <xyz.h> in c++. These MyProjectName.pri are now included in other MyProjectName.pro. To fix the relative path problem you have to add the current absolute path to every line:

1.1 External dependencies

Working with external dependencies on multiple operating system mostly consists of copy pasting chunks of plattform specific paths into your .pro file. This is really a tedious work because every OS handles the paths a bit differently. E.g. we do not have a separate subfolder for debug/release on linux.

1.2 Compiling performance killer “CONFIG += ordered”

Another big problem with qmake is the seemingly random compiler races. When you have many subprojects that are libraries for other subprojects, it would randomly fail because library libA depends on library libB and libC. But libC was not yet build at the time. Most of the time a simple second recompile would fix this. But this clearly shows some serious flaws. This problem never really was fixed with libA.depends = libB. Maybe (pretty sure) I made some mistakes, but my colleagues and I never solved this issue. The only way to make sure that the build order is fixed, was to set “CONFIG += ordered” which kills all build parallelism.

2. Why QBS lost against CMake

It was a real shocker when the QtCompany announced to no longer activly support QBS. I was even one of the people who pushed to make a second community vote. QBS syntax looks nice and familiar to everyone who ever coded QML. CMake does not. After working with CMake for some months now,

I can confidently say it was the right decision to use CMake instead of QBS as the default build system from Qt6 and forward.

CMake (with has mostly syntax flaws) works solid. QBS problems are more political than technical:

This is one of the main no go for many programmers that dislike Qt for its size (both in lines of code and library size). Also, many people hate MOC. This is the pre compiler that compiles your Qt C++ into regular C++. This is for writing nice code like emit mySignal();

2.2 Yet another build system

We already have build2, CMake, meson, scons that have many projects using outside of the Qt eco system.

2.3 No support for IDEs

As far as I know QtCreator is the only IDE that ever supported QBS.

2.4 vcpkg + CMake = ❤️

Remember my rant about external dependencies from paragraph 1.1? Well for me vcpkg is the holy grail for every C++ developer. Install dependencies with one command!




3. CMake is ugly, kinda

CMake is really ugly if you click on the first 10 google results. This is because google shows you old CMake stackoverflow answers from 2008 and often redirects you to the old documentation from 2.8. CMake syntax can be quite nice, because most of the time you only these commands:

ScreenPlay CMakeLists.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# Set the minimum requirement
cmake_minimum_required(VERSION 3.16.0)

# Set the Project name. This is later used for the executable name and the 
# very useful ${PROJECT_NAME}
project(ScreenPlay)

# Some Qt settings for resources and MOC
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)

# This is actually only synatx sugar. This only creates the variable src
# with these strings as array elements. This is used later at add_executable
set(src main.cpp
        app.cpp
        # Lets skip some content here
        src/util.cpp
        src/create.cpp)

set(headers app.h
        src/globalvariables.h
        # Lets skip some content here
        src/util.h
        src/create.h)

# Qt macro for big resources like our fonts
qt5_add_big_resources(resources  resources.qrc)

# Tell CMake to compile our qml into C++ in release mode
# to make it fast!
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(qml qml.qrc)
else()
    qtquick_compiler_add_resources(qml qml.qrc )
endif()

# Tell CMake to search for these libraries. Because we set the CMAKE_TOOLCHAIN_FILE earlier
# we no longer have to manage ugly relative paths by hand!
find_package(
  Qt5
  COMPONENTS Quick
             QuickCompiler
             Widgets
             Gui
             WebEngine
  REQUIRED)

# External vcpkg libraries
find_package(ZLIB REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(libzippp CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)

# CMake has two main commands: 
# add_executable for creating an executalbe
# add_library for creating a library
add_executable(${PROJECT_NAME} ${src} ${headers} ${resources} ${qml})

# Custom property to disable console window on Windows
# https://stackoverflow.com/questions/8249028/how-do-i-keep-my-qt-c-program-from-opening-a-console-in-windows
set_property(TARGET ${PROJECT_NAME} PROPERTY WIN32_EXECUTABLE true)

# Tell the linker to search for these dependencies. Most of the time vcpkg will
# tell you the name of the libary. If not look at the vcpkg/installed path for
# the name of the dll/lib/so/dynlib
# If you need to have dependencies inside your project structure you can simply
# add the project(MyLib) to the target_link_libraries. No inlude paths, 
# no additional stuff, it simply works!
target_link_libraries(${PROJECT_NAME}
    PRIVATE
    Qt5::Quick
    Qt5::Gui
    Qt5::Widgets
    Qt5::Core
    Qt5::WebEngine
    nlohmann_json::nlohmann_json
    libzippp::libzippp
    ScreenPlaySDK
    QTBreakpadplugin)

# Tell CMake to copy this file to your build dir if changed
# ${CMAKE_BINARY_DIR} is your build directory!
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/bin/assets/fonts)
configure_file(assets/fonts/NotoSansCJKkr-Regular.otf ${CMAKE_BINARY_DIR}/bin/assets/fonts COPYONLY)

4. Ninja makes CMake fast

CMake only generates instructions for your build system of choice. This can be a big advantage when working with people that prefer VisualStudio over QtCreator. When using CMake one can (should) choose Ninja as the default build system. Compiling projects with CMake+Ninja is fun. Ninja and CMake comes shipped with the Qt Maintenance in the tools category. Iterative changes are incredible fast and clean to look at with the [13/424] progress.

It’s so fast that working with Godots scons makes me activly want to convert Godot to CMake.

5. vcpkg is where CMake really shines!

Managing dependencys in C++ is tedious, many projects even ship dlls with their git repository. This is bad because it blows up the git repo size (we ignore git-lfs for now). A drawback is that vcpkg only supports one global version of packages (you manually install different versions of vcpkg, but this is more like a hack and only seldom needed). This is a feature on their roadmap.

1
2
  
    vcpkg install crashpad
In ScreenPlay we simply have an install_dependencies_windows.bat or install_dependencies_linux_mac.sh to clone vcpkg, build it and install all our dependencies. When working with QtCreator, we must set the CMAKE_TOOLCHAIN_FILE to the relative path of vcpkg. Also we must tell vcpkg on what OS and arch we use.

1
2
3
4
5
  
    # QtCreator setup. Extras -> Tools -> Kits ->  -> CMake Configuration -> Append this:
    CMAKE_TOOLCHAIN_FILE:STRING=%{CurrentProject:Path}/Common/vcpkg/scripts/buildsystems/vcpkg.CMake
    VCPKG_TARGET_TRIPLET:STRING=x64-windows
    

Need to install another library? Simply call vcpkg install myLibToInstall again and you are good to go!

6. Conclusion

Going with the flow has its advantages but comes at a cost. Build systems, like qbs, with a big potential get thrown under the bus. What to use is up to the developer and thats why my projects will use CMake from now on.

The next blog post will be about how to set-up Qt projects beyond “Hello World”. Stay tuned!

You can discuss this blog post here post in our forum.