Chapter 3. Working with Gradle Build Scripts

A Gradle script is a program. We use a Groovy DSL to express our build logic. Gradle has several useful built-in methods for handling files and directories, because we often deal with files and directories in our build logic.

In this chapter we will learn how we can use Gradle's features to work with files and directories. Also, we will take a look at how we can set properties in a Gradle build and use Gradle's logging framework. Finally, we see will how we can use the Gradle wrapper task to distribute a configurable Gradle with our build scripts.

Working with files

It is very common in a build script that we have to work with files and directories. For example, when we need to copy a file from one directory to another, or when we first create a directory to store the output of a task or program.

Locating files

To locate a file or directory relative to the current project, we can use the file() method. This method is actually a method of the Project object that is connected to our build script. In the previous chapter we learned how we could use an explicit reference to the project variable or simply invoke methods and properties of the Project object implicitly.

The file() method will resolve the location of a file or directory relative to the current project and not the current working directory. This is very useful because we can run a build script from a different directory than the location of the actual build script. File or directory references that are returned by the file() method are then resolved relative to the project directory.

We can pass any object as an argument to the file() method. Usually, we will pass a String or java.io.File object.

In the next example we will demonstrate how we can use the file() method to get a reference to a File object:

// Use String for file reference.
File wsdl = file('src/wsdl/sample.wsdl')

// Use File object for file reference.
File xmlFile = new File('xml/input/sample.xml')
def inputXml = project.file(xmlFile)

There are many more ways in which we can use the file() method. We can pass a URL or URI instance as an argument. Only file: URLs are now supported by Gradle. We can also use closure to define the file or directory. Finally, we could also pass an instance of the java.util.concurrent.Callable interface, where the return value of the call() method is a valid reference to a file or directory:

import java.util.concurrent.Callable

// Use URL instance to locate file.
URL url = new URL('file:/README')
File readme = file(url)

// Or a URI instance.
URI uri = new URI('file:/README')
def readmeFile = file(uri)

// Use a closure to determine the
// file or directory name.
def fileNames = ['src', 'web', 'config']
def configDir = file {
    fileNames.find { fileName ->
        fileName.startsWith('config')
    }
}

// Use Callable interface.
def source = file(new Callable<String>() {
    String call() {
        'src'
    }
})

With the file() method we create a new File object; this object can reference a file or a directory. We can use the isFile() or the isDirectory() method of the File object to see if we are dealing with a file or a directory. In case we want to check if the file or directory really exists, we use the exists() method. Because our Gradle build script is written in Groovy, we can also use the extra properties and methods added by Groovy to the File class. For example, we can use the text property to read the contents of a file. However, we can only test the File object after we have used the file() method to create it. What if we want to stop the build if a directory doesn't exist or if we are dealing with a file and we expected to be dealing with a directory? In Gradle we can pass an extra argument to the file() method, of type org.gradle.api.PathValidation. Gradle then validates if the created File object is valid for the PathValidation instance; if it isn't, the build is stopped and we get a nice error message telling us what went wrong.

Suppose we want to work with a directory named config, in our build script. The directory must be present, otherwise the build will stop:

def dir = project.file(new File('config'), PathValidation.DIRECTORY)

Now we can run the build and see from the output that the directory doesn't exist:

$ gradle -q build.gradle
...
* What went wrong:
A problem occurred evaluating root project 'chapter3'.
Cause: Directory '/samples/chapter3/config' does not exist.
...

We can also use the PathValidation argument to test if a File object is really a file and not a directory. Finally, we can check if the File object references an existing file or directory. If the file or directory doesn't exist, an exception is thrown and the build stops:

// Check file or directory exists.
def readme = project.file('README', PathValidation.EXISTS)

// Check File object is really a file.
def license = project.file('License.txt', PathValidation.FILE)

Using file collections

We can also work with a set of files or directories instead of just a single file or directory. In Gradle, a set of files is represented by the ConfigurableFileCollection interface. The nice thing is that a lot of classes in the Gradle API implement this interface.

We can use the files() method to define a file collection in our build script. This method is defined in the Project object we can access in our build script. The files() method accepts many different types of arguments, which makes it very flexible to use. For example, we can use String and File objects to define a file collection.

As with the file() method, paths are resolved relative to the project directory:

// Use String instances.
ConfigurableFileCollection multiple = files('README', 'licence.txt')

// Use File objects.
ConfigurableFileCollection userFiles = files(new File('README'), new File('INSTALL'))

// We can combine different argument types.
def combined = files('README', new File('INSTALL'))

But these are not the only arguments we can use. We can pass a URI or URL object, just as we could with the file() method:

def urlFiles = files(new URI('file:/README'), new URL('file:/INSTALL'))

We can also use an array, Collection, or Iterable object with filenames or another ConfigurableFileCollection instance as an argument:

// Use a Collection with file or directory names.
def listOfFileNames = ['src', 'test']
def mainDirectories = files(listOfFileNames)

// Use an array.
mainDirectories = files(listOfFileNames as String[])

// Or an implementation of the Iterable interface.
mainDirectories = files(listOfFileNames as Iterable)
// Combine arguments and pass another file collection.
def allDirectories = files(['config'], mainDirectories)

We can also use a closure or an instance of the Callable interface to define a list of files:

import java.util.concurrent.Callable

def dirs = files {
    [new File('src'), file('README')].findAll { it.directory }
}

def rootFiles = files(new Callable<List<File>>() {
    List<File> call() {
        [new File('src'), file('README'), file('INSTALL')].findAll { it.file }
    }
})

Finally, we can pass a Task object as an argument to the files() method. The output property of the task is used to determine the file collection. Let's look at the convert task we created in the previous chapter. This task has an outputs property with a single file, but this could also be multiple files or a directory. To get the file collection object in our build script, we simply pass the Task instance as an argument to the files() method:

task(convert) {
    def source = new File('source.xml')
    def output = new File('output.txt')

    // Define input file
    inputs.file source

    // Define output file
    outputs.file output

    doLast {
        def xml = new XmlSlurper().parse(source)
        output.withPrintWriter { writer ->
            xml.person.each { person ->
                writer.println "${person.name},${person.email}"
            }
        }
        println "Converted ${source.name} to ${output.name}"
    }
}

// Get the file collection from 
// the task outputs property.
def taskOutputFiles = files(convert)

It is also important to note that the file collection is lazy. This means the paths in the collection are not resolved when we define the collection. The paths in the collection are only resolved when the files are actually queried and used in the build script.

The ConfigurableFileCollection interface has useful methods to manipulate the collection, for example, we can use the + and - operators to add or remove elements from the collection, respectively:

// Define collection.
def fileCollection = files('README', 'INSTALL')

// Remove INSTALL file from collection.
def readme = fileCollection - files('INSTALL')

// Add new collection to existing collection.
def moreFiles = fileCollection + files(file('config', PathValidation.DIRECTORY))

To get the absolute path names for the elements in ConfigurableFileCollection, we can use the asPath property. The path names are separated by the operating system's path separator. On a Microsoft Windows operating system, the semi-colon (;) is used as a path separator, and in Linux or Mac OS X operating systems, the colon (:) is used. This means we can simply use the asPath property on any operating system and Gradle will automatically use the correct path separator:

task 'collectionPath' << {
    def fileCollection = files('README', 'INSTALL')
    println fileCollection.asPath
}

When we run the build script on Mac OS X, we get the following output:

$ gradle -q collectionPath
/samples/chapter3/README:/samples/chapter3/INSTALL

To get the File objects that make up the file collection, we can use the files property. We can also cast the collection to a list of File objects using the as keyword; if we know our collection is made up of just a single file or directory, then we can use the singleFile property to get the File object:

def fileCollection = files('README', [new File('INSTALL')])

// Get all elements as File objects.
def allFiles = fileCollection.files

// Or use casting with as keyword.
def fileObjects = fileCollection as File[]

def singleFileCollection = files('INSTALL')

// Get single file as File object.
def installFile = singleFileCollection.singleFile

Finally, we can apply a filter to our file collection with the filter() method. We pass a closure that defines which elements are to be in the filtered collection. The filtered collection is a live collection. This means that if we add new elements to the original collection, the filter closure is applied again for our filtered collection. In the following example we have the filterFiles task, where we define a file collection of two files with the names INSTALL.txt and README. Next, we define a new file collection with a filter that contains all files that have the filename extension .txt. This collection is a live, filtered collection because when we add a new file to the original collection, the filtered collection is also updated:

task 'filterFiles' << {
    def rootFiles = files('INSTALL', 'README')

    // Filter for files smaller than 5KB
    def smallFiles = rootFiles.filter {
        it.name.endsWith 'txt'
    }

    rootFiles = rootFiles + files('LICENSE.txt')
    
    // smallFiles now contains 2 files:
    // INSTALL and LICENSE
}

Working with file trees

In Gradle we can also work with file collections organized as a tree, for example, a directory tree on a disk or hierarchical content in a ZIP file. A hierarchical file collection is represented by a ConfigurableFileTree interface. This interface extends the ConfigurableFileCollection interface that we saw earlier.

To create a new file tree, we use the fileTree() method in our project. We can use several ways to define the file tree.

Note

If we don't provide a base directory, the current project directory is used as the base directory of the file tree.

