web-dev-qa-db-fra.com

Configuration du projet de plaque de cuisson dans Gradle avec Gradle Kotlin DSL

J'essaie actuellement d'améliorer la façon dont nos projets partagent leur configuration. Nous avons de nombreux projets de modules multi-modules pour toutes nos bibliothèques et nos microservices (c’est-à-dire de nombreux dépôts git). 

Mes objectifs principaux sont:

  • Ne pas dupliquer la configuration de mon référentiel Nexus dans chaque projet (je peux aussi supposer en toute sécurité que l'URL ne changera pas)
  • Rendre mes plugins Gradle personnalisés (publiés dans Nexus) disponibles pour chaque projet avec un minimum de passe-partout/duplication (ils devraient être disponibles pour chaque projet et la seule chose dont il se soucie est la version qu'il utilise)
  • Pas de magie - les développeurs doivent comprendre comment tout est configuré

Ma solution actuelle est une distribution graduée personnalisée avec un script init qui:

  • ajoute mavenLocal() et notre référentiel Nexus aux mises en pension du projet (très similaire à l'exemple de documentation de script Gradle init , sauf qu'il ajoute des mises en pension et les valide)
  • configure une extension qui permet d’ajouter nos plug-ins Gradle au chemin de classe buildscript (à l’aide de this solution de contournement ). Il ajoute également notre référentiel Nexus en tant que référentiel de buildscript, car c'est là que les plugins sont hébergés. Nous avons pas mal de plugins (construits sur l'excellent plug-in de la nébuleuse ) de Netflix pour différents types de passe-partout: configuration de projet standard (configuration de kotlin, configuration de test, etc.), publication, publication, documentation, etc., ce qui signifie que nos fichiers de projet build.gradle sont à peu près juste pour les dépendances.

Voici le script init (désinfecté):

/**
 * 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
                }
            }
        }

    }

}

Ensuite, je configure chaque nouveau projet en ajoutant les éléments suivants au fichier racine Build.gradle:

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'
    }
}

Bien que cette approche fonctionne, elle permet des constructions reproductibles (contrairement à notre configuration précédente qui appliquait un script de construction à partir d’une URL (qui n’était pas caché à l’époque)) et qui permettait de travailler en mode hors connexion, cela rend cela un peu magique et je me demandais si pourrait mieux faire les choses. 

Tout cela a été déclenché par la lecture un commentaire sur Github par Gradle dev Stefan Oehme déclarant qu'une construction devrait fonctionner sans recourir à un script d'initialisation, c.-à-d. Que les scripts d'initialisation doivent simplement être décoratifs et faire des choses comme l'exemple documenté - empêchant les dépôts non autorisés. , etc.

Mon idée était d’écrire des fonctions d’extension qui me permettraient d’ajouter notre repo Nexus et nos plugins à une construction de la même manière qu’ils étaient construits dans gradle (similaire aux fonctions d’extension gradleScriptKotlin() et kotlin-dsl() fourni par le Gradle Kotlin DSL.

J'ai donc créé mes fonctions d'extension dans un projet Kotlin Gradle:

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"
}

Avec l'intention de les utiliser dans le build.gradle.kts de mon projet comme suit:

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

buildscript {

    repositories {
        corporateNexus()
    }

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

Cependant, Gradle était incapable de voir mes fonctions lorsqu'il était utilisé dans le bloc buildscript (impossible de compiler le script). Leur utilisation dans les dépôts/dépendances de projet normaux a bien fonctionné (ils sont visibles et fonctionnent comme prévu). 

Si cela fonctionnait, j'espérais intégrer le fichier jar dans ma distribution personnalisée, ce qui signifie que mon script init pourrait simplement effectuer une validation simple au lieu de masquer le plug-in magique et la configuration du référentiel. Les fonctions d’extension n’auraient pas besoin de changer, il n’aurait donc pas besoin de publier une nouvelle distribution Gradle lors du changement de plug-in.

Ce que j'ai essayé:

  • l'ajout de mon fichier jar au chemin de classe du script de construction du projet test (par exemple, buildscript.dependencies) - ne fonctionne pas (peut-être que cela ne fonctionne pas de par sa conception, car il ne semble pas correct d'ajouter une dépendance à buildscript à laquelle il est fait référence dans le même bloc)
  • mettre les fonctions dans buildSrc (qui fonctionne pour les dépôts/repos de projet normaux mais pas buildscript, mais ce n'est pas une vraie solution car cela ne fait que déplacer le passe-partout)
  • déposer le fichier jar dans le dossier lib de la distribution

Donc, ma question se résume vraiment à:

  • Est-ce que ce que j'essaie de réaliser est possible (est-il possible de rendre les classes/fonctions personnalisées visibles pour le bloc buildScript)? 
  • Existe-t-il une meilleure approche pour la configuration d’un dépôt Nexus d’entreprise et la création de plug-ins personnalisés (publiés dans Nexus) disponibles pour de nombreux projets distincts (c’est-à-dire des bases de code totalement différentes) avec une configuration minimale standard?
28
James Bassett

J'ai promis à @eskatos de revenir et de commenter sa réponse. Alors la voici!

Ma solution finale consiste à:

  • Gradle 4.7 wrapper par projet (pointé sur un miroir de http://services.gradle.org/distributions setup dans Nexus en tant que référentiel de proxy brut, c’est-à-dire Vanilla Gradle mais téléchargé via Nexus)
  • Modules Gradle personnalisés publiés dans notre dépôt Nexus avec marqueurs de plug-in (générés par le plug-in de développement du plug-in Java Gradle )
  • Mise en miroir du portail Gradle Plugin dans notre référentiel Nexus (c'est-à-dire un référentiel proxy pointant vers https://plugins.gradle.org/m2 )
  • Un fichier settings.gradle.kts par projet qui configure notre miroir de portail de plug-in Maven Repo et Gradle (tous deux dans Nexus) en tant que référentiels de gestion de plug-ins.

Le fichier settings.gradle.kts contient les éléments suivants:

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")
        }
    }
}

Cela signifie que tous les plugins et leurs dépendances sont désormais proxy via Nexus, et Gradle trouvera nos plugins par identifiant, car les marqueurs de plugin sont également publiés sur Nexus. La présence de mavenLocal facilite également le test local des modifications de plug-ins.

Le fichier racine build.gradle.kts de chaque projet applique ensuite les plugins comme suit:

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...
}

Et configure l'encapsuleur Gradle pour qu'il utilise notre distribution en miroir, ce qui, lorsqu'il est combiné à ce qui précède, signifie que tout (gradle, plugins, dépendances) vient tous via Nexus):

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

J'espérais éviter le passe-partout dans les fichiers de paramètres en utilisant la suggestion de @ eskatos d'appliquer un script à partir d'une URL distante dans settings.gradle.kts. c'est à dire.

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

J'ai même réussi à générer un script basé sur un modèle (publié à côté de nos plugins) qui:

  • configuré le dépôt du plugin (comme dans le script de paramétrage ci-dessus)
  • utilisé une stratégie de résolution pour appliquer la version des plugins associés au script si l'id de plugin demandé était l'un de nos plugins et que la version n'était pas fournie (vous pouvez donc simplement les appliquer par identifiant)

Cependant, même si cela supprimait le standard, cela signifiait que nos versions reposaient sur une connexion à notre référentiel Nexus, car il semble que même si les scripts appliqués à partir d'une URL sont mis en cache, Gradle envoie quand même une requête HEAD pour vérifier pour les changements. Cela rendait également agaçant de tester localement les modifications de plug-in, car je devais le pointer manuellement vers le script de mon répertoire maven local. Avec ma configuration actuelle, je peux simplement publier les plugins sur maven local et mettre à jour la version de mon projet.

Je suis assez satisfait de la configuration actuelle - je pense que la façon dont les plugins sont appliqués est bien plus évidente pour les développeurs. Et il est beaucoup plus facile de mettre à niveau indépendamment Gradle et nos plugins, maintenant qu’il n’ya aucune dépendance entre les deux (et qu’aucune distribution de gradation personnalisée n’est requise).

3
James Bassett

Si vous souhaitez bénéficier de tous les avantages de Gradle Kotlin DSL, vous devez vous efforcer d’appliquer tous les plugins à l’aide du bloc plugins {}. Voir https://github.com/gradle/kotlin-dsl/blob/master/doc/getting-started/Configuring-Plugins.md

Vous pouvez gérer les référentiels de plugins et les stratégies de résolution (par exemple, leur version) dans vos fichiers de paramètres. À partir de Gradle 4.4, ce fichier peut être écrit en utilisant le DSL de Kotlin, également appelé settings.gradle.kts. Voir https://docs.gradle.org/4.4-rc-1/release-notes.html .

En gardant cela à l’esprit, vous pourriez alors avoir un plugin de script Settings centralisé qui configure tout et l’applique dans vos fichiers settings.gradle.kts de builds:

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

et:

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

Ensuite, dans vos scripts de construction de projet, vous pouvez simplement demander des plugins à partir de votre référentiel d'entreprise:

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

Si vous voulez que vos scripts de construction de projet dans une construction multi-projet ne répète pas la version du plugin, vous pouvez le faire avec Gradle 4.3 en déclarant des versions dans votre projet racine. Notez que vous pouvez également définir les versions dans settings.gradle.kts à l'aide de pluginManagement.resolutionStrategy si toutes les versions utilisent la même version de plug-in, c'est ce dont vous avez besoin.

Notez également que pour que tout cela fonctionne, vos plugins doivent être publiés avec leur artefact de marqueur plugin . Ceci est facilement fait en utilisant le plugin Java-gradle-plugin.

10
eskatos

J'ai fait quelque chose comme ça dans ma construction

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

Dans mon fichier sharedValues.gradle.kts, j'ai un code comme celui-ci:

/**
 * 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

Je suis un modèle similaire pour configurer la stratégie de résolution pour les plugins.

Le bon côté de ce modèle est que tout ce que vous configurez dans sharedValues.gradle.kts peut également être utilisé à partir de votre projet buildSrc, ce qui signifie que vous pouvez réutiliser les déclarations du référentiel.


Mis à jour:

Vous pouvez appliquer un autre script à partir d'une URL, par exemple:

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")
}

Hébergez simplement votre script que vous souhaitez que toutes vos versions partagent sur un serveur http (il est fortement recommandé d'utiliser HTTPS afin que votre version ne puisse pas être ciblée par une attaque entre deux personnes).

L'inconvénient, c'est que je ne pense pas que les scripts appliqués à partir d'URL ne soient pas mis en cache, ils seront donc re-téléchargés chaque fois que vous exécuterez votre build. .

2

Une solution proposée par Stefan Oehme alors que je rencontrais un problème similaire consistait à vendre ma propre distribution personnalisée de Gradle. Selon lui, c'est une chose courante à faire dans les grandes entreprises.

Créez simplement une fourchette personnalisée du dépôt gradle, ajoutez la sauce spéciale de votre entreprise à chaque projet en utilisant cette version personnalisée de gradle.

1
Jonathan Leitschuh

J'ai rencontré un problème similaire lorsque la configuration commune est répliquée dans chaque projet. Résolu par une distribution de dégradé personnalisée avec les paramètres communs définis dans le script init. 

Création d'un plugin Gradle pour la préparation de telles distributions personnalisées - custom-gradle-dist . Cela fonctionne parfaitement pour mes projets, par exemple. un build.gradle pour un projet de bibliothèque ressemble à ceci (c'est un fichier complet): 

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