Post

qmake/cmake Buildsystem-Konfigurationen für Qt-basierte dynamisch gelinkte Bibliotheken und gesamte Sitzungen/Projektmappen

qmake/cmake Buildsystem-Konfigurationen für Qt-basierte dynamisch gelinkte Bibliotheken und gesamte Sitzungen/Projektmappen

Wenn man größere Qt Projekte umsetzt, wird man sicherlich Teilkomponenten in Bibliotheken auslagern. Reine C++ Bibliotheken ohne Qt-Abhängigkeiten kann man einfach statisch ohne größere Probleme in das Programm linken. Bei Qt-basierten Bibliotheken bieten sich dynamisch gelinkte Bibliotheken an. Dieser Blogartikel beschreibt, wie man sowas mit den Buildsystemkonfigurationen qmake und cmake plattformübergreifend und für übliche Toolchains konfiguriert.

Zielstellung

Eine Qt-basierte Bibliothek soll als dynamisch gelinkte Bibliothek erstellt werden und in ein Programm gelinkt werden. Beide Projekte sollten zusammen in einer Sitzung verwaltet werden, unter folgenden Maßgaben:

  • es wird ausschließlich out-of-source gebaut, so wie das QtCreator standardmäßig macht, und die erstellten Dateien in /build/Desktop-Debug o.ä. ablegt. Bei cmake wird ebenfalls in einem separaten Erstellungsverzeichnis gebaut
  • die Binaries/Executables und die DLLs (unter Windows) sollen top-level unter /bin/debug oder /bin/release abgelegt werden. Es ist ok, wenn die Binaries von zwei separaten Toolchains (bspw. VisualStudio-Debug oder MinGW-Debug) überschreiben. Die Bibliotheken und alle generierten Dateien sollen aber separat in individuellen Shadow-Build-Verzeichnissen liegen. Das könnte man auch anders machen (binaries auch in den Shadow-Build-Ordnern ablegen), aber für das Debuggen und Zugriff auf Resourcen im Entwicklungsverzeichnis ist das pragmatisch, den relativen Pfad zum Entwicklungswurzelverzeichnis zu kennen, unabhängig davon, wo die Shadow-Build-Verzeichnisse liegen.

Ein Beispielprojekt

Um die Konfigurationsdateien und die kritischen Parameter zu erklären (und zu testen), hab ich ein kleines Testprogramm vorbereitet, welches als komplettes Beispiel herunterladbar ist:

Die Verzeichnisstruktur sieht so aus:

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
├── CMakeLists.txt
├── SharedLibraryBuildSystemConfigs.pro
├── bin/
├── build/
├── libs
│   ├── build.cmake
│   ├── build.pri
│   └── TestLib
│       ├── CMakeLists.txt
│       ├── resources
│       │   ├── gfx
│       │   │   ├── green.png
│       │   │   ├── red.png
│       │   │   └── yellow.png
│       │   └── TestLib.qrc
│       ├── src
│       │   ├── LEDLabel.cpp
│       │   ├── LEDLabel.h
│       │   └── TestLib_global.h
│       └── TestLib.pro
└── TestApp
    ├── CMakeLists.txt
    ├── src
    │   ├── main.cpp
    │   ├── MainWindow.cpp
    │   └── MainWindow.h
    └── TestApp.pro

Die *.pro und build.pri gehören zum qmake Buildsystem. Die CMakeLists.txt-Dateien und build.cmake gehören zum cmake Buildsystem.

Standard-Verzeichnisstruktur

Die Verzeichnisstruktur folgt einem einfachen Schema, das sich beliebig auf weitere Bibliotheken und Anwendungen erweitern lässt:

Anwendungen (Executables) liegen jeweils in einem eigenen Verzeichnis auf oberster Ebene, hier TestApp/. Bibliotheken werden dagegen alle gemeinsam unterhalb von libs/ gruppiert, hier also libs/TestLib/. Diese Trennung macht auf einen Blick klar, was am Ende ein eigenständiges Programm ergibt und was nur als Baustein dafür dient.

Innerhalb jedes Projektverzeichnisses (egal ob App oder Lib) wird die gleiche, grundlegende Verzeichnisstruktur verwendet:

  • src/ enthält sämtliche Quelltext- und Headerdateien, also bei TestLib die Dateien LEDLabel.h/.cpp und TestLib_global.h, bei TestApp entsprechend main.cpp und MainWindow.h/.cpp.
  • resources/ enthält alle Ressourcen, die über das Qt-Resource-System eingebunden werden – bei TestLib also das Verzeichnis gfx mit den PNG-Grafiken sowie die zugehörige TestLib.qrc.
  • Auf Ebene des Projektverzeichnisses selbst liegen die Build-Konfigurationsdateien für beide Buildsysteme: eine *.pro-Datei für qmake (TestLib.pro, TestApp.pro) und eine CMakeLists.txt für cmake. Auf die Inhalte dieser Dateien gehe ich in den folgenden Abschnitten im Detail ein.
  • Als Namenskonvention gilt: Der Name der .pro-Datei ergibt sich aus dem Namen des Projektverzeichnisses, hier also TestLib -> TestLib.pro und TestApp -> TestApp.pro.

Zusätzlich gibt es zwei projektübergreifende Dateien direkt im Wurzelverzeichnis der Sitzung: SharedLibraryBuildSystemConfigs.pro bindet als qmake-subdirs-Projekt die Unterprojekte libs/TestLib und TestApp zusammen, während die CMakeLists.txt im Wurzelverzeichnis die entsprechende Klammer für cmake bildet. Im Verzeichnis libs/ liegen außerdem build.pri und build.cmake – gemeinsam genutzte Konfigurationsfragmente, die von allen Bibliotheks- und App-Projekten eingebunden werden und die eigentliche Build-Logik (Ausgabepfade, Compiler-Flags usw.) an zentraler Stelle bündeln.

QMAKE

Zunächst wird der Quelltext und das qmake-Buildsystem erklärt. Wir beginnen mit dem Quelltext der Bibliothek.

Die TestLib Bibliothek

Quelltext und Headerdateimakros

Jede Bibliothek bringt eine eigene <LibName>_global.h mit, hier TestLib_global.h:

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef TESTLIB_GLOBAL_H
#define TESTLIB_GLOBAL_H

#include <QtCore/qglobal.h>

#if defined(TESTLIB_LIBRARY)
#  define TESTLIB_EXPORT Q_DECL_EXPORT
#else
#  define TESTLIB_EXPORT Q_DECL_IMPORT
#endif

#endif // TESTLIB_GLOBAL_H

Das darin definierte Makro TESTLIB_EXPORT wird vor jede Klasse gesetzt, die von außerhalb der Bibliothek sichtbar sein soll, also in LEDLabel.h:

1
2
3
4
5
#include "TestLib_global.h"

class TESTLIB_EXPORT LEDLabel : public QWidget {
    ...
};

