自动生成SDK 上传Maven仓库的版本号插件

在Android开发中,通过maven仓库管理SDK的版本、上传、依赖是很常见的方式。而其中版本号的管理是很多人忽略的点。本篇以实现一个自动管理SDK上传maven版本号的gradle插件为目标,说一说其中的门道。

通过Maven管理SDK包

三类第三方库

Android开发中,我们以『包』的形式引入第三方的库,大致可以分为三类:

  • 以C和C++为语言生成的动态连接库,也就是so文件
  • 以Java为语言生成的归档文件,也就是Jar文件
  • Android独有的包含资源文件的Android模块包,也就是aar文件

Gradle支持对maven仓库包依赖

通常除了直接导入包文件到项目中之外,Android官方提供的默认的依赖及编译工具Gradle,支持项目直接引入对Maven库中已有的资源包进行依赖。作为Android开发者而言,再熟悉不过了。例如依赖okhttp,我们可能只需要在模块的build.gradle文件中加入这么一行:

1
implementation 'com.squareup.okhttp3:okhttp:3.12.0'

maven插件上传SDK

对于SDK开发者而言,通常我们也是期望把自己的SDK上传到指定的maven仓库中,而客户端只需要简单的申明对SDK的某个版本的依赖,就可以了。通常情况下,我们会通过一个叫「maven」的gradle插件,来帮助我们上传SDK到maven仓库中。我们来简单回顾一下步骤:

首先是在模块的build.gralde下,申明引入『maven』插件:

1
apply plugin: 'maven'

假设我们的SDK包含Android的资源,也就是生成aar文件,那么我们申明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uploadArchives{
repositories {
mavenDeployer {
repository(url: release_repository_url) {
authentication(userName: "your user name", password: "your password")
}
snapshotRepository(url: snapshot_repository_url) {
authentication(userName: "your user name", password: "your password")
}
pom.version = "版本名称"
pom.project {
artifactId = "your artifactId"
groupId = "your group id"
packaging = 'aar'
}
}
}
}

以前面说的okhttp为例,”com.squareup.okhttp3”就是groupId, “okhttp”是artifactId,版本号则是在pom.version处申明。

到这里,回顾完一个Android SDK基本的上传流程了。接下来我们来说下版本号的问题。

SDK版本号

正式版和快照版

maven仓库通常分为snapshot快照仓库和release发布仓库,snapshot快照仓库用于保存开发过程中的不稳定版本,release正式仓库则是用来保存稳定的发行版本。具体到名称,只需要在该模块的版本号后加上-SNAPSHOT即可(大写)。maven仓库的管理允许统一版本名的快照包重复更新,但是对于发布版本则不允许修改,必须升级版本号。

A.B.C

对于SDK的管理者而言,一个规范合理的版本号控制流程是很重要的。通常业内的做法是A.B.C的做法,

A代表大版本号,大版本号的变动通常意味着非常大幅度的升级,甚至可以不保证向低版本兼容。例如著名的异步流编程API,ReactiveX,1.x版本和2.x版本有相当多的改变,1.x的某些写法在2.x版本中已经是不兼容了。这个大版本号很适合做SDK的大幅重构,重大基础库的迁移等,例如从Android Support库转而采用Android-X库。

B代表中版本号,也是我们日常发版升级对应的版本号,例如1.0.0 -> 1.1.0类似如此的升级。

C代表的是小版本号,目的是在版本之间,处理因为bug修复等情况的临时版本,例如1.0.0版本发布后,发现了一个重要bug,这是可以发布1.0.1这样的版本来修复问题。

版本号管理的烦恼

那我们说,版本号的烦恼在哪里呢?根据我自己的日常开发经验,可能有这么几处:

  • 缺少版本上传的历史记录,这一点可以说是很多SDK的痛点了,回顾某个很久之前发布的版本,忘记打tag,还原不了当时的代码情况。某个成员意外的执行上传任务,覆盖掉了原本的包,引入了包含bug甚至是编译不过的代码。

  • 版本号的唯一性,在开发中我们通常会先集成快照包来开发和提测,在上线前某个时刻替换成正式版本。而快照包的依赖存在一些问题,例如,Android Studio的gradle缓存问题,有时候明明已经上传最新的快照包,但是IDE就是不更新。每次手动修改版本号,且不说麻烦,还没办法对应的上git提交记录。

那么有什么好办法处理这些问题呢?

自动管理版本号的Gradle插件

我这里的处理方式是自己写一个简单的gradle插件,用来自动管理SDK上传Maven仓库时的版本号。

插件功能的设计

首先我们希望每次上传新包到Maven仓库,能够记录一下上传记录,包括:上传人,上传时间,上传的版本号, 当前的分支名,最后一次提交的commit messgae等等

其次,版本名上我们希望附带两个功能,一个是指定是否为快照包,自动的在版本号后面加上-SNAPSHOT。一个是可以选择在版本号后面跟上唯一性的id。

Gradle插件的开发

Gradle插件开发

首先是如何开发Gradle插件,这里我推荐这篇文章Gradle插件开发指南
。这里我们开发一个独立的gradle插件项目,并上传到maven仓库中。

新建Gradle插件工程

Gradle插件的开发建议选择Intellij IEAD, 通过创建Gradle项目即可,语言我这里指定Groovy, 其实Java或者Kotlin甚至是Scala都是可以的。

接着设置好gradle项目需要的GroupId, ArtifactId和Version信息,创建好工程。此时工程目录结构:

其中groovy目录用来存放groovy代码,java目录存放java代码,resources目录用来声明当前项目所需的资源,后面我们介绍。其中的build.gradle文件看起来是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
plugins {
id 'groovy'
}

