Ant - Das Apache Build-Werkzeug für Java

Ein Vergleich mit dem Make-System
Simon Lembke (WI4139)


... [ Seminar WS 2002/03 ] ... [ Ant ] ... [ Größere Projekte ] ... [ Größere Projekte (Fortsetzung) ] ... [ Fehlerbehandlung ] ...

Übersicht : Größere Projekte (Fortsetzung)


Suchpfade

Es macht eigentlich immer Sinn, die class-Dateien getrennt von den Quelldateien aufzubewahren. Damit Make die class-Dateien trotzdem findet, kann man entweder den Pfad an jeder Stelle einfügen, an der man sich auf sie bezieht, oder man erweitert den Pfad, in dem Make normalerweise sucht. Wenn nichts anderes angegeben ist wird nur das aktuelle Verzeichnis durchsucht. Um dies zu ändern hat man wiederum zwei Möglichkeiten: man schreibt die zu durchsuchenden Pfade in die Variable VPATH oder man benutzt die Anweisung vpath. Im ersten Fall werden alle Dateien, die als Quelle oder Ziel auftauchen, zusätzlich in den angegebenen Verzeichnissen gesucht. Mehrere Pfade werden durch Leerzeichen oder Doppelpunkte (unter Unix) bzw. Semikolons (unter Windows) getrennt.

VPATH := classes

Die Anweisung vpath bewirkt genau dasselbe, nur das man zusätzlich noch einschränken kann, nach welchen Dateien in den angegebenen Verzeichnissen gesucht wird.

vpath *.class classes

In diesem Fall wird nur nach Dateien, deren Name mit .class endet zusätzlich im Verzeichnis classes gesucht. Betrachten wir zum Beispiel folgende Regel:

Test.class : Test.java
	javac -d ${classdir} Test.java

Beim ersten Durchlauf wird der Java-Compiler die Datei Test.java kompilieren und im Verzeichnis ablegen, das in der Variablen classdir steht. Rufen wir Make direkt im Anschluss und ohne die Quelle geändert zu haben wieder auf, so wird Make den Java-Compiler erneut aufrufen, da es die Datei Test.class nicht findet. Wenn wir aber, wie oben angebenden, den Suchpfad ändern, so wird Make, unabhängig von der gewählten Methode, die class-Datei finden und feststellen, dass das Ausführen des Befehls unnötig ist.


Probleme

Bisher haben wir Make dazu gebracht, eine einzige Regel für eine ganze Gruppe von Dateien zu benutzen und Dateien nicht nur im aktuellen Verzeichnis zu suchen. Jetzt fehlt nur noch eine einfache Möglichkeit Make mitzuteilen, welche Klassen am Ende existieren sollen. Denn was bringt es, wenn eine Regel für mehrere gesuchte Dateien verwendet werden kann, aber keine Dateien gesucht werden?
Folgender Ausschnitt aus einem Makefile kann den gewünschten Erfolg nicht bringen:

compile : *.class
%.class : %.java
	javac -d . $<

In diesem Fall hängt zwar compile von allen class-Dateien im Verzeichnis ab, wenn jedoch eine class-Datei noch nicht existiert wird sie auch niemals erstellt werden. Dieses Verhalten wird an einem Beispiel sehr schnell deutlich. Angenommen wir haben in unserem Verzeichnis die Dateien A.java, B.java und C.java aber nur A.class und B.class, weil C.java vielleicht gerade erst erstellt wurde. Beim Aufruf von make compile geschieht nun folgendes: compile hängt von A.class und B.class ab. Diese beiden werden mit Hilfe der Pattern-rule bei Bedarf aktualisiert. Danach sind alle Vorbedingungen für compile erfüllt, C.java wurde aber noch nicht kompiliert.


Funktionen in Makefiles

Noch weniger Sinn macht es, compile von *.java abhängig zu machen, denn die Java-Dateien sind die Quellen, aus denen die class-Dateien erzeugt werden, wie man an der Pattern-rule sehr schön sehen kann. Aber die Idee geht in die richtige Richtung. Am besten wäre es, die Namen von allen vorhandenen java-Dateien zu nehmen, .java durch .class auszutauschen und compile von diesen Dateien abhängig zu machen.
Für solche und ähnliche Fälle gibt es Funktionen in Make, mit denen man Texttransformationen durchführen kann. Dazu gehören das Ausfiltern von bestimmten Mustern, das Sortieren von Listen oder eben das Austauschen von bestimmten Zeichenketten. Funktionen werden ähnlich wie Variablen benutzt:

$(funktion parameter)
${funktion parameter}

Für unsere Zwecke benötigen wir zwei Funktionen, nämlich wildcard um eine Liste der Java-Dateien zu erhalten und patsubst um die einzelnen Namen zu ändern. wildcard erwartet ein oder mehrere Muster, und liefert die Namen von allen Dateien im aktuellen Verzeichnis als Leerzeichen getrennte Liste zurück, die einem der Muster entsprechen.

FILES := ${wildcard *.java}

In der Variablen FILES steht danach z.B. A.java B.java C.java. Die Funktion patsubst tauscht alle Zeichenketten in einer Liste, die auf das übergebene Muster passen gegen eine andere übergebene Zeichenkette aus.

FILES := ${patsubst %.java,%.class,${FILES}}

