Plugin development depenency resolution conflict

I’m working on a plugin that configures several other gradle plugins. Right now I’m using my plugin to install the gradle application plugin, for some reason I think this is breaking dependency resolution for my project.
I have something that looks close to this:

public class ApplicationCustomization implements Plugin<Project> {

  @Override
  public void apply(Project project) {
    project.getPlugins().apply(ApplicationPlugin.class);
    ServiceConfiguration serviceConfiguration = PluginUtils.getServiceConfiguration(project);
    registerMainClass(project, serviceConfiguration);
    applicationPluginConfiguration(project);
  }
}

I’m trying to use gradle testKit to verify that I can add dependencies, this is what the test build.gradle looks like:

plugins {
    id 'com.myplugin'
}
repositories {
    mavenCentral()
}
service {
    mainClass = 'com.plugin.testing.MyTest'
}
dependencies {
    implementation "commons-io:commons-io:2.5"
    testImplementation "org.spockframework:spock-core:2.0-M4-groovy-3.0"
}

When I try to execute my test I’m getting errors about trying to modify the implementation configuration.

Cannot change dependencies of dependency configuration ':implementation' after it has been included in dependency resolution

Does anyone have any ideas on what I’m doing wrong here?

Does getServiceConfiguration, registerMainClass, or applicationPluginConfiguration do anything with dependency configurations? Specifically, do they resolve any of the configurations in the project?

Is this the type of thing that would cause that dependency resolution?

Project serviceCommon = project.getRootProject().getChildProjects().get(SERVICE_COMMON);
  DependencyHandler dependencies = project.getDependencies();
  dependencies.add(IMPLEMENTATION, serviceCommon);
  dependencies.add(TEST_IMPLEMENTATION, serviceCommon);

I’m assuming the answer is yes. Do you have any suggestions on how I can defer this behavior so that I don’t bump into the problem I’m seeing? Would this be a candidate for a do last type of task or something? I thought dependencies needed to be part of the configuration phase though?

Nope, adding dependencies to a configuration would not cause it to be resolved.

Ok, I think I figured out where this error is coming from. I’m staging dependencies in the build directory for sonar to scan. I’m clearly going about this the wrong way. Any suggestions?

project.getTasks().register(STAGE_DEPENDENCIES_FOR_SONAR, Sync.class, (task) -> {
   Set<File> runtimeClasspath = project.getConfigurations().getByName("runtimeClasspath").resolve()
   task.from(runtimeClasspath);
   task.into(String.format("%s/%s", project.getBuildDir(), PRODUCTION_DEPENDENCIES));
});

Oh I think the real issue is the test dependencies task:

project.getTasks().register(STAGE_TEST_DEPENDENCIES_FOR_SONAR, Sync.class, (task) -> {
    Set<File> testRuntimeClasspath = project.getConfigurations().getByName("testRuntimeClasspath").resolve();
    Set<File> runtimeClasspath = project.getConfigurations().getByName("runtimeClasspath").resolve();
    testRuntimeClasspath.removeAll(runtimeClasspath);
    task.from(testRuntimeClasspath);
    task.into(String.format("%s/%s", project.getBuildDir(), TEST_DEPENDENCIES));
});

I have updated the production dependencies task to just point at the configuration, and that got me passed that part.

I’m not sure how to remove the runtimeClasspath configuration dependencies from the testingRuntimeClasspath without resolving the dependencies.

I believe I figure out the solution for the test dependencies:

project.getTasks().register(STAGE_TEST_DEPENDENCIES_FOR_SONAR, Sync.class, (task) -> {
     Configuration testRuntimeClasspath = project.getConfigurations().getByName("testRuntimeClasspath");
     Configuration runtimeClasspath = project.getConfigurations().getByName("runtimeClasspath");
     task.from(testRuntimeClasspath.filter((file)-> !runtimeClasspath.contains(file)));
     task.into(String.format("%s/%s", project.getBuildDir(), TEST_DEPENDENCIES));
});

So I think I’m good. Thanks again for pointing me at the resolution time thing!

Good job finding the issue!

I’m curious why you want to exclude files in the runtime classpath from the test runtime classpath. What if the test runtime has some of the same dependencies as runtime (which in most cases is true since the test configurations extend from the main configurations)?

The license reporting tool wired into CI at my work expects them to be separated in this way. I’m not sure what the rationale is, but the tool is polyglot so maybe it has something to do with it originally being built for another language or something.