make

In der Open-Source-Bewegung darf eines ganz sicher nicht fehlen: "make". Mittels dieser vier Buchstaben wird das Kompilieren von einfachen oder auch sehr komplexen Programmen so kinderleicht, dass kaum ein Programm auf dieses Tool verzichtet.

Damit "make" auch weiß, was es zu tun hat, muss das sogenannte "Makefile" vorhanden sein. Um solche Makefiles besser verstehen zu können, wollen wir eines für ein imaginäres C-Programm schreiben. Es spielt aber wirklich keine Rolle, ob "make" für C, C++, Fortran oder für irgendetwas anderes als eine Programmiersprache eingesetzt wird. "make" lässt sich überall dort nützlich, wo es gilt, stupide Wiederholungsarbeit zu leisten.

Mit einem "make"-Aufruf wird entweder nach der Datei "makefile" oder "Makefile" in dem Verzeichnis gesucht, in dem der Anwender sich gerade befindet. Andere Namen für das Makefile sind ebenfalls machbar. "make" muss dann aber mit "make -f Makefile.new" darauf aufmerksam gemacht werden.

Grundlegendes

Zunächst sollten wir uns klar machen, was beim Programmieren eines Programms abläuft: Unser imaginäres C-Programm bestehe aus fünf C-Dateien und, sagen wir, einer Header-Datei. Zur Erzeugung des ausführbaren Programms sind mehrere Schritte notwendig: Zunächst müssen die C-Dateien unter Einbindung des Header-Files kompiliert werden. Zwar taucht unsere Header-Datei beim Kompilieren nirgends auf, doch wird diese in den C-Dateien mit...

#include "inlag.h"

... eingebunden. Nach diesem Schritt stehen fünf Object-Dateien zur Verfügung (Endung ".o"). Diese sind nun nur noch zu binden (linken) und schon haben wir das fertige Programm. Von Hand müssten wir mindestens folgendes eingeben:

gcc -c imag1.c 
gcc -c imag2.c 
gcc -c imag3.c 
gcc -c imag4.c 
gcc -c imag5.c 
gcc imag1.o imag2.o imag3.o imag4.o imag5.o -o imag

Nach diesen sechs Befehlen hätten wir unser Endprodukt "imag". Aber all das wollen wir einfach durch Eintippen von "make" erreichen. Und am liebsten wäre uns, wenn auch wirklich nur die Dateien kompiliert werden würden, die von uns wirklich verändert wurden bzw. die von einer veränderten Datei abhängen. Also ans Eingemachte:

Schaffe, schaffe, Häusle baue.

Das Makefile ist im Wesentlichen aus Makros, Schaltern, Regeln und Blöcken aufgebaut. Die Blöcke teilen "make" mit, was zu tun ist. Ohne einen solchen im Makefile passiert nichts. Sofern nicht anders aufgerufen, interessiert sich "make" nur für den ersten Block im Makefile.

Der Aufbau eines Blockes gestaltet sich nicht allzu kompiliziert: In der Zuordnungszeile stehen zu-iächst das Target (Ziel, also zum Beispiel der Name der zu erzeugenden Datei), gefolgt von einem Doppelpunkt und den Dateien, von denen das Target abhängig ist. Nach der Zuordnungszeile können eine oder mehrere Anweisungszeilen folgen. Diese beginnen mit einem TAB gefolgt von dem eigentlich auszuführenden Befehl. Diese Befehle sind in der Regel Shell-Kommandos. Das Ganze sähe dann ungefähr so aus:

imag1.o: imag1.c imag.h <TAB> gcc -c imag1.c

Wird eine Anweisungszeile enorm lang, lässt sich diese mit einem Backslash "" umbrechen. Das "" muss in diesem Fall das letzte Zeichen in der Anweisungszeile sein, sonst kommt es zu Fehlern.

Die Dateien in der ersten Zeile werden benötigt, damit überprüft werden kann, ob dieser Schritt wirklich notwendig ist. Sind die Dateien "imag1.c" und "imag.h" älter als die evtl. schon vorhandene Datei imag1.o, so führt ,make" die Anweisungszeile nicht aus, Das zu erzeugende "imag1.o" muss dann nämlich noch auf dem aktuellen Stand sein. Dies wird man auf jedem Rechner zu schätzen wissen, da sich so das Kompilieren deutlich verkürzen lässt, wenn nur schnell mal was an einer Datei geändert wurde und diese Änderung nun getestet werden soll. Im Grunde könnten wir den obigen Block fünf mal leicht modifiziert hinschreiben und noch einen Block...

imag: imag1.o imag2.o imag3.o imag4.o imag5.o 
<TAB> gcc imag1.o imag2.o imag3.o imag4.o imag5.o -o imag

...davor hinzufügen, und wir wären fertig. Das ist aber -echt viel Schreibarbeit und beim Hinzufügen einer neuen C-Datei zu unserem Projekt wären recht viele Änderungen am Makefile nötig. Also werden wir uns das Leben gleich noch einfacher machen. Unser Makefile sieht im Moment in etwa so aus:

# Schlechtes Beispiel für ein Makefile 
# Kommentare werden mit"#" eingefügt
imag: imag1.o imag2.o imag3.o imag4.o imag5.o 
<TAB> gcc imag1.o imag2.o imag3.o imag4.o imag5.o -o imag
imag1.o: imag1.c imag.h 
<TAB> gcc -c imag1.c
imag2.o: imag1.c imag.h 
<TAB> gcc -c imag2.c
imag3.o: imag1.c imag.h 
<TAB> gcc -c imag3.c
imag4.o: imag1.c imag.h 
<TAB> gcc -c ima94.c
imag5.o: imag1.c imag.h 
<TAB> gcc -c imag5.c

Dabei wird in etwa so vorgegangen: Oberster Block ist imag, beim Aufruf von "make" wird also zunächst dieser Block abgearbeitet. Da imag von imag1.o, ..., imag5.o abhängt, springt "make" nacheinander diese Blöcke an und überprüft, ob diese Targets "uptodate" sind. Ist dem nicht so, wird dies durch den Befehl in der Anweisungszeile nachgeholt. Sind schließlich imag1.o ... imag5.o auf dem neuesten Stand, so wird die Anweisungszeile des ersten Blockes ausgeführt. Aber dies passiert auch nur, wenn die Object-Dateien nicht alle älter sind als eine evtl. schon vorhandene Datei "imag". Anstatt "imag.h" bei jedem Block in der Zuordnungszeile aufzuführen, ließe sich auch ein neuer Block definieren:

imag1.o imag2.o imag3.o imag4.o imag5.o: imag.h

Bei diesem Block darf allerdings keine Anweisungszeile mehr folgen, da dann nicht erichtlich wäre, welchen Block es bei der Erzeugung einer Object-Datei aufzurufen gilt.

Von Makros und Regeln. Ein Makro lässt sich sehr leicht definieren:

OBJLIST = imag1.o imag2.o imag3.o imag4.o imag5.o

Somit enthält OBJLIST unsere bisher benötigten Object-Files. Soll im Makefile wieder auf ein Makro zugegriffen werden, geschieht dies durch $(OBJLIST). Bei der Definition lassen sich auch bereits definierte Makros wieder verwenden (siehe nächstes Makefile-Beispiel). In unserem Beispiel könnten wir also den ersten Block durch...

imag: $(OBJLIST) 
<TAB> gcc $(OBJLIST) -o imag

... ersetzen. Beim Aufruf von "make" besteht auch die Möglichkeit in dem Makefile definierte Makros zu überschreiben. Das obige OBJLIST ließe sich mit...

make OBJLIST="imag1.o imag2.o imag3.o imag4.o imag5.o imag6.o"

... um eine Object-Datei erweitern. Eine sinnvolle Anwendung liegt in der Kompilierung für verschiedene Prozessoren. Wird zur Auswahl des Zielprozessors ein Makro CPU benutzt, so lässt sich der Prozessor bequem mit "make CPU=030" auswählen.

Doch mit dem Makro OBJLIST hätten wir nur eine kleine Vereinfachung geschaffen. Das soll uns aber noch nicht genügen. Der Rest sind Blöcke, die sich nur minimal duch die Namen der zu kompilierenden Dateien unterscheiden. Eine Regel kann uns da viel Schreibarbeit abnehmen. Der Aufbau ist einem Block ähnlich: Zunächst steht eine Art Zuordnungszeile, an der sich erkennen lässt, weiche Endung in welche überführt werden soll. Danach steht eine Anweisungszeile in der beschrieben ist, wie sich das bewerkstelligen lässt. Die Regel sieht bei uns also wie folgt aus:

.c.o:
<TAB> gcc -c $<

$< ist die Kurzschreibweise von $(<), einem Pseudomakro, das in "make" bereits implementiert ist. Es enthält den Namen der aktuellen Ausgangsdatei. Soll beispielsweise imag1.o erzeugt werden, beinhaltet $< imag1.c, bei imag2.o beinhaltet $< imag2.c und sofort. Es gibt noch andere solcher Pseudomakros, zum Beispiel:

# Besser zu handhabendes Makefile
# Makros 
OBJLIST = imag1.o imag2.o imag3.o imag4.o imag5.o 
CC = gcc 
CPU = 020 
CFLAGS = -02 -M68$(CPU)
#Regel .c.o: <TAB> $(CC) $(CFLAGS) -c $<
#Blöcke imag: $(OBJLIST) <TAB> $(CC) $(OBJLIST) -o imag
$(OBJLIST): imag.h

Soll nun das Projekt um eine weitere C-Datei erweitert werden, ist lediglich eine Änderung des Makros OBJLIST nötig.

