Home > Mobile >  Is there a clean way to declare multiplatform dependencies in Gradle when you are NOT a Kotlin Multi
Is there a clean way to declare multiplatform dependencies in Gradle when you are NOT a Kotlin Multi

Time:06-30

So we have this build where we are using some libraries which have, for whatever reason, been distributed as different jar files for different native operating systems.

And of course, those libraries have arranged it so that if we include all of them at the same time, things blow up.

All good, we just make sure we include exactly the correct ones, like this:

if (isWindows()) {
    runtimeOnly(libraries.nd4j.windows)
    runtimeOnly(libraries.openblas.windows)
} else if (isMacOS()) {
    runtimeOnly(libraries.nd4j.macos)
    runtimeOnly(libraries.openblas.macos)
} else if (isLinux()) {
    runtimeOnly(libraries.nd4j.linux)
    runtimeOnly(libraries.openblas.linux)
}

Now, I can certainly code golf this a little. Bump the condition inside the function call, then move the condition into libraries so nobody sees it, things like that. That isn't the issue, though.

The issue is, when I publish this project, depending on what OS I was sitting at when I ran the build, the published project's POM will say it depended on the Windows variants of those jars. Someone else then tries to build against my project on macOS, and it doesn't work.

Had Gradle recognised the existence of profiles and supported it, those projects could have declared that they have multiple profiles which depend on the platform you're running the build. Our build could have then done the same thing and put that into our POM. Downstream projects would then see that, and everything would be fine.

Gradle, however, does not support Maven profiles for reading or for writing POMs, so the only way to split this seems to be to divide my project into four subprojects - a common part, a windows part, a macos part and a linux part, so that each can have different dependencies. All my code then lives in the common directory, and most dependency declarations live in the common subproject, and then each of the platform-specific subprojects would depend on common and the corresponding platform-specific version of the two nasty dependencies.

And now I'm the one creating the problem, because whoever wants to use my library will hit the same problem, and complain about it. And so on.

Is there no better way to go about this?

I know Kotlin/Multiplatform is a thing and have seen how those builds are structured, but this project isn't yet using Kotlin at all (It's a Java project, though the build scripts are in the process of being rewritten to Kotlin DSL), so it doesn't get to benefit from any of that unless I can somehow twist it to work for our not quite the same use case.

CodePudding user response:

The Gradle feature that allows a single dependency module to be resolved in different ways is variant-aware dependency resolution.

With it, a module can provide multiple variants of its artifacts, along with multiple sets of transitive dependencies, and a consumer can request the content from its dependencies that satisfies certain attributes, which should result in the matching dependency variants.

However, your dependencies are third-party libraries that are already there, and they, certainly, don't provide variants that you can pick from by just requesting attributes.

On your side, you can fix this by doing one of the following:

  • Specify component metadata rules that plug into your build's dependency resolution and modify the component metadata of the specific third-party dependencies, adding the Native JAR variants to them.

  • Build and publish your own "umbrella" libraries that will have variants that depend on the platform-specific parts of the third-party libraries. Then use the "umbrella" module as a dependency, requesting the attributes that match the current platform.

    For this, you need to configure a build of a library that doesn't have its own code, but it should still expose multiple variants that have different attributes. In those variants, you can specify different dependencies, e.g. depend on nd4j.windows in the Windows variant and on nd4j.linux in the variant that targets Linux.

    • Or make the library you distribute "multiplatform", so that there's no separate "umbrella" library, but instead it's your library that has multiple variants for different platforms. This could be preferable if you want to simplify the publication and distribution pipeline, even at the cost of more complexity in the main library's build.

Then, in your build, you need to request the attributes that correspond to the current host.

The common attributes in the Gradle API that you can reuse for distinguishing operating systems and architectures are OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE and MachineArchitecture.ARCHITECTURE_ATTRIBUTE

Depending on your choice, the published library will have either the original dependency or the dependency on the "umbrella" library, but it's going to be the same dependency regardless of the host platform.

On the other hand, both approaches impose the responsibility on the consumers of your publication to adjust their builds as well, either by specifying the right operating system attribute on their side, and potentially adding the same component metadata rules. Both of these can be done with a Gradle plugin that you can distribute, or manually.


Since you mentioned Kotlin/Multiplatform, you might be interested in that the above is roughly how Kotlin supports specifying a single dependency on multiplatform libraries, too. You can find platform-specific variants in published Kotlin libraries, for example, in the Gradle *.module metadata file of kotlinx.coroutines:

"variants": [
    ...
    {
      "name": "iosArm32MetadataElements-published",
      "attributes": {
        "org.gradle.category": "library",
        "org.gradle.usage": "kotlin-metadata",
        "org.jetbrains.kotlin.native.target": "ios_arm32",
        "org.jetbrains.kotlin.platform.type": "native"
      },
      "available-at": { ... }
    },
    {
      "name": "iosArm64ApiElements-published",
      "attributes": {
        "org.gradle.category": "library",
        "org.gradle.usage": "kotlin-api",
        "org.jetbrains.kotlin.native.target": "ios_arm64",
        "org.jetbrains.kotlin.platform.type": "native"
      },
      "available-at": { ... }
    },
    ...
]

In the case of Kotlin/Multiplatform, it's the Kotlin Gradle plugin that sets up the right attributes on the consumer end, accordingly to the platforms that the consumer targets.

  • Related