Ant: Make-Alternative für Java

Fleißige Ameise

Michael Marr

Ein Compileraufruf über die Kommandozeile reicht zwar zum Übersetzen von `Hello-World'-Programmen aus, bei komplexeren Anwendungen jedoch kommt man um Makefiles kaum herum. Deren Syntax ist allerdings gewöhnungsbedürftig und nimmt schon ein unbedachtes Leerzeichen übel. Ant ist im Rahmen des Apache-Jakarta-Projekts entstanden und bietet Java-Programmierern eine einfach zu benutzende Alternative zu Make.

Unterthema: EINGEBAUTE TASKS
Unterthema: ONLINE-RESSOURCEN
Unterthema: LISTING 1
Unterthema: LISTING 2
Unterthema: LISTING 3

Wer kennt nicht den Dreiklang von make config, make und make install? Das Build-Tool der Wahl für die C- und C++-Entwicklung ist Make, genauer gesagt GNU Make. Auch dem Java-Programmierer, der sich nicht von einer IDE abhängig machen möchte, blieb bisher nichts anderes übrig, als Make zu verwenden. Von besonderer Liebe dürfte diese Beziehung allerdings bei den wenigsten Entwicklern geprägt sein, einige behaupten sogar, dass Makefiles inhärent böse seien.

So ging es auch den Entwicklern von Ant. Sie hatten sich einige Male zu oft gefragt, ob das abgesetzte Make nicht geklappt hatte, weil da ein Leerzeichen vor einem Tabulator stand, und entschlossen sich, dass ein neues Tool her muss. Dieses sollte zudem den Build-Prozess von Java-Programmen plattformunabhängig gestalten, und damit man nicht noch eine neue Syntax erlernen muss, wählten sie als Format für die Build-Dateien XML.

Ant entstand als Teil des Apache-Jakarta-Projekts, ist freie Software, steht unter der Apache-Software-Lizenz und ist unter jakarta.apache.org/ant/ verfügbar. Die aktuelle stabile Version ist Ant 1.2.

Die Installation der binären Distribution gestaltet sich einfach:

Für genauere Anweisungen sei an dieser Stelle auf das hervorragende `Ant User Manual' verwiesen, das ebenfalls an der oben genannten Stelle erhältlich ist.

Um das Programm aufzurufen, genügt der Befehl ant. Als erstes sucht Ant im aktuellen Verzeichnis nach der Datei build.xml. Findet es diese Datei nicht, sucht es jeweils eine Verzeichnisebene höher weiter. Optional kann der Benutzer mit -buildfile eine andere Datei angeben. Listing 1 zeigt ein Beispiel für build.xml.

XML-Datei fungiert als Buildfile

Das Buildfile ist eine XML-Datei und enthält als Wurzelelement genau ein project-Element, mit den Attributen Name (name), Basisverzeichnis (basedir) und Default-Target (default). Ein Project besteht aus einem oder mehreren Targets, die mit Make-Targets vergleichbar sind und eine Reihe von auszuführenden Aufgaben (Tasks) beinhalten. Man kann ein Target für das Kompilieren, eines für das Erzeugen einer Jar-Datei und so weiter erstellen. Wie bei Make können die Targets voneinander abhängig sein. Ant löst diese Abhängigkeiten auf, wobei es jedes Target nur einmal ausführt, auch wenn mehrere andere es mit depends referenzieren (siehe Listing 2).

Mit if (oder unless) kann der Entwickler die Ausführung eines Target vom Vorhandensein einer Property abhängig machen. Die if-Bedingung gilt als erfüllt, wenn die Property gesetzt ist, unabhängig von ihrem Wert.

<property name="makejar" value="foo"/>
<target name="dist" if="makejar"> ... </target>

Eine Task ist ein Arbeitsschritt, den Ant ausführen soll. Ihren Ablauf steuern die Attribute des Task-Elements. Beispielsweise kennt Ant für das Kompilieren von Java-Quelldateien die Task javac:

