用 Kotlin写 gradle 脚本,与groovy有何不同? - Gradle探究
本文作者
作者:近地小行星
链接:
https://juejin.cn/post/7219988638050369594
本文由作者授权发布。
大家端午快乐!
该文为 Gradle 深入探索系列:
系列 1:我们写的build.gradle是如何跑起来的? - Gradle探究
下面我们一起来探究一下。
stage 1 执行buildscript和plugins部分,执行结果会对stage2 program的classpath有影响。 stage 2 eval脚本剩余的部分。
Lexer
fun lex(script: String, vararg topLevelBlockIds: TopLevelBlockId): Packaged<LexedScript>
class Packaged<T>(
val packageName: String?,
val document: T
)
class LexedScript(
val comments: List<IntRange>,
val topLevelBlocks: List<TopLevelBlock>
)
start()
advance()
tokenType
tokenText
tokenStart
tokenEnd
例如空格为KtTokens.WHITE_SPACE 注释为KtTokens.COMMENTS(这是个集合,包含了 EOL_COMMENT-行尾注释,BLOCK_COMMENT-块注释等) LBRACE -> { 左花括号 RBRACE -> } 右花括号 IDENTIFIER 这个涵盖的范围特别广,以 val a = buildscript()为例 val等关键字,变量如a、b,=等号,方法名buildscript等都属于此。
2. SearchingBlockStart
3. SearchingBlockEnd
val a = buildscript
buildscript {}
ProgramParser
检查特定的顶层block每个只出一次(buildscript, plugins等)。 检查特定的顶层block的顺序,pluginManagement一定要在第一个,plugins和buildscript优先级一样不做要求。 将源码中的注释擦除,注意擦除不是删除,而是替换换行之外的字符为WHITESPACE,避免起始结束位置错乱。 将特定的顶层block解析出来,若block内容不为空的话转为对应的Program子类,里面会记录其block的起始结束位置,将它们统一聚集到stage1中。 再将特定block的代码擦除,如果有剩余代码的话,则将其包在Program.Script内作为stage2。 若stage1、2都存在,将其包在Program.Staged返回,若只存在一个则只返回单个,若都没有则返回Program.Empty。
// 只有stage1的block,且block内没有内容,解析后为Empty
buildscript { }
plugins {
// comments
}
// stage1的block是空的,解析后为Program.Script
buildscript { }
println "stage2"
解析结果为:
Program
Empty
Script
Staged(Stage1, Script)
Stage1
Buildscript
PluginManagement
Plugins
Stage1Sequence(Buildscript, PluginManagement, Plugins)
PartialEvaluator
ResidualProgram
Static(instructions: List<Instruction>)
Dynamic(prelude: Static, source: ProgramSource)
Empty PluginManagement Buildscript Plugins Stage1Sequence Script Staged
ResidualProgramCompiler
abstract class ExecutableProgram {
abstract fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<?>)
}
abstract class StagedProgram : ExecutableProgram() {
abstract val secondStageScriptText: String
abstract fun loadSecondStageFor(...): CompiledScript
fun loadScriptResource(resourcePath: String): String
}
}
plugins {
kotlin("jvm") version "1.8.10"
}
buildscript {
print("test")
}
repositories {
mavenCentral()
}
class Program: ExecutableProgram.StagedProgram() {
fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>) {
host.setupEmbeddedKotlinFor(scriptHost)
val requestCollector = PluginRequestCollector(scriptHost.getScriptSource())
Build_gradle(scriptHost, requestCollector.createSpec(1), scriptHost.getTarget() as Project)
host.applyPluginsTo(scriptHost, requestCollector.getPluginRequests())
host.applyBasePluginsTo(scriptHost.getTarget() as Project)
host.evaluateSecondStageOf(this, scriptHost, "Project/TopLevel/stage2", sourceHash, host.accessorsClassPathFor(scriptHost))
}
// secondStageScriptText和loadScriptResource都是为了加载stage2的脚本文件内容,因为常量池大小64k的限制,如果超出这个大小才会用loadScriptResource,否则使用字面量
fun getSecondStageScriptText(): String {
return "repositories { mavenCentral() }"
}
fun loadSecondStageFor(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>, scriptTemplateId: String, sourceHash: HashCode, accessorsClassPath: ClassPath): CompiledScript {
return host.compileSecondStageOf(this, project, scriptTemplateId, sourceHash, ProgramKind.TopLevel, ProgramTarget.Project, accessorsClassPath);
}
}
class Build_gradle(
val host: KotlinScriptHost ,
val pluginDependencies: PluginDependenciesSpec,
val project: Project
): CompiledKotlinBuildscriptAndPluginsBlock(host, pluginDependencies) {
init {
plugins {
pluginDependencies.kotlin("jvm").version("1.8.10")
}
buildscript {
print("test")
}
}
}
class Program: ExecutableProgram() {
fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>) {
Build_gradle(host, scriptHost.getTarget() as Project)
}
}
class Build_gradle(
val host: KotlinScriptHost ,
val pluginDependencies: PluginDependenciesSpec,
val project: Project
): CompiledKotlinBuildscriptAndPluginsBlock(host, pluginDependencies) {
init {
project.repositories {
mavenCentral()
}
}
}
stage2的执行是stage1在其execute方法中触发的evaluateSecondStageOf,并且它还调用了accessorsClassPathFor去获取accessors的classpath,accessors在下个小节进行详细阐述。
KotlinCompiler
baseClass 和groovy的scriptBaseClass类似,作为script的基类。 defaultImports 和groovy自带了很多默认导包不同,kts需要自己添加导包。 hostConfiguration 添加classpath等。
可以参考Get started with Kotlin custom scripting, kts script加载 来自定义kts脚本。
precompile script 这又可以细分为buildSrc和includeBuild引入的。 external plugin 这是通过设置plugin的repository后apply进来的,也可以细分为两种,gradle官方提供的例如java,kotlin等,另外是用户自定义的。
precompile script
buildSrc和includeBuild中的src目录内的script都可以作为precompile script,具体参见官方文档 Developing Custom Gradle Plugins。
https://docs.gradle.org/current/userguide/custom_plugins.html#sec:precompiled_plugins
DefaultPrecompiledScriptPluginsSupport
ExtractPrecompiledScriptPluginPlugins。
GenerateExternalPluginSpecBuilders。
CompilePrecompiledScriptPluginPlugins。
GeneratePrecompiledScriptPluginAccessors。
Plugin提供可以用在脚本里面的有extension(如java,kotlin),task,convention(已经Deprecated了,和extension差不多),containerElement(named方法),configuration(如implementation,api),gradle是通过构建一个虚拟的build,然后对其project apply这些plugin,之后就可以在project对象中获取到上述的extension等信息,提取的代码见DefaultProjectSchemaProvider.schemaFor
收集到的信息被封装在ProjectSchema里,再就是通过org.gradle.kotlin.dsl.accessors.Emitter利用ASM字节码手段生成accessor了,这一步的操作是为了给正式编译kts脚本中使用到的extension等提供accessors源码。
在经过上面一系列操作之后,gradle才会开始执行compileKotlin task,这里和编译build.gradle.kts不同,并不是使用KotlinCompiler来完成的,而是通过为freeCompilerArgs属性添加-script-templates,-Xscript-resolver-environment这些kts脚本编译参数来完成的,和单纯编译kts脚本不同,src目录下除了kts脚本外还可以有正常的kotlin代码,需要混编。参考Kotlin compiler options | Kotlin Documentation。
https://kotlinlang.org/docs/compiler-reference.html#script-templates-classnames
plugins {
java
`my-build-src`// buidSrc下的precompiled script
id("my-include-build")// includBuild的precompiled script
}
Accessors
Accessor是什么
gradle本身提供的一些能力,例如plugins、files、repositories、dependencies。 plugin引入的extension,如java,publishing。
// build.gradle.kts
plugins {
id("java")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
val proguard = extensions.create<ProguardExtension>("proguard")
CompilationClassPath
ScopeClassPath
下面用gradleLib来简化 $HOME/.gradle/wrapper/dists/gradle-version/hashcode/gradle-version/lib 路径。
includeBuild和plugins的classpath是在stage1代码在eval时,通过执行applyPlugin,调用DefaultPluginRequestApplicator.defineScriptHandlerClassScope导入的
accessors classpath
参考链接
Get started with Kotlin custom scripting
https://kotlinlang.org/docs/custom-script-deps-tutorial.html#cd181e43
KEEP/scripting-support.md at master · Kotlin/KEEP · GitHub
https://github.com/Kotlin/KEEP/blob/master/proposals/scripting-support.md
GitHub - Kotlin/kotlin-script-examples: Examples of Kotlin Scripts and usages of the Kotlin Scripting API
https://github.com/Kotlin/kotlin-script-examples
KotlinConf 2019: Implementing the Gradle Kotlin DSL by Rodrigo Oliveira - YouTube
https://www.youtube.com/watch?v=OEFwnWxoazI
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
点击 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!