This development log (devlog) includes work done since the last devlog, which mainly focuses on the Universal Model, more specifically on dependency buckets (aka Gradle Configuration). The progress was primarily technical; hence we think sharing the major roadblocks we overcame around using Configuration in a nested hierarchy would be appropriate for this devlog.

When we talk about nested hierarchy, we mean several layers of domain object ownership. For example, a component, variant or artifact can own a dependency bucket, which in turn, a component or variant can own artifacts, and finally, a component can own variants. This kind of hierarchy causes a lot of headaches in terms of discovery. In a fully lazy build, how does Gradle know it collected every outgoing Configuration for a proper dependency resolution? The short answer is it can’t! The focus today is on how to configure Configuration without realizing the world properly. Stay tuned for more discussion on the discovery in the future.

Misbehaving plugins

Our first surprise was a misbehaving plugin. In our example, the Koltin Gradle plugin realizes the dependencies of a Configuration too early preventing a pull behaviour from our model using addAllLater trick. Thankfully, we have an internal concept of finalized state, which we can use to propagate the dependencies from our buckets to their matching Configuration.

Lesson: Plugin authors should avoid realizing DomainObjectCollection too early.

Completing the Configuration

To ensure we can finalize a Configuration before Gradle performs its dependency resolution, we need some ways to receive a notification when Gradle is about to use the Configuration. The only hook available to us is ConfigurationInternal#beforeLocking. It would be wrong to think Configuration#beforeResolve is the public API equivalent; the Configuration is already immutable. Thanks to the internal hook, we can perform additional configuration avoidance.

Resolving gap of the Configuration

Now that we can adequately avoid early configuration and finalize the Configuration itself, are we done? Unfortunately, we face our final surprise, Configuration.s are not locked when Gradle visit their task dependencies. A Configuration behave much like a FileCollection. We can iterate the resolved files or visit the task dependencies that produce those files. In practice, visiting the task dependencies looks at ProjectDependency as it’s the only kind of Dependency that contains meaningful TaskDependency. Sadly for us, visiting the task dependencies does not lock the Configuration and only looks at the current DependencySet. Because of misbehaving plugins, we couldn’t lazily attach our dependencies via addAllLater. The result is a bit confusing. The tasks using the Configuration won’t have the correct task dependencies but will still end up with the expected resolved files, which points to missing files. In our Universal Model, we work around this issue by finalizing the dependency bucket upon collecting the incoming files. However, in our JNI library plugin, we had to force the java plugin’s Configuration to lock at key locations.

Note that, as of Gradle 7.4, Gradle will ignore task dependencies provided by ProjectDependency when added via Configuration#withDependencies and Configuration#defaultDependencies actions as those actions execute during the Configuration locking.