Der Hintergrund: Bei dynamisch gelinkten Bibliotheken muss der Compiler wissen, welche Symbole (Klassen, Funktionen) die Bibliothek nach außen exportiert, damit sie in der DLL/.so überhaupt auftauchen und von anderen Programmen aufgelöst werden können. Q_DECL_EXPORT und Q_DECL_IMPORT sind Qt-Makros, die genau das plattformabhängig übernehmen – unter Windows expandieren sie zu __declspec(dllexport) bzw. __declspec(dllimport), unter Linux/macOS sind sie (mit sichtbarkeitsbasiertem Compiler-Setup) weitgehend leer, da dort standardmäßig alle Symbole einer .so sichtbar sind.

Entscheidend ist nun, dass dieselbe Headerdatei sowohl beim Bauen der Bibliothek als auch beim Verwenden der Bibliothek (im Anwendungsprojekt) eingebunden wird – aber mit jeweils anderer Bedeutung des Makros. Deshalb steuert das Define TESTLIB_LIBRARY per #if defined(...), welcher der beiden Fälle gerade vorliegt:

  • Beim Übersetzen der Bibliothek selbst muss TESTLIB_LIBRARY gesetzt sein, damit TESTLIB_EXPORT zu Q_DECL_EXPORT wird und die Klasse exportiert wird. In TestLib.pro geschieht das über:
    1
    
    DEFINES += TESTLIB_LIBRARY
    
  • Im Anwendungsprojekt (TestApp), das die Bibliothek nur benutzt, wird TESTLIB_LIBRARY nicht gesetzt. Dort expandiert TESTLIB_EXPORT also zu Q_DECL_IMPORT, wodurch der Compiler weiß, dass die Symbole aus einer externen DLL importiert werden.

Wichtig ist dabei, dass DEFINES += TESTLIB_LIBRARY ausschließlich in der .pro-Datei der Bibliothek selbst steht und nicht etwa in einer gemeinsam genutzten Konfigurationsdatei wie build.pri landet – sonst würde das Define auch beim Bauen von TestApp gesetzt und die Importdeklaration ginge verloren, was zu Linkerfehlern führt.

Unter MinGW würde der Linker (ld) ohne jegliche dllexport-Markierung automatisch alle Symbole exportieren. Sobald aber – wie hier – auch nur ein Symbol explizit mit dllexport markiert wird, gilt dieser Automatismus nicht mehr, und nur die markierten Symbole werden exportiert. Die Makros bleiben also auch bei MinGW sinnvoll, um gezielt zu steuern, was exportiert wird, statt versehentlich alle internen Symbole offenzulegen. Quelle: binutils ld docs — WIN32 (cygwin/mingw)

pro-Datei und gemeinschaftlich genutzte pri-Datei

Die TestLib.pro sieht wie folgt aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
TEMPLATE = lib
# TARGET ist optional. Ohne Angabe wird der aktuelle Verzeichnisname als 
# Target verwendet. Dank unserer Namenskonvention 
# <Verzeichnisname> -> <VerzeichnisName>.pro könnte man das also weglassen.
TARGET = TestLib

QT += widgets

# Allgemeine Einstellungen, muss nach der `TEMPLATE` Zeile eingebunden werden
include( ../build.pri )

# Setze das Define um die Bibliothek zu erstellen (nur in dieser pro-Datei)
DEFINES += TESTLIB_LIBRARY

SOURCES += \
    src/LEDLabel.cpp

HEADERS += \
    src/LEDLabel.h \
    src/TestLib_global.h

RESOURCES += \
    resources/TestLib.qrc

Gleich zu Beginn, aber nach TEMPLATE=lib wird in Zeile include( ../build.pri ) eine Datei mit allgemeinen Einstellungen eingebunden. Alles Projektspezifische (TEMPLATE, TARGET, QT, DEFINES, SOURCES, HEADERS, RESOURCES) steht direkt in der .pro-Datei der Bibliothek. Alles, was dagegen für sämtliche Bibliotheken und Anwendungen der Sitzung gleichermaßen gilt – insbesondere die Ausgabepfade –, ist in build.pri ausgelagert und wird hier lediglich eingebunden. So muss diese Logik nicht in jeder einzelnen .pro-Datei dupliziert werden, sondern wird an zentraler Stelle gepflegt.

Die build.pri sieht wie folgt aus:

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
# Gemeinsame Build-Konfiguration, die von allen Bibliotheks- und 
# Anwendungs-Projektdateien eingebunden wird.
#
# Legt die gemeinsamen Ausgabeverzeichnisse fest und, für Anwendungen, 
# den Suchpfad, der zum Linken gegen die in diesem Repository gebauten 
# dynamischen Bibliotheken benötigt wird.
#
# Verwendung (in einer .pro-Datei): include( <relativer Pfad>/libs/build.pri )

# C++17 wird für Qt6 benötigt
CONFIG += c++17

# Bibliotheken werden im Unterverzeichnis 'lib' des Build-Verzeichnisses abgelegt, damit
# alle generierten Dateien einer Build-Konfiguration zusammenbleiben und der Quellcode-Baum
# sauber bleibt. Bei einem Shadow-Build, wie ihn QtCreator standardmäßig anlegt, z.B.
# build/Desktop-Debug, landen die Bibliotheken also in build/Desktop-Debug/lib.
# Auf diese Weise können unterschiedliche Build-Konfigurationen (debug/release) 
# koexistieren, ohne sich gegenseitig die Bibliotheken zu überschreiben.
#
# Executables werden in bin/debug bzw. bin/release abgelegt.

# oberstes Build-Verzeichnis: das Shadow-Build-Verzeichnis, das dem Wurzelverzeichnis
# der Sitzung entspricht, z.B. build/Desktop-Debug
# Hinweis: dies setzt einen Shadow-Build voraus, wie er standardmäßig in 
# QtCreator konfiguriert ist.
BUILD_ROOT = $$shadowed($$PWD/..)

# Verzeichnisse, die alle dynamischen Bibliotheken bzw. alle Executables 
# dieser Sitzung aufnehmen
LIB_DIR = $$BUILD_ROOT/lib
CONFIG( debug, debug|release ) {
	BIN_DIR = $$PWD/../bin/debug
}
else {
	BIN_DIR = $$PWD/../bin/release
}

# Relative Unterverzeichnisse, um generierte Dateien zu sortieren (optional)
OBJECTS_DIR = obj
MOC_DIR = moc
RCC_DIR = rcc
UI_DIR = ui

# Einstellungen für Bibliotheksprojekte
equals( TEMPLATE, lib ) {
	DESTDIR = $$LIB_DIR
	# DLLs werden im gleichen Ort wie Binaries/EXE-Dateien abgelegt
	DLLDESTDIR = $$BIN_DIR
}

# Einstellungen für Apps
else:equals( TEMPLATE, app ) {

	DESTDIR = $$BIN_DIR

	# Suchpfad für die dynamischen Bibliotheken beim Linken
	LIBS += -L$$LIB_DIR

	# teilt dem dynamischen Linker mit, wo er die dynamischen Bibliotheken zur 
	# Laufzeit findet; so kann die Programmdatei unter Linux gestartet werden, 
	# ohne dass das Bibliotheksverzeichnis zusätzlich in LD_LIBRARY_PATH 
	# eingetragen werden müsste
	unix {
		QMAKE_RPATHDIR += $$LIB_DIR
	}

}

