Gradle Whirlwind Introduction

Thomas J Kennedy

Last modified: Jul 5, 2020
Contents:

For the remainder of this semester we will use Gradle for build and configuration management. We will also add a few quality of life tweaks (e.g., use Gradle to create Eclipse projects).

For now we will discuss the basics of Gradle, focusing on a few selected plugins.

1 Getting Started

The first step to using Gradle is setting up a specific directory structure. Let us pick one of my Java CS 350 Review Examples. Let us use JUnit 3 - Example 6.

Example 1: JUnit 3 Example 6 Directory Structure
├── build.gradle
├── config
│   └── checkstyle
│       └── checkstyle.xml
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── makefile
└── src
    ├── main
    │   └── java
    │       └── edu
    │           └── odu
    │               └── cs
    │                   └── cs350
    │                       └── examples
    │                           └── numbers
    │                               ├── package-info.java
    │                               ├── PrimeGenerator.java
    │                               └── RunPrimeGenerator.java
    └── test
        └── java
            └── edu
                └── odu
                    └── cs
                        └── cs350
                            └── examples
                                └── numbers
                                    └── TestPrimeGenerator.java

1.1 Top Level Directories & Files

Let us focus on just the top level.

Example 2: Directory Structure - Top Level
├── build.gradle
├── config
├── gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── makefile
└── src

Let us examine each item, one at a time.

1.2 The src Directory

Eclipse, by default, places test classes and main classes in the same directory.

Example 3: Eclipse src Directory
└── src
   └── edu
       └── odu
           └── cs
               └── cs350
                   └── examples
                       └── numbers
                           ├── package-info.java
                           ├── PrimeGenerator.java
                           ├── RunPrimeGenerator.java
                           └── TestPrimeGenerator.java

Gradle seperates test and main (production) code.

Example 4: Gradle src Directory
└── src
    ├── main
    │   └── java
    │       └── edu
    │           └── odu
    │               └── cs
    │                   └── cs350
    │                       └── examples
    │                           └── numbers
    │                               ├── package-info.java
    │                               ├── PrimeGenerator.java
    │                               └── RunPrimeGenerator.java
    └── test
        └── java
            └── edu
                └── odu
                    └── cs
                        └── cs350
                            └── examples
                                └── numbers
                                    └── TestPrimeGenerator.java

Take note of how the standard Java package directory structure exists for both the test code and main code.

You probably noticed a java subdirectory for both main and test:

└── src
    ├── main
    │   └── java
    └── test
        └── java

While most of our work will be with Java code, Gradle supports composite projects (i.e., projects that use generated code or multiple languages) and non-code resource files.

2 An Initial build.gradle

Gradle syntax has changed in recent versions. For the remainder of this discussion we will focus on Gradle 5.6.1.

Let us start with a quick look at the full build.gradle from JUnit3 Example 6.

JUnit 3 Example 6 - build.gradle

This is a complete build.gradle. It includes not only what we need to compile code, but also settings for code analysis tools, test coverage, and fancy test reports. Do not try to understand everything just yet.

2.1 Starting build.gradle

Let us take a few steps back. A simple initial build.gradle needs to:

Example 5: Initial build.gradle
plugins {
    id "java"
    id "eclipse"
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

This would be fine… if we did not also need to use JUnit and Hamcrest for our tests. We need to add:

  1. A repositories block that specifies the location from which dependencies can be downloaded.

  2. A dependencies block listing which dependencies (i.e., libraries) are needed.

repositories {
    jcenter()
}

dependencies {
    testCompile "junit:junit:4.12"
    testCompile "org.hamcrest:hamcrest-library:1.3"
}
 

Take note of the testCompile before both dependencies. JUnit and Hamcrest are only needed for test code.

2.2 Tweaking Test Settings

There are a few tweaks we can make to test settings:

  1. Continue running after a failure.
  2. Do not mark the entire build as failed if a test fails.
  3. Generate a quick JUnit Report in the terminal.
  4. Generate a detailed HTML test report.

These tweaks result in the following test block:

test {
    reports {
        html.enabled = true
    }
    ignoreFailures = true

    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
    }
}

2.3 Making an Executable Jar

The JUnit Example included a driver class, RunPrimeGenerator, with a main function. We should probably:

  1. Give the jar file a name.
  2. Specify which class contains the main function.
