Gradle Build Script Essentials

Build Blocks

There are 3 basic building blocks for a Gradle build:

  • projects
  • tasks
  • properties

Each build has at least one project, each project contains one or more tasks. Projects and tasks expose the properties that can be used to control the build.

Gradle Build Script Essentials_第1张图片
picture from *Gradle in Action*

Gradle applies the principles of domain-driven design (DDD) to model its own domain-building software. As a consequence, projects and tasks have a direct class representation in Gradle’s API.

Projects

A project represents a component you are trying to build (such as a jar), or a goal you are trying to achieve(for example, deploy an app).
There is a build.gradle file, each build script defines at least one project.
There is a class org.gradle.api.Project, when starting the build, Gradle will instantiate it based on the build.gradle file.

Gradle Build Script Essentials_第2张图片
Gradle Project API

A project can create new tasks, add dependencies and configurations, and apply plugins and other build scripts.

In a multi-project application, each part can be represented as a Gradle project and have its own build.gradle script file.

Tasks

task actions: an action defines an atomic unit of work that's executed when the task is run.
task dependencies: sometimes a task needs another task to run first.

Gradle Build Script Essentials_第3张图片
Gradle task API

Properties

Project and Task provide some properties through getter and setter methods.
You can also define some extra properties to be used by using ext namespace.

project.ext.myprop = 'myValue'

You can also define extra properties by gradle.properties file.

Example -- Manage the Project Version

Add Actions to Existing Tasks

build.gradle

version = '0.1-SNAPSHOT'
task printVersion {
  doFirst {
    println "Before reading the project version"
  }
  doLast {
    println "Version: $version"
  }
}
printVersion.doFirst { println "First action" }
printVersion << { println "Last action" }

result

> gradle build
First action
Before reading the project version
Version: 0.1-SNAPSHOT
Last action

Add Group and Description Properties

You can change printVersion to below code

task printVersion(group: 'versioning', description: 'Prints project version.') << {
  logger.quiet "Version: $version" 
}

or

task printVersion { 
  group = 'versioning'
  description = 'Prints project version.'
  doLast { 
    logger.quiet "Version: $version"
  }
}

The first one inits the properties when creating a task, the second inits them by the setter.

When running gradle tasks
gradle tasks
:tasks
...
Versioning tasks
printVersion - Prints project version.
...

Create POGO(Plain-old Groovy Objects) and Add a Configuration Block for Task

version.properties

major=0
minor=1
release=false

build.gradle

ext.versionFile = file('version.properties')

task loadVersion {
  version = readVersion()
}

class ProjectVersion {
  Integer major
  Integer minor
  Boolean release

  ProjectVersion(Integer major, Integer minor) {
    this.major = major
    this.minor = minor
    this.release = Boolean.FALSE
  }

  ProjectVersion(Integer major, Integer minor, Boolean release) {
    this(major, minor)
    this.release = release
  }

  @Override
  String toString() {
    "$major.$minor${release? '' : '-SNAPSHOT'}"
  }
}


task makeReleaseVersion(group: 'versioning', description: 'Makes project a release version') {
  doLast {
    version.release = true
    ant.propertyfile(file: versionFile) {
      entry(key: 'release', type: 'string', operation: '=', value: 'true')
      }
  }
}

ProjectVersion readVersion() {
  logger.quiet 'Reading the version file'
  Properties versionProps = new Properties()
  versionFile.withInputStream{
    stream -> versionProps.load(stream)
  }

  new ProjectVersion(versionProps.major.toInteger(), versionProps.minor.toInteger(), versionProps.release.toBoolean())
}

task printVersion << {
  logger.quiet "Version: $version"
}

You may find that the task loadVersion is a little different from the tasks before.

task loadVersion {
  version = readVersion()
}

There is no action defined in this task, nor the left shift operator.
This task is called a task configuration
Task configuration blocks are always executed before task actions. We can take a look at Gradle's build lifecycle.

