I want to execute a JavaExec task that uses the project’s compiled classes, as well as additional dependencies.
This is what I got so far:
configurations {
generateMatchers { extendsFrom runtime }
}
dependencies {
compile
'org.hamcrest:hamcrest-library:1.3'
generateMatchers 'org.hamcrest:hamcrest-generator:1.3'
}
repositories {
mavenCentral()
}
task generateMatchers(type: JavaExec, group: 'Build') {
classpath = configurations.generateMatchers
// sourceSets.main.runtimeClasspath
main = 'org.hamcrest.generator.config.XmlConfigurator'
args 'src/main/hamcrest/fooMatchers.xml',
'src/main/java',
'foo.FooMatchers',
'build/generated-sources/hamcrest'
doFirst {
new File('build/generated-sources/hamcrest').mkdirs()
}
}
generateMatchers fails because configurations.generateMatchers does not include my application’s classes; but sourceSets.main.runtimeClasspath does not (and should not) include hamcrest-generator.
You can concatenate file collections with ‘+’.
Well, that was surprisingly straight-forward.
For some reason, this automatically adds a dependency to the classes task, which causes an error because I want ‘classes’ to depend on ‘generateMatchers’. It works when I move the line into the doFirst-block, but it seems odd to me.
It’s expected because ‘sourceSets.main.runtimeClasspath’ contains the compiled classes of the source set, and task autowiring automatically adds the necessary task dependencies. Maybe you want ‘sourceSets.main.compileClasspath’.
The ‘compileClasspath’ does not work, because the task needs the compiled classes
I believe I read most of the user guide, but this is the first time I hear of autowiring. How does gradle determine what to wire? It would work fine if the task depended on ‘compileJava’ instead (that’s how I configured it right now).
I know that generating Hamcrest matchers is a challenge (it was hard enough with maven, too). I’ll just put my solution here, for discussion and reuse (please feel free to point out the best practices I violated;-).
configurations {
generateMatchers
}
dependencies {
compile
'org.hamcrest:hamcrest-library:1.3'
generateMatchers 'org.hamcrest:hamcrest-generator:1.3'
}
sourceSets{
hamcrest {
output.classesDir 'build/classes/main'
java {
srcDir 'build/generated-sources/hamcrest'
}
}
}
task generateMatchers(type: JavaExec, group: 'Build', dependsOn: compileJava) {
classpath = configurations.generateMatchers
main = 'org.hamcrest.generator.config.XmlConfigurator'
args 'src/main/hamcrest/fooMatchers.xml',
'src/main/java',
'foo.FooMatchers',
'build/generated-sources/hamcrest'
doFirst {
classpath += sourceSets.main.runtimeClasspath
new File('build/generated-sources/hamcrest').mkdirs()
}
}
compileHamcrestJava {
dependsOn generateMatchers
doFirst {
classpath += sourceSets.main.runtimeClasspath
}
}
classes { dependsOn compileHamcrestJava }
task run(type: JavaExec, dependsOn: classes) {
description = 'Runs the application'
main = 'foo.Foo'
classpath = sourceSets.main.runtimeClasspath
}
task sourcesJar(type: Jar, dependsOn: classes) {
classifier = 'sources'
from sourceSets.main.allSource + sourceSets.hamcrest.allSource
}
javadoc {
source += sourceSets.hamcrest.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives sourcesJar
archives javadocJar
}
1 Like
I can’t find an explanation of task autowiring in the user guide. In a nutshell, a domain object may implement ‘org.gradle.api.Buildable’, which allows it to carry metadata about which tasks are necessary for building it. (In particular, ‘FileCollection’ and therefore also ‘Configuration’, ‘SourceSetOutput’, etc. implement ‘Buildable’.) In many cases, Gradle can add this metadata automatically (an example would be retrieving a task’s output files with ‘task.outputs.files’), and it also carries over from one domain object to the next (such when adding two file collections, or when adding a ‘Buildable’ to a configuration.)
Whenever a ‘Buildable’ becomes an input of a task (see “inputs and outputs” in the user guide), Gradle automatically adds task dependencies from that task on the ‘Buildable’'s task dependencies. This process is called task autowiring, and takes place when the task execution graph is built. (Because ‘doFirst’ gets executed only after the task execution graph has been built, autowiring didn’t kick in in that case.) In a multi-project build, task dependencies like ‘project(":foo").compileJava.dependsOn(project(":bar").jar)’ are the result of autowiring.
As for your build, the general approach is fine. You could make more use of the ‘hamcrest’ source set and what you automatically get along with it: ‘configurations.hamcrestCompile’, ‘configurations.hamcrestRuntime’, ‘sourceSets.hamcrest.compileClasspath’, ‘sourceSets.hamcrest.runtimeClasspath’, ‘tasks.hamcrestClasses’, etc. Finally, ‘classes { dependsOn compileHamcrestJava }’ should be removed.
Thank you very much for the feedback. I am currently trying out different approaches. However, I always get to the same two questions:
- Is it “ok” to change the default output and to compile the hamcrest sources to ‘build/classes/main’? If not, how can I tell every other task that more class-files can be found in ‘build/classes/hamcrest’? - If ‘classes’ no longer depends on ‘compileHamcrestJava’, how can I tell every task that depends on ‘classes’ to also depend on ‘hamcrestClasses’?
I think the basic problem is that you are mixing two approaches: Adding a new source set (high-level approach), and reconfiguring tasks of an existing source set (low-level approach). Instead, I’d try to stick to one of the two approaches as much as possible. If you decide to go with the “new source set” approach, you’d leverage everything that comes with the new source set (see my previous post). You’d configure the new source set’s compile/runtime class path (preferably via its configurations), adapt the test source set’s compile/runtime class path (preferably via its configurations), add an “allJar” task that combines the outputs of the main and Hamcrest source sets into a single Jar, etc. With this approach, you shouldn’t need to configure much on the task level, and shouldn’t have to add many task dependencies due to task autowiring.
In case of the Jar task, you could potentially make an exception, and repurpose the main source set’s Jar task with something like ‘jar.from(hamcrestResources, compileHamcrestJava)’. This might simplify publication of the Jar to other projects or a repository (although it’s certainly doable with an “allJar” task as well).
A major (backwards-compatible) redesign of source sets is currently underway, with the goal of providing more flexibility when needed. I expect use cases like yours to benefit from this.
Well, since it works for me I’ll leave it at that then, and look at it again when a new release is out. I certainly learned a lot. Again, thank you very much for your help.
One final thing: I played around with autowiring, and I wanted ‘compileHamcrestJava’ to depend on ‘generateMatchers’. I tried this:
def hamcrestSrc = files("$buildDir/generated-sources/hamcrest") { builtBy 'generateMatchers' }
sourceSets {
hamcrest {
java.srcDirs hamcrestSrc
output.classesDir = "$buildDir/main/classes"
}
}
but it did not work as expected and printed this on the console:
Converting class org.gradle.api.internal.file.collections.DefaultConfigurableFileCollection to File using toString() method has been deprecated and is scheduled to be removed in Gradle 2.0. Please use java.io.File, java.lang.String, java.net.URL, or java.net.URI instead.
By reading the javadocs, I would have expected ‘SourceDirectorySet#srcDirs’ to accept file collections.
The docs are wrong, you can’t add a file collection. Source directory sets don’t currently work well with autowiring. It’s a known problem and will be improved as part of the redesign.
Thank, that was helpfull.