Skip to content

Commit

Permalink
Implement multiplatform common source flag #377
Browse files Browse the repository at this point in the history
  • Loading branch information
tschuchortdev committed May 22, 2023
1 parent 32cfae2 commit 82ec542
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import java.io.OutputStream
import java.io.PrintStream
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.absolutePathString

/**
* Base compilation class for sharing common compiler arguments and
Expand Down Expand Up @@ -141,6 +143,15 @@ abstract class AbstractKotlinCompilation<A : CommonCompilerArguments> internal c
// Directory for input source files
protected val sourcesDir get() = workingDir.resolve("sources")

protected data class SourceWithPath(val path: Path, val isCommonSource: Boolean)

protected val sourcesWithPath: List<SourceWithPath> by lazy {
sources.map {
val path = Paths.get(it.writeIfNeeded(sourcesDir).absolutePath)
SourceWithPath(path, it.isMultiplatformCommonSource)
}
}

protected inline fun <reified T> CommonCompilerArguments.trySetDeprecatedOption(optionSimpleName: String, value: T) {
try {
this.javaClass.getMethod(JvmAbi.setterName(optionSimpleName), T::class.java).invoke(this, value)
Expand Down Expand Up @@ -169,6 +180,8 @@ abstract class AbstractKotlinCompilation<A : CommonCompilerArguments> internal c
if (languageVersion != null)
args.languageVersion = this.languageVersion

args.commonSources = sourcesWithPath.filter { it.isCommonSource }.map { it.path.toString() }.toTypedArray()

configuration(args)

/**
Expand Down Expand Up @@ -203,7 +216,7 @@ abstract class AbstractKotlinCompilation<A : CommonCompilerArguments> internal c
}

/** Performs the compilation step to compile Kotlin source files */
protected fun compileKotlin(sources: List<File>, compiler: CLICompiler<A>, arguments: A): KotlinCompilation.ExitCode {
protected fun compileKotlin(sources: List<Path>, compiler: CLICompiler<A>, arguments: A): KotlinCompilation.ExitCode {

/**
* Here the list of compiler plugins is set
Expand All @@ -225,7 +238,7 @@ abstract class AbstractKotlinCompilation<A : CommonCompilerArguments> internal c
// 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)) {
sources.map(Path::absolutePathString).distinct() + if (sources.none(Path::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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import java.net.URLClassLoader
import java.nio.file.Path
import javax.annotation.processing.Processor
import javax.tools.*
import kotlin.io.path.absolutePathString

data class PluginOption(val pluginId: PluginId, val optionName: OptionName, val optionValue: OptionValue)

Expand Down Expand Up @@ -350,7 +351,7 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
}

/** Performs the 1st and 2nd compilation step to generate stubs and run annotation processors */
private fun stubsAndApt(sourceFiles: List<File>): ExitCode {
private fun stubsAndApt(sourceFiles: List<Path>): ExitCode {
if(annotationProcessors.isEmpty()) {
log("No services were given. Not running kapt steps.")
return ExitCode.OK
Expand Down Expand Up @@ -396,10 +397,10 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
)
)

val kotlinSources = sourceFiles.filter(File::hasKotlinFileExtension)
val javaSources = sourceFiles.filter(File::hasJavaFileExtension)
val kotlinSources = sourceFiles.filter(Path::hasKotlinFileExtension)
val javaSources = sourceFiles.filter(Path::hasJavaFileExtension)

val sourcePaths = mutableListOf<File>().apply {
val sourcePaths = mutableListOf<Path>().apply {
addAll(javaSources)

if(kotlinSources.isNotEmpty()) {
Expand All @@ -414,9 +415,9 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
Java files might generate Kotlin files which then need to be compiled in the
compileKotlin step before the compileJava step). So instead we trick K2JVMCompiler
by just including an empty .kt-File. */
add(SourceFile.new("emptyKotlinFile.kt", "").writeIfNeeded(kaptBaseDir))
add(SourceFile.new("emptyKotlinFile.kt", "").writeIfNeeded(kaptBaseDir).toPath())
}
}.map(File::getAbsolutePath).distinct()
}.map(Path::absolutePathString).distinct()

if(!isJdk9OrLater()) {
try {
Expand Down Expand Up @@ -446,10 +447,10 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
}

/** Performs the 3rd compilation step to compile Kotlin source files */
private fun compileJvmKotlin(sourceFiles: List<File>): ExitCode {
val sources = sourceFiles +
kaptKotlinGeneratedDir.listFilesRecursively() +
kaptSourceDir.listFilesRecursively()
private fun compileJvmKotlin(sourceFiles: List<Path>): ExitCode {
val sources = sourcesWithPath.map { it.path } +
kaptKotlinGeneratedDir.toPath().listFilesRecursively() +
kaptSourceDir.toPath().listFilesRecursively()

return compileKotlin(sources, K2JVMCompiler(), commonK2JVMArgs())
}
Expand Down Expand Up @@ -485,9 +486,9 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
}

/** Performs the 4th compilation step to compile Java source files */
private fun compileJava(sourceFiles: List<File>): ExitCode {
val javaSources = (sourceFiles + kaptSourceDir.listFilesRecursively())
.filterNot<File>(File::hasKotlinFileExtension)
private fun compileJava(sourceFiles: List<Path>): ExitCode {
val javaSources = (sourceFiles + kaptSourceDir.toPath().listFilesRecursively())
.filterNot(Path::hasKotlinFileExtension)

if(javaSources.isEmpty())
return ExitCode.OK
Expand All @@ -508,7 +509,7 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
val isJavac9OrLater = isJavac9OrLater(getJavacVersionString(javacCommand))
val javacArgs = baseJavacArgs(isJavac9OrLater)

val javacProc = ProcessBuilder(listOf(javacCommand) + javacArgs + javaSources.map(File::getAbsolutePath))
val javacProc = ProcessBuilder(listOf(javacCommand) + javacArgs + javaSources.map(Path::absolutePathString))
.directory(workingDir)
.redirectErrorStream(true)
.start()
Expand Down Expand Up @@ -558,7 +559,7 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
OutputStreamWriter(internalMessageStream), javaFileManager,
diagnosticCollector, javacArgs,
/* classes to be annotation processed */ null,
javaSources.map { FileJavaFileObject(it) }
javaSources.map { FileJavaFileObject(it.toFile()) }
.filter { it.kind == JavaFileObject.Kind.SOURCE }
).call()

Expand Down Expand Up @@ -591,9 +592,6 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
kaptIncrementalDataDir.mkdirs()
kaptKotlinGeneratedDir.mkdirs()

// write given sources to working directory
val sourceFiles = sources.map { it.writeIfNeeded(sourcesDir) }

pluginClasspaths.forEach { filepath ->
if (!filepath.exists()) {
error("Plugin $filepath not found")
Expand All @@ -618,7 +616,7 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
withSystemProperty("idea.use.native.fs.for.win", "false") {
// step 1 and 2: generate stubs and run annotation processors
try {
val exitCode = stubsAndApt(sourceFiles)
val exitCode = stubsAndApt(sourcesWithPath.map { it.path })
if (exitCode != ExitCode.OK) {
return makeResult(exitCode)
}
Expand All @@ -627,15 +625,15 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
}

// step 3: compile Kotlin files
compileJvmKotlin(sourceFiles).let { exitCode ->
compileJvmKotlin(sourcesWithPath.map { it.path }).let { exitCode ->
if(exitCode != ExitCode.OK) {
return makeResult(exitCode)
}
}
}

// step 4: compile Java files
return makeResult(compileJava(sourceFiles))
return makeResult(compileJava(sourcesWithPath.map { it.path }))
}

private fun makeResult(exitCode: ExitCode): Result {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,6 @@ class KotlinJsCompilation : AbstractKotlinCompilation<K2JSCompilerArguments>() {
sourcesDir.mkdirs()
outputDir.mkdirs()

// write given sources to working directory
val sourceFiles = sources.map { it.writeIfNeeded(sourcesDir) }

pluginClasspaths.forEach { filepath ->
if (!filepath.exists()) {
error("Plugin $filepath not found")
Expand All @@ -119,7 +116,7 @@ class KotlinJsCompilation : AbstractKotlinCompilation<K2JSCompilerArguments>() {
*/
withSystemProperty("idea.use.native.fs.for.win", "false") {
// step 1: compile Kotlin files
return makeResult(compileKotlin(sourceFiles, K2JSCompiler(), jsArgs()))
return makeResult(compileKotlin(sourcesWithPath.map { it.path }, K2JSCompiler(), jsArgs()))
}
}

Expand Down
27 changes: 21 additions & 6 deletions core/src/main/kotlin/com/tschuchort/compiletesting/SourceFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,38 @@ import java.io.File
abstract class SourceFile {
internal abstract fun writeIfNeeded(dir: File): File

/** Marks this source file as a source of the common module in a multiplatform project */
abstract val isMultiplatformCommonSource: Boolean

companion object {
/**
* Create a new Java source file for the compilation when the compilation is run
*
* @param isMultiplatformCommonSource marks this source file as a source of the common module in a multiplatform project
*/
fun java(name: String, @Language("java") contents: String, trimIndent: Boolean = true): SourceFile {
fun java(name: String, @Language("java") contents: String, trimIndent: Boolean = true, isMultiplatformCommonSource: Boolean = false): SourceFile {
require(File(name).hasJavaFileExtension())
val finalContents = if (trimIndent) contents.trimIndent() else contents
return new(name, finalContents)
return new(name, finalContents, isMultiplatformCommonSource = isMultiplatformCommonSource)
}

/**
* Create a new Kotlin source file for the compilation when the compilation is run
*
* @param isMultiplatformCommonSource marks this source file as a source of the common module in a multiplatform project
*/
fun kotlin(name: String, @Language("kotlin") contents: String, trimIndent: Boolean = true): SourceFile {
fun kotlin(name: String, @Language("kotlin") contents: String, trimIndent: Boolean = true, isMultiplatformCommonSource: Boolean = false): SourceFile {
require(File(name).hasKotlinFileExtension())
val finalContents = if (trimIndent) contents.trimIndent() else contents
return new(name, finalContents)
return new(name, finalContents, isMultiplatformCommonSource = isMultiplatformCommonSource)
}

/**
* Create a new source file for the compilation when the compilation is run
*
* @param isMultiplatformCommonSource marks this source file as a source of the common module in a multiplatform project
*/
fun new(name: String, contents: String) = object : SourceFile() {
fun new(name: String, contents: String, isMultiplatformCommonSource: Boolean = false) = object : SourceFile() {
override fun writeIfNeeded(dir: File): File {
val file = dir.resolve(name)
file.parentFile.mkdirs()
Expand All @@ -45,17 +54,23 @@ abstract class SourceFile {

return file
}

override val isMultiplatformCommonSource: Boolean = isMultiplatformCommonSource
}

/**
* Compile an existing source file
*
* @param isMultiplatformCommonSource marks this source file as a source of the common module in a multiplatform project
*/
fun fromPath(path: File) = object : SourceFile() {
fun fromPath(path: File, isMultiplatformCommonSource: Boolean = false) = object : SourceFile() {
init {
require(path.isFile)
}

override fun writeIfNeeded(dir: File): File = path

override val isMultiplatformCommonSource: Boolean = isMultiplatformCommonSource
}
}
}
7 changes: 6 additions & 1 deletion core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ internal fun isJdk9OrLater(): Boolean
= SourceVersion.latestSupported().compareTo(SourceVersion.RELEASE_8) > 0

internal fun File.listFilesRecursively(): List<File> {
return listFiles().flatMap { file ->
return (listFiles() ?: throw RuntimeException("listFiles() was null. File is not a directory or I/O error occured"))
.flatMap { file ->
if(file.isDirectory)
file.listFilesRecursively()
else
Expand All @@ -52,6 +53,10 @@ internal fun Path.listFilesRecursively(): List<Path> {
return files
}

internal fun Path.hasKotlinFileExtension() = toFile().hasKotlinFileExtension()

internal fun Path.hasJavaFileExtension() = toFile().hasJavaFileExtension()

internal fun File.hasKotlinFileExtension() = hasFileExtension(listOf("kt", "kts"))

internal fun File.hasJavaFileExtension() = hasFileExtension(listOf("java"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,29 @@ class KotlinCompilationTests {
assertClassLoadable(result, "KSource")
}

@Test
fun `can compile annotations that may only appear in multiplatform common module sources`() {
val result = KotlinCompilation().apply {
multiplatform = true
sources = listOf(
SourceFile.kotlin(
"kSource.kt",
"""
import kotlin.js.JsExport
@JsExport
fun add(a: Double, b: Double): Double {
return a + b
}
""".trimIndent(),
isMultiplatformCommonSource = true
)
)
}.compile()

assertThat(ExitCode.OK).isEqualTo(result.exitCode)
}

@Test
fun `runs with only java sources`() {
val result = defaultCompilerConfig().apply {
Expand Down

0 comments on commit 82ec542

Please sign in to comment.