Skip to content

Commit 8b231ff

Browse files
authored
Properly fix Java 8 API compatibility (#2350)
There are 7 methods in ByteBuffer that have the problem described in #2218 and was attempted to be fixed in #2219. Unfortunately, the fix in #2219 was incomplete as another of these 7 methods is used in the same class. Also it could happen anytime and with similar cases, that this happens again and is only recognized very late.
1 parent 271034b commit 8b231ff

File tree

4 files changed

+149
-56
lines changed

4 files changed

+149
-56
lines changed

buildSrc/src/main/kotlin/Java9Modularity.kt

Lines changed: 131 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@
33
*/
44

55
import org.gradle.api.*
6+
import org.gradle.api.file.*
7+
import org.gradle.api.provider.*
8+
import org.gradle.api.tasks.*
69
import org.gradle.api.tasks.bundling.*
710
import org.gradle.api.tasks.compile.*
11+
import org.gradle.jvm.toolchain.*
812
import org.gradle.kotlin.dsl.*
13+
import org.gradle.language.base.plugins.LifecycleBasePlugin.*
14+
import org.gradle.process.*
915
import org.jetbrains.kotlin.gradle.dsl.*
16+
import org.jetbrains.kotlin.gradle.plugin.*
1017
import org.jetbrains.kotlin.gradle.plugin.mpp.*
11-
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.*
1218
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.*
1319
import org.jetbrains.kotlin.gradle.targets.jvm.*
1420
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
21+
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
1522
import java.io.*
1623

1724
object Java9Modularity {
@@ -42,70 +49,154 @@ object Java9Modularity {
4249

4350
// derive the names of the source set and compile module task
4451
val sourceSetName = defaultSourceSet.name + "Module"
45-
val compileModuleTaskName = compileKotlinTask.name + "Module"
4652

4753
kotlin.sourceSets.create(sourceSetName) {
4854
val sourceFile = this.kotlin.find { it.name == "module-info.java" }
49-
val targetFile = compileKotlinTask.destinationDirectory.file("../module-info.class").get().asFile
55+
val targetDirectory = compileKotlinTask.destinationDirectory.map {
56+
it.dir("../${it.asFile.name}Module")
57+
}
5058

5159
// only configure the compilation if necessary
5260
if (sourceFile != null) {
53-
// the default source set depends on this new source set
54-
defaultSourceSet.dependsOn(this)
61+
// register and wire a task to verify module-info.java content
62+
//
63+
// this will compile the whole sources again with a JPMS-aware target Java version,
64+
// so that the Kotlin compiler can do the necessary verifications
65+
// while compiling with `jdk-release=1.8` those verifications are not done
66+
//
67+
// this task is only going to be executed when running with `check` or explicitly,
68+
// not during normal build operations
69+
val verifyModuleTask = registerVerifyModuleTask(
70+
compileKotlinTask,
71+
sourceFile
72+
)
73+
tasks.named("check") {
74+
dependsOn(verifyModuleTask)
75+
}
5576

5677
// register a new compile module task
57-
val compileModuleTask = registerCompileModuleTask(compileModuleTaskName, compileKotlinTask, sourceFile, targetFile)
78+
val compileModuleTask = registerCompileModuleTask(
79+
compileKotlinTask,
80+
sourceFile,
81+
targetDirectory
82+
)
5883

5984
// add the resulting module descriptor to this target's artifact
60-
artifactTask.dependsOn(compileModuleTask)
61-
artifactTask.from(targetFile) {
85+
artifactTask.from(compileModuleTask) {
6286
if (multiRelease) {
6387
into("META-INF/versions/9/")
6488
}
6589
}
6690
} else {
6791
logger.info("No module-info.java file found in ${this.kotlin.srcDirs}, can't configure compilation of module-info!")
68-
// remove the source set to prevent Gradle warnings
69-
kotlin.sourceSets.remove(this)
7092
}
93+
94+
// remove the source set to prevent Gradle warnings
95+
kotlin.sourceSets.remove(this)
7196
}
7297
}
7398
}
7499
}
75100

76-
private fun Project.registerCompileModuleTask(taskName: String, compileTask: KotlinCompile, sourceFile: File, targetFile: File) =
77-
tasks.register(taskName, JavaCompile::class) {
78-
// Also add the module-info.java source file to the Kotlin compile task;
79-
// the Kotlin compiler will parse and check module dependencies,
80-
// but it currently won't compile to a module-info.class file.
81-
compileTask.source(sourceFile)
101+
/**
102+
* Add a Kotlin compile task that compiles `module-info.java` source file and Kotlin sources together,
103+
* the Kotlin compiler will parse and check module dependencies,
104+
* but it currently won't compile to a module-info.class file.
105+
*/
106+
private fun Project.registerVerifyModuleTask(
107+
compileTask: KotlinCompile,
108+
sourceFile: File
109+
): TaskProvider<out KotlinJvmCompile> {
110+
apply<KotlinBaseApiPlugin>()
111+
val verifyModuleTaskName = "verify${compileTask.name.removePrefix("compile").capitalize()}Module"
112+
// work-around for https://youtrack.jetbrains.com/issue/KT-60542
113+
val verifyModuleTask = plugins
114+
.findPlugin(KotlinBaseApiPlugin::class)!!
115+
.registerKotlinJvmCompileTask(verifyModuleTaskName)
116+
verifyModuleTask {
117+
group = VERIFICATION_GROUP
118+
description = "Verify Kotlin sources for JPMS problems"
119+
libraries.from(compileTask.libraries)
120+
source(compileTask.sources)
121+
source(compileTask.javaSources)
122+
// part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
123+
@Suppress("INVISIBLE_MEMBER")
124+
source(compileTask.scriptSources)
125+
source(sourceFile)
126+
destinationDirectory.set(temporaryDir)
127+
multiPlatformEnabled.set(compileTask.multiPlatformEnabled)
128+
kotlinOptions {
129+
moduleName = compileTask.kotlinOptions.moduleName
130+
jvmTarget = "9"
131+
freeCompilerArgs += "-Xjdk-release=9"
132+
}
133+
// work-around for https://youtrack.jetbrains.com/issue/KT-60583
134+
inputs.files(
135+
libraries.asFileTree.elements.map { libs ->
136+
libs
137+
.filter { it.asFile.exists() }
138+
.map {
139+
zipTree(it.asFile).filter { it.name == "module-info.class" }
140+
}
141+
}
142+
).withPropertyName("moduleInfosOfLibraries")
143+
this as KotlinCompile
144+
// part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
145+
@Suppress("DEPRECATION")
146+
ownModuleName.set(compileTask.kotlinOptions.moduleName)
147+
// part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
148+
@Suppress("INVISIBLE_MEMBER")
149+
commonSourceSet.from(compileTask.commonSourceSet)
150+
// part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
151+
// and work-around for https://youtrack.jetbrains.com/issue/KT-60582
152+
incremental = false
153+
}
154+
return verifyModuleTask
155+
}
156+
157+
private fun Project.registerCompileModuleTask(
158+
compileTask: KotlinCompile,
159+
sourceFile: File,
160+
targetDirectory: Provider<out Directory>
161+
) = tasks.register("${compileTask.name}Module", JavaCompile::class) {
162+
// Configure the module compile task.
163+
source(sourceFile)
164+
classpath = files()
165+
destinationDirectory.set(targetDirectory)
166+
// use a Java 11 toolchain with release 9 option
167+
// because for some OS / architecture combinations
168+
// there are no Java 9 builds available
169+
javaCompiler.set(
170+
this@registerCompileModuleTask.the<JavaToolchainService>().compilerFor {
171+
languageVersion.set(JavaLanguageVersion.of(11))
172+
}
173+
)
174+
options.release.set(9)
82175

176+
options.compilerArgumentProviders.add(object : CommandLineArgumentProvider {
177+
@get:CompileClasspath
178+
val compileClasspath = compileTask.libraries
83179

84-
// Configure the module compile task.
85-
dependsOn(compileTask)
86-
source(sourceFile)
87-
outputs.file(targetFile)
88-
classpath = files()
89-
destinationDirectory.set(compileTask.destinationDirectory)
90-
sourceCompatibility = JavaVersion.VERSION_1_9.toString()
91-
targetCompatibility = JavaVersion.VERSION_1_9.toString()
180+
@get:CompileClasspath
181+
val compiledClasses = compileTask.destinationDirectory
92182

93-
doFirst {
183+
@get:Input
184+
val moduleName = sourceFile
185+
.readLines()
186+
.single { it.contains("module ") }
187+
.substringAfter("module ")
188+
.substringBefore(' ')
189+
.trim()
190+
191+
override fun asArguments() = mutableListOf(
94192
// Provide the module path to the compiler instead of using a classpath.
95193
// The module path should be the same as the classpath of the compiler.
96-
options.compilerArgs = listOf(
97-
"--release", "9",
98-
"--module-path", compileTask.libraries.asPath,
99-
"-Xlint:-requires-transitive-automatic"
100-
)
101-
}
102-
103-
doLast {
104-
// Move the compiled file out of the Kotlin compile task's destination dir,
105-
// so it won't disturb Gradle's caching mechanisms.
106-
val compiledFile = destinationDirectory.file(targetFile.name).get().asFile
107-
targetFile.parentFile.mkdirs()
108-
compiledFile.renameTo(targetFile)
109-
}
110-
}
194+
"--module-path",
195+
compileClasspath.asPath,
196+
"--patch-module",
197+
"$moduleName=${compiledClasses.get()}",
198+
"-Xlint:-requires-transitive-automatic"
199+
)
200+
})
201+
}
111202
}

formats/json/jvmMain/src/kotlinx/serialization/json/internal/CharsetReader.kt

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,7 @@ internal class CharsetReader(
2020
.onMalformedInput(CodingErrorAction.REPLACE)
2121
.onUnmappableCharacter(CodingErrorAction.REPLACE)
2222
byteBuffer = ByteBuffer.wrap(ByteArrayPool8k.take())
23-
// An explicit cast is needed here due to an API change in Java 9, see #2218.
24-
//
25-
// In Java 8 and earlier, the `flip` method was final in `Buffer`, and returned a `Buffer`.
26-
// In Java 9 and later, the method was opened, and `ByteFuffer` overrides it, returning a `ByteBuffer`.
27-
//
28-
// You could observe this by decompiling this call with `javap`:
29-
// Compiled with Java 8 it produces `INVOKEVIRTUAL java/nio/ByteBuffer.flip ()Ljava/nio/Buffer;`
30-
// Compiled with Java 9+ it produces `INVOKEVIRTUAL java/nio/ByteBuffer.flip ()Ljava/nio/ByteBuffer;`
31-
//
32-
// This causes a `NoSuchMethodError` when running a class, compiled with a newer Java version, on Java 8.
33-
//
34-
// To mitigate that, `--bootclasspath` / `--release` options were introduced in `javac`, but there are no
35-
// counterparts for these options in `kotlinc`, so an explicit cast is required.
36-
(byteBuffer as Buffer).flip() // Make empty
23+
byteBuffer.flip() // Make empty
3724
}
3825

