Integration von Ant und Groovy #
Groovy arbeitet ausgezeichnet mit ApacheAnt zusammen. Ein guter Ausgangslink zum Thema ist die Groovy-Dokumentation dazu unter http://groovy.codehaus.org/Ant+Integration+with+Groovy .
Ant in Groovy #
In den meisten Anleitungen zum Thema wird vor allem der AntBuilder von Groovy gelobt. Dieser erlaubt es, in Groovy die Ant-Bibliothek zu nutzen und in einer einfachen, objektorientierten Syntax Ant-Tasks auszuführen. Die Krönung dieses Ansatzes ist Gant, das einen kompletten Ersatz für Ant bietet.
Groovy in Ant #
Allerdings war das nicht mein Ansatz, mich dem Thema zu nähern. Ich habe schon länger Erfahrung mit Ant und vertraue diesem Arbeitstier eigentlich, obwohl ich es gerne etwas dynamischer hätte. Mit geht es also darum, Groovy in Ant (statt umgekehrt) zu benutzen. Die offizielle Dokumentation zu diesem Thema ist http://groovy.codehaus.org/The+groovy+Ant+Task, allerdings bietet dieses Thema so viel Möglichkeiten, daß ich diese Seite angefangen habe, um einige hochinteressante Eigenschaften zu vermitteln, die ich entdeckt habe.
Ein Skript, das sich nach außen verhält wie ein klassisches Ant-Skript hat insbesondere auch den Vorteil, daß es sich viel besser intergriert. So kann man sein Skript z.B. direkt in die NetBeans IDE einsetzen und auch die EclipseIDE bietet eine Unterstützung für Ant-Targets an.
Grundlage: <groovy>-Task definieren #
Wir benutzen einen neuen Task (d.h. einen neuen Ant-Befehl) namens groovy. Diesen muss man erstmal definieren. Das geht so:
<path id="groovy_classpath"> <fileset dir="${lib_home}/"> <include name="groovy-all-*.jar"/> </fileset> </path> <taskdef name="groovy" classname="org.codehaus.groovy.ant.Groovy" classpathref="groovy_classpath"/>
Dieser Code hat zwei Teile. Im ersten definiere ich einen Classpath, der Groovy's JAR-Datei enthält, im zweiten definiere ich meinen neuen Task. Man könnte den Classpath auch direkt dort angeben, aber ich habe mir angewöhnt, Classenpfade immer vorher zu deklarieren, weil man die sowieso später immer wieder erweitert. Jetzt kann man mit
<groovy><![CDATA[ println 'hello, world!' ]]</groovy>
innerhalb eines Targets sein erstes Groovy-Programm mit Ant starten.
Zugriff auf Properties #
Spass macht so eine Integration natürlich nur, wenn der Ant-Code und der Groovy-Code auch miteinander kommunizieren können. Die einfachste Stufe ist der Austausch von Properties. In der Groovy-Dokumentation stehen dazu sogenannte Bindings, das sind in Groovy sowas wie lokale Variablen eines Skriptes. Eine solche Binding ist eine Map namens properties. Diese kann man nicht nur lesend sondern auch schreibend benutzen!
<groovy><![CDATA[ println "Ich befinde mich im Verzeichnis $properties.basedir" properties.yeah='Ich bin so groovy' ]]</groovy> <echo message='$yeah'>
Zugriff auf komplexere Ant-Strukturen #
Wer ZUgriff z.B. auf einen Classpath, eine Liste oder eine sonstige aussergewöhnliche Ant-Datenstruktor braucht, kann diese über das "id"-Tag kennzeichnen und kann sie dann in Groovy über die Map project.references ansprechen (Beispiel aus der Doku):
<zipfileset id="found" src="foobar.jar" includes="**/*.xml"/> <groovy> project.references.found.each { println it.name } </groovy>
Ant in Groovy in Ant #
Wer jetzt wieder Ant-Befehle innerhalb seiner Groovy-Tasks benötigt (z.B. kann keiner so schön Dateien sammeln wie ein Fileset), kann die Binding ant benutzen. Unter http://groovy.codehaus.org/The+groovy+Ant+Task gibt es ein Beispiel hierzu - und ich habe es selber noch nicht gemacht - weshalb ich mir das jetzt für hier spare.
eigene Ant-Tasks #
Das ist wohl die komfortabelste und eleganteste Methode, Groovy in Ant zu benutzen. Ich hatte bisher immer einen Heiden-Respekt davor, Ant-Tasks selber zu schreiben. Irgendwie muss ja der projektspezifische Task als Classfile vorliegen, bevor ich ihn benutzen kann. Dazu muss ich ihn aber erst übersetzen und dafür brauche ich ein Ant-Skript. Ein klassisches Henne-Ei-Problem, das mich immer dazu bewogen hat, mich um das Thema zu drücken, um nicht für jeden "mal-eben"-Task ein eigenes Projekt aufmachen zu müssen. Dadurch, das man den Quellcode direkt in das build-Skript schreiben kann, entfällt diese Hürde mit Groovy ganz. :-)
Wichtig ist erstmal die Ant-Dokumentation zum Thema Writing your own Tasks. Die Grundanregung hatte ich aus dem Buch Groovy im Einsatz, das ich übrigens wärmstens empfehlen kann. Aus diesem Buch stammt der Trick, daß man den Task nicht mit Taskdef definiert (wie das üblich ist), sondern direkt im Groovy-Code die Binding project benutzt.
<target name="groovy-test" description="Groovy-Test"> <groovy> class TestTask extends org.apache.tools.ant.Task{ def wert='Test' public void setWert(String str){ wert=str } public void execute(){ println(wert) } } project.addTaskDefinition('groovytest', TestTask) </groovy> <groovytest/> <groovytest wert="Hello"/> </target>
Wie man sieht, steckt im Aufruf von ~addTaskDefinition() die eigentliche Magie. Eine Besonderheit war noch, daß man (im Gegensatz zum Beispiel im Buch) wirklich den Setter mit dem Argumenttyp "String" benötigt. Groovy erzeugt zwar automatisch setter zu öffentlichen Feldern, aber scheinbar hat dieser dann eine andere Parameter-Signatur. Wer mehr über die Übergabe von Parametern an unseren Task wissen will, sollte http://ant.apache.org/manual/tutorial-tasks-filesets-properties.html lesen.
Einen solchen einmal erzeugten Task kann man übrigens auch problemlos später im Skript wiederverwenden.
komplexes Beispiel #
Hier ein komplexes Beispiel aus der Praxis, das ich in der momentanen FlyingAnt-Version benutze, um Sourcecodebäume durch eine Template-Engine und eine Wiki-Engine zu filtern. Die eigentlichen Engines sind in einer externen Bibliothek, so daß dieses Beispiel nicht einfach ausprobiert werden kann. Dennoch sollte der Quelltext beim Verständnis des Umgangs mit <dirset>-Referenzen und eingebetteten <fileset>-Elementen weiterhelfen.
<target name="definetasks" depends="install-groovy"> <groovy><![CDATA[ import org.apache.tools.ant.Task import org.apache.tools.ant.types.FileSet import org.apache.tools.ant.types.DirSet /** * Ant-Task, der Dateien kopiert und diese dabei mit einer Template-Engine * bearbeitet. Bearbeitet werden die Dateien, die in einem eingefügten * <fileset> enthalten sind. Folgende Attribute können angegeben werden: * * todir: Verzeichnis, in das die Dateien kopiert werden (sonst das aktuelle) * * log (boolean): Anzeige jeder einzelnen Datei * * suffix: Dateierweiterung, die beim kopieren abgeschnitten wird. (z.B. ".tmpl") * * wiki (boolean): Vor der Template-Engine wird eine einfache Wiki-Engine aufgerufen * * includepathref: eine idref eines <dirset>, das die Include-Verzeichnisse enthält * * headerfile: Wird am Start des Dokuments eingebunden * * footerfile: Wird am Ende des Dokuments eingebunden */ class TemplateEngineTask extends Task{ def todir = '.' def log = true def wiki = false def suffix = '' def includepathref = '' def headerfile = '' def footerfile = '' ArrayList<FileSet> filesets=[] public void setTodir(String str){ todir=str } public void setLog(Boolean l){ log=l } public void setSuffix(String str){ suffix=str } public void setWiki(Boolean w){ wiki=w } public void setIncludepathref(String str){ includepathref=str } public void setHeaderfile(String str){ headerfile=str } public void setFooterfile(String str){ footerfile=str } public void addConfiguredFileSet(FileSet set){ filesets.add(set) } public void execute(){ // erstmal den Includepath erstellen println includepathref println project.references String[] includeDirs=['.'] DirSet incPath=project.references."$includepathref" if(incPath) includeDirs = incPath.getDirectoryScanner().getIncludedDirectories() // Jetzt kümmere ich mich um den eigentlichen Job for(fileset in filesets){ def from=fileset.getDir().toString() String[] files = fileset.getDirectoryScanner().getIncludedFiles() for(file in files){ if(log) println ("kopiere $file") def inhalt = new File(from+"/"+file).text if(headerfile) inhalt = "<@ $headerfile @>\n" + inhalt if(footerfile) inhalt = inhalt + "\n<@ $footerfile @>" if(wiki) inhalt = new de.bayen.util.WikiEngine().translate(inhalt) def ziel=new File(todir+"/"+(file-suffix)) File[] includeDirsAsFiles = includeDirs.collect {dirname->new File(incPath.dir.toString()+"/"+dirname)} def engine = new de.bayen.groovy.IncludeTemplateEngine( includeDirsAsFiles ) def template=engine.createTemplate(inhalt) def binding=[ sourcepath: from, // der wirkliche Sourcepath (site oder sitetemplate) properties: project.getProperties() ]+project.getProperties() ziel.write(template.make(binding).toString()) } } } } project.addTaskDefinition('templateengine', TemplateEngineTask) ]]></groovy> </target>
Natürlich ist es ab einem bestimmten Punkt sinnvoll, die TemplateEngineTask-Klasse auszulagern und in ein eingebundenes JAR-File zu stecken, weil es im Ant-Skript sonst sehr lang und unübersichtlich wird. Aber gerade diese Beispiel habe ich von einem ganz keinen Groovy-Teil immer weiter ausgebaut bis es sozusagen organisch herangewachsen war. Außerdem dient es ja nun auch als Beispiel.
Benutzen kann ich das jetzt wie einen ganz normalen Ant-Task, als ob die <templateengine> immer schon Teil von Ant gewesen wäre:
<dirset id="template_includepath" dir="${doc_home}"> <include name="site"/> <include name="sitetemplate"/> </dirset> <templateengine todir="${dist_home}" includepathref="template_includepath" suffix=".template"> <fileset dir="${site_home}"> <include name="**/*.template"/> </fileset> <fileset dir="${sitetmpl_home}"> <include name="**/*.template"/> </fileset> </templateengine> <templateengine todir="${dist_home}" includepathref="template_includepath" suffix=".wiki" wiki="true" headerfile="page_start.inc" footerfile="page_end.inc"> <fileset dir="${site_home}"> <include name="**/*.wiki"/> </fileset> <fileset dir="${sitetmpl_home}"> <include name="**/*.wiki"/> </fileset> </templateengine>
zusätzliche Bibliotheken #
Wer externe Bibliotheken benutzen will, kann diese ganz einfach in den Classpath hineinschreiben, der ganz am Anfang den groovy-Tag definiert. Diese können dann in allen groovy-Tasks innerhalb des Buildskriptes benutzt werden.
Performance #
Jedesmal, wenn das Ant-Skript an ein Groovy-Element kommt, stockt es bei mir ca. eine Sekunde. Das ist die Zeit, in der der Groovy-Interpreter das Skript in ein Classfile-Format übersetzt. Man kann das für lästig halten, aber meiner Meinung nach ist diese eine Sekunde die Möglichkeiten wert, die Groovy einem eröffnet. Wer wie oben besprochen einen eigenen Task in Groovy definiert, kann diesen natürlich immer wieder aufrufen, ohne daß er neu compiliert werden muss. Ebenso ist es natürlich nach wie vor möglich, in der klassischen Weise einen solchen Task auch extern als JAR-File (also vorkompiliert) abzulegen und dann direkt einzubinden, ohne im Ant-Skript überhaupt noch ein Wort von Groovy stehen zu haben.
Speichern der Groovy-Bibliothek #
Die Bibliothek groovy-all hat eine Größe von ca. 4MB. Man sollte darauf achten, daß man diese nicht in der VersionsVerwaltung landet. Hierzu habe ich mir ein Ant-Skript geschrieben, welches mir diese Bibliothek frisch (per Ivy) herunterlädt, wenn ich sie benötige (Ivy selbst lade ich natürlich auch selber herunter). Auf diese Art und Weise bläst man seine Projektdateien nicht durch Groovy auf. Wer Interesse an dem Skript hat, kann ThomasBayen fragen oder auf FlyingAnt nachschauen.
Fazit #
Ich glaube, das Groovy und Ant ein Team sind, das man in meinen Projekten nicht mehr so schnell trennen können wird. -- ThomasBayen