12

I'm trying to create a Gradle build script that will build a Java .jar file in 'release' or 'debug' modes and am having trouble parametrizing the script.

The question is: What is an idiomatic way to do this in a Gradle script, using the Java plugin? (or, if there's no idiomatic way, what's a hacky solution that actually works?)

I don't mind the method of parametrization, just as long as the command-line and IDE invocations can easily choose between the two output options. The jar file will be used as a library in other projects, e.g. an Android app and a JavaFX app, so I would like the method of parametrization to be invokable/depended-on from their own Gradle scripts.

Ideally I'd like to 'emulate' the Android gradle plugin's ability to have a Debug/Release version of every task, i.e.

$ ./gradlew build    
$ ./gradlew assembleRelease
$ ./gradlew checkDebug

but failing that even a top level buildDebug and buildRelease would be suitable.


Stuff I tried

This section isn't really relevant to the question.

Starting point

I have the following gradle file/project:

group 'TestGradleProjectGroup'
apply plugin: 'java'
sourceCompatibility = 1.8

version '1.0-release'
compileJava {
    options.debug = false
}

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.11'
}

This works fine and produces the jar file:

$ ls TestGradleModule/build/libs/
TestGradleModule-1.0-release.jar

which, when the extracted classes are inspected with javap, doesn't contain any debug information. Hurrah. No we need a way to make a debug version.

Add debug & release tasks

version '1.0-release'
compileJava {
    options.debug = false
}

task buildRelease(type: GradleBuild, dependsOn: build) {
    project.version = '1.0-release'
    compileJava {
        options.debug = false
    }
}

task buildDebug(type: GradleBuild, dependsOn: build) {
    project.version = '1.0-debug'
    compileJava {
        options.debug = true
    }
}

This didn't work, as the debug project is always built, even if buildRelease is the task given on the command line. I guess this is because the code for both tasks is run at Configuration time (Gradle build lifecycle), whereas I only want one to run. So I guess I want to run them at Execution time?

Add some doLast tasks

version '1.0-release'
compileJava {
    options.debug = false
}

task buildRelease(type: GradleBuild, dependsOn: build) {
    doLast {
        project.version = '1.0-release'
        compileJava {
            options.debug = false

        }
    }
}

task buildDebug(type: GradleBuild, dependsOn: build) {
    doLast {
        project.version = '1.0-debug'
        compileJava {
            options.debug = true
        }
    }
}

This is even worse. The output file is always 1.0-release, and that's because of the 'default' at the top level. If I comment that out then a versioned jar is not made, instead the default of TestGradleModule.jar is made. It seems that the contents of the doLast blocks are completely useless in their effect upon the compileJava task (but there's no warning about this?). I guess these modifications are too late to be done at execution time, or that there's something else that needs to be done so that the compileJava tasks is "configured" differently?

Using Configurations?

I noticed the manual for the Java plugin contained a reference to buildConfigName and uploadConfigName, and claimed they depend on "The tasks which produce the artifacts in configuration ConfigName.". Given that I was failing to parametrize stuff at Configuration time, the plugin's ability to do that looked promising:

group 'TestGradleProjectGroup'
apply plugin: 'java'
sourceCompatibility = 1.8

configurations {
    debug
    release
}

version '1.0-release'
compileJava {
    options.debug = false
}

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.11'
}

but:

  1. This didn't add buildRelease or buildDebug to the output of ./gradlew tasks --all, as I expected it to. But I could run those tasks.
  2. It only seemed to make buildRelease and buildDebug, not e.g. assembleRelease etc
  3. buildRelease didn't seem to depend on anything when run so therefore had no useful effect.

Iterating tasks?

As a final attempt I've tried to create all of the appropriate tasks and linking the dependencies for everything. I tried iterating over the tasks and adding the dependencies:

gradle.taskGraph.whenReady { taskGraph ->

    taskGraph.allTasks.each { taskIter ->

        println("iterating" + taskIter)

        def releaseTask = project.task(taskIter.name + "Release")
        def debugTask = project.task(taskIter.name + "Debug")

        taskIter.dependsOn += [releaseTask, debugTask].toSet()
        println("new taskIter.dependsOn:" + taskIter.dependsOn)

        /*
            set debug mode here,
            copy over effects of task to debug/release
            disable effects of task
        */
    }
}

but

  1. This didn't seem to create the tasks properly, they weren't accessible from the command line and running "build" didn't run "buildRelease" etc.
  2. I would also have to "move" all of the "actions" from the current, existing tasks and into the debug and release tasks, to avoid duplicating the effects of each. And I don't know how to do that.

In short, I have no idea what I'm doing. As a last resort I could just create all of the tasks manually, but that seems to defeat the point of using the the java plugin and is very spammy?

