发布过开源库的同学肯定深有感触,想要将一个开源库发布到 Maven Central 对于开发者来说并不简单,尤其是通过 Sonatype 来发布,需要满足一系列的条件,而且对于 Gradle 工程来说,尽管都是用 maven-publish
插件来进行发布,但不同的类型的工程,其发布所需的配置还有些不太一样,比如:Gradle Plugin__,__Androdi Library 和 __Java Library__,尤其是多模块的 Gradle 工程,要为每个模块写一堆看起来相似又不完全相同的 DSL 很是麻烦,而且 Gradle 的 DSL 对于新手来说,简直是一脸懵逼。
Sonatype
Sonatype 提供了自动同步到 Maven Central 的功能,但想要往 Sonatype 上发布开源库,需要先要经过一系列的步骤:
- 申请账号
- 提交新建项目的 JIRA 工单
- 回复第 2 步提交的 JIRA 工单,证明 groupId 对应的域名空间是有管理权限的
- 生成 GPG 密钥
- 然后配置 Gradle 工程,保证上传的内容满足以下条件
- 源代码 JAR 文件
- Javadoc JAR 文件
- POM 文件,包含以下内容
- Maven 坐标
- groupId
- artifactId
- version
- 项目信息
- 开源许可信息
- 开发者信息
- __SCM__(源代码管理)信息
- 上述每个文件对应的签名(__.asc__)文件
其中,前 4 步是一次性的工作,而最后一步是每个项目都要涉及到的。
Java/Kotlin Library 工程
Java/Kotlin Library 的 publishing
配置最简单,大致需要 3 步:
- 为 sources 和 javadoc 创建相应的 __JAR Task__, 如果是 Kotlin 工程,则需要通过 Kotlin/dokka 来生成 Javadoc
- 在
publications
中注册一个名字为 mavenJava 的 MavenPublication
- 为 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 来生成 sources 和 javadoc 对应的 __JAR__,必要的情况下,还需要为每个 variant 发布一个 __AAR__,一般是通过 android.libraryVariants
来遍历所有的 variant
:
1 2 3 4 5
| val android = extensions.getByName("android") as LibraryExtension
android.libraryVariants.forEach { variant -> }
|
但由于 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 Library 和 Android 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
|
待上传到 Sonatype 的 staging 仓库后,然后通过如下命令来发布到正式仓库:
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=**********
|