-
Notifications
You must be signed in to change notification settings - Fork 61
/
AbstractKotlinCompilation.kt
332 lines (278 loc) · 14.3 KB
/
AbstractKotlinCompilation.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
package com.tschuchort.compiletesting
import okio.Buffer
import org.jetbrains.kotlin.base.kapt3.KaptOptions
import org.jetbrains.kotlin.cli.common.CLICompiler
import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments
import org.jetbrains.kotlin.cli.common.arguments.validateArguments
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
import org.jetbrains.kotlin.cli.js.K2JSCompiler
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.util.ServiceLoaderLite
import java.io.File
import java.io.OutputStream
import java.io.PrintStream
import java.net.URI
import java.nio.file.Files
import java.nio.file.Paths
/**
* Base compilation class for sharing common compiler arguments and
* functionality. Should not be used outside of this library as it is an
* implementation detail.
*/
@ExperimentalCompilerApi
abstract class AbstractKotlinCompilation<A : CommonCompilerArguments> internal constructor() {
/** Working directory for the compilation */
var workingDir: File by default {
val path = Files.createTempDirectory("Kotlin-Compilation")
log("Created temporary working directory at ${path.toAbsolutePath()}")
return@default path.toFile()
}
/**
* Paths to directories or .jar files that contain classes
* to be made available in the compilation (i.e. added to
* the classpath)
*/
var classpaths: List<File> = emptyList()
/**
* Paths to plugins to be made available in the compilation
*/
var pluginClasspaths: List<File> = emptyList()
@Suppress("DEPRECATION")
@Deprecated(
"Renamed to componentRegistrars due to introduction of CompilerPluginRegistrar",
ReplaceWith("componentRegistrars"),
DeprecationLevel.ERROR
)
var compilerPlugins: List<ComponentRegistrar> = emptyList()
/**
* Legacy compiler plugins that should be added to the compilation.
* This option will be removed in the future; you should migrate to [CompilerPluginRegistrar].
*/
@Suppress("DEPRECATION")
@Deprecated("Deprecated in Kotlin compiler. Migrate to compilerPluginRegistrars instead")
var componentRegistrars: List<ComponentRegistrar> = emptyList()
/**
* Compiler plugins that should be added to the compilation
*/
var compilerPluginRegistrars: List<CompilerPluginRegistrar> = emptyList()
/**
* Commandline processors for compiler plugins that should be added to the compilation
*/
var commandLineProcessors: List<CommandLineProcessor> = emptyList()
/** Source files to be compiled */
var sources: List<SourceFile> = emptyList()
/** Print verbose logging info */
var verbose: Boolean = true
/**
* Helpful information (if [verbose] = true) and the compiler
* system output will be written to this stream
*/
var messageOutputStream: OutputStream = System.out
/** Inherit classpath from calling process */
var inheritClassPath: Boolean = false
/** Suppress all warnings */
var suppressWarnings: Boolean = false
/** All warnings should be treated as errors */
var allWarningsAsErrors: Boolean = false
/** Report locations of files generated by the compiler */
var reportOutputFiles: Boolean by default { verbose }
/** Report on performance of the compilation */
var reportPerformance: Boolean = false
var languageVersion: String? = null
/** Use the new experimental K2 compiler */
var useK2: Boolean by default { false }
/** Enable experimental multiplatform support */
var multiplatform: Boolean = false
/** Do not check presence of 'actual' modifier in multi-platform projects */
var noCheckActual: Boolean = false
/** Enable usages of API that requires opt-in with an opt-in requirement marker with the given fully qualified name */
var optIn: List<String>? = null
/** Additional string arguments to the Kotlin compiler */
var kotlincArguments: List<String> = emptyList()
/** Options to be passed to compiler plugins: -P plugin:<pluginId>:<optionName>=<value>*/
var pluginOptions: List<PluginOption> = emptyList()
/**
* Path to the kotlin-stdlib-common.jar
* If none is given, it will be searched for in the host
* process' classpaths
*/
var kotlinStdLibCommonJar: File? by default {
HostEnvironment.kotlinStdLibCommonJar
}
/** Enable support for the new K2 compiler. */
var supportsK2 = false
// Directory for input source files
protected val sourcesDir get() = workingDir.resolve("sources")
protected inline fun <reified T> CommonCompilerArguments.trySetDeprecatedOption(optionSimpleName: String, value: T) {
try {
this.javaClass.getMethod(JvmAbi.setterName(optionSimpleName), T::class.java).invoke(this, value)
} catch (e: ReflectiveOperationException) {
throw IllegalArgumentException(
"The deprecated option $optionSimpleName is no longer available in the kotlin version you are using",
e
)
}
}
protected fun commonArguments(args: A, configuration: (args: A) -> Unit): A {
args.pluginClasspaths = pluginClasspaths.map(File::getAbsolutePath).toTypedArray()
args.verbose = verbose
args.suppressWarnings = suppressWarnings
args.allWarningsAsErrors = allWarningsAsErrors
args.reportOutputFiles = reportOutputFiles
args.reportPerf = reportPerformance
args.useK2 = useK2
args.multiPlatform = multiplatform
args.noCheckActual = noCheckActual
args.optIn = optIn?.toTypedArray()
if (languageVersion != null)
args.languageVersion = this.languageVersion
configuration(args)
/**
* It's not possible to pass dynamic [CommandLineProcessor] instances directly to the [K2JSCompiler]
* because the compiler discovers them on the classpath through a service locator, so we need to apply
* the same trick as with [ComponentRegistrar]s: We put our own static [CommandLineProcessor] on the
* classpath which in turn calls the user's dynamic [CommandLineProcessor] instances.
*/
MainCommandLineProcessor.threadLocalParameters.set(
MainCommandLineProcessor.ThreadLocalParameters(commandLineProcessors)
)
/**
* Our [MainCommandLineProcessor] only has access to the CLI options that belong to its own plugin ID.
* So in order to be able to access CLI options that are meant for other [CommandLineProcessor]s we
* wrap these CLI options, send them to our own plugin ID and later unwrap them again to forward them
* to the correct [CommandLineProcessor].
*/
args.pluginOptions = pluginOptions.map { (pluginId, optionName, optionValue) ->
"plugin:${MainCommandLineProcessor.pluginId}:${MainCommandLineProcessor.encodeForeignOptionName(pluginId, optionName)}=$optionValue"
}.toTypedArray()
/* Parse extra CLI arguments that are given as strings so users can specify arguments that are not yet
implemented here as well-typed properties. */
parseCommandLineArguments(kotlincArguments, args)
validateArguments(args.errors)?.let {
throw IllegalArgumentException("Errors parsing kotlinc CLI arguments:\n$it")
}
return args
}
/** Performs the compilation step to compile Kotlin source files */
protected fun compileKotlin(sources: List<File>, compiler: CLICompiler<A>, arguments: A): KotlinCompilation.ExitCode {
/**
* Here the list of compiler plugins is set
*
* To avoid that the annotation processors are executed twice,
* the list is set to empty
*/
@Suppress("DEPRECATION")
MainComponentAndPluginRegistrar.threadLocalParameters.set(
MainComponentAndPluginRegistrar.ThreadLocalParameters(
listOf(),
KaptOptions.Builder(),
componentRegistrars,
compilerPluginRegistrars,
supportsK2
)
)
// in this step also include source files generated by kapt in the previous step
val args = arguments.also { args ->
args.freeArgs =
sources.map(File::getAbsolutePath).distinct() + if (sources.none(File::hasKotlinFileExtension)) {
/* __HACK__: The Kotlin compiler expects at least one Kotlin source file or it will crash,
so we trick the compiler by just including an empty .kt-File. We need the compiler to run
even if there are no Kotlin files because some compiler plugins may also process Java files. */
listOf(SourceFile.new("emptyKotlinFile.kt", "").writeIfNeeded(sourcesDir).absolutePath)
} else {
emptyList()
}
@Suppress("DEPRECATION")
args.pluginClasspaths = (args.pluginClasspaths ?: emptyArray()) +
/** The resources path contains the MainComponentRegistrar and MainCommandLineProcessor which will
be found by the Kotlin compiler's service loader. We add it only when the user has actually given
us ComponentRegistrar instances to be loaded by the MainComponentRegistrar because the experimental
K2 compiler doesn't support plugins yet. This way, users of K2 can prevent MainComponentRegistrar
from being loaded and crashing K2 by setting both [componentRegistrars] and [commandLineProcessors]
and [compilerPluginRegistrars] to the emptyList. */
if (componentRegistrars.union(compilerPluginRegistrars).union(commandLineProcessors).isNotEmpty())
arrayOf(getResourcesPath())
else emptyArray()
}
val compilerMessageCollector = PrintingMessageCollector(
internalMessageStream, MessageRenderer.GRADLE_STYLE, verbose
)
return convertKotlinExitCode(
compiler.exec(compilerMessageCollector, Services.EMPTY, args)
)
}
protected fun getResourcesPath(): String {
val resourceName = "META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar"
return this::class.java.classLoader.getResources(resourceName)
.asSequence()
.mapNotNull { url ->
val uri = URI.create(url.toString().removeSuffix("/$resourceName"))
when (uri.scheme) {
"jar" -> Paths.get(URI.create(uri.schemeSpecificPart.removeSuffix("!")))
"file" -> Paths.get(uri)
else -> return@mapNotNull null
}.toAbsolutePath()
}
.find { resourcesPath ->
ServiceLoaderLite.findImplementations(CompilerPluginRegistrar::class.java, listOf(resourcesPath.toFile()))
.any { implementation -> implementation == MainComponentAndPluginRegistrar::class.java.name }
}?.toString() ?: throw AssertionError("Could not get path to CompilerPluginRegistrar service from META-INF")
}
/** Searches compiler log for known errors that are hard to debug for the user */
protected fun searchSystemOutForKnownErrors(compilerSystemOut: String) {
if (compilerSystemOut.contains("No enum constant com.sun.tools.javac.main.Option.BOOT_CLASS_PATH")) {
warn(
"${this::class.simpleName} has detected that the compiler output contains an error message that may be " +
"caused by including a tools.jar file together with a JDK of version 9 or later. " +
if (inheritClassPath)
"Make sure that no tools.jar (or unwanted JDK) is in the inherited classpath"
else ""
)
}
if (compilerSystemOut.contains("Unable to find package java.")) {
warn(
"${this::class.simpleName} has detected that the compiler output contains an error message " +
"that may be caused by a missing JDK. This can happen if jdkHome=null and inheritClassPath=false."
)
}
}
protected val hostClasspaths by lazy { HostEnvironment.classpath }
/* This internal buffer and stream is used so it can be easily converted to a string
that is put into the [Result] object, in addition to printing immediately to the user's
stream. */
protected val internalMessageBuffer = Buffer()
protected val internalMessageStream = PrintStream(
TeeOutputStream(
object : OutputStream() {
override fun write(b: Int) = messageOutputStream.write(b)
override fun write(b: ByteArray) = messageOutputStream.write(b)
override fun write(b: ByteArray, off: Int, len: Int) = messageOutputStream.write(b, off, len)
override fun flush() = messageOutputStream.flush()
override fun close() = messageOutputStream.close()
},
internalMessageBuffer.outputStream()
)
)
protected fun log(s: String) {
if (verbose)
internalMessageStream.println("logging: $s")
}
protected fun warn(s: String) = internalMessageStream.println("warning: $s")
protected fun error(s: String) = internalMessageStream.println("error: $s")
internal val internalMessageStreamAccess: PrintStream get() = internalMessageStream
}
internal fun convertKotlinExitCode(code: ExitCode) = when(code) {
ExitCode.OK -> KotlinCompilation.ExitCode.OK
ExitCode.INTERNAL_ERROR -> KotlinCompilation.ExitCode.INTERNAL_ERROR
ExitCode.COMPILATION_ERROR -> KotlinCompilation.ExitCode.COMPILATION_ERROR
ExitCode.SCRIPT_EXECUTION_ERROR -> KotlinCompilation.ExitCode.SCRIPT_EXECUTION_ERROR
ExitCode.OOM_ERROR -> throw OutOfMemoryError("Kotlin compiler ran out of memory")
}