发布AAR/JAR/SourceCode到Maven中央仓库(最全)

1,809 阅读13分钟

前言

之前由于公司业务需要, 就捣鼓了一下maven远程发布,期间遇到了一些坑,正好最近也有朋友在做这块,就详细整理了一下.

注册 sonatype的Jira 帐号

注册地址: issues.sonatype.org/secure/Sign… 进入注册页面用过英文+数字组合的邮箱地址进行注册即可,任何变动issue都会以邮件方式进行通知.

登录Jira

注册完成后就登录: issues.sonatype.org/login.jsp

创建一个 Issue

Issue创建后的效果: issues.sonatype.org/browse/OSSR…. 进去后客人一点其中一个issue参考.

创建地址: issues.sonatype.org/secure/Crea… 选择【Community Support - Open Source Project Repository Hosting (OSSRH)】.

问题类型:选择【New Project】.

概要 :自己填

描述 :自己填, 英文说明为主, 非必填.

Group Id::如果用的是GitHub, 一定要是 com.github. 你的github用户名, 当然也可以使用GitHub的Page的域名io.github.你的github用户名. 如果有自己(公司)的域名和项目地址也可以, 官方人员会询问你是否有这个域名的所有权. 在你项目的pom里一定要使用这个groupId, 最好包路径也使用. Group Id定义为对外域名(com.xxx)开头: 【com.xxx.sdk】

Project URL: 是你的项目地址. 【gitlab.xxx/project.git…

SCM url: 是你的项目git地址. 【gitlab.xxx/project.git…

Username(s): 可以不用填,这是能辅助你提交项目的合作人的帐号,前提是他也得在这个Jira注册.

其他使用默认值就行了.

创建好Issue后就等待官方回复吧.通常如果在晚上的话回复会比较快,运气好几分钟就有回复了

当看到回复类似

com.xxx.sdk has been prepared, now user(s) myName can:

Deploy snapshot artifacts into repository oss.sonatype.org/content/rep… Deploy release artifacts into the staging repository oss.sonatype.org/service/loc… Release staged artifacts into repository 'Releases' please comment on this ticket when you promoted your first release, thanks

说明OK了,可以提交了. 假如使用的公司域名,官方会要求提供域名拥有者证明.如:

Do you own the domain xxx.com? If so, please verify ownership via one of the following methods:

Add a TXT record to your DNS referencing this JIRA ticket: OSSRH-xxx (Fastest) Setup a redirect to your Github page (if it does not already exist) If you do not own this domain, please read: central.sonatype.org/pages/choos… You may also choose a groupId that reflects your project hosting, in this case, something like

Would you like to use a free managed security reporting service (recommended)? Put hackerone.com/central-sec… as your project's security issue reporting URL. We'll take care of the rest. For more details on the Central Security Project, visit www.sonatype.com/central-sec…

这个里面告诉你, 最快的办法就是在你的域名解析中, 添加一条text记录, 具体操作如下:

进入到域名商网站, 登录之后选择域名解析

点击添加记录, 记录类型选择【txt】

主机记录是问题编号.【ossrh-xxx】

记录值写你的问题URL. 【issues.sonatype.org/browse/OSSR…

其他都不需要改, 点击确定

然后在issue回复下:

Yes, this domain is my own.

I have added the corresponding TXT resolution record.

如果填的没问题的话, 大概10分钟左右, 你就会收到审核通过的消息, 告诉你可以上传资源了(如之前的回复).

使用 GPG 生成密钥对(发布到远程库需要签名并验证)

安装GPG

本人使用mac电脑,因此使用brew安装的,很简单,打开终端,输入brew install gpg就行了,至于其他的平台,可以自行搜索。

Mac-mini:~ cjj$ brew install gpg

==> Downloading homebrew.bintray.com/bottles/gnu…

######################################################################## 100.0%

==> Pouring gnupg-1.4.20.el_capitan.bottle.tar.gz

/usr/local/Cellar/gnupg/1.4.20: 53 files, 5.4M

安装完成后,键入命令gpg --help:

Mac-mini:~ cjj$ gpg --help

gpg (GnuPG) 1.4.20

Copyright (C) 2015 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later gnu.org/licenses/gp…

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.

Home: ~/.gnupg

支持的算法:

公钥:RSA, RSA-E, RSA-S, ELG-E, DSA

对称加密:IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, CAMELLIA128, CAMELLIA192, CAMELLIA256

散列:MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224

压缩:不压缩, ZIP, ZLIB, BZIP2

语法:gpg [选项] [文件名]

签名、检查、加密或解密

默认的操作依输入数据而定

指令:

-s, --sign [文件名]        生成一份签名

