Wie in weitestgehend allen Programmiersprachen, so gibt es auch in CMake Variablen. Es gibt klassische lokale Variablen, Cache-Variablen und es kann auf Umgebungsvariablen des Systems zugegriffen werden. Es können auch Listen von Variablen erzeugt werden, deren Handhabung jedoch etwas gewöhnungsbedürftig ist. In diesem Artikel zeige ich euch, wie man Variablen in CMake verwendet und gebe dazu einige Beispiele. Auf YouTube findet ihr zu diesem Thema ebenfalls ein paar Videos von mir:
Diesen und andere Beiträge von mir kannst du als sauber formatierte PDF-Datei zum Ausdrucken oder offline Lesen erwerben. Mehr Informationen dazu findest du hier.
Nötige Vorkenntnisse
- CMake Grundkenntnisse: Dazu gehört etwa die Erstellung und Kompilierung eines ausführbaren Programms mit CMake. Ihr solltet dahin gehend auch mit den CMake-Befehlen cmake_minimum_required(), project() und add_executable() vertraut sein. Solltet ihr nicht wissen, wie ihr ein Programm mit CMake erstellt und kompiliert, schaut euch am besten zuvor diesen Artikel oder dieses YouTube-Video von mir an.
- CMake-Properties: In diesem Video auf YouTube gehe ich auf Properties in CMake ein. Zudem findet man eine Auflistung von Properties in der CMake-Dokumentation, eine genauere Erläuterung, was Properties eigentlich sind, fehlt hier jedoch.
- Funktionen: Die Erstellung einer Funktion in CMake mittels des function()-Befehls ist relativ selbsterklärend. Benötigt ihr weitere Informationen, schaut am besten in die CMake-Dokumentation.
- Unterordner einbinden: In CMake können Unterordner, die eine CMakeLists.txt-Datei enthalten, mittels des add_subdirectory()-Befehls eingebunden werden. Vereinfacht gesagt kann man sich vorstellen, dass die eingebundene CMakeLists.txt-Datei an die Stelle des add_subdirectory()-Befehls kopiert wird. Falls ihr weitere Informationen benötigt, findet ihr diese auch in der CMake-Dokumentation.
- If-Verzweigung: If-Verzweigungen in CMake unterscheiden sich generell nicht von if-Verzweigungen in anderen Programmiersprachen und sollten daher wohl den meisten Lesern geläufig sein. If-Verzweigungen in CMake erwähne ich kurz in diesem YouTube-Video, ansonsten hilft hier natürlich auch wieder die CMake-Dokumentation weiter.
- Alle hier benötigten Vorkenntnisse findet ihr auch in meinem Buch CMake für Einsteiger.
Lokale Variablen
In CMake können die Werte von lokalen Variablen mittels des set()-Befehls gesetzt werden:
set(<NameVar> <Wert> [PARENT_SCOPE])
Nach dem Namen der Variablen <NameVar> folgt der Wert <Wert> der in der Variablen gespeichert werden soll. Variablen haben in CMake stets den Datentyp String. Zwar werten manche CMake-Befehle die Variablen als einen anderen Datentyp (z.B. einen Integer) aus, aber im Grunde sind alle CMake-Variablen Strings. Es ist aber nicht notwendig den Wert <Wert> in Anführungszeichen zu setzen, wie es in vielen anderen Programmiersprachen der Fall ist, außer der Wert <Wert> enthält Leerzeichen. Ist es möglich, dass der gespeicherte Werte Leerzeichen enthalten kann, so wie es zum Beispiel bei Pfadangaben häufig der Fall ist, so sollten in jedem Fall Anführungszeichen verwendet werden.
Das Keyword PARENT_SCOPE wird im weiter unten liegenden Abschnitt „Variablenscope“ erläutert.
CMake-Variablen
CMake erzeugt selbst eine Vielzahl von Variablen, die verwendet und teilweise mittels des set()-Befehls verändert werden können. Zum Beispiel CMAKE_VERSION, in der die verwendete CMake-Version oder CMAKE_BUILD_TYPE, in der für Singlekonfigurationsgeneratoren, wie Makefile oder Ninja, die aktuelle verwendete Build-Konfiguration gespeichert ist. Eine Übersicht über alle CMake-Variablen könnt ihr in der CMake-Dokumentation nachlesen. Recht häufig, aber nicht immer, beginnen CMake-Variablen mit dem Präfix CMAKE_, daher sollte man darauf verzichten, eigene Variablen mit diesem Präfix in CMake zu definieren.
Häufig sind die Werte, die in CMake-Variablen gespeichert sind, die Default-Werte für CMake-Properties. Properties, oder übersetzt „Eigenschaften“, sind Variablen im Grunde sehr ähnlich. Sie besitzen einen Wert, welcher während der Laufzeit verändert werden kann. Im Gegensatz zu Variablen sind Properties jedoch an ein „CMake-Objekt“ gebunden. Es gibt verschiedene CMake-Objekte, doch an dieser Stelle ist vorwiegend das CMake-Objekt Target interessant. Targets besitzen eine Fülle von Properties, die die Kompilierung des Targets beeinflussen.
Bei der Namensgebung von CMake-Variablen und -Properties, die in Beziehung zueinander stehen, geht CMake meist den folgenden Weg: Die Variable hat den Namen CMAKE_ plus den Namen der Property. Hat eine Target Property etwa den Namen CXX_STANDARD, so wird der Default-Wert dieser Property bestimmt durch den Wert in der Variablen CMAKE_CXX_STANDARD.
Variablenscope
Ein Scope bezeichnet in der Programmierung einen Bereich, in dem ein Programmmodul (damit meine ich eine Variable, eine Funktion usw.) unter seiner eindeutigen Bezeichnung (also Variablennamen, Funktionsnamen etc.) bekannt ist. In CMake werden an verschiedenen Stellen neue Variablenscopes erzeugt:
- Das Erstellen einer Funktion mit dem Befehl function() erzeugt innerhalb dieser Funktion einen neuen Variablenscope. Zuvor vorhandene Variablen beim Aufruf der Funktion sind auch innerhalb des neuen Variablenscopes in der Funktion verfügbar.
- Innerhalb einer CMakeLists.txt-Datei wird ein neuer Variablenscope erzeugt. Wird nun eine weitere CMakeLists.txt-Datei durch den add_subdirectoy()-Befehl eingebunden, werden alle Variablen aus dem derzeitigen Scope in die eingebundene CMakeLists.txt-Datei übernommen und einer neuer Variablenscope erzeugt.
- In CMake gibt es zusätzlich Cache-Variablen, die über mehrere CMake Aufrufe verfügbar sind und damit gewissermaßen einen eigenen Variablenscope bilden. Weiter unten in diesem Artikel gehe ich noch einmal genauer auf Cache-Variablen ein.
Variablen, die innerhalb einer Funktion oder einer eingebunden CMakeLists.txt-Datei erzeugt oder verändert werden, werden nicht in den darüberliegenden (Parent) Variablenscope übernommen. Es sei den, bei Verwendung des set()-Befehls wird das optionale Keyword PARENT_SCOPE mit angegeben. In diesem Fall wird die Variable im darüberliegenden Variablenscope erzeugt bzw. verändert und der Wert im aktuellen Variablenscope bleibt unverändert. In den weiter unten liegenden Beispielen wird dieses Verhalten genauer erläutert.
Variablenwert auslesen
Um den Wert einer CMake-Variablen auszulesen, müsst ihr mittels des $-Operators und geschweiften Klammern auf die Variable zugreifen. Um etwa den Wert der Variablen CMAKE_BUILD_TYPE auszulesen und mittels des message()-Befehls auszugeben, könnt ihr den folgenden Codeschnipsel verwenden:
message("CMAKE_BUILD_TYPE = ${CMAKE_BUILD_TYPE}")
Variablen löschen
Vor der Verwendung einer Variable in CMake ist es nicht notwendig, dass diese definiert respektive ihr ein Wert zugewiesen wurde. Diese Variablen enthalten dann einfach einen leeren String. Möchte man daher den Wert einer Variablen löschen, kann man einfach den set()-Befehl verwenden und der Variable keinen Wert respektive einen leeren String zu weisen. Den gleichen Effekt hat übrigens der unset()-Befehl.
unset(MEINE_VARIABLE)
Beispiel 1: Lokale und CMake-Variablen
Im folgenden Beispiel verwende ich lokal erstellte Variablen, um die benötigte Source-Datei zu speichern. Zusätzlich verwende ich CMake-Variablen, um den C++17-Standard für die erstellte, ausführbare Datei zu aktivieren und den Namen der ausführbaren Datei festzulegen. Blicken wir dazu einmal auf die CMakeLists.txt-Datei:
cmake_minimum_required(VERSION 3.7)
project(cmake_local_var LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(SOURCE_FILE main.cpp)
add_executable(${PROJECT_NAME}_exe ${SOURCE_FILE})
In den Zeilen 5-7 werden die Werte der CMake-Variablen CMAKE_CXX_STANDARD, CMAKE_CXX_STANDARD_REQUIRED und CMAKE_CXX_EXTENSIONS gesetzt. Der Wert 17 in der Variablen CMAKE_CXX_STANDARD gibt an, dass der C++-17-Standard verwendet werden soll. Mit dem Setzen der Variablen CMAKE_CXX_STANDARD_REQUIRED auf ON wird eine Fehlermeldung ausgegeben, falls der verwendete Compiler den gewünschten C++-Standard nicht unterstützt. Über die Variable CMAKE_CXX_EXTENSIONS kann eingestellt werden, ob der erweiterte C++-Standard (falls verfügbar) verwendet werden soll oder nicht. In diesem Artikel gehe ich ausführlicher auf diese drei Variablen ein.
Die Werte, die in diesen drei Variablen gespeichert sind, sind die Default-Werte für die Target-Properties CXX_STANDARD, CXX_STANDARD_REQUIRED und CXX_EXTENSIONS. Somit werden die in den Zeilen 5-7 gespeicherten Werte automatisch in die Properties der erstellten ausführbaren Datei, also eines Targets, in Zeile 11 übertragen. Somit wird dieses Target mit dem C++17-Standard gebaut.
In Zeile 9 wird der lokalen Variable SOURCE_FILE der Dateiname main.cpp zugewiesen. Diese Variable wird in Zeile 11 verwendet, um der ausführbaren Datei die in der Variablen gespeicherten Datei als Source-Datei hinzuzufügen. Der Name der ausführbaren Datei wird aus dem Namen des Projektes PROJECT_NAME und dem Postfix _exe gebildet. Die Variable PROJECT_NAME wird durch den project()-Befehl mit dem übergebenen Namen des Projektes, hier cmake_local_var, befüllt. Somit lautet der gesamte Name der ausführbaren Datei cmake_local_var_exe.
Werfen wir nun einen kurzen Blick in die main.cpp-Datei:
#include <iostream>
int main()
{
int position[2] = {1, 2};
auto [x, y] = position;
std::cout << "Position: " << x << " " << y << std::endl;
return 0;
}
In Zeile 6 wird das sogenannte „structured binding“ aus dem C++17-Standard verwendet, um den Wert aus position[0] in der Variablen x und den Wert von position[1] in der Variablen y zu speichern. Anschließend werden die Werte aus den Variablen x und y in Zeile 8 auf der Konsole ausgegeben.
Da es das erste Beispiel in diesem Beitrag ist, zeige ich kurz den Build-Prozess unter Ubuntu 20.04, auch wenn dieser an sich keine spannenden Ausgaben hervorbringt. Ich gehe dabei davon aus, dass wir uns in einem Build-Ordner <BUILD_FOLDER> innerhalb des Projektes befinden. Die oben gezeigte CMakeLists.txt-Datei liegt also im darüberliegenden Ordner.
<BUILD_FOLDER>$ cmake ../
-- The CXX compiler identification is GNU 9.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: <BUILD_FOLDER>
<BUILD_FOLDER>$ make
Scanning dependencies of target cmake_local_var_exe
[ 50%] Building CXX object CMakeFiles/cmake_local_var_exe.dir/main.cpp.o
[100%] Linking CXX executable cmake_local_var_exe
[100%] Built target cmake_local_var_exe
In Zeile 16 der Ausgabe sehen wir, dass das Target cmake_local_var_exe erstellt wurde. Führen wir dieses nun einmal aus, sehen wir das die Verwendung des „structured binding“ wie erwartet funktioniert hat.
<BUILD_FOLDER>$ ./cmake_local_var_exe
Position: 1 2
Beispiel 2: Variablen in Funktionen
In diesem Beispiel schauen wir uns einmal die Verwendung von Variablen in Funktionen an. Dabei erstelle ich eine simple Funktion, in der ich zwei Variablen setze. Eine davon lokal im Scope der Funktion und eine im Parent-Scope. Vor, innerhalb und nach der Funktion geben wir uns einmal die Werte dieser beiden Variablen aus. Werfen wir nun zunächst einen Blick in die zugehörige CMakeLists.txt-Datei:
function(var_test)
set(FUNC_VAR 1)
set(PARENT_VAR 2 PARENT_SCOPE)
message("In Funktion FUNC_VAR: ${FUNC_VAR}")
message("In Funktion PARENT_VAR: ${PARENT_VAR}\n")
endfunction()
cmake_minimum_required(VERSION 3.7)
project(cmake_function_var LANGUAGES CXX)
message("Vor Funktion FUNC_VAR: ${FUNC_VAR}")
message("Vor Funktion PARENT_VAR: ${PARENT_VAR}\n")
var_test()
message("Nach Funktion FUNC_VAR: ${FUNC_VAR}")
message("Nach Funktion PARENT_VAR: ${PARENT_VAR}\n")
add_executable(hello_world main.cpp)
Die CMakeLists.txt-Datei beginnt mit der Definition der Funktion var_test in den Zeilen 1 bis 7 mittels des function()-Befehls. In dieser Funktion wird in Zeile 2 die lokale Variable FUNC_VAR auf den Wert 1 gesetzt. In Zeile 3 wird der Wert der Variablen PARENT_VAR auf 2 gesetzt, jedoch im darüberliegenden Scope, also in dem Scope, der die Funktion aufruft. Dies wird durch die Verwendung des Keywords PARENT_SCOPE im set()-Befehl erreicht. In den Zeilen 5 und 6 gebe ich jeweils die Werte dieser beiden Variablen aus.
Zeile 9 und 11 sollten euch bekannt sein. In Zeile 13 und 14 gebe ich ebenfalls die beiden Variablen FUNC_VAR und PARENT_VAR aus. Anschließend wird in Zeile 16 die oben definierte Funktion var_test aufgerufen und anschließend erneut die beiden Variablen in Zeile 18 und 19 ausgegeben. In Zeile 21 wird eine einfache ausführbare Datei mit einem Hello-World-Code kompiliert, was für dieses Beispiel nicht relevant ist. Schauen wir uns daher nun die Ausgabe des cmake-Befehls an, wobei ich unwichtige Passagen mit … abkürze.
<BUILD_FOLDER>$ cmake ../
...
Vor Funktion FUNC_VAR:
Vor Funktion PARENT_VAR:
In Funktion FUNC_VAR: 1
In Funktion PARENT_VAR:
Nach Funktion FUNC_VAR:
Nach Funktion PARENT_VAR: 2
...
-- Build files have been written to: <BUILD_FOLDER>
Vor dem Aufruf der Funktion sind die beiden Variablen FUNC_VAR und PARENT_VAR logischerweise leer bzw. enthalten einen leeren String, siehe Zeile 3 und 4. Innerhalb der Funktion selbst wurde die Variable FUNC_VAR auf 1 gesetzt und dies wird dann auch in Zeile 6 so ausgegeben. Der Wert 2 für die Variable PARENT_VAR wurde für den darüberliegenden Scope gesetzt, daher bleibt der Wert im aktuellen Scope, der Scope der Funktion, unverändert. Da dort nichts gespeichert war, wird in Zeile 7 dementsprechend kein Wert ausgegeben.
Nach dem Aufruf der Funktion, jetzt wieder im main Scope, ist die Variable FUNC_VAR leer, siehe Zeile 9. Das liegt daran, dass Änderungen in Variablen in darunterliegenden Scopes keine Auswirkungen auf den aktuellen Scope haben. Hingegen ist hier nun der Wert 2 in der PARENT_VAR Variablen gespeichert (Zeile 10), wie es in der Funktion var_test definiert wurde.
Beispiel 3: Variablen in eingebundenen CMakeLists.txt-Dateien
In diesem Beispiel schauen wir uns das Verhalten von Variablen in einer mittels des add_subdirectory()-Befehls eingebundenen CMakeLists.txt-Datei an. Dazu erstelle ich mehrere Variablen und geben deren Werte aus. Zum Verständnis muss ich kurz die Ordner- und Dateistruktur dieses Beispiels erläutern. Wie üblich befindet sich an oberster Stelle im Ordner dieses Beispiels eine CMakeLists.txt-Datei. Zusätzlich gibt es noch einen Unterordner src in dem sich eine main.cpp-Datei befindet und einen weiteren Unterordner subfolder mit einer zusätzlichen CMakeLists.txt-Datei. Schauen wir uns zunächst die CMakeLists.txt-Datei aus dem Hauptordner an:
cmake_minimum_required(VERSION 3.7...3.22)
project(main_project LANGUAGES CXX)
set(MAIN_VAR 2)
message("Minimale CMake Version Hauptordner: ${CMAKE_MINIMUM_REQUIRED_VERSION}")
message("Vor add_subdirectory: MAIN_VAR=${MAIN_VAR}")
message("Vor add_subdirectory: SUB_VAR=${SUB_VAR}")
message("Vor add_subdirectory: PARENT_VAR=${PARENT_VAR}\n")
add_subdirectory(subfolder)
message("Minimale CMake Version Hauptordner: ${CMAKE_MINIMUM_REQUIRED_VERSION}")
message("Nach add_subdirectory: MAIN_VAR=${MAIN_VAR}")
message("Nach add_subdirectory: SUB_VAR=${SUB_VAR}")
message("Nach add_subdirectory: PARENT_VAR=${PARENT_VAR}")
add_executable(hello_world src/main.cpp)
In Zeile 5 wird der Variablen MAIN_VAR der Wert 2 zugewiesen. In der Variablen CMAKE_MINIMUM_REQUIRED_VERSION, die durch den cmake_minimum_required()-Befehl in Zeile 1 gesetzt wird, wird die minimal benötigte CMake Version gespeichert. Der Wert dieser Variablen wird in Zeile 7 ausgegeben. Zusätzlich gebe ich den Wert der Variablen MAIN_VAR in Zeile 8 und den Wert zwei weiterer Variablen (SUB_VAR und PARENT_VAR) in den beiden folgenden Zeilen aus.
Die beiden zusätzlichen Variablen werden in der CMakeLists.txt-Datei aus dem Unterordner subfolder gesetzt, der mittels dem add_subdirectory()-Befehl in Zeile 12 eingebunden wird. Anschließend werden in den Zeilen 14-17 die gleichen vier Variablen erneut ausgegeben. Blicken wir nun in die eingebundene CMakeLists.txt-Datei.
cmake_minimum_required(VERSION 3.8...3.22)
project(sub_project LANGUAGES CXX)
set(SUB_VAR 4)
set(PARENT_VAR 6 PARENT_SCOPE)
message("Minimale CMake Version Unterordner: ${CMAKE_MINIMUM_REQUIRED_VERSION}")
message("In Unterordner: MAIN_VAR=${MAIN_VAR}")
message("In Unterordner: SUB_VAR=${SUB_VAR}")
message("In Unterordner: PARENT_VAR=${PARENT_VAR}\n")
In den Zeilen 4 und 6 wird den beiden eben schon erwähnten Variablen SUB_VAR und PARENT_VAR der Wert 4 respektive 6 zugewiesen. Jedoch gilt dies bei der Variable PARENT_VAR durch Verwendung des Keywords PARENT_SCOPE nur für den darüberliegenden Scope, also innerhalb der eben gezeigten CMakeLists.txt-Datei, die diese CMakeLists.txt-Datei eingebunden hat. Abschließend gebe ich auch hier alle vier Variablen einmal aus.
Blicken wir daher jetzt auf die Ausgabe bei Ausführung des cmake-Befehls:
$ cmake ..
...
Minimale CMake Version Hauptordner: 3.7
Vor add_subdirectory: MAIN_VAR=2
Vor add_subdirectory: SUB_VAR=
Vor add_subdirectory: PARENT_VAR=
Minimale CMake Version Unterordner: 3.8
In Unterordner: MAIN_VAR=2
In Unterordner: SUB_VAR=4
In Unterordner: PARENT_VAR=
Minimale CMake Version Hauptordner: 3.7
Nach add_subdirectory: MAIN_VAR=2
Nach add_subdirectory: SUB_VAR=
Nach add_subdirectory: PARENT_VAR=6
...
Zeile 3-6 zeigt die Ausgabe aus der ersten CMakeLists.txt-Datei. Die minimale CMake-Version ist 3.7, wie es in der ersten Zeile in der ersten CMakeLists.txt-Datei gesetzt wurde. MAIN_VAR enthält wenig überraschen den Wert 2 und da den beiden anderen Variablen noch kein Wert zugewiesen wurde, enthalten diese folglich einen leeren String.
In Zeile 8-11 folgt dann die Ausgabe aus dem Unterordner. Da durch den in Zeile 1 in der zweiten CMakeLists.txt-Datei verwendeten cmake_minimum_required()-Befehl die Variable CMAKE_MINIMUM_REQUIRED_VERSION überschrieben wird und den neuen Wert 3.8 erhält, wird auch dieser an dieser Stelle ausgegeben. Der Wert 2 in der Variablen MAIN_VAR wurde aus dem vorherigen Scope übernommen und der Wert 4 der Variablen SUB_VAR wurde in diesem Scope gesetzt. Da der Wert 6 für die Variable PARENT_VAR nur für den darüberliegenden Scope gesetzt wurde, enthält sie an dieser Stelle nur einen leeren String.
Abschließend werden in den Zeilen 13-16 die vier Variablen aus der ersten CMakeLists.txt-Datei erneut ausgegeben. Da sich Änderungen an Variablen ohne Verwendung des Keywords PARENT_SCOPE nicht auf den aufrufenden Scope auswirken, enthalten die Variablen CMAKE_MINIMUM_REQUIRED_VERSION und SUB_VAR ihren ursprünglichen Wert. Hingegen enthält nun die Variable PARENT_VAR den Wert 6, der ihr im untergeordneten Scope zugewiesen wurde.
Umgebungsvariablen
In CMake können auch Umgebungsvariablen des Systems verwendet werden. Dazu wird eine modifizierte Form der gewöhnlichen Variablen Notation mittels des Keywords ENV genutzt, zum Beispiel $ENV{UMGEBUNGSVARIABLE}. Genauso können auch Umgebungsvariablen neue Werte mittels des set()-Befehls zugewiesen werden.
Jedoch gilt diese Änderung nur innerhalb des aktuell laufenden CMake-Scripts und die Systemvariable selbst wird nicht verändert. Daher ist das Ändern von Umgebungsvariablen innerhalb eines CMake-Scripts nur selten nützlich und es sollte abgewogen werden, ob dies überhaupt notwendig ist. Oft ist die Verwendung klassischer CMake-Variablen an dieser Stelle sinnvoller.
Beispiel 4: Umgebungsvariablen auslesen
In diesem Beispiel lesen wir einige Umgebungsvariablen des Systems aus. Da auf unterschiedlichen Betriebssystemen andere Umgebungsvariablen gesetzt werden, werden bei euch vermutlich, wie bei mir weiter unten auch zu sehen, einige dieser Variablen leer sein.
cmake_minimum_required(VERSION 3.7...3.22)
project(env_var_project LANGUAGES CXX)
message("HOME = $ENV{HOME}")
message("PATH = $ENV{PATH}")
message("TEMP = $ENV{TEMP}")
message("USER = $ENV{USER}")
message("LANG = $ENV{LANG}")
add_executable(hello_world src/main.cpp)
Zu diesem Beispiel gibt es eigentlich nicht viel zu sagen. In den Zeilen 5-9 lese ich die Werte einiger typischer Umgebungsvariablen aus. Auf meinem System (Ubuntu 20.04) sieht die Ausgabe dann wie folgt aus:
$ cmake ../
...
HOME = /home/mschoos
PATH = /home/mschoos/.local/bin:/usr/local/sbin: ...
TEMP =
USER = mschoos
LANG = en_US.UTF-8
...
Die Ausgabe der PATH-Umgebungsvariable in Zeile 4 habe ich etwas abgekürzt. Auf Ubuntu existiert standardmäßig keine Umgebungsvariable TEMP, außer man hat diese selbst angelegt, daher ist die Ausgabe in Zeile 5 ein leerer String. Die Ausgaben auf eurem System werden sich entsprechend unterscheiden, insbesondere falls du ein anderes Betriebssystem verwenden solltest.
Cache-Variablen
CMake kann Variablen über mehrere Durchläufe eines CMake-Scriptes speichern respektive cachen. Dies geschieht in der automatisch erstellten CMakeCache.txt-Datei, welche sich im Build-Ordner befindet. CMake speichert automatisch bereits eine große Anzahl an sogenannten Cache-Variablen in dieser Datei. Da es sich um eine einfache Text-Datei handelt, kann man diese problemlos auslesen. Hier einmal ein kleiner Ausschnitt aus einer solchen Datei:
...
//Enable/Disable color output during build.
CMAKE_COLOR_MAKEFILE:BOOL=ON
//CXX compiler
CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/c++
//A wrapper around 'ar' adding the appropriate '--plugin' option
// for the GCC compiler
CMAKE_CXX_COMPILER_AR:FILEPATH=/usr/bin/gcc-ar-9
...
Genau wie in C++ dient die Zeichenfolge // dazu, einen Kommentar einzuleiten. Betrachten wir Zeile 6 einmal genauer. Zu Beginn der Zeile steht der Name der Cache-Variablen CMAKE_CXX_COMPILER. Es folgt ein Doppelpunkt und anschließend der Typ der Cache-Variablen, in diesem Falle FILEPATH. Zwar sind auch alle Cache-Variablen genau genommen Strings, dennoch gibt es verschiedene Typen von Cache-Variablen, auf die ich weiter unten noch einmal genauer zu sprechen komme. Es folgt ein Gleichheitszeichen und anschließend der Wert /usr/bin/c++, der in der Cache-Variablen gespeichert ist.
CMake-GUI
CMake bietet eine grafische Benutzeroberfläche, in der die in der CMakeCache.txt-Datei gespeicherten Cache-Variablen angezeigt und verändert werden können. Zudem können hier neue Cache-Variablen hinzugefügt werden.
In den beiden oberen Zeilen der GUI muss der Source-Ordner, das heißt der Ordner in dem die CMakeLists.txt-Datei liegt und der zugehörige Build-Ordner ausgewählt werden. In der Mitte der GUI werden dann die vorhandenen Cache-Variablen angezeigt. Im Bereich unten befindet sich der Output, der mit Klick auf den Configure-Button erzeugt werden kann. Falls ihr an dieser Stelle nur eine geringe Anzahl an Cache-Variablen angezeigt bekommt, müsst ihr die Checkbox Advanced (oberes Drittel, leicht rechts) aktivieren.
Der Wert einer jeden Cache-Variable kann hier geändert werden und über den Button Add Entry (rechts neben der Advanced-Checkbox) respektive Remove Entry können Cache-Variablen hinzugefügt respektive entfernt werden. Je nach Typ der Cache-Variablen werden diese anders dargestellt und die Art wie man ihren Wert ändert, unterscheidet sich.
Typen von Cache-Variablen
BOOL: Eine Cache-Variable des Typs BOOL ist eine klassische boolesche Variable, deren Wert entweder wahr oder falsch ist. Da alle Variablen in CMake Strings sind, wird auch ein boolescher Wert in CMake als String gespeichert. Diese können in CMake verschiedene Formen annehmen: TRUE/FALSE, ON/OFF, 1/0 und ein paar mehr. In der CMake-Dokumentation zur if-Verzweigung findet ihr dazu eine Übersicht. In der CMake-GUI wird diese Art der Cache-Variable als Checkbox dargestellt, siehe die Cache-Variable CMAKE_COLOR_MAKEFILE in der oberen Abbildung.
FILEPATH: In einer Cache-Variable des Typs FILEPATH ist der Pfad zu einer Datei auf der Festplatte gespeichert. In der CMake-GUI kann an dieser Stelle der Explorer geöffnet und eine Datei ausgewählt werden.
PATH: Eine Cache-Variable des Typs PATH ist ähnlich wie FILEPATH Cache-Variable, jedoch wird in dieser Variable nur ein Pfad und keine Datei gespeichert. Auch hier öffnet sich in der CMake-GUI der Explorer und ein Pfad kann ausgewählt werden.
STRING: Cache-Variablen des Typs STRING speichern einen beliebigen String. In der CMake-GUI erscheint dann an dieser Stelle entweder ein Dropdown-Menü um aus einer Liste von vordefinierten Werten wählen zu können oder ein editierbares Textfeld um den Wert der Variable zu ändern, falls keine vordefinierten Werte existieren. Vordefinierte Werte können als CMake-Liste in der Property STRINGS einer jeden Cache-Variablen gespeichert werden.
INTERNAL: Sollen Werte über mehrere CMake-Aufrufe gespeichert werden, die aber nicht von den Anwendenden verändert werden sollen, so kann eine Cache-Variable des Typs INTERNAL verwendet werden. Cache-Variablen dieses Typs werden nicht in der CMake-GUI angezeigt.
Cache-Variablen innerhalb der CMakeLists.txt
Um Cache-Variablen innerhalb der CMakeLists.txt-Datei zu erzeugen und einen Wert zuzuweisen, muss eine Variante des weiter oben eingeführten set()-Befehls verwendet werden.
set(<CacheVariable>
<Wert>
CACHE <VariablenTyp>
<Kommentarstring>
[FORCE]
)
Die ersten beiden Argumente, der Name der Variablen und den Wert, den der Variable zugewiesen wird, sind analog zur normalen Variablendeklaration. Anschließend folgt das Keyword CACHE, welches CMake mitteilt, dass an dieser Stelle eine Cache-Variable definiert werden soll.
Die darauffolgenden Argumente <VariablenTyp> und <Kommentarstring> haben keinerlei Einfluss auf das Verhalten der Cache-Variablen im CMake-Script. Der Typ der Variablen ist eine der oben genannten Typen und beeinflusst das Verhalten in der CMake-GUI. Der Kommentar wird als Tooltip in der CMake-GUI beim Hovern über der jeweiligen Cache-Variablen angezeigt.
Das optionale Keyword FORCE entscheidet darüber, ob eine Cache-Variable überschrieben wird, falls sie schon existiert. Wenn das FORCE-Keyword verwendet wird, dann wird die Cache-Variable überschrieben, ansonsten nicht. Das steht im Gegensatz zur Änderung von lokalen Variablen mittels des set()-Befehls, denn deren Wert wird immer überschrieben.
Lokale und Cache-Variablen sind in CMake zwei völlig unterschiedliche Arten von Variablen, daher ist es unter anderem möglich, dass eine lokale und eine Cache-Variable den gleichen Namen haben. Ist dies der Fall, nutzt CMake normalerweise die lokale und nicht die Cache-Variable, falls auf den Wert der Variablen zugegriffen wird. Wenn jedoch der Wert einer Cache-Variablen zuvor geändert wurde, beispielsweise weil diese noch nicht existierte oder mittels des FORCE-Keywords, dann nutzt CMake die Cache-Variable. Daher ist es sogar möglich, dass zwei aufeinanderfolgende CMake-Aufrufe unterschiedliche Ergebnisse hervorbringen. Entsprechend sollte man bei der Wahl der Variablennamen Vorsicht walten lassen, um dieses Problem zu umgehen.
Beispiel 5: Verwendung einer Cache-Variablen zur Erzeugung einer ausführbaren Datei
In diesem Beispiel erzeuge ich eine Cache-Variable des Typs BOOL und verwende den Wert dieser Cache-Variablen, um zu entscheiden, ob eine zweite ausführbare Datei erzeugt wird oder nicht.
cmake_minimum_required(VERSION 3.7...3.22)
project(cache_var_project LANGUAGES CXX)
set(BUILD_EXECUTABLE
TRUE
CACHE BOOL
"Erzeugt eine zweite ausführbare Datei."
)
add_executable(hello_world main.cpp)
if(${BUILD_EXECUTABLE})
add_executable(hello_world_2 main.cpp)
endif()
In den Zeilen 5-9 wird die Cache-Variable BUILD_EXECUTABLE des Typs BOOL erzeugt. Der Satz „Erzeugt eine zweite ausführbare Datei.“ wird als Tooltip in der CMake-GUI angezeigt. Der Wert von BUILD_EXECUTABLE wird auf TRUE gesetzt, allerdings nur, falls der Cache-Variablen zuvor kein Wert zugewiesen wurde, denn in diesem Aufruf wird auf das Keyword FORCE verzichtet.
In Zeile 13 wird der Wert der Cache-Variablen BUILD_EXECUTABLE in einer if-Verzweigung überprüft. Wird die Variable zu wahr ausgewertet, wird in Zeile 14 eine zweite ausführbare Datei erzeugt. Führt man diese CMakeLists.txt-Datei zum ersten Mal aus sieht die Build-Ausgabe wie folgt aus:
$ cmake ../
...
$ make
Scanning dependencies of target hello_world_2
[ 25%] Building CXX object CMakeFiles/hello_world_2.dir/main.cpp.o
[ 50%] Linking CXX executable hello_world_2
[ 50%] Built target hello_world_2
Scanning dependencies of target hello_world
[ 75%] Building CXX object CMakeFiles/hello_world.dir/main.cpp.o
[100%] Linking CXX executable hello_world
[100%] Built target hello_world
Wie ihr sehen könnt, werden zwei ausführbare Dateien hello_world und hello_world_2 erzeugt (Zeile 7 und 11). Ändert ihr nun beispielsweise über die CMake-GUI den Wert der Cache-Variablen BUILD_EXECUTABLE auf FALSE und führt cmake und make erneut aus, so wird nur eine ausführbare Datei erzeugt. Zuvor solltet ihr allerdings make clean ausführen, um die zuvor erstellten Dateien zu löschen.
Listen
In CMake sind Listen lediglich ein einzelner zusammenhängender String, in der die einzelnen Werte mittels eines Semikolons getrennt werden. Das kann das Arbeiten mit solchen Listen ziemlich mühselig gestalten. CMake bietet mit dem list()-Befehl jedoch die Möglichkeit, verschiedenste Operationen an einer Liste durchzuführen.
Innerhalb des list()-Befehls folgt zuerst ein Keyword, welches die Art der durchzuführenden Operation an der Liste definiert. Es gibt Keywords, um die Länge der Liste auszulesen, Werte hinzuzufügen oder zu löschen, die Liste zu sortieren usw. Eine vollständige Auflistung und Erklärung aller Befehle wäre für diesen Artikel zu umfangreich. Die Befehle sind jedoch in der Regel selbst erklärend, schaut daher einfach in die CMake-Dokumentation. Im folgenden Beispiel verwende ich beispielhaft ein paar dieser Befehle.
Beispiel 6: CMake-Liste erstellen und verändern
Ich erzeuge in diesem Beispiel zwei Listen und führe Operationen an diesen Listen mit dem list()-Befehl aus:
cmake_minimum_required(VERSION 3.15...3.22)
project(list_project LANGUAGES CXX)
set(FIBONACCI 0 1 1 2 3 5 8 13 21)
set(PRIM "2;3;5;7;11;13")
list(LENGTH FIBONACCI FIBONACCI_LENGTH)
message("FIBONACCI_LENGTH = ${FIBONACCI_LENGTH}")
list(APPEND PRIM 17 19)
message("PRIM = ${PRIM}")
list(POP_BACK FIBONACCI)
message("FIBONACCI = ${FIBONACCI}")
list(REVERSE PRIM)
message("PRIM = ${PRIM}")
add_executable(hello_world main.cpp)
In den Zeilen 5 und 6 erzeuge ich die beiden Listen FIBONACCI und PRIM. CMake-Listen können durch die beiden unterschiedlichen, hier dargestellten Arten, erzeugt werden. Entweder trennt man die einzelnen Elemente durch ein Leerzeichen wie in Zeile 5 oder durch ein Semikolon und setzt die gesamte Liste in Anführungszeichen wie in Zeile 6.
In Zeile 8 verwende ich das Keyword LENGTH im list()-Befehl, um die Länge der Liste FIBONACCI in die Variable FIBONACCI_LENGTH zu schreiben. In der folgenden Zeile gebe ich diesen Wert dann aus. Darauffolgend in Zeile 11 füge ich an das Ende der Liste PRIM die beiden Werte 17 und 19 unter Verwendung des Keywords APPEND an. Anschließend gebe ich die komplette Liste PRIM aus.
In Zeile 14 lösche ich den letzten Wert aus der Liste FIBONACCI durch Verwendung des Keywords POP_BACK. Da diese Funktion erst in CMake 3.15 eingeführt wurde, habe ich in Zeile 1 die minimal benötigte CMake-Version entsprechend angepasst. Anschließend gebe ich die Liste FIBONACCI aus. Schlussendlich drehe ich die Liste PRIM in Zeile 17 mithilfe des Keywords REVERSE um und gebe diese daraufhin aus.
Im folgenden Codeabschnitt zeige ich euch die Ausgabe dieses Beispiels:
$ cmake ../
...
FIBONACCI_LENGTH = 9
PRIM = 2;3;5;7;11;13;17;19
FIBONACCI = 0;1;1;2;3;5;8;13
PRIM = 19;17;13;11;7;5;3;2
...
Zusammenfassung CMake-Variablen
Da CMake-Variablen immer Variablen des Typs String sind, kann die Verwendung dieser Variablen etwas gewöhnungsbedürftig sein. Dies gilt insbesondere bei der Verwendung von CMake-Listen. Falls ihr Variablen über mehrere Aufrufe speichern müsst, greift auf Cache-Variablen zurück. Auch Umgebungsvariablen könnt ihr auslesen und verwenden, beachtet jedoch, dass ihr den Wert der Umgebungsvariable über das CMake Script hinaus nicht ändern könnt.
Mit den Erklärungen und Beispielen aus diesem Artikel solltet ihr keine größeren Probleme mehr haben, CMake-Variablen zu verwenden und mit ihnen zu arbeiten.
Weitere Informationen
Den Code zu diesem Beitrag könnt ihr gerne weiter verwenden, ihr findet ihn dazu auf GitHub.
In meinem Buch „CMake für Einsteiger“ und in meinen Videos auf YouTube stelle ich dieses und weitere CMake Themen noch einmal detaillierter vor. Bei Fragen oder Anmerkungen schreibt mir gerne einen Kommentar 🙂
Meine Webseite ist komplett werbefrei. Falls dir dieser Beitrag gefallen hat und du meine Arbeit gerne unterstützen möchtest, schau daher doch einmal auf meiner Support-Seite vorbei. Das würde mich sehr freuen :).