DEV Community

Ekaterina Petrova for Kotlin

Posted on

Publishing your Kotlin Multiplatform library to Maven Central

Now when we've built our first Kotlin Multiplatform library and learned about multiplatform library publishing format nothing can stop us from going public and releasing our library to MavenCentral!

Registering a Sonatype account

If this is your first library, or you’ve only ever used Bintray to do this before, you will need to first register a Sonatype account.

There are many articles describing the registration process on the internet. The one from GetStream is exhaustive and up to date. You can start by following the first four steps, including "Generating a GPG key pair":

  1. Register a Jira account with Sonatype (you can use my issue as an example). ✅
  2. Verify your ownership of the group ID you want to use to publish your artifact by creating a GitHub repo. ✅
  3. Generate a GPG key pair for signing your artifacts. ✅
  4. Publish your public key. ✅
  5. Export your private key. ✅

When the Maven repository and signing keys for your library are prepared, we are ready to move forward and set up our build to upload the library artifacts to a staging repository and then release them!

Setting up publication

Now we need to tell Gradle how to publish our libraries. Most of the work is already done by maven-publish and Kotlin Gradle Plugins – all the required publications are created automatically. We've already seen the result when we published our library to the local Maven repository. But for publishing it to Maven Central we need to take some additional steps:

  • Configure public Maven repository URL and credentials.
  • Provide a description and javadocs for all library components.
  • Sign publications.

Let's handle all of these tasks by writing some Gradle scripts! I suggest extracting all the publication-related logic from our library module build.script, so you can easily reuse it for other modules in the future.

The most idiomatic and flexible way to do that is to use Gradle’s precompiled script plugins. Our build logic will be provided as a precompiled script plugin and could be applied by plugin ID to every module of our library.

To implement this, we need to put our publication logic into a separate Gradle project:

  1. Add a new gradle project convention-plugins inside your library root project by creating a new folder named
    convention-plugins with build.gradle.kts in it.
    Gradle project structure

  2. Put the following in the build.gradle.kts:

    plugins {
        `kotlin-dsl` // Is needed to turn our build logic written in Kotlin into Gralde Plugin
    }
    
    repositories {
        gradlePluginPortal() // To use 'maven-publish' and 'signing' plugins in our own plugin
    }
    
  3. Create a convention.publication.gradle.kts file in the convention-plugins/src/main/kotlin directory. This is where all the publication logic will be stored.

  4. Put all the required logic there. Applying just maven-publish is enough for publishing to the local Maven repository, but not to Maven Central. In the provided script we get the credentials from local.properties or environment variables, do all the required configuration in the ‘publishing’ section, and sign our publications with the signing plugin:

    import org.gradle.api.publish.maven.MavenPublication
    import org.gradle.api.tasks.bundling.Jar
    import org.gradle.kotlin.dsl.`maven-publish`
    import org.gradle.kotlin.dsl.signing
    import java.util.*
    
    plugins {
       `maven-publish`
       signing
    }
    
    // Stub secrets to let the project sync and build without the publication values set up
    ext["signing.keyId"] = null
    ext["signing.password"] = null
    ext["signing.secretKeyRingFile"] = null
    ext["ossrhUsername"] = null
    ext["ossrhPassword"] = null
    
    // Grabbing secrets from local.properties file or from environment variables, which could be used on CI
    val secretPropsFile = project.rootProject.file("local.properties")
    if (secretPropsFile.exists()) {
       secretPropsFile.reader().use {
           Properties().apply {
               load(it)
           }
       }.onEach { (name, value) ->
           ext[name.toString()] = value
       }
    } else {
       ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID")
       ext["signing.password"] = System.getenv("SIGNING_PASSWORD")
       ext["signing.secretKeyRingFile"] = System.getenv("SIGNING_SECRET_KEY_RING_FILE")
       ext["ossrhUsername"] = System.getenv("OSSRH_USERNAME")
       ext["ossrhPassword"] = System.getenv("OSSRH_PASSWORD")
    }
    
    val javadocJar by tasks.registering(Jar::class) {
       archiveClassifier.set("javadoc")
    }
    
    fun getExtraString(name: String) = ext[name]?.toString()
    
    publishing {
       // Configure maven central repository
       repositories {
           maven {
               name = "sonatype"
               setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
               credentials {
                   username = getExtraString("ossrhUsername")
                   password = getExtraString("ossrhPassword")
               }
           }
       }
    
       // Configure all publications
       publications.withType<MavenPublication> {
    
           // Stub javadoc.jar artifact
           artifact(javadocJar.get())
    
           // Provide artifacts information requited by Maven Central
           pom {
               name.set("MPP Sample library")
               description.set("Sample Kotlin Multiplatform library (jvm + ios + js) test")
               url.set("https://github.com/KaterinaPetrova/mpp-sample-lib")
    
               licenses {
                   license {
                       name.set("MIT")
                       url.set("https://opensource.org/licenses/MIT")
                   }
               }
               developers {
                   developer {
                       id.set("KaterinaPetrova")
                       name.set("Ekaterina Petrova")
                       email.set("ekaterina.petrova@jetbrains.com")
                   }
               }
               scm {
                   url.set("https://github.com/KaterinaPetrova/mpp-sample-lib")
               }
    
           }
       }
    }
    
    // Signing artifacts. Signing.* extra properties values will be used
    
    signing {
       sign(publishing.publications)
    }
    
  5. Go back to your library project. Ask Gradle to prebuild your plugins by adding the following in the root settings.gradle:

    includeBuild("convention-plugins")
    
  6. Now we can apply this logic in our library build.script. In the plugins section, replace using maven-publish with using our conventional.publication.

    plugins {
       kotlin("multiplatform") version "1.4.30"
       id("convention.publication")
    }
    
  7. Don’t forget to create a local.properties file with all the necessary credentials and make sure you have added it to .gitignore 🗝:

    signing.keyId=...
    signing.password=...
    signing.secretKeyRingFile=...
    ossrhUsername=...
    ossrhPassword=...
    