--clearsign [文件名]   生成一份明文签名

-b, --detach-sign             生成一份分离的签名

-e, --encrypt                 加密数据

-c, --symmetric               仅使用对称加密

-d, --decrypt                 解密数据(默认)

--verify                  验证签名

--list-keys               列出密钥

--list-sigs               列出密钥和签名

--check-sigs              列出并检查密钥签名

--fingerprint             列出密钥和指纹

  -K, --list-secret-keys        列出私钥

--gen-key                 生成一副新的密钥对

--delete-keys             从公钥钥匙环里删除密钥

--delete-secret-keys      从私钥钥匙环里删除密钥

--sign-key                为某把密钥添加签名

--lsign-key               为某把密钥添加本地签名

--edit-key                编辑某把密钥或为其添加签名

--gen-revoke              生成一份吊销证书

--export                  导出密钥

--send-keys               把密钥导出到某个公钥服务器上

--recv-keys               从公钥服务器上导入密钥

--search-keys             在公钥服务器上搜寻密钥

--refresh-keys            从公钥服务器更新所有的本地密钥

--import                  导入/合并密钥

--card-status             打印智能卡状态

--card-edit               更改智能卡上的数据

--change-pin              更改智能卡的 PIN

--update-trustdb          更新信任度数据库

--print-md 算法 [文件]   

                          使用指定的散列算法打印报文散列值

选项:

-a, --armor                   输出经 ASCII 封装

-r, --recipient 某甲        为收件者“某甲”加密

-u, --local-user              使用这个用户标识来签名或解密

-z N                          设定压缩等级为 N (0 表示不压缩)

--textmode                使用标准的文本模式

-o, --output                  指定输出文件

-v, --verbose                 详细模式

-n, --dry-run                 不做任何改变

-i, --interactive             覆盖前先询问

--openpgp                 行为严格遵循 OpenPGP 定义

--pgp2                    生成与 PGP 2.x 兼容的报文

(请参考在线说明以获得所有命令和选项的完整清单)

范例:

-se -r Bob [文件名]          为 Bob 这个收件人签名及加密

--clearsign [文件名]         做出明文签名

--detach-sign [文件名]       做出分离式签名

--list-keys [某甲]           显示密钥

--fingerprint [某甲]         显示指纹

这些帮助信息非常有用,下边演示的很多功能也是基于上边这些参数的。这里把他们列出来,方便在使用的时候查询。 如果能够显示上边的信息,说明GPG安装成功了

生成密钥(需要翻墙)

安装成功后,使用gen-ken参数生成自己的密钥。按如下步骤在终端中输入:

生成一个新的签名文件(可选0生成无期限签名)

gpg --full-generate-key

gpg (GnuPG/MacGPG2) 2.2.20; Copyright (C) 2020 Free Software Foundation, Inc.

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.

请选择您要使用的密钥类型:

(1) RSA 和 RSA (默认)

(2) DSA 和 Elgamal

(3) DSA(仅用于签名)

(4) RSA(仅用于签名)

(14) Existing key from card

您的选择是? 1

RSA 密钥的长度应在 1024 位与 4096 位之间。

您想要使用的密钥长度?(2048) #这里直接回车

请求的密钥长度是 2048 位

请设定这个密钥的有效期限。

     0 = 密钥永不过期

  <n>  = 密钥在 n 天后过期

  <n>w = 密钥在 n 周后过期

  <n>m = 密钥在 n 月后过期

  <n>y = 密钥在 n 年后过期

密钥的有效期限是?(0) #这里直接回车

密钥永远不会过期

这些内容正确吗? (y/N) y

GnuPG 需要构建用户标识以辨认您的密钥。

真实姓名: aaa

电子邮件地址: bbb@gmail.com

注释:

您选定了此用户标识:

“aaa bbb@gmail.com

更改姓名(N)、注释(C)、电子邮件地址(E)或确定(O)/退出(Q)? O
#注意:这时候会有个弹窗要求输入签名密码(后续发布会用到)

我们需要生成大量的随机字节。在质数生成期间做些其他操作(敲打键盘 、移动鼠标、读写硬盘之类的)将会是一个不错的主意;这会让随机数 发生器有更好的机会获得足够的熵。

我们需要生成大量的随机字节。在质数生成期间做些其他操作(敲打键盘 、移动鼠标、读写硬盘之类的)将会是一个不错的主意;这会让随机数 发生器有更好的机会获得足够的熵。

gpg: 密钥 560BE3C8D4B75F28 被标记为绝对信任

gpg: 吊销证书已被存储为‘/Users/cjj/.gnupg/openpgp-revocs.d/F6FDDFE15FE779CBD917080B560BE3C8D4B75F28.rev’

