建议收藏,从 jCenter 迁移到 MavenCentral 完整方案

发布到 Maven Central 相关的教程挺多的,但是大部分存在问题。这篇文章是我在解决了很多问题的基础之上总结的,用来帮助需要的同学避免重蹈覆彻。需要的可收藏,万一用到了呢~

1、Farewell to Bintray jCenter

首先,告别下 bintray jcenter. 相比于 Maven Central,bintray jcenter 的性能和方便性确实好得多。可惜,jcenter 将要关闭了。jcenter 还是给我提供了很多的便利,对于其关闭深表可惜:

bintray

2、发布到 Maven Central

Step 1: 注册和激活 sonatype

发布到 Maven Central 之前,首先要到 sonatype 注册一个账号,

https://issues.sonatype.org/

注册账号完账号之后需要通过创建 issue 激活账号,页面如下:

激活

这里有几个需要注意的地方:

  1. 项目选择 Community Support - Open Source Project Repository Hosting
  2. 问题类型选择 New Project
  3. 概要:描述项目功能,不重要
  4. Group Id 比较重要,我们后续说明
  5. Project URL 填写自己开源项目地址即可,要与 Group Id 有一定的关联性
  6. SCM url 版本仓库的拉取地址,填写自己的项目的 git 链接地址,通常是项目地址后加 .git

对于这里的 group id,它就是发布完成之后,引用时的 group id. 你可以使用自己的域名,但是需要证明域名是你自己的。如果没用自己的域名,一个简单而通用的方式是使用 Github 的地址,比如我的 GitHub 地址是 https://github.com/Shouheng88 ,就可以写作 com.github.Shouheng88. 创建完 issue 之后几分钟内就会收到对方发送的邮件,邮件内会提示通过在 Github 里面创建项目来验证激活:

QQ截图20210414231719.png

当我们按照邮件说明创建完项目之后,修改问题状态,过几分钟之后就激活成功了。

Step 2: 申请密钥

为了确保中央存储库中可用组件的质量水平,OSSRH 对提交的文件有明确的要求。除了 jar 包和 pom 文件,Javadoc 和 Sources 是必须的(这点和 bintray jcenter 类似),并且每个文件都要有一个对应的 asc 文件,即 GPG 签名文件,用于校验文件。所以,这步骤中我们需要申请密钥。

OSX 下面可以使用 brew 安装,

$ brew update
$ brew install -v gpg

Windows 下面也可以直接下载安装,

https://www.gpg4win.org/get-gpg4win.html

安装完毕之后,可以通过如下指令生成密钥:

gpg --generate-key

创建密钥的过程中会要求输入密码,这里的密码非常重要,我们发布的时候会使用到它。

创建完毕之后可以通过 gpg -k 显示所有已创建的密钥:

显示密钥

这里的字符串 AB7FxxxxxxxxxxxxABA846D44F7B66 叫做密钥指纹。后面 8 位 D44F7B66,叫做 KEY ID,我们发布的时候将使用到它。

另外,进行文件签名的时候需要用到名为 secretKeyRingFile 的文件,我们可以通过如下命令生成:

gpg --export-secret-keys [密钥指纹] > secret.gpg

在 sonatype 的仓库提交后,需要从多个公钥服务器上下载匹配的公钥,然后来校验你上传的文件的签名。所以,我们需要通过下面的命令上传公钥到公钥服务器:

gpg --keyserver keyserver.ubuntu.com --send-keys [密钥指纹]

Step 3: 准备发布脚本

首先需要引入两个插件,

apply plugin: 'maven-publish'
apply plugin: 'signing'

然后编写发布的 gralde 脚本如下

