Gradle Build Tool

Pièges et bonnes pratiques

Paul Merlin (@eskat0s) - Gradle Inc.

Qui suis-je ?

speaker {
    name = "Paul Merlin"
    company = "Gradle Inc."
    locations = setOf("Montpellier, France", "Cevennes <3")
    oss  = "Apache Polygene PMC, former chair"
    successes = listOf(
        "BASIC 'Hello, World!' in 1986",
        "C 'Hello, World!' in 1989",
        "Java 'Hello, World!' in 1996"
        "Kotlin 'Hello, World!' in 2015",
        "tools", "daemons", "apps", "frameworks", "libs"
    ),
    failures = generateSequence(code) { bugs },
    twitter = "@eskat0s",
    github = "eskatos"
}

Qui êtes-vous ?

  • Qui construit pour la JVM ?

  • Qui construit pour les VM JavaScript ?

  • Qui construit du natif ?

  • Autre chose ? Quoi ?

  • Qui se sert uniquement de Gradle au boulot ?

  • Qui se sert uniquement de Maven au boulot ?

  • Qui se sert des deux au boulot ?

  • Qui se sert d’autres outils de build ? lesquels ?

  • Qui a déjà modifié/écrit un build Gradle ?

Agenda

  • Gradle, c’est quoi ?

  • Comment on s’en sert ?

  • Basique!

  • En pratique

  • Organiser la logique de build

  • Des builds performants

  • Maintenir et faire évoluer des builds

Gradle, c’est quoi ?

Gradle, c’est quoi ?

Gradle est un outil de construction et d’automatisation.

  • Gradle Build Tool

  • Tourne sur une JVM

  • Implémenté en Java

  • Apache License 2.0

Agnostique de l’écosystème

  • Écosystème JVM

    • Java, Kotlin, Groovy, Scala, Clojure …​

  • Écosystème natif

    • C, C++, Swift, …​

  • Android

  • Et bien d’autres

    • JavaScript, Python, Go, Rust, Asciidoctor, Docker …​

Gradle en quelques chiffres

  • >7.0M téléchargements par mois

  • #17 Projet Open Source

  • 35+ ingénieurs Gradle

  • 300K constructions par semaine @LinkedIn

Gradle Inc.

La compagnie derrière Gradle.

  • "Build Happiness"

  • Emploie des ingénieurs à plein temps

  • Produit aussi Gradle Enterprise

  • (Gradle consulting, support, service etc.)

  • (Training: online, public and in-house)

Gradle Enterprise

  • Produit commercial - Productivité des développeurs

  • Build Scans

    • enregistrement persistent et partageable

    • de ce qui s’est passé pendant un build

  • Build Cache

    • réutilisation des outputs de build

  • Installation sur site, cache distribué, historique des constructions, dashboards, export API etc…​

Gradle Enterprise

gradle enterprise

Build Scans gratuits en SaaS

Et le Build Cache ?

Gradle recrute!

  • Une équipe de développement totalement distribuée

  • Un projet intéressant utilisé par des millions

  • Des positions dans l’équipe Build Tool et Gradle Enterprise

Si ce qui suit est un problème intéressant à résoudre à vos yeux,

Comment se sert-on de Gradle ?

Comment se sert-on de Gradle ?

  • en ligne de commande

  • en intégration continue

  • depuis un IDE

  • via une API

gradle build

  • Gradle est avant tout un outil en ligne de commande

  • USAGE: gradle [option…​] [task…​]

  • Un client qui démarre et réutilise un démon

    • performance: JVM JIT & caches mémoire

  • Un wrapper enregistré dans le dépôt de sources

    • Version de l’outil de construction fixée

    • Pas besoin d’installation, seulement d’une JVM

    • git clone foo && cd foo && ./gradlew build

Intégration Continue

Les services d’intégration continue executent Gradle, tout simplement. Certains d’entre eux fournissent des fonctionnalités supplémentaires en consommant les résulats de construction:

  • sortie console,

  • résultats d’execution de tests (xUnit, coverage etc..),

  • URL du Build Scan,

  • etc …​

Quelques guides pour Jenkins, Travis, TeamCity.

