Provided Scope in Gradle

A few days ago, in Android Dependency Double Trouble, I needed to add something like Maven’s provided scope to a Gradle Java project. I learned how to do it from How do I best define dependencies as “provided”?, which offered two methods. The first looked like the simplest, but I couldn’t get it to work. Still, I got the second way working, and then moved on. I left myself a note to come back someday and figure out how I was messing up the simpler method.

I’d used the provided scope for a dependency from a project named TestSupport onto one named Core. Today I hit a problem when adding tests to my TestSupport project (that’s right, tests that test the code that supports my tests): the tests couldn’t find classes in the Core project. While flailing around to try and fix it, I learned more about the differences between the two methods of dealing with the provided configuration, and how to have fully working builds, with TestSupport tests, using either method.

What is “provided”, anyway?

My understanding of a dependency in the provided scope was that it is needed for compilation of a project, but it should not be distributed with it. Any project that depends on a project with a provided scope is expected to supply the provided dependencies at runtime. My new understanding is pretty much the same, but I’ve cleared up some confusion I had about what “distributed with it” means.

Initially, when I discovered that a copy of the Core classes were getting into my Android test package, I assumed that the TestSupport.jar artifact was a fat jar and included the Core classes upon which it depends. A quick look at the contents of TestSupport.jar showed that not to be the case. So how were the Core classes getting included?

What I’d failed to understand was that the Android test package depended not on TestSupport.jar, but on a configuration of the TestSupport project. When a dependency is declared on a project you can say which configuration you depend on, but by default, the default configuration is used. This includes the TestSupport.jar assembled by the project, but also the other entries in its runtime configuration, which include Core.jar. The build for the Android test package must be including TestSupport’s transitive dependencies.

This is all explained in the Gradle User Guide, and in particular in the figure named Java plugin – dependency configuration, which I’d read, but hadn’t fully absorbed. Here’s my version of that figure:

gradle_configurations_and_sourcesets

The rounded rectangles are configurations, and the closed-head arrows connecting them represent the extendsFrom relationship. Artifacts in any “superconfiguration” of a configuration are included within it. I’ve also added a different Gradle concept to the diagram: sourceSets. These are where Gradle looks to find the classpaths for compiling or running code, and they refer to the configurations. The default arrangement for a Java project is shown.

Fixing the methods

Now we can get an inkling of how the two methods for a provided scope work. Lets examine each in turn and try to fix them.

First, the second method

The second method, the one that worked for me, looks like this:

configurations {
    provided
}
 
sourceSets {
    main {
        compileClasspath += configurations.provided
    }
}
 
dependencies {
    provided project(':Core')
}

It creates a new provided configuration, but leaves it separated from the graph of other configurations. It puts it to work by updating the main sourceSet’s compileClasspath to refer to it in addition to the usual configurations.

second_provided

Now we see why my tests wouldn’t compile: because the provided configuration wasn’t referenced from the test sourceSet. Adding the provided configuration to the test sourceSet’s compileClasspath allows the tests to compile; adding it to the test sourceSet’s runtimeClasspath allows the tests to run. (I’m not sure that adding it to the main sourceSet’s runtimeClasspath achieves anything other than a certain symmetry, but I like symmetry and so did that too.) Here’s the fully working code:

configurations {
    provided
}
 
sourceSets {
    main {
        compileClasspath += configurations.provided
        runtimeClasspath += configurations.provided
    }
    test {
        compileClasspath += configurations.provided
        runtimeClasspath += configurations.provided
    }
}
 
dependencies {
    provided project(':Core')
}
Second, the first method

Next we look the first method (please forgive the backwardness):

configurations {
    provided
    compile.extendsFrom provided
}
 
dependencies {
    provided project(':Core')
}

It links the provided configuration into the graph of other configurations by having compile extend from it. This puts the provided dependencies into all the configurations shown on the diagram by artifact inheritance, and so there’s no danger of forgetting the test classpaths.

first_provided

But it means they are added to the default configuration. That’s exactly what I was trying to avoid when I turned to provided, and explains why this method didn’t work for me. It needs an extra step that excludes everything in the provided configuration from the default one, which turned out to be quite simple:

configurations {
    provided {
        dependencies.all { dep ->
            configurations.default.exclude group: dep.group, module: dep.name
        }
    }
    compile.extendsFrom provided
}
 
dependencies {
    provided project(':Core')
}

(In hindsight, the author of the forum question does add a comment about needing the extra exclusion step. But it was in response to a request about why the second method didn’t work, and it wasn’t clear to me which method he was speaking about until I’d understood all of this via other means.)

Which to choose?

The fixed first method is definitely shorter, and I think it expresses what provided is all about more clearly, so I’m now using it. I’m still a little conflicted because understanding it maybe requires the reader to know more about how Gradle. But then again, maybe that helps prevent them from just guessing (incorrectly, in my case) about how the other method works?

The future

This is far from the definitive implementation of provided for Gradle. Not least because I’ve not considered publishing of artifacts, and how provided dependencies appear in pom and ivy files etc. at all. Still, I don’t need any of that yet, and, after learning about configurations and sourceSets, I reckon I’ll have a shot at tackling any problems that show up in future.

I’ll finish by noting that [GRADLE-784] Provide a ‘provided’ configuration has been hanging around for a while on Gradle’s issue tracker. I don’t know whether its longevity is because of problems with publishing that I haven’t run into yet, or because it’s so easy (when you know how) to add provided yourself, or for some other reason. Mentioned within that issue there’s a plugin that adds a provided configuration: propdeps-plugin. Interestingly, it works like method 2, but also sets the provided configuration to extendFrom the compile one (the opposite way around to method 1), and has testRuntime extendFrom provided – so there could well be more complexity for me to discover. And there are reports of problems with using that plugin together with transitive dependencies and the new publishing system in Gradle. Those problems probably affect my version too.

13 thoughts on “Provided Scope in Gradle”

  1. Interesting read.
    These gradle concepts are so abstract, and have an enormous level of abstraction, that understanding them is very difficult.
    Step by step 🙂

  2. Thanks for this, it was useful. I did this to configure the main and test source sets at the same time:

    [main, test].each { set ->
          set.compileClasspath += configurations.provided
          set.runtimeClasspath += configurations.provided
    }
  3. Something worth noticing is that if you are using Intellij IDEA, the “Second Method” will work but the source sets are handled by IDEA scopes which will make your “provided” dependencies not to be loaded and mark errors (the classes wont be found and such).

    In my case I had to also add the idea gradle plugin and add my “provided” configuration to the PROVIDED scope of their IDE.

    apply plugin: 'idea'
     
    idea {
        module {
            scopes.PROVIDED.plus += [ configurations.provided ]
        }
    }
  4. Glad it helped, though I’m sure this is quite out of date now. I really should look into Gradle’s compileOnly dependencies and the new java-library plugin someday.

Leave a Reply

Your email address will not be published. Required fields are marked *