We can use the include and includes properties and methods to define a matching pattern to include a file (or files) in the file tree. With the exclude and excludes properties and methods, we can use the same syntax to exclude a file or multiple files from the file tree. The matching pattern style is described as an ANT-style matching pattern because the ANT build tool uses this style to define a syntax for matching filenames in file trees. The following patterns can be used:

  • * to match any number of characters
  • ? to match any single character
  • ** to match any number of directories or files

The following example demonstrates how we can create a file tree:

// Create file tree with base directory 'src/main'
// and only include files with extension .java
def srcDir = fileTree('src/main').include('**/*.java')

// Use map with arguments to create a file tree.
def resources = fileTree(dir: 'src/main', excludes: ['**/*.java, '**/*.groovy'])

// Create file tree with project directory as base
// directory and use method includes() on tree
// object to include 2 files.
FileTree base = fileTree()
base.includes ['README', 'INSTALL']

// Use closure to create file tree.
def javaFiles = fileTree {
    from 'src/main/java'
    exclude '*.properties'
}

To filter a file tree, we can use the filter() method like we do with file collections, but we can also use the matching() method. We pass a closure to the matching() method or an instance of the org.gradle.api.tasks.util.PatternFilterable interface. We can use the include, includes, exclude, and excludes methods to either include or exclude files from the file tree:

def sources = fileTree {
    from 'src'
}

def javaFiles = sources.matching {
    include '**/*.java'
}

def nonJavaFiles = sources.matching {
    exclude '**/*.java'
}

def nonLanguageFiles = sources.matching {
    exclude '**/*.scala', '**/*.groovy', '**/*.java'
}

def modifiedLastWeek = sources.matching {
    lastWeek = new Date() - 7
    include { FileTreeElement file ->
        file.lastModified > lastWeek.time
    }
}

We can use the visit() method to visit each tree node. We can check if the node is a directory or a file. The tree is then visited in breadth-wise order:

FileTree testFiles = fileTree(dir: 'src/test')

testFiles.visit { FileVisitDetails fileDetails ->
    if (fileDetails.directory) {
        println "Entering directory ${fileDetails.relativePath"
    } else {
        println "File name: ${fileDetails.name}"
    }
}

def projectFiles = fileTree()

projectFiles.visit(new FileVisitor() {
    void visitDir(FileVisitDetails details) {
        println "Directory: ${details.path}"
    }

    void visitFile(FileVisitDetails details) {
        println "File: ${details.path}, size: ${details.size}"
    }
})

Copying files

To copy files in Gradle, we can use the Copy task. We must assign a set of source files to be copied and the destination of those files. This is defined with a copy spec. A copy spec is defined by the org.gradle.api.file.CopySpec interface. The interface has a from() method we can use to set the files or directories we want to copy. With the into() method we specify the destination directory or file.

The following example shows a simple Copy task called simpleCopy with a single source directory src/xml and a destination directory definitions:

task simpleCopy(type: Copy) {
    from 'src/xml'
    into 'definitions'
}

The from() method accepts the same arguments as the files() method. When the argument is a directory, all files in that directory—but not the directory itself—are copied to the destination directory. If the argument is a file, then only that file is copied.

The into() method accepts the same arguments as the file() method. To include or exclude files, we use the include() and exclude() methods of the CopySpec interface. We can apply the ANT-style matching patterns just like we do with the fileTree() method.

The following example defines a task with the name copyTask and uses the include() and exclude() methods to select the set of files to be copied:

def getTextFiles = {
    '**/*.txt'
}

def getDestinationDir = {
    file('dist')
}

task copyTask(type: Copy) {
    // Copy from directory
    from 'src/webapp'

    // Copy single file
    from 'README.txt'

    // Include files with html extension.
    include '**/*.html', '**/*.htm'

    // Use closure to resolve files.
    include getTextFiles

    exclude 'INSTALL.txt'

    // Copy into directory dist
    // resolved via closure.
    into getDestinationDir
}

Another way to copy files is with the Project.copy() method. The copy() method accepts a CopySpec interface implementation, just like the Copy task. Our simpleCopy task could also have been written like this:

task simpleCopy << {
    copy {
        from 'src/xml'
        into 'definitions'
    }
}

Renaming files

With the rename() method of the CopySpec interface, we can rename files as they are copied. The method accepts a closure argument, with the closure argument being the name of the file. We can return a new filename to change the filename or return a null value to keep the original filename:

task copyAndRename(type: Copy) {
    from 'src'

    rename { String fileName ->
        if (fileName.endsWith('txt')) {
            String original = fileName
            String originalWithoutExtension = original - '.txt'
            originalWithoutExtension + '.text'
        }
    }

    into 'dist'
}

Besides using a closure to rename files during the copy action, we can use a String value as a regular expression or a java.util.regexp.Pattern object as a regular expression. We also provide the replacement String value when a filename matches the pattern. If the regular expression captures groups, we must use the $1 syntax to reference a group. If a file doesn't match the regular expression, the original filename is used:

task copyAndRenameRegEx(type: Copy)

copyAndRenameRegEx {
    from 'src'

    // Change extension .txt to .text.
    rename '(.*).txt', '$1.text'

    // Rename files that start with sample-
    // and remove the sample- part.
    rename ~/^sample-(.*)/, '$1'

    into 'dist'
}

Filtering files

To filter files we must use the filter() method of the CopySpec interface. We can pass a closure to the filter() method. Each line of the file is passed to the closure, and we must return a new String value to replace that line. Besides a closure, we can pass an implementation of the java.io.FilterReader interface. The ANT build tool already has several filter implementations that we can use in a Gradle build. We must import the org.apache.tools.ant.filters.* package to access the ANT filters. We can pass along the properties for a filter with this method invocation:

import org.apache.tools.ant.filters.*

task filterFiles(type: Copy) {
    from 'src/filter.txt'
    into 'dist'
    
    // Closure to replace each line.
    filter { line ->
        "I say: $line"
    }
    
    // Use ANT filter ReplaceTokens.
    filter(ReplaceTokens, tokens: [message: 'Hello'])
}

We set the following contents for src/filter.txt:

@message@ everyone

If we execute the filterFiles task, we get the resulting filter.txt file in the dist directory:

I say: Hello everyone

We can use the expand() method to expand property references in a file. The file is transformed with a groovy.text.SimpleTemplateEngine object, which is part of Groovy. Properties are defined as $property or ${property} and we can even include code such as ${new Date()} or ${value ? value : 'default'}.

In the following example we use the expand() method to replace the property languages in the file src/expand.txt:

task expandFiles(type: Copy) {from 'src/expand.txt'
    into 'dist'

    // Set property languages
    expand languages: ['Java', 'Groovy', 'Scala']

    rename 'expand.txt', 'expanded.txt'
}

We execute the expandFiles task with the following contents for src/expand.txt:

A list of programming languages: ${languages.join(', ')}

Then, we get the following new contents in the file dist/expanded.txt:

A list of programming languages: Java, Groovy, Scala.

Archiving files

To create an archive file, we can use the Zip, Tar, Jar, War, and Ear tasks. To define the source files for the archive and the destination inside the archive files, we use a CopySpec interface, just like with copying files. We can use the rename(), filter(), expand(), include(), and exclude() methods in the same way. So, we don't have to learn anything new; we can use what we have already learned.

To set the filename of the archive, we use any of these properties: baseName, appendix, version, classifier, and extension. Gradle will use the following pattern to create a filename: [baseName]-[appendix]-[version]-[classifier].[extension]. If a property is not set, then it is not included in the resulting filename. To override the default filename pattern, we can set the archiveName property and assign our own complete filename, which is used for the resulting archive file.

In the following example, we create a ZIP archive with the archiveZip task. We include all the files from the dist directory and put them in the root of the archive. The name of the file is set by the individual properties that follow Gradle's pattern:

task archiveDist(type: Zip) {
    from 'dist'

    // Create output filename.
    baseName = 'dist-files'
    appendix = 'archive'
    extension = 'zip'
    version = '1.0'
    classifier = 'sample'
}

When we run the archiveDist task, a new file called dist-files-archive-1.0-sample.zip is created in the root of our project. To change the destination directory of the archive file, we must set the destinationDir property. In the following example, we set the destination directory to build/zips. We also put the files in a files directory inside the archive file with the into() method. The name of the file is now set by the archiveName property:

task archiveFiles(type: Zip) {
    from 'dist'
    
    // Copy files to a directory inside the archive.
    into 'files'

    // Set destination directory.
    destinationDir = file("$buildDir/zips")

    // Set complete filename.
    archiveName = 'dist-files.zip'
}

To create a TAR archive with the optional gzip or bzip2 compression, we must use the tarFiles task. The syntax is the same as the task for type Zip, but we have an extra property compression that we can use to set the type of compression (gzip, bzip2) we want to use. If we don't specify the compression property, no compression is used to create the archive file.

In the following example, we create a tarFiles task of type Tar. We set the compression property to gzip. After running this task, we get a new file called dist/tarballs/dist-files.tar.gz:

task tarFiles(type: Tar) {
    from 'dist'

    // Set destination directory.
    destinationDir = file("$buildDir/tarballs")

    // Set filename properties.
    baseName = 'dist-files'
    extension = 'tar.gz'

    compression = Compression.GZIP // or Compression.BZIP2
}

The Jar, War, and Ear task types follow the same pattern as the Zip and Tar task types. Each type has some extra properties and methods to include files specific for that type of archive. We will see examples of these tasks when we look at how we can use Gradle in Java projects.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.176.80