Aus Linux-Magazin 02/2007

Einführung in das Buildsystem Cmake

© photocase.com

Ihre Software für die Übersetzung mit den Autotools portabel zu konfigurieren ist eine Aufgabe, die für viele Programmierer komplizierter als der eigene Code ist. Die Alternative Cmake verschafft den Entwicklern eine wohlverdiente Denkpause.

Als Linux-Nutzer hat man sich über die Jahre an die Schritte »./configure; make; make install« zum Kompilieren von Softwarepaketen gewöhnt. Hinter ihnen verbergen sich die Autotools, eine Sammlung von Shell-, M4- und Perl-Skripten, die dabei hilft, Software zu schreiben, die sich portabel auf verschiedenen Unix-Derivaten kompilieren lässt. Für viele Entwickler sind die Autotools aber wegen der Vielzahl ineinander greifender Programme eine große Hürde.

Ein alternatives Buildsystem namens Cmake [1] schickt sich nun an, “die Art und Weise, wie alle Software-Entwickler arbeiten, zu ändern”, wie Bill Hoffman, einer der Cmake-Entwickler, es ausdrückt. Inzwischen setzt eine ganze Reihe kommerzieller und freier Projekte Cmake ein. Das größte und bekannteste ist derzeit KDE 4, das aus Millionen Zeilen Code besteht [2] und sich heute auf allen Plattformen (Linux, *BSD, Mac OS X, Windows mit Mingw wie auch den MS-Compilern) kompilieren lässt. Weitere Projekte sind Scribus, der Chicken Scheme Interpreter, das Strategiespiel Boson, Plplot und der Debian-Fork der CDR-Tools. Ethereal, Lyx, Open Wengo und Libgphoto sind gerade dabei, Cmake für ihre Zwecke zu evaluieren.

Cross-Plattform

Das Cross-Plattform-Make steht unter der BSD-Lizenz und wird von der amerikanischen Firma Kitware Inc. [3] entwickelt, die sich auf Visualisierungssoftware für medizinische Anwendungen spezialisiert hat. Cmake entstand im Rahmen des Insight Segmentation and Registration Toolkit [4] zur Visualisierung des menschlichen Körpers. Das Tool vereint die Funktionalität der verschiedenen Bestandteile der Autotools in sich:

  • Erzeugung von Makefiles
  • Unterstützung verschiedener Compiler und Linker auf unterschiedlichen Plattformen
  • Unterstützung von System-Introspection
  • Erweiterbarkeit durch Makros

Einzige Voraussetzung für den Einsatz von Cmake ist ein C++-Compiler. Daher funktioniert Cmake zum Beispiel auch unter Windows mit MS Visual Studio problemlos. Cmake ist aber nicht nur ein Makefile-Generator, es erzeugt auch Eingangsdateien für das native Buildsystem der jeweiligen Plattform.

So kann es unter Unix Makefiles und Projekte für Kdevelop 3 erzeugen, bei Mac OS X zusätzlich Projektdateien für Xcode und unter Windows Makefiles für Cygwin, Mingw, Msys, Borland und Microsoft Make sowie die IDEs von Microsoft ab Visual Studio 6. Unter Windows und Mac OS X dürfen Entwickler also auch mit Cmake weiter mit ihren gewohnten Entwicklungsumgebungen arbeiten und müssen nicht zurück auf die Kommandozeile.

Für die Qualitätssicherung unterstützt Cmake die Einbindung von Unit-Tests, Nightly Builds und weiteren Tools wie Valgrind [5] oder KWStyle [6]. Die Ergebnisse sammelt ein zentraler Dart-Dashboard-Server [7].

Hallo Welt

Alle Beispiele in diesem Artikel setzen Cmake 2.4.3 oder neuer voraus. Das Tool liest Dateien mit dem Namen »CMakeLists.txt« (Groß- undKleinschreibung beachten) ein und generiert daraus die gewünschten Makefiles beziehungsweise Projektdateien. Im folgenden Hallo-Welt-Beispiel die Datei »main.c« soll zu einem Programm kompiliert und dann gelinkt werden:

#include <stdio.h>
int main()
{
   printf("Hallo Welt!n");
   return 0;
}

Die zugehörige »CMakeLists.txt« sieht wie folgt aus:

add_executable(hallowelt main.c)

Der Cmake-Befehl »add_executable()« bedeutet, dass aus der Quelldatei »main.c« ein ausführbares Programm namens »hallowelt« entstehen soll. Cmake beachtet dabei die plattformspezifischen Eigenschaften, das Executable wird also unter typischen Unix-Systemen den Namen »hallowelt« bekommen, unter Windows dagegen »hallowelt.exe«.

Cmake erwartet (Listing 1) als Argument jenes Verzeichnis, in dem der Quelltext und die »CMakeLists.txt« liegen. Hat Cmake die »CMakeLists.txt« im aktuellen Verzeichnis gelesen, einen Compiler gesucht und Makefiles erzeugt, kann der Entwickler sein Projekt schließlich übersetzen (Listing 2).

Listing 1: Konfigurieren

