Skip to content

Comments

Adopt AntBuilder groovydoc with javaVersion support#15420

Open
jamesfredley wants to merge 6 commits into7.0.xfrom
fix/groovydoc-java-version
Open

Adopt AntBuilder groovydoc with javaVersion support#15420
jamesfredley wants to merge 6 commits into7.0.xfrom
fix/groovydoc-java-version

Conversation

@jamesfredley
Copy link
Contributor

@jamesfredley jamesfredley commented Feb 20, 2026

Summary

Replaces Gradle built-in Groovydoc task execution with direct AntBuilder invocation of the Groovy org.codehaus.groovy.ant.Groovydoc Ant task, enabling the javaVersion parameter introduced in Groovy 4.0.27 (GROOVY-11668). The AntBuilder logic is centralized into a layered plugin architecture in build-logic/:

  • GroovydocEnhancerPlugin - Generic, publishable base plugin with all AntBuilder logic
  • GrailsGroovydocPlugin - Thin Grails-specific layer that applies the base and sets the Matomo footer

Also fixes a pre-existing bug in resolveProjectVersion() where external groovydoc links (geb, testcontainers, spring, spring-boot) were silently omitted.

Problem

Gradle Groovydoc task does not expose the javaVersion property (gradle/gradle#33659 is not merged). Without it, groovydoc defaults to JAVA_11 language level when parsing Java source files, causing failures when processing Java 17+ features like sealed classes, records, and pattern matching.

Solution

1. AntBuilder groovydoc with javaVersion

For each Groovydoc task:

  1. Clear Gradle built-in @TaskAction execution
  2. Replace with a doLast that uses AntBuilder to call org.codehaus.groovy.ant.Groovydoc directly
  3. Pass javaVersion: JAVA_17 (derived from gradle.properties) to enable correct Java 17+ source parsing

2. Layered plugin architecture

Base plugin: GroovydocEnhancerPlugin (org.apache.grails.buildsrc.groovydoc-enhancer)

Generic, publishable plugin with no Grails-specific logic:

  • Replaces Gradle's built-in Groovydoc execution with AntBuilder for javaVersion support
  • Registers documentation configuration for Ant classpath
  • Resolves source directories from source sets or ext.groovydocSourceDirs
  • Supports external doc links via ext.groovydocLinks
  • Handles GroovydocAccess enum via Class.forName (not on build-logic compile classpath)
  • Handles Gradle's mixed Property<T> / plain-value API via resolveGroovydocProperty()

GroovydocEnhancerExtension provides:

  • javaVersion (default: JAVA_17) - target Java version for Groovydoc output
  • javaVersionEnabled (default: true) - allows disabling for Groovy 3.x (Forge uses 3.0.25)
  • useAntBuilder (default: true) - switch back to Gradle's built-in task when Support javaVersion property for groovydoc gradle/gradle#33659 merges
  • footer - customizable HTML footer for generated docs

Grails plugin: GrailsGroovydocPlugin (org.apache.grails.buildsrc.groovydoc)

Thin Grails-specific layer:

  • Applies GroovydocEnhancerPlugin
  • Sets footer to the Matomo analytics snippet

Switch-back path

When Gradle merges their javaVersion PR, set groovydocEnhancer { useAntBuilder = false } to skip the AntBuilder action replacement and use Gradle's built-in task instead.

3. Bug fix: resolveProjectVersion()

Fixed a pre-existing bug where resolveProjectVersion() in docs-dependencies.gradle never returned the resolved version string. The function had an early return for null but was missing the return of the actual version value, causing all external groovydoc links to be silently omitted.

Files changed

File Scope Notes
build-logic/plugins/build.gradle Plugin registration Registers both groovydocEnhancer and grailsGroovydoc plugin IDs
build-logic/.../GroovydocEnhancerPlugin.groovy Base plugin Generic AntBuilder execution, source resolution, links
build-logic/.../GroovydocEnhancerExtension.groovy Base extension javaVersion, javaVersionEnabled, useAntBuilder, footer
build-logic/.../GrailsGroovydocPlugin.groovy Grails plugin Applies base plugin, sets Matomo footer
gradle/docs-dependencies.gradle Central config (~90 modules) Plugin apply + deps + dynamic links + resolveProjectVersion fix
gradle/docs-config.gradle Per-module setup Plugin apply + includeInApiDocs
grails-gradle/gradle/docs-config.gradle grails-gradle config Plugin apply + deps + custom dest dir
grails-forge/gradle/doc-config.gradle Forge (Groovy 3.0.25) Plugin apply (javaVersionEnabled = false) + deps
grails-data-hibernate5/docs/build.gradle Hibernate5 docs Plugin apply + deps + custom source collection
grails-data-mongodb/docs/build.gradle MongoDB docs Plugin apply + deps + custom source collection
grails-doc/build.gradle Aggregate groovydoc Simplified - footer from plugin
grails-data-docs/stage/build.gradle Data mapping aggregate Simplified - footer from plugin

Technical details

  • A resolveGroovydocProperty() helper handles the mix of plain values and Property<T> wrappers in Gradle Groovydoc task API
  • Source directories are passed via ext.groovydocSourceDirs for aggregate tasks, or derived from source sets for per-module tasks
  • The documentation configuration already includes groovy-ant in the central config; hibernate5 and mongodb docs needed it added
  • Disabled groovydoc tasks (test suites) are unaffected - Gradle checks enabled before running any actions
  • Dynamic links (geb-spock, testcontainers, spring-core, spring-boot) use doFirst to populate ext.groovydocLinks, plugin reads them in doLast

Testing

  • Per-module groovydoc: :grails-core:groovydoc, :grails-bootstrap:groovydoc
  • grails-gradle module: :grails-gradle-plugins:groovydoc
  • Aggregate groovydoc: :grails-doc:aggregateGroovydoc (3,929 HTML files generated)
  • Data mapping aggregate: :grails-data-docs-stage:aggregateDataMappingGroovydoc
  • Code style check: ./gradlew codeStyle passes
  • Forge groovydoc (with javaVersionEnabled = false)
  • All 7 groovydoc builds verified after plugin split
  • Matomo confirmed in all 6,295+ HTML files across all builds

Closes #15385

Replace Gradle's built-in Groovydoc task execution with AntBuilder to
support the javaVersion parameter introduced in Groovy 4.0.27
(GROOVY-11668). This is needed because Gradle's Groovydoc task does not
expose javaVersion (gradle/gradle#33659 is not merged), causing Java 17+
source parsing failures.

Changes across all groovydoc configurations:
- gradle/docs-dependencies.gradle: central config for ~90 modules and
  both aggregate tasks (aggregateGroovydoc, aggregateDataMappingGroovydoc)
- gradle/docs-config.gradle: per-module source directory setup
- grails-doc/build.gradle: aggregate task source directories
- grails-data-docs/stage/build.gradle: data mapping aggregate source dirs
- grails-gradle/gradle/docs-config.gradle: independent AntBuilder setup
- grails-data-hibernate5/docs/build.gradle: added groovy-ant dependency
  and AntBuilder execution
- grails-data-mongodb/docs/build.gradle: added groovy-ant dependency
  and AntBuilder execution
- grails-forge/gradle/doc-config.gradle: AntBuilder without javaVersion
  (forge uses Groovy 3.0.25 which predates the feature)

Closes #15385

Assisted-by: Claude Code <[email protected]>
…ion plugin

Move duplicated AntBuilder groovydoc execution, Matomo footer, documentation
configuration registration, and task defaults into a shared convention plugin
in build-logic. This eliminates ~490 lines of duplicated configuration across
8 build scripts while maintaining identical behavior.

The plugin provides:
- Documentation configuration registration with standard attributes
- Common Groovydoc task defaults (author, timestamps, scripts)
- AntBuilder-based execution with javaVersion support (Groovy 4.0.27+)
- Matomo analytics footer
- Source directory resolution from ext.groovydocSourceDirs or source sets
- External documentation link support via ext.groovydocLinks
- GrailsGroovydocExtension for per-project javaVersion control

Build scripts retain project-specific configuration: dependencies, titles,
source directories for aggregate tasks, and dynamic link resolution.

Assisted-by: Claude Code <[email protected]>
@jamesfredley jamesfredley marked this pull request as ready for review February 20, 2026 01:50
Copilot AI review requested due to automatic review settings February 20, 2026 01:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR centralizes Groovydoc configuration and execution by introducing a build-logic convention plugin that runs Groovydoc via AntBuilder (to support Groovy’s javaVersion option), and updates multiple modules to use the new plugin while removing duplicated Groovydoc setup.

Changes:

  • Added org.apache.grails.buildsrc.groovydoc convention plugin (GrailsGroovydocPlugin + GrailsGroovydocExtension) to run Groovydoc through org.codehaus.groovy.ant.Groovydoc, with optional javaVersion support.
  • Updated shared/per-module docs Gradle scripts to apply the plugin and centralize common defaults (e.g., Matomo footer, shared dependency setup).
  • Updated aggregate Groovydoc tasks to pass source directories via ext.groovydocSourceDirs for the new Ant-driven execution.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
build-logic/plugins/build.gradle Registers the new org.apache.grails.buildsrc.groovydoc convention plugin.
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy Implements AntBuilder-based Groovydoc execution and shared defaults (footer, configuration).
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocExtension.groovy Adds extension properties to control javaVersion and the javaVersion-passing toggle.
gradle/docs-dependencies.gradle Applies the convention plugin and reworks Groovydoc link population via ext.groovydocLinks.
grails-gradle/gradle/docs-config.gradle Applies the convention plugin and removes duplicated Groovydoc task configuration.
grails-forge/gradle/doc-config.gradle Applies the convention plugin; disables javaVersion passing for Groovy 3.x and adds Ant Groovydoc deps.
grails-doc/build.gradle Simplifies aggregate Groovydoc setup and provides ext.groovydocSourceDirs for Ant execution.
grails-data-docs/stage/build.gradle Updates aggregate Groovydoc source handling and provides ext.groovydocSourceDirs.
grails-data-hibernate5/docs/build.gradle Applies the convention plugin, adds required Ant Groovydoc deps, and supplies ext.groovydocSourceDirs.
grails-data-mongodb/docs/build.gradle Applies the convention plugin, adds required Ant Groovydoc deps, and supplies ext.groovydocSourceDirs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 126 to 130
Map<String, Object> antArgs = [
destdir: destDir.absolutePath,
sourcepath: sourcepath,
packagenames: '**.*',
windowtitle: gdoc.windowTitle ?: '',
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The Ant groovydoc call doesn't pass gdoc.classpath/gdoc.groovyClasspath (the documentation configuration is only used for taskdef). As a result, any Groovydoc classpath configured in build scripts is ignored during generation and can break type resolution. Pass the configured task classpath into the Ant task (e.g., classpath attribute or nested classpath).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not correct. The Ant groovydoc task does not have a separate classpath attribute - the taskdef classloader (loaded with the documentation configuration) IS the type resolution classpath. Groovydoc parses sources syntactically rather than compiling them, so the taskdef classpath provides everything needed for type resolution.

The documentation configuration includes groovy-all, groovy-ant, groovy-templates, and any project-specific runtime dependencies. This is verified working - all 6,295+ HTML files generate correctly across all 7 groovydoc builds.

Comment on lines 105 to 109
List<File> sourceDirs = resolveSourceDirectories(gdoc, project)
if (sourceDirs.isEmpty()) {
project.logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found")
return
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Groovydoc source filtering from the Gradle task (e.g., gdoc.source, gdoc.includes/gdoc.excludes) isn’t honored here because the Ant task is driven only by sourcepath + packagenames. This makes existing exclusions in build scripts ineffective and can change which classes end up in the published API docs. Consider deriving the Ant inputs from gdoc.source (or translating excludes/includes into Ant filesets/excludepackagenames) so task configuration still applies.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged, though this is not a practical issue. The Ant groovydoc task uses sourcepath + packagenames by design - it does not support nested fileset elements.

The excludes configured on the Gradle task (META-INF/**, *.yml, *.properties, *.xml, Application.groovy, Bootstrap.groovy, resources.groovy) are either non-code files that groovydoc naturally ignores (it only processes .groovy and .java files) or convention files that do not exist in framework source directories. The generated output is identical to what the previous per-module AntBuilder configurations produced.

Comment on lines 52 to 55
def gebVersion = resolveProjectVersion('geb-spock')
if(gebVersion) {
gdoc.link("https://groovy.apache.org/geb/manual/${gebVersion}/api/", 'geb.')
if (gebVersion) {
links << [packages: 'geb.', href: "https://groovy.apache.org/geb/manual/${gebVersion}/api/"]
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

resolveProjectVersion() (defined above) never returns the resolved version when it is present, so gebVersion/testContainersVersion/etc will always be null here and no external groovydoc links will be added. Ensure resolveProjectVersion() returns the resolved version value.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch - this is correct. This was a pre-existing bug on 7.0.x (not introduced by this PR). The function had an early return null for the not-found case but was missing the implicit return of version as the last expression. In Groovy, the if statement evaluates to null when the condition is false and there is no else, so the function always returned null.

Fixed in bf36531 by adding version as the last expression in the function. External groovydoc links (geb, testcontainers, spring, spring-boot) should now be included correctly.

import org.gradle.api.tasks.javadoc.Groovydoc

@CompileStatic
class GrailsGroovydocPlugin implements Plugin<Project> {
Copy link
Contributor

Choose a reason for hiding this comment

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

What are your thoughts about keeping this plugin generic? i.e. do not put specific grails-core configuration in it and instead configure it like we configured groovydoc before? That way if gradle merges the upstream change, we don't have to separate out all of the configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bf36531. The plugin is now split into two layers:

  1. GroovydocEnhancerPlugin (org.apache.grails.buildsrc.groovydoc-enhancer) - Generic base plugin with all the AntBuilder logic, source resolution, link support, and documentation configuration. No Grails-specific code. Publishable and reusable by anyone.

  2. GrailsGroovydocPlugin (org.apache.grails.buildsrc.groovydoc) - Thin layer that applies the base plugin and sets footer to the Matomo analytics snippet. This is the only Grails-specific piece.

The base extension also includes a useAntBuilder flag (default true). When Gradle merges their javaVersion support (gradle/gradle#33659), setting groovydocEnhancer { useAntBuilder = false } will skip the AntBuilder action replacement and let Gradle's built-in task run - no code changes needed to switch back.

Separate GrailsGroovydocPlugin into a generic GroovydocEnhancerPlugin
(with GroovydocEnhancerExtension) and a thin GrailsGroovydocPlugin that
applies the base plugin and sets the Matomo footer. This makes the core
AntBuilder groovydoc logic publishable and reusable by anyone, while
keeping Grails-specific customizations in their own layer.

The base plugin supports a useAntBuilder flag so projects can easily
switch back to Gradle's built-in Groovydoc task if Gradle merges their
javaVersion support (gradle/gradle#33659).

Also fix a pre-existing bug in resolveProjectVersion() in
docs-dependencies.gradle where the function never returned the resolved
version string, causing all external groovydoc links (geb,
testcontainers, spring, spring-boot) to be silently omitted.

Assisted-by: Claude Code <[email protected]>
Copy link
Contributor

@matrei matrei left a comment

Choose a reason for hiding this comment

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

Excellent!

Copy link
Contributor

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

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

Have you checked the build scans to ensure no new warnings have appeared with adding this?

@Inject
GroovydocEnhancerExtension(ObjectFactory objects, Project project) {
javaVersion = objects.property(String).convention(
"JAVA_${GradleUtils.findProperty(project, 'javaVersion') ?: '17'}" as String
Copy link
Contributor

Choose a reason for hiding this comment

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

javaVersion should always be set or it should error?

Copy link
Contributor Author

@jamesfredley jamesfredley Feb 20, 2026

Choose a reason for hiding this comment

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

I believe it will be null, unless you set it in the end project and we are defaulting to 17 here given that is the base for Grails 7.

Copy link
Contributor

Choose a reason for hiding this comment

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

Use a provider and it will be set correctly. project.provider { /* your existing JAVA_ code */ }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 3702656 - wrapped the convention in project.provider { ... } so javaVersion resolves lazily at execution time.

@jamesfredley
Copy link
Contributor Author

@jdaugherty Deprecations look the same

Before: https://develocity.apache.org/s/ocm5dcxmesrps/deprecations (this is run later, but was off 7.0.x)
After: https://develocity.apache.org/s/ovm3zrom23m7o/deprecations

Wrap the javaVersion convention in project.provider so the property
is resolved at execution time rather than configuration time.

Assisted-by: Claude Code <[email protected]>
GroovydocEnhancerExtension(ObjectFactory objects, Project project) {
javaVersion = objects.property(String).convention(
"JAVA_${GradleUtils.findProperty(project, 'javaVersion') ?: '17'}" as String
project.provider { "JAVA_${GradleUtils.findProperty(project, 'javaVersion') ?: '17'}" as String }
Copy link
Contributor

Choose a reason for hiding this comment

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

You shouldn't need the default of 17 now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The end grails project will not have javaVersion set in the project, unless they are attempting to overriode and change the version lower or higher for groovydoc support. 17 is here as the automatic default.

Copy link
Contributor

Choose a reason for hiding this comment

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

The end grails app? This is for our applications and we define javaVersion in the grade.properties - which is copied to every project in this build. This comment doesn't make sense to me. If it's not copied, we have a bug that's why we should leave it with no default.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

grails-core does, but if we were to publish this and another project used it ... was what I was thinking about. IE the end Grails App.

Copy link
Contributor

Choose a reason for hiding this comment

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

There are so many other changes that have to be made to publish this. Lets do whats best for grails core and address that when/if we decide to publish?


gdoc.actions.clear()
gdoc.doLast {
def destDir = gdoc.destinationDir.tap { it.mkdirs() }
Copy link
Contributor

Choose a reason for hiding this comment

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

Small suggestion here, since destinationDir is deprecated in Gradle 8 and getting removed in Gradle 9, should we maybe use destinationDirectory.get().asFile instead? Just thought it might help avoid some deprecation warnings down the line! Let me know if that makes sense.

Comment on lines +100 to +113
def docConfig = project.configurations.findByName('documentation')
if (!docConfig) {
project.logger.warn(
'Skipping groovydoc for {}: \'documentation\' configuration not found',
gdoc.name
)
return
}

project.ant.taskdef(
name: 'groovydoc',
classname: 'org.codehaus.groovy.ant.Groovydoc',
classpath: docConfig.asPath
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just noticed this while reading through, could we use gdoc.groovyClasspath here instead of looking up the configuration manually?
Since a task's groovyClasspath is already tracked as an input by Gradle, using it directly might be a little safer for the build cache.
Maybe something like this?

def classpath = gdoc.groovyClasspath
if (!classpath || classpath.empty) {
    project.logger.warn('Skipping groovydoc for {}: groovyClasspath is empty', gdoc.name)
    return
}

project.ant.taskdef(
        name: 'groovydoc',
        classname: 'org.codehaus.groovy.ant.Groovydoc',
        classpath: classpath.asPath
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Adopt newer groovydoc options

4 participants