Mastodon

Don't use Type-safe Project Accessors with Kotlin Gradle DSL

Another Gradle footgun

Don't use Type-safe Project Accessors with Kotlin Gradle DSL

Gradle 7.0 introduced the type-safe project accessors feature for IDE support of referencing projects in dependencies. This allowed you to replace this:

dependencies {
  implementation(project(":lib1"))
}

with this

dependencies {
  implementation(projects.lib1)
}

This is great! Improved IDE support is always a good thing for developer experience and it's super convenient to have autocomplete like this.

The Problem

There's a hidden but serious cost here.

To make this work, Gradle generates these accessors in Java, and it's based on the included projects in your settings. i.e.

// settings.gradle
include(":lib1")

This generates a file like this

package org.gradle.accessors.dm;

// imports...

@NonNullApi
public class RootProjectAccessor extends TypeSafeProjectDependencyFactory {


    @Inject
    public RootProjectAccessor(DefaultProjectDependencyFactory factory, ProjectFinder finder) {
        super(factory, finder);
    }

    /**
     * Creates a project dependency on the project at path ":lib1"
     */
    public Lib1ProjectDependency getLib1() { return new Lib1ProjectDependency(getFactory(), create(":lib1")); }

}

When Kotlin incremental compilation kicks in, it looks at the ABI of its depedencies. In this case, the accessors dependencies produce a certain ABI of all its public getters, and this is an input to all of your project's build files. The API surface area of these getters is more or less equal to the set of included projects.

Now, what happens if you change the set of included projects? The ABI changes, and those kotlin DSL build files all have to recompile now. That sucks! Especially in a larger codebase.

What's worse is that it's quite easy for your project's buildscript classpath to become an input to your project's compilation tasks (sadly I've yet to track down how or why this happens, but I do observe it in practice), and as a result your compilation tasks will also then be invalidated.

So, just removing an unused project from settings can cause a cascade of cache failures: buildscript file recompilation -> configuration cache miss -> possible compilation task cache miss.


This becomes especially rough if you use tools like Spotlight (which you should!) or Focus, as they are designed to minimize the set of included projects for a given build.

I'd actually go as far as saying that type-safe project accessors shouldn't be compatible with Kotlin Gradle DSL until there's a better IC solution or alternative IDE support, and highly recommend disabling them if you use them today. I put together a little script to do this if you want to borrow it here, though note it assumes a Spotlight all-projects.txt file (you can modify this as needed to read from elsewhere, such as settings.gradle.kts).

Other notes