发布过开源库的同学肯定深有感触,想要将一个开源库发布到 Maven Central 对于开发者来说并不简单,尤其是通过 Sonatype 来发布,需要满足一系列的条件,而且对于 Gradle 工程来说,尽管都是用 maven-publish 插件来进行发布,但不同的类型的工程,其发布所需的配置还有些不太一样,比如:Gradle PluginAndrodi LibraryJava Library,尤其是多模块的 Gradle 工程,要为每个模块写一堆看起来相似又不完全相同的 DSL 很是麻烦,而且 GradleDSL 对于新手来说,简直是一脸懵逼。

Sonatype

Sonatype 提供了自动同步到 Maven Central 的功能,但想要往 Sonatype 上发布开源库,需要先要经过一系列的步骤:

  1. 申请账号
  2. 提交新建项目JIRA 工单
  3. 回复第 2 步提交的 JIRA 工单,证明 groupId 对应的域名空间是有管理权限的
  4. 生成 GPG 密钥
  5. 然后配置 Gradle 工程,保证上传的内容满足以下条件
    • 源代码 JAR 文件
    • Javadoc JAR 文件
    • POM 文件,包含以下内容
      • Maven 坐标
        • groupId
        • artifactId
        • version
      • 项目信息
        • name
        • description
        • url
      • 开源许可信息
      • 开发者信息
      • SCM(源代码管理)信息
    • 上述每个文件对应的签名(.asc)文件

其中,前 4 步是一次性的工作,而最后一步是每个项目都要涉及到的。

Java/Kotlin Library 工程

Java/Kotlin Librarypublishing 配置最简单,大致需要 3 步:

  1. sourcesjavadoc 创建相应的 JAR Task, 如果是 Kotlin 工程,则需要通过 Kotlin/dokka 来生成 Javadoc
  2. publications 中注册一个名字为 mavenJavaMavenPublication
  3. mavenJava 签名

完整的示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
project.run {
val sourceSets = the<SourceSetContainer>()
val javadocJar = tasks.register("packageJavadocFor${name.capitalize()}", Jar::class.java) {
archiveClassifier.set("javadoc")
from(tasks["dokkaHtml"])
}
val sourcesJar = tasks.register("packageSourcesFor${name.capitalize()}", Jar::class.java) {
dependsOn(JavaPlugin.CLASSES_TASK_NAME)
archiveClassifier.set("sources")
from(sourceSets["main"].allSource)
}

publishing {
repositories {
maven {
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
}
}
publications {
register("mavenJava", MavenPublication::class) {
groupId = "${project.group}"
artifactId = project.name
version = "${project.version}"

from(components["java"])

artifact(sourcesJar.get())
artifact(javadocJar.get())

pom.withXml {
asNode().apply {
appendNode("name", project.name)
appendNode("url", "https://github.com/johnsonlee/${project.name}")
appendNode("description", project.description ?: project.name)
appendNode("scm").apply {
appendNode("connection", "scm:git:git://github.com/johnsonlee/${project.name}.git")
appendNode("developerConnection", "scm:git:[email protected]:johnsonlee/${project.name}.git")
appendNode("url", "https://github.com/johnsonlee/${project.name}")
}
appendNode("licenses").apply {
appendNode("license").apply {
appendNode("name", "Apache License")
appendNode("url", "http://www.apache.org/licenses/LICENSE-2.0")
}
}
appendNode("developers").apply {
appendNode("developer").apply {
appendNode("id", "johnsonlee")
appendNode("name", "Johnson Lee")
appendNode("email", "[email protected]")
}
}
}
}
}
}
}

signing {
sign(publishing.publications["mavenJava"])
}
}

Android Library 工程

Java/Kotlin Library 不同,Android Library 需要根据不同的 variant 来生成 sourcesjavadoc 对应的 JAR,必要的情况下,还需要为每个 variant 发布一个 AAR,一般是通过 android.libraryVariants 来遍历所有的 variant

1
2
3
4
5
val android = extensions.getByName("android") as LibraryExtension

android.libraryVariants.forEach { variant ->
// 为每个 variant 注册 MavenPublication
}

