Gradle: Erstellen von einem Java Application Bundle unter Mac OS X

Im letzten Artikel „Von Continuous Integration zu Continuous Delivery“ habe ich mich mit den Themen Continuous Integration und Continuous Delivery beschäftigt. In diesem Artikel wird diese Themen weitergeführt: Ich zeige, wie eine Java Applikation unter Mac OS X in ein sogenanntes Application Bundle umgewandelt wird. Als Application Bundle verhält sich eine Java Applikation wie eine normale native Mac OS X Applikation. Das Erstellen eines Application Bundle stellt somit einen essentiellen Schritt in einer Deployment Pipeline dar, der vor der Auslieferung der Software durchgeführt werden sollte.

Native Mac OS X Applikationen sind mehr als nur eine einfache ausführbare Datei, wobei ein Benutzer immer nur ein einzelnes Icon im Finder sieht. Unter Mac OS X besteht eine Applikation aus einer Verzeichnisstruktur, die sowohl die ausführbaren Dateien als auch andere Ressourcen enthält, die von einer Applikation benötigt werden. Diese Verzeichnisstruktur wird Application Bundle genannt und die Struktur eines Application Bundles ist von Apple genau definiert. Ein solches Application Bundle vereinfacht die Auslieferung für den Entwickler und verbirgt die Anwendungsinterna vor den Benutzer. Ein Application Bundle kann ganz einfach vom Benutzer über Drag ’n‘ Drop installieren werden, was die Übertragbarkeit einer Applikation stark verbessert.

Verzeichnisstruktur eines Applikation Bundles

Die tatsächliche Verzeichnisstruktur eines Application Bundles wird im Finder verborgen, sobald ein Verzeichnis mit der Endung .app gesuffixt wird. Außer dem Suffix gibt es ein zusätzliches Attribut, dass sogenannte Bundle Bit, dass an einem Verzeichnis gesetzt wird. Die Kombination aus Suffix und Bundle Bit macht ein Verzeichnis zu einem Application Bundle. Die genaue Struktur eines Application Bundles wird in Abbildung 1 dargestellt.

Verzeichnis-Struktur eines Applikation Bundles
Abbildung 1: Verzeichnisstruktur eines Applikation Bundles

Ein Java Application Bundle enthält die folgenden Verzeichnisse und Dateien:

  • Eine Info.plist Datei im Contents Verzeichnis, die wichtige Meta-Informationen enthält, die von Mac OS X benutzt werden. („Java Dictionary Info.plist Keys“)
  • Außerdem sollte eine Datei mit dem Namen PkgInfo in dem Contents Verzeichnis enthalten sein. Bei dieser Datei handelt es sich um eine einfache Textdatei, die den String APPL enthält, auf den vier Buchstaben oder vier Fragezeichen folgen. („Data Type Registration“)
  • Die Icons, die im Dock und im Finder angezeigt werden, befinden sich im Resources Verzeichnis.
  • Der Java-Code wird auch im Resources Verzeichnis abgespeichert und befindet sich dort im Verzeichnis Java.
  • Im MacOS Verzeichnis befindet sich der sogenannter JavaApplicationStub, dabei handelt es sich um eine native Anwendung, die die JVM hochfährt.

Gradle Build

Die meisten Java Projekte verfügen über ein Buildsystem, dass das Bauen der Software automatisiert und damit den Aufbau einer automatisierten Deployment Pipeline vereinfacht. Ein sehr populäres Buildsystem ist Gradle, dass viele Open Source Projekte mittlerweile benutzen (Spring und Hibernate). Gradle ist ein Buildsystem, dass eine Groovy basierende Domain Specific Language (DSL) zur Beschreibung der Projekte verwendet. Damit sind Gradle-Skripte auch direkt ausführbarer Code.

Gradle wurde für Builds entworfen, die aus einer Vielzahl von Projekten bestehen, wobei Gradle die grundlegende Philosophie verfolgt, dass das Buildsystem dem Anwender neben sinnvollen Voreinstellungen, die auf verbreiteten Konventionen beruhen, möglichst viele Freiheiten lassen soll. Außerdem soll der Benutzer die Möglichkeit haben, solche Voreinstellungen zu überschreiben, um seine Projektbesonderheiten abbilden zu können. Gradles Konzept will damit die Flexibilität von Ant mit der „build-by-convention“-Strategie von Maven zusammenbringen.

Wie flexibel Gradle ist kann im den folgenden Listing abgelesen werden. Das Listing zeigt den Buildscript eines Gradle Plugins, dass weiter unten noch genauer beschrieben wird. Gradles Build-Konzept übernimmt die von Maven eingeführten Standardkonventionen („convention over configuration“) für die Verzeichnisstruktur von Projekten (Abbildung 2). Wie man im Buildscript sieht muss man die Verzeichnisse, in denen sich der Quellcode befindet, nicht explizit angeben, um das Plugin zu bauen, sondern es wird einfach das src/main/groovy Verzeichnis verwendet, wie es die Mavenkonventionen vorgegeben.

apply plugin: 'groovy'
apply plugin: 'idea'

repositories {
    mavenCentral()
}

dependencies {
    compile gradleApi()
    compile localGroovy()
}

