背景
我们的工程结构是单仓,然后通过gradle
提供的复合构建(ComposeBuilding)
的机制来完成整个单仓模式的。
单仓就是指所有的代码都在一个仓库内编译,能保证这部分代码的稳定性,尤其是编译产物其实并不是特别可以值得信任的
之前也简单的介绍过复合构建(composebuilding)
,这个东西虽然好,但是天然具有一个问题,主工程的一部分通用的属性无法复用在符合构建的工程内。
协程 路由 组件化 1+1+1>3 文章是这个 有兴趣的可以看看
那么有没有一种手段可以让类似ext内的属性可以共享到所有复合构建的project
上呢?
奇怪的知识
接下来一个个知识点慢慢分析,然后让大家知道都干了些啥。
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
来导入插件。
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
将我们手动生成的在build
的initscript
插入到全局的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,不仅仅是因为简单的配置共享这些。还有一些天然的构建隔离,项目层级方面的,比如说互相引用的情况。