How do I exclude specific transitive dependencies of something I depend on?


(Trejkaz (pen name)) #1

Subproject A depends on module L which has an optional dependency on another module X.
Subproject B depends on module M which has a required dependency on X.

/a/build.gradle:

dependencies {
  compile libraries.l
}

/b/build.gradle:

dependencies {
  compile libraries.m
}

/dependencies.gradle:

ext.libraries = [
  l: [
    'com.example.l:l:1.0',
  ],

  m: [
    'com.example.m:m:1.0',
  ],
]

This optional dependency (a feature I still find highly dubious) on X results in pulling in additional jar files which I do not want to ship, partially because managing all the licence text for included libraries is a hassle, but also because it bloats the size of our distribution. We’re a desktop app and don’t have the luxury of hiding everything behind a nice server so that nobody knows how big it is.

If I put this into my common build then it excludes X, but does so even for module B, which breaks it.

compile.exclude group: 'com.example.x', module: 'x'

I know I could put it just into the build file for module B, but then the buildfile for other modules which behave like B end up duplicating code with that one.

Additionally, I feel like having this exclusion information closer to the list of modules which make up one dependency would make it easier to find the exclusion list when you’re looking for it.

So is there a way to keep this information about exclusions in dependencies.gradle, such that any module which includes module L will have the transitive dependency X excluded, while modules which include module M will not, and modules which include both will not?


(Dimitar Dimitrov) #2

In plain Gradle, what you are looking for can be achieved as:

dependencies {
  compile('com.example.m:m:1.0') {
     exclude group: 'org.unwanted', module: 'x 
  }
  compile 'com.example.l:1.0'
}

This would exclude the module X from the dependencies of M, but will still bring it if you depend on L.

Structuring that in a nicer way is a bit more complex and is largely a matter of taste (IMHO the ext.libraries approach puts some limitations here)


(Trejkaz (pen name)) #3

Is there a pattern for reusing dependencies that does allow for this sort of thing? Seeing as it’s such a common thing to want to do, I’d expect a single correct way to do it to exist, especially considering how long Gradle has been around now.


(Dimitar Dimitrov) #4

Well, what you essentially want is to rewrite the dependencies for certain module.

I can understand why you want to do it in a central place, and you can surely concoct something useing all { ... }, but from where I stand now, I would argue that it would be better if you repeat the exclusions everywhere to avoid surprises down the road. Centralizing stuff like this tends to cause more problems than it solves.

On the other hand, you can abbreviate it a bit by assigning the closure to a local variable - that is explicit and reasonably concise.

