41
Improving your Gradle builds Peter Ledbrook e: [email protected] w: http://www.cacoethes.co.uk t: @pledbrook

Improving your Gradle builds

Embed Size (px)

Citation preview

Improving your Gradle builds

Peter Ledbrook

e: [email protected]: http://www.cacoethes.co.ukt: @pledbrook

“Safety” in build tools

"Straight Razor" by Horst.Burkhardt - Own work. Licensed under CC BY-SA 3.0 via Wikimedia Commons

Gradle is often treated like a cut-throat razor: unsafeCode == spaghetti builds (allegedly)Some people think it’s better to limit build authors to configuration onlyIs this a good comparison though?

A better analogy

by Noel C. Hankamer - Creative Commons 2.0

Not all jobs are the same for a workmanNeed the right tools for the jobSome tools are dangerous - so you learn to use them Power drills, heavy duty knives, etc.

Gradle

Yes, you can create spaghetti builds, but…

“A bad workman blames his tools”

So how do you create maintainable builds?

First, understand the Gradle model

Gradle is an API

Project

Task

Repository Dependency

Configuration Source set (Java)

This is just part of the Gradle model, which allows you to model your own build processes.Show Gradle DSL and API references if possible: http://www.gradle.org/doc/latest/…

A basic Java build

apply plugin: "java"

version = "1.0"

repositories { jcenter() } dependencies { compile "asm:asm-all:3.3.1", "commons-io:commons-io:1.4" }

compileJava.options.incremental = true

An instance of Task

A Repository

Everything from Project

• project.tasks

• project.configurations

• project.repositories

• project.dependencies

• project.sourceSets (added by Java plugin)

All the model elements are available from the projectThe project itself can be accessed via the `project` property

The three phases

Initialisation

Configuration

Execution

Only relevant to multi-project builds

Configures tasks and builds the task graph

Executes the necessary tasks based on the task graph

Beyond the model that defines the build, this is what happens when you actually run that build

At execution

Y

X

Z

=>

Z

X Y

W

V

Linear execution orderTask graph

W

V

During configuration Gradle builds a Directed Acyclic Graph (DAG) of tasksBefore execution, the graph is turned into an execution orderThere is no distinction between targets and tasks like in Apache Ant

Applying that knowledge

Beware the config phase

Beware the config phase

• Configuration always happens

- even just running grade tasks!

- this will change soon(ish)

• Avoid expensive work

Profile your build!

gradle --profile <taskName>

This generates a report with timings for the different phases of each project in the build

Example

apply plugin: "java"

...

jar { version = new URL("http://buildsvr/version").text }

Evaluated every build!

(This will change soon in Gradle)

Potentially unreliable network call *every* time the build runsNo matter what tasks are executed

Gradle will perform configuration on-demand in the not too distant future, reducing the time cost of the configuration phase

Remember

task doSomething << { println "Doing it!" }

task customJar(type: Jar) { archivePath = customJarPath

doLast { println "Created the JAR file" } } Execution

Execution

Configuration

Consider always using `doLast()` instead of `<<` to avoid confusion between configuration and implicit execution

Project properties

Project properties

• Useful for build parameterisation

• Equivalent to global variables

- Same problems!

• Provide sensible defaults

• Assume values are strings

Default values

ext { jarFilename = project.hasProperty("jarFilename") ? jarFilename : "myArchive" }

task customJar(type: Jar) { archivePath = file("$buildDir/$jarFilename")

doLast { println "Created the JAR file: ${archivePath}" } }

Check property exists first

Default value

Values can be overridden via-P cmd line optiongradle.properties

Both only support strings

Prefer task properties

task customClean(type: Delete) { delete "$buildDir/$jarFilename" }

task customClean(type: Delete) { delete customJar.archivePath }

Use the path defined by the task

Don’t let project properties litter your buildIf the JAR task stops using the project property, `customClean` will continue to work

Refactoring

Custom tasksPrefer

task doIt(type: MyTask)

class MyTask extends DefaultTask { @TaskAction def action() { println "Hello world!" } }

task doIt << { println "Hello world!" } }

over

More verbose, but easier to maintainEasier to incorporate properties in custom task classesEasier to migrate the task implementation out of the build file

Custom tasks

Where the task class goes:

1. In build.gradle

2. In buildSrc/src/groovy/…

3. In a JAR file

IoC in tasks

class MyTask extends DefaultTask { File filePath = project.myArchivePath

@TaskAction def action() { println filePath.text } }

Don’t pull values from the environment

IoC in tasks

task doIt(type: MyTask) { filePath = project.myArchivePath }

Leave configuration in the build fileEffectively Inversion of Control - values should always be injected

Last one…

Consider breaking into multiple smaller projects

Understand your build

Task order vs dependency

task functionalTest(type: Test, dependsOn: test)

task functionalTest(type: Test) { mustRunAfter test }

Do the functional tests require the unit tests to run?

Or is it just the order that counts?

Leverage source setsInstead of

configurations { clientCompile }

task compileClient(type: JavaCompile) { classpath ... include "src/main/java/**/client/*" }

// Don't forget the test tasks and dependencies! ...

Leverage source setsUse source sets

sourceSets { client { java { srcDirs file("src/main/java") include "src/main/java/**/client/*" } } }

Get:• clientCompile, clientRuntime configurations• compileClientJava task

Use incremental build

class SignJar extends DefaultTask { @InputFiles FileCollection jarFiles @OutputDirectory File outputDir

File keystore String keystorePassword String alias String aliasPassword

@TaskAction void sign() { ... } }

Changes to these input files or output force the

task to run again

Plugins

• Encapsulate your conventions

• Package common features

• Lightweight alternative:

- apply from: "modules/check.gradle"

Know your users

Questions to ask

• Do you have only one type of user?

• What things can each type of user do?

• What do they want from the build?

• What should they see?

Example

compile

test

package/install

publishproject

lead only

Lazybones project

If the project is published, require credentialsFail fastOther users don’t require credentials

Interrogate task graph

gradle.taskGraph.whenReady { graph -> if (graph.hasTask(":lazybones-app:uploadDist")) { verifyProperty(project, 'repo.url') verifyProperty(project, 'repo.username') verifyProperty(project, 'repo.apiKey')

uploadDist.repositoryUrl = project.'repo.url' uploadDist.username = project.'repo.username' uploadDist.apiKey = project.'repo.apiKey' } }

Only check for credentials if user is trying to publish:

Only fail fast if `uploadTask` will be executedFail slow would require integration tests to run - adding minutes to deployment

Other uses

• Include class obfuscation conditionally

• Limit visibility of tasks based on role

• Update version based on ‘release’ task

Summary

• Understand the Gradle model

• Keep refactoring

• Understand your build

• Know your users

Thank you!

e: [email protected]: http://www.cacoethes.co.ukt: @pledbrook