Gradle 奇淫技巧之initscript pluginManagement

3,719 阅读5分钟

背景

我们的工程结构是单仓,然后通过gradle提供的复合构建(ComposeBuilding) 的机制来完成整个单仓模式的。

单仓就是指所有的代码都在一个仓库内编译,能保证这部分代码的稳定性,尤其是编译产物其实并不是特别可以值得信任的

之前也简单的介绍过复合构建(composebuilding),这个东西虽然好,但是天然具有一个问题,主工程的一部分通用的属性无法复用在符合构建的工程内。

协程 路由 组件化 1+1+1>3 文章是这个 有兴趣的可以看看

那么有没有一种手段可以让类似ext内的属性可以共享到所有复合构建的project上呢?

奇怪的知识

接下来一个个知识点慢慢分析,然后让大家知道都干了些啥。

demo工程地址在这里

initscript

这个是gradle 藏的比较深的方法,正常情况下会放在.gradle目录下。官方demo如下。

init.gradle
initscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.apache.commons:commons-math:2.0'
    }
}

gradle的生命周期中,这个是所有的最早执行的方法,我们可以定义一下全局属性都放在这个文件内,之后放在.gradle目录下。

从另外一个方面想哦,在settings.gradle执行之前就能执行initscript了,那么那么我们是不是可以做一些坏事了啊。

简单的说就是这个东西能作用与全局的gradle项目,自然也包括了复合构建的工程。

Plugin

我们一般来写gradle插件会是Plugin<Project>,含义是我们的插件只对于project生效,简单的说就是只要含有build.gradle就可以放入这个插件。

另外一只就是settings插件:Plugin<Settings>,这个一般都是针对于大的工程结构生效的,一个大Project只含有一个settings.gradle,我们可以在这个下面插入我们的plugin

这次我们介绍的是Plugin<Gradle>,这个是针对Gradle的。你可能不理解了,这个是什么啊。


class RepoSettingsPlugin : Plugin<Settings> {

    override fun apply(settings: Settings) {
        settings.gradle.addProjectEvaluationListener(object : ProjectEvaluationListener {
        })
    }
}

其中这个settings.gradle就是我们所说的针对Gradle的了。包括Project实例内也是有Gradle实例的。

Gradle的api虽然不是特别多,但是他可以获取到settings还有project的前置执行,也就是说我们可以提早插入我们想要的东西了。

PluginManagement

原来我们在声明一个插件的时候一定需要在根build.gralde下的buildscripts通过dependencies下,添加classpath来导入插件。

wecom-temp-b50b080813f231720d8ae5718606dda2.png

PluginManagement这个是gradle一直推广的新特性了,在AS的大黄蜂版本中已经最为默认配置更新了,我们后续只需要像上图一样,在根节点通过pluginName+version的形式,就可以从PluginManagement获取到对应的插件了。

当然前提是我们需要在settings下添加一些通用的配置。

pluginManagement {
    repositories {
        mavenLocal()
        gradlePluginPortal()
        google()
        maven {
            url "https://dl.bintray.com/kotlin/kotlin-eap"
        }
    }
}

基本上就和我们使用mavenCentral一样的能力,但是后续我们的一些别的插件就都可以不需要在classpath中声明,只需要在plugins下通过pluginName+version的形式就可以获取到了。

PluginManagement而这个的使用也是有前置条件的。其实这个是一个新的类似pom文件的东西。所以我们的插件在发布的时候需要通过java-gradle-plugin之后定义好gradlePlugin的dsl,这样在mavenPublish的时候就会把对应的文件上传上去了。


gradlePlugin {
    plugins {
        settingsPlugin {
            // 在 app 模块需要通过 id 引用这个插件
            id = 'kronos.settings'
            // 实现这个插件的类的路径
            implementationClass = 'com.kronos.plugin.repo.RepoSettingsPlugin'
        }
    }
}

当然也有一些极端情况,比如一些插件已经好几个版本没有迭代了,本身也不支持这个特性的情况下,我们其实还是有另外一种方式的。

