Automatische Task Execution im Buildship Plugin

Wenn Sie mit Eclipse arbeiten, dann kennen Sie das Buildship Plugin. Das Plugin ist der Kleber der das Buildsystem Gradle in die IDE integriert. Mit der Version 3.1 des Plugins ist es nun möglich Tasks in Gradle bei der Projektsynchronisation und während des automatischen Builds zu starten. Dieses waren die höchsten bewerteten Wünsche auf GitHub. Hier will ich kurz die Möglichkeiten die sich daraus ergeben kurz darstellen.

Tasks während der Projekt Synchronisation ausführen

Die Gradle Projektsynchronisation importiert das Gradle Projekt in den Workspace und konfiguriert die Java Toolchain, damit alles zusammen funkioniert. Wie bereits angesprochen, wollen viele User während dessen eigene Tasks ausführen, um zum Beispiel Konfigurationen zu erstellen oder updaten. Das manuelle Ausführen dieser Aufgaben ist störend und auch fehleranfällig. Durch die automatische Ausführung der Aufgaben, kann der Entwickler sich auf das Coden konzentrieren.

Wie nutzt man das Feature?

Damit man das Feature nutzen kann, muss Gradle in der Version 5.4 oder höher vorhanden sein und Buildship mindestens in der Version 3.1. Ab Gradle 5.4 wurde ein neues Attribut synchronizationTasks in dem Eclipse Plugin eingeführt.

plugins {
    id 'eclipse'
}

task generateCustomConfig {
    doLast {
          println "Generating custom configuration..."
    }
}

eclipse {
    synchronizationTasks generateCustomConfig
}

Das war es auch schon. Wenn Sie nun das Projekt importieren oder synchronisieren, dann wird der Task generateCustomConfig ausgeführt und die Meldung erscheint in der Konsole.

Hinweis: Task synchronsization ist mit einer Referenz auf Task deklariert. Es können also unterschiedliche Typen verwendet werden: Strings um den Task Pfad zu spezifizieren, eine Liste von Tasks und vieles mehr. Grundsätzlich kann jeder task dependency Typ verwendet werden.

Ausführung von Tasks während des Eclipse build Vorgangs

Neben den bereits ober vorgestellten Feature ist als zweite Einstiegsmöglichkeit, dass einklinken in den Buildprozess nun möglich. Wenn also der Benutzer eine Resource ändert und ein Build angestoßen wird, kann man nun Aufgaben durchführen lassen. Zum Beispiel kann man einen Code Generator anwerfen oder Validierungen durchführen.

Der Aufruf erfolgt sehr ähnlich:

plugins {
    id 'eclipse'
}

task generateCode {
    doLast {
        println 'Generating some code...'
    }
}

eclipse {
    autoBuildTasks generateCode
}

Zusammenfassung

Die neuen Möglichkeiten die das Eclipse Buildship Plugin nun bietet sind sehr interessant und nehmen dem Entwickler viel Arbeit ab und können möglich Fehler vermeiden zu helfen, indem man Aufgaben automatisieren kann.

Gradle Release Candidate 6.2

Mit der neuen Version von Gradle 6.2 werden wieder interessante Neuigkeiten Einzug in das beliebte Buildsystem erhalten.

Dependencies Verification

Zur Zeit kann es ein Risiko darstellen, wenn die Software externe Dependencies einbindet, da diese womöglich während des Transports manipuliert worden sein könnten. Diese gingen die Entwickler von Gradle nun an und implementierten die Überprüfung mittels Checksummen und Signaturen. Damit wird das Risiko, dass es eine malicious Software integriert wird, erheblich gesenkt.

Warnungen über Deprecated nun mit Links zur Hilfe versehen

Es werden nun verbesserte Hilfestellungen ausgegeben. Diese verlinken direkt zur Onlinehilfe von Gradle. In einigen Konsolen (KDE Konsole) lassen sich dann die Link bequem öffnen.

The compile configuration has been deprecated for dependency declaration. This will fail with an error in Gradle 7.0. Please use the implementation configuration instead. Consult the upgrading guide for further information: https://docs.gradle.org/6.2/userguide/upgrading_version_5.html#dependencies

