From f5f3903538392c66fff4aa001e2ee587ff27d169 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 Jun 2014 15:50:18 +0100 Subject: [PATCH] Resolve versionManagement configuration lazily and preserve exclusions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the versionManagement configuration was resolved as part of the Boot Gradle plugin being applied. This meant that no dependencies could be added to it and attempting to do so would result in a failure: “You can't change a configuration which is not in unresolved state”. This commit updates ApplyExcludeRules to wrap its processing in a before resolve action. This defers the resolution of the versionManagement configuration until one of the project’s other configurations is being resolved. Fixes #1077 In addition to the above, the transitive exclusions that the Gradle plugin provides were being lost if custom version management provided a version for the same dependency. This commit updates AbstractDependencies to preserve the exclusions from an existing dependency declaration while using the version from the newer dependency. This ensures that the exclusions remain while allowing versions to be overridden. Fixes #1079 --- .../gradle/CustomVersionManagementTests.java | 55 ++++++++++++++++ .../boot/gradle/ProjectCreator.java | 44 +++++++++++++ .../StarterDependenciesIntegrationTests.java | 16 +---- .../custom-version-management.gradle | 63 +++++++++++++++++++ .../test/resources/custom-versions.properties | 2 + ...ild.gradle => starter-dependencies.gradle} | 0 .../tools/AbstractDependencies.java | 21 ++++++- .../gradle/exclude/ApplyExcludeRules.java | 20 ++++-- 8 files changed, 201 insertions(+), 20 deletions(-) create mode 100644 spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/CustomVersionManagementTests.java create mode 100644 spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/ProjectCreator.java create mode 100644 spring-boot-integration-tests/src/test/resources/custom-version-management.gradle create mode 100644 spring-boot-integration-tests/src/test/resources/custom-versions.properties rename spring-boot-integration-tests/src/test/resources/{build.gradle => starter-dependencies.gradle} (100%) diff --git a/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/CustomVersionManagementTests.java b/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/CustomVersionManagementTests.java new file mode 100644 index 0000000000..26f8c01bba --- /dev/null +++ b/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/CustomVersionManagementTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed 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. + */ + +package org.springframework.boot.gradle; + +import java.io.IOException; + +import org.gradle.tooling.ProjectConnection; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.boot.dependency.tools.ManagedDependencies; + +/** + * Tests for using the Gradle plugin's support for custom version management + * + * @author Andy Wilkinson + */ +public class CustomVersionManagementTests { + + private static final String BOOT_VERSION = ManagedDependencies.get() + .find("spring-boot").getVersion(); + + private static ProjectConnection project; + + @BeforeClass + public static void createProject() throws IOException { + project = new ProjectCreator().createProject("custom-version-management"); + } + + @Test + public void exclusionsAreStillInPlace() { + project.newBuild().forTasks("checkExclusions") + .withArguments("-PbootVersion=" + BOOT_VERSION).run(); + } + + @Test + public void customSpringVersionIsUsed() { + project.newBuild().forTasks("checkSpringVersion") + .withArguments("-PbootVersion=" + BOOT_VERSION).run(); + } + +} diff --git a/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/ProjectCreator.java b/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/ProjectCreator.java new file mode 100644 index 0000000000..7a34a1be8a --- /dev/null +++ b/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/ProjectCreator.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed 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. + */ + +package org.springframework.boot.gradle; + +import java.io.File; +import java.io.IOException; + +import org.gradle.tooling.GradleConnector; +import org.gradle.tooling.ProjectConnection; +import org.gradle.tooling.internal.consumer.DefaultGradleConnector; +import org.springframework.util.FileCopyUtils; + +/** + * @author Andy Wilkinson + */ +public class ProjectCreator { + + public ProjectConnection createProject(String name) throws IOException { + File projectDirectory = new File("target/" + name); + projectDirectory.mkdirs(); + + File gradleScript = new File(projectDirectory, "build.gradle"); + FileCopyUtils.copy(new File("src/test/resources/" + name + ".gradle"), + gradleScript); + + GradleConnector gradleConnector = GradleConnector.newConnector(); + ((DefaultGradleConnector) gradleConnector).embedded(true); + return gradleConnector.forProjectDirectory(projectDirectory).connect(); + } +} diff --git a/spring-boot-integration-tests/src/test/java/org/springframework/boot/starter/StarterDependenciesIntegrationTests.java b/spring-boot-integration-tests/src/test/java/org/springframework/boot/starter/StarterDependenciesIntegrationTests.java index f6874e66d3..b2548b5d0b 100644 --- a/spring-boot-integration-tests/src/test/java/org/springframework/boot/starter/StarterDependenciesIntegrationTests.java +++ b/spring-boot-integration-tests/src/test/java/org/springframework/boot/starter/StarterDependenciesIntegrationTests.java @@ -23,9 +23,7 @@ import java.util.Arrays; import java.util.List; import org.gradle.tooling.BuildException; -import org.gradle.tooling.GradleConnector; import org.gradle.tooling.ProjectConnection; -import org.gradle.tooling.internal.consumer.DefaultGradleConnector; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -33,14 +31,14 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.springframework.boot.dependency.tools.ManagedDependencies; -import org.springframework.util.FileCopyUtils; +import org.springframework.boot.gradle.ProjectCreator; import static org.junit.Assert.fail; /** * Tests for the various starter projects to check that they don't pull in unwanted * transitive dependencies when used with Gradle - * + * * @author Andy Wilkinson */ @RunWith(Parameterized.class) @@ -76,15 +74,7 @@ public class StarterDependenciesIntegrationTests { @BeforeClass public static void createProject() throws IOException { - File projectDirectory = new File("target/starter-dependencies"); - projectDirectory.mkdirs(); - - File gradleScript = new File(projectDirectory, "build.gradle"); - FileCopyUtils.copy(new File("src/test/resources/build.gradle"), gradleScript); - - GradleConnector gradleConnector = GradleConnector.newConnector(); - ((DefaultGradleConnector) gradleConnector).embedded(true); - project = gradleConnector.forProjectDirectory(projectDirectory).connect(); + project = new ProjectCreator().createProject("starter-dependencies"); } @BeforeClass diff --git a/spring-boot-integration-tests/src/test/resources/custom-version-management.gradle b/spring-boot-integration-tests/src/test/resources/custom-version-management.gradle new file mode 100644 index 0000000000..d19e2d209b --- /dev/null +++ b/spring-boot-integration-tests/src/test/resources/custom-version-management.gradle @@ -0,0 +1,63 @@ +import org.gradle.api.artifacts.result.UnresolvedDependencyResult; + +buildscript { + repositories { + mavenLocal() + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${project.bootVersion}") + } +} + +repositories { + mavenLocal() + mavenCentral() +} + +configurations { + exclusions + springVersion +} + +apply plugin: 'spring-boot' + +dependencies { + exclusions "org.springframework.boot:spring-boot-starter" + springVersion "org.springframework:spring-core" + versionManagement files('../../src/test/resources/custom-versions.properties') +} + +task checkExclusions { + doFirst { + def commonsLogging = getResolvedDependencies(configurations.exclusions) + .find { it.selected.id.group == 'commons-logging' } + if (commonsLogging) { + throw new GradleException("commong-logging is a transitive dependency") + } + } +} + +task checkSpringVersion { + doFirst { + def springVersions = getResolvedDependencies(configurations.springVersion) + .findAll{it.selected.id.group == 'org.springframework'} + .collect {it.selected.id.version} + if (springVersions.size() != 1 || !springVersions.contains("4.0.3.RELEASE")) { + throw new GradleException("Spring version was not 4.0.3.RELEASE") + } + } +} + +def getResolvedDependencies(def configuration) { + def allDependencies = configuration.incoming + .resolutionResult.allDependencies + .split { it instanceof UnresolvedDependencyResult } + + def unresolved = allDependencies.first() + def resolved = allDependencies.last() + if (unresolved) { + throw new GradleException("Resolution failed: ${unresolved}") + } + resolved +} diff --git a/spring-boot-integration-tests/src/test/resources/custom-versions.properties b/spring-boot-integration-tests/src/test/resources/custom-versions.properties new file mode 100644 index 0000000000..20ac076407 --- /dev/null +++ b/spring-boot-integration-tests/src/test/resources/custom-versions.properties @@ -0,0 +1,2 @@ +org.springframework\:spring-core=4.0.3.RELEASE +org.springframework.boot\:spring-boot-starter=1.1.0.RELEASE \ No newline at end of file diff --git a/spring-boot-integration-tests/src/test/resources/build.gradle b/spring-boot-integration-tests/src/test/resources/starter-dependencies.gradle similarity index 100% rename from spring-boot-integration-tests/src/test/resources/build.gradle rename to spring-boot-integration-tests/src/test/resources/starter-dependencies.gradle diff --git a/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/AbstractDependencies.java b/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/AbstractDependencies.java index 0f8be49c18..32693cd0e9 100644 --- a/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/AbstractDependencies.java +++ b/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/AbstractDependencies.java @@ -16,14 +16,19 @@ package org.springframework.boot.dependency.tools; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import org.springframework.boot.dependency.tools.Dependency.Exclusion; + /** * Abstract base implementation for {@link Dependencies}. - * + * * @author Phillip Webb + * @author Andy Wilkinson * @since 1.1.0 */ abstract class AbstractDependencies implements Dependencies { @@ -53,10 +58,24 @@ abstract class AbstractDependencies implements Dependencies { } protected void add(ArtifactAndGroupId artifactAndGroupId, Dependency dependency) { + Dependency existing = this.byArtifactAndGroupId.get(artifactAndGroupId); + if (existing != null) { + dependency = mergeDependencies(existing, dependency); + } this.byArtifactAndGroupId.put(artifactAndGroupId, dependency); this.byArtifactId.put(dependency.getArtifactId(), dependency); } + private Dependency mergeDependencies(Dependency existingDependency, + Dependency newDependency) { + List combinedExclusions = new ArrayList(); + combinedExclusions.addAll(existingDependency.getExclusions()); + combinedExclusions.addAll(newDependency.getExclusions()); + + return new Dependency(newDependency.getGroupId(), newDependency.getArtifactId(), + newDependency.getVersion(), combinedExclusions); + } + /** * Simple holder for an artifact+group ID. */ diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/exclude/ApplyExcludeRules.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/exclude/ApplyExcludeRules.java index e1dcea1e98..8b493c6e59 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/exclude/ApplyExcludeRules.java +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/exclude/ApplyExcludeRules.java @@ -21,6 +21,7 @@ import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.ResolvableDependencies; import org.gradle.api.internal.artifacts.DefaultExcludeRule; import org.gradle.api.logging.Logger; import org.springframework.boot.dependency.tools.Dependency.Exclusion; @@ -45,12 +46,19 @@ public class ApplyExcludeRules implements Action { @Override public void execute(Configuration configuration) { - configuration.getDependencies().all(new Action() { - @Override - public void execute(Dependency dependency) { - applyExcludeRules(dependency); - } - }); + if (!VersionManagedDependencies.CONFIGURATION.equals(configuration.getName())) { + configuration.getIncoming().beforeResolve(new Action() { + @Override + public void execute(ResolvableDependencies resolvableDependencies) { + resolvableDependencies.getDependencies().all(new Action() { + @Override + public void execute(Dependency dependency) { + applyExcludeRules(dependency); + } + }); + } + }); + } } private void applyExcludeRules(Dependency dependency) {