How can I get Gradle to build dependencies before running my task? - gradle

Background: I am trying to hook the compiler for my own domain-specific language into Gradle. The DSL is compiled to Java source code, so I have built a task that runs before the Java compiler. The compiler cannot currently handle multiple projects with dependencies, so I'm trying to add that.
My DSL has packages like Java that get mapped to identical Java packages. The same should be true for projects. In that case, for each project, the DSL sources get compiled to Java source code, as well as meta-data (a JSON file per compiled class, containing information from the DSL's type system that cannot be mapped to Java types). When project A depends on B, the DSL compilation process for A needs the meta-data files from B. That meta-data should be packaged as resources into the JAR file together with the generated and compiled Java code, as well as possibly hand-written and compiled Java code.
FoobarPlugin.groovy:
class FoobarPlugin implements Plugin<Project> {
#Override
void apply(Project project) {
// create the compileFoobar task
CompileFoobarTask task = project.getTasks().create('compileFoobar', CompileFoobarTask.class);
task.group = 'build';
task.setDescription('Compiles Foobar to Java code.');
task.sourceDirectory = new File(project.projectDir, "src/main/foobar");
task.outputDirectory = new File(project.getBuildDir(), "foobar-java");
// compileFoobar must run before compiling Java code
project.tasks.compileJava.dependsOn(task);
// add the task's output folders as Java source folders
project.sourceSets.main.java.srcDirs += task.outputDirectory;
project.sourceSets.main.resources.srcDirs += task.outputDirectory;
project.sourceSets.test.java.srcDirs += task.outputDirectory;
project.sourceSets.test.resources.srcDirs += task.outputDirectory;
// Turn project dependencies into task dependencies. We have to delay this until the end of the configuration
// phase because project dependencies are not fully known until then.
project.gradle.addBuildListener(new BuildAdapter() {
#Override
void projectsEvaluated(Gradle gradle) {
project.configurations.compile.each {
task.dependencyOutputs += it
}
}
});
}
}
CompileFoobarTask.groovy:
class CompileFoobarTask extends DefaultTask {
#InputDirectory
File sourceDirectory;
#InputFiles
List<File> dependencyOutputs = new ArrayList<>();
#OutputDirectory
File outputDirectory;
#TaskAction
void run() {
FileUtils.write(new File(outputDirectory, "timestamp"), "" + System.currentTimeMillis(), StandardCharsets.UTF_8);
}
}
build.gradle from project A:
apply plugin: 'java'
apply plugin: foobar.gradle.FoobarPlugin
repositories {
mavenCentral()
}
dependencies {
compile project(':b')
}
build.gradle from project B:
apply plugin: 'java'
apply plugin: foobar.gradle.FoobarPlugin
repositories {
mavenCentral()
}
dependencies {
compile 'org.apache.commons:commons-lang3:3.0'
}
Test runs and output:
martin#xyz:~/git-repos/gradle-test$ ./gradlew clean a:compileFoobar
adding dependency /home/martin/git-repos/gradle-test/b/build/libs/b.jar to task task ':a:compileFoobar'
> Task :a:compileFoobar
running task ':a:compileFoobar'
BUILD SUCCESSFUL in 1s
4 actionable tasks: 2 executed, 2 up-to-date
martin#xyz:~/git-repos/gradle-test$ ./gradlew clean b:compileFoobar
adding dependency /home/martin/git-repos/gradle-test/b/build/libs/b.jar to task task ':a:compileFoobar'
> Task :b:compileFoobar
running task ':b:compileFoobar'
BUILD SUCCESSFUL in 469ms
4 actionable tasks: 2 executed, 2 up-to-date
martin#xyz:~/git-repos/gradle-test$ ./gradlew clean a:compileJava
adding dependency /home/martin/git-repos/gradle-test/b/build/libs/b.jar to task task ':a:compileFoobar'
> Task :a:compileFoobar
running task ':a:compileFoobar'
> Task :b:compileFoobar
running task ':b:compileFoobar'
BUILD SUCCESSFUL in 487ms
7 actionable tasks: 5 executed, 2 up-to-date
martin#xyz:~/git-repos/gradle-test$ ./gradlew clean b:compileJava
adding dependency /home/martin/git-repos/gradle-test/b/build/libs/b.jar to task task ':a:compileFoobar'
> Task :b:compileFoobar
running task ':b:compileFoobar'
BUILD SUCCESSFUL in 471ms
4 actionable tasks: 3 executed, 1 up-to-date
As you can see, even though I add b.jar as a dependency to a:compileFoobar, Gradle won't build that JAR before running a:compileFoobar. The Java plugin seems to do something different because running a:compileJava WILL build b.jar first. What do I have to do to achieve the same for my task?

What you need to do is to explicitly create a Task dependency between consumer project's compileFoobar task and the producer project's jar task (in your example where project a depends on project b, you need to create task dependency a:compileFoobar -> b.jar)
You can achieve this in your custom plugin, by checking if the current project has dependencies of type ProjectDependency: if so you create the task dependency accordingly.
Code sample (in your plugin apply() method):
// Turn project dependencies into task dependencies. We have to delay this until the end of the configuration
// phase because project dependencies are not fully known until then.
project.gradle.addBuildListener(new BuildAdapter() {
#Override
void projectsEvaluated(Gradle gradle) {
project.configurations.each { config ->
config.dependencies.each { dep ->
if (dep instanceof ProjectDependency) {
def producerProject = ((ProjectDependency) dep).dependencyProject
def producerJarTask = producerProject.tasks.jar
println " **** Project $project.name depends on $producerProject.name"
println " => create dependency between $task to $producerJarTask"
task.dependsOn(producerJarTask)
}
}
}
}
})
Build execution:
$ ./gradlew clean a:compileFoobar
**** Project a depends on b
=> create dependency between task ':a:compileFoobar' to task ':b:jar'
> Task :a:clean
> Task :b:clean
> Task :b:compileFoobar
> Task :b:compileJava NO-SOURCE
> Task :b:processResources NO-SOURCE
> Task :b:classes UP-TO-DATE
> Task :b:jar
> Task :a:compileFoobar

Related

gradle : How to run externalNativeBuildDebug task after a custom task

I am working on Android project which uses build.gradle. I want to run externalNativeBuildDebug task after a custom task.
In build.gradle, I specify:
task mytask()
{
println 'hello'
}
tasks.getByName("externalNativeBuildDebug").mustRunAfter(mytask)
But gradle throws this error :
Task with name 'externalNativeBuildDebug' not found in project ':app'.
I have defined externalNativeBuild block in android{} block
externalNativeBuild {
cmake {
path file('CMakeLists.txt')
version '3.18.1'
}
}
Is there a way to get externalNativeBuildDebug task and set its dependency?

How to create a custom task in gradle to pack java and kotlin code to a jar?

We have a multi modular setup and we are sharing some tests classes between the modules (mainly Fakes implementations). Our current solution (that you can find below) works just for classes written in Java, but we are looking at supporting also shared kotlin classes.
if (isAndroidLibrary()) {
task compileTestCommonJar(type: JavaCompile) {
classpath = compileDebugUnitTestJavaWithJavac.classpath
source sourceSets.testShared.java.srcDirs
destinationDir = file('build/testCommon')
}
taskToDependOn = compileDebugUnitTestSources
} else {
task compileTestCommonJar(type: JavaCompile) {
classpath = compileTestJava.classpath
source sourceSets.testShared.java.srcDirs
destinationDir = file('build/testCommon')
}
taskToDependOn = testClasses
}
task testJar(type: Jar, dependsOn: taskToDependOn) {
classifier = 'tests'
from compileTestCommonJar.outputs
}
How can I modify the compileTestCommonJar so it supports kotlin?
Here is what we do:
In the module with shared test classes, pack the test source set output into a jar
configurations { tests }
...
task testJar(type: Jar, dependsOn: testClasses) {
baseName = "test-${project.archivesBaseName}"
from sourceSets.test.output
}
artifacts { tests testJar }
In a module that depends on the shared classes
dependencies {
testCompile project(path: ":my-project-with-shared-test-classes", configuration: "tests")
}
PS: Honestly, I would prefer to have a separate Gradle module with common test classes as it's more explicit solution.
The task compileTestCommonJar(type: JavaCompile) compiles .java files only because its the task of JavaCompile type.
There's KotlinCompile task aswell, so you would need to merge it, it basically works similary to JavaCompile but compiles .kt files only.
Said that i wouldn't use task system to share the dependencies across, i would use separate module and work with default compileTestKotlin and compileTestJava task's outputs

Gradle: Executing task in subproject programmatically

My WAR file should contain Java source files from components.
In my root project build.gradle I am executing tasks in subprojects programmatically:
apply plugin: 'war'
jar.enabled = false
war {
// - Copy Java source files to the folder corresponding to the component;
into("/") { from { collectFilesFromCopyTask('copySourceFiles') } }
}
// Collects files from destinationDirs of copy tasks with matching taskName
def collectFilesFromCopyTask(taskName) {
FileCollection collectedFiles = files{}
// for each task in subprojects
subprojects.each {project ->
project.tasks.withType(Copy).matching { task -> task.name.equals( taskName ) }.each { copyFilesTask ->
println 'copyFilesTask.destinationDir=' + copyFilesTask.destinationDir
// execute task
copyFilesTask.execute()
// add destinationDir of the task to the collected files
collectedFiles += files(copyFilesTask.destinationDir)
}
}
return collectedFiles
}
In subproject I have task:
task copySourceFiles(type: Copy) {
destinationDir = file(project.buildDir.name + '/sourceFiles')
into('componentName') {
from(project.projectDir)
exclude('build')
exclude('bin')
exclude('src/main/webapp')
exclude('.gradle')
}
}
Console output:
[sts] -----------------------------------------------------
[sts] Starting Gradle build for the following tasks:
[sts] clean
[sts] build
[sts] -----------------------------------------------------
copyFilesTask.destinationDir=<...>application1\build\sourceFiles
:clean
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:war
copyFilesTask.destinationDir=<...>application1\build\sourceFiles
copyFilesTask.destinationDir=<...>application1\build\sourceFiles
copyFilesTask.destinationDir=<...>application1\build\sourceFiles
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build
, which means that collectFilesFromCopyTask() is executed 4 times.
It should be executed only once, from WAR task.
Never ever use the .execute() method of a task in Gradle.
Except of when ...... no, never ever do that.
It is not a supported thing to do and does probably not work as expected.
Always use task dependencies or task ordering dependencies to make sure dependent tasks are run or tasks are run in a specific order if they both run but otherwise do not depend on each other directly.
Make your war task depend on your copy tasks and make your war task use the outputs of those tasks (not a manual files(...) call).
EDIT:
war {
into("/") { from { subprojects.tasks*.findByName('copySourceFiles').findAll { it instanceof Copy } } }
}

Running task after only if another task is not up-to-date

I have a task that I want to run in gradle after the basic 'classes' task runs, but only if it was not UP-TO-DATE.
How can I model this in gradle?
The problem I'm trying to solve is that I have a wsgen task that I use to generate a wsdl file. The source that is used as a basis of the wsdl has changed, but the wsgen task still says it's up-to-date since the output file exists. I want to delete the wsdl file if the build/compile task ran and was not up-to-date.
Let's say I have my task as follows:
task deleteGeneratedWsdl() {
// delete file here.
}
// I Only want this to run if classes was not UP-TO-DATE
deleteGeneratedWsdl.dependsOn classes
Here is my wsgen task:
task wsgen() {
def inputDir = file('build/classes/main')
def dstDir = file('build/wsdl')
def outputWsdl = file('build/wsdl/MyWSDL.wsdl')
inputs.dir inputDir
outputs.file outputWsdl
doLast{
dstDir.mkdirs()
ant {
taskdef(name:'wsgen',
classname:'com.sun.tools.ws.ant.WsGen',
classpath:configurations.jaxws.asPath)
wsgen(keep:true,
destdir: dstDir,
genwsdl:'true',
classpath:'build/classes/main;../UTIL_PROJ/build/classes/main',
sei:'my.path.SourceFile')
}
}
}
wsgen.dependsOn classes
build.dependsOn wsgen

Gradle dependency does not work as my expectation

I have two sub projects subproject1 and subproject2. I'd like to add some classes from subproject2 to subproject1 and get the subproject1.jar. Below is my gradle file:
task copyClasses (dependsOn: [ ':subproject1:clean', ':subproject1:classes']) {
println "copyClasses "
doLast {
Task study = tasks.getByPath(':subproject1:jar')
study.doFirst {
copy {
println "copy ... "
println sourceSets.main.output.classesDir
println project(':subproject1').sourceSets.main.output.classesDir
from sourceSets.main.output.classesDir
into project(':subproject1').sourceSets.main.output.classesDir
}
}
}
}
task jarUpdated (dependsOn: [ clean, classes, copyClasses, ':subproject1:jar']) {
doLast {
println "jarUpdated"
}
}
But I got the build sequence as below:
$ gradle jarUpdated
copyClasses
:subproject1:compileJava
:subproject1:processResources UP-TO-DATE
:subproject1:classes
:subproject1:jar
:subproject2:compileJava
:subproject2:processResources UP-TO-DATE
:subproject2:classes
:subproject2:clean
:subproject1:clean
:subproject2:copyClasses
Calling Task.doFirst(Closure) after task execution has started has been deprecated and is scheduled to be removed in Gradle 2.0. Check the configuration of task ':subproject1:jar'.
:subproject2:jarUpdated
jarUpdated
BUILD SUCCESSFUL
My expectation is:
$ gradle jarUpdated
:subproject2:clean
:subproject2:compileJava
:subproject2:processResources UP-TO-DATE
:subproject2:classes
:subproject1:clean
:subproject1:compileJava
:subproject1:processResources UP-TO-DATE
:subproject2:copyClasses
copyClasses
copy ...
:subproject1:jar
:subproject2:jarUpdated
jarUpdated
BUILD SUCCESSFUL
Would you please suggest or point out what I missed? Thanks a lot!
The "easiest" way to do exactly what you asked for is probably something like this in your subproject1 build file.
jar {
from tasks.getByPath(':subproject2:compileJava')
}
However this is a very simplistic approach with a LOT of caveats, for example
Subproject 1 can not compile against subproject 2 classes
Any dependencies of Subproject 2 will not be included
etc
I would actually advise declaring subproject2 as a dependency of subproject1 and using one of the plugins that Peter suggested.

Resources