Dev-Mind

Publier une librairie open source sur Maven Central

22/01/2018
Java  Gradle  OSS 

Je n’avais encore jamais publié une librairie sous Maven Central, afin de la rendre accessible à tous. Il existe plusieurs manières de faire. J’ai choisi le mode classique, préconisé par SonaType, la société qui gère Maven Central et les produits Nexus.

Tout mes exemples sont définis avec Gradle et le code source présenté dans cet article est disponible sous Github.

Utiliser la méthode Sonatype

Les différentes versions des librairies open source Java sont déployées sous OSSRH (OSS Repository Hosting). OSSRH utilise Nexus Repository Manager pour gérer les librairies. La plateforme gère tout le cycle de vie des versions d’une librairie

  • les versions de développements (snapshots) peuvent être déployées

  • les versions tagguées sont poussées dans un dépôt staging (recette)

  • vous pouvez ensuite indiquer qu’une version de recette est releasée. La plateforme lance à ce moment plusieurs contrôles de qualité et pousse les binaires sur le dépôt central (Maven Central).

La documentation officielle est assez complète et vous la trouverez ici. Mais comme toute documentation il y a souvent un décalage entre le moment où elle a été décrite et le moment où vous l’appliquez. Sur une version récente de Linux et en utilisant les dernières versions de Gradle vous avez plusieurs points à savoir.

Commençons par le début. La documentation vous demande

  • de créer un compte sur le Jira de SonaType. Les identifiants de ce compte seront les mêmes que vous utiliserez pour pousser vos artefacts

  • d’ouvrir une issue dans laquelle vous demandez la création d’un nouveau projet. Les équipes Nexus vont faire un check manuel de votre demande. Ils sont assez réactifs

Ticket pour la création d’un projet

Même si nous allons utiliser Gradle, nous allons construire un descripteur de projet Maven (pom.xml), car Maven Central contenait à la base que des ressources pour les projes Maven.

Une fois que les équipes SonaType ont validé votre projet, vous pouvez envoyer votre librairie sur leurs serveurs. Mais différents checks seront faits afin d’assurer une qualité minimale des librairies

  • vous devez définir les metadata du projet dans un descripteur pom.xml avec les identifiants (artifactId, version, groupId), le type de packaging, les dépendances transitives et optionelles

  • en plus de votre artefact vous devez envoyer les sources et la javadoc

  • tous les artefacts doivent être signés avec GPG/PGP

Paramétrer Gradle

Regardons comment faire celà avec Gradle. Vous devez importer les plugins suivants pour votre projet Java

apply plugin: 'java'
apply plugin: 'signing'
apply plugin: 'maven'

Le plugin signing va être utilisé pour signer les artefacts.

Le plugin maven permet de générer un descripteur de projet Maven (pom.xml) et de publier vos artefacts sur Maven Central

Vous pouvez définir les metadata de votre projet…​

group = 'fr.dev-mind'
archivesBaseName = "mockwebserver"
version = rootProject.version
sourceCompatibility = 1.8

puis les tâches pour générer les différents artefacts : jar, sources et javadocs. Les artefacts peuvent contenir un fichier Manifest avec les infos essentielles de votre projet

ext.sharedManifest = manifest {
    attributes(
            "Implementation-Title": project.name,
            "Implementation-Version": version,
            "Implementation-Vendor": project.group,
            "Bundle-Vendor": project.group
    )
}
 
task sourcesJar(type: Jar) {
    from sourceSets.main.allJava
    manifest {
        from sharedManifest
    }
}
 
task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc
    manifest {
        from sharedManifest
    }
}
 
javadoc {
    source = sourceSets.main.allJava
    classpath = configurations.compile
    options.linkSource true
    options.addBooleanOption('Xdoclint:all,-missing', true)
}
 
jar {
    manifest {
        from sharedManifest
    }
}
 
artifacts {
    archives javadocJar, sourcesJar
}

Vous devez ensuite paramétrer la signature des artefacts. Dans l’exemple ci dessous, ceci n’est fait que lorsque la tâche uploadArchives est lancée (tâche permettant de publier vos librairies).

signing {
    required { gradle.taskGraph.hasTask("uploadArchives") }
    sign configurations.archives
}

Il ne reste plus qu’à paramétrer cette tâche uploadArchives avec les informations que l’on veut voir dans le pom.xml et les dépôts que vous allez utiliser

uploadArchives {
    repositories {
        mavenDeployer {
            beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
 
            repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
                authentication(userName: ossrhUsername, password: ossrhPassword)
            }
 
            snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
                authentication(userName: ossrhUsername, password: ossrhPassword)
            }
 
            pom.project {
                name project.name
                packaging 'jar'
                description "A scriptable web server for testing HTTP clients"
                url 'https://github.com/Dev-Mind/mockwebserver'
 
                scm {
                    connection 'scm:git:git://github.com/Dev-Mind/mockwebserver'
                    developerConnection 'scm:git:git://github.com/Dev-Mind/mockwebserver'
                    url 'https://github.com/Dev-Mind/mockwebserver'
                }
 
                licenses {
                    license {
                        name 'The Apache License, Version 2.0'
                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
 
                developers {
                    developer {
                        id 'javamind'
                        name 'Guillaume EHRET'
                        email 'guillaume@dev-mind.fr'
                    }
                }
            }
        }
    }
}

Erreur avec gpg2

J’ai suivi la documentation pour tout d’abord générer une clé via

$ gpg2 --key-gen