Die Datei Abschnitt für Abschnitt:

  • CONFIG += c++17 legt den Sprachstandard fest, den alle Teilprojekte der Sitzung verwenden sollen – zentral an einer Stelle definiert, statt in jeder .pro-Datei erneut.

  • BUILD_ROOT = $$shadowed($$PWD/..): Die qmake-Funktion $$shadowed(...) übersetzt einen Pfad im Quellverzeichnis in den entsprechenden Pfad innerhalb des aktuellen Shadow-Build-Verzeichnisses. $$PWD ist dabei stets das Verzeichnis, in dem die gerade verarbeitete .pro/.pri-Datei liegt – hier also libs/. $$PWD/.. zeigt somit auf das Wurzelverzeichnis der Sitzung, und $$shadowed(...) liefert dazu den passenden Pfad im aktiven Shadow-Build-Verzeichnis, egal ob gerade build/Desktop-Debug, build/Desktop-Release oder ein anders benanntes Verzeichnis aktiv ist. BUILD_ROOT zeigt also immer auf das Shadow-Build-Verzeichnis, das dem Wurzelverzeichnis der Sitzung entspricht.

  • LIB_DIR = $$BUILD_ROOT/lib: Alle Bibliotheken landen gemeinsam in einem lib-Unterverzeichnis des Shadow-Build-Verzeichnisses. Da BUILD_ROOT bereits pro Build-Konfiguration unterschiedlich ist (Debug und Release liegen ja in unterschiedlichen Shadow-Build-Verzeichnissen), ist auch LIB_DIR automatisch pro Konfiguration getrennt.

  • BIN_DIR: Im Gegensatz zu LIB_DIR liegt dieses Verzeichnis nicht im Shadow-Build-Verzeichnis, sondern fest unterhalb des Quellverzeichnisses ($$PWD/../bin/debug bzw. .../bin/release), unabhängig davon, mit welcher Toolchain oder welchem Shadow-Build-Verzeichnis gerade gebaut wird. Die Bedingung CONFIG( debug, debug|release ) unterscheidet zwischen Debug- und Release-Build. Das entspricht genau der eingangs formulierten Zielsetzung: Alle Executables und DLLs landen gebündelt in bin/debug bzw. bin/release, auch wenn zwei unterschiedliche Toolchains (z.B. MSVC-Debug und MinGW-Debug) sich dabei gegenseitig überschreiben. Das wird hier bewusst in Kauf genommen.

  • OBJECTS_DIR, MOC_DIR, RCC_DIR, UI_DIR: Diese vier Variablen legen fest, wohin qmake die beim Bauen anfallenden Zwischendateien ablegt. Objektdateien, MOC-generierte Dateien (aus dem Qt-Meta-Object-Compiler), RCC-generierte Dateien (aus kompilierten .qrc-Ressourcen) und UI-generierte Header (aus .ui-Dateien). Da diese Pfade relativ angegeben sind, werden sie relativ zum jeweiligen Shadow-Build-Verzeichnis des Teilprojekts aufgelöst. Die Zwischendateien von TestLib und TestApp kollidieren also nicht, obwohl beide dieselben Variablennamen obj, moc usw. verwenden.

  • Der Block equals( TEMPLATE, lib ) { ... } else:equals( TEMPLATE, app ) { ... } unterscheidet anhand des Werts von TEMPLATE, ob die aktuell verarbeitete .pro-Datei eine Bibliothek oder eine Anwendung beschreibt, und setzt je nachdem unterschiedliche Variablen:

    • Für Bibliotheken (TEMPLATE = lib) wird DESTDIR auf LIB_DIR gesetzt – dorthin baut qmake also die eigentliche .so/.dll/.dylib. Unter Windows wird zusätzlich DLLDESTDIR auf BIN_DIR gesetzt: qmake kopiert die erzeugte DLL dann automatisch zusätzlich in dieses Verzeichnis, sodass sie dort neben der .exe liegt und zur Laufzeit gefunden wird.
    • Für Anwendungen (TEMPLATE = app) wird DESTDIR auf BIN_DIR gesetzt – die Programmdatei landet also direkt im gemeinsamen bin/debug- bzw. bin/release-Verzeichnis. Außerdem wird LIBS += -L$$LIB_DIR ergänzt, damit der Linker beim Bauen der Anwendung weiß, wo er nach den Bibliotheken suchen soll. Unter Unix-artigen Systemen wird zusätzlich QMAKE_RPATHDIR += $$LIB_DIR gesetzt: Damit wird der Pfad zu LIB_DIR fest als RPATH in die erzeugte Programmdatei eingetragen, sodass der dynamische Linker die .so-Datei zur Laufzeit findet, ohne dass LD_LIBRARY_PATH gesetzt werden müsste.

Wie wirkt sich das nun konkret auf TestLib.pro aus? Da diese Datei mit TEMPLATE = lib beginnt, sorgt include( ../build.pri ) dafür, dass beim Bauen von TestLib automatisch DESTDIR = $$LIB_DIR und DLLDESTDIR = $$BIN_DIR gesetzt werden, ohne dass in TestLib.pro selbst auch nur ein Wort über Ausgabepfade steht. Die .pro-Datei der Bibliothek kann sich dadurch ganz auf das Projektspezifische beschränken: den Namen (TARGET), die verwendeten Qt-Module (QT += widgets), das Export-Define (DEFINES += TESTLIB_LIBRARY) sowie die Quell-, Header- und Ressourcendateien. Genau dieselbe build.pri bindet, wie im nächsten Abschnitt gezeigt, auch TestApp.pro ein. Dort greift dann aber der app-Zweig der Fallunterscheidung, und statt DESTDIR/DLLDESTDIR werden LIBS und QMAKE_RPATHDIR gesetzt, damit die Anwendung die Bibliothek zur Link- und zur Laufzeit findet.

Das TestApp Programm und die Sitzungs-pro-Datei

pro-Datei des Programms und Linken der Bibliothek

Die TestApp.pro sieht wie folgt aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
TEMPLATE = app
# Optional, siehe Erkläung in TestLib.pro
TARGET = TestApp

QT += widgets

# Allgemeine Einstellungen, muss nach der `TEMPLATE` Zeile eingebunden werden
include( ../libs/build.pri )

SOURCES += \
	src/main.cpp \
	src/MainWindow.cpp

HEADERS += \
	src/MainWindow.h

# Include-Pfad zur Bibliothek hinzufügen
INCLUDEPATH += ../libs/TestLib/src

# Linke gegen die Bibliothek, Suchpfad ist bereits in libs/build.pri definiert
LIBS += -lTestLib

Auch hier gilt wieder das gleiche Muster wie bei TestLib.pro: TEMPLATE = app steht ganz am Anfang, direkt danach folgt include( ../libs/build.pri ). Weil TEMPLATE beim Einbinden bereits auf app gesetzt ist, greift in build.pri diesmal der equals( TEMPLATE, app )-Zweig. DESTDIR wird also auf BIN_DIR gesetzt, und LIBS += -L$$LIB_DIR sowie (unter Unix) QMAKE_RPATHDIR += $$LIB_DIR werden automatisch ergänzt, ohne dass TestApp.pro selbst etwas davon wissen muss.