task compileJavaAppLuncher(type: Exec) {
    def javaAppLuncherDir = "${buildDir}/resources/main/com/github/zutherb/gradle/mapAppBundle"
    mkdir javaAppLuncherDir
    executable  "gcc"
    args        "-I", "/Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/include",
                "-I", "/Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/include/darwin",
                "-o", "${javaAppLuncherDir}/JavaAppLauncher",
                "-framework", "Cocoa",
                "-arch", "x86_64",
                "-isysroot", "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk",
                "-mmacosx-version-min=10.7",
                "${projectDir}/src/main/object-c/main.m",
                "-v"
}

tasks.processResources.dependsOn(compileJavaAppLuncher)
Abbildung 2: Maven Verzeichnisstruktur
Abbildung 2: Maven Verzeichnisstruktur

Gradle besteht aus einem abstrakten Kern und einer Vielzahl von Plugins. Selbst die  Implementierung des Java-Builds basiert auf einem Java-Plugin. Mit dieser Architektur gewinnt Gradle die Möglichkeit, Buildprozesse für beliebige Software-Plattformen bewerkstelligen zu können und liefert dem Anwender die Möglichkeit, seine „nicht-konventionellen“ Vorstellungen dem Tool beizubringen. Gradle liefert von Hause aus eine Menge von Plugins mit, die neben Java Groovy-, Scala- und sogar C++- Projekte bauen können.

Leider verfügt Gradle nicht von Hause aus über ein Plugin Mac OS X Anwendungen paketieren, auch wenn Gradle über ein sehr gutes Application Plugin verfügt, das eine Applikation für Windows und Linux paketiert.

Erstellen eines Application Bundles mit Gradle

Um ein Application Bundle mit Gradle zu erstellen, gibt das Projekt: gradle-macappbundle. Es funktioniert analog zu dem Gradle Application Plugin. Lediglich der MainClassName muss konfiguriert werden und dann erstellt das Plugin die oben genannte Verzeichnisstruktur. Das Plugin paketiert die Anwendung danach sogar in einem dmg Datei. Dieses MacAppBundle Plugin funktioniert wirklich gut, doch leider unterstützt der JavaApplicationStub nur Java6. Java7 Programme können leider nicht mit dem JavaApplicationStub geöffnet werden, was sehr schade ist, wenn man eine neuer Java-Version benutzen will. Außerdem wird die JRE nicht mit in die Applikation gepackt, was gerade bei den neueren Java-Versionen zu Problem führen kann, da Java von Apple nicht mehr mit MacOS ausgeliefert wird.

Auf der Suche nach einer Alternative bin ich bei Oracle fündig geworden. Der Oracle Appbundler Task verfügt über einen JavaApplicationStub, der in diesem Projekt allerdings JavaAppLauncher genannt wird und mit Java7/8 verwendet werden kann. Beim JavaAppLauncher handelt es sich um ein einfaches Objective C Programm, dass das über JNI eine JVM hochfährt und dabei die Info.plist Datei auswertet. Außerdem wird die JRE mit in die Anwendung kopiert, womit sichergestellt ist, dass die Java Applikation unter OSX immer läuft, auch wenn kein Java auf dem Rechner installiert ist. Prinzipiell ist es in einem Gradle-Script möglich auf einen Ant-Script zu zu greifen, allerdings hält sich Oracle bei der Paketierung nicht an die Konventionen die Apple propagiert. Deshalb habe ich mich entschlossen, diese beiden Projekte miteinander zu verbinden und ein eigenes Plugin auf deren Basis zu erstellen.

Der Quellcode von meinem Plugin ist in meinem github Repository verfügbar und unterscheidet sich in der Verwendung nicht vom gradle-macappbundle. Es verfügt über die folgenden neun Tasks:

  1. configMacApp – konfiguriert die Default-Werte für das Plugin
  2. generatePlist – generiert die Info.plist Datei
  3. generatePkgInfo – generiert die PkgInfo Datei
  4. copyToResourcesJava – kopiert die Jars in das .app Verzeichnis
  5. copyJavaAppLauncher – kopiert den JavaAppLauncher in das .app Verzeichnis, der zum Starten von Java benutzt wird
  6. copyJavaRuntime – kopiert die JRE in das .app Verzeichnis
  7. runSetFile – führt das Kommando SetFile aus, dass das Bundle Bit setzt
  8. createApp – Aggregator Task für die Tasks 9/10
  9. codeSign – erstellt die digitale Signatur für die Anwendung
  10. createDmg – erstellt die .dmg Datei, die das .app Verzeichnis enthält

Damit verfügt das Plugin über die komplette Funktionalität vom gradle-macappbundle Plugin und dem Oracle Appbundler Task. Um es zu verwenden muss man nur das Jar auf den ClassPath des Buildscripts packen und das Plugin im Buildscript hinzufügen. Das folgende Listing zeigt, wie man das Plugin in seinen eigenen Buildscript verwenden kann.

apply plugin: 'macAppBundle'

macAppBundle {
    mainClassName = "com.example.myApp.Start"
    icon = "myIcon.icns"
}

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.github.zutherb.gradle:gradle-macappbundle:0.1'
    }
}

Wenn man das Plugin entsprechend konfiguriert hat, wird das Plugin automatisch von Gradle benutzt, wenn man das Projekt mit dem build – Task baut. D.h. bei jedem Build wird automatisch eine dmg Datei erstellt, die das komplette Application Bundle enthält.

Viel Spaß damit.

Share Button