01 ~/src/hallo/ $ cmake .
02 -- Check for working C compiler: /usr/bin/gcc
03 -- Check for working C compiler: /usr/bin/gcc -- works
04 -- Check size of void*
05 -- Check size of void* - done
06 -- Check for working CXX compiler: /usr/bin/c++
07 -- Check for working CXX compiler: /usr/bin/c++ -- works
08 -- Configuring done
09 -- Generating done
10 -- Build files have been written to: ~/src/tests/hallo

Listing 2: Übersetzen

01 ~/src/hallo $ make
02 Scanning dependencies of target hallowelt
03 [100%] Building C object CMakeFiles/hallo.dir/main.o
04 Linking C executable hallowelt
05 [100%] Built target hallowelt
06 ~/src/hallo $ ./hallowelt
07 Hallo Welt!

Nach diesem recht einfachen Einstieg folgt nun ein komplexeres Projekt, das weitere Fähigkeiten von Cmake vorstellt. Es besteht aus einer Library und einem Programm, das diese Library benutzt. Die Quellen sind wie in Abbildung 1 organisiert. Im Verzeichnis »myproj/lib/« soll aus den Dateien »core.c«, »util.c« und ? je nach Plattform ? »unixtool.c« oder »wintool.c« eine Shared Library »libmyutils.so« entstehen, im Verzeichnis »myproj/app/« außerdem ein Programm »fooapp« aus den Dateien »main.cpp« und »process.cpp«.

Abbildung 1: Der Verzeichnisbaum des Beispielprojekts mit insgesamt drei Projektdateien »CMakeLists.txt«.

Abbildung 1: Der Verzeichnisbaum des Beispielprojekts mit insgesamt drei Projektdateien »CMakeLists.txt«.

Dieses Programm benutzt Funktionen aus »libmyutils.so«, das heißt, dass der Compiler beim Übersetzen von »fooapp« die Header »core.h« und »util.h« finden und der Linker »myapp« zu »libmyutil.so« linken muss. Nach dem Buildvorgang soll Cmake das Programm und die Library installieren. Die Inhalte der einzelnen »CMakeLists.txt« zeigen die Listings 3a bis 3c.

Listing 3a: »myproj/CMakeLists.txt«

01 project(FooProject)
02 add_subdirectory(lib)
03 add_subdirectory(app)

Listing 3b: »myproj/lib/CMakeLists.txt«

01 set(libSrcs core.c util.c)
02 
03 if (UNIX)
04    set(libSrcs ${libSrcs} unixtool.c)
05 else (UNIX)
06    set(libSrcs ${libSrcs} wintool.c)
07 endif (UNIX)
08 
09 add_library(util SHARED ${libSrcs})
10 
11 install(TARGETS util DESTINATION lib)

Listing 3c: »myproj/app/CMakeLists.txt«

01 include_directories(${CMAKE_SOURCE_DIR}/lib)
02 
03 set(fooappSrcs main.cpp process.cpp)
04 add_executable(fooapp ${fooappSrcs})
05 
06 target_link_libraries(fooapp util)
07 
08 install(TARGETS fooapp DESTINATION bin)

Die Datei »myproj/CMakeLists.txt« verzweigt mit »add_subdirectory« in die Unterverzeichnisse »lib« und »app«. Cmake erwartet daraufhin in beiden Verzeichnissen jeweils eine »CMakeLists.txt«, die es verarbeitet. Alle anderen eventuell vorhandenen Unterverzeichnisse ignoriert Cmake.

Achtung: Schreibweise

In Zeile 1 von »myproj/lib/CMakeLists.txt« (Listing 3b) kommt mit »set()« einer der wichtigsten Cmake-Befehle vor. Er weist einer Variablen einen Wert oder eine Liste von Werten zu. Im Beispiel ist »libSrcs« eine Liste mit den beiden Elementen »core.c« und »util.c«. Bei Befehlen spielt die Groß- undKleinschreibung keine Rolle, bei Variablennamen und Argumente jedoch schon. Argumente können sich auch über mehrere Zeilen erstrecken. So sind die beiden folgenden Beispiele äquivalent:

SET(libSrcs core.c util.c)
set (libSrcs
   core.c
   util.c
   )

Wie Zeile 3 von Listing 3b zeigt, gibt es auch in Cmake ein »if«. Die nach dem Schlüsselwort folgende Bedingung ist als Argument für das zugehörige optionale »else()« und das zwingend erforderliche »endif()« zu wiederholen. Dadurch ist bei »else()« und »endif()« leicht zu erkennen, zu welchem »if()« sie gehören. Zeile 4 fügt beim Kompilieren auf einem Unix-System die Datei »unixtool.c« zur Liste »libSrcs« hinzu:

set(libSrcs ${libSrcs} unixtool.c)

Der Ausdruck »${libSrcs}« liefert den Wert der Variablen, sodass Zeile 4 aufgelöst wird zu:

set(libSrcs core.c util.c unixtool.c)