公钥和私钥已经生成并被签名。

pub rsa2048 2020-08-01 [SC]

F6FDDFE15FE779CBD917080B560BE3C8D4B75F28

uid aaa bbb@gmail.com

sub rsa2048 2020-08-01 [E]

列出目前的签名文件列表

gpg --list-keys

为这个密钥创建一个吊销证书(可选) gpg --gen-revoke 560BE3C8D4B75F28

删除其中一个签名(可选)

#gpg --delete-key 85E38F69046B44C1EC9FB07B76D78F0500D026C4

导出公钥到public-key.txt

gpg --armor --output public-key.txt --export F6FDDFE15FE779CBD917080B560BE3C8D4B75F28

导出私钥到public-key.txt

gpg --armor --output private-key.txt --export-secret-keys

发送到公钥服务器

gpg --send-keys F6FDDFE15FE779CBD917080B560BE3C8D4B75F28 --keyserver hkp://subkeys.pgp.net

查询公钥F6FDDFE15FE779CBD917080B560BE3C8D4B75F28是否发布成功

gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys F6FDDFE15FE779CBD917080B560BE3C8D4B75F28

对公钥生成指纹(由于任何人都可以用你的名义上传公钥,我们可以生成公钥指纹,好让他人校验)

gpg --fingerprint F6FDDFE15FE779CBD917080B560BE3C8D4B75F28

导出gpg私钥

gpg --export-secret-keys -o \dir\secring.gpg

AndroidStudio发布

准备

用户空间的.gradle文件夹里需创建"gradle.properties"来存储签名的密钥和maven账户信息,此外,还需要把签名密钥gpg文件也一起放到.gradle文件夹里进行维护和引用.

Module中创建"gradle.properties"来存储发布信息,如版本号、groupId、artifactId等等

关键步骤

aar/jar文件的方式需要用'maven-publish'插件, 通过“artifact” api来指定aar文件的路径, 这里用的相对路径; 签名方式需要用“publishing.publications.maven”; 在module的"build.gradle"引用发包gradle文件,如: apply from: './maven_aar.gradle' 执行gradle task中的"publish"任务即可发布aar到远程仓库.

源码发布方式需要用'maven'插件, 通过“archives androidSourcesJar”和“pom.project.packaging”来打包发布源码; 签名方式需要用“configurations.archives”; 在module的"build.gradle"引用发包gradle文件,如: apply from: './maven.gradle' 执行gradle task中的uploadArchives任务即可发布aar到远程仓库.

发布aar文件方式
maven_aar.gradle
apply plugin: 'maven-publish'
apply plugin: 'signing'
//发布AAR文件
def getRepositoryUsername() {
    return hasProperty('MAVEN_USERNAME') ? MAVEN_USERNAME : ""
}

def getRepositoryPassword() {
    return hasProperty('MAVEN_PASSWORD') ? MAVEN_PASSWORD : ""
}

def getReleaseRepositoryUrl() {
    return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
            : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}

def getSnapshotRepositoryUrl() {
    return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
            : "https://oss.sonatype.org/content/repositories/snapshots/"
}

publishing {
    publications {
        maven(MavenPublication) {
            groupId POM_GROUP_ID
            artifactId POM_ARTIFACT_ID
            version VERSION_NAME
            pom {
                name = POM_NAME
                description = POM_DESCRIPTION
                url = POM_URL
                inceptionYear = POM_INCEPTION_YEAR

                scm {
                    url = POM_SCM_URL
                    connection = POM_SCM_CONNECTION
                    developerConnection = POM_SCM_DEV_CONNECTION
                }

                licenses {
                    license {
                        name = POM_LICENCE_NAME
                        url = POM_LICENCE_URL
                        distribution = POM_LICENCE_DIST
                        comments = POM_LICENCE_COMMENTS
                    }
                }

                developers {
                    developer {
                        id = POM_DEVELOPER_ID
                        name = POM_DEVELOPER_NAME
                        email = POM_DEVELOPER_EMAIL
                        url = POM_DEVELOPER_URL
                    }
                }

                issueManagement {
                    system = POM_ISSUE_MANAGEMENT_SYSTEM
                    url = POM_ISSUE_MANAGEMENT_URL
                }
            }
            // Tell maven to prepare the generated "*.jar/*.aar" file for publishing
            artifact("./libs/${POM_ARTIFACT}")
        }
    }
    repositories {
        maven {
            // 指定要上传的maven仓库
            url = VERSION_NAME.contains('SNAPSHOT') ? getSnapshotRepositoryUrl() : getReleaseRepositoryUrl()
            //认证用户和密码
            credentials {
                username getRepositoryUsername()
                password getRepositoryPassword()
            }
        }
    }
}
signing {
    sign publishing.publications.maven
}
gradle.properties

