Skip to content

Commit

Permalink
Polish and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
jkschneider committed Dec 3, 2015
2 parents 6a0615e + 4ce98d2 commit 123fdc6
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 34 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
==================

Expand Down
65 changes: 46 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand All @@ -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.

Expand All @@ -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.

Expand All @@ -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.

Expand All @@ -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
}
}
Expand Down Expand Up @@ -174,11 +174,11 @@ The resultant BOM would look like this:
</project>
```

## Version selection rules
## 5. Version selection rules

The hierarchy of preference for versions is:

### 1. Forced dependencies
### 5.1. Forced dependencies

```groovy
configurations.all {
Expand 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.

Expand All @@ -210,7 +210,7 @@ dependencies {
}
```

### 3. Dependency recommendations
### 5.3. Dependency recommendations

This is the basic case described elsewhere in the documentation;

Expand All @@ -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 {
Expand All @@ -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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Project> {
private Logger logger = Logging.getLogger(DependencyRecommendationsPlugin.class);

Expand All @@ -34,14 +31,12 @@ public void execute(JavaPlugin javaPlugin) {
project.getConfigurations().all(new Action<Configuration>() {
@Override
public void execute(final Configuration conf) {
final List<String> firstOrderDepsWithoutVersions = new ArrayList<>();

final RecommendationStrategyFactory rsFactory = new RecommendationStrategyFactory(project);
conf.getIncoming().beforeResolve(new Action<ResolvableDependencies>() {
@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);
}
}
});
Expand All @@ -50,19 +45,16 @@ 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()) {
if (requested.getGroup().equals(force.getGroup()) && requested.getName().equals(force.getName())) {
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);
}
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package netflix.nebula.dependency.recommender;

public enum RecommendationStrategies {
ConflictResolved(RecommendationsConflictResolvedStrategy.class),
OverrideTransitives(RecommendationsOverrideTransitivesStrategy.class);

private Class<? extends RecommendationStrategy> strategyClass;

RecommendationStrategies(Class<? extends RecommendationStrategy> strategyClass) {
this.strategyClass = strategyClass;
}

public Class<? extends RecommendationStrategy> getStrategyClass() {
return strategyClass;
}
}
Original file line number Diff line number Diff line change
@@ -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 <code>true</code> if a version has been recommended, <code>false</code> otherwise
*/
public abstract boolean recommendVersion(DependencyResolveDetails details, String version);

/**
* @param details the details to get coordinates from
* @return the coordinates in the form of "<group>:<name>", taken from details.requested.
*/
protected String getCoord(DependencyResolveDetails details) {
ModuleVersionSelector requested = details.getRequested();
return requested.getGroup() + ":" + requested.getName();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
Loading

0 comments on commit 123fdc6

Please sign in to comment.