31 votes

Configuration du projet Boilerplate dans Gradle avec Gradle Kotlin DSL

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

Mes principaux objectifs sont :

  • Pour ne pas avoir la configuration de mon dépôt Nexus dupliquée dans chaque projet (je peux également supposer que l'URL ne changera pas).
  • Mettre mes plugins Gradle personnalisés (publiés sur Nexus) à la disposition de tous les projets avec un minimum d'erreurs et de duplications (ils devraient être disponibles pour tous les projets, et la seule chose dont le projet se soucie est la version qu'il utilise).
  • Pas de magie - la façon dont tout est configuré doit être évidente pour les développeurs.

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

  • ajoute mavenLocal() et notre dépôt Nexus aux dépôts du projet (très similaire à la méthode Gradle init script exemple de documentation (sauf qu'il ajoute les dépôts et les valide).
  • configure une extension qui permet à nos plugins gradle d'être ajoutés au classpath de buildscript (à l'aide de cette solution de contournement ). Il ajoute également notre repo Nexus comme repo buildscript car c'est là que les plugins sont hébergés. Nous avons un certain nombre de plugins (construits à partir de l'excellent outil de Netflix, le plugins nebula ) pour diverses tâches : configuration standard du projet (configuration de Kotlin, configuration des tests, etc.), libération, publication, documentation, etc. build.gradle sont essentiellement destinés aux dépendances.

Voici l'init script (sanitisé) :

