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.file.DuplicatesStrategy
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.specs.Specs
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.tasks.Jar
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File
@ -588,4 +590,67 @@ open class StaticMethodsAndTools(private val project: Project) {
block(SourceSetContainer2(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)
}
}