Makefiles

Eine Einführung

Stand 2007-02-08
Autor: Wolfgang R. Schulz

0. Inhalt

1. Einleitung
2. Grundlagen
2.1. Targets
2.1.1. Das Ziel
2.1.2. Die Abhängigkeiten
2.1.3. Die Aktionen
2.2. Variablen
2.3. Build-Regeln
3. Spezialitäten
3.1. Lange Zeilen
3.2. Aktionen, die über Compilieren und Linken hinausgehen
3.3. Bedingte Ausführung
3.4. Unter-Makefiles
4. Schlußbemerkung

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

Eine Makefile kann aus bis zu 3 Sektionen bestehen: Variablen-Definitionen, Build-Regeln und Targets. Die Targets sind dabei die einzige Sektion, die immer vorhanden sein muß, die anderen beiden sind optional. Das kürzeste denkbare Makefile ist somit ein Zweizeiler.

Aber schauen wir uns die 3 Makefile-Sektionen im Einzelnen an. Wir beginnen mit der Target-Sektion, die 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.

Wird nun das Makefile ausgeführt, dann stellt das Maketool fest, daß 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.obj

[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, daß 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.

Anm.:  Es gibt durchaus Make-Tools, bei denen symbolische Ziele als solche gekennzeichnet werden. Ein Beispiel ist WMAKE des Watcom-Compilers. Hier erhält ein symbolisches Ziel immer mindestens eine Abhängigkeit namens .SYMBOLIC.

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, daß am Ende des Makelaufs die beiden Dateien TEST1.EXE und TEST2.EXE existieren und aktuell sind, zumindest wenn während des Makelaufs 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 würden immer ausgeführt werden. Diese Eigenschaft kann dazu benützt werden, um bestimmte Aktionen bei jedem Makelauf ausführen zu lassen. Beispiel:

all: test1.exe test2.exe
  @echo.
  @echo Fertig.

Zu beachten ist dabei, daß 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 fang 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 selber 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 mal aus. Hier nochmal 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.

[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 ist, 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).

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 entspr. 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 BS ist das aber durchaus so.)

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 zuviel 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, daß das unten stehende 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, daß eine Datei mit der Endung .cpp in eine Datei mit der Endung .obj überführt werden kann, in dem 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 selber keine Aktionen definiert.

Für jede Kombination von Dateiendungen muß 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 die gleiche 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-Source 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, daß sie keine eigenen Aktionen haben, greift die entspr. 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, daß 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 jetzt 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, daß die Abhängigkeiten in der nächsten Zeile fortgesetzt werden. Beachten Sie dabei, daß hinter dem Backslash kein Zeichen mehr folgen darf (auch kein Leerzeichen!) und daß 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 Ent- wickler 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, daß jeder Entwickler seine Freigabeversionen in einem bestimmten Verzeichnis auf dem 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, daß 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 mit Hilfe 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, daß 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, daß 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 2 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 erstellen, daß 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:

+---OUT001
|   +---DEBUG
|   \---RELEASE
\---OUT049
    +---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, daß 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, daß eine deutsche Release-Version des Programms erstellt wird. Die Output-Dateien werden dabei im Verzeichnis OUT049\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, daß wir auch die Sprachversionen für die Kennungen 032, 044 und 058 (willkürlich gewählt) erstellen könnten/wollten, dann müßten 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 aber 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 und Ä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. Schlußbemerkung

Hiermit ist unser Ausflug in die Welt der Makefiles beendet. Ich hoffe, es hat Ihnen ebensoviel 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, daß ich Ihnen trotzdem das Wesen der Makefiles näherbringen konnte, und daß ich Ihnen "Appetit auf mehr" machen konnte.

[Zurück zum Inhaltsverzeichnis]