Gradle
- a general purpose build automation tool
- flexible yet powerful for different use cases
- building android apps
- automating
Go
orpython
projects - generating documentations
- runs on Java Virtual Machine (JVM)
- build logic defined as instructions in a script
- plugins can provide predefined functionality
- tool can be executed from the terminal and IDE
- Gradle url
Installation
brew install gradle
- check version
gradle -v
Basic terminology
- Project: models a software component
- Build script: contains automation instructions for a project
- Task: defines executable automation instructions
Build logic
- gradle defines a Domain Specific Language (DSL)
- 2 options
- Groovy DSL
- Kotlin DSL
- 2 options
- can mix in imperative logic
File format
using
Groovy DSL
use the
.gradle
extension- e.g.:
build.gradle
- e.g.:
print Hello world
task
a method call on an API available to the Gradle build script- it is to express that we want to create a task for project
- string parameter
helloWorld
is the name of the task
doLast
defines the action executed at runtimeprintln
is a method call provided by Groovy to print the message to standard output
task helloWorld {
doLast {
println "Hello World"
}
}run file
call function not the filename
gradle helloWorld
output
> Task :helloWorld
Hello World
BUILD SUCCESSFUL in 823ms
1 actionable task: 1 executed
using
Kotlin DSL
use the
.gradle.kts
extension- e.g.:
build.gradle.kts
- e.g.:
print Hello world
tasks.create("helloWorld") {
doLast {
println("Hello World")
}
}run file
call function not the filename
gradle helloWorld
output
> Task :helloWorld
Hello World
BUILD SUCCESSFUL in 823ms
1 actionable task: 1 executed
Gradle wrapper
- it is a set of files checked into SCM alongside source code
- standardizes compatible gradle version for a project
- automatically downloads the gradle distribution with defined version
disadvantages of not using gradle wrapper
- gradle API can include breaking changes in between major versions
- maintaining multiple gradle installations on a developer machine is not convenient
benefits
- developers do not need to install gradle runtime
- developers can check out project source code and build right away
- wrapper works the same way on continuous integration servers
terminal command
gradle wrapper
- creates files and directories
- gradle directory
- gradlew
- gradlew.bat
- view properties created
cat gradle/wrapper/gradle-wrapper.properties
- output
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
- output
- run gradle wrapper instead of gradle on mac
./gradlew helloWorld
Build file and conventions
Build Execution under the Hood
- evaluates instructions in build scripts
- creates and configures tasks
- executes tasks in correct order
Single project build
src -> build.gradle
- characteristics
- resides in root directory of project
- contains all build logic
- can become hard to maintain
Multi module build
build.gradle -> module -> build.gradle
-> module -> build.gradle
- maintainable, highly cohesive build logic
Settings file in a build
build.gradle
settings.gradle -> module
-> module
characteristics
- resides in root directory of project hierarchy
- declares participating projects
- can change defaults
- e.g.: project name
Gradle auto derives project name from directory
gradle projects
output
> Task :projects
------------------------------------------------------------
Root project 'hello-world'
------------------------------------------------------------
Root project 'hello-world'
No sub-projects
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :tasks
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executedmodify project name
create
settings.gradle
file with the following contentsrootProject.name = "starter-example"
gradle projects
output
> Task :projects
------------------------------------------------------------
Root project 'starter-example'
------------------------------------------------------------
Root project 'starter-example'
No sub-projects
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :tasks
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
Properties file in a build
build.gradle
settings.gradle
gradle.properties
- characteristics
- resides in root directory of project hierarchy or gradle user home directory
- preconfigures runtime behavior
- create
gradle.properties
file with the following contents- changing log level for a console output to info
org.gradle.logging.level = info
- declare custom version of project
version = 1.0.0
- retrieve variable from property
- in
build.gradle
filetask helloWorld {
doLast {
println "Hello World, version = " + version
}
}
- in
gradle helloWorld
or./gradlew helloWorld
- changing log level for a console output to info
Defining and configuring a task
- view all available tasks
gradle tasks --all
Task purpose
- defines executable unit of work
- actions contain logic to be executed runtime
- general categorization: ad hoc tasks and tasks explicitly declaring a type
Ad Hoc Task
Default Task
^
| extends
Ad hoc Task
- characteristics
- implements one-off, simplistic action code by defining doFirst or doLast
- automatically extend DefaultTask without having to declare it
- the
helloWorld
is an example of anAd Hock Task
- it also has no explicity declare type
task helloWorld {
- it also has no explicity declare type
Task Declaring a type
Copy
^
| extends
Typed Task
characteristics
- explicitly declares type
- e.g.:
task copyFiles(type: Copy) {
from "sourceFiles"
into "target"
}
- e.g.:
- does not necessarily need to define actions as they are already provided by type
- explicitly declares type
real copy example
- in
build.gradle
file- check for all
.bat
files from current directory and into nested directories - then add them into test folder with their respective directories
task copyExample(type: Copy) {
from "."
into "test"
include "**/*bat"
includeEmptyDirs = false
} - check for all
- in
real copy and zip example
in
build.gradle
filetask copyExample(type: Copy) {
from "."
into "test"
include "**/*bat"
includeEmptyDirs = false
}
task createZip(type: Zip) {
from "test"
archiveFileName = "docs.zip"
destinationDirectory = file("test/dist")
}need to run each task individually
to enable it to run without running each task independently, use
dependsOn
keytask copyExample(type: Copy) {
from "."
into "test"
include "**/*bat"
includeEmptyDirs = false
}
task createZip(type: Zip) {
from "test"
archiveFileName = "docs.zip"
destinationDirectory = file("test/dist")
dependsOn copyExample
}
Task Execution Order
dependsOn
A -> B
-> C
dependsOn
- ensures that B and C is executed before A
- does not explicitly define if B or C is executed first
Fined-Gradle Dependency Control
dependsOn
A -> B
| mustRunAfter
v
-> C
dependsOn
- use
mustRunAfter
,shouldRunAfter
, orfinalizedBy
Directed Acyclic Graph (DAG)
node node
A -> B
graph edge
- at runtime, the gradle buils a directed acyclic graph for tasks in memory
- thus, runtime will know all the tasks participating in the build and their proper execution order
- task is represented as node
- task dependency is represented as graph edge
- Gradle does not provide a built-in feature for visualizing the task graph of a project
- to emulate the task dependencies in action
gradle taskname --dry-run
- can use the gradle-task-tree to render the tasks as a tree
- to emulate the task dependencies in action
Circular Dependencies (Not allowed)
- dependency cycles cannot be formed
- gradle will fail build if detected
- e.g.: task A cannot dependsOn task B, while task B cannot dependsOn task A at the same time
Domain Objects in Memory
- each node represents the DAG in memory
- tasks are just 1 example of domain object of a build
- domain objects can be inspected and modified from the build script
import domain objects
Gradle
:org.gradle.invocation.Gradle
- represents invocation of the build
- every invocation of a greater build is represented by a domain object called
Gradle
- this domain object has knowledge about the project hierarchy in a single project
- or multi project build provides pointers to the higher level properties of a build
- e.g.: the gradle user home directory, the used Gradle version can register callback logic to react to certain events in the build
- this domain object has knowledge about the project hierarchy in a single project
Gradle
-Project
:org.gradle.api.Project
- represents a software component and provides API access to object hierarchy
- it serves as the main entry point of a build
- provides methods for walking the whole hierarchy of domain objects
- e.g.: can ask for the reference to the Gradle instance, register new tasks, or get a modified typical environmental properties like the build output directory
Gradle
-Project
-Task
:org.gradle.api.Task
- represents unit of work with potential dependencies
- performs the actual work at runtime
- from the project, can register as many tasks as you like
- every task can declare task dependencies
- in most cases, tasks define at least 1 action
Gradle
-Project
-Task
-Action
:org.gradle.api.Action
- actual work performed during execution phase
- gradle executes actions in order of declaration
- can also define doFirst and doLast actions
Gradle - Project - Task - Action
|
v
Plugin org.gradle.api.Plugin- provides reusable logic for a project
- every plugin applied to a project is represented as a plugin domain object
- a plugin has full access to the project it works on
- thus can access other domain objects by name or by type and modify them as necessary
Build lifecycle phases
- every build performs 3 lifecycle phases
Initialization Phase
- evaluates settings file and sets up build
- file contains the information about the projects that should participate in the build
- settings file can exist for a single and multi project builds
Configuration Phase
evaluates build scripts and runs configuration logic
each project can define a distinct build script but doesn't have to
all code in build script will be exercised
during configuration phase, task actions are not executed
- tasks are only configured
configuration counts as assigning values to properties or calling task methods exposed by its API
make sure that code defined does not necessarily execute expensive logic as it would affect the performance
example in
build.gradle
file- always outide of
doFirst
anddoLast
actions - executed during configuration phase
// configuration code
task helloWorld {
// configuration code
doFirst {}
doLast {}
}- always outide of
Execution Phase
- executes task actions in correct order
- it looks at the directed acyclic graph that was built in memory and executes every task action in the the correct order
- example in
build.gradle
file- always inside of
doFirst
anddoLast
actions - executed during execution phase
task helloWorld {
doFirst {
// execution code
}
doLast {
// execution code
}
} - always inside of
- executes task actions in correct order
Plugins
- avoid repetitive code
- make build logic more maintainable
- provide reusable functionality across projects
2 types of plugins
Script Plugins
includes
build.gradle -> publishing.gradle
-> deployment.gradle
includessame syntax, just another build script that can be included in the main
build.gradle
fileprimary reason for wanting to use it is to split up build logic and make it more maintainable
example
create a
archiving.gradle
file- add the usual code from
build.gradle
task copyExample(type: Copy) {
from "."
into "test"
include "**/*bat"
includeEmptyDirs = false
}
task createZip(type: Zip) {
from "test"
archiveFileName = "docs.zip"
destinationDirectory = file("test/dist")
dependsOn copyExample
}- add the usual code from
create a
build.gradle
file- implement script plugin with the
apply
key word
apply from: "archiving.gradle"
- implement script plugin with the
run file as usual
gradle createZip
Binary Plugins
includes
build.gradle -> gradle core plugin
-> community plugin
includesimplemented as classes
bundled as JAR files
can reuse the functionality across multiple self-contained software projects
- these software projects usually live in different version control repositories
example
create a
archiving.gradle
file- add the usual code from
build.gradle
- use the
apply
key word - delete the
archiveFileName
anddestionationDirectory
as they are provided by thebase
plugin, thus no longer required
// single plugin
apply plugin: "base"
// multi plugins
plugins {
id 'java'
}- add the usual code from
task copyExample(type: Copy) {
from "."
into "test"
include "**/*bat"
includeEmptyDirs = false
}
task createZip(type: Zip) {
from "test"
dependsOn copyExample
}
```
- create a `build.gradle` file
- implement script plugin with the `apply` key word
```gradle
apply from: "archiving.gradle"
```
- run file as usual
> gradle createZip
Build a java project
Using Gradle Java Plugin
standard source code directories
src/main/java -> contains the production source code
src/main/resources -> contains resource files needed at runtime
src/test/java -> contains test source code
src/test/resources -> contains resource files needed at test execution time
build.gradlebuild output directories
build/classes -> contains compiles class files
build/libs -> contains generated JAR filebuild.gradle
// basic requirement
plugins {
id 'java'
id 'application'
}
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType(JavaCompile) {
//enable compilation in a separate daemon process
options.fork = true
}
jar {
// use preset jar file name
archiveBaseName = "newName"
}
application {
mainClass = "com.domain.appname.Main"
}
javadoc {
options.header = "appname"
options.verbose() // to print logs
}run wrapper
gradle wrapper
src/main/java
can be compiled using the taskcompileJava
./gradlew compileJava --console=verbose
copies files from
src/main_resources
intobuild
directory using taskprocessResources
./gradlew processResources --console=verbose
combine both
compileJava
task andprocessResources
withclasses
./gradlew classes --console=verbose
package jar file
./gradlew jar
run application
./gradlew run
- run with args ./gradlew run --args="add 1 2"
install application to allow running without java
./gradlew installDist
- run application
./build/install/appname/bin/appname
- run with args ./build/install/appname/bin/appname add 1 2
- run application
./build/install/appname/bin/appname
zip application
- single bundle
./gradlew distZip
- multi bundle
./gradlew distZip distTar
- single bundle
run
javadoc
./gradlew javadoc
- output in
build/docs/
directory - open docs open build/docs/javadoc/index.html
- output in
Dependency Management
- within gradle project, can define a dependency on libraries in Maven Central or any other binary repository
- at build time, gradle's dependency management engine downloads its artifacts
- stores them in the local cache for reuse
- adds them to the class path of the project
- gradle calls the scope of a dependency a configuration
- can be very specific about the scope of a dependency
- e.g.: can express that the dependency is only needed at runtime, but not for compilation process
- can be very specific about the scope of a dependency
- another type of dependency is
project dependency
- when application logic becomes complex
- will want to separate it based on functional boundaries, modules, or components
- a module, all can use other modules
- each of it are modeled as a gradle project
- referred to as
multi-project build
- when application logic becomes complex
- when a project is ready to ship
- will want to produce a library or distribution
- common practice to publish those artifacts to a binary repository for consumption by other developers or end users
- gradle supports publishing java libraries to Maven repositories
Dependency management in Java
- java ecosystem offers a mature set of reusable functionality
- the most popular libraries are available on Maven Central
- it is a centrally hosted binary repository
Declaring a dependency on an external library
need to know 3 aspects
dependency coordinates which is the Group, Artifact, Version (GAV) you want to consume
Group:Artifact:Version
- e.g.:
commons-cli:commons-cli:1.4
- e.g.:
to consume the dependency, need to declare the repository using the
repositories
method inbuild.gradle
fileplugins {
id 'java'
id 'application'
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
jar {
archiveBaseName = "newName"
}
application {
mainClass = "com.domain.appname.Main"
}
// allows gradle to know where to resolve the dependency from
repositories {
mavenCentral()
}define the GAV of the dependency by using the
dependencies
method withimplementation
scope- after adding the dependencies, you can import it in the java code
plugins {
id 'java'
id 'application'
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
jar {
archiveBaseName = "newName"
}
application {
mainClass = "com.domain.appname.Main"
}
repositories {
mavenCentral()
}
dependencies {
implementation "commons-cli:commons-cli:1.4"
}- to test if dependencies are working, install and run them
- installation
./gradlew installDist
- run
./build/install/appname/bin/appname
- run with args ./build/install/appname/bin/appname --operation functionname --value value1 --value2 value2
- installation
Dependency tree
- over time, the number of dependencies will grow
- declared dependencies oftentimes pull in transitive dependencies
- result in a large tree of dependencies to manage
- check dependency tree
./gradlew dependencies
- use
dependencyInsight
task to find out why the dependency is needed and where it is coming from./gradlew -q dependencyInsight --dependency commons-cli
Multi-project builds
projects with a lot of code easily become hard to maintain
breaking up a project into components increases cohesion and makes it more manageable
gradle can model each of the components with a project instance,
multi-project build
sample multi-project structure
api/src/main/java/com/domainname/appname/Appname.java
app/src/main/java/com/domainname/appname/Main.java
app/build.gradle
build.gradle
settings.gradleapp/build.gradle
plugins {
id 'application'
}
application {
mainClass = 'com.domainname.appname.Main'
}
repositories {
mavenCentral()
}
dependencies {
implementation project(':api')
implementation 'commons-cli:commons-cli:1.4'
}settings.gradle
rootProject.name = 'appname'
include ':api', ':app'build.gradle
allprojects {
version = '1.0.0'
}
subprojects {
apply plugin: 'java'
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}check projects taking part of the build using the
projects
task./gradlew projects
compile and copy resources, this will check if build works
./gradlew classes
Publishing libraries
library needs to be published to a binary repository
a published library can be identified by its coordinates (GAV)
the Maven Publish plugin automates the publishing process
api/build.gradle
publish to a local folder
plugins {
id 'maven-publish'
}
publishing {
publications {
maven(MavenPublication) {
groupId = 'org.domainname'
artifactId = 'appname'
from components.java
}
}
repositories {
maven {
url = "$rootProject.buildDir/m2repo"
}
}
}publish to a cloud binary repository
plugins {
id 'maven-publish'
}
publishing {
publications {
maven(MavenPublication) {
groupId = 'org.domainname'
artifactId = 'appname'
from components.java
}
}
repositories {
maven {
url = 'http://localhost:8082/artifactory/libs-release-local/'
credentials {
username = 'admin'
password = 'admin_123'
}
}
}
}run
publish
task in the root directory- works for multi-project builds
./gradlew publish
- works for multi-project builds
Testing a Java Project
- JUnit is the established standard test framework in the Java ecosystem
- importing into code
import org.junit.jupiter.api.Test;
Declaring JUnit dependencies
Test dependency configurations
- JUnit dependencies need to be declared with the correct configurations
- configuration will only work on the test code
- the dependencies declared with this configuration won't be bundled when shipping the app
- will use the test runtime instead
testImplementation -> Needed for compilation and text execution
testRuntime -> Only needed for test executionexample
build.gradle
at root directoryplugins {
id 'java'
id 'application'
}
version = '1.0.0'
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
application {
mainClass = 'com.domainname.appname.Main'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'commons-cli:commons-cli:1.4'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}
test {
useJUnitPlatform() // basic requirement to work
testLogging { // change logging behavior
events 'started', 'skipped', 'failed' // show on console the test events
exceptionFormat 'full' // gives a deeper hint on what is actually wrong
}
}- compile tests with
compileTestJava
task./gradlew compileTestJava
- run unit test
./gradlew test
- open test report
open build/reports/tests/test/index.html
- open xml test report
open build/test-results/test/TEST-com.linkedinlearning.calculator.CalculatorTest.xml
- compile tests with
Building with docker
build.gradle
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath 'com.bmuschko:gradle-docker-plugin:7.3.0'
}
}
apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'com.bmuschko.docker-java-application'
import com.bmuschko.gradle.docker.tasks.container.*
import com.bmuschko.gradle.docker.tasks.image.*
sourceCompatibility = 1.8
targetCompatibility = 1.8
group = 'com.dockerapp'
version = '1.0-SNAPSHOT'
mainClassName = "com.dockerapp.App"
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.12'
}
docker {
javaApplication {
baseImage = 'openjdk:latest'
}
}
task createContainer(type: DockerCreateContainer) {
dependsOn dockerBuildImage
targetImageId dockerBuildImage.getImageId()
}
task startContainer(type: DockerStartContainer) {
dependsOn createContainer
targetContainerId createContainer.getContainerId()
}build and run app normally
./gradlew build run
build docker image
./gradlew dockerBuildImage
create docker container
./gradlew createContainer
start docker container
./gradlew startContainer
check docker output
docker container logs containerId