Making a jar available to all scripts classpaths (even applied scripts)


(Thierry Guérin) #1

(originally titled builscript classpath hell)
I’ve spent way too much time trying to find this information, so here it is:
If you want to use classes from an external jar in an applied Gradle script, the simplest & surest way is to define a dependency towards this jar in buildSrc/build.gradle.

buildSrc/build.gradle

repositories {
    maven {
        url "myRepository"
    }
}

dependencies {
    runtime 'foo.bar:hello:1.0.0.0'
}

For future reference, history (this is quite long):

We had some utility classes in buildSrc/main:

package foo;
class A() {
	public String hello(B b) {
	 ...
	}
}
class B() {
}

Our project being quite big, we split build.gradle into several files:

build.gradle

import foo.A
ext.a = new A()
apply from: hello.gradle

hello.gradle

import foo.B
task hello {
	println a.hello(new B())
}

This was working fine, but we needed to re-use the classes A & B in another project, so these were moved to their own project, and their jar published to our local repository.
Following documentation, I then declared a dependency in build.gradle:

build.gradle

buildscript {
    repositories {
        maven {
            url "$myRepository"
        }
    }
    dependencies {
        classpath 'foo.bar:hello:1.0.0.0'
    }
}

import foo.A
ext.a = new A()
apply from: hello.gradle

When run, Gradle throws an error in script hello.gradle:

script '(...)\sampleproject\hello.gradle': 1: unable to resolve class foo.B

This is due to the main script classpath not being visible by the applied script classpath (which is quite confusing, as it is visible from the child scripts (i.e. scripts for subprojects)).
To work around this, like I did so many times before, I added the dependency in a buildscript statement in hello.gradle

hello.gradle

buildscript {
    repositories {
        maven {
            url "$myRepository"
        }
    }
    dependencies {
        classpath 'foo.bar:hello:1.0.0.0'
    }
}
import foo.B
task hello {
	println a.hello(new B())
}

However, this time it failed with :

No signature of method: foo.A.hello() is applicable for argument types: (foo.B) values: [...] 
Possible solutions: foo.A.hello(foo.B)                                                                                                            
The following classes appear as argument class and as parameter class, but are defined by different class loader:                                                                           
foo.B (defined by 'org.gradle.internal.classloader.VisitableURLClassLoader@7b197fa9' and 'org.gradle.internal.classloader.VisitableURLClassLoader@39e7bbb') 

Fortunately, the error is quite explicit: I’m calling method hello on an object (a) that has been defined in build.gradle (1st classloader), but passing an argument of class B which instance has been defined in hello.gradle (2nd classloader).
Unfortunately, I was at a loss on what to do next. After many failed attempts and searching the documentation & forums, it finally dawned on me that anything that was an the classpath of buildSrc would end up in the classpath of ALL scripts. Hence the runtime dependency as described at the start of this post.

It would be nice for the documentation to mention this (in particular in “External dependencies for the build script”), as the recommended way does not work for applied scripts.

Note: I wondered about just posting this in How to refer to classes on the buildscript classpath in an applied script, but as this was from the old forum and quite old, I went for a new post. If you feel i should have, I’ll move my post.