Publishing Kotlin Multiplatform library to Bintray

Using Gradle Kotlin DSL and bundled Gradle Maven Publish plugin

This is a follow-up on a request I got in a previous similar write-up I did some time ago: Publishing a Kotlin library to your Bintray repo using Gradle Kotlin DSL

I wanted to explore Kotlin Multiplatform a little, and was also curious about publishing the binaries — “it can’t be as simple as publishing a single jar for the JVM platform”, I thought, and was partially right. It can be daunting at the beginning, but once you get a hang of it and understand the basics it’s pretty simple really. Hopefully this article will help to get you started.

What is Kotlin Multiplatform?

From kotlinlang.org:

Support for multiplatform programming is one of Kotlin’s key benefits. It reduces time spent writing and maintaining the same code for different platforms while retaining the flexibility and benefits of native programming.

What does that mean in practice?

In essence Kotin Multiplatform provides a way for sharing code across multiple target platforms — you can share common data, state, or some logic across Linux-, MacOS-, Win-, Web-, Android-, iOS-, and of course JVM-based platforms.

The concept itself isn’t entirely new. For example in the mobile application development area today exist other quite popular solutions for sharing code between different mobile platforms, such as Flutter (created by Google) and React Native (created by Facebook). Even if you consider Java compiling to JVM bytecode, the language and the platform was designed with portability and sharing in mind.

Kotlin Multiplatform has the same goals in mind, but a different approach that allows it to target multiple platforms in ways that were previously not possible for a single language. Many other “multiplatform languages” compile to an intermediary language, which in turn requires a bridging adapter or a VM layer as an interpreter (for example a JVM for java). At the same time Kotlin Multiplatform code does not need an “intermediary interpreter” because it compiles down to the same code as the rest of the code that runs on a particular platform. In practice this means that it will compile to JVM bytecode, JavaScript, or native machine code based on the target platform.

There is more information and some examples in the official docs, which includes some information for publishing Kotlin Multiplatform libraries as well. The latter seemed to me like it was lacking some practical information to be able to follow it when you’re trying it out for the first time, so I had to do extra research before I could piece together some code that would successfully publish a library.

It is worth mentioning that Kotlin Multiplatform functionality is still in Alpha stages, and the APIs and functionality may change in future releases.

Creating a small Kotlin Multiplatform library and publishing it to Bintray

Creating a new project in Intellij IDEA

Official docs have a small guide on creating your first multiplatform project. I’ve mostly followed that at the beginning, just to see the output Intellij gives you when you use the built-in functionality for creating new multiplatform projects. It mostly looks fine and follows some pre-defined standard conventions for naming the directories and so on. It also gives you options to decide which platforms you want to target, and sets up a minimal viable build configuration targeting those platforms, which looks something like this:

plugins {
kotlin("multiplatform") version "1.4.21"
}
group = "io.github.serpro69"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
testRuns["test"].executionTask.configure {
useJUnit()
}
}
js(LEGACY) {
browser {
testTask {
useKarma {
useChromeHeadless()
webpackConfig.cssSupport.enabled = true
}
}
}
}
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
sourceSets {
val commonMain by getting
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val jvmMain by getting
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit"))
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
val nativeMain by getting
val nativeTest by getting
}
}

All the “magic” happens in the top-level kotlin extension block that is provided by the kotlin multiplatform plugin. Here you configure your targets, source sets, and dependencies for each targeted platform.

Where to publish?

I have discussed this topic in the article I mentioned in the beginning, and I still use Bintray for my OSS projects, at least for initial publishing of packages. I can later easily sync the package from there to JCenter and Maven Central, either from the UI, through Gradle Bintray Plugin configuration, or even the Bintray APIs.

I took the same path this time, though I have encountered some obstacles when it came to syncing the package to JCenter (Read on.)

Gradle Plugins