Entsprechend verwendet Cmake unter Windows die Datei »wintool.c«. In Zeile 9 von Listing 3b fügt »add_library()« dem Projekt eine Shared Library hinzu. Die Syntax entspricht der von »add_executable()«: erst der (logische) Name der Library, dann die Quelldateien, hier mit Hilfe der Variablen »libSrcs«. Das Argument »SHARED« legt fest, dass es sich um eine Shared Library handelt, andernfalls erzeugt Cmake eine statische.

Wie bei »add_executable()« passt Cmake auch hier den Name an die Plattform an, also unter Linux »libutil.so«, unter Mac OS X »libutil.dylib« und unter Windows »util.dll«. Die jeweiligen Prä- und Suffixe fügt es automatisch hinzu, in den Cmake-Dateien steht immer nur der logische Name der Library.

Libraries

Der »install()«-Befehl in Zeile 11 von Listing 3b weist Cmake dazu an, Installationsroutinen für das Target »util« zu erzeugen. Die Libutil wandert so in das Verzeichnis »lib« relativ zum Standardinstallationspfad, hier also »/usr/local/lib«. Nach Abarbeitung dieser Datei geht Cmake zurück zu »myproj/CMakeLists.txt« und verzweigt von dort in »myproj/app/CMakeLists.txt« (Listing 3c).

Hier erzeugt in Zeile 4 das schon bekannte »add_executable()« die Applikation »fooapp«. Anders als bei den Autotools dürfen Entwickler bei Cmake die Namen der Variablen frei wählen. Das ist ein Grundprinzip von Cmake: Nichts geschieht automatisch hinter dem Rücken des Anwenders, es gibt keine magischen Datei- oder Variablennamen.

Der »target_link_libraries()«-Befehl in Zeile 6 legt fest, welche Libraries das Target »fooapp« verwendet. In diesem Beispiel soll es die Library »util« sein, die in »myproj/lib« erzeugt wird. Cmake weiß, dass es sich um diese Library handelt, und kümmert sich automatisch um Folgendes:

  • »util« vor »fooapp« kompilieren;
  • »fooapp« zu »util« »linken«;
  • »RPATH« so setzen, dass »fooapp« beim Starten die uninstallierte Library »util« benutzt;
  • »RPATH« beim Installieren den Wünschen des Nutzers anpassen.

In- und Out-of-Source

In dem ersten einfachen Beispiel wurde Cmake im Wurzelverzeichnis des Quelltextbaums ausgeführt, dort startete auch »make« zum Kompilieren der Software. Das ist die herkömmliche Vorgehensweise, die bei Cmake In-Source-Build heißt. Zusätzlich unterstützt Cmake auch Out-of-Source-Builds, bei denen die Software nicht im Quelltextbaum kompiliert wird, sondern in einem beliebigen anderen Verzeichnis.

Out-of-Source- besitzen gegenüber den In-Source-Builds mehrere Vorteile: Der Quelltextbaum bleibt sauber und übersichtlich, er wird nicht durch Objektdateien oder andere Zwischendateien aufgebläht. Soll der Entwickler das Projekt komplett neu erzeugen, kann er einfach das ganze Buildverzeichnis löschen. Zu einem Quelltextverzeichnis darf er beliebig viele Buildverzeichnisse angelegen, etwa für ein Release- und ein Debug-Build. Um das obige Beispiel Out-of-Source zu kompilieren, legt er ein Buildverzeichnis an und startet Cmake dort. Wie beim ersten Beispiel ist der Pfad zum Quelltextverzeichnis das Argument. Listing 4 zeigt den Ablauf des Cmake- und Buildvorgangs.

Listing 4: Out-of-Source-Build (1)

01 ~/src $ mkdir myproj-build
02 ~/src/ $ cd myproj-build
03 ~/src/myproj-build $ cmake ~/src/myproj
04 -- Check for working C compiler: /usr/bin/gcc
05 -- Check for working C compiler: /usr/bin/gcc -- works
06 -- Check size of void*
07 -- Check size of void* - done
08 -- Check for working CXX compiler: /usr/bin/c++
09 -- Check for working CXX compiler: /usr/bin/c++ -- works
10 -- Configuring done
11 -- Generating done
12 -- Build files have been written to: ~/src/myproj-build
13 ~/src/myproj-build $ make
14 [ 20%] Building C object lib/CMakeFiles/util.dir/core.o
15 [ 40%] Building C object lib/CMakeFiles/util.dir/util.o
16 [ 60%] Building C object lib/CMakeFiles/util.dir/unixtool.o
17 Linking C shared library libutil.so
18 [ 60%] Built target util
19 Scanning dependencies of target fooapp
20 [ 80%] Building CXX object app/CMakeFiles/fooapp.dir/main.o
21 [100%] Building CXX object app/CMakeFiles/fooapp.dir/process.o
22 Linking CXX executable fooapp
23 [100%] Built target fooapp

Nach dem Kompilieren installiert das Kommando »make install« die Software. Die von Cmake erzeugten Makefiles stellen noch einige weitere Make-Targets zur Verfügung, eine Übersicht gibt »make help«. Ein weiteres Buildverzeichnis »myproj-build-2« soll im Folgenden dazu dienen, die Erzeugung von Kdevelop-Projektdateien zu demonstrieren, die Cmake ebenfalls beherrscht:

$ cd myproj-build-2
$ cmake ~/src/myproj -GKDevelop3
-- Check for working C compiler: /usr/bin/gcc
...
-- Build files have been written to: ~/src/myproj-build-2

Im Buildverzeichnis findet sich nun untere anderem die Kdevelop-Projektdatei »FooProject.kdevelop«, die sich mit Kdevelop öffnen lässt (Abbildung 2). Den Projektnamen bezieht Cmake vom »project()«-Befehl in Zeile 1 von »myproj/CMakeLists.txt«.

Abbildung 2: Die KDE-Entwicklungsumgebung Kdevelop mit dem von Cmake generierten Projekt. Neben den Quelldateien zeigt der Schnellöffner auch die Cmake-Projektdateien.

Abbildung 2: Die KDE-Entwicklungsumgebung Kdevelop mit dem von Cmake generierten Projekt. Neben den Quelldateien zeigt der Schnellöffner auch die Cmake-Projektdateien.

Bei Out-of-Source-Builds sind ein paar Dinge zu beachten. Im vorigen Beispiel nutzt die Anwendung Funktionen aus der Library »util« und verwendet dazu Header aus dem Verzeichnis »~/src/myproj/lib/«. Deshalb wird dieses Verzeichnis zum Include-Pfad des Compilers hinzugefügt. Dies geschieht in Zeile 1 von Listing 3c durch das Kommando »include_directories(${CMAKE_SOURCE_DIR}/lib)«.

Variablen

»CMAKE_SOURCE_DIR« ist eine von Cmake bereitgestellte Variable, die immer das Wurzelverzeichnis des Quelltextbaums enthält. Zum Navigieren im Quelltext- oder Buildverzeichnis-Baum kennt Cmake außerdem die folgenden Variablen: »CMAKE_CURRENT_SOURCE_DIR« steht für das aktuelle Verzeichnis im Quelltextbaum, »CMAKE_BINARY_DIR« enthält das Wurzelverzeichnis des Buildverzeichnisses (analog zu »CMAKE_SOURCE_DIR«) und schließlich zeigt »CMAKE_CURRENT_BINARY_DIR« auf das aktuelle Verzeichnis im Buildverzeichnis (analog zu »CMAKE_CURRENT_SOURCE_DIR«).

Beim Schreiben der Cmake-Dateien muss der Programmierer immer bedenken, ob er im Quelltext- oder im Buildverzeichnis arbeitet. Um in einem Projekt Out-of-Source-Builds zu unterstützen, ist Folgendes zu beachten:

  • Relative Pfade in »CMakeLists.txt« sind immer als relativ zu dem aktuellen »CMAKE_CURRENT_SOURCE_DIR« zu verstehen, zum Beispiel entspricht »add_executable(hello main.c)« dem »${CMAKE_CURRENT_SOURCE_DIR}/main.c«
  • Alle zu generierenden Dateien sollten entweder in »CMAKE_BINARY_DIR« oder »CMAKE_CURRENT_BINARY_ DIR« erzeugt werden. Das ist vor allem bei den Befehlen »configure_file()«, »file()« und »add_custom_command()« zu beachten. Bei ihnen ist immer der vollständige Pfad zu benutzen, etwa »${CMAKE_CURRENT_BINARY_DIR}/generated.h«.

Ein kleines Beispiel, das nur aus vier »CMakeLists.txt«-Dateien besteht (Abbildung 3), demonstriert die Funktion dieser Variablen. Jede dieser »CMakeLists.txt« gibt die vier genannten Variablen aus. Hier der Inhalt von »src/cmakevars/CMakeLists.txt«:

message(STATUS "${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR}")add_subdirectory(misc)add_subdirectory(app)
Abbildung 3: Die Dateistruktur zum Test der Cmake-Variablen besteht nur aus drei Projektdateien.

Abbildung 3: Die Dateistruktur zum Test der Cmake-Variablen besteht nur aus drei Projektdateien.

Die Listings 5a und 5b zeigen die Ausgaben von Cmake bei Out-of-Source- respektive In-Source-Benutzung.

Listing 5a: Out-of-Source-Build (2)

01 ~/src $ mkdir cmakevars-build
02 ~/src $ cd cmakevars-build
03 ~/src/cmakevars-build $ cmake ../cmakevars
04 --  ~/src/cmakevars ~/src/cmakevars          ~/src/cmakevars-build ~/src/cmakevars-build
05 --  ~/src/cmakevars ~/src/cmakevars/misc     ~/src/cmakevars-build ~/src/cmakevars-build/misc
06 --  ~/src/cmakevars ~/src/cmakevars/misc/sub ~/src/cmakevars-build ~/src/cmakevars-build/misc/sub
07 --  ~/src/cmakevars ~/src/cmakevars/app      ~/src/cmakevars-build ~/src/cmakevars-build/app
08 -- Configuring done
09 -- Generating done
10 -- Build files have been written to: ~/src/tests/ca/cmakevars-build

Listing 5b: In-Source-Build

