My thoughts on Gradle 2016-06-20

A while ago I wrote about why I prefer short build scripts and I briefly mentioned Gradle, a build system for Java. Today I’d like to share more of my thoughts on Gradle. I’ve been using it for most of my projects in the last couple of years. I wanted to summarize my thoughts here both for personal reflection and future reference, and hopefully I’ll help you decide if you want to use Gradle too.

I’ll mostly be comparing Gradle to two other build systems for Java: Ant and Maven. From personal experience these are the two most common build systems for Open Source Java projects. It seems like Maven is more common nowadays, but Ant used to be the de facto standard for building Java projects. I think Gradle is growing in popularity, especially since it is part of the official Android toolchain.

Things I like about Gradle

I have mostly positive things to say about Gradle, but I also have some complaints or nits that I think could be improved. Let’s start with the stuff I like in Gradle.

Goodbye XML

My favorite feature of Gradle is that it uses Groovy as the language for build scripts. Java build systems have a long tradition of XML-based build scripts (Ant & Maven), and I’m incredibly happy to use something that is not XML-based. XML may be nice for technical reasons but it sucks so bad to hand-write XML.

Thanks to being Groovy-based, Gradle has a nice and sparse syntax and it is even possible to write general purpose Groovy code inside the build script, allowing complex actions and decisions to be integrated in the build.

General purpose code in the build

Writing general purpose code in the build script is very useful because it allows adding any of the following complex high-level behaviors to the build:

  • Complex one-off project-specific build tasks.
  • Complex up-to-date checks for build tasks.
  • Environment-dependent build configuration.

Gradle makes it very simple to construct custom build tasks, making it easy to handle build steps that there may not already exist a plugin for. In comparison, you’d have to write your own one-off plugin to do something similar with Ant or Maven, a much larger investment for something that you’ll likely only use for one project.

Custom up-to-date checks are very useful for making a build more incremental. Gradle already has nicer incremental build support and improved dependency handling compared to Ant, but when using custom build tasks it may be useful to write your own up-to-date checks to decide when a rebuild is necessary in order to maximize incrementality.

Finally, I mentioned environment-dependent build configuration. This is more of a niche use case but one that I have found very useful in my projects. I usually generate version names using git describe, which gives nice incremental version names for Git projects. To generate such version strings in Gradle I use a common snippet of general-purpose code that I add in my build scripts. It looks something like this (edited for brevity):

// Runs 'git describe' and returns the output as a string.
def getVersion() {
    def output = tryCommand(['git', 'describe'])
    // Update resource files...
    output.readLines()[0] // Return the first line of output.
}

project.version = getVersion()

The getVersion() function above runs git describe to generate a version string. The function also stores the version string in a property file and does some error handling in case the git describe command failed.

Build by convention

Build by convention is not a new idea. I believe the concept was popularized by Maven, and Gradle has adopted the same conventions. Briefly, the idea of build by convention is this: if source files are stored in standard directories then you do not have to configure them in the build script.

Java projects use the following conventions: Java source files are stored in src/main/java, and test files are stored in src/test/java. If you follow this convention you’ll only need to use the Java plugin in Gradle to set up a new Java project:

apply plugin: 'java'

That’s it, a complete build script for a simple Java project. Getting started with your next project should be at least this simple.

Maven dependencies

In my eyes the best thing Maven gave the Open Source Java community was the Maven Central Repository. It’s a database of Java libraries which can be downloaded by Maven when building a project. If you don’t want to keep large Jar files in your repository you can add dependencies to the Central Repository and rely on Maven to fetch the libraries when the project is built.

The Maven Central Repository became hugely popular for Open Source Java projects, with most projects favoring the Central Repository over storing libraries in their own source repository. Support for Maven dependencies has since spread to many other build systems that can build Java or Scala applications.

Gradle also supports Maven dependencies natively. With just a few extra lines in your build script you can build with a Maven library:

apply plugin: 'java'
apply plugin: 'maven'
repositories {
    mavenCentral()
}
dependencies {
    test 'junit:junit:4.12'
}

The Maven Central Repository has become an invaluable tool for simplifying Open Source development, it reduces the manual work needed to set up a new project using Open Source libraries. Many useful plugins for Gradle itself are even hosted in the Central Repository.

I love using Maven dependencies in Gradle, and it really feels like I’ve emerged from the dark ages of using Ant into the glorious future of Java building with Gradle using Maven dependencies.

Build daemon

