2024-08-05
Publishing to Maven Central with Gradle
Series: Creating A Compose Multiplatform UI Library
Table of Contents
• Introduction
• TLDR
• Signing
• Create a GPG Key
• Configure the Signing Plugin
• Work around Gradle Signing Plugin Issue
• Maven Central
• Sign Up for Maven Central
• Prove you own your domain
• Configure your Maven Central API Token
• Configure the Maven Central Publications
• Publish to Maven Central
• View your online reference documentation
• Update your repository with the newest info
• Link to your Reference Documentation
• Add Instructions for Consuming Your Library
• Create a tagged release
• Test Consuming your Library
• Conclusion
This is article 6 in the series about creating a Compose Multiplatform UI library. In the previous article, we added appropriate README.md files to our repository, created a reference documentation website, bundled the site inside a javadoc.jar of each of our publications, and tested publishing by publishing to our local maven repository. In this article, we'll publish these publications to Maven Central.
At the end of this article, you will have:
Before we can publish to Maven Central, we need to be able to digitally sign our artifacts. This is to ensure that the artifacts have not been tampered with between the time they were published and the time they are consumed. GPG is the tool used for signing.
You only need one set of GPG keys for all libraries you publish
First, install GPG if you don't already have it installed. On a Mac, you can install it with homebrew:
$ brew install gpg
Next, create a GPG key with gpg --gen-key
and follow the instructions. You will need to provide a name, email, and passphrase.
When prompted, you should select (1) RSA and RSA
. You should also select the maximum key size of 4096
. You can choose how long the key should be valid for. I chose 0
for the key to never expire.
Remember your passphrase. You will need it later to sign your artifacts.
Finally, ensure you've got a key pair:
$ gpg --list-keys
/Users/ryan/.gnupg/pubring.kbx
------------------------------
pub rsa4096 2021-05-01 [SC]
08BA1270A166D26B015465985D93084BE8F8A051
uid [ultimate] FS Ryan <ryan@fsryan.com>
sub rsa4096 2021-05-01 [E]
Add the signing plugin to your segmented-display/build.gradle.kts
file:
plugins {
// other plugins
signing
}
Now sync your project and add the signing configuration to the same file:
fun Project.canSignArtifacts(): Boolean {
return hasProperty("signing.keyId") && hasProperty("signing.password") && hasProperty("signing.secretKeyRingFile")
}
signing {
if (project.canSignArtifacts()) {
sign(publishing.publications)
} else {
println("Cannot sign artifacts: missing signing information")
if (!project.hasProperty("signing.keyId")) {
println("\tMissing signing.keyId")
}
if (!project.hasProperty("signing.password")) {
println("\tMissing signing.password")
}
if (!project.hasProperty("signing.secretKeyRingFile")) {
println("\tMissing signing.secretKeyRingFile")
}
}
}
Then update the section where we configured publishing so that any added publications also get signed:
publishing {
publications.withType(MavenPublication::class.java) {
configureMultiplatformPublishing(project)
}
publications.whenObjectAdded {
(this as? MavenPublication)?.configureMultiplatformPublishing(project)
if (project.canSignArtifacts()) {
project.signing.sign(this)
}
}
}
In order to configure the signing plugin, we have a few options. As you can tell from the code above, I'm using the default properties the signing plugin looks for (signing.keyId
, signing.password
, and signing.secretKeyRingFile
). So in my case, I added the following to my ~/.gradle/gradle.properties
file:
signing.keyId=E8F8A051
signing.password=<redacted>
signing.secretKeyRingFile=/Users/ryan/.gnupg/secring.gpg
You can run the gpg --list-keys
command to get the values for signing.keyId and signing.secretKeyRingFile. You have to remember the password you used.
There is presently a problem with the Gradle signing plugin when publishing a multiplatform library that is what I assume to be a false positive when it checks for task dependencies. If you were to try to run the :segmented-display:publishToMavenLocal
task now, you'd receive the following error:
> Task :segmented-display:signIosSimulatorArm64Publication FAILED
FAILURE: Build failed with an exception.
* What went wrong:
A problem was found with the configuration of task ':segmented-display:signIosSimulatorArm64Publication' (type 'Sign').
- Gradle detected a problem with the following location: '/Users/ryan/Downloads/MyUILibrary/segmented-display/build/libs/segmented-display-0.0.3-javadoc.jar.asc'.
Reason: Task ':segmented-display:publishIosArm64PublicationToMavenLocal' uses this output of task ':segmented-display:signIosSimulatorArm64Publication' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.
Possible solutions:
1. Declare task ':segmented-display:signIosSimulatorArm64Publication' as an input of ':segmented-display:publishIosArm64PublicationToMavenLocal'.
2. Declare an explicit dependency on ':segmented-display:signIosSimulatorArm64Publication' from ':segmented-display:publishIosArm64PublicationToMavenLocal' using Task#dependsOn.
3. Declare an explicit dependency on ':segmented-display:signIosSimulatorArm64Publication' from ':segmented-display:publishIosArm64PublicationToMavenLocal' using Task#mustRunAfter.
For more information, please refer to https://docs.gradle.org/8.7/userguide/validation_problems.html#implicit_dependency in the Gradle documentation.
To work around this, segmented-display/build.gradle.kts
file:
// Workaround found here: https://slack-chats.kotlinlang.org/t/13149393/i-m-getting-the-following-two-errors-when-trying-to-publish-
// Further information found here: https://youtrack.jetbrains.com/issue/KT-46466
tasks.withType<AbstractPublishToMaven>().configureEach {
val signingTasks = tasks.withType<Sign>()
mustRunAfter(signingTasks)
}
I am not 100% sure that this is a false positive or that there is a bug in the signing plugin, but the workaround above does seem to work for me and other users who have run into this same issue.
Now, check that the :segmented-display:publishToMavenLocal
task runs successfully and then look inside the ~/.m2/repository/com/fsryan/segmented-display/0.0.3
directory to ensure that the artifacts are signed.
Previously, we had:
$ ls ~/.m2/repository/com/fsryan/ui/segmented-display/0.0.3
segmented-display-0.0.3-javadoc.jar segmented-display-0.0.3-sources.jar segmented-display-0.0.3.module
segmented-display-0.0.3-kotlin-tooling-metadata.json segmented-display-0.0.3.jar segmented-display-0.0.3.pom
But now we have:
$ ls ~/.m2/repository/com/fsryan/ui/segmented-display/0.0.3/
segmented-display-0.0.3-javadoc.jar segmented-display-0.0.3-sources.jar segmented-display-0.0.3.module
segmented-display-0.0.3-javadoc.jar.asc segmented-display-0.0.3-sources.jar.asc segmented-display-0.0.3.module.asc
segmented-display-0.0.3-kotlin-tooling-metadata.json segmented-display-0.0.3.jar segmented-display-0.0.3.pom
segmented-display-0.0.3-kotlin-tooling-metadata.json.asc segmented-display-0.0.3.jar.asc segmented-display-0.0.3.pom.asc
All those .asc
files are the ASCII-armored signature of the related files without the .asc
extension.
Here's the big moment. We're finally going to make our library available to the world. We'll do this by publishing to Maven Central, the most widely-used repository.
Sonatype's website has much better instructions than I can provide. Follow them. Don't worry--this article will still be here when you get back.
After you have created your account, you must prove that you own your domain by registring your namespace. Again, I won't repeat the instructions here. Follow them and come back.
Your namespace should be your reversed domain name. For example, mine is com.fsryan
, and the domain I own is fsryan.com
When you've verified your namespace, it should look like this:
Navigate to https://central.sonatype.com/account, and then click on the "Generate User Token". You should see something like this:
Make a note of the username and password. You'll need to update your gradle properties.
Open up the gradle/libs.versions.toml
file and add the vanniktech version of the sonatype central publishing plugin:
[plugins]
# other plugins
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.29.0" }
There are other gradle plugins that can be used to publish to Maven Central, but none of them are official. If you don't like the one I chose for myself, you may consider one of the others.
Then add the following gradle plugin to your segmented-display/build.gradle.kts
file:
plugins {
// other plugins
alias(libs.plugins.vanniktech.maven.publish)
}
Now, add your sonatype credentials to your ~/.gradle/gradle.properties
file:
mavenCentralUsername=<redacted>
mavenCentralPassword=<redacted>
The vanniktech maven publish plugin will add the necessary publishing tasks to publish to Maven Central. You can see them by running ./gradlew tasks --group=publishing
:
$ ./gradlew :segmented-display:tasks --group=publishing
> Task :segmented-display:tasks
------------------------------------------------------------
Tasks runnable from project ':segmented-display'
------------------------------------------------------------
Publishing tasks
----------------
generateMetadataFileForAndroidReleasePublication - Generates the Gradle metadata file for publication 'androidRelease'.
generateMetadataFileForIosArm64Publication - Generates the Gradle metadata file for publication 'iosArm64'.
generateMetadataFileForIosSimulatorArm64Publication - Generates the Gradle metadata file for publication 'iosSimulatorArm64'.
generateMetadataFileForIosX64Publication - Generates the Gradle metadata file for publication 'iosX64'.
generateMetadataFileForJvmPublication - Generates the Gradle metadata file for publication 'jvm'.
generateMetadataFileForKotlinMultiplatformPublication - Generates the Gradle metadata file for publication 'kotlinMultiplatform'.
generateMetadataFileForWasmJsPublication - Generates the Gradle metadata file for publication 'wasmJs'.
generatePomFileForAndroidReleasePublication - Generates the Maven POM file for publication 'androidRelease'.
generatePomFileForIosArm64Publication - Generates the Maven POM file for publication 'iosArm64'.
generatePomFileForIosSimulatorArm64Publication - Generates the Maven POM file for publication 'iosSimulatorArm64'.
generatePomFileForIosX64Publication - Generates the Maven POM file for publication 'iosX64'.
generatePomFileForJvmPublication - Generates the Maven POM file for publication 'jvm'.
generatePomFileForKotlinMultiplatformPublication - Generates the Maven POM file for publication 'kotlinMultiplatform'.
generatePomFileForWasmJsPublication - Generates the Maven POM file for publication 'wasmJs'.
publish - Publishes all publications produced by this project.
publishAllPublicationsToMavenCentralRepository - Publishes all Maven publications produced by this project to the mavenCentral repository.
publishAndroidReleasePublicationToMavenCentralRepository - Publishes Maven publication 'androidRelease' to Maven repository 'mavenCentral'.
publishAndroidReleasePublicationToMavenLocal - Publishes Maven publication 'androidRelease' to the local Maven repository.
publishIosArm64PublicationToMavenCentralRepository - Publishes Maven publication 'iosArm64' to Maven repository 'mavenCentral'.
publishIosArm64PublicationToMavenLocal - Publishes Maven publication 'iosArm64' to the local Maven repository.
publishIosSimulatorArm64PublicationToMavenCentralRepository - Publishes Maven publication 'iosSimulatorArm64' to Maven repository 'mavenCentral'.
publishIosSimulatorArm64PublicationToMavenLocal - Publishes Maven publication 'iosSimulatorArm64' to the local Maven repository.
publishIosX64PublicationToMavenCentralRepository - Publishes Maven publication 'iosX64' to Maven repository 'mavenCentral'.
publishIosX64PublicationToMavenLocal - Publishes Maven publication 'iosX64' to the local Maven repository.
publishJvmPublicationToMavenCentralRepository - Publishes Maven publication 'jvm' to Maven repository 'mavenCentral'.
publishJvmPublicationToMavenLocal - Publishes Maven publication 'jvm' to the local Maven repository.
publishKotlinMultiplatformPublicationToMavenCentralRepository - Publishes Maven publication 'kotlinMultiplatform' to Maven repository 'mavenCentral'.
publishKotlinMultiplatformPublicationToMavenLocal - Publishes Maven publication 'kotlinMultiplatform' to the local Maven repository.
publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.
publishWasmJsPublicationToMavenCentralRepository - Publishes Maven publication 'wasmJs' to Maven repository 'mavenCentral'.
publishWasmJsPublicationToMavenLocal - Publishes Maven publication 'wasmJs' to the local Maven repository.
There are several publishing tasks ToMavenCentralRepository
. These were all added by the vanniktech maven publish plugin.
We now need to make a small change to some of our custom code in segmented-display/build.gradle.kts
. Presently, we have this block:
with(pom) {
description.set("$publicationName target of the Compose Multiplatform FS Ryan library for rendering segmented displays")
inceptionYear.set("2024")
url.set("https://github.com/fsryan-org/fs-segmented-display")
issueManagement {
url.set("https://github.com/fsryan-org/fs-segmented-display/issues")
system.set("GitHub Issues")
}
licenses {
license {
name.set("The Apache Software License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
distribution.set("repo")
}
}
developers {
developer {
id.set("ryan")
name.set("Ryan Scott")
email.set("ryan@fsryan.com")
organization.set("FS Ryan")
organizationUrl.set("https://www.fsryan.com")
}
}
scm {
url.set("https://github.com/fsryan-org/fs-segmented-display.git")
developerConnection.set("scm:git:git@github.com:fsryan-org/fs-segmented-display.git")
}
}
We're going to export all of this to an extension function on MavenPom
:
fun MavenPom.configure(publicationName: String) {
description.set("$publicationName target of the Compose Multiplatform FS Ryan library for rendering segmented displays")
inceptionYear.set("2024")
url.set("https://github.com/fsryan-org/fs-segmented-display")
issueManagement {
url.set("https://github.com/fsryan-org/fs-segmented-display/issues")
system.set("GitHub Issues")
}
licenses {
license {
name.set("The Apache Software License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
distribution.set("repo")
}
}
developers {
developer {
id.set("ryan")
name.set("Ryan Scott")
email.set("ryan@fsryan.com")
organization.set("FS Ryan")
organizationUrl.set("https://www.fsryan.com")
}
}
scm {
url.set("https://github.com/fsryan-org/fs-segmented-display.git")
developerConnection.set("scm:git:git@github.com:fsryan-org/fs-segmented-display.git")
}
}
Then we'll call this function where we formerly were configuring the pom:
fun MavenPublication.configureMultiplatformPublishing(project: Project) {
pom.configure(name)
if (name != "androidRelease") {
artifact(project.tasks.withType<Jar>().first { it.name == "dokkaHtmlJar" })
}
}
We made this refactor so that we could reuse the MavenPom.configure
function. Now add the following to your segmented-display/build.gradle.kts
file:
mavenPublishing {
coordinates(
groupId = project.group.toString(),
artifactId = project.name,
version = project.version.toString()
)
val publicationName = name
pom {
configure(publicationName)
}
@Suppress("UnstableApiUsage")
configureBasedOnAppliedPlugins(javadocJar = false)
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
if (project.canSignArtifacts()) {
signAllPublications()
}
}
The configureBasedOnAppliedPlugins(javadocJar = false)
call in the above code is unfortunately necessary to avoid having the javadoc jar added twice. In adding this, the vanniktech maven publish plugin does too much, in my opinion. However, this workaround prevents the plugin from attempting to add the javadoc.jar.
Now we're going to try publishing to Maven Central. The vanniktech maven publish plugin adds a publishAndReleaseToMavenCentral
task that we can run, which we can see here:
$ ./gradlew :segmented-display:tasks --group=release
> Task :segmented-display:tasks
------------------------------------------------------------
Tasks runnable from project ':segmented-display'
------------------------------------------------------------
Release tasks
-------------
createStagingRepository - Create a staging repository on Sonatype OSS
dropRepository - Drops a staging repository on Sonatype OSS
publishAndReleaseToMavenCentral - Publishes to a staging repository on Sonatype OSS and releases it to MavenCentral
publishToMavenCentral - Publishes to a staging repository on Sonatype OSS
releaseRepository - Releases a staging repository on Sonatype OSS
So, let's run the publishAndReleaseToMavenCentral
task:
$ ./gradlew clean :segmented-display:publishAndReleaseToMavenCentral
At the end, you should see:
BUILD SUCCESSFUL in 1m 27s
If you don't see this, then you'll need to troubleshoot. There are A LOT of moving parts, and some of the moving parts are not well-documented. If you run into trouble, please comment below, and either I or a member of the community will do their best to help.
Supposing that your library was pushed to Maven Central, you should be able to see a deployment:
If you followed the instructions in the previous article, then your reference documentation site should be available to be scanned by javadoc.io. Check this by navigating to https://javadoc.io/ and searching for your library.
It can take a while to scan, but you can refresh:
When it's done, you should see your library's reference documentation:
Not only is your reference documentation page available, but it's also searchable:
When we searched for our reference documentation on javadoc.io, the tool gave us some markdown we could use to add a badge to our README.md that would direct users to our reference documentation. Let's add that to the top of our root README.md
file.
# segmented-display

[](https://javadoc.io/doc/com.fsryan.ui/segmented-display)
Especially for less-seasoned developers, it also helps to add code users can copy and paste to add the dependency on your library. You can do this by updating the root README.md
file to your repository. Here's an example of what you might add:
## Adding this Library to your Project
If using the kotlin multiplatform plugin, first, configure the compose plugin. Then add:
```kotlin
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.fsryan.ui:segmented-display:x.y.z")
}
}
}
```
Otherwise, configure Compose in your project. Then add the following to the dependencies block:
```kotlin
dependencies {
implementation("com.fsryan.ui:segmented-display:x.y.z")
}
```
Finally, you'll want to add a tagged release and push to your git remote (called origin
here). This will appear in the releases list on GitHub.
git add -A
git commit -m 'add instructions for consuming the library'
git tag -a 0.0.3 -m "Release 0.0.3"
git push origin --follow-tags
When this is done, the output should be like this:
$ git push origin --follow-tags
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 156 bytes | 156.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To github.com:fsryan-org/fs-segmented-display.git
* [new tag] 0.0.3 -> 0.0.3
Now, on your repository's releases page (mine was https://github.com/fsryan-org/fs-segmented-display/releases), create a new release.
Select the tag you just created (mine was 0.0.3
) and add a title and description. Submit the form, and you should have a release that your consumers could download for themselves:
It would be good to make sure that we have everything set up correctly by consuming our library from Maven Central. To do this, open up the composeApp/build.gradle.kts
file and add a dependency on the library from Maven Central (which is already a repository used by this project, as you can see from the first article in this series:
kotlin {
/* ... */
sourceSets {
/* ... */
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
// implementation(project(":segmented-display"))
implementation("com.fsryan.ui:segmented-display:0.0.3")
}
/* ... */
}
}
You can remove the project dependency, but I'm leaving it in so that I can more easily test local changes via the sample application.
You should switch back to the project dependency when you're done testing the library from Maven Central. This will allow your sample application to pick up local edits to the segmented-display
module's code without publishing anything.
You should then be able to run your project on all four targets.
Congratulations! You now have a multiplatform UI library published to Maven Central that anyone can use!
This concludes the series on creating a Compose Multiplatform UI library. Take a moment to pat yourself on the back if you made it all the way from the beginning. Also, please add a comment if you found anything in the series to be useful.
Deploying your library to Maven Central, however, is just the beginning. I allowed a bug to slip in to the article on improving the look of the segmented display that we should address with a new version and some release notes. I also want to cover setting up GitHub Actions to automate the release process when we merge to the main branch and talk about the lifecycle of a change. Those will come in some bonus articles that I will publish in the near future.