Automation Evolved – Anatomy of a Task – Artifact Dependency Management



Automation Evolved – Anatomy of a Task – Artifact Dependency Management

2 2


GradlePresentationForHJUG

This is the Presentation on basic Gradle that was given to the Houston Java Users Group on May 29, 2013

On Github AdamRoberts / GradlePresentationForHJUG

Automation Evolved

Open Source by Gradleware (Apache 2.0 license)

Why another build tool

Build Enabler

Defaults to Maven conventions Full integration with Ant DSL in Groovy Flexible dependency management Automation as well as build When setting up a build using gradle it defaults to using the Maven directory structure. But unlike maven gradle allows the source directories to be reconfigured to whatever structure you desire. Gradle has full support of ant builds, it can import and use custom ant tasks, it can import a build file to call or modify the targets. Gradle builds can also be called from an Ant build file. this means that it is easy to slowly convert an ant build to a gradle build one target at a time. So if Maven and Ant are to great why do we need a new tool, one reason is because xml sucks Rather than declaring you build steps in xml, Gradle uses Groovy as a DSL. This allows gradle builds to be easier to read and write than their xml counterparts. While having the full power of groovy means that custom build steps become trivial to implement in the build rather than requireing a new custom task be writen. Gradle has a powerful transitive dependency resolution system that allows the use of maven or ivy remote repositories. Or local file repositories, or just direct file references. Ant and Maven are custom designed for a build process. Gradle is designed to automate tasks, it has full support for automating a build procesws, but it also has an expanding role in automation of test suites and even some deployment tools.

Gradle's Anatomy

  • Projects
  • Tasks
  • Dependency Resolution
  • Lifecycle

Gradle Projects

  • "build.gradle" configures the project
  • Project provides the base DSL
  • Build steps are performed by Tasks
  • Plugins provide preconfigured tasks
A completely empty file that is named "build.gradle" is a valid build configuration. It doesn't do anything, but gradle will execute without errors. The project class provides nouns to configure the project in a declarative manner. nouns such as "configuration", "task", "file", "dependencies", "artifacts". The project class also provides verbs to make it easier to create custom imperative behavior. verbs such as "copy", "delete", "javaexec", "exec" You can add a single task manually or you can apply a plugin Plugins add a set of preconfigured interrelated tasks A task has an action that it performs to complete its portion of the build. Tasks are arranged in an Directed acyclic graph that ensures tasks are executed in the correct order.

Anatomy of a Task

Tasks perform an action
class GreetingTask extends DefaultTask {
    @TaskAction
    def greet() {
        println 'Hello World!'
    }
}
Adding the task to you build
task hello(type: GreetingTask)
Task output
$ gradle hello
:hello
Hello World!

BUILD SUCCESSFUL
A task has an action that it performs to complete its portion of the build. Although the action is technicaly optional, a task without without an action does not provide much value. The actual class definition can be created inline in your buid script, or it can be in a seperate script file that is imported, or it can be compiled to bytecode in a jar on the classpath. The named task instance does need to be created in your build script, although the majority of your build tasks will be created by applying plugins rather than manually.

Anatomy of a Task

Tasks dependencies control execution order
task helloAgain(type: GreetingTask, dependsOn: hello)
Task output
$ gradle helloAgain
:hello
Hello World!
:helloAgain
Hello World!

BUILD SUCCESSFUL
Tasks use dependencies on other tasks to control the order of the build. These dependencies create a Directed acyclic graph that ensures the build will end. If you define a loop of dependencies gradle will exclude all tasks in the loop from execution.

Anatomy of a Task

Add functions to "Do First"
task redundantGreeting(type: GreetingTask) {
    doFirst {
        println 'Hello Hello Hello'
    }
}
Task output
$ gradle redundantGreeting
:redundantGreeting
Hello Hello Hello
Hello World!

BUILD SUCCESSFUL
The list of functions in the "doFirst" can be empty. Any functions in the list will be executed prior to the execution of the task action. Currently the list is executed in the order that the functions are added, however depending on this behavior would lead to a fragile build, if they are order dependent make them an independent task and use dependsOn.

Anatomy of a Task

Add functions to "Do Last"
task helloAndGoodBye(type: GreetingTask) {
    doLast {
        println 'Goodbye World'
    }
}
Task output
$ gradle helloAndGoodbye
:helloAndGoodBye
Hello World!
Goodbye World

BUILD SUCCESSFUL
"doLast" behaves exactly the same as the "doFirst" list except they all execute after the task action. "doLast" does have an additional DSL syntax in the "%lt;%lt;" operator.

Anatomy of a Task

Tasks can be configurable
class ConfigurableGreetingTask extends DefaultTask {
    def person = 'World'
    @TaskAction
    def greet() {
        println "Hello ${person}!"
    }
}

task helloDuke(type: ConfigurableGreetingTask) {
    person = 'Duke'
}
Task output
$ gradle helloDuke
:helloDuke
Hello Duke!