通过pluginManagement内的resolutionStrategy,之后通过name映射到具体的下载地址的方式,强行使用新的特性。

resolutionStrategy {
      eachPlugin {
          if (requested.id.id == 'xxxxx') {
              useModule('xxx:xxx:version')
          }
          if (requested.id.id == 'xxxxxx') {
              useModule("xxx:xxx:version")
          }
      }
  }

如果在仓库内找不到,的情况下会根据寻找插件的id去指定的地址下载指定版本的plugin。

串起来

我们现在需要的就是在一个工程下,将所有的Project工程都补充上pluginManagement,保证他们的settings还是和原来一样。

这样我们就可以给全局的所有的工程的settings都补充上一样的逻辑,然后我们的切入点只有根节点的settings.gralde的一个插件。

第一个SettingsPlugin看看我是咋写的呢。

class PluginsVersionPlugin : Plugin<Settings> {

    override fun apply(target: Settings) {
        // 获取到最外面的转化文件
        FileUtils.getRootProjectDir(target.gradle)?.let {
            IncludeBuildInsertScript().execute(target, it)
        }
        target.gradle.plugins.apply(PluginVersionGradlePlugin::class.java)
    }
}

class IncludeBuildInsertScript {

    fun execute(target: Settings, root: File) {
        val initFile = getBuildTemp(root, "global.settings.pluginManagement.gradle")
        if (initFile.exists()) {
            initFile.delete()
        }
        initFile.appendText(PLUGIN_MANAGEMENT_SCRIPT)
        initFile.appendText("gradle.apply plugin: com.kronos.plugin.version.PluginVersionGradlePlugin.class")
        val fileList = mutableListOf<File>().apply {
            addAll(target.gradle.startParameter.initScripts)
        }
        fileList.add(initFile)
        target.gradle.startParameter.initScripts = fileList
    }

    fun getBuildTemp(root: File, path: String): File {
        val result = File(root.canonicalPath + File.separator + "build" + File.separator + path)
        touch(result)
        return result
    }

    private fun touch(file: File) {
        if (!file.parentFile.exists()) {
            file.parentFile.mkdirs()
        }
    }
}

这次我们非常的投机取巧,通过gradle.startParameter.initScripts 将我们手动生成的在buildinitscript插入到全局的gradle.initScripts中去。这样我们就可以在所有符合构建的工程中都插入这个PluginVersionGradlePlugin了。

class PluginVersionGradlePlugin : Plugin<Gradle> {

    override fun apply(target: Gradle) {
        target.settingsEvaluated {
            pluginManagement(DefaultPluginManagementAction(this))
            GradlePluginsVersion().execute(this)
        }
        target.addBuildListener(object : BuildAdapter() {
            override fun projectsEvaluated(gradle: Gradle) {
                super.projectsEvaluated(gradle)
                val rootProject = gradle.rootProject
                rootProject.configurations.all {
                    resolutionStrategy {
                        dependencySubstitution {
                            all {
                                if (requested is ModuleComponentSelector) {
                                    val selector = requested as ModuleComponentSelector
                                    val group = selector.group
                                    val module = selector.module
                                    val p = rootProject.allprojects.find { p ->
                                        p.group.toString() == group
                                                && p.name == module
                                    }
                                    if (p != null) {
                                        Logger.debug("select   $requested local project")
                                        useTarget(project(p.path), "selected local project")
                                    }
                                }
                            }

                        }
                    }
                }
            }
        })
    }

}

这部分代码就比较少了,插件内主要是通过target.addBuildListener添加一个projectsEvaluated的监听,然后给settings插入全局的PluginManagement以及对应的策略。

总结

有一说一,我还是从我大佬身上学习到不少很好玩的操作的,最近转到编译组了,做的内容其实挺有意思的,这部分也是从大佬的代码中剥离出来的。

我个人认为复合构建模式还是大于单工程include,不仅仅是因为简单的配置共享这些。还有一些天然的构建隔离,项目层级方面的,比如说互相引用的情况。