Now take a deep breath, make ./gradlew clean and sync the project.
Maven publication Gradle tasks
New Gradle tasks related to the Sonatype repository should appear in the publishing group – that means that everything is ready for you to publish your library!

Publishing your first library to MavenCentral

In the beginning of the article I promised you that you will be able to publish your library in just a one click – and now is the moment of truth! To upload your library to Sonatype Repository just run:

./gradlew publishAllPublicationsToSonatypeRepository
Enter fullscreen mode Exit fullscreen mode

The so-called staging repository will be created, and all the artifacts for all publications will be uploaded to that repository. All that is now needed is to check that all the artifacts you wanted to upload have made it there and to press the “release” button!

These final steps are well-described in the now-familiar article. In short, you need to:

  1. Go to https://s01.oss.sonatype.org/ and log in using the credentials you used in Sonatype Jira.
  2. Find your repository in the ‘Staging repositories’ section.
  3. Close it.
  4. 🚀 Release it!

Go back to the Jira Issue you created and let them know you released your first component to activate the sync to Maven Central. This step is needed only if it’s your first release.
Alt Text

Soon your library will become available at https://repo1.maven.org/maven2/ and other developers will be able to add it as a dependency. And in a couple of hours, it will become discoverable in Maven Central Repository Search!
Alt Text

Conclusion

Congratulations! 🎉 You’ve just created your first Kotlin Multiplatform Library and published it on Maven Central!

This is how you can now depend on the published version:

kotlin {
    android()
    ios ()
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("io.github.katerinapetrova:mpp-sample-lib:1.0.0")
            }
        }
}
Enter fullscreen mode Exit fullscreen mode

Pretty simple, right? If you check the external dependencies section, you will see that it is quite clever as your project only downloads the artifacts of the platforms you actually target, not all of them every time. Thanks to the Gradle module metadata feature, you only have to specify dependencies on the library once in the shared source set, even if it is used in platform source sets.

A lot of work has been done, but of course, it’s just the beginning of the journey. The most interesting part is ahead: to support and improve your library continuously, so you can provide your users the best DX and evolve the KMM ecosystem together with other library creators and contributors.

Next useful steps

GitHub logo KaterinaPetrova / mpp-sample-lib

Sample Kotlin Multiplatform library (jvm + ios + js)

Top comments (4)

Collapse
 
kristofferpaulsson profile image
Kristoffer Paulsson

Is this tutorial up-to-date with " If you're on the new Sonatype infrastructure (happens if you've registered after 2021-02-24 or requested it specifically)" as described in getstream.io/blog/publishing-libra...

Collapse
 
thanosfisherman profile image
Thanos Psaridis • Edited

Hi Katerina, Do you know how I can include dokka to the mix?

Collapse
 
mcroteau profile image
Mike Croteau • Edited

Hi @thanosfisherman, I hope this can be of help. gist.github.com/mcroteau/29dcb181d...

Let me know if it doesn't.
Best

Collapse
 
stefan_haustein_8c5a4d89b profile image
Stefan Haustein

The structure of the conventions-plugin generated by the KMP project wizard seems to look a lot different now? Is this tutorial still up to date?