Tony's rules for Gradle plugin authors
Tags: gradle
Published: 2022-04-25 01:30:01

The Gradle [API surface](https://docs.gradle.org/current/javadoc/org/gradle/api/package-summary.html) 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](https://github.com/gradle/gradle/issues/16344). 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

<a name="source-1" />An important bit of context for the following is that a Gradle build is divided into two[<sup>1</sup>](#endnotes) primary [phases](https://docs.gradle.org/current/userguide/build_lifecycle.html): **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`](https://docs.gradle.org/current/javadoc/org/gradle/api/NamedDomainObjectContainer.html#create-java.lang.String-org.gradle.api.Action-) method on Gradle's container types**

Use [`register`](https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/TaskContainer.html#register-java.lang.String-java.lang.Class-org.gradle.api.Action-) instead.

**Avoid the [`all`](https://docs.gradle.org/current/javadoc/org/gradle/api/DomainObjectCollection.html#all-org.gradle.api.Action-) callback on Gradle's container types**

Use [`configureEach`](https://docs.gradle.org/current/javadoc/org/gradle/api/DomainObjectCollection.html#configureEach-org.gradle.api.Action-) instead.

**Don't assume your plugin is applied after another**

Instead, use [`pluginManager.withPlugin()`](https://docs.gradle.org/current/dsl/org.gradle.api.plugins.PluginManager.html#org.gradle.api.plugins.PluginManager:withPlugin(java.lang.String,%20org.gradle.api.Action)).

**Avoid making any ordering assumptions of any kind**

[Lazy configuration](https://docs.gradle.org/current/userguide/lazy_configuration.html), 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](https://docs.gradle.org/current/userguide/configuration_cache.html), 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](https://docs.gradle.org/current/userguide/cross_project_publications.html) across projects by declaring dependencies. 

It also breaks the experimental [project isolation](https://gradle.github.io/configuration-cache/#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`](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html) or [`Property`](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Property.html) (see also [lazy configuration](https://docs.gradle.org/current/userguide/lazy_configuration.html)).

**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`](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#map-org.gradle.api.Transformer-) or [`flatMap`](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#flatMap-org.gradle.api.Transformer-).

**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>`](https://docs.gradle.org/current/javadoc/org/gradle/api/Action.html) 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`](https://docs.gradle.org/current/javadoc/org/gradle/api/model/ObjectFactory.html) instead. (This is a _configuration time_ concern.)

**Don't use lists in your custom extensions**

Use [domain object containers](https://docs.gradle.org/current/javadoc/org/gradle/api/model/ObjectFactory.html#domainObjectContainer-java.lang.Class-) instead. Once again, Gradle is able to provide enhanced DSL support this way.

**Don't skip the [documentation](https://docs.gradle.org/current/userguide/userguide.html)**

I know, it's a lot.

**Do test your plugins**

Especially if you publish them. See this [two](https://dev.to/autonomousapps/gradle-all-the-way-down-testing-your-gradle-plugin-with-gradle-testkit-2hmc) [part](https://dev.to/autonomousapps/defensive-development-gradle-plugin-development-for-busy-engineers-486c) series for help.

## Special thanks

Special thanks to [Zac Sweers](https://twitter.com/ZacSweers) for offering feedback on this post.

<a name="endnotes"/>
## Endnotes
<sup>1</sup> There are in fact [three phases](https://docs.gradle.org/current/userguide/build_lifecycle.html#sec:build_phases), but the Initialization phase is rarely of interest to the typical build maintainer. [<small>up</small>](#source-1)