Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build-logic/plugins/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.</p>
*
* @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"}).
*
* <p>Defaults to {@code "JAVA_${javaVersion}"} where {@code javaVersion}
* is read from the project property, falling back to {@code "JAVA_17"}.</p>
*/
final Property<String> 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).
*
* <p>Defaults to {@code true}.</p>
*/
final Property<Boolean> 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)
}
}
Original file line number Diff line number Diff line change
@@ -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<Project> {
Copy link
Contributor

Choose a reason for hiding this comment

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

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


static final String MATOMO_FOOTER = '''\
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDoNotTrack", true]);
_paq.push(["disableCookies"]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://analytics.apache.org/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '79']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->'''

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

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.

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<Map<String, String>> links = resolveLinks(gdoc)
String sourcepath = sourceDirs.collect { it.absolutePath }.join(File.pathSeparator)

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

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
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<String, String> l in links) {
link(packages: l.packages, href: l.href)
}
}
}
}
}

@CompileDynamic
private static List<File> resolveSourceDirectories(Groovydoc gdoc, Project project) {
if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) {
return (gdoc.ext.groovydocSourceDirs as List<File>).findAll { it.exists() }.unique() as List<File>
}

List<File> 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<File>
}

@CompileDynamic
private static List<Map<String, String>> resolveLinks(Groovydoc gdoc) {
if (gdoc.ext.has('groovydocLinks')) {
return gdoc.ext.groovydocLinks as List<Map<String, String>>
}
[]
}

static Object resolveGroovydocProperty(Object value) {
if (value instanceof Provider) {
return ((Provider) value).getOrNull()
}
value
}
}
60 changes: 15 additions & 45 deletions gradle/docs-dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')))
Expand Down Expand Up @@ -52,52 +44,30 @@ String resolveProjectVersion(String artifact) {

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 = '''<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDoNotTrack", true]);
_paq.push(["disableCookies"]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://analytics.apache.org/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '79']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->'''

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

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.

def testContainersVersion = resolveProjectVersion('testcontainers')
if(testContainersVersion) {
gdoc.link("https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/", 'org.testcontainers.')
if (testContainersVersion) {
links << [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) {
links << [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) {
links << [packages: 'org.springframework.boot.', href: "https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/"]
}
if (gdoc.ext.has('groovydocLinks')) {
links.addAll(gdoc.ext.groovydocLinks as List<Map<String, String>>)
}
gdoc.ext.groovydocLinks = links
}
}
27 changes: 6 additions & 21 deletions grails-data-docs/stage/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '''<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDoNotTrack", true]);
_paq.push(["disableCookies"]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://analytics.apache.org/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '79']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->'''

Set<Project> docProjects = rootProject.subprojects.findAll {
it.name in [
Expand Down Expand Up @@ -77,14 +60,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)
Expand Down
Loading
Loading