Größere Projekte, die Software auf Basis von Qt entwickeln, greifen gern zu Cmake, um den Code zu übersetzen. Dabei eignet sich das Qt-eigene Build-System Qmake durchaus auch für den Bau umfangreicher Anwendungen. Es beherrscht Shadow-Builds und kennt Abhängigkeiten für Pre- und Postbuild-Targets.
Seine Anwesenheit bleibt oft unbemerkt: Qmake, das ursprünglich von Trolltech entwickelte Build-System für Qt-Anwendungen und die Qt-Bibliothek macht in vielen kleinen bis mittelgroßen Projekten meist einfach, was Entwickler erwarten. Wachsen die Projekte allerdings, dann wechseln viele Qt-Entwickler plötzlich zur Qmake-Alternative Cmake, die ursprünglich als Build-System für das ITK (Insight Segmentation and Registration Toolkit) entstand.
Als Begründung hört man dann meist, Qmake sei für viele Aufgaben schlicht ungeeignet. Im KDE-Projekt etwa ist Cmake das Werkzeug der Wahl, wenn es darum geht, große Mengen an Quellcode zu übersetzen. Klassische Build-Systeme wie Autotools, Ant und Ähnliche spielen in der Qt-Entwicklung aufgrund des höheren Integrationsaufwands indes kaum eine Rolle. Dass Qmake aber viel mehr kann, zeigt dieser Artikel: Mit dem Qt-eigenen Build-System lassen sich durchaus auch umfangreiche, plattformunabhängige Softwareprojekte schultern.
Qmake versus Cmake
Qmake weist durchaus Ähnlichkeiten mit dem bereits im Jahr 2000 gestarteten Cmake auf: Beide Systeme erzeugen aus einer Projektdatei ein »Makefile« , an dem sich »make« und »make install« entlanghangeln. Ungeachtet zahlloser Optionen, mit deren Hilfe der Codebauer das Erzeugen des Makefiles sehr genau kontrolliert, folgt ein typischer Build-Vorgang für Cmake und Qmake den Befehlen »cmake && make && make install« respektive »qmake && make && make install« .
So ähnlich die prinzipielle Vorgehensweise beim Build ist, so unterschiedlich fällt die Syntax der beiden Build-Dateien aus. Diese Unterschiede veranschaulicht ein kleines Beispielprojekt, das ein “Hello-World”-Programm übersetzt, welches aus den Komponenten »main.cpp« , »hellowindow.cpp« , »hellowindow.h« sowie »hellowindow.ui« besteht. Es beinhaltet einen Installer und Übersetzungen, auf die der Artikel später eingeht.
Hallo Doppelwelt
Cmake-Projekte zeichnen sich dadurch aus, dass sie üblicherweise eine Datei namens »CMakeLists.txt« im Top-Level-Verzeichnis des Projekts ablegen (Listing 1). Prinzipiell lassen sich aber auch mit diesem System Unterprojekte in Form weiterer »CMakeLists.txt« -Dateien einbinden, ganz so wie bei Qmake.
Listing 1
Hello-World-Programm als Cmake-Projekt
01 PROJECT(helloworld)
02 FIND_PACKAGE(Qt4_REQUIRED)
03
04 SET(helloworld_SOURCES main.cpp hellowindow.cpp)
05 SET(helloworld_HEADERS hellowindow.h)
06 SET(helloworld_FORMS hellowindow.ui)
07
08 QT4_WRAP_CPP(helloworld_HEADERS_MOC ${helloworld_HEADERS})
09 QT4_WRAP_UI(helloworld_FORMS_HEADERS ${helloworld_FORMS})
10 INCLUDE(${QT_USE_FILE})
11 ADD_DEFINITIONS(${QT_DEFINITIONS})
12
13 SET(QT_USE_QTCORE)
14 SET(QT_USE_QTGUI)
15
16 ADD_EXECUTABLE(helloworld ${helloworld_SOURCES} ${helloworld_HEADERS_MOC} ${helloworld_FORMS_HEADERS})
17 TARGET_LINK_LIBRARIES(helloworld ${QT_LIBRARIES})
Der erfahrene Qt-Entwickler weiß, dass vor dem eigentlichen Kompilieren und Linken noch einige Tools und Codegeneratoren an die Arbeit gehen. Qmake-Projekte definiert er in einer ».pro« -Datei, die idealerweise den Namen des Ordners trägt, in dem sie liegt (Listing 2). Dieses System weist – ähnlich wie das von Cmake – bestimmten Variablen Werte zu und beeinflusst den Kompiliervorgang mithilfe vordefinierter Schalter (Listing 2). Auch Qmake ersetzt nicht das klassische Make, sondern ist vielmehr eine Art Makefile-Generator.
Listing 2
Hello World als Qmake-Projekt
01 TEMPLATE = app 02 TARGET = helloworld 03 QT = core gui 04 SOURCES = main.cpp hellowindow.cpp 05 HEADERS = hellowindow.h 06 FORMS = hellowindow.ui
Der auffallendste Unterschied zwischen beiden Build-Systemen besteht darin, dass die Qmake-Syntax deutlich einfacher zu lesen und zugleich schlanker ist. Entwickler von Shellskripten sollten besonders gut mit ihr zurecht kommen, da sie sich eng an die Syntax der Shell anlehnt. Das rührt auch daher, dass Qmake die Eigenheiten der Qt-Bibliothek kennt und so viele Schritte im Hintergrund erledigt. Cmake dagegen ist von Zusatzpaketen abhängig, die nicht der Qt-Hersteller, sondern die Community pflegt.
Shadow-Builds mit Qmake
Dass sich Qmake aber auch für komplexere Aufgaben eignet, zeigt sich bei der Unterstützung von fortgeschrittenen Funktionen wie Out-of-source-Builds oder Pre- und Postbuild-Targets.
Ein sogenannter Out-of-Source-Build – von Qt-Entwicklern auch Shadow-Build genannt – findet statt, wenn alle Dateien, die der Build-Prozess erstellt, außerhalb des Quellcode-Verzeichnisses landen. Dieses Prinzip unterscheidet sich maßgeblich von jenem klassischer Makefile-Projekte, in deren Verlauf die generierten Dateien wahllos das Sourcecode-Verzeichnis “zumüllen”.
Zu den größten Vorteilen eines solchen Shadow-Builds gehört, dass er nicht nur einen besseren Überblick bietet, sondern auch die Möglichkeit, mehrere unterschiedliche Bauvorgänge parallel zu betreiben. In größeren Projekten ist es beispielsweise üblich, gleichzeitig Pakete für 32- und 64-Bit-Architekturen sowohl im Debug- als auch im Release-Modus für sämtliche unterstützten Plattformen zu bauen. Die so erzeugten Distributionen gehen dann an das Test- oder Release-Team, wobei schnell mal 18 komplette Builds zusammenkommen.
Sowohl Qmake als auch Cmake unterstützen solche Shadow-Builds, wenn beide sie auch – wie Listing 3 beweist – auf unterschiedliche Weise umsetzen.
Listing 3
Shadow-Build mit Qmake und Cmake
01 mkdir ../build32-release 02 cmake ../build32-release && make 03 04 mkdir ../build32-release && cd ../build32-release 05 qmake ../src && make
Pre- und Postbuild-Ziele
Pre- und Postbuild-Targets definieren Aufgaben, die das Build-System jeweils vor oder nach dem Kompilieren eines Projekts erledigt. Zu den typischen Zielen gehören beispielsweise das Erzeugen eines Versions-Headers, von Übersetzungen oder das “Strippen” (also das Entfernen überflüssiger Symbole aus den Binaries)nach einem Release-Build.
Das Generieren von Header-Dateien gehört zu den verbreitetsten Prebuild-Targets. Das Build-System erzeugt sie, um automatisiert immer die aktuell kompilierte Softwareversion aus der Versionsverwaltung anzuzeigen. Setzt das Projekt zum Beispiel auf Subversion, kann dies wie folgt aussehen:
svnver version.template version.h ./
Aufgrund der Tatsache, dass einige Codedateien von dem so erzeugten Header abhängen, ruft das Build-System dieses Kommando vor dem Kompilieren auf.
Mit Qmake lässt sich so ein Prebuild-Target sehr einfach hinzufügen. Der Name des Ziels – in Listing 4 lautet er »versiontarget« – lässt sich frei wählen, allerdings muss ihn der Qmake-Nutzer konsistent verwenden.
Listing 4
Prebuild-Target mit Qmake
01 versiontarget.target = version.h 02 versiontarget.commands = svnver version.template version.h ./ 03 versiontarget.depends = FORCE 04 05 PRE_TARGETDEPS += version.h 06 QMAKE_EXTRA_TARGETS += versiontarget
Die Werte für die drei jeweils durch einen Punkt vom Zielnamen abgetrennten Variablen »target« , »commands« sowie »depends« gibt Qmake vor, die Bedeutung der Anweisungen verrät die mit Qt gelieferte Dokumentation.
In Listing 5 übernimmt die Variable »depends« eine besondere Aufgabe: Der Wert »FORCE« sorgt dafür, dass Qmake dieses Target bei jedem Kompiliervorgang ausführt. Anstelle von »FORCE« kann der Entwickler auch Dateinamen angeben, wodurch Qmake das Target nur dann erstellt, falls die Datei nicht existiert. Damit lassen sich in ein klassisches Makefile Abhängigkeiten einbauen.
Listing 5
Postbuild-Target mit Qmake
01 win32:CONFIG(release, debug|release) {
02 message(Creating installer package...)
03 installer_target.commands = makensis /NOCD installer_config.nsis
04 installer_target.depends = FORCE
05 QMAKE_EXTRA_TARGETS += installer_target
06 POST_TARGETDEPS += installer_target
07 }
Indem der Qmake-Nutzer die Variable »PRE_TARGETDEPS« durch die generierte Datei »version.h« ergänzt, wird das beschreibende Target zu einem Prebuild-Target. Dazu muss der Entwickler jedoch das Target noch an die Variable »QMAKE_EXTRA_TARGETS« anhängen, um schließlich das gewünschte Makefile-Target zu erzeugen.
Zu den klassischen Postbuild-Targets gehört das Erzeugen eines Installers, das demselben Schema folgt. Der einzige Unterschied: Die generierte Datei übergibt der Entwickler nun der Variable »POST_TARGETDEPS« anstelle von »PRE_TARGETDEPS« .
Ein noch flexiblerer Ansatz
Das nächste Beispiel zeigt anhand des Installer-Klassikers NSIS, wie ein solches Postbuild-Target für das Beispielprojekt “Hello World” aussehen kann. Zwei nette Syntax-Features versüßen dabei dem Entwickler die Arbeit. So bewirkt die folgende Anweisung, dass Qmake den Installer nur unter Windows und nur im Release-Modus erzeugt:
win32:CONFIG(release, debug|release) { ... }
Solche Bedingungsoperatoren machen das Erzeugen plattformspezifischer Anweisungen, falls sie denn notwendig sind, zum Kinderspiel.
Der eben gezeigte Weg zum Erzeugen von Pre- und Postbuild-Targets ist zwar kurz und einprägsam, hat jedoch einen kleinen Nachteil: Qmake führt die Targets aufgrund der »FORCE« -Option entweder immer aus oder setzt als Bedingung das Fehlen einer bestimmten Datei (»version.h« ) voraus. Dieser Umstand ist auf das erzeugte Makefile und das typische Verhalten von Make zurückzuführen.
Will der Qmake-Anwender allerdings Aktionen aufrufen, die sich nicht direkt auf das Dateisystem auswirken, wie etwa das Strippen eines Binaries oder das Aktualisieren einer Datei, muss er einen anderen Weg beschreiten. Die Projektbestandteile zum Installer und zu den Übersetzungen (Translations), die auszugsweise in Listing 5 und 6 auftauchen, zweckentfremden Unterprojekte vom Typ »lib« , um eine saubere Trennung der Zuständigkeiten und eine höhere Flexibilität zu erreichen. Ein weiterer Nutzen dieser Lösung besteht in ihrer Wiederverwendbarkeit: Ein einmalig eingerichtetes Translations-Projekt lässt sich in anderen Projekten recyceln, was den Arbeitsaufwand bei der Projektpflege deutlich reduziert.
Teilen und Herrschen
Viele große Projekte folgen einem festen Schema: Sie trennen die Anwendungs- von der Bedienlogik und vom User-Interface-Code. Hinzu kommen optionale Plugins, welche die Software später erweitern, sowie ein unternehmensweit genutztes SDK. Solch sauber getrennte Projekte führt man idealerweise mit der »subdirs« -Vorlage von Qmake zusammen.
Dabei hat sich ein einfaches Schema (Listing 6) bereits mehrfach bewährt. Die zusätzliche Angabe »CONFIG += ordered« bewirkt, dass Qmake die Unterprojekte, die der Entwickler in »SUBDIRS« definiert, in der angegebenen Reihenfolge baut. Hier macht es sich bezahlt, wenn die Projektdateien den Namen des Verzeichnisses tragen, in dem sie liegen. Dann genügt es, Qmake an dieser Stelle die Verzeichnisse der Unterprojekte zu übergeben, um alle notwendigen Dateien zu finden.
Listing 6
Top-Level-Projektdatei
01 TEMPLATE = subdirs 02 SUBDIRS = SDK GUI Plugins Translations Installer 03 CONFIG += ordered 04 TRANSLATIONS = Translations/Deutsch.ts Translations/English.ts
In dieser Projektstruktur steckt eine kleine Besonderheit, was die Definition der Übersetzungen angeht. Qt und damit auch Qmake bieten für Übersetzungen einen vorgefertigten Mechanismus an. Im Beispielprojekt definiert die Top-Level-Projektdatei (Abbildung 1) die Sprachen, in die der Entwickler seine Anwendung übersetzt sehen will – Qmake erstellt die Übersetzungen jedoch erst zum Schluss. So teilen sich alle Unterprojekte eine Übersetzungsdatei pro Sprache, was den Prozess der Internationalisierung sehr vereinfacht. Ein Nachteil dieser Variante: Plugins können in dieser Lösung nicht ohne weiteres eigene Sprachpakete mitliefern.

Abbildung 1: Das Projektfile des Hello-World-Programms lässt sich, wie auch die anderen Dateien, bequem im Qt Creator erstellen.
Fazit
Qmake ist ein durchaus mächtiges Build-Werkzeug. Es bringt alle Funktionen mit, die eine professionelle Umgebung fordert. Im Gegensatz zu Cmake ist es jedoch in jeder Qt-Installation enthalten. Durch die enge Verbundenheit mit der Qt-Bibliothek lassen sich Qmake-Projekte meist deutlich einfacher warten. Die Verfügbarkeit auf allen Plattformen bietet in dieser Form kein anderes Build-System. Anhand einer flexiblen Vorlage kann der Qt-Entwickler dann einfach und schnell Projekte beliebiger Größe aufsetzen und sich auf das Wesentliche konzentrieren: die Softwareentwicklung.





