From 3467bad8dbaa0da1e973e6a4cdb1bc1f07f1fc43 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 19 Feb 2026 20:00:48 -0500 Subject: [PATCH 1/6] Adopt AntBuilder groovydoc with javaVersion support 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 --- gradle/docs-config.gradle | 9 +++ gradle/docs-dependencies.gradle | 97 +++++++++++++++++++++--- grails-data-docs/stage/build.gradle | 10 ++- grails-data-hibernate5/docs/build.gradle | 62 +++++++++++++++ grails-data-mongodb/docs/build.gradle | 61 +++++++++++++++ grails-doc/build.gradle | 4 +- grails-forge/gradle/doc-config.gradle | 82 +++++++++++++++++--- grails-gradle/gradle/docs-config.gradle | 54 +++++++++++++ 8 files changed, 351 insertions(+), 28 deletions(-) diff --git a/gradle/docs-config.gradle b/gradle/docs-config.gradle index 5b5c7d17947..76a14b619bf 100644 --- a/gradle/docs-config.gradle +++ b/gradle/docs-config.gradle @@ -21,4 +21,13 @@ apply from: rootProject.layout.projectDirectory.file('gradle/docs-dependencies.g ext { includeInApiDocs = true +} + +// Set source directories for AntBuilder groovydoc execution (per-module tasks) +tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> + SourceSetContainer sourceSets = project.extensions.getByType(SourceSetContainer) + SourceSet mainSS = sourceSets.findByName('main') + if (mainSS) { + gdoc.ext.groovydocSourceDirs = (mainSS.groovy.srcDirs + mainSS.java.srcDirs).findAll { it.exists() } + } } \ No newline at end of file diff --git a/gradle/docs-dependencies.gradle b/gradle/docs-dependencies.gradle index df4244f39c1..23b75693b99 100644 --- a/gradle/docs-dependencies.gradle +++ b/gradle/docs-dependencies.gradle @@ -50,6 +50,21 @@ String resolveProjectVersion(String artifact) { } } +// Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. +// Gradle 8.x Groovydoc uses a mix of both types depending on the property. +Object resolveGroovydocProperty(Object value) { + if (value instanceof org.gradle.api.provider.Provider) { + return value.getOrNull() + } + return value +} + +// Compute the javaVersion string for groovydoc's JavaParser language level. +// Requires Groovy 4.0.27+ (GROOVY-11668). Gradle's built-in Groovydoc task does not +// expose this parameter (https://github.com/gradle/gradle/issues/33659), so we replace +// the task execution with AntBuilder to pass it directly to the Groovy Ant task. +String groovydocJavaVersion = "JAVA_${project.findProperty('javaVersion') ?: '17'}" + tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> gdoc.exclude('META-INF/**', '*yml', '*properties', '*xml', '**/Application.groovy', '**/Bootstrap.groovy', '**/resources.groovy') gdoc.groovyClasspath = configurations.documentation @@ -79,25 +94,83 @@ tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> ''' - doFirst { - def gebVersion = resolveProjectVersion('geb-spock') - if(gebVersion) { - gdoc.link("https://groovy.apache.org/geb/manual/${gebVersion}/api/", 'geb.') + // Replace Gradle's built-in Groovydoc execution with AntBuilder to support javaVersion. + // See: https://github.com/apache/grails-core/issues/15385 + gdoc.actions.clear() + gdoc.doLast { + def destDir = gdoc.destinationDir + destDir.mkdirs() + + // Source directories: set by modules via ext.groovydocSourceDirs, or derived from source sets + List sourceDirs + if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { + sourceDirs = (gdoc.ext.groovydocSourceDirs as List).findAll { it.exists() } + } else { + sourceDirs = [] + def sourceSetsExt = project.extensions.findByType(SourceSetContainer) + if (sourceSetsExt) { + def mainSS = sourceSetsExt.findByName('main') + if (mainSS) { + sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) + sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) + } + } } + sourceDirs = sourceDirs.unique() - def testContainersVersion = resolveProjectVersion('testcontainers') - if(testContainersVersion) { - gdoc.link("https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/", 'org.testcontainers.') + if (sourceDirs.isEmpty()) { + logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") + return } + ant.taskdef( + name: 'groovydoc', + classname: 'org.codehaus.groovy.ant.Groovydoc', + classpath: configurations.documentation.asPath + ) + + // Resolve dynamic links from project dependencies + List> groovydocLinks = [] + def gebVersion = resolveProjectVersion('geb-spock') + if (gebVersion) { + groovydocLinks << [packages: 'geb.', href: "https://groovy.apache.org/geb/manual/${gebVersion}/api/"] + } + def testContainersVersion = resolveProjectVersion('testcontainers') + if (testContainersVersion) { + groovydocLinks << [packages: 'org.testcontainers.', href: "https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/"] + } def springVersion = resolveProjectVersion('spring-core') - if(springVersion) { - gdoc.link("https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/", 'org.springframework.core.') + if (springVersion) { + groovydocLinks << [packages: 'org.springframework.core.', href: "https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"] } - def springBootVersion = resolveProjectVersion('spring-boot') - if(springBootVersion) { - gdoc.link("https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/", 'org.springframework.boot.') + if (springBootVersion) { + groovydocLinks << [packages: 'org.springframework.boot.', href: "https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/"] + } + if (gdoc.ext.has('groovydocLinks')) { + groovydocLinks.addAll(gdoc.ext.groovydocLinks as List>) + } + + def sourcepath = sourceDirs.collect { it.absolutePath }.join(File.pathSeparator) + + ant.groovydoc( + destdir: destDir.absolutePath, + sourcepath: sourcepath, + packagenames: '**.*', + windowtitle: gdoc.windowTitle ?: '', + doctitle: gdoc.docTitle ?: '', + footer: gdoc.footer ?: '', + access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', + author: resolveGroovydocProperty(gdoc.includeAuthor) as String, + noTimestamp: resolveGroovydocProperty(gdoc.noTimestamp) as String, + noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, + processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, + includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String, + javaVersion: groovydocJavaVersion + ) { + for (Map l in groovydocLinks) { + link(packages: l.packages, href: l.href) + } } } } \ No newline at end of file diff --git a/grails-data-docs/stage/build.gradle b/grails-data-docs/stage/build.gradle index b5d71cc03ad..4d13bb6b01d 100644 --- a/grails-data-docs/stage/build.gradle +++ b/grails-data-docs/stage/build.gradle @@ -77,14 +77,16 @@ combinedGroovydoc.configure { Groovydoc task -> } .flatten() - task.source(sources.collect{ SourceSet it -> [it.allSource.srcDirs, it.allSource.srcDirs] }.flatten().findAll { File srcDir -> - if(!(srcDir.name in ['java', 'groovy'])) { + def allSourceDirs = sources.collect { SourceSet it -> [it.allSource.srcDirs, it.allSource.srcDirs] }.flatten().findAll { File srcDir -> + if (!(srcDir.name in ['java', 'groovy'])) { return false } srcDir.exists() - }.unique()) - task.classpath = files(sources.collect{ SourceSet it -> it.compileClasspath.filter(File.&isDirectory) }.flatten().unique()) + }.unique() + task.source(allSourceDirs) + task.ext.groovydocSourceDirs = allSourceDirs + task.classpath = files(sources.collect { SourceSet it -> it.compileClasspath.filter(File.&isDirectory) }.flatten().unique()) task.destinationDir = project.layout.buildDirectory.dir('data-api/api').get().asFile task.inputs.files(task.source).withPropertyName("groovyDocSrc").withPathSensitivity(PathSensitivity.RELATIVE) diff --git a/grails-data-hibernate5/docs/build.gradle b/grails-data-hibernate5/docs/build.gradle index aed57721871..565da447cba 100644 --- a/grails-data-hibernate5/docs/build.gradle +++ b/grails-data-hibernate5/docs/build.gradle @@ -44,7 +44,11 @@ dependencies { documentation 'com.github.javaparser:javaparser-core' documentation "info.picocli:picocli:$picocliVersion" documentation 'org.apache.groovy:groovy-dateutil' + documentation 'org.apache.groovy:groovy-ant' + documentation 'org.apache.groovy:groovy-groovydoc' + documentation 'org.apache.groovy:groovy-templates' documentation 'org.fusesource.jansi:jansi' + documentation 'jline:jline' documentation project(':grails-bootstrap') documentation project(':grails-core') documentation project(':grails-spring') @@ -90,6 +94,18 @@ tasks.named('asciidoctor', AsciidoctorTask) { AsciidoctorTask it -> ] } +// Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. +Object resolveGroovydocProperty(Object value) { + if (value instanceof org.gradle.api.provider.Provider) { + return value.getOrNull() + } + return value +} + +// Compute the javaVersion string for groovydoc's JavaParser language level. +// Requires Groovy 4.0.27+ (GROOVY-11668). +String groovydocJavaVersion = "JAVA_${project.findProperty('javaVersion') ?: '17'}" + tasks.withType(Groovydoc).configureEach { it.dependsOn(rootProject.subprojects .findAll { it.findProperty('gormApiDocs') } @@ -131,6 +147,52 @@ tasks.withType(Groovydoc).configureEach { it.classpath = configurations.documentation it.groovyClasspath += configurations.documentation it.noVersionStamp = false + + // Collect source directories for AntBuilder groovydoc + List groovydocSrcDirs = coreProjects.collect { + rootProject.layout.projectDirectory.dir("$it/src/main/groovy").asFile + } + rootProject.subprojects + .findAll { sp -> sp.findProperty('gormApiDocs') } + .each { sp -> groovydocSrcDirs << new File(sp.projectDir, 'src/main/groovy') } + it.ext.groovydocSourceDirs = groovydocSrcDirs + + // Replace Gradle's built-in Groovydoc execution with AntBuilder to support javaVersion. + // See: https://github.com/apache/grails-core/issues/15385 + it.actions.clear() + it.doLast { Groovydoc gdoc -> + def destDir = gdoc.destinationDir + destDir.mkdirs() + + List sourceDirs = (gdoc.ext.groovydocSourceDirs as List).findAll { File f -> f.exists() }.unique() + if (sourceDirs.isEmpty()) { + logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") + return + } + + ant.taskdef( + name: 'groovydoc', + classname: 'org.codehaus.groovy.ant.Groovydoc', + classpath: configurations.documentation.asPath + ) + + def sourcepath = sourceDirs.collect { File f -> f.absolutePath }.join(File.pathSeparator) + + ant.groovydoc( + destdir: destDir.absolutePath, + sourcepath: sourcepath, + packagenames: '**.*', + doctitle: gdoc.docTitle ?: '', + footer: gdoc.footer ?: '', + access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', + author: resolveGroovydocProperty(gdoc.includeAuthor) as String, + noTimestamp: 'true', + noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, + processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, + includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String, + javaVersion: groovydocJavaVersion + ) + } } tasks.register('docs', Sync).configure { Sync docTask -> diff --git a/grails-data-mongodb/docs/build.gradle b/grails-data-mongodb/docs/build.gradle index 89cfce45305..81a0fca9987 100644 --- a/grails-data-mongodb/docs/build.gradle +++ b/grails-data-mongodb/docs/build.gradle @@ -56,7 +56,10 @@ tasks.register('resolveMongodbVersion').configure { Task docTask -> dependencies { documentation platform(project(':grails-bom')) documentation 'org.fusesource.jansi:jansi' + documentation 'jline:jline' documentation 'org.apache.groovy:groovy' + documentation 'org.apache.groovy:groovy-ant' + documentation 'org.apache.groovy:groovy-groovydoc' documentation 'org.apache.groovy:groovy-templates' documentation 'org.apache.groovy:groovy-dateutil' documentation 'com.github.javaparser:javaparser-core' @@ -100,6 +103,18 @@ tasks.named('asciidoctor', AsciidoctorTask) { AsciidoctorTask it -> ] } +// Compute the javaVersion string for groovydoc's JavaParser language level. +// Requires Groovy 4.0.27+ (GROOVY-11668). +String groovydocJavaVersion = "JAVA_${project.findProperty('javaVersion') ?: '17'}" + +// Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. +Object resolveGroovydocProperty(Object value) { + if (value instanceof org.gradle.api.provider.Provider) { + return value.getOrNull() + } + return value +} + tasks.withType(Groovydoc).configureEach { Groovydoc groovydoc -> groovydoc.dependsOn(rootProject.subprojects .findAll { it.findProperty('gormApiDocs') } @@ -140,6 +155,52 @@ tasks.withType(Groovydoc).configureEach { Groovydoc groovydoc -> groovydoc.classpath = configurations.documentation groovydoc.groovyClasspath += configurations.documentation groovydoc.noVersionStamp = false + + // Collect source directories for AntBuilder groovydoc + List groovydocSrcDirs = coreProjects.collect { + layout.projectDirectory.dir("$it/src/main/groovy").asFile + } + rootProject.subprojects + .findAll { sp -> sp.findProperty('gormApiDocs') } + .each { sp -> groovydocSrcDirs << new File(sp.projectDir, 'src/main/groovy') } + groovydoc.ext.groovydocSourceDirs = groovydocSrcDirs + + // Replace Gradle's built-in Groovydoc execution with AntBuilder to support javaVersion. + // See: https://github.com/apache/grails-core/issues/15385 + groovydoc.actions.clear() + groovydoc.doLast { Groovydoc gdoc -> + def destDir = gdoc.destinationDir + destDir.mkdirs() + + List sourceDirs = (gdoc.ext.groovydocSourceDirs as List).findAll { File f -> f.exists() }.unique() + if (sourceDirs.isEmpty()) { + logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") + return + } + + ant.taskdef( + name: 'groovydoc', + classname: 'org.codehaus.groovy.ant.Groovydoc', + classpath: configurations.documentation.asPath + ) + + def sourcepath = sourceDirs.collect { File f -> f.absolutePath }.join(File.pathSeparator) + + ant.groovydoc( + destdir: destDir.absolutePath, + sourcepath: sourcepath, + packagenames: '**.*', + doctitle: gdoc.docTitle ?: '', + footer: gdoc.footer ?: '', + access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', + author: resolveGroovydocProperty(gdoc.includeAuthor) as String, + noTimestamp: 'true', + noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, + processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, + includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String, + javaVersion: groovydocJavaVersion + ) + } } tasks.register('docs', Sync).configure { Sync docTask -> diff --git a/grails-doc/build.gradle b/grails-doc/build.gradle index 1802c28395b..63382c712e1 100644 --- a/grails-doc/build.gradle +++ b/grails-doc/build.gradle @@ -127,7 +127,9 @@ combinedGroovydoc.configure { Groovydoc gdoc -> sourceDirs } - gdoc.source(project.files((baseSourceDirs + includedBuildSourceDirs).unique())) + def allSourceDirs = (baseSourceDirs + includedBuildSourceDirs).unique() + gdoc.source(project.files(allSourceDirs)) + gdoc.ext.groovydocSourceDirs = allSourceDirs gdoc.classpath = files(sources.collect { SourceSet it -> it.compileClasspath.filter(File.&isDirectory) }.flatten().unique()) gdoc.destinationDir = project.layout.buildDirectory.dir('combined-api/api').get().asFile diff --git a/grails-forge/gradle/doc-config.gradle b/grails-forge/gradle/doc-config.gradle index d7b0eaf7fba..38b8e2a60d1 100644 --- a/grails-forge/gradle/doc-config.gradle +++ b/grails-forge/gradle/doc-config.gradle @@ -30,13 +30,15 @@ configurations.register('documentation') { dependencies { documentation "org.codehaus.groovy:groovy-templates:$groovyVersion" documentation "org.codehaus.groovy:groovy-dateutil:$groovyVersion" + documentation "org.codehaus.groovy:groovy-ant:$groovyVersion" + documentation "org.codehaus.groovy:groovy-groovydoc:$groovyVersion" } -tasks.withType(Groovydoc).configureEach { - classpath += project.configurations.documentation - windowTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" - docTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" - footer = ''' +tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> + gdoc.classpath += project.configurations.documentation + gdoc.windowTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" + gdoc.docTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" + gdoc.footer = ''' ''' - access = GroovydocAccess.PROTECTED - includeAuthor = false - includeMainForScripts = false - processScripts = false - noTimestamp = true - noVersionStamp = false + gdoc.access = GroovydocAccess.PROTECTED + gdoc.includeAuthor = false + gdoc.includeMainForScripts = false + gdoc.processScripts = false + gdoc.noTimestamp = true + gdoc.noVersionStamp = false + + // Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. + Closure resolveGroovydocProperty = { Object value -> + if (value instanceof org.gradle.api.provider.Provider) { + return value.getOrNull() + } + return value + } + + // Replace Gradle's built-in Groovydoc execution with AntBuilder for consistency + // with the main build. The javaVersion parameter is not available in Groovy 3.x + // (requires Groovy 4.0.27+, GROOVY-11668) - it will be added when forge upgrades. + // See: https://github.com/apache/grails-core/issues/15385 + gdoc.actions.clear() + gdoc.doLast { + def destDir = gdoc.destinationDir + destDir.mkdirs() + + List sourceDirs = [] + def sourceSetsExt = project.extensions.findByType(SourceSetContainer) + if (sourceSetsExt) { + def mainSS = sourceSetsExt.findByName('main') + if (mainSS) { + sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) + sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) + } + } + sourceDirs = sourceDirs.unique() + + if (sourceDirs.isEmpty()) { + logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") + return + } + + ant.taskdef( + name: 'groovydoc', + classname: 'org.codehaus.groovy.ant.Groovydoc', + classpath: configurations.documentation.asPath + ) + + def sourcepath = sourceDirs.collect { File f -> f.absolutePath }.join(File.pathSeparator) + + // Note: javaVersion omitted - not supported in Groovy 3.x (forge uses Groovy $groovyVersion) + ant.groovydoc( + destdir: destDir.absolutePath, + sourcepath: sourcepath, + packagenames: '**.*', + windowtitle: gdoc.windowTitle ?: '', + doctitle: gdoc.docTitle ?: '', + footer: gdoc.footer ?: '', + access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', + author: resolveGroovydocProperty(gdoc.includeAuthor) as String, + noTimestamp: resolveGroovydocProperty(gdoc.noTimestamp) as String, + noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, + processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, + includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String + ) + } } diff --git a/grails-gradle/gradle/docs-config.gradle b/grails-gradle/gradle/docs-config.gradle index be2aaf4f1c9..e3459998b6f 100644 --- a/grails-gradle/gradle/docs-config.gradle +++ b/grails-gradle/gradle/docs-config.gradle @@ -42,6 +42,18 @@ ext { includeInApiDocs = true } +// Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. +Object resolveGroovydocProperty(Object value) { + if (value instanceof org.gradle.api.provider.Provider) { + return value.getOrNull() + } + return value +} + +// Compute the javaVersion string for groovydoc's JavaParser language level. +// Requires Groovy 4.0.27+ (GROOVY-11668). +String groovydocJavaVersion = "JAVA_${project.findProperty('javaVersion') ?: '17'}" + TaskProvider groovydocTask = tasks.named('groovydoc', Groovydoc) groovydocTask.configure { Groovydoc it -> it.classpath = configurations.documentation @@ -70,6 +82,48 @@ groovydocTask.configure { Groovydoc it -> ''' it.destinationDir = project.file('build/docs/api') + + // Replace Gradle's built-in Groovydoc execution with AntBuilder to support javaVersion. + // See: https://github.com/apache/grails-core/issues/15385 + it.actions.clear() + it.doLast { + def destDir = it.destinationDir + destDir.mkdirs() + + SourceSetContainer sourceSets = project.extensions.getByType(SourceSetContainer) + List sourceDirs = ([] + sourceSets.main.groovy.srcDirs + sourceSets.main.java.srcDirs) + .findAll { File f -> f.exists() } + .unique() + + if (sourceDirs.isEmpty()) { + logger.lifecycle("Skipping groovydoc for ${it.name}: no source directories found") + return + } + + ant.taskdef( + name: 'groovydoc', + classname: 'org.codehaus.groovy.ant.Groovydoc', + classpath: configurations.documentation.asPath + ) + + def sourcepath = sourceDirs.collect { File f -> f.absolutePath }.join(File.pathSeparator) + + ant.groovydoc( + destdir: destDir.absolutePath, + sourcepath: sourcepath, + packagenames: '**.*', + windowtitle: it.windowTitle ?: '', + doctitle: it.docTitle ?: '', + footer: it.footer ?: '', + access: resolveGroovydocProperty(it.access)?.name()?.toLowerCase() ?: 'protected', + author: resolveGroovydocProperty(it.includeAuthor) as String, + noTimestamp: resolveGroovydocProperty(it.noTimestamp) as String, + noVersionStamp: resolveGroovydocProperty(it.noVersionStamp) as String, + processScripts: resolveGroovydocProperty(it.processScripts) as String, + includeMainForScripts: resolveGroovydocProperty(it.includeMainForScripts) as String, + javaVersion: groovydocJavaVersion + ) + } } tasks.named('javadoc').configure { From 3c50821ea7aa98b15561e8e6672a45c91411e8f9 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 19 Feb 2026 20:32:21 -0500 Subject: [PATCH 2/6] Centralize groovydoc configuration into GrailsGroovydocPlugin convention 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 --- build-logic/plugins/build.gradle | 4 + .../buildsrc/GrailsGroovydocExtension.groovy | 67 +++++++ .../buildsrc/GrailsGroovydocPlugin.groovy | 186 ++++++++++++++++++ gradle/docs-config.gradle | 9 - gradle/docs-dependencies.gradle | 121 +----------- grails-data-docs/stage/build.gradle | 17 -- grails-data-hibernate5/docs/build.gradle | 81 +------- grails-data-mongodb/docs/build.gradle | 82 +------- grails-doc/build.gradle | 17 -- grails-forge/gradle/doc-config.gradle | 93 +-------- grails-gradle/gradle/docs-config.gradle | 89 +-------- 11 files changed, 275 insertions(+), 491 deletions(-) create mode 100644 build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocExtension.groovy create mode 100644 build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy diff --git a/build-logic/plugins/build.gradle b/build-logic/plugins/build.gradle index e85254e1528..f750808908a 100644 --- a/build-logic/plugins/build.gradle +++ b/build-logic/plugins/build.gradle @@ -62,6 +62,10 @@ gradlePlugin { id = 'org.apache.grails.gradle.grails-code-style' implementationClass = 'org.apache.grails.buildsrc.GrailsCodeStylePlugin' } + register('grailsGroovydoc') { + id = 'org.apache.grails.buildsrc.groovydoc' + implementationClass = 'org.apache.grails.buildsrc.GrailsGroovydocPlugin' + } register('grailsRepoSettings') { id = 'org.apache.grails.buildsrc.repo' implementationClass = 'org.apache.grails.buildsrc.GrailsRepoSettingsPlugin' diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocExtension.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocExtension.groovy new file mode 100644 index 00000000000..98cefd434c0 --- /dev/null +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocExtension.groovy @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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 org.apache.grails.buildsrc + +import javax.inject.Inject + +import groovy.transform.CompileStatic + +import org.gradle.api.Project +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +/** + * Extension for configuring the Grails Groovydoc convention plugin. + * + *

Allows per-project control over the {@code javaVersion} parameter + * passed to the Groovy Ant groovydoc task. The {@code javaVersion} + * parameter was added in Groovy 4.0.27 (GROOVY-11668) and controls + * the JavaParser language level used when parsing Java sources.

+ * + * @since 7.0.8 + */ +@CompileStatic +class GrailsGroovydocExtension { + + /** + * The Java language level string passed to the groovydoc Ant task's + * {@code javaVersion} parameter (e.g. {@code "JAVA_17"}, {@code "JAVA_21"}). + * + *

Defaults to {@code "JAVA_${javaVersion}"} where {@code javaVersion} + * is read from the project property, falling back to {@code "JAVA_17"}.

+ */ + final Property javaVersion + + /** + * Whether to pass the {@code javaVersion} parameter to the groovydoc + * Ant task. Set to {@code false} for projects using Groovy versions + * older than 4.0.27 (which do not support the parameter). + * + *

Defaults to {@code true}.

+ */ + final Property javaVersionEnabled + + @Inject + GrailsGroovydocExtension(ObjectFactory objects, Project project) { + javaVersion = objects.property(String).convention( + "JAVA_${GradleUtils.findProperty(project, 'javaVersion') ?: '17'}" as String + ) + javaVersionEnabled = objects.property(Boolean).convention(true) + } +} diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy new file mode 100644 index 00000000000..d5e5875af2c --- /dev/null +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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 org.apache.grails.buildsrc + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.Usage +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.javadoc.Groovydoc + +@CompileStatic +class GrailsGroovydocPlugin implements Plugin { + + static final String MATOMO_FOOTER = '''\ + + +''' + + @Override + void apply(Project project) { + GrailsGroovydocExtension extension = project.extensions.create( + 'grailsGroovydoc', GrailsGroovydocExtension, project + ) + registerDocumentationConfiguration(project) + configureGroovydocDefaults(project) + configureAntBuilderExecution(project, extension) + } + + private static void registerDocumentationConfiguration(Project project) { + if (project.configurations.names.contains('documentation')) { + return + } + project.configurations.register('documentation') { Configuration config -> + config.canBeConsumed = false + config.canBeResolved = true + config.attributes { container -> + container.attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category, Category.LIBRARY)) + container.attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling, Bundling.EXTERNAL)) + container.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage, Usage.JAVA_RUNTIME)) + } + } + } + + @CompileDynamic + private static void configureGroovydocDefaults(Project project) { + project.tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> + gdoc.includeAuthor = false + gdoc.includeMainForScripts = false + gdoc.processScripts = false + gdoc.noTimestamp = true + gdoc.noVersionStamp = false + gdoc.footer = MATOMO_FOOTER + if (project.configurations.names.contains('documentation')) { + gdoc.groovyClasspath = project.configurations.getByName('documentation') + } + } + } + + @CompileDynamic + private static void configureAntBuilderExecution(Project project, GrailsGroovydocExtension extension) { + project.tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> + gdoc.actions.clear() + gdoc.doLast { + File destDir = gdoc.destinationDir + destDir.mkdirs() + + List sourceDirs = resolveSourceDirectories(gdoc, project) + if (sourceDirs.isEmpty()) { + project.logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") + return + } + + Configuration docConfig = project.configurations.findByName('documentation') + if (!docConfig) { + project.logger.warn("Skipping groovydoc for ${gdoc.name}: 'documentation' configuration not found") + return + } + + project.ant.taskdef( + name: 'groovydoc', + classname: 'org.codehaus.groovy.ant.Groovydoc', + classpath: docConfig.asPath + ) + + List> links = resolveLinks(gdoc) + String sourcepath = sourceDirs.collect { it.absolutePath }.join(File.pathSeparator) + + Map antArgs = [ + destdir: destDir.absolutePath, + sourcepath: sourcepath, + packagenames: '**.*', + windowtitle: gdoc.windowTitle ?: '', + doctitle: gdoc.docTitle ?: '', + footer: gdoc.footer ?: '', + access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', + author: resolveGroovydocProperty(gdoc.includeAuthor) as String, + noTimestamp: resolveGroovydocProperty(gdoc.noTimestamp) as String, + noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, + processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, + includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String + ] + + if (extension.javaVersionEnabled.get()) { + antArgs.put('javaVersion', extension.javaVersion.get()) + } + + project.ant.groovydoc(antArgs) { + for (Map l in links) { + link(packages: l.packages, href: l.href) + } + } + } + } + } + + @CompileDynamic + private static List resolveSourceDirectories(Groovydoc gdoc, Project project) { + if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { + return (gdoc.ext.groovydocSourceDirs as List).findAll { it.exists() }.unique() as List + } + + List sourceDirs = [] + SourceSetContainer sourceSets = project.extensions.findByType(SourceSetContainer) + if (sourceSets) { + SourceSet mainSS = sourceSets.findByName('main') + if (mainSS) { + sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) + sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) + } + } + sourceDirs.unique() as List + } + + @CompileDynamic + private static List> resolveLinks(Groovydoc gdoc) { + if (gdoc.ext.has('groovydocLinks')) { + return gdoc.ext.groovydocLinks as List> + } + [] + } + + static Object resolveGroovydocProperty(Object value) { + if (value instanceof Provider) { + return ((Provider) value).getOrNull() + } + value + } +} diff --git a/gradle/docs-config.gradle b/gradle/docs-config.gradle index 76a14b619bf..5b5c7d17947 100644 --- a/gradle/docs-config.gradle +++ b/gradle/docs-config.gradle @@ -21,13 +21,4 @@ apply from: rootProject.layout.projectDirectory.file('gradle/docs-dependencies.g ext { includeInApiDocs = true -} - -// Set source directories for AntBuilder groovydoc execution (per-module tasks) -tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> - SourceSetContainer sourceSets = project.extensions.getByType(SourceSetContainer) - SourceSet mainSS = sourceSets.findByName('main') - if (mainSS) { - gdoc.ext.groovydocSourceDirs = (mainSS.groovy.srcDirs + mainSS.java.srcDirs).findAll { it.exists() } - } } \ No newline at end of file diff --git a/gradle/docs-dependencies.gradle b/gradle/docs-dependencies.gradle index 23b75693b99..afb56aec4a6 100644 --- a/gradle/docs-dependencies.gradle +++ b/gradle/docs-dependencies.gradle @@ -16,15 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -configurations.register('documentation') { - canBeConsumed = false - canBeResolved = true - attributes { - attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY)) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL)) - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME)) - } -} +apply plugin: 'org.apache.grails.buildsrc.groovydoc' dependencies { add('documentation', platform(project(':grails-bom'))) @@ -50,127 +42,32 @@ String resolveProjectVersion(String artifact) { } } -// Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. -// Gradle 8.x Groovydoc uses a mix of both types depending on the property. -Object resolveGroovydocProperty(Object value) { - if (value instanceof org.gradle.api.provider.Provider) { - return value.getOrNull() - } - return value -} - -// Compute the javaVersion string for groovydoc's JavaParser language level. -// Requires Groovy 4.0.27+ (GROOVY-11668). Gradle's built-in Groovydoc task does not -// expose this parameter (https://github.com/gradle/gradle/issues/33659), so we replace -// the task execution with AntBuilder to pass it directly to the Groovy Ant task. -String groovydocJavaVersion = "JAVA_${project.findProperty('javaVersion') ?: '17'}" - tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> gdoc.exclude('META-INF/**', '*yml', '*properties', '*xml', '**/Application.groovy', '**/Bootstrap.groovy', '**/resources.groovy') - gdoc.groovyClasspath = configurations.documentation gdoc.windowTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" gdoc.docTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" - gdoc.access = GroovydocAccess.PROTECTED - gdoc.includeAuthor = false - gdoc.includeMainForScripts = false - gdoc.processScripts = false - gdoc.noTimestamp = true - gdoc.noVersionStamp = false - gdoc.footer = ''' - -''' - // Replace Gradle's built-in Groovydoc execution with AntBuilder to support javaVersion. - // See: https://github.com/apache/grails-core/issues/15385 - gdoc.actions.clear() - gdoc.doLast { - def destDir = gdoc.destinationDir - destDir.mkdirs() - - // Source directories: set by modules via ext.groovydocSourceDirs, or derived from source sets - List sourceDirs - if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { - sourceDirs = (gdoc.ext.groovydocSourceDirs as List).findAll { it.exists() } - } else { - sourceDirs = [] - def sourceSetsExt = project.extensions.findByType(SourceSetContainer) - if (sourceSetsExt) { - def mainSS = sourceSetsExt.findByName('main') - if (mainSS) { - sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) - sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) - } - } - } - sourceDirs = sourceDirs.unique() - - if (sourceDirs.isEmpty()) { - logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") - return - } - - ant.taskdef( - name: 'groovydoc', - classname: 'org.codehaus.groovy.ant.Groovydoc', - classpath: configurations.documentation.asPath - ) - - // Resolve dynamic links from project dependencies - List> groovydocLinks = [] + gdoc.doFirst { + List> links = [] def gebVersion = resolveProjectVersion('geb-spock') if (gebVersion) { - groovydocLinks << [packages: 'geb.', href: "https://groovy.apache.org/geb/manual/${gebVersion}/api/"] + links << [packages: 'geb.', href: "https://groovy.apache.org/geb/manual/${gebVersion}/api/"] } def testContainersVersion = resolveProjectVersion('testcontainers') if (testContainersVersion) { - groovydocLinks << [packages: 'org.testcontainers.', href: "https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/"] + links << [packages: 'org.testcontainers.', href: "https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/"] } def springVersion = resolveProjectVersion('spring-core') if (springVersion) { - groovydocLinks << [packages: 'org.springframework.core.', href: "https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"] + links << [packages: 'org.springframework.core.', href: "https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"] } def springBootVersion = resolveProjectVersion('spring-boot') if (springBootVersion) { - groovydocLinks << [packages: 'org.springframework.boot.', href: "https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/"] + links << [packages: 'org.springframework.boot.', href: "https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/"] } if (gdoc.ext.has('groovydocLinks')) { - groovydocLinks.addAll(gdoc.ext.groovydocLinks as List>) - } - - def sourcepath = sourceDirs.collect { it.absolutePath }.join(File.pathSeparator) - - ant.groovydoc( - destdir: destDir.absolutePath, - sourcepath: sourcepath, - packagenames: '**.*', - windowtitle: gdoc.windowTitle ?: '', - doctitle: gdoc.docTitle ?: '', - footer: gdoc.footer ?: '', - access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', - author: resolveGroovydocProperty(gdoc.includeAuthor) as String, - noTimestamp: resolveGroovydocProperty(gdoc.noTimestamp) as String, - noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, - processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, - includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String, - javaVersion: groovydocJavaVersion - ) { - for (Map l in groovydocLinks) { - link(packages: l.packages, href: l.href) - } + links.addAll(gdoc.ext.groovydocLinks as List>) } + gdoc.ext.groovydocLinks = links } } \ No newline at end of file diff --git a/grails-data-docs/stage/build.gradle b/grails-data-docs/stage/build.gradle index 4d13bb6b01d..5af7d885445 100644 --- a/grails-data-docs/stage/build.gradle +++ b/grails-data-docs/stage/build.gradle @@ -25,23 +25,6 @@ apply from: rootProject.layout.projectDirectory.file('gradle/docs-dependencies.g combinedGroovydoc.configure { Groovydoc task -> task.windowTitle = "Grails Data Mapping API - ${projectVersion}" task.docTitle = "Grails Data Mapping API - ${projectVersion}" - task.footer = ''' - -''' Set docProjects = rootProject.subprojects.findAll { it.name in [ diff --git a/grails-data-hibernate5/docs/build.gradle b/grails-data-hibernate5/docs/build.gradle index 565da447cba..be9c04f90de 100644 --- a/grails-data-hibernate5/docs/build.gradle +++ b/grails-data-hibernate5/docs/build.gradle @@ -31,13 +31,7 @@ ext { coreProjects = ['grails-datastore-core', 'grails-datamapping-core'] } -configurations { - documentation { - attributes { - attribute(Bundling.BUNDLING_ATTRIBUTE, (Bundling) (objects.named(Bundling, 'external'))) - } - } -} +apply plugin: 'org.apache.grails.buildsrc.groovydoc' dependencies { documentation platform(project(':grails-bom')) @@ -94,41 +88,12 @@ tasks.named('asciidoctor', AsciidoctorTask) { AsciidoctorTask it -> ] } -// Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. -Object resolveGroovydocProperty(Object value) { - if (value instanceof org.gradle.api.provider.Provider) { - return value.getOrNull() - } - return value -} - -// Compute the javaVersion string for groovydoc's JavaParser language level. -// Requires Groovy 4.0.27+ (GROOVY-11668). -String groovydocJavaVersion = "JAVA_${project.findProperty('javaVersion') ?: '17'}" - tasks.withType(Groovydoc).configureEach { it.dependsOn(rootProject.subprojects .findAll { it.findProperty('gormApiDocs') } .collect { ":${it.name}:groovydoc" }) it.docTitle = "GORM for Hibernate 5 - $project.version" - it.footer = ''' - -''' def sourceFiles = coreProjects.collect { rootProject.layout.projectDirectory.files("$it/src/main/groovy") @@ -140,15 +105,8 @@ tasks.withType(Groovydoc).configureEach { it.source = sourceFiles it.destinationDir = layout.buildDirectory.dir('combined-api/api').get().asFile - it.access = GroovydocAccess.PROTECTED - it.processScripts = false - it.includeMainForScripts = false - it.includeAuthor = false it.classpath = configurations.documentation - it.groovyClasspath += configurations.documentation - it.noVersionStamp = false - // Collect source directories for AntBuilder groovydoc List groovydocSrcDirs = coreProjects.collect { rootProject.layout.projectDirectory.dir("$it/src/main/groovy").asFile } @@ -156,43 +114,6 @@ tasks.withType(Groovydoc).configureEach { .findAll { sp -> sp.findProperty('gormApiDocs') } .each { sp -> groovydocSrcDirs << new File(sp.projectDir, 'src/main/groovy') } it.ext.groovydocSourceDirs = groovydocSrcDirs - - // Replace Gradle's built-in Groovydoc execution with AntBuilder to support javaVersion. - // See: https://github.com/apache/grails-core/issues/15385 - it.actions.clear() - it.doLast { Groovydoc gdoc -> - def destDir = gdoc.destinationDir - destDir.mkdirs() - - List sourceDirs = (gdoc.ext.groovydocSourceDirs as List).findAll { File f -> f.exists() }.unique() - if (sourceDirs.isEmpty()) { - logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") - return - } - - ant.taskdef( - name: 'groovydoc', - classname: 'org.codehaus.groovy.ant.Groovydoc', - classpath: configurations.documentation.asPath - ) - - def sourcepath = sourceDirs.collect { File f -> f.absolutePath }.join(File.pathSeparator) - - ant.groovydoc( - destdir: destDir.absolutePath, - sourcepath: sourcepath, - packagenames: '**.*', - doctitle: gdoc.docTitle ?: '', - footer: gdoc.footer ?: '', - access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', - author: resolveGroovydocProperty(gdoc.includeAuthor) as String, - noTimestamp: 'true', - noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, - processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, - includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String, - javaVersion: groovydocJavaVersion - ) - } } tasks.register('docs', Sync).configure { Sync docTask -> diff --git a/grails-data-mongodb/docs/build.gradle b/grails-data-mongodb/docs/build.gradle index 81a0fca9987..02de80eb950 100644 --- a/grails-data-mongodb/docs/build.gradle +++ b/grails-data-mongodb/docs/build.gradle @@ -31,13 +31,7 @@ ext { coreProjects = ['grails-datastore-core', 'grails-datamapping-core'] } -configurations { - documentation { - attributes { - attribute(Bundling.BUNDLING_ATTRIBUTE, (Bundling) (objects.named(Bundling, 'external'))) - } - } -} +apply plugin: 'org.apache.grails.buildsrc.groovydoc' tasks.register('resolveMongodbVersion').configure { Task docTask -> docTask.group = 'documentation' @@ -103,40 +97,12 @@ tasks.named('asciidoctor', AsciidoctorTask) { AsciidoctorTask it -> ] } -// Compute the javaVersion string for groovydoc's JavaParser language level. -// Requires Groovy 4.0.27+ (GROOVY-11668). -String groovydocJavaVersion = "JAVA_${project.findProperty('javaVersion') ?: '17'}" - -// Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. -Object resolveGroovydocProperty(Object value) { - if (value instanceof org.gradle.api.provider.Provider) { - return value.getOrNull() - } - return value -} - tasks.withType(Groovydoc).configureEach { Groovydoc groovydoc -> groovydoc.dependsOn(rootProject.subprojects .findAll { it.findProperty('gormApiDocs') } .collect { ":${it.name}:groovydoc" }) groovydoc.docTitle = "GORM for MongoDB - $project.version" - groovydoc.footer = ''' - -''' + groovydoc.includeAuthor = true def sourceFiles = coreProjects.collect { layout.projectDirectory.files("$it/src/main/groovy") @@ -148,15 +114,8 @@ tasks.withType(Groovydoc).configureEach { Groovydoc groovydoc -> groovydoc.source = sourceFiles groovydoc.destinationDir = layout.buildDirectory.dir('combined-api/api').get().asFile - groovydoc.access = GroovydocAccess.PROTECTED - groovydoc.processScripts = false - groovydoc.includeMainForScripts = false - groovydoc.includeAuthor = true groovydoc.classpath = configurations.documentation - groovydoc.groovyClasspath += configurations.documentation - groovydoc.noVersionStamp = false - // Collect source directories for AntBuilder groovydoc List groovydocSrcDirs = coreProjects.collect { layout.projectDirectory.dir("$it/src/main/groovy").asFile } @@ -164,43 +123,6 @@ tasks.withType(Groovydoc).configureEach { Groovydoc groovydoc -> .findAll { sp -> sp.findProperty('gormApiDocs') } .each { sp -> groovydocSrcDirs << new File(sp.projectDir, 'src/main/groovy') } groovydoc.ext.groovydocSourceDirs = groovydocSrcDirs - - // Replace Gradle's built-in Groovydoc execution with AntBuilder to support javaVersion. - // See: https://github.com/apache/grails-core/issues/15385 - groovydoc.actions.clear() - groovydoc.doLast { Groovydoc gdoc -> - def destDir = gdoc.destinationDir - destDir.mkdirs() - - List sourceDirs = (gdoc.ext.groovydocSourceDirs as List).findAll { File f -> f.exists() }.unique() - if (sourceDirs.isEmpty()) { - logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") - return - } - - ant.taskdef( - name: 'groovydoc', - classname: 'org.codehaus.groovy.ant.Groovydoc', - classpath: configurations.documentation.asPath - ) - - def sourcepath = sourceDirs.collect { File f -> f.absolutePath }.join(File.pathSeparator) - - ant.groovydoc( - destdir: destDir.absolutePath, - sourcepath: sourcepath, - packagenames: '**.*', - doctitle: gdoc.docTitle ?: '', - footer: gdoc.footer ?: '', - access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', - author: resolveGroovydocProperty(gdoc.includeAuthor) as String, - noTimestamp: 'true', - noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, - processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, - includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String, - javaVersion: groovydocJavaVersion - ) - } } tasks.register('docs', Sync).configure { Sync docTask -> diff --git a/grails-doc/build.gradle b/grails-doc/build.gradle index 63382c712e1..4f7c1d9d879 100644 --- a/grails-doc/build.gradle +++ b/grails-doc/build.gradle @@ -63,23 +63,6 @@ apply from: rootProject.layout.projectDirectory.file('gradle/docs-dependencies.g combinedGroovydoc.configure { Groovydoc gdoc -> gdoc.windowTitle = "Grails $projectVersion" gdoc.docTitle = "Grails $projectVersion" - gdoc.footer = ''' - -''' def docProjects = rootProject.subprojects .findAll { it.findProperty('includeInApiDocs') } diff --git a/grails-forge/gradle/doc-config.gradle b/grails-forge/gradle/doc-config.gradle index 38b8e2a60d1..4d22d4fba60 100644 --- a/grails-forge/gradle/doc-config.gradle +++ b/grails-forge/gradle/doc-config.gradle @@ -17,14 +17,10 @@ * under the License. */ -configurations.register('documentation') { - canBeConsumed = false - canBeResolved = true - attributes { - attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY)) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL)) - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME)) - } +apply plugin: 'org.apache.grails.buildsrc.groovydoc' + +grailsGroovydoc { + javaVersionEnabled = false } dependencies { @@ -38,85 +34,4 @@ tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> gdoc.classpath += project.configurations.documentation gdoc.windowTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" gdoc.docTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" - gdoc.footer = ''' - -''' - gdoc.access = GroovydocAccess.PROTECTED - gdoc.includeAuthor = false - gdoc.includeMainForScripts = false - gdoc.processScripts = false - gdoc.noTimestamp = true - gdoc.noVersionStamp = false - - // Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. - Closure resolveGroovydocProperty = { Object value -> - if (value instanceof org.gradle.api.provider.Provider) { - return value.getOrNull() - } - return value - } - - // Replace Gradle's built-in Groovydoc execution with AntBuilder for consistency - // with the main build. The javaVersion parameter is not available in Groovy 3.x - // (requires Groovy 4.0.27+, GROOVY-11668) - it will be added when forge upgrades. - // See: https://github.com/apache/grails-core/issues/15385 - gdoc.actions.clear() - gdoc.doLast { - def destDir = gdoc.destinationDir - destDir.mkdirs() - - List sourceDirs = [] - def sourceSetsExt = project.extensions.findByType(SourceSetContainer) - if (sourceSetsExt) { - def mainSS = sourceSetsExt.findByName('main') - if (mainSS) { - sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) - sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) - } - } - sourceDirs = sourceDirs.unique() - - if (sourceDirs.isEmpty()) { - logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") - return - } - - ant.taskdef( - name: 'groovydoc', - classname: 'org.codehaus.groovy.ant.Groovydoc', - classpath: configurations.documentation.asPath - ) - - def sourcepath = sourceDirs.collect { File f -> f.absolutePath }.join(File.pathSeparator) - - // Note: javaVersion omitted - not supported in Groovy 3.x (forge uses Groovy $groovyVersion) - ant.groovydoc( - destdir: destDir.absolutePath, - sourcepath: sourcepath, - packagenames: '**.*', - windowtitle: gdoc.windowTitle ?: '', - doctitle: gdoc.docTitle ?: '', - footer: gdoc.footer ?: '', - access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', - author: resolveGroovydocProperty(gdoc.includeAuthor) as String, - noTimestamp: resolveGroovydocProperty(gdoc.noTimestamp) as String, - noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, - processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, - includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String - ) - } } diff --git a/grails-gradle/gradle/docs-config.gradle b/grails-gradle/gradle/docs-config.gradle index e3459998b6f..8426b70a632 100644 --- a/grails-gradle/gradle/docs-config.gradle +++ b/grails-gradle/gradle/docs-config.gradle @@ -17,15 +17,8 @@ * under the License. */ -configurations.register('documentation') { - canBeConsumed = false - canBeResolved = true - attributes { - attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY)) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL)) - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME)) - } -} +apply plugin: 'org.apache.grails.buildsrc.groovydoc' + dependencies { add('documentation', platform(project(':grails-gradle-bom'))) add('documentation', 'org.fusesource.jansi:jansi') @@ -42,88 +35,10 @@ ext { includeInApiDocs = true } -// Resolve a Groovydoc task property that may be either a Gradle Property or a plain value. -Object resolveGroovydocProperty(Object value) { - if (value instanceof org.gradle.api.provider.Provider) { - return value.getOrNull() - } - return value -} - -// Compute the javaVersion string for groovydoc's JavaParser language level. -// Requires Groovy 4.0.27+ (GROOVY-11668). -String groovydocJavaVersion = "JAVA_${project.findProperty('javaVersion') ?: '17'}" - TaskProvider groovydocTask = tasks.named('groovydoc', Groovydoc) groovydocTask.configure { Groovydoc it -> it.classpath = configurations.documentation - it.groovyClasspath = configurations.documentation - it.access = GroovydocAccess.PROTECTED - it.includeAuthor = false - it.includeMainForScripts = false - it.processScripts = false - it.noTimestamp = true - it.noVersionStamp = false - it.footer = ''' - -''' it.destinationDir = project.file('build/docs/api') - - // Replace Gradle's built-in Groovydoc execution with AntBuilder to support javaVersion. - // See: https://github.com/apache/grails-core/issues/15385 - it.actions.clear() - it.doLast { - def destDir = it.destinationDir - destDir.mkdirs() - - SourceSetContainer sourceSets = project.extensions.getByType(SourceSetContainer) - List sourceDirs = ([] + sourceSets.main.groovy.srcDirs + sourceSets.main.java.srcDirs) - .findAll { File f -> f.exists() } - .unique() - - if (sourceDirs.isEmpty()) { - logger.lifecycle("Skipping groovydoc for ${it.name}: no source directories found") - return - } - - ant.taskdef( - name: 'groovydoc', - classname: 'org.codehaus.groovy.ant.Groovydoc', - classpath: configurations.documentation.asPath - ) - - def sourcepath = sourceDirs.collect { File f -> f.absolutePath }.join(File.pathSeparator) - - ant.groovydoc( - destdir: destDir.absolutePath, - sourcepath: sourcepath, - packagenames: '**.*', - windowtitle: it.windowTitle ?: '', - doctitle: it.docTitle ?: '', - footer: it.footer ?: '', - access: resolveGroovydocProperty(it.access)?.name()?.toLowerCase() ?: 'protected', - author: resolveGroovydocProperty(it.includeAuthor) as String, - noTimestamp: resolveGroovydocProperty(it.noTimestamp) as String, - noVersionStamp: resolveGroovydocProperty(it.noVersionStamp) as String, - processScripts: resolveGroovydocProperty(it.processScripts) as String, - includeMainForScripts: resolveGroovydocProperty(it.includeMainForScripts) as String, - javaVersion: groovydocJavaVersion - ) - } } tasks.named('javadoc').configure { From bf365318bb196349dc1c48e6f1bd8faeffa936b4 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 20 Feb 2026 10:39:10 -0500 Subject: [PATCH 3/6] Split groovydoc plugin into generic base and Grails layer 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 --- build-logic/plugins/build.gradle | 4 + .../buildsrc/GrailsGroovydocPlugin.groovy | 137 +------------- ...oovy => GroovydocEnhancerExtension.groovy} | 45 ++++- .../buildsrc/GroovydocEnhancerPlugin.groovy | 174 ++++++++++++++++++ gradle/docs-dependencies.gradle | 1 + grails-forge/gradle/doc-config.gradle | 2 +- 6 files changed, 221 insertions(+), 142 deletions(-) rename build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/{GrailsGroovydocExtension.groovy => GroovydocEnhancerExtension.groovy} (52%) create mode 100644 build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy diff --git a/build-logic/plugins/build.gradle b/build-logic/plugins/build.gradle index f750808908a..90240300d6a 100644 --- a/build-logic/plugins/build.gradle +++ b/build-logic/plugins/build.gradle @@ -62,6 +62,10 @@ gradlePlugin { id = 'org.apache.grails.gradle.grails-code-style' implementationClass = 'org.apache.grails.buildsrc.GrailsCodeStylePlugin' } + register('groovydocEnhancer') { + id = 'org.apache.grails.buildsrc.groovydoc-enhancer' + implementationClass = 'org.apache.grails.buildsrc.GroovydocEnhancerPlugin' + } register('grailsGroovydoc') { id = 'org.apache.grails.buildsrc.groovydoc' implementationClass = 'org.apache.grails.buildsrc.GrailsGroovydocPlugin' diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy index d5e5875af2c..7ce4b89113a 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy @@ -18,19 +18,10 @@ */ package org.apache.grails.buildsrc -import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.artifacts.Configuration -import org.gradle.api.attributes.Bundling -import org.gradle.api.attributes.Category -import org.gradle.api.attributes.Usage -import org.gradle.api.provider.Provider -import org.gradle.api.tasks.SourceSet -import org.gradle.api.tasks.SourceSetContainer -import org.gradle.api.tasks.javadoc.Groovydoc @CompileStatic class GrailsGroovydocPlugin implements Plugin { @@ -56,131 +47,9 @@ class GrailsGroovydocPlugin implements Plugin { @Override void apply(Project project) { - GrailsGroovydocExtension extension = project.extensions.create( - 'grailsGroovydoc', GrailsGroovydocExtension, project - ) - registerDocumentationConfiguration(project) - configureGroovydocDefaults(project) - configureAntBuilderExecution(project, extension) - } - - private static void registerDocumentationConfiguration(Project project) { - if (project.configurations.names.contains('documentation')) { - return - } - project.configurations.register('documentation') { Configuration config -> - config.canBeConsumed = false - config.canBeResolved = true - config.attributes { container -> - container.attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category, Category.LIBRARY)) - container.attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling, Bundling.EXTERNAL)) - container.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage, Usage.JAVA_RUNTIME)) - } - } - } - - @CompileDynamic - private static void configureGroovydocDefaults(Project project) { - project.tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> - gdoc.includeAuthor = false - gdoc.includeMainForScripts = false - gdoc.processScripts = false - gdoc.noTimestamp = true - gdoc.noVersionStamp = false - gdoc.footer = MATOMO_FOOTER - if (project.configurations.names.contains('documentation')) { - gdoc.groovyClasspath = project.configurations.getByName('documentation') - } - } - } - - @CompileDynamic - private static void configureAntBuilderExecution(Project project, GrailsGroovydocExtension extension) { - project.tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> - gdoc.actions.clear() - gdoc.doLast { - File destDir = gdoc.destinationDir - destDir.mkdirs() - - List sourceDirs = resolveSourceDirectories(gdoc, project) - if (sourceDirs.isEmpty()) { - project.logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") - return - } - - Configuration docConfig = project.configurations.findByName('documentation') - if (!docConfig) { - project.logger.warn("Skipping groovydoc for ${gdoc.name}: 'documentation' configuration not found") - return - } - - project.ant.taskdef( - name: 'groovydoc', - classname: 'org.codehaus.groovy.ant.Groovydoc', - classpath: docConfig.asPath - ) - - List> links = resolveLinks(gdoc) - String sourcepath = sourceDirs.collect { it.absolutePath }.join(File.pathSeparator) - - Map antArgs = [ - destdir: destDir.absolutePath, - sourcepath: sourcepath, - packagenames: '**.*', - windowtitle: gdoc.windowTitle ?: '', - doctitle: gdoc.docTitle ?: '', - footer: gdoc.footer ?: '', - access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', - author: resolveGroovydocProperty(gdoc.includeAuthor) as String, - noTimestamp: resolveGroovydocProperty(gdoc.noTimestamp) as String, - noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, - processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, - includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String - ] - - if (extension.javaVersionEnabled.get()) { - antArgs.put('javaVersion', extension.javaVersion.get()) - } - - project.ant.groovydoc(antArgs) { - for (Map l in links) { - link(packages: l.packages, href: l.href) - } - } - } - } - } - - @CompileDynamic - private static List resolveSourceDirectories(Groovydoc gdoc, Project project) { - if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { - return (gdoc.ext.groovydocSourceDirs as List).findAll { it.exists() }.unique() as List - } - - List sourceDirs = [] - SourceSetContainer sourceSets = project.extensions.findByType(SourceSetContainer) - if (sourceSets) { - SourceSet mainSS = sourceSets.findByName('main') - if (mainSS) { - sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) - sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) - } - } - sourceDirs.unique() as List - } - - @CompileDynamic - private static List> resolveLinks(Groovydoc gdoc) { - if (gdoc.ext.has('groovydocLinks')) { - return gdoc.ext.groovydocLinks as List> - } - [] - } + project.pluginManager.apply(GroovydocEnhancerPlugin) - static Object resolveGroovydocProperty(Object value) { - if (value instanceof Provider) { - return ((Provider) value).getOrNull() - } - value + GroovydocEnhancerExtension enhancer = project.extensions.getByType(GroovydocEnhancerExtension) + enhancer.footer.set(MATOMO_FOOTER) } } diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocExtension.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy similarity index 52% rename from build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocExtension.groovy rename to build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy index 98cefd434c0..0f7bd5f68d9 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocExtension.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy @@ -27,17 +27,24 @@ import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property /** - * Extension for configuring the Grails Groovydoc convention plugin. + * Extension for configuring the Groovydoc Enhancer convention plugin. * - *

Allows per-project control over the {@code javaVersion} parameter - * passed to the Groovy Ant groovydoc task. The {@code javaVersion} - * parameter was added in Groovy 4.0.27 (GROOVY-11668) and controls - * the JavaParser language level used when parsing Java sources.

+ *

This plugin replaces Gradle's built-in Groovydoc task execution with + * a direct AntBuilder invocation of the Groovy {@code org.codehaus.groovy.ant.Groovydoc} + * Ant task. This enables the {@code javaVersion} parameter (added in Groovy 4.0.27, + * GROOVY-11668) which controls the JavaParser language level used when parsing + * Java source files.

+ * + *

When Gradle natively supports the {@code javaVersion} property + * (see gradle#33659), + * set {@link #useAntBuilder} to {@code false} to revert to Gradle's built-in + * Groovydoc task execution while retaining all other configuration (footer, + * defaults, etc.).

* * @since 7.0.8 */ @CompileStatic -class GrailsGroovydocExtension { +class GroovydocEnhancerExtension { /** * The Java language level string passed to the groovydoc Ant task's @@ -57,11 +64,35 @@ class GrailsGroovydocExtension { */ final Property javaVersionEnabled + /** + * Whether to replace Gradle's built-in Groovydoc task execution with + * AntBuilder invocation. When {@code true} (default), the plugin clears + * the task's actions and replaces them with a {@code doLast} that uses + * AntBuilder. When {@code false}, the plugin only applies property + * defaults (footer, etc.) and lets Gradle's built-in task run normally. + * + *

Set to {@code false} when Gradle adds native {@code javaVersion} + * support (gradle/gradle#33659).

+ * + *

Defaults to {@code true}.

+ */ + final Property useAntBuilder + + /** + * HTML footer appended to every generated groovydoc page. Useful for + * analytics scripts, copyright notices, or custom branding. + * + *

Defaults to an empty string (no footer).

+ */ + final Property footer + @Inject - GrailsGroovydocExtension(ObjectFactory objects, Project project) { + GroovydocEnhancerExtension(ObjectFactory objects, Project project) { javaVersion = objects.property(String).convention( "JAVA_${GradleUtils.findProperty(project, 'javaVersion') ?: '17'}" as String ) javaVersionEnabled = objects.property(Boolean).convention(true) + useAntBuilder = objects.property(Boolean).convention(true) + footer = objects.property(String).convention('') } } diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy new file mode 100644 index 00000000000..ac56fd3e31b --- /dev/null +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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 org.apache.grails.buildsrc + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.Usage +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.javadoc.Groovydoc + +@CompileStatic +class GroovydocEnhancerPlugin implements Plugin { + + @Override + void apply(Project project) { + GroovydocEnhancerExtension extension = project.extensions.create( + 'groovydocEnhancer', GroovydocEnhancerExtension, project + ) + registerDocumentationConfiguration(project) + configureGroovydocDefaults(project, extension) + configureAntBuilderExecution(project, extension) + } + + private static void registerDocumentationConfiguration(Project project) { + if (project.configurations.names.contains('documentation')) { + return + } + project.configurations.register('documentation') { Configuration config -> + config.canBeConsumed = false + config.canBeResolved = true + config.attributes { container -> + container.attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category, Category.LIBRARY)) + container.attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling, Bundling.EXTERNAL)) + container.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage, Usage.JAVA_RUNTIME)) + } + } + } + + @CompileDynamic + private static void configureGroovydocDefaults(Project project, GroovydocEnhancerExtension extension) { + project.tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> + gdoc.includeAuthor = false + gdoc.includeMainForScripts = false + gdoc.processScripts = false + gdoc.noTimestamp = true + gdoc.noVersionStamp = false + String footerValue = extension.footer.getOrElse('') + if (footerValue) { + gdoc.footer = footerValue + } + if (project.configurations.names.contains('documentation')) { + gdoc.groovyClasspath = project.configurations.getByName('documentation') + } + } + } + + @CompileDynamic + private static void configureAntBuilderExecution(Project project, GroovydocEnhancerExtension extension) { + project.tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> + if (!extension.useAntBuilder.get()) { + return + } + + gdoc.actions.clear() + gdoc.doLast { + File destDir = gdoc.destinationDir + destDir.mkdirs() + + List sourceDirs = resolveSourceDirectories(gdoc, project) + if (sourceDirs.isEmpty()) { + project.logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") + return + } + + Configuration docConfig = project.configurations.findByName('documentation') + if (!docConfig) { + project.logger.warn("Skipping groovydoc for ${gdoc.name}: 'documentation' configuration not found") + return + } + + project.ant.taskdef( + name: 'groovydoc', + classname: 'org.codehaus.groovy.ant.Groovydoc', + classpath: docConfig.asPath + ) + + List> links = resolveLinks(gdoc) + String sourcepath = sourceDirs.collect { it.absolutePath }.join(File.pathSeparator) + + Map antArgs = [ + destdir: destDir.absolutePath, + sourcepath: sourcepath, + packagenames: '**.*', + windowtitle: gdoc.windowTitle ?: '', + doctitle: gdoc.docTitle ?: '', + footer: gdoc.footer ?: '', + access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', + author: resolveGroovydocProperty(gdoc.includeAuthor) as String, + noTimestamp: resolveGroovydocProperty(gdoc.noTimestamp) as String, + noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, + processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, + includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String + ] + + if (extension.javaVersionEnabled.get()) { + antArgs.put('javaVersion', extension.javaVersion.get()) + } + + project.ant.groovydoc(antArgs) { + for (Map l in links) { + link(packages: l.packages, href: l.href) + } + } + } + } + } + + @CompileDynamic + private static List resolveSourceDirectories(Groovydoc gdoc, Project project) { + if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { + return (gdoc.ext.groovydocSourceDirs as List).findAll { it.exists() }.unique() as List + } + + List sourceDirs = [] + SourceSetContainer sourceSets = project.extensions.findByType(SourceSetContainer) + if (sourceSets) { + SourceSet mainSS = sourceSets.findByName('main') + if (mainSS) { + sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) + sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) + } + } + sourceDirs.unique() as List + } + + @CompileDynamic + private static List> resolveLinks(Groovydoc gdoc) { + if (gdoc.ext.has('groovydocLinks')) { + return gdoc.ext.groovydocLinks as List> + } + [] + } + + static Object resolveGroovydocProperty(Object value) { + if (value instanceof Provider) { + return ((Provider) value).getOrNull() + } + value + } +} diff --git a/gradle/docs-dependencies.gradle b/gradle/docs-dependencies.gradle index afb56aec4a6..ec9c6da7e14 100644 --- a/gradle/docs-dependencies.gradle +++ b/gradle/docs-dependencies.gradle @@ -40,6 +40,7 @@ String resolveProjectVersion(String artifact) { if (!version) { return null } + version } tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> diff --git a/grails-forge/gradle/doc-config.gradle b/grails-forge/gradle/doc-config.gradle index 4d22d4fba60..d850444c627 100644 --- a/grails-forge/gradle/doc-config.gradle +++ b/grails-forge/gradle/doc-config.gradle @@ -19,7 +19,7 @@ apply plugin: 'org.apache.grails.buildsrc.groovydoc' -grailsGroovydoc { +groovydocEnhancer { javaVersionEnabled = false } From 99ad5de954f7d5a5c097cbcdb70e6b179d806c58 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Fri, 20 Feb 2026 17:31:04 +0100 Subject: [PATCH 4/6] style: simplify groovydoc plugin --- .../buildsrc/GrailsGroovydocPlugin.groovy | 5 +- .../buildsrc/GroovydocEnhancerPlugin.groovy | 78 ++++++++++--------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy index 7ce4b89113a..b10a77a93cb 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy @@ -48,8 +48,7 @@ class GrailsGroovydocPlugin implements Plugin { @Override void apply(Project project) { project.pluginManager.apply(GroovydocEnhancerPlugin) - - GroovydocEnhancerExtension enhancer = project.extensions.getByType(GroovydocEnhancerExtension) - enhancer.footer.set(MATOMO_FOOTER) + project.extensions.getByType(GroovydocEnhancerExtension) + .footer.set(MATOMO_FOOTER) } } diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy index ac56fd3e31b..38fb2b04e23 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy @@ -23,12 +23,10 @@ import groovy.transform.CompileStatic import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.artifacts.Configuration import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category import org.gradle.api.attributes.Usage import org.gradle.api.provider.Provider -import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.javadoc.Groovydoc @@ -38,7 +36,9 @@ class GroovydocEnhancerPlugin implements Plugin { @Override void apply(Project project) { GroovydocEnhancerExtension extension = project.extensions.create( - 'groovydocEnhancer', GroovydocEnhancerExtension, project + 'groovydocEnhancer', + GroovydocEnhancerExtension, + project ) registerDocumentationConfiguration(project) configureGroovydocDefaults(project, extension) @@ -49,56 +49,60 @@ class GroovydocEnhancerPlugin implements Plugin { if (project.configurations.names.contains('documentation')) { return } - project.configurations.register('documentation') { Configuration config -> - config.canBeConsumed = false - config.canBeResolved = true - config.attributes { container -> - container.attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category, Category.LIBRARY)) - container.attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling, Bundling.EXTERNAL)) - container.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage, Usage.JAVA_RUNTIME)) + project.configurations.register('documentation') { + it.canBeConsumed = false + it.canBeResolved = true + it.attributes { + it.attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category, Category.LIBRARY)) + it.attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling, Bundling.EXTERNAL)) + it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage, Usage.JAVA_RUNTIME)) } } } @CompileDynamic private static void configureGroovydocDefaults(Project project, GroovydocEnhancerExtension extension) { - project.tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> - gdoc.includeAuthor = false - gdoc.includeMainForScripts = false - gdoc.processScripts = false - gdoc.noTimestamp = true - gdoc.noVersionStamp = false - String footerValue = extension.footer.getOrElse('') + project.tasks.withType(Groovydoc).configureEach { + it.includeAuthor.set(false) + it.includeMainForScripts.set(false) + it.processScripts.set(false) + it.noTimestamp = true + it.noVersionStamp = false + def footerValue = extension.footer.getOrElse('') if (footerValue) { - gdoc.footer = footerValue + it.footer = footerValue } if (project.configurations.names.contains('documentation')) { - gdoc.groovyClasspath = project.configurations.getByName('documentation') + it.groovyClasspath = project.configurations.getByName('documentation') } } } @CompileDynamic private static void configureAntBuilderExecution(Project project, GroovydocEnhancerExtension extension) { - project.tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> + project.tasks.withType(Groovydoc).configureEach { gdoc -> if (!extension.useAntBuilder.get()) { return } gdoc.actions.clear() gdoc.doLast { - File destDir = gdoc.destinationDir - destDir.mkdirs() - - List sourceDirs = resolveSourceDirectories(gdoc, project) + def destDir = gdoc.destinationDir.tap { it.mkdirs() } + def sourceDirs = resolveSourceDirectories(gdoc, project) if (sourceDirs.isEmpty()) { - project.logger.lifecycle("Skipping groovydoc for ${gdoc.name}: no source directories found") + project.logger.lifecycle( + 'Skipping groovydoc for {}: no source directories found', + gdoc.name + ) return } - Configuration docConfig = project.configurations.findByName('documentation') + def docConfig = project.configurations.findByName('documentation') if (!docConfig) { - project.logger.warn("Skipping groovydoc for ${gdoc.name}: 'documentation' configuration not found") + project.logger.warn( + 'Skipping groovydoc for {}: \'documentation\' configuration not found', + gdoc.name + ) return } @@ -108,10 +112,12 @@ class GroovydocEnhancerPlugin implements Plugin { classpath: docConfig.asPath ) - List> links = resolveLinks(gdoc) - String sourcepath = sourceDirs.collect { it.absolutePath }.join(File.pathSeparator) + def links = resolveLinks(gdoc) + def sourcepath = sourceDirs + .collect { it.absolutePath } + .join(File.pathSeparator) - Map antArgs = [ + def antArgs = [ destdir: destDir.absolutePath, sourcepath: sourcepath, packagenames: '**.*', @@ -131,7 +137,7 @@ class GroovydocEnhancerPlugin implements Plugin { } project.ant.groovydoc(antArgs) { - for (Map l in links) { + for (var l in links) { link(packages: l.packages, href: l.href) } } @@ -142,19 +148,21 @@ class GroovydocEnhancerPlugin implements Plugin { @CompileDynamic private static List resolveSourceDirectories(Groovydoc gdoc, Project project) { if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { - return (gdoc.ext.groovydocSourceDirs as List).findAll { it.exists() }.unique() as List + return (gdoc.ext.groovydocSourceDirs as List) + .findAll { it.exists() } + .unique() } List sourceDirs = [] - SourceSetContainer sourceSets = project.extensions.findByType(SourceSetContainer) + def sourceSets = project.extensions.findByType(SourceSetContainer) if (sourceSets) { - SourceSet mainSS = sourceSets.findByName('main') + def mainSS = sourceSets.findByName('main') if (mainSS) { sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) } } - sourceDirs.unique() as List + sourceDirs.unique() } @CompileDynamic From 3702656344ce7baa21041815c26ba516630a1df7 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 20 Feb 2026 13:18:23 -0500 Subject: [PATCH 5/6] fix: use lazy provider for javaVersion convention Wrap the javaVersion convention in project.provider so the property is resolved at execution time rather than configuration time. Assisted-by: Claude Code --- .../apache/grails/buildsrc/GroovydocEnhancerExtension.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy index 0f7bd5f68d9..4ef1eaebaa1 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy @@ -89,7 +89,7 @@ class GroovydocEnhancerExtension { @Inject 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 } ) javaVersionEnabled = objects.property(Boolean).convention(true) useAntBuilder = objects.property(Boolean).convention(true) From 9115b7575364d29285db66d14a4202dda77188dd Mon Sep 17 00:00:00 2001 From: James Fredley Date: Sat, 21 Feb 2026 11:12:02 -0500 Subject: [PATCH 6/6] feat: publish GroovydocEnhancerPlugin for use in end-user Grails apps Add grails-gradle-groovydoc as a published Gradle plugin module that enables Grails applications using modern Java features (17+) to generate accurate Groovydoc documentation. Without this plugin, Groovydoc fails to parse Java sources using sealed classes, records, pattern matching, and other post-Java 11 language features. Changes: - New module grails-gradle/groovydoc with GroovydocEnhancerPlugin published as org.apache.grails.gradle:grails-gradle-groovydoc with plugin ID org.apache.grails.gradle.groovydoc - Forge DefaultFeature adds the plugin to all generated Grails apps - Profile base/profile.yml applies the plugin via grails CLI generation - grails-bom updated with the new artifact for BOM version management - build-logic retains its own copy for the framework build (bootstrap) Assisted-by: Claude Code --- grails-bom/build.gradle | 1 + .../feature/groovydoc/GroovydocEnhancer.java | 88 ++++++++ .../gradle/publish-root-config.gradle | 1 + grails-gradle/groovydoc/build.gradle | 72 ++++++ .../GroovydocEnhancerExtension.groovy | 49 +++++ .../groovydoc/GroovydocEnhancerPlugin.groovy | 208 ++++++++++++++++++ grails-gradle/settings.gradle | 3 + grails-profiles/base/profile.yml | 3 + 8 files changed, 425 insertions(+) create mode 100644 grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java create mode 100644 grails-gradle/groovydoc/build.gradle create mode 100644 grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy create mode 100644 grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy diff --git a/grails-bom/build.gradle b/grails-bom/build.gradle index 260239fcedd..74042673f0a 100644 --- a/grails-bom/build.gradle +++ b/grails-bom/build.gradle @@ -44,6 +44,7 @@ ext { // TODO: It should be possible to pull these build names using includedBuild, but I haven't found a way to do so gradleBuildProjects = [ 'grails-gradle-plugins':'org.apache.grails', + 'grails-gradle-groovydoc':'org.apache.grails.gradle', 'grails-gradle-model':'org.apache.grails.gradle', 'grails-gradle-common':'org.apache.grails.gradle', 'grails-gradle-tasks':'org.apache.grails', diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java new file mode 100644 index 00000000000..d040c446624 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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 org.grails.forge.feature.groovydoc; + +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; +import org.grails.forge.application.ApplicationType; +import org.grails.forge.application.generator.GeneratorContext; +import org.grails.forge.build.dependencies.Dependency; +import org.grails.forge.build.gradle.GradlePlugin; +import org.grails.forge.feature.Category; +import org.grails.forge.feature.DefaultFeature; +import org.grails.forge.feature.Feature; +import org.grails.forge.options.Options; + +import java.util.Set; + +@Singleton +public class GroovydocEnhancer implements DefaultFeature { + + @NonNull + @Override + public String getName() { + return "groovydoc-enhancer"; + } + + @Override + public String getTitle() { + return "Groovydoc Enhancer"; + } + + @NonNull + @Override + public String getDescription() { + return "Enables Groovydoc generation for projects using modern Java features (17+). " + + "Without this plugin, Groovydoc fails to parse Java sources that use sealed classes, " + + "records, pattern matching, and other post-Java 11 language features."; + } + + @Override + public boolean supports(ApplicationType applicationType) { + return true; + } + + @Override + public boolean isVisible() { + return false; + } + + @Override + public String getCategory() { + return Category.DOCUMENTATION; + } + + @Override + public boolean shouldApply(ApplicationType applicationType, Options options, Set selectedFeatures) { + return true; + } + + @Override + public void apply(GeneratorContext generatorContext) { + generatorContext.addBuildscriptDependency(Dependency.builder() + .groupId("org.apache.grails.gradle") + .artifactId("grails-gradle-groovydoc") + .buildSrc()); + + generatorContext.addBuildPlugin(GradlePlugin.builder() + .id("org.apache.grails.gradle.groovydoc") + .useApplyPlugin(true) + .build()); + } +} diff --git a/grails-gradle/gradle/publish-root-config.gradle b/grails-gradle/gradle/publish-root-config.gradle index 4282dbfe20c..dba8346e06e 100644 --- a/grails-gradle/gradle/publish-root-config.gradle +++ b/grails-gradle/gradle/publish-root-config.gradle @@ -26,6 +26,7 @@ group = 'this.will.be.overridden' def publishedProjects = [ 'grails-gradle-bom', 'grails-gradle-common', + 'grails-gradle-groovydoc', 'grails-gradle-model', 'grails-gradle-plugins', 'grails-gradle-tasks', diff --git a/grails-gradle/groovydoc/build.gradle b/grails-gradle/groovydoc/build.gradle new file mode 100644 index 00000000000..105c3d522c4 --- /dev/null +++ b/grails-gradle/groovydoc/build.gradle @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ + +plugins { + id 'groovy' + id 'java-gradle-plugin' + id 'org.apache.grails.buildsrc.properties' + id 'org.apache.grails.buildsrc.compile' + id 'org.apache.grails.buildsrc.publish' + id 'org.apache.grails.buildsrc.sbom' + id 'org.apache.grails.gradle.grails-code-style' +} + +version = projectVersion +group = 'org.apache.grails.gradle' + +ext { + pomTitle = 'Grails Gradle Groovydoc Enhancer Plugin' + pomDescription = 'A Gradle plugin that enhances Groovydoc generation to support modern Java source levels, allowing Grails applications using Java 17+ features to generate accurate Groovydoc documentation' + pomMavenPublicationName = 'pluginMaven' +} + +dependencies { + implementation platform(project(':grails-gradle-bom')) + + // compile with the Groovy version provided by Gradle + // to ensure build compatibility with Gradle, currently Groovy 3.0.x + // see: https://docs.gradle.org/current/userguide/compatibility.html#groovy + compileOnly "org.codehaus.groovy:groovy" + + // Testing - Gradle TestKit is auto-added by java-gradle-plugin + testImplementation('org.spockframework:spock-core') { transitive = false } + testImplementation 'org.codehaus.groovy:groovy-test-junit5' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} + +configurations { + testCompileClasspath.exclude group: 'org.apache.groovy', module: 'groovy' + testRuntimeClasspath.exclude group: 'org.apache.groovy', module: 'groovy' +} + +gradlePlugin { + plugins { + groovydoc { + displayName = 'Grails Groovydoc Enhancer Plugin' + description = 'Enhances Groovydoc generation with Java source level support for modern Java features (17+)' + id = 'org.apache.grails.gradle.groovydoc' + implementationClass = 'org.apache.grails.gradle.groovydoc.GroovydocEnhancerPlugin' + } + } +} + +apply { + from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') +} diff --git a/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy new file mode 100644 index 00000000000..eaeab010368 --- /dev/null +++ b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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 org.apache.grails.gradle.groovydoc + +import javax.inject.Inject + +import groovy.transform.CompileStatic + +import org.gradle.api.Project +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +@CompileStatic +class GroovydocEnhancerExtension { + + final Property javaVersion + + final Property javaVersionEnabled + + final Property useAntBuilder + + final Property footer + + @Inject + GroovydocEnhancerExtension(ObjectFactory objects, Project project) { + javaVersion = objects.property(String).convention( + project.provider { "JAVA_${project.findProperty('javaVersion') ?: '17'}" as String } + ) + javaVersionEnabled = objects.property(Boolean).convention(true) + useAntBuilder = objects.property(Boolean).convention(true) + footer = objects.property(String).convention('') + } +} diff --git a/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy new file mode 100644 index 00000000000..219ab891599 --- /dev/null +++ b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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 org.apache.grails.gradle.groovydoc + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.Usage +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.javadoc.Groovydoc + +/** + * A Gradle plugin that enhances Groovydoc generation to support modern Java + * source levels. Gradle's built-in {@link Groovydoc} task does not expose + * the {@code javaVersion} parameter (added in Groovy 4.0.27 via + * GROOVY-11668), + * so projects using Java 17+ features (sealed classes, records, etc.) fail + * to generate Groovydoc. + * + *

This plugin replaces the built-in task execution with a direct AntBuilder + * invocation that passes the {@code javaVersion} parameter, enabling accurate + * Groovydoc generation for modern Java source levels.

+ * + *

Configure via the {@code groovydocEnhancer} extension:

+ *
+ * groovydocEnhancer {
+ *     javaVersion = 'JAVA_17'       // Java source level for parsing
+ *     javaVersionEnabled = true      // set false for Groovy < 4.0.27
+ *     useAntBuilder = true           // set false when Gradle adds native support
+ *     footer = '<p>My Footer</p>'
+ * }
+ * 
+ * + * @since 7.0.8 + * @see GroovydocEnhancerExtension + * @see gradle#33659 + */ +@CompileStatic +class GroovydocEnhancerPlugin implements Plugin { + + @Override + void apply(Project project) { + GroovydocEnhancerExtension extension = project.extensions.create( + 'groovydocEnhancer', + GroovydocEnhancerExtension, + project + ) + registerDocumentationConfiguration(project) + configureGroovydocDefaults(project, extension) + configureAntBuilderExecution(project, extension) + } + + private static void registerDocumentationConfiguration(Project project) { + if (project.configurations.names.contains('documentation')) { + return + } + project.configurations.register('documentation') { + it.canBeConsumed = false + it.canBeResolved = true + it.attributes { + it.attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category, Category.LIBRARY)) + it.attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling, Bundling.EXTERNAL)) + it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage, Usage.JAVA_RUNTIME)) + } + } + } + + @CompileDynamic + private static void configureGroovydocDefaults(Project project, GroovydocEnhancerExtension extension) { + project.tasks.withType(Groovydoc).configureEach { + it.includeAuthor.set(false) + it.includeMainForScripts.set(false) + it.processScripts.set(false) + it.noTimestamp = true + it.noVersionStamp = false + def footerValue = extension.footer.getOrElse('') + if (footerValue) { + it.footer = footerValue + } + if (project.configurations.names.contains('documentation')) { + it.groovyClasspath = project.configurations.getByName('documentation') + } + } + } + + @CompileDynamic + private static void configureAntBuilderExecution(Project project, GroovydocEnhancerExtension extension) { + project.tasks.withType(Groovydoc).configureEach { gdoc -> + if (!extension.useAntBuilder.get()) { + return + } + + gdoc.actions.clear() + gdoc.doLast { + def destDir = gdoc.destinationDir.tap { it.mkdirs() } + def sourceDirs = resolveSourceDirectories(gdoc, project) + if (sourceDirs.isEmpty()) { + project.logger.lifecycle( + 'Skipping groovydoc for {}: no source directories found', + gdoc.name + ) + return + } + + 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 + ) + + def links = resolveLinks(gdoc) + def sourcepath = sourceDirs + .collect { it.absolutePath } + .join(File.pathSeparator) + + def antArgs = [ + destdir: destDir.absolutePath, + sourcepath: sourcepath, + packagenames: '**.*', + windowtitle: gdoc.windowTitle ?: '', + doctitle: gdoc.docTitle ?: '', + footer: gdoc.footer ?: '', + access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', + author: resolveGroovydocProperty(gdoc.includeAuthor) as String, + noTimestamp: resolveGroovydocProperty(gdoc.noTimestamp) as String, + noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, + processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, + includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String + ] + + if (extension.javaVersionEnabled.get()) { + antArgs.put('javaVersion', extension.javaVersion.get()) + } + + project.ant.groovydoc(antArgs) { + for (var l in links) { + link(packages: l.packages, href: l.href) + } + } + } + } + } + + @CompileDynamic + private static List resolveSourceDirectories(Groovydoc gdoc, Project project) { + if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { + return (gdoc.ext.groovydocSourceDirs as List) + .findAll { it.exists() } + .unique() + } + + List sourceDirs = [] + def sourceSets = project.extensions.findByType(SourceSetContainer) + if (sourceSets) { + def mainSS = sourceSets.findByName('main') + if (mainSS) { + sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) + sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) + } + } + sourceDirs.unique() + } + + @CompileDynamic + private static List> resolveLinks(Groovydoc gdoc) { + if (gdoc.ext.has('groovydocLinks')) { + return gdoc.ext.groovydocLinks as List> + } + [] + } + + static Object resolveGroovydocProperty(Object value) { + if (value instanceof Provider) { + return ((Provider) value).getOrNull() + } + value + } +} diff --git a/grails-gradle/settings.gradle b/grails-gradle/settings.gradle index 7d0b8a0a1b2..64a7bccebf5 100644 --- a/grails-gradle/settings.gradle +++ b/grails-gradle/settings.gradle @@ -74,3 +74,6 @@ project(':grails-gradle-model').projectDir = file('model') include 'grails-gradle-tasks' project(':grails-gradle-tasks').projectDir = file('tasks') + +include 'grails-gradle-groovydoc' +project(':grails-gradle-groovydoc').projectDir = file('groovydoc') diff --git a/grails-profiles/base/profile.yml b/grails-profiles/base/profile.yml index f1b9b54e294..4d42ea9d40d 100644 --- a/grails-profiles/base/profile.yml +++ b/grails-profiles/base/profile.yml @@ -30,7 +30,10 @@ build: - eclipse - idea - org.apache.grails.gradle.grails-app + - org.apache.grails.gradle.groovydoc dependencies: + - scope: build + coords: "org.apache.grails.gradle:grails-gradle-groovydoc" - scope: build coords: "org.apache.grails:grails-gradle-plugins" - scope: developmentOnly