task androidSourcesJar(type: Jar) {
    archiveClassifier.set("sources")
    from android.sourceSets.main.java.source
    exclude "**/R.class"
    exclude "**/BuildConfig.class"
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            // group id,发布后引用的依赖的 group id
            groupId 'com.github.Shouheng88'
            // 发布后引用的依赖的 artifact id
            artifactId 'sil'
            // 发布的版本
            version '0.1.0'
            // 发布的 arr 的文件和源码文件
            artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
            artifact androidSourcesJar
            pom {
                // 构件名称,可以自定义
                name = 'uix-common'
                // 构件描述
                description = 'Android UIX'
                // 构件主页
                url = 'https://github.com/Shouheng88/Android-uix'
                // 许可证名称和地址
                licenses {
                    license {
                        name = 'The Apache License, Version 2.0'
                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                // 开发者信息
                developers {
                    developer {
                        name = 'ShouHeng'
                        email = 'shouheng2015@gmail.com'
                    }
                }
                // 版本控制仓库地址
                scm {
                    url = 'https://github.com/Shouheng88/Android-uix'
                    connection = 'scm:git:github.com/Shouheng88/Android-uix.git'
                    developerConnection = 'scm:git:ssh://git@github.com/Shouheng88/Android-uix.git'
                }
            }
        }
    }
    repositories {
        maven {
            // 发布的位置,这里根据发布的版本区分了 SNAPSHOT 和最终版本两种情况
            def releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
            def snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
            url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
            credentials {
                // 这里就是之前在 issues.sonatype.org 注册的账号
                username ossrhUsername
                password ossrhPassword
            }
        }
    }
}

signing {
    sign publishing.publications
}

注意:这里的 group id 必须和申请的 sonatype 申请账号时申请的 group id 一致。这里的要发布到的仓库的地址可能是每个用户不同的,具体是多少要看查看自己的激活邮件(可查看下面的“到 Sonatype 处理发布结果”小节)。

此外,我们需要在 gradle.properties 中定义我们的账户相关的信息:

signing.keyId=密钥keyId
signing.password=密钥password
signing.secretKeyRingFile=私钥keyRingFile路径

sonatypeUsername=sonatype账号
sonatypePassword=sonatype密码

Step 4: 发布

配置完成之后发布就比较简单了,只需要在 AS 的右边栏的 gradle 任务栏里指定的 module 下面选择 publishing 任务,然后选择 publishMavenJavaPublishingToMavenRepository 即可:

发布

Step 5:到 Sonatype 处理发布结果

实际上每个用户发布到的分区可能是不同的,一个准确的查看的方法是,当你完成账号激活之后会收到一封邮件,这里写明了你的文件发布到的位置,

激活邮件

比如我的在 https://s01.oss.sonatype.org 域名下,而其他用户的可能在 https://oss.sonatype.org/ 域名下,如果我用自己注册的账号登录其他的域名就会报权限相关的错误,这里也是一个坑。

登录到自己的仓库之后点击左侧的 Staging Repositories 就可以看到我们刚刚发布的文件。

仓库

勾选我们的文件之后点击 Close,snoatype 将对我们发布的文件进行校验,校验需要一分钟左右的时间,校验完毕之后点击 Release,就最终发布了我们的文件。

Step 6: 引用我们的库

发布完成之后就可以引用了,引用依赖的方式和其他仓库完全一样,需要说明的是,需要先添加 Maven Central 的仓库的地址:

repositories {
    //推荐: release成功后会直接从mavenCentral拉取aar
    mavenCentral()
    //或者
    maven {url "https://s01.oss.sonatype.org/content/groups/public"}
    //或者
    maven {url "https://s01.oss.sonatype.org/content/repositories/releases"}
}

Step 7: 处理引用的问题

我发现大部分教程都是介绍到上面就结束了,但是实际上还有很重要的一个步骤需要解决。

通常我们的库并不是独立的,它可能通过各种方式引用其他的库,比如 implementation、api、compile 或者 compileOnly 等。使用 Bintray Jcenter 的时候,它可以自动根据我们项目的依赖关系生成 pom 文件中的依赖。但是发布到 Maven Central 的时候需要我们自己解决这个问题。如果不解决这个问题,引用的时候就只引用到了我们自己发布的这个 aar 文件。这显然是不行的。