<target name="compile">
<javac scrdir="src" destdir="classes"/>
</target>

Es gibt eine Reihe solcher eingebauten Tasks, man kann aber auch eigene erstellen.

Für ein Projekt können Entwickler Properties definieren und als Variablen in den Attributen einsetzen. Ant expandiert sie im Stil von Shell-Variablen:

<property name="builddir" value="classes" />

In einem Attribut eingesetzt wird beispielsweise ${builddir} zu classes. Die Definition der Properties erfolgt entweder direkt innerhalb des Project-Elements oder innerhalb eines Target. In letzterem Fall setzt Ant die Properties allerdings nur dann, wenn es das entsprechende Target ausführt. Die Standard Java System Properties wie java.version oder file.separator stellt Ant automatisch zur Verfügung.

Zu einigen Diskussionen auf der Ant-Mailingliste führte die Tatsache, dass es nicht möglich ist, den Wert eines Property durch eine neue Property-Task zu überschreiben. In Listing 3 wäre der Wert von builddir immer gleich classes, da Ant das Target one zuerst ausführt. Die Ant-Entwickler haben jedoch schon angekündigt diesen Teil der Semantik beim nächsten Versionssprung auf Ant 2.0 zu verändern.

Für den Zweck, in der Quelldatei vorkommende Werte in der Zieldatei global zu ersetzen, bietet Ant die Möglichkeit, Filter zu definieren:

<filter token="version" value="1.0" />

In diesem Fall ersetzt Ant in Tasks, die Dateien kopieren und bei denen das Filtern angeschaltet ist, alle in den Quelldateien vorkommenden Tokens in der Zieldatei durch den entsprechenden Wert. Im obigen Beispiel würde der Wert 1.0 den Token `version' ersetzen.

Übersichtlicher Classpath durch Schachtelung

Die Tasks java und javac haben ein Attribut classpath, mit dem der Klassenpfad gesetzt werden kann. Da ein umfangreicher Klassenpfad in einem Attribut aber schnell unübersichtlich wird, kann man ihn alternativ durch ein geschachteltes classpath-Element festlegen:

<javac scrdir="src" destdir="classes">
  <classpath>
   <pathelement path="lib/foo.jar;lib/bar.jar" />
   <pathelement location="lib/foobar.jar" />
   <pathelement location="classes/" />
  </classpath>
</javac>

Dabei kann <pathelement path= "... " /> eine durch `;' oder `:' getrennte Liste von Dateien angegeben. Beispielsweise erweitert die Zuweisung <pathelement path="${java.class.path}"/> den aktuellen Classpath um den der lokalen Java-Installation. Mit <pathelement location=" ... "/> kann der Entwickler eine einzelne Datei, etwa ein Jar-File, oder ein Verzeichnis angeben.

Soll in mehreren Targets der gleiche Classpath gelten, kann man direkt unterhalb des Project-Elements einen Pfad definieren und ihm eine ID geben:

<project name="ameise" default="distribute"
  basedir=".">
<path id="myclasspath">
  <pathelement location="lib/foobar.jar" />
</path>

Jetzt muss nur noch das Classpath-Element diesen Pfad referenzieren:

<javac scrdir="src" destdir="classes">
  <classpath refid="myclasspath" />
</javac>

Auswahl von Dateien mit Wildcards

In Tasks, die mit Verzeichnissen arbeiten - beispielsweise jar - kann der Programmierer in den Attributen includes und excludes angeben, welche Dateien die Task innerhalb des Verzeichnisbaumes berücksichtigen und welche sie ausschließen soll. Dabei gelten folgende Platzhalter:

Der Vergleich mit dem Suchmuster erfolgt dabei pro Verzeichnis. ?rc/*/ *.java beinhaltet demnach die Datei src/ix/test.java, aber nicht src/de/ix/test.java, da hier die Datei eine Ebene tiefer liegt. Mittels `**' können ganze Verzeichnisbäume berücksichtigt werden. src/**/*.java beinhaltet demnach alle .java-Dateien unterhalb von src, egal in welchem Verzeichnis sie dort liegen. Gibt man nur src/ beziehungsweise src\ an, interpretiert Ant den letzten Schrägstrich automatisch als `/ **'.

Da auch die Angabe von umfangreichen Selektionsmustern in einem Attribut schnell unübersichtlich wird, gibt es die Möglichkeit, File Sets zu definieren:

<jar jarfile="myjar.jar">
 <fileset dir="${basedir}>
  <include name="**/*.class" />
  <exclude name="**/ test/" />
 </fileset>
</jar>

Über ein Pattern Set lassen sich die Selektionsmuster global für das ganze Buildfile definieren:

<project name="ameise" default="distribute"
 basedir=".">
  <patternset id="classfiles">
  <include name="**/*.class" />
  <include name="**/*.properties" />
  <exclude name="**/* test/" />
</patternset>

Anhand der ID lässt sich dieses Pattern Set referenzieren:

<jar jarfile="myjar.jar">
 <fileset dir="${basedir}>
  <patternset refid="classfiles" />
 </fileset>
</jar>

Task-Liste ist individuell erweiterbar

Der Kasten `Eingebaute Tasks' zeigt eine Auswahl der von Ant zur Verfügung gestellten Tasks. Deren Syntax ist immer gleich:

<name attribut1="wert1" attribut2="wert2" ... />

Bezüglich der Verwendung der einzelnen Tasks und ihrer Attribute sei hier wieder auf das Ant User Manual verwiesen.

Zusätzlich kann der Programmierer eigene Tasks erstellen. Dazu muss er eine von org.apache.tools.ant.Task abgeleitete Klasse erstellen, die für jedes mögliche Attribut eine entsprechende set-Methode besitzt. Diese Methode erhält als Parameter den Wert des Attributs als String, bevor Ant die execute-Methode der Klasse aufruft. Die Zeile

<taskdef name="ixtask" classname="de.ix.IxTask" />

definiert die neue Task innerhalb des Project-Elements. Sie ist im gesamten Buildfile gültig.

Fazit

Mit Ant können Entwickler plattformunabhängige Makefiles erstellen. Die Extensible Markup Language (XML) verleiht den Buildfiles eine übersichtliche Struktur, und ihre Syntax ist einfach zu erlernen. Eingebaute Tasks erfüllen so gut wie alle Anforderungen, die Java-Programmierer an ein Maketool stellen. Für speziellere Aufgaben steht dem Entwickler die volle Mächtigkeit von Java zur Verfügung.

Mittlerweile hat die Entwicklung von Ant eine ziemliche Dynamik erreicht, und es ist für viele Open-Source-Projekte im Java-Umfeld zum Buildtool der Wahl geworden (beispielsweise Tomcat, JBoss und Castor). Einige Entwickler haben Ant bereits in ihre Lieblings-IDE integriert - etwa JBuilder, Visual Age for Java oder Netbeans -, und unter dem Namen Antidote wird an einem GUI-Frontend für die Erstellung der Buildfiles gearbeitet. (ka)

MICHAEL MARR

studiert Bauinformatik an der TU-Darmstadt und arbeitet nebenbei als freiberuflicher Java-Entwickler und Dozent.

Literatur

[1] Brett McLaughlin, Mike Loukides; Java and XML; O'Reilly, Sebastopol 2000

[2] Henning Behme, Stefan Mintert; XML in der Praxis; Professionelles Web-Publishing mit der Extensible Marcup Language, Addison-Wesley, Bonn 2000

Kasten 1


EINGEBAUTE TASKS

Operationen für Javajar, java, javac, javadoc, rmic, war
Dateioperationenchmod, copy, delete, mkdir, move
Archivoperationengzip, gunzip, tar, untar, zip, unzip
Download einer Datei von einer beliebigen URL (http/ftp)get
Zugriff auf CVS-Modulecvs
Ausführen von anderen Buildfilesant
Ausführen von Systemkommandosexec
Versenden einer E-Mailmail
Transformation von XML Dokumenten mit XSLTstyle

