Die im letzten Abschnitt gezeigte grafische Darstellung von Abhängigkeiten und Kommandos kann hilfreich sein, um im Einzelfall zu entscheiden, welche Kommandos zum Aktualisieren einer Zieldatei nötig sind.
Da diese Arbeit aber -falls man erst einmal die Abhängigkeiten wie beschrieben aufgestellt hat- vollkommen mechanisch erfolgt (anhand des letzten Änderungsdatums aller Dateien die nötigen Wege der Grafik entlanggehen, und die an den Pfeilen stehenden Kommandos ausführen), kann man sie auch einem Programm überlassen.
Dieses Programm existiert wohl auf jedem Unix- und GNU/Linux-System, und heißt make2.1.
Leider kann make mit einem gezeichneten Diagramm nicht viel anfangen; vielmehr liest es eine Datei, in der man in Textform exakt die Informationen der Grafik eintragen muß. Diese Datei heißt in aller Regel makefile. Beim Start von make wird die Datei mit dem Namen makefile gelesen, daraus die Abhängigkeiten und die zugehörigen Kommandos gelesen, und -je nach dem letzten Änderungsdatum aller beteiligten Dateien- werden Kommandos ganz, teilweise oder gar nicht ausgeführt. Dabei zählen nicht existierende Dateien als ,,sehr alt``, so daß sie bei Bedarf neu erzeugt werden.
Um jetzt diese Abhängigkeiten und die Kommandos eintragen zu können, muß man eine besondere Syntax beachten, die im Folgenden (etwas vereinfacht) vorgestellt wird.
Die meisten Bestandteile des makefile sind in je einer
logischen Zeile untergebracht.
Eine solche logische Zeile entspricht meistens einer tatsächlichen
Zeile der Datei. Allerdings kann man eine lange logische Zeile auf
mehrere tatsächliche Zeilen verteilen, wenn sie sonst zu lang wird
(identisch zu C/C++). Dazu trennt man die Zeile einfach auf, und
beendet jede tatsächliche Zeile außer der letzten mit einem
back slash(\
). Nach dem \
darf außer dem
Zeilenendezeichen (LF, line feed) nichts mehr kommen, auch
kein Leerzeichen oder Tabulator!
Die Datei besteht hauptsächlich aus:
#
-Zeichen
folgt (falls das #
-Zeichen nicht gerade in einem String
vorkommt), ist Kommentar und wird von make ignoriert.
An jeder anderen Stelle kann man ein solches Makro expandieren lassen,
indem man den Makronamen geklammert und mit einem $
vorweg schreibt.
Beispiel:
CC = gcc CFLAGS = -Wall -cdefiniert zwei Makros mit den Namen CC und CFLAGS. Damit kann man irgendwo im makefile schreiben:
$(CC) $(CFLAGS); dies wird dann zu:
gcc -Wall -cexpandiert.
Für den Namen des Makros darf man alle Zeichen verwenden außer
white space (zumindest nicht am Anfang oder Ende des Namens),
#
, =
sowie :
. Groß-/Kleinschreibung wird unterschieden.
Anstelle der runden Klammern sind auch geschweifte Klammern erlaubt.
Wenn der Name des Makros nur aus einem Zeichen besteht (und zwar nicht
aus $
, (
und {
), dann kann man
die Klammern auch ganz weglassen. $(A)
ist gleichbedeutend mit
$A
.
Ein Dollarzeichen $
an allen anderen Stellen im makefile muß zur Unterscheidung
als $$
geschrieben werden (beispielsweise in Dateinamen).
Im obigen Beispiel würde eine solche explizite Regel sinngemäß heißen:
Die Datei haupt.o hängt ab von haupt.cpp, up.h
und db.h, und kann mit dem Kommando gcc -c haupt.cpp
aktualisiert werden.
Die für make lesbare Form dieser Regel ist allerdings etwas anders:
haupt.o : haupt.cpp up.h db.h gcc -c haupt.cppIn der ersten (logischen) Zeile steht zuerst ein Target (oder mehrere durch Leerzeichen getrennt). Nach beliebigen Leerzeichen folgt ein Doppelpunkt, dann eins oder mehrere Leerzeichen, und dann alle prerequisites, also alle Dateien, von denen das Target abhängt (voneinander mit mindestens je einem Leerzeichen getrennt).
Nach dieser Zeile mit den Abhängigkeiten folgen eines oder mehrere Kommandos (jeweils in einer logischen Zeile), um das Target auf den aktuellen Stand zu bringen. Bei einer Objektdatei, die von C- oder C++-Quelltexten abhängt, ist das meist der entsprechende Compileraufruf. Eine solche Zeile mit einem Kommando muß immer mit einem Tabulatorzeichen beginnen; Leerzeichen stattdessen sind nicht zulässig (außer bei dem nmake von Microsoft).
Nach dem Tabulatorzeichen, aber vor dem eigentlichen Kommando darf
noch das Zeichen @
und/oder ein -
eingefügt werden.
Mit einem @
erreicht man, daß das von make ausgeführte
Kommando nicht zur Standardausgabe geschrieben wird.
Mit einem -
wird verhindert, daß make sich mit einer
Fehlermeldung beendet, wenn das aufgerufene Programm nicht den Wert 0
liefert. Programme sollten ja im Fehlerfall einen Wert ungleich 0
liefern, und im Erfolgsfall eine 0 an den Aufrufer zurückgeben. make
verwendet diesen Rückgabewert üblicherweise, um zu entscheiden, ob die
restlichen Kommandos zur Erzeugung des Targets überhaupt noch Sinn
machen (wozu sollte man den Linker aufrufen wollen, wenn ein Quelltext
nicht kompiliert werden kann?).
Wenn mehrere Kommandos nötig sind, um das Target zu erzeugen, folgen diese Kommandos jeweils einzeln auf einer (logischen) Zeile.
Jedes Kommando wird in einer eigenen Shell abgearbeitet. Deshalb ist es sinnlos, in einem Kommando das Verzeichnis zu wechseln, um in einem weiteren Kommando dann in dem neuen Verzeichnis beispielsweise etwas zu kompilieren.
Anstatt:
dir/haupt.o : dir/haupt.cpp dir/up.h dir/db.h cd dir gcc -c haupt.cppist es also nötig zu schreiben:
dir/haupt.o : dir/haupt.cpp dir/up.h dir/db.h cd dir; gcc -c haupt.cppDadurch läuft das gesamte Kommando
cd dir; gcc -c haupt.cpp
in
einer gemeinsamen Shell, und der Verzeichniswechsel mit cd dir
wirkt sich auch auf den Compilerlauf aus.
In einem solchen Kommando kann man alles schreiben, was die aufgerufene Shell als Kommando verträgt. Es können hier also entsprechende for- if-, while-Anweisungen geschrieben werden, oder was einem als Shellkommando alles einfällt.
Eine implizite Regel beginnt mit der Endung (inklusive dem Punkt) der
Abhängigkeit (beispielsweise .cpp
), direkt gefolgt von der Endung
der zu erzeugenden Datei, wie etwa .o
. Nach einer beliebigen
Folge von Leerzeichen kommt ein Doppelpunkt. Abhängigkeiten wie bei
einer expliziten Regel werden hier kaum vorkommen.
In einer oder mehreren folgenden logischen Zeilen werden dann die
Kommandos geschrieben, die zur Erzeugung des Targets nötig sind.
In diesen Kommandos kann man spezielle vordefinierte Makros verwenden,
um den Namen des Targets, der Abhängigkeiten und so weiter
einzusetzen. Diese werden noch im Folgenden erläutert; um das Beispiel
komplett schreiben zu können aber schon im Voraus: der Name der
ersten (und in diesem Fall einzigen) zu kompilierenden Datei (also des
prerequisites) kann in einer Regel als $<
geschrieben werden.
Beispiel: Um aus einer beliebigen C++-Datei eine Objektdatei zu kompilieren, kann man die folgende Regel verwenden:
.cpp.o: g++ -Wall -c $<Dabei wird
$<
durch den Namen der ersten (und damit der
einzigen) Abhängigkeit ersetzt, also durch irgendwas.cpp
.
Auch hier muß vor dem Kommando exakt ein Tabulatorzeichen stehen.
Eine implizite Regel wird von make nur verwendet, wenn für eine zu erzeugende Datei keine explizite Regel vorhanden ist.
Beim Start von make ist bereits ein Satz von impliziten Regeln
vorhanden. Diese kann man sich neben anderen Voreinstellungen mit:
make -p -f /dev/null
oder
make -qp
anzeigen lassen.
Wenn man dann noch beachtet, daß man zwar nicht innerhalb von Regeln, aber zwischen selbige Leerzeilen einstreuen kann, um die Datei etwas übersichtlicher zu machen, dann kann man für das obige Beispiel aus drei Quelltexten folgendes makefile schreiben:
# Time-stamp: "(25.11.01 09:00) makefile [Klaus Wachtler (aw38)]" # makefile für ein kleines Projekt aus haupt.cpp, up.cpp/.h, # sowie db.cpp/.h. haupt: haupt.o up.o db.o g++ haupt.o up.o db.o -o haupt haupt.o: haupt.cpp up.h db.h g++ -c haupt.cpp up.o: up.cpp up.h db.h g++ -c up.cpp db.o: db.cpp db.h g++ -c db.cpp
Durch die Verwendung einer impliziten Regel kann man die Datei etwas vereinfachen:
haupt: haupt.o up.o db.o g++ haupt.o up.o db.o -o haupt haupt.o: haupt.cpp up.h db.h up.o: up.cpp up.h db.h db.o: db.cpp db.h -cpp.o: g++ -c $<
(Bitte beachten: Vor den Kommandos g++
... stehen keine
Leerzeichen, sondern jeweils ein Tabulator!)
Mit dem Aufruf:
make
wird dann das Programm make gestartet. Dieses liest die Datei
makefile und findet als erstes target die Datei haupt.
Da diese Datei laut makefile von haupt.o, up.o sowie von db.o abhängt, prüft make als nächstes, ob diese drei Dateien existieren und auf dem neuesten Stand sind, also neuer als ihre eigenen prerequisites. Gegebenenfalls werden sie aktualisiert mit den angegebenen Kommandos, und dann wird -falls nötig- haupt neu gelinkt.
Davon abgesehen, daß make das erste gefundene Target zu aktualisieren versucht, ist die Reihenfolge der Regeln im makefile gleichgültig.
Wenn man nicht das erste im makefile aufgeführte target aktualisiert
haben möchte, gibt man einfach das gewünschte Target (oder mehrere)
als Argumente an:
make up.o db.o
würde die Dateien up.o und db.o aktualisieren, ohne sich
um haupt zu kümmern.
Falls man den Namen makefile nicht verwenden möchte, kann man
beim Aufruf von make mit der Option -f
einen anderen
Namen vorgeben.