Skip to content

Commit 7f4b80c

Browse files
qwwdfsadlion7
andauthored
Add explicit module-info.java for JPMS compatibility (#3629)
* Compiles hand-written module-info.java into Multi-Release JAR for all modules Contributed by @lion7 Fixes #2237 Co-authored-by: Gerard de Leeuw <[email protected]>
1 parent dc19e1f commit 7f4b80c

File tree

20 files changed

+369
-6
lines changed

20 files changed

+369
-6
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != core
181181
}
182182

183183
apply plugin: "bom-conventions"
184+
apply plugin: "java-modularity-conventions"
184185

185186
if (build_snapshot_train) {
186187
println "Hacking test tasks, removing stress and flaky tests"
+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
import org.gradle.api.*
6+
import org.gradle.api.attributes.*
7+
import org.gradle.api.file.*
8+
import org.gradle.api.provider.*
9+
import org.gradle.api.specs.*
10+
import org.gradle.api.tasks.bundling.*
11+
import org.gradle.api.tasks.compile.*
12+
import org.gradle.jvm.toolchain.*
13+
import org.gradle.kotlin.dsl.*
14+
import org.gradle.api.logging.Logger
15+
import org.jetbrains.kotlin.gradle.dsl.*
16+
import java.io.*
17+
18+
/**
19+
* This object configures the Java compilation of a JPMS (aka Jigsaw) module descriptor.
20+
* The source file for the module descriptor is expected at <project-dir>/src/module-info.java.
21+
*
22+
* To maintain backwards compatibility with Java 8, the jvm JAR is marked as a multi-release JAR
23+
* with the module-info.class being moved to META-INF/versions/9/module-info.class.
24+
*
25+
* The Java toolchains feature of Gradle is used to detect or provision a JDK 11,
26+
* which is used to compile the module descriptor.
27+
*/
28+
object Java9Modularity {
29+
30+
private class ModuleInfoFilter(
31+
private val compileKotlinTaskPath: String,
32+
private val javaVersionProvider: Provider<JavaVersion>,
33+
private val moduleInfoFile: File,
34+
private val logger: Logger
35+
) : Spec<FileTreeElement> {
36+
private val isJava9Compatible
37+
get() = javaVersionProvider.orNull?.isJava9Compatible == true
38+
private var logged = false
39+
40+
private fun logStatusOnce() {
41+
if (logged) return
42+
if (isJava9Compatible) {
43+
logger.info("Module-info checking is enabled; $compileKotlinTaskPath is compiled using Java ${javaVersionProvider.get()}")
44+
} else {
45+
logger.info("Module-info checking is disabled")
46+
}
47+
logged = true
48+
}
49+
50+
override fun isSatisfiedBy(element: FileTreeElement): Boolean {
51+
logStatusOnce()
52+
if (isJava9Compatible) return false
53+
return element.file == moduleInfoFile
54+
}
55+
}
56+
57+
@JvmStatic
58+
fun configure(project: Project) = with(project) {
59+
val javaToolchains = extensions.findByType(JavaToolchainService::class.java)
60+
?: error("Gradle JavaToolchainService is not available")
61+
val target = when (val kotlin = extensions.getByName("kotlin")) {
62+
is KotlinJvmProjectExtension -> kotlin.target
63+
is KotlinMultiplatformExtension -> kotlin.targets.getByName("jvm")
64+
else -> throw IllegalStateException("Unknown Kotlin project extension in $project")
65+
}
66+
val compilation = target.compilations.getByName("main")
67+
68+
// Force the use of JARs for compile dependencies, so any JPMS descriptors are picked up.
69+
// For more details, see https://github.com/gradle/gradle/issues/890#issuecomment-623392772
70+
configurations.getByName(compilation.compileDependencyConfigurationName).attributes {
71+
attribute(
72+
LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
73+
objects.named(LibraryElements::class, LibraryElements.JAR)
74+
)
75+
}
76+
77+
val compileJavaModuleInfo = tasks.register("compileModuleInfoJava", JavaCompile::class.java) {
78+
val moduleName = project.name.replace('-', '.') // this module's name
79+
val sourceFile = file("${target.name.ifEmpty { "." }}/src/module-info.java")
80+
if (!sourceFile.exists()) {
81+
throw IllegalStateException("$sourceFile not found in $project")
82+
}
83+
val compileKotlinTask =
84+
compilation.compileTaskProvider.get() as? org.jetbrains.kotlin.gradle.tasks.KotlinCompile
85+
?: error("Cannot access Kotlin compile task ${compilation.compileKotlinTaskName}")
86+
val targetDir = compileKotlinTask.destinationDirectory.dir("../java9")
87+
88+
// Use a Java 11 compiler for the module-info.
89+
javaCompiler.set(javaToolchains.compilerFor {
90+
languageVersion.set(JavaLanguageVersion.of(11))
91+
})
92+
93+
// Always compile kotlin classes before the module descriptor.
94+
dependsOn(compileKotlinTask)
95+
96+
// Add the module-info source file.
97+
// Note that we use the parent dir and an include filter,
98+
// this is needed for Gradle's module detection to work in
99+
// org.gradle.api.tasks.compile.JavaCompile.createSpec
100+
source(sourceFile.parentFile)
101+
include { it.file == sourceFile }
102+
103+
// The Kotlin compiler will parse and check module dependencies,
104+
// but it currently won't compile to a module-info.class file.
105+
// Note that module checking only works on JDK 9+,
106+
// because the JDK built-in base modules are not available in earlier versions.
107+
val javaVersionProvider = compileKotlinTask.kotlinJavaToolchain.javaVersion
108+
compileKotlinTask.exclude(ModuleInfoFilter(compileKotlinTask.path, javaVersionProvider, sourceFile, logger))
109+
110+
// Set the task outputs and destination directory
111+
outputs.dir(targetDir)
112+
destinationDirectory.set(targetDir)
113+
114+
// Configure JVM compatibility
115+
sourceCompatibility = JavaVersion.VERSION_1_9.toString()
116+
targetCompatibility = JavaVersion.VERSION_1_9.toString()
117+
118+
// Set the Java release version.
119+
options.release.set(9)
120+
121+
// Ignore warnings about using 'requires transitive' on automatic modules.
122+
// not needed when compiling with recent JDKs, e.g. 17
123+
options.compilerArgs.add("-Xlint:-requires-transitive-automatic")
124+
125+
// Patch the compileKotlinJvm output classes into the compilation so exporting packages works correctly.
126+
val destinationDirProperty = compileKotlinTask.destinationDirectory.asFile
127+
options.compilerArgumentProviders.add {
128+
val kotlinCompileDestinationDir = destinationDirProperty.get()
129+
listOf("--patch-module", "$moduleName=$kotlinCompileDestinationDir")
130+
}
131+
132+
// Use the classpath of the compileKotlinJvm task.
133+
// Also ensure that the module path is used instead of classpath.
134+
classpath = compileKotlinTask.libraries
135+
modularity.inferModulePath.set(true)
136+
}
137+
138+
tasks.named<Jar>(target.artifactsTaskName) {
139+
manifest {
140+
attributes("Multi-Release" to true)
141+
}
142+
from(compileJavaModuleInfo) {
143+
into("META-INF/versions/9/")
144+
}
145+
}
146+
}
147+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
// Currently the compilation of the module-info fails for
6+
// kotlinx-coroutines-play-services because it depends on Android JAR's
7+
// which do not have an explicit module-info descriptor.
8+
// Because the JAR's are all named `classes.jar`,
9+
// the automatic module name also becomes `classes`.
10+
// This conflicts since there are multiple JAR's with identical names.
11+
val invalidModules = listOf("kotlinx-coroutines-play-services")
12+
13+
configure(subprojects.filter {
14+
!unpublished.contains(it.name) && !invalidModules.contains(it.name) && it.extensions.findByName("kotlin") != null
15+
}) {
16+
Java9Modularity.configure(project)
17+
}

gradle/compile-jvm-multiplatform.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ kotlin {
1010
sourceSets {
1111
jvmMain.dependencies {
1212
compileOnly "org.codehaus.mojo:animal-sniffer-annotations:1.20"
13+
// Workaround until https://github.com/JetBrains/kotlin/pull/4999 is picked up
14+
api "org.jetbrains:annotations:23.0.0"
1315
}
1416

1517
jvmTest.dependencies {

integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt

+16-5
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ class PrecompiledDebugProbesTest {
1616
@Test
1717
fun testClassFileContent() {
1818
val clz = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt")
19-
val className: String = clz.getName()
20-
val classFileResourcePath = className.replace(".", "/") + ".class"
21-
val stream = clz.classLoader.getResourceAsStream(classFileResourcePath)!!
22-
val array = stream.readBytes()
19+
val classFileResourcePath = clz.name.replace(".", "/") + ".class"
20+
val array = clz.classLoader.getResourceAsStream(classFileResourcePath).use { it.readBytes() }
21+
assertJava8Compliance(array)
2322
// we expect the integration testing project to be in a subdirectory of the main kotlinx.coroutines project
2423
val base = File("").absoluteFile.parentFile
2524
val probes = File(base, "kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin")
@@ -31,8 +30,20 @@ class PrecompiledDebugProbesTest {
3130
assertTrue(
3231
array.contentEquals(binContent),
3332
"Compiled DebugProbesKt.class does not match the file shipped as a resource in kotlinx-coroutines-core. " +
34-
"Typically it happens because of the Kotlin version update (-> binary metadata). In that case, run the same test with -Poverwrite.probes=true."
33+
"Typically it happens because of the Kotlin version update (-> binary metadata). " +
34+
"In that case, run the same test with -Poverwrite.probes=true."
3535
)
3636
}
3737
}
38+
39+
private fun assertJava8Compliance(classBytes: ByteArray) {
40+
DataInputStream(classBytes.inputStream()).use {
41+
val magic: Int = it.readInt()
42+
if (magic != -0x35014542) throw IllegalArgumentException("Not a valid class!")
43+
val minor: Int = it.readUnsignedShort()
44+
val major: Int = it.readUnsignedShort()
45+
assertEquals(52, major)
46+
assertEquals(0, minor)
47+
}
48+
}
3849
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module kotlinx.coroutines.guava {
2+
requires kotlin.stdlib;
3+
requires kotlinx.coroutines.core;
4+
requires com.google.common;
5+
6+
exports kotlinx.coroutines.guava;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@SuppressWarnings("JavaModuleNaming")
2+
module kotlinx.coroutines.jdk8 {
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module kotlinx.coroutines.slf4j {
2+
requires kotlin.stdlib;
3+
requires kotlinx.coroutines.core;
4+
requires org.slf4j;
5+
6+
exports kotlinx.coroutines.slf4j;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import kotlinx.coroutines.CoroutineExceptionHandler;
2+
import kotlinx.coroutines.internal.MainDispatcherFactory;
3+
4+
module kotlinx.coroutines.core {
5+
requires transitive kotlin.stdlib;
6+
requires kotlinx.atomicfu;
7+
8+
// these are used by kotlinx.coroutines.debug.AgentPremain
9+
requires static java.instrument; // contains java.lang.instrument.*
10+
requires static jdk.unsupported; // contains sun.misc.Signal
11+
12+
exports kotlinx.coroutines;
13+
exports kotlinx.coroutines.channels;
14+
exports kotlinx.coroutines.debug;
15+
exports kotlinx.coroutines.debug.internal;
16+
exports kotlinx.coroutines.flow;
17+
exports kotlinx.coroutines.flow.internal;
18+
exports kotlinx.coroutines.future;
19+
exports kotlinx.coroutines.internal;
20+
exports kotlinx.coroutines.intrinsics;
21+
exports kotlinx.coroutines.scheduling;
22+
exports kotlinx.coroutines.selects;
23+
exports kotlinx.coroutines.stream;
24+
exports kotlinx.coroutines.sync;
25+
exports kotlinx.coroutines.time;
26+
27+
uses CoroutineExceptionHandler;
28+
uses MainDispatcherFactory;
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module kotlinx.coroutines.debug {
2+
requires java.management;
3+
requires java.instrument;
4+
requires kotlin.stdlib;
5+
requires kotlinx.coroutines.core;
6+
requires net.bytebuddy;
7+
requires net.bytebuddy.agent;
8+
requires org.junit.jupiter.api;
9+
requires org.junit.platform.commons;
10+
11+
// exports kotlinx.coroutines.debug; // already exported by kotlinx.coroutines.core
12+
exports kotlinx.coroutines.debug.junit4;
13+
exports kotlinx.coroutines.debug.junit5;
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import kotlinx.coroutines.internal.MainDispatcherFactory;
2+
import kotlinx.coroutines.test.internal.TestMainDispatcherFactory;
3+
4+
module kotlinx.coroutines.test {
5+
requires kotlin.stdlib;
6+
requires kotlinx.coroutines.core;
7+
8+
exports kotlinx.coroutines.test;
9+
10+
provides MainDispatcherFactory with TestMainDispatcherFactory;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@SuppressWarnings("JavaModuleNaming")
2+
module kotlinx.coroutines.jdk9 {
3+
requires kotlin.stdlib;
4+
requires kotlinx.coroutines.core;
5+
requires kotlinx.coroutines.reactive;
6+
requires org.reactivestreams;
7+
8+
exports kotlinx.coroutines.jdk9;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module kotlinx.coroutines.reactive {
2+
requires kotlin.stdlib;
3+
requires kotlinx.coroutines.core;
4+
requires kotlinx.atomicfu;
5+
requires org.reactivestreams;
6+
7+
exports kotlinx.coroutines.reactive;
8+
9+
uses kotlinx.coroutines.reactive.ContextInjector;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import kotlinx.coroutines.reactive.ContextInjector;
2+
import kotlinx.coroutines.reactor.ReactorContextInjector;
3+
4+
module kotlinx.coroutines.reactor {
5+
requires kotlin.stdlib;
6+
requires kotlinx.coroutines.core;
7+
requires kotlinx.coroutines.reactive;
8+
requires org.reactivestreams;
9+
requires reactor.core;
10+
11+
exports kotlinx.coroutines.reactor;
12+
13+
provides ContextInjector with ReactorContextInjector;
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@SuppressWarnings("JavaModuleNaming")
2+
module kotlinx.coroutines.rx2 {
3+
requires kotlin.stdlib;
4+
requires kotlinx.coroutines.core;
5+
requires kotlinx.coroutines.reactive;
6+
requires kotlinx.atomicfu;
7+
requires io.reactivex.rxjava2;
8+
9+
exports kotlinx.coroutines.rx2;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@SuppressWarnings("JavaModuleNaming")
2+
module kotlinx.coroutines.rx3 {
3+
requires kotlin.stdlib;
4+
requires kotlinx.coroutines.core;
5+
requires kotlinx.coroutines.reactive;
6+
requires kotlinx.atomicfu;
7+
requires io.reactivex.rxjava3;
8+
9+
exports kotlinx.coroutines.rx3;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import kotlinx.coroutines.android.AndroidDispatcherFactory;
2+
import kotlinx.coroutines.internal.MainDispatcherFactory;
3+
4+
module kotlinx.coroutines.android {
5+
requires kotlin.stdlib;
6+
requires kotlinx.coroutines.core;
7+
8+
exports kotlinx.coroutines.android;
9+
10+
provides MainDispatcherFactory with AndroidDispatcherFactory;
11+
}

0 commit comments

Comments
 (0)