My original goal was to be able to use classpath dependencies defined in buildscript
in build.gradle
, inside a script that was imported into build.gradle
using apply from:
. However, the external script didn't compile since the classes could not be resolved. After researching the issue, I found out that the logic needs to be duplicated and so I thought I would extract buildscript
into a separate file. Then I would be able to apply that inside build.gradle
and also inside the external script.
I didn't even get past successfully applying the external buildscript file from build.gradle
, let alone applying it from the external script. I've tried multiple things, but it seems like I always end up with one of two problems no matter what I try: either properties from gradle.properties
cannot be used, or the plugin cannot be found (even though the classpath dependency has been defined).
Currently my gradle/buildscript.gradle
file looks like this:
buildscript {
repositories {
maven { url "http://some.url.com" }
}
dependencies {
classpath "my.gradle.plugin:gradle-plugin:1.0.0"
classpath "my.library:my-library:$libraryVersion"
}
}
libraryVersion
has been defined in gradle.properties
. My build.gradle
is as follows:
buildscript {
apply from: "gradle/buildscript.gradle"
}
apply plugin: 'my.gradle.plugin.PluginClass'
When I do this, gradle complains that it cannot find a plugin with id my.gradle.plugin.PluginClass
. I tried removing the quotes, and I also trying project.plugin.apply(...)
using the plugin's FQN with and without quotes; both of these caused gradle to error out with a message saying that it could not find the property my
on the root project.
I also tried:
buildscript {
apply from: "gradle/buildscript.gradle", to: buildscript
}
apply plugin: 'my.gradle.PluginClass'
But this causes another error where gradle complains that it cannot resolve libraryVersion
in gradle/buildscript.gradle
. So I then tried this:
buildscript {
ext.libraryVersion = "1.0.1"
repositories {
maven { url "http://some.url.com" }
}
dependencies {
classpath "my.gradle.plugin:gradle-plugin:1.0.0"
classpath "my.library:my-library:$libraryVersion"
}
}
Which causes another error, where gradle says that there is no such property ext
on buildscript
. I understand that this is because there really is no "project" to speak of yet, since buildscript
is compiled separately. Then I changed my buildscript
block in build.gradle
back to:
buildscript {
apply from: "gradle/buildscript.gradle"
}
Now I don't get the ext
error, but I still get an error saying that it cannot find the plugin with the specified id.
I cannot hardcode libraryVersion
inside buildscript
, because I need it as a compile-time dependency in build.gradle
and I'd rather not have to maintain it in two places.
This is extremely confusing and frustrating, because the following buildscript
block works fine by itself in build.gradle
:
buildscript {
ext.libraryVersion = "1.0.1"
repositories {
maven { url "http://some.url.com" }
}
dependencies {
classpath "my.gradle.plugin:gradle-plugin:1.0.0"
classpath "my.library:my-library:$libraryVersion"
}
}
apply plugin: 'my-plugin-id' //No need to use FQN
dependencies {
compile "my.library:library-version:$libraryVersion"
}
The reason I tried to split out the buildscript
block is because I have a file other.gradle
that has some custom tasks that use classes from my.library
:
import my.library.SomeThing
task customTask(type: DefaultTask) {
//does something with SomeThing
}
But when I leave the buildscript
block in build.gradle
and apply the other file like so:
buildscript {
ext.libraryVersion = "1.0.1"
repositories {
maven { url "http://some.url.com" }
}
dependencies {
classpath "my.gradle.plugin:gradle-plugin:1.0.0"
classpath "my.library:my-library:$libraryVersion"
}
}
apply plugin: 'my-plugin-id' //No need to use FQN
dependencies {
compile "my.library:my-library:$libraryVersion"
}
apply from: 'gradle/other.gradle'
I get an error from gradle saying that it cannot resolve the class my.library.SomeThing
. I figured I could solve this and avoid duplication by having a common buildscript
file that I could then apply in both build.gradle
and other.gradle
.
I created a custom plugin inside buildSrc
to configure the project the way I wanted it, only to end up with a more complicated way to fail with the same result. The root cause was the same: there was no way to expose classpath dependencies to external scripts.
Is there comprehensive documentation regarding this sorts of behavior? Everything about this violates the principle of least-surprise. I would expect a buildscript
block that is being used in build.gradle
to "just work" when I move it to another file.
The semantics of apply
with respect to buildscript
blocks is not clear. In addition, the semantics of buildscript
itself when it appears in an external file are not clear either - there is a marked variation in behavior, especially with respect to plugins and external properties.
What is the best way to deal with this?
The buildScript block determines which plugins, task classes, and other classes are available for use in the rest of the build script. Without a buildScript block, you can use everything that ships with Gradle out-of-the-box.
The " buildscript " configuration section is for gradle itself (i.e. changes to how gradle is able to perform the build). So this section will usually include the Android Gradle plugin. The " allprojects " section is for the modules being built by Gradle.
A configuration is simply a named set of dependencies. The compile configuration is created by the Java plugin. The classpath configuration is commonly seen in the buildSrc {} block where one needs to declare dependencies for the build. gradle, itself (for plugins, perhaps).
The subproject producer defines a task named buildInfo that generates a properties file containing build information e.g. the project version. You can then map the task provider to its output file and Gradle will automatically establish a task dependency.
This is a bit of a rant, but there is also a solution. I was able to solve this without using a separate buildscript
file, but the workaround is unbelievably hackish. I think it's a major downside that you cannot share buildscript dependencies across external scripts.
The problem is that there is no semantic consistency because behavior seems to be dependent on how you decide to organize/modularize your build-logic. If this is a known problem, it needs to be specifically called out in the documentation somewhere - the only way I've been able to find mentions of this sort of surprising behavior is from gradle's own forums or from StackOverflow. I don't think it is unreasonable to expect a build which works with discrete units of build-logic in a single file, to also work when those discrete units are split across multiple files. Build logic shouldn't vary based on how you have decided to organize your files, as long as the semantics are consistent.
I understand that there may be technical limitations, but having builds break just because you moved logic from one file into another file is abstraction leakage because now I am required to know details and intricacies of doing so, beyond what one should be reasonably expected to know. I wouldn't even mind if this was explicitly and specifically called out, along with solutions/workarounds to bridge the disparity in semantics. However, current documentation regarding organizing build-logic mentions none of these caveats; it only documents the happy path.
/rant
So here's the solution. I saved a reference to the class itself by using extensions:
import my.library.SomeThing
import my.library.SomeOtherThing
buildscript {
ext.libraryVersion = "1.0.1"
repositories {
maven { url "http://some.url.com" }
}
dependencies {
classpath "my.gradle.plugin:gradle-plugin:1.0.0"
classpath "my.library:my-library:$libraryVersion"
}
}
apply plugin: 'my-plugin-id' //No need to use FQN
ext.SomeThing = SomeThing
ext.SomeOtherThing = SomeOtherThing
dependencies {
compile "my.library:my-library:$libraryVersion"
}
apply from: 'gradle/other.gradle'
Then in other.gradle
:
// Necessary; you can't just use ext.SomeThing in the task later because
// it is available at compile-time, but apparently not at runtime. Although
// it does work if you use project.ext.SomeThing. However, I just found this
// to be more convenient.
def SomeThing = ext.SomeThing
def SomeOtherThing = ext.SomeOtherThing
task someTask(type: DefaultTask) {
// You have to use def; you cannot use the actual type because
// it is not available at compile-time. Also, since you only
// have a class object, you cannot use "new" directly; you have to
// create a new instance by calling newInstance() on the class object
def someThing = SomeThing.newInstance(...)
// If you are calling static methods you can invoke them directly
// on the class object. Again, you have to use def if the return
// type is something defined within my-library.
def foo = SomeOtherThing.staticMethod(...)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With