01 ~/src/cmakevars $ cmake .
02 --  ~/src/cmakevars ~/src/cmakevars          ~/src/cmakevars ~/src/cmakevars
03 --  ~/src/cmakevars ~/src/cmakevars/misc     ~/src/cmakevars ~/src/cmakevars/misc
04 --  ~/src/cmakevars ~/src/cmakevars/misc/sub ~/src/cmakevars ~/src/cmakevars/misc/sub
05 --  ~/src/cmakevars ~/src/cmakevars/app      ~/src/cmakevars ~/src/cmakevars/app
06 -- Configuring done
07 -- Generating done
08 -- Build files have been written to: ~/src/cmakevars

Bei einem Out-of-Source-Lauf verändert Cmake die Werte der »*BINARY_DIR«-Variablen gegenüber In-Source. Das Prinzip dahinter: Die Variablen »*_SOURCE_DIR« zeigen immer in den Quelltextbaum, »*_BINARY_DIR« dagegen immer in die Buildverzeichnis-Struktur. Die »CMAKE_CURRENT_*«-Variablen beziehen sich immer auf das aktuelle Unterverzeichnis.

Der Cache

Ruft der Programmierer im obigen Beispiel noch einmal »make« auf, passiert mehr oder weniger nichts:

~/src/myproj-build $ make
[ 60%] Built target util
[100%] Built target fooapp

Bei einer Änderung an einer der »CMakeLists.txt«-Dateien veranlasst Make von sich aus einen automatischen Neustart von Cmake, um die entsprechenden Makefiles neu zu erzeugen:

$ touch ~/src/myproj/CMakeLists.txt
$ make
-- Configuring done
-- Generating done
-- Build files have been written to: ~/src/myproj-build
[ 60%] Built target util
[100%] Built target fooapp

Die Meldungen »Configuring done« und »Generating done« generiert Cmake beim Ausführen der »CMakeLists.txt«. Der Entwickler muss Cmake also prinzipiell nur ein einziges Mal zum initialen Erzeugen der Makefiles explizit ausführen. Die erzeugten Makefiles enthalten dann Regeln, um Cmake erneut aufzurufen, wenn sich eine der »CMakeLists.txt« geändert hat.

Übliche Ausgaben wie etwa “Checking for working C Compiler” fehlen, denn die Ergebnisse dieser Tests hat Cmake in einer Cachedatei namens »CMakeCache.txt« gespeichert. Bei wiederholten Aufrufen verwendet Cmake die Werte aus dem Cache, außer wenn der Eintrag den String »NOTFOUND« enthält. Dann führt Cmake den Test erneut durch. »CMakeCache.txt« speichert auch das Quelltextverzeichnis und das Buildverzeichnis sowie den verwendeten Generator, also beispielsweise »Unix Makefiles« oder »KDevelop3«.

Wer von Makefiles auf Kdevelop-Projekte umsteigen möchte, muss entweder ein neues Buildverzeichnis anlegen oder die »CMakeCache.txt« löschen, da Cmake sonst deren Einstellungen erneut verwenden würde. Wie erwähnt legt die Cmake-Variable »CMAKE_INSTALL_PREFIX« das Standardinstallationsverzeichnis »/usr/local« fest. Auch ihren Wert konserviert »CMakeCache.txt«. Um die Software in ein anderes Verzeichnis als »/usr/local« zu installieren, können Entwickler den Wert von »CMAKE_INSTALL_PREFIX« aber auf unterschiedliche Weisen anpassen:

  • Über »ccmake ~/src/myproj« (Abbildung 4).
  • Indem sie den Wert auf der Kommandozeile setzen, und zwar: »cmake ~/src/myproj/ -DCMAKE_INSTALL_PREFIX:PATH=/usr«.
  • Durch direktes Editieren der Datei »CMakeCache.txt«.

Auf diese Art und Weise lassen sich alle Cachevariablen beliebig modifizieren.

Abbildung 4: Das Curses-Interface »ccmake« beim Editieren der Cachevariablen. Mit dem Frontend lässt sich bequem die Cmake-Projektkonfiguration verändern.

Abbildung 4: Das Curses-Interface »ccmake« beim Editieren der Cachevariablen. Mit dem Frontend lässt sich bequem die Cmake-Projektkonfiguration verändern.

Externe Bibliotheken

Die bisherigen Beispielen haben zwar Programme und Bibliotheken erzeugt, aber keine externen Libraries benutzt, was in nicht trivialen Projekten aber immer notwendig ist. Eine Grafiksoftware zum Beispiel, die mit Jpeg-Bildern arbeitet, verwendet normalerweise die Jpeg-Library und die entsprechenden Jpeg-Header, die sich auf verschiedenen Systemen oft in unterschiedlichen Verzeichnissen befinden.

Bei Linux-Installationen liegen die Header der Jpeg-Library wie »jpeglib.h« typischerweise in »/usr/include«, also in einem der Standard-Include-Verzeichnisse des Compilers. Auf anderen Systemen wie Mac OS X, HP/UX oder Windows finden sich der Header und die Bibliothek meist an einer anderen Stelle. Cmake trägt diesem Umstand durch den Einsatz des »find_package()«-Befehls Rechnung:

find_package(JPEG REQUIRED)
include_directories(${JPEG_INCLUDE_DIR})
add_executable(jpegviewer main.cpp viewer.cpp)
target_link_libraries(jpegviewer ${JPEG_LIBRARIES})

Der Aufruf von »find_package()« veranlasst Cmake dazu, in seinem System-Modulverzeichnis eine Datei namens »FindJPEG.cmake« zu suchen und auszuführen. Für jedes Paket, das zum Projekt gehört, muss eine solche Datei »FindPaket.cmake« existieren.

Alle diese Dateien gehorchen dem gleichen Schema: Sie versuchen die zur Benutzung der Library nötigen Dateien im System zu finden und setzen eine Reihe von Variablen, die dann die Ergebnisse enthalten. Meist sind das eine Variable für die Include-Verzeichnisse und eine für die Libraries. Die Namensgebung folgt einem konsistenten Schema wie zum Beispiel »JPEG_INCLUDE_DIR« (alternativ »FOO_INCLUDES«) und »JPEG_LIBRARIES« (alternativ »FOO_LIB« und »FOO_LIBS«). Listing 6 zeigt die Datei »FindJPEG.cmake«.

Listing 6: »FindJPEG.cmake«

01 # - Find JPEG
02 # Find the native JPEG includes and library
03 # This module defines
04 #  JPEG_INCLUDE_DIR, where to find jpeglib.h, etc.
05 #  JPEG_LIBRARIES, the libraries needed to use JPEG.
06 #  JPEG_FOUND, If false, do not try to use JPEG.
07 # also defined, but not for general use are
08 #  JPEG_LIBRARY, where to find the JPEG library.
09 
10 FIND_PATH(JPEG_INCLUDE_DIR jpeglib.h)
11 
12 FIND_LIBRARY(JPEG_LIBRARIES NAMES jpeg)
13 
14 IF (JPEG_LIBRARIES AND JPEG_INCLUDE_DIR)
15   SET(JPEG_FOUND "YES")
16 ELSE (JPEG_LIBRARIES AND JPEG_INCLUDE_DIR)
17   SET(JPEG_FOUND "NO")
18 ENDIF (JPEG_LIBRARIES AND JPEG_INCLUDE_DIR)
19 
20 IF (JPEG_FOUND)
21    IF (NOT JPEG_FIND_QUIETLY)
22       MESSAGE(STATUS "Found JPEG: ${JPEG_LIBRARIES}")
23    ENDIF (NOT JPEG_FIND_QUIETLY)
24 ELSE (JPEG_FOUND)
25    IF (JPEG_FIND_REQUIRED)
26       MESSAGE(FATAL_ERROR "Could not find JPEG library")
27    ENDIF (JPEG_FIND_REQUIRED)
28 ENDIF (JPEG_FOUND)
29 
30 MARK_AS_ADVANCED(JPEG_LIBRARIES JPEG_INCLUDE_DIR)

Wo ist die Jpeg-Bibliothek?

Der Kern des Find-Jpeg-Moduls besteht aus den Befehlen »FIND_PATH()« und »FIND_LIBRARY()« in den Zeilen 10 und 12. Außer ihnen bietet Cmake noch »FIND_FILE()« und »FIND_PROGRAM()«. Diese vier Befehle sollen Dateien, Libraries und Programme in systemspezifischen Verzeichnissen finden. Beispielsweise sucht unter Linux der Aufruf »FIND_LIBRARY()« in Zeile 12 unter anderem in den Verzeichnissen »/lib«, »/usr/lib« und »/usr/local/lib«. Das Ergebnis gibt Cmake in der Variablen »JPEG_LIBRARY« zurück.

Der Abschnitt von Zeile 14 bis 19 in Listing 6 setzt die Ergebnisvariable »JPEG_FOUND«. Über sie kann die aufrufende »CMakeLists.txt« testen, ob die Jpeg-Library gefunden wurde, und gegebenenfalls zusätzliche Quelldateien des aktuellen Projekts kompilieren. Da in »CMakeLists.txt« das Schlüsselwort »REQUIRED« steht, bricht das Modul mit einer Fehlermeldung ab, falls die Jpeg-Library fehlt.

Auf unterschiedlichen Systemen besitzen C-Typen häufig verschiedene Größen, Header und weitere Details können sich unterscheiden. Soll Software trotzdem auf all diesen Systemen kompilieren und laufen, müssen diese Punkte vor Beginn des Buildvorgangs getestet werden. Die Listings 7a bis 7d zeigen ein komplettes Beispiel, das die Existenz der Header »stdio.h« und »blub.h« überprüft. Mit den Ergebnissen dieser Tests erzeugt Cmake aus »config.h.in« einen Header »config.h«, der dann die gewünschten Informationen enthält und in den Quelldateien benutzt wird.

Listing 7a: Header-Test in »CMakeLists.txt«

