自定义Gradle Plugin入门——Git仓库管理插件

1,111 阅读7分钟
功能介绍

以前的项目业务非常多,分了很多模块,每个模块都做成库上传都仓库。这样的话每次开发新业务切换分支都累个半死。

所以组里的大佬搞了一些脚本,有shell的,有python的,但是要多学一门语言就感觉很头疼,所以想着能不能用Gradle写个插件实现。好吧,为了不学shellpython,花了很长时间研究Gradle,笑死。

这个插件主要的功能就是从不同的远程拉取多个仓库到当前项目,后期切换分支的时候,修改一下分支名就好了,具体使用方式如下

gitClone {
    //很多项目的远程仓库不止一个,这里可以设置仓库的域名
    gitStore("https://gitee.com") {
        //配置仓库中的项目
        gitProject(
            //项目链接
            url = "jaso_chen.com/camera-study",
            //项目分支,以后切换分支大概就是修改branch
            branch = "master",
            //存放路径
            saveDir = buildDir.absolutePath + "/testDir",
            //是否需要重命名文件夹
            rename = "CameraStudy")
        gitProject(
            url = "jaso_chen.com/camera-study",
            branch = "testBranch",
            saveDir = buildDir.absolutePath + "/testDir",
            rename = "CameraStudyTest")
    }
}
实现步骤
  1. 创建一个Java-Module,包名为buildSrc,只有这个名字才能识别为插件

我也不知道为什么必须这个名字,也是百度到的,官方也是以这个为例,当时折腾好久都快崩溃了

step1.png

  1. 配置build.gradle.kts,因为是用kotlin编写的插件,所以转成kts来配置比较方便 这里要注意的是一定要添加implementation(gradleApi()),否则调用不了Gradle的类 其他配置最好也和这里的保持一致,不然报错比较麻烦
