Skip to content

Commit e58ae57

Browse files
zwioradkrasnoff
authored andcommitted
KTL-2962 Compilation with BTA for JVM (#876)
1 parent bdfbb2b commit e58ae57

File tree

13 files changed

+203
-40
lines changed

13 files changed

+203
-40
lines changed

build.gradle.kts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@ dependencies {
4444
implementation(libs.gson)
4545
implementation(libs.kotlinx.serialization.json)
4646
implementation(libs.kotlin.compiler.arguments.description)
47-
implementation(libs.kotlin.tooling.core)
4847
implementation(libs.junit)
4948
implementation(libs.logback.logstash.encoder)
5049
implementation(libs.kotlin.reflect)
5150
implementation(libs.kotlin.stdlib)
52-
implementation(libs.kotlin.compiler)
5351
implementation(libs.kotlin.script.runtime)
52+
implementation(libs.kotlin.build.tools.api)
53+
implementation(libs.kotlin.build.tools.impl)
54+
implementation(libs.kotlin.compiler.embeddable)
55+
implementation(libs.kotlin.tooling.core)
5456
implementation(project(":executors", configuration = "default"))
5557
implementation(project(":common", configuration = "default"))
5658
implementation(project(":dependencies"))

common/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ plugins {
33
}
44

55
dependencies {
6-
implementation(libs.kotlin.compiler)
7-
}
6+
implementation(libs.kotlin.compiler.embeddable)
7+
}

gradle/libs.versions.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
kotlin = "2.3.0-dev-9317"
2+
kotlin = "2.3.0-dev-9673"
33
spring-boot = "3.5.6"
44
spring-dependency-managment = "1.1.7"
55
springdoc = "2.8.13"
@@ -25,7 +25,9 @@ kotlin-stdlib-js = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-js",
2525
kotlin-stdlib-wasm-js = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-wasm-js", version.ref = "kotlin" }
2626
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
2727
kotlin-test-junit = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit", version.ref = "kotlin" }
28-
kotlin-compiler = { group = "org.jetbrains.kotlin", name = "kotlin-compiler", version.ref = "kotlin" }
28+
kotlin-build-tools-api = { group = "org.jetbrains.kotlin", name = "kotlin-build-tools-api", version.ref = "kotlin"}
29+
kotlin-build-tools-impl = { group = "org.jetbrains.kotlin", name = "kotlin-build-tools-impl", version.ref = "kotlin"}
30+
kotlin-compiler-embeddable = { group = "org.jetbrains.kotlin", name = "kotlin-compiler-embeddable", version.ref = "kotlin" }
2931
kotlin-tooling-core = { group = "org.jetbrains.kotlin", name = "kotlin-tooling-core", version.ref = "kotlin" }
3032
kotlin-compiler-arguments-description = { group = "org.jetbrains.kotlin", name = "kotlin-compiler-arguments-description", version.ref = "kotlin" }
3133
kotlin-script-runtime = { group = "org.jetbrains.kotlin", name = "kotlin-script-runtime", version.ref = "kotlin" }
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.compiler.server.compiler.components
2+
3+
import com.compiler.server.model.ErrorDescriptor
4+
import com.compiler.server.model.ProjectSeveriry
5+
import com.compiler.server.model.TextInterval
6+
import org.jetbrains.kotlin.buildtools.api.KotlinLogger
7+
8+
9+
/**
10+
* This custom implementation of Kotlin Logger is needed for sending compilation logs to the user
11+
* on the frontend instead of printing them on the stderr. CompilationLogger extracts data from logs
12+
* and saves it in [compilationLogs] map, so that compilation messages can be later displayed to
13+
* the user, and their position can be marked in their code.
14+
15+
* KotlinLogger interface will be changed in the future to contain more log details.
16+
* Implementation of the CompilationLogger should be therefore updated after KT-80963 is implemented.
17+
*
18+
* @property isDebugEnabled A flag to indicate whether debug-level logging is enabled for the logger.
19+
* If true, all messages are printed to the standard output.
20+
*/
21+
class CompilationLogger(
22+
override val isDebugEnabled: Boolean = false,
23+
) : KotlinLogger {
24+
25+
/**
26+
* Stores a collection of compilation logs organized by file paths.
27+
*
28+
* The map keys represent file paths as strings, and the associated values are mutable lists of
29+
* `ErrorDescriptor` objects containing details about compilation issues, such as error messages,
30+
* intervals, severity, and optional class names.
31+
*/
32+
var compilationLogs: Map<String, MutableList<ErrorDescriptor>> = emptyMap()
33+
34+
override fun debug(msg: String) {
35+
if (isDebugEnabled) println("[DEBUG] $msg")
36+
}
37+
38+
override fun error(msg: String, throwable: Throwable?) {
39+
if (isDebugEnabled) println("[ERROR] $msg" + (throwable?.let { ": ${it.message}" } ?: ""))
40+
try {
41+
addCompilationLog(msg, ProjectSeveriry.ERROR, classNameOverride = null)
42+
} catch (_: Exception) {}
43+
}
44+
45+
override fun info(msg: String) {
46+
if (isDebugEnabled) println("[INFO] $msg")
47+
}
48+
49+
override fun lifecycle(msg: String) {
50+
if (isDebugEnabled) println("[LIFECYCLE] $msg")
51+
}
52+
53+
override fun warn(msg: String, throwable: Throwable?) {
54+
if (isDebugEnabled) println("[WARN] $msg" + (throwable?.let { ": ${it.message}" } ?: ""))
55+
try {
56+
addCompilationLog(msg, ProjectSeveriry.WARNING, classNameOverride = "WARNING")
57+
} catch (_: Exception) {}
58+
}
59+
60+
61+
/**
62+
* Adds a compilation log entry to the `compilationLogs` map based on the string log.
63+
*
64+
* @param msg The raw log message containing information about the compilation event,
65+
* including the file path and error details.
66+
* @param severity The severity level of the compilation event, represented by the `ProjectSeveriry` enum.
67+
* @param classNameOverride An optional override for the class name that will be recorded in the log.
68+
* If null, it will be derived from the file path in the message.
69+
*/
70+
private fun addCompilationLog(msg: String, severity: ProjectSeveriry, classNameOverride: String?) {
71+
val path = msg.split(" ")[0]
72+
val className = path.split("/").last().split(".").first()
73+
val message = msg.split(path)[1].drop(1)
74+
val splitPath = path.split(":")
75+
val line = splitPath[splitPath.size - 4].toInt() - 1
76+
val ch = splitPath[splitPath.size - 3].toInt() - 1
77+
val endLine = splitPath[splitPath.size - 2].toInt() - 1
78+
val endCh = splitPath[splitPath.size - 1].toInt() - 1
79+
val ed = ErrorDescriptor(
80+
TextInterval(TextInterval.TextPosition(line, ch), TextInterval.TextPosition(endLine, endCh)),
81+
message,
82+
severity,
83+
classNameOverride ?: className
84+
)
85+
compilationLogs["$className.kt"]?.add(ed)
86+
}
87+
}

src/main/kotlin/com/compiler/server/compiler/components/KotlinCompiler.kt

Lines changed: 78 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.compiler.server.compiler.components
22

33
import com.compiler.server.executor.CommandLineArgument
44
import com.compiler.server.executor.JavaExecutor
5+
import com.compiler.server.model.CompilerDiagnostics
56
import com.compiler.server.model.ExtendedCompilerArgument
67
import com.compiler.server.model.JvmExecutionResult
78
import com.compiler.server.model.OutputDirectory
@@ -12,7 +13,9 @@ import com.compiler.server.utils.CompilerArgumentsUtil
1213
import component.KotlinEnvironment
1314
import executors.JUnitExecutors
1415
import executors.JavaRunnerExecutor
15-
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
16+
import org.jetbrains.kotlin.buildtools.api.ExperimentalBuildToolsApi
17+
import org.jetbrains.kotlin.buildtools.api.KotlinToolchains
18+
import org.jetbrains.kotlin.buildtools.api.jvm.JvmPlatformToolchain
1619
import org.jetbrains.org.objectweb.asm.ClassReader
1720
import org.jetbrains.org.objectweb.asm.ClassReader.*
1821
import org.jetbrains.org.objectweb.asm.ClassVisitor
@@ -63,7 +66,12 @@ class KotlinCompiler(
6366
?.joinToString("\n\n")
6467
}
6568

66-
fun run(files: List<ProjectFile>, addByteCode: Boolean, args: String, userCompilerArguments: Map<String, Any>): JvmExecutionResult {
69+
fun run(
70+
files: List<ProjectFile>,
71+
addByteCode: Boolean,
72+
args: String,
73+
userCompilerArguments: Map<String, Any>
74+
): JvmExecutionResult {
6775
return execute(files, addByteCode, userCompilerArguments) { output, compiled ->
6876
val mainClass = JavaRunnerExecutor::class.java.name
6977
val compiledMainClass = when (compiled.mainClasses.size) {
@@ -86,37 +94,86 @@ class KotlinCompiler(
8694
}
8795
}
8896

89-
fun test(files: List<ProjectFile>, addByteCode: Boolean, userCompilerArguments: Map<String, Any>): JvmExecutionResult {
97+
fun test(
98+
files: List<ProjectFile>,
99+
addByteCode: Boolean,
100+
userCompilerArguments: Map<String, Any>
101+
): JvmExecutionResult {
90102
return execute(files, addByteCode, userCompilerArguments) { output, _ ->
91103
val mainClass = JUnitExecutors::class.java.name
92104
javaExecutor.execute(argsFrom(mainClass, output, listOf(output.path.toString())))
93105
.asJUnitExecutionResult()
94106
}
95107
}
96108

97-
@OptIn(ExperimentalPathApi::class)
98-
fun compile(files: List<ProjectFile>, userCompilerArguments: Map<String, Any>): CompilationResult<JvmClasses> = usingTempDirectory { inputDir ->
99-
val ioFiles = files.writeToIoFiles(inputDir)
100-
usingTempDirectory { outputDir ->
101-
val arguments = ioFiles.map { it.absolutePathString() } +
102-
compilerArgumentsUtil.convertCompilerArgumentsToCompilationString(jvmCompilerArguments, compilerArgumentsUtil.PREDEFINED_JVM_ARGUMENTS, userCompilerArguments) +
103-
listOf("-d", outputDir.absolutePathString())
104-
K2JVMCompiler().tryCompilation(inputDir, ioFiles, arguments) {
105-
val outputFiles = buildMap {
106-
outputDir.visitFileTree {
107-
onVisitFile { file, _ ->
108-
put(file.relativeTo(outputDir).pathString, file.readBytes())
109-
FileVisitResult.CONTINUE
110-
}
109+
fun compile(files: List<ProjectFile>, userCompilerArguments: Map<String, Any>): CompilationResult<JvmClasses> =
110+
usingTempDirectory { inputDir ->
111+
val ioFiles = files.writeToIoFiles(inputDir)
112+
usingTempDirectory { outputDir ->
113+
val arguments = ioFiles.map { it.absolutePathString() } +
114+
compilerArgumentsUtil.convertCompilerArgumentsToCompilationString(
115+
jvmCompilerArguments,
116+
compilerArgumentsUtil.PREDEFINED_JVM_ARGUMENTS,
117+
userCompilerArguments
118+
)
119+
val result = compileWithToolchain(inputDir, outputDir, arguments)
120+
return@usingTempDirectory result
121+
}
122+
}
123+
124+
@OptIn(ExperimentalPathApi::class, ExperimentalBuildToolsApi::class, ExperimentalBuildToolsApi::class)
125+
private fun compileWithToolchain(
126+
inputDir: Path,
127+
outputDir: Path,
128+
arguments: List<String>
129+
): CompilationResult<JvmClasses> {
130+
val sources = inputDir.listDirectoryEntries()
131+
132+
val logger = CompilationLogger()
133+
logger.compilationLogs = sources
134+
.filter { it.name.endsWith(".kt") }
135+
.associate { it.name to mutableListOf() }
136+
137+
val toolchains = KotlinToolchains.loadImplementation(ClassLoader.getSystemClassLoader())
138+
val jvmToolchain = toolchains.getToolchain(JvmPlatformToolchain::class.java)
139+
val operation = jvmToolchain.createJvmCompilationOperation(sources, outputDir)
140+
operation.compilerArguments.applyArgumentStrings(arguments)
141+
142+
toolchains.createBuildSession().use { session ->
143+
val result = try {
144+
session.executeOperation(operation, toolchains.createInProcessExecutionPolicy(), logger)
145+
} catch (e: Exception) {
146+
throw Exception("Exception executing compilation operation", e)
147+
}
148+
return toCompilationResult(result, logger, outputDir)
149+
}
150+
}
151+
152+
private fun toCompilationResult(
153+
buildResult: org.jetbrains.kotlin.buildtools.api.CompilationResult,
154+
logger: CompilationLogger,
155+
outputDir: Path,
156+
): CompilationResult<JvmClasses> = when (buildResult) {
157+
org.jetbrains.kotlin.buildtools.api.CompilationResult.COMPILATION_SUCCESS -> {
158+
val compilerDiagnostics = CompilerDiagnostics(logger.compilationLogs)
159+
val outputFiles = buildMap {
160+
outputDir.visitFileTree {
161+
onVisitFile { file, _ ->
162+
put(file.relativeTo(outputDir).pathString, file.readBytes())
163+
FileVisitResult.CONTINUE
111164
}
112165
}
113-
val mainClasses = findMainClasses(outputFiles)
114-
JvmClasses(
166+
}
167+
Compiled(
168+
compilerDiagnostics = compilerDiagnostics,
169+
result = JvmClasses(
115170
files = outputFiles,
116-
mainClasses = mainClasses,
171+
mainClasses = findMainClasses(outputFiles),
117172
)
118-
}
173+
)
119174
}
175+
176+
else -> NotCompiled(CompilerDiagnostics(logger.compilationLogs))
120177
}
121178

122179
private fun findMainClasses(outputFiles: Map<String, ByteArray>): Set<String> =
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.compiler.server.configuration
2+
3+
import org.springframework.context.annotation.Configuration
4+
5+
@Configuration
6+
class BuildToolsConfig {
7+
init {
8+
/**
9+
* This flag is used by KotlinMessageRenderer in kotlin-build-tools-api to properly format
10+
* returned log messages during compilation. When this flag is set, the whole position of
11+
* a warning/error is returned instead of only the beginning. We need this behavior to
12+
* process messages in KotlinLogger and then correctly mark errors on the frontend.
13+
*
14+
* Setting this property should be removed after KT-80963 is implemented, as KotlinLogger
15+
* will return the full position of an error by default then.
16+
*/
17+
System.setProperty("org.jetbrains.kotlin.buildtools.logger.extendedLocation", "true")
18+
}
19+
}

src/main/kotlin/com/compiler/server/model/TextInterval.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.compiler.server.model
22

3-
import com.intellij.openapi.editor.Document
3+
import org.jetbrains.kotlin.com.intellij.openapi.editor.Document
44

55
data class TextInterval(val start: TextPosition, val end: TextPosition) {
66
data class TextPosition(val line: Int, val ch: Int) : Comparable<TextPosition> {

src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import com.compiler.server.model.JsCompilerArguments
77
import com.compiler.server.model.bean.VersionInfo
88
import component.KotlinEnvironment
99
import model.Completion
10-
import org.junit.Ignore
1110
import org.slf4j.LoggerFactory
1211
import org.springframework.stereotype.Component
1312

@@ -45,7 +44,6 @@ class KotlinProjectExecutor(
4544
}
4645

4746
// TODO(Dmitrii Krasnov): implement this method in KTL-2807
48-
@Ignore
4947
fun complete(project: Project, line: Int, character: Int): List<Completion> {
5048
return emptyList()
5149
}
@@ -117,5 +115,4 @@ class KotlinProjectExecutor(
117115
getVersion().version
118116
)
119117
}
120-
121118
}

src/test/kotlin/com/compiler/server/CompilerArgumentsEndpointTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import com.compiler.server.model.ProjectType
44
import com.compiler.server.model.bean.VersionInfo
55
import com.fasterxml.jackson.databind.ObjectMapper
66
import component.KotlinEnvironment
7-
import org.junit.jupiter.api.Test
87
import org.junit.jupiter.params.ParameterizedTest
98
import org.junit.jupiter.params.provider.EnumSource
109
import org.springframework.beans.factory.annotation.Autowired

src/test/resources/compiler-arguments/compose-wasm-expected-compiler-args.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,8 +1388,8 @@
13881388
"description": "Use an updated version of the exception proposal with try_table.",
13891389
"type": {
13901390
"type": "com.compiler.server.model.BooleanExtendedCompilerArgumentValue",
1391-
"isNullable": false,
1392-
"defaultValue": false
1391+
"isNullable": true,
1392+
"defaultValue": null
13931393
},
13941394
"disabled": false,
13951395
"predefinedValues": null

0 commit comments

Comments
 (0)