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.

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

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: increment<Classifier>Version - 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


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容