plugins {
    java
    kotlin("jvm") version ("1.6.10")
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    google()
    mavenCentral()
    gradlePluginPortal()
}
java {
    targetCompatibility = JavaVersion.VERSION_1_8
    sourceCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
    //添加Gradle相关的API,否则无法自定义Plugin和Task
    implementation(gradleApi())
    implementation(kotlin("stdlib"))
    implementation(kotlin("stdlib-jdk8"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
}
  1. 找到我们的类,开始编写代码

step2.png

如果刚刚创建项目没有修改主类,就没有这个类,可以自己随便创建一个

  1. 先继承Plugin这个类,并重写apply()方法
class GitPlugin : Plugin<Project> {
    override fun apply(target :Project) {
        //这里是插件的入口,当gradle引入了插件,在配置阶段就会执行这个方法
    }
}
  1. 我们这个插件需要做一个同时拉取多个仓库的功能,不过无论什么插件,第一步都是需要收集数据的

    但是我们的代码写在这个类中,怎么去收集数据呢? GradleProject为我们提供了一个扩展属性的列表,可以向这个列表中添加我们提供给Gradle调用的方法、变量

class GitPlugin : Plugin<Project> {
    override fun apply(target :Project) {
        //给gradle扩展一个方法,这样在build.gradle文件里面就可以传参给我们这里
        //我的想法是提供一个dsl,按规则传参,所以这里传入一个类,便于我们编写dsl
        //这里注意,如果传入的是对象,这个对象必须是open修饰的,否则Gradle无法构造
        target.extensions.create("gitClone", GitScope::class.java)
    }
}

在上面定义好扩展之后可以在build.gradle调用

//build.gradle
gitClone { //this = GitScope

}
  1. 需要定义DSL和收集数据,首先要定义好相关的类
//用于定义DSL
open class GitScope {
    internal val stores = arrayListOf<GitStore>()
}
//用于定义DSL
data class GitStore(val host: String) {
    internal val projects = arrayListOf<GitProject>()
}
//用于接收数据
data class GitProject(
    //项目链接
    val url: String,
    //项目分支,以后切换分支大概就是修改branch
    val branch: String = "",
    //存放路径
    val saveDir: String,
    //是否需要重命名文件夹
    val rename: String = ""
)
  1. 定义一些DSLgit执行的命令,由于功能比较多,这里只贴部分代码,详细的可以到项目里看

fun scope(scope: GitScope.() -> Unit) {
    val gitScope = GitScope()
    gitScope.scope()
}

fun GitScope.gitStore(host: String, scope: GitStore.() -> Unit) {
    val store = GitStore(host)
    store.scope()
    this.stores.add(store)
}

fun GitStore.gitProject(url: String, branch: String = "", saveDir: String, rename: String = "") {
    val project = GitProject(url, branch, saveDir, rename)
    this.projects.add(project)
}

//clone仓库
internal fun gitClone(repoUrl: String, dir: String) {
    "git clone $repoUrl $dir".exeCommand()
}

//切换分支
internal fun gitCheckout(branch: String, dir: String) {
    "git checkout -b $branch origin/$branch".exeCommand(dir)
}
  1. 现在先不管build.gradle怎么配置参数,先把功能给写完,我们先假设已经配置好参数了,那怎么获取参数呢?

    其实也很简单,怎么提供出去的,怎么获取回来。通过project.extensions.getByName("gitClone")就可以获取到我们传的对象。

//部分代码不完整
private fun gitCloneTask(task: Task) = runBlocking {
    //这里用了协程是为了可以多个仓库同时拉取,更重要的是为了等待所有任务完成
    //而且我们定义的是单个独立的任务,几乎瞬间就退出,所以需要协程,也为了学习协程
    coroutineScope.launch {
        //通过extensions获取到我们收集的数据
        val gitScope = task.project.extensions.getByName("gitClone") as GitScope
        //遍历每个仓库
        for (store in gitScope.stores) {
            cloneStoreTask(store)
        }
    }.join()
}

private fun CoroutineScope.cloneStoreTask(store: GitStore) {
    //遍历仓库里每个项目
    for (project in store.projects) {
        async {
            //命令里使用的斜杠要么是双斜杠\\,要么是反斜杠/,需要处理一下
            gitClone("${store.host}/${project.url}", project.branch,
                "${project.saveDir.replace("\\", "/")}/${project.rename}"
            )
        }
    }
}
  1. 功能都写好了,那么什么时候执行这段功能呢?我的想法是,提供一个Task任务,等开发者点一下,或者输入一串命令执行,那这样就需要给当前引入插件的Gradle定义一个任务

    apply()方法里可以拿到Project对象,就可以直接在tasks列表里创建一个任务

override fun apply(target: Project) {
    //配置git扩展
    target.extensions.create("gitClone", GitScope::class.java)
    //定义gitClone任务
    target.tasks.create("gitClone").apply {
        //定义分组,容易查找
        group = "git"
        //适当描述一下功能
        description = "用于管理多仓库初始化、切换分支"
        //在任务执行之后再执行我们的功能
        doLast(this@GitPlugin::gitCloneTask)
    }
}

task默认是在配置阶段运行的,也就是执行"clean"、"sync gradle"这些task都会被执行,为了避免每次都触发,我们把代码写在单独运行的时候再执行

  1. 插件的功能已经写好了,其他模块怎么使用呢,implementation吗,那肯定不是的,插件有插件依赖的方式,我们都见过,在plugins{}里引入,但是怎么引入呢?这就需要我们为我们写的插件做一个注册声明

step3.png

step4.png

META-INF/gradle-plugin

step5.png

implementation-class=com.chenchen.plugin.git.GitPlugin

  1. 这样我们的插件就弄好了,接下来是怎么引入和配置了,打开任意一个build.gradle,我这里选了app/build.gradle

由于我们的插件是用kotlin写的,里面包含一些kotlin特性,我不知道怎么在groovy使用,所以把build.gradle改成了build.gradle.kts

//app/build.gradle.kts
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    //添加插件
    id("GitPlugin")
}

//...省略一大片代码

//kts调用类的时候需要导包
import com.chenchen.plugin.git.*
//这个就是我们在插件里提供的扩展方法,具体的使用方式就如以下这样
gitClone {
    //很多项目的远程仓库不止一个,这里可以设置仓库的域名
    gitStore("https://gitee.com") {
        //配置仓库中的项目
        gitProject(
            //项目链接
            url = "jaso_chen.com/camera-study",
            //项目分支,以后切换分支大概就是修改branch
            branch = "master",
            //存放路径
            saveDir = buildDir.absolutePath + "/testDir",
            //是否需要重命名文件夹
            rename = "CameraStudy")
        gitProject(
            url = "jaso_chen.com/camera-study",
            branch = "testBranch",
            saveDir = buildDir.absolutePath + "/testDir",
            rename = "CameraStudyTest")
    }
}
  1. 编写好这段配置之后,点击sync project with gradle files同步一下,我们就可以在右边的gradle任务列表里找到这个任务了,我是在app/build.gradle.kts引入插件的,那在app模块里就可以找到git/gitClone这个任务

step6.png

自定义插件的方式到这就结束了。这个步骤比较初级,还有很多需要完善的,但是新手入门已经是够了,看起来非常简单,实际上我花了超过20小时的时间来完成,中间遇到各种编译不通过,依赖出问题,Gradle报错看不懂,等等问题

总而言之,Gradle非常复杂,但也非常有用,学一点皮毛能减少大量的时间,多摸鱼不好吗!!!

以上的功能还有不满意的地方:

  • 无法给Settings.gradle依赖,我想在拉代码的时候,同时自动帮这些库include进去,折腾好久,感觉做不到。

  • 只能给build.gradle这个级别的引入插件,虽然配置内容不多,但也显得比较臃肿,没法放在一个独立的文件去配置

如果有大佬懂的,或者以后实力上涨了,有解决办法了再继续完善。

项目地址:

gitlab.com/c297131019/…