Das Ersetzen mit Hilfe des „%“ funktioniert hier genau wie im Abschnitt Pattern-rules beschrieben. Nach dem Aufruf der Funktion sind in der Variablen FILES alle java-Dateien durch class-Dateien ersetzt. Der Inhalt lautet nun also A.class B.class C.class und kann als Voraussetzung für compile verwendet werden:

FILES := ${wildcard *.java}
FILES := ${patsubst %.java, %.class, ${FILES}}
compile : ${FILES}


%.class : %.java
javac -d . $<

Zusammenfassendes Beispiel

Wenn man nun die verschiedenen Konzepte aus den vorigen Abschnitten gemeinsam betrachtet, ergibt sich eine Möglichkeit, die Kompilation ganzer Pakete über ein Makefile zu steuern. Allein über das Anpassen des Suchpfades wird es jedoch nicht funktionieren, da Namen von Quelldateien durchaus doppelt vorkommen können, solange sie in verschiedenen Verzeichnissen liegen. Es könnte also passieren, dass einer Quelle eine falsche Zieldatei zugeordnet wird, und sie dadurch eventuell nicht oder aber unnötig kompiliert wird.
Für unseren Zweck erweitern wir das Beispiel aus dem vorigen Abschnitt so, dass nicht nur Quelldateien im aktuellen Verzeichnis berücksichtigt werden, sondern auch diejenigen, die in ausgewählten Unterverzeichnissen liegen. Zunächst legen wie ein paar Variablen an, die Quell- und Zielverzeichnis sowie alle zu berücksichtigenden Unterverzeichnisse enthalten:

SRCDIR = src
BUILDDIR = build
SUBDIRS = . de de/fh-wedel

Die Quellen liegen also im Verzeichnis src, die kompilierten Klassen werden in build abgelegt und berücksichtigt werden sollen alle Dateien im eigentlichen src-Verzeichnis (gekennzeichnet durch den Punkt), dessen Unterverzeichniss de und darunter zusätzlich das Verzeichniss fh-wedel. Wir müssen nun eine Liste der Quelldateien in allen angegebenen Verzeichnissen erstellen. Dazu benutzen wir zum einen die bereits vorgestellte Funktion wildcard, zum anderen eine Art for-Schleife, wie man sie aus den meisten Programmiersprachen kennt. Die foreach-Funktion in Make fügt, vereinfacht gesagt, für jedes Element in einer übergebenen Liste einen Text an eine Veriable an. Dieser angehängte Text kann beliebig zusammengesetzt sein und z.B. aus einem anderen Funktionsaufruf stammen. Der allgemeine Aufruf lautet:

$(foreach var,list,text)

Bei jedem Durchlauf wird der Variablen var der nächste Wert aus list zugewiesen, und erst danach text ausgewertet. Man kann also in text auf den jeweils aktuellen Wert von var zugreifen. Wenn die Schleife durchgelaufen ist wird der komplette Text aus allen Durchläufen zurückgegeben. Wir verwenden als Parameter text die wildcard-Funktion, um uns die Java-Dateien im jeweiligen Verzeichnis zurückgeben zu lassen.

FILES := $(foreach dir,$(SUBDIRS),$(wildcard ${SRCDIR}/${dir}/*.java))

Für jedes Verzeichnis in SUBDIRS wird also die wildcard-Funktion aufgerufen und damit eine Liste von Java-Dateien aus allen Unterverzeichnissen von SRCDIR erstellt. Durch den Punkt in SUBDIRS werden auch die Quellen aus SRCDIR mit aufgenommen.
Wie im letzten Abschnitt beschrieben müssen noch die Dateinamen von .java in .class und, weil vor jedem Dateinamen der Wert von SRCDIR steht, der Anfang des Pfades durch den Wert von BUILDDIR ersetzt werden, denn schließlich sollen die kompilierten Klassen dort gesucht werden.

CLASSES := $(patsubst %.java,%.class,$(FILES))
CLASSES := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(CLASSES))

Zum Schluss erstellen wir noch zwei Regeln: in der ersten, und damit der Standard-Regel, geben wir als Abhängigkeiten alle class-Dateien an, damit bei jedem Aufruf geprüft wird, ob eine oder mehrere neu erstellt werden müssen.

all : $(CLASSES)

Die zweite Regel ist natürlich eine Pattern-rule, da alle Klassen auf die gleiche Art und Weise kompiliert werden. Dabei müssen wir jedoch beachten, dass die Verzeichnisstruktur unterhalb des BUILDDIR auf das Verzeichnis SRCDIR abgebildet werden muss.

$(BUILDDIR)/%.class : $(SRCDIR)/%.java
   javac -classpath $(SRCDIR):$(BUILDDIR) -d $(BUILDDIR) $<

Damit wird jeder class-Datei in einem Unterverzeichnis von BUILDDIR ihre java-Datei im gleichen Unterverzeichnis von SRCDIR zugeordnet. Ein Aufruf von make all erstellt nun alle Klassen bzw. nur die, bei denen die Quellen geändert wurden.


... [ Seminar WS 2002/03 ] ... [ Ant ] ... [ Größere Projekte ] ... [ Größere Projekte (Fortsetzung) ] ... [ Fehlerbehandlung ] ...