webentwicklung-frage-antwort-db.com.de

Boilerplate-Projektkonfiguration in Gradle mit Gradle Kotlin DSL

Ich versuche derzeit, die Art und Weise zu verbessern, wie unsere Projekte ihre Konfiguration freigeben. Wir haben viele verschiedene Multi-Modul-Gradle-Projekte für alle unsere Bibliotheken und Microservices (d. H. Viele Git-Repos). 

Meine Hauptziele sind:

  • Damit meine Nexus-Repository-Konfiguration nicht in jedem Projekt dupliziert wird (ich kann auch davon ausgehen, dass sich die URL nicht ändert)
  • Um meine benutzerdefinierten Gradle-Plugins (für Nexus veröffentlicht) für jedes Projekt mit minimalem Speicherplatz/Duplizierung verfügbar zu machen (sie sollten für jedes Projekt verfügbar sein, und das einzige, was das Projekt interessiert, ist die Version, die es verwendet).
  • Keine Magie - es sollte für die Entwickler offensichtlich sein, wie alles konfiguriert ist

Meine derzeitige Lösung ist eine benutzerdefinierte Gradel-Distribution mit einem Init-Skript, das:

  • fügt mavenLocal() und unser Nexus-Repository zu den Projekt-Repos hinzu (sehr ähnlich dem Skript-Dokumentationsbeispiel von Gradle init , außer dass Repos hinzugefügt und validiert werden).
  • konfiguriert eine Erweiterung, mit der unsere Gradle-Plugins zum Buildscript-Klassenpfad hinzugefügt werden können (mithilfe von dieser Problemumgehung ). Außerdem wird unser Nexus-Repo als Buildscript-Repo hinzugefügt, da dort die Plugins gehostet werden. Wir haben einige Plugins (aufbauend auf Netflix 'exzellenten nebula plugins ) für verschiedene Boilerplates: Standard-Projekt-Setup (Kotlin-Setup, Test-Setup usw.), Veröffentlichung, Veröffentlichung, Dokumentation usw. Dies bedeutet, dass unsere Projektdateien build.gradle sind so ziemlich nur für Abhängigkeiten.

Hier ist das Init-Skript (bereinigt):

/**
 * Gradle extension applied to all projects to allow automatic configuration of Corporate plugins.
 */
class CorporatePlugins {

    public static final String NEXUS_URL = "https://example.com/repository/maven-public"
    public static final String CORPORATE_PLUGINS = "com.example:corporate-gradle-plugins"

    def buildscript

    CorporatePlugins(buildscript) {
        this.buildscript = buildscript
    }

    void version(String corporatePluginsVersion) {
        buildscript.repositories {
            maven {
                url NEXUS_URL
            }
        }
        buildscript.dependencies {
            classpath "$CORPORATE_PLUGINS:$corporatePluginsVersion"
        }
    }

}

allprojects {
    extensions.create('corporatePlugins', CorporatePlugins, buildscript)
}

apply plugin: CorporateInitPlugin

class CorporateInitPlugin implements Plugin<Gradle> {

    void apply(Gradle gradle) {

        gradle.allprojects { project ->

            project.repositories {
                all { ArtifactRepository repo ->
                    if (!(repo instanceof MavenArtifactRepository)) {
                        project.logger.warn "Non-maven repository ${repo.name} detected in project ${project.name}. What are you doing???"
                    } else if(repo.url.toString() == CorporatePlugins.NEXUS_URL || repo.name == "MavenLocal") {
                        // Nexus and local maven are good!
                    } else if (repo.name.startsWith("MavenLocal") && repo.url.toString().startsWith("file:")){
                        // Duplicate local maven - remove it!
                        project.logger.warn("Duplicate mavenLocal() repo detected in project ${project.name} - the corporate gradle distribution has already configured it, so you should remove this!")
                        remove repo
                    } else {
                        project.logger.warn "External repository ${repo.url} detected in project ${project.name}. You should only be using Nexus!"
                    }
                }

                mavenLocal()

                // define Nexus repo for downloads
                maven {
                    name "CorporateNexus"
                    url CorporatePlugins.NEXUS_URL
                }
            }
        }

    }

}

Dann konfiguriere ich jedes neue Projekt, indem ich der root-Datei build.gradle Folgendes hinzufügen:

buildscript {
    // makes our plugins (and any others in Nexus) available to all build scripts in the project
    allprojects {
        corporatePlugins.version "1.2.3"
    }
}

allprojects  {
    // apply plugins relevant to all projects (other plugins are applied where required)
    apply plugin: 'corporate.project'

    group = 'com.example'

    // allows quickly updating the wrapper for our custom distribution
    task wrapper(type: Wrapper) {
        distributionUrl = 'https://com.example/repository/maven-public/com/example/corporate-gradle/3.5/corporate-gradle-3.5.Zip'
    }
}

Dieser Ansatz funktioniert zwar und ermöglicht reproduzierbare Builds (im Gegensatz zu unserem vorherigen Setup, bei dem ein Buildskript von einer URL angewendet wurde - was zu dieser Zeit nicht im Cache abgelegt werden konnte), und das Offline-Arbeiten ermöglicht wird. Es macht es jedoch etwas magisch, und ich frage mich, ob ich könnte die Dinge besser machen. 

Dies wurde alles durch das Lesen von einem Kommentar zu Github von Gradle-Entwickler Stefan Oehme ausgelöst, der besagt, dass ein Build funktionieren sollte, ohne auf ein Init-Skript angewiesen zu sein. Dh Init-Skripts sollten nur dekorativ sein und Dinge wie das dokumentierte Beispiel tun, um nicht autorisierte Repos zu verhindern , usw.

Meine Idee war, einige Erweiterungsfunktionen zu schreiben, die es mir ermöglichen, unser Nexus-Repo und unsere Plugins zu einem Build hinzuzufügen, das aussah, als wären sie in Gradle eingebaut (ähnlich wie die Erweiterungsfunktionen gradleScriptKotlin() und kotlin-dsl() bereitgestellt von der Gradle Kotlin DSL.

Also habe ich meine Erweiterungsfunktionen in einem Kotlin Gradle-Projekt erstellt:

package com.example

import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.artifacts.dsl.RepositoryHandler
import org.gradle.api.artifacts.repositories.MavenArtifactRepository

fun RepositoryHandler.corporateNexus(): MavenArtifactRepository {
    return maven {
        with(it) {
            name = "Nexus"
            setUrl("https://example.com/repository/maven-public")
        }
    }
}

fun DependencyHandler.corporatePlugins(version: String) : Any {
    return "com.example:corporate-gradle-plugins:$version"
}

Mit dem Plan, sie in build.gradle.kts meines Projekts wie folgt zu verwenden:

import com.example.corporateNexus
import com.example.corporatePlugins

buildscript {

    repositories {
        corporateNexus()
    }

    dependencies {
        classpath(corporatePlugins(version = "1.2.3"))
    }
}

Gradle konnte meine Funktionen jedoch nicht sehen, wenn sie im buildscript-Block verwendet wurden (Skript konnte nicht kompiliert werden). Ihre Verwendung in den normalen Projekt-Repos/Abhängigkeiten funktionierte jedoch gut (sie sind sichtbar und funktionieren wie erwartet). 

Wenn dies funktionierte, hoffte ich, die Dose in meine benutzerdefinierte Distribution zu bündeln. Das heißt, mein Init-Skript könnte einfach eine einfache Überprüfung durchführen, anstatt die magische Plugin- und Repo-Konfiguration zu verstecken. Die Erweiterungsfunktionen müssen nicht geändert werden, sodass keine neue Gradle-Distribution freigegeben werden muss, wenn Plugins geändert werden.

Was ich probiert habe:

  • hinzufügen meines Jars zum Buildscript-Klassenpfad des Testprojekts (d. h. buildscript.dependencies) - funktioniert nicht (möglicherweise funktioniert das nicht konstruktiv, da es nicht richtig erscheint, buildscript eine Abhängigkeit hinzuzufügen, auf die im selben Block verwiesen wird).
  • die Funktionen in buildSrc setzen (was für normale Projekt-Deps/Repos aber nicht für buildscript geeignet ist, aber keine echte Lösung ist, da nur die Speicherplatte verschoben wird)
  • das jar im lib-Ordner der Distribution ablegen

Meine Frage lautet also:

  • Ist das, was ich zu erreichen versuche, möglich (können benutzerdefinierte Klassen/Funktionen für den buildScript-Block sichtbar gemacht werden)? 
  • Gibt es einen besseren Ansatz für die Konfiguration eines Nexus-Repos für Unternehmen und das Bereitstellen von benutzerdefinierten Plugins (für Nexus veröffentlicht) für viele separate Projekte (d. H. Völlig unterschiedliche Codebases) mit minimaler Boilerplate-Konfiguration?
28
James Bassett

Ich habe @eskatos versprochen, dass ich wiederkommen würde und eine Antwort auf seine Antwort geben würde - hier also!

Meine endgültige Lösung besteht aus:

  • Gradle 4.7-Wrapper pro Projekt (zeigt auf einen Spiegel von http://services.gradle.org/distributions - setup in Nexus als primäres Proxy-Repository, d. H. Es ist Vanilla Gradle, wird jedoch über Nexus heruntergeladen)
  • Benutzerdefinierte Gradle-Plugins, die in unserem Nexus-Repo zusammen mit Plugin-Markern veröffentlicht wurden (generiert durch das Java Gradle Plugin-Entwicklungs-Plugin )
  • Spiegelung des Gradle Plugin-Portals in unserem Nexus-Repo (d. H. Ein Proxy-Repo, das auf https://plugins.gradle.org/m2 zeigt)
  • Eine settings.gradle.kts-Datei pro Projekt, die unseren Maven Repo- und Gradle-Plugin-Portalspiegel (beide in Nexus) als Plugin-Verwaltungs-Repositorys konfiguriert.

Die settings.gradle.kts-Datei enthält Folgendes:

pluginManagement {
    repositories {
        // local maven to facilitate easy testing of our plugins
        mavenLocal()

        // our plugins and their markers are now available via Nexus
        maven {
            name = "CorporateNexus"
            url = uri("https://nexus.example.com/repository/maven-public")
        }

        // all external gradle plugins are now mirrored via Nexus
        maven {
            name = "Gradle Plugin Portal"
            url = uri("https://nexus.example.com/repository/gradle-plugin-portal")
        }
    }
}

Dies bedeutet, dass alle Plugins und ihre Abhängigkeiten nun über Nexus weitergeleitet werden, und Gradle wird unsere Plugins anhand der ID finden, da die Plugin-Marker auch bei Nexus veröffentlicht werden. Da mavenLocal vorhanden ist, können Änderungen an Plug-Ins einfach vor Ort getestet werden.

Die build.gradle.kts-Datei jedes Projekts wendet die Plugins dann wie folgt an:

plugins {
    // plugin markers for our custom plugins allow us to apply our
    // plugins by id as if they were hosted in gradle plugin portal
    val corporatePluginsVersion = "1.2.3"
    id("corporate-project") version corporatePluginsVersion
    // 'apply false` means this plugin can be applied in a subproject
    // without having to specify the version again
    id("corporate-publishing") version corporatePluginsVersion apply false
    // and so on...
}

Und konfiguriert den Gradle-Wrapper für die Verwendung unserer gespiegelten Distribution, was in Kombination mit den obigen Angaben bedeutet, dass alles (Gradle, Plugins, Abhängigkeiten) über Nexus kommt:

tasks {
    "wrapper"(Wrapper::class) {
        distributionUrl = "https://nexus.example.com/repository/gradle-distributions/gradle-4.7-bin.Zip"
    }
}

Ich hatte gehofft, die Boilerplate in den Einstellungsdateien zu vermeiden, indem ich den Vorschlag von @ eskatos anwendete, ein Skript von einer entfernten URL in settings.gradle.kts anzuwenden. d.h.

apply { from("https://nexus.example.com/repository/maven-public/com/example/gradle/corporate-settings/1.2.3/corporate-settings-1.2.3.kts" }

Es ist mir sogar gelungen, ein Templated Script (neben unseren Plugins veröffentlicht) zu generieren, das:

  • die Plugin-Repos konfiguriert (wie im obigen Einstellungsskript)
  • eine Auflösungsstrategie verwendet, um die mit dem Skript verknüpfte Version der Plugins anzuwenden, wenn die angeforderte Plugin-ID eines unserer Plugins war und die Version nicht bereitgestellt wurde (Sie können sie also einfach anhand der ID anwenden)

Obwohl die Boilerplate entfernt wurde, bedeutete dies, dass unsere Builds auf eine Verbindung zu unserem Nexus-Repo angewiesen waren, da es scheint, dass obwohl Skripts, die über eine URL angewendet werden, zwischengespeichert werden, Gradle trotzdem eine HEAD -Anfrage zur Überprüfung vornimmt für Änderungen. Ärgerlich war es auch, Plugin-Änderungen lokal zu testen, da ich es manuell auf das Skript in meinem lokalen Verzeichnis verweisen musste. Mit meiner aktuellen Konfiguration kann ich die Plugins einfach in Maven local veröffentlichen und die Version in meinem Projekt aktualisieren.

Ich bin mit dem aktuellen Setup ziemlich zufrieden - ich denke, es ist für Entwickler jetzt viel offensichtlicher, wie die Plugins angewendet werden. Es ist weitaus einfacher, Gradle und unsere Plugins unabhängig voneinander zu aktualisieren, da zwischen den beiden keine Abhängigkeit besteht (und keine benutzerdefinierte Gradle-Verteilung erforderlich ist).

3
James Bassett

Wenn Sie die volle Güte von Gradle Kotlin DSL nutzen möchten, sollten Sie alle Plugins mit dem plugins {}-Block anwenden. Siehe https://github.com/gradle/kotlin-dsl/blob/master/doc/getting-started/Configuring-Plugins.md

Sie können Plugin-Repositorys und Auflösungsstrategien (z. B. deren Version) in Ihren Einstellungsdateien verwalten. Ab Gradle 4.4 kann diese Datei mit dem Kotlin DSL, auch settings.gradle.kts, geschrieben werden. Siehe https://docs.gradle.org/4.4-rc-1/release-notes.html .

In diesem Sinne könnten Sie dann ein zentralisiertes Settings-Skript-Plugin haben, das die Einstellungen vornimmt und in Ihren Builds settings.gradle.kts-Dateien anwendet:

// corporate-settings.gradle.kts
pluginManagement {
    repositories {
        maven {
            name = "Corporate Nexus"
            url = uri("https://example.com/repository/maven-public")
        }
        gradlePluginPortal()
    }
}

und:

// settings.gradle.kts
apply(from = "https://url.to/corporate-settings.gradle.kts")

In Ihrem Projekterstellungsskript können Sie einfach Plugins von Ihrem Unternehmens-Repository anfordern:

// build.gradle.kts
plugins {
    id("my-corporate-plugin") version "1.2.3"
}

Wenn Sie möchten, dass Ihr Projekt Skripts in einem Multiprojekt-Build erstellt, um die Plugin-Version nicht zu wiederholen, können Sie dies mit Gradle 4.3 tun, indem Sie Versionen in Ihrem Stammprojekt deklarieren. Beachten Sie, dass Sie auch die Versionen in settings.gradle.kts mit pluginManagement.resolutionStrategy festlegen können, wenn alle Builds dieselbe Plugins-Version verwenden, die Sie benötigen.

Beachten Sie außerdem, dass Ihre Plugins mit ihrem Plugin-Marker-Artefakt veröffentlicht werden müssen, damit dies funktioniert. Dies kann leicht mit dem Java-gradle-plugin-Plugin durchgeführt werden.

10
eskatos

Ich habe in meinem Build so etwas gemacht

buildscript {
    project.apply {
        from("${rootProject.projectDir}/sharedValues.gradle.kts")
    }
    val configureRepository: (Any) -> Unit by extra
    configureRepository.invoke(repositories)
}

In meiner sharedValues.gradle.kts-Datei habe ich folgenden Code:

/**
 * This method configures the repository handler to add all of the maven repos that your company relies upon.
 * When trying to pull this method out of the [ExtraPropertiesExtension] use the following code:
 *
 * For Kotlin:
 * ```kotlin
 * val configureRepository : (Any) -> Unit by extra
 * configureRepository.invoke(repositories)
 * ```
 * Any other casting will cause a compiler error.
 *
 * For Groovy:
 * ```groovy
 * def configureRepository = project.configureRepository
 * configureRepository.invoke(repositories)
 * ```
 *
 * @param repoHandler The RepositoryHandler to be configured with the company repositories.
 */
fun repositoryConfigurer(repoHandler : RepositoryHandler) {
    repoHandler.apply {
        // Do stuff here
    }
}

var configureRepository : (RepositoryHandler) -> Unit by extra
configureRepository = this::repositoryConfigurer

Ich folge einem ähnlichen Muster für die Konfiguration der Auflösungsstrategie für Plugins.

Das Schöne an diesem Muster ist, dass alles, was Sie in sharedValues.gradle.kts konfigurieren, auch aus Ihrem buildSrc-Projekt verwendet werden kann, was bedeutet, dass Sie Repository-Deklarationen wiederverwenden können.


Aktualisierte:

Sie können ein anderes Skript von einer URL anwenden, indem Sie beispielsweise Folgendes ausführen:

apply {
    // This was actually a plugin that I used at one point.
    from("http://dl.bintray.com/shemnon/javafx-gradle/8.1.1/javafx.plugin")
}

Hosten Sie einfach Ihr Skript, das alle Ihre Builds auf einem HTTP-Server freigeben sollen (HTTPS wird dringend empfohlen, sodass Ihr Build nicht von einem Mann im mittleren Angriff angegriffen werden kann).

Der Nachteil dabei ist, dass ich glaube nicht, dass aus URLs angewendete Skripts nicht zwischengespeichert werden, so dass sie jedes Mal erneut heruntergeladen werden, wenn Sie Ihren Build ausführen. Dies könnte inzwischen behoben worden sein, da bin ich mir nicht sicher .

2

Eine Lösung, die mir Stefan Oehme anbot, als ich ein ähnliches Problem hatte, bestand darin, meine eigene Gradle-Distribution zu vertreiben. Seiner Meinung nach ist dies in großen Unternehmen üblich.

Erstellen Sie einfach eine benutzerdefinierte Gabel des Gradle-Repos, fügen Sie Ihrem Unternehmen mit dieser benutzerdefinierten Version von Gradle die spezielle Sauce Ihres Unternehmens hinzu.

1

Ich habe ein ähnliches Problem festgestellt, wenn die allgemeine Konfiguration in jedem Projekt repliziert wird. Gelöst durch eine benutzerdefinierte Abstufungsverteilung mit den allgemeinen Einstellungen, die in init-Skript definiert sind. 

Erstellt ein Gradle-Plugin für die Erstellung solcher benutzerdefinierten Distributionen - custom-gradle-dist . Es funktioniert perfekt für meine Projekte, z. Ein build.gradle für ein Bibliotheksprojekt sieht folgendermaßen aus (dies ist eine vollständige Datei): 

dependencies {
    compile 'org.springframework.kafka:spring-kafka'
}
0
denis.zhdanov