01 include(CheckIncludeFiles)
02 
03 check_include_files(stdio.h HAVE_STDIO_H)
04 check_include_files(blub.h HAVE_BLUB_H)
05 
06 set(V_MAJOR 1)
07 set(V_MINOR 0)
08 set(V_PATCH 2)
09 
10 configure_file(config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
11 
12 include_directories(${CMAKE_CURRENT_BINARY_DIR})
13 add_executable(configtest main.c)

Listing 7b: Header-Vorlage »config.h.in«

01 #cmakedefine HAVE_STDIO_H 1
02 #cmakedefine HAVE_BLUB_H 1
03 #define VERSION_MAJOR ${V_MAJOR}
04 #define VERSION_MINOR ${V_MINOR}
05 #define VERSION_PATCH ${V_PATCH}

Listing 7c: Generierter Header »config.h«

01 #define HAVE_STDIO_H 1
02 /* #undef HAVE_BLUB_H 1 */
03 #define VERSION_MAJOR 1
04 #define VERSION_MINOR 0
05 #define VERSION_PATCH 2

Listing 7d: Benutzung von »config.h«

01 #include <config.h>
02 
03 int main()
04 {
05 #ifdef HAVE_STDIO_H
06    printf("version is %d.%d.%dn", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
07 #ifndef HAVE_BLUB_H
08    printf("no blub.h foundn");
09 #endif
10 #endif
11    return 0;
12 }

Namenskonventionen

Zeile 1 von Listing 7a bindet ein Cmake-Modul namens »CheckIncludeFiles« ein. Cmake hängt automatisch die Endung ».cmake« an, die zugehörige Datei heißt also »CheckIncludeFiles.cmake«. Die Datei gehört zur Cmake-Standardinstallation und stellt das Makro »check_include_files()« bereit. Diese Namensgebungskonvention wird überall eingehalten: Der Dateiname wird in Höckerschreibweise (Camel Case) geschrieben, die enthaltenen Funktionen haben den gleichen Namen, allerdings im C-Stil, also jeweils durch Unterstriche verbundene Wörter.

Der Aufruf von »check_include_files()« in Zeile 3 bewirkt, dass Cmake durch das Kompilieren eines kleinen Testprogramms testet, ob der Header »stdio.h« vorhanden ist. Das Ergebnis schreibt es dann in die Variable »HAVE_STDIO_H«. Das Gleiche gilt für den Header »blub.h« in Zeile 4. In den Zeilen 6, 7 und 8 trägt das Skript die dreiteilige Versionsnummer 1.0.2 in entsprechende Variablen ein.

Der Befehl »configure_file()« in Zeile 10 liest die Datei »config.h.in« ein, nimmt Ersetzungen vor und schreibt den modifizierten Inhalt in die Datei »${CMAKE_CURRENT_BINARY_DIR}/config.h«. Die Anweisung »configure_file()« ersetzt alle Cmake-Variablen, also »${V_MAJOR}«, »${V_MINOR}« sowie »${V_PATCH}« durch ihren Wert. Zusätzlich ersetzt das Skript »#cmakedefine« durch »#define«, wenn die entsprechende Variable »TRUE« ist, bei »FALSE« durch ein auskommentiertes »#undef«.

So lassen sich Header erzeugen, die genau die jeweiligen Systemeigenschaften beschreiben und damit die Grundlage für portable Programme bilden. Die Anweisung »check_include_files()« ist natürlich nicht das einzige von Cmake bereitgestellte Makro. Es gibt Makros für viele Zwecke: zum Überprüfen von Variablengrößen oder der Member-Variablen von Strukturen, der Compiler-Optionen und einige andere Zwecke.

Neue Dateien beim Build

Eine der anspruchsvollen Aufgaben eines Buildsystems ist es, erst während des Buildvorgangs neue Dateien zu erzeugen, die es daraufhin kompiliert. Das ist zum Beispiel bei der Arbeit mit dem Corba-IDL-Compiler, dem Metaobject-Compiler »moc« von Qt oder auch mit Flex und Bison notwendig. Zur Demonstration genügt ein triviales Beispiel (Listing 8), das mit dem Befehl »cp« eine Datei erzeugt (nicht portabel).

Listing 8: Dateien erzeugen

01 add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.h
02                    COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.source ${CMAKE_CURRENT_BINARY_DIR}/generated.h
03                    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.source )
04 
05 add_executable(hello main.c ${CMAKE_CURRENT_BINARY_DIR}/generated.h)

Der Befehl »add_custom_command()« in Zeile 1 spezifiziert eine Anweisung, die die unter »OUTPUT« aufgeführte Datei »generated.h« erzeugen kann, den dazu notwendigen Befehl und von welcher Datei die zu generierende Datei anhängt, wann sie also neu erzeugt werden muss. Wie erwähnt sollten Programmierer Dateien nie im Quelltextbaum, sondern immer im Buildverzeichnis erzeugen, dafür sorgt hier »CMAKE_CURRENT_BINARY_DIR«. Wichtig ist, die generierte Datei zu den Quelldateien für das Target hinzuzufügen. Nur dann erkennt Cmake die Abhängigkeit des Target von der generierten Datei und erzeugt sie gegebenenfalls vor dem Target.

Cmake ist keine komplette Programmiersprache, unterstützt aber Makros, um die Les- und Wartbarkeit zu erhöhen. Die Listings 9a und 9b zeigen ein einfaches Beispiel. Die Zeilen 1 bis 6 (Listing 9a) definieren ein Makro mit dem Namen »print_all_args«, das mindestens ein Argument »_greeting« erwartet. Ruft man es mit mehr Argumenten auf, hilft die spezielle Variable »ARGN« dabei, auf sie zuzugreifen. In den Zeilen 3 bis 5 iteriert eine »foreach()«-Schleife über die zusätzlichen Argumente und gibt sie aus. Die in Zeile 8 definierte Liste dient in Zeile 10 dem Makro als Argument. Viele Dinge lassen sich vereinfachen, indem Entwickler Komplexität in einem Makro verstecken, um sich danach nicht mehr um die Details kümmern zu müssen.

Listing 9a: Makros definiert

01 macro(print_all_args _greeting)
02    message(STATUS "${_greeting}")
03    foreach(_currentItem ${ARGN})
04       message(STATUS "current item: ${_currentItem}")
01    endforeach(_currentItem)
06 endmacro(print_all_args)
07 
08 set(fooSrcs main.cpp widget.cpp process.cpp)
09 
10 print_all_args("Hallo Welt" ${fooSrcs})

Listing 9b: Makros angewandt

01 ~/src/macrotest$ cmake .
02 -- Hallo Welt
03 -- current item: main.cpp
04 -- current item: widget.cpp
05 -- current item: process.cpp
06 -- Configuring done
07 -- Generating done
08 -- Build files have been written to: ~/src/macrotest

Fazit

Der Artikel hat die wichtigsten Features von Cmake vorgestellt, die als Einstieg für eigene Projekte genügen. Nicht zur Sprache kamen zum Beispiel das leistungsfähige und flexible »RPATH«-Handling von Cmake, die Unterstützung von Regular Expressions und das Ausführen von Programmen. Diese Funktionen zusammen ermöglichen es, mit Cmake sehr leistungsfähige Buildskripte zu schreiben. Informationsquellen nennt der Kasten “Dokumentation”. Da inzwischen das KDE-Projekt Cmake einsetzt, sind auch im KDE Subversion Repository [2] viele Beispiele für die Benutzung von Cmake zu finden.

Dokumentation

Cmake bringt eine Manpage mit, die alle Befehle und enthaltenen Module dokumentiert. Die gleiche Dokumentation findet sich noch einmal auf der Cmake-Homepage. Das Cmake-Wiki bietet darüber hinaus eine große Menge an zusätzlichen Informationen: Tutorials, eine Übersicht über Cmake-interne Variablen sowie eine FAQ.

Wenn das alles nicht reicht, gibt es noch die Mailingliste [cmake@cmake.org], auf der auch die Entwickler vertreten sind und Fragen beantworten. Wer Papier bevorzugt, findet im Buchhandel “Mastering Cmake”, das auch Themen für Fortgeschrittene erläutert. Eine Vielzahl an Beispielen führt das Subversion-Repository von KDE [2].

In Cmake finden die C/C++-Entwickler ein Werkzeug, das den Buildvorgang von einem kaum noch durchschaubaren Mysterium wieder zum alltäglichen Handwerk macht, sodass sie sich wieder voll auf den eigentlichen Code konzentrieren können. Cmake wird aktiv entwickelt und die Entwickler- und Anwendergemeinde wächst rapide. Fast jeden Tag kündigt ein weiteres Projekt an, auf Cmake umzusteigen.

Es lohnt sich für jeden, der die Autotools nicht vollständig beherrscht, in Cmake einzusteigen, um seine Software wieder selbst kompilieren zu können – und das nicht nur unter Linux, sondern sofort auch unter Mac OS X und MS Windows. Viele Anzeichen deuten darauf hin, dass Cmake auf dem besten Weg ist, das neue Standard-Buildsystem für freie Software zu werden. (ofr)

Infos

[1] Cmake: [http://www.cmake.org]

[2] KDE Subversion Repository: [http://websvn.kde.org/trunk/KDE/kdelibs/cmake/modules/]

[3] Kitware Inc.: [http://www.kitware.com]

[4] Insight Segmentation and Registration Toolkit: [http://www.itk.org]

[5] Valgrind: [http://www.valgrind.org]

[6] KWStyle: [http://public.kitware.com/KWStyle]

[7] Cmake-Dashboard: [http://www.cmake.org/Testing/Dashboard/MostRecentResults-Nightly/Dashboard.html]

Der Autor

Alexander Neundorf hat 2002 sein Diplom im Studiengang Ingenieurinformatik an der TU Ilmenau erworben. Seither beschäftigt er sich bei der Jenoptik LOS GmbH als Entwickler mit digitalen Kameras für den professionellen Einsatz. Seit 1998 ist er aktiv am KDE Projekt beteiligt und seit 2006 Maintainer des Cmake-basierten Buildsystems für KDE 4.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 7 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben