Introduction

Target audience : Gradle developers who have already encountered magical or unexpected build behaviors.

In our adventure of creating the plugin`site-baker`, we followed a rigorous TDD approach. Each feature was tested, validated, and we proceeded with confidence. And then, one day, the unexpected arrived. The builds began to behave erratically. Changes in the plugin logic or in the configuration files seemed to be ignored, and our functional tests, formerly reliable, were failing without apparent reason.

This kind of problem can be incredibly frustrating. It calls into question the reliability of the tool and the validity of our code. After an intense debugging session, the culprit was identified: theGradle configuration cache.

The Symptom: Ghost Builds

The problem manifested itself in several ways:

  1. I was modifying a string in a task`println`, but the old string continued to display at runtime.

  2. I was changing a value in my file`managed-jbake-context.yml`, but the plugin acted as if the file had not been modified.

  3. Functional tests, which create test projects on the fly, were failing because the plugin did not seem to detect the freshly created configuration files.

Everything was happening as if Gradle were executing a ghost version of our build, ignoring our latest changes.

The Investigation: What is the Configuration Cache?

The configuration cache is a relatively modern and extremely powerful Gradle feature, enabled by default in newer versions. Its purpose is to make builds faster.

The principle is simple :
  1. During the first execution, Gradle executes the phase ofConfiguration(reading of`build.gradle.kts`, creation of tasks, resolution of dependencies) and builds a task graph.

  2. At the end of this phase, Gradleserializes this task graphand caches it.

  3. During subsequent runs, if nothing has changed (build scripts,gradle.properties, etc.), Gradlecompletely skips the configuration phaseand reuses the cached task graph.

The time gain is spectacular on large projects. However, this performance comes at a price: it imposes strict rules on how plugins must be written.

The lifecycle of the configuration cache
@startuml
start
:Première exécution;
:Phase de Configuration;
:Création du graphe de tâches;
:Mise en cache du graphe;
:Phase d'Exécution;
end

start
:Exécution suivante;
if (Cache valide ?) then (oui)
  :Restauration du graphe depuis le cache;
  note right
    La phase de configuration
    est sautée !
  end note
else (non)
  :Phase de Configuration;
  :Mise à jour du cache;
endif
:Phase d'Exécution;
stop
@enduml

The Cause of the Problem: A Non-Compliant Plugin

Our plugin`site-baker`violating, without knowing it, several rules of the configuration cache. For a task graph to be serializable, tasks must not contain references to complex objects such as the object`Project`or read files arbitrarily during the execution phase.

Our main error was reading the YAML file’s content directly inside the task execution logic, using a reference to the path stored in our extension. This approach is incompatible with caching because Gradle cannot know if the file’s content has changed if this reading is not modeled as atask entry(Task Input).

The Temporary Solution: Disable the Cache

To get unblocked and regain a predictable build behavior, the fastest solution was to disable the configuration cache. It is enough to add the following line in the file`gradle.properties`of the project that uses the plugin (or in our case, the test project)site-baker).

# site-baker/gradle.properties
org.gradle.configuration-cache=false

Instantly, the builds returned to their normal behavior. Each execution re‑ran the configuration step, and our changes were taken into account.

However, this is a workaround, not a lasting solution. It sacrifices performance and does not solve the underlying problem with our plugin.

The Real Solution: Making the Plugin Compatible

For a plugin to be a good citizen of the modern Gradle ecosystem, it must be compatible with the configuration cache. This implies rethinking the way data flows to our tasks.

The key is to use theProvider APIsof Gradle. Instead of passing direct values (like a`String` ou un File) to our tasks, we must spend some`Property<T>`or some`Provider<T>`.

Here is the refactoring plan :
  1. Declare the task entries:The task that parses the YAML file must declare this file as an input. We use the annotation for that.@InputFile.

[source,kotlin] ---- @get:InputFile abstract val configFile: RegularFileProperty ----


Related articles