前言
之前写过一篇Android_上传library到Bintray并发布到JCenter的文章,但不幸的是几个月前Jfrog
发布了终止Bintray
服务的声明,声明的大概意思是说2021年3月31号之后Jcenter仓库
将不再接收用户的组件提交,同时将Jcenter
设为只读代码仓库,无限期地提供现有组件供用户下载,也就是说目前Jcenter仓库
的状态是你无法再提交组件的更新,但你可以继续下载你以前托管的组件版本,所以现在你要做的就是把你的组件的新版本发布到其他仓库,例如Jitpack
和MavenCentral
,本篇文章的内容是教你如何发布组件到MavenCentral仓库
,同时搞了一个发布脚本简化发布过程。
前期准备
在讲解之前,有必要介绍一下Sonatype
、OSSRH
、MavenCentral
之间的关系,Sonatype是一家公司,它运营着MavenCentral仓库,我们想要发布组件到MavenCentral
,必须要通过Sonatype
的OSSRH
,OSSRH
即OSS Repository Hosting
,对象存储仓库托管,Sonatype
使用Nexus Repository Manager为组件提供存储库托管服务,我们发布组件时要先发布到Sonatype OSSRH
上,然后才能同步到MavenCentral
仓库,就好像我们之前使用Jcenter
时,要先发布到Jfrog Bintray
上,然后才能同步到Jcenter仓库
。
1、注册Sonatype账号
首先你需要注册一个Sonatype Jira
账号,Sonatype
使用Jira
管理你的groupId
申请过程,注册地址如下:
Sonatype Jira - Sign up
点开注册地址后,如图:
按要求填入你的邮箱地址、姓名、用户名、密码即可,其中用户名最好不要有中文,记住你的用户名和密码,会在后面用到。
2、申请groupId
我们平时使用托管在远程仓库的组件时都是通过它的GAV坐标
来定位的,GAV
即groupId
、artifactId
、version
,其中groupId
你可以理解为你自己在Sonatype OSSRH
创建的仓库,groupId
就是你仓库的名称,申请groupId
就是在Sonatype OSSRH
申请创建属于你的仓库,我们后面发布组件时要先发布到Sonatype OSSRH
上名为groupId
的仓库,然后才能同步到MavenCentral仓库
。
还有申请的groupId
并不是随便填的,按照Sonatype
的要求,groupId
必须要是一个域名的反写,所以你要拥有一个域名,当你申请groupId
时,Sonatype
会用某种方式让你证明你是这个域名的所有者。
如果你拥有某个域名,例如example.com域名
,你可以使用任何以com.example
开头的groupId
,例如com.example.test1
、com.example.test2
等,如果你没有自己的域名也没关系,Sonatype
支持代码托管平台的Pages
网站域名,例如Github
,你可以在你的Github账号
上开启你的Pages
服务,这样你就拥有了一个与Github
账号关联的个人域名,格式为{username}.github.io
,例如我的Github Pages网站
就是ydstar.github.io
,很多人都是利用这种托管在三方平台的网站搭建自己的博客网站,除了Github
,Sonatype
还支持GitLab
、Gitee
等,下面表格列出这些常用的代码托管平台Pages服务
开启的官方教程地址,和开启后对应的域名和相应的groupId
:
代码托管平台 | Pages服务开启教程地址 | 域名 | groupId |
---|---|---|---|
Github | https://pages.github.com/ | {username}.github.io | io.github.{username} |
Gitee | https://gitee.com/help/articles/4136 | {username}.gitee.io | io.gitee.{username} |
GitLab | https://about.gitlab.com/stages-devops-lifecycle/pages/ | {username}.gitlab.io | io.gitlab.{username} |
下面就以我的Github Pages
网站域名ydstar.github.io
为例,申请名为io.github.ydstar
的groupId
,首先打开Sonatype Jira
网站, 地址如下:
Sonatype Jira - Dashboard
首次进入需要你登陆,输入你刚才注册的Sonatype Jira
用户名和密码登陆,然后就进入Sonatype Jira
网站首页,然后点击导航栏的新建按钮,此时会弹出一个弹窗,会让你填一些申请groupId
时需要的信息,其中项目
选择Community Support - Open Source Project Repository Hosting (OSSRH)
,问题类型
选择New Project
,摘要
填一个标题,Group Id
就填你要申请的groupId
(很重要,我这里用的是Github创建的pages网站域名io.github.ydstar
),Project URL
随便填一个你的组件仓库地址,SCM url
也是随便填一个你的组件仓库版本控制地址,如下:
最后点击新建
按钮,它会创建一个issue
,issue名称
格式为OSSRH-{taskId}
,如我这里为OSSRH-75769
,你可以在问题面板
中看到它,然后接下来就是等待Sonatype Jira
的邮件通知,邮件会发送到你注册账号时填写的邮箱,它的第一封邮件会叫你在你的Github
中创建一个名为OSSRH-{taskId}
的空仓库,从而证明你是groupId
对应域名的拥有者,当你创建之后,你需要到OSSRH-{taskId}
下的comment
面板中回复,当你回复后,它又会再发一封邮件给你,告诉你groupId
已经申请完毕,此时你可以发布组件到Sonatype OSSRH
中,如何发布请看后面的内容,当你发布后,你需要在Sonatype OSSRH
中把你的组件同步到MavenCentral
后才可以通过GAV
引用它,如何同步请看后面的内容,当你同步后,你需要再次到OSSRH-{taskId}
下的comment面板
中回复,然后Sonatype OSSRH
才会为你激活组件同步到MavenCentral
的程序,整个交流过程可以参考我OSSRH-75769中的comment面板,如下:
回复的内容是什么不重要,只要你回复了就行,直到最后显示状态未已解决
,上述就是申请groupId
和激活MavenCentral
同步的整个流程,只需要在第一次发布组件时进行一次就行,以后发布组件时不需要再进行上面的过程,直接使用该groupId
就行。
3、生成gpg签名信息
Sonatype
要求发布到MavenCentral仓库
的组件中的每个文件都需要通过gpg签名,gpg
是一个命令行工具,提供了对数据的签名和加密能力,支持主流的签名和加密算法,并提供了密钥管理系统,要使用gpg签名
,我们必须先在电脑上安装gpg
,然后使用gpg
生成签名需要的密钥对。
我们首先来安装gpg
,对于mac
电脑,直接通过Homebrew
安装就行,在命令行执行:
$ brew install gpg
对于window
电脑,我们可以下载gpg
的执行文件安装,下载地址如下:
gpg download
安装完成后,在命令行输入gpg --version
输出gpg的版本信息表示安装完成,如下:
然后在命令行输入gpg --full-generate-key
进行密钥对生成,这个命令会让你一步步选择生成密钥对需要的信息,如下:
首先它会让你选择算法,我这里选择了RSA
,仅用于签名,然后会让你输入密钥长度,默认是3072bits
,然后会让你选择密钥的过期时间,我这里选择了永久有效,建议大家也选择永久有效,然后确认信息后会让你输入姓名、邮箱、注释来作为密钥的唯一标识,这里我生成的标识为houyadong
,最后确认后会弹出一个弹窗让你输入一个密码来保护你的私钥,记住你输入的密码后面会用到,点击确认后看到公钥和私钥已经生成并被签名
这句话说明密钥对生成完毕,我们可以通过gpg --list-keys
列出生成的公钥信息,通过gpg --list-secret-keys
列出生成的私钥信息,但其实列出的信息是类似的,我们以gpg --list-keys
为例,如下:
列出的信息首先有公钥的文件位置,这里为/Users/houyadong/.gnupg/pubring.kbx
,接着是pub rsa3072 2021-12-06 [SC]
,表示使用RSA算法
,公钥长度为3072位
,创建日期为2021-12-06
,接着是长长的一串id值47234CF52843D87E263C5C64A94EDD3B0FF22B1C
,它表示公钥的id
,我们使用时只需要用到它的后八位就行,所以我们后面使用这个公钥时只需要使用0FF22B1C
就行,最后是公钥的唯一标识uid
,根据我们前面输入的姓名、邮箱、注释生成。
我们签名只需要用到私钥,而公钥需要上传到公钥服务器,这样我们用私钥签名的文件上传到MavenCenral
后,它才能使用公钥验证这个文件,这里我选择了keyserver.ubuntu.com
这个公钥服务器,上传公钥的命令格式为gpg --keyserver 公钥服务器地址 --send-keys 公钥id
,如下我把刚刚生成的公钥0FF22B1C
上传到公钥服务器:
没有错误提示就表示上传成功,我们还可以使用gpg --keyserver 公钥服务器地址 --recv-keys 公钥id
从公钥服务器导入公钥到本地从而验证公钥是否上传成功,如下:
可以看到导入成功,表示公钥已经成功上传到公钥服务器。
还有最后一步,把私钥导出到一个文件中,这样我们发布组件时才能通过引用这个文件,从而使用私钥进行签名,命令格式为gpg -o 导出的文件路径 --export-secret-key 私钥id
,执行导出私钥命令会弹出一个弹窗让你输入私钥密码,输入正确后才能成功导出,如下把私钥导出到名为OFF22B1C.gpg
的文件中,私钥id和公钥id是一样的:
现在已经成功把私钥导出/Users/houyadong/key/0FF22B1C.gpg
中,记住你导出的私钥文件路径,后面会用到。
发布到OSSRH
现在万事俱备,可以发布组件了,发布组件使用maven-publish
插件,我们主要发布的内容有组件的aar
或jar文件
、sources文件
、javadoc文件
,pom文件
,还有这些文件的签名文件,以.asc
为后缀,签名要使用signing
插件,为了简化发布过程,我已经把发布过程编写成了一个gradle脚本
,使用时只需要apply
进来,然后填写发布的基本信息,执行发布任务就可以自动生成发布所需的所有文件并发布到OSSRH
中,下面简单介绍如何使用这个gradle
脚本。
首先在项目中创建script文件夹
和publication.gradle
文件:
然后把如下脚本复制粘贴上去
def helper = new Helper(project)
if(helper.isCheckPass){
apply plugin: 'maven-publish'
apply plugin: 'signing'
tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
options.addStringOption('encoding', 'utf-8')
options.addStringOption('charSet', 'utf-8')
}
if(helper.isJavaLibrary()){
java{
withJavadocJar()
withSourcesJar()
}
}
//maven仓库地址
publishing.repositories{
maven {
url = helper.getMavenUrl()
helper.log "mavenUrl = ${url}"
if(!helper.isSkipCredential(url)){
credentials {
def credentials = helper.getCredentials()
username credentials[0]
password credentials[1]
}
}
}
}
//定义发布过程
publishing.publications{
if(helper.isAndroidLibrary()){
def isAGPGreaterEqual360 = helper.getAGPVersion() >= '3.6.0'
android.libraryVariants.all{variant ->
if('release' != variant.buildType.name){
return
}
def flavorName = variant.flavorName
def variantName = "${flavorName}${helper.isEmpty(flavorName) ? 'r' : 'R'}elease"
helper.log "variant = ${variantName}"
"${flavorName}Androidlib"(MavenPublication) {
//组件GVA坐标
def gav = helper.getGAV(variant)
groupId gav[0]
artifactId gav[1]
version gav[2]
//要发布的产物
artifact helper.createSourcesJar(variant)
artifact helper.createJavadocJar(variant)
if(isAGPGreaterEqual360){
from components."${variantName}"
}else{
artifact helper.getReleaseOutput(variant)
}
//要生成的pom文件
helper.appendCommonInfoToPom(it, 'aar')
if(!isAGPGreaterEqual360){
//生成组件的依赖关系
helper.appendDependenciesToPom(variant, it)
}
}
}
}
if(helper.isJavaLibrary()){
"Javalib"(MavenPublication) {
def gav = helper.getGAV()
groupId gav[0]
artifactId gav[1]
version gav[2]
from components.java
helper.appendCommonInfoToPom(it, 'jar')
}
}
if(helper.isJavaPlatform()){
"platformlib"(MavenPublication){
def gav = helper.getGAV()
groupId gav[0]
artifactId gav[1]
version gav[2]
from components.javaPlatform
helper.appendCommonInfoToPom(it, 'jar')
}
}
if(helper.isPackLibrary()){
String[] paths = helper.getArtifactPath()
helper.log "artifactPath = $paths"
paths.each {path ->
def file = file(path)
def lastDotIndex = file.name.lastIndexOf('.')
if(lastDotIndex != -1){
def fileName = file.name.substring(0, lastDotIndex)
def ext = file.name.substring(lastDotIndex + 1)
"Pack${fileName.capitalize()}lib"(MavenPublication) {
def gav = helper.getGAV()
groupId gav[0]
artifactId fileName
version gav[2]
artifact file
helper.appendCommonInfoToPom(it, ext)
}
}else {
helper.logError "artifactPath传入错误,必须要带扩展名:path = ${path}"
}
}
}
}
//对每个输出进行签名
if(!helper.isSkipSignature()){
signing {
sign publishing.publications
}
}
}
class Helper{
private static def TAG = 'MavenPublishScript'
private def signingKeyId = 'signing.keyId'
private def signingPassword = 'signing.password'
private def signingSecretKeyRingFile = 'signing.secretKeyRingFile'
private def ossrhUsername = 'ossrh.username'
private def ossrhPassword = 'ossrh.password'
private def publishGroupId = 'publish.groupId'
private def publishArtifactId = 'publish.artifactId'
private def publishVersion = 'publish.version'
private def publishDescription = 'publish.description'
private def publishUrl = 'publish.url'
private def publishRepoReleaseUrl = 'publish.repoReleaseUrl'
private def publishRepoSnapshotUrl = 'publish.repoSnapshotUrl'
private def publishArtifactPath = 'publish.artifactPath'
private def publishIsAppendFavorName = 'publish.artifactId.isAppendFavorName'
private def publishIsSkipSignature = 'publish.isSkipSignature'
private def publishIsSkipCredential = 'publish.isSkipCredential'
private def publishDeveloperName = 'publish.developerName'
private def publishDeveloperEmail = 'publish.developerEmail'
private def publishLicenseName = 'publish.licenseName'
private def publishLicenseUrl = 'publish.licenseUrl'
private def publishScmUrl = 'publish.scmUrl'
private def publishScmConnection = 'publish.scmConnection'
private def publishScmDeveloperConnection = 'publish.scmDeveloperConnection'
private Project project
def isCheckPass = false
Helper(Project p){
this.project = p
this.isCheckPass = check()
}
static def isEmpty(String string){
return string == null || string.isEmpty()
}
def isSkipSignature(){
return readBoolean(publishIsSkipSignature)
}
def isSkipCredential(url){
return readBoolean(publishIsSkipCredential) || project.uri(url).scheme.toLowerCase() == 'file'
}
def getMavenUrl(){
String tempReleaseUrl = readProperty(publishRepoReleaseUrl)
String tempSnapshotsUrl = readProperty(publishRepoSnapshotUrl)
def releasesRepoUrl = !tempReleaseUrl.isEmpty() ? tempReleaseUrl : "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
def snapshotsRepoUrl = !tempSnapshotsUrl.isEmpty() ? tempSnapshotsUrl : "https://s01.oss.sonatype.org/content/repositories/snapshots/"
return readProperty(publishVersion).endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
}
def getCredentials(){
def credentials = new String[2]
credentials[0] = readProperty(ossrhUsername)
credentials[1] = readProperty(ossrhPassword)
return credentials
}
def getGAV(variant){
def gav = new String[3]
gav[0] = readProperty(publishGroupId)
gav[2] = readProperty(publishVersion)
if(variant == null || isEmpty(variant.flavorName)){
gav[1] = readProperty(publishArtifactId)
}else{
gav[1] = (readProperty(publishIsAppendFavorName).isEmpty() || readBoolean(publishIsAppendFavorName))
? (readProperty(publishArtifactId) + "-${variant.flavorName}")
: readProperty(publishArtifactId)
}
return gav
}
def getArtifactPath(){
String path = readProperty(publishArtifactPath)
return path.split(';')
}
def createSourcesJar(variant){
def name
if(variant == null){
name = 'sourcesJar'
}else{
def flavorName = variant.flavorName
name = "${flavorName}${isEmpty(flavorName) ? 's' : 'S'}ourcesJar"
}
return project.tasks.create(name, Jar){
archiveClassifier = 'sources'
from getSrcDirs(variant)
exclude "**/R.java", "**/BuildConfig.java"
}
}
def createJavadocJar(variant){
def name
if(variant == null){
name = 'javadocJar'
}else{
def flavorName = variant.flavorName
name = "${flavorName}${isEmpty(flavorName) ? 'j' : 'J'}avadocJar"
}
def javadoc = maybeCreateJavadoc(variant)
return project.tasks.create(name, Jar){
archiveClassifier = 'javadoc'
dependsOn javadoc
from javadoc.destinationDir
}
}
def getReleaseOutput(variant){
def result = findBundle(variant)
if(result == null){
if(variant == null){
result = "${project.buildDir}/libs/${project.getName()}.jar"
}else {
result = "${project.buildDir}/outputs/aar/${project.getName()}-${variant.flavorName}-release.aar"
}
}
return result
}
def appendCommonInfoToPom(publication, String ext){
def closure = {
pom {
if(ext != null && !ext.isEmpty()){
packaging = ext
}
//组件的基本信息
name = readProperty(publishArtifactId)
description = readProperty(publishDescription)
url = readProperty(publishUrl)
//licenses文件
licenses {
license {
name = readProperty(publishLicenseName)
url = readProperty(publishLicenseUrl)
}
}
//开发者信息
developers {
developer {
name = readProperty(publishDeveloperName)
email = readProperty(publishDeveloperEmail)
}
}
//版本控制仓库地址
scm {
url = readProperty(publishScmUrl)
connection = readProperty(publishScmConnection)
developerConnection = readProperty(publishScmDeveloperConnection)
}
}
}
closure.delegate = publication
closure.call()
}
def appendDependenciesToPom(variant, publication){
def closure = {
pom.withXml{
log 'appendDependenciesToPom...'
def dependenciesNode = asNode().appendNode('dependencies')
def runtimeClasspath = getRuntimeClasspath(variant)
def resolvedDependencies = [:]
//获取组件解析后的直接依赖
runtimeClasspath.resolvedConfiguration.firstLevelModuleDependencies.each{resolvedDependency ->
def group = resolvedDependency.moduleGroup
def name = resolvedDependency.moduleName
def version = resolvedDependency.moduleVersion
log "resolvedDependency = ${group}:${name}:${version}"
if(version != 'unspecified'){
resolvedDependencies["${group}:${name}"] = version
}
}
def runtimeConfigurations = new HashSet<>()
resolveConfigurations([runtimeClasspath], runtimeConfigurations)
//把依赖写入pom文件
runtimeConfigurations.each{configuration ->
configuration.dependencies.each{dependency ->
def group = dependency.group
def name = dependency.name
def version = dependency.version
def configurationName = configuration.name.toLowerCase()
log "dependency = ${configurationName} ${group}:${name}:${version}"
String key = "${group}:${name}"
if(resolvedDependencies.containsKey(key)){
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', group)
dependencyNode.appendNode('artifactId', name)
dependencyNode.appendNode('version', resolvedDependencies[key])
//避免重复写入依赖
resolvedDependencies.remove(key)
if(configurationName.indexOf("implementation") != -1
|| configurationName.indexOf("runtime") != -1
){
dependencyNode.appendNode("scope", "runtime")
} else {
dependencyNode.appendNode("scope", "compile")
}
//写入依赖的exclude
def excludeRules = new HashSet(configuration.excludeRules)
excludeRules.addAll(dependency.excludeRules)
if(configuration.transitive == false || dependency.transitive == false || excludeRules.size() > 0) {
def exclusionsNode = dependencyNode.appendNode('exclusions')
if (configuration.transitive == false || dependency.transitive == false) {
def exclusionNode = exclusionsNode.appendNode('exclusion')
exclusionNode.appendNode('groupId', '*')
exclusionNode.appendNode('artifactId', '*')
} else {
for (def excludeRule: excludeRules) {
def exclusionNode = exclusionsNode.appendNode('exclusion')
exclusionNode.appendNode('groupId', excludeRule.group == null ? '*' : excludeRule.group)
exclusionNode.appendNode('artifactId', excludeRule.module == null ? '*' : excludeRule.module)
}
}
}
}
}
}
}
}
closure.delegate = publication
closure.call()
}
def getAGPVersion() {
def version
def agpDependency = project.rootProject.buildscript?.configurations?.findByName(ScriptHandler.CLASSPATH_CONFIGURATION)?.incoming?.dependencies?.find {
it.group == "com.android.tools.build" && it.name == "gradle"
}
if(agpDependency != null){
version = agpDependency.version
}else {
Class> gradleVersionClazz = project.plugins.getPlugin('com.android.library').class.getClassLoader().loadClass('com.android.builder.model.Version')
version = (String) gradleVersionClazz.getDeclaredField('ANDROID_GRADLE_PLUGIN_VERSION').get(null)
}
return version
}
def isAndroidLibrary(){
return project.plugins.hasPlugin('com.android.library')
}
def isJavaLibrary(){
return project.plugins.hasPlugin('java-library') || project.plugins.hasPlugin('java')
}
def isJavaPlatform(){
return project.plugins.hasPlugin('java-platform')
}
def isPackLibrary(){
return !readProperty(publishArtifactPath).isEmpty()
}
def log(String msg){
project.logger.log(LogLevel.LIFECYCLE, "${TAG}: ${msg}")
}
def logError(String msg){
project.logger.log(LogLevel.ERROR, "${TAG}: ${msg}")
}
def readProperty(String key){
return project.hasProperty(key) ? project[key] : ''
}
def containProperty(String key){
return project.properties.containsKey(key)
}
def readBoolean(String key){
return readProperty(key).toLowerCase() == 'true'
}
private def isSupportPublish(){
return isAndroidLibrary() || isJavaLibrary() || isPackLibrary() || isJavaPlatform()
}
private def getRuntimeClasspath(variant){
def runtimeClasspath
try {
runtimeClasspath = variant.variantData.variantDependencies.runtimeClasspath
} catch (Exception e) {
logError "getRuntimeClasspath: e = ${e.message}"
runtimeClasspath = variant.variantData.variantDependency.runtimeClasspath
}
return runtimeClasspath
}
private def resolveConfigurations(Collection configurations, Set resolvedConfigurations) {
configurations.each {
resolvedConfigurations.add(it)
resolveConfigurations(it.extendsFrom, resolvedConfigurations)
}
}
private def findBundle(variant){
def bundle
if(variant == null){
bundle = project.tasks.findByName("jar")
}else{
bundle = project.tasks.findByName("bundle${variant.flavorName.capitalize()}Release")
if(bundle == null){
bundle = project.tasks.findByName("bundle${variant.flavorName.capitalize()}ReleaseAar")
}
}
return bundle
}
private def maybeCreateJavadoc(variant){
def javadoc = project.tasks.findByName('javadoc')
if(javadoc != null){
return javadoc
}else{
def name
if(variant == null){
name = 'javadoc'
}else{
def flavorName = variant.flavorName
name = "${flavorName}${isEmpty(flavorName) ? 'j' : 'J'}avadoc"
}
return project.tasks.create(name, Javadoc){
failOnError false
source getSrcDirs(variant)
classpath += project.files(project.android.getBootClasspath().join(File.pathSeparator))
classpath += getJavaClasspath(variant)
exclude '**/R.java', '**/BuildConfig.java'
}
}
}
private def getJavaClasspath(variant){
def javaClasspath
try {
javaClasspath = variant.javaCompileProvider.get().classpath
}catch(Exception e){
logError "getJavaClasspath: e = ${e.message}"
javaClasspath = variant.javaCompile.classpath
}
return javaClasspath
}
private def getSrcDirs(variant){
def srcDirs
if(variant == null){
srcDirs = project.sourceSets.main.allSource
}else {
try {
srcDirs = variant.variantData.variantSources.sortedSourceProviders.collect {
it.getJavaDirectories()
}
} catch (Exception e) {
logError "getSrcDirs: e = ${e.message}"
srcDirs = variant.variantData.javaSources.collect {
it.getDir()
}
}
if(srcDirs == null){
srcDirs = new HashSet()
srcDirs.addAll(project.android.sourceSets.main.java.srcDirs)
srcDirs.addAll(project.android.sourceSets.release.java.srcDirs)
if(!isEmpty(variant.flavorName)){
srcDirs.addAll(project.android.sourceSets."${variant.flavorName}".java.srcDirs)
srcDirs.addAll(project.android.sourceSets."${variant.flavorName}Release".java.srcDirs)
}
}
}
return srcDirs
}
private def loadLocalProperties(){
File localPropsFile = project.rootProject.file('local.properties')
if (localPropsFile.exists()) {
Properties props = new Properties()
props.load(new FileInputStream(localPropsFile))
props.each {name, value ->
project.ext[name] = value
}
return true
} else {
return false
}
}
private def check(){
if(!isSupportPublish()){
logError '目前只支持android工程、java工程、java-platform工程和pack打包的组件发布,暂不支持其他平台的组件发布'
return false
}
def isOnlyPackLib = true
def isPackLib = false
if(isPackLibrary()){
log '项目是一个pack工程'
isPackLib = true
}
if(isJavaLibrary()){
log '项目是一个java工程'
isOnlyPackLib = false
}
if(isJavaPlatform()){
log "项目是一个java-platform工程"
isOnlyPackLib = false
}
if(isAndroidLibrary()){
log "项目是一个android工程"
def version = getAGPVersion()
if(version < '3.0.0'){
logError '请升级android gradle pulgin版本, 不支持3.0.0以下的版本'
return false
}else{
log "AGP version = ${version}"
}
isOnlyPackLib = false
}
def gav = getGAV()
if(isPackLib && isOnlyPackLib){
if(isEmpty(gav[0])
|| isEmpty(gav[2])
){
logError '组件的GV不可以为空'
return false
}else{
log "GAV = ${gav[0]}:null:${gav[2]}"
}
}else{
if(isEmpty(gav[0])
|| isEmpty(gav[1])
|| isEmpty(gav[2])
){
logError '组件的GAV不可以为空'
return false
}else{
log "GAV = ${gav[0]}:${gav[1]}:${gav[2]}"
}
}
if(!loadLocalProperties()){
log '项目根目录下local.properties文件不存在'
}
if(readBoolean(publishIsSkipSignature)){
log '跳过签名校验'
}else{
if(!containProperty(signingKeyId)
|| !containProperty(signingPassword)
|| !containProperty(signingSecretKeyRingFile)
){
logError 'gpg的签名信息不可以为空'
return false
}else{
log "signingKeyId = ${readProperty(signingKeyId)}"
log "signingPassword = ${readProperty(signingPassword)}"
log "signingSecretKeyRingFile = ${readProperty(signingSecretKeyRingFile)}"
}
}
if(readBoolean(publishIsSkipCredential)){
log '跳过账号校验'
}else{
if(!containProperty(ossrhUsername)
|| !containProperty(ossrhPassword)
){
logError 'ossrh的账号信息不可以为空'
return false
}else{
log "ossrhUsername = ${readProperty(ossrhUsername)}"
log "ossrhPassword = ${readProperty(ossrhPassword)}"
}
}
return true
}
}
然后再在你的组件的build.gradle
中apply
该脚本:
apply from: "${rootProject.rootDir.path}/script/publication.gradle"
接下来准备好你在前面注册的Sonatype
账号、申请好的groupId
、生成好的gpg私钥信息
,然后在组件的gradle.properties(没有则创建)
中添加组件信息,其中GAV坐标
是必填信息,其他是可选信息:
### GAV坐标
publish.groupId=io.github.ydstar
publish.artifactId=uikit
publish.version=1.0.0
### 下面都是可选信息
# 基本描述
publish.description=发布组件到Maven仓库的gradle脚本,支持aar和jar发布
publish.url=https://github.com/ydstar/UiKit
# 开发者信息
publish.developerName=houyadong
[email protected]
# license信息
publish.licenseName=The Apache License, Version 2.0
publish.licenseUrl=http://www.apache.org/licenses/LICENSE-2.0.txt
# 如果发布的是android组件,当为false时不根据flavor动态生成组件的artifactId,如果你不想组件的artifactId拼接flavorName,可以设置为false,默认为true
publish.artifactId.isAppendFavorName=false
然后在项目根目录的local.properties(没有则创建)
中添加gpg签名信息
和ossrh账号信息
,记得要把local.properties
从你的版本控制中移除,避免泄漏你的签名信息和账号信息:
# gpg签名信息
signing.keyId=你的密钥id,例如0FF22B1C
signing.password=你的私钥密码
signing.secretKeyRingFile=你的私钥文件路径,例如/Users/houyadong/key/0FF22B1C.gpg
# ossrh账号信息
ossrh.username=你的ossrh账号名,即Sonatype账号用户名
ossrh.password=你的ossrh账号密码,即Sonatype账号密码
最后在命令行执行gradle任务发布组件,如果你发布的是android组件(aar)
,执行的任务的名称格式为publish{flavorName}AndroidlibPublicationToMavenRepository
或publish AndroidlibPublicationToMavenLocal
,如果你发布的是java组件(jar)
,执行的任务名称为publishJavalibPublicationToMavenRepository
或publishJavalibPublicationToMavenLocal
:
//发布所有android组件和java组件到maven本地仓库和maven远程release或snapshot仓库
gradle publish
//发布所有android组件和java组件到maven远程release或snapshot仓库
gradle publishAllPublicationsToMavenRepository
//发布android组件到maven本地仓库
gradle publishAndroidlibPublicationToMavenLocal
//发布android组件到maven远程release或snapshot仓库
gradle publishAndroidlibPublicationToMavenRepository
//发布java组件到maven本地仓库
gradle publishJavalibPublicationToMavenLocal
//发布java组件到maven远程release或snapshot仓库
gradle publishJavalibPublicationToMavenRepository
//发布所有android组件和java组件到maven本地仓库
gradle publishToMavenLocal
上述发布任务根据组件发布的是android组件(aar)
还是java组件(jar)
来执行,publishXXToMavenRepository
任务表示发布到远程仓库OSSRH
中,publishXXToMavenLocal
任务表示发布到maven
本地仓库,maven
本地仓库路径为{用户目录}/.m2/repository/
,也可以在AS左侧
的gradle
面板中查看这些任务,如下:
选中合适的任务双击执行就行,如果你执行的是publishXXToMavenRepository
任务,任务执行成功后你就可以到OSSRH查看你发布上去的组件,首次进入需要登陆,输入你的Sonatype
用户名和密码登陆,然后点击左下侧的Staging Repository就可以看到你发布到OSSRH
的组件,如下:
在Staging Repository
的面板中可以浏览你发布的组件,选中它,然后点击下面的content面板
就可以查看组件发布的详细文件。
同步到MavenCentral
组件发布到OSSRH
后,还不能通过GAV
引用它,还要同步到MavenCentral
后才能通过GAV
引用它,首先要在OSSRH
上Close
组件,让它从open状态
变为close状态
,如下:
当你点击Close
按钮后,会弹出一个弹窗叫你确认,确认后Close
按钮会变灰,此时下方的Activity
面板会显示close
过程中进行的规则校验,例如校验你发布的文件是否有签名、是否含有javadoc文件
、是否含有sources文件
,pom文件
是否合格等,当所有校验通过后你才可以同步组件到MavenCentral
,当校验通过后你会收到Sonatype
发送的邮件,此时你可以点击上方的Release
按钮同步组件到MavenCentral
,如下:
当你点击Release
按钮后,会弹出一个弹窗叫你确认,确认后Release
按钮会变灰,如果你是第一次发布组件,在申请groupId
时我讲过,你还要到OSSRH-{taskid}的comment
面板下回复它一次OSSRH
才会为你激活同步程序,激活同步程序后大概十几分钟你的组件就会被同步到MavenCentral
,这时你就可以在MavenCentral网站搜索到你的组件,并可以在gradle
或maven
等构建工具中通过GAV
引用它,激活同步程序后,你下次Release
组件时就会自动同步到MavenCentral
而无需再做其他操作。
结语
发布到MavenCentral
的步骤相比于发布到Jitpack
要复杂的多,Jitpack
只需要一个代码托管仓库账号,而MavenCentral
需要准备Sonatype账号
、个人域名、签名信息,但是MavenCentral
比Jitpack
成熟得多,它目前是最大的java组件
和其他开源组件的托管仓库,也是很多构建系统如Maven
的默认存储仓库,如何选择就看个人爱好了。