3926
@Suppress("NAME_SHADOWING")
@@ -106,7 +93,7 @@ internal class CharsetReader(
10693
// Method `position(I)LByteBuffer` does not exist in Java 8. For details, see comment for `flip` in `init` method
10794
(byteBuffer as Buffer).position(position + bytesRead)
10895
} finally {
109-
(byteBuffer as Buffer).flip() // see the `init` block in this class for the reasoning behind the cast
96+
byteBuffer.flip()
11097
}
11198
return byteBuffer.remaining()
11299
}

gradle/configure-source-sets.gradle

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,23 @@
22
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5+
java {
6+
toolchain {
7+
languageVersion.set(JavaLanguageVersion.of(11))
8+
}
9+
}
10+
11+
tasks.withType(JavaCompile).configureEach {
12+
options.release = 8
13+
}
14+
515
kotlin {
616
jvm {
717
withJava()
8-
configure([compilations.main, compilations.test]) {
18+
compilations.configureEach {
919
kotlinOptions {
1020
jvmTarget = '1.8'
21+
freeCompilerArgs += '-Xjdk-release=1.8'
1122
}
1223
}
1324
}

settings.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5+
plugins {
6+
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0'
7+
}
8+
59
rootProject.name = 'kotlinx-serialization'
610

711
include ':kotlinx-serialization-core'

0 commit comments

Comments
 (0)