Damit TestApp tatsächlich gegen TestLib linkt, sind noch zwei projektspezifische Ergänzungen nötig, die naturgemäß nicht in der gemeinsamen build.pri stehen können, da sie ja pro Anwendung/Bibliothek unterschiedlich sind:

  • INCLUDEPATH += ../libs/TestLib/src fügt das Quellverzeichnis von TestLib zum Include-Suchpfad hinzu (relativ zum Verzeichnis der TestApp.pro), damit MainWindow.cpp die Headerdatei LEDLabel.h per #include <LEDLabel.h> finden kann, ohne einen langen relativen Pfad angeben zu müssen.
  • LIBS += -lTestLib weist den Linker an, gegen TestLib zu linken. Den Suchpfad dafür (-L$$LIB_DIR) hat bereits build.pri gesetzt – hier muss also nur noch der Bibliotheksname selbst angegeben werden. Zu beachten ist, dass sich -lTestLib unter Windows und Linux gleichermaßen auf den logischen Bibliotheksnamen bezieht (qmake ergänzt automatisch die passenden Präfixe/Suffixe wie lib...so bzw. ...dll/...lib), sodass diese Zeile plattformübergreifend unverändert funktioniert.

Wichtig bei Verwendung des ld-Linkers unter Linux-GCC oder bei MinGW ist, dass die Reihenfolge der Bibliotheken in LIBS += ... korrekt sein muss. Bibliotheken, welche Symbole aus anderen Bibliotheken einbinden, müssen vor diesen aufgeführt werden.

Die Sitzungs-pro-Datei

Damit QtCreator (bzw. qmake direkt) beide Projekte, TestLib und TestApp, in einem Rutsch als zusammengehörige Sitzung öffnen und bauen kann, gibt es im Wurzelverzeichnis noch die SharedLibraryBuildSystemConfigs.pro:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Sitzungs-Datei, erstellt die Bibliothek und die darauf aufbauende Anwendung.

TEMPLATE = subdirs

SUBDIRS = \
	TestLib \
	TestApp

TestLib.file = libs/TestLib/TestLib.pro

# Der Pfad zur TestApp.pro-Datei muss nicht explizit angegeben werden, siehe
# Erklärung im Text.
TestApp.file = TestApp/TestApp.pro

# Die Anwendung verlinkt die Bibliothek, also erstelle diese als Abhängigkeit zuerst.
TestApp.depends = TestLib

TEMPLATE = subdirs markiert diese .pro-Datei als reines Sammelprojekt: Sie enthält selbst keinen Quelltext, sondern bündelt nur andere Projekte. SUBDIRS listet dazu die Kurznamen der Unterprojekte auf, TestLib.file und TestApp.file verweisen jeweils auf die zugehörige .pro-Datei relativ zum Wurzelverzeichnis. Dies ist immer dann notwendig, wenn die Unterprojekte (wie bei TestLib) nicht direkt unterhalb der Sitzungsdatei liegen, sondern in libs/TestLib. Bei TestApp kann das entfallen, da die .pro-Datei ja direkt in TestApp/TestApp.pro liegt.

Wichtig ist die letzte Zeile, TestApp.depends = TestLib: Da TestApp beim Linken auf die bereits gebaute TestLib-Bibliothek angewiesen ist, muss qmake sicherstellen, dass TestLib zuerst gebaut wird. Ohne diese explizite Abhängigkeit könnten – insbesondere bei parallelen Builds (make -j) – beide Teilprojekte gleichzeitig gebaut werden, und der Build von TestApp würde fehlschlagen, weil die Bibliothek zum Zeitpunkt des Linkens noch nicht existiert.

CMAKE

Analog zum qmake Buildsystem kann man das Projekt auch mit cmake konfigurieren. Die Struktur ist dabei ähnlich: für jedes Teilprojekt (TestLib und TestApp) gibt es je eine CMakeLists.txt-Datei, gemeinsame Definitionen liegen in einer Include-Datei build.cmake, und eine übergeordnete Sitzungsdatei CMakeLists.txt im Wurzelverzeichnis bindet beide Teilprojekte zusammen.

Ein wesentlicher konzeptioneller Unterschied zu build.pri fällt dabei sofort auf: Während build.pri in jeder einzelnen .pro-Datei per include(...) eingebunden werden musste, wird build.cmake bei cmake nur noch ein einziges Mal eingebunden – und zwar in der obersten Sitzungs-CMakeLists.txt im Wurzelverzeichnis, bevor die Unterprojekte per add_subdirectory() hinzugefügt werden. Weder libs/TestLib/CMakeLists.txt noch TestApp/CMakeLists.txt enthalten also noch ein include( .../build.cmake ).

Der Grund dafür liegt im Scoping-Modell von cmake: Variablen, die per set(...) gesetzt werden (wie hier CMAKE_CXX_STANDARD, die CMAKE_*_OUTPUT_DIRECTORY-Variablen oder CMAKE_AUTOMOC), gelten für den aktuellen Verzeichnisscope und werden automatisch an alle Verzeichnisse vererbt, die danach per add_subdirectory() hinzugefügt werden. Werden sie also im Wurzelverzeichnis gesetzt, bevor add_subdirectory(libs/TestLib) und add_subdirectory(TestApp) aufgerufen werden, gelten sie automatisch auch in diesen Unterverzeichnissen – ganz ohne dass die Unterprojekte selbst noch etwas einbinden müssten.

Bei qmake wird jede .pro-Datei eines Unterprojekts von einem eigenen, unabhängigen qmake-Lauf verarbeitet, weshalb build.pri dort in jeder Datei explizit eingebunden werden muss, damit die gemeinsamen Einstellungen überhaupt zur Wirkung kommen.

Die Sitzungs-CMakeLists.txt und die gemeinsame build.cmake

Die CMakeLists.txt im Wurzelverzeichnis übernimmt die Rolle von SharedLibraryBuildSystemConfigs.pro und ist zugleich die einzige Stelle, an der build.cmake eingebunden wird:

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
# Sitzungs-Projektdatei, erstellt die Bibliothek und die sie nutzende Anwendung.
#
# Konfigurieren und Bauen, ein Build-Verzeichnis pro Konfiguration:
#
#   cmake -S . -B build/Desktop-Debug -DCMAKE_BUILD_TYPE=Debug
#   cmake --build build/Desktop-Debug --parallel

cmake_minimum_required(VERSION 3.16)

project(SharedLibraryBuildSystemConfigs LANGUAGES CXX)

# -------------------------------------------------------------
# Einbindung der gemeinsamen cmake-Konfiguration
# -------------------------------------------------------------
include(libs/build.cmake)

# Qt wird hier im Grunde nur gesucht, um die Versionsnummer auszugeben.
# Die einzelnen Unterprojekte importieren die benötigten Module selbst.
find_package(Qt6 REQUIRED COMPONENTS Core)

message("*** Building with Qt6, Version ${Qt6Core_VERSION} ***")


