gradle插件编写及应用

gradle

Posted by River on March 7, 2022

前言

老Android都经历过从Eclipse切换到AS后对groovy脚本的适应期,groovy语言早期有效的参考资料很少; 不支持类似Java一样可以方便的查看对应方法的实现。大约从三年前的4.10版本开始支持Kotlin编写,一定程度上解放了使用groovy 编写带来的懵逼;本文讲实战,不聊深入的八股原理,下图为apk构建的简略流程图。

apk构建的简易流程图

1、gradle插件的架子

1.1、 apply plugin: ‘sunflower’后面的插件名称sunflower是怎么定义的。

如下图,sunflower就是resource下面的sunflower.properties的名字,不带后缀。 此文件支持多份,比如:
可以复制一份文件命名为sunflowe_android.properties;那么工程中可以使用:

apply plugin: ‘sunflowe_android’

apk构建的简易流程图

1.2、 classpath “com.xxx.plugin:sunflower:${project.properties[“sunflower.version”]}”是怎么定义的

其实和日常开发上传的aar组件版本格式一样,gradle的plugin插件也是通过和aar一样的格式上传的;

都是通过groupId:artifactId:version的格式上传插件的静态库。

apply plugin: 'maven'
apply plugin: 'maven-publish'

// nexus.properties文件的读取
Properties config = new Properties()
config.load(project.file("nexus.properties").newDataInputStream())
def nexus_versionName = config.getProperty('nexus_versionName')
。。。

uploadArchives {
        repositories {
            mavenDeployer {
                pom.groupId = nexus_groupId
                pom.artifactId = nexus_artifactId
                pom.packaging = 'aar'
                pom.name = nexus_fileName
                
                pom.version = nexus_versionName

                 。。。
                 repository(url: url) {
                        authentication(userName: System.getenv("maven_username"), password: System.getenv("maven_password"))
                 }
                
            }
        }
    }

apk构建的简易流程图

1.3、 插件源码的工程入口;

工程入口以类的全路径形式定义在resource目录下的sunflower.properties中:


implementation-class=com.xxx.plugin.sunflower.SunflowerPlugin

// 范型注意使用PluginAware以区分Setting还是Project
class SunflowerPlugin: Plugin<PluginAware> {
    private var plugin: SFPlugin? = null

    override fun apply(pluginAware: PluginAware) {
        plugin = PluginFactory.createPlugin(pluginAware)
        。。。
        plugin?.apply()
    }
}

// 这里主要区分Settings中插件的应用为了源码和aar的切换,开发时调试用的
object PluginFactory {
    fun createPlugin(pluginAware: PluginAware): SFPlugin? {
        return when(pluginAware) {
            is Settings -> SettingsPlugin(pluginAware)
            is Project -> {
                when(pluginAware.rootProject) {
                    pluginAware -> {
                        RootProjectPlugin(pluginAware)
                    }
                    else -> SubProjectPlugin(pluginAware)
                }
            }
            else -> null
        }
    }
}

2、强制依赖和源码切换

2.1、强制依赖实现;

在gitlab的仓库直接定义文件3rdDep.gradle,格式如下

ext{
    butterknifeVersion = '10.2.0'
    kotlinVersion = '1.3.61'

    dep = [
            android_tools: "com.android.tools.build:gradle:3.2.1",
            walle        : "com.meituan.android.walle:plugin:1.1.6",
            butterknife  : "com.jakewharton:butterknife-gradle-plugin:${butterknifeVersion}",
            kotlinPlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
    ]

}

创建一个Task

class XXXPlugin : Plugin<PluginAware> {

    override fun apply(pluginAware: PluginAware) {
        project = PluginFactory.createPlugin(pluginAware)
        ...
        // 当pluginAware是Project的时候
        val task = project.tasks.create("DepDefinitionPluginTask", ApplyHbRemoteDepTask::class.java)
        project.afterEvaluate() {
            task.apply()
        }
    }
}

task实现强制依赖

open class ApplyHbRemoteDepTask : DefaultTask() {

    @TaskAction
    fun apply() {
        ...
        depPluginAddress: String = "https://gitlab.hellobike.cn/Publics/NewAndroidBOSDepPlugin"

        val url = "$depPluginAddress/raw/$curBranch/dep/3rdDep.gradle"


        project.apply(mapOf(Pair("from", url)))

        project.extensions.extraProperties.get("dep") as Map<*, *>?

        project.subprojects {
        
            val depModuleSelectorNotations = mutableListOf<String>()
            
            collectDepModuleVersionSelectorNotations(depModuleSelectorNotations,
                    project.extensions.extraProperties.get("dep") as Map<*, *>?)

            it.configurations.all { configuration ->
                // 核心实现
                configuration.resolutionStrategy.force(depModuleSelectorNotations)
            }

        }
    }
}

private fun collectDepModuleVersionSelectorNotations(list: MutableList<String>, depConfig: Map<*, *>?) {
        depConfig?.values?.forEach {
            if (it is Map<*, *>) {
                collectDepModuleVersionSelectorNotations(list, it)
            } else if (it is String || it is GStringImpl) {
                val notation: String = it.toString()
//              println("[强制依赖]:" + notation)
                list.add(notation)
            }
        }
    }

