Getting JPMS to compile + assemble jars

This commit is contained in:
Robinson 2021-04-22 20:59:37 +02:00
parent fcbb4c93ce
commit 6eeecf0b41
3 changed files with 429 additions and 0 deletions

View File

@ -24,10 +24,12 @@ import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.file.DuplicatesStrategy import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.specs.Specs import org.gradle.api.specs.Specs
import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.tasks.Jar import org.gradle.jvm.tasks.Jar
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File import java.io.File
@ -588,4 +590,67 @@ open class StaticMethodsAndTools(private val project: Project) {
block(SourceSetContainer2(javaX)) block(SourceSetContainer2(javaX))
return javaX return javaX
} }
/**
* Should gradle try to infer that this project is a JPMS module by analysing JARs and the classpath?
*
* Only possible for gradle >= 6.4
*/
fun inferJpmsModule() {
if (GradleVersion.current() >= GradleVersion.version("6.4")) {
project.gradle.taskGraph.whenReady {
project.convention.configure(JavaPluginExtension::class.java) {
// Should a --module-path be inferred by analysing JARs and class folders on the classpath?
it.modularity.inferModulePath.set(true)
}
}
}
}
/**
* Fix issues where code (usually test code) needs access to **INTERNAL** scope objects.
* -- at the moment, this only fixes gradle -- not intellij
* There are also gradle 8 warnings when using this.
*
* https://stackoverflow.com/questions/59072889/how-to-test-kotlin-function-declared-internal-from-within-tests-when-java-test
* https://youtrack.jetbrains.com/issue/KT-20760
* https://youtrack.jetbrains.com/issue/KT-45787
* https://stackoverflow.com/questions/57050889/kotlin-internal-members-not-accessible-from-alternative-test-source-set-in-gradl
* https://youtrack.jetbrains.com/issue/KT-34901
*
* (related)
* https://github.com/JetBrains/kotlin/blob/master/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JVMCompilerArguments.kt
* https://github.com/bazelbuild/rules_kotlin/pull/465
* https://github.com/bazelbuild/rules_kotlin/issues/211
*
* Two things are required for this to work
*
* 1) The kotlin module names must be the same
* 2) The kotlin modules must be associated
*/
fun allowKotlinInternalAccessForTests(moduleName: String, vararg accessGroup: AccessGroup) {
// Make sure to cleanup the any possible license file on clean
println("\tAllowing kotlin internal access for $moduleName")
project.tasks.withType(KotlinCompile::class.java).forEach {
it.kotlinOptions.moduleName = moduleName // must be the same module name for everything!
}
accessGroup.forEach {
// allow code in a *different* directory access to "internal" scope members of code.
// THIS FIXES GRADLE - BUT NOT INTELLIJ!
val kotlinExt = project.extensions.getByName("kotlin") as org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
kotlinExt.target.compilations.getByName(it.sourceName).apply {
it.targetNames.forEach { targetName ->
associateWith(target.compilations.getByName(targetName))
}
}
}
}
class AccessGroup(val sourceName: String, vararg targetNames: String) {
val targetNames: Array<out String> = targetNames
}
} }

View File

