Buildscript classpath dependencies do not seem to get loaded on first run after gradle clean

Scenario:

  • I have a project containing 3 subprojects, projectA, projectB and projectC
  • projectB has a task generateCode which generates Java classes that inherit from projectA's classes
  • projectC has a task generateStuff which generates code that interfaces with the classes generated by projectB's generateCode task. To do so, it has to analyze the files generated by projectB's task via reflection.
  • projectC/build.gradle:
buildscript {
  dependencies {
    classpath project(':projectA')
  }
  repositories {
    // repositories containing projectA's dependencies
  }
}

dependencies {
  compile project(':projectA')
}

task compileSources(dependsOn: project(':projectB').generateCode, type: JavaCompile) {
  source files(project(':projectB').generateCode)
  destinationDir file('tmp')
  classpath = sourceSets.main.compileClasspath
}

task generateStuff(dependsOn: compileSources) {
  // snip
  doLast {
    def loader = new URLClassLoader([file('tmp').toURI().toURL()] as URL[], buildscript.classLoader)
    def clazz = loader.loadClass('com.example.projectB.GeneratedClass')
    // generate stuff
  }
}

If I run gradle generateStuff twice, it succeeds. But it reproducibly fails on the first run after a gradle clean, telling me it cannot find the base class ‘com.example.projectA.Base’ of ‘com.example.projectB.GeneratedClass’.

This isn’t going to work the way you want it to. The buildscript classpath is the classpath available to the build script itself. It necessarily has to be resolved before the script is executed. There currently isn’t a way to make this resolution defer until after another project in a multiproject build has executed (i.e. it is done before any project executes). It happens to work after you’ve already executed it once because it resolves to a jar that already exists on the filesystem, but there is no way to force the execution of project A before project C’s buildscript classpath is evaluated.

What you can do, however, is move the generation logic to the buildSrc project and have it generated there. This will get executed before constructing the buildscript classpath for any project. See the section in the userguide for more information on the buildSrc project.

That’s what I figured. Is there a chance you might support that in the future?

Would moving the generation logic to buildSrc really help in my case? projectC's build script depends on both projectA's classes and the generated code (from projectB), and it’s the dependency on projectA that seems to be problematic. Also, the code cannot be generated before building projectB.

Sorry, I was getting confused between the different projects in your example. What I meant was that the stuff that needs to be on the buildscript classpath for project C (i.e. whatever project A produces) can be moved to buildSrc, which will cause it be compiled first and placed on the buildscript classpath.

I’m not aware of any plans to do so. That’s not to say that it would never be supported, only that it’s not on the immediate horizon.

Ok, that would help, but I’d really want to avoid that. Moving projectB to buildSrc would mean forcing Gradle to build a large project that only some subprojects depend on before building anything else. It’d also hide a large part of our codebase inside buildSrc, next to our custom plugins.

If using buildSrc doesn’t work for you, another option would be to make the generateStuff task in project C an Exec task that sets the classpath to include everything it needs. Then project A would not need to be on the buildscript classpath - just the classpath of the Exec task.

I just suggested something similar to my coworker. I think what I’ll end up doing is compiling the code generation code as part of the main sourceSet (instead of having it inside the build script) and then execute it in an Exec task which compileTest depends on. (The code I’m generating in projectC is only needed by the tests.)