Since a few releases back, Gradle comes with a daemon that runs in the background – it’s like a little build server that runs on your machine and waits for build commands. The Gradle daemon is a huge win for Gradle users, because without it, Gradle has a pretty long cold-start time – very noticeable and somewhat annoying. When using the daemon my builds start instantly, and the build over all goes much faster!

Before the Gradle daemon was released my Ant builds used to be much faster than my Gradle builds, but now my Gradle builds are faster than my Ant builds.

Portable build

In 2014 I was collaborating remotely with a professor in Germany and implementing a demonstration compiler for a language feature which the professor had invented. I wrote the compiler in Java, using JastAdd, and I was working on Linux. The professor was working on Windows, and didn’t have Gradle installed.

When I sent my code to the professor so he could try it, I had to make it as simple as possible to compile and run the code to remove any friction and not waste his time. Gradle to the rescue! With Gradle you can generate scripts that bootstrap the build system on most Windows or Linux/Mac platforms. It worked flawlessly, and really helped in the collaboration.

These cross-platform bootstrap scripts are called wrapper scripts. They download Gradle and make it run and build your project. These wrapper scripts are excellent for sharing a project with people who may not have Gradle installed, or who may have an older version of Gradle that can’t build the project.

Things that could be improved

I do like Gradle a lot, but there are a couple issues that I think could be improved.

Obscured functionality

Although I’ve had some positive things to say about the brevity of Gradle build scripts above, I think the terseness can be problematic in some cases. The main issue I have with the way Gradle scripts work is that they hide very much of what’s going on under the surface. It takes a long time to grok what really happens, at least if you learn Gradle mostly by using it rather than by reading the documentation – my preferred way of stumbling my way through new tech.

One example of an obtuse construct is appending actions to a build task. For example, you could declare a build task like this:

task buildHouse << { doWork() }

Alternatively, you could accomplish the same thing like this:

task buildHouse {
    doLast { doWork() }
}

The second form explains much more about what is happening. The first form relies on you knowing what the << operator stands for. I quickly learned what it meant, but nonetheless there was a period where I thought << was a thing you always had to use. Later on I learned that there existed doLast and when I knew that doLast existed, it was easy to discover doFirst.

Using default behavior is a double edged sword – it makes everything very compact and neat for the experienced user, but it hinders discoverability for new users.

Gradle hides a lot of other defaults under the surface. When you get started you don’t have a clue about what is really happening and often you don’t need to know, but when you want to start making more advanced build rules and defining your own up-to-date checks you really need to know what’s happening and in what order things are happening. One gotcha I learned is that anything enclosed in braces is a closure, and closures can be used to do lazy evaluation thereby solving some issues caused by inadvertently using configuration variables before they have been properly configured.

Learning more advanced Gradle usage and writing my own plugins felt needlessly difficult due to obscure semantics. Part of this obscurity is inherited from Groovy which in itself has some pretty complicated invisible behavior, such as the name lookup / delegation semantics.

Multiple configuration files

The main build script in Gradle is named build.gradle, but there are also two other files involved that you will have to edit them when you start making more advanced builds: settings.gradle, gradle.properties.

It would be nice if everything project-specific could be consolidated into one file. Sometimes there is a need for non project specific configuration, but that should just require one extra file.

No implicit dependency on the build script

A minor issue I have with Gradle is that it does not track changes to the build script itself. If I edit a build task it would be reasonable to re-run that task on the next build, because the edits probably have affected the output of the build task.

Stupid logo

My last complaint is very non-technical and petty, but I think Gradle has a stupid logo. Gradle’s logo depicts an angry elephant. Why have an angry elephant as your logo when your motto is “Build happiness”? Gradle have even addressed the issue and noted that people will misunderstand this on their own blog, saying “You might find it a bit odd to see him swearing if our motto is ‘Build Happiness'”.

You are right it is odd, it doesn’t make sense. They go on to say that developers are often frustrated with their builds, and they call this phenomenon “Build Hell”, and the elephant is angry at Build Hell.

However, when I see that logo, I am reminded of unhappy moments caused by frustrating build problems. Do you want me to associate Gradle with Build Hell? You should want me to associate it with happiness. The elephant should be happy.

If you know that the logo is confusing, then the logo is bad. Logos must be as unconfusing as possible, otherwise they fail at their job of communicating your goals. Trying to be clever doesn’t work: if it’s not intuitive nobody gets your message.

gradlephant

Categories: Programming

Leave a Reply

Your email address will not be published. Required fields are marked *