From 0e1d6cb19a3a78bae370f798f0d019d7dc5001fb Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Mon, 22 Jun 2026 18:57:37 +0300 Subject: [PATCH] fix(jandex): register the index as a sourceSet output directory The plugin wrote the Jandex index into sourceSet.output.resourcesDir and relied on per-task dependsOn wiring so that consumers would run after the copy. That wiring was broad for the main sourceSet but covered only the jar and javadoc tasks for non-main sourceSets, so other consumers of the output -- the JMH bytecode generator, checkstyle, forbidden-apis -- had no declared dependency on processXxxJandexIndex. Gradle 9 turns that implicit dependency into a build failure (see pgjdbc/pgjdbc#4010). Build the index into a dedicated directory and register it as an extra output of the sourceSet via output.dir(builtBy = processXxxJandexIndex). Gradle then wires every consumer of the output automatically, so the manual dependsOn blocks for both main and non-main sourceSets are removed. Add a Gradle 9 integration test that drives a non-main sourceSet consumer across all JandexBuildAction modes (BUILD_AND_INCLUDE, BUILD, VERIFY_ONLY, NONE) and checks the index reaches the jar only for BUILD_AND_INCLUDE. Co-Authored-By: Claude Opus 4.8 --- .../com/github/vlsi/jandex/JandexPlugin.kt | 52 ++------- .../github/vlsi/jandex/JandexPluginTest.kt | 105 ++++++++++++++++++ 2 files changed, 115 insertions(+), 42 deletions(-) diff --git a/plugins/jandex-plugin/src/main/kotlin/com/github/vlsi/jandex/JandexPlugin.kt b/plugins/jandex-plugin/src/main/kotlin/com/github/vlsi/jandex/JandexPlugin.kt index 493f37a3..c2569c25 100644 --- a/plugins/jandex-plugin/src/main/kotlin/com/github/vlsi/jandex/JandexPlugin.kt +++ b/plugins/jandex-plugin/src/main/kotlin/com/github/vlsi/jandex/JandexPlugin.kt @@ -19,11 +19,7 @@ package com.github.vlsi.jandex import com.github.vlsi.jandex.JandexProcessResources.Companion.getTaskName import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSetContainer -import org.gradle.api.tasks.compile.JavaCompile -import org.gradle.api.tasks.javadoc.Javadoc -import org.gradle.jvm.tasks.Jar import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.withType @@ -69,13 +65,19 @@ open class JandexPlugin : Plugin { }) } - val resourceDir = sourceSet.output.resourcesDir!! + // Build the index into a dedicated directory and register it as an extra output of + // the sourceSet. Gradle then makes every consumer of sourceSet.output depend on + // processJandexIndex automatically, so no per-task wiring is needed. It also keeps + // Gradle 9 from reporting an implicit dependency for consumers the plugin does not + // know about (e.g. the JMH bytecode generator or checkstyle on non-main sourceSets). + val jandexResourcesDir = + project.layout.buildDirectory.dir("jandexResources/$sourceSetName") val processJandexIndex = tasks.register( getTaskName(sourceSet), JandexProcessResources::class ) { - description = "Copies Jandex index for $sourceSetName to the resources" - destinationDir = resourceDir + description = "Copies Jandex index for $sourceSetName to the sourceSet output" + destinationDir = jandexResourcesDir.get().asFile jandexBuildAction.set(task.flatMap { it.jandexBuildAction }) onlyIf { jandexBuildAction.get() != JandexBuildAction.NONE @@ -89,41 +91,7 @@ open class JandexPlugin : Plugin { }) } } - if (name == SourceSet.MAIN_SOURCE_SET_NAME) { - // Assume all sourceSets depend on main one, so we make ALL tasks depend on - // processJandexIndex from the main sourceSet - val compileJavaTaskName = sourceSet.compileJavaTaskName - tasks.withType() - .matching { it.name != compileJavaTaskName } - .configureEach { - dependsOn(processJandexIndex) - } - tasks.withType().configureEach { - dependsOn(processJandexIndex) - } - tasks.withType().configureEach { - dependsOn(processJandexIndex) - } - tasks.matching { - it.name.startsWith("forbiddenApis") || - it.name.startsWith("compile") && it.name.endsWith("Kotlin") && it.name != "compileKotlin" - } - .configureEach { - dependsOn(processJandexIndex) - } - } else { - // Non-main sourceSets depend on their processJandexIndex as well - val jarTaskName = sourceSet.jarTaskName - val sourcesJarTaskName = sourceSet.sourcesJarTaskName - tasks.withType().matching { it.name == jarTaskName || it.name == sourcesJarTaskName }.configureEach { - dependsOn(processJandexIndex) - } - sourceSet.javadocTaskName.let { taskName -> - tasks.withType().matching { it.name == taskName }.configureEach { - dependsOn(processJandexIndex) - } - } - } + sourceSet.output.dir(mapOf("builtBy" to processJandexIndex), jandexResourcesDir) } sourceSets.whenObjectRemoved { tasks.named(getTaskName(JANDEX_TASK_NAME, null)) { diff --git a/plugins/jandex-plugin/src/test/kotlin/com/github/vlsi/jandex/JandexPluginTest.kt b/plugins/jandex-plugin/src/test/kotlin/com/github/vlsi/jandex/JandexPluginTest.kt index f63fb631..8fd3d116 100644 --- a/plugins/jandex-plugin/src/test/kotlin/com/github/vlsi/jandex/JandexPluginTest.kt +++ b/plugins/jandex-plugin/src/test/kotlin/com/github/vlsi/jandex/JandexPluginTest.kt @@ -18,7 +18,11 @@ package com.github.vlsi.jandex import com.github.vlsi.gradle.BaseGradleTest import org.gradle.testkit.runner.TaskOutcome +import org.gradle.util.GradleVersion import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.ExecutionMode import org.junit.jupiter.params.ParameterizedTest @@ -89,4 +93,105 @@ class JandexPluginTest : BaseGradleTest() { } } } + + /** + * processJandexIndex contributes to the sourceSet output, so every task that consumes that + * output must depend on it. For non-main sourceSets the plugin used to wire only the jar and + * javadoc tasks, which left other consumers (the JMH bytecode generator, checkstyle, + * forbidden-apis) without the dependency. Gradle 9 rejects such an implicit dependency, failing + * the build. The fix registers the index as an extra output directory of the sourceSet so the + * dependency is wired automatically for every consumer, in every [JandexBuildAction] mode. + * + * See https://github.com/pgjdbc/pgjdbc/pull/4010 for the original report. + */ + @ParameterizedTest(name = "{0}") + @MethodSource("jandexBuildActions") + fun jandexDoesNotBreakNonMainSourceSetConsumers( + label: String, + jandexConfig: String, + indexIncludedInJar: Boolean + ) { + // The implicit-dependency check is only a warning before Gradle 9, so pin a Gradle 9 here + val testCase = TestCase(GradleVersion.version("9.0.0"), ConfigurationCache.ON) + createSettings(testCase) + projectDir.resolve("src/extra/java/acme").toFile().mkdirs() + projectDir.resolve("src/extra/java/acme/Extra.java").write( + """ + package acme; + public class Extra { + public int inc(int a) { + return a + 1; + } + } + """.trimIndent() + ) + projectDir.resolve("build.gradle").write( + """ + plugins { + id 'java-library' + id 'com.github.vlsi.jandex' + } + + repositories { + mavenCentral() + } + + $jandexConfig + + sourceSets { + extra + } + + // The jandex plugin makes extraJar depend on processExtraJandexIndex, so the index + // task is scheduled and contributes to the extra sourceSet output. + tasks.register('extraJar', Jar) { + archiveClassifier = 'extra' + from sourceSets.extra.output + } + + // A task that reads the sourceSet output, the way the JMH bytecode generator or + // checkstyle do. It must not trip Gradle's implicit-dependency validation against + // processExtraJandexIndex, which contributes to that same output. + tasks.register('useExtraOutput') { + def extraOutput = sourceSets.extra.output + inputs.files(extraOutput).withPropertyName('extraOutput') + outputs.dir(layout.buildDirectory.dir('useExtraOutput')).withPropertyName('outputDir') + doLast { + extraOutput.files.findAll { it.exists() } + } + } + """.trimIndent() + ) + val result = prepare(testCase, "extraJar", "useExtraOutput", "-i").build() + if (isCI) { + println(result.output) + } + assertNotNull(result.task(":processExtraJandexIndex")) { + "[$label] processExtraJandexIndex should be wired into the graph via the sourceSet output" + } + assertEquals(TaskOutcome.SUCCESS, result.task(":useExtraOutput")?.outcome) { + "[$label] a consumer of the extra sourceSet output must build without an implicit-dependency failure" + } + val extraJar = projectDir.resolve("build/libs/sample-extra.jar").toFile() + assertTrue(extraJar.exists()) { "[$label] extraJar should be built at $extraJar" } + java.util.zip.ZipFile(extraJar).use { zip -> + val indexEntry = zip.getEntry("META-INF/jandex.idx") + if (indexIncludedInJar) { + assertNotNull(indexEntry) { "[$label] the Jandex index should be packaged into the jar" } + } else { + assertNull(indexEntry) { "[$label] the Jandex index must not be packaged into the jar" } + } + } + } + + companion object { + @JvmStatic + fun jandexBuildActions(): List = listOf( + // label, build.gradle snippet that selects the JandexBuildAction, index expected in jar + arguments("default => BUILD_AND_INCLUDE", "", true), + arguments("BUILD", "jandex { includeIndexInJar(false) }", false), + arguments("VERIFY_ONLY", "jandex { skipIndexFileGeneration() }", false), + arguments("NONE", "jandex { skipDefaultProcessing() }", false), + ) + } }