Conditional dependency based on toolchain's version

In my build with a working JVM toolchain setup, I find myself needing to inject different version of a third-party library depending on the target runtime (the third party has legit reason to run unsafe stuff).

I know I can do something like this in the dependency declaration

  if (JavaVersion.current().isJava17Compatible()) {
    implementation 'some-group:some-artifact-id:1.2.5-jdk17'
  }
  else if (JavaVersion.current().isJava11Compatible()) {
    implementation 'some-group:some-artifact-id:1.2.4-jdk11'
  }
  else if (JavaVersion.current().isJava9Compatible()) {
    implementation 'some-group:some-artifact-id:1.2.3-jdk9'
   }

to conditionally inject dependency, and this allows me to specify the appropriate version of the third-party lib. However, this checks the version of the JVM running Gradle, not the toolchain’s version.

Is there a proper way to perform this action against the toolchain’s target JVM version? If not, are there any alternative to accomplish this?

Is it really the case that each jdk variant of said dependency is published under a different version?
Otherwise it might be the proper way to use a component metadata rule to properly define the variants of the library, then you don’t need anything more than to declare the dependency as the toolchain version should then already be considered automatically.

In short, yes.

The library evolved and provided baseline support for JPMS, then later on something like java 15 when there was some API change.

Support for older JDK was also gradually dropped as new releases were made, e.g. minimum JDK is 11 on the latest. Our release targeting 17 can pick up the latest, but the one targeting 8 and 9 can’t.

Also, they are merely releases targeting Maven, so there is no component metadata.

Ok, understood.

But you do configure the toolchain somewhere right?
And if I got it right, you are not using feature variants yourself for the different Java version, so that you could also build all in one go, but you somehow inject the toolchain version through a project property or similar?
Then why don’t you simply decide based on that property there too?

Besides that, it might maybe actually be preferable to define different feature variants in your own build, for the different Java versions. Then you could just declare the necessary dependencies on the according feature variant. Or you could have separate subprojects for the different Java versions. Heavily depends on the actual build and needs.

I am indeed using a variable to pick the toolchain, something like this:

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of("${targetJvmVersion")
    }
}

Our downstream consumers are 99% Gradle, so feature variant will work. I am honestly a Gradle n00b, so I am definitely open to any option at all.

Incidentally, a colleague who is much more experienced with Gradle also brought up variant as a solution, and we introduced configurations for java 8 vs 17 with something like this (I may be missing something, doing this by memory only):

configurations {
    main17
    main8
}
sourceSets {
    main17 {
        java {
             include 'src/main17/java/**'
        }
    }
    main8 {
        java {
             include 'src/main8/java/**'
        }
    }
}
dependencies {
    main17Api "some-group:some-artifact:latest-version-supporting-17"
    main8Api "some-group:some-artifact:latest-version-supporting-8"
}

This is as far as we go, and we are not sure how to tie the main17/main8 configurations to requiring the respective JDK during build.

Have a look at Modeling feature variants and optional dependencies which documents most of the details. Feel free to come back with more questions if you hit any roadblocks then. :slight_smile:

I have gone through that page multiple times, and it is not clear how one can inject the toolchain requirement/choice into the variant configurations.

My current build.gradle looks like this.


plugins{
    id 'java-library'
}

sourceSets {
    main17 { java { include 'src/main17/java/**' } }
    main8 { java { include 'src/main8/java/**' } }
}

java {
    registerFeature("java17") { usingSourceSet(sourceSets.main17)   }
    registerFeature("java8") { usingSourceSet(sourceSets.main8)   }

}

dependencies {
    main17Api "..."
    main8Api "...."
}

The toolchain block commonly seen in the normal non-variant build cannot be defined inside the register feature blcok. Putting that right after the registerFeature block does not make sense either, as I am not trying to have each tool chain build producing two variants.

Drilling down to the gradle objetcs, the “toolchain” is a function of the Java plugin, and it is absent from the FeatureSpec class.

Appreciate some pointer as to where the toolchain declaration should be placed.

The main compilation task of the feature variant controls the default value for the attribute.
So if you indeed have different source sets for the different variants because you have different code, then you would do:

tasks.named("compileMain17Java") {
    javaCompiler = javaToolchains.compilerFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
tasks.named("compileMain8Java") {
    javaCompiler = javaToolchains.compilerFor {
        languageVersion = JavaLanguageVersion.of(8)
    }
}

And you probably more want

sourceSets {
    main17 { java { srcDir 'src/main17/java' } }
    main8 { java { srcDir 'src/main8/java' } }
}

If you instead just have different dependencies but want to use the same code, you would probably more register all features from the main source set and just set the attribute explicitly on the according outgoing configurations like

configurations {
    java17ApiElements {
        attributes {
            attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
        }
    }
    java17RuntimeElements {
        attributes {
            attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
        }
    }
    java8ApiElements {
        attributes {
            attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
        }
    }
    java8RuntimeElements {
        attributes {
            attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
        }
    }
}

And finally in either way above, you might want to set the capability of the additional features to the same capability as the main feature variant.
Because then Gradle consumers can just depend on your artifact and Gradle will automatically select the correct feature variant according to their used Java version.

Or probably not even that, should be the default if I’m not remembering wrongly.