jar {
    baseName = "GeneratePrimes"

    manifest {
        attributes(
            "Main-Class": "edu.odu.cs.cs350.examples.numbers.RunPrimeGenerator"
        )
    }
}

Take note of edu.odu.cs.cs350.examples.numbers.RunPrimeGenerator. When specifying a Main-Class, a fully qualified class name (package and class) must be used.

2.4 Tweaking Javadoc Settings

You have seen Javadoc documentation a few times this semester (e.g., in the Unit Testing assignments). The java plugin automatically adds the javadoc task. Let us make one small tweak.

javadoc {
    failOnError false
}

This tweak tells Gradle to continue even if the codebase has incomplete Javadoc documentation.

2.5 Our build.gradle So Far

Currently, build.gradle includes:

Let us take a look at our current build.gradle.

Example 6: Updated build.gradle
plugins {
    id "java"
    id "eclipse"
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
}

dependencies {
    testCompile "junit:junit:4.12"
    testCompile "org.hamcrest:hamcrest-library:1.3"
}

jar {
    baseName = "GeneratePrimes"

    manifest {
        attributes(
            "Main-Class": "edu.odu.cs.cs350.examples.numbers.RunPrimeGenerator"
        )
    }
}

test {
    reports {
        html.enabled = true
    }
    ignoreFailures = true

    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
    }

    jacoco {
        append = false
    }
}

javadoc {
    failOnError false
}

2.6 Deprecated Options

In newer versions of Gradle (6.1+) one of the test options has been deprecated:

test {
    reports {
        html.enabled = true
    }
    ignoreFailures = true

    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
    }

    jacoco {
        append = false
    }
}

You should remove the append = false.

3 Importing Everything into Eclipse

You are probably using Eclipse. You have two choices:

  1. Set up a new project manually.
  2. Let Gradle do the heavy lifting.

Gradle is a build and configuration manager. With the right plugins Gradle can set up IDE projects. Do you remember that eclipse plugin from earlier?

plugins {
    id "java"
    id "eclipse"
}
 

Open a terminal, PowerShell, or command prompt window in the directory containing build.gradle.

If you are on macOS, Linux, or WSL, run

chmod a+rx gradlew
./gradlew eclipse

If you are on Windows, run

gradlew.bat eclipse

Once the command sequence completes, open Eclipse and import the .project file, using the import an existing project wizard.


4 The Fun Stuff

Technically the rest of our example build.gradle discussion requires the Configuration Management and Analysis Tools modules. Feel free to stop here for now and come back to these notes later.

It is never too early to introduce code analysis, code linting, and test coverage tools.

4.1 Preparing for More Gradle Plugins

Earlier we added a dependencies block to handle third party libraries for our code. Now we need a similar block for additional Gradle Plugins. The following lines need to be added to the very top of build.gradle.

buildscript {
    repositories {
        jcenter()
    }
}

4.2 Jacoco for Test Coverage

Let us start with jacoco. Jacoco is a test coverage tool. It tells us which lines of code are run by at least one of our tests. Jacoco is not a substitute for proper mutator-accessor test construction.

Jacoco requires an additional plugin line.

plugins {
    id "java"
    id "eclipse"

    id "jacoco"
}

Jacoco requires three tweaks to settings. First the test block needs to be updated.

test {
    reports {
        html.enabled = true
    }
    ignoreFailures = true

    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
    }

    jacoco {
        append = false
    }
}

The second jacoco settings block specifies the tool version.

jacoco {
    toolVersion = "0.8.4"
}

The final jacoco settings block specifies in which formats coverage reports should be generated.

jacocoTestReport {
    reports {
        html.enabled true
        xml.enabled true
        csv.enabled true
    }
}

The html format is for human readability. The xml and csv formats will be used when setting up continuous integration (CI).

4.3 build.gradle with Jacoco

Let us take a look at the full build.gradle after adding jacoco.

Example 7: build.gradle with Jacoco
buildscript {
    repositories {
        jcenter()
    }
}

plugins {
    id "java"
    id "eclipse"

    id "jacoco"
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
}

dependencies {
    testCompile "junit:junit:4.12"
    testCompile "org.hamcrest:hamcrest-library:1.3"
}

jar {
    baseName = "GeneratePrimes"

    manifest {
        attributes(
            "Main-Class": "edu.odu.cs.cs350.examples.numbers.RunPrimeGenerator"
        )
    }
}

test {
    reports {
        html.enabled = true
    }
    ignoreFailures = true

    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
    }

    jacoco {
        append = false
    }
}

