Using Gradle 1.12, is it possible to create a resolution strategy rule that replaces a module dependency with a project one under certain circumstances?
The reason for this is that we have many projects in the company (dozens), and I don't want to pollute the build scripts with things like:
dependencies {
elastic "company:somelib:1.0.+", "SomeLib"
}
Instead i'd like to achieve something along the lines of:
dependencies {
compile "company:somelib:1.0.+"
}
...
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (/* ... Check if project is in build ... */) {
details.useTarget project(':SomeLib')
}
}
}
So far I have not been able to replace a module dependency with a project one in a resolution strategy rule. Is there a way to achieve this?
EDIT: These are things I tried (all resulted in an error):
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (/* ... Check if project is in build ... */) {
details.useTarget project(':SomeLib')
details.useTarget ':SomeLib'
// Since I noticed this is how actual project dependencies look like
details.useTarget 'Project:SomeLib:version'
details.useTarget new DefaultProjectDependency(...)
}
}
}
For future reference, this is the code that I've used in the end. This example implements our very specific flavor of this desired behavior, but others could take it as a starting point and tweak as needed.
gradle.projectsEvaluated {
def prjMap = [:]
allprojects.each { prj ->
prjMap[prj.archivesBaseName] = prj
}
allprojects.each { prj ->
def replace = []
prj.configurations.each { conf ->
conf.dependencies.each { dep ->
if (dep.group == 'company' && prjMap[dep.name] != null) {
replace += [conf: conf.name, dep: dep]
}
}
}
replace.each { rep ->
println "Replacing: $prj.name\t$rep.conf\t$rep.dep.name ==>> ${prjMap[rep.dep.name].name}"
prj.configurations.all*.exclude(group: 'company', module: rep.dep.name)
rep.dep.properties.excludeRules.each { ex ->
prj.configurations.all*.exclude(group: ex.group, module: ex.module)
}
prj.dependencies.add(rep.conf, prjMap[rep.dep.name])
}
}
}
Note that while replacing, I used aggressive exclude statements. This is because we have a hellish nightmare of cyclic dependencies and lib projects declaring whole apps as a transitive dep because they need some value class. In a more sane environment, one could simply eliminate the previous dependency entry like so:
replace.each { rep ->
println "Replacing: $prj.name\t$rep.conf\t$rep.dep.name ==>> ${prjMap[rep.dep.name].name}"
prj.dependencies.remove(rep.dep)
prj.dependencies.add(rep.conf, prjMap[rep.dep.name])
}
No, a resolution strategy cannot do this. It might be possible to implement a solution that, at the end of the configuration phase, iterates over the configurations' dependencies and replaces certain external dependencies with project dependencies. Not sure if it can be done without using Gradle internals, though.
Related
I want to create a complete list of dependencies for all projects in my Gradle build to track changes over time.
For this purpose, both the list of projects and the list of all dependencies (direct and transitive) must be sorted. Without both, the diff between releases is useless.
My first attempt was
fun listDependencies(project: Project, configName: String) {
val config = project.configurations.findByName(configName) ?: return
println()
println("$configName of module ${project.name}:")
config.allDependencies.map {
" ${it.group}:${it.name}:${it.version}"
}.sorted().forEach {
println(it)
}
}
fun listDependencies(project: Project) {
listDependencies(project, "compileClasspath")
listDependencies(project, "testCompileClasspath")
}
/** Create sorted list of dependencies per module and only for the compileClasspath and testCompileClasspath */
task("listDependencies") {
doLast {
println("Dependency list per project")
allprojects.sortedBy { it.name }
.forEach {
listDependencies(it)
}
}
}
In the output, most versions are null and all transitive dependencies are missing.
I had to force dependency resolution by changing the config.allDependencies above to
config.resolvedConfiguration.resolvedArtifacts.map {
" ${it.moduleVersion}"
}.sorted().forEach {
println(it)
}
This works but gives me a warning that I shouldn't call resolvedConfiguration (Resolving unsafe configuration resolution errors).
What do I have to change to run this task once after the configuration phase only when I ask for it by running Gradle with ./gradlew listDependencies?
We don't need this task in every build.
I'm trying to do something which I feel should be relatively straightforward but nothing on the internet seems to be giving me what I'm after.
Basically, I want to take the compile/testCompile dependencies from my build.gradle (I don't want any of the sub-dependencies - just literally as they are in the file), and for each one I want to do something with the group name, name and version. Let's just say I want to print them.
So here is my build.gradle:
dependencies {
compile 'org.spring.framework.cloud:spring-cloud-example:1.0.6'
compile 'org.spring.framework.cloud:spring-cloud-other-example:1.1.6'
testCompile 'org.spring.framework.cloud:spring-cloud-example-test:3.1.2'
}
task printDependencies {
//some code in here to get results such as...
// org.spring.framework.cloud spring-cloud-other-example 1.1.6
}
Thanks all.
To iterate over all you dependencies, you can itareate over all the configurations and the over all configuration dependencies. Like so:
task printDependencies {
project.configurations.each { conf ->
conf.dependencies.each { dep ->
println "${dep.group}:${dep.name}:${dep.version}"
}
}
}
If you need exactly configuration dependencies, you can get them separately:
task printDependencies {
project.configurations.getByName('compile') { conf ->
conf.dependencies.each { dep ->
println "${dep.group}:${dep.name}:${dep.version}"
}
}
project.configurations.getByName('testCompile') { conf ->
conf.dependencies.each { dep ->
println "${dep.group}:${dep.name}:${dep.version}"
}
}
}
Or modify the first example, by adding condition to check conf.name
The end goal is to replace versions of transitive dependencies with a different version. The caveat is that the replacement should be picked up by those dependent upon the library being built (I don't know if it's a standard Gradle or a plugin but if we exclude a transitive dependency, the produced ivy.xml file will have that information).
One possible way to achieve the end goal is to exclude the dependency in question then force that dependency later on.
The way to exclude a dependency is with something like:
dependencies {
compile 'org:name:version' {
exclude(group: 'group', module: 'module')
}
}
The way to force a dependency is with something like:
configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails dependencyResolveDetails ->
final requestedDependency = dependencyResolveDetails.requested
if (requestedDependency.group == 'org' && requestedDependency.name == 'name') {
force 'group:module:good_version'
}
}
}
}
In order to tie the two together, the resolutionStrategy must know which dependencies actually excluded the transitive dependency that will later be forced. How can this be done in a generic way (assuming there's a generic way to do the exclude)? If there isn't a way to tie the two together, is there a different way to achieve the end goal?
First, abstract the dependencies so they look something like:
dependencies {
compile dep('org:name')
}
The dep function can then be defined in a way which will take care of the automatic exclusions:
final rev = [
'group:module': 'good_version'
'org:name': 'version']
ext.rev = { m ->
rev[m]
}
final dep = { m ->
"${m}:${ext.rev(m)}"
}
final forcedTransitiveDeps = [
'org:name': [
dep('group:module')]].withDefault({[]})
ext.forcedTransitiveDeps = { m ->
forcedTransitiveDeps[m]
}
ext.dep = { m ->
if (!forcedTransitiveDeps.containsKey(m)) {
project.dependencies.create(dep(m))
} else {
project.configure(
project.dependencies.create(dep(m)),
{
forcedTransitiveDeps[m].each { dependency ->
final dependencyParts = dependency.split(':')
exclude(group: dependencyParts[0], module: dependencyParts[1])
}
})
}
}
Finally, to re-introduce and force the dependencies:
subprojects { subproject ->
subproject.afterEvaluate {
subproject.configurations.all { Configuration configuration ->
final dependencies = configuration.getDependencies()
final forcedDependencies = (dependencies.collect { dependency ->
forcedTransitiveDeps("${dependency.group}:${dependency.name}")
}).flatten()
if (!forcedDependencies.isEmpty()) {
logger.info("Forcing ${subproject} dependencies on ${forcedDependencies} as per `ext.forcedTransitiveDeps`.")
dependencies.addAll(forcedDependencies.collect { forcedDependency ->
subproject.dependencies.create(forcedDependency)
})
}
}
}
}
I would like to add a certain project dependency to various subproject starting with a specific name.
I tried this
subprojects.findAll { project -> project.name.startsWith("myproject-sample") }.each { project ->
dependencies {
//compile project(":myproject-core")
}
}
but it gives
A problem occurred evaluating root project 'myproject'.
> Could not find method call() for arguments [:myproject-core] on project ':myproject-sample-hello-world'.
When I do this
subprojects {
dependencies {
compile project(":myproject-core")
}
}
it seems to work. But it adds the dep to all subprojects.
How should I add a project dep to a limited set of subprojects?
A clean solution is:
def sampleProjects = subprojects.findAll { it.name.startsWith("sample") }
configure(sampleProjects) {
dependencies {
compile project(":myproject-core")
}
}
Or:
subprojects {
if (project.name.startsWith("sample")) {
dependencies {
compile project(":myproject-core")
}
}
}
Both snippets assume that the sample projects have already had the java plugin applied (otherwise add apply plugin: "java" before the dependencies block).
The subprojects method delegates to an instance of the Project interface for each subproject, which is why your second example works (Project has a method called dependencies()). The each method however is simply passed a Project object as an argument. You then need to call the dependencies() method on that object. This requires a simple syntactical change.
subprojects.findAll { project ->
project.name.startsWith("myproject-sample")
}.each { project ->
project.dependencies {
compile project(":myproject-core")
}
}
I've hacked together combination of build.gradle and settings.gradle below for creating an ad-hoc multi-module project out of several single-module projects (e.g., an application and all of its dependencies, or a shared library and everything that uses that library).
settings.gradle:
// find all subprojects and include them
rootDir.eachFileRecurse {
if (it.name == "build.gradle") {
def projDir = it.parentFile
if (projDir != rootDir) {
include projDir.name
project(":${projDir.name}").projectDir = projDir
}
}
}
build.gradle::
// Make sure we've parsed subproject dependencies
evaluationDependsOnChildren()
// Map of all projects by artifact group and name
def declarationToProject = subprojects.collectEntries { p -> [toDeclaration(p), p] }
// Replace artifact dependencies with subproject dependencies, if possible
subprojects.each { p ->
def changes = [] // defer so we don't get ConcurrentModificationExceptions
p.configurations.each { c ->
c.dependencies.each { d ->
def sub = declarationToProject[[group:d.group, name:d.name]]
if (sub != null) {
changes.add({
c.dependencies.remove(d)
p.dependencies.add(c.name, sub)
})
}
}
}
for (change in changes) {
change()
}
}
This works, but it's hard to share -- if somebody else wants to do something similar they have to copy my *.gradle files or cut and paste.
What I'd like to do is take this functionality and encapsulate it in a plugin. The build.gradle part looks easy enough to do in the plugin apply() method, but it seems like the list of subprojects is already set in stone before the plugin gets a chance at it. Is there any way to get in earlier in the build process, e.g. by applying to something other than Project? Or should I resign myself to giving my plugin a task for overwriting settings.gradle?
Solution: Per Peter Niederweiser's answer, I moved the code above into two plugins, one to be called from settings.gradle and the other to be called from build.gradle. In settings.gradle:
buildscript {
repositories { /* etc... */ }
dependencies { classpath 'my-group:my-plugin-project:1.0-SNAPSHOT' }
}
apply plugin: 'find-subprojects'
And in build.gradle:
buildscript {
repositories { /* etc... */ }
dependencies { classpath 'my-group:my-plugin-project:1.0-SNAPSHOT' }
}
evaluationDependsOnChildren()
apply plugin: 'local-dependencies'
Note that calling the plugin from settings.gradle doesn't work in Gradle 1.11 or 1.12 but does work in Gradle 2.0.
You'd need to apply a plugin in settings.gradle, which I believe is supported in recent versions.