Neu: Ein shared dependency Cache

Der neue shared dependency Cache erlaubt das Cachen von Dependencies über mehrere Instanzen von Gradle. Dieses macht es möglich in einem gemeinsam geteilten Verzeichnis in flüchtigen Containern daruf zuzugreifen. Dieses ist read-only, so dass dieses ohne kopieren des zwischen den Container geteilt werden kann.

Lokales Docker Repository verwenden

Möchte man Docker Images zum Bauen einer Anwendung in Drone verwenden, dann muss i.d.R. Credentials für Basic Auth am Nexus Docker Repository übergeben werden. Dazu muss den Inhalt der config.json in ein neues Secret in Drone schreiben.

Dazu muss man sich an der Konsole einmal an dem Repository docker login anmelden.

docker login docker.XXXX.org

Das JSON der config Datei sieht in etwa dann so aus:

        "auths": {
                "docker.XXXX.org": {
                        "auth": "VXXXXXXX="
                }
        },

Den Inhalt kopieren sie bitte in ein neues Secret mit dem Key dockerconfigjson. Aus diesem wird Drone die Informationen für die Basic Authentication an dem Docker Repository extrahieren. In der .drone.yml muss nur noch image_pull_secrets eingefügt werden.

kind: pipeline
name: default

steps:
- name: setup
  image: docker.XXXX.org/edvpfau/archlinux-base-image:latest
  environment:
    NEXUS_URL:
      from_secret: NEXUS_URL
    NEXUS_USERNAME:
      from_secret: NEXUS_USERNAME
    NEXUS_PASSWORD:
      from_secret: NEXUS_PASSWORD
    NEXUS_REPOSITORY:
      from_secret: NEXUS_REPOSITORY
    SONAR_HOST:
      from_secret: SONAR_HOST
    SONAR_TOKEN:
      from_secret: SONAR_TOKEN
  commands:
    - JAVA_HOME=/usr/lib/jvm/default ./gradlew -Dsonar.host.url=$SONAR_HOST -Dsonar.login=$SONAR_TOKEN clean build test sonarqube

image_pull_secrets:
- dockerconfigjson

Nun steht der Nutzung eigener privater Images zum Bauen der Software nichts mehr im Wege. Das Image kann dann speziell an die Verwendete Software angepasst werden. Hier in dem Beispiel verwende ich ein Image, welches den Gradle Wrapper vorinstalliert hat und somit in jedem Durchgang nicht nur Zeit sondern auch Netzwerktraffic eingesparrt werden.

Maven Caching

Warum sollte man einen zentralen Cache verwenden? Nun, wenn man in einem Betrieb mit mehreren Mitarbeitern arbeitet, dann kann es unter Umständen schon zu Engpässen in der Bandbreite führen. Dieses gilt auch heute bei Modernen DSL Anschlüssen, da diese oftmals auch Telefonie bereitstellen und daher wegen QoS diese zusätzlich von der Verfügung stehenden Bandbreite abgezogen werden müssen.

Es gibt aber auch noch weitere Gründe warum man den Zugang zu dem Maven Repository zentralisieren sollte. Es kann sein, dass bestimmte Projekte sehr strenge Vorgaben an die zu verwendenden Dependencies machen. Dieses können zum Beispiel verwendete Lizenzen oder CVEs sein.

Nexus

Zunächst muss man in Nexus ein Maven 2 Proxy anlegen. Ich gehe hier davon aus, dass eine laufende Instanz von dem Nexus Server im internen Netz vorhanden ist. Der Nexus Server ist auch als Docker Image vorhanden und lässt sich in ein paar Minuten aufsetzen.

Proxy Einrichten

Wie bereits beschrieben muss zunächst ein Maven Proxy angelegt werden. Dazu meldet man sich als Administrator an und geht in den Einstellungen in die Repositories. Dort kann man mit create ein neues Repository anlegen. Wählen sie den Type Maven2(Proxy). Vergeben sie als Namen maven-central-proxy. Der Name ist dann Teil der URL unter dem dann im Nexus der Maven Proxy erreichbar ist.

