Declarative specification of libraries and versions
Automatic retrieval
Automatic resolution of transitive dependencies
Dependency scopes
You may need compile-only, test-only, and runtime-only dependencies
Customizable software sources
Gradle
A paradigmatic example of a hybrid automator:
Written mostly in Java
with an outer Groovy DSL
…and, more recently, a Kotlin DSL
Our approach to Gradle
We are going to learn a bit of how to use Gradle for simple JVM projects
Mostly by example
We are not going to learn Gradle
Again, this is done in Software Process Engineering for those interested
Gradle: main concepts
Project – A collection of files composing the software
The root project can contain subprojects
Build file – A special file, with the build information
situated in the root directory of a project
instructs Gradle on the organization of the project
Dependency – A resource required by some operation.
May have dependencies itself
Dependencies of dependencies are called transitive dependencies
Configuration – A group of dependencies with three roles:
Declare dependencies
Resolve dependency declarations to actual artifacts/resources
Present the dependencies to consumers in a suitable format
Task – An atomic operation on the project, which can:
have input and output files
depend on other tasks (can be executed only if those are completed)
Gradle: under the hood
The Gradle build script is a valid Kotlin script (using the Gradle API)
(There is actually a kludge concerning the plugins and buildscript blocks)
Anything that does not have valid Kotlin syntax is not a valid Gradle build script
Kotlin and Groovy picked as they enable easy DSL creation
The feeling is to just have to configure an existing software
Declarative, much like Maven plugins
When needed, it is easy to configure custom behaviour
fiddle with internals
write in functional or imperative fashion
Gradle: minimal java build
By default, Gradle inherits the maven convention for source organization:
+-- src # All sources +-- main # One folder per "source set", all code in a source set shares dependencies | +-- java # One folder per language | +-- kotlin
| +-- resources # Resources go here | \-- scala
\-- test# Test sources are separated, different dependencies +-- java # Same structure as main +-- kotlin
+-- resources # Resources go here \-- scala
Gradle: minimal java build
src/main/java/HelloWorld.java
build.gradle.kts
Yes, it’s a one-liner
Gradle: minimal example execution
The gradle command accepts the names of tasks to execute as parameters
The java plugin introduces several tasks:
compile<source set><language name> (e.g., compileTestJava): compiles a specific source set
compile<language name>: compiles all source sets of a language
build: runs all compilation tasks
tasks: displays available tasks
usually used as tasks --all to show also uncategorized tasks
Dependency management in Gradle
repositories
Where to retrieve software from
Well known repositories for Java and related technologies (including Scala):
The Central Repository: mavenCentral() – de-facto standard
Google Android Libraries: google()
✝ jcenter() sunsetted May 2021
Jitpack: maven { url("https://jitpack.io") }
Dependency management in Gradle
dependencies
Separated by scope
Scopes define when a dependency should be available
api (libraries only): abstractions are exposed publicly and clients are expected to use them
implementation: used internally, clients should not care
testImplementation: used to compile and run tests
(test)compileOnly: only available when compiling (typically used for annotations)
(test)runtimeOnly: only required at runtime (typically used when components are loaded reflectively)
Scopes map to configurations
Specified as <group>:<artifact>:<version>
+ is a special marker for “latest”
Maven-style ranges supported (e.g., (1.2, 1.4])
Transitive dependencies are resolved automatically (if available in some repository)
The build system as dependency
no guarantee that automation written with some tool at version X, will work at version Y!
$\Rightarrow$ The build system is itself a dependency
A global dependency on the build tool is hard to capture
Often, it becomes a prerequisite expressed in natural language
e.g., “you need Maven 3.6.1 to build this software”
Critical issues when different pieces of the same system depend on different build tool versions
The Gradle wrapper
Gradle proposes a (partial) solution with the so-called Gradle wrapper
A minimal program that simply downloads the version of Gradle written in a configuration file
Generable with the built-in task wrapper
gradle wrapper --gradle-version=<VERSION>
Prepares scripts for bash and cmd to run Gradle at the specified version
gradlew
gradlew.bat
The Gradle wrapper is the correct way to use Gradle, and we’ll be using it from now on.
Gradle: our toy example
src/main/java/it/unibo/sampleapp/RateAMovie.java
Gradle: our toy example
settings.gradle.kts
(one-liner)
Gradle: our toy example
build.gradle.kts
Gradle: toy example execution
The application plugin introduces a run task:
Depends on build
runs the specified main class passing the runtimeClasspath to the -cp option of java
./gradlew run
Note: execution requires a valid OMDB API Key stored as an environment variable OMDB_API_KEY
Gradle: multi-language projects
src/main/groovy/HelloGroovy.groovy
src/main/java/HelloWorld.java
Gradle: multi-language projects
src/main/kotlin/HelloKt.kt
src/main/scala/HelloScala.scala
Gradle: multi-language projects
build.gradle.kts
Quality Assurance
“It works”is not good enough
(besides, the very notion of “it works” is rather debatable)
Software quality should be continuously assessed
The assessment should be automatic whenever possible
QA should be integrated in the build system!
It is fine to fail the build if quality criteria are not met
Quality Assurance: levels
Static: the project is analyzed for defects without executing it or its components
Style and coherence
Flawed programming patterns
Known bugs, suboptimal programming, security flaws, …
Violations of the DRY principle
Dynamic: the project (or some parts) are executed
Testing
Multifaceted issue
$\Rightarrow$ Plenty of detail in the upcoming lectures
Quality Assurance: style and coherence
Automated checkers are also called linters, often provide an auto-formatting tool
Idiomatic and standardized code:
reduces complexity
improves understandability
prevents style-changing commits with unintelligible diffs
plugins {
java
scala
jacoco
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.scala-lang:scala3-library_3:3.7.4")
// The BOM (Bill of Materials) synchronizes all the versions of Junit coherently.
testImplementation(platform("org.junit:junit-bom:6.1.3"))
// The annotations, assertions and other elements we want to have access to when compiling our tests.
testImplementation("org.junit.jupiter:junit-jupiter")
// The engine that must be available at runtime to run the tests.
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
// Enables JUnit Platform (needed for JUnit 5)
tasks.named<Test>("test") {
useJUnitPlatform()
}
Quality Assurance: Scalatest + Scoverage
Let’s switch testing framework and enable coverage
src/main/scala/it/unibo/test/Test.scala
packageit.unibo.testtraitSometrait:deff:Intdef g:IntobjectSomeobject:deff = 10def g =77
Execution happens in a fresh system (virtual machine or container)
Often containers inside virtual machines
The specific point of the hierarchy at which the VM/container is spawned depends on the CI platform
Pipeline design
In essence, designing a CI system is designing a software construction, verification, and delivery pipeline
with the abstractions provided by the selected provider.
Think of all the operations required starting from one or more blank VMs
OS configuration
Software installation
Project checkout
Compilation
Testing
Secrets configuration
Delivery
…
Organize them in a dependency graph
Model the graph with the provided CI tooling
Configuration can grow complex, and is usually stored in a YAML file
(but there are exceptions, JetBrains TeamCity uses a Kotlin DSL).
CI in GitHub Actions
Very open-source friendly
Integrated in GitHub
Less mature than other integrators
My personal feeling is that it was built bottom-up…
Some questionable limitations
A lot of parallelism (20 concurrent jobs, 5 for macOS)
CI in GitHub Actions
step
A (possibly named) command or sequence of commands
Can be executed on the virtual machine or inside a container
Defines success or failure
Usually, the return value of the last command
action
A reusable step
DRY in CI $\Rightarrow$ Software Process Engineering
CI in GitHub Actions
job
A sequence of steps
Multiple jobs run in parallel
Executed on a virtual machine
A job is successful if all its steps are successful
Multiple jobs can be spawned at once via matrix expansion
e.g., spawn one job for each OS and version of the JVM
CI Matrix expansion $\Rightarrow$ Software Process Engineering
workflow
A named set of jobs
There can be multiple workflows per repository
DRY/Reusability in Software Process Engineering
Workflows are isolated
GitHub Actions: Configuration
Workflows are configured in the .github/workflows/ folder of the repository
Events that trigger a workflow are specified under the on key
on:push:# Trigger the workflow on pushbranches:- main# but only for the main branchpull_request:# Also trigger for pull requestsbranches:- main# but only for the main branchpage_build:# Also trigger on page_buildrelease:# Also trigger on release...types:- created# ...createdschedule:- cron:'*/30 5,17 * * *'# Cron syntax (see crontab.guru)
GitHub Actions: Environment preparation
GitHub Actions jobs run on a fresh virtual machine
The operating system can be selected (Windows, Ubuntu, macOS)
Multi OS builds $\Rightarrow$ Software Process Engineering
The repository needs to be cloned manually
Any operation should be configured manually
Rather than convention over configuration, GHA uses actions as a form of configuration reuse
Developing new actions $\Rightarrow$ Software Process Engineering
GitHub Actions: a simple gradle build
name:Clone Repositoryon:push:# On every push on any branchjobs:Build:# Job nameruns-on:ubuntu-latest# Operating system selectionenv:# Environment can be controlled at the job or step levelTERM:dumb# Sets the environment variable TERM to "dumb"steps:- name:Checkout# custom name Checkout the repositoryuses:actions/checkout@v6# Action implemented in this repository, tag starting with "v6"- uses:actions/setup-java@v5# Action implemented in repository "actions/setup-java" tag starting with "v5"with:# Actions parameters (action name omitted)java-version:25distribution:temurin- name:Buildrun:./gradlew check
Automated delivery
Once the reference environment in CI completed the software construction and testing,
it should sign and then deliver the result.
Possibly, even deploy the result, putting it at work
In this repository,
delivery is enabled to all the aforementioned destinations
No-retract
Your error will remain in the repositories forever and you will never be able to
fix it, you will need to create a new release with a different version number.
Why no-retract? An anecdote
In March 2016, Azer Koçulu unpublished more than 250 of his modules
from NPM, which is a popular package manager used by JavaScript
projects to install dependencies, because he was asked to rename the
module Kik, whose name is the same as that of an instant messaging app that
wanted to publish a homonym module.
Unfortunately, one of those dependencies was left-pad, an 11-line-long
JavaScript module, used by thousands of projects. This brought
breakages throughout many JavaScript products (most notably Node and
Babel). NPM had to un-unpublish left-pad.
Picking version numbers
Without compliance to some sort of formal specification, version
numbers are essentially useless for dependency management. By
giving a name and clear definition to the above ideas, it becomes
easy to communicate your intentions to the users of your software.