What makes a good build.gradle?
A build.gradle
usually starts out innocently enough as a simple, easy to understand file. After all, that’s why you (or someone on your team) chose Gradle as your build system! However, if you don’t spend time refactoring it and give it the love and attention it needs and deserves, it can go awry fairly quickly and come back to haunt you at 1am when you should really be out drinking sleeping, not fiddling with your project’s broken Gradle build. You can end up with a file hundreds of lines long nobody on your team wants to touch or even look at. In this post, I will explain what I think makes a good build.gradle
and outline a few strategies or things to consider in order to keep it that way.
My perspective on builds is shaped by many years spent working on build and deployment automation systems in a corporate environment where you will generally find many similar types of projects. Sharing build logic and conventions becomes particularly critical to maintain sanity while working in this type of environment. Some of these things may not necessarily apply to a hobby OSS project, or if you are using other build systems, but many will.
Another build.gradle gone bad… #
My colleague Seth Goings (@SethGoings) and I were having a bitch session conversation recently after I stumbled on a particularly complex build.gradle
file. Seth realized part of the problem was we haven’t really set good expectations for what a good build.gradle
file should look like:
I’ve been thinking about how we don’t really have a good “goal” for Gradle
builds (internally in this team or expressed). [W]hat do we really care
about? We care it’s clean… sure… but that’s so subjective, or has
been.
– Seth Goings
As I have said before, it is impossible to hold people accountable to expectations you never set.
You can’t hold people
accountable to expectations you never set.
— Douglas Borg (@doug_borg)
April 24,
2014
charset="utf-8">
I realized I should take my own advice and start setting some expectations for a good Gradle build. I brainstormed a few things I think are important and had some further discussions with Seth and my other CI / DevTools colleague at ReadyTalk (@ReadyTalkGeeks), Jason Miller (@stormbeta).
A good build.gradle
should: #
Be Brief. #
As a rule, I feel less than 100 lines is ideal with a general preference for smaller files. If your build script is getting longer than 100 lines, it is a “code smell” and you should consider extracting parts of your build into the buildSrc
folder, find a plugin, or create a plugin to hold any custom build logic.
In our lunch conversation, Seth mentioned one of the biggest things that attracted him to Gradle in the first place was the classic “Hello, world!” Gradle example.
A simple Java project built with Gradle looks something like this:
apply plugin: java
With a simple one-liner you get an amazing amount of build functionality based around conventions. If you are new to Gradle or would just like a refresher on just what, exactly, the java
plugin does, check out Petri Kainulainen’s excellent [Getting Started with Gradle post][] or Gradle’s own [Java QuickStart guide][].
[Getting Started with Gradle post]: http://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-our-first-Java-project/
[Java QuickStart guide]: http://www.gradle.org/docs/current/userguide/tutorial_Java_projects.html
This is truly the power of Gradle: the ease with which you can define your own conventions and build functionality and wrap it up into a plugin or set of plugins. You can then apply those plugins to all your organization’s projects.
Be Declarative. #
More than brief, your build.gradle
should be [declarative][]. It should describe your project: what sort of project it is, what dependencies it has, the artifacts it creates, and where they go. You may even want to place some of that information in organization-specific plugins as we do @ ReadyTalk.
[declarative]: http://latentflip.com/imperative-vs-declarative/
Imperative flow control statements (if
, case
, etc.), task declarations, import statements and the like should be avoided as much as possible. While it is entirely possible to create plugin classes and place any other sort of other Groovy and Java code within your build.gradle
file, I do not believe it is a good practice. By all means use the flexibility of Groovy to experiment or troubleshoot problems with your build, but avoid it as much as possible in your “production” build.gradle
in master. These sorts of things are also smells, and you should consider using or creating a plugin of some kind.
You should always strive to get your build to a point where you just have a plugins
block, a dependencies
block, an artifacts
block, and a plugin extension configuration block here or there. That’s it. Really. With very few exceptions, build functionality should be provided by plugins you configure declaratively.
Gradle is doubling down on a declarative, model-based approach in future development, so get used to thinking about your build this way if you want to keep up with the cool kids. Check out [Adam Murdoch’s talk][] from Gradle Summit 2014 if you are interested in hearing more on that and other things in store for Gradle.
[Adam Murdoch’s talk]: http://www.gradleware.com/video/gradle-into-the-future/
Bootstrap Itself. #
First of all, if you are not using the [Gradle Wrapper][], you should set that up for your project right away. I’ll wait here while you go do that.
[Gradle Wrapper]: http://www.gradle.org/docs/current/userguide/gradle_wrapper.html
$ gradle wrapper # <-- Run this on your Gradle project right now!
Gradle is an ever-evolving tool and as much as the Gradle team strives to maintain backwards compatibility, sometimes things break - especially if you are using incubating features. Using the Gradle Wrapper is crucial for reproducible builds, and is just plain convenient: you don’t need Gradle installed in order to run a Gradle build.
Similarly, pre-buildtime dependencies on anything other than Java should be minimized as much as possible. The Gradle Wrapper gets rid of a pre-buildtime dependency on Gradle itself, but if your build uses other SDKs, custom compilers, utilities etc., they should be managed in much the same way. Set them as dependencies
in your build.gradle
(or as part of a plugin) and extract or set them up at buildtime as part of running the Gradle build.
A goal I have for any project I get involved with is for a new developer to be able to check out the project from source control, run ./gradlew
and have everything “just work.” This also helps tremendously with managing your CI servers. If every project’s build takes care of itself, it makes maintaining CI servers much easier and you don’t have to worry about version conflicts, etc. between projects running on the same build server. As a side effect, you also grant more freedom and flexibility to your dev teams to take care of their own projects’ dependencies without having to go through a central Configuration Management gatekeeper (an all-too-common anti-pattern in my opinion).
Have A Single Entry Point. #
I like setting up the task you want a developer to run when they first come into the project as the [default task][]. Again, any developer (or other interested party) should be able to checkout your project, run ./gradlew
and have everything “just work” including things like compiling the code, running tests, static analysis tools, and assembling artifacts for as many platforms / variants as possible.
[default task]: http://www.gradle.org/docs/current/userguide/tutorial_using_tasks.html#sec:default_tasks
I also like having a single ci
task for your CI server to run. The ci
task should have the main “developer” task as a dependency and just add on whatever publishing and reporting tasks your CI server needs to perform. I prefer to keep any sort of CI server tasks defined in the Gradle build. I want the project configuration in my CI server to be dead simple: run ./gradlew ci
.
Right now I bet you are saying to yourself, “Self, didn’t Doug just say I should have a SINGLE entry point? It seems like he is talking about TWO entry points now…”
Ah, you make an excellent point, but I have the solution to your imagined quandary! If you make your ci
task smart enough, it can do the right thing based on the context it is running in (user, branch, platform, etc.) and you can collapse the “developer” task into the ci
task and truly have a single entrypoint for everyone. We are actually working on a just such a “smart” ci
task as part of our open-source [readytalk-ci][] plugin.
[readytalk-ci]: https://github.com/ReadyTalk/gradle-readytalk-ci
Mind Its Own Business. #
We haven’t seen this too much since migrating from Ant to Gradle a couple years ago, but I feel like it is something worth mentioning as it is still possible to fall into this trap with Gradle (or Ant, or any other build system, really).
Among the many other problems with our old Ant builds, one of the major causes of build instability came from the fact many or our projects reached directly into other projects to grab files, or do other work instead of just depending on the outputs or artifacts of those projects. A build (for ANY project using ANY build system), should mind its own business. It should only depend on specific outputs or artifacts of other projects and not be dependent on the way another project is structured or how it goes about producing those outputs.
If you are a software developer worth your mechanical keyboard, this really should not be a new idea to you. A build “minding its own business” is just another way to describe encapsulation and separation of concerns. Good software engineering principles apply to all code - the code you write for your product as well as the code you use to build and deploy it.
Along the same lines and of more specific importance to Gradle, is the idea project builds should also be [decoupled][]. This can sometimes be tricky since there are some initially attractive-sounding features Gradle provides which end up coupling your projects together. It is possible for one project to access another project’s object model and modify it - DO NOT DO THIS. Take the time to read through all the documentation on [multi-project builds][] to gain a better understanding of the ways projects can come to be coupled and ways to mitigate or avoid it altogether.
[decoupled]: https://gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
[multi-project builds]: https://gradle.org/docs/current/userguide/multi_project_builds.html
Be Platform Independent. #
As much as possible, a build.gradle
should be able to run on Windows, Linux, OSX, etc. Gradle strives to be platform independent and your build should strive for the same. Again, many “impossible-to-solve” multi-platform build issues can be actually be addressed by leveraging other tools. You can leverage tools like Docker, Fig and Vagrant as part of your Gradle build to solve even the hardest dependency and cross-platform build problems.
One of the biggest features I am waiting on to be implemented in the Gradle Daemon is something that would allow me to easily define a multi-platform build that runs in parallel, distributed across executors running on a number of different machines. We get around this now on our CI builds using Jenkins Matrix builds, but it is a mess and the solution we have worked up for developer builds is a kludgy hack using ssh and bash scripts around the actual Gradle Build.
Wrapping it up…. #
Wow. This post ended up being a bit longer than I intended, so thanks for sticking it out! I still feel like I am missing some things, but this will have to do for now.
To bring it all back together, I think a good build.gradle
is a short, declarative, and descriptive file that keeps to itself and has a dead-simple way to invoke it. The Gradle build it describes should minimize the amount of setup and documentation necessary to interact with your project. It should stick to conventions defined elsewhere and shared with other similar projects.
When you are working on a build.gradle
file, you should always feel a little dirty when you open up configuration or task closure. Every time you do, you are going rogue and defining an exception to a convention or model defined, or should be defined, in a plugin. Consider generalizing your exception so it is useful to other projects by contributing to an existing plugin or creating a new one if you think you are truly a special and unique snowflake.
There are clearly reasons some of the principles I outlined above will not work in all cases, but they are things to keep in mind and work towards. Not all of our projects actually stick to these points, but they SHOULD.
Every build.gradle
is a work in progress and you will always have to balance the time you spend on improving your build with working on your actual product. That said, software builds are often neglected and accumulate hidden costs (AKA technical debt) which can be hard to see or calculate. Consider spending some time improving your builds - even small improvements that get you closer to the ideals I outlined above can pay back huge dividends in the long run.
So, what do you think? Did I miss something? Do you want to have a nerd-fight over what I said about your beloved, 1,394-line build.gradle
you think is perfectly fine? Don’t like the cut of my jib? Hit me up on Twitter or e-mail and let me know!
I posted this to reddit as well: comment here and let’s get a good discussion going!