You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/05/19 15:23:26 UTC

(groovy) branch GROOVY-12019 updated (852c62ea4a -> 8fa4f511ac)

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

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


    omit 852c62ea4a GROOVY-12019: Enable gradle configuration cache
     new 8fa4f511ac GROOVY-12019: Enable gradle configuration cache

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (852c62ea4a)
            \
             N -- N -- N   refs/heads/GROOVY-12019 (8fa4f511ac)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../src/main/groovy/org.apache.groovy-base.gradle  |  8 ++-
 .../groovy/org.apache.groovy-distribution.gradle   | 28 ++++++++---
 .../groovy/gradle/PerformanceTestsExtension.groovy | 58 ++++++++++++++++++----
 .../groovy/gradle/SharedConfiguration.groovy       | 17 +++++++
 subprojects/groovy-binary/build.gradle             | 27 +++++-----
 5 files changed, 108 insertions(+), 30 deletions(-)


(groovy) 01/01: GROOVY-12019: Enable gradle configuration cache

Posted by su...@apache.org.
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 8fa4f511ac2b05e6a303cf8db3a8b17ce7de52ba
Author: Daniel Sun <su...@apache.org>
AuthorDate: Tue May 19 01:04:03 2026 +0900

    GROOVY-12019: Enable gradle configuration cache
---
 .../groovy/org.apache.groovy-asciidoctor.gradle    |  7 +++
 .../src/main/groovy/org.apache.groovy-base.gradle  |  9 +++-
 .../src/main/groovy/org.apache.groovy-core.gradle  | 30 +++++------
 .../groovy/org.apache.groovy-distribution.gradle   | 38 +++++++++-----
 .../groovy/org.apache.groovy-documented.gradle     |  8 +--
 .../main/groovy/org.apache.groovy-tested.gradle    | 48 +++++++++---------
 .../org/apache/groovy/gradle/JarJarTask.groovy     | 34 ++++++++++---
 .../groovy/gradle/PerformanceTestsExtension.groovy | 58 ++++++++++++++++++----
 .../groovy/gradle/SharedConfiguration.groovy       | 25 ++++++++--
 build.gradle                                       |  8 +--
 gradle.properties                                  |  8 +++
 subprojects/groovy-ant/build.gradle                |  9 +++-
 subprojects/groovy-binary/build.gradle             | 27 +++++-----
 subprojects/groovy-groovydoc/build.gradle          |  2 +-
 14 files changed, 218 insertions(+), 93 deletions(-)

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..8d00a80225 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()) {
+    // The latest available Asciidoctor Gradle plugins still emit a Gradle 9 deprecation
+    // during apply, so only documentation builds enable them and suppress that noise.
+    gradle.startParameter.warningMode = org.gradle.api.logging.configuration.WarningMode.None
+    pluginManager.apply('org.apache.groovy-asciidoctor')
+}
+
 def groovyLibrary = project.extensions.create('groovyLibrary', GroovyLibraryExtension, sharedConfiguration, java)
 
 java {
@@ -184,6 +190,7 @@ tasks.withType(Jar).configureEach { jar ->
 
 tasks.register('jarjar', JarJarTask) {
     String projectName = project.name
+    osgiExtension = rootProject.extensions.getByName('osgi')
     from = jar.archiveFile
     repackagedLibraries.from configurations.runtimeClasspath.incoming.artifactView {
         componentFilter { component ->
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..6e1b71299a 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-core.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-core.gradle
@@ -92,9 +92,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 +112,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 +122,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 +161,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-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/JarJarTask.groovy b/build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy
index cc24c8873f..bae7e397f7 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
@@ -28,6 +28,7 @@ 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 +47,21 @@ 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
+    @Internal
+    final String projectName
 
-    final protected String projectName = project.name
+    @Internal
+    Object osgiExtension
 
     @Input
     @Optional
@@ -84,7 +87,7 @@ class JarJarTask extends DefaultTask {
     Map<String, String> includedResources = [:]
 
     @OutputFile
-    final RegularFileProperty outputFile = project.objects.fileProperty()
+    final RegularFileProperty outputFile
 
     @Input
     boolean createManifest = true
@@ -94,8 +97,23 @@ class JarJarTask extends DefaultTask {
     private List<Action<? super Manifest>> manifestTweaks = []
 
     @Inject
-    JarJarTask(FileSystemOperations fileSystemOperations) {
+    JarJarTask(ObjectFactory objects, FileSystemOperations fileSystemOperations) {
+        // The OSGi BND extension and the manifestTweaks closures capture project-level
+        // Gradle domain objects (Project, ConfigurationContainer, SourceSetContainer, etc.)
+        // that cannot be serialized by the configuration cache.  Until the OSGi plugin is
+        // updated to a CC-compatible release, builds that include this task (dist, publish)
+        // run without the configuration cache, while compile/test builds are unaffected.
+        notCompatibleWithConfigurationCache(
+            'JarJarTask uses the OSGi BND Gradle extension, which captures project state ' +
+            'that is not configuration-cache serializable. Upgrade the OSGi plugin to a ' +
+            'CC-compatible release to enable the configuration cache for dist/publish tasks.'
+        )
         this.fs = fileSystemOperations
+        this.from = objects.fileProperty()
+        this.repackagedLibraries = objects.fileCollection()
+        this.jarjarToolClasspath = objects.fileCollection()
+        this.outputFile = objects.fileProperty()
+        this.projectName = project.name
     }
 
     void withManifest(Action<? super Manifest> action) {
@@ -164,7 +182,7 @@ class JarJarTask extends DefaultTask {
 
         // Step 3: generate an OSGi manifest referencing the repackaged classes
         if (createManifest) {
-            def mf = osgi.osgiManifest {
+            def mf = osgiExtension.osgiManifest {
                 symbolicName = this.projectName
                 instruction 'Import-Package', '*;resolution:=optional'
                 classesDir = tmpJar
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..f39ffe9f05 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
@@ -45,6 +45,7 @@ class SharedConfiguration {
     final Provider<Boolean> hasCodeCoverage
     final Provider<String> targetJavaVersion
     final Provider<String> groovyTargetBytecodeVersion
+    final boolean isDocumentationBuild
     final boolean isRunningOnCI
 
     @Nested
@@ -73,14 +74,19 @@ class SharedConfiguration {
         artifactory = new Artifactory(layout, providers, logger)
         signing = new Signing(this, objects, providers)
         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 +101,19 @@ 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.endsWith('dist') ||
+                normalized.endsWith(':dist')
+    }
+
     static class Artifactory {
         final Provider<String> username
         final Provider<String> password
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/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()
     }
 }