Gradle Build Script Essentials_第4张图片
gradle lifecycle

Whenever you execute a build, three distinct phases are run: initialization, configuration, execution.
During the initialization phase, Gradle creates a Project instance for your project. In the context of multi-project build, this phase is important. Depending on which project you are executing, Gradle figures out which dependencies need to be added during the build.
During the configuration phase, Gradle constructs a model representation of the tasks that will take part in the build. The incremental build feature determines if any of the tasks in the model are required to be run. This phase is perfect for setting up the configuration that’s required for your project or specific tasks. (Even if you just run gradle tasks, any configuration code will run.)
During the execution phase, the tasks are executed in correct order.

If you run

$ gradle printVersion

The output would be

Reading the version file.
:printVersion
Version: 0.1-SNAPSHOT

If you run

$ gradle makeReleaseVersion

The script will use ant to change the property file, which turns the release to be 'true'.
And then

$ gradle printVersion

The output would be

Reading the version file.
:printVersion
Version: 0.1

Writing and Using Custom Tasks

Custom tasks consist of two components:

  1. the custom task class that encapsulates the behavior of your logic
  2. the actual task that provides the value for the properties exposed by the task class to configure the behavior

The Custom Task Class

class ReleaseVersionTask extends DefaultTask {
  @Input Boolean release
  @OutputFile File destFile
  
  ReleaseVersionTask() {
    group = 'versioning'
    description = 'make a project release version'
  }

  @TaskAction  // Annotaion declares a method to be executed
  void start() {
    project.version.release = true
    ant.propertyFile(file: destFile) {
      entry(key: 'release', type: 'string', operation: '=', value: 'true')
    }
  }
}

The Actual Task

task makeReleaseVersion(type: ReleaseVersionTask) {
  release = version.release
  destFile = versionFile
}

And another advantage of using custom task class is that it can be reused in another task.

Using Built-in Task Types

Suppose there are some dependencies in releasing a project:
makeReleaseVersion->war->createDistribution->backupReleaseVersion->release

There are two built-in task types which derived from DefaultTask: Zip and Copy

Gradle Build Script Essentials_第5张图片
*Gradle in Action*
class ReleaseVersionTask extends DefaultTask {
  @Input Boolean release
  @OutputFile File destFile

  ReleaseVersionTask() {
    group = 'versioning'
    description = 'Makes project a release version'
  }

  @TaskAction
  void start() {
    project.version.release = true
    ant.propertyfile(file: destFile) {
      entry(key: 'release', type: 'string', operation: '=', value: 'true')
    }
  }
}

task makeReleaseVersion(type: ReleaseVersionTask) {
  release = version.release
  destFile = versionFile
}

task createDistribution(type: Zip, dependsOn: makeReleaseVersion) {
  from war.outputs.files

  from(sourceSets*.allSource) {
    into 'src'
  }

  from(rootDir) {
    include versionFile.name
  }
}

task backupReleaseDistribution(type: Copy) {
  from createDistribution.outputs.files
  into "$buildDir/backup"
}

task release(dependsOn: backupReleaseDistribution) <<{
  logger.quiet 'Releasing the project...'
}

As you can see, you can declare a dependency between two tasks by using "dependsOn" explicitly (task createDistribution). Furthermore, you can use an output for an input for another task to infer the dependency(task backupReleaseDistribution).
The following directory tree shows the relevant artifacts generated by the build:

Gradle Build Script Essentials_第6张图片
Directory Tree

Task Rules

Let's see two similar tasks

task incrementMajorVersion( group: 'versioning', description: 'Increments project major version.') << {
  String currentVersion = version.toString() 
  ++version.major
  String newVersion = version.toString() 
  logger.info "Incrementing major project version: $currentVersion ->  $newVersion"
  ant.propertyfile(file: versionFile) { 
    entry(key: 'major', type: 'int', operation: '+', value: 1)
  }
}
task incrementMinorVersion(group: 'versioning', description: 'Increments project minor version.') << {
  String currentVersion = version.toString() 
  ++version.minor
  String newVersion = version.toString() 
  logger.info "Incrementing major project version: $currentVersion ->  $newVersion"
  ant.propertyfile(file: versionFile) { 
    entry(key: 'minor', type: 'int', operation: '+', value: 1)
  }
}