如发布网络管理库


VERSION_NAME=3.0.1

POM_NAME=sdk-lib
POM_ARTIFACT_ID=sdk-lib
POM_GROUP_ID=com.xxx.sdk
POM_ARTIFACT=sdk-lib-3.0.1.aar   
POM_URL=https://gitlab.xxx/sdkproject
POM_PACKAGING=aar
POM_DESCRIPTION=直播sdk网络库封装的组件库

POM_SCM_URL=https://gitlab.lizhi.fm/androidComponent/NetworkManager
POM_SCM_CONNECTION=scm:git@https://gitlab.lizhi.fm/androidComponent/NetworkManager.git
POM_SCM_DEV_CONNECTION=scm:git@https://gitlab.lizhi.fm/androidComponent/NetworkManager.git

POM_LICENCE_COMMENTS=A business-friendly OSS license
POM_LICENCE_NAME=The Apache Software License, Version 2.0
POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo

POM_DEVELOPER_ID= xxx    #自行填写开发者id
POM_DEVELOPER_NAME= xxx   #自行填写开发者姓名
POM_DEVELOPER_EMAIL= xxx   #自行填写开发者邮箱
POM_DEVELOPER_URL=https://www.lizhi.fm/
POM_ISSUE_MANAGEMENT_SYSTEM=GitLab
POM_ISSUE_MANAGEMENT_URL=https://gitlab.lizhi.fm/androidComponent/NetworkManager/issues
POM_INCEPTION_YEAR=2020

如果发布jar包,把上面的POM_PACKAGING改为“jar”, POM_ARTIFACT为jar的路径如sdk-lib-3.0.1.jar(放在libs目录下)

发布源码方式
maven.gradle
apply plugin: 'maven'
apply plugin: 'signing'

def isReleaseBuild() {
    return VERSION_NAME.contains("SNAPSHOT") == false
}

def getReleaseRepositoryUrl() {
    return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
            : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}

def getSnapshotRepositoryUrl() {
    return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
            : "https://oss.sonatype.org/content/repositories/snapshots/"
}

def getRepositoryUsername() {
    return hasProperty('MAVEN_USERNAME') ? MAVEN_USERNAME : ""
}

def getRepositoryPassword() {
    return hasProperty('MAVEN_PASSWORD') ? MAVEN_PASSWORD : ""
}

afterEvaluate { project ->
    uploadArchives {
        repositories {
            mavenDeployer {
                configurePOM(pom)

                beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }

                repository(url: getReleaseRepositoryUrl()) {
                    authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
                }
                snapshotRepository(url: getSnapshotRepositoryUrl()) {
                    authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
                }
            }
        }
    }
    signing {
        required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
        sign configurations.archives
    }

    task androidSourcesJar(type: Jar) {
        classifier = 'sources'
        from android.sourceSets.main.java.sourceFiles
    }

    artifacts {
        archives androidSourcesJar
    }
}

private configurePOM(def pom) {
    pom.project {
        groupId POM_GROUP_ID
        artifactId POM_ARTIFACT_ID
        version VERSION_NAME
        name POM_NAME
        packaging POM_PACKAGING
        description POM_DESCRIPTION
        url POM_URL
        inceptionYear POM_INCEPTION_YEAR

        scm {
            url POM_SCM_URL
            connection POM_SCM_CONNECTION
            developerConnection POM_SCM_DEV_CONNECTION
        }

        licenses {
            license {
                name POM_LICENCE_NAME
                url POM_LICENCE_URL
                distribution POM_LICENCE_DIST
                comments POM_LICENCE_COMMENTS
            }
        }

        developers {
            developer {
                id POM_DEVELOPER_ID
                name POM_DEVELOPER_NAME
                email POM_DEVELOPER_EMAIL
                url POM_DEVELOPER_URL
            }
        }

        issueManagement {
            system POM_ISSUE_MANAGEMENT_SYSTEM
            url POM_ISSUE_MANAGEMENT_URL
        }
    }
}
gradle.properties

如发布sdk源码:

VERSION_NAME=1.0.28

POM_NAME=sdk-lib
POM_ARTIFACT_ID=sdk-lib
POM_GROUP_ID=com.xxx.sdk
POM_URL=https://gitlab.xxx/sdkproject
POM_PACKAGING=aar
POM_DESCRIPTION=这里写SDK的简介