Tatsächlich hätten wir die Regel gar nicht benötigt, da in "make" bereits viele solcher Regeln zum Beispiel für verschiedene Programmiersprachen vorhanden sind. Mit unserer eigenen Regel haben wir die bereits vorhandene überschrieben. Hätten wir keine Regel definiert, so hätte "make" bei der internen Regel auf den in $(CC) stehenden Compiler und zusätzlich auf die in $(CFLAGS) definierten Compilerflags zurückgegriffen. Um die interne Regel zu benutzen, müssen diese Makros jedoch nicht definiert sein.

Mach mich an!

Gewisse Eigenarten von "make" lassen sich per Schalter auch im Makefile steuern. Um die oben erwähnten internen Regeln abzuschalten und nur seine eigenen zuzulassen, wird der SUFFIXES-Schalter benutzt:

.SUFFIXES: 
.SUFFIXES: c.o

Somit existiert bei diesem Aufruf von "make" nur noch unsere eigene Regel. Eigene Regeln sind dann sinnvoll, wenn die internen Regeln nicht den eigenen Anforderungen entsprechen. In unserem Beispiel könnte also getrost die interne Regel für C verwendet werden. Weitere Schalter sind:

Zum Schluss...

noch ein paar kleinere Ergänzungen: Häufig finden sich in Makefiles Manipulationen von Dateinamen. In unserem Beispiel bedeutet dies, dass wir nicht die Object-Dateien auflisten, sondern die Source-Dateien:

SRCLIST = imag1.c imag2.c imag3.c imag4.c imag5.c

Das Makro OBJLIST erzeugen wir jetzt aus dem Makro SRCLIST, in dem wir die Endung c durch o ersetzen:

OBJLIST = $(SRCLIST:.c=.o)

Dies kann zum Beispiel nützlich sein, wenn ein Backup der aktuellen Version erzeugt werden soll. In diesem Fall werden die Namen der Quelltexte und nicht die der Object-Files benötigt. Unser endgültiges Makefile mit vielen der oben aufgezeigten Möglichkeiten sieht dann etwa so aus:

# Endgültige Version des Makefiles
#Makros 
SRCLIST = imag1.c imag2.c imag3.c ima94.c imag5.c 
OBJLIST = $(SRCLIST:.c=.o) 
INCLIST = imag.h 
PROGNAME = imag 
CC = gcc 
CPU = 020 
CFLAGS = -O2 -m68$(CPU) 
SHELL /bin/bash
#Schalter 
.SUFFIXES = 
.SUFFIXES = c.o 
.SILENT
#Regel 
.c.o: 
<TAB> $(CC) $(CFLAGS) -c $<
#Blöcke 
$(PROGNAME): $(OBJLIST) 
<TAB> $(CC) $(OBJLIST) -o $@ 
<TAB> strip $@
$(OBJLIST): $(INCLIST)
clean:
<TAB> rm -f $(PROGNAME) *.o
backup: 
<TAB> mkdir backup/(date +%y%m%d) 
<TAB> cp $(SRCLIST) $(INCLIST) backup/(date+%y%m%d)
help:
<TAB> echo "Parameter dieses Makefiles:" 
<TAB> echo "Hier können die durch die Blöcke 
definierten Möglichkeiten stehen"

Um nun andere Blöcke als den ersten aufzurufen, genügt ein "make <target>", also zum Beispiel "make help". Das hier gezeigte Beispiel kratzt nur an den Möglichkeiten des mit "make" Machbaren. Beispielsweise haben wir ganz außer Acht gelassen, dass sich "make" auch rekursiv aufrufen lässt. In Verbindung mit dem Überschreiben von Makros, kann so zum Beispiel die SRCLIST automatisch generiert werden. Insbesondere druch gute ShellProgrammierung lassen sich diesem universellen Tool viele Anwendungen entlocken. Nicht vergessen werden sollte, dass viele unterschiedliche Versionen von "make" existieren, die in ihren Fähigkeiten zum Teil stark variieren. Ist gar kein "make" auf dem Rechner vorhanden, muss es evtl. mit "gmake" versucht werden. Die hier abgehandelten Möglichkeiten sollten jedoch mit allen "make"-Versionen und -Variationen möglich sein. Unter [1] lässt es sich als rpm-Package für Sparemint herunterladen. Sozobon C für TOS findet sich unter [2] und beinhaltet ebenfalls ein "make", was mit jeder Shell (beispielsweise Mupfel) benutzt werden kann. Mit [3] schließlich steht eine ausführliche Anleitung zur Verfügung.

Nicht nur für Programmierer ist "make" es Wert, einmal näher betrachtet zu werden. Zugegebenermaßen scheint das Schreiben eines Makefiles zunächst etwas kryptisch und umständlich. Doch wenn diese Hürde erstmal genommen ist, möchte man "make" nie mehr missen. STC

[1] http://sparemint.atariforge.net/sparemint/html/packages/make.html
[2] http://www.nic.funet.fi/index/atari/programming/sozobonx/
[3] http://wm.gnu.org/manual/make-3.77/html-chapter/make-toc.html


Matthias Alles
Aus: ST-Computer 06 / 2003, Seite

Links

Copyright-Bestimmungen: siehe Über diese Seite