Pod
  • 3,938
  • 2
  • 37
  • 45

4 Answers4

6

After many hours of searching ...

The "Configure by DAG" section of the "Build Script Basics" Gradle tutorial describes a way to do this, i.e.

https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#configure-by-dag

Quote from the tutorial

"Gradle has a configuration phase and an execution phase. After the configuration phase, Gradle knows all tasks that should be executed. Gradle offers you a hook to make use of this information. A use-case for this would be to check if the release task is among the tasks to be executed. Depending on this, you can assign different values to some variables."

Here is an example of applying gradle.taskGraph.whenReady{} using buildRelease() and buildDebug() tasks...

gradle.taskGraph.whenReady { taskGraph ->
    if (taskGraph.hasTask(buildRelease)) {
        compileJava.options.debug = false
        project.version = '1.0-release'
    } else if (taskGraph.hasTask(buildDebug)) {
        compileJava.options.debug = true
        project.version = '1.0-debug'
    }
}

task buildRelease(type: GradleBuild, dependsOn: build) {
}

task buildDebug(type: GradleBuild, dependsOn: build) {
}

IMO, Gradle is a total bear to learn. I sure do miss good ol' make.

beyeriii
  • 301
  • 3
  • 6
5

Do you need both jars to be created with a single run of gradle? If not, it could be as easy as using some additional argument to gradle, such as

compileJava {
    options.debug = project.hasProperty('debugBuild')
}

gradle assemble -PdebugBuild

See here for documentation: https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_properties_and_system_properties

Also not that you might get better help in the gradle forums.

tkruse
  • 10,222
  • 7
  • 53
  • 80
  • Thanks for the answer. I would have preferred both jars in a single run, (or at least the option to), but it seems difficult to get that to happen. So I'm going to use this approach in my library, as most people won't need the release-only version and will know if they need it or not. – Pod Dec 15 '17 at 10:29
  • @tkruse, you don't need to pass a value for the debugBuild property. Doing so makes it confusing if the user runs `gradle assemble -PdebugBuild=false` and ends up with a debug-build. You can just run the command as: `gradle assemble -PdebugBuild` to get a debug-build, or `gradle assemble` to end up with a release-build. The reason is that you check for the presence of the property via `project.hasProperty('debugBuild')`, and not the property's value. – wcmatthysen Apr 17 '19 at 21:40
  • I agree. I probably copy&pasted from code where I was using the value in a different way. – tkruse Apr 18 '19 at 00:51
  • Notice that you may have to pass `-PdebugBuild` to other gradle commands as well, not only `assemble`, otherwise gradle may re-run the `compileJava` task with `options.debug=false`. See [this answer](https://stackoverflow.com/a/71981227/7050476) for more details. – afarah Apr 23 '22 at 15:49
1

Your question is detailed since you made a deep analysis of the problem. Although I believe that this issue could be solved if you would think in simpler way.
So if we think simple, a possible solution to your case is to run the debug task after the release is finalized. See the example below:

compileJava {
    options.debug = false
}

task buildRelease(type: GradleBuild, dependsOn: build) {
    doLast {
        project.version = '1.0-release'
        compileJava {
            options.debug = false

        }
    }
}

task buildDebug(type: GradleBuild, dependsOn: build) {
    doLast {
        project.version = '1.0-debug'
        compileJava {
            options.debug = true
        }
    }
}

// Here you are telling the buildDebug task to be executed right after the 
// buildRelease task is finalized.
buildDebug.mustRunAfter buildRelease

In the above way you control the order of the task execution without introducing an explicit dependency between them.

1

Not sure but perhaps my gradle-java-flavours plugin could help here. Eg:

plugins {
    id "com.lazan.javaflavours" version "1.2"
}
javaFlavours {
    flavour 'debug'
    flavour 'release'
}
debugJar {
    version = "${project.version}-debug"
}
releaseJar {
    version = "${project.version}-release"
}

Each flavour gets it's own Jar and JavaCompile tasks etc and you can specify custom sources, resources, tests, dependencies for each flavour too.

See here for a bit more overview on the tasks, directories and configurations avaliable and here for a test which covers the functionality.

This approach differes from your GradleBuild style since the "flavours" can happily live together in a single project which builds multiple artifacts rather than running a project twice with two different parameters.

lance-java
  • 25,497
  • 4
  • 59
  • 101
  • I actually came across this plugin during my research on the problem, but never got chance to look into it. I might adopt the plugin for future use as I'd definitely like to mirror the android style of [task]Release and [task]Debug. Unfortunately I was on vacation at the time of the bounty expiring and so didn't have time to check the solution and give it the bounty ;( – Pod Dec 15 '17 at 10:32