A code generator task in a multi-project gradle build - gradle

I have studied thousand similar questions on SO and I am still lost. I have a simple multiproject build:
rootProject.name = 'mwe'
include ":Generator"
include ":projectB"
include ":projectC"
with a top level build.gradle as follows (settings.gradle):
plugins { id "java" }
allprojects { repositories { jcenter() } }
and with two kinds of project build.gradle files. The first one (Generator) exposes a run command that runs the generator taking the command line argument:
plugins {
id "application"
id "scala"
}
dependencies { compile "org.scala-lang:scala-library:2.12.3" }
mainClassName = "Main"
ext { cmdlineargs = "" }
run { args cmdlineargs }
The code generator is to be called from projectB (and an analogous projectC, and many others). I am trying to do this as follows (projectB/build.gradle):
task TEST {
project (":Generator").ext.cmdlineargs = "Hurray!"
println ("Value set:" + project(":Generator").ext.cmdlineargs )
dependsOn (":Generator:run")
}
Whatever I try to do (a gradle newbie here) I am not getting what I need. I have two problems:
The property cmdlineargs is not set at the point that task :projectB:TEST is run. The println sees the right value but the argument passed to the executed main method is the one configured in Generator/build.gradle, not the one in projectB/build.gradle. As pointed out in responses this can be work around using lazy property evaluation, but this does not solve the second problem.
The generator is only run once, even if I build both projectB and projectC. I need to run Generator:run for each of projectB and projectC separately (to generate different sources for each dependent project).
How can I get this to work? I suppose a completely different strategy is needed. I don't have to use command line and run; I can also try to run the main class of the generator more directly and pass arguments to it, but I do find the run task quite convenient (the complex classpath is set up automatically, etc.). The generator is a Java/Scala project itself that is compiled within the same multi-project build.

Note: tasks aren't like methods in java. A task will execute either 0 or 1 times per gradle invocation. A task will never execute twice (or more) in a single Gradle invocation
I think you want two or more tasks. Eg:
task run1(type:xxx) {
args 'foo'
}
task run2(type:xxx) {
args 'bar'
}
Then you can depend on run1 or run2 in your other projects.

Related

How can I generate an entire gradle subproject and have it as a dependency in another project?

So let's say I have the following settings.gradle:
include 'math-server'
project(':math-server').projectDir = file('math/server')
include 'math-client-gen'
project(':math-client-gen').projectDir = file('math/client')
include 'ai'
Now I'd like to not commit the any of the files in math-client-gen (including the build.gradle) since those are generated by a build job in math-server:
// math/server/build.gradle
task generateClient(type:Exec) {
workingDir '.'
inputs.dir('./app')
inputs.dir('.')
outputs.dir('../client')
commandLine './client-generator/generate.sh'
}
The generate.sh leverages the openapi client generator for kotlin.
Now the ai project relies on the math-client-gen:
// ai/build.gradle
dependencies {
compile project(':math-client-gen')
}
Now I have currently found two suboptimal ways to make this work.
Option 1 is to run ./gradlew :math-server:generateClient before I'm able to run ./gradlew :ai:build. This sucks, since you cannot build ai on its own anymore.
Option 2 is to commit the files, which of course also isn't the way it should be.
I'm sure there is a better way to do it with gradle, but I just didn't manage to find it yet. As a compromise, I'd be willing to commit the generated math-client-gen/build.gradle if it doesn't work without that.
What's the best solution to this problem?
Note: I also saw something like:
implementation files(layout.buildDirectory.dir('classes')) {
builtBy 'compile'
}
in the docs, that looks promising, but i'd like to have it for an entire subproject and not just some source files if possible.
// ai/build.gradle
afterEvaluate {
tasks.build.dependsOn(project(":math-client").tasks["generateClient"])
}
To automate your first option.
I ended up committing the build.gradle of the math-client-gen and have this line there:
// math/client/build.gradle
compileKotlin {
dependsOn ':math-server:buildMathClient'
}
This ensures that the client is always generated when this project is listed as a dependency somewhere.
Then you can simply add the math-client-gen as a dependency in other projects, no additional steps required:
// ai/build.gradle
dependencies {
compile project(':math-client-gen')
}

Exclude file from Gradle test dependency

I have rather expensive tests in my gradle java project, so I would like to avoid running them too often. Unfortunately, gradle reruns the tests on every build, since some log file in the resource-folder is changing.
Is there any way to exclude log-files from the dependency checks of :processTestResources and :test? I tried to include a exclude command in my test task, but this doesn't seem to do anything. My test task is
test {
maxHeapSize = "2048m"
workingDir = "src/test/resources/test-instance"
environment "LD_LIBRARY_PATH", "xpressmp/lib:/opt/gurobi/linux64/lib"
environment "XPRESS", "xpressmp/bin"
environment "XPRESSDIR", "xpressmp"
exclude("*.log")
exclude("*.lp")
}
I think what you are after is
sourceSets {
test {
resources {
exclude '*.log'
}
}
}
Excluding in the task would only exclude the test class from running, not which files are considered input for the task.
Btw. you can also use JUnit Categories to separate your tests into Short-Running and Long-Running tests and then make different tasks or a project property to only run the fast tests or all tests or only the slow tests. Or you can split the tests in different sourcesets and make separate tasks for it.
Define a new Test task:
task notGenericNotFT( type: Test, dependsOn: testClasses ){
filter { excludeTestsMatching 'generic.*' }
// excludes a whole package, "generic". NB this is not a regex:
// '*' is simply "wildcard" and dot means dot ... other more
// sophisticated "ANT-style" patterns are available in class Test
filter { excludeTestsMatching '*_FT' }
// also exclude all test classes ending in "_FT" (e.g. for "functional test")
}
To understand where these things come from, examine class Test and class TestFilter.
Also be aware that the gradle command line parser is quite intelligent and permissive with case-sensitivity (even in Linux!), so you can do this:
...$ ./gradlew notgen
... and it will run (as long as "notgen" designates this task unambiguously).

