Für kleinere Programmpakete ist das Makefile meist noch recht
übersichtlich. Allerdings verleiten die vielen Möglichkeiten, die make
bietet, dazu dass man sich schnell im Regel-Dschungel die Orientierung
verliert und das Makefile immer mehr ein undurchsichtiges Eigenleben
entwickelt. Und wehe, das Wissen über den Aufbau und Ablauf der
Makefiles konzentriert auf einen einzelnen Spezialisten. Wenn dieser
dann nicht mehr zur Verfügung steht, kann die weitere Pflege und
Wartung der Makefiles den eigentlichen Entwicklungsaufwand übersteigen.
Während man bei kleineren Projekten das Makefile notfalls nochmals
aufsetzen kann, ist dies bei größeren Projekten oft mit erheblichem
Aufwand verbunden. Daher sollte man auch (oder gerade) bei Makefiles nach
dem Motto
So einfach wie möglich - aber nicht einfacher
handeln.
Dieses Kapitel beschäftigt sich mit verschiedenen Aspekten, wie man
mit make größere Projekte verwalten kann. Später werden wir
Makefile-Richtlinien kennenlernen, die die Einarbeitung und Wartung
von (eigenen oder fremden) Makefiles erleichtern können.
Einige der Probleme, die den Einsatz von make erschweren, sind:
-
Verzeichnis-Baum
-
Bedingte Kompilierung (#if ... #endif)
-
versteckte Abhängigkeiten (zum Beispiel über Header-Dateien)
-
Versionierung
Manche der Probleme resultieren aus Unzulänglichkeiten des Compilers,
manche resultieren aus Annahmen und Beschränkungen einiger Unix-Werkzeuge.
Diese spiegeln sich zum Teil in den eingebauten Suffix-Regeln wieder.
Ursprünglich war make nur zur Vereinfachung der Kompilierung gedacht,
hat sich aber über die Jahre zu einem mächtigen Entwicklungswerkzeug
gemausert. Nicht zuletzt auch deswegen, weil es inzwischen eine Reihe
von Werkzeugen gibt, die um make herum gebaut wurden, um die
Einschränkungen aufzuheben.
|
In der Praxis werden Dummy-Ziele recht häuig eingesetzt, um mehrere
Ziele zusammenzufassen:
# compile all
all : anna berta carmen
anna : anna.c
$(CC) -g -o anna anna.c
berta : berta.c
$(CC) -g -o berta berta.c
carmen : carmen.c
$(CC) -g -o carmen carmen.c
|
Der Entwickler braucht nur make all einzugeben und sämtliche
Programme werden übersetzt.
|
Eine etwas subtilere Variante von Dummy-Zielen sind
Timestamp-Targets. Damit werden Ziele bezeichnet, die zwar angelegt
werden, aber nicht als Ziel gebraucht werden. In Wirklichkeit werden
sie zur Synchronisation von Aktivitäten verwendet:
TIMESTAMP.strip : anna berta carmen
strip $?
touch $@
|
Was macht dieses Ziel? Falls TIMESTAMP.strip nicht existiert oder
eines der abhängigen Dateien anna, berta oder carmen neuer ist,
werden die entsprechenden Dateien ge-strip-t (das strip-Kommando
entfernt die Symbol-Tabelle aus dem Programm. Dadurch wird das
Programm kürzer, kann aber dafür nicht mehr debuggt werden.) und danach
TIMESTAMP.strip angelegt bzw. mit einem neuen Zeitstempel versehen
(über das touch-Kommando).
Die Datei TIMESTAMP.strip dient also nur dazu, festzustellen ob
anna, berta oder carmen schon einen strip" hinter sich
haben. Diesen Trick findet man häufiger in Makefiles. Wenn Sie sich
also schon immer gefragt haben, zu was Dateien der Größe 0 gut sein
sollen, hier ist eine mögliche Antwort.
Allerdings hat diese Lösung auch einen Haken: man sieht der Datei
TIMESTAMP.strip nicht an, zu was sie gut sein soll und ein
ordnungsliebender Mensch könnte leicht auf die Idee kommen, diese Datei zu
löschen, da sie die Größe 0 hat - weg damit! Daher ist es besser,
dieser Datei einen sinnvollen Inhalt zu geben, damit sie
-
eine Größe > 0 hat und
-
damit der ahnungslose Benutzer einen Schimmer bekommt,
zu was diese Datei gut sein könnte.
Dies kann man zum Beispiel durch folgende Regel erreichen:
TIMESTAMP.strip : anna berta carmen
strip $?
echo "last strip of anna, berta or carmen:" > $@
date >> $@
|
|
Am wenigsten problematisch ist es, wenn man sämtliche Dateien in einem
einzigen Verzeichnis hat. Leider ist dieses Vorgehen bei größeren
Projekten nicht praktikabel und üblicherweise hat man seine Dateien
über mehrere Verzeichnisse verteilt.
Eine Möglichkeit, mit dem Verzeichnisbaum fertig zu werden, besteht
darin, in jedes Verzeichnis ein Makefile zu plazieren, das über das
Makefile im übergeordneten Verzeichnis aufgerufen wird.
Das oberste Makefile könnte dabei folgendermaßen aussehen:
SUBDIRS = src lib
all :
for d in $(SUBDIRS); do \
(cd $$d; make all) \
done
|
Voraussetzung dafür ist natürlich, dass die drunterliegende Makefiles
ein Ziel all besitzen.
|
Der rekursive Aufruf von make ist auch dazu geeignet, Informationen
und Flags durchzureichen.
Beispiel:
CFLAGS = -O
DEBUGFLAGS = -g $(CFLAGS)
testbin :
make bin "CFLAGS=$(DEBUGFLAGS)"
|
In diesem Beispiel wird durch make testbin dasselbe Makefile noch ein Mal
aufgerufen, jedoch mit geänderten CFLAGS. Mit demselben Verfahren
können auch Makros in drunterliegenden Makefiles überschrieben werden.
GNU-make und die meisten make-Versionen besitzen auch ein internes
MAKE-Makro. Damit lautet die obere testbin-Regel:
testbin :
$(MAKE) bin "CFLAGS=$(DEBUGFLAGS)"
|
Der Vorteil des internen MAKE-Makros ist die Weitergabe der Optionen
beim Aufruf von make. Wird beispielsweise make mit der Option -n
aufgerufen, so wird damit auch alle weiteren makes mit -n aufgerufen
(die Option -n zeigt nur die Kommandos an, führt sie aber nicht
aus).
|
Jedes Makefile hat seine eigenen Makros. Um die Verwaltung und Verwirrung
gering zu halten, sollte jedes Makefile dieselben Makro-Namen
besitzen. Jeder make-Aufruf sollte wichtige Makros weiterreichen.
Beispiel:
all :
for d in $(SUBDIRS); do \
(cd $$d; \
make all "CFLAGS=$(CLAGS) \
LDFLAGS=$(LDFLAGS) \
LIBFLAGS=$(LIBFLAGS)) \
done
|
|
|
Objekt-Dateien hängen nicht nur von C-Sourcen, sondern auch von
Header-Dateien ab, d.h. man müsste diese eigentlich mit in die
Abhängigkeiten aufnehmen:
love.o : love.c darling.h
$(CC) love.c
|
Dies wird man aber in den seltensten Fällen in Makefiles antreffen,
und zwar meist aus folgenden Gründen:
-
Faulheit des Programmierers
-
versteckte Abhängigkeiten
-
zu dynamisch
-
zu großer Overhead
Glücklicherweise erhält der Programmierer hier Unterstützung vom
(GNU-)Compiler: Mit der Option -M generiert der Compiler eine Liste
von Abhängigkeiten, die ins Makefile übernommen werden können:
prompt% gcc -M love.c
love.o: love.c darling.h
|
Einfacher geht es mit dem Programm makedepend:
depend:
makedepend -- $(CFLAGS) -- $(SRC_FILES)
|
Es fügt an das Ende des Makefiles die fehlenden Abhängigkeiten ein:
love.o : love.c
$(CC) love.c
depend:
makedepend -- $(CFLAGS) -- $(SRC_FILES)
# DO NOT DELETE
love.o: darling.h
|
Zusammen mit der ersten Abhängigkeit (love.o : love.c) wird jetzt
love.c neu übersetzt, wenn sich love.c oder darling.h ändert.
|
Viele Makefiles innerhalb verschiedener Verzeichnisse eines Projekts
sehen sich in großen Teilen ähnlich: es werden die gleichen CFLAGS definiert,
der gleiche Compiler aufgerufen, die gleichen Suffix-Regeln verwendet,
usw. Was liegt näher, als diese Gemeinsamkeiten in einer gemeinsamen
Datei zu verwalten?
Glücklicherweise kennen GNU-make und viele andere make-Varianten eine
include-Anweisung, mit der diese gemeinsame Datei eingebunden
werden kann:
Hiermit wird die Datei common.mk eingebunden. Syntaktisch sieht das
ganze dann so aus, dass diese Datei hier an diese Stelle
hineinkopiert wird.
Bei der Verwendung der include-Anweisung ist darauf zu achten, das
include am Zeilenanfang steht und dahinter mindestens ein Leerzeichen
oder Tabulator-Zeichen folgt.
|
|