Für die Upstream URL für das Maven Repository https://repo1.maven.org/maven2 ein.

Maven

In Maven kann man die Repositories in der settings.xml verwalten.

<settings>
  <mirrors>
    <mirror>
      <!--This sends everything else to /public -->
      <id>nexus</id>
      <mirrorOf>*</mirrorOf>
      <url>http://nexus.XXXX.de/repository/maven-central-proxy/</url>
    </mirror>
  </mirrors>
  <profiles>
    <profile>
      <id>nexus</id>
      <!--Enable snapshots for the built in central repo to direct -->
      <!--all requests to nexus via the mirror -->
      <repositories>
        <repository>
          <id>central</id>
          <url>http://central</url>
          <releases><enabled>true</enabled></releases>
          <snapshots><enabled>true</enabled></snapshots>
        </repository>
      </repositories>
     <pluginRepositories>
        <pluginRepository>
          <id>central</id>
          <url>http://central</url>
          <releases><enabled>true</enabled></releases>
          <snapshots><enabled>true</enabled></snapshots>
        </pluginRepository>
      </pluginRepositories>
    </profile>
  </profiles>
  <activeProfiles>
    <!--make the profile active all the time -->
    <activeProfile>nexus</activeProfile>
  </activeProfiles>

  <!-- set if non anonymous connection allowed -->
  <servers>
    <server>
      <id>nexus</id>
      <username>XXX</username>
      <password>YYY</password>
    </server>
  </servers>    
</settings>

Gradle

In Gradle hat man mehrere Möglichkeiten ein anderes Repository zu verwenden. Das Naheliegendste ist in dem build.gradle das Repository direkt anzugeben. Doch Gradle bietet noch mehr Wege, um hier einzugreifen.

In https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.dsl.RepositoryHandler.html wird beschrieben, dass das Maven Central Repository mit der URL https://repo.maven.apache.org/maven2/ verwendet wird. Diese lässt sich zentral für alle Projekte auf dem Host durch ein Skript ersetzen, welches ich in dem nächsten Abschnitt vorstelle.

Gradle Init Skript

Das Gralde Init-Skript wird ausgeführt, wenn man Gradle startet bzw. ein Task ausgeführt wird. Das Init-Skript kann eine Datei sein die init.gradle heißt und im Ordner ~/.gradle liegt oder es ist eine Datei die mit .gradle endet und in dem Verzeichnis ~/.gradle/init.d liegt.

Ich habe mit für letzteres entschieden und eine Datei maven.gradle in dem Verzeichnis ~/.gradle/init.d erstellt. Es muss zunächst die nexusUrl angepasst werden. Diese setzt sich aus der Server URL und dem Namen des Maven2 Proxy Repositories zusammen. Hier also maven-central-proxy. Das Skript filter Repositories die mit https://repo.maven.apache.org/maven2 heraus und fügt dafür die URL von dem Nexus Server ein. Das Ganze wird auf der Konsole geloggt, sodass man auch gleich erkennt welche Repositories in dem Projekt verwendet werden.

Das Skript maven.gradle:

//
// init.d/maven.gradle
// -------------------
//
// This replaces all occurance of mavenCentral repositories with the local Nexus repository
//

def nexusUrl = "https://nexus.XXXX.de/repository/maven-central-proxy"
def prefix = "> Init.d/maven.gradle :"

allprojects {
    buildscript {
        repositories {
            all { ArtifactRepository repo -> 
                if ((repo instanceof MavenArtifactRepository) && repo.url.toString().startsWith("https://repo.maven.apache.org/maven2")) {
                    project.logger.warn "${prefix}Buildscript Repository: ${repo.url} removed. Only $nexusUrl is allowed"
                    remove repo
                }
            }
            maven {
                url "$nexusUrl"
            }
        }
    }

    repositories {
        all { ArtifactRepository repo ->
            if ((repo instanceof MavenArtifactRepository) && repo.url.toString().startsWith("https://repo.maven.apache.org/maven2")) {
                project.logger.warn "${prefix}Repository: change ${repo.url} ---> $nexusUrl"
                remove repo
            } else {
               println "${prefix}Repository: " + repo.url.toString()
            }
        }
        maven { 
            url "$nexusUrl" 
        }
    }
}

