Table of Contents generated with DocToc
- Gradle course with Pluralsight
- Build by convention
- Written in Groovy, it's a Groovy DSL
- Dependency management with Ivy or Maven
- Supports multi-project builds
- Can build things other than Java projects such as JavaScript or C++
- Highly customizable
- Declarative Build Language (expresses intent)
- We tell Gradle what we would like to happen, not how
- Makes build scripts easier to read than Ant or Maven xml
- Maintainable
$ brew cask install java
$ brew install gradle
- Build file is typically named
build.gradle
- Build file contains tasks, and
- plugins
- dependencies
Save as build.gradle
:
task hello {
doLast {
println "Hello, Gradle"
}
}
To run this, cd
to directly where build.gradle
is located and run
$ gradle hello
- Initialization phase
- Configuration phase
- doFirst (method)
- doLast (method)
apply plugin: 'java'
The java plugin uses a convention for directory structure
├── example
│ ├── build.gradle
│ └── src
│ ├── main
│ │ └── java
│ │ └── learning
│ │ └── App.java
│ └── test
│ └── java
│ └── learning
│ └── AppTest.java
Running gradle tasks
displays a list of all the tasks that the java plugin has installed.
Run gradle build
to build the project.
Gradle output will mark a task UP-TO-DATE
if there's nothing to do. Re-running a build will only rebuild things that have changed.
Gradle output goes to the build
folder, example compiled classes, libs, jar files, etc.
Simple example can be run java -cp build/classes/main com.pluralsight.Hello
Recommended way to run gradle to ensure the same version is always used on a project by everyone on the team. In the build file, add a wrapper task to set the gradle version:
task wrapper(type: Wrapper) {
gradleVersion = '2.11'
}
Execute the wrapper task:
$ gradle wrapper
This task sets up the gradle wrapper. Generates two new files in project root, gradlew
(shell script) and gradlew.bat
(for windows).
Also generates a gradle
directory in project root, containing a wrapper directory with gradle-wrapper.jar
and gradle-wrapper.properties
.
Now instead of running tasks via gradle
, instead use the wrapper, gradlew
. For example:
./gradlew build
This will first download the version of gradle specified in the wrapper, install it in your home .gradle
directory, then run the task using that gradle version.
The gradle wrapper scripts and jar files can be checked into version control, along with the gradle wrapper task specified in the build file. This ensures that everyone who checks out the project and runs the build will do so with the same gradle version.
Gradle DSL is written in Groovy, which is a JVM language. See Groovy Fundamentals Course on Pluralsight.
- Code that Gradle will execute
- Has a lifecycle (different parts of the task will run at different times)
- Initialization phase
- Configuration phase
- Execution phase
- Has properties
- Common properties such as description, and group it belongs to
- Custom to that task such as directory files are being copied to or from
- Properties are typically configured during the Configuration Phase of the lifecycle
- Has actions (code that executes), divided into two parts:
- First action - code that needs to run before other code within the task
- Last action - code that executes as part of the task
- Has dependencies: A task may require that another task complete before this one can execute. Gradle will work out the task dependencies and take care that tasks run in the correct order.
Groovy is object oriented. In a Gradle build script, top level object is project
.
This object is used to define everything within build script.
To create a task:
project.task("Task1")
To list all tasks
$ gradle tasks
Notice "Task1" is in a group called "Other tasks".
Can also define a task without project
keyword, because Gradle knows "project" is the top level object and will delegate everything to it:
task("Task2")
Another way to define a task, don't need brackets:
task "Task3"
Finally, don't even need the quotes to define a task:
task Task4
To add a property to a task:
Task4.description = "My super duper awesome task"
Description will appear in shell when running gradle tasks
.
First need to write a task action. All tasks have a doLast
method, which is the last thing the task does. It gets passed a Groovy closure, which is code that lives between braces.
task Task4
Task4.description = "My super duper awesome task"
Task4.doLast { println "This is Task 4" }
To run the task:
$ gradle Task4
Another way to write a task action is to use left shift operator <<
instead of explicit doLast
.
<<
is overridden by Groovy to add a closure to doLast
.
task Task3
Task3 << println "This is Task 3"
Can declare a task and add action in one line:
task Task5 << { println "This is Task 5" }
Note you can keep adding clojures, and Gradle will append them:
task Task5 << { println "This is Task 5" }
Task5 << { println "Another closure" }
Can add properties and actions all at once:
task Task6 {
description "Task 6 is the best task ever"
doLast {
println "Task 6 is running"
}
}
- Initialization phase: Used to configure multi project builds
- Configuration phase: Executes code in the task that's not in the action, for example, setting the "description" property
- Execution phase: Execute the task actions such as
doFirst
,doLast
Every task has a doFirst
method, for example:
task Task6 {
description "Task 6 is the best task ever"
doFirst {
"Task 6 first"
}
doLast {
println "Task 6 is running"
}
}
A task can have multiple doFirst
's and multiple doLast
's. When left shift <<
operator is used to append a task, it gets added to the doLast
. To add multiple doFirst
's, simply call it multiple times:
task Task6 {
description "Task 6 is the best task ever"
doFirst {
println "Task 6 first"
}
doLast {
println "Task 6 is running"
}
}
Task6.doFirst {
println "Task 6 another doFirst closure was appended"
}
Output from running gradle Task6
:
:Task6
Task 6 another doFirst closure was appended
Task 6 first
Task 6 is running
BUILD SUCCESSFUL
Each task has a dependsOn
method that can be passed the tasks that this task depends on:
Task6.dependsOn Task5
Now running Task6 will FIRST run Task5, then Task6. Notice that Task6's doFirst
methods run after Task5's doLast
methods:
Task5
This is Task 5
Another closure
:Task6
Task 6 another doFirst closure was appended
Task 6 first
Task 6 is running
Continuing on, if Task5 depends on Task4, then running Task6 will first run 4, 5, then 6.
A task can also have multiple dependencies:
Task6.dependsOn Task5
Task6.dependsOn Task3
Can also specify multiple dependencies as comma separated list:
Task6.dependsOn Task5, Task3
Can also specify dependencies inside the task closure:
task Task7 {
description "This is task 7"
dependsOn Task6
doFirst {
println "Task 7 doFirst"
}
doLast {
println "Task 7 doLast"
}
}
Dependencies are built during the configuration phase. When a task is executed, the dependency graph has already been determined, and Gradle can simply walk through the graph, executing tasks.
Properties can be defined using local variables with def
keyword.
Then the variable can be used in tasks using string interpolation.
Local variable has scope of the given build file in which its declared. This might not be suitable for a multi-project build if need the variable available in other build files.
Variables can also be defined inside a task, in a closure.
def projectVersion = "2.1.0"
Task6 {
doLast {
println "This is task 6 - version $projectVersion"
}
}
To have a variable available in a larger scope, across projects, use extra properties.
project.ext.projectVersion = "2.1.0"
...
doFirst {
println "Task 6 first - some big project var $project.ext.biggerScopeVariable"
}
Because its set on project, don't need to state it explicitly, so can just say:
ext.projectVersion = "2.1.0"
...
doFirst {
println "Task 6 first - some big project var $biggerScopeVariable"
}
Note: To suppress gradle's output when running tasks, to only see task output, use -q
flag for quiet logging:
$ gradle -q taskA
When a task has multiple dependencies, the order is undefined, gradle will pick some order:
taskA.dependsOn taskB
taskA.dependsOn taskC, taskD
Can't predict in what order B, C, D will run in.
- mustRunAfter
- If two tasks execute, one must run after the other
- Circular dependencies will fail the build
- shouldRunAfter
- If two tasks execute, one should run after the other
- Ignores circular dependencies
- finalizedBy
- Inverted dependency
Examples:
taskB.mustRunAfter taskC, taskD
taskB.shouldRunAfter taskD
mustRunAfter
and shouldRunAfter
only kick in when both tasks are run. For example:
task task1 << { println "task 1" }
task task2 << { println "task 2" }
task2.mustRunAfter task1
Then running task1 by itself will NOT cause task 2 to run:
$ gradle -q task1
However, running both at command line will enforce that task1 always runs first, then task 2. The following two commands will produce the same output:
$ gradle -q task1 task2
$ gradle -q task2 task1
All the tasks created up to now have been Ad-hock tasks such as:
task Task1
task Task2 << {}
task Task3 {}
It would be more useful to be able to define a task once, and then re-use it later, passing in some configuration to control its behaviour. This is where Typed Tasks come in.
For example, to copy files, need more complex code (open file, read file, write file), zipping files.
Gradle comes with some built in typed tasks, docs (scroll down to "Task types" in side nav).
For example, to use copy type:
task copyImages (type: Copy){
from 'src'
into 'dest'
}
By specifying type: Copy
, gradle treats this copyImages
task as if it was the copy task. The task implementation is then just configuration.
Copy task can get quite complex. Some of the config can be extracted using "copy specification".
def contentSpec = copySpec {
exclude 'file1.txt'
from 'src'
}
/* Does the same thing as copyImages */
task copyImages2 (type: Copy) {
with contentSpec
into 'dest'
}
If there are a large number of files to exclude, rather than specifying them one at a time, can execute groovy code in the copy spec. Specify a closure for exclude, which gets an iterator it
:
def contentSpec = copySpec {
exclude {it.file.name.startsWith('file1')}
from 'src'
}
Can also rename files, restructure directories, expand files (text replacement). For example, to replace resourceRefName
with jdbc/JacketDB
as file web.xml is being copied from src
directory into config
directory:
task copyConfig (type: Copy) {
include 'web.xml'
from 'src'
into 'config'
expand ([
resourceRefName: 'jdbc/JacketDB'
])
}
expand
property takes a dictionary with names of text to replace and what it should be replaced with.
Placeholders start with $
in file, for example web.xml might have <res-ref-name>$resourceRefName</res-ref-name>
.
To start, simply add the java gradle plugin to the build file:
apply plugin: 'java'
The java plugin adds many tasks to the project including:
- build
- clean
- javadoc
- test
Expects standard (maven) project layout:
- src/main/java
- src/main/resources
- src/test/java
- src/test/resources
If you use a different convention for project layout, can be customized using gradle SourceSets, to define where sources are located. For example,
sourceSets {
main {
java {
srcDir 'src/java'
}
resources {
srcDir 'src/resources'
}
}
}
To build a versioned jar, specify version number in build.gradle, for example:
version = '1.0.0.SNAPSHOT'
Then running gradle build
will generate a versioned jar in build/libs
.
The build
task is dependent upon a number of other tasks, as can be seen from the output:
:compileJava
:processResources
:classes
:jar
:assemble
:compileTestJava
:processTestResources
:testClasses
:test
:check
:build
compileJava
generates the classes files in build/lib/classes
.
classes
task seems to do the same thing as compileJava
, BUT, it depends on processResources
.
processResources
is a copy task, which copies resources from src/main/resources
into build/classes
.
Therefore classes
ties together the compilation compileJava
and the copying of resources processResources
.
jar
is a low level task that creates the jar file in build/lib
.
assemble
is the java plugin task that creates all the jar files within the project. This is a lifecycle task in the gradle plugin.
Each time the build is run, gradle has to re-launch the jvm. It would be better if gradle could re-use an existing vm. Gradle can do this using the daemon.
To use the daemon via cli:
$ gradle --daemon clean build
The first build run with daemon may still be slow, but subsequent builds will be faster.
Alternatively, daemon can be specified via the GRADLE_OPTS environment variable.
But the best way is to create a gradle.properties
file in home directory and turn on the daemon:
org.gradle.daemon=true
Gradle also supports multi-project builds to link them together. For example when a class in one project depends on a class from another project. This is done in a top level gradle build file to specify the projects that make up the multi-project build.
- Add top level settings.gradle, which lists all the projects
- Add top level build.gradle to configure all the projects
- defines project wide build functionality
- specify dependencies between projects
settings.gradle
includes all the projects, for example:
include 'Repository', 'Jacket'
build.gradle
specifies any commonalities across projects (eg: applying java plugin to all projects). Also specify dependencies between projects.
To apply java plugin to all projects, wrap the apply plugin call in the allprojects
method closure:
allprojects {
apply plugin: 'java'
}
Specify which projects are part of the multi-build, :
specifies relative to current location. Furthermore, specify project configuration if needed, for example, Jacket
project depends on Repository
:
project(':Repository'){}
project(':Jacket'){
dependencies {
compile project(':Repository')
}
}
Now can go to the Jacket project (which depends on Repository), run gradle build
, and it will step up to Repository project for the dependency and build will succeed.
Typical project will have dependencies on:
- Other projects
- External libraries (eg: on the web somewhere)
- Internal libraries (eg: on company internal nexus repo)
Dependencies can be satisfied from:
- Other projects
- File system
- Maven repositories
- Ivy repositories (gradle support deprecated?)
Can Have Many Configurations
Depenedency will often only be needed for a specific action, eg: junit only needed when running tests, but not for compiling application. Can define custom configurations, and also plugins bring in their own configurations.
For example, Java plugin has these configurations:
- compile
- runtime
- testCompile
- testRuntime
Dependencies don't have to be isolated, for example runtime
dependency extends the compile
time dependency. Furthermore, dependencies can be included or excluded from each configuration.
Transitive Dependencies
Some dependencies will depend on other libraries, these are called transitive dependencies.
To list dependencies for a given project (-q
is quite):
$ gradle -q dependencies
To list dependencies for a given configuration within a project
$ gradle -q dependencies --configuration compile
Maven Remote:
repositories {
mavenCentral()
}
repositories {
jcenter() // https
}
Or to use jcenter over http:
repositories {
url "http://jcenter.bintray.com/"
}
To use your local maven installation (i.e. ~/.m2
)
repositories {
mavenLocal()
}
To use an internal company repo such as nexus:
repositories {
maven {
url "http://repo.mycompany.com/maven2"
}
}
Repositories can be configured, for example, if need to specify a username and password to access company repo.
Can also specify multiple repositories. Dependencies will be resolved in order which repos are listed:
repositories {
maven {
url "http://repo.mycompany.com/maven2"
}
ivy {
url "http://repo.mycompany,com/repo"
}
}
When using a repository, dependencies must specify group, name, and version. For example:
dependencies {
compile group: 'log4j', name: 'log4j', version: '1.2.17'
}
Or can use shorthand syntax:
dependencies {
compile 'log4j:log4j:1.2.17'
}
Then when running a build, Gradle will download the jars and pom files (metadata). Next time running the build, will not need to download jar/pom again because it gets cached.
Dependencies are cached when they get downloaded.
- File based
- Metadata and files stored separately
- Repository caches are independent
Names of the files in the cache are SHA1 based. To determine if file needs to be downloaded again, look at repository, create SHA1 of file, check the name in the file system, if SHA1's are different, needs to be re-downloaded.
Dependencies can be refreshed by passing --refresh-dependencies
flag to build (will run SHA1 check described above).
For force refresh, can safely delete cached files, and on next build, everything will be downloaded again.
Gradle cache is at ~/.gradle/caches/modules-2
Testing is integral to the Java plugin. It defines:
- source set where the test sources are located
- task to compile the Tests
- task to run the tests
By convention, the source set will look for tests in src/test/java
, but that can be customized.
Test compilation output goes to build/classes/test
.
When tests are run, reports go to build/reports/test
.
$ gradle test
Tests are also run as part of gradle build
.
A failing test fails the build.
By default, gradle test
runs all the tests.
A filter can be used to run only a subset of tests, or a single test, or all tests from a package. Wildcards are supported (otherwise need to specify fully qualified name of the test).
Pass a closure to the test command which is a filter, then pass a closure to the filter to specify tests that should be added:
test {
filter {
includeTestsMatching 'com.foo.shouldCreateASession'
includeTestsMatching '*shouldCreateASession'
}
}
Can also define a custom task for testing with filter:
task singleTest (type: test) {
dependsOn testClasses
filter {
includeTestsMatching '*shouldCreateASession'
}
}
Filter can also be specified at cli, to override any existing filter configuration:
$ gradle test --tests *shouldCreateASession
All of the above notes on testing is for unit testing only. But what about other types of tests such as integration?