2.2、反向依赖

2.1、工程根目录下定义一份切换aar和源码实现的文件repo.xml

<?xml version="1.0" encoding="UTF-8" ?>
<repo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.w3school.com.cn"
    xsi:schemaLocation="http://www.w3school.com.cn repo.xsd">

    <!--srcBuild:是否开启源码依赖,该配置优先级低于module中的srcBuild-->
    <!--branch:
        分支 用于在源码调试时统一分支,会自动进行分支切换,
        如果有未暂存的文件修改会导致编译失败,并提示
        该配置优先级 低于 module 配置中的 branch
        -->

    <default srcBuild="false"/>

    <!--
        origin:远程路径
        substitute: 将被替换掉的远程依赖 group:name
        name:唯一标示某一个模块,该属性名 也是 该模块在原仓库的默认文件夹名称
    -->

    <!--middle 层-->
    <module name="middle-bundlelibrary"
        origin="ssh://git@gitlab.xxx.cn:10022/Torrent/XXXAndroidMiddleApp.git"
        srcBuild="false" substitute="com.xxx.middle:middle-bundlelibrary" />

</repo>

2.2、 Plugin入口处指定源码文件的配置信息

这里的实现需要在Setting文件进行:apply plugin: ‘com.xxx.repo-setting’

class RepoSettingsPlugin : Plugin<PluginAware> {
    override fun apply(settings: PluginAware) {
    
        // 当PluginAware是一个Setting的时候
        settings = PluginFactory.createPlugin(pluginAware)

        val repoInfo = RepoInflater.inflate(settings.rootDir)

        repoInfo.moduleInfoMap.forEach { s, module ->
            if (module.srcBuild ) {
                // 执行一些git命中clone到指定的目录
                module.settingProject()
                
                // 源码工程囊括进来,就是常见的在Setting文件中include ':app'
                settings.include(":${module.name}")
                RepoLogger.info("${module.name} 路径为 ${module.modulePath}");
                
                // 指定include工程的文件地址
                settings.project(":${module.name}").projectDir = module.modulePath
            }
        }
        
    }
}
// module的数据结构 
open class ModuleInfo(
        val name: String,
        val origin: String,
        val path: String?,
        val srcBuild: Boolean,
        val substitute: String?,
        val repoManageProjectDir: File,
        val branch: String

)

这里的实现需要在工程的build.gradle文件进行:apply plugin: ‘com.xxx.repo-setting’

    class RepoSettingsPlugin:Plugin<PluginAware> {

        override fun apply(project: PluginAware) {
    
            // 当PluginAware是一个RootProject的时候
        rootProject = PluginFactory.createPlugin(pluginAware)
        
        val repoInfo = RepoInflater.inflate(project.rootProject.projectDir)
        
        val substituteModule = repoInfo.substituteModules 
        
        rootProject.subprojects.forEach { target ->
    
                target.afterEvaluate {
                
                    substituteModules.forEach { substituteModule ->
                
                    target.configurations.all{
                            it.dependencySubstitution.substitute(
                            // com.hellobike.middle:middle-bundlelibrary
                            it.dependencySubstitution.module(substituteModule.targetModule)) 
                            //替换成本地源码文件地址
                            .with(it.dependencySubstitution.project(substituteModule.project)) }
                    }
            }            
        }    
    }
}

// RepoInfo的数据结构
class RepoInfo(val repoManageProjectDir:File) {

    var moduleInfoMap: MutableMap<String, ModuleInfo> = mutableMapOf()

    var substituteModules: List<SubstituteModule> = mutableListOf()

    var defaultBranch: String? = null;

    var srcBuild: Boolean = false;


}

3、部分省略操作的补充

3.1、包括cmd操作在内的git操作

    class CmdUtil {
    companion object {
        fun execute(command: String): Boolean {

            return execute(command, null)
        }

        fun execute(command: String,dir: File?):Boolean{
            return execute(command, dir, true)
        }

        fun execute(command: String, dir: File?, interruptWhenCmdFailed: Boolean): Boolean {
            // 核心逻辑 
            val process = Runtime.getRuntime().exec(command, null, dir)
            val result = process.waitFor()
            if (result != 0) {
                val failureMsg = "[CMD] - failure to execute command [${command} under ${dir}\n message: ${process.errorStream.bufferedReader().readText()}"
                if (interruptWhenCmdFailed) {
                    throw  CmdExecuteException(command, failureMsg)

                } else {
                    System.out.println(failureMsg)
                }
            } else {
                System.out.println("【CMD】- execute command [${command}] under ${dir} success\n ")
            }
            return result == 0;
        }
    }
}