Hinweis: Gradle missachtet hier die settings.xml. Wenn eine Authentifizierung am Nexus nötig ist (kein anonymous), dann müssen in den beiden maven-Blöcken jeweils ein Credentials Block eingefügt werden. Sonst erhält man ein 401 HTTP Error.

credentials {
  username "XXX"
  password "YYY"
} 

Gradle Build

Wenn man jetzt einen Build startet, dann kann man auf der Konsole erkennen, wie das Skript das Maven Central Repository, welches über mavenCentral() eingebunden wurde, ersetzt wird.

[sascha@Workstation TestServer]$ ./gradlew build
> Init.d/maven.gradle :Repository: https://nexus.XXXX.de/repository/maven-central-proxy

> Configure project :
> Init.d/maven.gradle :Repository: change https://repo.maven.apache.org/maven2/ ---> https://nexus.XXXX.de/repository/maven-central-proxy
> Init.d/maven.gradle :Repository: https://repo.spring.io/snapshot
> Init.d/maven.gradle :Repository: https://repo.spring.io/milestone

Buildship 3

Die neue Version des Buildship Plugins wird die Version 3 sein. Die aktuelle stable Version ist 2.2.1.

Offen Punkte die noch zu fixen sind, sind in den Issues zu finden.

Neue Installationsquelle hinzufügen

http://download.eclipse.org/buildship/updates/e49/snapshots/3.x/

Installation überprüfen

Die erfolgreiche Installation kann in About Eclipse überprüft werden.

Neues Projekt mit Spring Tool Suite 4 erstellen

Nach dem Neustart von Eclipse kann jetzt ein Projekt mit dem Starter erstellt werden. Es kann nun Gradle Buildship 3.x ausgewählt werden.

Neues in Gradle 6.0

  • Mit Gradle 6 wird nun JDK 13 unterstützt!
  • Stark erweitertes Dependency Management
  • Die Module Metadata werden jetzt per default erstellt und publiziert
  • Out of the box Unterstüzung von JavaDoc.jar und source.jar

Erstes Update nach dem Point Release

Kurz nach dem Release 6.0 kam auch schon das erste Fix für Gradle 6.0 heraus.

Im allgemeinen wurden kleinere Fehler behoben. Weitere Informationen können sie unter den Gradle 6.0.1 Release Notes erfahren.

Wrapper updaten

./gradlew wrapper --gradle-version=6.0.1

Download

Die aktuellen Versionen von Gradle lassen sich auf der Release Page herunterladen.

Eclipse und der Modulepath

Mit Java 9 wurden die Module neu eingeführt (Projekt Jigsaw). Wenn man ein Projekt mit Modulen baut, dann müssen die Module auch in dem Modulepath vorhanden sein.

Das Buildship-Plugin von Eclipse setzt leider nicht alle Module automatisch auf den Modulepath, sodass bei jeder Änderung am gradle.build Skript man von Hand die Einträge im Buildpath Konfigurationsdialog verschieben muss.

Folgendes kleines Skript erledigt die Aufgabe für uns, wenn der Gradle Task eclipse ausgeführt wird.

//
// Will be executed when gradle Task Eclipse was called
//

eclipse {
    project {
        natures 'org.eclipse.buildship.core.gradleprojectnature'
    }

    classpath {
        file {
            whenMerged {
                entries.findAll { isModule(it) }.each {
                    it.entryAttributes['module'] = 'true'
                }

                entries.findAll { isSource(it) && isTestScope(it) }.each {
                    it.entryAttributes['test'] = 'true'
                }

                entries.findAll { isLibrary(it) && isTestScope(it) }.each {
                    it.entryAttributes['test'] = 'true'
                }
            }
        }

        defaultOutputDir = file('build')
        downloadSources = true
        downloadJavadoc = true
    }
}

boolean isLibrary(entry) { return entry.properties.kind.equals('lib') }
boolean isTestScope(entry) { return entry.entryAttributes.get('gradle_used_by_scope').equals('test'); }
boolean isModule(entry) { isLibrary(entry) && !isTestScope(entry); }
boolean isSource(entry) { return entry.properties.kind.equals('src'); }