IDE ⇒ Gradle

  • Certains IDEs supportent Gradle nativement

  • On importe un build Gradle directement dans l’IDE

    • IntelliJ IDEA, CLion, Eclipse, Netbeans

    • mais aussi les Language Servers (LSP)

  • L’IDE interroge Gradle pour obtenir le modèle du build

    • les sets de sources, leurs dépendances etc..

    • les tâches disponibles

    • la configuration de l’IDE

Gradle ⇒ IDE

Pour d’autres IDE qui ne fonctionnent qu’à partir de fichiers de configuration, Gradle peut générer ceux-ci.

  • IntelliJ IDEA et Eclipse (déprécié en faveur de l’import)

  • Visual Studio

  • XCode

Le build configure l’IDE

Dans tous les cas, l’objectif est de configurer l’IDE

  • depuis le build

  • depuis le dépôt de sources

  • pour tout le monde pareil

  • sans configuration manuelle

Tooling API

Il est également possible de piloter Gradle via une api, la Tooling API.

C’est ce que les IDE permettant l’import de builds Gradle utilisent.

Gradle, les bases

Gradle, les bases

  • Scripts de build en Kotlin et Groovy

  • En fait, Gradle c’est une API Java

  • Plus un DSL en Kotlin ou Groovy

  • Configurer et executer des tâches

  • Résoudre des dépendances

  • Éviter le répéter travail

gradle task dag

Gradle Plugins

  • Core Plugins (java, jacoco, maven-publish …​)

  • Community Plugins (kotlin, android, golang, docker, asciidoctor …​)

gradle task dag

Gradle Plugins

  • Des plugins Gradle contribuent

    • des tâches configurables et réutilisables

    • des extensions Gradle configurables

gradle task dag

Gradle Plugins

  • Des plugins Gradle contribuent un modèle à configurer

    • dans les scripts de build

    • en utilisant un DSL

gradle task dag

Une librairie Java

plugins {
   `java-library`
}

dependencies {
   api("com.acme:foo:1.0")
   implementation("com.zoo:monkey:1.1")
}

tasks.withType<JavaCompile> {
    // ...
}

Une application C++

plugins {
    `cpp-application`
}

application {
    baseName = "my-app"
}

toolChains {
    // ...
}

Éviter de répéter le travail

  • Unité de travail: une tâche

  • @Input*Task@Output*

  • UP_TO_DATE - Build Incrémental

    • Les inputs n’ont pas changé, les outputs sont présents et inchangés

  • FROM_CACHE - Build Cache

    • Les inputs n’ont pas changé, les outputs ont été rapatrié depuis le cache

En pratique

En pratique

  • Hello, Gradle World!

  • Un petit build

  • Un gros build

  • Plein de builds

Créer un nouveau build

gradle init

Un petit build

DEMO

Un petit build

  • Un seul projet

  • Les plugins contribuent un modèle qui se configure via le DSL

  • Configuration vs. Execution

Configuration vs. Execution

itsatrap

Cycle de vie d’un build

  • Démarrage

  • Initialisation

  • Settings

  • Configuration des Projets

  • Execution

Un gros build

DEMO

Un gros build

  • Multi-projets

    • 3 dans notre exemple

    • 10 à 100, raisonnable et fréquent

    • 500 et plus, moins fréquent mais ça existe

  • Hiérachie de projets

    • Configuration

    • Configurer les sous-projets

Plein de builds

Plein de builds

  • Séparer un gros build en plusieurs petits

  • Différentes équipes, cycle de livraison différent etc…​

  • Mono-repo vs. multi-repo

  • Mono-build vs. multi-builds

Plein de builds

DEMO

Plein de builds

  • Settings

  • Composite Builds - Included Builds

    • Fini les -SNAPSHOTS !

    • Utiles aussi pour travailler sur des librairies externes

    • Augmente/Limite le scope disponible dans l’IDE

  • Source Dependencies

    • Dépendre d’un dépôt git distant

    • Librairie ou fix non publié

    • Sources non modifiables

En pratique

  • Pas de logique de build commune

  • Pas de réutilisation

  • Comment organiser sa logique de build ?

Organiser la logique de build

Organiser la logique de build