group 'xxx.xxxx'
version '1.0-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
compile 'org.codehaus.groovy:groovy-all:2.3.11'
testCompile group: 'junit', name: 'junit', version: '4.12'
}

如果想用Android Studio来创建Gradle工程的话,可能会麻烦一些,在一个Android工程中,选择新建 Java Library module “plugin”,然后手动删除掉不需要的目录和文件,手动新建resources目录。

实现功能

首先是Gradle插件开发的模版套路,实现Plugin接口,其中需要实现apply方法

1
2
3
4
5
6
class AutoVersionPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
//...
}
}

接着在resources文件夹下新建目录及文件:META-INF/gradle-plugins/auto_version_plugin.properties

1
2
//xxx对应着AutoVersionPlugin的完整包名
implementation-class=xxx.AutoVersionPlugin

到这一步,Gradle的壳就搭建起来了,接下来我们需要写业务逻辑:

第一步我们需要捕获maven插件的uploadArchives的task, 这里需要注意到的一点是uploadArchives属于工程的自定义task,而自定义task需要在工程的after evaluate执行完之后才能拿的到:

1
2
3
4
5
6
7
project.afterEvaluate {
//Only after evaluate, we can find custom tasks
Task uploadTask = it.tasks.findByPath("${it.path}:uploadArchives")
uploadTask.doFirst {
//在uploadArchives Task执行之前做一些事
}
}

好的,有了这一步的基础,我们接下来尝试定义gradle插件中可以用的DSL。首先我们定义DSL字段:

1
2
3
4
5
6
7
8
class AutoVersion {
String version = ''
String fixVersion = ''
boolean isSnapShot = true
String gitPath = ''
boolean needFileLog = false
String logFilePath
}

对应着实际调用就类似于:

1
2
3
4
5
6
7
autoVersion {
version '1.0.0'
fixVersion '1.1.0-SNAPSHOT'
isSnapShot true
needFileLog true
logFilePath rootProject.projectDir.absolutePath + File.separator + 'xxx'
}

接着我们只需要,在Plugin的apply方法中创建对应的DSL即可:

1
2
3
4
5
6
7
class AutoVersionPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
AutoVersion autoVersion = project.getExtensions().create("autoVersion", AutoVersion.class)
//...
}
}

以上我们的准备工作ok了,接下来我们需要处理两个问题,如何修改uploadArchives中的版本号参数,以及如何获取当前工程git信息。

首先是处理uploadArchives,这个借助gradle的api就可以处理

1
2
3
4
5
//将Task uploadTask强转为Upload即可
static def changeUploadTaskVersion(Upload uploadTask, String version) {
def deployer = uploadTask.repositories.getByName("mavenDeployer") as DefaultGroovyMavenDeployer
deployer.pom.version = version
}

其次是Git信息的获取,这里我们引入第三方库来帮助我们处理:

1
implementation 'org.eclipse.jgit:org.eclipse.jgit:5.4.0.201906121030-r'

这里我封装了GitInfo类,通过传入Git的路径,可以自动获取当前用户名,分支名,commit提交id和log记录等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class GitInfo {

private Git mGit
private Repository mRepository

GitInfo(String gitPath) {
String path = "${gitPath}${File.separator}.git"
File repoDir = new File(path)
if (!repoDir.exists()) {
throw new IllegalStateException('no git info')
}
FileRepositoryBuilder builder = new FileRepositoryBuilder()
mRepository = builder.setGitDir(repoDir)
.readEnvironment() // scan environment GIT_* variables
.findGitDir() // scan up the file system tree
.build()
mGit = new Git(mRepository)
}

String name() {
Config config = mRepository.getConfig()
return config.getString("user", null, "name");
}

String branch() {
return mRepository.branch
}

String commitId() {
Iterable<RevCommit> logs = mGit.log().call()
Iterator<RevCommit> iterator = logs.iterator()
if (!iterator.hasNext()) {
return "no_commit_id"
}
RevCommit revCommit = iterator.next()
if (revCommit == null) {
return "no_commit_id"
}
return revCommit.name
}

String shortCommitId() {
Iterable<RevCommit> logs = mGit.log().call()
Iterator<RevCommit> iterator = logs.iterator()
if (!iterator.hasNext()) {
return "no_commit_id"
}
RevCommit revCommit = iterator.next()
if (revCommit == null) {
return "no_commit_id"
}
return revCommit.name.substring(0, 7)
}

String lastLog() {
Iterable<RevCommit> logs = mGit.log().call()
Iterator<RevCommit> iterator = logs.iterator()
if (!iterator.hasNext()) {
return "message"
}
RevCommit revCommit = iterator.next()
if (revCommit == null) {
return "message"
}
return revCommit.shortMessage
}

}

其他的部分例如如何写文件,逻辑判断等这里就不多说了。

上传插件及使用

上传方式之前就介绍过了,使用起来也很简单,在Android工程的根目录的build.gradle中,首先引入我们上传的插件:

1
2
3
4
5
buildscript {
dependencies {
classpath '${your group id}:auto-version:${your version}'
}
}

其次找到需要上传的模块所在的build.gradle文件,假设这里已经引入了maven插件并配置了uploadArchives task,
我们引入我们的auto-version plugin

1
2
3
4
5
6
7
8
9
apply plugin: 'auto_version_plugin'

autoVersion {
version '1.0.0'
fixVersion '1.1.0-SNAPSHOT'
isSnapShot true
needFileLog true
logFilePath rootProject.projectDir.absolutePath + File.separator + 'xxx'
}

这样,上传maven任务和此前就一样,执行

1
gradle uploadArchives

总结

本篇介绍了如何开发一个独立的gradle插件,来协助管理maven发布的版本号和记录发布信息。不足之处,多多包涵。