gradle custom task class makes itself uncompilable - gradle

I'm trying to write a custom Gradle task class and use it in another subproject. I'm having problems tying the build together.
In this example, I have named the subprojects "a" (for application) and "p" (for plugin, although I'm not using a plugin object but just providing a task class to the build script).
settings.gradle
include 'p'
include 'a'
p/build.gradle
apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'maven-publish'
group 'test'
version '1.0'
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}
dependencies {
implementation gradleApi()
implementation localGroovy()
}
p/src/main/groovy/p/MyTask.groovy
package p
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;
class MyTask extends DefaultTask {
#TaskAction
void run() {
System.out.println('yay!');
}
}
a/build.gradle
buildscript {
repositories {
mavenLocal()
}
dependencies {
classpath 'test:p:1.0'
}
}
group = 'test'
version = '1.0'
apply plugin: 'java'
task myTask(type: p.MyTask) {
}
The "plugin" is built by running, inside the p folder:
../gradlew clean build publishToMavenLocal
Inside the a folder:
../gradlew myTask
prints "yay!"
However, while developing, bugs happen. When I simulate a bug in MyTask:
MyTask() {
throw new RuntimeException("an error");
}
and build the plugin (in folder p):
../gradlew clean build publishToMavenLocal
it fails as expected.
Now I "fix" the bug by removing the broken constructor again, and rebuild in folder p:
../gradlew clean build publishToMavenLocal
but this command fails with the same error.
As far as I understand, the reason is that:
the broken plugin is in my local maven repo
trying to build the plugin detects the settings.gradle in the parent folder
gradle tries to configure all projects referenced from settings.gradle
this loads the broken plugin
the build fails
To verify, I comment out the include line for a in settings.gradle, and it works again. Reverting settings.gradle, and it still works, because now the "fixed" plugin is in my maven repo, and rebuilding the plugin will just overwrite it again with the working version.
The bottom line is that bugs in my custom task class (or custom plugin, or any other buildscript code) have the potential to make themself un-compilable, with the workaround being to edit or temporarily rename settings.gradle. The more complex the project gets, the more cumbersome this becomes: Renaming does not work if the plugin code itself contains multiple subprojects, and even commenting out becomes "commenting out the right lines".
What is the intended way to fix this problem?

Complex logic for a single (multi-) project is best organized in buildSrc. You can for the most part regard it as a normal sub-project but just for the build classpath instead. Plugins and tasks that you create here are automatically available for all projects in the multi-project.
If, for some reason, you rather continue to work with a local Maven repository, you could think about publishing stable releases of the plugin with a version number so it is easier to roll back.

Related

Gradle: Use a sibling subproject as plugin

I have a project with two subprobjects: gradle-plugin and plugin-consumer. I want to apply the plugin from gradle-plugin to plugin-consumer. I tried to do this:
// plugin-consumer/build.gradle
buildscript {
dependencies {
classpath project(':gradle-plugin')
}
}
apply plugin: 'my.plugin.id'
But I was greeted with the following error:
A problem occurred configuring project ':plugin-consumer'.
> Cannot use project dependencies in a script classpath definition.
I assume this is not supported because it'd require fully building gradle-plugin before plugin-consumer can be configured.
Fortunately I can use a fileTree dependency to accomplish my goal:
// plugin-consumer/build.gradle
buildscript {
dependencies {
classpath fileTree(includes: ['*.jar'], dir: '../gradle-plugin/build/libs')
}
}
apply plugin: 'my.plugin.id'
This works, but it feels like a massive hack and leads to "bootstrapping problems".
For example, I can't clean gradle-plugin because the (deleted) jar file is necessary for configuring plugin-consumer, which must be done to (re)build gradle-plugin.
Fortunately this can be avoided by always running build immediately after clean (in the same 'run' so to speak). This can be done manually (gradle clean build) or automatically (with clean.finalizedBy(build)). Again, this works, but feels like a hack.
At last, my actual question: is there a better way to do this?
Please note that gradle-plugin is an independent plugin that's not only used by plugin-consumer, therefore buildSrc is unfortunately not an appropriate solution here.
You can publish the plugin to your local Maven repository with the Maven Publish plugin. Then simply consume it like any other artifact.
Assuming you have something similar in your plugin project:
plugins {
`maven-publish`
`java-gradle-plugin`
}
Simply publish it locally:
./gradlew :my-plugin-project:publishToMavenLocal
Then in your consuming project, something like:
buildscript {
repositories {
mavenLocal()
}
dependencies {
"classpath"("com.example:my-plugin-gav:1.0.0-SNAPSHOT")
}
}
// apply plugin

