背景
调研火山引擎的多仓开发插件时遇到一个很有趣的问题。
接入 mars-gradle-plugin
但是问题来了,官方文档是基于 groovy
写的,但是使用 kts
的开发者应该怎么写呢?
这样明显是行不通的,编译器会报错找不到 rootProject.veMarsExt 这个属性。
翻源码 or 反编译
首先得找个这个插件的远程地址
但很不幸,只有二进制产物(问了字节的童鞋,没有上传源码) ,没有 sources.jar
,没办法,只能 download
二进制产物然后通过 jadx
查看反编译后的代码了。
一、插件入口
二、InitSettingsAction
三、InitSettingsAction 的 run 方法
还好,调用链不长,逻辑也算清晰,很快就理清了脉络。核心:
- 给
rootProject
创建了一个名为veMarsExt
的 extension - 读取根目录下的
dependency-lock.json
,并解析为deps:Map<String, String?>
- 最后把这个
deps
赋值给veMarsExt
的deps
属性
okk,到这里,就瞬间明白为啥 implementation rootProject.veMarsExt.deps.player_demo
在 groovy
里能 work
了,原因就是 mars-gradle-plugin
已经给 rootProject
创建了一个名为 veMarsExt
的 extension
kts 的正确写法
代码语言:javascript复制import com.bytedance.mars.veMarsExt
dependencies {
implementation("androidx.appcompat:appcompat:1.4.2")
implementation("com.google.android.material:material:1.6.1")
// user
val user: String by resolveDependencies()
implementation(user)
}
/**
* 获取 veMarsExt 里的 deps
*/
fun resolveDependencies(): Map<String, String?> {
val ext = rootProject.extensions["veMarsExt"] as? veMarsExt
?: return emptyMap()
return ext.deps.toMap()
}
稍微麻烦了亿点点(毕竟 kotlin
没有 groovy
那么动态):
- 需要
import com.bytedance.mars.veMarsExt
- 定义一个
resolveDependencies
方法,用于解析rootProject
下的veMarsExt
里的deps
- 通过
Map
的委托,获取到key
对应的value
(第 7 行),即坐标依赖
思考
虽然理清了怎么在 build.gradle.kts
下使用 mars-gradle-plugin
解析坐标依赖,但还是很不友好,比如:
{
"dependencies": [
{
"artifactId": "share",
"groupId": "com.mars.lib",
"version": "1.0.2"
},
{
"artifactId": "comment",
"groupId": "com.mars.lib2",
"version": "1.0.2"
},
{
"artifactId": "player",
"groupId": "com.mars.lib2",
"targets": [
{
"flavorName": "demo",
"version": "1.0.2.demo"
},
{
"flavorName": "full",
"version": "1.0.2.full"
}
]
},
{
"artifactId": "lib-android",
"groupId": "com.component.demo",
"targets": [
{
"flavorName": "demo",
"version": "0.0.30.demo-alpha.0"
},
{
"flavorName": "full",
"version": "0.0.30.full-alpha.0"
}
]
}
]
}
开发者声明了 depenendency-lock.json
,但他却不知道 veMarsExt#deps
里的 key
的生成规则是啥,看起来似乎是将 artifactId
的 -
转为 _
(实际上还真是),**比如 artifactId
为 lib-android
生成的 deps
里对应的 key
应该为 lib_android
**。
这就很麻烦,大部分开发者得像我一样去反编译插件的源码,才能确认 deps
的生成规则,最后才能正确的申明依赖,这也太离谱了吧!
所以有没有更友好一点的方式呢?
那,必须是有的
这里,我就先抛砖引玉,给出我思考出来的一种解法。
一种更为优雅的方案
Gradle 插件 kotlinPoet
最先想到的一种简单且不失风度的解决方案就是这个了,与火山引擎的 mars-gradle-plugin
不同的是,**这个方案的插件需要在 buildSrc
的 build.gradle(.kts) 被 apply
**,然后:
- 还是从
dependency-lock.json
里读取依赖信息 - 通过
kotlinPoet
在buildSrc
的kotlin
目录下生成Dependency.kt
用 kotlinPoet
进行元编程之前,我期望生成的 Dependency.kt
能满足以下条件:
Dependency
是一个单例Dependency
有多个enum class
,这些enum class
根据产物的groupId
生成(相同groupId
的枚举值在同一个enum class
内)Dependency
内代码缩进正常,well fortmatted- 避免生成的
enum class
名和kotlin
的保留关键字冲突
基于上述的期望,Dependency.kt
可能长这样:
object Dependency {
enum class androidx_lifecycle {
`lifecycle_extensions` {
override val gav: String
get() = "androidx.lifecycle:lifecycle-extensions:2.2.0"
},
`lifecycle_viewmodel_ktx` {
override val gav: String
get() = "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
},
`lifecycle_livedata_ktx` {
override val gav: String
get() = "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
},
`lifecycle_runtime_ktx` {
override val gav: String
get() = "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
};
abstract val gav: String
}
enum class androidx_fragment {
`fragment` {
override val gav: String
get() = "androidx.fragment:fragment:1.2.4"
},
`fragment_ktx` {
override val gav: String
get() = "androidx.fragment:fragment-ktx:1.2.4"
};
abstract val gav: String
}
}
好像有亿点点复杂,用 kotlinPoet
写出来的代码可能不太好维护。
就这样,暂时没有想到更好的方案(主要我这人有代码洁癖),就先战略性放弃了。
转机
在讲这个方案之前,故事还得从盘古开天说起。 不至于,不至于。
其实就是有一天,突然翻到森哥的一篇是时候放弃 JavaPoet/KotlinPoet 了 ,内心 OS:你让我放弃就放弃啊,我不管,KotlinPoet 天下第一...
但看到文章里有这么一段话:
哎,妈鸭,真香
Gradle 插件 模版引擎
模版引擎
- mustache
模版代码
放置于 gradle plugin
的 resource
目录:
以 xxx.kt.mustache
为文件名,内容如下:
package {{packageName}}
/*
* AUTO-GENERATED, DO NOT MODIFY THIS FILE!
*/
@Suppress("ClassName", "RemoveRedundantBackticks", "EnumEntryName", "SpellCheckingInspection")
object {{implementationClass}} {
{{#deps}}
enum class {{groupId}} {
{{#artifacts}}
`{{artifactId}}` {
override val gav: String
get() = "{{gav}}"
}{{separator}}
{{/artifacts}}
abstract val gav: String
}
{{/deps}}
}
Mustache
是一个 logic-less
(轻逻辑)模板解析引擎,稍微学习下语法就可以写出 Dependency.kt
对应的模版代码
动态生成 Dependency.kt
接下来,就是如何实现插件的问题了,思路大致如下:
- find kotlinSourceSet dir
找到 buildSrc KotlinSourceSet 所在的文件目录,如图:
- set Dependency generate dir(optional)
如果想要生成的 Dependency.kt
有 package
,可以从 Extension
读取 packageName
,然后:
val generatedDir = ktDir.resolve(packageName.replace(".", "/"))
- register GenDependency task
注册一个名为 GenDependency 的 task
hook KotlinCompile Task(已废弃)
将 GenDependency task 挂在 KotlinCompile Task 的前,这样生成的 Dependency.kt 源码就会被编译了
之前的思路是把 Dependency.kt
生成到 buildSrc
的 build/generated
下的一个子目录里,这就需要:
- 将这个子目录添加到 kotlinSourceSet
- 将 GenDependency 这个 task 挂 KotlinCompile task 前
现在的方案不需要了,故此说明。
模版引擎生成代码
为了美观&容易理解,仅贴出最核心的源码实现:
代码语言:javascript复制abstract class GenerateDependencyTask : DefaultTask() {
// dependency-lock.json 文件
@get:InputFile
abstract val inputFile: RegularFileProperty
// Dependency.kt 输出目录
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
// 包名,例如:info.hellovass
@get:Input
abstract val packageName: Property<String>
// 模板引擎:Mustache
private val engine: TemplateEngine by lazy(::MustacheEngine)
@TaskAction
fun run() {
val dependencies = inputFile.asFile.get().deserialize<Dependencies>()
val outputDir = outputDirectory.asFile.get()
val fileWriter = outputDir.resolve("Dependency.kt").writer()
val packageName = packageName.get()
fileWriter.use { writer -> engine.render(
template = "template/Deps.kt.mustache",
model = DependencyModel(
packageName = packageName,
implementationClass = "Dependency",
deps = toDeps(dependencies.dependencies)
),
writer = writer
)
}
}
}
模版引擎生成 Dependency.kt
的代码主要参考了森哥这个 example 的思路。其实,思路也很简单,还记得上面贴的 Dependency.kt.mustache
代码嘛,这里再贴一次:
package {{packageName}}
/*
* AUTO-GENERATED, DO NOT MODIFY THIS FILE!
*/
@Suppress("ClassName", "RemoveRedundantBackticks", "EnumEntryName", "SpellCheckingInspection")
object {{implementationClass}} {
{{#deps}}
enum class {{groupId}} {
{{#artifacts}}
`{{artifactId}}` {
override val gav: String
get() = "{{gav}}"
}{{separator}}
{{/artifacts}}
abstract val gav: String
}
{{/deps}}
}
企业级理解:
- 模版代码准备好坑位(
mustache
各种占位语法) - 插件准备好数据(
DependencyModel
)填坑
使用
- 在
buildSrc
的 build.gradle(.kts) apply 这个插件 - 将
dependency-lock.json
放置到根目录下 sync
一把,即可在buildSrc
生成Dependency.kt
添加依赖
build.gradle.kts
代码语言:javascript复制import info.hellovass.Dependency
dependencies {
implmentation(Dependency.androidx_legacy.legacy_support_v4.gav)
}
build.gradle
代码语言:javascript复制import info.hellovass.Dependency
dependencies {
implementation( Dependency.androidx_legacy.legacy_support_v4.gav)
}
kts
引用 Dependency
里的 enum class
倒是很方便,但是在 groovy
就没那么简单了,直接这么写是会报错的!
需要这样:
代码语言:javascript复制import info.hellovass.Dependency
dependencies {
implementation( Dependency.androidx_legacy.@legacy_support_v4.gav)
}
这个小技巧是从 https://touk.pl/blog/2018/05/28/testing-kotlin-with-spock-part-2-enum-with-instance-method/ 学来的,你学废了吗?
参考
- https://www.volcengine.com/docs/6436/110098
- 是时候放弃 JavaPoet/KotlinPoet 了 | Johnson Lee
- https://touk.pl/blog/2018/05/28/testing-kotlin-with-spock-part-2-enum-with-instance-method/
- https://square.github.io/kotlinpoet/