When can a plugin change a project's dependencies?

I’ve got a plugin whose job is to add dependencies to a project. It uses a DSL extension to decide which dependencies to add.

It’s only 20 lines, if you’d like to take a look.

Right now I’m adding dependencies in Project.afterEvaluate. It works, but if I use it in a multi-project build then I get this warning:

Changed dependencies of configuration 'xxxx:compile' after it has been included in dependency resolution. This behaviour has been deprecated and is scheduled to be removed in Gradle 3.0.

So then I tried Project.beforeEvaluate, which doesn’t work at all (no errors, but the dependencies just don’t get added).

When can a plugin change a project’s dependencies? If I just do it immediately in the apply method, then the DSL extension won’t be populated by the buildscript…

You shouldn’t need to use either. Just add the dependency in the plugin’s apply() method. If you continue to get the warning then it means some other piece of configuration is eagerly resolving the ‘compile’ configuration.

If I change the dependencies in the directly in Plugin.apply(), then the DSL extension won’t be set by the buildscript. E.g., if I do this:

public class MyExtension {
    public String prop = null
}
public class MyPlugin {
    public void apply(Project project) {
        MyExtension extension = project.getExtensions().create('myext', MyExtension.class);
        System.out.println("myextension.prop=" + extension.prop)
    }
}

And in my buildscript

myext {
    prop = 'somevalue'
}

Then gradle will print myextension.prop=null.

So is there a way to read what the buildscript set in the DSL extension within the buildscript directly in project.apply?

This makes things difficult :slight_smile:

You have a couple of options:

  1. Ditch the dependency on the extension. This could mean some extra configuration on the user’s part. From what I can tell the extension mainly houses some logic on what repo to register, dependency to add. The plugin user could either a) do this themselves or b) call some extension method directly rather than the plugin doing it
  2. Do the configuration when the user set’s the extension property. This can be tricky because you must assume that it might be set multiple times (last one wins) so you have to do some book keeping to upsert the appropriate configuration
2 Likes

Thanks very much!

As an aside, I love that Gradle lets me create arbitrary tasks with arbitrary inputs and outputs, and link them up ad-infinitum. Our deployment pipeline has been radically streamlined and transformed by this capability.

But it seems that Gradle 2.0 makes it difficult to do computation that runs before a project’s dependencies are calculated, and Gradle 3.0 is baking this limitation even deeper into the core. For OSGi / Eclipse / P2 development, this capability is a requirement. Determining a project’s dependencies is a dynamic step that requires computation, not just a transparent lookup (as for Maven).

I guess there are speed reasons for these limitations, but it would be nice if there were a way for a project to have a task whose result affects a project’s dependencies. I’m investigating different workarounds, maybe I’ll find one that makes this limitation irrelevant. But for now it seems like a smell to me that I can stack tasks on each other to arbitrary height, but not an arbitrary “depth” underneath a project’s dependencies (acknowledging that when a task is below the dependencies, it may delay configuration times).

It is perfectly acceptable to compute dependencies as you describe. The issue being that configurations are to be considered immutable once they have been resolved. Right now this isn’t being enforced (only a warning is thrown) but Gradle 3.0 will error on this condition. Mainly, this will allow us to optimize dependency resolution, as we can then assume that results can be cached and reused.

The warning you are seeing is likely because you are eagerly triggering dependency resolution somewhere. This could be as simple as doing something like configurations.compile.each { } somewhere in your build script.

Here is an example of a script that builds with no warnings despite modifying dependencies at execution time. The reason this works is because the compile configuration isn’t resolved until it’s first used, which happens to be when the compileJava task is run.

apply plugin: 'java'

repositories {
    mavenCentral()
}

task addDependency {
    doLast {
        dependencies.add('compile', 'commons-collections:commons-collections:3.2')
    }
}

tasks.withType(JavaCompile) {
    dependsOn addDependency
}
2 Likes

I’m using a plugin that is probably doing something like configurations.compile.each {}. I’ll have to dig in to their code, or maybe move it out to a separate build so it doesn’t trigger this.

Thanks very much!

A thought - if I get the error message:

changed dependencies of configuration 'xxxx:compile' after it has been included in dependency resolution

It could be a lot of detective work to figure out who caused the initial resolution, and who requested it after it changed. It might be neat if the message instead was:

changed dependencies [yada yada]. Rerun with '--record-callers' to diagnose.`

And after running with --record-callers I could see something like:

Configuration 'compile' resolved by:
    [stacktrace at the time of resolution]
Configuration 'compile' modified by:
    [stacktrace at the time of modification]

It’s a very low-priority ask :slight_smile:

I added some diagnostic stuff into my application that recorded stacktraces at key moments to help with debugging later events, and it has ended up being very useful.

You can get the latter via -Dorg.gradle.deprecation.trace=true

1 Like