但由于 libraryVariants 的配置是 lazy 模式,所以,需要在 project.afterEvaluate 回调中执行,完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
project.run {
val android = extensions.getByName("android") as LibraryExtension

afterEvaluate {
publishing {
publications {
android.libraryVariants.forEach { variant ->
val javadoc = tasks.register("javadocFor${variant.name.capitalize()}", Javadoc::class.java) {
dependsOn("dokkaHtml")
source(android.sourceSets["main"].java.srcDirs)
classpath += files(android.bootClasspath + variant.javaCompileProvider.get().classpath)
exclude("**/R.html", "**/R.*.html", "**/index.html")
}

val javadocJar = tasks.register("packageJavadocFor${variant.name.capitalize()}", Jar::class.java) {
dependsOn(javadoc)
archiveClassifier.set("javadoc")
from(tasks["dokkaHtml"])
}

val sourcesJar = tasks.register("packageSourcesFor${variant.name.capitalize()}", Jar::class.java) {
archiveClassifier.set("sources")
from(android.sourceSets["main"].java.srcDirs)
}

create(variant.name, MavenPublication::class.java) {
groupId = project.group
artifactId = project.name
version = project.version
from(components[variant.name])
artifact(javadocJar)
artifact(sourcesJar)

pom.withXml {
asNode().apply {
appendNode("name", project.name)
appendNode("url", "https://github.com/johnsonlee/${project.name}")
appendNode("description", project.description ?: project.name)
appendNode("scm").apply {
appendNode("connection", "scm:git:git://github.com/johnsonlee/${project.name}.git")
appendNode("developerConnection", "scm:git:[email protected]:johnsonlee/${project.name}.git")
appendNode("url", "https://github.com/johnsonlee/${project.name}")
}
appendNode("licenses").apply {
appendNode("license").apply {
appendNode("name", "Apache License")
appendNode("url", "http://www.apache.org/licenses/LICENSE-2.0")
}
}
appendNode("developers").apply {
appendNode("developer").apply {
appendNode("id", "johnsonlee")
appendNode("name", "Johnson Lee")
appendNode("email", "[email protected]")
}
}
}
}
}
}
}
}
}

signing {
sign(publishing.publications)
}
}

Gradle Plugin 工程

Gradle 官方提供了 java-gradle-plugin 用来生成 Gradle Plugin 相关的 POM 文件,但其中的内容只包含了 Maven 坐标信息和基本的工程信息,根据不能满足 Sonatype 的要求,要想发布到 Sonatype,还需要开发者自己来手动配置,但如何对 java-gradle-plugin 生成的 POM 进行修改,Gradle 官方并没有提供相应的文档,其实并不难,只是跟前面的 Java/Kotlin LibraryAndroid Library 都不一样,因为 java-gradle-plugin 已经自动创建了 MavenPublication 了,所以,并不需要再次创建或者注册 MavenPublication 只需要遍历一下,然后为 POM 追加上必要的信息就行了,完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
project.run {
publishing {
publications {
val sourceSets = the<SourceSetContainer>()
val javadocJar = tasks.register("packageJavadocFor${name.capitalize()}", Jar::class.java) {
dependsOn("dokkaHtml")
archiveClassifier.set("javadoc")
from(tasks["dokkaHtml"])
}
val sourcesJar = tasks.register("packageSourcesFor${name.capitalize()}", Jar::class.java) {
archiveClassifier.set("sources")
from(sourceSets["main"].allSource)
}

withType<MavenPublication>().configureEach {
groupId = "${project.group}"
version = "${project.version}"
artifact(javadocJar)
artifact(sourcesJar)
}
}
}

signing {
sign(publishing.publications)
}
}

一劳永逸

在看了上面针对不同类型的工程配置 publishing 后,发现,其实大部分代码都是类似的,如果是一个多模块的工程,配置起来就比较麻烦了,有的模块是需要发布的,有的模块是不需要发布的,通过 allprojects 或者 subprojects 来配置也不简单,既然大部分代码相似,能不能让整个配置更简单一些呢?答案是 —— 必须有!这就是 sonatype-publish-plugin 的初衷,真的就一行代码搞定:

1
2
3
4
5
6
plugins {
id("io.johnsonlee.sonatype-publish-plugin") version "1.3.0"
}

group = "io.johnsonlee"
version = "1.0.0"

开发者只需要配置好相应的环境变量就可以通过命令直接上传了:

1
2
3
4
5
6
7
./gradlew clean publishToSonatype \
-POSSRH_USERNAME=johnsonlee \
-POSSRH_PASSWORD=********** \
-POSSRH_PACKAGE_GROUP=io.johnsonlee \
-Psinging.keyId=xxxxxxxx \
-Psinging.password=******** \
-Psinging.secretKeyRingFile=/Users/johnsonlee/.gnupg/secring.gpg

待上传到 Sonatypestaging 仓库后,然后通过如下命令来发布到正式仓库:

1
2
3
4
./gradlew closeAndReleaseRepository \
-POSSRH_USERNAME=johnsonlee \
-POSSRH_PASSWORD=********** \
-POSSRH_PACKAGE_GROUP=io.johnsonlee

发布成功后,便可以在 Maven Central 上搜索到了,关于详细介绍,请参阅项目介绍

该插件不仅支持支持发布到 Sonatype,还支持发布到私有 Nexus 仓库,例如公司内网的 Nexus 服务,只需要配置一下这几个属性或环境变量即可:

  • NEXUS_URL
  • NEXUS_USERNAME
  • NEXUS_PASSWORD

然后通过如下命令来发布到私有 Nexus 仓库:

1
2
3
4
./gradlew clean publish \
-PNEXUS_URL=http://nexus.johnsonlee.io/ \
-PNEXUS_USERNAME=johnsonlee \
-PNEXUS_PASSWORD=**********