Kasten 2


ONLINE-RESSOURCEN

Ant Homepagejakarta.apache.org/ant/index.html
Ant User Manualjakarta.apache.org/ant/jakarta-ant/docs/
JBuilder-Integrationwww.dieter-bogdoll.de/java/AntRunner/
VA-Java-Integrationjakarta.apache.org/cvsweb/index.cgi/jakarta-ant/docs/VAJAntTool.html

Kasten 3


LISTING 1

Als Buildfile erwartet Ant eine XML-Datei.

  <?xml version="1.0" ?>
  <project name="ameise" default="distribute" basedir=".">

     <!-- Konstanten fuer die Verzeichnis-Struktur -->
     <property name="source.dir" value="src"/>
     <property name="build.dir" value="classes" />
     <property name="dist.dir" value="." />

     <!-- Auswahl des Compilers -->
     <!-- modern = JDK 1.3 ; classic = JDK 1.1/1.2 ; jikes = Jikes-->
     <property name="build.compiler" value="modern" />

     <!-- sonstige Konstanten -->
     <property name="version" value="1.0"/>
     <property name="manifest" value="${source.dir}/manifest.mf"/>

     <!-- classpath -->
     <path id="classpath">
           <pathelement location="libs/foo.jar"/>
           <pathelement location="libs/bar.jar"/>
     </path>

     <!-- Datei-Auswahlmuster fuer das Jar-File
          Es werden alle classes und properties Dateien und alle Dateien,
          die in einem resources-Verzeichnis liegen
          beruecksichtigt. Verzeichnisse mit Namen test werden
          ausgeschlossen. -->
     <patternset id="forjar">
          <include name="**\*.class" />
          <include name="**/resources/*.*" />
          <include name="**/*.properties" />
          <exclude name="**/test/" />
     </patternset>

     <target name="prepare">
         <!-- Ant internen Zeitstempel setzen -->
         <tstamp/>
         <!-- Das classes-Verzeichnis erstellen -->
         <mkdir dir="${build.dir}" />
     </target>

     <target name="compile" depends="prepare">
         <!-- kompilieren der Quelldateien -->
         <javac srcdir="${source.dir}" destdir="${build.dir}">
             <classpath>
                 <path refid="classpath"/>
             </classpath>
         </javac>

         <!-- kopieren der Dateien, die in den
              resources-Verzeichnissen liegen-->
         <copy todir="${build.dir}" overwrite="y">
            <fileset dir="${source.dir}">
                     <!-- beruecksichtigt alle Verzeichnisse mit
                          Namen "resources" und alle .properties Dateien-->
                     <include name="**/resources/" />
                     <include name="**/*.properties" />
            </fileset>
         </copy>
     </target>

     <!-- Erstellen der Jar-Datei -->
     <target name="distribute" depends="compile">
           <jar jarfile="${dist.dir}/ameise${version}.jar"
            manifest="${manifest}" >
           <fileset dir="${build.dir}">
              <patternset refid="forjar" />
           </fileset>
         </jar>
     </target>

     <target name="clean">
         <delete dir="${build.dir}" />
     </target>
  </project>

Kasten 4


LISTING 2

project ist das Wurzelelement jedes Ant-Buildfiles.

  <project name="foo" default="dist" basedir=".">
     <target name="init">
     ...
     </target>

     <target name="compile" depends="init">
     ...
     </target>

     <target name="dist" depends="compile,init">
     ...
     </target>
  </project>

Kasten 5


LISTING 3

Ein Property-Wert lässt sich nicht überschreiben. builddir behält den Wert classes.

  <target name="one">
     <property name"builddir" value="classes"/>
  </target>

  <target name="two" depends="one">
     <property name"builddir" value="bin"/>
  </target>