Visual Studio Code und Makefiles
Ich zeige wie man mit Visual Studio Code und Makefiles arbeitet um C++ Projekte zu erstellen die unabhängig von der IDE gebaut und installiert werden können.
Visual Studio Code und Makefiles
Ein Makefile beschreibt wie man den zugehörigen Source Code für ein System baut. Das Makefile ist dabei das Rezept für das Programm make. make gehört schon seit 1976 zum Toolset vom EDV Dienstleister, damit lässt sich beliebiger Source Code über ein Makefile bauen. Es beschreibt was (target) es tun soll, wie (rule) es das tun soll und welche Abhängigkeiten (andere targets) es dazu braucht. Makefile begegnen einem Entwickler hauptsächlich bei Open Source Projekten, ganz stark im Linux Umfeld. Sie sind vergleichbar mit Pipeline für die Azure Cloud. Durch dieses Regelwerk ist das Bauen und Installieren von Software aus dem Source Code durch lediglich zwei Befehle möglich:
make sudo make install
Sind alle Voraussetzungen erfüllt (Compiler und Bibliotheken vorhanden), dann ist nach einigen Minuten die gewünschte Software einsatzbereit.
Erstes Makefile
Für mein erstes Makefile war dieser Artikel sehr hilfreich. Er beschreibt gut und ausführlich den Aufbau. Am besten startet man gleich mit einem neuen einfachen Projekt. Ich habe begonnen mit ClanLib eines meiner Studiumsprojekte zu refactoren, das eignet sich gut dafür. Ursprünglich unter Windows 7 und Visual Studio erstellt entwickelt, baue ich es nun unter Ubuntu Linux und der freien IDE Visual Studio Code. Ich habe das Projekt bis auf das Minimum ausgeräumt, so dass nur noch ein neues Fenster mit ClanLib gerendert wird. Der fertige Code samt Makefile findest du unter folgendem GitHub Repo und Branch.
Tutorial
Mein Makefile habe ich basieren auf diesem Artikel zum Thema Visual Studio Code und C++ Entwicklung erstellt. Die Einteilung in include, src und bin Ordner habe ich nicht ganz so streng in nur src und bin geändert. Die ausführbare Datei wird im Hauptverzeichnis erstellt und nicht in bin, das hat den Grund, dass diese später genau so relativ zu weiteren Verzeichnissen mit Assets liegen wird. Welche Elemente beinhaltet nun mein Makefile und wofür benötigt man diese? Der erste Block besteht aus folgenden Definitionen:
EXE=engine BIN=bin SRC=src OBJ=$(BIN)/app.o $(BIN)/precomp.o LIBS=clanApp clanDisplay clanCore clanGL PACKAGES = $(patsubst %,%-4.1,$(LIBS))
Jede Zeile definiert eine Variable mit dem dazugehörigen Wert. In der Variable EXE steht „engine“, in der Variable LIBS stehen die nötigen Bibliotheken von ClanLib mit einem Leerzeichen getrennt. Spannender ist OBJ, dort wird eine andere Variable BIN verwendet. Die Verwendung wird mit der Schreibweise $(VARIABLENNAME) eingesetzt, das Ergebnis ist dann „bin/app.o bin/precomp.o“.
# CXXFLAGS += `pkg-config --cflags $(PACKAGES)` -mthreads CXXFLAGS += `pkg-config --cflags $(PACKAGES)` -pthread LDFLAGS += -ldl
Im nächsten Block werden Compiler Flags gesetzt. Die erste Zeile ist da ein Sonderfall und zeigt wie man eine ganze Zeile auskommentiert. Alles nach dem # wird ignoriert. Die beiden definierten Flags benötigt der Compiler, damit er die ClanLib Bibliotheken fürs Bauen finden und verwenden kann.
all: $(EXE) $(EXE): $(OBJ) $(CXX) $(CXXFLAGS) $(OBJ) -o $(EXE) `pkg-config --libs $(PACKAGES)` $(EXTRA_LIBS) clean: rm -f $(BIN)/* $(EXE) $(BIN)/%.o : $(SRC)/%.cpp $(CXX) $(CXXFLAGS) -c $< -o $@
Am Ende des Makefiles findet man die Regeln. Ruft man im Verzeichnis vom Makefile das make Kommando aus, dann wird automatisch die „all“ Regel ausgeführt. Dieses baut die mit OBJ definierten Objekte. Die Regel dafür findet man in den letzten beiden Zeilen. Pro *.o Datei wird die zugehörige *.cpp Datei aus dem src Verzeichnis gebaut. Die Regel dazu ist einfach: der Compiler baut mit den Compilerflags das Objekt. Die nötigen Argumente für g++ sind -c und -o. Danach kann die zweite Zeile von der all Regel ausgeführt werden, in der die Objekte mit den Bibliotheken con ClanLib gelinkt werden und die ausführbare Datei erstellt wird.
Zusätzlich dazu enthält das Makefile eine clean Regel. Diese führt man mit
make clean
aus. Es werden dabei alle Dateien im bin Verzeichnis und die ausführbare Datei gelöscht. Wie du erkennen kannst, kann ein Makefile mit beliebigen Regeln erweitert werden. Jede Regel lässt sich individuell an die Bedürfnisse anpassen. Über diese Syntax lassen sich alle Kommandos abbilden die man über die Kommandozeile unter Linux ausführen kann. Der Build Prozess ist somit vollständig durch ein Makefile abbildbar.
Automatisieren
Visual Studio Code erlaubt eine nahtlose Integration des Workflows egal welcher Programmiersprache über die IDE. Tasks wie das Bauen der Software oder das Ausführen vom Debugger lassen sich durch *.json Files für das jeweilige Szenario anpassen. In meinem Fall ist das nun die Programmiersprace C++ mit dem g++ (bzw. gcc) Compiler und das Tool make. Ein möglicher Build Task sieht konfiguriert wie folgt aus:
Soweit nichts komplexes, definiert ist der Prozess in der tasks.json Datei. Der build Befehl startet eine shell in der das Kommando make direkt im Projektpfad ausgeführt wird. Wenn ich also im geöffneten Projekt SRTG + SHIFT + B klicke baut der Source Code zu einem Binary.
Ähnlich funktioniert das auch für den Debugger vom Visual Studio Code. Über die IDE lässt sich die Debug Version vom Programm bauen und starten, es lassen sich Haltepunkte setzen und die Werte von Variablen ausgeben. Damit das auch für meine C++ Umgebung funktioniert habe ich die launch.json Datei wie folgt angepasst:
Als Debugger wird in diesem GDB verwendet. Damit das klappt habe ich das Compiler Flag -ggdb dem Makefile hinzugefügt.
Fazit
Ich habe gezeigt wie ein einfaches Makefile aufgebaut ist. Mit etwas Übung hat man den Umgang mit Makefiles schnell verstanden. In Kombination mit einer guten IDE wie Visual Studio Code ist der Aufwand ein neues Projekt aufzusetzen gering. Gerade für den Code den man später öffentlich aufrufbar machen möchte eignen sich Makefiles besonders, da sie das Projekt nicht an eine spezielle IDE und ein spezielle Konfiguration binden.