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 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 {
  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)
  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:

  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

    <zip destfile="temporary/${jarName}_${release}.zip">
      <fileset dir = "temporary" >
        <include name = "proj.war" />
        <include name = "proj_cluster_mesg.jar" />
        <include name = "PROJ_SCadhoc*.zip" />
    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}"/>

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

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.


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'


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.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

September 28, 2017
Fast lane to Gradle syntax for Java developers

1. Intro

We are using Gradle as build system for our BCD-UI framework. Even after having created the first working build scripts I wondered sometimes about Gradle’s syntax. Especially how a task definition’s syntax works, while it was easy to use, stayed unclear.
When looking at Gradle as a Java developer, you probably stumble over the syntax. It looks like a nice balance between declarative and imperative, but learning it in addition to Gradle is one more hurdle. Not everybody wants to become a Groovy champion first to start with Gradle.
Here we try to help you, limiting Groovy knowledge as far as possible to read and write your first Gradle scripts.

Attention: We do neither explain Gradle here, nor Groovy. This is all about helping understanding Gradle’s syntax by explaining parts of Groovy’s syntax.

Since a Gradle script basically a Groovy script, you may wonder, why the title is not “Groovy syntax for Java developers”. There are two reasons:

  1. Gradle DSL does only use a part of Groovy syntax

  2. Gradle does extend Groovy syntax a bit

There are already some very good Groovy syntax introductions for Java developers, for example Groovy Tutorial for Java Developers from Tim Roes.

Our goal

Let’s have a look at this typical Gradle fragment:

task myTask( type: Copy ) {
  from 'srcDirA'
  into 'destDir'
  includeEmptyDirs = false
  doLast {
    println "Copy finished"
When I saw this, I had several questions, among them
  1. What is myTask syntactically. Looks like a call to a never declared function

  2. What are the outer curl braces for, some kind of class definition?

  3. When do I need to have use a plain value, an equal sign plus value and when something with curl brackets?

Seems to define a Gradle task named myTask, copying files from 2 sourceDirs to destDir and print ‘copy finished’ at the end.
While you may have an idea what this does, the syntax may not be clear, which hurts in more complex cases or when trying to create even an easy script yourself.

2. Some Groovy syntax

You can Groovy code online with Groovy Web Console here.
Don’t be surprised if it sometimes switches from ‘Output’ to ‘Result’ in the area below the text entry. Later you’ll understand why (see implicit return).

2.1. DSLs

Groovy is, as Java, a language that is compiled into Java Virtual Machine bytecode and executed in a Java VM. It allows to write code more compact compared to Java, which some see critical in general, but which comes nicely when it comes to DSL Domain Specific Languages.
A DSL is not purpose agnostic, like Java or C, being general purpose languages, instead the keywords of each DSL rather belong to a specific domain. In the case of Gradle, the domain is build scripts.

2.2. Some Groovy syntax

We only touch a few aspects of Groovy syntax here.
First, Groovy is almost a superset of Java, i.e. every Java code is also Groovy code you can mix-in Java code lines seamlessly and talk to the other code’s objects. But it has extensions which make it easy to create DSLs and declaration-like syntax.

def str = 'I am a string'                 (2) (3) (4)
// Math.sqrt(str)                         (5)
println str                               (6) (7)
def msg = "$str is ${str.length()} long"  (8)
println Math.sqrt( str.length() )         (9)
str = 200                                 (10)
println str.class.name                    (11)
1 No need for an enclosing class and static void main, Groovy creates that for you in the background. Start the script with code.
2 Groovy knows type inference. Each variables has a typed a given time, as in Java, but the compiler can infer types from the context. In most cases you can just use the generic def
3 Normal strings have single quotes '' in Groovy
4 You can omit the semicolon ; at the end of statements in most cases, if you do one statement per line
5 Groovy knows, txt is a java.lang.String, so compiler and IDE can complain here. Note that it is detected a compile time and not only during runtime as it would be in a scripting language.
6 println() is a shortcut to System.out.println().
7 You can omit the parentheses () around parameters in method calls, if it is not a nested call and has at least one argument
8 GStrings strings are strings in double quotes "", offering interpolation and more
9 You can mix Java and Groovy statements seamlessly
10 Groovy variables defined with def (and not an explicit type name) can change there type over time
11 Each class attribute can be accessed in a property-like manner, Groovy translates this into a call to the getter behind the scenes

While there is much much more about Groovy syntax, see link above, this is enough for our purpose of understanding above Gradle task definition.

2.3. Closures

Groovy supports closures. Closures are functions which can be passed around like objects. If you know Java 8 lambdas or Javascript closures, you have an idea.

To define a closure, just write the body of it surrounded by curl brackets:

// A Groovy script in java-near syntax
import groovy.lang.*;

Closure<Integer> myClosure = {
  int i = 4;
  return 4 * 2;
println( myClosure() ); // -> '8'

This code was as close to Java as possible. let’s have a look at the more compact style we learned above

//                                       (1)
def myClosure = {                        (2)
  def i = 4
  4 * 2                                  (3)
println myClosure()  // -> '8'           (5)
1 No need for import. Groovy imports more than Java per default
2 Use = { …​ } to define the closure
3 You can omit the return key word, the return value will be the value of the last expression of the method or closure
4 We could omit the most outer () but not the inner one. An remember, the second reason why we could not omit the inner one is because that call had not parameters


To define parameters of closures, list the parameters’ names followed by , same syntax as for Java lambdas:

def myClosure = { myVal1, myVal2 ->
  myVal1 * myVal2 * 2
println myClosure(2,4) // -> '16'

Named parameters

Groovy also allows for named parameters. Declare one argument, provide named parameters and access them via name:

def myClosure = { args ->   args.val2 * args.val1 * 2 }
println myClosure( val1: 2, val2: 4 ) // '16'

2.4. Closures as parameters

Closures can be passed around and provided as arguments

def times_2 = { valA -> valA * 2 } (1)
def times_4 = { valA -> valA * 4 }
def show = { valA, cl ->
  println cl( valA )              (2)
show 2, times_2  // -> '4'
show 2, times_4  // -> '8' (3)
1 This closure just returns the argument times 2
2 Here we take the first parameter, call the provided closure with it and print the result
3 Here we call execAndPrint with the value 2 and the closure times4 as arguments

If the closure is the last parameter, you may append it after the call.

def show = { valA, cl -> println cl( valA ) }
show( 2,  { valA -> valA * 8 } )  // -> '16'       (1)
show( 2 ) { valA -> valA * 8 }    // -> '16'       (2)
1 Closure parameter provided as second parameter
2 Closure parameter provided behind the the call, outside of the parenthesis

2.5. The closures’ delegate

But why aren’t groovy closures not just Java lambdas? There are 2 things, Groovy closures can do, which Java lambdas can’t, which are important for us here

  1. Groovy closures have access to local non-constant variables from the context they were defined in

  2. Groovy closures have a user changeable delegate object, which is used for looking up local methods and attributes

Let’s look at the following closure

def config = {
  def srcDir = 'fromDir'
  from srcDir
  includeEmptyDirs = true

While srcDir is a simple local variable, where do from, into and includeEmptyDirs come from?

Let’s create the following class

class MyClass {

  // Some attributes
  def fromDirs = []             (1)
  def includeEmptyDirs = false

  // A method
  void from( fromDir ) {        (2)
    fromDirs.add( fromDir )

  // A method allowing to configre us with a closure
  void configure( closure ) {        (3)
    closure.delegate = this
    closure.resolveStrategy = Closure.DELEGATE_FIRST
1 Two instance attributes
2 A method, appending its argument to fromDirs
3 A method, getting a closure, changing the closure’s delegate to the instance and executing the closure

With this we can do

def myClass = new MyClass()
myClass.configure( config )

println myClass.fromDirs         // -> 'fromDir'
println myClass.includeEmptyDirs // -> true

3. Our own implementation

I am sure this gives you an idea, how configuration closure work in Gradle. To match the syntax even closer, we add a task() call. Here is the complete example:

// The class we want to configure
class MyClass {                                    (1)
  def fromDirs = []
  def includeEmptyDirs = false
  void from( fromDir ) {
    fromDirs.add( fromDir )
  void configure( closure ) {
    closure.delegate = this
    closure.resolveStrategy = Closure.DELEGATE_FIRST

// A generic task factory
def taskCreate = { name, clazz, configClosure ->   (2)
  def t = clazz.newInstance()                      (3)
  t.configure( configClosure )                     (4)
  binding.setProperty( name, t )                   (5)


// One step closer to Gradle
taskCreate ( 'aTask', MyClass ) {                  (6)
  from 'aDir'
  includeEmptyDirs = true

// aTast is now a configured instance of MyClass
assert aTask.fromDirs == ['aDir']
assert aTask.includeEmptyDirs == true
1 Our class, accepting the configure closure
2 A factory function, getting a task name, type and configure closure
3 Create a class of the given type, will be MyClass
4 Apply the configure closure on the new task
5 “Publish” the new task by its name with some Groovy binding magic
6 Create the class in Gradle like syntax.
Note how we wrote the last parameter, i.e. the configure closure, behind that parenthesis.

Is this how Gradle does it? In principle yes, but in case of Gradle all this is executed in the scope of a project object, see below.

4. Check what we learned

Gradle sets up an instance of class Project for each gradle.build file. The content of your build script is then executed in the scope of this instance and all calls and property access are calls to method’s and properties of the project.

Let’s look at the sample above again (with small modifications).

task ('myTask', type: Copy ) {     (1)
  from 'srcDir'                    (2)
  into 'destDir'
  doLast {                         (3)
    println "Copy finished"
1 task is a call to a method defined in Gradle’s Project class.
It gets 3 parameters, the name of the new task, a set of named parameters and a closure (task configuration closure), starting here.
Gradle does create an instance of Copy with the given values
It then applies the closure spanning from the first to the last line to the new instance.
2 from is a call to a method of Copy with th string ‘source’ as its parameter
3 doLast is al call to a method as well with a closure as the parameter

Th difference between this and the sample above is, that myTask in the first sample is not provided as a string but rather looks like a keyword.

task myTask ( type: Copy ) { ... }

That is in fact something, which the Gradle team introduced as yet an extension to Groovy syntax by interpreting the AST.

Let’s try another example

task myTask( type: Copy ) {        (1)
  from ( 'srcDir' ) {              (2)
    exclude '**/*.properties'
  into 'destDir'
  includeEmptyDirs = false         (3)
1 Call the project’s task() method, provide the name of the task, a named parameter type providing a class we want to derive from and a configuration closure to immediately setup our new task instance
2 Here we call the from() method providing 2 parameters, ‘srcDir’ and a closure.
Within the closure we call exclude(), providing one parameter. exclude() is executed in the context of the from specifier.
3 This assigns false to the project’s property includeEmptyDirs. As groovy is transparently using a setter, many internal things could go on here.

Of course all the magic is nested here and exclude() is a method on an instance of some CopySpec class, which is created inside of from(). I guess you can imagine by now, how that works.