There are basically two options:

  • Maven Publish Plugin — provides the functionality for publishing artifacts to a Maven repository. It comes bundled with gradle and can publish to any Maven repository really — OSSRH, JCenter, JBoss, Bintray, to name a few.
  • com.jfrog.bintray Gradle Plugin — created and maintained by JFrog, it provides functionality specifically to publish artifacts to Bintray. It is more feature-rich and provides more options specific to Bintray through plugin configuration, and that is what I usually use if I’m targeting Bintray to publish artifacts.

There is one limitation you should be aware of if you decide to go with bintray plugin. From official docs:

This plugin does not support publishing Gradle module metadata required for hierarchical structure support. Use this workaround to enable metadata publishing or migrate to the maven-publish plugin.

For this reason I decided to go with the Maven Publish Plugin instead.

Configuring maven-publish plugin

First we need to declare the plugin by adding maven-publish to the plugins block:

plugins {
kotlin("multiplatform") version "1.4.20"
`maven-publish`
}

Next we use the publishing extension and configure the repository where we want to publish the artifacts, and publications:

publishing {
repositories {
maven {
val bintrayUser = findProperty("bintrayUser") as String?
val bintrayKey = findProperty("bintrayKey") as String?
setUrl("https://api.bintray.com/maven/serpro69/maven/todo-or-die/;publish=1;override=0")

credentials {
username = bintrayUser
password = bintrayKey
}
}
}
publications {
filterIsInstance<MavenPublication>().forEach { publication ->
publication.pom {
name.set(project.name)
description.set(project.description)
packaging = "jar"
url.set("https://github.com/serpro69/${project.name}")
developers {
developer {
id.set("serpro69")
name.set("Sergii Prodan")
email.set("serpro@disroot.org")
}
}
licenses {
license {
name.set("MIT")
url.set("https://github.com/serpro69/${project.name}/blob/master/LICENCE.md")
}
}
scm {
connection.set("scm:git:https://github.com/serpro69/${project.name}.git")
developerConnection.set("scm:git:git@github.com:serpro69/${project.name}.git")
url.set("https://github.com/serpro69/${project.name}")
}
}
}
}
}

In the maven repository block we need to set the URL of the repo where we want to upload the artifacts. In this case we are going to be using the REST API provided by Bintray:

setUrl("https://api.bintray.com/maven/<orgName>/<repoName>/<packageName>/;publish=1;override=0")

The publications section is quite straightforward and I don’t think needs a long explanation. The plugin automatically creates publication tasks for each of the targeted platforms, so we simply iterate over them and configure each:

publications {
filterIsInstance<MavenPublication>().forEach { publication ->
// omitted for brevity
}
}

Now all we need to do is execute one of the publish gradle tasks, providing bintrayUser and bintrayKey which are usually secretly stored on the CI server.

Final Thoughts

So far I liked what I saw was possible to do with Kotlin Multiplatform. It is not as straightforward as targeting the JVM, using external dependencies can feel clumsy at times, and it’s still not that mature to use it in big projects. But it has a great promise and I am sure will become very popular after the initial release.

I mentioned before that I had some issues with syncing the package with JCenter, and it was basically because some artifacts were missing, particularly building common code does not produce sources.jar file and native code does not have the “main” .jar file, so when I tried to sync the package, this is the response I got from jfrog:

JCenter hosts java applications that follow maven convention. In addition to the .pom file, your version should include a binary jar file, a sources jar, and optionally a javadoc jar.

Under the Artifact id “todo-or-die”: sources.jar file is missing and under the artifact id “ todo-or-die-native” .jar file is missing.

I haven’t had time to look into this yet, and I’m assuming it’s a limitation of Kotlin Multiplatform at the moment. I would prefer not to create “dummy” jar files to have the package synced, so still looking for a workaround. If you know a way — please mention that in the comments.

The complete code can be found in this repo: https://github.com/serpro69/todo-or-die. Do make suggestions or PRs if you know a better way to handle what I’m describing here.

Thanks for reading and I hope you have found something useful for yourself in this short write-up on publishing a Kotlin Multiplatform library.

Searching for the answer to the Ultimate Question by night, tester by calling, serendipitously became a developer by day… Automating all things 24x7.