Mit Gradle Java kompilieren

Wenn man mit ./gradlew einen Build anstößt, so verwendet Gradle immer den Java Compiler den es in der Environment Variable JAVA_HOME findet. Das ist normalerweise auch ok so, aber manchmal möchte eine IDE oder im Allgeimeinen eine ältere Version verwendet, aber Quelltexte mit einer höheren Java Version übersetzen. Dann steigt Gradle aus, da der Compiler dieses nicht kompilieren kann.

Crosscompiling

Gegeben sei ein Quelltext der Java 12 voraussetzt. Dieser soll nun bei gegebener JVM OpenJDK 64-Bit GraalVM CE 19.2.0.1 übersetzt werden. Dazu muss man Gradle ein anderes Executable javac gesetzt werden. Folgender Code setzt für die Tasks AbstractCompile, Javadoc, JavaExec, Test den Javac in dem Java Home /usr/lib/jvm/java-12-openjdk.

sourceCompatibility = 12

def javaHome="/usr/lib/jvm/java-12-openjdk"
def javaExecutablesPath = new File(javaHome, 'bin')
def javaExecutables = [:].withDefault { execName ->
    def executable = new File(javaExecutablesPath, execName)
    assert executable.exists(): "There is no ${execName} executable in ${javaExecutablesPath}"
    executable
}
tasks.withType(AbstractCompile) {
    options.with {
        fork = true
        forkOptions.javaHome = file(javaHome)
    }
}
tasks.withType(Javadoc) {
    executable = javaExecutables.javadoc
}
tasks.withType(Test) {
    executable = javaExecutables.java
}
tasks.withType(JavaExec) {
    executable = javaExecutables.java
}

Das Gradle Deploy Plugin

Das https://gradle-ssh-plugin.github.io/docs/ gradle plguin eigent sich um zum Beispiel eine Spring Boot Jar auf einen remote Server zu deployen.

Upload per SSH auf einen Server

Hier nun ein Beispiel, wie man eine Datei auf einen remote Server kopiert. Dazu muss zunächst der Server definiert werden. Im Anschluss wird ein Deploy Task definiert, der von dem bootJar Task abhängt.

/**
 * This deployment script uses ssh to copy the spring boot jar to a remote server. The credentials are stored
 * in the credential store plugin from nu.studer. 
 *
 * set user and password for deployment
 *
 * to set username
 * ./gradlew addCredentials --key deployUsername --value sascha
 *
 * to set password
 * read -s -p "Enter Password: "; ./gradlew addCredentials --key deployPassword --value $REPLY; unset REPLY
 */


// define remote server
remotes {
    server {
        host = 'server'
        user     = credentials.deployUsername
        password = credentials.deployPassword
    }
}

// copy jar to server
task deploy(dependsOn: bootJar) {
    doLast {
        ssh.run {
            session(remotes.server) {
                put from: bootJar.archivePath.path, into: '/somejar-0.0.1.jar'
            }
        }
    }
}

Weitere Möglichkeiten gradle-ssh Plugin

Das war hier natürlich nur ein sehr einfach gehaltenes Bespiel. Es lassen sich durch die zahlreichen Funktionen wie das Ausführen, Kopieren und vielen mehr fast alles realisieren.

Einen bestimmten Test ausführen

Der Task test kennt Filter. Mit diesen Filtern kann man die auszuführenden Tests einschränken.

./gradlew test --tests *Soup.isBlackEyedBeansSoup -ds

Sternchen sind als Platzhalter erlaubt. Der Punkt trennt Klassen von Methode(n). Auch hier sind Platzhalter erlaubt. In dem Beispiel oben wird die Methode isBlackEyedBeanSoup in allen Klassen die mit Soup enden aufgerufen.

Zusätzliche Informationen ausgeben

Die normale Ausgabe unterdrückt Informationen die die Fehleranalyse ermöglichen.

  • -d, –debug Log in debug mode (includes normal stacktrace).

  • -s, –stacktrace Print out the stacktrace for all exceptions.