POM_SCM_URL=https://gitlab.xxx/sdkproject
POM_SCM_CONNECTION=scm:git@https://gitlab.xxx/sdkproject.git
POM_SCM_DEV_CONNECTION=scm:git@https://gitlab.xxx/sdkproject.git

POM_LICENCE_COMMENTS=A business-friendly OSS license
POM_LICENCE_NAME=The Apache Software License, Version 2.0
POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo

POM_DEVELOPER_ID= xxx    #自行填写开发者id
POM_DEVELOPER_NAME= xxx   #自行填写开发者姓名
POM_DEVELOPER_EMAIL= xxx   #自行填写开发者邮箱
POM_DEVELOPER_URL=https://www.xxx.com/  #你的个人域名/公司域名
POM_ISSUE_MANAGEMENT_SYSTEM=GitLab
POM_ISSUE_MANAGEMENT_URL=https://gitlab.xxx/sdkproject/issues
POM_INCEPTION_YEAR=2020  #开发时间
gradle.properties

本地电脑用户空间的.gradle目录下gradle.properties文件:

MAVEN_USERNAME=xxx      #自行填写maven 账号
MAVEN_PASSWORD=xxx      #自行填写maven账号密码

signing.keyId=xxx      #自行填写gpg证书后八位
signing.password=xxx    #自行填写gpg证书密码
signing.secretKeyRingFile=/Users/cjj/.gradle/private-key.gpg   #gpg文件存储路径

中央仓库操作步骤

执行发布task后即可登录maven中央仓库:oss.sonatype.org/#stagingRep…

点击Staging repositories;

在右侧搜索框输入你的group id, 然后点击refresh, 就会看到你的提交信息;

选中, 点击close, 这时当前的纪录就会变成一个小齿轮, 表示nexus在校验你的jar,按照剧本, 你的提交应该会全部通过(图标显示数字就是失败, 反之则是成功.进度可在activity窗口查看);

再次选中当前记录, 点击refresh,点击release, 就会上传成功了, 这时sonatype会给你发邮件并默认清空暂存仓库该次记录;

注意: 如果是snapshot版本, 则不需要审核, 直接上传就可以直接引用, 但是在maven仓库(search.maven.org/)是搜索不到的,Artifact Search中却可以搜到.

发布完成

以上这些步骤都操作完后,issue官方会给一条同步回复,意思大致就是10分钟左右发布到Central,search.maven.org最多需要2小时.

Central sync is activated for com.xxx.sdk. After you successfully release, your component will be published to Central, typically within 10 minutes, though updates to search.maven.org can take up to two hours.

引用

发布完在Artifact Search中搜到目标库后就可以通过maven的仓库地址来引用目标库了(这时候还没有同步到阿里仓库, 因此需要使用maven的仓库地址). maven仓库引用后会加快阿里仓库的同步进度.

maven仓库地址: https://oss.sonatype.org/service/local/staging/deploy/maven2/

maven snapshot仓库地址: https://oss.sonatype.org/content/repositories/snapshots/

阿里仓库地址: http://maven.aliyun.com/nexus/content/groups/public

在Project的"build.gradle"中配置如下信息:

buildscript 
    repositories {
        //公共
        maven {
            url 'https://maven.google.com/'
            name 'Google'
        }
        //阿里云仓库
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
        jcenter()
        //snapshot仓库
        maven { url  "https://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url "https://maven.aliyun.com/repository/public/" }
        //maven中央仓库
        maven { url "https://oss.sonatype.org/service/local/staging/deploy/maven2/" }
    }
  ...
}

allprojects {
    repositories {
        //公共
        maven {
            url 'https://maven.google.com/'
            name 'Google'
        }
        //阿里云仓库
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
        jcenter()
        //snapshot仓库
        maven { url  "https://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url "https://maven.aliyun.com/repository/public/" }
        //maven中央仓库
        maven { url "https://oss.sonatype.org/service/local/staging/deploy/maven2/" }
    }
    ...
}

踩坑

1、开发者信息等信息不全导致上传失败.至少第一次上传需配置全信息,不然maven那边是不会通过的;

2、签名文件格式需gpg格式.之前使用txt格式的在close的时候一直不能通过验证;

3、maven服务器负载问题.maven真的很不稳定,期间上传操作稍微比较频繁,然后服务器就超负载了,结果就是在暂存仓库堆积了一大堆的上传记录,drop操作也不响应.最后的解决办法就是去issue留言让官方去处理了.

4、假如开发/发布有时间要求,一定要注意:maven这边是跟我们日夜颠倒的,有问题一般他们在中国的晚上(他们的白天)才回应,因此,定计划时一定要预留充足的发布时间(特别是第一次发布).