Wie alles begann?

Die Ursprünge von Buildr [1] liegen bei der BPEL-Engine Apache ODE [2]. ODE ist ein komplexes Middlewareprojekt, das sehr hohe Anforderungen an das Build-System stellt. So besteht es aus über 35 Modulen, unterstützt neun Datenbanken, wird in drei verschiedenen Editionen paketiert und hängt von über 120 Bibliotheken ab. Die Datenbankanbindung erfolgt wahlweise über OpenJPA oder Hibernate, beide müssen im Buildprozess berücksichtigt werden. Die Datenbankskripte sollen nicht nur für eine Datenbank sondern gleichzeitig für alle neun erzeugt werden. Zusätzlich werden XML-Parser und -Serializer mittels XMLBeans generiert und eigene Annotation-Prozessoren für die Code-Generierung verwendet. Dazu kommen weitere „Kleinigkeiten“, wie die Anforderung, dass alle ausgelieferten Textdateien, auch die generierten, die Apache-Lizenz im Kopf führen müssen.

Basierend auf den guten Erfahrungen, die das ODE-Projektteam mit Maven 1 gemacht hatte, war man optimistisch auch diese Anforderungen mit Maven, diesmal in Version 2 umsetzen zu können. Das war auch tatsächlich möglich, allerdings mit deutlich höherem Aufwand als erwartet. Für eine so einfache Aufgabe wie das Zusammenführen von zwei SQL-Dateien benötigt man beispielsweise 34 Zeilen XML. Die SQL-Skripte für alle Datenbanken zu erzeugen war mit Maven und Plugins gar nicht möglich, sodass man an dieser Stelle auf Ant-Skripte ausweichen musste. Im Ergebnis war die Build-Logik von Apache ODE insgesamt 6739 Zeilen XML-Code gewachsen, verteilt auf 53 Dateien. Die Verteilung auf mehrere von einander abhängige pom.xml-Dateien muss man in Kauf nehmen, wenn man sein Projekt auf mehrere Module verteilen möchte. Man kann sich vorstellen, dass das auf die Kosten der Wartbarkeit geht. Zudem sind die Konfigurationsoptionen verschiedener Plugins nicht immer selbsterklärend und ändern sich mitunter zwischen Plugin-Versionen. In der Praxis führt dies zu schwer aufzuspürenden Problemen und zu nicht reproduzierbaren Builds. Scherzhaft wurde das das Maven Uncertainty Principle, in Anlehnung an Heisenbergs Unschärferelation, genannt.

Notausgänge

Das muss doch besser gehen, doch wo liegt eigentlich genau das Problem? Maven verfolgt einen rein deklarativen Ansatz mithilfe einer XML-basierten DSL. Diese DSL ist jedoch ausschließlich in der Lage, das „Was?“ zu beschreiben, nicht jedoch das „Wie?“. Für die tatsächliche Implementierung der gewünschten Funktionalität sind die Plugins verantwortlich, sie allein bestimmen das „Wie“. Die einzige Möglichkeit, Einfluss darauf zu nehmen ist die Konfiguration der Plugins (im Rahmen der angebotenen Funktionalität) oder einen Notausgang zu wählen. Ein solcher Notausgang könnte z.B. ein selbst geschriebenes Maven-Plugin, der Aufruf eines Ant-Scripts oder, wie in Maven 1, ein jelly-Script sein. Die Lösung kann also nur die bessere Verbindung der deklarativen und imperativen Ansätze sein, die eine nahtlose Integration von Notausgängen erlaubt [3]. Buildr hat sich genau das zum Ziel gesetzt und setzt dabei auf bewährte Mittel. Die guten Eigenschaften von Maven, wie z.B. die Abhängigkeitsverwaltung sollen beispielsweise beibehalten werden. Als Basis für die DSL wird aber auf XML verzichtet und stattdessen Ruby eingesetzt. Dadurch hat man jederzeit die Möglichkeit die Mächtigkeit von der Scriptsprache als Notausgang zu verwenden, wenn die deklarativen Elemente nicht mehr ausreichen. Der Buildr-basierte Build von Apache ODE besteht nun nur noch aus 912 Zeilen Ruby-Code, der Übersichtlichkeit halber auf drei Dateien verteilt. Ein wichtiger Nebeneffekt: Der Buildprozess ist sogar doppelt so schnell.

