建议收藏,从 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
}

总结

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容