This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch GROOVY-12019
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 5cd5b7ec403dc7e3fecc4e00852256cf9c79b02b
Author: Daniel Sun <[email protected]>
AuthorDate: Tue May 19 01:04:03 2026 +0900

    GROOVY-12019: Enable gradle configuration cache
---
 build-logic/build.gradle                           |   3 +-
 .../groovy/org.apache.groovy-asciidoctor.gradle    |   7 +
 .../src/main/groovy/org.apache.groovy-base.gradle  |  47 +++++--
 .../src/main/groovy/org.apache.groovy-core.gradle  |  41 +++---
 .../groovy/org.apache.groovy-distribution.gradle   |  38 ++++--
 .../groovy/org.apache.groovy-documented.gradle     |   8 +-
 .../main/groovy/org.apache.groovy-library.gradle   |  22 ++--
 .../org.apache.groovy-publish-validation.gradle    |   8 +-
 .../org.apache.groovy-published-library.gradle     |  15 ++-
 .../main/groovy/org.apache.groovy-tested.gradle    |  48 +++----
 .../apache/groovy/gradle/GroovydocAntPlugin.groovy | 102 +++++++++++----
 .../org/apache/groovy/gradle/JarJarTask.groovy     |  90 ++++++++++---
 .../groovy/gradle/PerformanceTestsExtension.groovy |  58 +++++++--
 .../groovy/gradle/SharedConfiguration.groovy       | 142 ++++++++++++++++++++-
 build.gradle                                       |   8 +-
 gradle.properties                                  |   8 ++
 gradle/verification-metadata.xml                   |  14 +-
 subprojects/groovy-ant/build.gradle                |   9 +-
 subprojects/groovy-binary/build.gradle             |  27 ++--
 subprojects/groovy-groovydoc/build.gradle          |   2 +-
 20 files changed, 530 insertions(+), 167 deletions(-)

