ANZEIGE
ANZEIGE
key2it.de   Der Schlüssel zur Informationstechnik
 Start

Grundlagen: Makefiles

Grundlagen: Makefiles basiert auf den EDV-Info-Artikeln "Grundlagen: Makefiles I (v1)" und "Grundlagen: Makefiles II (v1)" aus dem Jahr 2000.

Dieser Artikel bietet eine Einfuehrung in die Thematik der Makefiles,
wie sie im Bereich der Softwareentwicklung zum Einsatz kommen.

In Teil I finden Sie die Abschnitte Einleitung und Grundlagen, in
Teil II Spezialitaeten und die Schlussbemerkung.

Der Autor ist Dipl.Ing.(FH) der Technischen Informatik und entwickelt
seit vielen Jahren Software fuer die verschiedensten Betriebsysteme.
Sein Erfahrungsspektrum reicht von maschinennaher Programmierung
(Device-Treiber) bis zur Anwendungsentwicklung (GUI-Programmierung).
Dabei kamen die verschiedensten Entwicklungswerkzeuge (Compiler,
Assembler, Debugger, IDEs, Make-Tools usw.) zum Einsatz. Speziell
waehrend seiner Taetigkeit als Device-Treiber-Entwickler wurden
Makefiles sehr intensiv eingestzt.

0. Inhaltsverzeichnis
=====================

1. Einleitung
2. Grundlagen
2.1. Targets
2.1.1. Das Ziel
2.1.2. Die Abhaengigkeiten
2.1.3. Die Aktionen
2.2. Variablen
2.3. Build-Regeln
3. Spezialitaeten
3.1. Lange Zeilen
3.2. Aktionen, die ueber Compilieren und Linken hinausgehen
3.3. Bedingte Ausfuehrung
3.4. Unter-Makefiles
4. Schlussbemerkung



1. Einleitung
=============

Dieser Artikel beschaeftigt sich mit Makefiles. Makefiles sind Text-
dateien, die von Make-Tools wie NMAKE, DMAKE usw. verwendet werden,
um Compile-Laeufe zu steuern. Allerdings koennen Makefiles wesentlich
mehr. Das, und natuerlich die Grundlagen, soll in diesem Artikel vor-
gestellt werden.

Die Beispiele beziehen sich auf NMAKE, also das Make-Tool, welches
mit den MicroSoft-Compilern mitgeliefert wird. Grundsaetzlich ist das
Gesagte aber auf jedes andere Make-Tool uebertragbar, jedoch unter-
scheidet 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.



2. Grundlagen
=============

Ein Makefile kann aus bis zu 3 Sektionen bestehen: Variablen-Defini-
tionen, Build-Regeln und Targets. Die Targets sind dabei die einzige
Sektion, die immer vorhanden sein muss, die anderen beiden sind
optional. Das kuerzeste denkbare Makefile ist somit ein Zweizeiler.

Aber schauen wir uns die 3 Makefile-Sektionen im Einzelnen an. Wir
beginnen mit der Target-Sektion, da sie eigentlich das "Ziel"
unserer Bemuehungen ist.



2.1. Targets
------------

Die Targets bilden das Kernstueck eines jeden Makefiles. Hier wird
die eigentliche Arbeit erledigt. Ein typisches Target koennte zum
Beispiel so aussehen:

test.exe: test.cpp
cl test.cpp

Das Target besteht dabei aus 3 Elementen: Dem Ziel, den
Abhaengigkeiten und den Aktionen. In unserem Beispiel ist "test.exe"
das Ziel. Dieses Ziel soll erzeugt werden. "test.cpp" stellt die
Abhaengigkeiten dar, d.h. das Ziel haengt von diesen Dateien (bzw.
dieser Datei) ab. Die Zeile "cl test.cpp" schliesslich beschreibt die
Aktionen, die durchzufuehren sind, um aus den Abhaengigkeiten das
Ziel zu erzeugen.

Wird nun das Makefile ausgefuehrt, dann stellt das Maketool fest,
dass test.exe von test.cpp abhaengt. Es geht dann her und ueberprueft
das Dateidatum der beiden Dateien. Ist test.cpp neuer als test.exe,
dann werden die angegegebenen Aktionen ausgefuehrt, sonst nicht.

Das ist auch der Kernpunkt eines Makefiles: Aktionen werden nur aus-
gefuehrt, wenn die Abhaengigkeiten neuer als das Ziel sind.

