First look at Declarative Gradle

In our update in November 2023, we announced a new experimental project called Declarative Gradle. That post introduced our ideas for a developer-first software definition and how we planned to fulfill our vision for a declarative build language for Gradle. Since then, we’ve been working hard to create the first early access preview (EAP) of Declarative Gradle.

This blog post provides an update on the project’s progress and outlines how you can try it out, provide feedback, and influence our next steps.

First look at Declarative Gradle

What is Declarative Gradle? #

Part of our vision for Gradle Build Tool is to deliver an elegant and extensible declarative build language that allows developers to describe any kind of software in a clear and understandable way. Gradle’s build language is already extensible in the most fundamental ways, which results in a high degree of flexibility, but it’s not always fully declarative, clear, and understandable.

We believe that Declarative Gradle will offer a fundamental advancement in the Gradle user experience for software developers thanks to a developer-first software definition, a declarative DSL, and improvements in developer tooling made possible by these.

Note that Declarative Gradle is still in an experimental stage and is not ready for production use. We are providing an early access preview to gather initial feedback from the community.

Declarative Configuration Language #

Gradle’s existing Kotlin and Groovy DSLs give users access to a full programming language and ecosystem. This makes build scripts very powerful, but it can also make it harder for beginners to understand them and for vendors to provide tooling on top of Gradle.

With Declarative Gradle, we’re introducing a new configuration language. In addition to .gradle and .gradle.kts build files, Gradle will also recognize .gradle.dcl, where DCL stands for “Declarative Configuration Language”.

This language falls into the family of declarative configuration languages, meaning it is non-procedural and disallows common imperative constructs like loops, conditionals, and functions. This aligns well with the idea that a software definition should express what it should do, not how it should do it.

The declarative language syntax is technically a small subset of the Kotlin language. The declarative language forbids arbitrary code and most Kotlin language features while keeping the basics of the Kotlin syntax. Interpreting declarative files doesn’t involve the Kotlin compiler and is extremely fast.

One of our goals is to make it easy to migrate to a .gradle.dcl from non-declarative files without learning an entirely new language. The syntax of declarative files is nearly identical to Kotlin DSL build script syntax.

A subproject in a build can only use one DSL: Kotlin, Groovy, or DCL. In order to facilitate an incremental migration in the future, a build can mix and match different DSLs between subprojects. For example, a build could use a settings.gradle.dcl file, a build.gradle.kts root build script and all other subprojects could have a build.gradle.dcl. Some Declarative Gradle features may only work in the declarative DSL but builds can run and be imported by an IDE using any combination of DSLs.

Developer-first configuration #

One of our key goals with Declarative Gradle is to make configuring a build easier for software developers. Developer-first configuration matches the level of abstraction of the software definition in build files to the software domain that developers are familiar with. This essentially means creating a higher-level build model and providing better separation of concerns between software developers and build engineers.

Even with current best practices, software developers often need to have Gradle-specific knowledge to make changes to things that are important to them.

To address this issue, we developed the concept of a software type. A software type is a higher-level model of the software to be built. All of the configuration relevant for software developers, like dependencies or target platforms, is located in one place. Examples of software types include a Java/Kotlin/Android application or library. In a declarative build file, software developers can only configure the project with software types.

To illustrate the concept of software types, let’s start with what a typical project build script today may look like:

build.gradle.kts

plugins {
   id("application")
}

application.mainClass = "com.example.Main"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

dependencies {
    implementation("com.google.guava:guava:31.0-jre")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}

