Tony's rules for Gradle plugin authors
Tony Robalik
Posted on April 25, 2022
The Gradle API surface is huge. It is also littered with unspoken rules whose enforcement mechanism is inscrutable runtime failures.
I want to say "we can do better," but really, we can't. The best we can do at present is mitigate by internalizing the following rules.
A rule by any other name
I call these "rules," but in many cases they can be only guidelines. Sometimes we have to break a rule because there really is no other way to achieve our goals. Nevertheless, the following rules were all learned the hard way, and should only be violated consciously.
The rules
An important bit of context for the following is that a Gradle build is divided into two1 primary phases: configuration and execution. Most of the rules are about what it is permissible to do in one phase or the other. Each phase carries with it different restrictions.
Don't do expensive computations in the configuration phase
It slows down the build. Such computations should be encapsulated in a task action.
Avoid the create
method on Gradle's container types
Use register
instead.
Avoid the all
callback on Gradle's container types
Use configureEach
instead.
Don't assume your plugin is applied after another
Instead, use pluginManager.withPlugin()
.
Avoid making any ordering assumptions of any kind
Lazy configuration, callbacks, and provider chains are the name of the game.
Don't access a Project
instance inside a task action
It breaks the configuration cache, and will eventually be deprecated.
Don't access another project's Project
instance
This is called cross-project configuration and is extremely fragile. It creates implicit, nearly un-modelable dependencies between projects and can only lead to grief. Instead, share artifacts across projects by declaring dependencies.
It also breaks the experimental project isolation feature, but that won't be truly relevant for a while.
Avoid afterEvaluate
It introduces subtle ordering issues which can be very challenging to debug.
What you're looking for is probably a Provider
or Property
(see also lazy configuration).
Don't call get()
on a Provider
outside a task action
The whole point of using a provider is to evaluate it as late as possible. Calling get()
—evaluating it—will lead to painful ordering issues if done too early.
Don't use internal APIs
Gradle considers internal APIs fair game for making breaking changes in even minor releases. Therefore, using such an API is inherently fragile and will lead to major, completely avoidable, headaches.
Don't use Kotlin lambdas in your public API
I know, it's tempting. They're right there. Use Action<T>
instead. Gradle enhances the bytecode at runtime to provide a nicer DSL experience for users of your plugin
Don't create objects yourself
Use the ObjectFactory
instead. (This is a configuration time concern.)
Don't use lists in your custom extensions
Use domain object containers instead. Once again, Gradle is able to provide enhanced DSL support this way.
Don't skip the documentation
I know, it's a lot.
Do test your plugins
Especially if you publish them. See this two part series for help.
Special thanks
Special thanks to Zac Sweers for offering feedback on this post.
Endnotes
1 There are in fact three phases, but the Initialization phase is rarely of interest to the typical build maintainer. up
Posted on April 25, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.