class GitUtil {
    companion object {
        //get Cur branch
        fun curBranch(): String {
            return CmdUtil.executeForOutput("git rev-parse --abbrev-ref HEAD", null, false).replace("\n", "")
        }

        fun curBranchName(dir: File): String {
            return CmdUtil.executeForOutput("git rev-parse --abbrev-ref HEAD", dir).replace("\n", "")
        }

        fun clone(dir: File, gitUrl: String, branch: String): Boolean {
            return CmdUtil.execute("git clone ${gitUrl} -l ${dir} -b ${branch}", null, true)
        }

        fun clone(dir: File, gitUrl: String): Boolean {
            return CmdUtil.execute("git clone ${gitUrl} -l ${dir}", null, true)
        }

        fun slightClone(dir: File, gitUrl: String, branch: String, depth: Int): Boolean {
            return CmdUtil.execute("git clone ${gitUrl} -l ${dir}", null, true)

        }

        fun isClean(dir: File): Boolean {
            return CmdUtil.executeForOutput("git status -s", dir).trim() == "";
        }

        fun fileStatus(dir: File) :String{
            return CmdUtil.executeForOutput("git status -s ", dir)
        }

        fun isLocalExistBranch(dir: File, branch: String): Boolean {
            return File(dir, ".git/refs/heads/$branch").exists()
        }

        fun isBranchChanged(dir:File ,branch:String):Boolean {
            return !curBranchName(dir).equals(branch)
        }

        fun checkoutBranch(dir: File, branch: String) {
            CmdUtil.execute("git checkout ${branch}", dir)
        }

        fun checkoutRemoteBranch(dir: File, branch: String) {
            CmdUtil.execute("git checkout -b $branch origin/$branch", dir)
        }

        fun checkoutNewBranch(dir: File, branch: String) {
            CmdUtil.execute("git checkout -b $branch", dir)
        }

        fun isRemoteBranch(dir: File, branch: String): Boolean {
            CmdUtil.execute("git fetch", dir)
            val text = CmdUtil.executeForOutput("git branch -r",dir)
            return text.contains("origin/$branch")
        }

        fun pullRebase(dir: File): Boolean {
            return CmdUtil.execute("git pull --rebase", dir, false)
        }
    }


}

3.2、xml文件的解析

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;


public RepoInfo parseRepo(File repoFile) throws ParserConfigurationException, IOException, SAXException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();

        FileInputStream inputStream = new FileInputStream(repoFile);
        Document doc = builder.parse(inputStream);

        Element rootElement = doc.getDocumentElement();

        RepoInfo repoInfo = null;
        NodeList defaultNodeList = rootElement.getElementsByTagName(TAG_DEFAULT);
        if (defaultNodeList.getLength() > 1) {
            throw new RuntimeException("[repo] - Make sure there is only one <default/> element in repo.xml");
        } else if (defaultNodeList.getLength() == 1) {


            Element item = (Element) defaultNodeList.item(0);
            File repoDirPath = repoProjectsDefaultDir;

            String branch = item.getAttribute(ATTRIBUTE_DEFAULT_BRANCH);

            final boolean repoDirSetting = item.hasAttribute(ATTRIBUTE_REPO_DIR);
            String repoDir = item.getAttribute(ATTRIBUTE_REPO_DIR);
            if (repoDirSetting && repoDir != null) {
                if (new File(repoDir).isAbsolute()) {
                    repoDirPath = new File(repoDir);
                } else {
                    repoDirPath = new File(gradleProjectDir, repoDir);
                }
            }


            //如果repo.xml 没有设置默认分支,则执行git命令获取当前分支名作为默认的分支
            if (branch == null || branch.length() == 0) {
                branch = GitUtil.Companion.curBranchName(gradleProjectDir);
            }

            //如果是临时分支,尝试获取jvm branch
            //如果jvm branch配置还是没有则 默认取master
            if ("HEAD".equals(branch)){
                //
                final String jvmBranch = System.getProperty("BRANCH");
                if (jvmBranch ==null){
                    branch = "master";
                }else {
                    if (jvmBranch.startsWith("origin/")){
                        branch = jvmBranch.replace("origin/", "");
                    }else {
                        branch = jvmBranch;
                    }
                }
            }


            RepoLogger.Companion.info(" 设置默认分支" + branch);

            repoInfo = new RepoInfo(repoDirPath);
            repoInfo.setDefaultBranch(branch);
            repoInfo.setSrcBuild(Boolean.parseBoolean(item.getAttribute(ATTRIBUTE_SRCBUILD)));

        }

        final NodeList moduleNodeList = rootElement.getElementsByTagName(TAG_MODULE);

        for (int i = 0; i < moduleNodeList.getLength(); i++) {
            final Element moduleElement = (Element) moduleNodeList.item(i);
            parseModuleInfo(repoInfo, moduleElement);
        }


        final NodeList substituteNodeList = rootElement.getElementsByTagName(TAG_SUBSTITUTE);
        for (int i = 0; i < substituteNodeList.getLength(); i++) {
            final Element substituteModuleItem = (Element) substituteNodeList.item(i);
            parseSubstituteModule(repoInfo, substituteModuleItem);
        }
        return repoInfo;

    }


总结

  1. gradle插件其实结构较为简单,多编写几次一般能上手;
  2. 纸上得来终觉浅,觉知此事要躬行;不断思考coding是最好的实践;
  3. Android编译构建过程十分复杂宏大,有待深入挖掘。