Pourquoi ?
  • Beaucoup de logique de build dans les scripts

    • Maintenabilité :-(

    • Réutilisation :-(

  • Extraire des conventions

    • Maintenabilité :-)

    • Réutilisation :-)

Organiser la logique de build

Comment ?
  • En extrayant du code pour qu’il soit réutilisable

  • Refactoring typique

  • Extraire vers des plugins Gradle réutilisables

  • et configurables si besoin

Cycle de vie d’un build

Rappel
  • Démarrage

  • Initialisation (& plugins)

  • Settings (& plugins)

  • Configuration des Projets (& plugins)

  • Execution

Organiser un build

Avec buildSrc
  • Chaque build a, par convention, un build inclu pour sa logique de build

  • ./buildSrc

  • C’est un build comme un autre, on peut y coder en Kotlin, Groovy, Java etc..

  • En utilisant l’API Gradle ou son DSL

  • Ce que produit ce build est disponible pour Settings et tous les Projets

Organiser un build avec buildSrc

DEMO

Organiser un build avec buildSrc

  • Tous les scripts de projets deviennent declaratifs

  • On peut tester la logique de build

  • Même mechanismes que le développement de plugins Gradle

  • Ou de n’importe quel code sur la JVM, même outillage

Organiser plusieurs builds

  • Partager de la logique de build avec plusieurs builds

  • Publier un plugin Gradle

  • Utiliser les composite builds

  • Combiner les deux si besoin de publier

Composite build logic

DEMO

Composite build logic

  • Composite builds avec un peu de cérémonie

  • Très similaire à buildSrc

  • N-Repositories

  • Logique de build testée

  • Toujours les mêmes mechanismes et outillage

  • Rien de nouveau sous le soleil

On n’a pas parlé des tâches ?

Pas encore …​

Interlude

Pfiouuuuu

Pfiouuuuu

Mais, j’ai déjà assez à faire avec le code de production!

C’est sans compter les tests unitaires, d’intégration, fonctionnels, bariolés …​

OUI …​ MAIS

  • Qui doit s’occuper du build ?

  • Qui se sert du build ?

    • Tout le monde

    • @dev, @ide, @ci, @qa, @ops etc…​

    • Le produit du build c’est ce qui va en prod

    • @users

Pfiouuuuuu

Qui doit s’occuper du build ?

  • Projet solo

    • C’est bibi !

  • Projet en équipe(s)

    • Tout le monde, éventuellement un build-master/team

  • Grandes organisations

    • Idéalement un build-master ou une build-team, pour l’uniformité et la réutilisation

  • YMMV - Mais il faut s’en occuper, ça n’a pas à être pénible

Pfiouuuuuu

Et puis …​

  • Je peux automatiser n’importe quoi

    • Générer des trucs et des machins

    • Les envoyer dans les nuages

    • Interagir avec des APIs

    • Déployer sur les ordinateurs d’un autre (aka. le Cloug™)

  • C’est un bon moyen de réduire la fracture entre les Devs des Ops (hein?)

Pfiouuuuuu

Et puis …​

  • Je peux automatiser n’importe quoi

    • Générer des trucs et des machins

    • Les envoyer dans les nuages

    • Interagir avec des APIs

    • Déployer sur les ordinateurs d’un autre (aka. le Cloug™)

  • Le build c’est aussi un bon moyen de rapprocher les Devs des Ops (hein?)

Pfiouuuuuu

Dans tous les cas …​

  • Ne pas sur-concevoir

  • KISS - C’est pas de la rocket-science non plus

  • Ou plutôt: Keep It As Simple As Possible

Pfiouuuuuu

  • Le build c’est un peu comme les tests

  • S’en occuper, c’est rendre service à son futur soi/utilisateur

  • Si le build est bien fait et testé

    • Je peux le faire évoluer facilement

    • Je ne le casse pas sans m’en rendre compte

  • Si le build est maintenu performant, je ne perds pas de temps

Des builds performants

Des builds performants

compiling

Tout le monde veut un build qui va vite™

Des builds performants

  • Éviter de répéter le travail

  • Faire le travail plus vite

