DEV Community

Cover image for Tony's rules for Gradle plugin authors
Tony Robalik
Tony Robalik

Posted on • Originally published at autonomousapps.com

Tony's rules for Gradle plugin authors

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.

Instead, use map or flatMap.

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

Discussion (6)

Collapse
prenagha profile image
Padraic Renaghan

Thanks for the tips. This has given me a few ideas to cleanup my Gradle build...

A couple of questions:

Where I am creating a new configuration, then need to set config.isCanBeConsumed=true, that method on the Provider is now deprecated, and the message says to call get().isCanBeConsumed, but that seems to go against the idea of lazily creating things. Is the get() fine here, or do you suggest something like config.map{ it.isCanBeConsumed=true }?

Also totally missing how map and flatMap differ.

I have a spot where I need to see if a configuration exists already, and if not create it. The problem is I can't seem to find a method to see if something exists that doesn't create/get() the object. the find... methods return T (not Provider), and the named method throws exception if object doesn't exist. I hate to go through the work of using register, only to have the object created too early later when I am only checking to see if it is there.
Is there some way I am missing to see if a named domain object exists without creating it?
The get names looked promising, but there is a lot of work under those covers creating a new TreeMap every time.

Thanks

Collapse
autonomousapps profile image
Tony Robalik Author

You should set things like isCanBeConsumed = true when creating the configuration, so the question of whether to call get() on that Provider is moot.

That said, it's important to understand that, specifically with Configurations, they really aren't ever lazy. This is a case where the "rule" becomes a "guideline." Configurations are always eager, so it's basically OK to use create() and all {} for this type of container.

Also totally missing how map and flatMap differ.

flatMap is when you're mapping over another Provider, so it just "flattens" the provider chain.

I have a spot where I need to see if a configuration exists already, and if not create it.

You can use maybeCreate() for this use-case.

Collapse
prenagha profile image
Padraic Renaghan

Thanks for the follow-up

Collapse
esafirm profile image
Esa Firman

Don't access another project's Project instance

Is this also apply on accessing the root of the project?

Collapse
autonomousapps profile image
Tony Robalik Author

Short answer: yes. Long answer: it depends.

Collapse
matansab profile image
Matan S

Also, does it apply on the root accessing the children?