Ein Makefile enthaelt 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 auszufuehrendes Target
angegeben werden. Es wird dann das angegebene Target ausgewertet.
Beispiel:

NMAKE test.obj



2.1.1. Das Ziel

Jedes Target MUSS ein Ziel enthalten. Sowohl die Abhaengigkeiten als
auch die Aktionen sind optional und koennen 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 waehrend 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
ausgefuehrt.

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

Symbolische Ziele koennen verwendet werden, um Hierarchien (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 haengt von
"test1.exe" und "test2.exe" ab. Diese beiden wiederum haengen 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" abhaengig ist, werden diese beiden auch
ausgewertet, und zwar in genau dieser Reihenfolge. Das fuehrt dazu,
dass am Ende des Makelaufs die beiden Dateien TEST1.EXE und TEST2.EXE
existieren und aktuell sind, zumindest wenn waehrend 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"
durchgefuehrt werden.

Da es in unserem Beispiel nie eine Datei "all" geben wird, ist das
Target "all" immer "out of date", d.h. seine Aktionen wuerden immer
ausgefuehrt werden. Diese Eigenschaft kann dazu benuetzt werden, um
bestimmte Aktionen bei jedem Makelauf ausfuehren zu lassen. Beispiel:

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

Zu beachten ist dabei, dass die Aktionen des Targets "all" erst
ausgefuehrt werden, NACHDEM die Targets "test1.exe" und "test2.exe"
ausgewertet wurden. Will man Aktionen VOR dem Auswerten dieser
Targets ausfuehren lassen, kann das ueber 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 Abhaengigkeiten immer von links nach rechts ausgewertet
werden, passiert folgendes:

o Zuerst wird das Target "prepare" ausgewertet. Da dies ebenfalls
ein symbolisches Target ist, werden seine Aktionen immer ausge-
führt.
o Das Target "prepare" hat selber keine Abhaengigkeiten, also werden
keine weiteren Auswertungen durchgefuehrt.
o Nachdem das Target "prepare" ausgewertet ist, wird das Target
"test1.exe" ausgewertet usw.
o Nachdem alle Abhaengigkeiten ausgewertet sind, werden die Aktionen
von "all" ausgefuehrt.

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

Natuerlich koennen anstelle der Echo-Anweisungen auch "sinnvolle"
Dinge getan werden (z.B. Anlegen von benoetigten Verzeichnissen,
Wegkopieren der Ergebnisse usw.), aber dazu spaeter mehr.



2.1.2. Die Abhaengigkeiten

Wie wir in den vorangegangenen Beispielen gesehen haben, kann ein
Target keine, eine oder mehrere Abhaengigkeiten enthalten. Sind
mehrere Abhaengigkeiten vorhanden, werden diese immer von links nach
rechts ausgewertet.

Ist eine Abhaengigkeit eine Datei, wird das Dateidatum mit dem Datum
des Ziels verglichen. Ist die Abhaengigkeit neuer oder das Ziel gar
nicht vorhanden, werden die Aktionen des Targets ausgefuehrt. Dies
geschieht jedoch erst, nachdem ALLE Abhaengigkeiten ausgewertet
wurden.

Betrachten wir nun die Faelle im Einzelnen:

o Target ohne Abhaengigkeiten: Die Aktionen des Targets werden aus-
gefuehrt, wenn das Ziel nicht vorhanden ist.
o Target mit einer Abhaengigkeit: Die Aktionen des Targets werden
ausgefuehrt, wenn die Abhaengigkeit neuer als das Ziel ist, bzw.
wenn das Ziel nicht vorhanden ist.
o Target mit mehreren Abhaengigkeiten: Die Aktionen des Targets
werden ausgefuehrt, wenn mindestens eine Abhaengigkeit neuer als
das Ziel ist, bzw. das Ziel nicht vorhanden ist, aber erst nach-
dem ALLE Abhaengigkeiten ausgewertet wurden.



2.1.3. Die Aktionen

Ein Target kann keine, eine oder mehrere Aktionen haben. Diese werden
ausgefuehrt, wenn das Target "out of date" ist, d.h. wenn mindestens
eine Abhaengigkeit neuer als das Ziel ist oder das Ziel nicht
existiert (genauer: Keine Datei existiert, die wie das Ziel heisst).

Die Aktionen werden in der Reihenfolge ausgefuehrt, in der sie
angegeben sind, also von oben nach unten.

Tritt bei der Ausfuehrung einer Anweisung ein Fehler auf, bricht der
Make-Lauf an dieser Stelle ab. Ist dies nicht gewuenscht, kann der
Anweisung ein Bindestrich vorangestellt werden. Dies unterdrueckt die
Fehlerpruefung 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 geloescht werden kann. (Anm.:
Unter WinNT gibt "erase" KEINEN Fehler-Code zurueck, wenn es keine
Datei zum Loeschen findet. Unter anderen BS ist das aber durchaus
so.)

Ausserdem gibt es die Moeglichkeit, die Anzeige der Kommandozeile zu
unterdruecken. Dies geschieht durch ein vorangestelltes @. Beispiel:

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

Die beiden Praefixe koennen auch kombiniert werden. Beispiel:

clean:
-@erase *.obj
-@erase *.exe



2.2. Variablen
--------------

Bei komplexeren Make-Projekten kann es wuenschenswert sein, immer
wiederkehrende Zeichenfolgen abzukuerzen, bzw. Einstellungen vorzu-
nehmen, die fuer 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 moeglichen Stellen des Makefiles geschehen, also auch
bei der Definition weiterer Variablen. Beispiel:

CFLAGS=$(CGFLAGS) $(CDFLAGS)

Waehrend 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 beschliessen. Beachten
Sie dabei folgendes:

o Zeilen, die mit einem # beginnen, sind Kommentare und werden vom
Make-Tool ignoriert.
o Es genuegt an EINER Stelle einzugreifen, wenn z.B. die Compiler-
Optionen fuer ALLE OBJ-Module geaendert werden sollen.
o Beachten Sie die Verwendung der Variablen PROJ. Sie wird an allen
drei moeglichen Positionen eines Targets ausgewertet: Als Ziel,
als Abhaengigkeit und in einer Aktion.
o Das Ganze ist eigentlich immer noch zuviel Tipparbeit, das geht
noch kleiner! Aber dazu spaeter mehr.
o 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, dass 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

#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------



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:

o Es werden keine Abhaengigkeiten definiert.
o Das Ziel ist nicht eine Datei, sondern eine Art Schablone, die
angibt fuer 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" ueberfuehrt werden kann, in dem die
Aktion "cl $(CFLAGS) $< " ausgefuehrt wird. Diese Build-Regel wird
vom Make-Tool auf alle Targets angewendet, die als Ziel ein OBJ-
Datei und als erste Abhaengigkeit eine CPP-Datei haben, sofern das
Target selber KEINE Aktionen definiert.

Fuer jede Kombination von Dateiendungen muss eine eigene Build-Regel
definiert werden. Sollen mit dem Makefile z.B. auch C-Dateien be-
arbeitet werden, so waere noch folgende Build-Regel hinzuzufuegen:

.c.obj:
cl $(CFLAGS) $<

Das scheint im ersten Ansatz laestig 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
muessen/koennen (z.B. Watcom). Oder denken sie an Projekte, die auch
Assembler-Source enthalten:

.asm.obj:
masm $(AFLAGS) $<

Bleibt noch die Bedeutung der Variablen "$< " zu klaeren. Sie ist nur
in Build-Regeln gueltig und liefert den Namen der ersten
Abhaengigkeit.

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 entspr. Build-Regel und die Source-Files werden korrekt compi-
liert.

Zugegeben, in diesem Beispiel ist die Tipp-Ersparnis nicht allzu
gross, aber denken Sie an ein Projekt mit >20 Source-Files. Es wird
durch den Einsatz von Build-Regeln uebersichtlicher und leichter zu
pflegen.



Kapitel 3 und 4 finden Sie in Teil II von Makefiles.

===---------------------------------------------------------
Dieser Artikel bietet eine Einfuehrung in die Thematik der Makefiles,
wie sie im Bereich der Softwareentwicklung zum Einsatz kommen.

In Teil I finden Sie die Abschnitte Einleitung und Grundlagen, in Teil
II Spezialitaeten und die Schlussbemerkung.

Der Autor ist Dipl.Ing.(FH) der Technischen Informatik und entwickelt
seit vielen Jahren Software fuer die verschiedensten Betriebsysteme.
Sein Erfahrungsspektrum reicht von maschinennaher Programmierung
(Device-Treiber) bis zur Anwendungsentwicklung (GUI-Programmierung).
Dabei kamen die verschiedensten Entwicklungswerkzeuge (Compiler,
Assembler, Debugger, IDEs, Make-Tools usw.) zum Einsatz. Speziell
waehrend seiner Taetigkeit als Device-Treiber-Entwickler wurden
Makefiles sehr intensiv eingestzt.

0. Inhaltsverzeichnis
=====================

1. Einleitung
2. Grundlagen
2.1. Targets
2.1.1. Das Ziel
2.1.2. Die Abhaengigkeiten
2.1.3. Die Aktionen
2.2. Variablen
2.3. Build-Regeln
3. Spezialitaeten
3.1. Lange Zeilen
3.2. Aktionen, die ueber Compilieren und Linken hinausgehen
3.3. Bedingte Ausfuehrung
3.4. Unter-Makefiles
4. Schlussbemerkung



Kapitel 1 und 2 finden Sie in Teil I von Makefiles.



3. Spezialitaeten
================

3.1. Lange Zeilen
-----------------

Wenn man groessere Projekte realisiert kommt es durchaus vor, dass
z.B. Abhaengigkeiten sehr lang werden. Damit das Makefile trotzdem
uebersichtlich bleibt, gibt es die Moeglichkeit, Zeilen ueber das
Zeilenende hinaus fortzusetzen. Das hoert 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 Abhaengigkeit 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 Abhaengigkeiten in der naechsten 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 "Zeilenverlaengerung". Targets
sind der haeufigste Anwendungsfall, grundsaetzlich lassen sich jedoch
alle Zeilen auf diese Weise "verlaengern".




3.2. Aktionen, die ueber Compilieren und Linken hinausgehen
----------------------------------------------------------

Wie bereits in der Einleitung erwaehnt, werden Makefiles
ueblicherweise zum Steuern eines Compile-Laufs verwendet. Jedoch
koennen sie wesentlich mehr.

Stellen Sie sich z.B. eine Situation vor, in der mehrere Entwickler
an einem Projekt arbeiten, jeder davon aber an einem unabhaengigen
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 muessen, ist
vereinbart, dass jeder Entwickler seine Freigabeversionen in einem
bestimmten Verzeichnis auf dem Netzwerk abliefert.

Diese Aufgabe kann von einem Makefile mit uebernommen 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 - Loescht 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:

o Das erste Target ist das Target "help". Es gibt eine kurze An-
leitung aus, wie das Makefile zu verwenden ist. Da es das erste
Target ist, wird es immer ausgefuehrt, wenn NMAKE ohne Parameter
aufgerufen wird.
Anm.: Die Anfuehrungszeichen um den Text sind noetig, damit die
Formatierung des Texts bei der Ausgabe erhalten bleibt.
Nicht sehr schoen, aber was will man machen...
o Das Target "release" erledigt die eigentliche Freigabe-Arbeit. Es
sorgt dafuer, 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.
o Der Entwickler wird i.d.R. mit dem Target "all" arbeiten, um sein
Teilprojekt zu erstellen und zu testen. Wenn er eine stabile Ver-
sion hat, wird er diese mit Hilfe des Targets "release" ins
Freigabe-Verzeichnis stellen.



3.3. Bedingte Ausfuehrung
------------------------

Was waere die Arbeit mit Computern ohne "if"? Nicht auszudenken,
nicht wahr? Die Entwickler von Make-Tools wissen das natuerlich auch
und haben aus diesem Grunde die Moeglichkeit geschaffen, Teile eines
Makefile bedingt ausfuehren 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 aendern zu muessen waere mehr als laestig. 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 - Loescht 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 Ausfuehrung des Makefiles wird an folgenden Stellen
eingesetzt:

o Im Abschnitt "Defaults". Hier wird sichergestellt, dass eine
Variable, die wir spaeter auswerten wollen, auch definiert ist.
o Im Abschnitt "Compiler-Flags" werden die Flags fuer den Compiler-
Aufruf gesetzt. Dabei werden fuer Debug- und Release-Version
verschiedene Flag-Saetze verwendet.
o Der Abschnitt "Linker-Flags" arbeitet wie der Abschnitt "Compiler-
Flags", nur eben fuer die Linker-Optionen.
o Im Abschnitt "Sub-Targets" wird beim Target
"$(RELEASEDIR)\$(PROJ).exe" eine Warnung ausgegeben, wenn eine
Debug-Version ins Freigabe-Verzeichnis kopiert wird.



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.

Sinnvoll 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 fuer
verschiedene Laender in verschiedenen Sprachen erzeugt werden. Aus
diesem Grund wurden alle Texte in ein Modul namens TEXTnnn.CPP
ausgelagert. Das "nnn" steht dabei fuer eine dreistellige
Laenderkennung, die sich nach den internationalen Telefonvorwahlen
richtet (also. 001 für USA, 049 fuer Deutschland usw.).

Neben den Dateien MAIN.CPP, MODULE1.CPP und MODULE2.CPP, die fuer
alle Versionen benoetigt werden, gibt es noch die Dateien TEXT001.CPP
und TEXT049.CPP (wir wollen uns der Einfachheit halber auf 2
Laenderversionen beschraenken). TEXT001.CPP enthaelt die
Programmtexte in Englisch, TEXT049.CPP in Deutsch.

Mit unserem bisherigen Wissen ueber Makefiles faellt es uns nicht
schwer, ein Makefile zu erstellen, dass eine dieser Versionen
erstellt. Welche Version das ist, wollen wir ueber eine Variable
"LANG" vorgeben, die auf die Laenderkennung der Sprache gesetzt wird,
fuer die ein Programm erzeugt werden soll.

Neben dieser Anforderung wollen wir gleich noch eine weitere
Anforderung definieren: Die Ergebnisse der Compile-Laeufe sollen nach
Sprach-, Debug- und Release-Version getrennt abgelegt werden. Das
fertige Programm kann unter einem eindeutigen Namen ins
Freigabeverzeichnis kopiert werden. Damit koennen wir dann fast alle
Register ziehen, die ein Makefile beherrscht. :-)