Reading Gradle build parameters

I have a Gradle project with subprojects that I can issue separate build commands if I don't want to build all the subprojects at once. For example,
parent
subprojectA
subprojectB
subprojectC
I can then issue commands like ./gradlew :subprojectA:assemble :subprojectC:assemble. What I like to do is construct a meaning git tag from each subproject group and version values for those subprojects that are being built, i.e., group and version for only subprojectA and subprojectC in this case. I am thinking of writing a standalone plugin to do this but unsure where / how to get this information at build time. Any suggestions would be much appreciated.
Both group and version (as well as any other project property) is available globally in your build script.
task myTask << {
println group + "-" + version
}
If you are writing a binary plugin you can also access properties off the Project object itself via the property() method.
def group = project.property('group')
Edit
If you want to determine if a particular project is being built you can inspect the TaskExecutionGraph.
gradle.taskGraph.whenReady { graph ->
if (graph.hasTask(':subprojectA:assemble')) {
println 'Will build subprojectA'
}
}

gradle use variables in parent task that are defined in child

I have a multiproject gradle build where I declare a task in the parent build that uses a variable that gets declared in the child projects (the value can change depending on the subproject). However, I get an error during the configuration phase that the variable doesn't exist. My setup looks like
build.gradle (top level)
subprojects {
myTask {
prop = valueDefinedInChild
}
}
And then
build.gradle (subproject)
valueDefinedInChild = 'someValue'
Is there a way to do this correctly?
There is a way to do this (project.evaluationDependsOnChildren()), but I recommend to only use it as a last resort. Instead, I'd configure the commonalities at the top level, and the differences at the subproject level:
build.gradle (top level):
subprojects {
task myTask { // add task to all subprojects
// common configuration goes here
}
}
build.gradle (subproject):
myTask {
prop = 'someValue'
}
Another way to avoid repeating yourself is to factor out common code into a separate script, and have subprojects include it with apply from:. This is a good choice when the logic only applies to selected subprojects, or in cases where it's desirable to avoid coupling between parent project and subprojects (e.g. when using Gradle's new configuration on demand feature).

How to compile single class dependency in gradle outside the main project

I have a gradle project that contains only Selenium/TestNG test classes. They are executed against a deployed war application. All works fine and now I'm adding a java utility that will query the test base and print list of tests that belong to a given TestNG group. The utility should be compiled and executed separate from the main project, as users may want to query the test base before test execution.
I added the following to build.gradle:
task listgroups(dependsOn:'buildUtil' ) <<{
ant.java(classname: 'util.TestGroupScanner', fork: true,
classpath: "src/test/java")
}
task buildUtil {
compile {
source = "src/test/java/util"
}
}
However, when calling listgroups task, I'm getting the following error:
C:\console-bg1>g listgroups
FAILURE: Build failed with an exception.
(...)
* What went wrong:
A problem occurred evaluating root project 'console-bg1'.
> Could not find method compile() for arguments [build_4emu7duna2isgubc1k8uts8k9
8$_run_closure6_closure11#d210ab] on root project 'console-bg1'.
I'm not sure how to resolve this issue and needless to say, haven't found an answer online so far. Any pointers appreciated.
The problem is in the buildUtil task, as the error suggests. The buildUtil declares a compile closure, but such closure does not exist for the default task.
Let me try to clarify what your setup is. The util.TestGroupScanner source is in the src/test/java/util directory, which you want to compile separately from other source (presumably src/main/java and src/test/java). The buildUtil task is supposed to compile sources in src/test/java/util, and the listgroups task executes the scanner utility on sources src/test/java folder.
In this case, I'd suggest you declare a new source set for your utility sources, like this:
sourceSets {
util {
java {
srcDir 'src/test/java/util'
}
}
}
This will automatically create a compile task called compileUtilJava for you, that will compile those sources. I also think you'll want to include utility classes in the classpath when executing your tool, which can be retrieved by sourceSets.util.output.classesDir. So now your listgroups task will look like:
task listgroups(dependsOn: 'compileUtilJava' ) <<{
ant.java(classname: 'util.TestGroupScanner', fork: true,
classpath: "src/test/java:" + sourceSets.util.output.classesDir)
}
One thing I have noticed about your setup, is that src/test/java/util source folder is nested under src/test/java. Gradle will assume src/test/java to be the default folder for your project test, and will therefore automatically include it, and all of its children when running tests. Since you want to keep your utility folder separate from the default setup, I would recommend you put it in src/testutil/java, to avoid any clashes. If you do, don't forget to update the sourceSets setup above with the correct source path.
To solve this with gradle, I suggest to create a specific sourceset for your util class and add a task of type JavaExec that executes this class for printing your testng groups. Have a look at the following snippet:
apply plugin:'java'
...
...
configurations{
testUtilCompile.extendsFrom testCompile
}
...
...
sourceSets{
testUtil{
java {
srcDir "src/test/java"
include "util/**"
}
}
}
task printGroups(type:JavaExec){
main = "util.TestGroupScanner"
classpath = sourceSets.testUtil.runtimeClasspath
}
regards,
René

Resources