How to use Gradle "platforms" to align dependency versions in a multi-project setup?

14.4k views Asked by At

I am trying to learn how to use “platforms” to align dependency versions between projects in a multi-project setup. So far I’ve seen:

  1. https://docs.gradle.org/6.2.1/userguide/platforms.html
  2. https://docs.gradle.org/6.2.1/userguide/dependency_management_terminology.html#sub::terminology_platform
  3. https://docs.gradle.org/6.2.1/userguide/dependency_version_alignment.html#sec:virtual_platform
  4. https://docs.gradle.org/6.2.1/userguide/dependency_constraints.html#sec:adding-constraints-transitive-deps
  5. https://docs.gradle.org/6.2.1/userguide/java_platform_plugin.html
  6. … and some more external sites trying to find examples, such as https://dzone.com/articles/gradle-goodness-use-bill-of-materials-bom-as-depen

I understand how I can declare constraints within a project. I think I also understand how to use BOMs for this purpose. However, I would like to use an “enforced platform project” for this purpose and I don’t understand a number of things here:

  1. Do I have to use a “java platform” plugin or not? We have non-Java projects. Our configurations don’t quite fit into the “api” and “runtime” buckets.
  2. Even if we were all Java, for any one project, we can’t have separate versions for its “api” and for its “runtime”. While I do understand the level of control this may offer in some cases, I do not understand how are these meant to work together, to make sure that the project gets the specified dependency.
  3. How does Gradle know which configurations’ constraints to match between the project using the platform and the platform specifications? I think I saw the examples defining “api” and other constraints in the platform and I “understand” that the projects would reference this by declaring api platform(project(':platform')). I hope Gradle isn’t trying to match “api” to “api” by simple name matching. I’d need multiple different dependency configurations to all be aligned against the same single platform “configuration” whatever it is called.

In general, I didn’t find enough information to feel confident about what this does or how it works. Can someone fill in the blanks or point me to some document showing more examples and details than the above? At this time I don't understand what should I actually write for that platform project (its build.gradle) and/or how would I correctly reference it from the current projects we have.

Thanks!

UPDATE 1: Posted a minimal experiment to test my (lack of) understanding of this at https://discuss.gradle.org/t/can-someone-tell-me-what-i-am-doing-wrong-to-align-dependency-versions-across-projects/35601 ...

2

There are 2 answers

1
Cisco On

Do I have to use a “java platform” plugin or not?

No. If you have non-Java projects then you should not use the Java platform plugin. As the plugin name indicates, it's for Java projects.

Gradle offers an official platform plugin for Java projects, but anything outside of that such as C++/Swift, you will need to roll your own plugin/implementation of a platform. You can refer to the source code to help with your implementation.

Even if we were all Java, for any one project, we can’t have separate versions for its “api” and for its “runtime”

You don't need to have separate versions for each configuration. api extends implementation and runtimeClasspath (runtimeOnly) extends from implementation. So declaring dependencies for api should be sufficient enough. Refer to the dependency diagram here.

How does Gradle know which configurations’ constraints to match between the project using the platform and the platform specifications?

By how you specify it in your project and the Java platform plugin's implementation of a platform. For example, given the following platform:

// my-platform

plugins {
    `java-platform`
}

dependencies {
    constraints {
        api("commons-httpclient:commons-httpclient:3.1")
    }
}

I can do any of the following in a project:

dependencies {
    api(platform(":my-platform"))
    implementation(platform(":my-platform"))
    annotationProcessor(platform(":my-platform"))
}

I hope Gradle isn’t trying to match “api” to “api” by simple name matching

Matching is done based on the usage. See this line and these lines. api is just the configuration name Gradle chosen for their plugin, see here. They literally could have chosen any name, but more likely chose api/runtime to keep things similar with what they have.

Most of the documentation you will find on platforms is geared toward Java developers. I believe this primarily due to the concept of platform being heavily inspired by Maven's BOM

If you really want to know how Gradle is doing things with the platform, then either painfully examine the source code or write a simple Gradle plugin that uses the platform, and then write a test using the GradleRunner and debug with breakpoints. Example plugin could be:

public class ExamplePlatformPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        project.getRepositories().mavenCentral();

        DependencyHandler dependencies = project.getDependencies();

        // api(platform(""org.springframework.boot:spring-boot-dependencies:2.2.6.RELEASE"")
        Dependency springBootPlatform = dependencies.platform("org.springframework.boot:spring-boot-dependencies:2.2.6.RELEASE");
        dependencies.add(JavaPlugin.API_CONFIGURATION_NAME, springBootPlatform);

        // api("org.apache.commons:commons-lang3")
        dependencies.add(JavaPlugin.API_CONFIGURATION_NAME, "org.apache.commons:commons-lang3");
    }
}
0
swpalmer On

An alternative to using "platforms" is to use a Version Catalog. That's a lighter weight method to define a set of dependency versions in a central place, but not require all of those dependencies for everything that references it.

A Version Catalog can be a simple .toml file in your project's gradle subfolder, or it can also come from a plugin repository. You can use it to make collections of dependencies that you can refer to as a single entity called a "bundle". E.g. refer to both API and implementation jars for something like JAXB.

An example might look like:

[versions]
json-b        = "3.0.0"
yasson        = "3.0.2"

[libraries]
json-b-api  = { module = "jakarta.json.bind:jakarta.json.bind-api", version.ref = "json-b" }
json-b-impl = { module = "org.eclipse:yasson", version.ref = "yasson" }

[bundles]
json-b = [
    "json-b-api",
    "json-b-impl",
]

which you would refer to in your build.gradle file like so:

dependencies {
    // JSON Binding (api and implementation)
    implementation libs.bundles.json.b
}

See the latest documentation at: https://docs.gradle.org/current/userguide/platforms.html#sub:platforms-vs-catalog