1. Einleitung
Dieser Artikel beschäftigt sich mit Makefiles. Makefiles sind Textdateien, die von Make-Tools wie NMAKE, DMAKE usw. verwendet werden, um Compile-Läufe zu steuern. Allerdings können Makefiles wesentlich mehr. Das, und natürlich die Grundlagen, soll in diesem Artikel vorgestellt werden.
Die Beispiele beziehen sich auf NMAKE, also das Make-Tool, welches mit den Microsoft-Compilern mitgeliefert wird. Grundsätzlich ist das Gesagte aber auf jedes andere Make-Tool übertragbar, jedoch unterscheidet sich die Syntax von Make-Tool zu Make-Tool etwas (also z. B. % anstatt $ usw.).
Wenn Sie die Beispiele ausprobieren wollen, dann speichern Sie sie unter dem Namen MAKEFILE und rufen NMAKE ohne Parameter auf, sofern im Text nichts anderes angegeben ist.
[Zurück zum Inhaltsverzeichnis]
2. Grundlagen
Ein Makefile kann aus bis zu drei Sektionen bestehen: Variablen-Definitionen, Build-Regeln und Targets. Die Targets sind dabei die einzige Sektion, die immer vorhanden sein muss, die anderen beiden sind optional. Das kürzeste denkbare Makefile ist somit ein Zweizeiler.
Aber schauen wir uns die drei Makefile-Sektionen im Einzelnen an. Wir beginnen mit der Target-Sektion, da sie eigentlich das "Ziel" unserer Bemühungen ist.
[Zurück zum Inhaltsverzeichnis]
2.1. Targets
Die Targets bilden das Kernstück eines jeden Makefiles. Hier wird die eigentliche Arbeit erledigt. Ein typisches Target könnte zum Beispiel so aussehen:
test.exe: test.cpp cl test.cpp
Das Target besteht dabei aus 3 Elementen: Dem Ziel, den Abhängigkeiten und den Aktionen. In unserem Beispiel ist test.exe das Ziel. Dieses Ziel soll erzeugt werden. test.cpp stellt die Abhängigkeiten dar, d. h. das Ziel hängt von diesen Dateien (bzw. dieser Datei) ab. Die Zeile cl test.cpp schließlich beschreibt die Aktionen, die durchzuführen sind, um aus den Abhängigkeiten das Ziel zu erzeugen.
Aktionszeilen sind einzurücken, damit sie vom Make-Tool als solche erkannt werden. Oft genügen dafür ein oder mehrere Leerzeichen, jedoch bestehht z. B. GNU-Make darauf, dass zur Einrückung tatsächlich ein Tabulatorzeichen verwendet wird.
Wird nun das Makefile ausgeführt, stellt das Make-Tool fest, dass test.exe von test.cpp abhängt. Es geht dann her und überprüft das Dateidatum der beiden Dateien. Ist test.cpp neuer als test.exe, dann werden die angegegebenen Aktionen ausgeführt, sonst nicht.
Das ist auch der Kernpunkt eines Makefiles: Aktionen werden nur ausgeführt, wenn die Abhängigkeiten neuer als das Ziel sind.
Ein Makefile enthält i. d. R. mehr als ein Target. Wird beim Aufruf des Make-Tools nichts anderes angegeben, wird immer das erste Target ausgewertet. Alternativ kann beim Aufruf ein auszuführendes Target angegeben werden. Es wird dann das angegebene Target ausgewertet. Beispiel:
NMAKE test.exe
[Zurück zum Inhaltsverzeichnis]
2.1.1. Das Ziel
Jedes Target muss ein Ziel enthalten. Sowohl die Abhängigkeiten als auch die Aktionen sind optional und können ggf. weggelassen werden. Hinter dem Ziel steht immer ein Doppelpunkt. Das Ziel kann eine Datei sein, die durch die Aktion(en) erzeugt wird, es kann aber auch ein sog. "symbolisches" Ziel sein.
Das Make-Tool behandelt symbolische Ziele wie "normale" Ziele, d. h. es sieht das Ziel als Dateinamen an. Die Ziele werden dadurch zu symbolischen Zielen, dass während des Make-Laufs nie eine Datei mit diesem Namen erzeugt wird. Das Ziel ist also immer "out of date", d. h. die ggf. angegebenen Aktionen des Ziels werden immer ausgeführt.
Symbolische Ziele können verwendet werden, um Hirarchien (so eine Art Unterprogramme) aufzubauen. Beispiel:
all: test1.exe test2.exe test1.exe: test1.cpp cl test1.cpp test2.exe: test2.cpp cl test2.cpp
In diesem Beispiel ist all das erste Target. Es hängt von test1.exe und test2.exe ab. Diese beiden wiederum hängen von ihren jeweiligen Source-Dateien ab. Wird nun der Make-Lauf gestartet, wertet das Make-Tool das Target all aus. Da dieses von den Targets test1.exe und test2.exe abhängig ist, werden diese beiden auch ausgewertet, und zwar in genau dieser Reihenfolge. Das führt dazu, dass am Ende des Make-Laufs die beiden Dateien test1.exe und test2.exe existieren und aktuell sind, zumindest wenn während des Make-Laufs keine (Compiler-)Fehler aufgetreten sind.
Das Target all hat selbst keine Aktionen. Das ist auch nicht notwendig, da alle erforderlichen Aktionen durch die "Unter-Targets" durchgeführt werden.
Da es in unserem Beispiel nie eine Datei all geben wird, ist das Target all immer "out of date", d. h. seine Aktionen werden immer ausgeführt. Diese Eigenschaft kann dazu verwendet werden, um bestimmte Aktionen bei jedem Make-Lauf ausführen zu lassen. Beispiel:
all: test1.exe test2.exe @echo. @echo Fertig.
Zu beachten ist dabei, dass die Aktionen des Targets all erst ausgeführt werden, NACHDEM die Targets test1.exe und test2.exe ausgewertet wurden. Will man Aktionen vor dem Auswerten dieser Targets ausführen lassen, kann das über die Definition eines Hilfstargets erreicht werden, z. B.:
all: prepare test1.exe test2.exe @echo. @echo Fertig. prepare: @echo Ich fange jetzt an!
Da die Abhängigkeiten immer von links nach rechts ausgewertet werden, passiert folgendes:
- Zuerst wird das Target prepare ausgewertet. Da dies ebenfalls ein symbolisches Target ist, werden seine Aktionen immer ausgeführt.
- Das Target prepare hat selbst keine Abhängigkeiten, also werden keine weiteren Auswertungen durchgeführt.
- Nachdem das Target prepare ausgewertet ist, wird das Target test1.exe ausgewertet usw.
- Nachdem alle Abhängigkeiten ausgewertet sind, werden die Aktionen von all ausgeführt.
Verwirrt? Probieren Sie es einfach aus! Hier noch einmal das komplette Beispiel:
all: prepare test1.exe test2.exe @echo. @echo Fertig. prepare: @echo Ich fang jetzt an! test1.exe: test1.cpp cl test1.cpp test2.exe: test2.cpp cl test2.cpp
Natürlich können anstelle der Echo-Anweisungen auch "sinnvolle" Dinge getan werden (z. B. Anlegen von benötigten Verzeichnissen, Wegkopieren der Ergebnisse usw.), aber dazu später mehr.
Noch ein genereller Hinweis zu symbolischen Targets:
Es gibt durchaus Make-Tools, bei denen symbolische Ziele als solche gekennzeichnet werden können oder müssen. Ein Beispiel für letzteres ist WMAKE des Watcom-Compilers. Hier erhält ein symbolisches Ziel immer mindestens eine Abhängigkeit namens .SYMBOLIC:
prepare: .SYMBOLIC @echo Ich fang jetzt an!
Bei GNU-Make kann (muss aber nicht) ein Target durch die Verwendung von .PHONY als symbolisch markiert werden:
.PHONY: prepare prepare: @echo Ich fang jetzt an!
Das Target wird dann immer als "out of date" angesehen, selbst wenn eine Datei mit diesem Namen existieren sollte.
[Zurück zum Inhaltsverzeichnis]
2.1.2. Die Abhängigkeiten
Wie wir in den vorangegangenen Beispielen gesehen haben, kann ein Target keine, eine oder mehrere Abhängigkeiten enthalten. Sind mehrere Abhängigkeiten vorhanden, werden diese immer von links nach rechts ausgewertet.
Ist eine Abhängigkeit eine Datei, wird das Dateidatum mit dem Datum des Ziels verglichen. Ist die Abhängigkeit neuer oder das Ziel gar nicht vorhanden, werden die Aktionen des Targets ausgeführt. Dies geschieht jedoch erst, nachdem alle Abhängigkeiten ausgewertet wurden.
Betrachten wir nun die Fälle im Einzelnen:
- Target ohne Abhängigkeiten: Die Aktionen des Targets werden ausgeführt, wenn das Ziel nicht vorhanden ist.
- Target mit einer Abhängigkeit: Die Aktionen des Targets werden ausgeführt, wenn die Abhängigkeit neuer als das Ziel ist, bzw. wenn das Ziel nicht vorhanden ist.
- Target mit mehreren Abhängigkeiten: Die Aktionen des Targets werden ausgeführt, wenn mindestens eine Abhängigkeit neuer als das Ziel bzw. das Ziel nicht vorhanden ist, aber erst nachdem alle Abhängigkeiten ausgewertet wurden.
[Zurück zum Inhaltsverzeichnis]
2.1.3. Die Aktionen
Ein Target kann keine, eine oder mehrere Aktionen haben. Diese werden ausgeführt, wenn das Target "out of date" ist, d. h. wenn mindestens eine Abhängigkeit neuer als das Ziel ist oder das Ziel nicht existiert – genauer: keine Datei existiert, die wie das Ziel heißt (siehe hierzu jedoch auch die Hinweise zu symbolischen Zielen am Ende von Abschnitt 2.1.1.).
Die Aktionen werden in der Reihenfolge ausgeführt, in der sie angegeben sind, also von oben nach unten.
Tritt bei der Ausführung einer Anweisung ein Fehler auf, bricht der Make-Lauf an dieser Stelle ab. Ist dies nicht gewünscht, kann der Anweisung ein Bindestrich vorangestellt werden. Dies unterdrückt die Fehlerprüfung durch das Make-Tool. Beispiel:
clean: -erase *.obj -erase *.exe
In diesem Beispiel wird der Make-Lauf auch dann fortgesetzt, wenn das Erase-Kommando einen Fehler liefert, weil die entsprende Datei z. B. nicht vorhanden ist und ergo nicht gelöscht werden kann. (Anm.: Unter WinNT gibt erase keinen Fehler-Code zurück, wenn es keine Datei zum Löschen findet. Unter anderen Betriebsystemen ist das aber durchaus anders.)
Außerdem gibt es die Möglichkeit, die Anzeige der Kommandozeile zu unterdrücken. Dies geschieht durch ein vorangestelltes @. Beispiel:
all: prepare test1.exe test2.exe @echo. @echo Fertig.
Die beiden Präfixe können auch kombiniert werden. Beispiel:
clean: -@erase *.obj -@erase *.exe
[Zurück zum Inhaltsverzeichnis]
2.2. Variablen
Bei komplexeren Make-Projekten kann es wünschenswert sein, immer wiederkehrende Zeichenfolgen abzukürzen bzw. Einstellungen vorzunehmen, die für den gesamten Make-Lauf gelten sollen. Zu diesem Zweck bietet sich die Verwendung von Variablen an.
Variablen werden durch eine einfache Zuweisung definiert. Beispiele:
CGFLAGS=/c /D_WINDOWS /GB /W4 /Zp1 CRFLAGS=/DNDEBUG /ML /Ox CDFLAGS=/Ge /MLd /Od /Zi
Soll der Inhalt der Variablen abgerufen werden, setzt man den Variablennamen in runde Klammern und stellt ein $ voran. Der Abruf kann an allen möglichen Stellen des Makefiles geschehen, also auch bei der Definition weiterer Variablen. Beispiel:
CFLAGS=$(CGFLAGS) $(CDFLAGS)
Während des Make-Laufs findet dann eine Textersetzung statt. Aus o. g. Zeile wird dann
CFLAGS=/c /D_WINDOWS /GB /W4 /Zp1 /Ge /MLd /Od /Zi
Ein weiteres Beispiel, das den Abruf einer Variablen in einem Target demonstriert:
test.obj: test.cpp cl $(CFLAGS) test.cpp
In den kurzen Beispielen kann man die Vorteile der Variablen noch nicht richtig erkennen. Aus diesem Grund wollen wir das Kapitel "Variablen" mit einem etwas komplexeren Beispiel beschließen. Beachten Sie dabei folgendes:
- Zeilen, die mit einem # beginnen, sind Kommentare und werden vom Make-Tool ignoriert.
- Es genügt, an einer Stelle einzugreifen, wenn z. B. die Compiler-Optionen für alle OBJ-Module geändert werden sollen.
- Beachten Sie die Verwendung der Variablen PROJ. Sie wird an allen drei möglichen Positionen eines Targets ausgewertet: Als Ziel, als Abhängigkeit und in einer Aktion.
- Das Ganze ist eigentlich immer noch zu viel Tipparbeit, das geht noch kleiner! Aber dazu später mehr.
- Wenn Sie einen C/C++-Compiler besitzen, dann ist es vielleicht eine gute Idee, an dieser Stelle mit dem Lesen innezuhalten und ein kleines Testprojekt aufzusetzen, das das untenstehende Makefile verwendet.
#------------------------------------------------------------------------- # NMAKE-Makefile #------------------------------------------------------------------------- PROJ=test #------------------------------------------------------------------------- # Compiler-Flags #------------------------------------------------------------------------- CGFLAGS=/c /D_CONSOLE /GB /nologo /W4 /Zp1 CRFLAGS=/DNDEBUG /ML /Ox CDFLAGS=/Ge /MLd /Od /Zi CFLAGS=$(CGFLAGS) $(CDFLAGS) #------------------------------------------------------------------------- # Linker-Flags #------------------------------------------------------------------------- LGFLAGS=/NOLOGO LRFLAGS= LDFLAGS=/DEBUG LFLAGS=$(LGFLAGS) $(LDFLAGS) #------------------------------------------------------------------------- # Main-Targets #------------------------------------------------------------------------- all: $(PROJ).exe @echo. @echo Fertig. clean: -erase *.pdb -erase *.ilk -erase *.obj -erase *.exe #------------------------------------------------------------------------- # Sub-Targets #------------------------------------------------------------------------- main.obj: main.cpp module1.h cl $(CFLAGS) main.cpp module1.obj: module1.cpp module1.h cl $(CFLAGS) module1.cpp $(PROJ).exe: main.obj module1.obj link $(LFLAGS) /OUT:$(PROJ).exe main.obj module1.obj #------------------------------------------------------------------------- #------------------------------------------------------------------------- #-------------------------------------------------------------------------
[Zurück zum Inhaltsverzeichnis]
2.3. Build-Regeln
Build-Regeln sind im Prinzip auch eine Art Variablen. Sie nehmen jedoch keine Zeichenketten auf, sondern definieren Aktionen.
Eine Build-Regel ist wie ein Target aufgebaut, jedoch mit folgenden Unterschieden:
- Es werden keine Abhängigkeiten definiert.
- Das Ziel ist nicht eine Datei, sondern eine Art Schablone, die angibt, für welche Datei-Transformation die Build-Regel angewendet werden soll.
Aber machen wir einfach mal ein Beispiel:
.cpp.obj: cl $(CFLAGS) $<
Diese Build-Regel besagt, dass eine Datei mit der Endung .cpp in eine Datei mit der Endung .obj überführt werden kann, indem die Aktion cl $(CFLAGS) $< ausgeführt wird. Diese Build-Regel wird vom Make-Tool auf alle Targets angewendet, die als Ziel ein OBJ-Datei und als erste Abhängigkeit eine CPP-Datei haben, sofern das Target selbst keine Aktionen definiert.
Für jede Kombination von Dateiendungen muss eine eigene Build-Regel definiert werden. Sollen mit dem Makefile z. B. auch C-Dateien bearbeitet werden, so wäre noch folgende Build-Regel hinzuzufügen:
.c.obj: cl $(CFLAGS) $<
Das scheint im ersten Ansatz lästig zu sein, da die Aktion beidesmal dieselbe ist. Es gibt jedoch Compiler, bei denen zur Compilierung von C- und CPP-Dateien unterschiedliche Programme aufgerufen werden müssen/können (z. B. Watcom). Oder denken sie an Projekte, die auch Assembler-Sourcen enthalten:
.asm.obj: masm $(AFLAGS) $<
Bleibt noch die Bedeutung der Variablen $< zu klären: Sie ist nur in Build-Regeln gültig und liefert den Namen der ersten Abhängigkeit.
Das Makefile-Beispiel aus Kapitel 2.2. sieht beim Einsatz von Build-Regeln so aus:
#------------------------------------------------------------------------- # NMAKE-Makefile #------------------------------------------------------------------------- PROJ=test #------------------------------------------------------------------------- # Compiler-Flags #------------------------------------------------------------------------- CGFLAGS=/c /D_CONSOLE /GB /nologo /W4 /Zp1 CRFLAGS=/DNDEBUG /ML /Ox CDFLAGS=/Ge /MLd /Od /Zi CFLAGS=$(CGFLAGS) $(CDFLAGS) #------------------------------------------------------------------------- # Linker-Flags #------------------------------------------------------------------------- LGFLAGS=/NOLOGO LRFLAGS= LDFLAGS=/DEBUG LFLAGS=$(LGFLAGS) $(LDFLAGS) #------------------------------------------------------------------------- # Build-Rules #------------------------------------------------------------------------- .cpp.obj: CL $(CFLAGS) $< #------------------------------------------------------------------------- # Main-Targets #------------------------------------------------------------------------- all: $(PROJ).exe @echo. @echo Fertig. clean: -erase *.pdb -erase *.ilk -erase *.obj -erase *.exe #------------------------------------------------------------------------- # Sub-Targets #------------------------------------------------------------------------- main.obj: main.cpp module1.h module1.obj: module1.cpp module1.h $(PROJ).exe: main.obj module1.obj link $(LFLAGS) /OUT:$(PROJ).exe main.obj module1.obj #------------------------------------------------------------------------- #------------------------------------------------------------------------- #-------------------------------------------------------------------------
Beachten Sie hierbei die Targets mit den Zielen main.obj und module1.obj. Dadurch, dass sie keine eigenen Aktionen haben, greift die entsprechende Build-Regel und die Source-Files werden korrekt compiliert.
Zugegeben, in diesem Beispiel ist die Tipp-Ersparnis nicht allzu groß, aber denken Sie an ein Projekt mit >20 Source-Files. Es wird durch den Einsatz von Build-Regeln übersichtlicher und leichter zu pflegen.
[Zurück zum Inhaltsverzeichnis]
3. Spezialitäten
3.1. Lange Zeilen
Wenn man größere Projekte realisiert, kommt es durchaus vor, dass z. B. Abhängigkeiten sehr lang werden. Damit das Makefile trotzdem übersichtlich bleibt, gibt es die Möglichkeit, Zeilen über das Zeilenende hinaus fortzusetzen. Das hört sich im ersten Moment vielleicht ein wenig verwirrend an, ist es aber gar nicht. Schauen wir uns dazu ein Beispiel an:
main.obj: main.cpp module1.h module2.h module3.h module4.h\ module5.h module6.h module7.h module8.h cl $(CFLAGS) main.cpp
Die Abhängigkeit dieses Targets besteht aus den Dateien main.cpp sowie module1.h bis module8.h. Der "Trick" dabei ist der Backslash hinter module4.h. Durch diesen wird dem Make-Tool angezeigt, dass die Abhängigkeiten in der nächsten Zeile fortgesetzt werden. Beachten Sie dabei, dass hinter dem Backslash kein Zeichen mehr folgen darf (auch kein Leerzeichen!) und dass die neue Zeile mit einem Leerzeichen beginnen sollte.
Das Beispiel oben zeigt ein Target mit "Zeilenverlängerung". Targets sind der häufigste Anwendungsfall, grundsätzlich lassen sich jedoch alle Zeilen auf diese Weise "verlängern".
[Zurück zum Inhaltsverzeichnis]
3.2. Aktionen, die über Compilieren und Linken hinausgehen
Wie bereits in der Einleitung erwähnt, werden Makefiles üblicherweise zum Steuern eines Compile-Laufs verwendet. Jedoch können sie wesentlich mehr.
Stellen Sie sich z. B. eine Situation vor, in der mehrere Entwickler an einem Projekt arbeiten, jeder davon aber an einem unabhängigen Teil (eigenes EXE oder eigene DLL) des Projekts. Jeder dieser Entwickler arbeitet an seinem Teilprojekt auf seiner lokalen Platte. Um bei der Freigabe einer neuen Gesamtprojektversion nicht die Ergebnisse der Teilprojekte zusammensuchen zu müssen, ist vereinbart, dass jeder Entwickler seine Freigabeversionen in einem bestimmten Verzeichnis im Netzwerk abliefert.
Diese Aufgabe kann von einem Makefile mit übernommen werden. Schauen wir uns das einmal anhand eines Beispiels an:
#------------------------------------------------------------------------- # NMAKE-Makefile #------------------------------------------------------------------------- PROJ=TEST RELEASEDIR=N:\PROJECTS\P.012\RELEASE #------------------------------------------------------------------------- # Compiler-Flags #------------------------------------------------------------------------- CGFLAGS=/c /D_CONSOLE /GB /nologo /W4 /Zp1 CRFLAGS=/DNDEBUG /ML /Ox CDFLAGS=/Ge /MLd /Od /Zi CFLAGS=$(CGFLAGS) $(CDFLAGS) #------------------------------------------------------------------------- # Linker-Flags #------------------------------------------------------------------------- LGFLAGS=/NOLOGO LRFLAGS= LDFLAGS=/DEBUG LFLAGS=$(LGFLAGS) $(LDFLAGS) #------------------------------------------------------------------------- # Build-Rules #------------------------------------------------------------------------- .cpp.obj: CL $(CFLAGS) $< #------------------------------------------------------------------------- # Main-Targets #------------------------------------------------------------------------- help: @echo. @echo "Aufruf : NMAKE target" @echo. @echo "Targets : help - Zeigt diesen Text an." @echo " all - Erstellt das Ziel $(PROJ).EXE." @echo " release - Kopiert $(PROJ).EXE nach $(RELEASEDIR)." @echo " clean - Löscht die Ergebnisdateien (*.OBJ, *.EXE usw.)." @echo. @echo "Beispiele: NMAKE all" @echo " NMAKE release" @echo. all: $(PROJ).exe @echo. @echo Fertig. release: $(RELEASEDIR)\$(PROJ).exe clean: -erase *.pdb -erase *.ilk -erase *.obj -erase *.exe #------------------------------------------------------------------------- # Sub-Targets #------------------------------------------------------------------------- main.obj: main.cpp module1.h module1.obj: module1.cpp module1.h $(PROJ).exe: main.obj module1.obj link $(LFLAGS) /OUT:$(PROJ).exe main.obj module1.obj $(RELEASEDIR)\$(PROJ).exe: $(PROJ).exe copy $(PROJ).exe $(RELEASEDIR) #------------------------------------------------------------------------- #------------------------------------------------------------------------- #-------------------------------------------------------------------------
Beachten Sie dabei folgendes:
- Das erste Target ist das Target help. Es
gibt eine kurze Anleitung aus, wie das Makefile zu verwenden ist. Da es
das erste Target ist, wird es immer ausgeführt, wenn NMAKE ohne
Parameter aufgerufen wird.
Anm.:Die Anführungszeichen um den Text sind nötig, damit die Formatierung des Texts bei der Ausgabe erhalten bleibt. Nicht sehr schön, aber was will man machen … - Das Target release erledigt die eigentliche Freigabe-Arbeit. Es sorgt dafür, dass das Teilprojektergebnis (in unserem Beispiel test.exe) in das vereinbarte Verzeichnis auf dem Netzwerk kopiert wird, falls dort nicht schon eine aktuelle Version liegt.
- Der Entwickler wird i. d. R. mit dem Target all arbeiten, um sein Teilprojekt zu erstellen und zu testen. Wenn er eine stabile Version hat, wird er diese mithilfe des Targets release ins Freigabe-Verzeichnis stellen.
[Zurück zum Inhaltsverzeichnis]
3.3. Bedingte Ausführung
Was wäre die Arbeit mit Computern ohne if? Nicht auszudenken, nicht wahr? Die Entwickler von Make-Tools wissen das natürlich auch und haben aus diesem Grunde die Möglichkeit geschaffen, Teile eines Makefile bedingt ausführen zu lassen.
Stellen Sie sich die Situation vor, dass ein Programm alternativ mit und ohne Debug-Information erstellt werden soll. Dazu jedes Mal das Makefile ändern zu müssen, wäre mehr als lästig. Man kann jedoch Variablen beim Aufruf des Make-Tools auf der Kommandozeile mitgeben. Wenn diese dann im Makefile ausgewertet werden, kann dadurch der Make-Lauf entsprechend gesteuert werden.
Aber halten wir uns nicht lange mit der trockenen Theorie auf, kommen wir lieber gleich zu einem Beispiel:
#------------------------------------------------------------------------- # NMAKE-Makefile #------------------------------------------------------------------------- PROJ=TEST RELEASEDIR=N:\PROJECTS\P.012\RELEASE #------------------------------------------------------------------------- # Defaults #------------------------------------------------------------------------- !ifndef DEBUG DEBUG=0 !endif #------------------------------------------------------------------------- # Compiler-Flags #------------------------------------------------------------------------- CGFLAGS=/c /D_CONSOLE /GB /nologo /W4 /Zp1 CRFLAGS=/DNDEBUG /ML /Ox CDFLAGS=/Ge /MLd /Od /Zi !if $(DEBUG)==0 CFLAGS=$(CGFLAGS) $(CRFLAGS) !else CFLAGS=$(CGFLAGS) $(CDFLAGS) !endif #------------------------------------------------------------------------- # Linker-Flags #------------------------------------------------------------------------- LGFLAGS=/NOLOGO LRFLAGS= LDFLAGS=/DEBUG !if $(DEBUG)==0 LFLAGS=$(LGFLAGS) $(LRFLAGS) !else LFLAGS=$(LGFLAGS) $(LDFLAGS) !endif #------------------------------------------------------------------------- # Build-Rules #------------------------------------------------------------------------- .cpp.obj: CL $(CFLAGS) $< #------------------------------------------------------------------------- # Main-Targets #------------------------------------------------------------------------- help: @echo. @echo "Aufruf : NMAKE [DEBUG=1] target" @echo. @echo "Targets : help - Zeigt diesen Text an." @echo " all - Erstellt das Ziel $(PROJ).EXE." @echo " release - Kopiert $(PROJ).EXE nach $(RELEASEDIR)." @echo " clean - Löscht die Ergebnisdateien (*.OBJ, *.EXE usw.)." @echo. @echo "Optionen : DEBUG=1 - Es wird eine Debug-Version erstellt." @echo. @echo "Beispiele: NMAKE DEBUG=1 all" @echo " NMAKE release" @echo. all: $(PROJ).exe @echo. @echo Fertig. release: $(RELEASEDIR)\$(PROJ).exe clean: -erase *.pdb -erase *.ilk -erase *.obj -erase *.exe #------------------------------------------------------------------------- # Sub-Targets #------------------------------------------------------------------------- main.obj: main.cpp module1.h module1.obj: module1.cpp module1.h $(PROJ).exe: main.obj module1.obj link $(LFLAGS) /OUT:$(PROJ).exe main.obj module1.obj $(RELEASEDIR)\$(PROJ).exe: $(PROJ).exe !if $(DEBUG)!=0 @echo. @echo WARNUNG: Es wird eine Debug-Version nach $(RELEASEDIR) kopiert! @echo. !endif copy $(PROJ).exe $(RELEASEDIR) #------------------------------------------------------------------------- #------------------------------------------------------------------------- #-------------------------------------------------------------------------
Die bedingte Ausführung des Makefiles wird an folgenden Stellen eingesetzt:
- Im Abschnitt "Defaults". Hier wird sichergestellt, dass eine Variable, die wir später auswerten wollen, auch definiert ist.
- Im Abschnitt "Compiler-Flags" werden die Flags für den Compiler-Aufruf gesetzt. Dabei werden für Debug- und Release-Version verschiedene Flag-Sätze verwendet.
- Der Abschnitt "Linker-Flags" arbeitet wie der Abschnitt "Compiler-Flags", nur eben für die Linker-Optionen.
- Im Abschnitt "Sub-Targets" wird beim Target $(RELEASEDIR)\$(PROJ).exe eine Warnung ausgegeben, wenn eine Debug-Version ins Freigabe-Verzeichnis kopiert wird.
[Zurück zum Inhaltsverzeichnis]
3.4. Unter-Makefiles
Manchmal kann die Verwendung von Hilfs-Makefiles (oder Unter-Makefiles) sinnvoll sein. Das sind Makefiles, die von einem anderen Makefile aus aufgerufen (nicht per !INCLUDE eingebunden!) werden.
Sinvoll kann das z. B. sein, wenn mehrere Programme aus fast dem gleichen Sourcen-Pool erstellt werden sollen. Aber kreieren wir doch einfach ein Beispiel:
Gegeben sei eine Applikation, die aus den Source-Dateien main.cpp, module1.cpp und module2.cpp besteht. Dieses Programm soll für verschiedene Länder in verschiedenen Sprachen erzeugt werden. Aus diesem Grund wurden alle Texte in ein Modul namens textNNN.cpp ausgelagert. Das "NNN" steht dabei für eine dreistellige Länderkennung, die sich nach den internationalen Telefonvorwahlen richtet (also 001 für USA, 049 für Deutschland usw.).
Neben den Dateien main.cpp, module1.cpp und module2.cpp, die für alle Versionen benötigt werden, gibt es noch die Dateien text001.cpp und text049.cpp (wir wollen uns der Einfachheit halber auf zwei Länderversionen beschränken). text001.cpp enthält die Programmtexte in Englisch, text049.cpp in Deutsch.
Mit unserem bisherigen Wissen über Makefiles fällt es uns nicht schwer, ein Makefile zu schreiben, das eine dieser Versionen erstellt. Welche Version das ist, wollen wir über eine Variable LANG vorgeben, die auf die Länderkennung der Sprache gesetzt wird, für die ein Programm erzeugt werden soll.
Neben dieser Anforderung wollen wir gleich noch eine weitere Anforderung definieren: Die Ergebnisse der Compile-Läufe sollen nach Sprach-, Debug- und Release-Version getrennt abgelegt werden. Das fertige Programm kann unter einem eindeutigen Namen ins Freigabeverzeichnis kopiert werden. Damit können wir dann fast alle Register ziehen, die ein Makefile beherrscht. :-)
Der Verzeichnisbaum für die Erzeugung der verschiedenen Outputs wird so aussehen:
+---OUT_001 | +---DEBUG | \---RELEASE \---OUT_049 +---DEBUG \---RELEASE
Das Makefile nennen wir mit Blick in die Zukunft SUB.MAK. Hier sein Inhalt:
#------------------------------------------------------------------------- # NMAKE-Makefile # WinNT wird als Build-Umgebung vorausgesetzt. #------------------------------------------------------------------------- PROJ=TEST RELEASEDIR=N:\PROJECTS\P.012\RELEASE #------------------------------------------------------------------------- # Defaults #------------------------------------------------------------------------- !ifndef DEBUG DEBUG=0 !endif !ifndef LANG LANG=049 !endif #------------------------------------------------------------------------- # Parameter-Prüfung #------------------------------------------------------------------------- !if "$(LANG)"!="001" && "$(LANG)"!="049" !error Unbekannte Sprachkennung angegeben! !endif #------------------------------------------------------------------------- # Compiler-Flags #------------------------------------------------------------------------- CGFLAGS=/c /D_CONSOLE /GB /nologo /W4 /Zp1 CRFLAGS=/DNDEBUG /ML /Ox CDFLAGS=/Ge /MLd /Od /Zi !if $(DEBUG)==0 CFLAGS=$(CGFLAGS) $(CRFLAGS) !else CFLAGS=$(CGFLAGS) $(CDFLAGS) !endif #------------------------------------------------------------------------- # Linker-Flags #------------------------------------------------------------------------- LGFLAGS=/NOLOGO LRFLAGS= LDFLAGS=/DEBUG !if $(DEBUG)==0 LFLAGS=$(LGFLAGS) $(LRFLAGS) !else LFLAGS=$(LGFLAGS) $(LDFLAGS) !endif #------------------------------------------------------------------------- # Sonstige Variablen #------------------------------------------------------------------------- LANGOUT=OUT_$(LANG) !if $(DEBUG)==0 OBJOUT=$(LANGOUT)\RELEASE !else OBJOUT=$(LANGOUT)\DEBUG !endif !if $(DEBUG)==0 RELEASENAME=$(RELEASEDIR)\$(PROJ)$(LANG).exe !else RELEASENAME=$(RELEASEDIR)\$(PROJ)$(LANG)D.exe !endif #------------------------------------------------------------------------- # Build-Rules #------------------------------------------------------------------------- .cpp{$(OBJOUT)}.obj: CL $(CFLAGS) -Fo$(OBJOUT)\ $< #------------------------------------------------------------------------- # Main-Targets #------------------------------------------------------------------------- help: @echo. @echo "ACHTUNG: Dieses Makefile ist ein Unter-Makefile für MAKEFILE." @echo " Es ist nicht dafür gedacht, direkt aufgerufen zu werden!" @echo. compile: prepare $(OBJOUT)\$(PROJ).exe @echo. @echo Fertig. docopy: prepare $(RELEASENAME) #------------------------------------------------------------------------- # Sub-Targets #------------------------------------------------------------------------- prepare: @if not exist $(LANGOUT)\ mkdir $(LANGOUT) @if not exist $(OBJOUT)\ mkdir $(OBJOUT) $(OBJOUT)\main.obj: main.cpp module1.h module2.h $(OBJOUT)\module1.obj: module1.cpp module1.h text.h $(OBJOUT)\module2.obj: module2.cpp module2.h text.h $(OBJOUT)\text$(LANG).obj: text$(LANG).cpp text.h $(OBJOUT)\$(PROJ).exe: $(OBJOUT)\main.obj $(OBJOUT)\module1.obj\ $(OBJOUT)\module2.obj $(OBJOUT)\text$(LANG).obj link $(LFLAGS) /OUT:$(OBJOUT)\$(PROJ).exe $(OBJOUT)\main.obj\ $(OBJOUT)\module1.obj $(OBJOUT)\module2.obj $(OBJOUT)\text$(LANG).obj $(RELEASENAME): $(OBJOUT)\$(PROJ).exe copy $(OBJOUT)\$(PROJ).exe $(RELEASENAME) #------------------------------------------------------------------------- #------------------------------------------------------------------------- #-------------------------------------------------------------------------
Nehmen Sie sich etwas Zeit, dieses Makefile anzusehen. Sie werden feststellen, dass die exzessive Verwendung von Variablen im Abschnitt "Sub-Targets" viele Dinge sehr elegant löst. Aber schauen wir uns zuerst den Aufruf dieses Makefiles an:
NMAKE -f sub.mak LANG=049 compile
Diese Zeile führt dazu, dass eine deutsche Release-Version des Programms erstellt wird. Die Output-Dateien werden dabei im Verzeichnis OUT_049\RELEASE erzeugt.
NMAKE -f sub.mak LANG=001 DEBUG=1 docopy
Dieser Aufruf erstellt die US-Debug-Version des Programms und kopiert sie unter dem Namen TEST001D.EXE in das Freigabeverzeichnis.
Wenn wir einmal annehmen, dass wir auch die Sprachversionen für die Kennungen 032, 044 und 058 (willkürlich gewählt) erstellen könnten/wollten, dann müssten wir dazu diese Folge von Aufrufen verwenden:
NMAKE -f sub.mak LANG=001 compile NMAKE -f sub.mak LANG=032 compile NMAKE -f sub.mak LANG=044 compile NMAKE -f sub.mak LANG=049 compile NMAKE -f sub.mak LANG=058 compile
Das kann man zwar über eine Batch-Datei erledigen lassen, schöner ist jedoch ein Haupt-Makefile, das auch noch andere Annehmlichkeiten zur Verfügung stellt. Ein solches Haupt-Makefile könnte so aussehen:
#------------------------------------------------------------------------- # NMAKE-Makefile # WinNT wird als Build-Umgebung vorausgesetzt. #------------------------------------------------------------------------- #------------------------------------------------------------------------- # Defaults #------------------------------------------------------------------------- !ifndef DEBUG DEBUG=0 !endif #------------------------------------------------------------------------- # Main-Targets #------------------------------------------------------------------------- help: @echo. @echo "Aufruf : NMAKE [DEBUG=1] target" @echo. @echo "Targets : help - Zeigt diesen Text an." @echo " all - Erstellt alle Ziele." @echo " allcopy - Erstellt alle Ziele und kopiert sie ins" @echo " Freigabeverzeichnis." @echo " ger - Erstellt die Deutsche Version." @echo " gercopy - Erstellt die Deutsche Version und kopiert sie ins". @echo " Freigabeverzeichnis." @echo " usa - Erzeugt die US-Version." @echo " usacopy - Erstellt die US-Version und kopiert sie ins". @echo " Freigabeverzeichnis." @echo " clean - Löscht alle Ergebnisdateien (*.OBJ, *.EXE usw.)." @echo. @echo "Optionen : DEBUG=1 - Es wird eine Debug-Version erstellt." @echo. @echo "Beispiele: NMAKE DEBUG=1 all" @echo " NMAKE gercopy" @echo. all: ger usa allcopy: gercopy usacopy ger: @echo. nmake /NOLOGO /f sub.mak DEBUG=$(DEBUG) LANG=049 compile usa: @echo. nmake /NOLOGO /f sub.mak DEBUG=$(DEBUG) LANG=001 compile gercopy: @echo. nmake /NOLOGO /f sub.mak DEBUG=$(DEBUG) LANG=049 docopy usacopy: @echo. nmake /NOLOGO /f sub.mak DEBUG=$(DEBUG) LANG=001 docopy clean: -erase /F /S /Q *.pdb -erase /F /S /Q *.ilk -erase /F /S /Q *.obj -erase /F /S /Q *.exe #------------------------------------------------------------------------- #------------------------------------------------------------------------- #-------------------------------------------------------------------------
Den größten Teil dieses Makefiles stellen die Main-Targets dar. Diese steuern den gesamten Make-Lauf auf komfortable Weise. Der Aufruf
NMAKE allcopy
erstellt die Release-Version aller Sprachversionen und kopiert sie ins Freigabeverzeichnis.
Soll dem Projekt nun eine neue Sprache (z. B. Französisch = 033) hinzugefügt werden, sind folgende Schritte nötig:
- Erstellen der Datei text033.cpp durch Kopieren von text049.cpp nach text033.cpp und Übersetzen der Texte ins Französische.
- MAKEFILE: Einfügen der Targets fra und fracopy durch Kopieren der Targets ger und gercopy sowie Ändern der Länderkennung von 049 auf 033.
- MAKEFILE: Das Target fra in die Abhängigkeiten von all und das Target fracopy in die Abhängigkeiten von allcopy mit aufnehmen.
- SUB.MAK: Die Sprachkennung 033 in den Abschnitt "Parameter-Prüfung" mit aufnehmen.
- Fertig.
[Zurück zum Inhaltsverzeichnis]
4. Schlussbemerkung
Hiermit ist unser Ausflug in die Welt der Makefiles beendet. Ich hoffe, es hat Ihnen ebenso viel Spaß gemacht wie mir.
Natürlich konnten nicht alle Aspekte beleuchtet werden. Viele Dinge, die über das oben Gesagte hinausgehen, sind auch stark vom verwendeten Make-Tool abhängig und würden den Rahmen dieser Einführung deutlich sprengen. Ich hoffe, dass ich Ihnen trotzdem das Wesen der Makefiles näherbringen und Ihnen "Appetit auf mehr" machen konnte.