Skip to content

fix: make config.docs Groovydoc aggregation work on Gradle 9 - #24

Merged
sbglasius merged 2 commits into
mainfrom
fix/docs-aggregation-gradle-9
Oct 17, 2026
Merged

sbglasius merged 2 commits into
mainfrom
fix/docs-aggregation-gradle-9

Conversation

@sbglasius

Copy link
Copy Markdown
Collaborator

Problem

In consumers on Gradle 9 (e.g. grails-export on Grails 8 / Gradle 9.8.1), ./gradlew docs fails in aggregateGroovyApiDoc:

Resolution of the configuration ':grails-export:detachedConfiguration129' was attempted without an exclusive lock. This is unsafe and not allowed.

The root task reads the plugin project's Groovydoc classpath and groovyClasspath, so it resolves another project's configurations. Gradle 9 rejects that in parallel builds (org.gradle.parallel=true), where a task only holds its own project's lock.

There is a second Gradle 9 problem: a Groovydoc task without groovy-base gets no default for the new required properties (noIndex, noHelp, noDeprecatedList, …), so validation fails.

Fix (config.docs.gradle)

  • The root project builds its own classpath: a aggregateGroovydoc dependency-scope configuration (Grails BOM at the build's grailsVersion, org.apache.groovy:groovy, plus a project dependency on the plugin project) and a resolvable aggregateGroovydocClasspath (runtime, library, jar). The Groovy classpath is inferred from it with groovyRuntime. No other project's configuration is resolved.
  • Apply groovy-base so the Groovydoc task gets its defaults and groovyRuntime is available.

Tests

  • New PluginDocsSpec runs aggregateGroovyApiDoc with --parallel on the current Gradle (8.14.4) and on the Grails 8 Gradle line (9.8.1). With the old wiring it fails on 9.8.1 with the same exclusive-lock error. With the fix it passes on both.
  • ./gradlew -p conventions test passes on JDK 21: 9 tests, the Grails 8 line included.
  • The template's own ./gradlew docs still works on Grails 7 / Gradle 8.14.4.
  • Published to mavenLocal and pointed a copy of the Sync common files from grails-plugin-template gpc/grails-export#69 branch at it, with the Gradle 9.8.1 wrapper. ./gradlew docs build passes on Grails 8.0.0.

aggregateGroovyApiDoc read the plugin project's Groovydoc classpath and Groovy
classpath, so the root task resolved another project's configurations. Gradle 9
rejects that in parallel builds ("Resolution of the configuration ... was
attempted without an exclusive lock"), which broke ./gradlew docs on Grails 8.

- Resolve the plugin project through a resolvable configuration of the root
  project, with versions from the Grails BOM at the build's grailsVersion, and
  infer the Groovy classpath from it
- Apply groovy-base so the Groovydoc task gets the conventions Gradle 9
  requires (noIndex, noHelp, ...) and groovyRuntime
- Add PluginDocsSpec, building the aggregation in parallel on the current
  Gradle and on the Grails 8 Gradle line

@sbglasius sbglasius left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This an AI assisted review

Summary

The fix is correct. The root task now resolves only its own aggregateGroovydocClasspath. It no longer reaches into :plugin's Groovydoc classpath/groovyClasspath, so it stays within Gradle 9's per-project lock under --parallel. Applying groovy-base to get the new Groovydoc property conventions and groovyRuntime is also the right approach. I ran ./gradlew -p conventions test --tests '*PluginDocsSpec*' locally and both rows passed (Gradle 8.14.4 and 9.8.1).

Nothing blocking. A few suggestions:

1. The Gradle 9 test row still uses the Grails 7 BOM

PluginDocsSpec writes grailsVersion=${System.getProperty('grailsVersion')} (7.0.11) for both rows. The 9.8.1 row is therefore Gradle 9 + Grails 7 BOM/Groovy 4, which no real consumer runs. The grails8Version system property is already passed to the tests but isn't used here. Consider pairing each Gradle version with its Grails line ([gradle, grails, javaVersion]), with @Requires({ Runtime.version().feature() >= data.javaVersion }) as GrailsLineSpec does. That way the Groovy 5 / Grails 8 BOM path (the one that broke grails-export) is covered in CI and not only by the manual mavenLocal check.

2. dependsOn(upstream) is now redundant

Before this change, depending on :plugin:groovydoc came with reading its classpath. Now the only thing taken from the upstream task is source. Source files don't require the upstream Groovydoc to have run, and the plugin jar is already pulled in through the classpath build dependency. So every docs run also generates plugin/build/docs/groovydoc for nothing. The dependency can be dropped (keeping source = { upstream.get().source }).

3. Side effects of groovy-base on the root project

groovy-base brings in java-base → base. The root project now has clean/assemble/check/build tasks, a java extension and an empty sourceSets container. Root clean now deletes the root build/ dir, including build/docs. That's probably fine or even desirable, but it changes behaviour for consumers, so it may be worth a line in the release notes. It could also conflict with a consumer that registers its own clean in the root project.

4. Minor / style

  • The aggregateGroovydoc.configure { ... } block can go inside the configurations.dependencyScope('aggregateGroovydoc') { ... } closure. Then the declaration and its dependencies sit in one place.
  • Optional: the plugin's runtimeElements already carries Groovy and its BOM constraints when it declares implementation platform(...). The explicit BOM + org.apache.groovy:groovy (and the hard grailsVersion requirement) may then be unnecessary. Keeping them is a safer bet for plugins that rely on the dependency-management plugin, so this is just a thought.
  • Not added by this PR, but nearby: destinationDir = rootProject.layout.buildDirectory.dir('docs/gapi').get().asFile resolves eagerly. source = { upstream.get().source } still touches another project's task, which will matter if configuration cache or isolated projects is ever turned on.

…t groovydoc dependency

- Pair each Gradle version in PluginDocsSpec with its Grails line (7.x on
  Gradle 8, 8.x on Gradle 9 with Java 21), like GrailsLineSpec
- aggregateGroovyApiDoc no longer depends on the plugin project's groovydoc
  task: only its sources are used, and the plugin jar is built through the
  classpath
@sbglasius
sbglasius merged commit fabd4e9 into main Oct 17, 2026
5 checks passed
@sbglasius
sbglasius deleted the fix/docs-aggregation-gradle-9 branch October 17, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant