Gradle Plugin的开发及发布
Posted 乐翁龙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Gradle Plugin的开发及发布相关的知识,希望对你有一定的参考价值。
Gradle Plugin的开发及发布
前言
经过前面部署Jenkins自动化打包任务,以及桌面端APK工具的开发,基本已经把国内项目组所面临的问题解决了。但是呢,Jenkins这一套着实是繁琐,而且对海外项目组支持也不友好。所以我又决定开发一个gradle插件来作为一个轻量级的打包发布方案,该方案可以同时兼顾公司海内外的所有项目。整体目标大概就是如下:
1、依赖assembleXxxRelease等Task构建新任务;
2、将打包生成的APK文件上传到FTP服务器;
3、根据服务器上APK文件的地址生成二维码;
4、发送通知消息到钉钉群组;
该插件现已开源,GitHub地址在:ApkPublisherGradlePlugin
插件的开发
gradle插件的开发又涉及多种方式,我们这里在不同的阶段分别使用不同方式进行演示。
首先使用AS新建GradleSample5项目。
学习阶段(build.gradle脚本)
在这个阶段我们只需学习如何创建任务(Task),并且了解如何依赖任务即可。
首先创建任务“activeTask1”,我们需要在app模块下的build.gradle文件中编写如下代码:
tasks.register("activeTask1")
doLast
println(">>>>> Greeting from activeTask1")
点击 Sync Now 后就可以在右侧gradle面板中看到我们新创建的任务了。
双击该任务即可执行,并打印出日志信息:
11:42:39: Executing 'activeTask1'...
> Task :app:activeTask1
>>>>> Greeting from activeTask1
BUILD SUCCESSFUL in 284ms
1 actionable task: 1 executed
11:42:40: Execution finished 'activeTask1'.
接下来,我们需要创建一个activeTask2,该任务需要依赖activeTask1,也就是说执行task2的时候要先执行task1,那么关键就是dependsOn():
tasks.register("activeTask2")
dependsOn("activeTask1")
doLast
println(">>>>> Greeting from activeTask2")
那么这时候执行activeTask2的话,输出信息则如下所示:
11:46:41: Executing 'activeTask2'...
> Task :app:activeTask1
>>>>> Greeting from activeTask1
> Task :app:activeTask2
>>>>> Greeting from activeTask2
BUILD SUCCESSFUL in 180ms
2 actionable tasks: 2 executed
11:46:41: Execution finished 'activeTask2'.
所以这就是我们后续依赖assemble相关任务所需要学习的一点前置内容了。
开发阶段(buildSrc)
buildSrc是Gradle项目的一个特殊目录,我们先看下官网给的整体介绍。
Complex build logic is usually a good candidate for being encapsulated either as custom task or binary plugin. Custom task and plugin implementations should not live in the build script. It is very convenient to use buildSrc for that purpose as long as the code does not need to be shared among multiple, independent projects.
The directory buildSrc is treated as an included build. Upon discovery of the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script. For multi-project builds there can be only one buildSrc directory, which has to sit in the root project directory. buildSrc should be preferred over script plugins as it is easier to maintain, refactor and test the code.
buildSrc uses the same source code conventions applicable to Java and Groovy projects. It also provides direct access to the Gradle API. Additional dependencies can be declared in a dedicated build.gradle under buildSrc.
整体大致意思呢就是,gradle发现buildSrc目录后,会自动编译和测试其中的代码,会优先于其他的脚本。这对我们开发插件来说就非常的便利了,如果我们开发一个插件必须先发布然后再从项目中依赖然后再执行的话,效率就大打折扣了。
我们在项目的根目录下创建一个buildSrc的目录,然后创建一个build.gradle的文件放入该目录下,build.gradle文件内容如下所示:
点击Sync Now后可以看到,buildSrc目录样式变了,说明AS成功识别了该目录。
接下来我们来开发示例插件了,这里我们使用Java而不使用Groovy。因为其一我对Groovy不熟悉,其二Groovy太灵活了,我虽然能写出来相关的代码,可以我无法查看到相关的类提示等,这就导致我好奇某个类、某个变量到底是怎么来的。所以我选择了Java(Kotlin当然也不错),这样我可以明确知道相关类型,也方便查看源码。
Plugin
buildSrc目录建好后我们创建src目录,然后建立插件类MyPlugin并实现Plugin接口如下:
如果这个时候你跟我出现一样的情况,例如上面的红色报错,而且方法中连一个简单的String类都无法导入,那么说明AS的JDK配置出现了问题。请先查看下文疑难杂症篇章,解决后再进行插件的开发。
我们实现apply方法后在其中打印相关信息:
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class MyPlugin implements Plugin<Project>
@java.lang.Override
public void apply(Project project)
System.out.println(">>>>> Greeting from buildSrc MyPlugin");
然后在buildSrc的build.gradle中声明该插件:
plugins
id 'java-gradle-plugin'
gradlePlugin
plugins
pluginDemo
id = "my-plugin"
implementationClass = 'com.vsloong.myplugin.MyPlugin'
在app模块下的build.gradle中添加该插件的依赖:
plugins
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//添加插件的依赖
id 'my-plugin'
Sync后即可看到相关打印信息:
…
Task :buildSrc:validatePlugins UP-TO-DATE
Task :buildSrc:check UP-TO-DATE
Task :buildSrc:buildConfigure project :app
>>>>> Greeting from buildSrc MyPluginTask :prepareKotlinBuildScriptModel UP-TO-DATE
BUILD SUCCESSFUL in 13s
这表明我们的插件已经成功运行起来了,接下来需要注册我们自己的Task了。
Task
创建MyTask类继承自DefaultTask,这其中有两点需要注意:
1、在构造函数中添加分组(分组名自行定义),便于我们找到该任务;
2、添加一个doTaskAction()(方法名自行定义)的方法打印相关信息,注意该方法需要使用 @TaskAction 注解;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;
public class MyTask extends DefaultTask
public MyTask()
setGroup("myTaskGroup");
@TaskAction
public void doTaskAction()
System.out.println(">>>>> Greeting from buildSrc MyTask");
然后将MyTask注册到任务列表中,在MyPlugin的apply()方法中注册我们的任务,任务名是myTask1:
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class MyPlugin implements Plugin<Project>
@java.lang.Override
public void apply(Project project)
System.out.println(">>>>> Greeting from buildSrc MyPlugin");
project.getTasks().register("myTask1", MyTask.class);
Sync项目后即可在右侧gradle面板中查看到我们创建的任务组以及任务:
双击myTask1任务即可在控制台查看到相关输出信息:
…
Task :app:myTask1
>>>>> Greeting from buildSrc MyTaskBUILD SUCCESSFUL in 2s
经过上述的一系列步骤之后我们对Plugin以及Task有了一个大致的了解了,接下来我们回归到开始时候的任务,首先我们需要创建自己的任务并依赖于项目中的assembleRelease等任务。
这里我们可能需要了解下Gradle的生命周期,见官网 Build Lifecycle,具体不再详述。这就好比我们在Activity的生命周期中收到onCreate、onResume等回调一样,在Gradle的生命周期中也有相应的回调,比如我们要用到的afterEvaluate(Action<? super Project> var1),在项目评估完后即可收到该回调,收到该回调后我们就可以获取到项目中的所有任务,并插入自己的任务。
接下来我们先改造下MyTask,因为我们的任务需要依赖于匹配到的assembleXxxRelease任务,所以我们设置一个dependsOnTaskName变量,并且通过构造函数传递进来,然后调用dependsOn()方法进行依赖。
注意:构造函数中添加参数后就需要使用@Inject进行注解:
public class MyTask extends DefaultTask
private String dependsOnTaskName;
@Inject
public MyTask(String dependsOnTaskName)
setGroup("myTaskGroup");
this.dependsOnTaskName = dependsOnTaskName;
dependsOn(dependsOnTaskName);
@TaskAction
public void doTaskAction()
System.out.println(">>>>> Greeting from buildSrc MyTask");
然后在MyPlugin的apply()方法中,添加gradle项目评估完成的回调,在回调中获取所有匹配到的assembleXxxRelease任务,然后创建publishXxxRelease任务,同时记得传递需要依赖的任务名给MyTask。
public class MyPlugin implements Plugin<Project>
@java.lang.Override
public void apply(Project project)
System.out.println(">>>>> Greeting from buildSrc MyPlugin");
project.afterEvaluate(new Action<Project>()
@Override
public void execute(Project project)
for (Task task : project.getTasks())
String taskName = task.getName();
if (taskName.startsWith("assemble") && taskName.endsWith("Release"))
String myTaskName = taskName.replace("assemble", "publish");
System.out.println(">>>>> 匹配到任务 = " + myTaskName);
project.getTasks().register(myTaskName, MyTask.class, taskName);
);
此时,Sync项目后可以在右侧gradle面板中看到我们创建的任务了:
双击publishRelease任务可以从输出日志中看到,先执行了releaseAssemble任务后再执行的publisherRelease任务:
…
Task :app:packageRelease
…
Task :app:assembleReleaseTask :app:publishRelease
>>>>> Greeting from buildSrc MyTaskBUILD SUCCESSFUL in 1m 24s
49 actionable tasks: 43 executed, 6 up-to-date
11:25:03: Execution finished ‘publishRelease’.
上面是未配置项目Flavor的情况,如果项目中有多个Flavor的情况呢?比如我们在app模块下的build.gradle中添加如下Flavor:
android
defaultConfig
//添加 产品、环境 两个维度
flavorDimensions "product", "environment"
productFlavors
//产品1
app1
dimension "product"
//产品2
app2
dimension "product"
//开发环境
develop
dimension "environment"
//生产环境
product
dimension "environment"
Sync项目后即可在右侧看到我们创建的任务组中多了新增的这些风味:
Extension
要实现文章开头的业务,我们还需要配置FTP地址,端口号,钉钉机器token人等信息,这就需要使用到Extension了。新建MyPluginExtension类:
public class MyPluginExtension
public String host;
public String port;
然后在MyPlugin类中创建相关Extension的信息:
public class MyPlugin implements Plugin<Project>
@java.lang.Override
public void apply(Project project)
project.getExtensions().create("myExtensionInfo", MyPluginExtension.class);
//....
读取的时候可以在需要的地方使用getExtension()方法读取,比如在MyTask类中:
public class MyTask extends DefaultTask
@TaskAction
public void doTaskAction()
MyPluginExtension extension = getProject().getExtensions().getByType(MyPluginExtension.class);
System.out.println(">>>>> extension.host = " + extension.host);
System.out.println(">>>>> extension.port = " + extension.port);
此时执行相关任务后输出结果肯定为null:
Task :app:publishApp1ProductRelease
>>>>> extension.host = null
>>>>> extension.port = nullBUILD SUCCESSFUL in 1m 7s
在app模块下的build.gradle文件中最外层,添加我们插件所需的配置信息如下:
myExtensionInfo
host = "192.168.1.1"
port = 21
然后再执行相关任务就能拿到配置信息了:
Task :app:publishApp1ProductRelease
>>>>> extension.host = 192.168.1.1
>>>>> extension.port = 21BUILD SUCCESSFUL in 3s
OK,至此我们已经讲解完了和我们的目标相关的Plugin和Task内容了,剩下的关于二维码、FTP以及DingTalk SDK的使用请查看源码吧。
发布阶段(独立module)
经过上面两个阶段的内容我们以及开发完毕一个简陋的插件了,接下来我们先尝试发布到本地文件夹,然后再尝试发布到MavenCentral中。
如果你是初次写插件,我建议你再新建一个工程,然后创建一个新的module将该工程中的buildSrc中的内容全部拷贝到新module中,这样还容易处理一点,不会受上述阶段应用插件和配置插件信息的影响。
这里呢,我是将buildSrc目录直接改名为了 module-plugin-my,然后在setting.gradle文件中包含该模块:
include ':module-plugin-my'
同时需要将app模块下的build.gradle做一下修改,注掉了自己的插件id,注掉了插件的配置信息:
plugins
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
// id 'my-plugin'
//myExtensionInfo
// host = "192.168.1.1"
// port = 21
//
OK,接下来准备发布了。
发布到本地
先在module-plugin-my模块下的build.gradle中添加maven-publish插件,然后配置发布内容:
plugins
id 'java-gradle-plugin'
id 'maven-publish'
publishing
publications
maven(MavenPublication)
groupId = "io.github.vsLoong"
artifactId = "my-plugin"
version = "0.0.1"
from components.java
repositories
maven
//发布到该工程下的localRepo文件夹下
url = "../localRepo"
gradlePlugin
plugins
apkPublisher
id = "my-plugin"
implementationClass = 'com.vsloong.myplugin.MyPlugin'
脚本内容如上所示,Sync工程后,可以在右侧gradle面板中找到publish任务组了:
双击publish任务,构建完毕后即可在工程目录下查看到生成的依赖仓库:
那么如何应用该仓库这里有得好好说说了,7.0后的配置有些变化,但是使用之前的方式的话还是没问题的,这里就演示了通用的写法,在项目的build.gradle文件开头添加如下信息:
buildscript
repositories
maven
url("./localRepo")
dependencies
classpath "io.github.vsLoong:my-plugin:0.0.1"
然后在app模块下的build.gradle文件中应用该插件,并添加插件配置信息,Sync项目后即可成功看到我们自定义的Tasks成功显示在gradle面板了:
plugins
//......
id 'my-plugin'
myExtensionInfo
host = "192.168.1.1"
port = 21
发布到MavenCentral
准备sonatype账号
MavenCentral是由sonatype运营的,所以首先我们需要创建一个sonatype的账号,网上文章非常多了,这里就提一点,现在填写groupId的时候已经不支持com.github开头的命名了,建议使用io.github.yourgroupid。
创建pgp证书
账号注册好并拥有上传权限后,我们还需要的一个东西就是签名证书。类似我们的apk文件需要签名后才能发布到应用市场一样,我们的插件也需要一个签名。创建签名证书的话需要使用GPG4,我是Windows电脑所以使用了win工具,官网地址是:https://gpg4win.org 。我下载的版本是Gpg4win-4.0.4,页面如下所示。
点击 文件-> New OpenPGP Key Pair… ,填写自己的名称和邮箱地址:
填写完后记得点击高级设置,将密钥类型设置为RSA,4096比特,OK后进入创建证书的页面,填写证书密码,这里的密码需要记住后续会用到:
创建好证书后,回到主页面右键该证书,然后导出公钥、备份私钥:
导出和备份的时候也请务必手动更改其文件的后缀名为pgp:
最后右键你创建好的证书,然后选择“在服务器上发布…”,发布成功后过段时间,在主面板中点击“在服务器上查找”,能找到自己名称的证书的话,则表示这一步已经成功了。
配置sonatype和证书
经过上两步的操作后我们有了sonatype的账号密码以及仓库的上传权限,还有了已经发布到服务器的证书。上传插件到仓库需要这些信息,但这些信息又是不能暴漏到项目中的,否则这些信息被别人拿到岂不是可以随意上传内容到你的仓库了,所以这里的话我们需要将这些信息配置到本地。首先查看你的Gradle user home文件夹在哪里,例如我这里更改到的是D盘下AndroidGradle目录下:
那么我们就将备份的私钥文件拷贝到该目录,然后创建gradle.properties文件并输入如下内容:
# pgp信息
signing.keyId=证书密钥ID的后8位
signing.password=证书的密码
signing.secretKeyRingFile=D\\:\\\\AndroidGradle\\\\你的私钥文件.pgp
# sonatype账号、密码
ossrhUsername=sonatype账号
ossrhPassword=sonatype密码
发布Snapshot版本
上述的pgp证书及sonatype信息在本地设置好后,编辑module-plugin-my模块下的build.gradle文件(请仔细按照如下脚本内容配置,避免出错):
plugins
id 'java-gradle-plugin'
//发布使用
id 'maven-publish'
id 'signing'
//发布所需的相关内容
def MAVEN_GROUP_ID = "io.github.vsLoong"
def MAVEN_ARTIFACT_ID = "my-plugin"
def MAVEN_ARTIFACT_VERSION = "0.0.1-SNAPSHOT"
def REPOSITORY_URL_SNAPSHOT = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
def REPOSITORY_URL_RELEASE = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
gradlePlugin
plugins
apkPublisher
id = MAVEN_ARTIFACT_ID
implementationClass = 'com.vsloong.myplugin.MyPlugin'
// 将源码打包
task publishSourcesJar(type: Jar)
classifier = 'sources'
exclude "**/R.class" //排除`R.class`
exclude "**/BuildConfig.class" //排除`BuildConfig.class`
// mavenCentral要求必须有Javadoc的信息
task publishJavadocsJar(type: Jar, dependsOn: publishSourcesJar)
archiveClassifier.set('javadoc')
afterEvaluate
publishing
publications
maven(MavenPublication)
groupId = MAVEN_GROUP_ID
artifactId = MAVEN_ARTIFACT_ID
version = MAVEN_ARTIFACT_VERSION
from components.java
artifact publishSourcesJar
artifact publishJavadocsJar
pom
name = 'Apk publisher gradle plugin'
description = 'A gradle plugin that helps upload APK files to FTP servers and generate QR code images with download links.'
url = 'https://github.com/vsLoong/ApkPublisherGradlePlugin'
licenses
license
name = 'The Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
developers
developer
id = 'vsLoong'
name = 'vsLoong'
email = '你的邮箱地址'
scm
connection = 'https://github.com/vsLoong/ApkPublisherGradlePlugin.git'
developerConnection = 'https://github.com/vsLoong/ApkPublisherGradlePlugin.git'
url = 'https://github.com/vsLoong/ApkPublisherGradlePlugin'
repositories
maven
allowInsecureProtocol true
url = MAVEN_ARTIFACT_VERSION.endsWith('SNAPSHOT') ? REPOSITORY_URL_SNAPSHOT : REPOSITORY_URL_RELEASE
credentials
username = ossrhUsername
password = ossrhPassword
// 证书及签名信息
signing
sign publishing.publications
配置完毕Sync项目后在右侧双击 publishMavenPublicationToMavenRepository(这个名字取决于上述脚本中的相关命名),然后即可成功上传到MavenCentral了:
请注意脚本中的一处版本判断的代码,如果版本号后缀是SNAPSHOT,那么会发布到Snapshots仓库,否则会发布到Releases仓库。发布到Snapshots仓库的话没有那么严格,可以先发布到该仓库进行尝试。
发布到Snapshots仓库成功后可以登录 https://s01.oss.sonatype.org/#welcome 进行查看,此时在项目中也可以依赖到该插件的SNAPSHOT版本。
发布Release版本
发布到Releases仓库的话,你的插件会先放在 暂存库 中,我们需要点击左侧的 Build Promotion -> Staging Repositories,然后选中你发布的插件操作Close以及Release步骤。Close这一步会检测你发布上来内容的各种规范等,如果你在Close的阶段成功了,那么恭喜你少走了很多弯路,我所踩的坑都已经罗列在下文疑难杂症中了,同时上文脚本也基本是最完善的脚本了。
下面是终于close成功的截图,就差点击Release按钮就可以发布了。Release后再耐心等等就可以从Maven Central中搜索到你的插件了,搜索地址在 https://search.maven.org/。
疑难杂症
JDK路径
也有可能你建立Android工程的时候AS已经提示过你了,例如下图这种情况。
但是离谱的情况就是你按照提示select a JDK 处理完后,即便是选择了自己安装的JDK11的路径,但是AS仍旧不生效。
这时我们可以查看下 .idea/misc.xml文件,相关配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" default="true" languageLevel="JDK_11"
project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK" version="2">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
破解Gradle Gradle Plugin技术及玩转transform
破解Gradle Gradle Plugin技术及玩转transform
破解Gradle Gradle Plugin技术及玩转transform
Gradle之Android Gradle Plugin 主要 Task 分析