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 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, weshalbbuild.pridort 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 dasCore-Modul, und zwar nur, um im nachfolgendenmessage(...)-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 eigenenfind_package()-Aufruf.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.- Eine Entsprechung zu
TestApp.depends = TestLibfehlt bewusst: Die Build-Reihenfolge ergibt sich automatisch austarget_link_libraries(TestApp PRIVATE TestLib ...)inTestApp/CMakeLists.txt(siehe unten). Die Reihenfolge der beidenadd_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: Dabuild.cmakejetzt 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 zuCONFIG += c++17.STANDARD_REQUIRED ONsorgt dafür, dass der Build hart fehlschlägt, wenn der Compiler C++17 nicht unterstützt.EXTENSIONS OFFdeaktiviert compilerspezifische Erweiterungen zugunsten von striktem Standard-C++. Weil diese drei Variablen im Wurzelverzeichnis gesetzt werden, gelten sie automatisch fürTestLibundTestAppgleichermaßen, ohne dass die beidenCMakeLists.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_DIRzeigt dabei immer auf das Verzeichnis der Datei, die gerade verarbeitet wird – hierbuild.cmakeselbst, alsolibs/–, unabhängig davon, aus welchem Verzeichnis herausinclude(libs/build.cmake)aufgerufen wurde.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) – 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 voncmake --build ... --config Debugfeststeht. Das Ergebnis entspricht 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).LIBRARYbetrifft die dynamischen Bibliotheken unter Unix (.so).RUNTIMEbetrifft Executables und, unter Windows, auch die DLLs selbst – deshalb landen unter WindowsTestApp.exeundTestLib.dlldurch das einmalige Setzen vonCMAKE_RUNTIME_OUTPUT_DIRECTORYautomatisch im gleichen Verzeichnis, was bei qmake erstDLLDESTDIRexplizit leisten musste.- Der
if (UNIX) ... endif()-Block setztCMAKE_BUILD_RPATH, das cmake-Gegenstück zuQMAKE_RPATHDIR, und unterscheidet dabei je nachIS_MULTI_CONFIG, weil Multi-Config-Generatoren anLIB_DIRautomatisch noch ein Unterverzeichnis je Konfiguration anhängen (lib/Debug,lib/Release). - Der Block mit
IS_MULTI_CONFIGundCMAKE_BUILD_TYPEbildet 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 ohneCONFIG += debug(nämlich einen Release-Build erzeugt), wirdCMAKE_BUILD_TYPEexplizit aufReleasegesetzt, sofern kein Multi-Config-Generator verwendet wird. if (CMAKE_BUILD_TYPE STREQUAL "Release") add_compile_definitions(QT_FORCE_ASSERTS) endif()ist neu hinzugekommen: Qt deaktiviertQ_ASSERTstandardmäßig in Release-Builds (dort, woNDEBUGgesetzt ist). MitQT_FORCE_ASSERTSbleiben 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 dasCONFIG += qtimplizit; 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ürTEMPLATE = libautomatisch, 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:NOMINMAXverhindert Compile-Fehler durch die Makrosmin/maxaus<windows.h>, wenn manstd::min/std::maxverwendet;/wd4996unterdrückt MSVCs Warnungen zu “unsicheren” CRT-Funktionen;/Zc:__cplusplussorgt dafür, dass das Makro__cplusplusden tatsächlich verwendeten C++-Standard widerspiegelt (MSVC meldet ohne dieses Flag traditionell immer199711L, egal welcher Standard eingestellt ist);/permissive-schaltet MSVC-spezifische Sprach-Erweiterungen ab und ergänzt soCMAKE_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 einlib-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=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.
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, sindCMAKE_CXX_STANDARD, die Ausgabeverzeichnisse,CMAKE_AUTOMOCetc. bereits gesetzt, wennadd_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örtlichesTestLib. 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 )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 – nach dem Hinzufügen einer neuen Datei muss dafür aber cmake neu konfiguriert werden, daGLOBdas Verzeichnis nur zum Konfigurationszeitpunkt einliest.add_library(${PROJECT_NAME} SHARED ...)erzeugt das eigentliche Bibliotheks-Target. Die.qrc-Datei wird direkt als Quelle angegeben; daCMAKE_AUTORCCaktiviert 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.0mit SymlinklibTestLib.so.1undlibTestLib.so).target_compile_definitions(${PROJECT_NAME} PRIVATE TESTLIB_LIBRARY)setzt das Export-Define,PRIVATE, damit es nicht an Targets weitergereicht wird, die gegenTestLiblinken – analog zuDEFINES += TESTLIB_LIBRARY, das ausschließlich inTestLib.prosteht.target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src)macht dassrc-Verzeichnis nicht nur fürTestLibselbst, sondern dankPUBLICauch für jedes Target verfügbar, das später gegenTestLiblinkt. Das ist ein wesentlicher konzeptioneller Unterschied zu qmake: Dort mussteTestApp.proden Include-Pfad noch von Hand überINCLUDEPATH += ../libs/TestLib/srcergä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 zentralenfind_package()-Aufruf in der Sitzungs-CMakeLists.txtzu verlassen. Das machtTestLib/CMakeLists.txtin 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 gegenQt6::Widgets, wiederPUBLIC, weilLEDLabel.hselbst Qt-Widgets-Typen wieQWidgetverwendet.
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.
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) 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.
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ü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).