Éviter de répéter le travail

  • Unité de travail: une tâche

  • @Input*Task@Output*

  • UP_TO_DATE - Build Incrémental

    • Les inputs n’ont pas changé, les outputs sont présents et inchangés

  • FROM_CACHE - Build Cache

    • Les inputs n’ont pas changé, les outputs ont été rapatrié depuis le cache

Build incrémental

Si rien n’a changé, aucune tâche ne devrait être executée

  • Conçu pour le développement en local

  • Comparaison des inputs

  • Comparaison des outputs

  • Évite d’executer une tâche

Build incrémental

abstract class CustomTask : DefaultTask() {

    @get:InputDirectory
    abstract val sourceDirectory: DirectoryProperty

    @get:OutputFile
    abstract val outputFile: FileProperty

    // ...
}

Build incrémental

  • Limitations

    • Fonctionne localement uniquement

    • Optimisé pour des changements incrémentaux

  • Pièges

    • Inputs volatiles (identifiants uniques, timestamps, ordre non stable)

Inputs volatiles

tasks.jar {
    manifest {
        attribute("Build-ID", UUID.randomUUID().toString())
    }
}

Les identifiants uniques ou timestamps sont à proscrire pour des builds reproductibles et performants!

Build Cache

  • est-un stockage semi-permanent

  • activé avec --build-cache

  • repose sur le build incrémental

  • stocke les outputs des tâches

    • l'addresse c’est les inputs de la tâche

    • le contenu c’est les outputs de la tâche

    • avec quelques astuces/complications

Build cache local

@CacheableTask
abstract class CustomTask : DefaultTask() {
    // ...
}
  • Est utile pour

    • travailler sur des branches

    • git bisect

    • clean accidentel :-)

Build cache partagé (remote)

  • Est utile

    • en intégration continue pour réutiliser les outputs

      • entre les changesets, entre les agents CI, entre les jobs CI

    • En developpement

      • pas besoin de reconstruire les changements des autres

  • Meilleure stratégie

    • seule la CI pousse les outputs dans le cache partagé

Build cache - Les pièges

  • Inputs volatiles (identifiants uniques, timestamps, ordre non stable)

  • Chemins absolus - "Relocatability"

  • Différence de plateformes - Line separators

abstract class CustomTask : DefaultTask() {

    @get:InputDirectory
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val sourceDirectory: DirectoryProperty
    // ...
}

Build cache

Des tâches performantes

DEMO

Des tâches performantes

  • La clé c’est les inputs et outputs

  • Déclarer les bonnes meta-données

  • Les tests de tâches aident beaucoup à s’assurer que ça fonctionne

Optimiser un build

Avant tout

  • Utilisez les dernières version de JVM et de Gradle

  • Donnez assez de mémoire à Gradle

  • Le tuning de JVM et ses flags obscurs font souvent plus de mal qu’autre chose

  • Prenez plutôt le temps de faire des améliorations structurelles

  • Activez --parallel (org.gradle.parallel=true)

  • Activez --build-cache (org.gradle.caching=true)

Ne pas optimiser à l’aveugle

  • Identifiez vos cas d’utilisation

  • Automatiser vos mesures

  • Identifiez le goulet d’étranglement principal

  • Fixez le goulet

  • Vérifiez le fix en mesurant de nouveau

  • Répéter

Automatisez vos mesures

configurationTime {
    tasks = ["help"]
}
cleanBuild {
    tasks = ["build"]
    cleanup-tasks = ["clean"]
}
cachedCleanBuild {
    tasks = ["build"]
    cleanup-tasks = ["clean"]
    gradle-args = ["--build-cache"]
}

Où est le problème ?

  • Commencez par observer un build

    • gradle --scan ou gradle --profile

lifecycle

Signes évidents de problèmes

  • Startup/buildSrc/Settings > 1s

  • Temps de configuration > 10ms/projet

  • Changer une ligne de code ~= clean build

  • Un build censé être un NO-OP qui fait quelque chose

  • Long temps de GC (Garbage Collection)

Optimiser la configuration

  • Quoi ?

    • Application les plugins

    • Evaluation les scripts de build

    • Execution les callbacks (e.g. afterEvaluate {})

  • Quand ?

    • gradle help ou gradle tasks

    • Synchronisation d’un IDE

    • à chaque invocation, c’est un coût fixe!