Der Verzeichnisbaum fuer 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-Pruefung
#-------------------------------------------------------------------------

!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 fuer MAKEFILE."
@echo " Es ist nicht dafuer 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 loest. Aber schauen wir uns
zuerst den Aufruf dieses Makefiles an:

NMAKE -f sub.mak LANG=049 compile

Diese Zeile fuehrt dazu, dass eine deutsche Release-Version des
Programms erstellt wird. Die Output-Dateien werden dabei im Ver-
zeichnis 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, dass wir auch die Sprachversionen fuer die
Kennungen 032, 044 und 058 (willkuerlich gewaehlt) erstellen
koennten/ wollten, dann muessten 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 ueber eine Batch-Datei erledigen lassen, schoener
ist aber ein Haupt-Makefile, das auch noch andere Annehmlichkeiten
zur Verfuegung stellt. Ein solches Haupt-Makefile koennte 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 - Loescht 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 groessten 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. Franzoesisch = 033) hin-
zugefuegt werden, sind folgende Schritte noetig:

o Erstellen der Datei TEXT033.CPP durch Kopieren von TEXT049.CPP
nach TEXT033.CPP und Uebersetzen der Texte ins Franzoesische.
o MAKEFILE: Einfuegen der Targets "fra" und "fracopy" durch Kopieren
der Targets "ger" und "gercopy" und Aendern der Laenderkennung von
049 auf 033.
o MAKEFILE: Das Target "fra" in die Abhaengigkeiten von "all" und
das Target "fracopy" in die Abhaengigkeiten von "allcopy" mit
aufnehmen.
o SUB.MAK: Die Sprachkennung 033 in den Abschnitt "Parameter-
Pruefung" mit aufnehmen.
o Fertig.



4. Schlussbemerkung
==================

Hiermit ist unser Ausflug in die Welt der Makefiles beendet. Ich
hoffe, es hat Ihnen ebensoviel Spass gemacht wie mir.

Natuerlich konnten nicht alle Aspekte beleuchtet werden. Viele Dinge,
die ueber das oben gesagte hinausgehen, sind auch stark vom verwen-
deten Make-Tool abhaengig und wuerden den Rahmen dieser Einfuehrung
deutlich sprengen. Ich hoffe, dass ich Ihnen trotzdem das Wesen der
Makefiles naeherbringen konnte, und dass ich Ihnen "Appetit auf mehr"
machen konnte.

Wolfgang Schulz, 2000, Di. 28.03.2000 18:57, aktualisiert Di. 08.08.2017 12:07

Kontakt | Impressum | Datenschutz | © 2017-2023 NFL / Opetus Software GmbH

Die Benutzung von Informationen und Hinweisen aus Artikeln, Meldungen u.a. dieser Website erfolgt auf eigene Gefahr! Die Autoren haften nicht für irgendwelche Schäden, die durch die Nutzung dieser Informationen, ob direkt oder indirekt, resultieren.

pmail32.de · mercury32.de · key2it.de · edv-nachrichten.de