In Gradle, how programmatically to exclude then force a dependency? - gradle

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)
})
}
}
}
}

Related

Gradle 7.2 Version Catalog specify library build type

I'm refactoring a multi module project with version catalogs and I have to add a dependency that is currently like this:
implementation com.mygroup:my-artifact:1.0.0:debug#aar
Since version catalogs doesn't allow to specify the aar type, a workaround would be to specify it directly in the gradle file like this:
implementation(libs.myDependency) { artifact { type = 'aar' } }
This works, but there's an extra complexity: I need to also specify the build type, in the example from above is debug, I cannot find a way to add it.
What I've tried is:
TOML
[libraries]
myDependency = { module = "com.mygroup:my-artifact", version = "1.0.0:debug" }
Gradle
implementation(libs.myDependency) { artifact { type = 'aar' } }
For some reason this doesn't work, how can I also specify the build type?
Found a way to do this! Need to add the classifier into the artifact.
So for the given regular declaration:
build.gradle
dependencies {
implementation com.mygroup:my-artifact:1.0.0:debug#aar
}
The version catalogs way would be:
TOML
[libraries]
myDependency = { module = "com.mygroup:my-artifact", version = "1.0.0" }
build.gradle
dependencies {
implementation(libs.myDependency) { artifact { classifier = 'debug'; type = 'aar' } }
}
or (multiline)
build.gradle
dependencies {
implementation(libs.myDependency) {
artifact {
classifier = 'debug'
type = 'aar'
}
}
}

Is there a way to include a specific module from a group while excluding rest in gradle?

I am using a library which has a transitive dependency on a module lets assume "abc.xyz:abc-module:1.1.1", the problem is, however, all of the modules from that group are excluded in my build.gradle for some reason using
configurations {
compile.exclude group: "abc.xyz"
}
It causes that transitive dependency to be ignored as expected. Is there a way I can specify only to include abc-module while excluding the remaining one as previously?
I think you should be able to do what you want with a component selection rule like
configurations {
compile {
resolutionStrategy {
componentSelection {
all { ComponentSelection selection ->
if (selection.candidate.group == 'abc.xyz' && selection.candidate.module != 'abc-module') {
selection.reject('Dependencies from group "abc.xyz" except of "abc-module" are not allowed.')
}
}
}
}
}
}

Gradle dependency resolution strategy with maven deployer

I am working on an android project. We are using the DependencyResoultionStrategy to swap some dependency versions. The code looks like this:
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
final version = getVersionForDependency(project, details.requested.group, details.requested.name)
if (version != null) {
details.useVersion(version)
}
}
So for example, the project requests the dependency group:name:1.1.2 but it is swapped so the dependency group:name:1.2.0 is used. This works perfectly and the project is built with the right dependency (the second one).
We also have a publish task, which deploys the project to a local maven repository. We use the maven plugin for this, the code looks like this:
apply plugin: 'maven'
task publish(dependsOn: uploadArchives)
uploadArchives {
configurations {
deployerFTP
}
repositories {
mavenDeployer {
configuration = configurations.deployerFTP
repository(URL) {
authentication(USERNAME, PASSWORD)
}
}
}
dependencies {
deployerFTP "org.apache.maven.wagon:wagon-ftp:2.4"
}
}
The problem is, if I publish the library, in the resulting .pom file, the dependency group:name:1.1.2 is entered, not the one which is actually used. How can I change this behavior, so the pom contains the right dependency?
I have found an answer, simply add this code block:
mavenDeployer {
// ...
pom.whenConfigured { pom ->
pom.dependencies = pom.dependencies.collect { dep ->
def version = getVersionForDependency(project, dep.groupId, dep.artifactId)
if (version != null) {
dep.version = version
}
return dep
}
}
}

Gradle remove specific dependency configuration from generated pom

I need to ignore some dependencies defined in the configuration "configurations.nonDistributable" when using gradles plugin maven-publish to generate pom files, I haven't found a reliable way of doing this, except for manually parsing the XML to remove them. Am I missing something, does gradle allow for an easier way of doing this?
build.gradle example:
configurations{
nonDistributable
}
dependencies {
nonDistributable ('org.seleniumhq.selenium:selenium-java:2.52.0'){
exclude group:'com.google.guava' // included in elasticsearch
}
nonDistributable ('com.assertthat:selenium-shutterbug:0.3') {
transitive = false
}
nonDistributable 'mysql:mysql-connector-java:5.1.40'
nonDistributable fileTree(dir: 'non-distributable-libs', include: '*.jar')
}
// generate subprojects pom
subprojects {
apply plugin: 'maven-publish'
model {
tasks.generatePomFileForMavenJavaPublication {
destination = file("$buildDir/../../$distDir/build/pom/$project.name-pom.xml")
}
}
afterEvaluate { project ->
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}
}
}
// generate root project pom
model {
tasks.generatePomFileForMavenJavaPublication {
destination = file("$buildDir/../$distDir/build/pom/$project.name-pom.xml")
}
}
afterEvaluate { project ->
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}
}
You can create your own publication pom. It would look something like this:
customMaven(MavenPublication) {
artifactId 'myArtifactId'
pom.withXml {
def dependencies = asNode().appendNode('dependencies')
configurations.specialConfiguration.getResolvedConfiguration().getFirstLevelModuleDependencies().each {
def dependency = dependencies.appendNode('dependency')
dependency.appendNode('groupId', it.moduleGroup)
dependency.appendNode('artifactId', it.moduleName)
dependency.appendNode('version', it.moduleVersion)
}
}
}
Then you can create a special configuration that extends from only those configurations that you want to include.
I am using this to create a special pom that contains all testRuntime dependencies to be used for integration tests separated from the main project.

Gradle: replace module dependency with project dependency

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.

Resources