Optimiser la configuration

Principales causes de lenteur

  • Résolution de dépendances à la configuration

  • I/O à la configuration

  • Plugins inefficaces

  • Logique répetée

Résolution de dépendances à la configuration

depres config time

Résolution de dépendances à la configuration

tasks.register<Jar>("uberJar") {
    from(sourceSets["main"].output)
    from(configurations["runtime"].map { it.isDirectory ? it : zipTree(it) })
    classifier = "uber-jar"
}

configurations["runtime"].map provoque la résolution de dépendances

Résolution de dépendances à la configuration

tasks.register<Jar>("uberJar") {
    from(sourceSets["main"].output)
    from({ configurations["runtime"].map { it.isDirectory ? it : zipTree(it) } })
    classifier = "uber-jar"
}

Préférez les évaluations paresseuses

I/O à la configuration

tasks.register("projectStats") {
    val statsFile = file("$buildDir/stats.txt")
    statsFile.parentFile.mkdirs()
    statsFile.writeText("Source files: ${sourceSets["main"].java.size()}")
}

Attention en écrivant des tâches !

I/O à la configuration

io config time

Ce script à l’air couteux

I/O à la configuration

tasks.register("projectStats") {
    val statsFile = file("$buildDir/stats.txt")
    inputs.files(sourceSets["main"].java)
    outputs.file(statsFile)
    doLast {
        statsFile.parentFile.mkdirs()
        statsFile.writeText("Source files: ${sourceSets["main"].java.size()}")
    }
}

Ne pas oublier doLast {}

I/O à la configuration

abstract class ProjectStats : DefaultTask() {

    @get:InputFiles
    abstract val sources: ConfigurableFileCollection

    @get:OutputFile
    abstract val statsFile: FileProperty

    @TaskAction
    fun stats() = statsFile.get().asFile.apply {
        parentFile.mkdirs()
        writeText("Source files: ${sources.size()}")
    }
}

tasks.register<ProjectStats>("projectStats") {
    sources.from(sourceSets["main"].java)
    statsFile.set(file("$buildDir/stats.txt))
}

Plugins inefficaces

inefficient plugins

Plugins inefficaces

  • Sur tous les projets ?

  • Est-il possible de réutiliser le travail ?

  • Exemple: lire la version depuis git rev-parse HEAD

Plugins inefficaces

subprojects {
    apply(plugin = "set-version-from-git")
}

Plugins inefficaces

plugins {
    id("set-version-from-git")
}
subprojects {
    version = rootProject.version
}

Optimiser la configuration

Optimisez les algorithmes

flames

gradle-profiler --profile async-profiler

Optimiser l’execution

  • Execution des tâches

    • Build Incrémental

    • Build Cache

Execution parallèle

Éxecution sérielle

perf serial

Éxecution parallèle

perf parallel

Compilation plus rapide

  • Modularisation

    • ⇒ Compilation avoidance on non-abi change

    • ⇒ Parallélisation

  • Code découplé

    • ⇒ Compilation incrémentale plus efficace

  • Processeurs d’annotations

    • ⇒ Vérifiez bien qu’ils soient incrémentaux

Résolution de dépendances

  • Dynamic versions & locking

  • Repositories

    • As few as possible

    • Filtering repositories content

    • no mavenLocal()

Surveiller la performance du build

  • Pour qu’elle ne regresse pas

  • L’intégration continue peut alerter, voir fournir des tabeaux de bords

  • Gradle Enterprise fourni des solutions dédiées à la gestion de la performance du build

perf dashboard

Maintenir et faire évoluer des builds

Maintenir et faire évoluer des builds

  • C’est du code comme un autre

  • Il faut mettre à jour les dépendances

  • Il faut le tester

  • Il faut le refactorer petit à petit pour qu’il soit meilleur

En résumé

  • le build ce n’est pas sale

  • le build ce n’est pas sale

  • le build ce n’est pas sale

En résumé

  • le build c’est de l’automatisation

  • et l’automatisation c’est la vie (de développeur)

  • tout comme pour le code de production, il faut

    • connaître ses outils

    • tester

    • maintenir

  • Gradle c’est bien™

Questions