BUILD SUCCESSFUL
This task using a public member its configuration setup is a gross oversimplification of how most tasks are configured as most tasks belong to a family of tasks that share a common configuration object. But this presentation's goal is not custom task development, it is just to have an understanding of how the gradle lifecycle works so you can make better use of the tool.

Anatomy of a Task

The DSL is flexible
task version1(type: ConfigurableGreetingTask) {
    person = 'Duke'
    doFirst() {
        println 'Hello Everyone!'
    }
    doLast() {
        println "I hope you are having a good day ${person}"
    }
}
Identical
task version2(type: ConfigurableGreetingTask)
version2.person = 'Duke'
version2.doFirst() {
    println 'Hello Everyone!'
}
version2 << {
    println "I hope you are having a good day ${person}"
}
The gradle dsl is designed to be a truely fluent language, this also means that there are multiple ways to accomplish the same configuration.

Anatomy of a Task

A task can be "finalized By" another task (incubating)
task cleanUp {
    doFirst() { println 'Cleanup executed' }
}
task fails {
    finalizedBy cleanUp
    doFirst() { throw new RuntimeException() }
}
Task output
$ gradle fails
:fails FAILED
:cleanUp
Cleanup executed

FAILURE: Build failed with an exception.
...

Artifact Dependency Management

Configurations
configurations {
    compile
    runtime {
        description = "Used at runtime but should not be inherited"
        extendsFrom compile
    }
}
configurations.compile {
    description = 'You can access an already declared configuration'
}
Creating a new configuration is easy You can extend an existing configuration to inherit the dependencies You can easily access an already created configuration

Artifact Dependency Management

Dependency Declarations
dependencies {
    compile 'org.slf4j:slf4j-api:1.7.5'
    runtime 'org.slf4j:slf4j-log4j12:1.7.5'
    runtime group: 'org.apache', name: 'tomcat', version: '7.0.34', ext: 'zip'
    compile files('libs/a.jar', 'libs/b.jar')
    compile("com.sun.jersey:jersey-json:1.12") {
        exclude group: 'stax', module: 'stax-api'
    }
}
slf4j will be accessed from a declared repository and made available to any task that uses the compile dependency when the configuration's settings need to be overriden for a single artifact they can be File dependencies are never transitive now that we have used the configurations you can see how each one cooresponds to a seperate classpath

Artifact Dependency Management

Repository Setup
repositories {
    mavenCentral()
    maven { url "http://repo.mycompany.com/maven2" }
    ivy { url "http://repo.mycompany.com/repo" }
    flatDir { dirs 'lib1', 'lib2' }
    localRepository { dirs 'lib' }
}

Plugins

To use a plugin:
apply plugin: 'example-plugin'
The plugin then:
configurations {
    pluginConf
}
class PluginTask extends DefaultTask {
    @TaskAction
    def action() {
        def deps = project.configurations.pluginConf.allDependencies
        ... do something ...
    }
}
task pluginTask(type: PluginTask)

The Java Plugin

Figure out how to make the image either bigger or scrollable

Project Extensions

Source Sets
apply plugin: 'java'

sourceSets {
    newCode
}
New Tasks
$ gradle tasks --all
:tasks

------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------
...
newCodeClasses - Assembles binary 'newCode'.
...
    compileNewCodeJava - Compiles source set 'newCode:java'.
    processNewCodeResources - Processes source set 'newCode:resources'.
...
BUILD SUCCESSFUL
When a "most" language plugins are applied, they modify the project object to add a "sourcesets" noun to the DSL.

Built in plugins *

  • Language plugins - Java, Scala, Groovy, Antlr, cpp
  • Integration plugins - war, jetty, ear, osgi, maven
  • Code Quality - checkstyles, codenarc, findbugs, jdepend, pmd
  • Development Support - sonar, eclipse, idea
Plugins provide tasks to accomplish the specific build goals Plugins are used to add a set of interrelated tasks A task has an action that it performs to complete its portion of the build.

Build Lifecycle

  • Initialization - find all the files
  • Configuration - parse the build file's configuration
  • Execution - execute the task graph
During the initialization phase Gradle locates every build file (build.gradle, settings.gradle, gradle.properties, subprojects) and instantiates a project instance for every build. During the configuration phase every build file is parsed, and the configuration is executed. During the Execution phase the task graph is executed up to the task specified on the command line (or default)
Manual Webapp Setup
jettyRun {
    copy {
        from 'src/bdd/resources/web.xml'
        into "${buildDir}/webapp/WEB-INF"
    }
}
Fixed
jettyRun {
    doFirst {
        copy {
            from 'src/bdd/resources/web.xml'
            into "${buildDir}/webapp/WEB-INF"
        }
    }
}
The common mistake is the copy will always occur, even if the jettyRun task is not executed.

Examples

Presentation by Adam Roberts for the Houston Java Users Group