上一篇文章分析过groovy script
的加载流程,gradle很早的时候已经提供了对Kotlin脚本的支持
而且由于IDE对Kotlin的支持,例如代码提示、类型推断等都比groovy更好,官方也是更推荐大家使用Kotlin脚本替换groovy脚本,并且gradle本身项目也已经完全使用了kts脚本
我们知道和groovy不同,Kotlin是一门静态语言,那Kotlin脚本的加载运行流程和groovy又有什么不同呢?
下面我们一起来探究一下
kts脚本加载流程
和groovy脚本一样,kts脚本也分为2个阶段,在kts这叫stage
- stage 1 执行buildscript和plugins部分,执行结果会对stage2 program的classpath有影响
- stage 2 eval脚本剩余的部分
2个阶段都会生成Program子类,目录在$HOME/.gradle/caches/gradle-version/kotlin-dsl/scripts/hashcode/classes
下
和groovy语言本身提供了丰富的动态能力不同,gradle对kts的解析有大部分工作是自己完成的,涉及到编程原理部分,比如从Lexer阶段开始对kts内的token做解析等
整体流程如下图所示
Lexer
Lexer接收脚本文件内容及脚本对应的TopLevelBlockIds(buildscript
/initscript
/pluginManagement
/plugins
)这些,返回经过lex处理过的Packaged<LexedScript>
,里面包含注释及提取的第一轮需要执行的顶层block
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>
)
其是通过org.jetbrains.kotlin.lexer.KotlinLexer
的能力进行解析的
start()
advance()
tokenType
tokenText
tokenStart
tokenEnd
start
接收文本内容,开始进行词法分析,编译的第一步就是进行词法分析,程序的内容会被全部解析为token
advance
跳至下一个token部分
tokenType
token的类型
例如空格为
KtTokens.WHITE_SPACE
注释为KtTokens.COMMENTS
(这是个集合,包含了 EOL_COMMENT-行尾注释,BLOCK_COMMENT-块注释等)
LBRACE
->{
左花括号
RBRACE
->}
右花括号
IDENTIFIER
这个涵盖的范围特别广,以val a = buildscript()
为例
val等关键字,变量如a、b,=等号,方法名buildscript等都属于此
tokenText
token的内容,分析IDENTIFIER
特别需要,还是上面的statement为例,val的tokenType
是IDENTIFIER,它的tokenText
为val
tokenStart
,tokenEnd
token的起始,结束位置
gradle kts的Lexer
目的是为了将顶层的特定block解析出来,这些block比较特殊,是需要优先执行的。解析的结果就是包含哪些特殊的block以及它们的起始结束位置
Lexer
靠不断的迭代tokenType
来解析脚步内容,遇到WHITE_SPACE
或者注释类型的token就跳过,它以state记录当前所处状态,state有3种类型
SearchingTopLevelBlock
SearchingBlockStart
SearchingBlockEnd
默认处于搜索SearchingTopLevelBlock
状态,在这个状态下如果碰到了PACKAGE
,会把包名解析出来并保存,如果碰到IDENTIFIER
,则判断是否有符合的toplevelblock
,且当前depth为0,当前depth是对花括号的记录,碰到花括号depth会+1,出花括号-1,因为这里是要收集顶层的block,所以进入花括号属于内层的需要忽略
当SearchingTopLevelBlock
状态下检测到符合条件的block时会进入SearchingBlockStart
,很容易看出它和SearchingBlockEnd
是一对,目的就是为了将block的闭包起始结束位置记录下来,start就是在找 { 的位置,找到后就进入,
start状态下只要不是IDENTIFIER
或者LBRACE
的情况,都会重置回SearchingTopLevelBlock
默认状态重新开始查找
SearchingBlockEnd
状态,因为内部还可能有闭包,所以end里仍需要进入 { 对depth+1,退出 } 时-1,当depth为0时将start和end点记录下来
还有个细节,是在start状态遇到IDENTIFIER
时,还会检测block是否是符合条件的,如果不符合是会重置状态的,因为可能出现下面这种情况,buildscript
可以被引用,当解析到第一个buildscript
时会进入start状态,如果start不对IDENTIFIER检查,到第二个buildscript
时就会重置状态,而错过buildscript
的解析了
val a = buildscript
buildscript {}
ProgramParser
ProgramParser是把Lexer分析后的Packaged<LexedScript>
解析成Packaged<Program>
这里也会进行一些顶层block的检测工作,检测的逻辑和对groovy script处理的一样
- 检查特定的顶层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
PartialEvaluator的目的是为了将stage1的部分先reduce
,作为stage2的prelude
,stage2部分在运行时再编译运行,这个逻辑和groovy脚本的处理也很相似
它将Parser解析后的Program
转为ResidualProgram
,residual有剩余,残留的意思
ResidualProgram
Static(instructions:List<Instruction>
)
Dynamic(prelude: Static, source: ProgramSource)
ResidualProgram
有2个子类,Static
和Dynamic
Static
只包含了instructions
的集合
Dynamic
是由作为prelude
的Static
和剩余部分的源码组成的
在Parser后其实被划分为7种情况,empty,3种特定顶层block只出现一个的场景,3种中有任意2个或以上出现的场景Stage1Sequence
,没有特殊顶层block出现的场景Script
,stage1和stage2并存的场景Staged
Empty PluginManagement Buildscript Plugins Stage1Sequence Script Staged
只有Staged
的情况才会被evaluate
为Dynamic
,其中stage1部分作为static
,剩余的源码部分为source
其他情况都是Static
的
可以看出Static
主要是服务于stage1中特殊的顶层block的,比较重要的一些instruction
有
SetupEmbeddedKotlin
,ApplyPluginRequestsOf
,Eval
值得注意的一点是,这里对plugins的evaluate
有优化,对其单独进行了Lex,将插件id等提取出来
ResidualProgramCompiler
PartialEvaluator生成的ResidualProgram
实际上是一堆粗略的指令 ResidualProgramCompiler就是将这堆指令使用asm技术翻译成字节码
对于Static
和Dynamic
的处理方式差不多,主要区别在于Dynamic
的分为2个阶段,第一个阶段也是static
的,生成的第二个阶段代码实际只是将源码先保存在字节码中,等实际执行的时候再去调用Interpreter走整个流程
Static
编译为ExecutableProgram
的子类
Dynamic
编译为ExecutableProgram.StagedProgram
的子类
下面为删减的代码,编译后的Program
需要实现execute
方法,这里只会有部分特定顶层block的执行代码,脚本内其余部分代码是通过kotlin compiler提供的能力编译的
如果kts脚本被PartialEvaluator reduce
为了Static
,那它会被编译为ExecutableProgram
的子类,若此时文件内只有plugins
时,它在这个execute
中就能完成所有工作了,不会通过kotlin compiler生成其他代码,如果不是,那会将其他代码编译为类似Build_gradle
的文件,并在execute
中对其进行初始化
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
}
}
当kts脚本被reduce
为Dynamic
时,这种情况可能更加常见,也更复杂,以下面这个较为简单,除plugins
、buildscript
外仅有repositories
block的脚本为例
plugins {
kotlin("jvm") version "1.8.10"
}
buildscript {
print("test")
}
repositories {
mavenCentral()
}
将会生成下面的代码(伪代码)
stage1部分
Program.kt
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);
}
}
Build_gradle.kt
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")
}
}
}
代码比较多,核心部分在于plugins
、buildscript
这2个顶层block编译在了Build_gradle
中,execute
中先执行了setupEmbeddedKotlinFor
,后面又初始化了Build_gradle
,因Build_gradle
初始化时就执行了顶层block,后续就是applyPluginsTo
去应用plugins
引入的插件,最后是对stage2部分的编译及运行
setupEmbeddedKotlinFor
是为了统一embededKotlin
版本,都是用gradle自带的kotlin版本的,precompile
脚本用的kotlin版本和build.gradle.kts
保持一致
stage1部分的Build_gradle
也不一定会有,这一步和Static
生成的逻辑一样,只是多了加载stage2的loadSecondStageFor
等部分,下面列了stage2编译后的伪代码,但是实际上此时并没有发生stage2的编译,虽然也是由ResidualProgramCompile
来完成的,但stage2部分的编译发生在stage1执行时,由Interpreter
来触发的
stage2
Program.kt
class Program: ExecutableProgram() {
fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>) {
Build_gradle(host, scriptHost.getTarget() as Project)
}
}
Build_gradle.kt
class Build_gradle(
val host: KotlinScriptHost ,
val pluginDependencies: PluginDependenciesSpec,
val project: Project
): CompiledKotlinBuildscriptAndPluginsBlock(host, pluginDependencies) {
init {
project.repositories {
mavenCentral()
}
}
}
从伪代码可以看出stage2部分才执行了repositories
的mavenCentral()
方法
stage2的执行是stage1在其execute
方法中触发的evaluateSecondStageOf
,并且它还调用了accessorsClassPathFor
去获取accessors
的classpath,accessors
在下个小节进行详细阐述
Program
由ResidualProgramCompiler
使用字节码技术生成的,Build_gradle
是由kotlin script的KotlinToJVMBytecodeCompiler
生成的
KotlinCompiler
gradle kts实际是用kotlin-compiler-embeddable
去编译kts脚本的
Kotlin Script最重要的是Script Defination,这是一组用来定义并配置script类型的参数,主要有
- baseClass 和groovy的
scriptBaseClass
类似,作为script的基类 - defaultImports 和groovy自带了很多默认导包不同,kts需要自己添加导包
- hostConfiguration 添加
classpath
等
Script Defination有几种定义方法,官方例子中以注解方式配置参数较为常见,gradle中也有注解部分,但更多还是通过代码手动设置的参数
KotlinScript
类似groovy script的org.gradle.api.Script
,继承关系如下
classDiagram
KotlinScript <|-- DefaultKotlinScript
DefaultKotlinScript <|-- CompiledKotlinBuildScript
DefaultKotlinScript <|-- CompiledKotlinInitScript
DefaultKotlinScript <|-- CompiledKotlinSettingsScript
DefaultKotlinScript <|-- PrecompiledInitScript
DefaultKotlinScript <|-- PrecompiledSettingsScript
DefaultKotlinScript <|-- PrecompiledProjectScript
CompiledKotlinBuildScript <|-- CompiledKotlinBuildscriptBlock
CompiledKotlinBuildScript <|-- CompiledKotlinBuildscriptAndPluginsBlock
CompiledKotlinInitScript <|-- CompiledKotlinInitscriptBlock
CompiledKotlinSettingsScript <|-- CompiledKotlinSettingsBuildscriptBlock
CompiledKotlinSettingsScript <|-- CompiledKotlinSettingsPluginManagementBlock
除此以外还需要配置outputDirectory
(生成的class的输出目录),jvm版本,脚本源码路径等等。配置完整体的compiler环境后,调用compileBunchOfSources(environment)
即可对kts文件进行编译
可以参考Get started with Kotlin custom scripting, kts script加载 来自定义kts脚本
Plugins
plugins block在stage1中会被生成为applyPlugins
代码,之后和groovy脚本对plugin的加载一样
plugin主要可以分为两类
- precompile script 这又可以细分为
buildSrc
和includeBuild
引入的 - external plugin 这是通过设置
plugin
的repository后apply
进来的,也可以细分为两种,gradle官方提供的例如java
,kotlin
等,另外是用户自定义的
precompile script
buildSrc
和includeBuild
中的src目录内的script都可以作为precompile script
,具体参见官方文档 Developing Custom Gradle Plugins
这类脚本使用的语言不受限制,只要是JVM的就行,实际上这些脚本会被编译为Plugin
的子类,gradle提供了kotlin-dsl
和groovy-gradle-plugin
的插件让我们可以kts或groovy来编写此类脚本,用java也是可以的
kotlin-dsl
的实现为KotlinDslPlugin
,它会apply
PrecompiledScriptPlugins
,这个plugin的作用就是Kotlin source-sets下的*.gradle.kts脚本文件输出为Gradle plugin产物,具体是通过DefaultPrecompiledScriptPluginsSupport
来实现的
DefaultPrecompiledScriptPluginsSupport
对precompile script
的处理分为以下几步
- ExtractPrecompiledScriptPluginPlugins
把precompiled script里面的plugins
block提取出来,单独输出到outputDir里
- GenerateExternalPluginSpecBuilders
从compile classpath中找到带有gradle plugins的jar包,判断依据是其有META-INF/gradle-plugins/*.properties
文件
为这些plugin生成accessor
,这部分accessor
是用在plugins
block内的,例如java
,kotlin
等PluginSpecBuilders
- CompilePrecompiledScriptPluginPlugins
用ExtractPrecompiledScriptPluginPlugins
提取的plugins block和GenerateExternalPluginSpecBuilders
生成的PluginSpecBuilders
来编译plugins block,这里也是使用KotlinCompiler.compileKotlinScriptModuleTo
来处理的,到这一步的处理其实类似stage1的过程
- GeneratePrecompiledScriptPluginAccessors
有了前面compile过后的plugins block,就可以来生成type safe accessors了
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
至此precompiled script
编译流程完成,buildSrc
和includeBuild
在执行的时机上有所不同,buildSrc
在root/build.gradle.kts之前,includeBuild
实际上是介于root/build.gradle.kts编译过程的stage1与stage2之间,大体上并不影响,因为precompiled script
就是为了stage2的编译做准备
apply
plugin引入的plugin不是在stage1处理过的,所以没有为其生成accessor
类,在build脚本里面是无法使用它的extension
的
includeBuild
里面的precompiled script
因为是在stage1,2之间生成,所以没有对应的PluginSpecBuilders
生成,所以只能用id("")
方法去调用
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
这些能力是如何能在脚本中被使用到的呢,尤其extension
是plugin自己定义的,gradle无法进行约束,不像groovy语言本身提供了动态能力在运行时去派发这些方法调用到具体的extension
上去
Kotlin是强类型静态语言,在编译时这些类就需要在classpath里面找得到,否则无法compile
gradle会生成对应的accessor
,来让脚本可以使用到这些能力
举个具体的例子,我们可以在build.gradle.kts
脚本里面使用其他插件提供的extension
,例如在引入插件java后可以使用java
extension对其进行一些配置
// build.gradle.kts
plugins {
id("java")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
我们也可以自定义plugin,并提供自己的extension
,例如
val proguard = extensions.create<ProguardExtension>("proguard")
定义了proguard extension
,这行代码引入了2个东西,一个是extension
本身,在这里也就是ProguardExtension
,里面可以提供方法来进行配置,另一个是extension
的name,这里是proguard
,也就是我们引入了这个plugin后,在脚本中可以使用的名字,类似上面java插件中的java
extension。
CompilationClassPath
分为2类,一类是ScopeClassPath
,一类是accessors classpath
,compilationClassPath
就是将2者加起来
- ScopeClassPath
ScopeClassPath
是固有的一些classpath,不用编译plugin就自带,可以分为3种
下面用gradleLib来简化 $HOME/.gradle/wrapper/dists/gradle-version/hashcode/gradle-version/lib 路径
gradleApi
.gradle/caches/version/generated-gradle-jars/gradle-api-version.jar
gradleLib/groovy相关jar包
gradleLib/kotlin标准库相关jar包
gradleLib/gradle-installation-beacon-version.jar
等等
gradleApiExtensions
.gradle/cache/version/generated-gradle-jars/gradle-kotlin-dsl-extensions.jar
给gradle api生成kotlin拓展方法的源码,例如files
、repositories
等,代码细节见ApiExtensionsJarGenerator
gradleKotlinDslJars
gradleLib/kotlin标准库相关jar包
gradleLib/gradle-kotlin-dsl-version.jar(这个jar包是gradle提供的基础的api的kotlin拓展方法,例如apply,dependencies,maven等。实际上这个jar包就是kotlin dsl的源码,上述的Interpreter、ResidualProgramCompiler等也在这个jar包里)
gradleLib/gradle-kotlin-dsl-tooling-models-version.jar
exportedClassPath
buildSrc
jar包的classpath是在project对象prepare的过程中就给导入了的
includeBuild
和plugins的classpath是在stage1代码在eval
时,通过执行applyPlugin
,调用DefaultPluginRequestApplicator.defineScriptHandlerClassScope
导入的
具体代码见KotlinScriptClassPathProvider
- accessors classpath
stage1
由InterpreterHost.pluginAccessorsFor
触发,最终会调用
GeneratePluginAccessors
,从project的buildSrcClassLoaderScope找到pluginDescriptorsClassPath,和precompiled script
流程中的GenerateExternalPluginSpecBuilders
找plugin一样,之后对这些plugin生成accessor
,以便在plugins block中可以被调用到,这里是为buildSrc
引入的plugin生成PluginAccessors
其次是在DefaultPluginRequestApplicator.applyPlugins
中触发includeBuild
的构建,生成对应的accessors
stage2
由ProgramHost.accessorsClassPathFor
触发,最终调用GenerateProjectAccessors
,和precompiled script
中的GeneratePrecompiledScriptPluginAccessors
类似,因为plugin在stage1阶段已经被apply
了,所以这里可以从project对象获取到插件引入的task、extension等,为其生成accessors
生成的代码位于$HOME/.gradle/caches/*version*/kotlin-dsl/accessors
缓存
对setting.gradle.kts
和build.gradle.kts
进行evaluate
是Configuration阶段,被称为configuration cache
有2层缓存,内存缓存和文件缓存
文件缓存靠gradle本身task的执行流程机制保障,没有内存缓存时,gradle会创建一个类似task的执行流程,来加载kts脚本
内存缓存在StandardKotlinScriptEvaluator.classloadingCache
里,如果是daemon运行方式(默认方式),这个进程是常驻的所以可以作为内存缓存,如果之前没有daemon进程例如首次启动,或者手动关闭了daemon进程例如-Dorg.gradle.daemon=false
的话,就相当于没有内存缓存
参考链接
Get started with Kotlin custom scripting
KEEP/scripting-support.md at master · Kotlin/KEEP · GitHub
GitHub - Kotlin/kotlin-script-examples: Examples of Kotlin Scripts and usages of the Kotlin Scripting API
KotlinConf 2019: Implementing the Gradle Kotlin DSL by Rodrigo Oliveira - YouTube