# Die Anwendung linkt gegen die Bibliothek, daher wird die Bibliothek zuerst hinzugefügt;
# die tatsächliche Build-Reihenfolge wird durch target_link_libraries() in TestApp erzwungen
add_subdirectory(libs/TestLib)
add_subdirectory(TestApp)
  • cmake_minimum_required(VERSION 3.16) legt die minimal benötigte cmake-Version fest. Eine Entsprechung dazu gibt es bei qmake nicht.
  • project(SharedLibraryBuildSystemConfigs LANGUAGES CXX) deklariert den Namen der Sitzung und beschränkt die aktivierten Sprachen auf C++. Das entspricht in etwa dem impliziten Namen, den qmake aus dem Dateinamen der Sitzungs-.pro-Datei ableitet.
  • include(libs/build.cmake) bindet, wie oben beschrieben, die gemeinsame Konfiguration genau einmal ein – noch bevor die Unterprojekte hinzugefügt werden, damit diese die gesetzten Variablen übernehmen können.
  • find_package(Qt6 REQUIRED COMPONENTS Core) sucht hier ausschließlich das Core-Modul, und zwar nur, um im nachfolgenden message(...)-Aufruf die gefundene Qt6-Version auszugeben. Welche Qt-Module ein Teilprojekt tatsächlich zum Linken benötigt (hier z.B. Widgets), sucht sich jedes Teilprojekt – wie unten gezeigt – selbst über einen eigenen find_package()-Aufruf.
  • add_subdirectory(libs/TestLib) und add_subdirectory(TestApp) binden die beiden Unterprojekte ein. Ein Gegenstück zu qmakes SUBDIRS/TestLib.file/TestApp.file ist dafür nicht nötig: add_subdirectory() erwartet direkt den (relativen) Pfad zum Verzeichnis mit der jeweiligen CMakeLists.txt.
  • Eine Entsprechung zu TestApp.depends = TestLib fehlt bewusst: Die Build-Reihenfolge ergibt sich automatisch aus target_link_libraries(TestApp PRIVATE TestLib ...) in TestApp/CMakeLists.txt (siehe unten). Die Reihenfolge der beiden add_subdirectory()-Aufrufe dient daher nur der Lesbarkeit, nicht der Korrektheit.

Die libs/build.cmake ist das cmake-Gegenstück zu build.pri:

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
85
86
87
88
89
# Gemeinsame Build-Konfiguration, eingebunden von allen Bibliotheks- und Anwendungs-CMake-Dateien.
#
# Legt die gemeinsamen Ausgabeverzeichnisse fest und, für Anwendungen, den Suchpfad,
# der zum Linken gegen die in diesem Repository gebauten dynamischen Bibliotheken benötigt wird.
#
# Verwendung (in der obersten CMakeLists.txt):
#  include( libs/build.cmake )

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Bibliotheken werden im Unterverzeichnis 'lib' des Build-Verzeichnisses abgelegt, damit
# alle generierten Dateien einer Build-Konfiguration zusammenbleiben und der Quellcode-Baum
# sauber bleibt. Bei einem Build-Verzeichnis pro Konfiguration, z.B.
# build/Desktop-Debug, landen die Bibliotheken also in build/Desktop-Debug/lib.
# Auf diese Weise können unterschiedliche Build-Konfigurationen (debug/release)
# koexistieren, ohne sich gegenseitig die Bibliotheken zu überschreiben.
#
# Executables werden in bin/debug bzw. bin/release abgelegt.

# Wurzelverzeichnis des Repositorys; diese Datei liegt in <root>/libs, ihr eigenes
# Verzeichnis dient also als Anker - genau wie $$PWD in build.pri hängt das nicht
# davon ab, welche CMakeLists.txt diese Datei einbindet
get_filename_component(SOURCE_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)

# Verzeichnisse, die alle dynamischen Bibliotheken bzw. alle Executables dieser
# Sitzung aufnehmen; der Generator-Ausdruck wählt das Executable-Verzeichnis anhand
# der Konfiguration aus und unterdrückt dabei nebenbei das Konfigurations-Unterverzeichnis,
# das Multi-Config-Generatoren sonst anhängen würden
set(LIB_DIR "${CMAKE_BINARY_DIR}/lib")
set(BIN_DIR "${SOURCE_ROOT}/bin/$<IF:$<CONFIG:Debug>,debug,release>")