@ -0,0 +1,335 @@
/*
* 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.kotlin
import org.gradle.api.GradleException
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.internal.HasConvention
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.testing.Test
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
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 .
@Suppress("MemberVisibilityCanBePrivate")
class JavaXConfiguration(javaVersion: JavaVersion, private val project: Project) {
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 kotlin files are present(meaning kotlin is used), we should setup the kotlin tasks
val hasKotlin = project.projectDir.walkTopDown().find { it.extension == "kt" }?.exists() ?: false
val hasJava = project.projectDir.walkTopDown().find {
it.extension == "java" && !(it.name == "module-info.java" ||
it.name == "package-info.java" ||
it.name == "EmptyClass.java") }?.exists() ?: false
val moduleFile = project.projectDir.walkTopDown().find { it.name == "module-info.java" }
var moduleName: String
val sourceSets = project.extensions.getByName("sourceSets") as SourceSetContainer
// standard
val main: SourceSet = sourceSets.named("main", org.gradle.api.tasks.SourceSet::class.java).get()
val test: SourceSet = sourceSets.named("test", org.gradle.api.tasks.SourceSet::class.java).get()
val compileMainJava: JavaCompile = project.tasks.named("compileJava", JavaCompile::class.java).get()
val compileTestJava: JavaCompile = project.tasks.named("compileTestJava", JavaCompile::class.java).get()
lateinit var compileMainKotlin: KotlinCompile
lateinit var compileTestKotlin: KotlinCompile
// plugin provided
val mainX: SourceSet = sourceSets.maybeCreate("main$nameX")
val testX: SourceSet = sourceSets.maybeCreate("test$nameX")
// 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 compileModuleInfoX: JavaCompile = project.tasks.create("compileModuleInfo$nameX", JavaCompile::class.java)
lateinit var compileMainXKotlin: KotlinCompile
lateinit var compileTestXKotlin: KotlinCompile
// 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::class.java)
init {
if (moduleFile == null) {
throw GradleException("Cannot manage JPMS build without a `module-info` file.")
}
// 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 {
hasJava && hasKotlin -> "Initializing [JPMS $ver] '$moduleName' -> Java/Kotlin"
hasJava -> "Initializing [JPMS $ver] '$moduleName' -> Java"
hasKotlin -> "Initializing [JPMS $ver] '$moduleName' -> Kotlin"
else -> throw GradleException("Unable to initialize unknown JPMS type, no Java or Kotlin files found.")
}
println("\t$info")
if (hasKotlin) {
// can only setup the NORMAL tasks first
compileMainKotlin = project.tasks.named("compileKotlin", KotlinCompile::class.java).get()
compileTestKotlin = project.tasks.named("compileTestKotlin", KotlinCompile::class.java).get()
}
// setup compile/runtime project dependencies
mainX.apply {
java.apply {
// I don't like the opinionated sonatype directory structure.
setSrcDirs(project.files("src$ver"))
include("**/*.java") // want to include java files for the source. 'setSrcDirs' resets includes...
exclude("**/module-info.java", "**/EmptyClass.java") // we have to compile these in a different step!
// note: if we set the destination path, that location will be DELETED when the compile for these sources starts...
}
if (hasKotlin) {
kotlin {
setSrcDirs(project.files("src$ver"))
include("**/*.kt") // want to include java files for the source. 'setSrcDirs' resets includes...
// note: if we set the destination path, that location will be DELETED when the compile for these sources starts...
}
}
resources.setSrcDirs(project.files("resources$ver"))
compileClasspath += main.compileClasspath + main.output
runtimeClasspath += main.runtimeClasspath + main.output + compileClasspath
}
testX.apply {
java.apply {
setSrcDirs(project.files("test$ver"))
include("**/*.java") // want to include java files for the source. 'setSrcDirs' resets includes...
// note: if we set the destination path, that location will be DELETED when the compile for these sources starts...
}
if (hasKotlin) {
kotlin {
setSrcDirs(project.files("test$ver"))
include("**/*.kt") // want to include java files for the source. 'setSrcDirs' resets includes...
// note: if we set the destination path, that location will be DELETED when the compile for these sources starts...
}
}
resources.setSrcDirs(project.files("testResources$ver"))
compileClasspath += mainX.compileClasspath + test.compileClasspath + test.output
runtimeClasspath += mainX.runtimeClasspath + test.runtimeClasspath + test.output
}
// run the testX verification
runTestX.apply {
dependsOn("test")
description = "Runs Java $ver tests"
group = "verification"
outputs.upToDateWhen { false }
shouldRunAfter("test")
// The directories for the compiled test sources.
testClassesDirs = testX.output.classesDirs
classpath = testX.runtimeClasspath
}
//////////////
// done setting info for the source-sets, now we can setup configurations and other tasks
// if this is done out-of-order, things aren't configured correctly
//////////////
if (hasKotlin) {
compileMainXKotlin = project.tasks.named("compileMain${nameX}Kotlin", KotlinCompile::class.java).get()
compileTestXKotlin = project.tasks.named("compileTest${nameX}Kotlin", KotlinCompile::class.java).get()
}
// 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("test${nameX}Implementation").extendsFrom(configs.getByName("testImplementation")).extendsFrom(configs.getByName("testCompileOnly"))
configs.maybeCreate("test${nameX}Runtime").extendsFrom(configs.getByName("testImplementation")).extendsFrom(configs.getByName("testRuntimeOnly"))
// setup task graph and compile version
compileMainXJava.apply {
dependsOn(compileMainJava)
sourceCompatibility = ver
targetCompatibility = ver
}
compileTestXJava.apply {
dependsOn(compileTestJava)
sourceCompatibility = ver
targetCompatibility = ver
}
if (hasKotlin) {
compileMainXKotlin.apply {
dependsOn(compileMainKotlin)
sourceCompatibility = ver
targetCompatibility = ver
kotlinOptions.jvmTarget = ver
kotlinOptions.moduleName = compileMainKotlin.kotlinOptions.moduleName // must be the same module name
}
compileTestXKotlin.apply {
dependsOn(compileTestKotlin)
sourceCompatibility = ver
targetCompatibility = ver
kotlinOptions.jvmTarget = ver
kotlinOptions.moduleName = compileTestKotlin.kotlinOptions.moduleName // must be the same module name
}
}
compileModuleInfoX.apply {
// we need all the compiled classes before compiling module-info.java
dependsOn(compileMainJava)
if (hasKotlin) {
dependsOn(compileMainKotlin)
}
val proj = this@JavaXConfiguration.project
val allSource = proj.files(
main.allSource.srcDirs,
mainX.allSource.srcDirs
)
val allCompiled = if (hasKotlin) {
proj.files(
compileMainJava.destinationDir,
compileMainKotlin.destinationDir
)
} else {
proj.files(
compileMainJava.destinationDir
)
}
source = allSource.asFileTree // the files live in this location
include("**/module-info.java")
sourceCompatibility = ver
targetCompatibility = ver
inputs.property("moduleName", moduleName)
destinationDir = compileMainXJava.destinationDir
classpath = this@JavaXConfiguration.project.files() // this resets the classpath. we use the module-path instead!
// modules require this!
doFirst {
// the SOURCE of the module-info.java file. It uses **EVERYTHING**
options.sourcepath = allSource
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
))
}
doLast {
val intellijClasses = File("${this@JavaXConfiguration.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 moduleInfo = destinationDir.walkTopDown().filter { it.name == "module-info.class" }.toList()
val packageInfo = destinationDir.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(destinationDir).path)
it.copyTo(newLocation, overwrite = true)
}
}
}
}
project.tasks.named("jar", Jar::class.java).get().apply {
dependsOn(compileModuleInfoX)
// NOTE: This syntax screws up, and the entire contents of the jar are in the wrong place...
// from(mainX.output.classesDirs) {
// exclude("META-INF")
// into("META-INF/versions/$ver")
// }
from(mainX.output.classesDirs)
val sourcePaths = mainX.output.classesDirs.map {it.absolutePath}.toSet()
// println("SOURCE PATHS+ $sourcePaths")
doFirst {
// this is how to correctly RE-MAP the location of files in jar
eachFile { details ->
val absolutePath = details.file.absolutePath
val length = details.path.length + 1
val sourceDir = absolutePath.substring(0, absolutePath.length - length)
if (sourcePaths.contains(sourceDir)) {
// println("Moving: " + absolutePath)
// println(" : " + details.path)
details.path = "META-INF/versions/${ver}/${details.path}"
}
}
}
// this is required for making the java 9+ multi-release version possible
manifest.attributes["Multi-Release"] = "true"
}
}
}

View File

@ -0,0 +1,29 @@
/*
* 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 org.gradle.api.tasks.SourceSet
class SourceSetContainer2(private val javaX: JavaXConfiguration) {
fun main(block: SourceSet.() -> Unit) {
block(javaX.mainX)
}
fun test(block: SourceSet.() -> Unit) {
block(javaX.testX)
}
}