Makefiles

Eine Einführung

Stand 2021-11-16
Autor: Wolfgang R. Schulz

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:

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:

[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:

#-------------------------------------------------------------------------
# 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:

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:

[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:

[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:

[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.

[Zurück zum Inhaltsverzeichnis]