# ARCHIVE betrifft statische Bibliotheken und die Windows-Import-Bibliotheken, LIBRARY
# die dynamischen Bibliotheken unter Unix. RUNTIME betrifft die Executables und, unter
# Windows, auch die DLLs - was in build.pri DLLDESTDIR übernimmt.
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${LIB_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${LIB_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${BIN_DIR}")

# teilt dem dynamischen Linker mit, wo er die dynamischen Bibliotheken zur Laufzeit
# findet; so kann die Programmdatei gestartet werden, ohne dass das
# Bibliotheksverzeichnis zusätzlich in LD_LIBRARY_PATH eingetragen werden müsste
get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if (UNIX)
	if (IS_MULTI_CONFIG)
		# Multi-Config-Generatoren hängen den Konfigurationsnamen an das einfache
		# (generator-ausdrucksfreie) Bibliotheks-Ausgabeverzeichnis an
		set(CMAKE_BUILD_RPATH "${LIB_DIR}/$<CONFIG>")
	else()
		set(CMAKE_BUILD_RPATH "${LIB_DIR}")
	endif()
endif()


# bildet das qmake-Standardverhalten nach: ohne explizit angeforderte Konfiguration
# wird ein Release-Build erzeugt
if (NOT IS_MULTI_CONFIG AND NOT CMAKE_BUILD_TYPE)
	set(CMAKE_BUILD_TYPE Release CACHE STRING "Build configuration (Debug or Release)" FORCE)
endif()

if (CMAKE_BUILD_TYPE STREQUAL "Release")
	add_compile_definitions(QT_FORCE_ASSERTS)
endif()


# führt die Qt-Codegeneratoren automatisch für alle Targets aus, dies ersetzt die
# Behandlung, die qmake implizit für SOURCES/HEADERS/RESOURCES/FORMS übernimmt
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

# Aktiviert Position-Independent Code für dynamische Bibliotheken
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Visual-Studio-Compiler-spezifische Optionen
if (MSVC)
	# Fügt VC-Compiler-spezifische Flags hinzu:
	# - NOMINMAX-Define  -> verhindert Compile-Fehler bei Verwendung von std::min oder std::max
	# - /Zc:__cplusplus  -> lässt VC im Makro __cplusplus die korrekte C++-Version melden
	# - /wd4996          -> deaktiviert "unsichere" CRT-Warnungen
	# - /permissive-     -> deaktiviert Microsoft-C++-Erweiterungen (zusätzlich zu 'set(CMAKE_CXX_EXTENSIONS OFF)' nötig)
	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D\"NOMINMAX\" /wd4996 /Zc:__cplusplus /utf-8 /permissive-")
endif()

if (WIN32)
	# unter Windows wollen wir das lib-Präfix nicht
	set(CMAKE_SHARED_LIBRARY_PREFIX "")
endif()

Auch diese Datei Abschnitt für Abschnitt, insbesondere die Punkte, die sich seit der ersten Fassung geändert bzw. neu hinzugekommen sind:

  • Der include_guard(...) ist entfallen: Da build.cmake jetzt nur noch von genau einer Stelle (der Sitzungs-CMakeLists.txt) eingebunden wird, gibt es kein Mehrfach-Einbinden mehr, gegen das man sich absichern müsste.
  • CMAKE_CXX_STANDARD, CMAKE_CXX_STANDARD_REQUIRED, CMAKE_CXX_EXTENSIONS: das cmake-Gegenstück zu CONFIG += c++17. STANDARD_REQUIRED ON sorgt dafür, dass der Build hart fehlschlägt, wenn der Compiler C++17 nicht unterstützt. EXTENSIONS OFF deaktiviert compilerspezifische Erweiterungen zugunsten von striktem Standard-C++. Weil diese drei Variablen im Wurzelverzeichnis gesetzt werden, gelten sie automatisch für TestLib und TestApp gleichermaßen, ohne dass die beiden CMakeLists.txt-Dateien selbst noch etwas dafür tun müssten.
  • get_filename_component(SOURCE_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) entspricht $$PWD/.. bei qmake. CMAKE_CURRENT_LIST_DIR zeigt dabei immer auf das Verzeichnis der Datei, die gerade verarbeitet wird – hier build.cmake selbst, also libs/ –, unabhängig davon, aus welchem Verzeichnis heraus include(libs/build.cmake) aufgerufen wurde.
  • LIB_DIR = "${CMAKE_BINARY_DIR}/lib" ist das Gegenstück zu LIB_DIR = $$BUILD_ROOT/lib. CMAKE_BINARY_DIR ist dabei immer das oberste Build-Verzeichnis der gesamten Sitzung (das mit cmake -B ... angegebene Verzeichnis) – ein Gegenstück zu qmakes $$shadowed(...) wird also gar nicht erst benötigt.
  • BIN_DIR = "${SOURCE_ROOT}/bin/$<IF:$<CONFIG:Debug>,debug,release>" verwendet einen Generator-Ausdruck ($<...>), der erst beim Build (nicht schon bei der Konfiguration) ausgewertet wird – notwendig bei Multi-Config-Generatoren, wo die Konfiguration erst beim Aufruf von cmake --build ... --config Debug feststeht. Das Ergebnis entspricht der CONFIG( debug, debug|release ) { ... } else { ... }-Verzweigung aus build.pri.
  • CMAKE_ARCHIVE_OUTPUT_DIRECTORY, CMAKE_LIBRARY_OUTPUT_DIRECTORY, CMAKE_RUNTIME_OUTPUT_DIRECTORY: cmake unterscheidet, anders als qmakes einzelnes DESTDIR, nach Art der Ausgabedatei statt nach Projekttyp. ARCHIVE betrifft statische Bibliotheken und, unter Windows, die Import-Bibliotheken (.lib). LIBRARY betrifft die dynamischen Bibliotheken unter Unix (.so). RUNTIME betrifft Executables und, unter Windows, auch die DLLs selbst – deshalb landen unter Windows TestApp.exe und TestLib.dll durch das einmalige Setzen von CMAKE_RUNTIME_OUTPUT_DIRECTORY automatisch im gleichen Verzeichnis, was bei qmake erst DLLDESTDIR explizit leisten musste.
  • Der if (UNIX) ... endif()-Block setzt CMAKE_BUILD_RPATH, das cmake-Gegenstück zu QMAKE_RPATHDIR, und unterscheidet dabei je nach IS_MULTI_CONFIG, weil Multi-Config-Generatoren an LIB_DIR automatisch noch ein Unterverzeichnis je Konfiguration anhängen (lib/Debug, lib/Release).
  • Der Block mit IS_MULTI_CONFIG und CMAKE_BUILD_TYPE bildet qmakes Standardverhalten nach: Wird keine Konfiguration explizit angegeben, baut cmake von Haus aus ohne Optimierungen und ohne Debug-Informationen. Damit sich dieses Beispielprojekt in diesem Fall so verhält wie qmake ohne CONFIG += debug (nämlich einen Release-Build erzeugt), wird CMAKE_BUILD_TYPE explizit auf Release gesetzt, sofern kein Multi-Config-Generator verwendet wird.
  • if (CMAKE_BUILD_TYPE STREQUAL "Release") add_compile_definitions(QT_FORCE_ASSERTS) endif() ist neu hinzugekommen: Qt deaktiviert Q_ASSERT standardmäßig in Release-Builds (dort, wo NDEBUG gesetzt ist). Mit QT_FORCE_ASSERTS bleiben die Assertions auch im Release-Build aktiv – praktisch, um Fehler frühzeitig zu bemerken, statt sie nur im (seltener getesteten) Debug-Build zu finden.
  • CMAKE_AUTOMOC, CMAKE_AUTORCC, CMAKE_AUTOUIC: aktivieren die automatische Behandlung von Qt-spezifischen Quelldateien – Meta-Object-Compiler, Resource-Compiler und UI-Compiler. Bei qmake übernimmt das CONFIG += qt implizit; bei cmake muss man es explizit einschalten. Auch diese drei Variablen werden dank Vererbung nur einmal im Wurzelverzeichnis gesetzt.
  • set(CMAKE_POSITION_INDEPENDENT_CODE ON) ist ebenfalls neu: Sie sorgt dafür, dass sämtlicher Code – auch der einer eventuellen, später hinzukommenden statischen Zwischenbibliothek – mit -fPIC übersetzt wird. Das ist Voraussetzung dafür, dass ein Objekt anschließend überhaupt in eine dynamische Bibliothek eingebunden werden kann; qmake setzt das für TEMPLATE = lib automatisch, bei cmake muss man das explizit sagen.
  • Der if (MSVC) ... endif()-Block ergänzt ein paar Compiler-Flags, die unter Visual Studio in der Praxis sehr hilfreich sind: NOMINMAX verhindert Compile-Fehler durch die Makros min/max aus <windows.h>, wenn man std::min/std::max verwendet; /wd4996 unterdrückt MSVCs Warnungen zu “unsicheren” CRT-Funktionen; /Zc:__cplusplus sorgt dafür, dass das Makro __cplusplus den tatsächlich verwendeten C++-Standard widerspiegelt (MSVC meldet ohne dieses Flag traditionell immer 199711L, egal welcher Standard eingestellt ist); /permissive- schaltet MSVC-spezifische Sprach-Erweiterungen ab und ergänzt so CMAKE_CXX_EXTENSIONS OFF, das bei MSVC allein nicht greift.
  • if (WIN32) set(CMAKE_SHARED_LIBRARY_PREFIX "") endif() ist die dritte Ergänzung: Ohne dieses Flag würde cmake unter Windows standardmäßig ein lib-Präfix voranstellen (libTestLib.dll), analog zu Unix-Konventionen. qmake tut das unter Windows nicht, daher wird das Präfix hier für ein einheitliches Ergebnis explizit entfernt.

Bei einem Single-Config-Generator (Unix Makefiles, Ninja) entspricht ein Build-Verzeichnis, analog zum qmake-Shadow-Build, genau einer festen Konfiguration, die bei cmake -S . -B build/Desktop-Debug -DCMAKE_BUILD_TYPE=Debug gewählt wird. Bei einem Multi-Config-Generator (Visual Studio, Ninja Multi-Config) verwaltet dagegen ein einziges Build-Verzeichnis alle Konfigurationen gleichzeitig, und die Konfiguration wird erst beim Aufruf von cmake --build build --config Debug festgelegt. build.cmake behandelt beide Fälle einheitlich, das Beispielprojekt selbst geht aber, analog zu qmake, von einem Build-Verzeichnis pro Konfiguration aus.

Die TestLib Bibliothek

Der Quelltext von TestLib ist identisch zum qmake-Beispiel oben, insbesondere die Export-/Import-Logik über TestLib_global.h und das Define TESTLIB_LIBRARY. Was sich unterscheidet, ist ausschließlich die Buildsystem-Konfiguration.

Die libs/TestLib/CMakeLists.txt sieht wie folgt aus:

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
project( TestLib )

file( GLOB SRCS src/*.cpp )
file( GLOB MOC_HDRS src/*.h )

add_library(${PROJECT_NAME} SHARED
	${SRCS}
	${MOC_HDRS}
	resources/TestLib.qrc
)

# gleiches Versionierungsschema, das qmake standardmäßig anwendet -> libTestLib.so.1.0.0
set_target_properties(${PROJECT_NAME} PROPERTIES
	VERSION 1.0.0
	SOVERSION 1
)

# Setzt das Define zum Bauen der Bibliothek (nur in der Projektdatei der Bibliothek selbst).
# PRIVATE ist hier essenziell: Targets, die gegen die Bibliothek linken, dürfen dieses
# Define nicht sehen, damit sie die Import- statt die Export-Deklaration erhalten.
target_compile_definitions(${PROJECT_NAME} PRIVATE TESTLIB_LIBRARY)

# öffentliche Header der Bibliothek, propagiert an alles, was dagegen linkt,
# daher PUBLIC
target_include_directories(${PROJECT_NAME} PUBLIC 
	${CMAKE_CURRENT_LIST_DIR}/src
)

# Qt-Modul Widgets wird hier gesucht
find_package(Qt6 REQUIRED COMPONENTS Widgets)

# Das Bibliotheks-Target linkt gegen dieses Modul (das Gegenstück zu
# 'QT += widgets' in den .pro-Dateien) und propagiert diese Abhängigkeit an
# die Nutzer der Bibliothek. Die Bibliothek verwendet die Qt-Typen in ihren
# öffentlichen Headern, daher PUBLIC.
target_link_libraries(${PROJECT_NAME} PUBLIC 
	Qt6::Widgets
)

Im Vergleich zur ersten Fassung fallen zwei Dinge auf:

  • Ein include( .../build.cmake ) am Anfang ist nicht notwendig, denn wie oben erklärt, sind CMAKE_CXX_STANDARD, die Ausgabeverzeichnisse, CMAKE_AUTOMOC etc. bereits gesetzt, wenn add_subdirectory(libs/TestLib) diese Datei verarbeitet (weil sie aus der Session-CMakeLists.txt- vererbt werden).
  • Die Datei beginnt jetzt mit project( TestLib ), und der Target-Name wird überall als ${PROJECT_NAME} referenziert statt als wörtliches TestLib. Das ist keine funktionale Notwendigkeit, sondern eine kleine Vereinfachung: Der Projekt-/Bibliotheksname steht dadurch nur noch an einer einzigen Stelle in der Datei, was Tippfehler bei künftigen Umbenennungen vermeidet.

Alles Weitere ist inhaltlich unverändert gegenüber der ersten Fassung:

  • file( GLOB SRCS src/*.cpp ) und file( GLOB MOC_HDRS src/*.h ) sammeln die Quell- und Headerdateien. Im Gegensatz zu qmakes SOURCES += \ ...-Listen wird hier mit GLOB gearbeitet, sodass neue Dateien nicht manuell eingetragen werden müssen – nach dem Hinzufügen einer neuen Datei muss dafür aber cmake neu konfiguriert werden, da GLOB das Verzeichnis nur zum Konfigurationszeitpunkt einliest.
  • add_library(${PROJECT_NAME} SHARED ...) erzeugt das eigentliche Bibliotheks-Target. Die .qrc-Datei wird direkt als Quelle angegeben; da CMAKE_AUTORCC aktiviert ist, erkennt cmake automatisch, dass daraus generierter Code erzeugt und mit übersetzt werden muss.
  • set_target_properties(... VERSION 1.0.0 SOVERSION 1) sorgt für die gleiche Versionierung wie im qmake-Build (libTestLib.so.1.0.0 mit Symlink libTestLib.so.1 und libTestLib.so).
  • target_compile_definitions(${PROJECT_NAME} PRIVATE TESTLIB_LIBRARY) setzt das Export-Define, PRIVATE, damit es nicht an Targets weitergereicht wird, die gegen TestLib linken – analog zu DEFINES += TESTLIB_LIBRARY, das ausschließlich in TestLib.pro steht.
  • target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src) macht das src-Verzeichnis nicht nur für TestLib selbst, sondern dank PUBLIC auch für jedes Target verfügbar, das später gegen TestLib linkt. Das ist ein wesentlicher konzeptioneller Unterschied zu qmake: Dort musste TestApp.pro den Include-Pfad noch von Hand über INCLUDEPATH += ../libs/TestLib/src ergänzen; bei cmake “reist” diese Anforderung automatisch mit dem Target mit.
  • find_package(Qt6 REQUIRED COMPONENTS Widgets) sucht die für dieses Teilprojekt benötigten Qt-Module selbst, statt sich auf einen zentralen find_package()-Aufruf in der Sitzungs-CMakeLists.txt zu verlassen. Das macht TestLib/CMakeLists.txt in sich geschlossen: Wer wissen will, wovon die Bibliothek abhängt, muss nicht erst in der Sitzungs-Datei nachsehen.
  • target_link_libraries(${PROJECT_NAME} PUBLIC Qt6::Widgets) verlinkt gegen Qt6::Widgets, wieder PUBLIC, weil LEDLabel.h selbst Qt-Widgets-Typen wie QWidget verwendet.

PUBLIC, PRIVATE und INTERFACE sind der Kern des modernen, targetbasierten cmake: Jede Eigenschaft eines Targets (Include-Pfade, Defines, verlinkte Bibliotheken, Compile-Optionen) wird als PRIVATE (nur für dieses Target selbst), PUBLIC (für dieses Target und alle, die dagegen linken) oder INTERFACE (nur für die, die dagegen linken, nicht für das Target selbst) deklariert. Dadurch propagieren sich Anforderungen automatisch durch den Abhängigkeitsgraphen, ohne dass jedes abhängige Projekt sie erneut angeben müsste.

Das TestApp Programm

Die TestApp/CMakeLists.txt sieht wie folgt aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
project( TestApp )

file( GLOB SRCS src/*.cpp )
file( GLOB MOC_HDRS src/*.h )

add_executable(${PROJECT_NAME}
	${SRCS}
	${MOC_HDRS}
)

# Qt-Modul Widgets wird hier gesucht
find_package(Qt6 REQUIRED COMPONENTS Widgets)

# Linkt gegen die dynamische Bibliothek. Anders als im qmake-Projekt müssen hier
# weder Include-Pfad noch Bibliotheks-Suchpfad gesetzt werden - beide werden vom
# TestLib-Target propagiert, das cmake außerdem dazu veranlasst, die Bibliothek zuerst zu bauen.
target_link_libraries(${PROJECT_NAME} PRIVATE
	TestLib
	Qt6::Widgets
)

Auch hier das gleiche Bild wie bei TestLib: kein include(...) mehr am Anfang, dafür project( TestApp ) und ${PROJECT_NAME} statt des wörtlichen Namens, sowie ein eigener find_package(Qt6 REQUIRED COMPONENTS Widgets)-Aufruf, obwohl TestLib das gleiche Modul bereits gesucht hat – find_package() ist dahingehend robust, dass ein bereits gefundenes Paket bei einem erneuten Aufruf mit passenden Anforderungen nicht neu gesucht, sondern das Ergebnis einfach wiederverwendet wird.

Weil add_executable (statt add_library) verwendet wird, greift beim Bauen automatisch CMAKE_RUNTIME_OUTPUT_DIRECTORY = BIN_DIR aus build.cmake (vererbt aus dem Wurzelverzeichnis) – TestApp landet also im selben bin/debug- bzw. bin/release-Verzeichnis wie schon bei qmake.

Der auffälligste Unterschied zu TestApp.pro ist, wie schlank target_link_libraries(${PROJECT_NAME} PRIVATE TestLib Qt6::Widgets) ausfällt: Es fehlt sowohl ein Gegenstück zu INCLUDEPATH += ../libs/TestLib/src als auch zu LIBS += -L$$LIB_DIR -lTestLib. Das liegt an dem oben erwähnten PUBLIC bei target_include_directories() und target_link_libraries() in TestLib/CMakeLists.txt: Weil TestLib seinen Include-Pfad und seine eigene Qt-Abhängigkeit als PUBLIC deklariert hat, propagieren sich beide automatisch an jedes Target, das per target_link_libraries() gegen TestLib linkt. PRIVATE bei TestApp bedeutet dabei nur, dass TestApp selbst kein Ziel ist, gegen das wiederum andere Targets linken könnten.

Auch die Build-Reihenfolge muss hier, anders als bei der Sitzungs-.pro-Datei, nirgends explizit angegeben werden: Da TestApp per target_link_libraries() von TestLib abhängt, ermittelt cmake diese Abhängigkeit automatisch aus dem Abhängigkeitsgraphen der Targets und baut TestLib in jedem Fall vor TestApp.

Die LIBS += ...-Reihenfolge-Falle aus dem qmake-Abschnitt (Bibliotheken, die Symbole aus anderen Bibliotheken benötigen, müssen vor diesen aufgeführt werden) betrifft target_link_libraries() bei cmake nicht in gleicher Schärfe: cmake reicht die vollständige, transitiv aufgelöste Abhängigkeitsliste in der richtigen Reihenfolge an den Linker durch, unabhängig von der Reihenfolge der Argumente im target_link_libraries()-Aufruf selbst.

Insgesamt zeigt sich am Vergleich der beiden Buildsysteme der grundsätzliche Unterschied: qmake arbeitet variablenbasiert und je Projektdatei unabhängig (Einstellungen wie INCLUDEPATH oder LIBS gelten nur für die aktuell verarbeitete .pro-Datei und müssen über include(...) in jeder Datei erneut eingebunden bzw. bei projektspezifischen Abhängigkeiten manuell ergänzt werden), während cmake sowohl verzeichnisbasiert vererbte Variablen (für sitzungsweite Einstellungen wie Ausgabepfade oder den Sprachstandard – einmal im Wurzelverzeichnis gesetzt, überall darunter gültig) als auch targetbasierte Propagation über PUBLIC/PRIVATE/INTERFACE (für projektspezifische Abhängigkeiten entlang des Targets-Graphen) kennt. Für dieses kleine Beispielprojekt mit nur einer Bibliothek und einer Anwendung macht sich der Unterschied nur in wenigen Zeilen bemerkbar – bei wachsender Anzahl an Bibliotheken reduziert das cmake-Modell den Pflegeaufwand aber deutlich, weil gemeinsame Einstellungen nur noch an einer einzigen Stelle stehen.

Das kleine Testprogramm mit den beiden Buildsystemdateien kann als komplettes Beispiel heruntergeladen werden:

APPENDIX: Statisch gelinkte Qt-basierte Bibliotheken mit Resourcen

Man kann Qt-basierte Bibliotheken natürlich auch statisch linken, wenn man z.B. verhindern möchte, dass die DLL durch eine andere ausgetauscht wird oder es durch Versionsunterschiede Probleme gibt. Dabei gibt es häufig aber Probleme mit fehlenden Ressourcen. Dazu eine kleine Erklärung.

Das Hauptproblem mit eingebetteten Resourcen

Normalerweise lädt Qt alle Ressourcen einer Anwendung beim Programmstart vollautomatisch. Dies geschieht über den Konstruktor einer internen, globalen Variable.

Wenn man Ressourcen jedoch in eine statische Bibliothek (.lib oder .a) auslagert und diese mit deiner Hauptanwendung verknüpft, passiert Folgendes:

  • Linker-Optimierung: Der C++ Linker optimiert das finale Programm.
  • Fehlender Zugriff: Da im Quellcode scheinbar niemand direkt auf diese globalen Variablen der Bibliothek zugreift, hält der Linker sie für ungenutzt.
  • Ressourcenverlust: Er wirft sie heraus, und die Ressourcen fehlen zur Laufzeit (Bilder, Icons oder Config-Dateien werden nicht angezeigt).

Die Lösung: Das C++-Makro Q_INIT_RESOURCE(name) wird im Qt-Framework verwendet, um eingebettete Ressourcen (wie Bilder, Icons oder Übersetzungen) aus einer .qrc-Ressourcendatei manuell zu initialisieren und zu laden. Durch den Aufruf von Q_INIT_RESOURCE(name) zwingst du den Linker dazu, die Ressourcen in die finale ausführbare Datei einzubinden und im Qt-Ressourcensystem zu registrieren.

Wichtige Regeln zur Verwendung

  • Der Übergabeparameter: Als name übergibt man den Basisnamen der .qrc-Datei ohne Dateiendung (Beispiel: Für icons.qrc nutzt du Q_INIT_RESOURCE(icons);).
  • Keine Namespaces: Das Makro darf nicht innerhalb eines C++-Namespaces aufgerufen werden, da die generierten Initialisierer im globalen Namespace liegen. Falls man sich in einem Namespace befindest, musst der Aufruf außerhalb platziert werden.
  • Der beste Ort: Es wird üblicherweise direkt am Anfang der main()-Funktion der Hauptanwendung oder im Initialisierungsprozess der Bibliothek aufgerufen.

Code-Beispiel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <QApplication>
#include <QFile>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    // Zwingt das Laden der Ressourcen aus 'mein_archiv.qrc'
    Q_INIT_RESOURCE(mein_archiv); 

    // Jetzt ist der Pfad sicher verfügbar
    QFile file(":/images/logo.png"); 

    return app.exec();
}

Falls man Ressourcen dynamisch zur Laufzeit komplett aus dem Speicher entfernen möchte (z. B. beim Entladen eines Plugins), gibt es das Gegenstück namens Q_CLEANUP_RESOURCE(name).

This post is licensed under CC BY 4.0 by the author.