Skip to content

Commit e5a3907

Browse files
bnormtschuchortdev
authored andcommitted
Create an AbstractKotlinCompilation for common code
1 parent d66d9b7 commit e5a3907

File tree

7 files changed

+306
-529
lines changed

7 files changed

+306
-529
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package com.tschuchort.compiletesting
2+
3+
import io.github.classgraph.ClassGraph
4+
import okio.Buffer
5+
import org.jetbrains.kotlin.base.kapt3.KaptOptions
6+
import org.jetbrains.kotlin.cli.common.CLICompiler
7+
import org.jetbrains.kotlin.cli.common.ExitCode
8+
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
9+
import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments
10+
import org.jetbrains.kotlin.cli.common.arguments.validateArguments
11+
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
12+
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
13+
import org.jetbrains.kotlin.cli.js.K2JSCompiler
14+
import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite
15+
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
16+
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
17+
import org.jetbrains.kotlin.config.Services
18+
import java.io.File
19+
import java.io.OutputStream
20+
import java.io.PrintStream
21+
import java.net.URI
22+
import java.nio.file.Files
23+
import java.nio.file.Paths
24+
25+
abstract class AbstractKotlinCompilation<A : CommonCompilerArguments> {
26+
/** Working directory for the compilation */
27+
var workingDir: File by default {
28+
val path = Files.createTempDirectory("Kotlin-Compilation")
29+
log("Created temporary working directory at ${path.toAbsolutePath()}")
30+
return@default path.toFile()
31+
}
32+
33+
/**
34+
* Paths to directories or .jar files that contain classes
35+
* to be made available in the compilation (i.e. added to
36+
* the classpath)
37+
*/
38+
var classpaths: List<File> = emptyList()
39+
40+
/**
41+
* Paths to plugins to be made available in the compilation
42+
*/
43+
var pluginClasspaths: List<File> = emptyList()
44+
45+
/**
46+
* Compiler plugins that should be added to the compilation
47+
*/
48+
var compilerPlugins: List<ComponentRegistrar> = emptyList()
49+
50+
/**
51+
* Commandline processors for compiler plugins that should be added to the compilation
52+
*/
53+
var commandLineProcessors: List<CommandLineProcessor> = emptyList()
54+
55+
/** Source files to be compiled */
56+
var sources: List<SourceFile> = emptyList()
57+
58+
/** Print verbose logging info */
59+
var verbose: Boolean = true
60+
61+
/**
62+
* Helpful information (if [verbose] = true) and the compiler
63+
* system output will be written to this stream
64+
*/
65+
var messageOutputStream: OutputStream = System.out
66+
67+
/** Inherit classpath from calling process */
68+
var inheritClassPath: Boolean = false
69+
70+
/** Suppress all warnings */
71+
var suppressWarnings: Boolean = false
72+
73+
/** All warnings should be treated as errors */
74+
var allWarningsAsErrors: Boolean = false
75+
76+
/** Report locations of files generated by the compiler */
77+
var reportOutputFiles: Boolean by default { verbose }
78+
79+
/** Report on performance of the compilation */
80+
var reportPerformance: Boolean = false
81+
82+
83+
/** Additional string arguments to the Kotlin compiler */
84+
var kotlincArguments: List<String> = emptyList()
85+
86+
/** Options to be passed to compiler plugins: -P plugin:<pluginId>:<optionName>=<value>*/
87+
var pluginOptions: List<PluginOption> = emptyList()
88+
89+
/**
90+
* Path to the kotlin-stdlib-common.jar
91+
* If none is given, it will be searched for in the host
92+
* process' classpaths
93+
*/
94+
var kotlinStdLibCommonJar: File? by default {
95+
findInHostClasspath(hostClasspaths, "kotlin-stdlib-common.jar",
96+
kotlinDependencyRegex("kotlin-stdlib-common"))
97+
}
98+
99+
// Directory for input source files
100+
protected val sourcesDir get() = workingDir.resolve("sources")
101+
102+
protected fun commonArguments(args: A, configuration: (args: A) -> Unit): A {
103+
args.pluginClasspaths = pluginClasspaths.map(File::getAbsolutePath).toTypedArray()
104+
105+
args.verbose = verbose
106+
107+
args.suppressWarnings = suppressWarnings
108+
args.allWarningsAsErrors = allWarningsAsErrors
109+
args.reportOutputFiles = reportOutputFiles
110+
args.reportPerf = reportPerformance
111+
112+
configuration(args)
113+
114+
/**
115+
* It's not possible to pass dynamic [CommandLineProcessor] instances directly to the [K2JSCompiler]
116+
* because the compiler discovers them on the classpath through a service locator, so we need to apply
117+
* the same trick as with [ComponentRegistrar]s: We put our own static [CommandLineProcessor] on the
118+
* classpath which in turn calls the user's dynamic [CommandLineProcessor] instances.
119+
*/
120+
MainCommandLineProcessor.threadLocalParameters.set(
121+
MainCommandLineProcessor.ThreadLocalParameters(commandLineProcessors)
122+
)
123+
124+
/**
125+
* Our [MainCommandLineProcessor] only has access to the CLI options that belong to its own plugin ID.
126+
* So in order to be able to access CLI options that are meant for other [CommandLineProcessor]s we
127+
* wrap these CLI options, send them to our own plugin ID and later unwrap them again to forward them
128+
* to the correct [CommandLineProcessor].
129+
*/
130+
args.pluginOptions = pluginOptions.map { (pluginId, optionName, optionValue) ->
131+
"plugin:${MainCommandLineProcessor.pluginId}:${MainCommandLineProcessor.encodeForeignOptionName(pluginId, optionName)}=$optionValue"
132+
}.toTypedArray()
133+
134+
/* Parse extra CLI arguments that are given as strings so users can specify arguments that are not yet
135+
implemented here as well-typed properties. */
136+
parseCommandLineArguments(kotlincArguments, args)
137+
138+
validateArguments(args.errors)?.let {
139+
throw IllegalArgumentException("Errors parsing kotlinc CLI arguments:\n$it")
140+
}
141+
142+
return args
143+
}
144+
145+
/** Performs the compilation step to compile Kotlin source files */
146+
protected fun compileKotlin(sources: List<File>, compiler: CLICompiler<A>, arguments: A): KotlinCompilation.ExitCode {
147+
148+
/**
149+
* Here the list of compiler plugins is set
150+
*
151+
* To avoid that the annotation processors are executed twice,
152+
* the list is set to empty
153+
*/
154+
MainComponentRegistrar.threadLocalParameters.set(
155+
MainComponentRegistrar.ThreadLocalParameters(
156+
listOf(),
157+
KaptOptions.Builder(),
158+
compilerPlugins
159+
)
160+
)
161+
162+
// if no Kotlin sources are available, skip the compileKotlin step
163+
if (sources.none(File::hasKotlinFileExtension))
164+
return KotlinCompilation.ExitCode.OK
165+
166+
// in this step also include source files generated by kapt in the previous step
167+
val args = arguments.also { args ->
168+
args.freeArgs = sources.map(File::getAbsolutePath).distinct()
169+
args.pluginClasspaths = (args.pluginClasspaths ?: emptyArray()) + arrayOf(getResourcesPath())
170+
}
171+
172+
val compilerMessageCollector = PrintingMessageCollector(
173+
internalMessageStream, MessageRenderer.GRADLE_STYLE, verbose
174+
)
175+
176+
return convertKotlinExitCode(
177+
compiler.exec(compilerMessageCollector, Services.EMPTY, args)
178+
)
179+
}
180+
181+
protected fun getResourcesPath(): String {
182+
val resourceName = "META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar"
183+
return this::class.java.classLoader.getResources(resourceName)
184+
.asSequence()
185+
.mapNotNull { url ->
186+
val uri = URI.create(url.toString().removeSuffix("/$resourceName"))
187+
when (uri.scheme) {
188+
"jar" -> Paths.get(URI.create(uri.schemeSpecificPart.removeSuffix("!")))
189+
"file" -> Paths.get(uri)
190+
else -> return@mapNotNull null
191+
}.toAbsolutePath()
192+
}
193+
.find { resourcesPath ->
194+
ServiceLoaderLite.findImplementations(ComponentRegistrar::class.java, listOf(resourcesPath.toFile()))
195+
.any { implementation -> implementation == MainComponentRegistrar::class.java.name }
196+
}?.toString() ?: throw AssertionError("Could not get path to ComponentRegistrar service from META-INF")
197+
}
198+
199+
/** Searches compiler log for known errors that are hard to debug for the user */
200+
protected fun searchSystemOutForKnownErrors(compilerSystemOut: String) {
201+
if (compilerSystemOut.contains("No enum constant com.sun.tools.javac.main.Option.BOOT_CLASS_PATH")) {
202+
warn(
203+
"${this::class.simpleName} has detected that the compiler output contains an error message that may be " +
204+
"caused by including a tools.jar file together with a JDK of version 9 or later. " +
205+
if (inheritClassPath)
206+
"Make sure that no tools.jar (or unwanted JDK) is in the inherited classpath"
207+
else ""
208+
)
209+
}
210+
211+
if (compilerSystemOut.contains("Unable to find package java.")) {
212+
warn(
213+
"${this::class.simpleName} has detected that the compiler output contains an error message " +
214+
"that may be caused by a missing JDK. This can happen if jdkHome=null and inheritClassPath=false."
215+
)
216+
}
217+
}
218+
219+
/** Tries to find a file matching the given [regex] in the host process' classpath */
220+
protected fun findInHostClasspath(hostClasspaths: List<File>, simpleName: String, regex: Regex): File? {
221+
val jarFile = hostClasspaths.firstOrNull { classpath ->
222+
classpath.name.matches(regex)
223+
//TODO("check that jar file actually contains the right classes")
224+
}
225+
226+
if (jarFile == null)
227+
log("Searched host classpaths for $simpleName and found no match")
228+
else
229+
log("Searched host classpaths for $simpleName and found ${jarFile.path}")
230+
231+
return jarFile
232+
}
233+
234+
protected val hostClasspaths by lazy { getHostClasspaths() }
235+
236+
/* This internal buffer and stream is used so it can be easily converted to a string
237+
that is put into the [Result] object, in addition to printing immediately to the user's
238+
stream. */
239+
protected val internalMessageBuffer = Buffer()
240+
protected val internalMessageStream = PrintStream(
241+
TeeOutputStream(
242+
object : OutputStream() {
243+
override fun write(b: Int) = messageOutputStream.write(b)
244+
override fun write(b: ByteArray) = messageOutputStream.write(b)
245+
override fun write(b: ByteArray, off: Int, len: Int) = messageOutputStream.write(b, off, len)
246+
override fun flush() = messageOutputStream.flush()
247+
override fun close() = messageOutputStream.close()
248+
},
249+
internalMessageBuffer.outputStream()
250+
)
251+
)
252+
253+
protected fun log(s: String) {
254+
if (verbose)
255+
internalMessageStream.println("logging: $s")
256+
}
257+
258+
protected fun warn(s: String) = internalMessageStream.println("warning: $s")
259+
protected fun error(s: String) = internalMessageStream.println("error: $s")
260+
}
261+
262+
internal fun kotlinDependencyRegex(prefix:String): Regex {
263+
return Regex("$prefix(-[0-9]+\\.[0-9]+(\\.[0-9]+)?)([-0-9a-zA-Z]+)?\\.jar")
264+
}
265+
266+
/** Returns the files on the classloader's classpath and modulepath */
267+
internal fun getHostClasspaths(): List<File> {
268+
val classGraph = ClassGraph()
269+
.enableSystemJarsAndModules()
270+
.removeTemporaryFilesAfterScan()
271+
272+
val classpaths = classGraph.classpathFiles
273+
val modules = classGraph.modules.mapNotNull { it.locationFile }
274+
275+
return (classpaths + modules).distinctBy(File::getAbsolutePath)
276+
}
277+
278+
internal fun convertKotlinExitCode(code: ExitCode) = when(code) {
279+
ExitCode.OK -> KotlinCompilation.ExitCode.OK
280+
ExitCode.INTERNAL_ERROR -> KotlinCompilation.ExitCode.INTERNAL_ERROR
281+
ExitCode.COMPILATION_ERROR -> KotlinCompilation.ExitCode.COMPILATION_ERROR
282+
ExitCode.SCRIPT_EXECUTION_ERROR -> KotlinCompilation.ExitCode.SCRIPT_EXECUTION_ERROR
283+
}

core/src/main/kotlin/com/tschuchort/compiletesting/ExitCode.kt

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)