You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2026/10/05 17:51:25 UTC
(grails-gradle-publish) 01/01: Make publishing compatible with the configuration cache
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a commit to branch fix/configuration-cache
in repository https://gitbox.apache.org/repos/asf/grails-gradle-publish.git
commit 191f409c56cba6535e605e3acd46e61542625fb1
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Mon Oct 5 11:29:46 2026 -0400
Make publishing compatible with the configuration cache
The pom's withXml closure and the module metadata doLast closure captured
the project and the grailsPublish extension, so the pom and module metadata
tasks could not be stored in the configuration cache, and both resolved
configurations through the project when they ran. They are now actions that
only hold values and providers: whether the project is a platform is decided
when it is configured, and the resolved versions come from a provider, which
the configuration cache evaluates when it stores the entry. Nothing is
resolved while transitiveDependencies is disabled, and the versions are read
from the resolution result of each configuration.
The close and release tasks of the Nexus publish plugin cannot be stored when
they run without its publishing tasks, as when a separate build closes the
staging repository, so they are marked as not compatible with the
configuration cache: such builds now run without storing an entry instead of
failing.
Every functional test build now runs with the configuration cache and fails
on any problem, including the containerized release. A new test publishes
twice, reusing the stored entry, and checks the platform managed versions in
the pom and the module metadata. The bom example reads the Groovy version
before its pom customization runs, the managed-dependency-versions example is
published without the configuration cache since the Spring dependency
management plugin does not support it, and the publications sharing
artifacts are published one after the other, as tasks now run in parallel.
---
README.md | 22 ++
examples/bom/build.gradle | 7 +-
examples/managed-dependency-versions/build.gradle | 3 +
.../gradle/publish/GradleSpecification.groovy | 6 +
.../gradle/publish/GrailsPublishPluginSpec.groovy | 77 +++++-
.../examples/ContainerizedReleaseSpec.groovy | 5 +-
.../publish/examples/ExampleProjectsSpec.groovy | 6 +-
.../gradle-plugin-project/build.gradle | 9 +
.../build.gradle | 44 ++--
.../platform-managed-versions/gradle.properties | 15 ++
.../platform-managed-versions/settings.gradle | 20 ++
.../main/groovy/org/grails/example/Managed.groovy | 27 ++
.../org/grails/example/ManagedFixtures.groovy | 29 +++
.../publish/GrailsPublishGradlePlugin.groovy | 286 +++++++++++++++------
.../publish/GrailsPublishGradlePluginTest.groovy | 73 ++++++
15 files changed, 511 insertions(+), 118 deletions(-)
diff --git a/README.md b/README.md
index ced06fa..e003fce 100644
--- a/README.md
+++ b/README.md
@@ -270,6 +270,28 @@ capability, so consumers that do not explicitly request the capability are unaff
tree is also what allows Gradle to resolve a project dependency (including a self dependency such as
`cliApi project(path)`) on a project with multiple publications.
+### Configuration Cache
+
+The plugin is compatible with the [configuration cache](https://docs.gradle.org/current/userguide/configuration_cache.html),
+publishing included. The versions written into the pom and the Gradle module metadata for dependencies declared without
+one (when `transitiveDependencies` is enabled, the default) are resolved when the configuration cache entry is stored,
+and a build reusing the entry publishes them without resolving the configurations again.
+
+The `pomCustomization` closure is stored with the task generating the pom, so it must only use its `XmlProvider`
+argument and the pom it is delegated to, not the project or other state of the build script. Read any other value it
+needs into a local variable beforehand, as the [bom](examples/bom/build.gradle) example does.
+
+Some builds still cannot store a configuration cache entry, because of the plugins this one works with:
+
+- Gradle does not store publishing tasks of a repository with explicit credentials (the `MAVEN_PUBLISH_` and
+ `NEXUS_PUBLISH_` credentials), so such builds run without storing an entry.
+- The close and release tasks of the Nexus publish plugin reference the project when they run without its publishing
+ tasks, as when a separate build closes the staging repository. This plugin marks them as not compatible with the
+ configuration cache, so such builds also run without storing an entry instead of failing.
+- The Spring dependency management plugin (`io.spring.dependency-management`) adds a pom customization referencing the
+ project to every Maven publication, even with `generatedPomCustomization` disabled. A build applying it has to
+ publish with `--no-configuration-cache`, or manage its versions with Gradle platforms instead.
+
### Release Signing
`release` builds are expected to be signed by this build. To disable this behavior, add the following Gradle code:
diff --git a/examples/bom/build.gradle b/examples/bom/build.gradle
index 046e055..724b53e 100644
--- a/examples/bom/build.gradle
+++ b/examples/bom/build.gradle
@@ -54,10 +54,13 @@ grailsPublish {
desc = 'A Bill of Materials published with the Grails Publish plugin'
developers = [jdaugherty: 'James Daugherty']
- // The generated pom can be post-processed; here the Groovy version is exposed as a Maven property
+ // The generated pom can be post-processed; here the Groovy version is exposed as a Maven property. The closure runs
+ // when the pom is generated, where the configuration cache gives it no access to the project, so the version is
+ // read beforehand into a local variable
+ String bomGroovyVersion = groovyVersion
pomCustomization = { xml ->
def root = xml.asNode()
def properties = root.properties ? root.properties[0] : root.appendNode('properties')
- properties.appendNode('groovy.version', groovyVersion)
+ properties.appendNode('groovy.version', bomGroovyVersion)
}
}
diff --git a/examples/managed-dependency-versions/build.gradle b/examples/managed-dependency-versions/build.gradle
index f4b4c8b..3eeb0da 100644
--- a/examples/managed-dependency-versions/build.gradle
+++ b/examples/managed-dependency-versions/build.gradle
@@ -20,6 +20,9 @@
// Dependencies declared without a version, managed by a Gradle platform or by the Spring dependency management
// plugin (as Grails plugins do). The published pom and Gradle module metadata carry the resolved versions, and
// the dependencyManagement section the Spring plugin adds to the pom is removed.
+//
+// The Spring plugin's pom customization references the project, so publishing this project requires
+// --no-configuration-cache when the configuration cache is enabled.
plugins {
id 'java-library'
id 'groovy'
diff --git a/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/GradleSpecification.groovy b/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/GradleSpecification.groovy
index d8e1c58..bac7eaf 100644
--- a/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/GradleSpecification.groovy
+++ b/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/GradleSpecification.groovy
@@ -39,6 +39,12 @@ abstract class GradleSpecification extends Specification {
void setupSpec() {
basePath = Files.createTempDirectory("gradle-projects")
Path testKitDirectory = Files.createDirectories(basePath.resolve('.gradle'))
+ // The test kit directory is the Gradle user home of every build, so its gradle.properties win over those of
+ // the projects: every build runs with the configuration cache and fails on any configuration cache problem
+ testKitDirectory.resolve('gradle.properties').toFile().text = '''\
+ org.gradle.configuration-cache=true
+ org.gradle.configuration-cache.problems=fail
+ '''.stripIndent()
gradleRunner = GradleRunner.create()
.withPluginClasspath()
.withTestKitDir(testKitDirectory.toFile())
diff --git a/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/GrailsPublishPluginSpec.groovy b/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/GrailsPublishPluginSpec.groovy
index fe53001..180eddd 100644
--- a/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/GrailsPublishPluginSpec.groovy
+++ b/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/GrailsPublishPluginSpec.groovy
@@ -19,6 +19,8 @@
package org.apache.grails.gradle.publish
+import groovy.json.JsonSlurper
+import groovy.xml.XmlSlurper
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.UnexpectedBuildFailure
import spock.lang.PendingFeature
@@ -1250,7 +1252,7 @@ tasks.named('javadoc', Javadoc) {
runner = setGradleProperty("projectVersion", "0.0.1-SNAPSHOT", runner)
when:
- def result = executeTask("assemble", ["--configuration-cache"], runner)
+ def result = executeTask("assemble", runner)
then: 'javadocJar is in the graph - `publish` alone would pass on the publication dependency'
assertTaskSuccess("javadocJar", result)
@@ -1259,9 +1261,9 @@ tasks.named('javadoc', Javadoc) {
new File(runner.projectDir, 'build/libs').listFiles().any { it.name.endsWith('-javadoc.jar') }
when: 'the build runs again'
- // CI runs with `org.gradle.configuration-cache=false`, so without this the jar's task state -
- // the exclude spec and the providers it reads - is only ever exercised by hand
- def cachedResult = executeTask("assemble", ["--configuration-cache"], runner)
+ // every build runs with the configuration cache (see GradleSpecification), so this build runs the jar's task
+ // state - the exclude spec and the providers it reads - as loaded from the stored entry
+ def cachedResult = executeTask("assemble", runner)
then: 'the configuration cache entry is reusable'
cachedResult.output.contains('Reusing configuration cache.')
@@ -1272,20 +1274,83 @@ tasks.named('javadoc', Javadoc) {
GradleRunner runner = setupTestResourceProject('other-artifacts', 'test-sources')
when:
- def result = executeTask("testSourcesJar", ["--configuration-cache", "--configuration-cache-problems=fail"], runner)
+ def result = executeTask("testSourcesJar", runner)
then: 'the onlyIf check runs without needing the project'
assertTaskSuccess("testSourcesJar", result)
findJarFileEntry("org/grails/example/MyProjectTest.class", new File(runner.projectDir, 'build/libs/test-sources-0.0.1-SNAPSHOT-tests.jar'))
when: 'the build runs again'
- def cachedResult = executeTask("testSourcesJar", ["--configuration-cache", "--configuration-cache-problems=fail", "--rerun-tasks"], runner)
+ def cachedResult = executeTask("testSourcesJar", ["--rerun-tasks"], runner)
then: 'the configuration cache entry is reusable'
cachedResult.output.contains('Reusing configuration cache.')
assertTaskSuccess("testSourcesJar", cachedResult)
}
+ def "publishing reuses the configuration cache entry and still writes the platform managed versions"() {
+ given:
+ File repository = File.createTempDir("platform-managed-versions")
+ toCleanup << repository
+
+ and:
+ GradleRunner runner = setupTestResourceProject('other-artifacts', 'platform-managed-versions')
+ runner = setGradleProperty("mavenPublishUrl", repository.absolutePath, runner)
+ runner = addEnvironmentVariable("GRAILS_PUBLISH_RELEASE", "false", runner)
+ String groovyVersion = System.getProperty('groovyVersion')
+
+ when: 'the first build stores the configuration cache entry'
+ def result = executeTask("publish", runner)
+
+ then:
+ result.output.contains('Configuration cache entry stored.')
+ assertTaskSuccess("generatePomFileForMavenPublication", result)
+ assertTaskSuccess("generateMetadataFileForMavenPublication", result)
+ managedVersions(repository) == [pom: groovyVersion, module: groovyVersion, mainModule: groovyVersion, inceptionYear: '2025']
+
+ when: 'the published files are removed and the build runs again from the stored entry'
+ repository.deleteDir()
+ repository.mkdirs()
+ def cachedResult = executeTask("publish", ["--rerun-tasks"], runner)
+
+ then: 'the pom and the module metadata are generated again, without the project'
+ cachedResult.output.contains('Configuration cache entry reused.')
+ assertTaskSuccess("generatePomFileForMavenPublication", cachedResult)
+ assertTaskSuccess("generateMetadataFileForMavenPublication", cachedResult)
+
+ and: 'the versions resolved when the entry was stored, and the pom customization, are in the published files'
+ managedVersions(repository) == [pom: groovyVersion, module: groovyVersion, mainModule: groovyVersion, inceptionYear: '2025']
+
+ and: 'the checksums of a Maven publish are still limited to md5 and sha1'
+ !new File(repository, 'org/grails/example/platform-managed-versions/0.0.1-SNAPSHOT').list().any {
+ it.endsWith('.sha256') || it.endsWith('.sha512')
+ }
+ }
+
+ /**
+ * The versions published for the test fixtures dependency on groovy-json, which only the plugin fills in (version
+ * mapping reads the main runtime classpath alone), and for the main dependency on groovy.
+ */
+ private static Map<String, String> managedVersions(File repository) {
+ File[] files = new File(repository, 'org/grails/example/platform-managed-versions/0.0.1-SNAPSHOT').listFiles()
+ File pomFile = files.find { it.name.endsWith('.pom') }
+ File moduleFile = files.find { it.name.endsWith('.module') }
+
+ def pom = new XmlSlurper().parse(pomFile)
+ def module = new JsonSlurper().parse(moduleFile) as Map
+ List<Map> variants = module.variants as List<Map>
+ Map fixturesDependency = (variants.find { it.name == 'testFixturesApiElements' }.dependencies as List<Map>)
+ .find { it.module == 'groovy-json' }
+ Map mainDependency = (variants.find { it.name == 'runtimeElements' }.dependencies as List<Map>)
+ .find { it.module == 'groovy' }
+ [
+ pom : pom.dependencies.dependency.find { it.artifactId.text() == 'groovy-json' }.version.text(),
+ module : (fixturesDependency.version as Map)?.requires,
+ mainModule : (mainDependency.version as Map)?.requires,
+ inceptionYear: pom.inceptionYear.text(),
+ ] as Map<String, String>
+ }
+
def "gradle properties are used by the plugin applied in a subproject"() {
given:
File tempDir = File.createTempDir("properties-from-parent-project")
diff --git a/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/examples/ContainerizedReleaseSpec.groovy b/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/examples/ContainerizedReleaseSpec.groovy
index ae6d6fc..ac28787 100644
--- a/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/examples/ContainerizedReleaseSpec.groovy
+++ b/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/examples/ContainerizedReleaseSpec.groovy
@@ -106,7 +106,10 @@ class ContainerizedReleaseSpec extends ExampleProjectSpecification {
ExecResult build = container.execInContainer(ExecConfig.builder()
.workDir('/project')
.envVars(environment)
- .command((['gradle', '--no-daemon', '--stacktrace', '--init-script', '/init/local-plugin.init.gradle']
+ // the container has its own Gradle user home, so the configuration cache is enabled here, as it is for
+ // the builds run by the test kit (see GradleSpecification)
+ .command((['gradle', '--no-daemon', '--stacktrace', '--configuration-cache', '--configuration-cache-problems=fail',
+ '--init-script', '/init/local-plugin.init.gradle']
+ MockNexus.trustArgumentsFor('/keys/nexus-truststore.p12')
+ ['publishToSonatype', 'closeSonatypeStagingRepository']) as String[])
.build())
diff --git a/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/examples/ExampleProjectsSpec.groovy b/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/examples/ExampleProjectsSpec.groovy
index 2c59f35..00da893 100644
--- a/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/examples/ExampleProjectsSpec.groovy
+++ b/plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/examples/ExampleProjectsSpec.groovy
@@ -291,8 +291,10 @@ class ExampleProjectsSpec extends ExampleProjectSpecification {
GradleRunner runner = setupExample('managed-dependency-versions')
runner = setGradleProperty('mavenPublishUrl', repository.toString(), runner)
- when:
- BuildResult result = run(runner, 'publish')
+ when: 'published without the configuration cache, which the dependency management plugin does not support here'
+ // io.spring.dependency-management adds its own pom customization to every Maven publication, even when
+ // generatedPomCustomization is disabled, and that action holds the project, so the pom task cannot be stored
+ BuildResult result = run(runner, '--no-configuration-cache', 'publish')
then:
result.task(':publishMavenPublicationToMavenRepository').outcome == TaskOutcome.SUCCESS
diff --git a/plugin/src/functionalTest/resources/publish-projects/other-artifacts/gradle-plugin-project/build.gradle b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/gradle-plugin-project/build.gradle
index ac1aa07..c9fabad 100644
--- a/plugin/src/functionalTest/resources/publish-projects/other-artifacts/gradle-plugin-project/build.gradle
+++ b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/gradle-plugin-project/build.gradle
@@ -79,4 +79,13 @@ if (System.getenv('PUBLISH_SHARED_ARTIFACTS')) {
publicationName = 'maven'
addComponents = true
}
+
+ // Both publications write the same coordinates, each with its own pom. With the configuration cache, tasks of a
+ // project run in parallel, so publish one after the other, keeping the files of one publication from being mixed
+ // with the signatures of the other
+ tasks.withType(AbstractPublishToMaven).configureEach { task ->
+ if (task.name.startsWith('publishMavenPublicationTo')) {
+ task.mustRunAfter(task.name.replace('publishMavenPublicationTo', 'publishPluginMavenPublicationTo'))
+ }
+ }
}
\ No newline at end of file
diff --git a/plugin/src/functionalTest/resources/publish-projects/other-artifacts/gradle-plugin-project/build.gradle b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/build.gradle
similarity index 62%
copy from plugin/src/functionalTest/resources/publish-projects/other-artifacts/gradle-plugin-project/build.gradle
copy to plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/build.gradle
index ac1aa07..59e8f9c 100644
--- a/plugin/src/functionalTest/resources/publish-projects/other-artifacts/gradle-plugin-project/build.gradle
+++ b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/build.gradle
@@ -38,45 +38,31 @@ group = 'org.grails.example'
apply plugin: 'java-library'
apply plugin: 'groovy'
+apply plugin: 'java-test-fixtures'
dependencies {
- implementation "org.apache.groovy:groovy:$groovyVersion"
-}
-
-// the java-gradle-plugin creates its own pluginMaven publication, which must be reused in either plugin order
-if (System.getenv('APPLY_JAVA_GRADLE_PLUGIN_FIRST')) {
- apply plugin: 'java-gradle-plugin'
-}
-apply plugin: 'org.apache.grails.gradle.grails-publish'
-if (!System.getenv('APPLY_JAVA_GRADLE_PLUGIN_FIRST')) {
- apply plugin: 'java-gradle-plugin'
-}
+ // managed by a platform: Gradle's version mapping writes the version of the main variants
+ implementation platform("org.apache.groovy:groovy-bom:$groovyVersion")
+ implementation 'org.apache.groovy:groovy'
-gradlePlugin {
- plugins {
- example {
- id = 'org.grails.example.gradle-plugin-project'
- implementationClass = 'org.grails.example.MyProject'
- }
- }
+ // only on the test fixtures classpaths, which version mapping does not read: the Grails Publish plugin resolves
+ // the version written into the pom and the Gradle module metadata
+ testFixturesApi platform("org.apache.groovy:groovy-bom:$groovyVersion")
+ testFixturesApi 'org.apache.groovy:groovy-json'
}
+apply plugin: 'org.apache.grails.gradle.grails-publish'
grailsPublish {
- githubSlug = 'apache/grails-cores'
+ githubSlug = 'apache/grails-gradle-publish'
license {
name = 'Apache-2.0'
}
- title = 'Grails Gradle Plugin - Example Project'
- desc = 'A testing project for the grails gradle plugin'
+ title = 'Platform Managed Versions'
+ desc = 'A project whose dependency versions are managed by a platform'
developers = [
jdaugherty: 'James Daugherty',
]
-}
-
-// publish the artifacts through a second publication as well, so the publications share them
-if (System.getenv('PUBLISH_SHARED_ARTIFACTS')) {
- grailsPublish {
- publicationName = 'maven'
- addComponents = true
+ pomCustomization = { xml ->
+ xml.asNode().appendNode('inceptionYear', '2025')
}
-}
\ No newline at end of file
+}
diff --git a/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/gradle.properties b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/gradle.properties
new file mode 100644
index 0000000..2290586
--- /dev/null
+++ b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/gradle.properties
@@ -0,0 +1,15 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+projectVersion=0.0.1-SNAPSHOT
\ No newline at end of file
diff --git a/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/settings.gradle b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/settings.gradle
new file mode 100644
index 0000000..9a4bde0
--- /dev/null
+++ b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/settings.gradle
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+rootProject.name = 'platform-managed-versions'
diff --git a/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/src/main/groovy/org/grails/example/Managed.groovy b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/src/main/groovy/org/grails/example/Managed.groovy
new file mode 100644
index 0000000..8264e08
--- /dev/null
+++ b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/src/main/groovy/org/grails/example/Managed.groovy
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.grails.example
+
+class Managed {
+
+ String greet(String name) {
+ "Hello $name"
+ }
+}
diff --git a/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/src/testFixtures/groovy/org/grails/example/ManagedFixtures.groovy b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/src/testFixtures/groovy/org/grails/example/ManagedFixtures.groovy
new file mode 100644
index 0000000..cd2221a
--- /dev/null
+++ b/plugin/src/functionalTest/resources/publish-projects/other-artifacts/platform-managed-versions/src/testFixtures/groovy/org/grails/example/ManagedFixtures.groovy
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.grails.example
+
+import groovy.json.JsonOutput
+
+class ManagedFixtures {
+
+ static String greetingJson(String name) {
+ JsonOutput.toJson([greeting: new Managed().greet(name)])
+ }
+}
diff --git a/plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy b/plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy
index 16725ca..54fdcbe 100644
--- a/plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy
+++ b/plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy
@@ -23,6 +23,7 @@ import groovy.json.JsonSlurper
import groovy.namespace.QName
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
+import io.github.gradlenexus.publishplugin.AbstractTransitionNexusStagingRepositoryTask
import io.github.gradlenexus.publishplugin.InitializeNexusStagingRepository
import io.github.gradlenexus.publishplugin.NexusPublishExtension
import io.github.gradlenexus.publishplugin.NexusPublishPlugin
@@ -36,8 +37,13 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.XmlProvider
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.ConfigurationContainer
+import org.gradle.api.artifacts.ModuleVersionIdentifier
import org.gradle.api.artifacts.PublishArtifact
-import org.gradle.api.artifacts.ResolvedArtifact
+import org.gradle.api.artifacts.result.DependencyResult
+import org.gradle.api.artifacts.result.ResolvedComponentResult
+import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.artifacts.type.ArtifactTypeDefinition
import org.gradle.api.artifacts.dsl.RepositoryHandler
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
@@ -351,6 +357,14 @@ Note: properties are read as Gradle properties (the root project's gradle.proper
}
}
+ // The close and release tasks of the Nexus publish plugin (2.0.0) cannot be stored in the configuration
+ // cache when they run without its publishing tasks, as in a release closing the staging repository in a
+ // separate build: their transition check options reach the Nexus extension, and through it the project.
+ // Gradle then runs such a build without storing a configuration cache entry instead of failing it.
+ project.rootProject.tasks.withType(AbstractTransitionNexusStagingRepositoryTask).configureEach { AbstractTransitionNexusStagingRepositoryTask task ->
+ task.notCompatibleWithConfigurationCache('The staging repository transition tasks of the Nexus publish plugin reference the project')
+ }
+
if (!hasNexusPublishApplied) {
project.rootProject.extensions.configure(NexusPublishExtension) { NexusPublishExtension it ->
if (nexusPublishDescription) {
@@ -653,36 +667,12 @@ Note: properties are read as Gradle properties (the root project's gradle.proper
throw new InvalidUserDataException(createErrorMessage('developers'))
}
- pom.withXml { XmlProvider xml ->
- Node pomNode = xml.asNode()
-
- if (!project.extensions.findByType(JavaPlatformExtension)) {
- // Spring boot dependency management plugin will add the dependencyManagement section,
- // we do not want to publish this information as we will determine the specific versions
- // and set them instead
- NodeList dependencyManagement = (NodeList) pomNode.get('dependencyManagement')
- if (dependencyManagement) {
- dependencyManagement.replaceNode {}
- }
- }
-
- if (pomCustomization.isPresent()) {
- Closure customization = pomCustomization.get()
- customization.delegate = pom
- customization.resolveStrategy = Closure.DELEGATE_FIRST
- customization.call(xml)
- }
-
- // fix dependencies without a version, this can occur when the spring dependency management plugin is used
- // disabling that plugin will cause gradle to fail on any unresolved, or by disabling the check with:
- // https://github.com/gradle/gradle/issues/23030
- //tasks.withType(GenerateModuleMetadata).configureEach {
- // suppressedValidationErrors.add('dependencies-without-versions')
- //}
- if (gpe.transitiveDependencies.get()) {
- setDependencyVersions(pomNode, project, versionResolutionConfigurations)
- }
- }
+ // the action runs when the pom is generated, so it is given values and providers only: with the
+ // configuration cache, neither the project nor this extension is available then
+ boolean javaPlatform = project.extensions.findByType(JavaPlatformExtension) != null
+ Provider<Boolean> transitiveDependencies = gpe.transitiveDependencies
+ pom.withXml(new PomXmlAction(pom, javaPlatform, pomCustomization, transitiveDependencies,
+ resolvedVersionsProvider(project, transitiveDependencies, versionResolutionConfigurations)))
}
}
@@ -801,7 +791,24 @@ Note: properties are read as Gradle properties (the root project's gradle.proper
}
}
+ /**
+ * Sets the versions of the pom dependencies declared without one from the versions resolved by the named
+ * configurations of the project.
+ *
+ * @deprecated reads the project when the pom is generated, which the configuration cache does not allow; the
+ * plugin itself uses {@link #setDependencyVersions(Node, Provider)}
+ */
+ @Deprecated
protected void setDependencyVersions(Node pomNode, Project project, List<String> configurationNames) {
+ setDependencyVersions(pomNode, project.provider { resolvedVersions(project, configurationNames) })
+ }
+
+ /**
+ * Sets the versions of the pom dependencies declared without one.
+ *
+ * @param resolvedVersions the resolved versions, keyed by `group:name`; only queried when the pom has dependencies
+ */
+ protected static void setDependencyVersions(Node pomNode, Provider<Map<String, String>> resolvedVersions) {
def mavenPomNamespace = 'http://maven.apache.org/POM/4.0.0'
def dependenciesQName = new QName(mavenPomNamespace, 'dependencies')
def dependencyQName = new QName(mavenPomNamespace, 'dependency')
@@ -815,26 +822,16 @@ Note: properties are read as Gradle properties (the root project's gradle.proper
}
NodeList dependencyNodes = (nodes.get(0) as Node).getAt(dependencyQName) as NodeList
- LinkedHashSet<ResolvedArtifact> resolvedArtifacts = []
- for (String configurationName : configurationNames) {
- def configuration = project.configurations.findByName(configurationName)
- if (configuration != null) {
- resolvedArtifacts.addAll(configuration.resolvedConfiguration.resolvedArtifacts)
- }
- }
-
- dependencyNodes.findAll { dependencyNode ->
+ Map<String, String> versions = resolvedVersions.get()
+ List<Node> dependenciesWithoutVersion = dependencyNodes.findAll { dependencyNode ->
NodeList versionNodes = (dependencyNode as Node)[versionQName] as NodeList
return versionNodes.size() == 0 || (versionNodes.first() as Node).text().trim().isEmpty()
- }.each { objectNode ->
- def dependencyNode = objectNode as Node
+ } as List<Node>
+ for (Node dependencyNode : dependenciesWithoutVersion) {
def groupId = (dependencyNode[groupIdQName].first() as Node).text()
def artifactId = (dependencyNode[artifactIdQName].first() as Node).text()
- def managedVersion = resolvedArtifacts.find {
- it.moduleVersion.id.group == groupId &&
- it.moduleVersion.id.name == artifactId
- }?.moduleVersion?.id?.version
+ String managedVersion = versions["${groupId}:${artifactId}" as String]
if (!managedVersion) {
throw new InvalidUserDataException("No version found for dependency $groupId:$artifactId.")
}
@@ -1035,36 +1032,15 @@ Note: properties are read as Gradle properties (the root project's gradle.proper
* resolved classpaths the pom is completed from.
*/
protected void configureModuleMetadataVersions(Project project, GrailsPublishExtension gpe, String publicationName, List<String> configurationNames) {
+ // the action runs when the module metadata is generated, so it is given values and providers only: with the
+ // configuration cache, neither the project nor this extension is available then
+ Provider<Boolean> transitiveDependencies = gpe.transitiveDependencies
+ Provider<Map<String, String>> resolvedVersions = resolvedVersionsProvider(project, transitiveDependencies, configurationNames)
project.tasks.withType(GenerateModuleMetadata).configureEach { GenerateModuleMetadata task ->
if (task.publication.orNull?.name != publicationName) {
return
}
- task.doLast {
- if (!gpe.transitiveDependencies.get()) {
- return
- }
- File moduleFile = task.outputFile.get().asFile
- Map<String, String> resolvedVersions = resolvedVersions(project, configurationNames)
- Map module = new JsonSlurper().parse(moduleFile, 'UTF-8') as Map
- boolean changed = false
- for (Map variant : (module.variants ?: []) as List<Map>) {
- for (Map dependency : (variant.dependencies ?: []) as List<Map>) {
- Map version = dependency.version as Map
- if (version?.requires || version?.strictly || version?.prefers) {
- continue
- }
- String resolved = resolvedVersions["${dependency.group}:${dependency.module}" as String]
- if (resolved == null) {
- throw new InvalidUserDataException("No version found for dependency ${dependency.group}:${dependency.module} of variant ${variant.name} in the module metadata of publication ${publicationName}.")
- }
- dependency.version = [requires: resolved]
- changed = true
- }
- }
- if (changed) {
- moduleFile.setText(formatLikeGradle(JsonOutput.toJson(module)), 'UTF-8')
- }
- }
+ task.doLast(new ModuleMetadataVersionsAction(publicationName, transitiveDependencies, resolvedVersions))
}
}
@@ -1080,20 +1056,65 @@ Note: properties are read as Gradle properties (the root project's gradle.proper
}.join('\n') + '\n'
}
- /** The versions of the artifacts resolved by the named configurations, keyed by `group:name` */
+ /**
+ * The versions of the components resolved by the named configurations, keyed by `group:name`. Nothing is resolved
+ * until the provider is queried, and nothing at all while `transitiveDependencies` is disabled. A task action reads
+ * the versions through this provider instead of the project: with the configuration cache, the provider is
+ * evaluated when the cache entry is stored, and a build reusing the entry reads the stored versions.
+ */
+ protected static Provider<Map<String, String>> resolvedVersionsProvider(Project project, Provider<Boolean> enabled, List<String> configurationNames) {
+ ConfigurationContainer configurations = project.configurations
+ List<String> names = new ArrayList<>(configurationNames)
+ project.provider {
+ enabled.get() ? resolvedVersions(configurations, names) : [:] as Map<String, String>
+ }
+ }
+
+ /** The versions of the components resolved by the named configurations, keyed by `group:name` */
protected static Map<String, String> resolvedVersions(Project project, List<String> configurationNames) {
+ resolvedVersions(project.configurations, configurationNames)
+ }
+
+ /**
+ * The versions of the components resolved by the named configurations, keyed by `group:name`; for a module
+ * resolved by more than one of them, the version of the configuration listed first. Configurations that do not
+ * exist are skipped.
+ */
+ protected static Map<String, String> resolvedVersions(ConfigurationContainer configurations, List<String> configurationNames) {
Map<String, String> versions = [:]
for (String configurationName : configurationNames) {
- def configuration = project.configurations.findByName(configurationName)
+ Configuration configuration = configurations.findByName(configurationName)
if (configuration != null) {
- for (ResolvedArtifact artifact : configuration.resolvedConfiguration.resolvedArtifacts) {
- versions.putIfAbsent("${artifact.moduleVersion.id.group}:${artifact.moduleVersion.id.name}" as String, artifact.moduleVersion.id.version)
- }
+ collectResolvedVersions(configuration.incoming.resolutionResult.rootComponent.get(), versions)
}
}
versions
}
+ /** Adds the version of every component the root component depends on, directly or transitively */
+ private static void collectResolvedVersions(ResolvedComponentResult root, Map<String, String> versions) {
+ Set<ResolvedComponentResult> visited = [] as Set<ResolvedComponentResult>
+ Deque<ResolvedComponentResult> pending = new ArrayDeque<>()
+ pending.add(root)
+ while (!pending.isEmpty()) {
+ ResolvedComponentResult component = pending.poll()
+ for (DependencyResult dependency : component.dependencies) {
+ if (!(dependency instanceof ResolvedDependencyResult)) {
+ continue
+ }
+ ResolvedComponentResult selected = ((ResolvedDependencyResult) dependency).selected
+ if (!visited.add(selected)) {
+ continue
+ }
+ ModuleVersionIdentifier id = selected.moduleVersion
+ if (id != null) {
+ versions.putIfAbsent("${id.group}:${id.name}" as String, id.version)
+ }
+ pending.add(selected)
+ }
+ }
+ }
+
protected String getDefaultClassifier() {
'plugin'
}
@@ -1233,5 +1254,114 @@ Note: properties are read as Gradle properties (the root project's gradle.proper
}
}
}
-}
+ /**
+ * Completes the generated pom: removes the dependencyManagement section (unless the project is a platform),
+ * applies the pom customization, and fills in the versions of dependencies declared without one.
+ *
+ * The action is part of the state of the pom generation task, so it only holds values and providers, which keeps
+ * it compatible with the configuration cache. The versions are read from a provider, which the configuration
+ * cache evaluates when it stores the task.
+ */
+ @PackageScope
+ static class PomXmlAction implements Action<XmlProvider> {
+
+ private final MavenPom pom
+ private final boolean javaPlatform
+ private final Provider<Closure> pomCustomization
+ private final Provider<Boolean> transitiveDependencies
+ private final Provider<Map<String, String>> resolvedVersions
+
+ PomXmlAction(MavenPom pom, boolean javaPlatform, Provider<Closure> pomCustomization,
+ Provider<Boolean> transitiveDependencies, Provider<Map<String, String>> resolvedVersions) {
+ this.pom = pom
+ this.javaPlatform = javaPlatform
+ this.pomCustomization = pomCustomization
+ this.transitiveDependencies = transitiveDependencies
+ this.resolvedVersions = resolvedVersions
+ }
+
+ @Override
+ void execute(XmlProvider xml) {
+ Node pomNode = xml.asNode()
+
+ if (!javaPlatform) {
+ // Spring boot dependency management plugin will add the dependencyManagement section,
+ // we do not want to publish this information as we will determine the specific versions
+ // and set them instead
+ NodeList dependencyManagement = (NodeList) pomNode.get('dependencyManagement')
+ if (dependencyManagement) {
+ dependencyManagement.replaceNode {}
+ }
+ }
+
+ if (pomCustomization.isPresent()) {
+ Closure customization = pomCustomization.get()
+ customization.delegate = pom
+ customization.resolveStrategy = Closure.DELEGATE_FIRST
+ customization.call(xml)
+ }
+
+ // fix dependencies without a version, this can occur when the spring dependency management plugin is used
+ // disabling that plugin will cause gradle to fail on any unresolved, or by disabling the check with:
+ // https://github.com/gradle/gradle/issues/23030
+ //tasks.withType(GenerateModuleMetadata).configureEach {
+ // suppressedValidationErrors.add('dependencies-without-versions')
+ //}
+ if (transitiveDependencies.get()) {
+ setDependencyVersions(pomNode, resolvedVersions)
+ }
+ }
+ }
+
+ /**
+ * Fills in the versions the Gradle module metadata is missing, see
+ * {@link #configureModuleMetadataVersions(Project, GrailsPublishExtension, String, List)}.
+ *
+ * The action is part of the state of the module metadata task, so it only holds values and providers, which keeps
+ * it compatible with the configuration cache. The versions are read from a provider, which the configuration
+ * cache evaluates when it stores the task.
+ */
+ @PackageScope
+ static class ModuleMetadataVersionsAction implements Action<Task> {
+
+ private final String publicationName
+ private final Provider<Boolean> transitiveDependencies
+ private final Provider<Map<String, String>> resolvedVersions
+
+ ModuleMetadataVersionsAction(String publicationName, Provider<Boolean> transitiveDependencies,
+ Provider<Map<String, String>> resolvedVersions) {
+ this.publicationName = publicationName
+ this.transitiveDependencies = transitiveDependencies
+ this.resolvedVersions = resolvedVersions
+ }
+
+ @Override
+ void execute(Task task) {
+ if (!transitiveDependencies.get()) {
+ return
+ }
+ File moduleFile = ((GenerateModuleMetadata) task).outputFile.get().asFile
+ Map<String, String> versions = resolvedVersions.get()
+ Map module = new JsonSlurper().parse(moduleFile, 'UTF-8') as Map
+ boolean changed = false
+ for (Map variant : (module.variants ?: []) as List<Map>) {
+ for (Map dependency : (variant.dependencies ?: []) as List<Map>) {
+ Map version = dependency.version as Map
+ if (version?.requires || version?.strictly || version?.prefers) {
+ continue
+ }
+ String resolved = versions["${dependency.group}:${dependency.module}" as String]
+ if (resolved == null) {
+ throw new InvalidUserDataException("No version found for dependency ${dependency.group}:${dependency.module} of variant ${variant.name} in the module metadata of publication ${publicationName}.")
+ }
+ dependency.version = [requires: resolved]
+ changed = true
+ }
+ }
+ if (changed) {
+ moduleFile.setText(formatLikeGradle(JsonOutput.toJson(module)), 'UTF-8')
+ }
+ }
+ }
+}
diff --git a/plugin/src/test/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePluginTest.groovy b/plugin/src/test/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePluginTest.groovy
index cf1a9bc..166cdc9 100644
--- a/plugin/src/test/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePluginTest.groovy
+++ b/plugin/src/test/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePluginTest.groovy
@@ -19,11 +19,16 @@
package org.apache.grails.gradle.publish
+import groovy.namespace.QName
+import groovy.xml.XmlParser
import org.gradle.api.GradleException
import org.gradle.api.InvalidUserDataException
+import org.gradle.api.artifacts.Configuration
import org.gradle.api.component.SoftwareComponentFactory
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.testfixtures.ProjectBuilder
@@ -460,6 +465,74 @@ class GrailsPublishGradlePluginTest extends Specification {
iude.message == 'An additional publication named `cli` is already registered.'
}
+ def 'resolved versions are only resolved when transitive dependencies are enabled'() {
+ given: 'a project depending on a sibling project'
+ def root = ProjectBuilder.builder().withName('root').build()
+ def lib = ProjectBuilder.builder().withName('lib').withParent(root).build()
+ lib.plugins.apply('java-library')
+ lib.group = 'org.example'
+ lib.version = '1.2.3'
+ def app = ProjectBuilder.builder().withName('app').withParent(root).build()
+ app.plugins.apply('java-library')
+ app.dependencies.add('implementation', app.dependencies.project(path: ':lib'))
+
+ and:
+ Property<Boolean> enabled = app.objects.property(Boolean).value(false)
+ Provider<Map<String, String>> versions = GrailsPublishGradlePlugin.resolvedVersionsProvider(app, enabled,
+ ['runtimeClasspath', 'notAConfiguration'])
+
+ expect: 'nothing is resolved while disabled'
+ versions.get() == [:]
+ app.configurations.getByName('runtimeClasspath').state == Configuration.State.UNRESOLVED
+
+ when:
+ enabled.set(true)
+
+ then: 'the versions of the resolved components, skipping configurations that do not exist'
+ versions.get() == ['org.example:lib': '1.2.3']
+ }
+
+ def 'dependencies without a version in the pom get the resolved version'() {
+ given:
+ Node pom = new XmlParser().parseText('''\
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <dependencies>
+ <dependency><groupId>org.example</groupId><artifactId>managed</artifactId></dependency>
+ <dependency><groupId>org.example</groupId><artifactId>empty</artifactId><version> </version></dependency>
+ <dependency><groupId>org.example</groupId><artifactId>pinned</artifactId><version>9.9.9</version></dependency>
+ </dependencies>
+ </project>'''.stripIndent())
+ Map<String, String> versions = ['org.example:managed': '1.0.0', 'org.example:empty': '2.0.0', 'org.example:pinned': '3.0.0']
+
+ when:
+ GrailsPublishGradlePlugin.setDependencyVersions(pom, ProjectBuilder.builder().build().providers.provider { versions })
+
+ then:
+ pom.'**'.findAll { it instanceof Node && localName(it as Node) == 'version' }*.text() == ['1.0.0', '2.0.0', '9.9.9']
+ }
+
+ def 'a dependency without a version that was not resolved fails the pom'() {
+ given:
+ Node pom = new XmlParser().parseText('''\
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <dependencies>
+ <dependency><groupId>org.example</groupId><artifactId>unknown</artifactId></dependency>
+ </dependencies>
+ </project>'''.stripIndent())
+
+ when:
+ GrailsPublishGradlePlugin.setDependencyVersions(pom, ProjectBuilder.builder().build().providers.provider { [:] })
+
+ then:
+ def iude = thrown(InvalidUserDataException)
+ iude.message == 'No version found for dependency org.example:unknown.'
+ }
+
+ /** The name of a pom node, which has a namespace unless the plugin appended it */
+ private static String localName(Node node) {
+ node.name() instanceof QName ? ((QName) node.name()).localPart : node.name().toString()
+ }
+
private static boolean causeChainContains(Throwable throwable, String expected) {
Throwable current = throwable
while (current != null) {