Custom Distribution mit Gradle

Das Gradle-Plug-in application erzeugt eine praktikable Distribution für eine Java-Applikation, was aber wenn mehr Flexibilität gebraucht wird? Zum Beispiel ein über-JAR oder ein JAR das sich durch Doppelklick startet lässt.

Das Plug-in application basiert auf dem Plug-in distribution. Es definiert dass alle JARs ins Unterverzeichnis lib kopiert werden und das Startskripte für Windows und Linux/Unix im Unterverzeichnis bin angelegt werden. Wenn man möchte kann man noch weitere Dateien ergänzen:

plugins {
    id 'java'
    id 'application'
}

application.mainClassName = 'de.muspellheim.example.App'

distributions {
    main {
        contents {
            from "doc"
        }
    }
}

Hier werden alle Dateien im Projektverzeichnis doc mit in die Distribution aufgenommen.

Ein uber-JAR erzeugen

Ein uber-JAR ist ein einzelnes JAR, welches die Applikation einschließlich aller ihrer Abhängigkeiten zusammenfasst. Es wird der Inhalt aller JARs schlicht zu einem JAR zusammenkopiert.

Ein uber-JAR erleichtert die Verteilung einer Java-Applikation, da nur noch ein JAR weitergegeben werden muss. Um die Abhängigkeiten muss man sich dabei nicht mehr kümmern, da sie ja enthalten sind.

Mit Gradle kann man das wie folgt machen:

jar {
    manifest {
        attributes 'Main-Class': mainClassName
    }

    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Hinweis: Ein uber-JAR darf nicht verwendet werden, wenn Dateien in den JARs mehrfach vorkommen. Zum Beispiel wenn man Service Provider Interfaces (SPI) verwendet, liegen im Verzeichnis META-INF/services der JARs gleichnamige Dateien, wenn mehrere JARs eine Implementierung für das gleiche Interface bereitstellen. Diese Dateien würden beim Erstellen des uber-JARs überschrieben, so das zur Laufzeit Implementierungen fehlen.

Wenn man jetzt die Distribution mit dem application-Plug-in baut, hat man ein großes uber-JAR, aber immer noch alle Abhängigkeiten zusätzlichen im lib-Verzeichnis. Um das zu vermeiden, konfigurieren wir das distribution-Plug-in selber.

Als erstes wird das application-Plugin ausgetauscht:

plugins {
    id 'java'
    id 'distribution'
}

Dann die main-Distribution mit dem erwarteten Inhalt konfigurieren:

distributions {
    main {
        contents {
            from jar
            from "$projectDir/src/dist"
        }
    }
}

An dieser Stelle können wie mit dem application-Plug-in gewohnt, weiter Dateien zum Inhalt der Distribution hinzugefügt werden.

Da wir das application-Plug-in nicht mehr verwenden, gibt es auch keine Definition für die Main-Klasse mehr. Die kann aber einfach als lokale Variable angelegt werden. Dazu muss

application.mainClassName = 'de.muspellheim.example.App'

durch

def mainClassName = 'de.muspellheim.example.App'

ersetzt werden.

Zum Schluss fehlt noch der run-Task zum Starten der Applikation:

task run(type: JavaExec) {
    group 'application'
    classpath = sourceSets.test.runtimeClasspath
    main = mainClassName
}

Nun kann ein Distributionspaket mit dem Gradle-Task distZip erstellt werden.

Ein komplettes Beispiel

Eine vollständiges Beispiel der build.gradle kann so aussehen:

import org.apache.tools.ant.filters.FixCrLfFilter

plugins {
    id 'java'
    id 'distribution'
}

version = '1.0.0'

def mainClassName = 'de.muspellheim.example.App'

jar {
    manifest {
        attributes 'Main-Class': mainClassName
        attributes 'Implementation-Version': project.version
    }

    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

distributions {
    main {
        contents {
            from jar
            from(projectDir) {
                include 'README.md'
                include 'CHANGELOG.md'
                rename 'md', 'txt'
                filter(FixCrLfFilter, eol: FixCrLfFilter.CrLf.newInstance('crlf'))
            }
            from "$projectDir/src/dist"
        }
    }
}

distZip {
    version = ''
    doLast {
        archiveFile.get().asFile.renameTo(destinationDirectory.file("${project.name}-v${project.version}.zip").get().asFile)
    }
}
assemble.dependsOn = [distZip]

task run(type: JavaExec) {
    group 'application'
    classpath = sourceSets.test.runtimeClasspath
    main = mainClassName
}

Bei dieser Variante gibt es noch ein paar Erweiterungen:

  • Die Version der Applikation wird in das Manifest des JARs geschrieben, so dass diese später zu Beispiel in einem About-Dialog ausgelesen werden kann.
  • Die README und CHANGELOG werden mit eingepackt und dabei die Dateiendung zu txt geändert und die Zeilenenden für Windowsnutzer angepasst.
  • Für die ZIP-Datei der Distribution wird die Version entfernt, damit sie im Verzeichnisname in der ZIP-Datei nicht mehr enthalten ist. Nach dem Erzeugen der ZIP-Datei wird diese umbenannt, damit die Version im Namen der ZIP-Datei dagegen noch enthalten ist.
  • Der Task distZip wird als einzige Abhängigkeit vom Task assemble gesetzt, so dass nur noch ein ZIP und kein TAR mehr erzeugt wird.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.