October 6, 2017
Gradle overview

1. Intro

We are using Gradle as build system for our BCD-UI framework. To spread the usage in our company, we created this introduction into Gradle as a starting point. For preparation, we created also an introduction into Gradle syntax for Java developers, bypassing most of Groovy details.

2. Gradle

Gradle is a build system with build scripts written in Groovy. Gradle provides an extensive domain specific language DSL and plugins for builds.

If you are unfamiliar with Groovy – no problem. Check our tutorial Gradle syntax for Java developers.

Each build.gradle build script is mapped into a Project, which consists of a set of tasks, such as compile or jar.

build.gradle mainly consists of defining your tasks and configuring plugins.

Simple Gradle build script sample
// A simple plain task
task taskA {                 (1) (2)
  doLast {                   (3)
    println 'taskA being executed'
  }
}


// A task instance extending builtin Copy task type
task myCopy( type: Copy ) {  (4)

  // Configure Copy
  dependsOn taskA            (5)
  from  'srcDirA'
  into  'destDir'
  includeEmptyDirs = false

  // Add custom functionality
  doLast {                   (6)
    println "Copy finished"
  }
}
1 taskA is a plain task, derived from Gradle’s DefaultTask, which provides dependsOn() and doLast() among other things for configuration.
2 You provide a configuration closure, the outer {} pair, to configure our task
3 You normally use a doLast action closure , here the inner {} pair, to extend, what a task does.
4 myCopy instantiates Gradle’s builtin task of type Copy.
5 Copy defines from(), into(), includeEmptyDirs and more, which you can use to configure the copy.
6 Because Copy also extends from DefaultTask, you can use all features from above as well. As you would guess, doLast action closure, here the println, is executed after the copy, a doFirst would be executed before.

If you start gradle from console, Gradle creates a Project instance, reads the build file, finds myCopy and that ot depends on taskA and executes them in the right order.

Command line (use gradlew if gradle is unknwon)
gradle myCopy

or IDE with myCopy task to be executed

Figure 1. IDEA Gradle tool window
Figure 2. Eclipse ‘Gradle Tasks’ view (Window → Show view)

Per default your tasks show up in group ‘other’. You can change that by setting group 'my-group' in a task’s configuration.

3. Build phases

When Gradle is started, it runs the build in 2 phases

  1. Configuration phase to learn and configure all tasks and understand their dependencies

  2. Execution phase of the tasks

There is actually a 3rd phase before, which Gradle uses to determine how many projects the build involves.

3.1. Configuration phase

During configuration phase Gradle ready the build files and executes all configurations. It also builds its task dependency graph. Every top-level code, for example task or maven repository and dependency configurations are applied here.

This includes top-level code of configuration closures, for example println “During configuration ..” below outputs already during this phase.

Each task is configured, including tasks, which are not executed alter.

3.2. Execution phase

In this phase tasks are executed in the order of their dependency.
Everything you define in doFirst/doLast is done in this phase before / after the tasks main work. Gradle calls these action closures.

3.3. Sample

Configuration time vs. execution time
task myTask {
  println "During configuration of the task, even if task is never executed"

  doFirst {
    println "At the beginning of the task's execution"
  }
  doLast {
    println "At the end of the task's execution"
  }
}

4. Incremental build

To support incremental builds, Gradle takes snap shots of all inputs and outputs of a task in form of hash values. Whenever a task is to be executed, it checks whether input or output changed. If this is not the case, the task is considered up-to-date and for performance reasons not executed.

Imagine you have some generated and some manual Java code. Then, if you change some of the manual code, Gradle can skip the generation step. This speeds up builds tremendously. Not only tasks at the beginning of the dependency chain can be treated as up-to-date, but any task, even in the middle of a build.

How Gradle determines input and output depends.

4.1. Implicit inputs and outputs

In case of Copy, ‘from’ folders are automatically seen as input, ‘into’ as output. This works analogously for all predefined task types.

4.2. Explicit inputs and outputs

In other cases you have to make it more explicit using inputs and outputs in your task configuration. This is usually the case, if there are (doFirst or doLast) action closures, called ah-hoc tasks, as Gradle cannot determine input and output from them.

Using inputs and outputs for incrememental build support
task taskA {

  inputs.dir   file('inFolder')              (1)
  outputs.file file('folder/myOutFile.txt')  (2)

  doLast {
    println 'In- or output files changed'    (3)
  }
}
1 Whole directory recursively checked for up-to-dateness of the task.
2 Single file checked for changes
3 Skipped if neither inFolder nor folder/myOutFile.txt changed since last execution.

Or use onlyIf with a closure returning a boolean

Using onlyIf
task taskB {
  onlyIf { file('folder/myFile.txt').exists() }
  doLast {
    println 'MyFile.txt is missing'
  }
}

This task is only executed, if folder/myFile.txt does not exist.

As mentioned, the configuration phase is still executed for all tasks, even if they are not in the dependency tree or turn out to be up-to-date.

5. Plugins

Gradle’s basic configuration already holds a lot of predefined tasks types, as you can see at Gradle documentation, navigating to ‘Task types’. Each of these you can instantiate with the task directive, providing the type parameter.

Gradle plugins, from Gradle or 3rd party, add more to your project

  • Convention properties

  • Predefined tasks (not just types)

  • New task types

5.1. War plugin

As an example, let’s have a look at the small war plugin. To make use of the plugin add

apply plugin: 'war'

to the beginning of your script. The war plugin extends the Java plugin

Among other things, the war plugin adds the following to the project

  • A task war of task type War, depending on a task compile

  • A convention property webAppDirName with a default value of ‘src/main/webapp’

When you execute the war task, it will

  1. Compile your Java sources found at src/main/java, because it depends on compile

  2. Create a <projectname>.war file at build/libs, containing

    1. Compiled classes found at default location build/classes

    2. All files found at src/main/resources

    3. All files found at webAppDirName. They are assumed to be the static web application files

You may have noticed that most input conventions follow maven project layout. This is the case because the Java plugin does so.

As you see, task war is ready to be used, just comply to its folder layout conventions. In Gradle you overwrite all conventions if that better fits to your project layout. To further configure war, you can do the following

Further configure an already defined task
webAppDirName = 'webapp'     (1)

war {                        (2)
  dependsOn MyOtherTask
  from 'additionalSrc'       (3)
  doFirst {                  (4)
    // Before .war file is created, but after compile
  }
  doLast {
    // After .war file was created
  }
}
1 Overwrite the default where the static files are found
2 Note, this is a predefined task called war, not a task definition. Here you overwrite its configuration.
3 Add some additional files to the root of the .war file
4 Do some action just before the war is created (unless the task is found up-to-date)

War task type extends Archive task type, documentation shows all options.

5.2. Java plugin

Gradle’s Java plugin provides a lot of things, we think it makes sense to start with the following subset:

Source sets

Source sets are sets of sources and resources belonging together. For example production and test sources or sets of sources per jar.
Java plugin predefines some like main and test and you can define your own.
Folder structure default follows maven standard layout. For the predefined main and test and a custom abc it look like below.

src / main / java        (1)
           / resources
      test / java
           / resources
      abc  / java
           / resources   (2)
1 Java sources for main source set, i.e. production
2 Static resources like XML for some custom source set abc

To define your own source sets or adjust the default folder structure, use the following syntax:

sourceSets {
  abc {
    java {
      srcDirs = ['java']
    }
    resources {
      srcDirs = ['staticResources']
    }
  }
}

Dependencies

Dependencies are jars or classes needed to compile the source sets.
Can be loaded from a local folder or from maven repository for example. Maven dependencies are applied recursively per default, i.e. their declared dependencies are loaded as well.

dependencies {
  repositories {
    mavenCentral()
  }
  runtime group: 'commons-collections', name: 'commons-collections', version: '3.2.2'
  runtime fileTree(dir: 'localLibs', include: '*.jar')

  // Provided during runtime by servlet container
  compileOnly group: 'javax.servlet', name:' javax.servlet-api', version: '3.0.1'
}

Dependency configurations

runtime dependencies are needed for compile and during runtime
compileOnly are needed for compile but are resolved otherwise at runtime, they are not included in the distribution

Added tasks

Java plugin adds a lot of tasks, some even per source set, for example sourceSetCompile.
Most important build, which includes compile and creating all artifacts.
The default artifact is the jar, which contains all compiled classes and resources of main. Add additional artifacts like this:

task abcJar(type: Jar) {
  archiveName 'abc'
  from sourceSets.abc.output
}
artifacts {
  archives abcJar
}

Output folder convention

Created jars go to build/libs.

6. A custom task type

An ad-hoc task
task myTask  {
  doLast {
    // Task action
  }
}

This creates an ad-hoc task. That is fine if the action is limited and only needed once.
As we have seen, predefined task types like Copy can be instantiated to tasks with different configurations without repeating the code.

You can create a task type yourself and of course it is very easy:

A custom task type
class ShowFilesTask extends DefaultTask {   (1)
  @Input                                    (2)
  String folder = '.';                      (3)
  @Input
  String extension = '*';

  @TaskAction                               (4)
  void show() {
    project.fileTree(folder).include( '**/*.'+extension ).each {
      println it
    }
  }
}

task showSrcHtml( type: ShowFilesTask ) {   (5)
  folder    = 'src'
  extension = 'html'
  doLast { println "$name done" }
}

task showAllTxt( type: ShowFilesTask ) {
  extension = 'txt'
}
1 Create a class extending Gradle’s DefaultTask
2 @Input / @Output indicate that if this changes, the task is not up-to-date. To check for file content you need to use @InputFile/@InputDirectory
3 All public properties can be set in configuration
4 Mark the execution method with @TaskAction annotation
5 Use your task type like a builtin one

You can place your task type definition inline in your build file or at buildSrc/src/main/groovy/. Both will be compiled and added to the classpath by Gradle automatically.

7. Connection to Ant

Gradle supports smooth migration from Ant by allowing to include end execute Ant tasks and depend on Ant targets.
Basically Gradle provides a property ant for access to the Ant world.

7.1. Use Ant tasks

  1. To call an Ant task, call it as a method on ant, usually in a doLast action closure

  2. Text content of an Ant task becomes the parameter:

    <echo>Hello</echo>
    ant.echo('Hello')
  3. Attributes become named parameters

    <mkdir dir="webapp/META-INF"/>
    ant.mkdir( dir:"webapp/META-INF" )

    A full example with both cases:

    task callEcho {
     doLast {
       ant.echo('Hello', level: 'info' )
     }
    }
  4. Nested tags of an Ant task go into a closure, for each tag there is a method

    Ant
    <zip destfile="temporary/${jarName}_${release}.zip">
      <fileset dir = "temporary" >
        <include name = "proj.war" />
        <include name = "proj_cluster_mesg.jar" />
        <include name = "PROJ_SCadhoc*.zip" />
      </fileset>
    </zip>
    Gradle
    ant.zip( destfile: "temporary/${jarName}_${release}.zip" ) {
     ant.fileset( dir: 'temporary' ) {
        ant.include( name: "${jarName}_${release}.war" )
        ant.include( name: "proj_cluster_mesg.jar" )
        ant.include( name: "proj_SCadhoc*.zip" )
     }
    }

7.2. Depend on Ant targets

  1. To make your Ant targets available, import your build script and use them in dependsOn, this time without prepending ant.

    Depend on a custom Ant target
    ant.importBuild 'build.xml'
    
    task myGradleTask {
      dependsOn myAntTask
    }

7.3. Ant properties

  1. Ant properties can be access via ant.properties map:

    ant.properties.myMessage = 'Hello Gradle'
    <target name="printMyMessage">
     <echo message="${myMessage}"/>
    </target>