How to avoid duplicate plugin declarations in each build script?

I have a Gradle build that invokes various other gradle scripts, using apply from. Both the main build.gradle and each of the sub-scripts make use of the same build script plugin (the gradle-cargo-plugin, specifically).
The only way I've managed to get this to work is to repeat the declaration of the plugin in each script:
build.gradle:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.gradle.api.plugins:gradle-cargo-plugin:1.5.1'
}
}
apply from: 'other.gradle'
// do something with the cargo plugin
other.gradle:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.gradle.api.plugins:gradle-cargo-plugin:1.5.1'
}
}
// do something else with the cargo plugin
As you can see, the buildscript section is repeated in each script. Changing that dependency becomes tedious and error-prone, but the sub-script don't inherit the dependency from the main build.gradle.
Is there a way to clean this up, either by allowing the invoked scripts to inherit the buildscript dependency, or a different way to delegate to the sub-scripts instead of using apply from?
Works fine for me with Gradle 2.1. Build scripts declared in a build.gradle's buildscript block are visible in script plugins.
Plugins in the new plugin portal (http://plugins.gradle.org/) can be applied in a single line, and don't require a buildscript block (with Gradle 2.1 and higher).

Gradle: Inject plugins to subprojects

I came across gradle plugin to help me to deal with dotted property names. It works fine in single project when used like this:
apply plugin: 'config'
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.esyfur:gradle-config-plugin:0.4.+'
}
}
task printProps() {
println(config.grafana.url)
}
However, I want to use this plugin in multiple projects (multi-module) and would like idealy not to repeat such initialization in every project but rather inject it somehow to have it more manageable.
I have failed to find out how to do it or if it can be done. I tried e.g. in parent build.gradle use this:
allprojects {
apply plugin: 'config'
}
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.esyfur:gradle-config-plugin:0.4.+'
}
}
but it does not work. Gradle complains that it can not find property 'config' on task.
Update
After Peter's comment I started wondering around and creating an example project from the scratch and incrementally get it closer to my 'real project'. I was not completely precise in describing my setup. In settings.gradle I use
rootProject.children.each { project ->
project.buildFileName = "${project.name}.gradle"
}
which causes this problem. Everything works fine When switched back to build.gradle names.
The problem is that your settings.gradle doesn't configure rootProject.buildFileName to match the non-standard filename used for the build script. Therefore the build script doesn't get evaluated, and its buildscript block never takes effect.

How to let gradle build dependent sub-project first when using non-compile dependencies

How can I tell gradle to build a certain sub-projects first, even though I don't have a compile dependency to them? How are project dependencies handled internally?
Example:
settings.gradle:
include "app", "schema"
build.gradle:
allprojects {
apply plugin: 'java'
}
schema/build.gradle:
// empty
app/build.gradle:
configurations {
schemas
}
dependencies {
schemas project(":schema")
schemas "org.example:example-schema:1.0"
}
task extractSchema(type: Copy) {
from {
configurations.schemas.collect { zipTree(it) }
}
into "build/schemas"
}
//compileJava.dependsOn extractSchema
And when running:
$ cd app
$ gradle extractSchema
I get:
Cannot expand ZIP 'schema/build/libs/schema.jar' as it does not exist.
What I want is that gradle automatically builds all sub-projectes defined in the configurations.schemas dependency list first (if they are projects).
Note: I want to be able to share the extractSchema task across multiple gradle projects, so it is important that gradle takes the list of sub-project to be built first from the configurations.schemas list.
Thanks
Gradle build order is never on the project level, but always on the task level. from(configuration.schemas) would infer task dependencies automatically, but in case of from(configuration.schemas.collect { ... }), this doesn't work because the resulting value is no longer Buildable. Adding dependsOn configurations.schemas should solve the problem.

