布景

调研火山引擎的多仓开发插件时遇到一个很有趣的问题。

接入 mars-gradle-plugin

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

可是问题来了,官方文档是根据 groovy 写的,可是运用 kts 的开发者应该怎样写呢?

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

这样显着是行不通的,编译器会报错找不到 rootProject.veMarsExt 这个特点

翻源码 or 反编译

首先得找个这个插件的长途地址

但很不幸,只要二进制产物(问了字节的童鞋,没有上传源码) ,没有 sources.jar,没办法,只能 download 二进制产物经过 jadx 看反编译后的代码了。

一、插件入口

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

二、InitSettingsAction

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

三、InitSettingsAction 的 run 方法

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

还好,调用链不长,逻辑也算清晰,很快就理清了脉络。中心:

  1. 给 rootProject 创建了一个名为 veMarsExt 的 extension
  1. 读取根目录下的 dependency-lock.json,并解析为 deps:Map<String, String?>
  1. 最终把这个 deps 赋值给 veMarsExt 的 deps 特点

okk,到这儿,就瞬间了解为啥 implementation rootProject.veMarsExt.deps.player_demo 在 groovy 里能 work 了,原因便是现已给 rootProject 创建了一个名为 veMarsExt 的 extension

kts 的正确写法

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 那么动态):

  1. 需求 import com.bytedance.mars.veMarsExt
  1. 定义一个 resolveDependencies 方法,用于解析 rootProject 下的 veMarsExt 里的 deps
  1. 经过 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 的生成规则,最终才能正确的申明依靠,这也太离谱了吧!

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

所以有没有更友爱一点的方式呢?

那,有必要是有的

这儿,我就先抛砖引玉,给出我考虑出来的一种解法。

一种更为优雅的计划

Gradle 插件 + kotlinPoet

最先想到的一种简略且不失风姿的解决计划便是这个了,与火山引擎的 mars-gradle-plugin 不同的是,这个计划的插件需求在 buildSrc 的 build.gradle(.kts) 被 apply,然后:

  1. 还是从 dependency-lock.json 里读取依靠信息
  1. 经过 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 天下第一…

但看到文章里有这么一段话:

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

哎,妈鸭,真香

Gradle 插件 + 模版引擎

模版引擎

  • mustache

模版代码

放置于 gradle plugin 的 resource 目录:

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

以 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

接下来,便是怎么完成插件的问题了,思路大致如下:

  1. find kotlinSourceSet dir

找到 buildSrc KotlinSourceSet 地点的文件目录,如图:

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

  1. Dependency generate dir(optional)

如果想要生成的 Dependency.kt 有 package,可以从 Extension 读取 packageName,然后:

val generatedDir = ktDir.resolve(packageName.replace(".", "/"))
  1. register GenDependency Task

注册一个名为 GenDependency 的 Task

  1. Hook KotlinCompile Task

将这个 task 挂在 KotlinCompile Task 的前面~~~~,这样生成的 Dependency.kt 源码就会被编译了

之前的思路是把 Dependency.kt 生成到 buildSrc 的 build/generated 下的一个子目录里,这就需求做两件事:

  1. 将这个子目录增加到 kotlinSourceSet
  1. 将 GenDependency 这个 task 挂 KotlinCompile Task 前面

现在的计划不需求了,故此说明。

模版引擎生成代码

为了漂亮&容易了解,仅贴出最中心的源码完成:

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)填坑

运用

  1. 在 buildSrc 的 build.gradle(.kts) apply 这个插件
  1. 将 dependency-lock.json 放置到根目录下
  1. sync 一把,即可在 buildSrc 生成 Dependency.kt

增加依靠

build.gradle.kts

import info.hellovass.Dependency
dependencies {
    implmentation(Dependency.androidx_legacy.legacy_support_v4.gav)
} 

build.gradle

import info.hellovass.Dependency
dependencies {
    implementation( Dependency.androidx_legacy.legacy_support_v4.gav)
} 

kts 引用 Dependency 里的 enum class 却是很便利,可是在 groovy 就没那么简略了,直接这么写是会报错的!

需求这样:

import info.hellovass.Dependency
dependencies {
    implementation( Dependency.androidx_legacy.@legacy _support_v4.gav)
} 

这个小技巧是从 touk.pl/blog/2018/0… 学来的,你学废了吗?

参阅

  • www.volcengine.com/docs/6436/1…
  • 是时分抛弃 JavaPoet/KotlinPoet 了 | Johnson Lee
  • touk.pl/blog/2018/0…
  • square.github.io/kotlinpoet/