diff --git a/conventions/src/main/groovy/config.docs.gradle b/conventions/src/main/groovy/config.docs.gradle index 0d918fb..343abbd 100644 --- a/conventions/src/main/groovy/config.docs.gradle +++ b/conventions/src/main/groovy/config.docs.gradle @@ -3,6 +3,11 @@ // https://github.com/grails-plugins/grails-plugin-template/tree/main/conventions import grails.plugins.conventions.PluginDocsExtension +plugins { + // Gives the Groovydoc tasks their conventions (Gradle 9 requires the noIndex, noHelp, ... flags) and groovyRuntime + id 'groovy-base' +} + // Defaults follow the template naming convention (-root, , -docs); override them with // pluginDocs { pluginProject = ':my-plugin'; docsProject = ':my-docs' } def docsExtension = extensions.create('pluginDocs', PluginDocsExtension) @@ -12,6 +17,32 @@ docsExtension.docsProject.convention(":${project.name - 'root'}docs".toString()) def docProject = docsExtension.docsProject.map { project(it) } def pluginProject = docsExtension.pluginProject.map { project(it) } +// Gradle 9 does not let a task resolve another project's configurations, so the aggregation does not read the plugin +// project's Groovydoc classpath: it resolves the plugin project (and the Groovy it is built with) through its own +def aggregateGroovydoc = configurations.dependencyScope('aggregateGroovydoc') { + description = 'Dependencies of the aggregated Groovy API documentation' +} +aggregateGroovydoc.configure { + def grailsVersion = providers.gradleProperty('grailsVersion').orElse(providers.provider { + throw new GradleException('config.docs needs grailsVersion in gradle.properties') + }) + dependencies.addLater(grailsVersion.map { + project.dependencies.platform("org.apache.grails:grails-bom:${it}") + }) + dependencies.add(project.dependencies.create('org.apache.groovy:groovy')) + dependencies.addLater(docsExtension.pluginProject.map { project.dependencies.project(path: it) }) +} +def aggregateGroovydocClasspath = configurations.resolvable('aggregateGroovydocClasspath') { + description = 'Classpath of the aggregated Groovy API documentation' + extendsFrom(aggregateGroovydoc.get()) + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME)) + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR)) + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL)) + } +} + tasks.register('cleanDocs', Delete) { description = 'Deletes the documentation output' group = 'documentation' @@ -23,12 +54,12 @@ tasks.register('aggregateGroovyApiDoc', Groovydoc) { description = 'Generates Groovy API Documentation for the plugin project under build/docs/gapi' group = 'documentation' + // Only the sources are taken from the plugin project's Groovydoc task; the plugin jar is built through classpath def upstream = pluginProject.flatMap { it.tasks.named('groovydoc', Groovydoc) } as Provider dependsOn(tasks.named('cleanDocs')) - dependsOn(upstream) access = GroovydocAccess.PROTECTED includeAuthor = false @@ -39,8 +70,8 @@ tasks.register('aggregateGroovyApiDoc', Groovydoc) { source = { upstream.get().source } destinationDir = rootProject.layout.buildDirectory.dir('docs/gapi').get().asFile - classpath = files({ upstream.get().classpath }) - groovyClasspath = files({ upstream.get().groovyClasspath }) + classpath = aggregateGroovydocClasspath.get() + groovyClasspath = groovyRuntime.inferGroovyClasspath(aggregateGroovydocClasspath.get()) } tasks.register('docs') { diff --git a/conventions/src/test/groovy/grails/plugins/conventions/PluginDocsSpec.groovy b/conventions/src/test/groovy/grails/plugins/conventions/PluginDocsSpec.groovy new file mode 100644 index 0000000..decfa91 --- /dev/null +++ b/conventions/src/test/groovy/grails/plugins/conventions/PluginDocsSpec.groovy @@ -0,0 +1,77 @@ +package grails.plugins.conventions + +import java.nio.file.Path + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.gradle.util.GradleVersion +import spock.lang.Requires +import spock.lang.Specification +import spock.lang.TempDir + +/** + * config.docs runs Groovydoc in the root project over the sources of the plugin project. Gradle 9 does not let a task + * resolve another project's configurations, so the aggregation must only resolve configurations of its own project. + */ +class PluginDocsSpec extends Specification { + + @TempDir + Path projectDir + + private void write(String path, String text) { + def file = projectDir.resolve(path).toFile() + file.parentFile.mkdirs() + file.text = text + } + + @Requires({ Runtime.version().feature() >= data.javaVersion }) + def 'config.docs aggregates the Groovydoc of a Grails #grailsVersion plugin project on Gradle #gradleVersion'() { + given: 'a root project with config.docs and a plugin project laid out like the template' + write('settings.gradle', ''' +dependencyResolutionManagement { + repositories { mavenCentral() } +} +rootProject.name = 'sample-root' +include 'sample' +''') + write('gradle.properties', "grailsVersion=${grailsVersion}\n") + write('build.gradle', "plugins { id 'config.docs' }\n") + write('sample/build.gradle', """ +plugins { id 'groovy' } +dependencies { + implementation platform('org.apache.grails:grails-bom:${grailsVersion}') + implementation 'org.apache.groovy:groovy' +} +""") + write('sample/src/main/groovy/sample/Greeter.groovy', ''' +package sample + +/** Greets people. */ +class Greeter { + String greet(String name) { "Hello ${name}" } +} +''') + + when: + def result = GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withPluginClasspath() + .withGradleVersion(gradleVersion) + // consumers build in parallel (org.gradle.parallel=true), where a task only holds the lock of its own project + .withArguments('aggregateGroovyApiDoc', '--parallel', '--stacktrace') + .forwardOutput() + .build() + + then: + result.task(':aggregateGroovyApiDoc').outcome == TaskOutcome.SUCCESS + projectDir.resolve('build/docs/gapi/sample/Greeter.html').toFile().file + + and: 'only the sources are taken from the plugin project, its own Groovydoc is not generated' + result.task(':sample:groovydoc') == null + + where: + gradleVersion | grailsVersion | javaVersion + GradleVersion.current().version | System.getProperty('grailsVersion') | 17 + System.getProperty('grails8Gradle') | System.getProperty('grails8Version') | 21 + } +}