Skip to content

Add explicit module-info.java for JPMS compatibility #3297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != core
}

apply plugin: "bom-conventions"
apply plugin: "java-modularity-conventions"

if (build_snapshot_train) {
println "Hacking test tasks, removing stress and flaky tests"
Expand Down
116 changes: 116 additions & 0 deletions buildSrc/src/main/kotlin/Java9Modularity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

import org.gradle.api.*
import org.gradle.api.attributes.*
import org.gradle.api.tasks.bundling.*
import org.gradle.api.tasks.compile.*
import org.gradle.jvm.toolchain.*
import org.gradle.kotlin.dsl.*
import org.jetbrains.kotlin.gradle.dsl.*

/**
* This object configures the Java compilation of a JPMS (aka Jigsaw) module descriptor.
* The source file for the module descriptor is expected at <project-dir>/src/module-info.java.
*
* To maintain backwards compatibility with Java 8, the jvm JAR is marked as a multi-release JAR
* with the module-info.class being moved to META-INF/versions/9/module-info.class.
*
* The Java toolchains feature of Gradle is used to detect or provision a JDK 11,
* which is used to compile the module descriptor.
*/
object Java9Modularity {

@JvmStatic
fun configure(project: Project) = with(project) {
val javaToolchains = extensions.getByName("javaToolchains") as JavaToolchainService
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change it to

Suggested change
val javaToolchains = extensions.getByName("javaToolchains") as JavaToolchainService
val javaToolchains = extensions.findByType(JavaToolchainService::class.java)
?: error("Gradle JavaToolchainService is not available")

val target = when (val kotlin = extensions.getByName("kotlin")) {
is KotlinJvmProjectExtension -> kotlin.target
is KotlinMultiplatformExtension -> kotlin.targets.getByName("jvm")
else -> throw IllegalStateException("Unknown Kotlin project extension in $project")
}
val compilation = target.compilations.getByName("main")

// Force the use of JARs for compile dependencies, so any JPMS descriptors are picked up.
// For more details, see https://github.com/gradle/gradle/issues/890#issuecomment-623392772
configurations.getByName(compilation.compileDependencyConfigurationName).attributes {
attribute(
LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
objects.named(LibraryElements::class, LibraryElements.JAR)
)
}

val compileJavaModuleInfo = tasks.register("compileJavaModuleInfo", JavaCompile::class.java) {
val moduleName = project.name.replace('-', '.') // this module's name
val sourceFile = file("${target.name.ifEmpty { "." }}/src/module-info.java")
if (!sourceFile.exists()) {
throw IllegalStateException("$sourceFile not found in $project")
}
val compileKotlinTask = compilation.compileKotlinTask as org.jetbrains.kotlin.gradle.tasks.KotlinCompile
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compilation.compileKotlinTask is deprecated.
You could use

            val compileKotlinTask = compilation.compileTaskProvider.get()
                as? org.jetbrains.kotlin.gradle.tasks.KotlinCompile
                ?: error("Cannot access Kotlin compile task ${compilation.compileKotlinTaskName}")

val targetDir = compileKotlinTask.destinationDirectory.dir("../java9")

// Use a Java 11 compiler for the module-info.
javaCompiler.set(javaToolchains.compilerFor {
languageVersion.set(JavaLanguageVersion.of(11))
})

// Always compile kotlin classes before the module descriptor.
dependsOn(compileKotlinTask)

// Add the module-info source file.
// Note that we use the parent dir and an include filter,
// this is needed for Gradle's module detection to work in
// org.gradle.api.tasks.compile.JavaCompile.createSpec
source(sourceFile.parentFile)
include { it.file == sourceFile }

// The Kotlin compiler will parse and check module dependencies,
// but it currently won't compile to a module-info.class file.
// Note that module checking only works on JDK 9+,
// because the JDK built-in base modules are not available in earlier versions.
val javaVersion = compileKotlinTask.kotlinJavaToolchain.javaVersion.orNull
Copy link
Member

@ALikhachev ALikhachev Feb 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep it lazy as well.

You could do it by defining such class

private class ModuleInfoFilter(
    private val compileKotlinTaskPath: String,
    private val javaVersionProvider: Provider<JavaVersion>,
    private val moduleInfoFile: File,
    private val logger: Logger
) : Spec<FileTreeElement> {
    private val isJava9Compatible
        get() = javaVersionProvider.orNull?.isJava9Compatible == true
    private var logged = false

    private fun logStatusOnce() {
        if (logged) return
        if (isJava9Compatible) {
            logger.info("Module-info checking is enabled; $compileKotlinTaskPath is compiled using Java ${javaVersionProvider.get()}")
        } else {
            logger.info("Module-info checking is disabled")
        }
        logged = true
    }

    override fun isSatisfiedBy(element: FileTreeElement): Boolean {
        logStatusOnce()
        if (isJava9Compatible) return false
        return element.file == moduleInfoFile
    }
}

and then use it like

val javaVersionProvider = compileKotlinTask.kotlinJavaToolchain.javaVersion
compileKotlinTask.exclude(ModuleInfoFilter(compileKotlinTask.path, javaVersionProvider, sourceFile, logger))

Yes, it's more verbose, but performs the check lazily. Otherwise it depends on the order, so if the task configured before toolchains, then it may get an incorrect Java version.

if (javaVersion?.isJava9Compatible == true) {
logger.info("Module-info checking is enabled; $compileKotlinTask is compiled using Java $javaVersion")
} else {
logger.info("Module-info checking is disabled")
// Exclude the module-info.java source file from the Kotlin compile task,
// to prevent compile errors when resolving the module graph.
compileKotlinTask.exclude { it.file == sourceFile }
}

// Set the task outputs and destination directory
outputs.dir(targetDir)
destinationDirectory.set(targetDir)

// Configure JVM compatibility
sourceCompatibility = JavaVersion.VERSION_1_9.toString()
targetCompatibility = JavaVersion.VERSION_1_9.toString()

// Set the Java release version.
options.release.set(9)

// Ignore warnings about using 'requires transitive' on automatic modules.
// not needed when compiling with recent JDKs, e.g. 17
options.compilerArgs.add("-Xlint:-requires-transitive-automatic")

// Patch the compileKotlinJvm output classes into the compilation so exporting packages works correctly.
val kotlinCompileDestinationDir = compileKotlinTask.destinationDirectory.asFile.get()
options.compilerArgs.addAll(listOf("--patch-module", "$moduleName=$kotlinCompileDestinationDir"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please try to preserve laziness here, e.g. like

val destinationDirProperty = compileKotlinTask.destinationDirectory.asFile
options.compilerArgumentProviders.add({
    val kotlinCompileDestinationDir = destinationDirProperty.get()
    listOf("--patch-module", "$moduleName=$kotlinCompileDestinationDir")
})


// Use the classpath of the compileKotlinJvm task.
// Also ensure that the module path is used instead of classpath.
classpath = compileKotlinTask.libraries
modularity.inferModulePath.set(true)
}

tasks.getByName<Jar>(target.artifactsTaskName) {
Copy link
Member

@ALikhachev ALikhachev Feb 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change it to

Suggested change
tasks.getByName<Jar>(target.artifactsTaskName) {
tasks.named<Jar>(target.artifactsTaskName) {

You're triggering unconditional task configuration even on running ./gradlew help:
Screenshot 2023-02-16 at 14 25 16

manifest {
attributes("Multi-Release" to true)
}
from(compileJavaModuleInfo) {
into("META-INF/versions/9/")
}
}
}
}
17 changes: 17 additions & 0 deletions buildSrc/src/main/kotlin/java-modularity-conventions.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

// Currently the compilation of the module-info fails for
// kotlinx-coroutines-play-services because it depends on Android JAR's
// which do not have an explicit module-info descriptor.
// Because the JAR's are all named `classes.jar`,
// the automatic module name also becomes `classes`.
// This conflicts since there are multiple JAR's with identical names.
val invalidModules = listOf("kotlinx-coroutines-play-services")

configure(subprojects.filter {
!unpublished.contains(it.name) && !invalidModules.contains(it.name) && it.extensions.findByName("kotlin") != null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be more correct to run the configuration once some Kotlin plugin is applied, like

plugins.withType(org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin::class) {
    // perform some configuration
}

instead of checking for the extension presence right now. The order of plugin appliance should not matter.

}) {
Java9Modularity.configure(project)
}
2 changes: 2 additions & 0 deletions gradle/compile-jvm-multiplatform.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ kotlin {
sourceSets {
jvmMain.dependencies {
compileOnly "org.codehaus.mojo:animal-sniffer-annotations:1.20"
// Workaround until https://github.com/JetBrains/kotlin/pull/4999 is picked up
api "org.jetbrains:annotations:23.0.0"
}

jvmTest.dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ class PrecompiledDebugProbesTest {
@Test
fun testClassFileContent() {
val clz = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt")
val className: String = clz.getName()
val classFileResourcePath = className.replace(".", "/") + ".class"
val stream = clz.classLoader.getResourceAsStream(classFileResourcePath)!!
val array = stream.readBytes()
val classFileResourcePath = clz.name.replace(".", "/") + ".class"
val array = clz.classLoader.getResourceAsStream(classFileResourcePath).use { it.readBytes() }
assertJava6Compliance(array)
// we expect the integration testing project to be in a subdirectory of the main kotlinx.coroutines project
val base = File("").absoluteFile.parentFile
val probes = File(base, "kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin")
Expand All @@ -31,8 +30,20 @@ class PrecompiledDebugProbesTest {
assertTrue(
array.contentEquals(binContent),
"Compiled DebugProbesKt.class does not match the file shipped as a resource in kotlinx-coroutines-core. " +
"Typically it happens because of the Kotlin version update (-> binary metadata). In that case, run the same test with -Poverwrite.probes=true."
"Typically it happens because of the Kotlin version update (-> binary metadata). " +
"In that case, run the same test with -Poverwrite.probes=true."
)
}
}

private fun assertJava6Compliance(classBytes: ByteArray) {
DataInputStream(classBytes.inputStream()).use {
val magic: Int = it.readInt()
if (magic != -0x35014542) throw IllegalArgumentException("Not a valid class!")
val minor: Int = it.readUnsignedShort()
val major: Int = it.readUnsignedShort()
assertEquals(50, major)
assertEquals(0, minor)
}
}
}
7 changes: 7 additions & 0 deletions integration/kotlinx-coroutines-guava/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module kotlinx.coroutines.guava {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires com.google.common;

exports kotlinx.coroutines.guava;
}
3 changes: 3 additions & 0 deletions integration/kotlinx-coroutines-jdk8/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@SuppressWarnings("JavaModuleNaming")
module kotlinx.coroutines.jdk8 {
}
7 changes: 7 additions & 0 deletions integration/kotlinx-coroutines-slf4j/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module kotlinx.coroutines.slf4j {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires org.slf4j;

exports kotlinx.coroutines.slf4j;
}
29 changes: 29 additions & 0 deletions kotlinx-coroutines-core/jvm/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import kotlinx.coroutines.CoroutineExceptionHandler;
import kotlinx.coroutines.internal.MainDispatcherFactory;

module kotlinx.coroutines.core {
requires transitive kotlin.stdlib;
requires kotlinx.atomicfu;

// these are used by kotlinx.coroutines.debug.AgentPremain
requires static java.instrument; // contains java.lang.instrument.*
requires static jdk.unsupported; // contains sun.misc.Signal

exports kotlinx.coroutines;
exports kotlinx.coroutines.channels;
exports kotlinx.coroutines.debug;
exports kotlinx.coroutines.debug.internal;
exports kotlinx.coroutines.flow;
exports kotlinx.coroutines.flow.internal;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please elaborate on how exactly you got these packages?

It seems like some of them should not be exported in the first place -- all internal (though I have to double check some of them), scheduling and intrinsics

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These packages are mostly used in reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt. So I need to export them from core so I don't get a compile error in kotlinx-coroutines-reactive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered exporting the internal packages just to the kotlinx.coroutines modules that depend on them? For example:

Suggested change
exports kotlinx.coroutines.flow.internal;
exports kotlinx.coroutines.flow.internal to kotlinx.coroutines.reactive;

exports kotlinx.coroutines.future;
exports kotlinx.coroutines.internal;
exports kotlinx.coroutines.intrinsics;
exports kotlinx.coroutines.scheduling;
exports kotlinx.coroutines.selects;
exports kotlinx.coroutines.stream;
exports kotlinx.coroutines.sync;
exports kotlinx.coroutines.time;

uses CoroutineExceptionHandler;
uses MainDispatcherFactory;
}
14 changes: 14 additions & 0 deletions kotlinx-coroutines-debug/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module kotlinx.coroutines.debug {
requires java.management;
requires java.instrument;
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires net.bytebuddy;
requires net.bytebuddy.agent;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, probably not. I will check this out further.

requires org.junit.jupiter.api;
requires org.junit.platform.commons;

// exports kotlinx.coroutines.debug; // already exported by kotlinx.coroutines.core
exports kotlinx.coroutines.debug.junit4;
exports kotlinx.coroutines.debug.junit5;
}
11 changes: 11 additions & 0 deletions kotlinx-coroutines-test/jvm/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import kotlinx.coroutines.internal.MainDispatcherFactory;
import kotlinx.coroutines.test.internal.TestMainDispatcherFactory;

module kotlinx.coroutines.test {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;

exports kotlinx.coroutines.test;

provides MainDispatcherFactory with TestMainDispatcherFactory;
}
9 changes: 9 additions & 0 deletions reactive/kotlinx-coroutines-jdk9/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@SuppressWarnings("JavaModuleNaming")
module kotlinx.coroutines.jdk9 {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires kotlinx.coroutines.reactive;
requires org.reactivestreams;

exports kotlinx.coroutines.jdk9;
}
10 changes: 10 additions & 0 deletions reactive/kotlinx-coroutines-reactive/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module kotlinx.coroutines.reactive {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires kotlinx.atomicfu;
requires org.reactivestreams;

exports kotlinx.coroutines.reactive;

uses kotlinx.coroutines.reactive.ContextInjector;
}
14 changes: 14 additions & 0 deletions reactive/kotlinx-coroutines-reactor/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import kotlinx.coroutines.reactive.ContextInjector;
import kotlinx.coroutines.reactor.ReactorContextInjector;

module kotlinx.coroutines.reactor {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires kotlinx.coroutines.reactive;
requires org.reactivestreams;
requires reactor.core;

exports kotlinx.coroutines.reactor;

provides ContextInjector with ReactorContextInjector;
}
10 changes: 10 additions & 0 deletions reactive/kotlinx-coroutines-rx2/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@SuppressWarnings("JavaModuleNaming")
module kotlinx.coroutines.rx2 {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires kotlinx.coroutines.reactive;
requires kotlinx.atomicfu;
requires io.reactivex.rxjava2;

exports kotlinx.coroutines.rx2;
}
10 changes: 10 additions & 0 deletions reactive/kotlinx-coroutines-rx3/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@SuppressWarnings("JavaModuleNaming")
module kotlinx.coroutines.rx3 {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires kotlinx.coroutines.reactive;
requires kotlinx.atomicfu;
requires io.reactivex.rxjava3;

exports kotlinx.coroutines.rx3;
}
11 changes: 11 additions & 0 deletions ui/kotlinx-coroutines-android/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import kotlinx.coroutines.android.AndroidDispatcherFactory;
import kotlinx.coroutines.internal.MainDispatcherFactory;

module kotlinx.coroutines.android {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;

exports kotlinx.coroutines.android;

provides MainDispatcherFactory with AndroidDispatcherFactory;
}
9 changes: 8 additions & 1 deletion ui/kotlinx-coroutines-javafx/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

buildscript {
dependencies {
// this line can be removed when https://github.com/openjfx/javafx-gradle-plugin/pull/135 is released
classpath("org.javamodularity:moduleplugin:1.8.12")
}
}

plugins {
id("org.openjfx.javafxplugin") version "0.0.9"
id("org.openjfx.javafxplugin") version "0.0.13"
}

configurations {
Expand Down
13 changes: 13 additions & 0 deletions ui/kotlinx-coroutines-javafx/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import kotlinx.coroutines.internal.MainDispatcherFactory;
import kotlinx.coroutines.javafx.JavaFxDispatcherFactory;

module kotlinx.coroutines.javafx {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires javafx.base;
requires javafx.graphics;

exports kotlinx.coroutines.javafx;

provides MainDispatcherFactory with JavaFxDispatcherFactory;
}
12 changes: 12 additions & 0 deletions ui/kotlinx-coroutines-swing/src/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import kotlinx.coroutines.internal.MainDispatcherFactory;
import kotlinx.coroutines.swing.SwingDispatcherFactory;

module kotlinx.coroutines.swing {
requires kotlin.stdlib;
requires kotlinx.coroutines.core;
requires java.desktop;

exports kotlinx.coroutines.swing;

provides MainDispatcherFactory with SwingDispatcherFactory;
}