diff --git a/CHANGELOG.md b/CHANGELOG.md index 4107415..ab81cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +3.1.0 / 2015-12-03 +================== + +* Offer two strategies for how recommendations interact with transitive dependencies: + - `ConflictResolved` - If there is no first order recommend-able dependency, a transitive will conflict resolve with dependencies in the recommendations listing + - `OverrideTransitives` - If a recommendation conflicts with a transitive, pick the transitive + 3.0.3 / 2015-11-04 ================== diff --git a/README.md b/README.md index 0657501..0e67fbd 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Table of Contents * [Conflict resolution and transitive dependencies](#conflict-resolution-and-transitive-dependencies) * [Accessing recommended versions directly](#accessing-recommended-versions-directly) -## Usage +## 1. Usage **NOTE:** This plugin has not yet been released! @@ -41,7 +41,7 @@ buildscript { apply plugin: 'nebula.dependency-recommender' ``` -## Dependency recommender configuration +## 2. Dependency recommender configuration Dependency recommenders are the source of versions. If more than one recommender defines a recommended version for a module, the first recommender specified will win. @@ -58,7 +58,7 @@ dependencies { } ``` -## Built-in recommendation providers +## 3. Built-in recommendation providers Several recommendation providers pack with the plugin. The file-based providers all a shared basic configuration that is described separately. @@ -69,7 +69,7 @@ Several recommendation providers pack with the plugin. The file-based providers * [Map](https://github.com/nebula-plugins/nebula-dependency-recommender/wiki/Map-Provider) * [Custom](https://github.com/nebula-plugins/nebula-dependency-recommender/wiki/Custom-Provider) -## Producing a Maven BOM for use as a dependency recommendation source +## 4. Producing a Maven BOM for use as a dependency recommendation source Suppose you want to produce a BOM that contains a recommended version for commons-configuration. @@ -96,20 +96,20 @@ publishing { parent(MavenPublication) { // the transitive closure of this configuration will be flattened and added to the dependency management section dependencyManagement.fromConfigurations { configurations.compile } - + // alternative syntax when you want to explicitly add a dependency with no transitives dependencyManagement.withDependencies { 'manual:dep:1' } - + // the bom will be generated with dependency coordinates of netflix:module-parent:1 artifactId = 'module-parent' version = 1 - + // further customization of the POM is allowed if desired pom.withXml { asNode().appendNode('description', 'A demonstration of maven POM customization') } } } repositories { - maven { + maven { url "$buildDir/repo" // point this to your destination repository } } @@ -174,11 +174,11 @@ The resultant BOM would look like this: ``` -## Version selection rules +## 5. Version selection rules The hierarchy of preference for versions is: -### 1. Forced dependencies +### 5.1. Forced dependencies ```groovy configurations.all { @@ -196,7 +196,7 @@ dependencies { } ``` -### 2. Direct dependencies with a version qualifier +### 5.2. Direct dependencies with a version qualifier Direct dependencies with a version qualifier trump recommendations, even if the version qualifier refers to an older version. @@ -210,7 +210,7 @@ dependencies { } ``` -### 3. Dependency recommendations +### 5.3. Dependency recommendations This is the basic case described elsewhere in the documentation; @@ -224,24 +224,51 @@ dependencies { } ``` -### 4. Transitive dependencies +### 5.4. Transitive dependencies -Whenever a recommendation provider can provide a version recommendation for a transitive dependency AND there is a first order dependency on that transitive that has no version specified, the recommendation overrides versions of the module that are provided by transitively. +Transitive dependencies interact with the plugin in different ways depending on which of two available strategies is selected. -Consider the following example with dependencies on `commons-configuration` and `commons-logging`. `commons-configuration:1.6` depends on `commons-logging:1.1.1`. Even though `commons-configuration` indicates that it prefers version `1.1.1`, `1.0` is selected because of the recommendation provider. +#### 5.4.1. `OverrideTransitives` Strategy (default) + +In the following example version `commons-logging:commons-logging:1.0` is selected even though `commons-logging` is not explicitly mentioned in dependencies. This would not work with the ConflictResolved strategy: ```groovy dependencyRecommendations { + strategy OverrideTransitives // this is the default, so this line is NOT necessary map recommendations: ['commons-logging:commons-logging': '1.0'] } dependencies { compile 'commons-configuration:commons-configuration:1.6' - compile 'commons-logging:commons-logging' } ``` -Conversely, if no recommendation can be found for a dependency that has no version, but a version is provided by a transitive the version provided by the transitive is applied. In this scenario, if several transitives provide versions for the module, normal Gradle conflict resolution applies. +#### 5.4.2. `ConflictResolved` Strategy + +Consider the following example with dependencies on `commons-configuration` and `commons-logging`. `commons-configuration:1.6` depends on `commons-logging:1.1.1`. In this case, the transitive dependency on `commons-logging` via `commons-configuration` is conflict resolved against the recommended version of 1.0. Normal Gradle conflict resolution selects 1.1.1. + +```groovy +dependencyRecommendations { + strategy ConflictResolved + map recommendations: ['commons-logging:commons-logging': '1.0'] +} + +dependencies { + compile 'commons-configuration:commons-configuration:1.6' +} +``` + +However, if we have a first-order recommendation eligible dependency on `commons-logging`, 1.0 will be selected. + +```groovy +dependencies { + compile 'commons-configuration:commons-logging' +} +``` + +#### 5.4.3. Bubbling up recommendations from transitives + +If no recommendation can be found in the recommendation sources for a dependency that has no version, but a version is provided by a transitive, the version provided by the transitive is applied. In this scenario, if several transitives provide versions for the module, normal Gradle conflict resolution applies. ```groovy dependencyRecommendations { @@ -254,11 +281,11 @@ dependencies { } ``` -## Conflict resolution and transitive dependencies +## 6. Conflict resolution and transitive dependencies * [Resolving differences between recommendation providers](https://github.com/nebula-plugins/nebula-dependency-recommender/wiki/Resolving-Differences-Between-Recommendation-Providers) -## Accessing recommended versions directly +## 7. Accessing recommended versions directly The `dependencyRecommendations` container can be queried directly for a recommended version: diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java b/src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java index d1c52f0..9c21ed4 100644 --- a/src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java +++ b/src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java @@ -13,9 +13,6 @@ import org.gradle.api.plugins.ExtraPropertiesExtension; import org.gradle.api.plugins.JavaPlugin; -import java.util.ArrayList; -import java.util.List; - public class DependencyRecommendationsPlugin implements Plugin { private Logger logger = Logging.getLogger(DependencyRecommendationsPlugin.class); @@ -34,14 +31,12 @@ public void execute(JavaPlugin javaPlugin) { project.getConfigurations().all(new Action() { @Override public void execute(final Configuration conf) { - final List firstOrderDepsWithoutVersions = new ArrayList<>(); - + final RecommendationStrategyFactory rsFactory = new RecommendationStrategyFactory(project); conf.getIncoming().beforeResolve(new Action() { @Override public void execute(ResolvableDependencies resolvableDependencies) { for (Dependency dependency : resolvableDependencies.getDependencies()) { - if (dependency.getVersion() == null || dependency.getVersion().isEmpty()) - firstOrderDepsWithoutVersions.add(dependency.getGroup() + ":" + dependency.getName()); + rsFactory.getRecommendationStrategy().inspectDependency(dependency); } } }); @@ -50,7 +45,6 @@ public void execute(ResolvableDependencies resolvableDependencies) { @Override public void execute(DependencyResolveDetails details) { ModuleVersionSelector requested = details.getRequested(); - String coord = requested.getGroup() + ":" + requested.getName(); // don't interfere with the way forces trump everything for (ModuleVersionSelector force : conf.getResolutionStrategy().getForcedModules()) { @@ -58,11 +52,9 @@ public void execute(DependencyResolveDetails details) { return; } } - String version = getRecommendedVersionRecursive(project, requested); - if (version != null && firstOrderDepsWithoutVersions.contains(coord)) { + if(rsFactory.getRecommendationStrategy().recommendVersion(details, version)) { logger.info("Recommending version " + version + " for dependency " + requested.getGroup() + ":" + requested.getName()); - details.useVersion(version); } } }); diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategies.java b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategies.java new file mode 100644 index 0000000..a4d2f6d --- /dev/null +++ b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategies.java @@ -0,0 +1,16 @@ +package netflix.nebula.dependency.recommender; + +public enum RecommendationStrategies { + ConflictResolved(RecommendationsConflictResolvedStrategy.class), + OverrideTransitives(RecommendationsOverrideTransitivesStrategy.class); + + private Class strategyClass; + + RecommendationStrategies(Class strategyClass) { + this.strategyClass = strategyClass; + } + + public Class getStrategyClass() { + return strategyClass; + } +} diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategy.java b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategy.java new file mode 100644 index 0000000..80d59c4 --- /dev/null +++ b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategy.java @@ -0,0 +1,34 @@ +package netflix.nebula.dependency.recommender; + +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.DependencyResolveDetails; +import org.gradle.api.artifacts.ModuleVersionSelector; + +/** + * Defines in which cases recommendations are applied + */ +public abstract class RecommendationStrategy { + + /** + * This hook is called for each dependency in a project. It collects the dependencies we are interested in for determining if a recommendation should be applied. + * @param dependency the dependency to inspect. + */ + public abstract void inspectDependency(Dependency dependency); + + /** + * Puts the recommended version on details.useVersion depending on the strategy used + * @param details the details to recommend a version for + * @param version the version to be potentially recommended for the requested artifact + * @return true if a version has been recommended, false otherwise + */ + public abstract boolean recommendVersion(DependencyResolveDetails details, String version); + + /** + * @param details the details to get coordinates from + * @return the coordinates in the form of ":", taken from details.requested. + */ + protected String getCoord(DependencyResolveDetails details) { + ModuleVersionSelector requested = details.getRequested(); + return requested.getGroup() + ":" + requested.getName(); + } +} diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategyFactory.java b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategyFactory.java new file mode 100644 index 0000000..d90b861 --- /dev/null +++ b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategyFactory.java @@ -0,0 +1,29 @@ +package netflix.nebula.dependency.recommender; + +import netflix.nebula.dependency.recommender.provider.RecommendationProviderContainer; +import org.gradle.api.Project; + +/** + * Creates RecommendationStrategy lazily on demand and caches it. + * This is used to allow for scoped recommendationStrategies (e.g. per configuration as in DependencyRecommendationsPlugin) + */ +public class RecommendationStrategyFactory { + private final Project project; + private RecommendationStrategy recommendationStrategy; + + public RecommendationStrategyFactory(Project project) { + this.project = project; + } + + public RecommendationStrategy getRecommendationStrategy() { + if(recommendationStrategy == null) { + try { + RecommendationProviderContainer recommendationProviderContainer = project.getExtensions().getByType(RecommendationProviderContainer.class); + recommendationStrategy = recommendationProviderContainer.getStrategy().getStrategyClass().newInstance(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + return recommendationStrategy; + } +} diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationsConflictResolvedStrategy.java b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationsConflictResolvedStrategy.java new file mode 100644 index 0000000..1b9f499 --- /dev/null +++ b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationsConflictResolvedStrategy.java @@ -0,0 +1,33 @@ +package netflix.nebula.dependency.recommender; + +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.DependencyResolveDetails; + +import java.util.ArrayList; +import java.util.List; + +/** + * Recommendations are conflict resolved against transitive dependencies. In the event that a transitive dependency + * has a higher version of a library than a recommendation, the recommendation only wins if there is a first order + * recommend-able dependency. + */ +public class RecommendationsConflictResolvedStrategy extends RecommendationStrategy { + + private List firstOrderDepsWithoutVersions = new ArrayList<>(); + + @Override + public void inspectDependency(Dependency dependency) { + if (dependency.getVersion() == null || dependency.getVersion().isEmpty()) { + firstOrderDepsWithoutVersions.add(dependency.getGroup() + ":" + dependency.getName()); + } + } + + @Override + public boolean recommendVersion(DependencyResolveDetails details, String version) { + if (version != null && firstOrderDepsWithoutVersions.contains(getCoord(details))) { + details.useVersion(version); + return true; + } + return false; + } +} diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationsOverrideTransitivesStrategy.java b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationsOverrideTransitivesStrategy.java new file mode 100644 index 0000000..2a6ba92 --- /dev/null +++ b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationsOverrideTransitivesStrategy.java @@ -0,0 +1,28 @@ +package netflix.nebula.dependency.recommender; + +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.DependencyResolveDetails; + +import java.util.ArrayList; +import java.util.List; + +public class RecommendationsOverrideTransitivesStrategy extends RecommendationStrategy { + + private List firstOrderDepsWithVersions = new ArrayList<>(); + + @Override + public void inspectDependency(Dependency dependency) { + if (dependency.getVersion() != null && !dependency.getVersion().isEmpty()) { + firstOrderDepsWithVersions.add(dependency.getGroup() + ":" + dependency.getName()); + } + } + + @Override + public boolean recommendVersion(DependencyResolveDetails details, String version) { + if (version != null && !firstOrderDepsWithVersions.contains(getCoord(details))) { + details.useVersion(version); + return true; + } + return false; + } +} diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java b/src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java index 341527b..9670fdf 100644 --- a/src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java +++ b/src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java @@ -1,6 +1,7 @@ package netflix.nebula.dependency.recommender.provider; import groovy.lang.Closure; +import netflix.nebula.dependency.recommender.RecommendationStrategies; import org.gradle.api.Action; import org.gradle.api.Namer; import org.gradle.api.Project; @@ -12,7 +13,13 @@ import java.util.Map; public class RecommendationProviderContainer extends DefaultNamedDomainObjectList { + private Project project; + private RecommendationStrategies strategy = RecommendationStrategies.OverrideTransitives; + + // Make strategies available without import + public static final RecommendationStrategies OverrideTransitives = RecommendationStrategies.OverrideTransitives; + public static final RecommendationStrategies ConflictResolved = RecommendationStrategies.ConflictResolved; private final Action addLastAction = new Action() { public void execute(RecommendationProvider r) { @@ -110,4 +117,12 @@ public String getRecommendedVersion(String group, String name) { } return null; } + + public RecommendationStrategies getStrategy() { + return strategy; + } + + public void setStrategy(RecommendationStrategies strategy) { + this.strategy = strategy; + } } diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/publisher/MavenBomXmlGenerator.groovy b/src/main/groovy/netflix/nebula/dependency/recommender/publisher/MavenBomXmlGenerator.groovy index 051eecd..fc3b9e6 100644 --- a/src/main/groovy/netflix/nebula/dependency/recommender/publisher/MavenBomXmlGenerator.groovy +++ b/src/main/groovy/netflix/nebula/dependency/recommender/publisher/MavenBomXmlGenerator.groovy @@ -1,13 +1,10 @@ package netflix.nebula.dependency.recommender.publisher - import netflix.nebula.dependency.recommender.ModuleNotationParser -import org.gradle.api.IllegalDependencyNotation import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.ModuleVersionIdentifier import org.gradle.api.artifacts.ResolvedDependency import org.gradle.api.publish.maven.MavenPublication -import org.gradle.util.GUtil class MavenBomXmlGenerator { Project project @@ -44,7 +41,7 @@ class MavenBomXmlGenerator { generateDependencyManagementXml(pub, dependencies.collect { ModuleNotationParser.parse(it) }) } - protected generateDependencyManagementXml(MavenPublication pub, Iterable deps) { + protected static generateDependencyManagementXml(MavenPublication pub, Iterable deps) { pub.pom.withXml { Node root = it.asNode() def dependencyManagement = root.getByName("dependencyManagement") diff --git a/src/test/groovy/netflix/nebula/dependency/recommender/RecommendationProviderContainerSpec.groovy b/src/test/groovy/netflix/nebula/dependency/recommender/RecommendationProviderContainerSpec.groovy index b00d70a..f007cca 100644 --- a/src/test/groovy/netflix/nebula/dependency/recommender/RecommendationProviderContainerSpec.groovy +++ b/src/test/groovy/netflix/nebula/dependency/recommender/RecommendationProviderContainerSpec.groovy @@ -145,6 +145,7 @@ class RecommendationProviderContainerSpec extends Specification { def 'transitive dependency versions are not overriden by recommendations unless there is a corresponding first order dependency'() { setup: project.dependencyRecommendations { + strategy ConflictResolved map recommendations: ['commons-logging:commons-logging': '1.1'] } @@ -161,6 +162,25 @@ class RecommendationProviderContainerSpec extends Specification { commonsLang.moduleVersion == '1.1.1' } + def 'transitive dependency versions are overriden by recommendations with OnlyReccomendIfNoFirstOrderWithVersionExists strategy'() { + setup: + project.dependencyRecommendations { + map recommendations: ['commons-logging:commons-logging': '1.1'] + } + + when: + project.dependencies { + compile 'commons-configuration:commons-configuration:1.6' + // no first order dependency on commons-logging, but still recommend with ONLY_RECOMMNED_IF_NO_FIRST_ORDER_WITH_VERSION_EXISTS strategy + } + + def commonsConfig = project.configurations.compile.resolvedConfiguration.firstLevelModuleDependencies.iterator().next() + def commonsLang = commonsConfig.children.find { it.moduleName == 'commons-logging' } + + then: + commonsLang.moduleVersion == '1.1' + } + def 'transitive dependencies are used as a source of recommendations when no explicit recommendation is provided for a module'() { setup: project.dependencyRecommendations {