Gradle multiproject gives "Could not find property 'sourceSets' on project" error

I had quite good gradle configuration, that built everything just fine. But one of the projects of my multi-project build derived from the rest of them so much, that I would gladly move it to another git repo and configure submodules to handle it.
First, I moved Project and its resources to subfolder Libraries/MovedProject. After altering some lines in gradle configurations it worked fine. But then I decided to write a new build.gradle just for this project, and move all configurations there from the main one.
And this is where everything stopped working. When I try to call any task it always ends
with Could not find property 'sourceSets' on project ':Libraries/MovedProject'. Line which is responsible for it is:
dependencies {
...
if (noEclipseTask) {
testCompile project(':Libraries/MovedLibrary').sourceSets.test.output
}
}
which I use for running tests in which I use classes from other projects. If I remove that line, the build fails only when it reaches compileTestJava task of projects that make use of MovedProject. If I remove that line and call gradle :Libraries/MovedLibrary:properties I can see :
...
sourceCompatibility: 1.7
sourceSets: [source set main, source set test]
standardOutputCapture: org.gradle.logging.internal.DefaultLoggingManager#1e263938
...
while gradle :Libraries/MovedLibrary:build builds correctly.
Currently I've got everything set up as following:
directories:
/SomeMainProject1
/SomeMainProject2
/SomeMainProject3
/Libraries
/MovedProject
build.gradle
dependencies.gradle
project.gradle
tasks.gradle
/Builder
dependencies.gradle
project.gradle
tasks.gradle
build.gradle
settings.gradle
settings.gradle
include Libraries/MovedProject,
SomeMainProject1,
SomeMainProject2,
SomeMainProject3
sourceSets for MovedProject are defined in Libraries/MovedProject/project.gradle:
sourceSets {
main {
java {
srcDir 'src'
srcDir 'resources'
}
resources { srcDir 'resources' }
}
test { java {
srcDir 'test/unit'
} }
}
dependencies that makes use of sourceSets.test.output are stored in Builder/dependancies.gradle, and set for each project that needs MovedProject to run tests:
project(':SomeMainProject1') {
dependencies {
...
if (noEclipseTask) {
testCompile project(':Libraries/net.jsdpu').sourceSets.test.output
}
}
}
What would be the easiest way to get rid of that error and make gradle build projects with current directory structure? I would like to understand why gradle cannot see that property.
The line in question is problematic because it makes the assumption that project :Libraries/MovedLibrary is evaluated (not executed) before the current project, which may not be the case. And if it's not, the source sets of the other project will not have been configured yet. (There won't even be a sourceSets property because the java-base plugin hasn't been applied yet.)
In general, it's best not to reach out into project models of other projects, especially if they aren't children of the current project. In the case of project A using project B's test code, the recommended solution is to have project B expose a test Jar (via an artifacts {} block) that is then consumed by project A.
If you want to keep things as they are, you may be able to work around the problem by using gradle.projectsEvaluated {} or project.evaluationDependsOn(). See the Gradle Build Language Reference for more information.
I had a similar error happen to me in a multimodule project, but for me the cause was as simple as I had forgotten to apply the java-library plugin within the configurations, I only had maven-publish plugin in use.
Once I added the plugin, sourceSets was found normally:
configure(subprojects) {
apply plugin: 'maven-publish'
apply plugin: 'java-library'
....

Resources