Android_发布library到mavenCentral(2022年最新版本)

前言

之前写过一篇Android_上传library到Bintray并发布到JCenter的文章,但不幸的是几个月前Jfrog发布了终止Bintray服务的声明,声明的大概意思是说2021年3月31号之后Jcenter仓库将不再接收用户的组件提交,同时将Jcenter设为只读代码仓库,无限期地提供现有组件供用户下载,也就是说目前Jcenter仓库的状态是你无法再提交组件的更新,但你可以继续下载你以前托管的组件版本,所以现在你要做的就是把你的组件的新版本发布到其他仓库,例如JitpackMavenCentral,本篇文章的内容是教你如何发布组件到MavenCentral仓库,同时搞了一个发布脚本简化发布过程。

前期准备

在讲解之前,有必要介绍一下SonatypeOSSRHMavenCentral之间的关系,Sonatype是一家公司,它运营着MavenCentral仓库,我们想要发布组件到MavenCentral,必须要通过SonatypeOSSRHOSSRHOSS 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坐标来定位的,GAVgroupIdartifactIdversion,其中groupId你可以理解为你自己在Sonatype OSSRH创建的仓库,groupId就是你仓库的名称,申请groupId就是在Sonatype OSSRH申请创建属于你的仓库,我们后面发布组件时要先发布到Sonatype OSSRH上名为groupId的仓库,然后才能同步到MavenCentral仓库

还有申请的groupId并不是随便填的,按照Sonatype的要求,groupId必须要是一个域名的反写,所以你要拥有一个域名,当你申请groupId时,Sonatype会用某种方式让你证明你是这个域名的所有者。

如果你拥有某个域名,例如example.com域名,你可以使用任何以com.example开头的groupId,例如com.example.test1com.example.test2等,如果你没有自己的域名也没关系,Sonatype支持代码托管平台的Pages网站域名,例如Github,你可以在你的Github账号上开启你的Pages服务,这样你就拥有了一个与Github账号关联的个人域名,格式为{username}.github.io,例如我的Github Pages网站就是ydstar.github.io,很多人都是利用这种托管在三方平台的网站搭建自己的博客网站,除了GithubSonatype还支持GitLabGitee等,下面表格列出这些常用的代码托管平台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.ydstargroupId,首先打开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也是随便填一个你的组件仓库版本控制地址,如下:

最后点击新建按钮,它会创建一个issueissue名称格式为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插件,我们主要发布的内容有组件的aarjar文件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.gradleapply该脚本:

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}AndroidlibPublicationToMavenRepositorypublish AndroidlibPublicationToMavenLocal,如果你发布的是java组件(jar),执行的任务名称为publishJavalibPublicationToMavenRepositorypublishJavalibPublicationToMavenLocal

//发布所有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引用它,首先要在OSSRHClose组件,让它从open状态变为close状态,如下:

当你点击Close按钮后,会弹出一个弹窗叫你确认,确认后Close按钮会变灰,此时下方的Activity面板会显示close过程中进行的规则校验,例如校验你发布的文件是否有签名、是否含有javadoc文件、是否含有sources文件pom文件是否合格等,当所有校验通过后你才可以同步组件到MavenCentral,当校验通过后你会收到Sonatype发送的邮件,此时你可以点击上方的Release按钮同步组件到MavenCentral,如下:

当你点击Release按钮后,会弹出一个弹窗叫你确认,确认后Release按钮会变灰,如果你是第一次发布组件,在申请groupId时我讲过,你还要到OSSRH-{taskid}的comment面板下回复它一次OSSRH才会为你激活同步程序,激活同步程序后大概十几分钟你的组件就会被同步到MavenCentral,这时你就可以在MavenCentral网站搜索到你的组件,并可以在gradlemaven等构建工具中通过GAV引用它,激活同步程序后,你下次Release组件时就会自动同步到MavenCentral而无需再做其他操作。

结语

发布到MavenCentral的步骤相比于发布到Jitpack要复杂的多,Jitpack只需要一个代码托管仓库账号,而MavenCentral需要准备Sonatype账号、个人域名、签名信息,但是MavenCentralJitpack成熟得多,它目前是最大的java组件和其他开源组件的托管仓库,也是很多构建系统如Maven的默认存储仓库,如何选择就看个人爱好了。

你可能感兴趣的:(Android_发布library到mavenCentral(2022年最新版本))