我通过对比 Bintray JCenter 对各种引用方式,编写了下面的代码用来生成 pom 中的依赖关系:

withXml {
    def dependenciesNode = asNode().appendNode('dependencies')
    project.configurations.all { configuration ->
        def name = configuration.name
        if (name != "implementation" && name != "compile" && name != "api") {
            return
        }
        println(configuration)
        configuration.dependencies.each {
            println(it)
            if (it.name == "unspecified") {
                // 忽略无法识别的
                return
            }
            def dependencyNode = dependenciesNode.appendNode('dependency')
            dependencyNode.appendNode('groupId', it.group)
            dependencyNode.appendNode('artifactId', it.name)
            dependencyNode.appendNode('version', it.version)
            if (name == "api" || name == "compile") {
                dependencyNode.appendNode("scope", "compile")
            } else { // implementation
                dependencyNode.appendNode("scope", "runtime")
            }
        }
    }
}

这里会先对引用的方式做一个判断,然后根据引用的方式选择对应的 scope,同时处理了一些异常的情况。

最终的发布脚本

task androidSourcesJar(type: Jar) {
    archiveClassifier.set("sources")
    from android.sourceSets.main.java.source
    exclude "**/R.class"
    exclude "**/BuildConfig.class"
}

publishing {
    // 定义发布什么
    publications {
        mavenJava(MavenPublication) {
            // group id,发布后引用的依赖的 group id
            groupId 'com.github.Shouheng88'
            // 发布后引用的依赖的 artifact id
            artifactId 'sil'
            // 发布的版本
            version '0.1.0'
            // 发布的 arr 的文件和源码文件
            artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
            artifact androidSourcesJar
            pom {
                // 构件名称,可以自定义
                name = 'uix-common'
                // 构件描述
                description = 'Android UIX'
                // 构件主页
                url = 'https://github.com/Shouheng88/Android-uix'
                // 许可证名称和地址
                licenses {
                    license {
                        name = 'The Apache License, Version 2.0'
                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                // 开发者信息
                developers {
                    developer {
                        name = 'ShouHeng'
                        email = 'shouheng2015@gmail.com'
                    }
                }
                // 版本控制仓库地址
                scm {
                    url = 'https://github.com/Shouheng88/Android-uix'
                    connection = 'scm:git:github.com/Shouheng88/Android-uix.git'
                    developerConnection = 'scm:git:ssh://git@github.com/Shouheng88/Android-uix.git'
                }
                // 解决依赖关系
                withXml {
                    def dependenciesNode = asNode().appendNode('dependencies')
                    project.configurations.all { configuration ->
                        def name = configuration.name
                        if (name != "implementation" && name != "compile" && name != "api") {
                            return
                        }
                        println(configuration)
                        configuration.dependencies.each {
                            println(it)
                            if (it.name == "unspecified") {
                                // 忽略无法识别的
                                return
                            }
                            def dependencyNode = dependenciesNode.appendNode('dependency')
                            dependencyNode.appendNode('groupId', it.group)
                            dependencyNode.appendNode('artifactId', it.name)
                            dependencyNode.appendNode('version', it.version)
                            if (name == "api" || name == "compile") {
                                dependencyNode.appendNode("scope", "compile")
                            } else { // implementation
                                dependencyNode.appendNode("scope", "runtime")
                            }
                        }
                    }
                }
            }
        }
    }
    // 定义发布到哪里
    repositories {
        maven {
            // 发布的位置,这里根据发布的版本区分了 SNAPSHOT 和最终版本两种情况
            def releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
            def snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
            url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
            credentials {
                // 这里就是之前在 issues.sonatype.org 注册的账号
                username ossrhUsername
                password ossrhPassword
            }
        }
    }
}

signing {
    sign publishing.publications
}

总结

以上是一个简单的解决方案,经过上述操作已经可以帮助我们解决发布过程中的许多问题。当然,这里还是有可以优化的空间,比如处理项目依赖等各种情况。

希望本文对你有所帮助,如有疑问可在下方留言。

推荐阅读更多精彩内容