Abbildung 1: Einordnung von Buildr im Buildprozess

Auf der Basis von Ruby setzt Buildr auf Rake auf. Rake ist ein populäres Build-Werkzeug in der Ruby-Welt und operiert auf einem gerichteten azyklischen Graphen, um Abhängigkeiten zwischen Tasks zu definieren. Dafür stellt es eine einfache DSL zur Verfügung. Die Tasks selbst werden in Ruby implementiert und können dementsprechend komplexe Aufgaben bearbeiten. Zusätzlich können sogenannte FileTasks kausale Abhängigkeiten zwischen Dateien definieren. Wird eine solche Abhängigkeit zwischen .java- und .class-Datei definiert, so weiß Rake, dass es die .class-Datei nur dann neu erzeugen muss, wenn die .java-Datei ein neueres Änderungsdatum als die .class-Datei hat. Dennoch wurde Rake für die Buildprozesse von Ruby-Projekten entwickelt. Um effizient Java-Projekte bauen zu können, liefert Buildr die nötigen Erweiterungen der DSL, um die wichtigsten Elemente eines Builds, nämlich Kompilieren, Testen, Paketieren und Releasen, deklarativ in der Scriptsprache verwenden zu können.

Die Kernkonzepte

Abbildung 2: Abhängigkeiten der wichtigsten globalen Tasks.
package(:zip).path("#{id}-docs-#{version}").tap do |path| path.include _('target/docs') path.include _('README') end

Ein Blick in die Praxis

Obwohl Buildr sowohl mit Ruby als auch mit JRuby genutzt werden kann, ist die Verwendung von Letzterem zu empfehlen. Die Installation geht recht einfach vonstatten. Falls noch nicht vorhanden muss eine aktuelle JRuby-Version installiert werden und in den PATH aufgenommen werden. Das gelingt am Besten mit rvm (unter Linux/Mac) oder pik (unter Windows). Die Installation von Buildr erfolgt dann mithilfe von Gem:

gem install buildr

Mit buildr –version erfahren Sie welche Version installiert wurde. Zur Drucklegung aktuell ist Version 1.4.9.

Wechseln Sie nun in ein Projektverzeichnis Ihrer Wahl. Für den Anfang sollte es kein zu kompliziertes Projekt sein. Rufen Sie buildr auf und lassen sie eine buildfile erstellen. Buildr versucht nun anhand einer existierenden pom.xml oder der Verzeichnisstruktur zu erkennen, um was für ein Projekt es sich handelt. Das funktioniert meist nicht besonders gut, stellt aber einen guten Startpunkt dar. Ein funktionierendes Beispielprojekt können Sie unter [5] auschecken, das soll uns nun auch als Grundlage dienen. Es handelt sich dabei um ein sehr vereinfachtes Multimodul-Projekt, bestehend aus einem Modul für eine API (api) und einem Modul für die Implementierung (impl). Nachdem Buildr eine neue buildfile erzeugt hat, versucht er das Projekt direkt zu bauen. Das schlägt allerdings fehl, weil die Implementierung die Logback-Bibliothek verwendet und diese Abhängigkeit Buildr noch nicht bekannt ist.

Listing 1 zeigt, wie die vollständige buildfile für dieses Projekt aussieht. Zu Beginn wird die aktuelle Versionsnummer für das Projekt festgelegt. Der Variablenname ist eine Konvention und wird von Buildrs Release-Task verwendet, um die nächsthöhere Version zu bestimmen und zu setzen. Danach wird das Maven-Central-Repository in die Liste der bekannten Repositories aufgenommen. Die Variable LOGBACK wird als Array definiert und referenziert die Core- und Classic-Artefakte der Bibliothek. Alternativ hätte man auch schreiben können:

LOGBACK = group('logback-classic', 'logback-core, :under => 'ch.qos.logback', :version => '1.0.7')

Danach wird das Wurzelprojekt definiert. Das DSL-Schlüsselwort desc gibt dem Projekttask eine natürlichsprachliche Beschreibung während define die Artefakt-Id (multi-java) als Parameter übergeben bekommt. In dem folgenden Block wird die Group-Id und die Versionsnummer gesetzt, damit sind alle GAV-Koordinaten bestimmt. Die Unterprojekte api und impl „erben“ diese Koordinaten, die Artefakt-Ids werden dabei zusammengesetzt. Dieses Verhalten kann aber umkonfiguriert werden. Als nächstes wird das API-Projekt definiert. Dem Compiler wird übergeben, dass er Java-5-Bytecode erzeugen soll, danach wird ein Jar, sowie Javadocs und eine Sourcedistribution erzeugt. Die Implementierung benötigt zur Compile-Zeit und zur Laufzeit die Logback-Bibliothek, deshalb wird sie in der compile with-Direktive zusammen mit der Abhängigkeit zu dem API-Projekt angegeben. Die Methode transitive() berechnet aus den POMs der beiden Logback Artefakte die transitiven Abhängigkeiten und übergibt sie dem Compiler. Danach werden die Tests ausgeführt und dann ein Jar sowie die Javadocs erzeugt.

# Version number for this release VERSION_NUMBER = "1.0"

Specify Maven 2.0 remote repositories here, like this:

repositories.remote << " http://repo.maven.apache.org/maven2/"

LOGBACK = [‘ch.qos.logback:logback-classic:jar:1.0.7’, ‘ch.qos.logback:logback-core:jar:1.0.7’]

desc “The Multi-java project” define “multi-java” do Add project.version = VERSION_NUMBER project.group = “org.example”

define "api" do
  compile.using(:source =&gt; '1.5', :target =&gt; '1.5')
  package(:jar)
  package(:javadoc)
  package(:sources)
end

define "impl" do
  compile.with(project("api"), transitive(LOGBACK))
      .using(:source =&gt; '1.5', :target =&gt; '1.5')
test
package(:jar)
package(:javadoc)
end

end

Damit ist der Buildprozess definiert. Mit buildr -T erhalten sie eine Übersicht über die Tasks, die auf dem Projekt ausgeführt werden können. buildr artifacts beispielsweise lädt die referenzierten Artefakte aus dem Maven-Repository herunter. buildr eclipse erzeugt .classpath- und .project-Dateien für den Import in Eclipse. Um den eigentlichen Build auszuführen, rufen sie buildr auf. Daraufhin werden die beiden Unterprojekte kompiliert und die Tests ausgeführt. Möchten Sie nur das Implementierungsprojekt testen, können sie entweder in das impl-Verzeichnis wechseln und dort buildr test, oder in dem Wurzelverzeichnis buildr multi-java:impl:test aufrufen.

Um die Projekte zu Paketieren rufen Sie buildr package auf. Danach finden Sie in den target-Verzeichnissen die JAR-Dateien. buildr install kopiert die Artefakte in Ihr lokales Maven-Repository.

Fazit

Das Ziel dieses Artikels war es, Apache Buildr mit seinen grundlegenden Konzepten vorzustellen und dem Leser den Einstieg zu erleichtern. Es wurden die Probleme von rein deklarativen Buildsystemen diskutiert und mit Buildr eine Lösung präsentiert, die die deklarativen und die imperativen Ansätze geschickt kombiniert. So können die Builds mit Buildr schneller, einfacher, schlanker und wartbarer als mit Maven umgesetzt werden. Natürlich konnten die Möglichkeiten von Buildr im Rahmen dieses Artikels nur angerissen werden. Wer sich intensiver mit dem Thema beschäftigen möchte sei auf die gute Dokumentation auf der Projekt-Webseite verwiesen. Dort finden sich auch Verweise zu Plug-ins, How-Tos sowie Buildfiles von Projekten die ebenfalls Buildr verwenden.

Referenzen

  1. http://buildr.apache.org/  ↩

  2. http://ode.apache.org/  ↩

  3. Fowler, Martin: JRake. 2006. http://martinfowler.com/bliki/JRake.html  ↩

  4. Michel Guymon: lock_jar. https://github.com/mguymon/lock_jar/  ↩

  5. https://github.com/vanto/buildr-jm-demo  ↩