jdaugherty commented on code in PR #43:
URL: 
https://github.com/apache/grails-gradle-publish/pull/43#discussion_r4190489945


##########
plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy:
##########
@@ -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)) {

Review Comment:
   Fixed in 187e710. `collectResolvedVersions` now collects the 
`UnresolvedDependencyResult`s and throws a `GradleException` naming each of 
them, with the resolution failure as its cause. With the configuration cache 
the provider is evaluated when the entry is stored, so the failure fails 
storing it and nothing is cached; the next build configures again. New 
functional test: "a dependency that cannot be resolved fails storing the 
configuration cache entry, so a later build resolves it again".
   
   On the second effect: following your comment on line 825, the versions are 
now only resolved when a dependency has no version. Without the configuration 
cache, a broken classpath whose versions are not needed therefore no longer 
stops the publish; when they are needed, it fails as before. With the 
configuration cache they are resolved when the entry is stored, so any 
unresolved dependency fails the build.



##########
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 ->

Review Comment:
   Fixed in 187e710. The plugin now orders the publish tasks of publications 
with the same coordinates (groupId, artifactId and version) that publish to the 
same repository, or both to Maven local, by task name 
(`orderPublishTasksSharingCoordinates`). The fixture's workaround is removed. 
The "publications sharing artifacts" case of `ReleaseSigningSpec` now asserts 
the order for both targets, and a unit test covers which tasks get ordered.



##########
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',

Review Comment:
   Right, it cannot store an entry. I checked, and even without the close 
task's marking, the Nexus publish tasks make Gradle run this build without the 
configuration cache, because the Nexus plugin configures its repository with 
explicit credentials. In 187e710 the spec asserts that outcome ("Configuration 
cache disabled because incompatible task was found." and no entry stored), the 
comment says so, and `--configuration-cache-problems=fail` is dropped. The 
release path's task state (the signing and publishing tasks) is serialized by 
the MAVEN_PUBLISH release feature of `PublishTargetsSpec`, which now asserts 
its entry is stored, and the separate close build now asserts it runs without 
storing one.



##########
plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy:
##########
@@ -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 ->

Review Comment:
   Fixed in 187e710: the marking is registered once per build (a flag on the 
build's extra properties), and still also when the root project applied the 
Nexus plugin itself (unit test). On combined builds: without the marking, 
`publishToSonatype closeAndReleaseSonatypeStagingRepository` still runs without 
storing an entry, as does a Nexus snapshot publish with no transition task in 
it, because the Nexus plugin configures its repository with explicit 
credentials. So today the marking costs those builds nothing. The comment 
claiming they could be stored was wrong and is corrected. The root project's 
tasks are configured in the same place that applies the Nexus plugin to the 
root project, which already reaches across projects.



##########
plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy:
##########
@@ -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) {

Review Comment:
   Fixed in 187e710. When a subclass overrides `setDependencyVersions(Node, 
Project, List)`, the plugin calls the override when the pom is generated, as 
before, and marks that publication's pom task as not compatible with the 
configuration cache, since the override receives the project. A unit test 
generates the pom with an overriding subclass.



##########
plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy:
##########
@@ -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]

Review Comment:
   Fixed in 187e710: the resolved version is merged into the existing version 
object, keeping `rejects` and any other key. Covered by a unit test and by the 
platform-managed-versions functional fixture, which now rejects a version of 
groovy-json and checks that the reject is published.



##########
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

Review Comment:
   Done in 187e710, as a fallback rather than the first choice: when neither 
MAVEN_PUBLISH_USERNAME/PASSWORD nor the 
mavenPublishUsername/mavenPublishPassword properties are given and the 
repository is http or https, the plugin uses 
`repo.credentials(PasswordCredentials)`, so Gradle reads 
`mavenUsername`/`mavenPassword` itself and the build stores an entry. Explicit 
credentials still come first, because `mavenUsername` is a common name in 
user-home gradle.properties files for other repositories, and preferring it 
would change existing builds. The README documents it, and a new 
`PublishTargetsSpec` feature publishes with 
`ORG_GRADLE_PROJECT_mavenUsername`/`mavenPassword` over https, stores the 
entry, and publishes again from it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to