dependencies {
  def withoutX = { exclude group: 'org.unwanted', module: 'x }
  compile 'com.example.m:m:1.0', withoutX
  compile 'com.example.l:1.0'
}

It is more beneficial if you have multiple dependencies where you want to suppress X.


(Trejkaz (pen name)) #5

Docs for DependencyHandler show this method:

Dependency add(String configurationName, Object dependencyNotation, Closure configureClosure)

So I tried to use this to remove duplication. The shared values:

ext.libraries = {
  //...
  jruby_core: 'org.jruby:jruby-core:9.0.5.0',
  jruby_core_closure: {
    exclude('com.martiansoftware:nailgun-server')
  },
  //...
}

When trying to use it:

dependencies {
  add('compile', libraries.jruby_core, libraries.jruby_core_closure)
}

(I’m trying add() because I couldn’t get compile() itself to work, but I think being able to use compile() would be more elegant. Being able to put the closure and the notation into the same thing would be even more elegant since it removes more duplication.)

I get an error:

> Cannot convert the provided notation to an object of type Dependency: dependencies_57urgqmegl6y6w9aheb5kwegg$_run_closure1@56f3f9da.
  The following types/formats are supported:
    - Instances of Dependency.
    - String or CharSequence values, for example 'org.gradle:gradle-core:1.0'.
    - Maps, for example [group: 'org.gradle', name: 'gradle-core', version: '1.0'].
    - FileCollections, for example files('some.jar', 'someOther.jar').
    - Projects, for example project(':some:project:path').
    - ClassPathNotation, for example gradleApi().

I find this odd, because the add method is documented as having three parameters - a string, a dependency notation object and a closure. I have passed the closure as the third parameter as-is, but somehow it seems like it’s being misinterpreted as a dependency notation object.


(Trejkaz (pen name)) #6

Further investigation, now I can’t even use the syntax which was originally suggested.

  compile('org.jruby:jruby-core:9.0.5.0') {
    exclude('com.martiansoftware:nailgun-server')
  }

Gives:

> Could not find method compile() for arguments [org.jruby:jruby-core:9.0.5.0, build_7sitauikbgnylchfbg4j698l1$_run_closure1$_closure3@2564410b] on project ':scripting-impl'.

If I remove the closure:

  compile('org.jruby:jruby-core:9.0.5.0')

Now the build works fine… so it’s like it can’t find the overload which takes a Closure? :confused:


(Saifur Rahman Mohsin) #7

Hi. Thanks for this, super helpful. I want to know how do I go about adding multiple withouts. For example, here’s a sample where I’m trying to achieve this…

dependencies {
    def withoutSupportv4 = { exclude group: 'com.android.support', module: 'support-v4' }
    def withoutSupportv13 = { exclude group: 'com.android.support', module: 'support-v13' }
    def withoutSupportDesign = { exclude group: 'com.android.support', module: 'design-v13' }

    // For Material Datepicker
    compile deps.datePicker, withoutSupportv4, withoutSupportv13, withoutSupportDesign

}

Unfortunately, this doesn’t work for me. it works with only the extra one parameter i.e. compile deps.datePicker, withoutSupportv4 but adding more commas fails and I get this error:

Error:(93, 0) Cannot convert the provided notation to an object of type Dependency: build_ey73hh1h8f4tc0h2ljim03u1k$_run_closure2$_closure11@122c5b55.
The following types/formats are supported:
  - Instances of Dependency.
  - String or CharSequence values, for example 'org.gradle:gradle-core:1.0'.
  - Maps, for example [group: 'org.gradle', name: 'gradle-core', version: '1.0'].
  - FileCollections, for example files('some.jar', 'someOther.jar').
  - Projects, for example project(':some:project:path').
  - ClassPathNotation, for example gradleApi().

Comprehensive documentation on dependency notations is available in DSL reference for DependencyHandler type.

(Trejkaz (pen name)) #8

Putting all three exclude lines into one closure will work. I found the same trap - if you have more than one artifact you want to apply the excludes to, that won’t work either, so you just have to duplicate a lot of stuff…


(Dimitar Dimitrov) #9

You have a few options here. The most trivial is to put all excludes in one closure - if certain dependency is not in the transitives, the exclude clause will have no power. Using your code above:

dependencies {
    def withoutStuff = { 
        exclude group: 'com.android.support', module: 'support-v4' 
        exclude group: 'com.android.support', module: 'support-v13'
        exclude group: 'com.android.support', module: 'design-v13' 
    }

    // For Material Datepicker
    compile deps.datePicker, withoutStuff
}

Assuming you want to suppress “design-v13” in datepicker, but use if from foobar you may use 2 closures (closer to your original approach):

dependencies {
    def noSupport = { 
        exclude group: 'com.android.support', module: 'support-v4' 
        exclude group: 'com.android.support', module: 'support-v13'
    }
    def withoutStuff = { 
        delegate.configure(withoutSupport) // equivalent to:  noSupport.delegate=delegate; noSupport()
        exclude group: 'com.android.support', module: 'design-v13' 
    }    

    // For Material Datepicker
    compile deps.datePicker, noSupport
    compile deps.foobar, withoutStuff
}

And finally if you fancy another design you may use a higher order function to create the dependency config closure (depending on the build complexity and audience that may be overkill):

dependencies {
     // note that this is a closure returning closure
    def withExcludes = { boolean excludeDesign -> return { 
        exclude group: 'com.android.support', module: 'support-v4' 
        exclude group: 'com.android.support', module: 'support-v13'
        if (excludeDesign) {
          exclude group: 'com.android.support', module: 'design-v13' 
        }
    }}

    // For Material Datepicker
    compile deps.datePicker, withExcludes(true)
    compile deps.foobar, withExcludes(false)
}

I hope this helps - these are just illustrations of what you can do with the Gradle DSL. It is worth spending some time to understand the Gradle domain model, and then you can come up with your own variations.


(Schalk Cronjé) #10

Only comment I would make on @ddimitrov’s excellent suggestion is to put the withoutStuff and other closures in the ext block instead. i.e.

ext {
  withoutStuff = { 
        exclude group: 'com.android.support', module: 'support-v4' 
        exclude group: 'com.android.support', module: 'support-v13'
        exclude group: 'com.android.support', module: 'design-v13' 
    }
} 

It is purely stylistic, but it keeps the dependencies closure uncluttered.


(Dimitar Dimitrov) #11

Indeed, another reason to use the ext { ... } block is to share the closures between subprojects.

On the other hand, you may want to keep the closures in the dependencies { ... } block if you want to make the exclusions more visible (for example because the average team member is not experienced with Gradle and doesn’t know what ext { ... } is, but is skilled enough to look around and figure the pattern).