8. Multi projects

In case you want to split your build project into multiple ones, you can do this easily. You can create a sub folder for each sub project and have the top-level folder hold the main project.
Just add a settings.gradle file to you main project and list you sub projects as

settings.gradle
include 'SubProject1', 'SubProject1'

Each sub project has its own build.gradle file and is more or less isolated from the other projects. If a task of a project then depends on a task of another project, you can use

Depend on a task of another project in a multi project environment
task taskB {
  dependsOn ':SubProject1:Mytask'
  ...
}

You can also put common logic of all projects in the top-level build script, for example

allprojects {
  apply from: "${rootProject.rootDir}/gradle/script/common.gradle"
}

Will make common.gradle part of each build.gradle in the project hierarchy.

Even on top of multi-projects, you can use Gradle’s composite builds, see documentation.

9. Also Noteworthy

Gradle has plenty of further options which we could not cover. These are useful for beginning as well:

Import build files

You can split your build file to get easier to maintain parts to reuse parts

apply from 'shared.gradle'

Gradle wrapper

Gradle does not rely on a global Gradle installation on you machine. This makes it much easier to checkout a project and start, including working with the right Gradle version even for past releases. If you have ever had an Ant version conflict, you know the possible hassle.

Gradle wrapper is a started with version information, which downloads Gradle binaries transparently, if needed. This Gradle installation is local to the project it is done in.
Eclipse and IDEA create the wrappers if you create a Gradle project initially. Just make 4 files [gradlew, gradlew.bat, gradle/wrapper/gradle-wrapper.jar, gradle/wrapper/gradle-wrapper.properties] part of your version repository and you are done.

Properties

Project and task have pre-defined properties, more are added by plugins. As we showed, assigning and reading them is straight forward.
You will keep your own data often in variable, declared with def varName
But you can also add extra properties to project and tasks as a clean way of given them a kind of scope. To prevent the user from accidental mistyping a predefined property and instead implicitly declaring a new one, Gradle introduced an explicit property extension ext:

// Extra properties for project
ext {
  someProjectProp1 = 'some value'
  someProjectProp2 = 'some value'
}

// An extra property for taskB
task taskB {
  ext.myTaskProp = 'some value'
}

// Read an extra property
if( taskB.ext.myTaskProp == '' ) {
  println 'myTaskProp set at taskB'
}

Files

file() is a method of project and turns each relative path in an an absolute path by interpreting the path as relative to the project root.

Create a file
def di = file(new File("$buildDir/def/abc")) (1)
di.mkdirs()                                  (2)
def fi = new File(di.path+'/xyz.txt')
fi.createNewFile()
fi.text = 'abc'                              (3)
1 Make sure, path is relative to project (in this case not really necessary as buildDir is absolute already)
2 Convenient method to create a nested folder structure
3 Easy access to file content

Copy task

Copy type has many features, some are:

task myCopy( type: Copy ) {
  into "$buildDir/tmp"                          (1)
  from 'webapp'                                 (2)
  from ( 'src' ) {                              (3)
    include '**/*.java'                         (4)
    into 'src'                                  (5)
    expand([ prop1: 'val_1', prop2: 'val_2' ])  (6)
    eachFile { f ->                             (7)
      if( f.name =~ '.*Test.java' )
        f.path = 'test/'+f.name                 (8)
    }
  }
  includeEmptyDirs false                        (9)
}
1 Every into below is relative to this. buildDir is a Gradle well-known project property, defaults to ‘build’.
2 Copy full folder structure with all files
3 The following config closure is only applied to files in src
4 include, exclude follow Ant notation. Collects all includes, removes all excludes, then copies
5 Still relative to outer into
6 Keyword expansion. ${prop1} becomes val_1 in copied file
7 Do an action per file. Can also be used to skip, rename or change them or create additional files, change target folder etc.
8 Redirect the file
9 Don’t create folders that would stay empty
Post Views: 2,214

Leave a Reply

Your email address will not be published. Required fields are marked *