tasks.named<Test>("test") {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

With the exception of the Test task configuration, the build script is already pretty declarative. It seems obvious that it builds an application with a defined main class, a specific version of Java, a few dependencies, and runs tests with a different version of Java. However, it exposes a few Gradle-specific concepts, like plugins and tasks that require some knowledge of how Gradle works.

If multiple projects are configured the same way, you can share the configuration by moving it into build logic and packaging it as a convention plugin. This is the recommended approach for organizing build logic in modern Gradle builds.

The convention plugin would look like this:

com.example.java-application-conventions.gradle.kts

plugins {
   id("application")
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}

tasks.named<Test>("test") {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

And the project’s build script would change:

build.gradle.kts

plugins {
   id("com.example.java-application-conventions")
}

application.mainClass = "com.example.Main"

dependencies {
    implementation("com.google.guava:guava:31.0-jre")
}

This is a nice improvement compared to the previous example. Commonalities between different projects are extracted and reusable, and the main build file is shorter and more declarative. However, the software developer still needs to understand the plugin to make changes across all subprojects like upgrading the target Java version or the version of JUnit. Software developer concerns like the configuration of the Java version are still mixed with build engineer concerns like the Test task configuration.

Using the software types provided by Declarative Gradle, the developer-specific part of the configuration is moved to a declarative file, as in the following example.

build.gradle.dcl

javaApplication {
    mainClass = "com.example.Main"
    javaVersion = 11

    dependencies {
        implementation("com.google.guava:guava:31.0-jre")
    }

    testing {
        testJavaVersion = 17
        dependencies {
            implementation("org.junit.jupiter:junit-jupiter:5.10.2")
        }
    }
}

This ensures a clean separation between the software developer and build engineer concerns. For common tasks, developers don’t need to touch the plugins at all. They only need to deal with declarative configuration instead of writing build scripts. Note how the configuration of testing is simplified and made fully declarative. Additionally, the declarative format has benefits for IDE integration, which we’ll explain in the next section.

With software types, common configuration is expressed via declarative settings files. With this approach, you can declare defaults for that software type. A project using that software type can still override properties as necessary.

This would look like:

settings.gradle.dcl

plugins {
    id("org.gradle.experimental.jvm-ecosystem") version "0.1.7"
}

defaults {
    javaApplication {
        javaVersion = 11

        testing {
            testJavaVersion = 17
            dependencies {
                implementation("org.junit.jupiter:junit-jupiter:5.10.2")
            }
        }
    }
}

The project would be simplified to:

build.gradle.dcl

javaApplication {
    mainClass = "com.example.Main"

    dependencies {
        implementation("com.google.guava:guava:31.0-jre")
    }
}

An individual project can only declare a single software type. Instead of applying convention plugins in each project, a new ecosystem plugin is applied in the settings file, and available software types are made available in each project. Based on the software type used in a project, Gradle will apply the appropriate plugin that backs the software type. For example, if a project says it’s building a Java library, the org.gradle.java-library plugin will be applied implicitly.

In contrast to existing Kotlin or Groovy DSL build scripts, only the software type is available in declarative software definition files, not any other block like repositories {} or configurations {}. Our intention is to remove the need for convention plugins for scenarios that only define defaults for a particular type of project.

Software types are simpler to configure because the high-level model is closer to what software developers need to understand and build engineer concerns are clearly separated. Sharing configuration between projects also requires no extra ceremony and writing of Gradle plugins. Additionally, this approach enables the IDE and tooling improvements described below.

Better IDE experience #

IDE integration is a fundamental part of developer experience and a major area of focus of Declarative Gradle. We have been exploring how we can improve the IDE experience with higher-level models and stricter configuration language.

Thanks to a very resilient parser, all of the features described below work also in case of project configuration errors. This will allow IDEs to operate on broken builds and make it easier to fix errors.

Like the IDE’s support for Kotlin DSL, the first step was to cover the basics for the new declarative language–syntax highlighting and code completion. This first EAP comes with basic editor support in Android Studio. The IDE provides syntax highlighting of .gradle.dcl files and code completion for software types and their properties. The code completion is context-aware, meaning only relevant suggestions are provided by the IDE. The IDE assistance works much faster because of the declarative format since it doesn’t have to wait for the entire build to be configured.

Another major driver of the Declarative Gradle initiative is that the high degree of flexibility of regular build scripts makes it difficult for tools outside of Gradle to understand and make changes to the build configuration. The presence of code constructs such as variables, local methods, and conditional expressions contributes to the difficulties. By making software definitions declarative, we make it easier for vendors to provide better IDE support.

This is about more than just the file syntax. Even with declarative files, IDEs would still need to encode a lot of knowledge to approximate the understanding that Gradle and Gradle plugins have about the build. Each tool tends to duplicate this knowledge with its own set of corner cases and limitations.

We believe that there’s a better way. By centralizing the implementation of such changes in Gradle and its plugin ecosystem, we can make it simpler for new tools to integrate with Gradle and existing integrations more robust when making changes to build configuration.

The combination of declarative DSL and Software Types allows external tools, such as IDEs, to query information from Gradle quickly. This information is represented in document-like data structures and tracks information back to its source in declarative files. This is foundational for IDE vendors to create tooling that allows them to better present information about a build to software developers. With Declarative Gradle, the IDE can easily display information defined in build files in the UI and let users navigate to the exact place where the information is defined.

The following video shows the Gradle Client, a GUI application we built to demonstrate the features that are not available in IDEs yet. It is used to inspect the declarative software definitions and present an interactive navigable view of the configured model. It relies solely on APIs provided by Gradle.

Finally, we are also providing a simpler way for tools to automatically change the software definition using the concept of mutations. Simple examples of mutations would be adding a dependency, upgrading a version of Gradle, adding a new subproject of a certain type, and so on. This EAP provides APIs to express and run such domain-specific mutations of a software definition.

The next video shows the Gradle Client in action, presenting available mutations and modifying the software definition.

Mutations can be defined by Gradle itself, third-party plugins, IDEs, and other external tools. The mutation infrastructure can deal with complex cases where a single conceptual change results in changes in multiple parts of the build.

Try Declarative Gradle today #

The Declarative Gradle project consists of several experimental parts:

  • Changes in Gradle to support DCL files
  • Changes in Android Studio to support DCL files
  • Prototype plugins demonstrating software types and higher-level models
  • A Gradle test client demonstrating features not yet implemented in the IDE

The features and samples we link to below require nightly builds of Gradle, Android Studio, and our declarative Gradle prototype plugins. These plugins wrap existing plugins, like Android Gradle Plugin (AGP) and Kotlin Multiplatform (KMP), with a new declarative model that works with all of the Declarative Gradle features available in the EAP. Note that our prototype plugins are a temporary measure until we incorporate these ideas back into the upstream plugins or Gradle itself.

Try Declarative Gradle

After you’ve tried things out, we’d appreciate it if you could submit your feedback. This should only take a few minutes and is anonymous unless you choose to provide your email address.

What’s next? #

This blog post explains what we’ve done so far.

Over the next few months, we’ll be working directly with our partners at Google to make the Android Gradle Plugin (AGP) compatible with the Declarative DSL without an additional plugin, by exposing relevant software types directly. This will make it possible for very simple Android libraries and applications to be built entirely with Declarative Gradle. As a part of this, we’ll also explore solutions for constructs present in Gradle plugins such as collections, containers, enums, and varargs in the Declarative DSL.

Later we will also look into supporting other ecosystems like Kotlin Multiplatform or native languages, with the continued focus on experimentation and demonstrating fresh ideas.

We are investigating questions around extensibility and composability of software types. We want to make it possible to extend the software type models in build-logic plugins and to compose optional features and shared configuration in settings and project declarative files. We are also looking into making software types available in the Kotlin and Groovy DSLs. A Kotlin DSL build script using software types should then be nearly identical to the declarative DSL version.

We are collaborating with Google and JetBrains to provide excellent IDE integration in Intellij IDEA and Android Studio, including taking advantage of the mutation framework. We are also experimenting with support in other IDEs like Visual Studio Code in collaboration with Microsoft and in our own Eclipse plugin, Buildship.

Finally, we’ll also be working on making it easier to spin up new builds that use the Declarative DSL, just like gradle init does today with Kotlin and Groovy DSL. This will make it even easier to give Declarative Gradle a try.

We are currently exploring these areas and more. Follow our progress at declarative.gradle.org.

Discuss