Resolving Configuration Dependencies Across Gradle Plugins with Kotlin
A few weeks ago, I was developing a Gradle plugin to automate library project versioning. One of this plugin's two jobs was to ensure that the version assigned to the current build was made available to other components needing it. In my case, the consumer of this version was another plugin that configured the maven artifacts to be published. The first thing that came to mind was to fetch the version, which is partially stored remotely, when the plugin is applied. This plugin, however, has some configuration of its own that needed to be evaluated before it could resolve the build's version. On the other side of the aisle, evaluation in Gradle happens all at once and, once it has happened, plugins cannot be reconfigured. At this point two things were obvious to me: (1) I needed to wait until my versioning plugin got configured and (2) I couldn't wait until afterEvaluate, Gradle's next lifecycle callback. So what gives?
After going down a few rabbit holes in Gradle world I came to the conclusion that Gradle alone was not going to solve my problem. It very much felt like one of those frameworks/languages/systems in which there is one right way and one right way only to do things. This way, whatever it may be, was clearly not the right shape to complete my puzzle, so I turned some place else for a solution: Kotlin. I soon after discovered what would become one of my favorite Kotlin features, delegation, which led me to the solution to the problem, lazy properties. Let's dissect the following build script:
This is just about the same scenario I described above: a configurable plugin, DataSource, producing some output we need in order to configure yet another plugin, DataSink. Some print statements were added to better understand what's going on throughout execution. If we look towards the bottom of the snippet, we can see DataSink's Extension accessing DataSource's lazyProperty to populate its own property, receiver. Notice how DataSource is configured before DataSink; this is crucial to ensure that by the time we access lazyProperty the remaining extension properties have already been populated. Next, take a look at the definition of lazyProperty. lazyProperty is, as its name indicates, a Kotlin lazy property. Lazy properties are properties whose values are set only once to the return value of an initializer function passed to the lazy() function, usually in the shape of a lambda, the very first time it's called. By moving lazyProperty's evaluation logic to this lambda, we are circumventing the Gradle lifecycle's limitation in two ways: (1) we let DataSource get configured before using lazyProperty and (2) we let lazyProperty evaluate to configure other plugins. If we run the snippet we get the following output:
As we can see, the two plugins are applied first. Testing shows that the order in which they're applied doesn't matter. This makes sense, as plugins will be configured in the order their extension configurations are found in the build script. Additionally, as mentioned earlier, the extensions' properties have not yet been populated. The next stage is evaluation; this is where the value of lazyProperty is evaluated, which happens when it's first accessed in DataSink's configuration block. The final stage is afterEvaluate. At this point, the plugins have been configured and, as we can see, receiver holds the correct value.
Enjoy!