diff --git a/src/dorkbox/gradle/StaticMethodsAndTools.kt b/src/dorkbox/gradle/StaticMethodsAndTools.kt index 7a99fa4..55cf893 100644 --- a/src/dorkbox/gradle/StaticMethodsAndTools.kt +++ b/src/dorkbox/gradle/StaticMethodsAndTools.kt @@ -16,15 +16,11 @@ package dorkbox.gradle import dorkbox.gradle.deps.DependencyScanner -import dorkbox.gradle.jpms.JavaXConfiguration -import dorkbox.gradle.jpms.SourceSetContainer2 +import dorkbox.gradle.jpms.JpmsMultiRelease +import dorkbox.gradle.jpms.JpmsOnly +import dorkbox.gradle.jpms.JpmsSourceSetContainer import dorkbox.os.OS -import org.gradle.api.Action -import org.gradle.api.GradleException -import org.gradle.api.JavaVersion -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.Task +import org.gradle.api.* import org.gradle.api.file.DuplicatesStrategy import org.gradle.api.specs.Specs import org.gradle.api.tasks.SourceSetContainer @@ -115,7 +111,6 @@ open class StaticMethodsAndTools(private val project: Project) { * Shows info if kotlin is enabled, shows exact information as to what the source-set directories are for java and kotlin */ fun debug() { - println("\tEnabling debug") debug = true project.afterEvaluate { @@ -687,6 +682,9 @@ open class StaticMethodsAndTools(private val project: Project) { kotlinActions(task.kotlinOptions) } }) + + // now we auto-check if it's necessary to enable JPMS support for PRIMARY locations (ie, not the src9 location) + JpmsOnly.runIfNecessary(javaVersion, project, this) } catch (ignored: Exception) { } } @@ -762,16 +760,16 @@ open class StaticMethodsAndTools(private val project: Project) { /** * Load JPMS for a specific java version using the default configuration */ - fun jpms(javaVersion: JavaVersion): JavaXConfiguration { - return JavaXConfiguration(javaVersion, project, this) + fun jpms(javaVersion: JavaVersion): JpmsMultiRelease { + return JpmsMultiRelease(javaVersion, project, this) } /** * Load and configure JPMS for a specific java version */ - fun jpms(javaVersion: JavaVersion, block: SourceSetContainer2.() -> Unit): JavaXConfiguration { - val javaX = JavaXConfiguration(javaVersion, project, this) - block(SourceSetContainer2(javaX)) + fun jpms(javaVersion: JavaVersion, block: JpmsSourceSetContainer.() -> Unit): JpmsMultiRelease { + val javaX = JpmsMultiRelease(javaVersion, project, this) + block(JpmsSourceSetContainer(javaX)) return javaX } diff --git a/src/dorkbox/gradle/jpms/JavaXConfiguration.kt b/src/dorkbox/gradle/jpms/JpmsMultiRelease.kt similarity index 89% rename from src/dorkbox/gradle/jpms/JavaXConfiguration.kt rename to src/dorkbox/gradle/jpms/JpmsMultiRelease.kt index 5ad13db..34bc572 100644 --- a/src/dorkbox/gradle/jpms/JavaXConfiguration.kt +++ b/src/dorkbox/gradle/jpms/JpmsMultiRelease.kt @@ -17,11 +17,7 @@ package dorkbox.gradle.jpms import dorkbox.gradle.StaticMethodsAndTools -import org.gradle.api.Action -import org.gradle.api.GradleException -import org.gradle.api.JavaVersion -import org.gradle.api.Project -import org.gradle.api.Task +import org.gradle.api.* import org.gradle.api.file.SourceDirectorySet import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSetContainer @@ -42,13 +38,10 @@ import java.io.File //target/classes-java9 . @Suppress("MemberVisibilityCanBePrivate", "ObjectLiteralToLambda") -class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project, gradleUtils: StaticMethodsAndTools) { +class JpmsMultiRelease(javaVersion: JavaVersion, private val project: Project, gradleUtils: StaticMethodsAndTools) { val ver: String = javaVersion.majorVersion - // this cannot be ONLY a number, there must be something else -- intellij will *not* pickup the name if it's only a number - val nameX = "_$ver" - - // If the kotlin plugin is applied, and there is a compileKotlin task.. Then kotlin is enabled + // If the kotlin plugin is applied, and there is a compileKotlin task. Then kotlin is enabled val hasKotlin = gradleUtils.hasKotlin val moduleFile = project.projectDir.walkTopDown().find { it.name == "module-info.java" } @@ -65,17 +58,19 @@ class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project, lateinit var compileMainKotlin: KotlinCompile lateinit var compileTestKotlin: KotlinCompile - + val lower = "jpms" + val upper = "Jpms" // plugin provided - val mainX: SourceSet = sourceSets.maybeCreate("main$nameX") - val testX: SourceSet = sourceSets.maybeCreate("test$nameX") + // These generate warnings in Gradle 8.x -- Fixed in 8.4 + val mainX: SourceSet = sourceSets.maybeCreate("${lower}Main") + val testX: SourceSet = sourceSets.maybeCreate("${lower}Test") // the compile task NAME must match the source-set name - val compileMainXJava: JavaCompile = project.tasks.named("compileMain${nameX}Java", JavaCompile::class.java).get() - val compileTestXJava: JavaCompile = project.tasks.named("compileTest${nameX}Java", JavaCompile::class.java).get() + val compileMainXJava: JavaCompile = project.tasks.named("compile${upper}MainJava", JavaCompile::class.java).get() + val compileTestXJava: JavaCompile = project.tasks.named("compile${upper}TestJava", JavaCompile::class.java).get() - val compileModuleInfoX: JavaCompile = project.tasks.create("compileModuleInfo$nameX", JavaCompile::class.java) + val compileModuleInfoX: JavaCompile = project.tasks.create("compileJpmsModuleInfo", JavaCompile::class.java) lateinit var compileMainXKotlin: TaskProvider lateinit var compileTestXKotlin: TaskProvider @@ -83,7 +78,7 @@ class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project, // have to create a task in order to the files to get "picked up" by intellij/gradle. No *test* task? Then gradle/intellij won't be able run // the tests, even if you MANUALLY tell intellij to run a test from the sources dir // https://docs.gradle.org/current/dsl/org.gradle.api.tasks.testing.Test.html - val runTestX: Test = project.tasks.create("test${nameX}Test", Test::class.java) + val runTestX: Test = project.tasks.create("${lower}Test", Test::class.java) init { if (moduleFile == null) { @@ -135,7 +130,7 @@ class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project, }) if (hasKotlin) { - val kotlin = project.extensions.getByType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension::class.java).sourceSets.getByName("main$nameX") + val kotlin = project.extensions.getByType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension::class.java).sourceSets.getByName("${lower}Main") kotlin.kotlin.apply { // I don't like the opinionated sonatype directory structure. @@ -174,7 +169,7 @@ class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project, }) if (hasKotlin) { - val kotlin = project.extensions.getByType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension::class.java).sourceSets.getByName("test$nameX") + val kotlin = project.extensions.getByType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension::class.java).sourceSets.getByName("${lower}Test") kotlin.kotlin.apply { // I don't like the opinionated sonatype directory structure. @@ -219,20 +214,20 @@ class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project, ////////////// if (hasKotlin) { - compileMainXKotlin = project.tasks.named("compileMain${nameX}Kotlin", KotlinCompile::class.java) - compileTestXKotlin = project.tasks.named("compileTest${nameX}Kotlin", KotlinCompile::class.java) + compileMainXKotlin = project.tasks.named("compile${upper}MainKotlin", KotlinCompile::class.java) + compileTestXKotlin = project.tasks.named("compile${upper}TestKotlin", KotlinCompile::class.java) } // have to setup the configurations, so dependencies work correctly val configs = project.configurations - configs.maybeCreate("main${nameX}Implementation").extendsFrom(configs.getByName("implementation")).extendsFrom(configs.getByName("compileOnly")) - configs.maybeCreate("main${nameX}Runtime").extendsFrom(configs.getByName("implementation")).extendsFrom(configs.getByName("runtimeOnly")) - configs.maybeCreate("main${nameX}CompileOnly").extendsFrom(configs.getByName("compileOnly")) + configs.maybeCreate("${lower}MainImplementation").extendsFrom(configs.getByName("implementation")).extendsFrom(configs.getByName("compileOnly")) + configs.maybeCreate("${lower}MainRuntime").extendsFrom(configs.getByName("implementation")).extendsFrom(configs.getByName("runtimeOnly")) + configs.maybeCreate("${lower}MainCompileOnly").extendsFrom(configs.getByName("compileOnly")) - configs.maybeCreate("test${nameX}Implementation").extendsFrom(configs.getByName("testImplementation")).extendsFrom(configs.getByName("testCompileOnly")) - configs.maybeCreate("test${nameX}Runtime").extendsFrom(configs.getByName("testImplementation")).extendsFrom(configs.getByName("testRuntimeOnly")) - configs.maybeCreate("test${nameX}CompileOnly").extendsFrom(configs.getByName("testCompileOnly")) + configs.maybeCreate("${upper}TestImplementation").extendsFrom(configs.getByName("testImplementation")).extendsFrom(configs.getByName("testCompileOnly")) + configs.maybeCreate("${upper}TestRuntime").extendsFrom(configs.getByName("testImplementation")).extendsFrom(configs.getByName("testRuntimeOnly")) + configs.maybeCreate("${upper}TestCompileOnly").extendsFrom(configs.getByName("testCompileOnly")) // setup task graph and compile version @@ -278,7 +273,7 @@ class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project, dependsOn(compileMainKotlin) } - val proj = this@JavaXConfiguration.project + val proj = this@JpmsMultiRelease.project val allSource = proj.files( main.allSource.srcDirs, @@ -294,7 +289,7 @@ class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project, inputs.property("moduleName", moduleName) destinationDirectory.set(compileMainXJava.destinationDirectory.asFile.orNull) - classpath = this@JavaXConfiguration.project.files() // this resets the classpath. we use the module-path instead! + classpath = this@JpmsMultiRelease.project.files() // this resets the classpath. we use the module-path instead! // modules require this! @@ -323,7 +318,7 @@ class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project, override fun execute(task: Task) { task as JavaCompile - val intellijClasses = File("${this@JavaXConfiguration.project.buildDir}/classes-intellij") + val intellijClasses = File("${this@JpmsMultiRelease.project.buildDir}/classes-intellij") if (intellijClasses.exists()) { // copy everything to intellij also. FORTUNATELY, we know it's only going to be the `module-info` and `package-info` classes! val directory = task.destinationDirectory.asFile.get() diff --git a/src/dorkbox/gradle/jpms/JpmsOnly.kt b/src/dorkbox/gradle/jpms/JpmsOnly.kt new file mode 100644 index 0000000..d92371e --- /dev/null +++ b/src/dorkbox/gradle/jpms/JpmsOnly.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2021 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dorkbox.gradle.jpms + +import dorkbox.gradle.StaticMethodsAndTools +import org.gradle.api.* +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.compile.JavaCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.io.File + +// from: http://mail.openjdk.java.net/pipermail/jigsaw-dev/2017-February/011306.html +/// If you move the module-info.java to the top-level directory directory then I would expect this should work: +// +//javac --release 8 -d target/classes src/main/java/com/example/A.java src/main/java/com/example/Version.java +//javac -d target/classes src/main/java/module-info.java +//javac -d target/classes-java9 -cp target/classes src/main/java9/com/example/A.java +//jar --create --file mr.jar -C target/classes . --release 9 -C +//target/classes-java9 . + +object JpmsOnly { + + // check to see if we have a module-info.java file. + // if we do, then we are "JPMS" enabled, and require some fiddling. + // it is INCREDIBLY stupid this is as difficult as this is. + // https://youtrack.jetbrains.com/issue/KT-55389/Gradle-plugin-should-expose-an-extension-method-to-automatically-enable-JPMS-for-Kotlin-compiled-output + fun runIfNecessary(javaVersion: JavaVersion, project: Project, gradleUtils: StaticMethodsAndTools) { + val ver = javaVersion.majorVersion.toIntOrNull() + + if (ver == null || ver < 9) { + // obviously not going to happen! + return + } + + + // If the kotlin plugin is applied, and there is a compileKotlin task.. Then kotlin is enabled + val hasKotlin = gradleUtils.hasKotlin + + val moduleFile = project.projectDir.walkTopDown().find { it.name == "module-info.java" } + var moduleName: String + + if (moduleFile == null) { + // Cannot manage JPMS build without a `module-info.java` file.... + return + } + + val srcName = moduleFile.parentFile.name + if (srcName.startsWith("src") && srcName.last().isDigit()) { + // if the module-file is in our srcX directory, we ignore these steps, since we would be building a multi-release jar + return + } + + // also the source dirs have been configured/setup. + moduleName = moduleFile.readLines()[0].split(" ")[1].trimEnd('{') + if (moduleName.isEmpty()) { + throw GradleException("The module name must be specified in the module-info file! Verify file: $moduleFile") + } + + val info = when { + hasKotlin -> "Initializing JPMS $ver, Java/Kotlin [$moduleName]" + else -> "Initializing JPMS $ver, Java [$moduleName]" + } + println("\t$info") + + + + + val sourceSets = project.extensions.getByName("sourceSets") as SourceSetContainer + + val main: SourceSet = sourceSets.named("main", org.gradle.api.tasks.SourceSet::class.java).get() + val compileMainJava: JavaCompile = project.tasks.named("compileJava", JavaCompile::class.java).get() + + lateinit var compileMainKotlin: KotlinCompile + + if (hasKotlin) { + // can only setup the NORMAL tasks first + compileMainKotlin = project.tasks.named("compileKotlin", KotlinCompile::class.java).get() + } + + // make sure defaults are loaded + StaticMethodsAndTools.idea(project) { + if (module.sourceDirs == null) { + module.sourceDirs = setOf() + } + + // make sure there is something there! + module.testSources.from(setOf()) + module.testResources.from(setOf()) + } + + project.tasks.named("compileJava", JavaCompile::class.java) { + // modules require this! + it.doFirst(object: Action { + override fun execute(task: Task) { + task as JavaCompile + + val allCompiled = if (hasKotlin) { + project.files(compileMainJava.destinationDirectory.asFile.orNull, compileMainKotlin.destinationDirectory.asFile.orNull) + } else { + project.files(compileMainJava.destinationDirectory.asFile.orNull) + } + + // the SOURCE of the module-info.java file. It uses **EVERYTHING** + task.options.compilerArgs.addAll(listOf( + "-implicit:none", + "-Xpkginfo:always", // compile the package-info.java files as well (normally it does not) + "--module-path", main.compileClasspath.asPath, + "--patch-module", "$moduleName=" + allCompiled.asPath // add our existing, compiled classes so module-info can find them + )) + } + }) + + it.doLast(object: Action { + override fun execute(task: Task) { + task as JavaCompile + + val intellijClasses = File("${project.layout.buildDirectory.locationOnly.get()}/classes-intellij") + if (intellijClasses.exists()) { + // copy everything to intellij also. FORTUNATELY, we know it's only going to be the `module-info` and `package-info` classes! + val directory = task.destinationDirectory.asFile.get() + + val moduleInfo = directory.walkTopDown().filter { it.name == "module-info.class" }.toList() + val packageInfo = directory.walkTopDown().filter { it.name == "package-info.class" }.toList() + + val name = when { + moduleInfo.isNotEmpty() && packageInfo.isNotEmpty() -> "module-info and package-info" + moduleInfo.isNotEmpty() && packageInfo.isEmpty() -> "module-info" + else -> "package-info" + } + + println("\tCopying $name files into the intellij classes location...") + + moduleInfo.forEach { + val newLocation = File(intellijClasses, it.relativeTo(directory).path) + it.copyTo(newLocation, overwrite = true) + } + + packageInfo.forEach { + val newLocation = File(intellijClasses, it.relativeTo(directory).path) + it.copyTo(newLocation, overwrite = true) + } + } + } + }) + } + } +} diff --git a/src/dorkbox/gradle/jpms/SourceSetContainer2.kt b/src/dorkbox/gradle/jpms/JpmsSourceSetContainer.kt similarity index 92% rename from src/dorkbox/gradle/jpms/SourceSetContainer2.kt rename to src/dorkbox/gradle/jpms/JpmsSourceSetContainer.kt index b641074..a3b9b2e 100644 --- a/src/dorkbox/gradle/jpms/SourceSetContainer2.kt +++ b/src/dorkbox/gradle/jpms/JpmsSourceSetContainer.kt @@ -18,7 +18,7 @@ package dorkbox.gradle.jpms import org.gradle.api.tasks.SourceSet -class SourceSetContainer2(private val javaX: JavaXConfiguration) { +class JpmsSourceSetContainer(private val javaX: JpmsMultiRelease) { fun main(block: SourceSet.() -> Unit) { block(javaX.mainX) }