You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/04/25 16:11:13 UTC
(grails-core) 06/07: Make issue-11767 spring-config-metadata merge properly cacheable
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch upgrade/gradle-9.3.1
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit d9a218175ac98c4b9e81267e0d28b793b1ca7f83
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Sat Apr 25 11:53:59 2026 -0400
Make issue-11767 spring-config-metadata merge properly cacheable
Replaces the previous outputs.upToDateWhen { false } / mutate-in-place
implementation flagged by jdaugherty in PR #15365 review with a
properly-declared incremental task that Gradle can mark UP-TO-DATE
between builds.
Changes:
* Merge writes to a new build/merged-spring-configuration-metadata/
directory instead of mutating build/classes/{java,groovy}/main.
* Inputs are declared as a lazy FileCollection that filters to existing
files at execution time, so Gradle no longer fails the validation
when one or both compilers are NO-SOURCE.
* outputs.cacheIf { true } enables Gradle build-cache reuse.
* The task explicitly dependsOn compileJava and compileGroovy so the
compile output dirs always populate before the merge runs.
* The jar task uses an eachFile filter to drop the per-compiler copies
but keep the merged copy, instead of a global exclude that would have
also discarded the merged document.
* Removed the long block comment - the inline 7-line summary above the
task is enough context for future readers.
Verified:
* gradle :grails-test-examples-plugins-issue-11767:clean :...:jar
produces build/libs/...jar containing exactly one
META-INF/spring-configuration-metadata.json with both
PluginJavaMicronautBean (groups + properties) and
PluginGroovyMicronautBean entries merged.
* A second jar invocation reports compileJava, compileGroovy,
mergeMicronautConfigMetadata, and jar all UP-TO-DATE - the previous
always-rerun behavior is gone.
* The downstream consumer (:grails-test-examples-issue-11767) compiles
cleanly, confirming the merged metadata still satisfies its
@ConfigurationProperties('my') consumer code.
Assisted-by: claude-code:claude-opus-4-7
---
.../plugins/issue-11767/build.gradle | 91 ++++++++++++++--------
1 file changed, 57 insertions(+), 34 deletions(-)
diff --git a/grails-test-examples/plugins/issue-11767/build.gradle b/grails-test-examples/plugins/issue-11767/build.gradle
index 1052a19aaf..1b68d01892 100644
--- a/grails-test-examples/plugins/issue-11767/build.gradle
+++ b/grails-test-examples/plugins/issue-11767/build.gradle
@@ -42,45 +42,55 @@ apply {
from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle')
}
-// Root-cause fix for duplicate META-INF/spring-configuration-metadata.json:
-//
// This plugin intentionally exposes both a Java bean (PluginJavaMicronautBean) and a
-// Groovy bean (PluginGroovyMicronautBean) that share the @ConfigurationProperties('my')
-// prefix to exercise the original issue 11767 scenario. As a side effect, two different
-// Micronaut tools produce the same metadata file:
-// - micronaut-inject-java runs as a Java annotation processor during compileJava and
-// writes build/classes/java/main/META-INF/spring-configuration-metadata.json
-// describing PluginJavaMicronautBean.
-// - micronaut-inject-groovy (pulled in transitively via grails-micronaut) runs as a
-// Groovy AST transform during compileGroovy and writes
-// build/classes/groovy/main/META-INF/spring-configuration-metadata.json describing
-// PluginGroovyMicronautBean.
-// The two files are not equivalent: each describes the beans visible to its own compiler.
-// The shared CompilePlugin convention sets jar.duplicatesStrategy = FAIL so the build
-// surfaces any double-configuration. Here the duplicate is by design, so we merge the
-// two JSON documents into a single file (preserving every group / property / hint) and
-// remove the now-merged source so the jar task sees exactly one metadata file. This is
-// preferable to dropping one of the documents (DuplicatesStrategy.EXCLUDE) because that
-// would silently lose the bean type information for one of the languages.
+// Groovy bean (PluginGroovyMicronautBean) under @ConfigurationProperties('my') to
+// exercise the issue 11767 scenario. As a side effect, micronaut-inject-java (Java AP)
+// and micronaut-inject-groovy (Groovy AST) each emit a non-equivalent
+// META-INF/spring-configuration-metadata.json describing only the beans visible to
+// their compiler, which collides with the shared jar.duplicatesStrategy = FAIL.
+// Merge the two JSON documents into one combined file in a dedicated build directory,
+// then exclude the per-compiler copies from the jar and feed the merged file in.
+def metadataRelativePath = 'META-INF/spring-configuration-metadata.json'
+def javaMetadata = layout.buildDirectory.file("classes/java/main/${metadataRelativePath}")
+def groovyMetadata = layout.buildDirectory.file("classes/groovy/main/${metadataRelativePath}")
+def mergedMetadataDir = layout.buildDirectory.dir('merged-spring-configuration-metadata')
+def mergedMetadata = mergedMetadataDir.map { it.file(metadataRelativePath) }
+
def mergeMicronautConfigMetadata = tasks.register('mergeMicronautConfigMetadata') {
description = 'Merges the spring-configuration-metadata.json emitted separately by ' +
'micronaut-inject-java and micronaut-inject-groovy into one combined document.'
group = 'build'
- def metadataRelativePath = 'META-INF/spring-configuration-metadata.json'
- def javaMetadataProvider = layout.buildDirectory.file("classes/java/main/${metadataRelativePath}")
- def groovyMetadataProvider = layout.buildDirectory.file("classes/groovy/main/${metadataRelativePath}")
-
- // Re-run whenever either compile step emits new metadata; the task mutates files
- // inside the compile output directories so we cannot rely on standard up-to-date
- // tracking. The work is cheap (two small JSON files) so always running is acceptable.
- outputs.upToDateWhen { false }
+ // Both files may or may not exist depending on which compiler ran and what beans
+ // it found, so feed them as a lazy FileCollection that filters to existing files
+ // at execution time. Declaring the producing tasks as dependencies anchors them
+ // in the task graph so this task runs after compileJava and compileGroovy.
+ dependsOn tasks.named('compileJava'), tasks.named('compileGroovy')
+ inputs.files(provider {
+ [javaMetadata.get().asFile, groovyMetadata.get().asFile].findAll { it.exists() }
+ }).withPropertyName('inputMetadataFiles').withPathSensitivity(PathSensitivity.NONE)
+ outputs.file(mergedMetadata).withPropertyName('mergedMetadata')
+ outputs.cacheIf { true }
doLast {
- def javaFile = javaMetadataProvider.get().asFile
- def groovyFile = groovyMetadataProvider.get().asFile
- if (!javaFile.exists() || !groovyFile.exists()) {
- return // Only one (or neither) compile step emitted metadata; no conflict to merge
+ def javaFile = javaMetadata.get().asFile
+ def groovyFile = groovyMetadata.get().asFile
+ def outputFile = mergedMetadata.get().asFile
+ outputFile.parentFile.mkdirs()
+
+ def javaExists = javaFile.exists()
+ def groovyExists = groovyFile.exists()
+ if (!javaExists && !groovyExists) {
+ outputFile.text = '{}'
+ return
+ }
+ if (!javaExists) {
+ outputFile.text = groovyFile.text
+ return
+ }
+ if (!groovyExists) {
+ outputFile.text = javaFile.text
+ return
}
def slurper = new groovy.json.JsonSlurper()
def parsedJava = slurper.parse(javaFile)
@@ -92,10 +102,23 @@ def mergeMicronautConfigMetadata = tasks.register('mergeMicronautConfigMetadata'
if (parsedGroovy[section]) merged.addAll(parsedGroovy[section])
if (merged) combined[section] = merged
}
- groovyFile.text = groovy.json.JsonOutput.toJson(combined)
- javaFile.delete() // Single canonical file remains under build/classes/groovy/main
+ outputFile.text = groovy.json.JsonOutput.toJson(combined)
}
}
tasks.named('classes').configure { finalizedBy mergeMicronautConfigMetadata }
-tasks.named('jar').configure { dependsOn mergeMicronautConfigMetadata }
+tasks.named('jar', Jar).configure {
+ dependsOn mergeMicronautConfigMetadata
+ // The two per-compiler copies in build/classes/{java,groovy}/main are replaced
+ // by the merged document from build/merged-spring-configuration-metadata/.
+ // A global exclude would drop our merged copy too, so filter by absolute source
+ // path - only the merged-dir copy survives.
+ def mergedDirPath = mergedMetadataDir.get().asFile.absolutePath
+ eachFile { FileCopyDetails details ->
+ if (details.path == metadataRelativePath
+ && !details.file.absolutePath.startsWith(mergedDirPath)) {
+ details.exclude()
+ }
+ }
+ from(mergedMetadataDir)
+}