diff --git a/src/dorkbox/gradle/StaticMethodsAndTools.kt b/src/dorkbox/gradle/StaticMethodsAndTools.kt index a8ad711..d0f0a72 100644 --- a/src/dorkbox/gradle/StaticMethodsAndTools.kt +++ b/src/dorkbox/gradle/StaticMethodsAndTools.kt @@ -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 = targetNames + } } diff --git a/src/dorkbox/gradle/jpms/JavaXConfiguration.kt b/src/dorkbox/gradle/jpms/JavaXConfiguration.kt new file mode 100644 index 0000000..b40a68b --- /dev/null +++ b/src/dorkbox/gradle/jpms/JavaXConfiguration.kt @@ -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" + } + } +} diff --git a/src/dorkbox/gradle/jpms/SourceSetContainer2.kt b/src/dorkbox/gradle/jpms/SourceSetContainer2.kt new file mode 100644 index 0000000..b641074 --- /dev/null +++ b/src/dorkbox/gradle/jpms/SourceSetContainer2.kt @@ -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) + } +}