/**
 * 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 ce qui suit au fichier build.gradle de la racine :

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, permette des constructions reproductibles (contrairement à notre configuration précédente qui appliquait une construction script à partir d'une URL - qui, à l'époque, n'était pas cachable), et permette de travailler hors ligne, elle rend les choses un peu magiques et je me demandais si je pouvais faire mieux.

Tout cela a été déclenché par la lecture un commentaire sur Github par Stefan Oehme, développeur Gradle, indiquant qu'une construction devrait fonctionner sans dépendre d'un init script, c'est-à-dire que les init script devraient juste être décoratifs et faire des choses comme l'exemple documenté - empêcher les dépôts non autorisés, etc.

Mon idée était d'écrire quelques fonctions d'extension qui me permettraient d'ajouter notre repo Nexus et nos plugins à une construction d'une manière qui ressemblerait à ce qu'ils ont été intégrés dans gradle (similaire aux fonctions d'extension gradleScriptKotlin() y kotlin-dsl() fourni par le DSL Kotlin de Gradle.

J'ai donc créé mes fonctions d'extension dans un projet gradle kotlin :

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 mon projet. build.gradle.kts comme suit :

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

buildscript {

    repositories {
        corporateNexus()
    }

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

Cependant, Gradle n'a pas pu voir mes fonctions lorsqu'elles ont été utilisées dans l'application buildscript bloc (impossible de compiler script). Les utiliser dans les dépôts/dépendances normaux du projet a pourtant bien fonctionné (ils sont visibles et fonctionnent comme prévu).

Si cela fonctionne, j'espérais regrouper le jar dans ma distribution personnalisée, ce qui signifie que mon init script pourrait simplement faire une validation simple au lieu de cacher le plugin magique et la configuration du repo. Les fonctions d'extension n'auraient pas besoin de changer, donc il ne serait pas nécessaire de publier une nouvelle distribution Gradle lorsque les plugins changent.

Ce que j'ai essayé :

  • ajouter mon jar au classpath du buildscript du projet de test (i.e. buildscript.dependencies ) - ne fonctionne pas (peut-être que cela ne fonctionne pas à dessein car il ne semble pas correct d'ajouter une dépendance à buildscript à laquelle il est fait référence dans le même bloc)
  • en mettant les fonctions dans buildSrc (ce qui fonctionne pour les dépôts/récupérations normaux du projet mais pas pour les buildscript mais ce n'est pas une vraie solution car elle ne fait que déplacer le boilerplate).
  • en laissant tomber le bocal dans le lib du dossier de la distribution

Donc ma question se résume vraiment à :

  • Est-ce que ce que j'essaie de faire est possible (est-il possible de rendre les classes/fonctions personnalisées visibles pour l'utilisateur ? buildScript ) ?
  • Existe-t-il une meilleure approche pour configurer un repo Nexus d'entreprise et rendre les plugins personnalisés (publiés dans Nexus) disponibles dans de nombreux projets distincts (c'est-à-dire des bases de code totalement différentes) avec un minimum de configuration passe-partout ?

0 votes

Je pense que pour résumer ce que vous essayez de faire, vous voulez ajouter des extensions à l'application buildscript Blocage ? Avez-vous une limite inférieure de la version de Gradle que vous utilisez ?

0 votes

@mkobit non, je suis en train de passer à la version 4.1. Je suppose que j'essaie vraiment d'améliorer la façon dont nos constructions configurent nexus, et de rendre nos plugins disponibles pour le projet. La solution actuelle (comme documenté au début de cette question) fonctionne, mais la configuration des plugins en particulier se sent comme un hack !

0 votes

Merci de clarifier. Il y a peut-être quelques moyens qui peuvent fonctionner ou non ou améliorer ce que vous avez déjà fait. L'une d'elles serait d'écrire un init script plugin que vous pouvez appliquer dans le settings.gradle . Dans l'actuel 4.2-rc il existe également un soutien pour Les plugins script sont mis en cache et ne sont téléchargés que lorsque cela est nécessaire, au lieu de l'être à chaque build. qui pourrait améliorer certains problèmes que vous rencontrez. Une autre idée pourrait être de fournir un portail de plugin personnalisé comme ( github.com/linkedin/custom-gradle-plugin-portal ).

13voto

eskatos Points 1010

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

Vous pouvez gérer les dépôts 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 Kotlin, alias settings.gradle.kts . Voir https://docs.gradle.org/4.4-rc-1/release-notes.html .

Dans cette optique, vous pourriez alors avoir un système centralisé Settings script plugin qui met les choses en place et l'appliquer dans vos builds. settings.gradle.kts des fichiers :

// 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 votre projet de construction scripts, vous pouvez simplement demander des plugins à partir de votre dépôt d'entreprise :

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

Si vous voulez que votre projet scripts dans un build multi-projets ne répète pas la version du plugin, vous pouvez le faire avec Gradle 4.3 en déclarant les versions dans votre projet Root. Notez que vous pouvez également définir les versions dans settings.gradle.kts en utilisant pluginManagement.resolutionStrategy si vous avez besoin que toutes les constructions utilisent la même version de plugins.

Notez également que pour que tout ceci fonctionne, vos plugins doivent être publiés avec leur plugin marqueur artefact . Cela se fait facilement en utilisant l'option java-gradle-plugin plugin.

0 votes

Je n'ai pas encore eu le temps de l'essayer, mais la prime est lancée et je pense que c'est la réponse la plus conforme à l'esprit de ma question. Profitez de vos 500 points :)

0 votes

:) Veuillez revenir avec des commentaires une fois que vous aurez eu l'occasion de mettre en œuvre ce système.

1 votes

Bonjour Paul, j'ai enfin eu le temps de revenir et de poster mes résultats - voir la réponse ci-dessous. Fondamentalement, j'ai suivi vos suggestions, moins l'application à partir d'une URL (car cela rendait les tests de mise à jour du plugin plus difficiles, et introduisait une dépendance sur une connexion à Nexus, car c'est là que j'ai hébergé le script).

5voto

Hound Dog Points 3240

J'ai promis à @eskatos de revenir et de donner mon avis sur sa réponse - la voici donc !

Ma solution finale consiste à :

  • wrapper Gradle 4.7 par projet (pointé sur un miroir de http://services.gradle.org/distributions configuré dans Nexus comme un dépôt proxy brut, c'est-à-dire qu'il s'agit de Gradle vanille mais téléchargé via Nexus)
  • Les plugins Gradle personnalisés publiés dans notre repo Nexus avec les marqueurs de plugins (générés par l'application Java Gradle Plugin Développement Plugin )
  • Miroir du portail de plugins Gradle dans notre repo Nexus (c'est-à-dire un repo proxy pointant sur https://plugins.gradle.org/m2 )
  • A settings.gradle.kts par projet qui configure notre repo maven et le miroir du portail de plugins gradle (tous deux dans Nexus) comme dépôts de gestion des plugins.

Le site 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 maintenant proxiés via Nexus, et Gradle trouvera nos plugins par id puisque les marqueurs de plugins sont également publiés sur Nexus. En ayant mavenLocal dans cet espace facilite le test des modifications apportées au plugin en local.

Racine de chaque projet build.gradle.kts 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 le wrapper gradle pour utiliser notre distribution miroir, ce qui, combiné avec ce qui précède, signifie que tout (gradle, plugins, dépendances) passe par Nexus) :

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

J'espérais éviter le texte passe-partout dans les fichiers de paramètres en utilisant la suggestion de @eskatos d'appliquer un script à partir d'une URL distante en 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 templé (publié aux côtés de nos plugins) qui :

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

Cependant, même si cela a supprimé le boilerplate, cela signifiait que nos constructions dépendaient d'une connexion à notre repo Nexus, car il semble que même si les scripts appliqués à partir d'une URL sont mis en cache, Gradle fait une requête HEAD de toute façon pour vérifier les changements. Cela rendait également ennuyeux de tester les changements de plugins localement, car je devais le pointer manuellement sur le scripts dans mon répertoire maven local. Avec ma configuration actuelle, je peux simplement publier les plugins dans maven local et mettre à jour la version dans mon projet.

Je suis assez satisfait de la configuration actuelle - je pense qu'il est beaucoup plus évident pour les développeurs maintenant comment les plugins sont appliqués. Et il est beaucoup plus facile de mettre à jour Gradle et nos plugins de manière indépendante, maintenant qu'il n'y a plus de dépendance entre les deux (et qu'il n'y a plus besoin d'une distribution gradle personnalisée).

2voto

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 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 schéma similaire pour configurer la stratégie de résolution des plugins.

Ce qui est bien avec ce modèle, c'est que tout ce que vous configurez dans la section sharedValues.gradle.kts peut également être utilisé à partir de votre buildSrc ce qui signifie que vous pouvez réutiliser les déclarations de référentiel.


Mis à jour :

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

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 voulez que tous vos builds partagent sur un serveur http (je recommande fortement d'utiliser HTTPS pour que votre build ne puisse pas être ciblé par une attaque de type man in the middle).

L'inconvénient de ceci est que je ne pense pas que les scripts appliqués à partir d'urls ne sont pas mis en cache, ils seront donc retéléchargés à chaque fois que vous exécutez votre build. Cela a peut-être été corrigé depuis, je ne suis pas certain.

0 votes

Hmm c'est une approche similaire à l'approche de la solution de contournement que j'utilise actuellement - sauf que mon boilerplate est défini une fois dans l'initscript (à l'intérieur de ma distribution personnalisée) au lieu de l'être dans le fichier buildSrc de chaque base de code.

0 votes

Bonjour Jonathon, j'ai mis à jour ma question. J'espère que cela rendra plus évident le fait que nous avons tous deux une approche très similaire - sauf que vous configurez les choses à partir de votre site Web. sharedValues script via extra (qui pourrait être hébergé à distance et mis en cache dans les nouvelles versions de Gradle), et je configure les choses à partir d'un plugin init script via une extension. J'espère trouver une meilleure solution - je ne suis satisfait d'aucune de nos solutions (désolé !).

1voto

Une solution qui m'a été proposée par Stefan Oehme lorsque j'avais un problème similaire était de vendre ma propre distribution personnalisée de Gradle. Selon lui, il s'agit d'une pratique courante dans les grandes entreprises.

Il suffit de créer un fork personnalisé du repo gradle, d'ajouter la sauce spéciale de votre entreprise à chaque projet utilisant cette version personnalisée de gradle.

1 votes

Pour moi, c'est le dernier recours. Cela fonctionnera certainement, et comme nous en avons discuté sur Slack, c'est probablement la façon dont les grandes entreprises comme Netflix résolvent ce problème, mais la surcharge de maintenance est un point massif contre cette approche pour moi (avoir à maintenir constamment un fork d'un repo si actif). Avec mon approche actuelle, je dois simplement changer de version à chaque fois que Gradle est mis à jour et que ma nouvelle dist personnalisée est publiée sur Nexus - je n'ai pas à fusionner et je n'ai pas à construire à partir des sources.

0voto

denis.zhdanov Points 1669

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

Création d'un plugin gradle pour la préparation de ces distributions personnalisées. berceau personnalisé-dist . Il fonctionne parfaitement pour mes projets, par exemple une build.gradle pour un projet de bibliothèque ressemble à ceci (il s'agit d'un fichier complet) :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X