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-Debugo.ä. ablegt. Bei cmake wird ebenfalls in einem separaten Erstellungsverzeichnis gebaut - die Binaries/Executables und die DLLs (unter Windows) sollen top-level unter
/bin/debugoder/bin/releaseabgelegt 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 beiTestLibdie DateienLEDLabel.h/.cppundTestLib_global.h, beiTestAppentsprechendmain.cppundMainWindow.h/.cpp.resources/enthält alle Ressourcen, die über das Qt-Resource-System eingebunden werden – beiTestLibalso das Verzeichnisgfxmit den PNG-Grafiken sowie die zugehörigeTestLib.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 eineCMakeLists.txtfü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 alsoTestLib->TestLib.proundTestApp->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_LIBRARYgesetzt sein, damitTESTLIB_EXPORTzuQ_DECL_EXPORTwird und die Klasse exportiert wird. InTestLib.progeschieht das über:1
DEFINES += TESTLIB_LIBRARY
- Im Anwendungsprojekt (
TestApp), das die Bibliothek nur benutzt, wirdTESTLIB_LIBRARYnicht gesetzt. Dort expandiertTESTLIB_EXPORTalso zuQ_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 jeglichedllexport-Markierung automatisch alle Symbole exportieren. Sobald aber – wie hier – auch nur ein Symbol explizit mitdllexportmarkiert 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++17legt 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.$$PWDist dabei stets das Verzeichnis, in dem die gerade verarbeitete.pro/.pri-Datei liegt – hier alsolibs/.$$PWD/..zeigt somit auf das Wurzelverzeichnis der Sitzung, und$$shadowed(...)liefert dazu den passenden Pfad im aktiven Shadow-Build-Verzeichnis, egal ob geradebuild/Desktop-Debug,build/Desktop-Releaseoder ein anders benanntes Verzeichnis aktiv ist.BUILD_ROOTzeigt also immer auf das Shadow-Build-Verzeichnis, das dem Wurzelverzeichnis der Sitzung entspricht. -
LIB_DIR = $$BUILD_ROOT/lib: Alle Bibliotheken landen gemeinsam in einemlib-Unterverzeichnis des Shadow-Build-Verzeichnisses. DaBUILD_ROOTbereits pro Build-Konfiguration unterschiedlich ist (Debug und Release liegen ja in unterschiedlichen Shadow-Build-Verzeichnissen), ist auchLIB_DIRautomatisch pro Konfiguration getrennt. -
BIN_DIR: Im Gegensatz zuLIB_DIRliegt dieses Verzeichnis nicht im Shadow-Build-Verzeichnis, sondern fest unterhalb des Quellverzeichnisses ($$PWD/../bin/debugbzw..../bin/release), unabhängig davon, mit welcher Toolchain oder welchem Shadow-Build-Verzeichnis gerade gebaut wird. Die BedingungCONFIG( debug, debug|release )unterscheidet zwischen Debug- und Release-Build. Das entspricht genau der eingangs formulierten Zielsetzung: Alle Executables und DLLs landen gebündelt inbin/debugbzw.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 vonTestLibundTestAppkollidieren also nicht, obwohl beide dieselben Variablennamenobj,mocusw. verwenden. -
Der Block
equals( TEMPLATE, lib ) { ... } else:equals( TEMPLATE, app ) { ... }unterscheidet anhand des Werts vonTEMPLATE, ob die aktuell verarbeitete.pro-Datei eine Bibliothek oder eine Anwendung beschreibt, und setzt je nachdem unterschiedliche Variablen:- Für Bibliotheken (
TEMPLATE = lib) wirdDESTDIRaufLIB_DIRgesetzt – dorthin baut qmake also die eigentliche.so/.dll/.dylib. Unter Windows wird zusätzlichDLLDESTDIRaufBIN_DIRgesetzt: qmake kopiert die erzeugte DLL dann automatisch zusätzlich in dieses Verzeichnis, sodass sie dort neben der.exeliegt und zur Laufzeit gefunden wird. - Für Anwendungen (
TEMPLATE = app) wirdDESTDIRaufBIN_DIRgesetzt – die Programmdatei landet also direkt im gemeinsamenbin/debug- bzw.bin/release-Verzeichnis. Außerdem wirdLIBS += -L$$LIB_DIRergänzt, damit der Linker beim Bauen der Anwendung weiß, wo er nach den Bibliotheken suchen soll. Unter Unix-artigen Systemen wird zusätzlichQMAKE_RPATHDIR += $$LIB_DIRgesetzt: Damit wird der Pfad zuLIB_DIRfest als RPATH in die erzeugte Programmdatei eingetragen, sodass der dynamische Linker die.so-Datei zur Laufzeit findet, ohne dassLD_LIBRARY_PATHgesetzt werden müsste.
- Für Bibliotheken (
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/srcfügt das Quellverzeichnis vonTestLibzum Include-Suchpfad hinzu (relativ zum Verzeichnis derTestApp.pro), damitMainWindow.cppdie HeaderdateiLEDLabel.hper#include <LEDLabel.h>finden kann, ohne einen langen relativen Pfad angeben zu müssen.LIBS += -lTestLibweist den Linker an, gegenTestLibzu linken. Den Suchpfad dafür (-L$$LIB_DIR) hat bereitsbuild.prigesetzt – hier muss also nur noch der Bibliotheksname selbst angegeben werden. Zu beachten ist, dass sich-lTestLibunter Windows und Linux gleichermaßen auf den logischen Bibliotheksnamen bezieht (qmake ergänzt automatisch die passenden Präfixe/Suffixe wielib...sobzw....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 inLIBS += ...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 werden in einer cmake-Include-Datei abgelegt build.cmake und es gibt eine übergeordnete Sitzungsdatei CMakeLists.txt im Wurzelverzeichnis.
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.
CMakeLists.txt und gemeinschaftlich genutzte build.cmake
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
# Allgemeine Einstellungen, legt die Ausgabeverzeichnisse fest
include( ${CMAKE_CURRENT_LIST_DIR}/../build.cmake )
file( GLOB SRCS src/*.cpp )
file( GLOB MOC_HDRS src/*.h )
add_library(TestLib SHARED
${SRCS}
${MOC_HDRS}
resources/TestLib.qrc
)
# gleiches Versionierungsschema, das qmake standardmäßig anwendet -> libTestLib.so.1.0.0
set_target_properties(TestLib PROPERTIES
VERSION 1.0.0
SOVERSION 1
)
# Setze das Define um die Bibliothek zu erstellen (nur in der eigenen Projektdatei
# der Bibliothek). PRIVATE ist hier essentiell: Targets, die gegen die Bibliothek
# linken, dürfen dieses Define nicht sehen, damit sie die Import- statt der
# Export-Deklaration erhalten.
target_compile_definitions(TestLib PRIVATE TESTLIB_LIBRARY)
# öffentliche Header der Bibliothek, propagiert an alles, was dagegen linkt
target_include_directories(TestLib PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src)
# die Bibliothek exponiert Qt-Typen in ihren öffentlichen Headern, daher PUBLIC
target_link_libraries(TestLib PUBLIC Qt6::Widgets)
Auch hier gilt das gleiche Prinzip wie bei TestLib.pro: Ganz oben wird mit include( ${CMAKE_CURRENT_LIST_DIR}/../build.cmake ) eine gemeinsam genutzte Datei eingebunden, die die Ausgabepfade und weitere sitzungsweite Einstellungen festlegt. ${CMAKE_CURRENT_LIST_DIR} ist dabei das cmake-Gegenstück zu $$PWD bei qmake – es zeigt immer auf das Verzeichnis der gerade verarbeiteten CMakeLists.txt-Datei, unabhängig davon, von wo aus sie über add_subdirectory() eingebunden wurde.
Alles Projektspezifische steht wieder direkt in der CMakeLists.txt der Bibliothek:
file( GLOB SRCS src/*.cpp )undfile( GLOB MOC_HDRS src/*.h )sammeln die Quell- und Headerdateien. Im Gegensatz zu qmakesSOURCES += \ ...-Listen wird hier mitGLOBgearbeitet, sodass neue Dateien nicht manuell eingetragen werden müssen. Der Nachteil: Nach dem Hinzufügen einer neuen Datei muss cmake neu konfiguriert werden (cmake -S . -B build/...), daGLOBdas Verzeichnis nur zum Konfigurationszeitpunkt einliest und Änderungen am Dateibestand sonst nicht bemerkt. Für ein wachsendes Projekt ist das ein bewusster Kompromiss aus Bequemlichkeit und Robustheit – alternativ könnte man die Dateien wie bei qmake explizit auflisten.add_library(TestLib SHARED ...)erzeugt das eigentliche Bibliotheks-Target. Die.qrc-Datei wird direkt mit als Quelle angegeben; da weiter untenCMAKE_AUTORCCaktiviert ist (siehebuild.cmake), erkennt cmake automatisch, dass daraus generierter Code erzeugt und mit übersetzt werden muss – analog dazu, wie qmakeRESOURCES += ...implizit behandelt.set_target_properties(... VERSION 1.0.0 SOVERSION 1)sorgt dafür, dass die erzeugte Bibliothek unter Linux mit der gleichen Versionierung wie im qmake-Build entsteht (libTestLib.so.1.0.0mit SymlinklibTestLib.so.1undlibTestLib.so). Ohne diese Angabe würde cmake keine Versionssuffixe anhängen.target_compile_definitions(TestLib PRIVATE TESTLIB_LIBRARY)setzt das Export-Define. Entscheidend ist hierPRIVATE: Das Define gilt nur beim Übersetzen vonTestLibselbst, wird aber nicht an Targets weitergereicht, die später gegenTestLiblinken. Das entspricht genau dem Verhalten von qmake, woDEFINES += TESTLIB_LIBRARYebenfalls nur inTestLib.prosteht.target_include_directories(TestLib PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src)macht dassrc-Verzeichnis der Bibliothek als Include-Pfad verfügbar – und zwar nicht nur fürTestLibselbst, sondern dankPUBLICauch für jedes Target, das später pertarget_link_libraries()gegenTestLiblinkt. Das ist ein wesentlicher konzeptioneller Unterschied zu qmake: Dort mussteTestApp.proden Include-Pfad zur Bibliothek noch von Hand überINCLUDEPATH += ../libs/TestLib/srcergänzen. Bei cmake genügt es, diese Anforderung einmal am Bibliotheks-Target selbst zu formulieren – sie “reist” dann automatisch mit dem Target mit, siehe Abschnitt zuTestAppunten.target_link_libraries(TestLib PUBLIC Qt6::Widgets)verlinkt gegen dieQt6::Widgets-Bibliothek. Auch hier wiederPUBLIC, weilLEDLabel.h(ein öffentlicher Header vonTestLib) selbst Qt-Widgets-Typen wieQWidgetverwendet – werTestLib.heinbindet, braucht also ebenfalls die Include-Pfade und (beim Linken) die Bibliothek vonQt6::Widgets. Wäre Qt nur intern in der.cpp-Datei verwendet worden, hättePRIVATEgenügt.
PUBLIC,PRIVATEundINTERFACEsind der Kern des modernen, targetbasierten cmake: Jede Eigenschaft eines Targets (Include-Pfade, Defines, verlinkte Bibliotheken, Compile-Optionen) wird alsPRIVATE(nur für dieses Target selbst),PUBLIC(für dieses Target und alle, die dagegen linken) oderINTERFACE(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.
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
# Gemeinsame Build-Konfiguration, die von allen Bibliotheks- und
# Anwendungs-CMake-Dateien 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.
# Dies ist das cmake-Gegenstück zu build.pri und folgt den gleichen Konventionen.
#
# Verwendung (in einer CMakeLists.txt):
# include( ${CMAKE_CURRENT_LIST_DIR}/<relativer Pfad>/libs/build.cmake )
# Die unten gesetzten Ausgabeverzeichnis-Variablen haben Verzeichnisscope und
# werden gelesen, wenn ein Target angelegt wird - deshalb muss diese Datei von
# jeder CMakeLists.txt eingebunden werden, die ein Target erzeugt. Der Guard
# gilt daher pro Verzeichnis, nicht global.
include_guard(DIRECTORY)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# führe 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)
# Wurzelverzeichnis des Repositories; diese Datei liegt in <Wurzel>/libs, ihr
# eigenes Verzeichnis ist also der Ankerpunkt - genau wie $$PWD in build.pri
# hängt dies nicht davon ab, welche CMakeLists.txt diese Datei einbindet
get_filename_component(SOURCE_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
# 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 zugleich 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-Importbibliotheken,
# LIBRARY die dynamischen Bibliotheken unter unix. RUNTIME betrifft die
# Executables und, unter Windows, auch die DLLs - was DLLDESTDIR in build.pri erreicht.
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
if (UNIX)
if (IS_MULTI_CONFIG)
# Multi-Config-Generatoren hängen den Konfigurationsnamen an das
# einfache (generator-ausdrucksfreie) Bibliotheksausgabeverzeichnis an
set(CMAKE_BUILD_RPATH "${LIB_DIR}/$<CONFIG>")
else()
set(CMAKE_BUILD_RPATH "${LIB_DIR}")
endif()
endif()
Auch diese Datei Abschnitt für Abschnitt:
include_guard(DIRECTORY)verhindert, dass die Datei innerhalb desselben Verzeichnisscopes mehrfach abgearbeitet wird. Wichtig ist dabeiDIRECTORYstatt des Standardverhaltens (global): Die hier gesetztenCMAKE_*_OUTPUT_DIRECTORY-Variablen wirken nur im Verzeichnisscope, in dem sie gesetzt wurden (und dessen Unterverzeichnissen) – deshalb mussbuild.cmakevon jederCMakeLists.txt, die ein Target anlegt, erneut eingebunden werden, ein globaler Guard würde das zweite und jedes weitereinclude()fälschlich unterdrücken.CMAKE_CXX_STANDARD,CMAKE_CXX_STANDARD_REQUIRED,CMAKE_CXX_EXTENSIONS: das cmake-Gegenstück zuCONFIG += c++17.STANDARD_REQUIRED ONsorgt dafür, dass der Build hart fehlschlägt, wenn der Compiler C++17 nicht unterstützt, statt stillschweigend auf einen älteren Standard zurückzufallen.EXTENSIONS OFFdeaktiviert compilerspezifische Erweiterungen (z.B. GNU-Extensions bei GCC) zugunsten von striktem Standard-C++.CMAKE_AUTOMOC,CMAKE_AUTORCC,CMAKE_AUTOUIC: Diese drei Schalter aktivieren die automatische Behandlung von Qt-spezifischen Quelldateien – der Meta-Object-Compiler für Klassen mitQ_OBJECT, der Resource-Compiler für.qrc-Dateien und der UI-Compiler für.ui-Dateien. Bei qmake übernimmt dasCONFIG += qtimplizit; bei cmake muss man es explizit einschalten. Da diese drei Variablen ebenfalls Verzeichnisscope haben, gehören sie folgerichtig in dieselbe gemeinsame Datei wie die Ausgabepfade.get_filename_component(SOURCE_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)entspricht$$PWD/..bei qmake und liefert das Wurzelverzeichnis der Sitzung als absoluten Pfad im Quellbaum.get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)fragt ab, ob der aktuell verwendete Generator ein Multi-Config-Generator ist (z.B. Visual Studio oder Ninja Multi-Config, die Debug und Release im selben Build-Verzeichnis verwalten) oder ein Single-Config-Generator (z.B. Unix Makefiles oder das einfache Ninja, bei denenCMAKE_BUILD_TYPEpro Build-Verzeichnis fest gewählt wird, so wie es die Sitzungs-Datei unten mit je einem Verzeichnis pro Konfiguration voraussetzt). Diese Unterscheidung hat bei qmake keine Entsprechung, da qmake grundsätzlich pro Shadow-Build-Verzeichnis genau eine Konfiguration kennt.LIB_DIR = "${CMAKE_BINARY_DIR}/lib"ist das Gegenstück zuLIB_DIR = $$BUILD_ROOT/lib.CMAKE_BINARY_DIRist dabei immer das oberste Build-Verzeichnis der gesamten Sitzung (das mitcmake -B ...angegebene Verzeichnis), unabhängig davon, aus welchem Unterverzeichnis herausbuild.cmakegerade eingebunden wird – es gibt bei cmake also gar kein Gegenstück zu$$shadowed(...)nötig, daCMAKE_BINARY_DIRdiese Rolle von Haus aus übernimmt.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. Das ist bei Multi-Config-Generatoren notwendig, weil dort die Konfiguration erst beim Aufruf voncmake --build ... --config Debugfeststeht.$<IF:$<CONFIG:Debug>,debug,release>wählt je nach aktiver Konfigurationdebugoderrelease– das Ergebnis entspricht exakt derCONFIG( debug, debug|release ) { ... } else { ... }-Verzweigung ausbuild.pri.CMAKE_ARCHIVE_OUTPUT_DIRECTORY,CMAKE_LIBRARY_OUTPUT_DIRECTORY,CMAKE_RUNTIME_OUTPUT_DIRECTORY: cmake unterscheidet, anders als qmakes einzelnesDESTDIR, nach Art der Ausgabedatei statt nach Projekttyp.ARCHIVEbetrifft statische Bibliotheken und, unter Windows, die Import-Bibliotheken (.lib), die zusammen mit einer DLL entstehen.LIBRARYbetrifft die dynamischen Bibliotheken unter Unix (.so).RUNTIMEbetrifft Executables und, unter Windows, auch die DLLs selbst. Genau diese letzte Eigenart machtDLLDESTDIRausbuild.priüberflüssig: Da unter Windows sowohlTestApp.exeals auchTestLib.dllalsRUNTIME-Artefakt gelten, landen beide durch das einmalige Setzen vonCMAKE_RUNTIME_OUTPUT_DIRECTORY = BIN_DIRautomatisch im gleichen Verzeichnis. Unter Linux ist das irrelevant, da dort ohnehin RPATH zum Auffinden der.so-Datei verwendet wird.- Der
if (UNIX) ... endif()-Block setztCMAKE_BUILD_RPATH, das cmake-Gegenstück zuQMAKE_RPATHDIR. Er trägtLIB_DIRals RPATH in die beim Build erzeugten Binaries ein, damit diese die Bibliothek zur Laufzeit finden, ohneLD_LIBRARY_PATHsetzen zu müssen. Die Fallunterscheidung nachIS_MULTI_CONFIGist nötig, weil Multi-Config-Generatoren anLIB_DIRautomatisch noch ein Unterverzeichnis je Konfiguration anhängen (lib/Debug,lib/Release) – etwas, das qmake dank separater Shadow-Build-Verzeichnisse pro Konfiguration gar nicht erst kennt.
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=Debuggewä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 voncmake --build build --config Debugfestgelegt.build.cmakebehandelt beide Fälle einheitlich, das Beispielprojekt selbst geht aber, analog zu qmake, von einem Build-Verzeichnis pro Konfiguration aus.
Da TestLib.CMakeLists.txt mit include( .../build.cmake ) beginnt, greifen CMAKE_LIBRARY_OUTPUT_DIRECTORY und CMAKE_RUNTIME_OUTPUT_DIRECTORY automatisch, sobald add_library(TestLib SHARED ...) das Target anlegt – ganz ohne dass in TestLib/CMakeLists.txt selbst ein Wort über Ausgabepfade fällt, genau wie im qmake-Pendant.
Das TestApp Programm und die Sitzungs-CMakeLists.txt
CMakeLists.txt des Programms und Linken der Bibliothek
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
# Allgemeine Einstellungen, legt das Ausgabeverzeichnis und den Suchpfad für
# die Bibliothek zur Laufzeit fest
include( ${CMAKE_CURRENT_LIST_DIR}/../libs/build.cmake )
file( GLOB SRCS src/*.cpp )
file( GLOB MOC_HDRS src/*.h )
add_executable(TestApp
${SRCS}
${MOC_HDRS}
)
# Linke gegen die dynamische Bibliothek. Anders als im qmake-Projekt müssen
# hier weder Include-Pfad noch Bibliothekssuchpfad gesetzt werden - beide
# werden vom TestLib-Target propagiert, das cmake auch dazu veranlasst, die
# Bibliothek zuerst zu bauen.
target_link_libraries(TestApp PRIVATE
TestLib
Qt6::Widgets
)
Auch hier wieder das gleiche Muster: include( .../build.cmake ) steht am Anfang, add_executable() legt das Programm-Target an. Weil add_executable (statt add_library) verwendet wird, greift beim Bauen automatisch CMAKE_RUNTIME_OUTPUT_DIRECTORY = BIN_DIR aus build.cmake – 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(TestApp 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 genau 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 – es ist schließlich eine Anwendung und keine Bibliothek.
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, unabhängig von der Reihenfolge, in der die add_subdirectory()-Aufrufe in der Sitzungs-CMakeLists.txt stehen.
Die
LIBS += ...-Reihenfolge-Falle aus dem qmake-Abschnitt (Bibliotheken, die Symbole aus anderen Bibliotheken benötigen, müssen vor diesen aufgeführt werden) betriffttarget_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 imtarget_link_libraries()-Aufruf selbst.
Die Sitzungs-CMakeLists.txt
Die CMakeLists.txt im Wurzelverzeichnis übernimmt die Rolle von SharedLibraryBuildSystemConfigs.pro:
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-Datei, erstellt die Bibliothek und die darauf aufbauende 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)
# bildet das qmake-Standardverhalten nach: ohne explizit angeforderte
# Konfiguration wird ein Release-Build erzeugt
get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if (NOT IS_MULTI_CONFIG AND NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build config (Debug or Release)" FORCE)
endif()
# Qt wird hier einmalig gesucht, die einzelnen Targets linken nur noch gegen
# die Import-Targets (dies ist das Gegenstück zu 'QT += widgets' in den .pro-Dateien)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
# die Anwendung linkt gegen die Bibliothek, also wird diese 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, ohne die manche der hier verwendeten Sprachfeatures (u.a. generator-ausdrucksfähigeCMAKE_RUNTIME_OUTPUT_DIRECTORY-Handhabung) nicht verlässlich funktionieren. 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++ (ohne C, was cmake sonst standardmäßig mit aktivieren würde). Das entspricht in etwa dem impliziten Namen, den qmake aus dem Dateinamen der Sitzungs-.pro-Datei ableitet.- Der Block mit
IS_MULTI_CONFIGundCMAKE_BUILD_TYPEbildet qmakes Standardverhalten nach: Wird keine Konfiguration explizit angegeben (weder per-DCMAKE_BUILD_TYPE=...noch implizit durch einen Multi-Config-Generator), baut cmake von Haus aus ohne Optimierungen und ohne Debug-Informationen, weilCMAKE_BUILD_TYPEdann schlicht leer ist. Damit dieses Beispielprojekt sich in diesem Fall so verhält wie qmake ohneCONFIG += debug(nämlich einen Release-Build erzeugt), wirdCMAKE_BUILD_TYPEin diesem Fall explizit aufReleasegesetzt. Bei Multi-Config-Generatoren unterbleibt das bewusst, da dort ohnehin alle Konfigurationen gleichzeitig zur Verfügung stehen und beim Bauen ausgewählt werden. find_package(Qt6 REQUIRED COMPONENTS Widgets)sucht die Qt6-Installation und die benötigte Komponente genau einmal, zentral in der Sitzungs-Datei. Anders als bei qmakesQT += widgets, das in jeder.pro-Datei wiederholt werden muss, die Qt-Widgets verwendet, wird bei cmake das daraus resultierende Import-TargetQt6::Widgetseinmalig verfügbar gemacht und in den einzelnenCMakeLists.txt-Dateien nur noch pertarget_link_libraries()referenziert.add_subdirectory(libs/TestLib)undadd_subdirectory(TestApp)binden die beiden Unterprojekte ein. Ein Gegenstück zu qmakesSUBDIRS/TestLib.file/TestApp.fileist dafür nicht nötig:add_subdirectory()erwartet direkt den (relativen) Pfad zum Verzeichnis mit der jeweiligenCMakeLists.txt, unabhängig davon, ob dieses Verzeichnis wieTestAppdirekt unterhalb der Sitzungs-Datei liegt oder wielibs/TestLibtiefer verschachtelt ist.- Eine Entsprechung zu
TestApp.depends = TestLibfehlt hier bewusst: Wie oben beschrieben, ergibt sich die Build-Reihenfolge bereits automatisch austarget_link_libraries(TestApp PRIVATE TestLib ...)inTestApp/CMakeLists.txt. Die Reihenfolge der beidenadd_subdirectory()-Aufrufe hier dient daher nur der Lesbarkeit, nicht der Korrektheit.
Insgesamt zeigt sich am Vergleich der beiden Sitzungs-Dateien der grundsätzliche Unterschied zwischen den beiden Buildsystemen: qmake arbeitet variablenbasiert und projektweit (Einstellungen wie INCLUDEPATH oder LIBS gelten für die gesamte .pro-Datei und müssen bei Bedarf manuell zwischen Projekten synchronisiert werden), während cmake targetbasiert arbeitet und Anforderungen über PUBLIC/PRIVATE/INTERFACE gezielt entlang des Abhängigkeitsgraphen der Targets propagiert. 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, die sich gegenseitig verwenden, reduziert das targetbasierte Modell von cmake den Pflegeaufwand aber deutlich.
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 auch statisch linken. Dabei gibt es häufig aber Probleme mit fehlenden Ressourcen. Dazu eine kleine Erklärung (auch wenn ich dynamisch gelinkte Bibliotheken bevorzuge).
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üricons.qrcnutzt duQ_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).