Grundlagen: MakefilesGrundlagen: 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
|