Lorsque vous générez votre clé vous devez spécifier une passphrase. Personnellement j’ai du saisir des passphrase sans espace pour ne pas avoir de problème par la suite quand j’avais à resaisir ces informations.

J’utilise une version récente de Linux qui utilise une version 2.1.15

$ gpg2 --version

Plusieurs choses ont été améliorées dans les versions > 2.1 de gpg. Si vous affichez les clés créées, la taille de ces clés a été augmentée et la nouvelle taille n’est pas encore supportée par le plugin Gradle. Le plugin se base sur les librairies Java org.bouncycastle et il faut qu’ils migrent vers les dernières versions de cette librairie

$ gpg2 --list-secret-keys
 
/home/devmind/.gnupg/pubring.kbx
------------------------------
pub   rsa2048 2018-01-13 [SC]
      6933FACC1931DD8A89CED163582D3134
uid         [ultimate] Guillaume EHRET <guillaume@dev-mind.fr>
sub   rsa2048 2018-01-13 [E]</guillaume@dev-mind.fr>

Pour retrouver un format de clé court utilisez la commande suivante

$ gpg2 --list-secret-keys --keyid-format short
 
/home/devmind/.gnupg/pubring.kbx
--------------------------------
pub   rsa2048/C6EED57A 2018-01-13 [SC]
uid         [ultimate] Guillaume EHRET <guillaume@dev-mind.fr>
sub   rsa2048/7DY5B54F 2018-01-13 [E]</guillaume@dev-mind.fr>

Vous pouvez maintenant publier votre clé publique sur un serveur de clé

$ gpg2 --keyserver hkp://pool.sks-keyservers.net --send-keys C6EED57A

Vous pouvez reporter ces informations dans le fichier gradle.properties global (elles ne doivent pas être envoyées dans votre dépôt de sources public). Ce fichier devra également contenir les identifiants que vous avez utiliser sur Sonatype

signing.keyId=C6EED57A
signing.password=CeciEstMonpassword
signing.secretKeyRingFile=/home/devmind/.gnupg/secring.gpg
 
ossrhUsername=devmind
ossrhPassword=CeciEstMonpassword

Après avoir fait cette action mon build Gradle ne fonctionnait toujours pas et retournait l’erreur suivante

* What went wrong:
Execution failed for task ':signArchives'.
> Unable to read secret key from file: /home/devmind/.gnupg/secring.gpg (it may not be a PGP secret key ring)

Le stockage des clés à changé. Il ne se fait plus dans un fichier secring.gpg mais sous forme de sous-répertoires dans le répertoire .gnupg. Heureusement il est encore possible de générer ce fichier pour assurer la rétrocompatibilité.

$ gpg2 --export-secret-keys > ~/.gnupg/secring.gpg

Un ticket a été ouvert pour modifier le plugin signin de Gradle et une solution a été apportée à partir de Gradle 4.5.

A partir de Gradle 4.5

Une autre solution a été mise en place dans la dernière version de Gradle, la version 4.5. Le plugin signing utilise une implémentation Java pour gérer les signatures va GPG. Cette implémentation ne peut pas utiliser gpg-agent pour gérer les clés privées. Avec Gradle 4.5 vous pouvez maintenant utiliser cet agent en utilisant useGpgCmd() (GnuPG doit bien évidemment être installé).

signing {
    required { gradle.taskGraph.hasTask("uploadArchives") }
    useGpgCmd()
    sign configurations.archives
}

Vous devez toujours générer votre clé et l’enregistrer sur le serveur de clé. Vous n’avez plus besoin par contre de générer un fichier pour assurer la rétrocompatibilité. Sans autre configuration, le plugin `signing` trouvera `gpg2` dans le path et vous demandera de saisir la passphrase via une boite de dialogue

Pour automatiser le tout vous pouvez ajouter la configuration suivante dans votre build.gradle global

signing.gnupg.executable=gpg
signing.gnupg.useLegacyGpg=false
signing.gnupg.keyName=C6EED57A
signing.gnupg.passphrase=CeciEstMonpassword

ossrhUsername=devmind
ossrhPassword=CeciEstMonpassword

Pour plus d’informations vous pouvez lire la documentation.

Publier sous Sonatype

Une fois que les problèmes vus au paragraphe précédent ont été réglés, vous pouvez publier vos artefacts chez Sonatype.

Les versions suffixées par -SNAPSHOT sont envoyées vers https://oss.sonatype.org/content/repositories/snapshots/

Les versions tagguées (sans -SNAPSHOT) sont envoyées vers https://oss.sonatype.org/service/local/staging/deploy/maven2/

Par contre les versions tagguées ne sont pas encore disponible de tous à cette étape. Comme nous l’avons vu au début de l’article les librairies publiées passe d’abord par une phase de recette (staging).

Vous devez lancer le Nexus de Sonatype : https://oss.sonatype.org/#stagingRepositories et sélectionner votre librairie dans le bas de la liste

Sonatype staging repository

Dans la barre de bouton le bouton Drop permet de supprimer votre librairie et le bouton Close de passer à la phase suivante…​ Je vous l’accorde ce n’est pas très parlant ce nommage de bouton.

Sonatype staging repository boutons

Une fois que vous avez confirmé le passage à l’étape suivante, les contrôles de validité du projet sont lancés.

Sonatype staging checks

Vous pouvez cliquer sur le bouton Refresh pour mettre à jour l’état de votre librairie. Si tout s’est bien passé le bouton Release dans la barre de bouton s’est activé. En cliquant dessus votre librairie sera publiée et dupliquée sur les différents serveurs Sonatype pour être accessible dans un délai maximal de 2h.


Commentaires