javadoc {
    failOnError false
}

jacoco {
    toolVersion = "0.8.4"
}

jacocoTestReport {
    reports {
        html.enabled true
        xml.enabled true
        csv.enabled true
    }
}

4.4 Project Reports

The Gradle project report plugin generates a few different reports. I generally focus on two reports in particular:

This plugin is not immediately useful. However, it adding it requires one small (one line) addition to the plugin block:

plugins {
    id "java"
    id "eclipse"

    id "project-report"
    id "jacoco"
}

Why not add the one line now?

4.5 SpotBugs & Findbugs

Spotbugs is the successor to Findbugs. Findbugs has been abandoned since 2015. You may see a few mentions to Findbugs in the lecture notes and a few older examples.

Use SpotBugs in Java/Gradle projects.

Spotbugs checks code for potential bugs, including:

A full list of bugs can be found in the Spotbugs documentation.

4.5.1 Adding SpotBugs

The first step is to add another line to our plugins block.

plugins { id “java” id “eclipse”

id "com.github.spotbugs" version "2.0.0"
id "project-report"
id "jacoco"

}

SpotBugs requires a little configuration. Just like with Junit and Javadoc, we want to continue the build and analysis if a tool detects failures. Let us also set up an HTML report.

spotbugsMain {
    ignoreFailures = true
    effort = "max"
    reportLevel = "medium"
    reports {
       xml.enabled = false
       html.enabled = true
    }
}

I usually disable SpotBugs for test code.

spotbugsTest.enabled = false

4.6 PMD

PMD is similar to SpotBugs. PMD performs analysis of code, looking for mistakes such as:

A full listing of PMD rules can be found in the PMD documentation.

4.6.1 Adding PMD

PMD requires another plugin line.

plugins {
    id "java"
    id "eclipse"

    id "com.github.spotbugs" version "2.0.0"
    id "project-report"
    id "jacoco"
    id "pmd"
}

Just like with SpotBugs, we want the build process to continue after failures (i.e., detected issues).

pmd {
    ignoreFailures = true
    ruleSets = [
        "category/java/bestpractices.xml",
        "category/java/codestyle.xml",
        "category/java/design.xml",
        "category/java/errorprone.xml",
        "category/java/performance.xml"
    ]
}

Setting up ruleSets requires selecting specific sets of rules. I usually stick with the five rulesets listed in the example.

5 Back to build.gradle

After adding Jacoco, Javadoc, SpotBugs, and PMD we end up with a fairly complete build.gradle. We can now

  1. Run tests.
  2. Generate test coverage reports.
  3. Generate documentation.
  4. Run code analysis tools.

Putting everything together we end up with…

Example 8: build.gradle with Tools Added
buildscript {
    repositories {
        jcenter()
    }
}

plugins {
    id "java"
    id "eclipse"

    id "com.github.spotbugs" version "2.0.0"
    id "project-report"
    id "jacoco"
    id "pmd"
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
}

dependencies {
    testCompile "junit:junit:4.12"
    testCompile "org.hamcrest:hamcrest-library:1.3"
}

jar {
    baseName = "GeneratePrimes"

    manifest {
        attributes(
            "Main-Class": "edu.odu.cs.cs350.examples.numbers.RunPrimeGenerator"
        )
    }
}

test {
    reports {
        html.enabled = true
    }
    ignoreFailures = true

    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
    }

    jacoco {
        append = false
    }
}

javadoc {
    failOnError false
}

jacoco {
    toolVersion = "0.8.4"
}

jacocoTestReport {
    reports {
        html.enabled true
        xml.enabled true
        csv.enabled true
    }
}

// SpotBugs
spotbugsMain {
    ignoreFailures = true
    effort = "max"
    reportLevel = "medium"
    reports {
       xml.enabled = false
       html.enabled = true
    }
}

spotbugsTest.enabled = false
// End SpotBugs config

pmd {
    ignoreFailures = true
    ruleSets = [
        "category/java/bestpractices.xml",
        "category/java/codestyle.xml",
        "category/java/design.xml",
        "category/java/errorprone.xml",
        "category/java/performance.xml"
    ]
}

6 Checkstyle

What about Checkstyle? Checkstyle performs code style analysis, including checking:

Configuring Checkstyle is a little more involved than the other tools we have discussed so far. Let us leave a discussion of Checkstyle configuration and use for the Analysis Tools module.