Im ersten Artikel der geplanten Ant Serie habe ich beschrieben, wie meiner Meinung nach ein wiederverwendbares Buildsystem aussehen sollte. Dort wurde ein projektspezifisches build.xml
postuliert, in dem grundsätzliche Einstellungen des Projekts vorgenommen wurden. Die eigentliche Logik des Builds findet sich dann einer wiederverwendbaren Datei names build-root.xml
, die in den Projektbuild importiert wird. Der weitere Aufbau eben dieser Datei soll in diesem Blogeintrag betrachtet werden.
Zunächst einmal ist an dieser Stelle ein einheitlicher Build-Lifecycle zu definieren, also eine Abfolge der einzelnen Schritte, die zum Ende zu den gewünschten Ergebnissen und Aktivitäten führen. Dieser Lifecycle sollte über alle Projekte und Plattformen hinweg einheitlich gehalten sein, damit sich jeder Entwickler sofort im jeweiligen Projekt zurecht finden kann und ohne Problem die korrekten Ansatzpunkte für seine eigenen Erweiterungen finden kann.
Da ich mit dem hier beschriebenen Buildsystem (derzeit) keine konkreten Projekte verwalten möchte, ist ein einfacher und überschaubarer Lifecycle erst einmal vollkommen ausreichend. Er besteht aus einigen wenigen target
Definitionen, die aufeinander aufbauen.
Was hier im Moment so einfach und selbstverständlich aussieht, ist jedoch bei der Definition einer größeren Buildumgebung bereits eine der schwierigsten Aufgaben. Für alle diese target
Element muss bereits zu diesem frühen Zeitpunkt festgelegt werden, welche Ergebnisse an diesen Punkten erwartet werden.
Unterstützt man dann auch noch unterschiedliche Plattformen, ist auch die Semantik der jeweiligen Ergebnisse einheitlich zu beschreiben. So muss schon für ein so einfaches target
wie zum Beispiel clean
geklärt werden, ob nur die Quellen in ein Binärformat übersetzt werden oder ob — wie zum Beispiel in C
Projekten an dieser Stelle die .o
Dateien bereits zu ausführbaren Programmen gelinkt werden sollen.
Aber auch die Reihenfolge der Targets muss präzise festgelegt werden, um später unangenehme Überraschungen zu vermeiden. Daher sollte das build_root.xml
neben den einzelnen target
Definitionen auch ausführliche Kommentare enthalten1.
- clean: Es werden alle generierten Artefakte eines vorangehenden Buildlaufs gelöscht und das Projekt so in einen jungfräulichen Status zurück versetzt.
- init: An dieser Stelle werden alle vorbereitenden Tätigkeiten durchgeführt, die vor dem eigentlichen Buildlauf stattfinden sollten.
- compile: Der im Projekt enthaltenen Quellcode wird übersetzt und es werden die enstprechenden binären Artefake erzeugt.
- package: Bei der Paketierung erfolgt die Zustammenstellung der binären Artefakte zu Auslieferungseinheit. Im Fall von Projekten auf der Java Plattform finder hier die Erzeugung der
.jar
beziehungsweise.war
Dateien statt. - build: Das
build
Target ist ein Meta-Target und steurt den normalen Buildlauf. Dieser besteht aus den Schrittenclean
undpackage
.
Das build_root.xml
mit diesen target
Elementen sieht dann wie folgt aus:
<?xml version="1.0"?>
<project name="build_root">
<target name="clean" />
<target name="init" />
<target name="compile"
depends="init" />
<target name="package"
depends="compile" />
<target name="build"
depends="clean, package" />
</project>
Da das projektspezifische Ant File als default Target build
aktiviert, können wir diese Kombination bereits testen.
Thranduil:demo ralf$ ant
Buildfile: /Volumes/Macintosh HD/Users/ralf/demo/build.xml
clean:
init:
compile:
package:
build:
BUILD SUCCESSFUL
Total time: 0 seconds
Thranduil:demo ralf$
Ant arbeitet nun alle definierten Targets in der gewünschten Reihenfolge ab. Da in den Targets derzeit nur noch keine Verarbeitungsschritte definiert sind, passiert noch nicht viel. Damit der Build nun vollständig funktioniert, müssen jetzt nur noch diese Target um diese Regeln ergänzt werden.
Dann sind wir fertig.
Wirklich?
Nein, denn auf Grund der unterschiedlichen Anforderungen der Projekte geraten wir ganz schnell in Situationen, die eine allgemein gültige Definition der Verarbeitungsregeln erlauben. So etwas passiert spätestens dann, wenn eine alternative Plattform unterstützt werden soll.
Und selbst wenn diese naive Implementierung funktioniert, sie ist schnell so überflüssig, dass alle Projekte dann zügig wieder zu einer Copy-Paste Wiederverwertung zurückkehren.
Es muss also eine alternative Lösung gefunden werden.
Extension-Points
Diese Alternative gibt es schon lange in Ant. Es handelt sich um die extension-point
Elemente. Diese gleichen den bekannten target
Elemente, enthalten jedoch keine Verarbeitungsregeln. Diese Elemente können jedoch durch alternative target
Definitionen erweitert werden. Dazu wird das Target bei seiner Definition einfach um ein extensionOf
Attribut erweitert:
<?xml version="1.0"?>
<project name="project" basedir="." default="build">
<target name="init" />
<extension-point name="-build"
depends="init" />
<target name="build"
depends="-build" />
<target name="my:build"
extensionOf="-build">
<echo message="building..." />
</target>
</project>
In diesem Beispiel basiert ein build
Target auf einen Erweiterungspunkt namens -build
. Dieser definiert zwar keine weitere Verabeitung, erfordert aber die Ausführung des init
Targets. Zu einem späteren Zeitpunkt dann erfolgt die Definition einer Erweiterung my:build
. Da my:build
eine Erweiterung von -build
darstellt, wird sie bei der Verarbeitung durch Ant an genau dieser Stelle im Ablauf ausgeführt:
Thranduil:demo ralf$ ant -f extensionpoint.xml
Buildfile: /Volumes/Macintosh HD/Users/ralf/demo/extensionpoint.xml
init:
my:build:
[echo] building...
-build:
build:
BUILD SUCCESSFUL
Total time: 0 seconds
Thranduil:demo ralf$
Ein Erweiterungspunkt kann um beliebig viele Erweiterungen ergänzt werden. Allerdings ist nicht definiert, in welcher Reihenfolge diese ausgeführt werden. Dennoch sind die Erweiterungspunkte das ideale Mittel, unser Buildsystem weiter zu strukturieren.
Ein Problem allerdings tritt damit in Erscheinung: alle diese Elemente teilen sich einen Namensraum und sind daher erste einmal nicht voneinander zu unterscheiden. Mein Buildsystem folgt daher einer Konvention bei der Namensgebung:
-
Die klassischen
target
Elemente sind ausschließlich Bestandteil des vordefinierten Lifecycles und dürfen beim Start des Ant verwendet werden. An dieser Stelle verwende ich einfache Namen wiebuild
undclean
. -
Zu den
target
Elementen gibt es ein oder mehrereextension-point
Definitionen. Diese nutzen ein vorangestelltes-
Zeichen und sind auf diese Weise als Erweiterungspunkt gekennzeichnet. -
Erweiterungen selbst bestehen immer aus einem Präfix mit dem beispielsweise die Plattform gekennzeichnet ist und dem Namen des Erweiterunsgpunkts. Beide Elemente werden durch ein
:
Zeichen voneinander getrennt. -
Es gibt einige Interne Erweiterungen des Buildsystems, für die ich stets den Präfix
intern
reserviere. Diese unterlaufen ein wenig die Konvention und könnentarget
oder Erweiterung sein.
Diese Möglichkeit, Erweiterungspunkte zu definieren, führt allerdings dazu, dass der Lifecycle insgesamt etwas komplizierter wird. Damit spätere Erweiterungen die richtigen Stellen in diesem Graphen finden, werde hier bereits zusätzliche Punkte ergänzt.
Neben den zusätzlichen Erweiterungspunkten finden sich einige intern:
Targets in diesem Diagramm wieder. In meinen Buildsystemen verwende ich ein Target wie build
exklusiv für den Aufruf. Die Funktionalität dieses Targets wird dann innerhalb des intern:build
Targets implementiert und auf dieser Ebene findet auch eine Wiederverwertung statt2. Meine Implementierung dieses Graphen als Ant Build sieht dann wie folgt aus:
<?xml version="1.0"?>
<project name="build_root">
<extension-point name="-clean" />
<target name="intern:clean"
depends="-clean" />
<target name="clean"
depends="intern:clean" />
<extension-point name="-init" />
<target name="intern:init"
depends="-init" />
<target name="init"
depends="intern:init" />
<extension-point name="-compile.pre"
depends="intern:init" />
<extension-point name="-compile" />
<extension-point name="-compile.post" />
<target name="intern:compile"
depends="-compile.pre, -compile, -compile.post" />
<target name="compile"
depends="intern:compile" />
<extension-point name="-package.pre"
depends="intern:compile" />
<extension-point name="-package" />
<extension-point name="-package.post" />
<target name="intern:package"
depends="-package.pre, -package, -package.post" />
<target name="package"
depends="intern:package" />
<target name="intern:build"
depends="intern:clean, intern:package" />
<target name="build"
depends="intern:build" />
</project>
Mit diesen Definitionen ist der Lifecycle in der Datei build_root.xml
fürs erste nun einmal vollständig definiert. Mit einigen wenigen Erweiterungen könnte dem Buildsystem nun Leben eingehaucht werden.
Ein Aufruf des Ant zeigt auf jeden Fall, dass bereits jetzt die einzelnen Build-Targets in der gewünschten Reihenfolge aktiviert werden.
Thranduil:demo ralf$ ant
Buildfile: /Volumes/Macintosh HD/Users/ralf/demo/build.xml
-clean:
intern:clean:
-init:
intern:init:
-compile.pre:
-compile:
-compile.post:
intern:compile:
-package.pre:
-package:
-package.post:
intern:package:
intern:build:
build:
BUILD SUCCESSFUL
Total time: 0 seconds
Thranduil:demo ralf$
In dieser Artikelserie über den Ant sind weitere Artikel erschienen:
-
Eine ersten Artikel mit ersten Gedanken zu einem neuen projekt-übergreifendem Buildsystem
-
Definition eines einheitlichen und projekt-unabhähgigen Ant Lifecycle, Beschreibung von Erweiterungspunkten und Erweiterungen.
-
In den Beispielen fehlen diese Kommentare. Als Entschuldigung möchte ich vorbringen, dass es sich hier nur um ein illustrierendes Beispiel handelt, das ich in dieser Form nicht produktiv zu nutzen wage. ↩
-
Jedes dieser primären Targets wird daher nur originär aufgerufen. Von den Top-Level Targets wie
build
wird dementsprechend innerhalb des Builds kein weiteres Target aktiv. ↩