They are very similar, thus we can use task rules to replace these two tasks, which executes specific logic based on a task name pattern.
The pattern consists of two parts: the static portion of the task name and a placeholder.

tasks.addRule("Pattern: incrementVersion - increments the project version classifier") { String taskName ->
  if (taskName.startsWith('increment') && taskName.endsWith('Version')) {
    task(taskName) << {
      String classifier = (taskName - 'increment' - 'Version').toLowerCase()
      String currentVersion = version.toString()

      switch(classifier) {
        case 'major': ++version.major
                      break
        case 'minor': ++version.minor
                      break
        default: throw new GradleException('Invalid version type')
      }

      String newVersion = version.toString()

      logger.info "Increment $classifier project version: $currentVersion - > $newVersion"

      ant.propertyfile(file: versionFile) {
        entry(key: classifier, type: 'int', operation: '+', value: '1')
      }
    }
  }
}

Hooking into the Build Lifecycle

As a build script developer, you’re not limited to writing task actions or configuration logic, which are evaluated during a distinct build phase. Sometimes you’ll want to execute code when a specific lifecycle event occurs.

There are two ways to write a callback to build lifecycle events:

  1. within a closure
  2. with an implementation of a listener interface provided by the Gradle API.

Here is an example of build lifecycle hooks


Gradle Build Script Essentials_第7张图片
Examples of build lifecycle hook

Hook with a closure

gradle.taskGraph.whenReady { TaskExecutionGraph taskGraph ->
  if (taskGraph.hasTask(release)) {
    if (!version.release) {
      version.release = true
      ant.propertyfile(file: versionFile) {
        entry(key:'release', operation: '=', type: 'string', value: 'true')
      }
    }
  }
}

Hook with a listener

It can be done in two steps:

  1. Implement the specific Listener interface
  2. Register the listener implementation with the build

In this example, the listener interface to be implemented is TaskExecutionGraphListener

class ReleaseVersionListener implements TaskExecutionGraphListener {
  final static String releaseTaskPath = ':release'

  @override
  void graphPopulated(TaskExecutionGraph taskGraph) {
    if (taskGraph.hasTask(releaseTaskPath)) {
      List tasks = taskGraph.allTasks
      Task releaseTask = tasks.find {it.path == releaseTaskPath}
      Project project = releaseTask.project

      if (!project.version.release) {
        project.version.release = true
        project.ant.propertyfile(file: project.versionFile) {
          entry(key:'release', type: 'string', operation: '=', value: 'true')
        }
      }
    }
  }
}

And you need to register the implemented listener

gradle.taskGraph.addTaskExecutionGraphListener(new ReleaseVersionListener())

Summary

Every gradle build scripts consist of two components: one or more projects and tasks.
At runtime, Gradle creates a model from the build definition, stores it in a memory, and make it accessible for you to access through methods.
As an example, we implement build logic to control the release version of the project, which stored in an external property file. We can add simple tasks to the build script, and define build logic in tasks actions. Every task is derived from the org.gradle.api.DefaultTask.
Then, we learned the build lifecycle and the execution order of its phases. There are task actions and task configurations in Gradle. Task actions are executed in the execution phase, while task configurations are executed in the configuration phase. Any other code defined outside of a task action is considered a configuration and therefore executed beforehand during the configuration phase.
Then we learned to structure the build scripts. Such as writing our own custom tasks to hold the build logic, moving the compilable code into buildsrc folder.
Finally, you can register build life-cycle hooks that execute code whenever the targeted event is fired.

你可能感兴趣的:(Gradle Build Script Essentials)