diff --git a/build-logic/build.gradle b/build-logic/build.gradle
index b80d74e86c..109d26d6d3 100644
--- a/build-logic/build.gradle
+++ b/build-logic/build.gradle
@@ -27,12 +27,13 @@ repositories {
 }
 
 dependencies {
+    implementation 'biz.aQute.bnd:biz.aQute.bndlib:6.4.1'
     implementation 'org.asciidoctor:asciidoctor-gradle-jvm:4.0.5'
     implementation 'org.asciidoctor:asciidoctor-gradle-jvm-pdf:4.0.5'
     implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:6.0.4'
     implementation 'org.nosphere.apache:creadur-rat-gradle:0.8.1'
     implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.4.2'
-    implementation 'me.champeau.jmh:jmh-gradle-plugin:0.7.2'
+    implementation 'me.champeau.jmh:jmh-gradle-plugin:0.7.3'
     implementation 'org.cyclonedx:cyclonedx-gradle-plugin:3.0.2'
     implementation "com.fasterxml.jackson:jackson-bom:2.21.3" // later version 
for cyclonedx
     implementation "org.slf4j:slf4j-api:2.0.17" // later version for cyclonedx
diff --git a/build-logic/src/main/groovy/org.apache.groovy-asciidoctor.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-asciidoctor.gradle
index f269a28fdf..3ac2b10a89 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-asciidoctor.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-asciidoctor.gradle
@@ -40,6 +40,13 @@ configurations {
 }
 
 tasks.withType(AbstractAsciidoctorTask).configureEach {
+    // asciidoctor-gradle-jvm 4.0.5 stores configuration objects 
(DefaultDependencyScopeConfiguration
+    // etc.) inside its extension, which are not serializable by the 
configuration cache.
+    // Remove this call once the plugin ships a CC-compatible release.
+    notCompatibleWithConfigurationCache(
+        'asciidoctor-gradle-jvm 4.0.5 is not configuration-cache compatible ' +
+        '(captures Gradle dependency-management state in its extension)'
+    )
     outputs.cacheIf { true }
     
usesService(ConcurrentExecutionControlBuildService.restrict(AbstractAsciidoctorTask,
 gradle))
     configurations 'asciidocExtensions'
diff --git a/build-logic/src/main/groovy/org.apache.groovy-base.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-base.gradle
index b3135b82e3..9386ea793a 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-base.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-base.gradle
@@ -36,7 +36,6 @@ plugins {
     id 'org.apache.groovy-common'
     id 'org.apache.groovy-internal'
     id 'org.apache.groovy-tested'
-    id 'org.apache.groovy-asciidoctor'
 }
 
 /**
@@ -47,6 +46,13 @@ if (sharedConfiguration.hasCodeCoverage.get()) {
     pluginManager.apply(JacocoPlugin)
 }
 
+if (sharedConfiguration.isDocumentationBuild && 
layout.projectDirectory.dir('src/spec/doc').asFile.isDirectory()) {
+    // asciidoctor-gradle-jvm 4.0.5 still emits a Gradle 9 deprecation during 
apply.
+    // Keep docs builds opt-in, but leave warnings enabled so unrelated 
deprecations stay visible.
+    // TODO: Drop this note once the build can move to asciidoctor-gradle-jvm 
4.0.6+.
+    pluginManager.apply('org.apache.groovy-asciidoctor')
+}
+
 def groovyLibrary = project.extensions.create('groovyLibrary', 
GroovyLibraryExtension, sharedConfiguration, java)
 
 java {
@@ -97,6 +103,16 @@ configurations {
             }
         }
     }
+    asciidocElements {
+        canBeConsumed = true
+        canBeResolved = false
+        attributes {
+            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, 
Category.DOCUMENTATION))
+            attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 
DocsType.USER_MANUAL))
+        }
+        // No artifact — projects without src/spec/doc produce an empty 
variant.
+        // org.apache.groovy-asciidoctor.gradle adds the actual artifact when 
applied.
+    }
     javadocClasspath {
         canBeConsumed = true
         canBeResolved = false
@@ -185,10 +201,14 @@ tasks.withType(Jar).configureEach { jar ->
 tasks.register('jarjar', JarJarTask) {
     String projectName = project.name
     from = jar.archiveFile
+    // Eagerly resolve the module list so the componentFilter closure captures 
only a
+    // plain List<String> — never the GroovyLibraryExtension object — keeping 
the task
+    // compatible with the Gradle configuration cache.
+    List<String> repackagedDeps = groovyLibrary.repackagedDependencies.get()
     repackagedLibraries.from 
configurations.runtimeClasspath.incoming.artifactView {
         componentFilter { component ->
             if (component instanceof ModuleComponentIdentifier) {
-                return component.module in 
groovyLibrary.repackagedDependencies.get()
+                return component.module in repackagedDeps
             }
             return false
         }
@@ -213,12 +233,23 @@ tasks.register('jarjar', JarJarTask) {
     ]
     outputFile = tasks.named('jar').flatMap { 
layout.buildDirectory.file("libs/${it.archiveBaseName.get()}-${it.archiveVersion.get()}${(it.archiveClassifier.get()
 && it.archiveClassifier.get() != 'raw') ? '-' + it.archiveClassifier.get() : 
''}.jar") }
 
-    withManifest {
-        String autoModName = "org.apache.${projectName.replace('-','.')}"
-        attributes('Automatic-Module-Name': autoModName, 'Bundle-Name': 
"Groovy module: $projectName")
-        groovyLibrary.configureManifest(it, excludedFromManifest)
-        classpath = configurations.runtimeClasspath
-    }
+    // All manifest data is stored as plain serializable values so the task is
+    // compatible with the Gradle configuration cache.
+    bndClasspath.from(configurations.runtimeClasspath)
+    String gbv = sharedConfiguration.groovyBundleVersion.get()
+    bndInstruction('Automatic-Module-Name', 
"org.apache.${projectName.replace('-', '.')}")
+    bndInstruction('Bundle-Name', "Groovy module: $projectName")
+    bndInstruction('Bundle-ManifestVersion', '2')
+    bndInstruction('Bundle-Description', 'Groovy Runtime')
+    bndInstruction('Bundle-Vendor', 'The Apache Software Foundation')
+    bndInstruction('Bundle-Version', gbv)
+    bndInstruction('Bundle-License', 'Apache-2.0')
+    bndInstruction('Specification-Title', 'Groovy: a powerful, multi-faceted 
language for the JVM')
+    bndInstruction('Specification-Vendor', 'The Apache Software Foundation')
+    bndInstruction('Specification-Version', gbv)
+    bndInstruction('Implementation-Title', 'Groovy: a powerful, multi-faceted 
language for the JVM')
+    bndInstruction('Implementation-Vendor', 'The Apache Software Foundation')
+    bndInstruction('Implementation-Version', gbv)
 }
 
 tasks.withType(AbstractCompile).configureEach {
diff --git a/build-logic/src/main/groovy/org.apache.groovy-core.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-core.gradle
index e3726d6510..7f2484c363 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-core.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-core.gradle
@@ -72,11 +72,12 @@ tasks.named('jar') {
 
 tasks.named('jarjar') { JarJarTask jjt ->
     def groovyBundleVersion = sharedConfiguration.groovyBundleVersion.get()
-    jjt.withManifest {
-        instruction '-nouses', 'true'
-        instruction 'Export-Package', "*;version=${groovyBundleVersion}"
-        instruction 'Eclipse-ExtensibleAPI', 'true' // GROOVY-8713, 
GROOVY-11582
-    }
+    jjt.bndInstruction('-nouses', 'true')
+    jjt.bndInstruction('Export-Package', "*;version=${groovyBundleVersion}")
+    jjt.bndInstruction('Eclipse-ExtensibleAPI', 'true')  // GROOVY-8713, 
GROOVY-11582
+    jjt.bndInstruction('DynamicImport-Package', '*')     // GROOVY-3192
+    jjt.bndInstruction('Eclipse-BuddyPolicy', 'dependent') // GROOVY-5571
+    jjt.bndInstruction('Main-Class', 'groovy.ui.GroovyMain')
 }
 
 // Gradle classloading magic with Groovy will only work if it finds a *jar*
@@ -92,9 +93,9 @@ def bootstrapJar = tasks.register('bootstrapJar', Jar) {
 
 // The main Groovy compile tasks has a special setup because
 // it uses the "bootstrap compiler"
-tasks.withType(GroovyCompile).configureEach {
+tasks.withType(GroovyCompile).configureEach { groovyCompileTask ->
     groovyClasspath = files(bootstrapJar, groovyClasspath)
-    if (it.name == 'compileGroovy') {
+    if (groovyCompileTask.name == 'compileGroovy') {
         classpath = files(bootstrapJar, classpath)
     }
     options.incremental = true
@@ -112,6 +113,8 @@ interface CoreServices {
 }
 
 def execOperations = objects.newInstance(CoreServices).execOperations
+def bridgerClasspath = files(rootProject.configurations.tools)
+def classesToBridge = groovyCore.classesToBridge
 
 tasks.named('compileJava') {
     options.fork = true
@@ -120,9 +123,9 @@ tasks.named('compileJava') {
 
     doLast {
         execOperations.javaexec { spec ->
-            spec.classpath(rootProject.configurations.tools)
+            spec.classpath(bridgerClasspath)
             spec.mainClass = 'org.jboss.bridger.Bridger'
-            spec.args(groovyCore.classesToBridge.asList().collect { 
it.absolutePath })
+            spec.args(classesToBridge.asList().collect { it.absolutePath })
         }
     }
 }
@@ -159,16 +162,16 @@ def generateGrammarSourceTask = 
tasks.named("generateGrammarSource") {
 
     doLast {
         def parserFilePattern = 'Groovy*'
-        def outputPath = generateGrammarSource.outputDirectory.canonicalPath
-        def parserPackagePath = 
"${outputPath}/${PARSER_PACKAGE_NAME.replace('.', '/')}"
-        file(parserPackagePath).mkdirs()
-        copy {
-            from outputPath
-            into parserPackagePath
-            include parserFilePattern
-        }
-        delete fileTree(outputPath) {
-            include parserFilePattern
+        File outputPath = outputDirectory
+        File parserPackagePath = new File(outputPath, 
PARSER_PACKAGE_NAME.replace('.', '/'))
+        parserPackagePath.mkdirs()
+        outputPath.listFiles()?.findAll { it.name.startsWith('Groovy') }?.each 
{ file ->
+            java.nio.file.Files.copy(
+                    file.toPath(),
+                    new File(parserPackagePath, file.name).toPath(),
+                    java.nio.file.StandardCopyOption.REPLACE_EXISTING
+            )
+            file.delete()
         }
     }
 }
diff --git a/build-logic/src/main/groovy/org.apache.groovy-distribution.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-distribution.gradle
index 46ad218108..4d46ba5897 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-distribution.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-distribution.gradle
@@ -26,10 +26,15 @@ plugins {
     id 'org.apache.groovy-common'
     id 'org.apache.groovy-aggregating-project'
     id 'org.apache.groovy-doc-aggregator'
-    id 'org.asciidoctor.jvm.pdf'
 }
 
 def distributionExtension = project.extensions.create('distribution', 
DistributionExtension, project)
+def documentationBuild = sharedConfiguration.isDocumentationBuild && 
layout.projectDirectory.dir('src/spec/doc').asFile.isDirectory()
+
+if (documentationBuild) {
+    pluginManager.apply('org.apache.groovy-asciidoctor')
+    pluginManager.apply('org.asciidoctor.jvm.pdf')
+}
 
 configurations {
     baseProjects {
@@ -133,6 +138,9 @@ def distBin = tasks.register('distBin', Zip) {
 
 def distSdk = tasks.register("distSdk", Zip) {
     def groovyBundleVersion = sharedConfiguration.groovyBundleVersion.get()
+    // Capture project.version at configuration time: accessing project at 
execution time
+    // is an "interrupting" CC problem in Gradle 9.x and causes a 
Gradle-internal exception.
+    def projectVersion = project.version.toString()
     description = 'Generates the binary, sources, documentation and full 
distributions'
     archiveBaseName = 'apache-groovy'
     duplicatesStrategy = DuplicatesStrategy.EXCLUDE
@@ -149,10 +157,9 @@ def distSdk = tasks.register("distSdk", Zip) {
         with distributionExtension.srcSpec
     }
     doFirst {
-        def av = project.version.toString()
-        if ((av.endsWith('SNAPSHOT') && 
!groovyBundleVersion.endsWith('SNAPSHOT'))
-                || (!av.endsWith('SNAPSHOT') && 
groovyBundleVersion.endsWith('SNAPSHOT'))) {
-            throw new GradleException("Incoherent versions. Found 
groovyVersion=$av and groovyBundleVersion=${versions.groovyBundle}")
+        if ((projectVersion.endsWith('SNAPSHOT') && 
!groovyBundleVersion.endsWith('SNAPSHOT'))
+                || (!projectVersion.endsWith('SNAPSHOT') && 
groovyBundleVersion.endsWith('SNAPSHOT'))) {
+            throw new GradleException("Incoherent versions. Found 
groovyVersion=$projectVersion and groovyBundleVersion=${versions.groovyBundle}")
         }
     }
 }
@@ -198,21 +205,28 @@ tasks.register("installGroovy", Sync) {
 }
 
 tasks.register("doc") {
-    dependsOn 'javadocAll', 'groovydocAll', 'docGDK', 'asciidocAll', 
'asciidoctorPdf'
+    dependsOn 'javadocAll', 'groovydocAll', 'docGDK'
+    if (documentationBuild) {
+        dependsOn 'asciidocAll', 'asciidoctorPdf'
+    }
 }
 
 tasks.register("asciidocAll", Copy) {
     from configurations.allAsciidoc
-    from tasks.named('asciidoctor')
+    if (documentationBuild) {
+        from tasks.named('asciidoctor')
+    }
     into layout.buildDirectory.dir("asciidocAll/html5")
     duplicatesStrategy = DuplicatesStrategy.EXCLUDE
 }
 
-tasks.named('asciidoctorPdf') {
-    baseDirFollowsSourceFile()
-    logDocuments = true
-    sourceDir = file('src/spec/doc')
-    outputDir = layout.buildDirectory.dir("asciidocAll/pdf")
+if (documentationBuild) {
+    tasks.named('asciidoctorPdf') {
+        baseDirFollowsSourceFile()
+        logDocuments = true
+        sourceDir = file('src/spec/doc')
+        outputDir = layout.buildDirectory.dir("asciidocAll/pdf")
+    }
 }
 
 // The Groovy distribution module isn't a Java library
diff --git a/build-logic/src/main/groovy/org.apache.groovy-documented.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-documented.gradle
index 3b43cdde34..4cde3d6929 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-documented.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-documented.gradle
@@ -63,9 +63,11 @@ tasks.withType(Javadoc).configureEach {
         if (JavaVersion.current() >= JavaVersion.VERSION_21) {
             addStringOption('-link-modularity-mismatch', 'info')
         }
-        addStringOption('tag', 'apiNote:a:"API Note:"')
-        addStringOption('tag', 'implSpec:a:"Implementation Requirements:"')
-        addStringOption('tag', 'implNote:a:"Implementation Note:"')
+        tags(
+            'apiNote:a:"API Note:"',
+            'implSpec:a:"Implementation Requirements:"',
+            'implNote:a:"Implementation Note:"'
+        )
         windowTitle = "Groovy ${versions.groovy}"
         docTitle = "Groovy ${versions.groovy}"
         classpath += project.file('src/main/java') // to pick up package.html
diff --git a/build-logic/src/main/groovy/org.apache.groovy-library.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-library.gradle
index 12232b12fe..860b14392d 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-library.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-library.gradle
@@ -32,18 +32,16 @@ dependencies {
 
 tasks.named('jarjar') { JarJarTask jjt ->
     def groovyBundleVersion = sharedConfiguration.groovyBundleVersion.get()
-    jjt.withManifest {
-        instruction '-nouses', 'true'
-        instruction 'Export-Package', "*;version=${groovyBundleVersion}"
-        instruction 'Fragment-Host', 'groovy' // GROOVY-9402, GROOVY-11570
-        def folder = file("${projectDir}/src/main/resources/META-INF/services")
-        if (folder.exists() && folder.listFiles().count { it.name ==~ 
/^(?!(org.codehaus.groovy.transform.ASTTransformation)$).*$/ } > 0) {
-            instruction 'Require-Capability', 
'osgi.extender;filter:="(osgi.extender=osgi.serviceloader.processor)"'
-            instruction 'Require-Capability', 
'osgi.extender;filter:="(osgi.extender=osgi.serviceloader.registrar)"'
-            
folder.eachFileMatch(~/^(?!(org.codehaus.groovy.transform.ASTTransformation)$).*$/)
 {
-                instruction 'Require-Capability', 
"osgi.serviceloader;filter:=\"(osgi.serviceloader=${it.name})\";cardinality:=multiple"
-                instruction 'Provide-Capability', 
"osgi.serviceloader;osgi.serviceloader=\"${it.name}\""
-            }
+    jjt.bndInstruction('-nouses', 'true')
+    jjt.bndInstruction('Export-Package', "*;version=${groovyBundleVersion}")
+    jjt.bndInstruction('Fragment-Host', 'groovy') // GROOVY-9402, GROOVY-11570
+    def folder = file("${projectDir}/src/main/resources/META-INF/services")
+    if (folder.exists() && folder.listFiles().count { it.name ==~ 
/^(?!(org.codehaus.groovy.transform.ASTTransformation)$).*$/ } > 0) {
+        jjt.appendBndInstruction('Require-Capability', 
'osgi.extender;filter:="(osgi.extender=osgi.serviceloader.processor)"')
+        jjt.appendBndInstruction('Require-Capability', 
'osgi.extender;filter:="(osgi.extender=osgi.serviceloader.registrar)"')
+        
folder.eachFileMatch(~/^(?!(org.codehaus.groovy.transform.ASTTransformation)$).*$/)
 {
+            jjt.appendBndInstruction('Require-Capability', 
"osgi.serviceloader;filter:=\"(osgi.serviceloader=${it.name})\";cardinality:=multiple")
+            jjt.appendBndInstruction('Provide-Capability', 
"osgi.serviceloader;osgi.serviceloader=\"${it.name}\"")
         }
     }
 }
diff --git 
a/build-logic/src/main/groovy/org.apache.groovy-publish-validation.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-publish-validation.gradle
index ecb6c05717..8887f99804 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-publish-validation.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-publish-validation.gradle
@@ -18,10 +18,12 @@
  */
 
 tasks.withType(PublishToMavenRepository).configureEach {
+    def isReleaseVersion = sharedConfiguration.isReleaseVersion.get()
     doLast {
-        if (sharedConfiguration.isReleaseVersion.get()) {
-            pluginManager.withPlugin('java') {
-                
project.configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.each
 {
+        if (isReleaseVersion) {
+            def publishedProject = delegate.project
+            if (publishedProject.pluginManager.hasPlugin('java')) {
+                
publishedProject.configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.each
 {
                     if (it.moduleVersion.id.version.endsWith("-SNAPSHOT")) {
                         throw new GradleException("Found snapshot dependency 
for non-snapshot Groovy: " + it.moduleVersion)
                     }
diff --git 
a/build-logic/src/main/groovy/org.apache.groovy-published-library.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-published-library.gradle
index b07d5717d0..4804167999 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-published-library.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-published-library.gradle
@@ -21,6 +21,7 @@ import org.cyclonedx.model.License
 import org.cyclonedx.model.LicenseChoice
 import org.cyclonedx.model.OrganizationalContact
 import org.cyclonedx.model.OrganizationalEntity
+import org.gradle.api.publish.maven.tasks.PublishToMavenRepository
 
 plugins {
     id 'maven-publish'
@@ -819,9 +820,7 @@ publishing {
 }
 
 signing {
-    required = {
-        sharedConfiguration.signing.shouldSign(gradle.taskGraph)
-    }
+    required = sharedConfiguration.signing.shouldSign()
     sign publishing.publications.maven
     if (sharedConfiguration.signing.useGpgCmd.get()) {
         useGpgCmd()
@@ -829,7 +828,9 @@ signing {
 }
 
 gradle.taskGraph.whenReady { taskGraph ->
-    if (sharedConfiguration.signing.shouldSign(gradle.taskGraph)) {
+    boolean shouldSign = sharedConfiguration.signing.shouldSign(taskGraph)
+    signing.required = shouldSign
+    if (shouldSign) {
         // Use Java 6's console or Swing to read input (not suitable for CI)
         if (!sharedConfiguration.signing.hasAllKeyDetails()) {
             printf '\n\nWe have to sign some things in this build.' +
@@ -858,6 +859,12 @@ gradle.taskGraph.whenReady { taskGraph ->
     }
 }
 
+tasks.withType(PublishToMavenRepository).configureEach {
+    if (repository.name == 'LocalFile') {
+        mustRunAfter(rootProject.tasks.named('clean'))
+    }
+}
+
 String promptUser(String prompt) {
     def response = ''
     if (System.console() != null) {
diff --git a/build-logic/src/main/groovy/org.apache.groovy-tested.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-tested.gradle
index afc0cf7a22..c76139adcc 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-tested.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-tested.gradle
@@ -57,9 +57,9 @@ def aggregator = TestResultAggregatorService.register(
 // so a developer's polluted ~/.groovy/grapes doesn't leak into tests.
 // Enable on CI with: ./gradlew test -Pgroovy.grape.bridge-cache=true
 def grapeBridgeCache = (findProperty('groovy.grape.bridge-cache') ?:
-        System.properties['groovy.grape.bridge-cache']) == 'true'
+        providers.systemProperty('groovy.grape.bridge-cache').orNull) == 'true'
 
-tasks.withType(Test).configureEach {
+tasks.withType(Test).configureEach { testTask ->
     def fs = objects.newInstance(TestServices).fileSystemOperations
     def grapeDirectory = new File(temporaryDir, '.groovy')
     def options = ['-ea', "-Xms${groovyJUnit_ms}", "-Xmx${groovyJUnit_mx}",
@@ -80,7 +80,7 @@ tasks.withType(Test).configureEach {
     systemProperty 'http.agent',
         "Apache-Maven/3.9.14 (Java ${System.getProperty('java.version')}; 
${System.getProperty('os.name')} ${System.getProperty('os.version')})"
     systemProperty 'groovy.force.illegal.access', 
findProperty('groovy.force.illegal.access')
-    def testdb = System.properties['groovy.testdb.props']
+    def testdb = providers.systemProperty('groovy.testdb.props').orNull
     if (testdb) {
         systemProperty 'groovy.testdb.props', testdb
     }
@@ -88,22 +88,21 @@ tasks.withType(Test).configureEach {
     // to the test JVM. Lets users override Grape behaviour — debug flags, 
future
     // settings — without adding one-off plumbing per flag. Project properties 
(-P)
     // win over system properties (-D) when both are set, matching Gradle 
convention.
+    // providers.systemPropertiesPrefixedBy() is CC-tracked (unlike 
System.properties),
+    // so changes to -D groovy.grape.* flags correctly invalidate the cache 
entry.
     def grapeProps = new LinkedHashMap<String, String>()
     project.properties.each { k, v ->
         if (k.toString().startsWith('groovy.grape.') && v != null) {
             grapeProps[k.toString()] = v.toString()
         }
     }
-    System.properties.each { k, v ->
-        if (k.toString().startsWith('groovy.grape.') && v != null) {
-            grapeProps.putIfAbsent(k.toString(), v.toString())
-        }
+    providers.systemPropertiesPrefixedBy('groovy.grape.').get().each { k, v ->
+        grapeProps.putIfAbsent(k, v)
     }
     grapeProps.each { key, value ->
         systemProperty key, value
     }
-    def headless = System.properties['java.awt.headless']
-    if (headless == 'true') {
+    if (providers.systemProperty('java.awt.headless').orNull == 'true') {
         systemProperty 'java.awt.headless', 'true'
     }
     systemProperty 'apple.awt.UIElement', 'true'
@@ -132,7 +131,7 @@ tasks.withType(Test).configureEach {
     scanForTestClasses = true
     ignoreFailures = false
     classpath = files('src/test/groovy') + classpath
-    exclude buildExcludeFilter(it.name == 'test')
+    exclude buildExcludeFilter(testTask.name == 'test')
     ext.resultText = ''
 
     testLogging {
@@ -144,6 +143,7 @@ tasks.withType(Test).configureEach {
     useJUnitPlatform()
     usesService(aggregator)
     usesService(ConcurrentExecutionControlBuildService.restrict(Test, gradle, 
2))
+    File projectDirectory = project.layout.projectDirectory.asFile
 
     doFirst {
         fs.delete {
@@ -164,7 +164,7 @@ tasks.withType(Test).configureEach {
                     into(new File(grapeDirectory, 'grapes'))
                     exclude '**/*.lck', '**/*.lastUpdated'
                 }
-                logger.lifecycle "Bridged ~/.groovy/grapes -> 
${grapeDirectory}/grapes"
+                testTask.logger.lifecycle "Bridged ~/.groovy/grapes -> 
${grapeDirectory}/grapes"
             }
             // Also bridge ~/.m2/repository so the test JVM's localm2 Ivy 
resolver
             // (configured as file:${user.home}/.m2/repository/) finds 
artifacts
@@ -181,10 +181,10 @@ tasks.withType(Test).configureEach {
                     into(m2Target)
                     exclude '**/*.lastUpdated', '**/_remote.repositories'
                 }
-                logger.lifecycle "Bridged ~/.m2/repository -> ${m2Target}"
+                testTask.logger.lifecycle "Bridged ~/.m2/repository -> 
${m2Target}"
             }
         }
-        logger.debug "Grape directory: ${grapeDirectory.absolutePath}"
+        testTask.logger.debug "Grape directory: ${grapeDirectory.absolutePath}"
     }
 
     doLast {
@@ -204,7 +204,7 @@ tasks.withType(Test).configureEach {
             }
         }
         fs.delete {
-            delete(files(".").filter { it.name.endsWith '.class' })
+            delete(projectDirectory.listFiles()?.findAll { 
it.name.endsWith('.class') } ?: [])
         }
     }
 
@@ -215,7 +215,7 @@ tasks.withType(Test).configureEach {
                 def green = '\u001B[32m'
                 def yellow = '\u001B[33m'
                 def reset = '\u001B[0m'
-                logger.lifecycle "${desc.name}: 
${green}${result.successfulTestCount} passed${reset}, " +
+                testTask.logger.lifecycle "${desc.name}: 
${green}${result.successfulTestCount} passed${reset}, " +
                     "${yellow}${result.skippedTestCount} skipped${reset} " +
                     "(${result.testCount} tests)"
             }
@@ -278,15 +278,15 @@ Closure buildExcludeFilter(boolean legacyTestSuite) {
 
 // Warn (rather than silently NO-SOURCE) when the groovy/grape exclusion
 // swallows the entirety of a project's test source set.
-gradle.taskGraph.whenReady { graph ->
-    def testTask = tasks.named('test').get()
-    if (!graph.hasTask(testTask)) return
-    if (providers.systemProperty('junit.network').getOrNull()) return
-    boolean hasGrapeTests = sourceSets.test.allSource.srcDirs.any {
-        new File(it, 'groovy/grape').isDirectory()
-    }
-    if (hasGrapeTests) {
-        logger.warn("WARNING: ${testTask.path} will skip groovy/grape/* tests; 
set -Djunit.network=true to include them")
+boolean hasGrapeTests = sourceSets.test.allSource.srcDirs.any {
+    new File(it, 'groovy/grape').isDirectory()
+}
+def junitNetwork = providers.systemProperty('junit.network')
+tasks.named('test', Test).configure { testTask ->
+    doFirst {
+        if (!junitNetwork.getOrNull() && hasGrapeTests) {
+            testTask.logger.warn("WARNING: ${testTask.path} will skip 
groovy/grape/* tests; set -Djunit.network=true to include them")
+        }
     }
 }
 
diff --git 
a/build-logic/src/main/groovy/org/apache/groovy/gradle/GroovydocAntPlugin.groovy
 
b/build-logic/src/main/groovy/org/apache/groovy/gradle/GroovydocAntPlugin.groovy
index dab78236fa..2c6f518091 100644
--- 
a/build-logic/src/main/groovy/org/apache/groovy/gradle/GroovydocAntPlugin.groovy
+++ 
b/build-logic/src/main/groovy/org/apache/groovy/gradle/GroovydocAntPlugin.groovy
@@ -20,6 +20,7 @@ package org.apache.groovy.gradle
 
 import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
+import groovy.ant.AntBuilder
 
 import org.gradle.api.GradleException
 import org.gradle.api.Plugin
@@ -70,6 +71,24 @@ class GroovydocAntPlugin implements Plugin<Project> {
     @CompileDynamic
     private static void configureGroovydocTasks(Project project, 
GroovydocAntExtension extension) {
         project.tasks.withType(Groovydoc).configureEach { gdoc ->
+            def conventionalSourceDirs = project.files('src/main/groovy', 
'src/main/java')
+            if (!gdoc.ext.has('groovydocSourceDirs')) {
+                SourceSetContainer sourceSets = 
project.extensions.findByType(SourceSetContainer)
+                def main = sourceSets?.findByName('main')
+                if (main != null) {
+                    def configuredSourceDirs = 
project.files(main.groovy.srcDirs + main.java.srcDirs)
+                    if (!configuredSourceDirs.files.empty) {
+                        gdoc.ext.groovydocSourceDirs = configuredSourceDirs
+                    }
+                }
+                if (!gdoc.ext.has('groovydocSourceDirs')) {
+                    if (!conventionalSourceDirs.files.empty) {
+                        gdoc.ext.groovydocSourceDirs = conventionalSourceDirs
+                    }
+                }
+            }
+            def antSourceDirs = resolveSourceDirectoriesInput(project, gdoc, 
conventionalSourceDirs)
+
             gdoc.inputs.property('antJavaVersion', 
extension.javaVersion.orElse(''))
             gdoc.inputs.property('antShowInternal', extension.showInternal)
             gdoc.inputs.property('antNoIndex', extension.noIndex)
@@ -81,6 +100,9 @@ class GroovydocAntPlugin implements Plugin<Project> {
             gdoc.inputs.files(extension.additionalStylesheets)
                     .withPropertyName('antAdditionalStylesheets')
                     .optional(true)
+            gdoc.inputs.files(project.files(antSourceDirs))
+                    .withPropertyName('antSourceDirs')
+                    .optional(true)
 
             if (!extension.useAntBuilder.get()) {
                 return
@@ -88,17 +110,21 @@ class GroovydocAntPlugin implements Plugin<Project> {
 
             gdoc.actions.clear()
             gdoc.doLast {
-                executeGroovydoc(gdoc, extension, project)
+                GroovydocAntPlugin.executeGroovydoc(delegate as Groovydoc, 
extension, antSourceDirs)
             }
         }
     }
 
     @CompileDynamic
-    private static void executeGroovydoc(Groovydoc gdoc, GroovydocAntExtension 
extension, Project project) {
+    private static void executeGroovydoc(Groovydoc gdoc, GroovydocAntExtension 
extension, Object sourceDirectories) {
         File destDir = resolveFile(gdoc.destinationDir)
         destDir.mkdirs()
 
-        List<File> sourceDirs = resolveSourceDirectories(gdoc, project)
+        def runtimeSourceDirectories = gdoc.ext.has('groovydocSourceDirs') ? 
gdoc.ext.groovydocSourceDirs : sourceDirectories
+        List<File> sourceDirs = 
normalizeSourceDirectories(runtimeSourceDirectories, gdoc.name)
+        if (sourceDirs.isEmpty()) {
+            sourceDirs = inferSourceDirectories(gdoc.source.files)
+        }
         if (sourceDirs.isEmpty()) {
             throw new GradleException(
                     "Groovydoc task '${gdoc.name}': no source directories 
found. " +
@@ -113,7 +139,9 @@ class GroovydocAntPlugin implements Plugin<Project> {
             )
         }
 
-        project.ant.taskdef(
+        AntBuilder ant = new AntBuilder()
+
+        ant.taskdef(
                 name: 'groovydoc',
                 classname: 'org.codehaus.groovy.ant.Groovydoc',
                 classpath: classpath.asPath
@@ -150,8 +178,8 @@ class GroovydocAntPlugin implements Plugin<Project> {
         def overviewText = gdoc.overviewText
         if (overviewText != null) {
             File overviewTmp = new File(
-                    project.layout.buildDirectory.get().asFile,
-                    "tmp/${gdoc.name}/overview.html"
+                    gdoc.temporaryDir,
+                    'overview.html'
             )
             overviewTmp.parentFile.mkdirs()
             overviewTmp.text = overviewText.asString()
@@ -161,7 +189,7 @@ class GroovydocAntPlugin implements Plugin<Project> {
         def links = gdoc.links ?: []
         def extraStylesheets = extension.additionalStylesheets.files
 
-        project.ant.groovydoc(antArgs) {
+        ant.groovydoc(antArgs) {
             links.each { l ->
                 link(packages: (l.packages ?: []).join(','), href: l.url ?: '')
             }
@@ -172,31 +200,55 @@ class GroovydocAntPlugin implements Plugin<Project> {
     }
 
     @CompileDynamic
-    private static List<File> resolveSourceDirectories(Groovydoc gdoc, Project 
project) {
+    private static Object resolveSourceDirectoriesInput(Project project, 
Groovydoc gdoc, FileCollection conventionalSourceDirs) {
         def override = gdoc.ext.has('groovydocSourceDirs') ? 
gdoc.ext.groovydocSourceDirs : null
         if (override != null) {
-            Collection<File> files
-            if (override instanceof FileCollection) {
-                files = ((FileCollection) override).files
-            } else if (override instanceof Collection) {
-                files = (Collection<File>) override
-            } else {
-                files = project.files(override).files
-            }
-            return files.findAll { it.exists() }.unique() as List<File>
+            return override
         }
 
         SourceSetContainer sourceSets = 
project.extensions.findByType(SourceSetContainer)
-        if (sourceSets != null) {
-            def main = sourceSets.findByName('main')
-            if (main != null) {
-                List<File> dirs = []
-                dirs.addAll(main.groovy.srcDirs.findAll { it.exists() })
-                dirs.addAll(main.java.srcDirs.findAll { it.exists() })
-                return dirs.unique()
+        def main = sourceSets?.findByName('main')
+        if (main != null) {
+            def configuredSourceDirs = project.files(main.groovy.srcDirs + 
main.java.srcDirs)
+            if (!configuredSourceDirs.files.empty) {
+                return configuredSourceDirs
             }
         }
-        []
+        conventionalSourceDirs
+    }
+
+    @CompileDynamic
+    private static List<File> normalizeSourceDirectories(Object value, String 
taskName) {
+        if (value == null) {
+            return []
+        }
+        Collection<File> files
+        if (value instanceof FileCollection) {
+            files = ((FileCollection) value).files
+        } else if (value instanceof Collection) {
+            files = (Collection<File>) value
+        } else {
+            throw new GradleException("Groovydoc task '${taskName}': source 
directories must be a FileCollection or Collection<File>.")
+        }
+        files.findAll { it.exists() }.unique() as List<File>
+    }
+
+    @CompileDynamic
+    private static List<File> inferSourceDirectories(Collection<File> files) {
+        files.collectMany { File file ->
+            File dir = file.directory ? file : file.parentFile
+            if (dir == null) {
+                return []
+            }
+            String path = dir.absolutePath.replace('\\', '/')
+            for (String marker : ['/src/main/java', '/src/main/groovy', 
'/src/antlr', '/build/generated/sources/antlr4']) {
+                int idx = path.indexOf(marker)
+                if (idx >= 0) {
+                    return [new File(path.substring(0, idx + marker.length()))]
+                }
+            }
+            [dir]
+        }.findAll { it.exists() }.unique() as List<File>
     }
 
     @CompileDynamic
diff --git 
a/build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy 
b/build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy
index cc24c8873f..ce199744d0 100644
--- a/build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy
+++ b/build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy
@@ -18,16 +18,16 @@
  */
 package org.apache.groovy.gradle
 
+import aQute.bnd.osgi.Analyzer
 import groovy.transform.AutoFinal
 
 import javax.inject.Inject
 
-import org.gradle.api.Action
 import org.gradle.api.DefaultTask
 import org.gradle.api.file.ConfigurableFileCollection
 import org.gradle.api.file.FileSystemOperations
 import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.java.archives.Manifest
+import org.gradle.api.model.ObjectFactory
 import org.gradle.api.tasks.CacheableTask
 import org.gradle.api.tasks.Classpath
 import org.gradle.api.tasks.Input
@@ -46,19 +46,25 @@ class JarJarTask extends DefaultTask {
 
     @InputFile
     @Classpath
-    final RegularFileProperty from = project.objects.fileProperty()
+    final RegularFileProperty from
 
     @InputFiles
     @Classpath
-    final ConfigurableFileCollection repackagedLibraries = 
project.objects.fileCollection()
+    final ConfigurableFileCollection repackagedLibraries
 
     @InputFiles
     @Classpath
-    final ConfigurableFileCollection jarjarToolClasspath = 
project.objects.fileCollection()
+    final ConfigurableFileCollection jarjarToolClasspath
 
-    final protected osgi = project.rootProject.extensions.osgi
+    /**
+     * Classpath passed to the BND {@link Analyzer} for OSGi import resolution.
+     * Typically the project's {@code runtimeClasspath}.
+     */
+    @Classpath
+    final ConfigurableFileCollection bndClasspath
 
-    final protected String projectName = project.name
+    @Internal
+    final String projectName
 
     @Input
     @Optional
@@ -84,22 +90,53 @@ class JarJarTask extends DefaultTask {
     Map<String, String> includedResources = [:]
 
     @OutputFile
-    final RegularFileProperty outputFile = project.objects.fileProperty()
+    final RegularFileProperty outputFile
 
     @Input
     boolean createManifest = true
 
-    private final FileSystemOperations fs
+    /**
+     * BND instructions used when generating the OSGi manifest.
+     * Entries override the default analyzer properties unless explicitly 
appended
+     * through {@link #appendBndInstruction(String, String)}.
+     */
+    @Input
+    Map<String, String> bndInstructions = [:]
 
-    private List<Action<? super Manifest>> manifestTweaks = []
+    private final FileSystemOperations fs
 
     @Inject
-    JarJarTask(FileSystemOperations fileSystemOperations) {
+    JarJarTask(ObjectFactory objects, FileSystemOperations 
fileSystemOperations) {
         this.fs = fileSystemOperations
+        this.from = objects.fileProperty()
+        this.repackagedLibraries = objects.fileCollection()
+        this.jarjarToolClasspath = objects.fileCollection()
+        this.bndClasspath = objects.fileCollection()
+        this.outputFile = objects.fileProperty()
+        this.projectName = project.name
     }
 
-    void withManifest(Action<? super Manifest> action) {
-        manifestTweaks.add(action)
+    /**
+     * Sets a BND manifest instruction, replacing any previous value for 
{@code key}.
+     */
+    void bndInstruction(String key, String value) {
+        setBndInstruction(key, value)
+    }
+
+    /**
+     * Sets a BND manifest instruction, replacing any previous value for 
{@code key}.
+     */
+    void setBndInstruction(String key, String value) {
+        bndInstructions[key] = value
+    }
+
+    /**
+     * Appends a BND manifest instruction using a comma separator, matching 
BND's
+     * syntax for multi-value headers such as {@code Require-Capability}.
+     */
+    void appendBndInstruction(String key, String value) {
+        def existing = bndInstructions.get(key)
+        bndInstructions[key] = existing != null ? "${existing},${value}" : 
value
     }
 
     @Internal
@@ -164,13 +201,28 @@ class JarJarTask extends DefaultTask {
 
         // Step 3: generate an OSGi manifest referencing the repackaged classes
         if (createManifest) {
-            def mf = osgi.osgiManifest {
-                symbolicName = this.projectName
-                instruction 'Import-Package', '*;resolution:=optional'
-                classesDir = tmpJar
+            def analyzer = new Analyzer()
+            try {
+                analyzer.setJar(tmpJar)
+                bndClasspath.files.each { File f ->
+                    if (f.exists()) {
+                        analyzer.addClasspath(f)
+                    }
+                }
+                // Defaults — all overridable by entries in bndInstructions
+                analyzer.setProperty('Bundle-SymbolicName', projectName)
+                analyzer.setProperty('Import-Package', 
'*;resolution:=optional')
+                // Strip BND-generated housekeeping headers that must not 
appear in released jars
+                analyzer.setProperty('-removeheaders',
+                    
'Bnd-LastModified,Tool,Created-By,Originally-Created-By,Ant-Version')
+                // User-specified instructions (override defaults above)
+                bndInstructions.each { k, v -> analyzer.setProperty(k, v) }
+
+                def manifest = analyzer.calcManifest()
+                manifestFile.withOutputStream { os -> manifest.write(os) }
+            } finally {
+                analyzer.close()
             }
-            manifestTweaks*.execute(mf)
-            mf.writeTo(manifestFile)
 
             ant.zip(destfile: outputFile, modificationtime: tstamp, update: 
true) {
                 zipfileset(dir: manifestFile.parent, includes: 
manifestFile.name, prefix: 'META-INF')
diff --git 
a/build-logic/src/main/groovy/org/apache/groovy/gradle/PerformanceTestsExtension.groovy
 
b/build-logic/src/main/groovy/org/apache/groovy/gradle/PerformanceTestsExtension.groovy
index 35e950c399..883676156d 100644
--- 
a/build-logic/src/main/groovy/org/apache/groovy/gradle/PerformanceTestsExtension.groovy
+++ 
b/build-logic/src/main/groovy/org/apache/groovy/gradle/PerformanceTestsExtension.groovy
@@ -19,6 +19,8 @@
 package org.apache.groovy.gradle
 
 import groovy.transform.CompileStatic
+import org.gradle.process.CommandLineArgumentProvider
+import org.gradle.api.file.ConfigurableFileCollection
 import org.gradle.api.artifacts.Configuration
 import org.gradle.api.artifacts.ConfigurationContainer
 import org.gradle.api.artifacts.dsl.DependencyHandler
@@ -28,7 +30,13 @@ import org.gradle.api.attributes.LibraryElements
 import org.gradle.api.attributes.Usage
 import org.gradle.api.file.FileCollection
 import org.gradle.api.file.ProjectLayout
+import org.gradle.api.file.RegularFileProperty
 import org.gradle.api.model.ObjectFactory
+import org.gradle.api.tasks.Classpath
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
 import org.gradle.api.tasks.JavaExec
 import org.gradle.api.tasks.SourceSetContainer
 import org.gradle.api.tasks.TaskContainer
@@ -43,7 +51,7 @@ class PerformanceTestsExtension {
     private final DependencyHandler dependencies
     private final SourceSetContainer sourceSets
     private final ProjectLayout layout
-    private final List<File> testFiles = []
+    private final ConfigurableFileCollection testFiles
 
     @Inject
     PerformanceTestsExtension(ObjectFactory objects,
@@ -59,10 +67,11 @@ class PerformanceTestsExtension {
         this.dependencies = dependencies
         this.sourceSets = sourceSets
         this.layout = layout
+        this.testFiles = objects.fileCollection()
     }
 
     void testFiles(FileCollection files) {
-        testFiles.addAll(files.asList())
+        testFiles.from(files)
     }
 
     void versions(String... versions) {
@@ -106,21 +115,52 @@ class PerformanceTestsExtension {
             ].each { conf.dependencies.add(dependencies.create(it)) }
         }
         def outputFile = 
layout.buildDirectory.file("compilation-stats-${version}.csv")
+        def compilationClasspath = objects.fileCollection().from(groovyConf)
+        def performanceArguments = 
objects.newInstance(CompilerPerformanceTestArguments)
+        performanceArguments.outputFile.set(outputFile)
+        performanceArguments.compilationClasspath.from(compilationClasspath)
+        performanceArguments.testFiles.from(testFiles)
         def perfTest = tasks.register("performanceTestGroovy${version}", 
JavaExec) { je ->
             je.group = "Performance tests"
             je.mainClass.set('org.apache.groovy.perf.CompilerPerformanceTest')
             je.classpath(groovyConf, sourceSets.getByName('test').output)
             je.jvmArgs = ['-Xms512m', '-Xmx512m']
             je.outputs.file(outputFile)
-            je.doFirst {
-                def args = [outputFile.get().toString(), "-cp", 
groovyConf.asPath]
-                args.addAll(testFiles.collect { it.toString() })
-                je.setArgs(args)
-                println je.args.asList()
-            }
+            je.argumentProviders.add(performanceArguments)
         }
         tasks.named("performanceTests", PerformanceTestSummary) { pts ->
-            pts.csvFiles.from(perfTest)
+            pts.dependsOn(perfTest)
+            pts.csvFiles.from(outputFile)
+        }
+    }
+
+    @CompileStatic
+    static class CompilerPerformanceTestArguments implements 
CommandLineArgumentProvider {
+        @Internal
+        final RegularFileProperty outputFile
+
+        @Classpath
+        final ConfigurableFileCollection compilationClasspath
+
+        @InputFiles
+        @PathSensitive(PathSensitivity.RELATIVE)
+        final ConfigurableFileCollection testFiles
+
+        @Inject
+        CompilerPerformanceTestArguments(ObjectFactory objects) {
+            outputFile = objects.fileProperty()
+            compilationClasspath = objects.fileCollection()
+            testFiles = objects.fileCollection()
+        }
+
+        @Override
+        Iterable<String> asArguments() {
+            List<String> args = new ArrayList<>(testFiles.files.size() + 3)
+            args.add(outputFile.get().asFile.absolutePath)
+            args.add('-cp')
+            args.add(compilationClasspath.asPath)
+            args.addAll(testFiles.files.collect { File file -> 
file.absolutePath })
+            args
         }
     }
 }
diff --git 
a/build-logic/src/main/groovy/org/apache/groovy/gradle/SharedConfiguration.groovy
 
b/build-logic/src/main/groovy/org/apache/groovy/gradle/SharedConfiguration.groovy
index 81e6aca9fc..b18030bcfa 100644
--- 
a/build-logic/src/main/groovy/org/apache/groovy/gradle/SharedConfiguration.groovy
+++ 
b/build-logic/src/main/groovy/org/apache/groovy/gradle/SharedConfiguration.groovy
@@ -33,6 +33,24 @@ import org.gradle.api.tasks.Nested
 
 @CompileStatic
 class SharedConfiguration {
+    private static final List<String> DOCUMENTATION_TASK_NAMES = [
+            'asciidocAll',
+            'asciidoctor',
+            'asciidoctorPdf',
+            'doc',
+            'docGDK',
+            'dist',
+            'distBin',
+            'distDoc',
+            'distSdk',
+            'groovydocAll',
+            'javadocAll'
+    ]
+    private static final List<String> APACHE_PUBLISH_TASK_PATHS = [
+            ':artifactoryPublish',
+            ':publishAllPublicationsToApacheRepository'
+    ]
+
     final Provider<String> groovyVersion
     final Provider<Boolean> isReleaseVersion
     final Provider<Date> buildDate
@@ -45,6 +63,7 @@ class SharedConfiguration {
     final Provider<Boolean> hasCodeCoverage
     final Provider<String> targetJavaVersion
     final Provider<String> groovyTargetBytecodeVersion
+    final boolean isDocumentationBuild
     final boolean isRunningOnCI
 
     @Nested
@@ -71,16 +90,24 @@ class SharedConfiguration {
                 .orElse(providers.systemProperty("installDirectory"))
         isRunningOnCI = detectCi(rootProjectDirectory, logger)
         artifactory = new Artifactory(layout, providers, logger)
-        signing = new Signing(this, objects, providers)
+        boolean apachePublishRequested = startParameter.taskNames.any { String 
taskName ->
+            isApachePublishTask(taskName)
+        }
+        signing = new Signing(this, objects, providers, apachePublishRequested)
         binaryCompatibilityBaselineVersion = 
providers.gradleProperty("binaryCompatibilityBaseline")
+        // Evaluate eagerly at construction time (startParameter is available 
here) so that
+        // no lazy provider captures a StartParameter reference, which Gradle 
would otherwise
+        // need to serialize for the configuration cache.
+        boolean hasJacocoTask = startParameter.taskNames.any { String name -> 
name.contains('jacoco') }
         hasCodeCoverage = providers.gradleProperty("coverage")
                 .map { Boolean.valueOf(it) }
-                .orElse(
-                        providers.provider { startParameter.taskNames.any { it 
=~ /jacoco/ } }
-                )
+                .orElse(hasJacocoTask)
                 .orElse(false)
         targetJavaVersion = providers.gradleProperty("targetJavaVersion")
         groovyTargetBytecodeVersion = 
providers.gradleProperty("groovyTargetBytecodeVersion")
+        isDocumentationBuild = startParameter.taskNames.any { String taskName 
->
+            isDocumentationTask(taskName)
+        }
         File javaHome = new File(providers.systemProperty('java.home').get())
         String javaVersion = providers.systemProperty('java.version').get()
         String userdir = providers.systemProperty('user.dir').get()
@@ -95,6 +122,102 @@ class SharedConfiguration {
         isCi
     }
 
+    private static boolean isDocumentationTask(String taskName) {
+        String normalized = taskName.toLowerCase(Locale.ROOT)
+        normalized.contains('asciidoc') ||
+                normalized.contains('asciidoctor') ||
+                normalized.contains('javadoc') ||
+                normalized.contains('groovydoc') ||
+                normalized.endsWith('doc') ||
+                normalized.endsWith(':doc') ||
+                normalized.contains('docgdk') ||
+                normalized == 'dist' ||
+                normalized.startsWith('dist') ||
+                normalized.contains(':dist') ||
+                matchesTaskAbbreviation(taskName, DOCUMENTATION_TASK_NAMES)
+    }
+
+    private static boolean isApachePublishTask(String taskName) {
+        String normalized = taskName.trim()
+        normalized == 'artifactoryPublish' ||
+                normalized.endsWith(':artifactoryPublish') ||
+                normalized == 'publishAllPublicationsToApacheRepository' ||
+                
normalized.endsWith(':publishAllPublicationsToApacheRepository')
+    }
+
+    // Gradle keeps the originally requested selector in 
startParameter.taskNames and expands
+    // task abbreviations only later, after configuration. Recognize 
camel-case prefixes here
+    // so requests like `jA` still opt into the documentation wiring they 
resolve to.
+    private static boolean matchesTaskAbbreviation(String taskName, 
List<String> candidateTaskNames) {
+        String selector = taskSelector(taskName)
+        selector && candidateTaskNames.any { String candidateTaskName ->
+            matchesTaskSelector(selector, candidateTaskName)
+        }
+    }
+
+    private static String taskSelector(String taskName) {
+        String normalized = taskName.trim()
+        int lastSeparator = normalized.lastIndexOf(':')
+        lastSeparator >= 0 ? normalized.substring(lastSeparator + 1) : 
normalized
+    }
+
+    private static boolean matchesTaskSelector(String requestedTaskName, 
String actualTaskName) {
+        if (actualTaskName.regionMatches(true, 0, requestedTaskName, 0, 
requestedTaskName.length())) {
+            return true
+        }
+
+        List<String> requestedSegments = taskNameSegments(requestedTaskName)
+        List<String> actualSegments = taskNameSegments(actualTaskName)
+        if (requestedSegments.size() > actualSegments.size()) {
+            return false
+        }
+
+        for (int i = 0; i < requestedSegments.size(); i += 1) {
+            String requestedSegment = 
requestedSegments.get(i).toLowerCase(Locale.ROOT)
+            String actualSegment = 
actualSegments.get(i).toLowerCase(Locale.ROOT)
+            if (!actualSegment.startsWith(requestedSegment)) {
+                return false
+            }
+        }
+        true
+    }
+
+    private static List<String> taskNameSegments(String taskName) {
+        List<String> segments = []
+        StringBuilder currentSegment = new StringBuilder()
+        for (int i = 0; i < taskName.length(); i += 1) {
+            char current = taskName.charAt(i)
+            if (current in ['-', '_', '.']) {
+                if (currentSegment.length() > 0) {
+                    segments.add(currentSegment.toString())
+                    currentSegment.setLength(0)
+                }
+                continue
+            }
+            if (currentSegment.length() > 0 && startsNewSegment(taskName, i, 
currentSegment.charAt(currentSegment.length() - 1))) {
+                segments.add(currentSegment.toString())
+                currentSegment.setLength(0)
+            }
+            currentSegment.append(current)
+        }
+        if (currentSegment.length() > 0) {
+            segments.add(currentSegment.toString())
+        }
+        segments
+    }
+
+    private static boolean startsNewSegment(String taskName, int index, char 
previous) {
+        char current = taskName.charAt(index)
+        if (!Character.isUpperCase(current)) {
+            return false
+        }
+        if (Character.isLowerCase(previous)) {
+            return true
+        }
+        int nextIndex = index + 1
+        nextIndex < taskName.length() && 
Character.isLowerCase(taskName.charAt(nextIndex))
+    }
+
     static class Artifactory {
         final Provider<String> username
         final Provider<String> password
@@ -136,6 +259,7 @@ class SharedConfiguration {
 
     static class Signing {
         private final SharedConfiguration config
+        private final boolean apachePublishRequested
         final Property<String> keyId
         final Property<String> secretKeyRingFile
         final Property<String> password
@@ -143,7 +267,7 @@ class SharedConfiguration {
         final Provider<Boolean> forceSign
         final Provider<Boolean> trySign
 
-        Signing(SharedConfiguration config, ObjectFactory objects, 
ProviderFactory providers) {
+        Signing(SharedConfiguration config, ObjectFactory objects, 
ProviderFactory providers, boolean apachePublishRequested) {
             keyId = objects.property(String).convention(
                     providers.gradleProperty("signing.keyId")
             )
@@ -160,11 +284,17 @@ class SharedConfiguration {
             trySign = providers.gradleProperty("trySign")
                     .map { Boolean.valueOf(it) }.orElse(false)
             this.config = config
+            this.apachePublishRequested = apachePublishRequested
+        }
+
+        boolean shouldSign() {
+            trySign.get() || (config.isReleaseVersion.get() &&
+                    (forceSign.get() || apachePublishRequested))
         }
 
         boolean shouldSign(TaskExecutionGraph taskGraph) {
             trySign.get() || (config.isReleaseVersion.get() &&
-                    (forceSign.get() || [':artifactoryPublish', 
':publishAllPublicationsToApacheRepository'].any {
+                    (forceSign.get() || apachePublishRequested || 
APACHE_PUBLISH_TASK_PATHS.any {
                         taskGraph.hasTask(it)
                     }))
         }
diff --git a/build.gradle b/build.gradle
index b54602173e..ad1626af24 100644
--- a/build.gradle
+++ b/build.gradle
@@ -256,10 +256,10 @@ licenseReport {
     ]
 }
 
-gradle.taskGraph.whenReady { graph ->
-    if (graph.hasTask(':generateLicenseReport') && 
gradle.startParameter.parallelProjectExecutionEnabled) {
-        throw new GradleException('generateLicenseReport is not compatible 
with parallel builds (Gradle 9+ issue). Please re-run with: ./gradlew 
generateLicenseReport --no-parallel')
-    }
+if (gradle.startParameter.parallelProjectExecutionEnabled && 
gradle.startParameter.taskNames.any {
+    it == 'generateLicenseReport' || it.endsWith(':generateLicenseReport')
+}) {
+    throw new GradleException('generateLicenseReport is not compatible with 
parallel builds (Gradle 9+ issue). Please re-run with: ./gradlew 
generateLicenseReport --no-parallel')
 }
 
 sonarqube {
diff --git a/gradle.properties b/gradle.properties
index 81f71ccbef..2ff6b943fb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -41,6 +41,14 @@ org.gradle.jvmargs=-Xms1200m -Xmx2g 
-XX:MaxMetaspaceSize=1500m -XX:+UseG1GC
 # enable the Gradle build cache
 org.gradle.caching=true
 
+# enable the Gradle configuration cache
+org.gradle.configuration-cache=true
+# treat CC problems as warnings so that dist/publish builds (which include 
third-party
+# plugins such as asciidoctor-gradle-jvm 4.0.5 that are not yet CC-compatible) 
succeed
+# rather than failing during CC-entry storage.  Compile/test builds have no CC 
problems
+# and continue to benefit from the cache as normal.
+org.gradle.configuration-cache.problems=warn
+
 # enable --parallel
 org.gradle.parallel=true
 
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 70aa232ea0..019b0b2408 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -61,6 +61,7 @@
          <ignored-key id="012579464D01C06A" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="0C27E8FAC93B3B19" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="0CA7139CBC7026F9" reason="Key couldn't be downloaded 
from any key server"/>
+         <ignored-key id="0DA8A5EC02D11EAD" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="1063FE98BCECB758" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="156E8DA37ABC7C91" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="1861C322C56014B2" reason="Key couldn't be downloaded 
from any key server"/>
@@ -88,6 +89,7 @@
          <ignored-key id="7D713008CC07E9AD" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="86E02C5A42196CA8" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="99CA0918C37E2AE4" reason="Key couldn't be downloaded 
from any key server"/>
+         <ignored-key id="A41F13C999945293" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="AB049DF4AB24C1EF" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="AECDB81D38EA9C89" reason="Key couldn't be downloaded 
from any key server"/>
          <ignored-key id="B16698A4ADF4D638" reason="Key couldn't be downloaded 
from any key server"/>
@@ -1171,9 +1173,9 @@
             <sha512 
value="e15e95d63f2be7909e64ddc0079793ca4bb3f90630b09cb74244c019f607590a9938fa86a1a7d213dcf216e4bc5d1648610efc3c64ad0aca071bd1d133b33048"
 origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
-      <component group="me.champeau.jmh" name="jmh-gradle-plugin" 
version="0.7.2">
-         <artifact name="jmh-gradle-plugin-0.7.2.jar">
-            <sha512 
value="e433e221c9167bb937ec0df7626b5bbbe99e08c566a6d54ba7d2e04252dd452b4ca957b57e687f85f958df0ce12678e17d0bcb527e1077770c8b7390ce58f56c"
 origin="Generated by Gradle" reason="Artifact is not signed"/>
+      <component group="me.champeau.jmh" name="jmh-gradle-plugin" 
version="0.7.3">
+         <artifact name="jmh-gradle-plugin-0.7.3.jar">
+            <sha512 
value="9498bba9227230019052448f8e2effb458fcf6403cd2d665af38ce8c6bedc1f4da54426043d5a30dfb786bcfcd4256f130fcf8a5ba0455871b9d8bed960b65d1"
 origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
       <component group="me.champeau.openbeans" name="openbeans" 
version="1.0.2">
@@ -1406,6 +1408,12 @@
             <sha512 
value="5e1fcf9f552ab86beceef6743609e97d700dcbaa5b5ee58858e765c1238cd4dbca2852e1eb415bc2a09dad478d31197ddf09b2ba148dd30be8c9ecd565775226"
 origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="org.apache.commons" name="commons-compress" 
version="1.28.0">
+         <artifact name="commons-compress-1.28.0.jar">
+            <pgp value="2DB4F1EF0FA761ECC4EA935C86FDC7E2A11262CB" />
+            <sha512 
value="f1f140f4f40ab3cf3265919db9dbb95a631a29aa784e305f291de4e68876bb711d9217d62d7937cddddd393982decdf71a608b4b3ab7f4e6375cf28af2893d7f"
 origin="Generated by Gradle"/>
+         </artifact>
+      </component>
       <component group="org.apache.commons" name="commons-lang3" 
version="3.12.0">
          <artifact name="commons-lang3-3.12.0.jar">
             <sha512 
value="fbdbc0943cb3498b0148e86a39b773f97c8e6013740f72dbc727faeabea402073e2cc8c4d68198e5fc6b08a13b7700236292e99d4785f2c9989f2e5fac11fd81"
 origin="Generated by Gradle"/>
diff --git a/subprojects/groovy-ant/build.gradle 
b/subprojects/groovy-ant/build.gradle
index 872c7a17ef..45ec426f37 100644
--- a/subprojects/groovy-ant/build.gradle
+++ b/subprojects/groovy-ant/build.gradle
@@ -55,9 +55,14 @@ dependencies {
 
 tasks.withType(Test).configureEach {
     // Supply the external jar (resolved, not checked in) to the GROOVY-9197 
Ant test.
+    // Accessing 'configurations' inside doFirst (execution time) is forbidden 
by the
+    // configuration cache; capture a CC-safe ConfigurableFileCollection at 
configuration
+    // time instead, and resolve it lazily when the task action runs.
+    def externalJarFiles = objects.fileCollection()
+        .from(configurations.named('externalJarForJointCompilationTest'))
+    inputs.files externalJarFiles
     doFirst {
-        systemProperty 'groovy.ant.test.externalJar',
-            
configurations.externalJarForJointCompilationTest.singleFile.absolutePath
+        systemProperty 'groovy.ant.test.externalJar', 
externalJarFiles.singleFile.absolutePath
     }
 
     Integer feature = 
TargetJavaHomeSupport.featureVersionFromReleaseFile(TargetJavaHomeSupport.targetJavaHome(project))
diff --git a/subprojects/groovy-binary/build.gradle 
b/subprojects/groovy-binary/build.gradle
index 558b7ac2ec..6a59534b62 100644
--- a/subprojects/groovy-binary/build.gradle
+++ b/subprojects/groovy-binary/build.gradle
@@ -21,7 +21,6 @@ import org.apache.tools.ant.filters.ReplaceTokens
 plugins {
     id 'org.apache.groovy-distribution'
     id 'org.apache.groovy-published-library'
-    id 'org.apache.groovy-asciidoctor'
 }
 
 //only used when testing locally built artifacts, not for publishing
@@ -39,12 +38,14 @@ docAggregation {
             '**/*.interp' // Antlr generated file
 }
 
-tasks.named('asciidoctor') {
-    attributes reldir_root: '.',
-            reldir_jmx: '.',
-            reldir_swing: '.',
-            reldir_console: '.',
-            reldir_groovysh: '.'
+if (sharedConfiguration.isDocumentationBuild) {
+    tasks.named('asciidoctor') {
+        attributes reldir_root: '.',
+                reldir_jmx: '.',
+                reldir_swing: '.',
+                reldir_console: '.',
+                reldir_groovysh: '.'
+    }
 }
 
 distribution {
@@ -137,11 +138,13 @@ distribution {
         into('html/gapi') {
             from tasks.named('groovydocAll')
         }
-        into('html/documentation') {
-            from configurations.allAsciidoc
-            from tasks.named('asciidoctor')
-            from tasks.named('asciidoctorPdf')
-            exclude '.asciidoctor'
+        if (sharedConfiguration.isDocumentationBuild) {
+            into('html/documentation') {
+                from configurations.allAsciidoc
+                from tasks.named('asciidoctor')
+                from tasks.named('asciidoctorPdf')
+                exclude '.asciidoctor'
+            }
         }
         into('html/groovy-jdk') {
             from tasks.named('docGDK')
diff --git a/subprojects/groovy-groovydoc/build.gradle 
b/subprojects/groovy-groovydoc/build.gradle
index 0ec415ae78..0ee1d02a59 100644
--- a/subprojects/groovy-groovydoc/build.gradle
+++ b/subprojects/groovy-groovydoc/build.gradle
@@ -38,7 +38,7 @@ dependencies {
 
 compileJava {
     doLast {
-        mkdir "${sourceSets.main.java.classesDirectory.get().asFile}/META-INF"
+        new File(destinationDirectory.get().asFile, 'META-INF').mkdirs()
     }
 }
 

Reply via email to